ひらめの日常

日常のメモをつらつらと

『データ指向アプリケーションデザイン』を読んだ ー 第II部 分散データ 5~6章

こちらの続きです hiramekun.hatenablog.com

本はこちら

5章 レプリケーション

レプリケーションとは、ネットワークで接続された複数のマシンに同じデータのコピーを保存しておくことを指す。レプリケーションを行う理由は以下の通り。

  • レイテンシを下げる:データを物理的にユーザーの近くで保持するため
  • 可用性を高める:一部に障害があってもシステムが動作し続けるようにするため
  • スループットを高める:読み取りのクエリを処理するマシン数をエスケースアウトせせるため

この章では、パーティショニングは考えずにレプリケーションについてまとめる

リーダーとフォロワー

データベースのコピーを保存する各ノードはレプリカと呼ばれる。全ての書き込みが、全てのレプリカに行き渡るようにするには、リーダーベースレプリケーションがよく使われる(別名 アクティブ/パッシブ、マスター・スレーブレプリケーションMySQL ではマスター・スレーブと呼ばれることが多い。MySQL :: MySQL 5.6 リファレンスマニュアル :: 17 レプリケーション)。動作原理は以下の通り。

  1. レプリカのうち、一つはリーダーに指定される。データベースに書き込む場合、クライアントはリクエストをリーダーに送信する。
  2. 他のレプリカはフォロワーと呼ばれる。リーダーはデータを自身のストレージに書き込んだ後、その変更をレプリケーションログもしくは変更ストリームの一部として全フォロワーに送信する
  3. 各フォロワーはこのログをリーダーから受け取った後、その内容に従ってリーダー上で処理されたのと同じ順序で全ての書き込みを自身のストレージに行う。
  4. クライアントがデータベースから読み取りを行う場合には、リーダー・フォロワー関係なく全てのレプリカにリクエストを送ることができる。

同期的か非同期的か

レプリケーションを行うシステムで重要なのは、レプリケーションが同期的に行われるのか、非同期的に行われるのかどうか。もし非同期である場合、書き込みが即時反映されないレプリカが存在する事になる。その一方で、同期型レプリケーションにも利点と欠点が存在する。

  • 利点:フォロワーが持っているデータが最新であり、リーダーとの一貫性が保証される。
  • 欠点:同期型のフォロワーが反応を返さなかった場合、リーダーは全ての書き込みをブロックするため書き込み処理ができなくなってしまう。

よって可用性を考えるのであれば、全てのフォロワーを同期型にするのは現実的ではない。実際に同期型レプリケーションを有効にするのであれば、複数のフォロワーのうち一つを同期型にして、残りは非同期型にするのが良い。もし同期型のフォロワーが利用できなくなった場合は、非同期型のフォロワーを一つ同期型に変更すれば良い。

また、全てのフォロワーを非同期にすることも頻繁にある。

  • 利点:全てのフォロワーに遅延が生じていても、リーダーが書き込み処理を継続できる。
    • 特にフォロワーが多い場合や、フォロワーが地理的に分散している場合は大きな利点となる。
  • 欠点:リーダーに障害が発生した場合、フォロワーにレプリケーションされていない書き込みは全て失われる。

障害への対処

  • フォロワーの障害:キャッチアップリカバリ

    • フォロワーは障害発生前に最後に処理したトランザクションをログから判断できるので、フォロワーはリーダーに接続し、接続が切れた後に生じた全てのデータ変更を要求できる
  • リーダーの障害:フェイルオーバー

    • フェイルオーバー時の手順は複雑で、簡潔に以下の手順を踏む

      • フォロワーのいずれかをリーダーに昇格させる
      • クライアントは新しい書き込み先を新しいリーダーに設定し直す
      • 他のフォロワーはデータ変更を新しいリーダーから受信する
    • フェイルオーバーが発生した場合、問題となることは多く存在する

      • 非同期のレプリケーションを使用する場合、新しいリーダーは以前のリーダーから全ての書き込みを受信していない可能性がある。その場合、新しいリーダーは受信してない書き込みを破棄することが多い

      • 受信していない書き込みを破棄する場合、障害の発生要因となるので注意が必要。

        同期が遅れていたMySQLのフォロワーがリーダーに昇格。その結果 auto increment によりリーダーで割り当て済みのprimary keyを発行。Redisにもprimary key は保存されていたので、MySQLとRedisでデータ不整合が発生 GitHub availability this week | The GitHub Blog

    • 2つのノードが共に自身をリーダーだと信じてしまうスプリットブレインが起こりうる

レプリケーションログの実装

レプリケーションラグにまつわる問題

書き込むが少なく、読み取りのクエリが多いアプリケーションでは、多くのフォロワーを作り、リクエストを分散させる選択肢がよく取られる。その際、レプリケーションは非同期的に行う必要がある。同期的にレプリケーションする場合、フォロワーの一つがダウンするとシステム全体が書き込み不能になってしまうため。

非同期的にレプリケーションをする場合、リーダーで書き込まれた内容がフォロワーに反映されるまでラグがある。最終的にリーダーとフォロワーが同じ状態になることを、結果整合性 と呼ぶ。また、リーダーとフォロワーの間に異なる状態が存在する時間差を レプリケーションラグ と呼ぶ。

