データ指向アプリケーションデザインを読んだので、メモをまとめてみました。
600ページを超える内容で、全体で12章ありますが、本記事では序盤の4章までをまとめています。
本書では膨大な知識が語られており、今回は極一部のみメモっています。
気になる方はぜひ実際に本書を手に取ってみていただければと思います。
1章 信頼性、スケーラビリティ、メンテナンス性に優れたアプリケーション
はじめに:現代のデータ指向アプリケーションの全体像
現代のアプリケーションは、データという観点から見ると、
・データベース
・キャッシュ
・検索インデックス
・ストリーム処理
・バッチ処理
など、様々なデータ処理コンポーネントによって構成されている。これらは既に多くがツールやライブラリとして提供されており、ゼロから構築する必要はない。
しかし、アプリケーションの要件に応じて、最適なデータシステムの選択が不可欠であり、それぞれの機能や制約を理解しておく必要がある。
境界が曖昧になる現代のデータツール
データベース、メッセージキュー、キャッシュなどは明確に分かれていたが、最近では境界が曖昧になってきている。
重要なのは、単一のツールに依存せず、複数の処理タスクに分解し、用途ごとに適したツールを選択して連携させること。
1、信頼性(Reliability)
定義:障害が発生しても、システムが正しく動作し続ける能力
フォールトとフォールトトレランス
フォールト(fault):問題の原因となるイベント(例:ネットワーク切断、バグ、操作ミス)
障害(failure):フォールトによってサービスが提供できなくなる状態
システムは、障害を防ぐことよりも「耐障害性(フォールトトレランス)」を持たせる設計が現実的
障害への具体的対処
障害の種類 | 主な対策例 |
---|---|
ハードウェア障害 | RAID、冗長化、ホットスワップなどの冗長構成 |
ソフトウェアバグ | 自動テスト、フェイルファスト設計、監視、ログ収集 |
ヒューマンエラー | UI設計改善、操作制限、ロールバック、影響範囲の最小化、監査ログの導入 |
2、スケーラビリティ(Scalability)
定義:負荷が増えても、システムの性能が維持される能力
「負荷(Load)」の定義はシステムにより異なる
負荷指標の例
・Webサーバー:秒間リクエスト数
・データベース:読み書きの比率
・チャットアプリ:同時接続ユーザー数
・キャッシュ:ヒット率
「性能」の測定指標
・バッチ処理:スループット(処理件数/秒、ジョブ完了時間)
・オンライン処理:レスポンスタイム(応答速度)
レスポンスタイムとレイテンシの違い
▪️レスポンスタイム:クライアントから見た応答速度(処理時間+通信/待機時間)
▪️レイテンシ:処理が始まるまでの待機時間
平均ではなくパーセンタイルで測定
平均レスポンスタイムは一部の遅延に影響されやすいため、パーセンタイル(例:95、99)を重視
パーセンタイルはSLO(目標)やSLA(契約)の根拠にも用いられる
例:Amazonではレスポンスタイム100msの増加で売上が1%減少、1秒の遅延で顧客満足度が16%低下。ただし、P99.99レベルの改善は投資対効果が低いため、外部要因と割り切る判断も重要
負荷への対応戦略
方法 | 内容 | 特徴 |
---|---|---|
スケールアップ(垂直拡張) | より高性能なマシンに置き換え | シンプルだがコストが高い |
スケールアウト(水平拡張) | 複数台で分散処理(シェアード・ナッシング) | 柔軟だが設計の難度が上がる |
多くのシステムは スケールアップとスケールアウトを併用して最適化されている。
3、メンテナンス性(Maintainability)
定義:長期運用・変更への耐性と、運用者・開発者の作業容易性
■ソフトウェア設計における3つの設計原則
原則 | 内容 |
---|---|
運用性(Operability) | システム運用がしやすいように設計されていること |
単純性(Simplicity) | 新たに加わったエンジニアが理解しやすい構造であること |
進化性(Evolvability) | 将来的な要件変更・機能追加に柔軟に対応できること |
運用性(Operability)の実現手段
・障害の早期検出と迅速な復旧
・パフォーマンス低下の原因特定と可視化
・自動化された構成管理、標準ツールとの連携
・ドキュメントの整備と操作手順の明示化
単純性(Simplicity)と抽象化
システムの複雑さを抑えるために、抽象化を活用
偶発的な複雑性(一時的な対応や技術的負債)を排除することで、理解しやすさ・保守性を向上
進化性(Evolvability)とアジャイル対応
要件の変化や新機能追加に対応するために、変更しやすい設計(柔軟なモジュール分割、インターフェース定義)が必要。変化を前提とした設計手法(例:アジャイル開発)が有効なアプローチ
2章 データモデルとクエリ言語
1、階層的なデータモデルの構造
多くのアプリケーションでは、複数の抽象レイヤーにまたがるデータモデルを使用しており、それぞれのレイヤーは下位のレイヤーの構造に基づいて構築されている。
データモデルのレイヤー構造(上位→下位):
レイヤー | 内容 |
---|---|
1、アプリケーションレベル | オブジェクト、データ構造、およびそれらを操作するAPIによるモデル化 |
2、ストレージレベルの論理構造 | JSON、XML、リレーショナルテーブルなどにおける構造化データの表現 |
3、物理的な表現 | ディスク・メモリ・ネットワーク上でのバイト列 |
4、ハードウェアレベル | 電流・光パルス・磁気など、物理的な表現形式 |
2、リレーショナルモデル
リレーショナルモデル(関係モデル)は1970年代に登場し、長らくデータベースの主流であり続けた。
台頭の理由:
・汎用性の高さ:構造化されたビジネスロジックを超え、幅広いユースケースに対応
・表現力:多対多の関係性やJOIN操作が可能
・標準化と整合性の強さ:SQLという標準クエリ言語を持ち、ACIDトランザクションによる整合性保証がある
3、非リレーショナルモデル(NoSQL)
NoSQLは特定の技術ではなく、「Not Only SQL」の略とされ、リレーショナルモデル以外の選択肢全般を指す。
採用される背景(リレーショナルモデルに対する利点):
・水平方向のスケーラビリティの容易さ
・オープンソース・低コストな運用
・柔軟なスキーマや特殊なクエリパターンへの対応
ユースケースに応じてリレーショナルとNoSQLを使い分けたり併用するアーキテクチャ(ポリグロット・パーシステンス)が現実的。
NoSQLの種類と用途
■ ドキュメントデータベースとグラフデータベース
ドキュメントデータベース | グラフデータベース | |
---|---|---|
構造 | JSONやBSON形式など、半構造化された文書をそのまま保存 | ノード(エンティティ)とエッジ(関係性)でデータをモデル化 |
用途・ユースケース | データ間の関係が薄い、自己完結した構造の情報(例:履歴書、注文書など) | ソーシャルネットワーク、推薦システム、組織階層構造など、関係性が複雑なデータに適する |
利点 | ・アプリケーションオブジェクトとデータベースの間に変換レイヤーが不要 ・関連情報を1つの文書としてまとめられるため、単一クエリで完結できる |
・JOINを多用せずに複雑な関連情報を高速に探索できる ・「友達の友達」などの再帰的関係を自然に表現できる |
3章 ストレージと抽出
1、ストレージエンジンの選択
ストレージエンジンとは、データベースの物理的な読み書きを担当する低レベルコンポーネントであり、アプリケーションのワークロード特性に応じた選定が重要である。
一から構築する必要はないが、選択を誤るとパフォーマンスに大きく影響する
ストレージエンジンには大きく分けて以下の2系統がある:
・トランザクション処理(OLTP)向け:頻繁な読み書きと更新に最適化
・分析処理(OLAP)向け:大量データのスキャンや集計処理に最適化
2、インデックス
インデックスは、データベースから特定の値を高速に検索するための補助構造。
主なインデックスの種類:
▪️Bツリー(Balanced Tree)
・最も一般的なインデックス構造
・多くのリレーショナルデータベース(MySQL、PostgreSQLなど)や一部NoSQLで採用
・データは階層構造のツリーに配置されており、ページ(固定サイズのブロック)単位で管理
・範囲検索や順序付きデータの処理に強い
▪️ハッシュインデックス
・キーに対してハッシュ関数を適用し、対応する位置にデータを格納
・等価検索(完全一致)には高速だが、範囲検索はできない
・主にインメモリ型のストアやKVS(キー・バリュー・ストア)で用いられる
▪️SSTable(Sorted String Table)
データがキーでソートされた状態でファイルに格納されている
・主に書き込み性能を重視したストレージエンジン(例:LSMツリー)で使用される
主な特徴:
データをまとめてファイルに書き込む(追記型)
古いデータは後でマージ処理によって統合(コンパクション)
▪️LSMツリー(Log-Structured Merge Tree)
SSTableを階層的に管理し、書き込み性能を最大化する構造
書き込みは一旦メモリ内(MemTable)に保存 → バッファが満杯になるとディスクへ追記
定期的に複数のSSTableをマージし、ストレージの整合性を維持
特徴:
書き込みが高速(追記型)
読み取り時は複数のSSTableをスキャンする必要があるため、読み取り性能はやや劣る
代表的な実装:LevelDB、RocksDB、Cassandra
▪️Bツリー vs LSMツリー(使い分けの観点)
特徴 | Bツリー | LSMツリー |
---|---|---|
読み取り性能 | 高速(ランダムアクセスに強い) | やや遅い(複数ファイルをスキャン) |
書き込み性能 | 中程度(更新のたびにツリーを調整) | 非常に高速(追記&バルクマージ) |
範囲検索 | 得意 | 可能だが最適化が必要 |
ユースケース | OLTP(更新が多くない) | 高頻度の書き込みや大量挿入に強い(ログ、分析系) |
3、OLTPとOLAPの違い
区分 | OLTP | OLAP |
---|---|---|
主な用途 | 一般ユーザーによる日常的な操作 | アナリストによるデータ分析 |
代表操作 | 商品購入、予約登録、口座振替など | 売上集計、顧客分析、傾向把握など |
データ構造 | 行指向ストレージ | 列指向ストレージ |
クエリ負荷 | 軽量・高頻度 | 重量・低頻度(集計が中心) |
更新頻度 | 高い(トランザクションごと) | 低い(読み取り中心) |
4、データウェアハウス
データウェアハウスは、OLTPシステムから切り離された分析用データベースであり、アドホックなクエリをOLTPの負荷に影響を与えることなく実行可能にする仕組み。
ETLの一般的な流れ:
1、データの抽出(Extract)
OLTPシステムからリードオンリーの形でデータをコピー
2、データの変換(Transform)
分析しやすいスキーマ(例:スタースキーマ)へ整形・クレンジング
3、データの格納(Load)
データウェアハウスにロードされ、OLAPで活用される
5、スタースキーマとスノーフレークスキーマ
スタースキーマ
データウェアハウスでよく使われる構造
中央にファクトテーブル(数値の記録)、周囲にディメンションテーブル(属性情報)を配置
ファクトテーブルにはトランザクションや測定値などが格納される
スノーフレークスキーマ
スタースキーマのディメンションテーブルをさらに正規化し、階層的なサブディメンションに分割した形
冗長性を削減できるが、クエリが複雑になることがある
列指向ストレージ(Columnar Storage)
多くのOLTPでは行指向ストレージが使われている:1行分のデータが連続して格納されており、トランザクション処理に適している。
一方、OLAPでは列指向ストレージが効果的:
各列ごとにデータをまとめて格納
典型的な分析クエリでは、数列しか使われないため不要な列の読み取りを省略できる
結果として、読み取りパフォーマンスが大幅に向上
特に効果的な場面:
数百万〜数億レコードから「特定の列だけ集計」するようなクエリ
▪️マテリアライズドビュー
頻繁に実行される分析クエリの結果を、あらかじめ計算し保存しておく仕組み
通常のビューは「クエリの別名」に過ぎず、参照時に毎回実行されるが、
マテリアライズドビューは実際のクエリ結果をディスクに書き込み、キャッシュとして機能する。
利点:
複雑なクエリの応答時間を短縮
日次や週次での更新を行えば、リアルタイム性とパフォーマンスのバランスが取れる
4章 エンコーディングと進化
1、データの2つの表現
アプリケーションはデータを常に2つの異なる形で扱う:
種類 | |
---|---|
インメモリ表現(メモリ内) | データはプログラムの構造体(リスト、配列、ハッシュ、オブジェクト)として表現され、操作される |
永続化・転送時の表現 | データをファイルに保存したりネットワークで送信する際は、自己完結したバイト列としてエンコードされる(例:JSON, Protobuf) |
▪️エンコーディング:インメモリデータ → バイト列(シリアライズ)
▪️デコーディング:バイト列 → インメモリ構造(デシリアライズ)
エンコーディングの重要性
異なるプロセスやコンピュータ間でデータをやりとりするためには、共通のフォーマットが必要。データベース・キュー・キャッシュ・API通信などあらゆる場所で、エンコード/デコードが発生する。
「フォーマットを選ぶことは、将来の変更の仕方を選ぶこと」にもなる。
代表的なエンコーディング形式の比較
フォーマット | 特徴 | 例 |
---|---|---|
JSON | 可読性が高く、動的型に強い。柔軟だがバイナリ非対応。 | Web API、ログ |
XML | タグベースで構造化。冗長性があるが標準化されている。 | 古いB2B連携、SOAP |
Protocol Buffers(Protobuf) | Google製のバイナリフォーマット。高速・軽量・スキーマあり。 | 高性能な内部RPC |
Avro | Hadoopエコシステム向け。スキーマと一体化しやすい。 | Kafka、Big Data処理 |
Thrift | Facebook発のRPC/シリアライズ。柔軟だが複雑。 | 内部サービス間通信 |
2、スキーマと進化性(Schema Evolution)
スキーマとは:
エンコード・デコードの両者が、どのようなデータ構造であるかを共有するための仕様
JSONのようにスキーマレスな形式もあるが、スキーマがある方が型安全で検証もしやすい
スキーマ進化とは:
アプリケーションを停止せずにデータ構造を変える(後方・前方互換性の維持)
特に長期間運用されるシステムや分散環境で重要
互換性の種類
互換性のタイプ | |
---|---|
後方互換性(Backward Compatibility) | 新しいコードが古いデータを正しく扱える(例:新しいフィールドを無視) |
前方互換性(Forward Compatibility) | 古いコードが新しいデータを扱える(例:知らないフィールドを無視) |
双方向互換性(Full Compatibility) | どちらのバージョンも互換性を保つ(理想だが困難) |
スキーマ進化の例(ProtobufやAvro)
フィールドの追加 → 新フィールドにデフォルト値を与えれば後方互換性が保てる
フィールドの削除 → 古いクライアントが知らないデータを扱うリスクあり
フィールドの型変更 → 型変換ルールやバージョン分離が必要
スキーマ管理の現実的な方法
バージョン管理(例:schema-v1、schema-v2などの識別子付き)
明示的なスキーマレジストリ(Kafka Schema Registryなど)
移行手順のドキュメント化と段階的リリース(canary deploy)
バイナリ vs テキスト形式のトレードオフ
観点 | テキスト形式(JSON, XML) | バイナリ形式(Protobuf, Avro) |
---|---|---|
人間の可読性 | 高い | 低い(ほぼ読めない) |
パース性能 | 遅い | 低い(ほぼ読めない) |
サイズ効率 | 大きい | 小さい |
スキーマ依存 | ゆるい(スキーマレスも可) | 厳密(スキーマが必要) |