Apache Kafkaを使ったアプリ設計で反省している件を正直ベースで話す

Distributed computing Advent Calendar 2017の12/15分の投稿です。

Apache Kafka: Producer, Broker and Consumer

2017年は生まれて始めてApache Kafkaを本格的に業務利用(PoCではなく本番運用)した年でした。Apache Kafka的なメッセージングミドルウェアそのもののは、社内的な事情でよく使っていたのでその使い勝手に対して困惑はほとんど無かったですし、ミドルウェアとして非常に安定しているため、Kafkaクラスタそのものでの不具合らしい不具合が発生したことは一度もありませんでした。

しかし、Kafkaのトピック設計などに関してのベストプラクティスは事例ベースでもあまり見かけたことがなく、チームメンバーと悩むことも多かったです。このストーリーでは、主にKafkaを利用したアプリ設計で考えたことや失敗したことを振り返りつつ共有します。なお、パーティション数や各種バッファサイズなどのチューニング要素は今回取り扱わないのでご注意下さい。

また、システム全体のアーキテクチャは下記ストーリーにある通り、オンプレの工場からIoTデータがクラウドにアップロードされるような構成をとります。

https://medium.com/@laqiiz/e625ff41f5c8

Entire System Diagram

反省ポイント1:システム受付部分

前提:API経由でのKafka登録をする

システム要件次第ですが、システム的な境界面ではKafkaをインターフェースとして直接公開せずに、何かしらのRESTやgRPCなどのWebAPIでラップするパターンが少なく無いのではないでしょう?というか上図の通りで我々のチームがそうでした。

API側でメッセージの加工をしてKafkaに投入すると…?

API経由でKafkaを構成した時に悩んだポイントは、Producer, Consumerのどちらで入力メッセージの加工(NormalizeやEnrichmentなど)処理を行なうかです。デバイスごとに送信するメッセージのフォーマットや項目の意味が微妙に異なるため、どこかでそれらを吸収する必要がありました。

言い換えると、貪欲(Producer側≒API)に処理するか、遅延(Consumer側)で処理するかのどちらかです。検討の結果、Kafkaにデータを登録する際に加工したほうが、後続のConsumerが増えたときにも対応しやすいだろうということが決め手となり、Producer側がその責務で持つことで決定しました。

当初採用した処理方式

しかし、あとあと考えるとAPI側で加工処理を行なうためにはマスタデータの参照が必要だと気が付きました。内容はデバイスIDからエリアコードを取得するような静的な内容が多いですが、幾つかの項目は要求の都度、オンデマンドに参照することが必要です。

そのため、例えばデータストアにメンテナンスが発生した場合にはAPI機能そのものが動かなくなったり、そうしないための運用オペレーションを練る必要があるなど、かなり辛い事実が判明しました。

正直、とても後悔と反省している点です。

Ingest Topicの勧め

その回避策の一案として、メッセージを未加工でそのまま受け入れるトピック(Ingest Topic)を作成することを考えています。Ingest Topicにメッセージが登録された後に、後続のConsumerアプリにでマスタ参照および加工処理を行います。必要に応じてKafkaにライトバックしても良いと思います。その場合はKafka Streamが上手くハマるのではないでしょうか。

次期処理モデル

これによって境界であるAPIの処理は、例えデータストアが大障害を起こしても、影響を受けずにKafkaにデータ登録できようになるはずです(すなわちクラウド側にオンプレからデータを安全に収集できます)。当然ながら、Event TopicのConsumerは標準フォーマットになったメッセージを利用することができます。

Ingest Topic設計

Ingest Topicを一つにするか、分割するかは必要なスループットや対象データ量など非機能要件で決まることが多いと思います。このシステムではAPIのエンドポイントごとにIngest Topicを一つ作成する予定です。構築しているシステムはいわゆる、”プラットフォーム”だったため、APIエンドポイント数は自分たちで制御可能と考えたことも理由に挙げられます。もし、そうではなく外部要因に追従してAPIを増やすしかない場合などは、Inguest Topicを一つにするといった判断も出てくるのでは無いかと思います。

Event Topic設計

Kafla StreamなどでETL処理を行った後、common formatメッセージはEvent Topic(という名のトピック)に登録します。と言いながら、Event Topicは実際にはメッセージ種別ごとに複数作成します。Event Topicは複数作成しますが、APIエンドポイント単位でIngest Topicを分ける設計ですので、ETLのサービスの中ではトピックの動的なルーティングを行なう必要はありません。

反省ポイント2:システム内部処理

Kafkaクラスタをサービス全体から見た時に単なるキューイング機構として捉えていたため、Consumerが何かしら処理を行った後にそのイベントをKafkaにライトバックすることは基本的に行っていませんでした。

これが一番影響したのは、Spark Streamingのストリーミング処理にてデータストアに永続化し、その永続化データを参照して外部システムにリアルタイムに転送したいという要件が出てきた時です。

これを達成するには、どのPrimary-Key(HBaseではRow Key)で永続化されたか知る必要がありますが、それらの情報はKafkaに書き戻していませんでした。また、そのような設計でアプリを組んでいなかったので改修も困難。そのため結局、ストリーミングアプリの隣に類似ロジックを含む、別Consumerアプリを構築するハメになりました。また、一部はサイクル起動でのバッチプログラムを作成し対応しました。

イベントドリブンで動いていた処理フローが途切れると、タイムスケジュールなどの別トリガーで起動するプログラムが増えるのでシステムが複雑になってしまいます。本当に最初の設計が重要ですね。

具体的には、どのイベントで着火し・そして次に何を着火させるべきかという視点でConsumerアプリを設計する規約を作ればよかったと考えています。AWSサービスへのアクションはLambdaでイベントを拾うこともできるので、Lambda →Kafkaへのイベント伝播も含めて、イベントのワークフローを途切れさせない設計をしていれば、Kafkaの実力をもっと活かせたはずなので、とても残念にとらえています。

まとめ

  • Kafkaのトピック設計は色々な観点から考慮する
  • Consumer処理の完了通知をKafkaにライトバックさせるかどうかは大事なポイント。Lambdaなどでイベントを救済できるかも考えておこう
  • このあたり、Enterprise Integration Patternsをちゃんと読めば何かしらの解はでそう