レプリーケーションラグによって、いくつか問題が生じる

  • クライアントが自分で書き込んだ内容をフォロワーから読み取る際に、内容が反映されていない可能性がある。
    • 書き込んだ内容を読み込んだ際にその内容が反映されている一貫性のことを、read-after-write一貫性 と呼ぶ
    • 解決策としては、(1) ユーザーが変更した可能性のあるものを読み取る場合はリーダーから読み取りを行う (2) 最後の更新からある一定の時間内に行う読取は、リーダーから行う などがある
  • 同じクライアントが異なるフォロワーにアクセスし、古い状態のデータが見えることがある。
    • モノトニックな読み取り では、これが生じないことを保証する
    • 連続で同じクライアントがデータを読み取る場合に、古い状態のデータが見えないことを保証する
    • 各クライアントが常に読み取りを同じレプリカから行うようにすれば、モノトニックな読み取りを実現できる
  • 一貫性のあるプレフィックス読み取り を行い、書き込みの順序通りに読み取りが行われるようにする

6章 パーティショニング

レプリケーションでは同じデータを複数のノードで保持することだった。パーティションはそれとは異なり、データを分割して配置することを表す。これはシャーディングとも呼ばれる。

パーティショニングする主な理由は、スケーラビリティ。大規模なデータセットを数多くのディスクに分散して配置できるため、クエリの負荷を分散させることができる。

パーティショニングしたデータをレプリケーションして複数のノードに保存することが多い。

キー・バリューデータのパーティショニング

パーティショニングの目的は、データとクエリの負荷をノードで均等に分散させること。パーティショニングが均等になっておらず、一部のパーティションが他と比べて多くのデータやクエリを担当している状態を スキュー と呼ぶ。また、この状態で高負荷が集中しているパーティションホットスポット と呼ぶ。

キー・バリューデータに対して、データとクエリの負荷をノードで均等に分散させるための手法はいくつか存在している。

  • キーの範囲に基づくパーティショニング
  • キーのハッシュに基づくパーティショニング
    • 多くの分散データストアではこちらを採用
    • ハッシュ関数が決まれば、それぞれのパーティションハッシュ値の範囲を割り当てて分割することで、パーティション間で均等にキーを分散することができる
    • ハッシュ値を使ってパーティショニングすることで、キーのソート順序は失われてしまうため、検索時の効率性は下がるのが欠点

パーティショニングとセカンダリインデックス

レコードを特定するためではなく効率的な検索処理に用いられるセカンダリインデックスを持つ場合は、パーティショニングではユニークなキーによって分割されるため、追加でセカンダリインデックスを利用するための仕組みが必要になる。

  • ドキュメントによるセカンダリインデックスでのパーティショニング
  • 語によるセカンダリインデックスでのパーティショニング
    • 全てのパーティションをカバーするグローバルインデックスを構築する方法。
    • セカンダリインデックスの値に応じてパーティショニングすることで実現する。
    • 検索時にはドキュメントによるセカンダリインデックスよりも効率的。その一方で、書き込み時には多くのパーティションに書き込むこともり、パフォーマンス低下の可能性がある。

パーティションのリバランシング

負荷をクラスタ内のあるノードから別のノードへと移行するプロセスを リバランシング と呼ぶ。

キーをノードで割った剰余でパーティションを行なっている場合、ノード数が変化するとほとんどのキーはノード間を移動する必要があり、必要以上にデータを移動させる必要がある。そのため、単に剰余を用いるのはパーティショニング時に得策ではない。

ノード数よりも多くのパーティションを用意し、一つのノードに複数のパーティションを割り当てる方式であれば、負荷に応じてパーティション内のデータは変えずにパーティションをノード間で移動することでリバランシングが可能になる。この方式では、パーティション数は最初から大きめの値を固定で設定する。

一方で、動的にパーティショニングを行う方式もある。データ量に応じて動的にパーティションする場合は、データの量が増えるとパーティションを分割し、データの量が減った場合はパーディションを結合する。パーティション数をノード数に比例させる方法もある。これは、ノードあたりのパーティションを固定するというものになる。

自動のリバランスと手動のリバランス

リバランシングはリクエストを再ルーティンし、大量のデータをノード間で移動させる負荷の大きい処理。障害検知も自動で行なっている場合、自動でリバランシングさせると、データを移動する際に一時的にノードが過負荷に陥り、クラスタの再配置が実行される可能性がある。これが行われると、さらにパフォーマンスが悪化する。なので、リバランシングにはどこかで人を介在させると良い。

リクエストのルーティング

データをパーティショニングできたとして、クライアントはどのようにして自身のデータが属するノードと通信するかが問題となる。これには大きく分けて3つのアプローチがある

  • クライアントが任意のノードに接続する。その後、ノードが適切なノードにリクエストを転送する
  • クライアントからのリクエストは全てルーティング層に送られ、ルーティングそうが適切なノードにリクエストを転送する
  • クライアントがパーティショニングと、ノードの割り当てを認識し、直接適切なノードにリクエストを送信する

続き

hiramekun.hatenablog.com