ddia-principles について
『Designing Data-Intensive Applications』の要点をまとめた学習ガイドです。データシステムの設計や運用を学ぶ人向けに、信頼性、ストレージ、分散整合性、派生データなどを整理しています。
主な機能
- 01信頼性とスケーラビリティの原則を要約
- 02データモデルとストレージエンジンを比較
- 03レプリケーションとパーティショニングを解説
- 04トランザクション分離と異常を整理
- 05分散整合性とコンセンサスを網羅
活用シーン
- 01データシステム設計の概念を復習
- 02ストレージやレプリケーション方式を比較
- 03整合性とトランザクションのトレードオフを学習
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ luoling8192/ ai-coding-principles --skill ddia-principlesインストール後、Claude Code で/ddia-principlesを入力して読み込まれたか確認します。
詳細
データ集約型アプリケーションを設計する — 要点ガイド
出典:Martin Kleppmann、Designing Data-Intensive Applications 中心的な主張:現代のアプリケーションにおける課題の中核はデータであり、コンピューティングではありません。
パート I:データシステムの基礎
第1章:信頼性、スケーラビリティ、保守性
3つの柱
| 柱 | 定義 | 主な指標 |
|---|---|---|
| 信頼性 | 障害が発生してもシステムが正しく動作すること | 障害 ≠ 失敗:障害に耐え、失敗を防ぐ |
| スケーラビリティ | 負荷の増加にシステムが適切に対応できること | パーセンタイルで測定:p50、p95、p99、p999 |
| 保守性 | システムの運用、理解、進化が容易であること | 運用性 + シンプルさ + 進化性 |
障害のカテゴリ
- ハードウェア:ランダムかつ独立して発生します(ディスク、RAM、電源)。冗長化(RAID、二重電源)で軽減します。
- ソフトウェア:すべてのノードに同時に影響する系統的なバグです(うるう秒のバグ)。プロセス分離、監視、カオスエンジニアリングで軽減します。
- 人的要因:停止の最大の原因です(設定ミス)。優れた抽象化、サンドボックス、カナリアデプロイ、高速なロールバックで軽減します。
スケーラビリティのパターン
- 垂直(スケールアップ):より大きなマシンを使います。シンプルですが、上限があります。
- 水平(スケールアウト):マシンを増やします(共有なし)。複雑ですが、上限はありません。
- 弾力的:負荷を検知して自動スケールします。予測できないワークロードに適しています。
Twitterのファンアウトに関するケーススタディ:書き込みは4.6k回/秒ですが、読み取りは300k回/秒です。解決策:ほとんどのユーザーにはタイムラインを事前計算し(書き込み時ファンアウト)、著名人には読み取り時にマージします。
パフォーマンス:平均値ではなくパーセンタイルを使う
- p50 = 中央値です。p99では、ユーザー体験にとってテールレイテンシが重要です。
- Amazon:100msの遅延は売上を1%減少させます。
- テールレイテンシの増幅:1つの遅いバックエンド呼び出しによって、並列リクエスト全体が遅くなります。
第2章:データモデルとクエリ言語
モデル選択ガイド
| モデル | 最適な用途 | 弱点 |
|---|---|---|
| リレーショナル | 構造化データ、複雑な結合、ACIDトランザクション | スキーマが厳格、OOPとのインピーダンスミスマッチ |
| ドキュメント | 階層型データ、柔軟なスキーマ、データ局所性 | 結合が不得意、多対多の関係 |
| グラフ | 高度につながったデータ、深さが可変のトラバーサル | ツールが未成熟、パーティション分割が困難 |
スキーマ戦略
- 書き込み時スキーマ(リレーショナル):書き込み時に構造を強制します。早期にエラーを検出できますが、移行コストがかかります。
- 読み取り時スキーマ(ドキュメント):読み取り時に構造を解釈します。柔軟ですが、アプリに検証の負担がかかります。
正規化と非正規化
- 正規化:単一の信頼できる情報源、一貫した更新を実現しますが、結合が必要です。
- 非正規化:読み取りは高速になりますが、一貫性が損なわれるリスクや更新時の異常が生じます。
傾向:モデルは収束しつつあります。PostgreSQLはJSONをサポートし、MongoDBは結合を追加しました。思想ではなく、アクセスパターンに基づいて選びます。
第3章:ストレージと検索
ストレージエンジンの比較
| 特徴 | B-Tree | LSM-Tree |
|---|---|---|
| 書き込みスループット | 低い(インプレース更新 + WAL) | 高い(シーケンシャルな追記) |
| 読み取りレイテンシ | より予測しやすい | 複数のSSTableを確認する場合があります |
| 書き込み増幅 | 高い | 低い |
| スペース効率 | 断片化する可能性があります | 圧縮効率が高い |
| トランザクションのサポート | よりシンプル(ツリーノードをロック) | より複雑 |
| 使用例 | PostgreSQL、MySQL、Oracle | LevelDB、RocksDB、Cassandra |
OLTPとOLAP
| 観点 | OLTP | OLAP |
|---|---|---|
| アクセス | ランダム、少数のレコード | シーケンシャルスキャン、数百万行 |
| ユーザー | エンドユーザー | アナリスト |
| データ | 現在の状態 | 過去のイベント |
| 規模 | GB–TB | TB–PB |
| 最適化対象 | 低レイテンシ | スループット |
データウェアハウジング
- ETL:OLTP から抽出 → 変換 → ウェアハウスにロードします。
- スター・スキーマ:中央のファクトテーブル(イベント)+ディメンションテーブル(属性)。ファクトテーブルは 100 列以上、ペタバイト規模になることがあります。
- 列指向ストレージ:各列を個別に保存します。クエリが少数の列だけにアクセスする場合、I/O を大幅に削減できます。ビットマップエンコーディング、ランレングス圧縮、ベクトル化処理が可能になります。
第 4 章:エンコーディングと進化
フォーマットの比較
| フォーマット | サイズ(例) | スキーマ | 進化 | 言語間の互換性 |
|---|---|---|---|---|
| JSON | 81 bytes | 暗黙的 | 手動 | 優秀 |
| Thrift | 59 bytes | 必須 | フィールドタグ | 良好 |
| Protobuf | 33 bytes | 必須 | フィールドタグ | 優秀 |
| Avro | 32 bytes | 必須 | 名前の照合 | 良好 |
互換性のルール
- 後方互換性:新しいコードで古いデータを読み取れます。(常に必須です)
- 前方互換性:古いコードで新しいデータを読み取れます。(ローリングアップグレードに必要です)
- ルール:デフォルト値を持つフィールドのみ追加・削除します。削除したフィールドタグは再利用しないでください。
データフローパターン
- データベース経由:ローリングデプロイ中に複数のコードバージョンが共存します。データはコードより長く存続します。
- サービス経由(REST/RPC):クライアントより先にサーバーを更新します。リクエストには後方互換性、レスポンスには前方互換性が必要です。
- 非同期メッセージング経由:プロデューサーとコンシューマーを分離します。独立したバージョン進化をサポートします。
避けるもの:言語固有のシリアライズ(Java Serializable、Python pickle)—ベンダーロックインとセキュリティリスクにつながります。
第 II 部:分散データ
第 5 章:レプリケーション
レプリケーションモデル
| モデル | 書き込み | 競合 | ユースケース |
|---|---|---|---|
| シングルリーダー | 1 つのノード | なし | 最も一般的(PostgreSQL、MySQL) |
| マルチリーダー | 複数のノード | 解決が必要 | マルチデータセンター、オフラインクライアント |
| リーダーレス | 任意のノード | 解決が必要 | Cassandra、Riak、Voldemort |
同期レプリケーションと非同期レプリケーション
- 同期:耐久性がありますが、レプリカに障害が発生するとブロックします。
- 非同期:高速ですが、リーダーに障害が発生するとデータを失うリスクがあります。
- 準同期:1 つのレプリカは同期し、残りは非同期です。現実的な妥協案です。
レプリケーションラグの問題と解決策
| 問題 | 症状 | 解決策 |
|---|---|---|
| 書き込み後の読み取り | ユーザーが自分の書き込みを確認できない | ユーザー自身のデータはリーダーから読み取る |
| 単調読み取り | データが時間を遡る | ユーザーを 1 つのレプリカに固定する |
| 一貫したプレフィックス読み取り | 因果順序が崩れる | 因果関係のあるデータを同じパーティションに書き込む |
競合解決
- 最終書き込み優先(LWW):シンプルですが、データを失います。キーが不変の場合に限り安全です。
- マージ:値の和集合、連結、CRDT データ構造を使用します。
- アプリケーションレベル:すべてのバージョン(「兄弟」)を返し、アプリに判断させます。
- バージョンベクトル:レプリカごとの因果依存関係を追跡します。
クォーラム:w + r > n
- w = 書き込み確認応答数、r = 読み取りクエリ数、n = レプリカの総数です。
- スロッピークォーラム:パーティション発生中にホーム以外のノードでの書き込みを受け入れます(ヒンテッドハンドオフ)。可用性が向上する一方、一貫性は弱まります。
第 6 章:パーティショニング(シャーディング)
パーティショニング戦略
| 戦略 | メリット | デメリット |
|---|---|---|
| キー範囲 | 範囲クエリが効率的 | 連番キーでホットスポットが発生するリスク |
| ハッシュ | 均等に分散 | 範囲クエリができない |
| 複合 | 最初の部分をハッシュ化し、残りをソート | より複雑ですが、両方の利点を備える |
セカンダリインデックスのパーティショニング
- ローカル(ドキュメントベース):各パーティションが自身のデータにインデックスを付けます。書き込みはシンプルですが、読み取りでは散布・収集が必要です。
- グローバル(用語ベース):インデックスを用語ごとにパーティショニングします。読み取りは効率的ですが、書き込みでは複数のパーティションを更新します。
リバランシング
- 固定パーティション数:ノード数より多くのパーティションを用意します。ノードの変更時に再分配します。(Riak、Elasticsearch)
- 動的:サイズに応じて分割・マージします。(HBase、RethinkDB)
- ノード数に比例:ノードごとに固定数のパーティションを割り当てます。(Cassandra)
リクエストのルーティング
- 任意のノードにラウンドロビン方式で送信(必要に応じてノードが転送)
- ルーティング層(パーティションを認識するプロキシ)
- クライアント認識型(クライアントがパーティションマップを把握)
- ZooKeeper:パーティションとノードの対応付けを一元管理します。HBase、Kafkaで使用されています。
第7章:トランザクション
分離レベル(弱い → 強い)
| レベル | 防止するもの | 許容するもの | 実装 |
|---|---|---|---|
| Read Committed | ダーティリード、ダーティライト | 非反復読み取り、更新の消失 | 行ロック+古い値のコピー |
| Snapshot Isolation | + 非反復読み取り | ライトスキュー、ファントム | MVCC(マルチバージョン) |
| Serializable | すべて | なし | 2PL、逐次実行、またはSSI |
並行性の異常
| 異常 | 説明 | 例 |
|---|---|---|
| ダーティリード | コミットされていないデータを読み取る | 書き込み途中の送金を読み取る |
| ダーティライト | コミットされていないデータを上書きする | 2人の購入者が同じ商品を「獲得」する |
| 更新の消失 | 読み取り・変更・書き込みの競合 | 2つのカウンター増分処理が同時に実行される |
| ライトスキュー | 古い読み取り結果に基づいて判断する | 2人の医師がどちらも当直を外れる |
| ファントム | 新しい行によってクエリ結果が変わる | 会議室の二重予約 |
Serializableの実装
- 逐次実行:単一スレッド、インメモリ。高速ですが、スループットに限界があります。(VoltDB、Redis)
- 2フェーズロック(2PL):共有ロック/排他ロックをコミットまで保持します。強力ですが、遅く、デッドロックが発生しやすくなります。
- SSI(Serializable Snapshot Isolation):楽観的方式です。自由に実行し、コミット時に競合を検出します。読み取り負荷の高いワークロードで最高のパフォーマンスを発揮します。(PostgreSQL 9.1+)
第8章:分散システムの課題
3つの信頼性のない要素
ネットワーク:非同期パケットネットワークには、配信の保証もタイミングの保証もありません。クラッシュとネットワーク遅延を区別することはできません。障害検出に使えるのはタイムアウトだけですが、正しいタイムアウト値は存在しません。
クロック:
- 壁時計:逆戻りすることがあります(NTP補正)。イベントの順序付けには決して使用しないでください。
- 単調増加時計:経過時間の計測には安全ですが、ノード間の比較には使えません。
- クォーツ時計のずれ:約200ppm → 30秒ごとに6msの誤差。
プロセス:GCの停止、VMの一時停止、ページフォールトにより、スレッドが予告なく停止します。一時停止したノードは、時間が経過したことを認識できません。
主な原則
- 障害 ≠ 故障:部分障害を前提に設計します。一部のノードは動作し、ほかのノードは動作しないことがあります。
- 真実は多数決で決まる:個々のノードだけではシステムの状態を判断できません。クォーラム投票によって決定します。
- フェンシングトークン:単調増加するトークンによって、ゾンビプロセスが状態を破損するのを防ぎます。
- 安全性と活性:安全性(悪いことが決して起きないこと)は常に維持されなければなりません。活性(良いことがいずれ起きること)には条件が伴う場合があります。
第9章:一貫性と合意
一貫性モデル(強い → 弱い)
| モデル | 保証 | コスト |
|---|---|---|
| 線形化可能性 | 1つのコピーがあるかのように振る舞い、すべての操作がアトミック | 高レイテンシ、パーティション発生時の可用性低下 |
| 因果一貫性 | 原因と結果の順序を尊重 | パフォーマンスが向上し、パーティションにも耐性がある |
| 結果整合性 | レプリカが最終的に収束 | 最高のパフォーマンス、最も弱い保証 |
線形化可能性のユースケース
- リーダー選出/分散ロック
- 一意性制約(ユーザー名、ファイル名)
- チャネル間の調整(メッセージキュー+ストレージ)
合意アルゴリズム
- 2PC:コーディネーター方式で、コーディネーターの障害時にはブロックします。実用的ですが、脆弱です。
- Paxos/Raft/Zab:エポックベースのリーダー選出+クォーラム投票。ブロックしません。etcd、ZooKeeper、Consulで使用されています。
- FLP不可能性:クラッシュが発生する純粋な非同期システムでは、合意の達成は不可能です。実用的なアルゴリズムではタイムアウトを使用します。
全順序ブロードキャスト ≡ 合意 ≡ 線形化可能CAS
この3つの問題は数学的に等価です。1つを解決すれば、すべてを解決できます。
ZooKeeper / etcd パターン
- 調整のための小規模なコンセンサスクラスター(3~5 ノード)。
- 線形化可能なアトミック CAS 操作。
- セッションのハートビートによる障害検出。
- 用途:リーダー選出、パーティション割り当て、分散ロック、サービスディスカバリー。
第 III 部:派生データ
第 10 章:バッチ処理
Unix の哲学 → MapReduce
- 各プログラムは、1 つのことをうまく行います。
- あるプログラムの出力 = 別のプログラムの入力です。
- 入力は不変で、処理は決定論的です。
MapReduce パイプライン
Input → Mapper (extract key-value) → Sort/Partition → Reducer (aggregate by key) → Output
分散結合戦略
| 結合タイプ | 使用する状況 | 方法 |
|---|---|---|
| ソートマージ | 両方の入力が大きい | 結合キーでソートし、リデューサーでマージします |
| ブロードキャストハッシュ | 一方の入力が小さい(RAM に収まる) | 小さい側をハッシュテーブルとして読み込みます |
| パーティション化ハッシュ | 両方の入力が同じ方法でパーティション分割されている | パーティションごとにハッシュ結合します |
MapReduce の先へ:データフローエンジン(Spark、Flink、Tez)
- ワークフロー全体を単一のジョブとして扱います。
- 中間結果をパイプライン処理します(HDFS への完全な実体化を避けます)。
- 可能な場合はデータをメモリ内に保持します。
- 障害復旧のために計算のリネージ(RDD)を追跡します。
- 反復アルゴリズムをサポートします(Pregel/BSP モデルによるグラフ処理)。
第 11 章:ストリーム処理
メッセージブローカーモデル
| モデル | 配信 | 順序 | 再生 | 用途 |
|---|---|---|---|---|
| AMQP/JMS | メッセージごとに確認応答し、その後削除 | 順序の保証なし | なし | タスクキュー、非同期 RPC |
| ログベース(Kafka) | オフセットベースで保持 | パーティション内の順序 | あり | イベントストリーミング、CDC |
変更データキャプチャ(CDC)
データベースの変更をイベントストリームとして抽出し、派生システム(検索インデックス、キャッシュ、データウェアハウス)を同期します。信頼できる情報源はデータベースのままであり、派生ビューはコンシューマーです。
イベントソーシング
状態を、ビジネスイベントの追加専用シーケンス(DB 操作ではありません)としてモデル化します。イベントは不変の事実です。現在の状態 = イベント履歴を畳み込んだ結果です。
ストリーム結合
| 結合 | 入力 A | 入力 B | 状態 |
|---|---|---|---|
| ストリーム-ストリーム | イベント | イベント | 時間ウィンドウ付きバッファー |
| ストリーム-テーブル | イベント | DB スナップショット(CDC 経由) | ローカルの実体化テーブル |
| テーブル-テーブル | CDC ストリーム | CDC ストリーム | 派生した実体化ビュー |
時間とウィンドウ処理
| ウィンドウ | 説明 |
|---|---|
| タンブリング | 固定サイズで重複なし(例:1 分ごと) |
| ホッピング | 固定サイズで重複あり(例:30 秒ごとに 1 分間のウィンドウ) |
| スライディング | 相互の時間しきい値内にあるすべてのイベント |
| セッション | アクティビティの間隔でグループ化(例:30 分間の非アクティブ状態) |
イベント時刻 ≠ 処理時刻です。 正確性を保つため、常にイベント時刻を使用します。ウォーターマークまたは訂正イベントの発行によって、遅延イベントを処理します。
処理保証
- マイクロバッチ処理(Spark Streaming):約 1 秒のレイテンシーで、アトミックな小規模バッチを処理します。
- チェックポイント(Flink):メッセージバリアを使用した定期的なスナップショットです。
- 冪等性:メッセージオフセットまたは一意の ID を使用して重複を排除します。
- エンドツーエンドの厳密に 1 回の処理には、冪等な出力と重複排除の両方が必要です。
第 12 章:データシステムの未来
データ統合パターン
すべてを処理できる単一のデータベースはありません。統合の基盤としてイベントログを使用します。
- すべての書き込みを、信頼できるイベントログ経由で行います。
- 派生システム(インデックス、キャッシュ、ML モデル)がログを消費します。
- 決定論的で冪等な関数が、レイヤー間のデータを変換します。
データベースの分割
関心事を分離します:
- レコードシステム:信頼できる書き込みを記録します。
- 派生システム:インデックス、キャッシュ、実体化ビューが変更ストリームを消費します。
- 段階的な移行が可能になり、旧システムと新システムを並行稼働できます。
エンドツーエンドの厳密な一回限りの処理
低レベルの保証(TCP、DB トランザクション)だけでは、アプリケーションの正しさは保証されません。次の点が必要です。
- 操作識別子:アプリケーションレベルで UUID ベースの重複排除を行います。
- べき等な操作:一度実行しても複数回実行しても、同じ効果が得られます。
- 一意制約の適用:パーティション分割されたストリーム処理によって行います。
非同期の制約適用
分散トランザクションの代わりに、次の手順を実行します。
- 制約フィールドに基づいて、リクエストをパーティション分割されたログにルーティングします。
- ストリームプロセッサーが競合するリクエストの順序を決定します。
- 違反を拒否し、出力ストリームを通じてクライアントに通知します。
- 一部のアプリケーションでは、一時的な違反を補償トランザクションで許容できます。
監査可能性
- データを不変のイベントログとして扱うことで、再構築と検証が可能になります。
- 暗号学的な監査証跡(Merkle ツリー)を実装します。
- バックアップの復元とデータの再構築を定期的にテストします。
意思決定フレームワーク:クイックリファレンス
データモデルの選択
Many-to-many relationships? → Relational or Graph
Hierarchical / nested data? → Document
Highly connected data? → Graph
Flexible / evolving schema? → Document (schema-on-read)
Strong consistency required? → Relational (ACID)
ストレージエンジンの選択
Write-heavy workload? → LSM-tree (RocksDB, Cassandra)
Read-heavy, predictable? → B-tree (PostgreSQL, MySQL)
Analytical queries? → Column store (ClickHouse, Redshift)
Full-text search? → Inverted index (Elasticsearch)
レプリケーション戦略の選択
Single datacenter? → Single-leader
Multi-datacenter? → Multi-leader
Offline-first clients? → Multi-leader or Leaderless
Maximum availability? → Leaderless with sloppy quorum
Strong consistency? → Single-leader with sync replication
分離レベルの選択
Read-only analytics? → Snapshot isolation
General OLTP? → Read committed (default in most DBs)
Financial / critical? → Serializable (prefer SSI over 2PL)
High write contention? → Serial execution (if data fits in RAM)
バッチ処理とストリーム処理の選択
Historical data reprocessing? → Batch (Spark, Flink batch mode)
Real-time derived views? → Stream (Kafka + Flink/Spark Streaming)
Both needed? → Unified engine (Flink) over Lambda architecture