データ指向アプリケーションデザイン
Last updated
Was this helpful?
Last updated
Was this helpful?
5.5 時間
デザインについては実はあまり読んだことがないかも。現場での実践経験をもとにデザインしている。
多くのアプリケーションは演算指向ではなく、データ指向
多くのアプリケーションで必要となる機能
データベース
キャッシュ
検索インデックス
ストリーム処理
バッチ処理
データシステムの原理と実用性、データ指向アプリケーションを構築するためにそれらを使う方法をたどる
信頼性とスケーラビリティを持ちながらメンテナンスしやすいデータシステムの基礎
ツールの分類はあいまいになっている
単一のツールでは要求を満たせないことが増えている
適切なツールを用いて効率よく処理する
アプリケーションを設計することはデータシステムを設計すること
ソフトウェアシステムにおける重要な3点
信頼性
スケーラビリティ
メンテナンス性
信頼性とは、何か問題が生じたとしても正しく動作し続けること
フォールトとは問題を起こしうるもの
フォールトに耐性があるものを耐障害性を持つ(フォールトトレラント)であるという
フォールトは仕様を満たしていないコンポーネント
障害はシステムが全体として必要なサービスのユーザーへの提供を止めてしまった場合
ハードウェア
ハードウェアに冗長性をもたせる
ハードウェアの冗長化に加えて、ソフトウェアによる耐障害性の手法を使うことによって信頼性を高める
ソフトウェア
システム内のシステマティックなエラーもフォールトの一つ
汎用的な解決策はない
ヒューマンエラー
人間には信頼性がない
複数のアプローチを組み合わせる
エラーの可能性が最小限になるようにシステムを設計する
ミスを犯しやすい部分を分離する
ユニットテストからマニュアルテストまでテストを徹底的に行う
容易にリカバリできるようにする
モニタリングの仕組みを用意する
優れた管理方法とトレーニングの実践
信頼性を犠牲にして開発コストを下げたりする場合があるが、信頼性を犠牲にしていることを強く意識するべき
負荷の増大に対してシステムが対応できる能力のこと
負荷は負荷パラメータによって可視化できる
負荷が増大したときに起こることの調査方法
負荷パラメータを増やしながら、システムのリソースを一定に保ったときにシステムのパフォーマンスにどのような影響があるか
負荷パラメータを増やしたときに、パフォーマンスを一定に保つためにはリソースをどの程度増やす必要があるか
システムのパフォーマンスの表現方法
バッチ処理の場合はスループット
オンラインシステムの場合はレスポンスタイム
典型的なレスポンスタイムを知りたい場合は算術平均ではなく、パーセンタイルによる値を用いること
パーセンタイルは、SLAやSLOに用いられることがある
負荷への対処
スケールアップ
スケールアウト
複数のマシンに負荷を分散させることあ、シェアードナッシングアーキテクチャと呼ばれる
エラスティック
負荷の増大を検知して、自動的にコンピューティングリソースを追加できるシステム
システムのアーキテクチャはアプリケーションに固有の度合いが強い
汎用的な技術を組み合わせる
メンテナンス性
運用性
優れた運用性は定型のタスクを容易にし、運用チームが勝ちの大きな活動に集中して力を注げるようにしてくれる
単純性
単純にするということは偶発的な複雑さを除くということ
抽象化によって、実装からのみくる偶発的な複雑さを除く
進化性
データシステムにおいてアジリティを高めるための方法
アプリケーション開発者はアプリケーションに適したストレージエンジンを選択できる必要がある
データの保存、取り出しに関するデータベースの内部動作に留意すべき
ログとは追記のみが行われるデータファイル
データベースから特定のキーの値を効率的に探索するためにはデータ構造=インデックスが必要
ナイーブにファイルに書く込む例の場合は探索のコストが O(N)
メタデータとはデータを探索するために追加で置いておくもの
インデックスはデータベースの内容には影響がなく、クエリのパフォーマンスに影響がある
書き込みの場合はインデックスへのオーバヘッドがあるので書き込み速度が低下する
トレードオフ
3.1.1 ハッシュインデックス
インメモリハッシュマップを用いたインデックスの例
ハッシュマップはすべてインメモリ上に存在する
(例にはキーのオフセットに対してREADする終端位置が書いていないが \n
ということと想定)
追記のみの場合、いずれディスク領域を使い切る
ログがあるサイズ以上になったらファイルをクローズして、ログをセグメントに分割する
ログのクローズ後は新しいセグメントファイルに書き込みを行う
それぞれのセグメントに対してマージとコンパクション処理を行う
ログ中で重複しているキーを捨てて、キーの最新の値のみを残す
マージ中も古いセグメントファイルを参照することでREADとマージを並行して行うことが可能
各セグメントごとにインメモリハッシュテーブルを持つ
キーの値を探索するときは、最新のセグメントから探索して、存在しなければ2番目に新しいセグメントを探索。これを繰り返す
現実的なエンジンにするための検討事項
ファイルフォーマット
シーケンシャルに生の文字列が続くバイナリフォーマット
レコードの削除
削除の用の特殊なレコードの追加
クラッシュのリカバリ
インメモリハッシュテーブルの再構築は、セグメントファイルをすべて読み直すことで可能
セグメントファイルが増大するにつれ時間がかかるので、ハッシュマップのスナップショットを保持することでリカバリを高速化する
部分的に書き込まれたレコード
チェックサム
並行性の制御
Writerは1プロセスのみ。Readerはイミュターブルなので複数のプロセスで並行して可能
追記のみの有効性
(ディスク上の)セグメントの追記とマージはシーケンシャルなWriteなので高速
並行処理とクラッシュリカバリがシンプルになる
TODO: クラッシュに関しては中途半端な状態にならない、という意味かな?
フラグメンテーションを避けることができる
制約
ハッシュテーブルがメモリ上に存在する必要があるため、キーの数が(メモリに収まらないくらい)大きくなると使えない
範囲に対するクエリの効率が良くない
3.1.2 SSTableとLSMツリー
3.1.2.1 SSTable
SSTable(Sorted String Table)
セグメントファイルのフォーマットに制約を追加する
(すべての)セグメントファイルは キーでソート済
各セグメントファイルは キーで一意
利点
セグメントのマージがシンプルで効率的にできるようになる
すべてのキーをメモリ上に保持する必要がない
あるキーの値を知りたいときは、その前後キーのオフセットが分かれば、区間内を走査すればよいため
範囲のキーをディスクにWriteする前に圧縮しておく
(TODO: 圧縮するタイミングはよくわからなかった。キーでソートされているのでRangeのクエリが得意なのはわかる)
SSTableの構築と管理方法
インメモリツリー(AV木や赤黒木)を用いる。memtableと呼ばれる
memtableのしきい値を超えた場合、memtableからセグメントファイルを生成。memtableはキーでソート済のためファイル生成は十分高速
Readはmemtable -> 最新のセグメントファイル -> ...
バックグラインドでマージとコンパクションを実施
制約
クラッシュ時にmemtableに書き込まれているが、まだセグメントファイルに書き込まれていないデータを失う
すべてのmemtableに書き込まれたデータと同じデータをディスク上のログとして別に書き込んでおく(未ソート)
3.1.2.2 LSMツリー
Log-Structureed Merge-Tree(LSMツリー)
TODO: SSTable(と同じような原理)をベースにしているデータ構造
全文検索エンジン(ElasticSearch, Solrなど)のApache Luceneも似たような原理
(転置インデックス)
3.1.2.3 パフォーマンスの最適化
LSMツリーの最適化
値がないことの探索
memtableから調べてすべてのセグメントファイルのハッシュテーブルを走査しないといけないため時間がかかる場合がある
bloom filterを用いると効率的
SSTableのコンパクションとマージ戦略
最も一般的なのは サイズごと, 階層(Level)ごと のコンパクション
3.1.3 Bツリー
1970年代に登場した、多くのリレーショナルデータベースにおいて標準的なインデックスの実装
非リレーショナルデータベースにおいても多く使われている
キーペアはソートされた状態で保持
log-structuredインデックスは、データベースを可変のセグメントに分割してセグメントにはシーケンシャルに書き込む
Bツリーはデータベースを固定サイズのブロックorページに分割
ブロックサイズは通常4KB
ハードウェア上のディスクも固定サイズのブロックを並べる
メモリではなくディスク上に存在する
あるページを根として参照しているページをたどることで葉の値を取得する
Bツリーへの操作
更新: 葉を探索して、直接そのページの値を更新する
追加: 範囲に合致する葉を見つけ、キーの値を追加する。追加するページが存在しない場合は上位のノードを分割
(削除: 書いていないけど、説明も大変)
Bツリーの操作とディスク
更新操作はディスク上のページの上書き
追加操作で複数のページの上書きが必要になる場合もある
対クラッシュ
WAL(write-aheadログ, redoログ)をもたせる
更新された内容をBツリーの反映させる前にWALログに書き込む
クラッシュ時はWALログを用いて整合性の取れた状態に戻す(ロールフォワード)
並行性
複数のスレッドから同時に更新がある場合は、ラッチ(軽量なロック)を取得することで保護
Bツリーの最適化
細かいテクニックなので割愛
3.1.4 BツリーとLSMツリーの比較
ワークロードによるが一般的にはLSMツリーはBツリーよりも書き込みが高速、読み込みが低速
ストレージエンジンのパフォーマンス計測における考慮事項
LSMツリーの長所
書き込み負荷が高いアプリケーションでは、データベースへのディスクの書き込みがボトルネックになることがある
LSMツリーは一般的にBツリーよりもスループットを高く保つことができる
書き込みの増幅度を抑えることができる場合がある
複数のページを更新する必要がない
シーケンシャルに書き込めば良い
圧縮率を高めることができる
LSMツリーの短所
コンパクション処理が実行中のパフォーマンスに影響する場合がある
コンパクションにより書き込みのスループットが高くなってしまう
データベース量が大きくなり、コンパクションが間に合わなくなる場合がある
セカンダリインデックス
キーはユニークでなくても良い
マッチする行のリストで保持
それぞれのキーに行の識別子を追加してキーをユニークとして扱う
3.1.5.1 インデックスへの値の保存
インデックスのキーの結果の値は実際の行か別の場所への参照
葉からヒープファイルを参照することでデータが複製されることを防ぐ
クラスタ化インデックス
データをインデックス内に保存
非クラスタ化インデックス
データへの参照のみをインデックス内に保存
ヒープ内のデータは非ソート
(TODO: 使い分けがよくわからない。というか非クラスタ化インデックス is 何?)
3.1.5.2 複合インデックス
連結インデックス
複数の列を扱うインデックス
多次元インデックス
複数の列を扱うより汎用的なインデックス
地理情報など(応用はいろいろある)
2次元の値を1次元に変換
特化したインデックスの生成(Rツリー)
3.1.5.3 全文検索と曖昧インデックス
全文検索エンジン
曖昧なキーの検索
編集距離
レーベンシュタインオートマトン
3.1.5.4 全データのメモリでの保持
ディスクの良さ
永続性
RAMよりもGBあたりの単価が低い
RAMも安くなっている
インメモリデータベース
メモリ内のデータ構造をディスクに書き込める形式にエンコードするオーバヘッドを削減できることがメリット
プライオリティキューなどの様々なデータモデルを扱うことができる
インメモリデータベースの拡張
アンチキャッシングアプローチ
メモリが不足しているときにディスクに逃がす
OSのメモリとスワップの関係に似ているが、ページ単位ではなく個々のレコード単位で扱うことができるので効率的
オンライントランザクション処理(OLTP)
インタラクティブなトランザクション処理
オンライン分析処理(OLAP)
データウェアハウス
OLTP処理はビジネスにとって重要で分析目的とは分離する
データウェアハウスを用いる
ETLを用いてOLTPのデータをデータウェアハウスに取り込む
データウェアハウスの利点は、分析のユースケースにそってデータベースを最適化できること
分析用途のスキーマ設計はあまり多様性はない
スタースキーマ
ファクトテーブル(Futureでいうとトランテーブル)とディメンションテーブル(マスタテーブル)を用いた分析
スノーフレークスキーマ
ディメンションを更に細かく分けたケース
OLTPデータベースは行指向
列指向ストレージ
列ごとの値をまとめて格納する
Parquetも列指向
圧縮
列に含まれる値は似た値が多いため、列指向ストレージは圧縮に適している
ビットマップエンコーディングやランレングス圧縮が効果的
メインメモリからのCPUキャッシュへの帯域の効率的な利用
CPUパイプラインの効率化
分岐ミスなどの削減
SIMD
ベクトル化処理による効率化
ソート順序
よく使うクエリに基づいて最適化
ソートすることでランレングス圧縮がより効果的になる(似た値が並ぶため)
同じデータをレプリケーションする際に異なる順序でソートしてくことで複数のクエリパターンに最適化することができる
Vertica
LSMツリーを用いてインメモリストアに書き込みをし、あるタイミングでファイルに書き出す
クエリはメモリとディスク上の列データを両方参照。オプティマイザが吸収
マテリアライズドビュー
仮想のビュー
元のテーブルへの更新でマテリアライズドビューを更新しないといけないため、OLTPには向いていない
読み取り専用のデータウェアハウス向け
オンライントランザクション処理と分析処理の最適化の違い
アクセスパターンの違い
OLTPのストレージエンジンの違い
log-structured
インプレース
log-structuredストレージエンジン
ディスク上のランダムアクセスの書き込みをシーケンシャルな書き込みに変換し、書き込みのスループットを高める
分析系のワークロードはOLTPとは異なる
列指向ストレージが有効
互換性
後方互換性
古いコードによって書かれたデータを新しいコードが読めること
前方互換性
新しいコードによって書かれたデータを古いコードが読めること
後方互換性を満たすのは難しくない(古いコードは既知だから)
前方互換性は新しいコードで追加された部分を無視する必要があるので難しい
データをエンコードする様々なフォーマット
スキーマの変更をどのように扱い、新旧のデータとコードが混在するシステムをどのようにサポートするか
フォーマットがデータストレージと通信でどのように使われているか
(TODO: スキーマってバージョン番号を含むメタデータのこと?)
データの表現
メモリ内での表現
バイト列の並び
メモリ内での表現と異なり、エンコードする必要がある
エンコーディング(シリアライゼーション、マーシャリング)
インメモリからバイト列への変換
デコーディング(デシリアライズ、アンマーシャリング)
バイト列からインメモリへの変換
インメモリオブジェクトをバイト列にエンコードする機能が組み込まれている
特定のプログラミング言語に固有
デコーディングする際のセキュリティの問題
前方互換性や後方互換性が保たれない
非効率
JSON, XML, CSVなど存在する
微妙な問題は存在する
合意形成が重要
JSONやXMLをバイナリフォーマットで扱うエンコーディングが開発されている
JSONであればMessagePack, ...
バイナリエンコーディングライブラリ
Apache Thrift
BinaryProtocol
CompactProtocol
Protocol Buffers
スキーマあり
スキーマの進化
フィールド
エンコードされたデータの意味にとってフィールドタグが極めて重要
新しいタグ番号を用いることによって古いコードは新しい番号を無視することできる(前方互換性)
フィールドがユニークなタグ番号を持っていれば、タグ番号の意味は変わることなく古いコードを読むことができる
制約としては、新しく追加するフィールドは必須にはできない(エラーになるため)
削除の場合は同じタグ番号を使わなければOK
データ型
切り捨てられる場合はあるが、互換性を保つことができる
バイナリのエンコーディングフォーマット
タグ番号がない
エンコーディングとデコーディングで全く同じスキーマを使う必要がある(?)
ライターのスキーマとリーダのスキーマは同じである必要はなく、互換性だけあれば良い
フィールドの順序が異なっていても良い
スキーマ解決の際にフィールド名でマッチさせるため
前方互換性
デフォルト値ももっているフィールドの追加や削除が可能
スキーマを知る方法
ファイルの先頭に含めておく
レコードの先頭にバージョンを含めておく
双方向プロトコルを用いる
Thrift、Protocol Buffers、Avroのバイナリエンコーディングがシンプルで使いやすかったため、広範囲のプログラミング言語でサポートされた
メリット
バイナリJSONよりもコンパクトになること
スキーマがドキュメンテーションとして価値がある
スキーマを管理することで互換性を保つことができる
スキーマからコードを生成することによってコンパイル時に型チェックができるようになる
データフローの形態
データベース経由
サービス呼び出し経由
非同期のメッセージパッシング経由
4.2.1 データベース経由
データベースの場合は前方互換性も後方互換性も求められることが多い
古いコードが新しく追加されたフィールドを更新してはいけない(nullになるので)
データをスキーマに合わせて書き直すことはできる(マイグレーション)が多くの場合は負担になるのでやらない
スキーマの進化が重要
4.2.2 サービス経由でのデータフロー:RESTとRPC
サービス指向/マイクロサービスアーキテクチャにおいて鍵となる設計目標は、それぞれのサービスを独立してデプロイして、進化させられるようにすることでアプリケーションの変更とメンテナンスを容易にすること
サーバーとクライアントが使用するデータのエンコーディングはサービスAPI間で互換性を保つ必要がある
WebサービスのアプローチにはRESTとSOAPがある
RESTはプロトコルではなく、HTTPの原理の上に構築された設計哲学
リソースの識別にURLを用いるシンプルなデータフォーマット
キャッシュの制御、認証、コンテントタイプのネゴシエーションにHTTPの機能を使う
SOAPはネットワークAPIのリクエストを発行するためのXMLベースのプロトコル
SOAPメッセージは手作業で構築するには複雑
RPC
リモートにあるネットワークサービスへのリクエストの発行を、同一プロセス内でのプログラミング言語における関数やメソッドの呼び出しと同じように見せる
リモート呼び出しとローカルの呼び出しは異なる
ネットワークのリクエストの予測は不可能。リトライなどは必要
タイムアウトによって結果を返さずに制御が戻ることがある
冪等性
ネットワークリクエストはレイテンシが大きくなるし、ばらつきが出る
パラメータはバイト列に変換する必要がある
クライアントとサーバー間の言語は異なる可能性があるため、データ型を変換する必要があるかもしれない
4.2.3 メッセージパッシングによるデータフロー
非同期メッセージパッシングシステム
バッファとしての機能
メッセージの再配信
送信側と受信側が素結合になる
あるメッセージを複数のプロセスで受信できる
メッセージブローカーは特定のデータモデルを強制しないため、任意のエンコーディングフォーマットを利用できる
前方互換性、後方互換性が保ちやすい
複数のマシンにデータベースを分散させたい理由
スケーラビリティ
耐障害性/高可用性
レイテンシ
II.1.1 シェアードナッシングアーキテクチャ
水平スケーリング
ノード間の調整は、通常のネットワークを通じてソフトウェアで制御
データを分散させたときに生じる問題
複数のノードにデータを分散させる方法
レプリケーション
同じデータのコピーを複数のノードに保持する方法
冗長性を提供
パーティショニング
パーティションというサブセットに分割する方法