大規模Email配信システムのクラウドジャーニー

by moajo | May 13, 2021
software-architecture | #aws #infrastructure

こんにちは、AI 基盤部の大谷です。 最近は兼務で MLOps 以外にも様々なシステムを構築しています。

弊社では全社的にオンプレミスからクラウドに、よりマネージドに寄せていこうという大きな指針が定められています。 (参考: フルスイングの記事) しかし、古くから運用されているサービスなどでは、未だにオンプレミスで構築されているものも少なくありません。 また、クラウドにホストされている場合でも、マネージドサービスを完全に活用しきれていない場合もあり、EC2 ベースの IaaS な構成はまだまだ多く存在しています。

とあるサービスでも、クラウド化はされているものの、マネージドサービスを活用しきれていないメール配信システムが運用されていました。 一般にメール配信システムは、挙動の違う複数のメールプロバイダにスムーズに配信するために多くのことを気にする必要があり、その分管理コストも高くなりがちです。 (メール送信技術については 過去の技術ブログもご参照ください)

最近このシステムを AWS マネージドに寄せて再設計することで、管理コストを大幅に低減するプロジェクトを行いました。 このプロジェクトは、継続的に大量のメールを配信しているシステムを、ダウンタイム無しでクラウドネイティブなアーキテクチャに完全移行するという技術的にとても面白いものでした!

今回はこのプロジェクトにおいて、どのようなことを考え、どのようなアプローチでプロジェクトを成功させたのかをご紹介します。

背景: 今までのメール配信システム

今まで運用されてきたこのシステムは、EC2 上のメールサーバ(Postfix)とそれをラップする API サーバで構築されていました。

alt

API サーバは複数のアプリケーションサーバから叩かれるもので、配信日時をスケジュールしたり、認証認可の仕組み等を実装していました。 メール送信リクエストは一旦Q4Mで実装された job キューに格納され、バックグラウンドのワーカーが非同期で実際の送信処理を行います。

ところで、Email 配信を行なう際には、配信元 IP アドレスの レピュテーション(reputation) という概念が重要になります。 世の中には大量のスパムメールが飛び交っているため、各メールサービスは未知のドメインから大量のメールが届くとその IP アドレスをスパム IP と判定して受け取りを拒否してしまうからです。 レピュテーションが高い IP アドレスとは、 十分な送信実績を持ち、多くのメールを送信しても受信拒否されることがない IP アドレス のことです。 EC2 からメール送信を行う場合、自前で IP アドレスを「育てる」ことで、高いレピュテーションを維持しなければなりません。

IP アドレスのレピュテーションは送信数だけでなく、迷惑なメールを送信したり、ウィルスを含んでいたり、ユーザからスパムとマークされたりなど様々な要因で複雑に変化します。 そのため、このシステムでは複数の IP アドレスをプールして育て、なにかの要因でレピュテーションが意図せず低下してしまった場合に備えていました。

また、レピュテーションの指標として バウンスレート苦情レート も重要になります。 バウンスとは、メール配信が正常に行われないことです。ネットワーク障害等の一時的な理由(ソフトバウンス)や、ユーザからの明示的な拒否などの半永久的な理由(ハードバウンス)など、様々な要因で発生します。 苦情は、配信されたメールが迷惑メールとしてマークされたりしたときに生じるものです。送信者への通知はプライバシー等に配慮されて詳細が隠されるので、苦情を送信側から詳細にモニタリングすることは困難です。 これらのイベントは一般に各メールプロバイダの mailer daemon から機械的なエラーメールが返信されてくることで検知できるので、レピュテーション維持のためにこれらをモニタリングするのが重要になります。

この旧システムでは、バウンス/苦情のモニタリングをはじめ、レピュテーションを維持するために多くのメンテナンスコストが掛かっていました。 IP アドレスのレピュテーションやインフラの管理、運用コストを下げたい という希望と、 大量のメールを安定送信できるだけの信頼性を維持するという要件をどのように両立させるか、という点がこの移行プロジェクトのキモになっていました。

フルマネージド Email 配信サービス: Amazon Simple Email Service(SES)

AWS には SES というマネージド Email 配信サービスがあります。 このサービスは送信元 IP アドレスのレピュテーションを AWS が管理してくれる便利なサービスで、普通の用途でメールを配信したい場合にはほとんどのニーズをカバーしているのではないでしょうか。 他のクラウドプロバイダのメール配信サービス等とも比較した上で、今回の移行プロジェクトでは SES を使用することになりましたが、このプロジェクトでは かなりの秒間トラフィックや厳しい送信要件等 があり、単に SES を使えば良いというほどシンプルな話にはなりませんでした。

クラウドジャーニーの始まり

