書評『データ指向アプリケーションデザイン』
May 14, 2023 | 23 min read | 1,405 views
はじめに
『データ指向アプリケーションデザイン』(Martin Kleppmann 著、斉藤 太郎 監訳、玉川 竜司 訳、O’Reilly Japan、2019年)を読んだので、まとめと感想を書きます。
総評
本書は、今日のアプリケーションにおいて避けては通れないデータ処理に関する広範な技術を概観し、それらの特性とトレードオフを理解することを目指した野心的な本です。内容はかなり骨太で、特に分散システム特有の問題や合意のアルゴリズムを扱う8章と9章は、自分の経験が少ないこともあって咀嚼するのに苦労しました。実務と行き来しながら時間をかけて理解する類の本だと思いますが、一度通読してまとめることで、データエンジニアリングで必要な知識の地図を頭の中に構築することができたように思います(読み始めてから本記事を書き終えるまで、1年くらいかかりました)。
監訳者の斉藤氏が内容を紹介したスライドがあるので、興味のある方はこちらもご覧になると良いかもしれません。
内容のまとめ
全体としては次のような構成になっています。
- 第I部(1-4章):データ指向アプリケーションの設計を支える基本的な概念を紹介します
- 第II部(5-9章):1台のマシンに保存されているデータから、スケーラビリティや耐障害性、レイテンシのため複数のマシンに分散配置されているデータに視点を移します
- 第III部(10−12章):単一のデータベースから議論を発展させ、複数のデータベースをを組み合わせたシステムについて述べます
以下、章ごとに私が理解した内容をまとめます。間違いなどがあればコメントをいただけますと幸いです。
1章 信頼性、スケーラビリティ、メンテナンス性に優れたアプリケーション
今日の多くのアプリケーションは、演算指向ではなくデータ指向であると言えます。すなわち、制約条件がCPUではなくデータ(量、複雑さ、変化する速度)となることがほとんどです。こういったアプリケーションでは、データベース、キャッシュ、検索インデックス、ストリーム処理、バッチ処理などの機能が必要になります。これらの機能を抽象化するツールまたはツール群(データシステム)は数多くありますが、アプリケーション開発においてそれらの仕組みや性質について知っておくことは重要です。
まず、本書の全体を通して必要となる基本的な概念である、信頼性、スケーラビリティ、メンテナンス性を導入します。
信頼性とは、何か問題が生じたとしても正しく動作し続けることです。ここでいう問題には、以下のような種類があります。
- ハードウェア障害:ディスクのクラッシュ、停電、ネットワークの切断など。こういった事象は多数のマシンが関わる大規模なシステムでは日常的に発生し、対策として冗長化が行われます。
- ソフトウェアのエラー:単なるバグ、プロセスの暴走、依存先サービスの障害など。これらを完全に解決する方法はなく、テストやモニタリングなどを徹底することで対策することしかできません。
- ヒューマンエラー:設定ミスなど。障害の最大の原因とも言われています。対策としては優れたインターフェイスやサンドボックス環境の用意などがあります。
スケーラビリティとは、負荷の増大に対してシステムが対応できる能力のことです。能力を測る指標にはスループットとレスポンスタイムなどがあり、99.9パーセンタイルなどの統計量が使われます。
スケーラビリティについて議論するためにはまず、負荷が何に依存しているかを知ることが大事です。Twitterを例に考えてみましょう。負荷は、タイムラインの読み込みとツイートの書き込みのどちらに強く依存しているのでしょうか?その度合いは、ユーザのフォロー数やフォロワー数によってどのように変わるのでしょうか?これらの要素がどうなっているかによって、最適なシステム設計が変わってきます。
なお、パフォーマンスを上げる一般的な方法としては、スケールアップ(垂直スケーリング;マシンを強力なものにする)とスケールアウト(水平スケーリング;マシンの数を増やす)があります。
メンテナンス性とは、運用時のコストの低さのことです。これは、システムの単純性や変更容易性によって達成されます。
2章 データモデルとクエリ言語
次に、アプリケーションにおいてデータを表現する存在であるデータモデルについて見ていきます。主なデータモデルとしては、リレーショナルモデル、ドキュメントモデル、グラフモデルがあります。
リレーショナルモデルは、データをリレーション(表のこと)で表現します。それぞれのリレーションはタプル(行のこと)の集合です。リレーショナルデータベース(relational database; RDB)は、1960年代にビジネスデータの処理(トランザクション処理やバッチ処理)を起源として発展しました。その後、汎用性の高さからユースケースが拡大し、今でも現役で使われています。
2010年代になると、さらなるスケーラビリティや柔軟性を志向するNoSQLが登場しました。そのうちのドキュメントモデルという類型はJSONのような形式でデータを表現します。ドキュメントモデルには、オブジェクト指向言語におけるインピーダンスミスマッチ(オブジェクトとデータの間の変換処理)が小さいという利点がある一方、ドキュメント間の関係を表現しづらいという欠点があります。多くのデータベースでリレーショナルとドキュメントのハイブリッドモデルを採用する動きがあります。
データ間で多対多の関係が一般的な場合には、グラフモデルが適しています。例えば、Webグラフを表現するならば、Webページを頂点、ページ間のリンクを辺で表すことができます。Neo4jはグラフデータベースの代表例です。
データを操作するクエリ言語には、SQLのように宣言的なものと、IMSやCODASYLのように命令的なものがあります。宣言的言語では操作方法ではなく操作完了後の理想状態を指定するので、システム側で計算処理の最適化ができます(例:BigQuery)。
3章 ストレージと抽出
データベースを外から見てきたので、次はデータベースの内側を見ていきます。この内部処理は通常抽象化されていますが、様々なストレージエンジンの中から適切なものを選ぶ際に理解しておく必要があります。特に、トランザクション用のエンジンと分析用のエンジンでは性質が大きく異なります。
データベースのナイーブな実装例として、書き込み時には最終行への追記だけを行い(上書きはしない)、読み取り時には先頭から末尾まで順に探索するというものを考えてみましょう。この実装だと書き込みは定数時間で実行できますが、読み取りにはレコード数に比例する時間がかかってしまいます。読み取りを高速化するデータ構造として一般的にインデックスが用いられますが、ここには書き込みの速度を低下させるというトレードオフが存在します。
インデックスには様々なバリエーションがあります。
ハッシュインデックスは名前の通り、ログ(追記だけを行うレコードの並び)をハッシュテーブルで管理します。全てのキーがRAMに収まっている必要がありますが、読み書きともに高速に実行できます。値の実体はディスクからロードするため、RAMに乗せなくても構いません。ログがあるサイズに達したらそのファイルを閉じてコンパクション(重複キーから最新の値のみを残す処理)を行い、以降は新しいファイルに書き込むようにすることで、ディスク領域を使い切ることを回避できます。このとき、小さいセグメント同士をマージすることもできます。あるキーとその値を削除するためには、墓石(tombstone)と呼ばれる特別なレコードを追加します。追記ではなく更新をした方が効率がいいように思えるかもしれませんが、追記のシーケンシャルな書き込みは更新のランダムな書き込みより高速であること、クラッシュ時のリカバリと並行処理が単純に扱えることといった利点があります。ハッシュインデックスの制約は、全てのキーをメモリに収める必要があることと範囲検索ができないことです。ハッシュインデックスを用いたデータベースの代表例にBitcaskがあります。
ソート済み文字列テーブル(sorted string table; SSTable)は、キーを事前にソートしておくことによって範囲検索ができるようにしたものです。こうすると、メモリに全てのキーを乗せる必要がなくなるという利点もあります。セグメントごとに1つのキーをメモリに乗せておけばどのセグメントを読むべきかがわかり、1つのセグメントであれば高速にスキャンできるからです。キーを任意の順序で書き込み、ソートされた順序で読み取るためのデータ構造として、インメモリのBツリー(red-blackツリーやAVLツリー)を用います。このインメモリのBツリーはmemtableとも呼ばれます。SSTableとmemtableを組み合わせたデータ構造がLSMツリー(log-structured merged-tree)です。LSMツリーは、Apache Cassandra、LevelDB、RocksDB、HBase、Bigtableなど様々な分散データベースで利用されています。また、全文検索に用いられるインデックスエンジンであるLuceneの単語辞書としても利用されています。
Bツリー(balanced tree)は、データベースを一度に読み書きできる固定サイズのブロック(ページ)に分割します。現在もっとも一般的なインデックスです。“balanced”というのは、N個のキーを均等な深さO(log N)を持つ木で保存するという意味です。Bツリーの書き込みは、最終行への追記ではなく該当ブロックの上書きで行います。この部分の信頼性を高めるために、write-aheadログ(WAL)という追記のみのログファイルを用意します。
LSMツリーとBツリーとを比較すると、前者は書き込みが速く、後者は読み取りが速いです。また、最近はMemcachedやVoltDBやRedisなどのインメモリデータベースも発展してきています。インメモリデータベースを使うと、書き込みを高速化できます(読み取りはキャッシュによって既に高速化されています)。
データベース上で行う処理には、ユーザが利用するトランザクション処理(online transaction processing; OLTP)と、社内のアナリストが利用する分析処理(online analytic processing; OLAP)があります。前者は低負荷ですが大量に発生します。一方、後者は高負荷ですが発生頻度は低いです。SQLはこのうちの両方に対応できますが、データベースは用途によって分離したほうが都合の良いことが多いです。分析処理に特化したデータベースはデータウェアハウスとして発展しました。データウェアハウスはしばしば、経時的に発生するイベントからなるファクトテーブルと、各列の属性からなるディメンションテーブルによって構成されます。この構成をスタースキーマと呼びます。カラムをいくつかのカテゴリに分割したものをスノーフレークスキーマと呼びます。
また、分析においては列ごとに統計量を計算するため、列指向ストレージのほうが効率が良くなります。
4章 エンコーディングと進化
アプリケーションを変更するとき、ほとんどの場合でデータとコードの変更を伴います。そうすると、一時的に新旧のデータと新旧のコードが共存することになります。後方互換性と前方互換性を保つにはどうすればいいでしょうか。特に、新しいコードによって書かれたデータを古いコードで読む(前方互換性が必要)には、未知の変更に対策しなければなりません。次は、このような観点に注目しながら、データをエンコードする様々なフォーマットを見ていきます。
データをインメモリの表現からバイト列に変換することをエンコーディングと呼びます。ネットワーク経由でデータを送信する場合はエンコーディングが必要です。しかし、多くのプログラミング言語には言語固有のエンコーディングフォーマットが用意されていますが、これを使うと他の言語で読みにくくなってしまいます。他にも、前方互換性やパフォーマンスに懸念があることが多いので、特定の言語の組み込みエンコーディングを使うのは望ましくありません。
JSONやXML、CSVはテキストフォーマットなので人間には読みやすいですが、数値のエンコーディングに曖昧さがあります。また、CSVにはスキーマの概念がありません。
Apache ThriftやProtocol Buffers、Avroは、スキーマを前提とすることで効率よくバイナリエンコーディングできるフォーマットです。スキーマはドキュメンテーションやコード生成に役立ちますが、バイナリのままだと人間には読めないという欠点があります。
これらのエンコーディングを用いてデータをあるプロセスから別のプロセスに渡す(データフロー)には、いくつかの方法があります。
- データベースを経由する
- サービス(RESTやRPC)を経由する(参考:マイクロサービスアーキテクチャ)
- 非同期のメッセージパッシング(例:Apache Kafka。詳しくは11章)
5章 レプリケーション
スケーラビリティ、耐障害性、レイテンシを改善するための最も基本的な方法は、レプリケーションです。レプリケーションとは、ネットワークで接続された複数のマシンに同じデータのコピーを保持しておくことです。レプリケーションによって、
- (ユーザから地理的に近い場所でデータを保持することで)レイテンシを下げる
- スループットを上げる
- 可用性を高める
ことができます。なお、ここではデータは各マシンに全体が収まる程度のサイズだとします(パーティショニングが必要な場合については6章で検討します)。データが変化しないのであればレプリケーションは容易ですが、変化する場合は同期に関する問題が発生します。
データベースのコピーを保持する各ノードをレプリカと呼びます。変更が全てのレプリカに行き渡ることを保証するために、次に紹介する3つのレプリケーションアルゴリズムのうちいずれかが用いられます。
シングルリーダーレプリケーションでは、1つのリーダー(leader)と複数のフォロワーを設定します。データに書き込みが入ったら、まずリーダーがローカルストレージに書き込みを行い、変更データを他のフォロワーに送信します。各フォロワーは変更データをローカルストレージに反映させます。読み取り時にはリーダーまたはフォロワーから読み取ります。新しくフォロワーを追加する場合は、リーダーのスナップショットをコピーした後、最新の変更を反映させます。リーダーに障害が起きた場合は、最も追従しているレプリカをリーダーに昇格させます。障害時には問題がいろいろと起こり得るので、手動で対処する場合もあります。シングルリーダーには、リーダーに書き込みの負荷が集中することでボトルネックになりやすいという問題があります。
マルチリーダーレプリケーションは、リーダーを複数建てることで、シングルリーダーのボトルネックを解消します。一方で、書き込みの衝突という複雑な問題が発生するため、この方法は基本的には推奨されません。マルチリーダーが妥当な構成となる状況は、地理的に分散したデータセンターのそれぞれにリーダを置く場合やオフラインでも動作させたいアプリケーション、リアルタイムな共同編集ツールなどです。Google Docsが良い例です。全ての編集者のデバイスがリーダーとして動作します。自身の変更内容は自身のブラウザにはすぐに反映されますが、共同作業者には非同期で連携されます。衝突の解決には、後勝ち(last writes wins; LWW)や何らかの方法によるマージ(文字列の連結など)、ユーザに判断させるなどの方法があり得ます。
リーダーレスレプリケーションでは、全てのレプリカで書き込みを受け付けます。読み取り時には複数のレプリカに並列でリクエストを送信し、バージョン番号にしたがって結果をマージします。
結果整合性(eventual consistency)が保証されていたとしても、レプリケーションラグ(レプリカの遅延度合い)が問題になることはあります。例えば、自分が行った変更が自分ですぐに確認できること(read-after-write一貫性またはread-your-writes一貫性) 、一度読めたものが消えないようにすること(モノトニックな読み取り)、読み取れるようになる順序が書き込みの順序と一致すること(一貫性のあるプレフィックス読み取り)などです。
6章 パーティショニング
さらに大規模なデータになると、レプリケーションだけではなく、パーティショニングが必要になります。パーティショニングはレプリケーションとは異なり、同じノードの中で大規模なデータを分割するために行われます。パーティションは、MongoDBやElasticsearchやSolrではシャード、Bigtableではタブレット、HBaseではリージョン、RiakやCassandraではvnodeと呼ばれています。
パーティショニングが均等になっていないと、一部のパーティションにデータやクエリが集中するスキュー(skew)が発生し、負荷分散の効果が大きく損なわれます。不均等な高負荷がかかっているパーティションをホットスポットと呼びます。単純にキーの範囲でパーティショニングをすると、範囲に対するクエリは効率的に処理できますが、スキューが起きやすくなります。MD5のようなキーのハッシュに基づくパーティショニングは、範囲に対するクエリは非効率になりますが、スキューは防ぐことができます。それでも、数十万のフォロワーを持つユーザによって1レコードへの読み取りが集中するような場合はクエリが同じパーティションに集中してしまいます。このようなケースには多くのデータシステムが対応していないので、アプリケーション側での対策が必要です。また、時間が経つにつれてパーティション間のバランスが崩れていくので、随時リバランシングを行う必要があります。
検索サーバの重要な機能にセカンダリインデックスがありますが、この場合は状況がもっと複雑になります。せカンダリインデックスは、各パーティションの内部でローカルに作成する方法と、パーティションをまたいでグローバルに作成する方法があります。前者だと書き込み先が1つのパーティションで済み、後者だと読み取り先が1つのパーティションで済みます。
7章 トランザクション
データシステムで生じうる問題について理解するためには、トランザクションについて学ぶ必要があります。トランザクションとは、データの複数の読み書き操作を論理的な単位としてまとめる方法のことです。トランザクションは成功(コミット)または失敗(中断またはロールバック)し、失敗したら安全にリトライできます。読み書き操作が途中で終了したり互いに干渉したりするケースを想定しなくて良いので、実装が楽になります。
トランザクションが提供する安全性の保証は、しばしばACID(atomicity(原子性)、consistency(一貫性)、isolation(分離性)、durability(永続性))という略語で表現されますが、これが実際に意味するところはデータベースによって異なります。ちなみに、NoSQLの文脈でよく用いられるBASE(basically available(基本的に利用可能)、soft state(厳密でない状態遷移)、eventual consistency(結果整合性))はもっと曖昧です。
- 原子性:エラー時にトランザクションのすべての書き込みを破棄できること。「中断可能性」という言葉の方が適切です
- 一貫性:常に成立する何らかの命題があること(例:会計システムにおいて、貸方と借方は常に等しい)。これはアプリケーションの特性なので、ACIDに入れるべきでありませんでした
- 分離性:並行したトランザクションが互いに干渉しないこと
- 永続性:成功したトランザクションが消えないこと
2つのトランザクションが同時に同じデータを扱おうとすると、レース条件が問題になります。このバグはテストで検出しづらく、再現するのも難しいですが、データベースに「トランザクションの分離性」を持たせると、 この並行性の問題をアプリケーション開発者から隠蔽できます。分離性にはいくつかのバリエーションがあります。
- Read Commited: 最も基本的なレベルで、未コミットのデータが読まれたり(ダーティーリード)更新されたり(ダーティライト)することが生じないことを保証します。読み取りスキュー(例:口座Aから口座Bにお金を移したときに、口座Aから出金できているのに口座Bには着金していない瞬間が存在してしまうということ)は許容します
- スナップショット分離:読み取りスキューを防ぐため、それぞれのトランザクションが「一貫性のあるスナップショット」のみから読み取りを行います
- 更新のロストの回避:複数の「読み取り-計算-書き戻し」という操作が同じデータに対して同時に起きると更新がロストします。これはロックや賢いマージ方法で防ぐこともできますが、多くのデータベースでは後勝ち(last write wins)がデフォルトになっています
- 直列化可能:すべてのレース条件を解決する最も強い分離レベルで、トランザクションを直列に実行した場合と同じ結果を保証します。書き込みスキュー(同時に複数のトランザクションが発生すると全体としての制約条件が守られなくなること)などが生じません。実装方法としては、文字通りの直列実行、ツーフェーズロック、直列化可能スナップショット分離があります
8章 分散システムの問題
マシンが地理的に分散していると、以下のように厄介な問題が大量に発生します。
- ネットワークは故障することがあります。ネットワークを介したノード間でのやり取りにおいてレスポンスが来ないとき、送信者はそれがネットワークと受信者のどちらの障害かを区別することができません
- ノードのクロックは他のノードとずれているかもしれないので、クロックに依存するのは危険です
数千ノードからなる分散システムでは、常にどこかが故障していると想定すべきですが、上記のような部分障害は非決定的なので、テストが非常に困難です。
分散システムにおいて他のノードについて何かを確実に知ることはできません。できるのは、信頼できないネットワークを介して受信したメッセージに基づく推測だけです。このような状況下で何か(例:あるノードの障害)を判断するために、クオラムという多数決の仕組みで真実に関する合意を行います。意図的に嘘をつくノードが存在するByzantine障害というケースも存在しますが、通常は考慮しなくても問題ありません。
9章 一貫性と合意
ここまでで分散システムで生じうる問題について見てきたので、次は耐障害性を持つ分散システムを構築するためのアルゴリズムとプロトコルについて例(本記事では省略)を通して学びます。分散システムに耐障害性を持たせるためには、何らかの形で合意を取る仕組みが必要です。合意に失敗すると、2つのノードがどちらの自分がリーダーであると信じているような危険な状況(スプリットブレイン)につながるからです。多くのデータベースが提供する結果整合性では、レプリカがいつ収束するのかということについては保証がありません。そこで、以下のような強い一貫性が必要になります。
- 線形化可能性(linearizability):最も強い一貫性モデルで、操作に全順序を与えます。レプリケーションされたデータがあたかも一つしかないように振る舞います。すなわち、書き込みが成功したら、すべてのレプリカでその値が読めるようになります。挙動が理解しやすいという魅力がありますが、ネットワークの遅延が大きいときに速度が大幅に落ちるという欠点もあります
- 因果律:線経過可能性よりも弱い一貫性モデルで、操作に半順序を与えます。2つの出来事の間に因果関係がある場合は前後関係が決定できますが、前後関係が決まらない場合も許容します。ネットワークの遅延による影響が比較的小さく済みます
線形化可能性や因果律によって定義される順序関係は、複数ノード間で合意を取るための基礎になります。この仕組みの応用例として、Kubernetesの内部でも使われているZooKeeperやetcdといったコーディネーションサービスがあります。
10章 バッチ処理
システム間のやり取りには、リクエスト・レスポンス型のサービス(オンラインシステム)、バッチ処理(オフラインシステム)、ストリーム処理(準リアルタイムシステム)があります。スケーラブルなシステムを構築する上で、バッチ処理は特に重要な構成要素です。例えば、2004年に発表されたバッチ処理のアルゴリズムであるMapReduceは「Googleをあれほどまでにスケーラブルにしたアルゴリズム」とまで称されています。
Unixにおいてファイルとパイプがプログラム間のインターフェイスとなっているように、MapReduceでは分散ファイルシステムがこのインターフェイスとなっています。例えば、MapReduceの有名な実装であるHadoopは、HDFS(Hadoop distributed file system)という分散ファイルシステムを利用しています。MapReduceは分散ファイルシステムを使って次のようにデータを処理します。
- 入力ファイル群を読み取り、レコードに分割する
- mapper関数を呼び、各レコードからキーと値を取り出す
- すべてのキーと値のペアをキーでソートする
- reducer関数を呼び、3のペアを順に処理していく(ソートしてあるので多くの状態をメモリに保持せずに実行できる)
mapperとreducerを単純な形に制約することで、並列処理に関わる複雑さをMapReduceフレームワーク側で抽象化できます。1回のMapReduceでは単純なことしかできないので、連鎖してワークフローにするのが一般的です。これによって検索インデックスや機械学習システム、ETLパイプラインの構築が実現されています。
MapReduceを使うと、テーブルの結合のアルゴリズムを説明することができます。
- ソートマージ結合:結合をreducer側で行います。結合される双方のテーブルを、キーを取り出すmapperに渡します。パーティショニング、ソートを経てreducerでマージを行います。どんな入力にも使えるという利点がありますが、ソートなどで高負荷がかかるという欠点があります
- ブロードキャストハッシュ結合:結合をmapper側で行います。2つのテーブルのうちどちらかが小さいなら、パーティション化せずにハッシュテーブルに全体をロードします。そして、大きい方のテーブルを1行ずつスキャンして各レコードに対してハッシュを検索します
- パーティション化ハッシュ結合:結合をmapper側で行います。2つのテーブルが同じ方法でパーティション化されているなら、各パーティションに対して独立にブロードキャストハッシュ結合を実行します
11章 ストリーム処理
バッチ処理の弱点である遅延を短くするには、処理を1秒ごとやイベントごとに実行するストリーム処理を採用できます。この文脈では、レコードをイベントと呼びます。イベントストリームはバッチデータとは異なり、有限ではなくインクリメンタルに処理されます。
イベントはプロデューサ(パブリッシャまたは送信者)によって生成され、複数のコンシューマ(サブスクライバまたは受信者)によって消費されます。関連するイベント群をトピックまたはストリームと呼びます。このパブリッシュ/サブスクライブ(pub/sub)モデルでは、コンシューマから頻繁なポーリングが発生します。これを効率的に処理するため、イベント通知の配信という目的に特化した様々なメッセージングシステムが開発されています。これらのシステムは以下の2つの観点で特徴づけられます。
- プロデューサの送信速度がコンシューマの消費速度より速い時にどうなるか?(選択肢として、「メッセージがドロップする」「メッセージをキューに詰める」「フロー制御を入れる」の3通りがあります)
- ノードがクラッシュしたらメッセージは失われるか?
多くのメッセージングシステムは、中間ノードを経由せずに直接通信します。例えば、レイテンシの低さが重要な金融業界ではUDPマルチキャストが使われています。メッセージを失いたくない場合には、メッセージブローカーによって仲介することができます。AMQP/JMSスタイルのメッセージブローカーはメッセージキューを持ち、コンシューマから承認(acknowledgement)を受け取るまで再配信を繰り返します。これに対して、ログベースのメッセージブローカーはメッセージをログに追記し、コンシューマはそのログを順番に読むことでメッセージを受信します。
ストリーム処理を使うと、主に3つのことができます。
- イベント中のデータを取り出してデータベース、キャッシュ、検索インデックスなどに書き込むこと
- メール通知などのイベントをプッシュすること。この場合、最終的なコンシューマは人間です
- 1つ以上の入力ストリームから1つ以上の出力ストリームを作ること
ストリーム処理がバッチ処理と大きく異なるのは、ストリームには終わりがないという点です。この決定的な違いによって、ソートマージ結合やクラッシュ後に最初からやり直すといったことが不可能になります。ストリームとテーブルを結合する際には、時間によって変化しうる列(税率など)の存在が「緩やかに変化する次元(slowly changing dimension)」という問題を引き起こします。これはバージョンIDをつけたスナップショットを保存しておくことで対処できます。
12章 データシステムの将来
最後に、データシステムの「あるべき姿」や開発者が意識すべき原則について論じます。
- すべての問題を解決できる単一のツールは存在しないので、問題の特性に応じて複数のソフトウェアを組み合わせることを常に検討すべきです
- アプリケーションをデータフローの観点から捉え直し、複数の疎結合なコンポーネントに解体することで、アプリケーションの進化が容易になります
- 正確なアプリケーションを作るためには、冪等性と制約の担保が重要です。ただし、実際のビジネスでは、完全な制約よりも謝罪による解決をとることもあります
- ソフトウェアとデータの組み合わせは人々の生活を向上させることができると同時に、差別、搾取、情報漏洩、監視などを通じて大きな害を及ぼすこともあり得ます。開発者はこのリスクを理解し、責任を持たなければなりません
Written by Shion Honda. If you like this, please share!