私はWebアプリケーションを設計していて、自動化された電子メールの送信を管理するためのアーキテクチャーを設計する方法を考えています。
現在、私はこの機能を自分のWebアプリに組み込んでおり、メールはユーザー入力/インタラクション(新しいユーザーの作成など)に基づいて送信されます。問題は、メールサーバーに直接接続するのに数秒かかることです。アプリケーションを拡大すると、これは将来の大きなボトルネックになるでしょう。
システムアーキテクチャ内で大量の自動メールの送信を管理する最良の方法は何ですか?
大量のメールが送信されることはありません(1日あたり最大2000)。メールをすぐに送信する必要はありません。最大10分の遅延で問題ありません。
更新:メッセージキューイングが回答として与えられましたが、これはどのように設計されますか?これはアプリで処理され、静かな期間に処理されますか、またはキューを管理するだけの新しい「メールアプリ」またはWebサービスを作成する必要がありますか?
すでに述べたOzz のような一般的なアプローチは メッセージキュー です。設計の観点から見ると、メッセージキューは基本的に FIFOキュー であり、これはかなり基本的なデータ型です。
メッセージキューが特別なのは、アプリケーションがエンキューを担当する一方で、別のプロセスがデキューを担当することです。キューイング用語では、アプリケーションがメッセージの送信者であり、デキューイングプロセスが受信者です。明らかな利点は、処理するメッセージがある限り、プロセス全体が非同期であり、受信者が送信者とは無関係に機能することです。明らかな欠点は、全体を機能させるために送信者という追加のコンポーネントが必要になることです。
アーキテクチャはメッセージを交換する2つのコンポーネントに依存するようになったので、そのために プロセス間通信 という高級用語を使用できます。
アプリケーションの特定のアクションは、電子メールを生成します。メッセージキューを導入すると、これらのアクションは代わりにメッセージをキューにプッシュする必要があります(それ以上)。これらのメッセージには、受信者が処理するときに電子メールを作成するために必要な最小限の情報が含まれている必要があります。
メッセージの形式と内容は完全にあなた次第ですが、小さいほど良いことを覚えておいてください。キューは書き込みと処理が可能な限り高速である必要があり、大量のデータをキューに投入するとボトルネックが発生する可能性があります。
さらに、いくつかのクラウドベースのキューサービスはメッセージサイズに制限があり、より大きなメッセージを分割する場合があります。気が付かないでしょう。分割されたメッセージは、要求したときに1つとして提供されますが、複数のメッセージに対して課金されます(もちろん、有料のサービスを使用していると仮定します)。
ここではWebアプリケーションについて説明しているため、レシーバーの一般的なアプローチは単純なcronスクリプトです。 x
分(または秒)ごとに実行され、次のようになります。
n
量のメッセージをポップし、Getまたはfetchではなくpopと言っていることに注意してください。これは、レシーバーがアイテムをキューから取得するだけでなく、アイテムをクリアする(つまり、キューからアイテムを削除するまたは処理済みとしてマーク)。それがどの程度正確に行われるかは、メッセージキューの実装とアプリケーションの特定のニーズによって異なります。
もちろん、私が説明しているのは本質的に バッチ操作 であり、キューを処理する最も簡単な方法です。必要に応じて、より複雑な方法でメッセージを処理することもできます(これには、より複雑なキューも必要になります)。
レシーバーは、トラフィックを考慮に入れ、実行時のトラフィックに基づいて、処理するメッセージの数を調整できます。単純なアプローチは、過去のトラフィックデータに基づいてトラフィックの多い時間を予測し、x
分ごとに実行するcronスクリプトを使用すると仮定すると、次のようなことができます。
if(
now() > 2pm && now() < 7pm
) {
process(10);
} else {
process(100);
}
function process(count) {
for(i=0; i<=count; i++) {
message = dequeue();
mail(message)
}
}
非常に素朴で汚いアプローチですが、うまくいきます。そうでない場合、まあ、他のアプローチは、各反復でサーバーの現在のトラフィックを見つけ、それに応じて処理アイテムの数を調整することです。絶対に必要でない場合は、マイクロ最適化を行わないでください。時間を浪費することになります。
アプリケーションがすでにデータベースを使用している場合、その上の単一のテーブルが最も簡単な解決策になります。
CREATE TABLE message_queue (
id int(11) NOT NULL AUTO_INCREMENT,
timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed enum('0','1') NOT NULL DEFAULT '0',
message varchar(255) NOT NULL,
PRIMARY KEY (id),
KEY timestamp (timestamp),
KEY processed (processed)
)
それは本当にそれ以上複雑ではありません。もちろん、必要に応じて複雑にすることもできます。たとえば、優先フィールドを追加できます(これは、FIFOキューではなく、実際に必要な場合) 、誰が気にしますか?)処理済みのフィールドをスキップして、単純にすることもできます(ただし、処理した行を削除する必要があります)。
データベーステーブルは1日あたり2000メッセージに理想的ですが、おそらく1日あたり数百万のメッセージに対して適切にスケーリングされません。考慮すべき要素は100万あり、インフラストラクチャ内のすべてがアプリケーションの全体的なスケーラビリティに影響を与えます。
いずれにせよ、データベースベースのキューをボトルネックとしてすでに特定していると仮定すると、次のステップはクラウドベースのサービスを調べることです。 Amazon SQS は私が使用したサービスの1つで、約束どおりのサービスを提供しました。同様のサービスはかなりたくさんあると思います。
メモリベースのキューも、特に有効期間が短いキューの場合に考慮する必要があります。 memcached は、メッセージキューのストレージとして優れています。
キューを構築するストレージが何であれ、スマートで抽象化します。送信者も受信者も特定のストレージに縛られるべきではありません。そうでなければ、後で別のストレージに切り替えることは完全なPITAになります。
私はあなたがしていることに非常に似ている電子メールのためのメッセージキューを構築しました。これはPHPプロジェクトで、私はそれを中心に構築しました Zend Queue 、 いくつかのアダプタ を提供するZend Frameworkのコンポーネントさまざまなストレージ。私のストレージ:
私のメッセージはできる限りシンプルで、私のアプリケーションは重要な情報([user_id, reason]
)。メッセージストアは、その配列のシリアル化されたバージョンでした(最初はPHPの内部シリアル化形式、次にJSON、私が切り替えた理由を覚えていません)。 reason
は定数であり、もちろん、どこかにreason
をより完全な説明にマップする大きなテーブルがあります(私は、不可解なreason
でクライアントに約500通の電子メールを送信できましたより完全なメッセージの代わりに)。
規格:
ツール:
興味深い読み物:
なんらかのキューイングシステムが必要です。
簡単な方法の1つは、データベーステーブルに書き込み、このテーブルに別の外部アプリケーションプロセス行を持つことですが、他にも多くのキューイングテクノロジを使用できます。
メールに重要性を持たせることで、特定のメールがほとんどすぐに処理されるようにし(パスワードのリセットなど)、重要度の低いメールをまとめて後で送信することができます。
大量のメールが送信されることはありません(1日あたり最大2000)。
キューに加えて、考慮すべき2番目のことは、特殊なサービス(たとえば、MailChimp)を介してメールを送信することです(私はこのサービスとは関係ありません)。それ以外の場合、Gmailなどの多くのメールサービスはすぐにあなたの手紙をスパムフォルダーに送信します。
私はキューシステムを別の2つのテーブルでモデル化しました。
CREATE TABLE [dbo].[wMessages](
[Id] [uniqueidentifier] NOT NULL,
[FromAddress] [nvarchar](255) NOT NULL,
[FromDisplayName] [nvarchar](255) NULL,
[ToAddress] [nvarchar](255) NOT NULL,
[ToDisplayName] [nvarchar](255) NULL,
[Graph] [xml] NOT NULL,
[Priority] [int] NOT NULL,
PRIMARY KEY CLUSTERED ( [Id] ASC ))
CREATE TABLE [dbo].[wMessageStates](
[MessageId] [uniqueidentifier] NOT NULL,
[Status] [int] NOT NULL,
[LastChange] [datetimeoffset](7) NOT NULL,
[SendAfter] [datetimeoffset](7) NULL,
[SendBefore] [datetimeoffset](7) NULL,
[DeleteAfter] [datetimeoffset](7) NULL,
[SendDate] [datetimeoffset](7) NULL,
PRIMARY KEY CLUSTERED ( [MessageId] ASC )) ON [PRIMARY]
) ON [PRIMARY]
これらのテーブル間には1-1の関係があります。
Messagesメッセージの内容を保存するためのテーブル。実際のコンテンツ(宛先、CC、BCC、件名、本文など)は、XML形式でグラフフィールドにシリアル化されます。その他のFrom、To情報は、グラフをデシリアライズせずに問題を報告するためだけに使用されます。このテーブルを分離すると、テーブルのコンテンツを別のディスクストレージに分割できます。メッセージを送信する準備ができたら、すべての情報を読み取る必要があるため、すべてのコンテンツを主キーインデックスを持つ1つの列にシリアル化しても問題はありません。
MessageState追加の日付ベースの情報とともにメッセージコンテンツの状態を格納するためのテーブル。このテーブルを分離することにより、追加のインデックスを備えたメカニズムに高速アクセスできますIOストレージ。他の列はすでに自明です。
このテーブルをスキャンする別のスレッドプールを使用できます。アプリケーションとプールが同じマシンに存在する場合、EventWaitHandleクラスを使用して、これらのテーブルの挿入についてアプリケーションからプールに信号を送ることができます。そうでない場合は、定期的にタイムアウトでスキャンするのが最適です。