Postgres-XC gtmの多重化(改訂2版:2011.01.01)

Postgres-XCの単一障害点であるgtmを多重化するmgtmを作ったので公表する。
2010年12月現在、最新版はver0.9.3だが、mgtmはver0.9.2で動作する。

なお、機能には大きな制約がある:

  1. 基本的にver0.9.2でpgbenchを実行することを目標とし、シーケンスなどはサポートしていない。
  2. coordinatorやdatanodeには"autovacuum=off"を設定しなければならない。

(2016.2.11) 多重化gtmが動くVagrant boxを作成して公開: [Vagrant box| GitHub] したので、興味があればどうぞ。

多重化の分類

便宜上、以下のように分類する。

  • プロセス多重化: メッセージの相互通信
    • Active Replication(能動的多重化): Multi-Master
    • 仮実装のみ行った

    • Passive Replication(受動的多重化): Master-Slave
    • N重化、2重化の実装を行った

  • データ多重化:メモリ上のデータを共有
  • 今回は見送り。

概念

概念図を以下に示す。

gtmの多重化の概念図

GTM間通信

一般的な方法

プロセス間で同期メッセージの交換によるデータの一貫性を確保する場合の、一般的な通信方法は以下のとおりである。

  • Active Replication(能動的多重化)
  • Total Order Broadcast(Atomic Broadcastともいう)

  • Passive Replication(受動的多重化)
  • View Synchronous Communicationなど

これらのプロトコルは一般的に重い。

手元ですぐに使えるこれらプロトコルをサポートするライブラリはコンセンサス問題を複数回(3、4回)解かなければならず、処理が非常に重い。
具体的に書くと、コンセンサス問題は一回の実行でメッセージ交換(ラウンドという)を3回程度繰りかえす。 それを複数回(3、4回)解くということはGTM間で9回から12回のメッセージ交換(9〜12ラウンド)を行って、 やっと1つのメッセージがGTM間で共有されるということである。

もちろん、コンセンサス問題を(明示的には)利用しないTotal Order Broadcastプロトコルも存在するが、 3ラウンドのメッセージ交換が必要になる。

今回試した方法

Passive Replication(受動的多重化)に限るが、以下に示すプロトコルを使った。

  • N重化: Causal Order Broadcast (あるGTMが送信したメッセージについて因果関係を保持する通信)
  • 原理的にはCausal Orderよりも制約の緩いFIFO Orderで大丈夫だが手元のライブラリはCausal Orderしかなかったのでこちらを使った。

  • 2重化: Lazy Reliable Broadcast
  • こちらもFIFO Orderでよいのだが、よりパフォーマンスのよいこちらを使った。

これらのプロトコルは、2回程度のメッセージ交換(2ラウンド程度)で、1つのメッセージがGTM間で共有できる。

軽いプロトコルを使った代わりに、mgtmとmgtm_proxyで障害リカバリ機構を組み込んだ。 リカバリ機構とは以下のようなものである:

  • GTM側:
  • 正常なGTM間でだけ"同一状態を保てば良い"
  • Proxy側:
  • GTMに届かなかったメッセージを再送する機構を持つ

今回、Active Replication(能動的多重化)については、効率のよいTotal Order Broadcastを実装/用意できなかった。理由は次の項に示す。

実装

実装はJavaでフルスクラッチした。Java版のgtmとgtm_proxyで公開したものを コアにしている。

また、 GTM間の通信ライブラリは Introduction to Reliable and Secure Distributed Programmingの教育用ライブラリ:

を使った。教育用なのでメモリリークやメッセージロストがある。実用的なものでなく、あくまで検証用のライブラリである。

JGroupを使わなかったのは、(1)所詮試作なので性能を求めなかったこと、 (2)問題がある場合にAppiaのほうがソースコードの総量が少ないので対処可能と考えた、(3)マニュアルにTotal Order Broadcastの記述がないこと、(4)(まったく別の理由から)フルスクラッチを目指していたから。しかし、実際に実装してみるとappiaの(Total Order Broadcastの)数々の不具合に泣かされたので戦略ミスだったかもしれない。

デモンストレーション

Passive Replicationのデモンストレーション画像(29M)を用意した。

