データ指向アプリケーションデザインのまとめ

www.oreilly.co.jp

昨年夏から続けてきたデータ指向アプリケーションデザインの輪読がほぼ終わったので、書かれていた内容を咀嚼するために理解した内容をアウトプットすることにしました。

それぞれの技術レイヤで一般的な選択肢と選択基準を文章化して、後から見返したときにデータシステムを設計の参考になるように基準となる軸が明確になるよう意識しています。

詳しい方から見ると私の理解が甘く間違いを書いてしまっているところもあるかもしれません。見つけられたらご指摘下さい。

整理した観点

単一のシステム

データモデルの選択

選択肢

選択基準

スキーマに柔軟性を持たせるかとアプリケーションにデータのマージが必要かどうかで判断する。

パフォーマンスを考えると、スキーマを変更することがなければスキーマオンライト(RDB)、スキーマに柔軟性を求めるならスキーマオンリードのデータモデル。

スキーマオンリードを選択したときの観点。ドキュメントデータベースは結合のサポートが弱い。グラフデータベースはドキュメントベースに比べてデータの持ち方が複雑。単一のデータだけを扱えればいいならパフォーマンスが高いドキュメントデータベースを選び, 関係性の抽出を行うパフォーマンスを重視するならグラフデータベースを選ぶ。

ストレージエンジンの選択

選択肢

  • データの持ち方
    • 行指向
    • 列指向
  • インデックスの有無
    • インデックス無し
    • インデックス有り
      • インデックスはメモリ上に保持
        • ハッシュインデックス
      • インデックスをファイルに保持可能
        • SSTable
        • LSMツリー(SSTableにlog-structuredを取り入れたもの)
        • Bツリー

選択基準

データを扱うアプリケーションのワークロードがトランザクション処理(OLTP)なのか分析(OLAP)なのかで選択する。

OLTPは全てのデータの中から一部を取り出して扱うのでランダムアクセスに強くなるようにインデックスが有る方がよい。データの持ち方は行指向。

OLAPは一度に大量のデータをスキャンするのでデータのアクセスができるだけシーケンシャルになるように列指向でデータを保持するのがよい。インデックスの有無は必要に応じて決める。

インデックスを有りにするとデータの読み込みとクエリ実行のパフォーマンスが上がるがデータ追記のパフォーマンスが下がる。

インデックスを有りにすると決めたのならほとんどの場合でBツリーを選べば良い。Bツリーはコンタントにパフォーマンスが高くなる。LSMツリーを採用するのはコンパクション中にパフォーマンスが落ちることを許容できてそれ以外のときにBツリーよりも高いスループットを求めるとき。

ただし、OLAPで列指向のデータの持ち方をして、かつインデックスを有りにするならLSMツリーを採用する。Bツリーは使えない。

データシステム間の通信

選択肢

  • エンコード(デコード)方式
    • 言語バインド有り
      • バイナリフォーマット
        • 言語固有のフォーマット
    • 言語バインド無し
  • データフローの形式
    • データベース経由
    • サービス呼び出し経由
      • REST
      • RPC
    • 非同期のメッセージパッシング経由

選択基準

テキストフォーマットはバイナリフォーマットに比べてデータサイズが大きくなる。ただし、人間が読みやすいメリットがある。

バイナリフォーマットにスキーマが有ると、そのデータを扱うアプリケーションの前方互換性と後方互換性の保証が可能になる。ただし、それらを保証できるかはフィールドに必須フィールドをいれるのか、フィールドに初期値を設定するかといった設計による。

ThriftとProctocol Buffersに比べてAvroはデータサイズが小さい。その代わり、Avroはスキーマ中のフィールドの順番がライターとリーダーで一致しない。フィールドの値の扱いはリーダーが決める。

エンコード方式で言語固有のフォーマットを選択することはまずない。

  

データフローの形式を考える。通信相手に送信したあとレスポンスを返して欲しいときはREST。レスポンスが不要な場合は非同期のメッセージパッシング経由。後者は送信相手が一時的に応答負荷になっていたとしても正常に戻った後にメッセージを受け取ってもらえることが期待できる。ただし、メッセージが届くことが保証されているわけではない。(exactry-onceのメッセージブローカがれば保証される?)

