Azure Functions で Domain-Driven Design (DDD) の Domain Event を実装する
この記事は Sansan Advent Calendar 2017 の 25 日目、すなわち最終日の記事です *1。ネタもないので参加する気はなかったのですが、とある社内バー的なもので酔っ払っていたところ会社の新卒君である id:kanjirz50 が目を輝かせて「最終日を飾ってください!!」と言ってきたのであまりのピュアネスが眩しくて二つ返事で快諾してしまいました。というわけでネタに困っています。
なんでそんなにネタに困っているかというと、実はここ 2 年弱程、開発者ではなくて開発マネージャーをやっていた関係で、技術ネタがないのです。この 11 月から改めて開発者になったので、今はリハビリ中でして、技術ネタは徐々に溜まってくると思います。 Advent Calendar と言っても必ずしも技術ネタである必要はなくて、 Sansan Advent Calendar 2017 では 5 日目とか 20 日目がジョブチェンジとか社内転職とかをネタにしているので、開発マネージャーの立場から開発者に復帰した経緯とか、開発マネージャーとして経験したことや学んだこととかを書くという手はあるんですが、復帰後の話を書くにはまだ少し時期尚早な事情があるので、そういうネタは今後のために取っておこうと思います。
さて、
結論
から書きますよ。 Azure Functions で Domain Event を実装する手法のいち提案は以下。
- Service Bus Topic / Subscription を使う手がある
- 現時点では Subscription から Queue への転送も使うと良さそう
はい、ツラツラと書いていきます。
Domain Event って何?
発想自体は GoF のデザインパターンにおける Observer パターンと一緒だと思ってます。 Observer パターンそれ自体はインプロセスでクラス同士の関連で実装されることが多いですが、 Domain Event ではこの表現形式が時にプロセスやネットワーク境界をまたぎます。今回この記事でテーマにしているのは、ネットワーク越しの Domain Event です。
Domain Event は、 Domain-Driven Design (DDD) 界隈では定番となっている設計手法です。あんまり頑張って説明しなくても世の中に良いドキュメントが溢れているのでそちらを参照されたいです。とだけ言うのもあんまりにも突き放し過ぎな気もするので適当にググって出てきたページへのリンクでも張ろうと思ったら、マイクロソフトの機械翻訳が笑えたのでそのページへのリンクを張ります。
話は脱線しますが最近のマイクロソフトさんはアーキテクチャ設計に関するドキュメントをドッサリ出していてすごいですね。読みきれないです。上記は Microservices に関する一連の重量級のドキュメント「マイクロサービスで DDD と CQRS パターンを使ってビジネスの複雑さに取り組む」の 1 ページです。昨今の流行りをもうこれでもかというぐらい盛り込んでいてタイトルだけでお腹いっぱい。昔は Web で読めるのはマイクロソフトテクノロジーの使い方的なドキュメントに留まっていた印象があるのですが、良い時代になりました。あとは機械翻訳さえ洗練されれば言うことなしですね。
さて Domain Event ですが、実は DDD の原著であり聖典たる Eric Evans の DDD 本ではたしか出てきていない手法で、
Domain-Driven Design: Tackling Complexity in the Heart of Software
- 作者: Eric Evans
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2003/08/22
- メディア: ハードカバー
- 購入: 4人 クリック: 113回
- この商品を含むブログ (89件) を見る
それより 10 年も後に出た Vaughn Vernon による名著 IDDD で示されたものだった気がします。
Implementing Domain-Driven Design (English Edition)
- 作者: Vaughn Vernon
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2013/02/06
- メディア: Kindle版
- この商品を含むブログを見る
その辺りの経緯は詳しく知りませんが、きっと Vaughn Vernon 自身が一から考えだしたものというわけでもなくて、 DDD Community の 10 年間の積み重ねのなかで、どこかで生まれたものなのではないかと想像しています。
なぜ Domain Event?
私の理解では「依存関係を逆転させるため」です。広い意味で DIP (Dependency Inversion Principle: 依存性逆転の原則)、と言ったら DIP を拡大解釈しすぎかもしれませんが、私は本質的に似通っているように感じています。以前に AWS Dev Day Tokyo 2017 というところで登壇した時の自分の資料にちょうどいい図があったので埋め込みます。
左側の図 (NOT Domain Event) で言うところの Operation A は次に Operation B が実行されるべきだと知っています。これは A が B に依存している状態です。 Domain Event によって A は自身の完了後に B や C が実行されるべきだという知識を持たなくて良くなります。代わりに A の責務は、自身の完了を Event Aggregator に知らせることだけになります。これで A から B への依存はなくなりました。代わりに B から Domain Event への依存が発生しますが、 Domain Event の抽象度が十分に高ければ問題ないことが多いです。
よくある例だと、企業向け SaaS における設計だとして、 A が何らかの重要なデータの更新処理、 B が管理者ユーザーへのメール送信処理、とかですね。これは DDD 的には A が Domain Layer で、 B が Application Layer で起こっていることであり、 Layer をまたぐ例です。 Domain Layer は Application Layer に依存すべきではないので、 Domain Event により依存関係を逆転させます。
今回私が Domain Event を使ったのは Layer またぎではなくて、 DDD 的用語で言えば Bounded Context をまたぐケースでした。 Bounded Context とは何ぞや、という話をし始めるとキリがないのでまた別の機会に。ちょっと諸事情により今回の記事のネタになった実例を挙げづらいので無理やり別のところから例を持ってきます。 企業向け SaaS である Sansan では、企業の名刺データを一括管理できます。ユーザーが名刺をスキャンしたら独自のテクノロジーなんかを駆使してテキストデータ化され、テナント企業向けの名刺 DB に蓄積されていくわけなんですが、もちろん名刺 DB 構築以外にも機能があります。例えば名刺がテキストデータ化されたタイミングで、当該名刺データをもとに Salesforce 上にリードや取引先責任者をつくる機能があったりします。前述の例の A と B で言えば、 A が名刺のテキストデータ化、 B が Salesforce への書き込みです。 名刺のテキストデータ化処理の最後に Salesforce への書き込み処理の起動処理があるのは、名刺のテキストデータ化文脈から見ると不純物であり、不適切な依存であるというわけです。 Bounded Context をどの程度の粒度と定義するかにもよりますが、 A と B が異なる Bounded Context に属するケースの典型例ですね。 Domain Event はこういった不適切な依存関係の解消に役立ちます。
どうやって Domain Event?
息切れしてきたので駆け足で。 Azure の PaaS を組み合わせて実装するには、 Pub/Sub モデル用に設計されている Service Bus の Topic/Subscription が使えます。 Domain Event とは Observer パターンであり、 Observer パターンとは Pub/Subモデルですから、まさにおあつらえ向きというわけです。
Topic/Subscription と Queue を準備する
こんな感じで Topic を作って、、
Subscription をぶら下げます。 Topic : Subscription = 1 : N にできるので、上の方のスライドの右側の図のように、 A の処理後に B と C がトリガーされる、というのが容易に実現できます。
それでもって Queue も作っておきます。最初の結論で書いた、 Subscription から Queue への転送における転送先用です。
そして、ここがミソなんですが、 Subscription から Queue への転送を設定します。これが 2017/12/26 現在 Azure Portal からはできないようで、 Service Bus Explorer 等から設定することになります。
残念ながら ARM Template でも設定できないそうです。 もしくは ARM Template で subscription の forwardTo
プロパティとして設定します (長らく古い情報のまま放置してしまいましたが 2018-11-19 に修正)。他には Azure CLI からとかならできる感じでしょうか、調べてませんが。
コードを書く
ここまでできたら後は簡単で、 Topic にメッセージを放り込むところと Queue からメッセージを読み出すところを実装するのみです。愚直に C# で書いてみます。
まずは Topic にメッセージを放り込むところ。 Visual Studio で HTTP Trigger の Function を新規作成しただけのコードに、放り込むコードを足しただけのものです。足したのは日本語でコメントした箇所です。放り込む型は何でも良いのですが、ここでは string にしています。任意の型を JSON にシリアライズして放り込んだりすれば良いのでないでしょうか。
using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Host; using Microsoft.Azure.WebJobs.ServiceBus; namespace FunctionApp2 { public static class Function1 { [FunctionName("Function1")] public static async Task<HttpResponseMessage> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, // Topic にメッセージを放り込むための引数 [ServiceBus("function1-completed", EntityType = EntityType.Topic, Connection = "ServiceBusConnectionString")] IAsyncCollector<string> asyncCollector, TraceWriter log) { log.Info("C# HTTP trigger function processed a request."); // parse query parameter string name = req.GetQueryNameValuePairs() .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0) .Value; // Get request body dynamic data = await req.Content.ReadAsAsync<object>(); // Set name to query string or body data name = name ?? data?.name; // ここで放り込む await asyncCollector.AddAsync(name); return name == null ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body") : req.CreateResponse(HttpStatusCode.OK, "Hello " + name); } } }
そして受け取る側のコードはこんな感じ。 ServieBusTrigger
のところが Subscription でなく Queue をバインドしているところがポイントです。上の方で Subscription から Queue への転送を設定しているので、ちゃんとメッセージがやってきます。
using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Host; using Microsoft.ServiceBus.Messaging; namespace AnotherBoundedContext { public static class Subscriber1 { [FunctionName("Subscriber1")] public static void Run( [ServiceBusTrigger("subscriber1", AccessRights.Listen, Connection = "ServiceBusConnectionString")]string name, TraceWriter log) { log.Info($"C# ServiceBus queue trigger function processed message: {name}"); } } }
なぜ Subscription から Queue に転送するか
最後に、なぜ Subscription を直接バインドせず、一手間加えて Queue への転送を設定するかです。今回この判断をした直接の理由は、 Functions と Service Bus の連携において、 2017/12/26 現在ではまだ Exponential Backoff 的なものが実装されていないことです。
例えば上の例で Subscriber1 が外部サービスの Web API 呼び出しを含むとします。そして外部サービスが (REST だとして) 500 系のレスポンスコードを返してきたとします。 500 系のレスポンスコードは一時的な (transient) エラーを表しますから、少し間を置いて同じリクエストを再度送ると、その時には成功する可能性があります。このリトライは、インメモリでループ的に実行することもできますが、せっかく Service Bus を利用しているなら、 Service Bus 自身が備えているメッセージの再実行機構に乗っかることができます。一般的にメッセージングを使う設計においては、 1 つのメッセージの処理は長くなりすぎないようにした方が良いとされていますが、ループ的にリトライ処理をするとこれに反してしまうので、今回はメッセージの再実行機構に乗っかってリトライすることにします。
ただ、何の制御もしないと場合によっては 1 回目のエラーの直後に 2 回目のメッセージ処理が走る可能性があります (Queue に他のメッセージが溜まっていない場合そうなります)。呼び出し先の外部サービスはエラーを返したのだから、さすがに少し間を置いてあげた方が良いと思われます。また、 1 回目の失敗から 2 回目の実行の間隔より 2 回目の失敗から 3 回目の実行の間隔は、長くした方が良いでしょう。このようにリトライ回数が増えるごとに実行間隔を長くする設計手法を Exponential Backoff と言います。
Functions と Service Bus を連携する場合、組み込みの Exponential Backoff は 2017/12/26 時点でサポートされておらず、実装しようとしたらおそらく、 Functions の実行自体はエラー終了ではなく成功とし、自前で ScheduledEnqueueTimeUtc
を設定した BrokeredMessage
を入り口と同じ Queue に放り込むようなコードを書くことになります。そういうことをしようとした時に、 Subscription を直接使っていると不都合が生じます。 Subscription を直接使っていると、リトライのために BrokeredMessage
を放り込む先は当該 Subscription が結び付けられた Topic になりますが、そこに放り込んだメッセージは当該 Topic に結び付く全ての Subscription に配信されます。エラーになったのはそのうち 1 つの Subscription の処理ですから、これは不適切です。
というわけで、いったん Queue に転送することで取り回しが楽になりそうです。 Dead Letter が出た場合なんかにも、同じことが言えそうです。
まとめ
書いてるうちにとても長くなりましたが、この記事はこんなところで。使ってみると分かりますが Functions はまだまだ発展途上感があります。今後の進化に期待ですね。
*1:書いているうちに 26 日になってしまいました…
AWS Dev Day Tokyo 2017で登壇しました
ブログがあまりにも休止状態で自分にドン引きしていますが、先日開催されたAWS Summit Tokyo 2017に併設の開発者向けイベントAWS Dev Day Tokyo 2017で登壇しました。スライドがこちら。
後ほどAWS Summit Tokyo 2016のようにレポートサイトが公開されるそうです。レポートサイトになったらURLが安定しそうですね。 開催後にレポートサイトが公開されていました (2018-11-19追記)。
大きな会場で多くの聴衆の前での登壇となり、内心おっかなびっくりだったのですが、ピンマイクを装着させてもらったことでちょっと楽しくなってしまい、壇上をウロチョロしながらオーバーアクションで喋ったら面白いかもしれない、等と思いつつウロチョロしておりました。しかしなかなか思い切ったアクションというのは難しいもので、やはりこういうのは場数だな、等と思いながら楽しい時間を終えました。こういった機会はありがたいものです。
『C#エンジニア養成読本』の一部を執筆しました
ありがたいことに機会をいただき、『C#エンジニア要請読本』という書籍の一部を執筆しました。今回が初めての執筆です。
こちら共著のムック本でして、他の超強力な著者陣に混じってオマエ誰的な立ち位置を満喫させていただいております *1。
ターゲット層としては C# 初心者および初級者という感じ。新人から 2-3 年目の方にピッタリでしょうし *2、他の言語である程度経験がある方が「C# ってどんなもんよ?」とパラ見するのにちょうど良さそうです。つい 1 ヶ月ほど前に出たばかりの Visual Studio 2015 や C# 6.0 の話題もしっかりあります *3。
という書籍なんですが、私が書いたパートには C# が一切出てきません笑 執筆の話をお受けする際、他の超強力著者陣にビビって「俺ら (同僚の id:kendik も共著&初執筆です) が書けるとしたらこの辺ですかね~」となったからです。ちょっとぐらい C# 書きたかったかも。。と少々後悔しつつも、私の C# 力はごくごく平均的なため、テーマが「C#」ではやっぱり厳しかったかも。。などと思っております。
それにしても Amazon で自分の名前を検索したら出てくるってのは感動モノです。機会を下さった方々に感謝です。
Amazon SES および SendGrid によるメール送信のスループットを調べるツールを C# で書いた
仕事で使うためにやっつけで書いていたのですが、やってるうちに勢い余って割と作り込むあるあるを踏み抜き、せっかくなので公開することにしました。
何ができるかっていうと
大したことはできません。片手間でできるものなんてそんなものですね。あえてできることを挙げるとすると:
- Amazon SES のメール送信 API のスループット確認
- SendGrid のメール送信 API のスループット確認
- 何らかの SMTP サーバー (Amazon SES や SendGrid、Mandrill やなんかもみんな対応してますね) のメール送信のスループット確認
はい、それだけです。スループットを手軽に確認するために、コマンドライン引数で動作を微調整できます:
- 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 ライブラリ に力の入った解説がありますからそちらをどうぞ。私のコードではいろいろ抽象化してる関係上よく分からんことになってますが、
この辺で using
を開始してその後でループをぶん回してますね。
まとめ
たかがメール送信、されどメール送信ですね。
今だから学びたい!DDD (Domain-Driven Design) - Sansan .NET勉強会 #10 に登壇しました
勤務先で開催された今だから学びたい!DDD (Domain-Driven Design) - Sansan .NET勉強会 #10 という勉強会に登壇しました。私が好きな設計手法である DDD についての発表ということで、けっこう気合いが入っていました。時間の都合上、内容が浅くなってしまったのが心残りです。でも次回以降も同じテーマで機会をいただけるなら、気合いを入れて勉強して発表したいです。
発表資料
(第八回) 「ネクストスケープ×Sansan 」 .NET勉強会で発表しました!!
2月27日の金曜日、(第八回) 「ネクストスケープ×Sansan 」 .NET勉強会に登壇しました。その際の発表資料を公開します。
自分はそもそも生粋の開発者気質でして、ID連携というテーマ自体にはそれほど興味を持てなかったというのが正直あります。きっと自分だけじゃないと思って今回のテーマを選びました。なんで興味が持てないか。いろいろな原因があると思うのですが、自分の場合は以下のような感じでした。
- 「所詮ログイン、簡単なことでしょ?」という感覚
- そのくせ調べ出したら情報が大量に出てきて「決定版」が何なのか分からない
- サービス・アプリケーションが主に解決したい問題・課題 (Domain) とは関係がない
きっと同じように感じている開発者が一定数いるはずと信じ、その人たちが自分と同じところで悩むことが少しでも減るように。という思いで作ったスライドです。細かいことは端折ってます。
Visual Studioでお手軽にメモリリーク調査♪
Visual Studio 2013 (VS) の "Performance and Diagnostics Hub" が超いいです。とうわけでこの記事は自分向けのリンク集です。
.NETアプリのメモリリーク調査には従来からWinDbgという方法がありましたが、WinDbgはとても手軽とは言えません。Performance and Diagnostics Hubなら、VSでデバッグ実行しながら好きな時にメモリ状態のスナップショットを取れます。スナップショットを取る前には好きな時にGCを実行できます。スナップショットには、メモリ使用量はもちろん、オブジェクト型毎のインスタンス数が記録されます。これらの値はスナップショット間での比較が可能なので、リークしている (≒インスタンス数が増え続ける) オブジェクトを特定できます。この記事の方がイメージが分かるスクリーンショットが多いですね。
なおVSのエディションがUltimateなら、Production環境 (ソースがない) でも解析が可能みたいですね。ProdDumpを使って取得したdumpファイルをVSで読み込んで、マネージドメモリーの調査ができるようです。良さそうですね。自分はUltimateエディションを使えないので試せませんが。
メモリリーク調査の基本的な考え方を知るには、"実戦的WPF・Silverlightメモリーリーク解消方法" (パワポ直リン注意) というパワポが良いと思いました。