この時点では、暫定的なアーキテクチャとして以下のような構成を検討しました

alt

Postfix をラップしていた API サーバを廃し、各アプリケーションから直接 SES を叩くようにする構成です。 SES を直接叩くので、認証認可は IAM で簡単に実装できます。それ以外の要件については SES の機能の範囲で実現可能だと考えていました。

ここからは、このアーキテクチャが実現可能かを確認するために行った様々な調査検討を、時系列順に紹介していきます。

SES の特性調査: API レイテンシ

このプロジェクトでは各アプリケーションから SES を直接同期的に叩くため、SES の API レイテンシによってはアプリケーションサーバの負荷が増大してしまう可能性がありました。

SES は HTTPSMTP の 2 種類の API を持っています。さらに、SMTP API は VPC エンドポイントを作成することが可能なので、全部で 3 通りのインタフェースが存在することになります。

まず、このシステムにおける平均的な秒間リクエスト数と同等の負荷をかけて、平均レイテンシを調査しました。

API 平均レイテンシ(ms)
HTTP 97
SMTP 163
SMTP(VPC endpoint) 159

従来の Postfix をラップしていた API のレイテンシは平均 37ms 程度だったので、最も高速な HTTP API でも 2 倍以上、60ms程度遅くなることになります。 ここで、メール API のレイテンシが 60ms 遅くなることによるエンドユーザーへの影響を調べるため、メール API を利用しているアプリケーションのアクセスログを確認しました。 すると、アプリケーション側のレイテンシは概して 200~300ms 程度のレイテンシであることがわかりました。 ユーザにとってメール送信はそれほど頻繁に行なう操作ではないですし、このレイテンシが 60ms 遅くなったとしても致命的な問題にはなりません。 性能悪化を伴うものの、SES の HTTP API を叩くことは許容できる と判断しました。

(まだ、HTTP 以外の API を積極的に使う理由は特に無さそうでした。)

キャリアメールへの到達性

実はネット上で SES について調べると、「日本のキャリアメールには届きにくい」という情報が散見されます。 昔はそのような問題があったようですが、SES の到達性は日々改善されているようなので、改めてちゃんと検証します。 (何もしなくとも時とともに改良されてゆくのはマネージドサービスだからこその利点ですね。きっと裏側では SES の信頼性を維持するために膨大な労力がかけられているのでしょう。データセンターの方角には足を向けて寝られません。)

念の為に日本の 3 大キャリアのキャリアメールアドレスを用意し、それぞれに対して通常送っている内容のメールが到達することを QA チームと連携して確認しました。 結果的には、全く問題なく配信できることがわかりました。

レピュテーションの確実な維持

SES で使用される送信元 IP アドレスには、 共有 IP専用 IP の 2 種類があります。

共有 IP はデフォルトで使用される IP プールであり、全世界の SES ユーザと共有されている IP です。 SES ではアカウントから送信するメールのバウンスレートや苦情レートについて厳しい要件があり、 一定以上の割合になるとアカウントが強制的に停止されることもあります。 SES の全ユーザはこのような基準をクリアしているため、共有 IP アドレスのレピュテーションは高い水準を維持できるようになっていますし、逆に大量の送信実績のある IP アドレスを最初から使用できるので、IP アドレスを自分で育てる手間も省けます。

一方専用 IP は、共有 IP とは独立したプールで、そのアカウント専用のものです。 最初のウォームアップ(ある程度のレピュテーションが得られるまでの送信実績を作る)部分はマネージドにやってくれるので、EC2 インスタンスの EIP を自前で育てるよりは楽ですが、その後のレピュテーション管理は自分で行う必要があります。 共有 IP は前述の仕組みである程度高いレピュテーションを維持していますが、短期間で急にスパムを撒き散らすようになったユーザがいた場合、一時的にその悪影響を他のユーザが受けてしまうこともありえます。 専用 IP は他のユーザの送信による影響を一切受けないので、確実にレピュテーションをコントロールしたい場合には推奨される方法です。

今回のプロジェクトでは、より確実なレピュテーション管理が重要な要素だったので、専用 IP を使用することを検討しました。

さらに、旧システムの今までの経験から、一部のキャリアメール等ではまれに突然到達性の悪化が発生していたので、これらのキャリアメールドメインに対してはそれぞれ個別の専用 IP プールを用意することを検討しました。 そうすることで、その他大勢のドメインへの配信に万が一にも影響が起きないようなります。(Configuration setを使うことで IP プールの使い分けができます)

…結局、自前で管理するもの多いな・・・?