データベース経由とRPCについては力不足で選択基準を整理できていなかった。データベースの扱いを除外するのはローカルのデータベースなのかリモートのデータベースなのかで扱いが変わるため。リモートのデータベースの場合は分散システムの話が関わってくる。レプリケーションやパーティショニングの有無やトランザクションの種類, 求める一貫性の強さ, 必要なパフォーマンスでデータベース経由を採用できるか決まる。RPCを除外するのは完全に自分の力不足。*1

高負荷への対応

水平スケールを選択肢にいれており続く大規模環境とも範囲が重なるが、垂直スケールとの比較のために小規模環境の節にいれることにした.

選択肢

  • 垂直スケール
  • 水平スケール

選択基準

コストをかけることができて現実に構築できるマシンスペックでデータシステムを運用できるなら垂直スケール。コストを抑える代わりに分散システムの様々な問題に対処していくことができるなら水平スケール。

一区切り

「高負荷への対応」で水平スケールを採用するならば 単一システムで扱った範囲に加えてこのあとに続く分散システムについても設計を行う.

分散システム

パーティショニング

選択肢

  • パーティショニング無し
  • パーティショニング有り
    • パーティショニング方式(キーバリュー型のデータの場合のみを考える)
      • レンジパーティショニング
      • ハッシュパーティショニング
    • セカンダリインデックス
    • リバランスの戦略

選択基準

分散システムでデータやクエリの負荷をある程度均等に分配したいときにパーティショニング有りを選ぶ。

ここではデータがキーバリュー型であることを想定する。

パーティショニングの方式はレンジパーティショニングとハッシュパーティショニングが基本になる。どちらを選ぶかはデータの偏りによる。パーティションに含まれるデータができるだけ均等になるように選ぶ。

セカンダリインデックスが必要になるのはプライマリーキーに加えて別の条件でも検索したくなるとき。セカンダリインデックスを採用すると読み取りのパフォーマンスが上がる一方、セカンダリインデックスの更新が必要になるので書き込みのパフォーマンスは下がる。

セカンダリインデックスはローカルインデックスとグローバルインデックスの二種類。全体に対する検索ではグローバルインデックスがある方が効率的だが、データを追加するときに発生するセカンリインデックスの更新処理はローカルインデックスと比べて重い。一方で、絞り込みの検索ならローカルインデックスの方が効率が良い。グローバルインデックスが必要になる代表例は全文検索システム。

リバランスの戦略はデータ量の変動とノード数の変動が発生したときにパーティションを均一に保つかどうかで決める。均一に保つことを諦めるかわりにシンプルに運用できるのがパーティション数固定。均一に保つことを目指すならデータ量に合わせてパーティション数を増やす。一方、データ量が増えるときにパーティションごとのデータ量を均等にするだけではなく、ノードあたりのデータ量も均等にすることを目指すならノード数による変動を選ぶ。

レプリケーション

選択肢

選択基準

分散システムで耐障害性を持たせない選択肢はないので実質レプリケーション無しは選択肢に入らない.

レプリケーションアーキテクチャはデータの一貫性と可用性のバランスで決める。書き込みの衝突を考慮せずに済むようにしたいならシングルリーダーを採用する。ただし、ノードやネットワーク障害、レイテンシのスパイクに弱い。マルチデータセンタを考えるならシングルリーダだと耐障害性に不安があったりレイテンシの問題が出てくるのでマルチリーダ。どのレプリケーションアーキテクチャあっても読み取りの整合性の考慮は必要。

リーダレスが選択肢に入る基準は整理できていない。単一データセンタに閉じていてノード障害に強くしたい場合?

レプリケーション方式は単純だが決定性に欠けるステートメントベース、決定性はあるがストレージフォーマットのバージョンが必ずリーダーとフォロワーで一致していないといけないため運用が難しいwrite-aheadログの転送、ストレージフォーマットのバージョンに後方互換性を持たせることができる論理(行ベース)ログ、バグを仕込んでしまう可能性が他より高いが自由度が高いトリガベースから選ぶ。

データ処理のエンジン

選択肢

選択基準

有限のデータを処理した結果が欲しいならバッチ処理。データの量が不定でその都度入ってくるデータを処理して結果が欲しいならストリーム処理。

