[翻訳] サーバレスアプリケーションにおけるEvent-Driven Architectures と Event-Based Computeの比較

2023.04.25

About this article

この記事は、
Event-Driven Architectures vs. Event-Based Compute in Serverless Applications
の記事を
Momentoが著者および[Riywo]氏(https://twitter.com/riywo)の許可を得て邦訳しました。

著者のAlex Debrieについて

元記事の著者であるAlex Debrieは欧米のサーバレス界隈で有名な人物です。
彼はDynamoDBに関する書籍の著者でもあり、AWSのサーバレスヒーローでもあります。

Translated article

コメント&補足

翻訳元の記事「Event-driven vs Event-based」という記事は、
イベントドリブンアーキテクチャとイベントベースのアーキテクチャの違いについて
わかりやすく説明しており、それぞれの
メリット・デメリットについても述べられています。
イベントドリブンアーキテクチャは、アプリがイベントを受け取った際に
反応する方法です。 イベントベースアーキテクチャは、アプリケーションがイベントに   基づいてデータを収集し、処理する方法です。

はじめに

私は最近サーバレスのトレーニングを何人かのエンジニアに行いましたが、
サーバレスアーキテクチャの討論の際に出てきた
2つの概念の間に混乱があることが分かりました。

一方で、私はAWS Lambda をイベントベースコンピュートとして説明していて、
それはサーバレスアプリケーションでどの様にコードを書き
アーキテクチャを設計するかについて、重要な示唆を与えてくれます。

他方で、多くのサーバレスアプリケーションは、
アプリケーションを跨った疎結合で非同期なイベント処理に依存した
イベントドリブンアーキテクチャを使っています。

この二つの概念 — イベントドリブンアーキテクチャとイベントベースコンピュート — は
同じ様なものに聞こえ、AWS 上でのサーバレスアプリケーションでは
しばしば一緒に利用されますが、同じものではありません。
さらに、片方で使われるパターンはもし他方を使っていないなら
必ずしもそちらにも適応できる必要はないのです。

KEY POINT

AWS Lambda コンピュートのイベントベース性は、AWS Lambda とそれ以外の
コンピュートモデルを根本的に区別するものであり、
サーバレスアプリケーション開発者に独特の制約と要求をもたらします。

この投稿では、イベントドリブンアーキテクチャと
イベントベースコンピュートの両方を見ていきます。
それぞれの鍵となる特徴と、皆さんのアプリケーションに対する
隠された意味を調査していきます。

イベントドリブンアーキテクチャとは?

コメント&補足

このセクションでは、イベントドリブンのアーキテクチャについての解説をしています。
このタイプのアーキテクチャでは、他プロセスをブロックすることなく
イベントに対応できるように設計することが必要です。

イベントドリブンアーキテクチャ (Event Driven Architecture)は
大流行していますので、まずはそちらから始めましょう。

イベントドリブンアーキテクチャは

(1)非同期に
(2)イベントを通じて

通信をするサービス群として特徴づけられます。
この2つの要素こそが他のアーキテクチャパターンと
イベントドリブンアーキテクチャを区別するものになります。

イベントドリブンアーキテクチャは非同期で通信する

もし皆さんがバックエンドのGraphQL API を呼び出すフロントエンドクライアントや、
ほかのREST API やRPC 経由で他のサービスを呼び出す
バックエンドサービスに携わったことがあれば、
リクエスト-レスポンスパターンは経験したことがあるでしょう。
これは1つのクライアントから1つのサービスを呼び出す典型的な通信パターンです。

これは同期的なフローで、クライアントはサービスからリクエストの結果を
取得するためにすべてのレスポンスを待ちます。
同期的なフローはそのリクエストから何が起こったかを
直接的にフィードバックを得られるので単純です。  

しかし、同期的な通信に頼るのはコストもかかります。
特に、Eメールを送ったりレポートを生成するような遅いタスクを行う場合には、
アプリケーション全体のパフォーマンスを低下させます。
さらに、サービスの可用性も下がります。もしサービスA がサービスB からの
同期的なレスポンスに依存している場合、サービスA の稼働率は
サービスB よりも高くはなれません。
サービスB のダウンタイムはサービスA のダウンタイムになります。

非同期パターンでは、これらの問題は軽減されます。
サービス間はまだ通信していても、即座のレスポンスは全く期待されていません。
下流のサービスは、上流のサービスと強く紐づくことなく、
自身のスケジュールで通信を処理することができます。
これは、デバッグや結果整合性、それ以外にもたくさんの課題をもたらしますが、
同期通信における欠点を軽減してくれます。

リクエスト-レスポンス対イベントドリブンについてもっと知りたい方は、
元AWS Developer Advocate のTalia Nassi
イベントドリブンアーキテクチャへの移行の利点に関する投稿もご覧下さい。

イベントドリブンアーキテクチャはイベントを経由して通信する

驚くべきことではありませんが、"イベント" の部分が
イベントドリブンアーキテクチャ特有のものです。
イベントドリブンアーキテクチャでは、
サービス達は他のサービスが利用し反応するイベントをばらまきます。

そのため、イベントは2種類の登場人物を必要とします。

  • イベントプロデューサー

そのサービス内で何かが起こったことを説明するイベントをパブリッシュします。
例: ユーザーが作成された、注文が配達された、またはログイン試行が失敗した

  • イベントコンシューマー

パブリッシュされたイベントをサブスクライブし、それに反応します。
例: ローカルの状態を更新、集計を増加させる、ワークフローを起動する等

真のイベントドリブンアーキテクチャの鍵となる特徴は、
プロデューサーとコンシューマーが完全に疎結合であることです。
プロデューサーは誰がイベントを使っているか、コンシューマーがイベントを
どのように使っているかなどを気にする必要はありません。

イベントプロデューサーは夜のニュース番組のキャスターの様なものです。
誰かが番組を見ているかどうかは関係なく、キャスターは起こったニュースを伝えるでしょう。

これを、もっと古典的なメッセージドリブンアーキテクチャと呼ばれる、
システム内の1つのコンポーネントが処理をさせるために
メッセージキュー(例: SQS やRabbitMQ) に
メッセージを格納するアーキテクチャと比較してみましょう。
メッセージドリブンパターンはイベントドリブンパターンと同じく非同期です。
しかし、プロデューサーのメッセージは特定のコンシューマーの仕事のために
目的をもって送られます。
キューの利用は弾力性を高め、最初のコンポーネントのレスポンス時間を速くしますが、
メッセージのプロデューサーとコンシューマー間に強い結びつきがあるという意味で、
通常は真のイベントドリブンであるとはみなされません。

イベントドリブンがテレビのキャスターとすると、
メッセージドリブンはあなたの上司がレポートを作成するように依頼するEメールを
あなたに送る様なものです。
レポートという明確に要求されたアウトプットがあるだけでなく、
処理して欲しい特定の人物(あなた)がそこにはあります。

NOTE
世の中のいくつかの情報では、メッセージドリブンのワークフローを
イベントドリブンアーキテクチャの一部とみなしているものがあります。
私はどちらかというと反対ですが、大抵はそんなに気になるほどではありません。
メッセージドリブンパターンを使うための示唆や理由の一部が
イベントドリブンパターンと似ていて、
そのためにいくつかの教訓が似ていることには強く同意します。

イベントドリブンアーキテクチャの主な利点は、柔軟性と弾力性です。
もし既存のイベントに新しいコンシューマーを追加したくなった時も、
イベントのプロデューサーと協調する必要はありません。
イベントは既にパブリッシュされていて新しいサービスでも利用可能になっているので、
すぐにイベントを処理し始めることができます。

イベントドリブンアーキテクチャは使われる様になってからしばらく経っています。
もし90年代から2000年代にかけてエンタープライズ企業で時間を過ごした
開発者と話す機会があれば、
おそらくエンタープライズサービスバスに関する不満を聞くことになるでしょう。
最近でいうと、Apache Kafka の台頭とJay Kreps (元々のKafka の作成者の一人)の
ログとストリームの有用性に関するとても効果的な伝道は、
イベントドリブンアーキテクチャの中で新しい形で息をしています。

世の中にはイベントソーシングCQRS といった
純粋主義パターンを含む、イベントドリブンアーキテクチャのたくさんの亜種があります。
私は通常これらを避けることを進めています。
これらはすべての可能性について考えて想像する分には楽しいですが、
メンテナンスやデバッグが苦痛になってしまうのを見てきました。

以上はイベントドリブンアーキテクチャの雑なレビューに過ぎません。
これについてもっと知りたければ、AWS Developer Advocate のDavid Boyne
イベントドリブンのすべてについてフォローしておくべき人物です。
Serverless Land の多数の可視化を含む、   イベントドリブンアーキテクチャの多数の優れた記事があります。

イベントベースコンピュートとは?

コメント&補足

このセクションでは、イベントベースコンピュートについて解説しています。
イベントベースコンピュートは、サーバレス環境でよく使用されます。
このアーキテクチャは、アプリをよりシンプルで効率的にするための設計手法です。

さて、イベントドリブンアーキテクチャについて少し理解したところで、
イベントベースコンピュートに話を移してどれくらい違うものなのかを見ていきましょう。

イベントベースコンピュートには2つの鍵となる特徴があります。
1つ目に、コンピュートインスタンスの存在は処理されるイベントの発生と密に結合しています。
2つ目に、コンピュートは同時に1つのイベントだけに対応します。

これは少し抽象的なので、もっと具体的にしてみましょう。
Lambda 関数を作るときを考えてみましょう。短いコードを書き、ZIP ファイルを作成、
AWS にアップロードして、Lambda のサービス上で設定を行います。
この設定では、EC2 インスタンスやFargate コンテナとは違って、
実際にはまだコードを実行しません。
デフォルトでは、Lambda コンピュートが実行される実際のインスタンスは存在しません。
Lamba 関数はインスタンスを持つ可能性を持っていますが、まだ実体化はされていません。

その関数を実体化するには、イベントソースと繋げる必要があります。
Lambda と連携しているサービスはたくさんあります
最も人気のあるソースはおそらく、
API Gateway (HTTP リクエスト)、SQS (キュー処理)、EventBridge (イベントバス)、
そしてKinesis / DynamoDB Streams (ストリーム処理)でしょう。

WARNING
気を付けて欲しいの が、上でリンクしているLambda のドキュメント上では、
私としてはイベントドリブンと説明したくない多くのイベントソースを含めて、
'イベントドリブン'という用語をつかっています。詳しくはこの後説明します。

一旦イベントソースを設定し、その設定されたサービスを通してイベントが流れ始めると、 
関数に息が吹き込まれます。
Lambda のサービスは関数のインスタンスを作成し、
引き金となったイベントが関数によって処理される様に受け渡します。
関数は需要に応じてイベントを処理し、イベントの引き金にレスポンスを返します。

最適化のために、Lambda サービスは短い時間内に起こる他のイベントを捌けるように、
関数のインスタンスを実行されたままの状態でしばらく保持することがあります。
しかし、この仕様はほとんど皆さんの制御外のものです。

重要なことは、一つの動いているコンピュートのインスタンスの目的は、
一つの、特定のイベントを捌くだけだということです。
これが、ただやってくるリクエストを捌いたり、
キューやストリームからメッセージをポーリングしている
古典的なインスタンスやコンテナからLambda を区別します。
それはさらに、Fargate タスクをスケジュールで作成する様なものからもLambda を区別します。
例えタスクの作成がイベントに基づいていても、そのタスクは実行している間には、
タスクを作成したイベント自体に自然には気づくことはありません。

イベントベースコンピュートの示唆

これでAWS Lambda がどういう風にイベントベースコンピュートであることが分かりました。
でもそれがどうしたことでしょうか?実際アプリケーションにどう影響するのでしょうか?

私の考えでは、これはAWS Lambda に関する最も重要な考え方の転換です。
これによって、Lambda を使って開発するエンジニアには
以下の様ないくつかの好みが生まれます。

  • Django、Express、Spring Boot の様な重厚な既存のウェブフレームワークを関数内で使うのを避ける (いくらかはSnapStart の様なもので軽減はできます)

  • より小さいライブラリを選ぶ (例: Prisma よりもKysely)

  • 接続数の上限がないデータベース(例: DynamoDB)、またはコンピュートの外でコネクションプーリングを管理してくれるもの(PgBouncer やAmazon RDS Proxy) を使う

  • 待ち時間があるときは setTimeout() をアプリケーションにコードで使うよりも、プラットフォーム側(SQS、Step Functions、cron)にオフロードする

これらの好みは普遍的なものではありませんので、
Lambda ベースのアーキテクチャ毎に多くの多様性があります。
しかし、特に他のコンピュート手法を使ったアーキテクチャと比較する時には
こうした傾向をよく見かけるでしょう。

Lambda のイベントベースの特性からは、
皆さんが常に頭に置いておくべき2つの主な示唆が導かれます。

1つ目に、ステートレス性と高速な初期化について普段よりもっと意識する必要があります。
インスタンスベースやコンテナベースのアプリケーションでは、アプリケーションの初期化、
データベース接続の確立、ローカルキャッシュの構築、他の初期化処理を
リクエスト処理可能にする前に行うことができます。

でもLambda コンピュートではこれは当てはまりません。
繰り返しますが、イベントに反応する際にコンピュートを初期化することになります。
そこにはアクティブなHTTP リクエストがあって、
その反対側には今まさに本物のユーザーがいて、
最新のツイートが見れることや、Taylor Swift のチケットを
購入するのを待っています。
皆さんは、自分のコードが無駄なセットアップをせずに、
高速に起動して実行できるようになっていることを確認する必要があります。

特に、これは関数のコードを小さく浅く保つということを意味しています。
複数のファイルに跨った多層にもわたる依存関係の読込と初期化を避けましょう。
状況によっては、esbuild や他のツールを使ってコードを1つのファイルにまとめあげて、
初期のディスク読込時間を減らすことを検討することができるでしょう。

また、後続のリクエストのパフォーマンスを改善する技をつかうことも検討しましょう。
データベース等のネットワーク接続の確立や、動的な設定を取得する様な、
コールドスタートの初期化時にしなければならないいくつかの種類の仕事がありますが、
全リクエストをコールドスタートの様にしないために複数のリクエスト間で
これらのリソースをどうやれば再利用できるかを理解すべきです。

2つ目に、
スケーリングの意味が、単体のインスタンス内の並行度ではなく
コンピュートインスタンスの数になるということです

1つのLambda 関数のインスタンス内では、たった1つのイベントしか同時に処理できません。
もし複数のイベントが同時に発生したら、それらのイベントを捌くためにLambda サービスが
もっと多くの関数のインスタンスを立ち上げます。でも常にイベントは1つずつ処理されます。

これはアプリケーションのコードをどの様に書くかだけでなく、
どの言語を選択するかをも変えてしまう可能性があります。
Ben Kehoe は、非同期第一のアプローチであるが故に、
Node はサーバレスの実行環境としては間違っていると言いました。
このアプローチは、並列で複数のウェブリクエストやキューのメッセージを
バッチ処理する古典的なバックエンドアプリケーションでは有用です。
しかし、Lambda はイベントを1つずつ処理するため、
通常はそれほど非同期処理を必要としません。
イベントを1つずつ順番に処理しているので、
もっと率直な手続き型の処理を有効に使うことができます。

Ben の投稿は6年前となってしまって、その時からはNode.js にも
いくつかの変更(特にasync / await) があって、
Lambda の世界でNode のモデルの尖った部分を
削る手助けをしてくれる様になりました。
しかし、どんな言語を選んだとしても、
この1イベントモデルがアプリケーションにどの様に作用するかを考慮すべきです。

Lambda は関数インスタンス内で垂直にスケールするのではなく、
関数インスタンスの数が水平にスケールするので、
コンピュートレイヤー内では並行するリクエスト間でリソースを共有することができません。
わかりやすい例としては、データベースのコネクションプールで、
古典的なウェブサーバではこれを使って複数のリクエスト間で共有をおこなっています。
これが、上述した様に、Lambda のユーザが接続数上限のないデータベース(例: DynamoDB)や、
コンピュートの外でのデータベースプーリングの実装(例: Amazon RDS Proxy経由)を
好む理由につながります。

さらには、コンピュートは1イベントずつ処理してアクティブな間の分だけ課金されるので、
可能な限りアプリケーションが何もしていない時間を減らしたいものです。
バックグラウンドジョブを行うためにウェブサーバで
setTimeout() を使うことはもう無いでしょう。
もっとよくある例を出すと、関数内では規則的なロングポーリングを避けたくなるでしょう。
次に進む前に何かが終わるのを待っているのであれば、
それ自体を別のイベントに変えることができないかを考えてみましょう。
もしできないとしたら、ポーリングの引き金をコンピュートの外で実装しましょう。

“event”に関する混乱

コメント&補足
このセクションでは、AWS Lamdaと各サービスを組み合わせることにより、
イベントドリブンまたはイベントベースなのかということについて解説しています。

ようやくイベントドリブンアーキテクチャと
イベントベースコンピュートが分かったところですが、
これらはお互いにどう影響するのでしょう?
AWS Lambda ではイベントドリブンアーキテクチャを使わなければいけないのでしょうか?
イベントベースコンピュートをKubernetes で使うことはできるのでしょうか?

上述したように、
私はLambda が連携しているサービスを説明しているLambda のドキュメントでは
この領域の混乱は解消しないと考えています。
そのドキュメントではAPI Gateway からのHTTP リクエストの様な古典的定義では
イベントドリブンとはならない多くのイベントソースを、
イベントドリブンとして説明しています。
さらには、イベントドリブンパターンを活用している多くのサービスとの連携
(例: DynamoDB Streams, Kinesis Streams, Apache Kafka)を
イベントドリブンではない、と定義しています。

幸運にも、私は既にこれらがどのようにオーバーラップしているかを示すベン図を作成しました。

  • Event-based ⇒ イベントベース
  • Event-driven ⇒ イベントドリブン

基本的な原則は以下です。

  • AWS Lambda 関数は必ずイベントベースコンピュートです
  • AWS Lambda 関数をイベントドリブンパターンに使うことができます
  • イベントドリブンまたは非イベントドリブンパターンをAWS Lambda 以外のコンピュートでも使うことができます。

ここでは全てのケースをカバーしようとはしません。
イベントベースコンピュートを可能にする他のツール(OpenFaaS、Knative)
といったものもあります。
私はそれらは詳しくありませんが、多くの同じ原則を当てはめることができるでしょう。

Lambda + AWS サービス: どれがイベントドリブンでその理由は?

この投稿を公開した後でEmily Shea が、なぜいくつかのAWS サービスがLambda と繋がった時に
イベントドリブン/非イベントドリブンとなるかの理由を詳しく説明すると良い、
と教えてくれました。
以下が手短な概略になります。注意点はいくつかありますが、
各サービスがどの様にLambda を使っているか一般的な形で概要を説明したいと思います。

  • API Gateway + Lambda
    一般的にイベントドリブンではありません。これはリクエスト-レスポンスパターンであり、
    クライアントはLambda 関数からのレスポンスを
    リクエストの結果として受け取ります。
    もしAPI Gateway を古典的なREST API用途(例: “ユーザ取得”、”カートに追加”、等) で
    使っているなら、それはイベントドリブンではありません。
    同期処理であり密結合です。

そうは言うものの、API Gateway を
イベントドリブンシステムの入り口として使うことはできます。
例えば、サービスやフロントエンドのクライアントはAPI Gateway の
エンドポイントにイベントを発信して、それを経由して
Kinesis Stream やEventBridghe バスに送られるかもしれません。
大きな違いはレスポンスコードやボディになります。
もし '202 Accepted' のレスポンスコードや、
eventId または messageId といったプロパティだけを含む
レスポンスボディの場合、そのAPI はイベントドリブンシステムへの
入り口である可能性を示しています。

  • SQS + Lambda 一般的にイベントドリブンではありません。上の同期的なリクエスト-レスポンスパターンとは違って、非同期です。
    しかし、キューベースのパターンは
    通常プロデューサーが求める特定のタスクを持っています(例: “ユーザー作成後のウェルカムメールを送信する”)。
    このため、これは”メッセージドリブン”パターンです。または、”ポイント-ツー-ポイント” とも呼ばれます。

ここでも但し書きが必要です。SQS はバッファまたはスロットリング機構として
イベントドリブンシステムの一部で使われているかも知れません。
例えば、SNS やEventBridgeのコンシューマー (以下で議論しています) は
メッセージをSQS にプッシュして、そこから処理をしているかもしれません。
SQS はスロットリングやリトライロジックに対してよりより制御と可視化をもたらしてくれます。

  • SNS + Lambda イベントドリブンです!少なくとも理論上は。SNS トピックは多数のコンシューマーを持つことができ、
    SNS トピックへのパブリッシャーは通常何か特定の出力を期待してはいません。
    非同期であり疎結合なパターンです。

  • EventBridge + Lambda これもイベントドリブンです!基本的にSNS と同じ特徴を持ちます。
    実際には、EventBridge は1つのイベントバスで異なるタイプのイベントを扱うことにのみ注力しているので、
    SNS 以上にイベントドリブンと言ってもいいでしょう。

  • Kinesis + Lambda 多くはイベントドリブンです!
    SNS やEventBridge 同様ですが、Kinesis の様なストリームベースのソリューションは
    1イベント毎を処理する代わりにバッチ処理を提供しています。
    私の勘では、Kinesis は’真の’イベントドリブンアーキテクチャからは少し遠い形で使われていて、
    その理由は非同期バッチ処理はプロデューサーとコンシューマーが
    互いに知っているような場合でもうまくいくからです。

  • Step Functions + Lambda イベントドリブンではありません!
    Step Function のステートマシンは特定の、定義された、複数ステップのワークフローです。
    いくらかは非同期の要素があるかも知れませんが、プロデューサーとコンシューマーの疎結合はここにはありません!
    とはいえ、ステートマシン実行のステップ間で、イベントを発信したいことがあるかもしれません。
    Yan Cui のコレオグラフィ対オーケストレーションの投稿がこのトピックをカバーしていて、
    私のお気に入りのサーバレスに関する投稿の一つでもあります。

Emily Shea の提案と、Maik Wiesmüller の元々の記事に感謝します。

まとめ

この投稿では、
イベントドリブンアーキテクチャとイベントベースコンピュートの違いを見てきました。
全てのタイプのコンピュートが(正しい状況で使えば)イベントドリブンアーキテクチャを
活用することができることを見ました。
さらに全てのLambda の利用はイベントベースコンピュートに依存していることも見ました。
イベントベースコンピュートのいくつかの隠された示唆と
それがどの様にアプリケーションの設計に影響するかも見ました。

もしこの投稿について何か質問や修正があれば、以下にコメントを残して頂くか、
直接私にメールを下さい!

References