画像内でのgtm, coordinator1, coordinator2の配置

  1. 0:00 〜 0:18 : mgtm起動
  2. mgtm-1, mgtm-2, mgtm-3を順次起動

  3. 0:19 〜 0:28 : リーダ選出
  4. mgtm間でリーダ選出、mgtm-1がリーダ。mgtm-1の画面に"I'm Leader"と表示されている。

  5. 0:29 〜 0:47 : mgtm_proxy起動
  6. 0:48 〜 0:57 : coordnatorとdatanode起動
  7. 0:58 〜 1:13 : pgbench設定
  8. データベースpgbenchを作成し、"pgbench -i"を実行

  9. 1:14 〜 2:23 : coordinatorデータ共有
  10. pgxc_ddlでcoordinator間のデータを共有

  11. 2:24 〜 3:10 : pgbenchの実行
  12. coordinator1,coordinator2でpgbenchを実行

  13. 3:11 〜 3:25 :mgtm-1をダウン
  14. mgtm-1をダウンさせてみる。
    mgtm_proxyはmgtm-1のダウンを検出し、2秒待つ。
    その間にリーダ選出機構が起動し、mgtm-2が新しいリーダになる。
    次いで、mgtm_proxyはmgtm-2に接続し、障害リカバリを行った後に処理を継続する。

    最終的にpgbenchは破綻なく結果を返す。

  15. 3:26 〜 3:55 : 動作確認
  16. リーダがmgtm-2になって動作確認の意味でpgbenchを実行。問題無く動作している。

  17. 3:56 〜 4:20 : mgtm-2をダウン
  18. mgtm-2をダウンさせてみる。以降も問題無くmgtm-3にリーダが切り替わり、pgbenchは破綻なく動作を続ける。



ベンチマーク

QuadCoreマシン上にXenで3台の仮想サーバを用意し、pgbenchを実行した。比較のためにオリジナルのgtm、およびJava版のgtmも計測した。
結果の解釈であるが、HDDを全DBが共有しているなど、ベンチマーク結果そのものにはあまり意味がなく、mgtmやオリジナルとの比較でのみ意味がある数値である。

オリジナルgtmとJava版のgtmはまだまだ余裕がある(pgbenchでgtmに負荷をかけきれていない。DB側がボトルネックになっている可能性が高い)。

多重化版mgtmはオリジナルgtmの1/3以下の性能であり、mgtmがボトルネックになっていることは確実である。
ただし、試作の段階でこの性能なので許容範囲ではないかと考えている。

なお、Active Replication版はappiaライブラリが異常に遅く、計測不可能であった。代わりに測定環境を変えて Total Order Broadcastを使った場合のデモ画像を用意した。Etherealでパケットをキャプチャした様子も含まれている。appiaライブラリが教育用である旨よく判ると思う。







オプション 接続コスト mgtm (3重化) dgtm (2重化) jgtm (Java版) gtm (オリジナル)
Min – Max Min – Max Min – Max Min – Max
[-c 4 -T 10]x 1 含む 53.34 – 78.61 78.52 – 94.99 136.54 – 236.03 147.28 – 188.12
含まない 56.36 – 82.84 82.45 – 99.75 137.34 – 237.4 183.78 – 235.35
[-c 4 -T 10]x 2 含む 20.9 – 28.2 12.73 – 15.29 57.23 – 79.41 55.37 – 114.62
含まない 22.29 – 29.92 13.4 – 16.13 57.65 – 79.91 69.24 – 142.71
[-c 8 -T 10] x 2 含む 18.68 – 44.65 11.54 – 26.66 79.4 – 110.16 55.14 – 88.43
含まない 19.92 – 47.17 12.22 – 28.13 79.6 – 111.37 91.6 – 126.45

mgtm(多重化版)とjgtm(Java版)のプロファイルを示す。

mgtmは内部通信のための処理(PipedStream, WaitNotifyなど)が多くの時間を占めている。
jgtmのほとんどが入力待ち(SocketInputSteam.read())に費されていることと対照的である(この数値のみ大きいのが"jgtmには余裕がある"との根拠である)。

内部メッセージ処理の高速化など施す余裕があると思われるので、少なくとも2倍程度の性能向上は見込めると考えている。さらにパフォーマンスを高めるための方策も多々あると思う。

