atsukanrockのブログ

Microsoft系技術を中心にぼちぼち更新します

Amazon SES および SendGrid によるメール送信のスループットを調べるツールを C# で書いた

仕事で使うためにやっつけで書いていたのですが、やってるうちに勢い余って割と作り込むあるあるを踏み抜き、せっかくなので公開することにしました。

github.com

何ができるかっていうと

大したことはできません。片手間でできるものなんてそんなものですね。あえてできることを挙げるとすると:

はい、それだけです。スループットを手軽に確認するために、コマンドライン引数で動作を微調整できます:

  • concurrency: 何本スレッド立ち上げるか
  • mail-count: 何通メール送るか
  • strategy: どうやってメール送るか (例えば 1 なら Amazon SES の SendRawEmail API を叩くし、4 なら SMTP を叩きます)

GUI なんていうお洒落なものはありません。

ソースコードダイジェスト

できるだけ素早くメール送信したいって時にはいろいろと常套手段があるようでして、私も全然専門じゃないもので詳しいことは分からないのですが、ある程度調べて分かったことは詰め込んでおります。

マルチスレッド処理の初速を上げる

Parallel.ForEach(Enumerable.Range(0, 10000), x => { /* 何かする */ });

とか書いても .NET のマルチスレッドは心優しいので *1 いきなり 1 万個スレッドを立ち上げるようなことはしません。CPU コア数 (かその 2 倍) ぐらいだけスレッドプールのスレッドを申し訳なさそうに拝借して処理を始めてみて、どうやらもっとスレッドを増やした方が効率的みたいだぞ?といったことに自動的に気が付いて徐々に使うスレッド数を増やしていきます。ちょっと記憶が定かでないですが、メール送信ぐらいの I/O バウンドな処理で 100 スレッドぐらいまで並列度が増すのに 1 分以上はかかった気がします。その間に送られたメールは数千通的な。

という話があるのでスループット確認には不向きですねってことで次のようにして初速を上げてます。

ThreadPool.SetMinThreads(Math.Max(options.Concurrency, 200), Math.Max(options.Concurrency, 200));

同時コネクション数を増やす

.NET で作る普通のデスクトップアプリケーションの場合、同一サーバーエンドポイントに対する同時コネクション数が 2 に限定されています。例えばスレッドを 10 個起動し、それぞれの上で SMTP コネクションを張ってドバドバっとメールを送るなんてことができません。2 本までは張れますが、残りはコネクションを張ることができず待たされます。Web ブラウザーなんかの HTTP コネクションと同じ感じですね。

これだとスループット確認のために十分な負荷をかけられないので、こいつを増やします。具体的には App.config の中で以下のように書いてます。

<system.net>
  <connectionManagement>
    <!-- See http://weblogs.asp.net/johnbilliris/don-t-forget-to-tune-your-application -->
    <add address="https://email.es-east-1.amazonaws.com/" maxconnection="1000" />
    <add address="email-smtp.us-east-1.amazonaws.com:587" maxconnection="1000" />
    <add address="https://api.sendgrid.com/" maxconnection="1000" />
    <add address="smtp.sendgrid.net:587" maxconnection="1000" />
  </connectionManagement>
</system.net>

なお、同僚に教えてもらいましたが ServicePointManager.DefaultConnectionLimit なんていうプロパティもあるそうで、これでも同じようなことができるようです。こいつの場合はエンドポイント別の指定ができませんが、ほとんどの場合こいつで十分そうですよね。

SMTP セッションを使い回す

こんな話は SMTP でメールいっぱい送るプログラムを書いたことがないと *2 知る由もないのですが、SMTP というプロトコルはセッションの open / close が非常に重いです。重いっていうか無駄。1 通のメールを送るのにサーバーと 7 回とかやり取りしないといけないそうで。そのうち実際のメールを送ってるやり取りの回数は 3 回とかだそうで、半分以上はセッションの open / close というのですから驚きを禁じ得ません。例えば 10 通メールを送るとして、1 通毎にセッションを開いたり閉じたりしたら 7 * 10 = 70 やり取りです。1 本のセッションで 10 通まとめて送れば 7 + 3 * 9 = 34 やり取り!エコ!!

実際には open / close のための通信量が微少なのに対しメール本体はドカンと行きますから、やり取り回数削減のインパクトほどにはスループット改善しないことの方が多いでしょうけれど *3、それでも無駄は減らすに限りますね。日本人ですから。

というわけで .NET でどうすんだ?というと簡単です。標準で付いてくる SmtpClient クラスのインスタンスを使い回します。

using (var client = new SmtpClient())
{
    foreach (var message in messages)
    {
        client.Send(message);
    }
}

みたいにするだけです。詳しくは MSDN ライブラリ に力の入った解説がありますからそちらをどうぞ。私のコードではいろいろ抽象化してる関係上よく分からんことになってますが、

https://github.com/atsukanrock/EmailThroughputChecker/blob/master/EmailThroughputChecker/Program.cs#L37

この辺で using を開始してその後でループをぶん回してますね。

まとめ

たかがメール送信、されどメール送信ですね。

*1:「スレッドプールを用いるもの」に限る話で、new Thread しちゃえば心優しくない使い方もできる

*2:もしくは telnetSMTP!!

*3:サーバーがどれだけ遠いか = レイテンシも変数に入ってきますね