専用 IP を使用する場合、AWS がレピュテーションをマネージしてくれなくなるので、今まで EC2 上でやっていた IP アドレスの管理業務を今後もやり続けることになります。 また、送信先ドメイン間でレピュテーションが相互に影響しないように IP プールを使い分ける場合、実際の送信量の変化を踏まえて、どのように IP プールを分割するかを常に考え続けることも必要になります。 このあたりまで検討したところで、専用 IP を自前で管理するんだったら、マネージドに寄せる旨味が半減 することに気付きます。

我々は管理コストを徹底的に引き下げ、優秀なエンジニアたちをもっと創造的な仕事にフォーカスさせることを目指して、マネージドに寄せる手法を選択していたはずでした。 マネージドに寄せたとしても、結局管理に手間暇掛ける必要があるようでは、画竜点睛を欠くようなものです。

一方で、レピュテーションを確実に維持するのはもちろん重要で、外せない要件です。 なにか 管理が楽で、しかもレピュテーションリスクの低い夢のようなアプローチ はないのでしょうか・・・?

新・アーキテクチャ

AWS サポートからも助言をいただきながら、我々は以下のような構成にたどり着きました。

alt

SES の共有 IP アドレスを、複数リージョンで使用し、それらにトラフィックを分散しながら利用するというものです。 入口は SQS キューになっていて、トラフィック分散処理を行なう Lambda 関数がキューイングされたメール配信リクエストを無限にスケールしながら捌きます。 トラフィック分散は DynamoDB 内の割合テーブルに基づいて、送信先ドメイン毎に各リージョンの使用割合を決定します。 リージョン毎に共有アドレスプールは独立しているので、実質的には複数の IP プールへのリスク分散が期待できます。 また、各リージョンでは SES の送信イベントに基づくバウンス・苦情レートを、送信先ドメイン毎にカスタムメトリクスとして記録する機構を実装していて、各ドメインでの急なレピュテーションの悪化等を検知できるようにしています。

SQS はメッセージが重複して配信される可能性がありますが、これを自動的に排除できるFIFO キュー はスループットが足りないのでこのシステムには使用できませんでした。 そこで、DynamoDB を使って自前で重複排除ロジックを実装します。 これは PutItem リクエストに attribute_not_exists 条件をつけた上で、SQS のメッセージ ID を DynamoDB に PUT していくだけで実現できます。(重複していたら PUT に失敗するのでそれを検知すればいい) DynamoDB には TTL を設定し、古いメッセージ id は自動削除されるため保存コストの心配もありません。

このアーキテクチャには以下の利点があります。

  • 専用 IP を管理する手間がかからない
  • 万が一特定のメールプロバイダへの到達性が悪化しても、DynamoDB 上の割合テーブルを更新するだけで別リージョンにフォールバックできる
  • システム全体が完全にサーバレスなので、維持管理コストが低い
  • リージョンを追加するだけで容易にスケールする

このように、 可能な限りマネージドな機能を使うことで管理コストを下げる一方、シンプルな仕組みでリスクを分散することで堅牢性とも両立させる ことができました!

性能評価

構築したシステムに対してスループットを検証します。 SES には メールボックスシミュレータ という機能があるので、このアドレスに対して大量のメール送信を実行してみます。 今回は SQS をトリガーにするシステムなので、簡易的に以下の手順で試験しました。

  • SQS に紐づく Lambda 関数のトリガーを disable にする
  • キューに大量のリクエストを入れる
  • 関数のトリガーを enable にする

このやり方では同時に大量のリクエストを生成する必要がないので、簡単にシステムの最大スループットを確認できます。

alt アプリケーションからのメール送信リクエストは最大で毎分 6 万 8 千件、秒間では 1000 件以上のスループットを達成できました! なお SES は内部的に送信処理をバッファリングするようで、各リージョンの SES の送信メトリクスは毎分 6 千件程度の値となりました。

alt

つまりこの試験では実際のメール送信は遅延していることになりますが、これは分散するリージョンを増やせば容易に解消でき、またこれほど大量のメール送信リクエストが長時間持続することは実際には無いため、実運用では問題ありません。 実運用ではトラフィックのスパイク時に数分程度配信が遅延する可能性がありますが、この程度の一次的なメール配信遅延は一般的には正常系の範囲内であると考えられます。

実際、このシステムは本番環境に既に投入されていて、大きな問題なく旧環境からのシームレスな移行に成功しています。

まとめ

今回はできるだけマネージドに寄せて管理コストを下げることと、 その上で堅牢なシステムを作る ことをいかに両立するかという観点で、DeNA でどのように設計が行われているのかを紹介しました。 マネージドサービスは単に使うだけでなく、深く検討して組み合わせることでその効果を最大化できると思っています。

これからも、より良いアーキテクチャを探求し続けながら、プロダクトの改善に取り組んでいきます。


この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします!

また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog 記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい!