asoview! TECH BLOG

アソビュー株式会社のテックブログ

Amazon Kinesis Data Streams + Protocol Buffersで実現するイベント駆動アーキテクチャー

アソビュー! Advent Calendar 2020 の18日目です。

初めまして、アソビュー!SREのkirimaruです。
最近Amazon Kinesis Data Streams用の社内向けライブラリ「Pelican」を開発したのでその話を書こう!と思ったのですが、Amazon Kinesis Data Streamsの活用事例と合わせて、そもそもこのライブラリを開発する経緯をまず書こうと思います。

余談ですが僕のアイコンはこのライブラリ用のアイコンとして作ったのものです。

 

tech.asoview.co.jp

アソビュー! Advent Calendar 2020 の2日目に上記の記事が投稿されています。
その中で今回の主題のひとつであるProtocol Buffersについて下記のように触れています。

🆕 Protocol Buffers / gRPC
以前の記事の時点では、RESTfulなAPIを利用するケースが多かったですが、最近では、同期、非同期連携が増えており、サービス間の型定義を行い、安定して連携できるプロトコルとして、Protocol BuffersやgRPCを利用した開発が増えています。

同記事に同期処理であるAPI連携についての記載がありますが、タイトルから分かる通り、非同期連携についてはAmazon Kinesis Data Streamsを利用したイベント駆動アーキテクチャーで実現しています。

イベント駆動アーキテクチャーは基本的にPub/Subメッセージングモデルをべースに構築されているため、まずこれについて簡単に説明させてください。Pub/Subメッセージングモデルとは、メッセージを仲介するBrokerを介して、送受信者がデータのやり取りを行うモデルです。

よく比較されるメッセージキューモデルとの大きな違いの1つは、送信側と受信側の関係性です。メッセージキューモデルでは送信側(Producer)と受信側(Consumer)が1対1であるのに対し、Pub/Subメッセージングモデルでは送信側(Publisher)と受信側(Subscriber)がN対Nになります。

また、受信後のデータの扱いにも大きな差があります。
メッセージキューモデルでは送信側(Producer)と受信側(Consumer)が1対1であるため、受信側がデータを受信した後に、キューに残っているデータは消されます。
しかし、Pub/Subメッセージングモデルでは送信側(Publisher)と受信側(Subscriber)がN対Nになっているため、受信側がデータを受信した後にもデータは消されず保持し続けます。そのため受信側はどこまでデータを読んだのかを記録しておく必要があります。

さてここからは下記の順番で各構成要素にも簡単に触れながら、実際にご紹介していければと思います。

  • Amazon Kinesis Data Streamsとは
  • イベント駆動アーキテクチャーとProtocol Buffersについて
  • どう活用しているのか
  • 運用していく中で出てきた課題

Amazon Kinesis Data Streamsとは

https://docs.aws.amazon.com/ja_jp/streams/latest/dev/key-concepts.html

 

aws.amazon.com

Amazon Kinesis Data Streams (KDS) は、大規模にスケーラブルで持続的なリアルタイムのデータストリーミングサービスです。

公式では上記のように説明されています。前述したPub/SubメッセージングモデルのBrokerにあたるサービスで、アプリケーションやセンサー等から送られてくるデータを別のサービスまで届けるためのサービスです。

類似のサービスとしてAWSでは他にもキューサービスであるSQSや、Pub/SubメッセージングモデルのSNS、 Apache ActiveMQ および RabbitMQ のマネージドサービスであるAmazon MQといったサービスがありますが、より大規模なストリーミングデータを扱うため、スケーリングによる高いスループットやデータの安全性の担保などが特徴のサービスになっています。

公式ページの導入事例にもありますが、リアルタイム性や高いスループットを活かし、ログの分析などでも使われています。実際にアソビュー!でもCloudwatch Logsと連携してログの分析も行っています。

ログ分析アーキテクチャ(これでブログ一本書けるのでは…?)

「Apache Kafka」と比較されることの多いサービスですが、下記が比較としてはわかりやすかったです。

最近では「Amazon Managed Streaming for Apache Kafka (Amazon MSK)」というKafkaのマネージドサービスもでてきています。

導入にあたってはもちろんKafkaとの比較検討も行いましたが、上記のMSKが検討当時にGAでなかったこともあり、下記がAmazon Kinesis Data Streamsを選ぶ決め手になりました。

  • 社内のインフラがほぼAWSで構築されていたこと
  • 可用性の高さとデータ保証
    (実際2年以上の利用で致命的な障害がなかった)
  • フルマネージドサービスなため運用コストが低いこと

イベント駆動アーキテクチャーとProtocol Buffersについて

改めて、イベント駆動アーキテクチャーとは、システムの状態が変更されるといった特定の出来事(=イベント)に対して反応し処理を行うアーキテクチャーです。
この特定の出来事(=イベント)をPublisherが発行し、Subscriberがそれを監視して、そのイベントに対しての処理を行います。

従来のリクエスト・リプライ方式によるAPIの同期連携に比べてアプリケーション間の連携が疎結合になり、アプリケーションの拡張性や可用性が向上する一方、複雑性が上がったり、トランザクション管理やリアルタイム性の担保が難しくなります。

この時イベントは「イミュータブル」かつ「過去形」で表され、「命令」ではなく「観察可能な事象」となります。

イベント駆動アーキテクチャー具体例簡易図

AWSでは先程軽く触れたようにイベント駆動アーキテクチャーを実現するためのサービスを様々な形で用意していますが、下記のように分類されるようです。

ベストプラクティスではEventBridgeもしくはSNS(+ SQS)での実現を推奨しており、MSKやMQはオンデマンドからの移行に際して使う想定がされているようです。ではなぜアソビュー!がベストプラクティスに従っていないかというと、上記の図の分類の差が関わってきます。

検討当時EventBridgeはありませんでしたが、EventBridgeとSNSはどちらも「イベントルーター」です。
イベント駆動アーキテクチャーにとって大事な要素のひとつとしてPublisher/Subscribe間の送受信データの事前契約があります。

It is vitally important when using this pattern to settle on a standard data format (e.g., XML, JSON, Java Object, etc.) and establish a contract versioning policy right from the start.
イベント駆動アーキテクチャーでは契約するデータ形式(XML、JSON、Javaオブジェクトなど)を事前に決定し、バージョン管理ポリシーを確立することは非常に重要です。

https://www.oreilly.com/programming/free/files/software-architecture-patterns.pdf

至極当然ですが「送信側がどういうデータをどういう時に送信するのか」を受信側と握っていなければ受信側は欲しいイベントに対して処理を行えません。
AWSのイベントルーターはそれぞれルールやトピックでAWS上に事前にこの定義を行いますが、イベントストアはその名の通りイベントを格納するだけで、イベント駆動アーキテクチャーを構築する場合、この定義はAWSの外側で管理します。

察しの良い方はお気づきかもしれませんが、そうここで生きてくるのがProtocol Buffersです。

 

qiita.com

放り投げる形になってしまって申し訳ないのですが、「Protocol Buffersとは」ということについては上記の記事が本当によくまとまっているので、そちらにお任せします。

アソビュー!ではProtocol Buffersを利用し、messageとしてイベントを定義しています。こうすることで下記のメリットを得られます。

  • Publisher/Subscribe間の送受信データの事前契約を行える
  • 複雑になりがちなイベント駆動アーキテクチャーのイベント管理をスキーマ定義で管理できる
  • AWSの外側でイベントの情報を管理できる

特に、AWSの外側に開発者であれば読むことも触ることもできるProtocol Buffersでイベントの情報を管理することで、AWSに馴染みのない開発者でも素早くイベント駆動アーキテクチャーを使った実装ができることは大きなメリットだと考えています。

また、このProtocol BuffersはCICDを通してライブラリとして自動配信しています。これにより、ライブラリを読み込むだけでサービス間連携を安全に素早く実現できるだけでなく、gRPC通信のserviceとして定義することで、リクエスト・リプライ方式とイベント駆動アーキテクチャーの両方のサービス間インターフェース定義を一元管理することができます。

Protocol Buffersを通してサービス間連携を一元管理する

どう活用しているのか

では実際どんな場面で使っているのか、いくつか具体例をご紹介します。

現在は主にLambdaとSpring Boot Application間でイベント駆動アーキテクチャーを利用しています。前述のログ分析機構でも利用していましたが、Amazon Kinesis Data StreamsはLambdaとの相性もいいです。細かく分解されたFunctionを使ってイベント駆動アーキテクチャーを使っていくのはなかなか胸アツな構成だと(個人的に)思ってます。

⏲ cuckoo-clock (Lambda/Publisher)

例えばこれ、鳩時計です。本当にこの名前です。
時間をイベントとして発行するLmabdaになっており、実行はCloudwatch Event(EventBridge)をトリガーにしています。

二度手間では?と思われるかもしれませんが、個人的には結構面白いやつだと思っています。
「〇〇時になった」というイベントをSubscriber側は受け取るわけですが、このタイミングで送りたいメールを送ったり、バッチ処理のトリガーにしたりと用途は結構ありますし、複数のアプリケーションをひとつのイベントで動かせます。なにより、アプリケーションや機能の実行トリガーをAmazon Kinesis Data Streamsに統一できることがポイント高いです。

🔔 notification-service (Lambda/Subscriber)

Subscriber側だとこれが様々な場面で利用できる優れものです。
名前の通り、通知(メールやSlackなど)をするためのLmabdaなのですが、これは本当にイベント駆動アーキテクチャーと相性がいいです。

ECサイトでは特に通知を送る場面が多いと思います。「会員登録の完了」「商品の購入」「メルマガの配信やPush通知」等々…もちろんアソビュー!も例に漏れず多いです。通知を送る機能を切り出して共通化し、イベント駆動アーキテクチャーで非同期に呼び出す。こうすることで、管理コストを低下させつつ拡張性上げることができたり、処理全体の高速化や可用性の向上が望めたりとメリットが多いです。

