Kafka Streamsのご紹介 こんにちは。LINEでサーバ開発エンジニアとして働いているYuto Kawamuraです。主にHBase、KafkaといったLINEの中核的なストレージを開発・運営しています。 昨年下期からは、IMF(Internal Message FlowまたはFund)と呼ばれる新規プロジェクトも担当しています。このIMFプロジェクトの目的は大きく2つあります。 内部システム間のevent deliveryを統一された方法で行うデータパイプラインの開発 LINEサーバシステムにおけるバックグラウンド処理を担当するコンポーネントの一つであるtalk-dispatcherの置き換え この2つの目的は互いに関連性がないようにみえますが、これらの目的を達成するために、Apache Kafkaとストリームプロセッシング技術を適用することを考えています。Apache Kafkaは、LinkedInによって開発され、使われてきた大容量の分散メッセージングシステムです。ユニークな機能を多数提供していますが、一番重要な特徴は次のとおりです。 ディスクベースの永続化を提供すると同時に、ページキャッシュを活用してインメモリに引けをとらない高いスループットを実現しています。 複数のconsumerが一つのtopic(queueのような概念)から複数回メッセージを取得できます。このようなやり方が可能なのは、クライアントがそのqueueからどこまでデータを取得したか、その位置を知らせてくれる「offset」を管理しているためです。 Kafkaエンジニアリングの基本やIMFプロジェクトのコンセプトアイデアなど、面白いテーマはたくさんありますが、今回はストリームプロセッシングの実装方法にフォーカスしてご紹介します。 ストリームプロセッシングフレームワーク ストリームプロセッシングには、Apache Storm、Apache Spark、Apache Flink、Apache Samzaなど広く使われているフレームワークが複数あります。ここで、最初に採用したのはApache Smazaでした。 SamzaはKafkaと同様、LinkedInで開発されました。Kafkaと連携するように設計されているため、Kafkaとの統合に標準対応しています。基本的にはよく動作しましたが、サービスに直接的に影響を与え得るコアインフラを構築することを考えると、いくつかの懸念点がありました。 SamzaはYARNと連携するように設計されています。YARNはとてもうまく作られている分散リソース割当フレームワークであり、広く使われていますが、今回の使途には適さないと考えれらるいくつかの点がありました。 当初バッチ処理のために設計されたものであり、ストリームプロセッシングは可能ではありますが、Hadoopを継承した一部の部分がストリームプロセッシングに適していないと感じました。 LINEのデプロイシステムであるPMC(Project Management Console. LINEの主要サービスを管理するために使用するツール。CMDB(configuration management database)のビルド機能と配布機能を合わせたサービス)との親和性がありません。 全体のアーキテクチャをシンプルに保つことを意識した上で、今回のケースにおいてはYARNの主要な機能であるリソースのアイソレーションや割り当てといった機能は必要ありませんでした。その理由は次のとおりです。 サーバは、基本的にサービスごとに別途割り当てられます。 物理メモリを消費するのはほとんどがheapですが、JVMはheapサイズの上限を制限するオプションを持っています。 アプリケーションの特性上、CPUは問題になりません。 ネットワーク問題があることは想定されますが、YARNにはネットワークI/Oを制限する機能がありません。 社内のエンジニアリング設備は、「host」の概念に徹底的に従っています。例えば、サービスのモニタリングに使用する独自開発ツールであるIMONは、hostごとのメトリクスを確認したりアラームを送信したりすることができ、Kibanaはhostごとのログを保存します。そのため、jobを実行するhostの決定をYARNに任せるには、さらなる作業が必要でした。 Apache SamzaのDevelopment activityはあまり活発ではないように見えました。 言うまでもなく、YARNはリソースプール上で多くのjobを実行するためには有効です。現在、統計用jobや調査用に実行されるアドホックなjobの実行に用いられています。 Kafka Streams 2016年3月10日、Confluent社(LinkedInでApache Kafkaを開発した人々が設立した会社)が、 「Introducing Kafka Streams: Stream Processing Made Simple」というタイトルのブログ記事を投稿しました。この記事を読んで、Kafka Streamsは我々が求めていたものに限りなく近いと思いました(この記事を読むまでは、独自の実装を開発しようかとも思っていました)。他の一般的なストリームプロセッシングフレームワークが「実行フレームワーク」であるのに対し、Kafka Streamsは「ライブラリ」です。Apache Samzaから継承した概念も一部ありますが、重要な差があります。 ただのライブラリであり、実行フレームワークではないため、ユーザーが手動で実行させる必要があります。特定のフレームワーク上で実行するか、またはpublic static void main()を使うかは、開発者に委ねられています。 Kafkaのプリミティブな機能を活用し、コア機能を最小のコードでシンプルに実装しています。 シンプルなDSLを使用してプロセッシングトポロジーを定義できます。 Kafkaのコミュニティが開発をしているので、非常に活発な開発活動を期待できます。 Rolling restartに対応しています。この特徴は、単一のインスタンスのみをアップデートした上で、プロダクションのトラフィックを受けながら動作確認するためにも便利です。 Kafka Streamsは、Kafkaバージョン0.10.0で公開されました。私たちが初めてKafka Streamsを試してみたのはまだリリース前のときだったので、ソースリポジトリから自前でartifactをビルドする必要がありました。なお、Kafka Streamsは、バージョン0.10.0.0以上のKafka brokerを必要としますが、現在使用しているクラスターのバージョンは0.9.0.1です。従って、互換性のないプロトコルをダウングレードするために、クライアントライブラリを手動でパッチするなどの多少筋が悪い作業が必要でした(もちろん、新しいバージョンがリリースされる次第、クラスターをアップグレードする予定です)。それでも、新たに実装を自前で作るよりは簡単な作業だっだと考えています。 次は、Kafka Streamsが提供する特徴的な機能について説明します。 Masterless Kafka Streamsは、障害検知、処理ノード間のコーディネーション、パーティションの割り当てなどを行うために通常の分散システムに存在するマスターという概念がありません。その代わり、Kafka独自のコーディネーションメカニズムに全面的に依存しています。つまり、worker間の直接的な通信はありません。KafkaStreamsのインスタンスを作成すると、与えられたapplicationIdに対するconsumerの一つとして、Kafka brokerにsubscribeします。リバランスが必要な場合、またはfailoverが発生した場合は、Kafkaのbrokerがそれを検知して処理するため、worker間の通信は不要です。 High-level-DSL APIとLow-level API Kafka Streamsは、ストリームプロセッシングのプログラミングのためにHigh-level-DSLとLow-level APIの2つのAPIに対応しています。 High-level-DSL 多くの場合、ストリームプロセッシングは、ストリームにtransform、filter、join、aggregateなどの処理を適用し、その結果を保存するといった流れになります。このように基本的な演算処理を行うには、High-level-DSLインターフェースが適しています。High-level-DSLを使用すれば、Scala Collections APIとかなり類似した形でcollection、transformなどの演算をプログラミングできます。以下は、IMFプロジェクトでloopback topic replicatorを使用した例です(loop topic replicatorの用途は、特定の用途に合わせてオリジナルのtopicのメッセージをフィルタリングし、新しいtopicに保存することです)。 KStreamBuilder builder = new KStreamBuilder(); KStream<Long, OperationLog> stream = builder.stream(sourceTopic.keySerde(), sourceTopic.valSerde(), sourceTopic.name()); Map<String, Set<OpType>> categories = loadCategories(); for (Map.Entry<String, Set<OpType>> (…)
↧