はじめに
This is a translation of The 7 Ways to Wash Dishes and the Case for Message-driven Reactive Systems by Jamie Allen, which originally appeared on the Lightbend blog.
本記事は Lightbend のJamie Allen氏によるブログ「The 7 Ways to Wash Dishes and the Case for Message-driven Reactive Systems」を翻訳したものです。なお、翻訳に際して、LightbendのEugene Yokota氏にレビュー頂きました。ありがとうございました。
理解が難しいリアクティブシステムのコンセプト(非同期とかノンブロッキングとか)を、皿洗いという日常的な行動に当てはめて解説されていて、面白い記事だったので翻訳してみました。以下翻訳です。
非同期、ノンブロッキング、協調、並列性...を理解する
リアクティブアプリケーションのコアコンセプトを効率的に説明する良い方法を見つけようと、私は数年前から取り組んできました。協調 (coordination) の必要性を最小限におさえつつ非同期かつノンブロッキングであること、そして並列性(parallelism)を高めることにより線形(linear)のスケーラビリティを実現することがリアクティブアプリケーションのコアコンセプトと言えるでしょう。これは経験豊かな開発者でさえきっちりと理解することが困難である難解な用語の集まりですが、本当にリアクティブなアプリケーションを構築するために非常に重要であると考えています。
ここ数ヶ月で私はより明確にこれらのコンセプトを表現する方法を発見したかもしれません。また、日常のメタファを通じて隣接する興味深いコンセプト (パイプライン、バッチ、fork/join、アムダールの法則など) も一緒に考えることができました。コンセプトを考えるとき、私は現実世界に当てはめて考えると分かりやすくなると思っていて、Lightbend 顧客の皆さんに説明するにも受けが良いようです。
食器洗浄機
私は皿洗いが好きです。「ちょっと変わった人だ。」と思うかもしれませんが、会ってみるとちょっと変わっていると皆さん思っているみたいなので、仕方が無いですね。私が特に変わっているのは、手洗いだけでなく、さらに綺麗にするため食器洗浄機に皿を入れることです。大学時代にルームメイトと住んでいましたが、その人がチーズたっぷりのパスタを食べて、何日も皿や鍋をキッチンのシンクに放置した挙句、1980年代の食器洗浄機にそのまま入れただけで固くこびりついた食べカスが全て取り除かれると思ってる人だったのが今でもトラウマになっています。この喩え話を色々な人にしている過程で、他にも似たような皿洗い神経症を患わっている仲間を何人か見つけたことは、強迫的な皿洗い行動と共に生きていくのに大きな支えとなっています。
毎晩、夕食後、シンクに綺麗にしないといけない皿の山があります。このプロセスはストリーミングデータの処理で見るようなものです。私は変換処理(transformations)をパイプライン化したプロセスを採用しています。まず、それぞれの皿の食べかすをすすぎ、洗剤とスポンジでこすり洗いして、洗剤を落とすためにもう一度すすいで、最後に食器洗浄機に入れます。私が一度に1つだけのタスクだけを行うように、このプロセスは同期(synchronous; すべて1プロセッサ/人で制御)、かつ逐次的(sequential; 並べ替え不可)です。私はそれぞれの皿を個々のワークパイプラインに送るか、逐次性の保証を緩めて、最初にすべての皿の汚れをすすぎ、次にすべての皿を洗い、もう一度すべてをすすぎ、そのグループで食器洗浄機に入れることで、それぞれの作業で皿をまとめて処理します。そうすることで、実行(蛇口、洗浄用スポンジ、食器洗浄機など)の代わりにデータ(各皿)の局所性(locality)を増加させ、わずかにパフォーマンスを向上させたかもしれませんが、私のパフォーマンスはまだ「すべての作業を単一プロセッサのみで行う」という条件で拘束されています。
ここで、私に食器洗いが大好きな友人がいるとします。その人の皿洗い好きを承知で、ある日、手伝ってもらうため自分の家にその友人を招待します。これでスレッドプールを作成できました。それは、達成したい仕事に貢献するかもしれないし、しないかもしれない複数の実行スレッドです。友人が到着すると、皿の山を伝え皿洗いをお願いしました。友人はシンクに進みパイプライン化されたプロセスを通じて皿洗いをはじめます。私はこれで、ちょうどスレッドプール上でJavaのCallableかRunnableを使うように非同期の仕事を生み出しました。私が彼らの後ろに立って、作業が終わるのを待ち何もしない場合は、ブロックされたスレッドです。私はやるべき仕事を生み出し、任意の時間で作業を行う実行スレッドにその仕事を委譲しました。他のタスクが完了するまで私はJavaのFutureインスタンスのように何もしていないのです。私が突っ立っているだけではなく、レモネードを持っていくと、ノンブロッキングになります(ScalaのFutureかJava8のCompletableFutureのように)。しかし、目の前の作業に対して生産的ではありませんし、友人はおそらく私にかなりイライラとしてきています。また、友人が私よりかなり効率的な皿洗い(例えばより高速なプロセッサ)でないかぎり、仕事はより速く行われません。
理想としては、私たちが二人共この作業がより効率的かつ迅速に行われるように貢献できることです。ある時点で、私は作業を行う友人にジョインします。私の友人には皿の山から皿を掴んでそれをすすぎ、洗う責務があります。私はこの時点で皿を取り、再びすすぎ、食器洗浄機に入れます。これでノンブロッキングであり作業に対しても生産的です。しかし、作業をこのように多段化することにより、我々が最適に仕事をすることに対して我々の能力に影響を与えるリソースを共有しています。友人に委任された作業を担当する実行スレッドとして、どのくらい汚いかに応じて洗う時間が不定量で、私は各皿を待たなければなりません(CPU集約型の仕事の本質)。さらに悪い事に我々は二人共が皿をすすぐために蛇口を使用する必要があり、状態(蛇口)に互いに排他的な操作を持っています。コミュニケーションで仲裁しなければならなく、多くの競合の仲裁、コンピュータのカーネルによって相互に排他的なロック(mutex)が必要になります。これは共有可変状態 (shared mutable state) への競合を回避するのに必要な協調 (コーディネーション、coordination) の典型的な例です。状態(蛇口)が競合しない(必要なときに我々のいずれも使用していない)場合、我々はすぐにタスクを進行することができますが、競合する(蛇口が必要な時、他の一人が使用している)場合、我々は他が完了するまで待って立ち往生します。
リソース競合とそれに伴う協調を改善する方法は、フットプリント (footprint) を増加させることです。私の家が別のシンクがあるくらい大きいと、私は皿を取り、友人と独立して私の作業ができ、より効率的になります。ForkJoinPoolを使用しているJava8やScalaの並列コレクションと似ています。私は皿を1枚つかむことによって、作業を分岐(fork)し、友人と私は有効なコアとして作業を行っています。そして、我々は食器洗浄機に皿を入れるときに再度joinします。しかしながら、並列コレクションのようなforkとjoinフェーズはまだ協調を必要とします。処理されるため我々自身の間でデータ、つまりは皿を分割する必要があり、我々は変換されたコレクション(食器洗浄機)のデータ(皿)にjoinしなければなりません。
このforkは作業がどのように分散されるかによって安価にも、高価にもなります。私が単にプレートの上半分を掴んだ場合、それは簡単な手順です。しかしながら、なんらかの規定された順序で友人と私の間で分散した場合はどうでしょう?その場合はるかに多くのコストがかかります。そして、joinポイントも同様に順序に応じてコストがかかることになります。これはアムダールの法則のコストをもたらします。仕事を並列化したにもかかわらず、forkとjoinの作業が大きすぎた場合、逐次的に行った場合より時間がかかることがあります。
我々の作業は処理効率を最大化するために可能な限り並列にすることが必要で、これを行うためにはさらにフットプリントを増やす必要があります。誰が何をやっているかを理解しようとしないようにするか、或いは、共通キューから作業を受け取るようにするか、なんとかして友人と私の両方で皿を仲介することができれば理想的です。そして、我々は2台の食器洗浄機を持っている場合、単一ジョインポイントの競合を持っていません。フットプリントの増加は追加費用を意味していますが、協調の必要性を減らし、並列性を増やすことでスケーラビリティが線形(linear)になることを意味します。つまり、私がプロセッサー/シンク/食器洗浄機を追加すれば、同じ要素によって処理できる皿(変換できるデータ)の数が増えます。このようなスケーラビリティと効率性を向上するというバリューは、コモディティなハードウェアを業務に追加することによって生じるコストの増加を十分正当化することができるかもしれません。
まとめ
最終的な私の究極の目標は、非同期、ノンブロッキング、並列実行を協調が必要になるポイントを最小限に抑えた形で実現することです。作業を一旦ブローカーに仲介させることによって、各皿 (または皿のひとまとまり) をメッセージとして扱って、どのパイプラインが変換処理 (皿の洗浄) を実行するかを気にする必要が無くなりました。これが位置透過性の本質で、それがリアクティブシステムにおける弾力性 (elasticity) を駆動して、負荷が増加すればノードを追加でスピンアップしたり、仕事が減少すればノードをシャットダウンするといったことが可能となります。さらに位置透過性は、例えばパイプラインの一部が失敗して完全に綺麗になるまで変換できなかった皿が再処理されるといったことに関しても無関心なため、レジリエンスを支えることにもなります。変化し続ける負荷に応じて弾力性があることと、システムのあらゆる種類の障害に対してレジリエンスを持つことで、はじめてレスポンシブなユーザ経験を保証することができます。そのため、メッセージ駆動アーキテクチャはリアクティブアプリケーションの本質と言えるでしょう。