Bringing the Unix Philosophy to the Twenty-First Century

要約

KafkaがUnix哲学をどのように取り込んだのかを紹介しましょう。あと、LinkedIn?での事例も。

詳細

Unixとデータベースが、たどってきた道のりは違うにせよ、それぞれソフトウェア開発のよい設計指針に沿って成長してきたことがわかった。 これまでたどってきた道のりから、今後の展望を見ていこう。

msos_0417.png

図4-17:Can we improve contemporary data systems by borrowing the best ideas from Unix but avoiding its mistakes?

ここでは、Unix哲学をデータベースの世界にあてはめるとどうなるかを考えよう。

最初にいっておきたいのは、Unixといえども完璧ではないということ。

msos_0418.png

図4-18:Pros and cons of Unix pipes.

シンプルで統一されたバイトストリームのインターフェイスという考えかたはすばらしいと思うが、Unixにも限界はある。

  • 単一マシンで使うことを想定したものであること。肥大化するデータやトラフィックをさばくには、分散システムへの移行が不可欠だ。TCPコネクションをファイルのように扱うこともできるけど、うまいやりかただとは思えない。TCPそのものは悪くないけど、分散パイプの実装として使うには低レベルすぎる。*1*2
  • Unixのパイプは送り側も受け側も単一プロセスであることを想定している。複数のプロセスに結果を送ったり、複数のプロセスからの入力をとりまとめたりすることはできない(teeコマンドを使えば分岐させられるけど、パイプ自体はあくまでも一対一だ
  • ASCII(あるいはUTF-8)テキストのデータは読みやすいけど、あっというまいぐちゃぐちゃになる。なんといっても、入力のパースがたいへん(バイト列をレコード単位に切り分けて、さらにそれをフィールドに分けて、……)。xargsみたいなシンプルなツールにさえ半ダースものコマンドラインオプションがあって、入力のパース方法を切り替えられるようになっている。テキストベースのインターフェイスも悪くはないが、今思えばもっとリッチなデータモデルときちんとしたスキーマを使ったほうがよかっただろう。*3*4
  • Unixのプロセスは、比較的短い実行時間を想定したものだ。パイプラインの途中にあるひとつのプロセスが落ちたときには、パイプライン全体を再起動しないと復旧できない。たかだか数秒で終わる処理ならそれで問題ないけど、何年も動かし続けるアプリケーションでそれはまずいだろう。

でも私たちは、こういった弱点を克服したうえでUnix哲学のいいとこ取りをできる手段を知っている。そう、Kafkaとストリームプロセッシングだね。

msos_0419.png

図4-19:The data flow between stream processing jobs, using Kafka for message transport, resembles a pipeline of Unix tools.

Unix的に見ると、Kafkaはプロセスとプロセスをつなぐパイプのようなもの。そしてSamzaみたいなフレームワークは、標準入出力を読み書きするための標準ライブラリみたいなものだ。*5

Kafka StreamとSamzaは、他のストリームプロセッシングフレームワークよりもUnix的な視点に近い。 StormやSpark StreamingやFlinkなら、ストリームオペレータ(ボルト)のトポロジー(プロセッシンググラフ)を作って、それをメッセージ転送用の自前のフレームワークにつなぐ必要がある。 Kafka StreamsとSamzaには、独自のメッセージ転送プロトコルなどない、Unixツールが常に標準入出力を使うのと同様、オペレータ間の通信はすべてKafka経由で行う。

Kafka Streamsには低レベルのAPIもあるし、DSLを使って処理を定義することもできる。 Kafka StreamsとSamzaの低レベルプログラミングモデルはとても柔軟。各オペレータを個別にデプロイできるし、新しいアプリケーションが増えたときにグラフを拡張させることもできる。 また、グラフ内のすきなところに新しいコンシューマを追加できる。

しかし、Unixのパイプにも問題があった。お手軽にさっとつないでみるのにはいいけど、長期間稼働し続ける大規模なアプリケーションに使うモデルとしてはよくない。 Unix哲学を参考にしてシステムを作るのであれば、この問題を何とかしないといけない。

Kafkaでは、Unixパイプの弱点をこんなふうに克服した。

msos_0420.png

図4-20:How Kafka addresses the problems with Unix pipes.

  • 単一マシンの制約ははじめからない。Kafkaは元々分散環境に対応したものだ。
  • Unixパイプは一対一のものだた、Kafkaは複数のプロデューサーやコンシューマーを指定できる。
  • Kafkaは耐障害性に優れている。複数のノードにデータをレプリケートできるので、ひとつのノードがダウンしても他のノードがカバーしてくれる。
  • Kafkaでは、バイトストリームではなくメッセージストリームを扱う。これで、入力をパースする一手間を減らせる。メッセージ自体は単なるバイト配列にすぎないので、お好みのフォーマットを使えばいい。JSON, Avro, Thrift, Protocol Buffers, なんでもOK。*6*7*8

KafkaがUnixのパイプと異なるところを簡単にまとめる。

msos_0421.png

図4-21:Side-by-side comparison of Apache Kafka and Unix pipes.

  • Unixパイプはバイトストリームを扱うが、Kafkaはメッセージストリームを扱う。この違いが決め手になるのは、複数のプロセスがひとつのストリームに同時に書き込む場合だ。複数のメッセージが入り交じってしまう心配はない。
  • Unixパイプは単なるインメモリのバッファだが、Kafkaはすべてのメッセージをディスクに永続化する。この点では、どちらかといえばパイプよりはテンポラリファイルへの書き込みに近いと言える。
  • Unixでは、パイプの受け手側の処理が遅い場合に送り手側のプロセスがブロックされる。Kafkaでは送信側と受信側がもっと切り離されていて、受信側の処理速度が送信側や他の受信者に影響することはない。
  • Kafkaのデータストリームはトピックと呼ばれ、トピック名で参照できる(Unixの名前付きパイプみたいなもの)。Unixパイプラインのプログラムはすべて同時にスタートするので、通常はパイプに明示的な名前は必要ない。でも長期稼働するアプリケーションでは、途中で何かを足したり引いたり置き換えたりすることもあるので、名前が必要になってくる。*9

こういった違いはあるものの、Kafkaを分散データ向けUnixパイプと考えるのには意味があると思う。たとえば、Kafkaのメッセージが流れる順序は固定で、これはUnixパイプのバイトストリームと同じだ。 イベントログのデータを扱うにあたって、これは重要な特性である。これは、AMQPやJMSにはないものだ。

msos_0422.png

図4-22:Unix tools, stream processors and functional programming share a common trait: inputs are immutable, processing has no global side-effects, and the output is explicit.

Unixツール群とストリームプロセッサーはとても似ていることがわかった。どちらも、なんらかの入力ストリームを読んでそれを処理して、その結果の出力ストリームを作っている。

重要なのは、何らかの処理をしても入力そのものには変化がない(イミュータブルである)こと。 sedやawkで何かのファイルを処理しても、明示的に上書きしない限り入力ファイル自体はそのままである。 また、Unixツールの大半は宣言的である。つまり、入力が同じなら、常に同じ結果が得られる。

このふたつの性質を見ると、何となく関数プログラミングに似ているようにも見える。 必ずしもHaskellなどの関数型言語を使わないといけないというわけではないけれど、 関数型のコードからは多くのメリットが得られる。

msos_0423.png

図4-23:Loosely coupled stream processors are good for organizational scalability: Kafka topics can transport data from one team to another, and each team can maintain its own stream processing jobs.

Unixライクな設計指針で作られたKafkaは、大規模システムの構築にも対応する。 大規模な組織では、さまざまなチームがそれぞれのデータをKafkaにパブリッシュする。 各チームが独自にストリーム処理ジョブを作ることもできる。 ストリームは複数のコンシューマーに対応しているので、新しいコンシューマーを追加するときにも他チームとの調整などは不要。

私たちはこのアイデアを「ストリームデータプラットフォーム」と呼んでいる。 このアーキテクチャでは、Kafkaのデータストリームがシステム間の通信チャンネルとして機能する。 各チームは、自分たちが受け持つパートがうまく機能することに注力する。 Unixツールを組み合わせて行うのがデータを処理するタスクであるのに対して、 分散ストリーミングシステムを組み合わせて行うのは大規模組織全体の運営だ。*10

Unixライクな手法では、疎結合にすることで大規模システムに立ち向かう。 ストリームのインターフェイスが統一されているおかげで、各コンポーネントを独自に開発・デプロイできる。 Kafkaのバッファ機能や耐障害性のおかげで、システムのどこかの部分で問題が発生したとしても、それが他の部分に派生することはない。 スキーマ管理のおかげで、データ構造の変更を安全に行える。他のチームに迷惑をかけることなく、どんどん開発を進めていけるというわけだ。*11

最後に、LinkedIn?での事例を紹介する。

msos_0424.png

図4-24:What happens when someone views a job posting on LinkedIn??

LinkedIn?では、企業が求人情報を投稿できて、求職者はそれを見て応募することができる。 LinkedIn?のユーザーが求人の投稿を見たときに、何が起こるのだろう?

求人画面の処理をするサービスが、「member 123 viewed job 456 at time 789」みたいなイベントをKafkaに発行する。これで、この情報がKafkaに格納されたのでいろいろ活用できるようになる。*12

システム監視
企業はLinkedIn?にお金を払って求人を出すので、サイトがきちんと動いていることが重要。アクセス数が急減した場合はアラームが発生する。調査すべき問題だからだ。
関連性やリコメンド
同じ情報も何度も見せられるとユーザーは不快になる。ユーザーがその投稿を何回見たのかを記録して、それを順位付けの指標にすればいい。誰が見たのかを記録しておけば、リコメンドにも使える(Xを見た人はYも見ています)。
不正利用の防止
すべての求人をスクレイピングしてスパムを送ったりなど、利用規約に違反する行為は防ぎたい。そのための第一歩が、誰が何をしたのかを記録しておくことだ。
分析
求人を出した企業は、どんな人たちがそれを見ているのかの情報を(Google Analyticsみたいな感じで)知りたいものだ*13。そうすれば、どんな内容が求職者の気を引くのかなどがわかるようになる。
Hadoopやデータウェアハウスへのインポート
LinkedIn?の内部的なビジネスアナリティクス、マネジメント層向けのダッシュボード、A/Bテストの検証などに使う。

これらのシステムはそれぞれ複雑なもので、それぞれ別のチームがメンテナンスしている。 Kafkaは、耐障害性に優れたスケーラブルなパイプの実装を提供する。 Kafkaを基盤とするストリームデータプラットフォームのおかげで、これらのシステムを個別に開発できるようになり、 より堅牢な方法でそれらを組み合わせられるようになる。

担当者のつぶやき

みんなの突っ込み



*1 Mark Cavage: “There’s Just No Getting around It: You’re Building a Distributed System,” ACM Queue, volume 11, number 4, April 2013. doi:10.1145/2466486.2482856
*2 Bert Hubert: “The ultimate SO_LINGER page, or: why is my tcp not reliable,” blog.netherlabs.nl, 18 January 2009.
*3 Ronald Duncan: “Text File formats – ASCII Delimited Text – Not CSV or TAB delimited text,” ronaldduncan.wordpress.com, 31 October 2009.
*4 Gwen Shapira: “The problem of managing schemas,” radar.oreilly.com, 4 November 2014.
*5 Jay Kreps: “Why local state is a fundamental primitive in stream processing,” radar.oreilly.com, 31 July 2014.
*6 Martin Kleppmann: “Schema evolution in Avro, Protocol Buffers and Thrift,” martin.kleppmann.com, 5 December 2012.
*7 Jay Kreps: “Putting Apache Kafka to use: A practical guide to building a stream data platform (Part 2),” confluent.io, 24 February 2015.
*8 Schema Registry,” Confluent Platform Documentation, docs.confluent.io.
*9 Vince Buffalo: “Using Named Pipes and Process Substitution,” vincebuffalo.org, 8 August 2013.
*10 Jay Kreps: “Putting Apache Kafka to use: A practical guide to building a stream data platform (Part 1),” confluent.io, 24 February 2015.
*11 Jay Kreps: “Putting Apache Kafka to use: A practical guide to building a stream data platform (Part 2),” confluent.io, 24 February 2015.
*12 Ken Goodhope, Joel Koshy, Jay Kreps, et al.: “Building LinkedIn’s Real-time Activity Data Pipeline,” Bulletin of the IEEE Computer Society Technical Committee on Data Engineering, volume 35, number 2, pages 33–45, June 2012.
*13 Praveen Neppalli Naga: “Real-time Analytics at Massive Scale with Pinot,” engineering.linkedin.com, 29 September 2014.