mgtm(多重化版)のプロファイル
CPU TIME (ms) BEGIN (total = 2088312)
rank   self  accum   count trace method
   1 20.65% 20.65%     102 310332 java.net.SocketInputStream.read
   2 16.72% 37.37%      71 310215 java.net.PlainSocketImpl.accept
   3  8.15% 45.51%     561 310755 java.net.SocketInputStream.read
   4  7.95% 53.46%    1118 311286 java.io.PipedInputStream.read
   5  7.91% 61.38%      10 304520 java.lang.Object.wait
   6  7.91% 69.28%    3142 304530 java.lang.ref.ReferenceQueue.remove
   7  7.35% 76.63%     506 306181 java.lang.Object.wait
   8  7.31% 83.94%    1118 310836 java.io.PipedInputStream.read
   9  7.21% 91.16%       5 306839 appia.TimerManager.goToSleep
  10  3.47% 94.63%       2 310692 java.net.PlainSocketImpl.accept
  11  3.08% 97.71%       1 310567 jp.interdb.jgtm.utils.WaitNotify.Wait
  12  0.76% 98.47%    1108 311434 java.io.PipedInputStream.read
  13  0.66% 99.13%     559 311284 jp.interdb.jgtm.utils.WaitNotify.Wait
  14  0.04% 99.17%     559 311281 java.io.PipedOutputStream.flush
jgtm(Java版)のプロファイル
CPU TIME (ms) BEGIN (total = 129099)
rank   self  accum   count trace method
   1 79.79% 79.79%    4258 302409 java.net.SocketInputStream.read
   2  3.62% 83.41%       2 302282 java.net.PlainSocketImpl.accept
   3  1.00% 84.41%    3808 302584 jp.interdb.jgtm.gtm.GTM_Transactions.GTM_GetTransactionSnapshot
   4  0.76% 85.17%    3808 302610 jp.interdb.jgtm.gtm.GTM.ProcessGetSnapshotCommandMulti
   5  0.52% 85.69%   19376 302599 java.nio.Bits.putIntL
   6  0.52% 86.21%   19165 302591 java.nio.Bits.putIntB
   7  0.48% 86.69%   38541 302593 java.nio.HeapByteBuffer.putInt
   8  0.46% 87.14%    7741 302559 jp.interdb.jgtm.gtm.GTM_Transactions.GTM_GXIDToHandle
   9  0.43% 87.57%   22836 302440 java.nio.Bits.getIntB
  10  0.40% 87.97%   45814 302557 java.util.AbstractList$Itr.next
  11  0.39% 88.37%    4256 302517 jp.interdb.jgtm.gtm.GTM.command
  12  0.34% 88.70%  131564 302486 java.nio.HeapByteBuffer._put
  13  0.33% 89.03%   37022 302577 jp.interdb.jgtm.gtm.GTM_Transactions.GlobalTransactionIdPrecedes
  14  0.32% 89.35%  126460 302438 java.nio.HeapByteBuffer._get

簡単なまとめ

  • GTM多重化のフィージビリティスタディを行った
    1. Passive Replication方式での多重化で、現実的に利用可能な処理速度を有していることを確認した
    2. FIFO Order,もしくはCuasalOrderと障害リカバリの組み合わせでディペンダブルなGTM多重化を構築できた
  • 未解決事項
    1. Active Replication方式は、適当な通信ライブラリを準備できず仮測定のみ。
    2. (上記のMessagePassingと異なる)データ共有方式は未検討

議論

高信頼通信の利用について

今回試作したシステムでは、GTM間でメッセージを送信する際に高信頼通信(Reliable Broadcast)を使った。 図。 通信に必要な性質は以下のとおり。 (1)高信頼通信 * 正常なGTM間で: a)(eventuary)必ずメッセージが到達すること b)1回のみ送信されること c)勝手にメッセージをつくらないこと (2)FIFOオーダー あると非常に便利レベルは以下。 (3)GTM間でデータの一致を保証 (4)性能を出したいのなら、非同期

単純な
「もっと簡単に実装できるのでは?」という意見に対して、 高信頼通信を使う理由を説明する。 まず、典型的な自前Broadcastをみてみよう。 図 3台以上のGTM間でデータを一致させたいのだから、同期通信(ランデブー通信)でメッセージを送るのがよいだろう。 すると、 (1)スレーブの障害で、timeoutまで全体の処理が待たされる (2)マスタの障害時、複数のスレーブ間でデータの不一致が生じる可能性がある。 これは2相コミットでの問題と同根である。 図(2相コミットの問題)。 Sync Rep Design http://archives.postgresql.org/pgsql-hackers/2010-12/msg02484.php

Last-modified: 2012-10-28