Netflixの広告システムはシンプルに始まった。クライアントがサーバーを呼び、サーバーがMicrosoftのAPIを呼び、レスポンスが返ってくる。同期。クリーン。動く。1つのエンドポイント。1つの依存。リリース。
そしてスケールがやってきた。
ある時点で、すべての広告インプレッションが5つの異なるシステムに通知する必要が出てくる。請求がインプレッションを記録する必要がある(50ms)。アナリティクスがダッシュボードを更新する必要がある(100ms)。フリークエンシーキャップがカウントを更新する必要がある(30ms)。サードパーティベンダーがトラッキングピクセルを発火する必要がある(200ms—外部呼び出しならこれでも楽観的)。不正検出がパターンをチェックする必要がある(80ms)。
同期呼び出しだと、それぞれを順番に呼び出す。請求を呼ぶ、待つ。アナリティクスを呼ぶ、待つ。フリークエンシーキャップを呼ぶ、待つ。ベンダーを呼ぶ、待つ。不正検出を呼ぶ、待つ。やっとレスポンス。
待ち時間を足すと460ミリ秒。ユーザーは自分が見ることのないアナリティクスダッシュボードのためにほぼ0.5秒待っている。それらのサービスのどれか1つが遅かったりダウンしていたら?すべてがブロックされる。
同期呼び出しは依存の網になり、新しい統合を追加するたびにシステム全体がより脆くなる。
真ん中にキューを置く。
広告が再生されたら、Producerが1つのイベントをKafkaにパブリッシュして、すぐにリターンする—最大で5ミリ秒。ユーザーはレスポンスを得る。完了。
一方、5つの独立したConsumerがそのイベントを自分のペースでピックアップする。請求が処理する。アナリティクスが処理する。不正検出が処理する。請求が遅くても、アナリティクスは気にしない。サードパーティベンダーのエンドポイントがタイムアウトしていても、ユーザーは数秒前にすでにレスポンスを得ている。
各Consumerが独立してイベントを処理する。障害は分離される—請求がダウンしてもアナリティクスは動く。何か見逃したらイベントをリプレイできる。新しいConsumerがProducerのコードに一切触れずにサブスクライブできる。
実際にはこんな感じ:
同じイベント、5つのConsumer、ゼロカップリング。ユーザーは460msではなく5msでレスポンスを得る。
各ユースケースに別々のパイプラインを構築するな。タイムスタンプ、user_id、ad_idを抽出する請求パイプラインを作るチームを見てきた—そして全く同じフィールドを抽出するアナリティクスパイプラインを作る。そしてベンダーパイプラインがまた同じことをする。
代わりに、1つの標準化イベントをパブリッシュしろ。スキーマをドキュメント化しろ。バージョン管理しろ。そのデータが必要な人は誰でもサブスクライブして必要なものをフィルタできる。暗号化やエンリッチメントなどの共通操作は5回ではなく1回だけ起こる。
メリットは複利で増える:新しいConsumerがProducerを変更せずに参加できる、きれいな関心の分離、監査証跡が無料。
イベントには間違ったタイミングと正しいタイミングがある。
早すぎる: 2-3サービス、毎秒100リクエストくらい。1チームがすべてをオーナー。シンプルなリクエスト/レスポンスで問題なく動く。監査要件なし。何か失敗しても、許容できるダウンタイムは…許容できる。Kafkaに手を出すな。オーバーキル。
ちょうどいい: 5サービス以上。毎秒1,000リクエスト以上。複数チームが同じデータを必要としている。非同期処理が必要。監査証跡が重要。あるシステムの障害が他にカスケードすることは許されない。
イベント駆動は無料ではない。即時整合性の代わりに結果整合性になる。Kafkaには運用負担がある—設定して忘れられるデータベースではない。デバッグが難しくなる;コールスタックではなくキューをトレースすることになる。メッセージ順序が噛み付いてくることがある。学習曲線がある。
でも多くの苦痛もなくなる。サービス間の密結合がなくなる。カスケード障害が分離された障害になる。ブロッキング呼び出しがなくなる。単一障害点が単一障害点でなくなる。遅いConsumerが他をブロックしない。
NetflixはMicrosoftのAPIから始めた。直接の同期呼び出し。痛みを感じて問題を理解してから初めて独自のイベント駆動広告プラットフォームを構築した。
それが正しい道だ。まず同期呼び出し。非同期が必要になったらバックグラウンドジョブ(Redisキュー、シンプルに保て)。リプレイ、監査証跡、本格的なスケールが必要になったらイベントストリーミング。
最高のアーキテクチャは現在のスケールに合ったものだ。3年後に必要かもしれないものではない。
まず同期。痛みを感じたらイベント駆動。
— blanho
ほとんどの開発者は決済をCRUDのように扱う。そして金が消える。
直接のデータ転送が扱いにくくなったら、間接参照のレイヤーを追加しろ。Netflixはこれを痛い目で学んだ。
1,000万件の保存検索がある。新しいアイテムが来る。1,000万件のクエリを実行せずにすべてのマッチをどう見つける?
# Producer(シンプル、速い)
def on_ad_play(ad_id, user_id):
event = {
"type": "AD_IMPRESSION",
"ad_id": ad_id,
"user_id": user_id,
"timestamp": now(),
"device": get_device_info()
}
kafka.produce("ad-events", event)
return {"status": "ok"} # 即座にリターン、5ms
# Consumer(請求チームがオーナー)
def billing_consumer():
for event in kafka.consume("ad-events"):
if event["type"] == "AD_IMPRESSION":
record_impression(event["ad_id"])
# 50ms、500msかかっても構わない
# ユーザーはすでにレスポンスを得ている