また、これらの通知を送りたいタイミングは同時に別の処理を行いたいタイミングでもあると思います。リアルタイム性が強く要求される機能には使えませんが、通知を送りつつ同時に別のアプリケーションが違う処理を行う、といったこともひとつのイベントを発行することで実現できます。

自分もこれを書いている途中で見つけた(早く教えて欲しかった…)上記の資料も、ここまで呼んで興味を抱いていただけた方にはとても参考になる内容だと思うので紹介させてください。これも冒頭で紹介した記事を書いた disc99🐼 が書いています。

運用していく中で出てきた課題

概ね良好に機能していましたし、非常に便利な構成ですが、残念ながら使っていく中で何の問題もなかった、とはなりませんでした。

その使い勝手の良さ故に「使われすぎて」しまったのです。

GetRecordsは、単一のシャードから呼び出しごとに最大 10 MB のデータを取得でき、呼び出しごとに最大 10,000 レコードを取得できます。
https://docs.aws.amazon.com/ja_jp/streams/latest/dev/service-sizes-and-limits.html

Amazon Kinesis Data Streamsのレコード取得には上記の制限があります。これはシャード毎に適用されるため、シャードを増やすことで制限が緩和されスループットも上がっていきます。しかし、単一シャードで運用していたことで、読み込み過多による遅延が発生しました。

じゃあシャード増やせばええやん。という話で、もちろんそれで解決する問題なのですが、これができなかったのです……。

Spring Bootのバージョン問題

Amazon Kinesis Data Streamsを利用する際にハードルとなることのひとつとして、利用できるライブラリの選択肢の狭さがあると思います。アソビュー!では多くのアプリケーションがJavaとSpringを使って開発を行っているためSpring Cloud Streamを利用していましたが、複数シャードへの対応や一貫性の担保など、いくつかの欲しい機能が新しいバージョンでしか使えないといった問題がありました。

Spring Bootのバージョンを上げられればいいのですが、その優先度が中々上げられないだけでなく、イベント駆動アーキテクチャーを使うアプリケーションも増えていき、対象全てに行うことがどんどん難しくなっていきました。

Pelican - Kinesis Client/Producer Wrapper Library for asoview!

スループットを上げる仕組み(複数シャード、拡張ファンアウトなど)を利用したいが、Spring Bootのバージョンを上げられない(上げるコストが高くて着手できない)ことで、Spring Cloud Streamでその機能が使えない……そんな状況を改善するために、Kinesis Client Library(KCL)を使う選択をしました。

ただ、高度に抽象化されたSpring Cloud StreamからKCLへの移行はこれはこれでコストが高いです。
そこで下記の基本方針を元に、ラッパーライブラリを開発することになりました。これが冒頭お話しさせていただいた「Pelican」です。

基本方針: 非同期処理の詳細を抽象化したライブラリにより、以下を達成する
1. 並列性(水平スケール): マルチシャード、拡張ファンアウトなどへの対応
2. 一貫性: partition keyによる一貫性のサポート
(スケールアウトのリミットが発生する要因になるためいざというときのオプション)
3. 可用性: Kinesis Streamは1つだけ(シャードは複数)による、
SLOとシンプルな構成の実現
4. 技術進歩: Spring FWに依存しない処理によるアップデートコストの低減、
将来的な技術進歩によるミドルウェアの変更容易性の確保

Pelicanを利用することでSpringのバージョンに囚われずに、Amazon Kinesis Data Streamsが提供する機能や高可用性を享受できるだけでなく、イベント駆動アーキテクチャーを利用した開発を高速に行える状況が作れました。さらに今後は監視の充実や運用の強化もPelicanを通して行っていこうと考えています。

まだまだ発展途上なPelicanですが、よりイベント駆動アーキテクチャーを活用できるようになったと思います。

Springに依存はしないけどSpringのアプリケーションで使うことが前提のライブラリってどういうことだ?と当時は苦しんでいたりもしたのですが、自分ひとりで作ったためPelicanに対しては結構愛着があります。
次回はこのPelicanについて、もう少し深堀りしていこうと思います。

最後に

いかがだったでしょうか。

イベント駆動アーキテクチャーは刺さるところには非常によく刺さるアーキテクチャーだと思います。
ただイベント駆動アーキテクチャーはあくまでアーキテクチャーパターンであり、その構成要素を組み立てるサービスやモジュール、言語などは様々な選択肢があります。Amazon Kinesis Data Streamsの高い可用性とスループットという恩恵を受けつつ、前述したメリットを実現できるのは今回ご紹介した構成ならではの強みだと思いますが、これも選択肢のひとつです。

適切なアーキテクチャーとそれを利用する箇所は今後も考えていきたいですし、この構成がひとつの検討材料になれば嬉しいです。

 

 

アソビュー!SREでは一緒に働くメンバーを積極的に募集しています!
EKSの導入やAmazon Kinesis Data Streamsの活用など、日々より良い技術を取り入れていき、より良いサービスを提供していく、非常にやりがいのある仕事です!
興味がありましたらお気軽にご応募いただければと思います。

 

www.wantedly.com