一区切り

レプリケーションの一貫性とトランザクションの一貫性の覚書を書いておく。アプリケーションの要件から具体的なデータベースを選ぶときの参考としての覚書。

レプリケーションによって発生する読み取り(R)と書き込み(W)の問題

ここで扱う問題は全てレプリケーション間のコピーが非同期、あるいはリーダーと1つのフォロワーは同期的にコピーされるが残りのフォロワーとは非同期でコピーされる準同期であることに起因したもの。クライアントが一つしかなくリクエストに並行性がなかったとしてもレプリケーションの方法やタイミングによって整合性が取れなくなることがある。

リーダーと全てのフォロワーでデータのレプリケーションが同期的に行われることはパフォーマンスを考えると採用されない。

リーダーのアーキテクチャ 一貫性の性質 防げるトラブル
シングルリーダー read-after-write一貫性 「自分が」書いた内容をすぐにまた読み込むと値が空っぽ、あるいは上書きする前の結果が返ってくる
モノトニックな読み取り レプリケーションのコピーを実行中に複数のクライアントから読み込み処理があったとき、発生順序が先だったクライアントの読み込みがコピー後の正しい結果で、発生順序が後だったクライアントの読み込みがコピー前のものになる
一貫性のあるプレフィックス読み取り 因果関係がある書き込みがあったとき、その書き込み結果を読み込むと因果関係が無視した順序で読み込んでしまう(e.g. 質問とそれに対する答えの会話の書き込みがあったのに、答えの文言を先に読んで後から質問の文言を読んでしまう)
マルチリーダー シングルリーダーであったこと全部 シングルリーダーであったこと全部
LWWなど 書き込みの衝突
リーダーレス クオラム(条件付き) 読み取り内容が最新である保証がない
読み取り修復 レプリカ間のデータが等しい保証がない
エントロピー処理 レプリカ間のデータが等しい保証がない
LWWなど リーダーレスには順序制御がない
バージョンベクトル 複数クライアントが書き込んでいるとき、上書きなのか並行書き込みなのかを判断できない

トランザクションの一貫性と分離レベル

この話は分散システムもレプリケーションも前提ではない。*2

クライアントが複数あって、それぞれが単一オブジェクト、あるいは複数オブジェクトを操作するときの一貫性の強さ。

  • 直列化可能性は最も強い一貫性。並行して実行された複数のトランザクションがあったとしても、順番に行儀良く実行されたときと同じ結果になる。悲観的ロックである2PLで実現する。
  • 直列化可能なスナップショット分離。楽観的ロックで実現する

ただし、2PLがパフォーマンスを悪化させるため現実的ではない。現実的には分離性を確立して並行したトランザクションを受け入れる。*3

分離性には以下のような例がある。

対応策 防げるトラブル
スナップショット分離(SQL標準で定義される概念で一番違いのはリピータブルリード) 読み取りスキュー
アトミックな書き込み 更新のロスト
ロック*4 更新のロスト
compare-and-set*5 更新のロスト
衝突の実体化(ロック) 書き込みのスキュー*6
read-committed ダーティリードとダーティライト

メモ

  • ハッシュインデックスとBツリーの比較は?
  • もしも大量の画像を扱うならオブジェクトストレージを選択することになると思うが、まとめた内容で関連するのは耐障害性/高可用性に関する部分のみ?
  • レプリケーション方式は 論理(行ベース)ログを選んでおけば良いように見えるがデメリットは?
  • レプリケーションの有無とトランザクションに関係はない。
  • マルチリーダーとリーダーレスを選択する境界は?
  • ダーティライトの回避策の一般解は行レベルのロックと紹介されていた。分散DBでロックを取る方法は合意の利用なので、read-committedを実現するには合意が必要?
  • 線形化可能と直列化可能に関連があるのはなんとなく分かったが、関係性を言語化するのが難しい。今言語化できるのは下記が精一杯。

*1:RPC恐い

*2:こう書いたが正直自身がない

*3:Twitterで@tzkbさんからこのスレッドを紹介されてトランザクションの一貫性と分離レベルは別の概念だということを理解した

*4:レプリケーションがあると使えない

*5:レプリケーションがあると使えない

*6:ファントムによって引き起こされる