ステータス:実測(2026-10-31)。 TT Labのテスト用VM3台(KubeVirt、Ubuntu 24.04)上のDockerで計測した値です。各テストは1回ずつ実施し、VMの電源強制断は行っていません(未計測)。構成とテスト方法は第1部に記載しています。作成規則:実測データを基本とし、計測できなかったものは「未計測」、原因を確認できなかった解釈は「推定」と表記します。
目次
- 1. 概要
- 2. 障害種類別の最長書き込み停止時間
- 3. 分断:孤立した旧マスターは書き込みを受け付けるのか
- 4. オペレーターが必ず知るべき罠
- 5. この測定の限界
- 6. まとめ:2台+証人構成にする際のチェックリスト
1. 概要
第1部では、データノード2台だけでは1台を失ったときに5つのサービスすべてが停止することを見ました。今回は投票のみを行う証人VMを追加した構成(データノードdb1・db2+証人)に3種類の障害を投入しました。
- 孤立:1台のノードの通信ネットワークをiptablesで切断した。ノードは稼働しているが、相手と切断された分断状態である。
- 正常停止:1台のノードのコンテナをすべて
docker stop(猶予30秒)で停止させた。 - 証人停止:証人のetcd・Sentinel・OpenSearch・Keeper・SeaweedFSマスターをすべて停止させた。
結果を先に述べます。
- 自動切り替えが行われました。 孤立状態ではPostgreSQLは22.6秒から32.7秒、Redis Sentinelは6.5秒から7.0秒で新しいマスターを昇格させました。正常停止ではPostgreSQLが2.8秒から3.1秒で切り替わりました。
- 確認された書き込みの損失はすべてのテスト、すべてのサービスで0件でした。
- 証人を90秒間停止しても、PostgreSQL・Redis・OpenSearchの書き込み失敗は0件でした。 データノード2台が生存していれば過半数(2/3)が維持されるためです。
- しかし、 「データノード2台」という条件はそのままです。 Redisの
min-replicas-to-write 1、ClickHouseのinsert_quorum=2、SeaweedFSのレプリケーション001はいずれも「相手のデータノードが存在する必要がある」という設定であるため、相手が復帰するまで書き込みをブロックしました。以下の表における長い値のほとんどはこれが原因です。
2. 障害種類別の最長書き込み停止時間
最長書き込み停止時間は、成功した書き込みと次の成功の間の最も長い間隔(秒)です。括弧内は失敗数/試行数です。 ClickHouse は通常の挿入、
quorum=2 は insert_quorum=2 で実行した挿入、 S3 はHAProxyの背後にある2つのSeaweedFS S3ゲートウェイです。
| サービス | db1孤立 90秒 | db2孤立 90秒 | db1孤立 40秒 | db2正常停止 90秒 | db1正常停止 90秒 | 証人停止 90秒 |
|---|---|---|---|---|---|---|
| PostgreSQL | 23.7 (400/5910) | 28.3 (491/5915) | 33.7 (601/3120) | 3.4 (25/5944) | 3.7 (34/5943) | 0.09 (0/4780) |
| Redis | 105.0 (471/4364) | 105.1 (1964/5855) | 55.2 (371/2463) | 268.8 ※ (5349/5968) | 134.8 (2673/5970) | 0.09 (0/4782) |
| OpenSearch | 66.3 (16/2347) | 64.7 (15/2364) | 40.2 (9/1196) | 0.3 (0/2990) | 0.7 (0/2986) | 0.13 (0/2392) |
| ClickHouse | 5.3 (1/1476) | 5.2 (1/1476) | 5.2 (1/776) | 2.5 (2/1491) | 2.4 (2/1490) | 5.4 (1/1175) |
| ClickHouse quorum=2 | 60.6 (303/1475) | 59.9 (300/1451) | 10.0 (51/752) | 122.9 (601/1488) | 123.6 (602/1486) | 5.3 (1/1175) |
| SeaweedFS S3 | 95.7 (19/898) | 111.2 (11/955) | 61.2 (11/505) | 122.4 (11/898) | 131.1 (12/856) | 27.6 (2/1055) |
※db2正常停止におけるRedisの268.8秒は、公式イメージの不具合により再起動されたRedisが再起動ループに陥り、最終的に復帰できなかった値です(セクション4.5)。修正後に同じテストをdb1に対して行ったのが右から2番目の列です。書き込み間隔はPostgreSQL・Redisが0.05秒であるため、0.09秒は切断がなかったことを意味します。
読み取り(参照)の失敗はほとんどありませんでした。OpenSearch _count はテストごとに0〜1回、ClickHouse・S3は証人あり構成のすべてのテストで0〜1回でした。
2.1 マスターが切り替わるまで
障害を発生させた時点からの経過秒数です。
| db1孤立 90秒 | db2孤立 90秒 | db1孤立 40秒 | db2正常停止 | db1正常停止 | |
|---|---|---|---|---|---|
| Patroni 新プライマリ | 22.6 | 27.4 | 32.7 | 3.1 | 2.8 |
| Sentinel 新マスター | 6.5 | 6.6 | 7.0 | 7.9 | 7.8 |
| OpenSearch ノード削除 (yellow) | 64.8 | 65.2 | 削除前に復旧 | 0.6 | 0.5 |
| SeaweedFS 新マスターリーダー | 12.1 | リーダーは孤立ノードではない | リーダーは孤立ノードではない | 影響なし | 15.0 |
| 復旧後に合流 | PG 15.5 (pg_rewind と再起動を含む)、OpenSearch green 5.1 | PG 6.6、OpenSearch green 2.8 | PG 14.2、OpenSearch green 6.9 | OpenSearch green 23.2 (開始基準) | OpenSearch green 26.3 |
2.2 サービス別の読み取り
- PostgreSQL:孤立状態では旧マスターのリーダーキーの有効期限(
ttl: 30)が切れてから昇格するため22〜34秒かかりました。正常停止ではPatroniが自らキーを手放したため3秒で切り替わりました。旧プライマリサーバーは復旧後にpg_rewindで巻き戻され、レプリカとして再結合しました。孤立時間を短縮したい場合、ttlを短縮することが直接的な手段ですが、短縮した値は計測していません(未計測)。 - Redis:切り替え自体は6.5〜7.9秒で完了しました。しかし、 新しいマスターも
min-replicas-to-write 1であるため、レプリカ(旧マスター)が復帰するまでNOREPLICASにより書き込みが拒否されました。 書き込み停止時間は「障害時間+10〜15秒」でした。データノードが2台だけの場合、この設定は「1台がダウンすると書き込み停止」と同義です。代償と選択肢についてはセクション4.1で確認します。 - OpenSearch:正常停止時はノードが自発的に離脱するため停止時間は1秒未満でした。孤立時は約65秒間、一部の書き込みが停止しました(セクション4.4)。
- ClickHouse:両方のレプリカが書き込みを受け付けるため、通常の挿入はHAProxyがダウンした側を除外するまでの2〜5秒間しか途切れませんでした。
insert_quorum=2は相手が復帰するまでTOO_FEW_LIVE_REPLICASにより失敗しました。追いつきは高速でした。150秒の停止後、896個のレプリケーションキューを16秒で処理しました。 - SeaweedFS:レプリケーション
001においてボリュームサーバーが1台だけになると、新しい書き込み割り当てが失敗します。これは予想通りの挙動です。予想外だったのは、失敗が即座に返されず、 リクエストごとに約10秒間ブロックされた挙句にタイムアウトした点です(セクション4.7)。読み取りはいかなるテストでも失敗しませんでした。
2.3 証人を停止しても
| PostgreSQL | Redis | OpenSearch | ClickHouse 通常 / quorum=2 | S3 |
|---|---|---|---|---|
| 失敗 0 / 4780 | 失敗 0 / 4782 | 失敗 0 / 2392 | 5.4秒 1件 / 5.3秒 1件 | 27.6秒、2件 |
ClickHouseとS3の短い途切れは、 その瞬間にKeeperのリーダーとSeaweedFSマスターのリーダーが証人上で稼働していたためです。リーダーの再選出にそれぞれ約5秒と12秒かかり、S3はボリュームサーバーが新しいリーダーに再結合するまでさらに途切れました。証人は「投票を行うだけ」の存在ですが、Raftにおいてはリーダーになる可能性もある点に留意する価値があります。
3. 分断:孤立した旧マスターは書き込みを受け付けるのか
自動切り替えが行われればそれで終わりではありません。切断された側の旧マスターがその間に書き込みを受け付け、後から破棄した場合、アプリケーションは成功レスポンスを受け取っていながらデータを失うことになります。db1(当時のマスター)を40秒間孤立させ、 db1の内部から 0.5秒ごとに自身のローカルサービスへ直接書き込みを行いました。復帰後、その書き込みが残っているかを照合しました。
| サービス | 孤立した旧マスターが受け付けた書き込み | 復帰後に残存した書き込み | 防止メカニズム |
|---|---|---|---|
| PostgreSQL | 0件。孤立直後から失敗 | 該当なし | 同期レプリケーション(レプリケーションの確認を待つ)+合意ストアを失った場合の降格 |
| Redis | 孤立後0〜10.7秒の間に22件 | 0件(22件すべて消失) | min-replicas-max-lag 10 経過した後に初めて NOREPLICAS。この10秒間がデータロスウィンドウである |
| OpenSearch | 100件 | 100件 | ※作成直後のインデックスであり、レプリケーションがin-syncセットに入る前であった(通常のインデックスでは書き込みはブロックされた)。分岐は発生しなかった |
| ClickHouse | 0件。Keeperの過半数を失ったため挿入がブロックされた | 該当なし | Keeperの過半数 |
| SeaweedFS filer | 0件。タイムアウト | 該当なし | マスターの過半数 |
過半数ルールをそのまま遵守するPostgreSQL・ClickHouse・SeaweedFSでは、孤立した側が書き込みを拒否しました。データロスが発生したのはRedisのみでした。
4. オペレーターが必ず知るべき罠
ここからは、今回の測定で実際に遭遇した事象について説明します。
4.1 Redisの min-replicas-max-lag は分断時のデータロスウィンドウそのものである
Redisのレプリケーションは非同期です。マスターが書き込みを受け付けると即座に成功を返し、レプリケーションには後から送信されます。 min-replicas-to-write 1 と
min-replicas-max-lag 10 は、「直近10秒以内に応答したレプリケーションが1台以上ある場合のみ書き込みを受け付ける」という意味です。孤立したマスターは、レプリケーションからの最後の応答が10秒を超えるまで書き込みを受け付け続けます。
実測においてそのウィンドウは正確にその通りに開きました。孤立したdb1は最初の10.7秒間に22件を成功として受け付け、その間に+7.0秒の時点でSentinelがdb2を新しいマスターとして昇格させました。復帰後、db1はdb2のレプリケーションとなり、db2のデータで上書きされたため、 22件は1件も残りませんでした。 アプリケーション側から見ると「成功」レスポンスを受け取った書き込みです。
選択肢は3つありますが、どれもタダではありません。
min-replicas-max-lagを小さくする。ウィンドウは縮小しますが、レプリケーションがわずかに遅延しただけでも書き込みが拒否されます。min-replicas-to-writeを0にします。データノードを1つ失っても書き込みは継続されますが(セクション2.2の105秒の中断は解消されます)、分割時に旧マスターが受け取る書き込みが無制限に増加します。- このウィンドウ自体をなくすには、Redisではなく同期的合意を持つストレージが必要です。キャッシュのように失っても問題ないデータにのみRedisを使うのも一つの手です。
今回の構成は1つ目の設定(1, 10)であり、その代償として「分割時の最大約10秒の書き込みロスト」と「データノードを1つ失うと書き込み停止」という2つのコストが測定されました。
4.2 失敗として応答されたが保存された書き込み、および冪等キー
確認済みの書き込みの損失は0件でしたが、逆向きのズレが存在しました。 失敗レスポンスを受け取ったが実際には保存されていた書き込みです。
| 立会人(アレスター)あり構成のテスト | OpenSearch | ClickHouse quorum=2 | S3 |
|---|---|---|---|
| db1 孤立 90秒 | 16 / 失敗 16 | 303 / 失敗 303 | 4 / 失敗 19 |
| db2 孤立 90秒 | 15 / 失敗 15 | 300 / 失敗 300 | 3 / 失敗 11 |
| db1 孤立 40秒 | 9 / 失敗 9 | 51 / 失敗 51 | 4 / 失敗 11 |
| db2 正常停止 90秒 | 0 / 失敗 0 | 601 / 失敗 601 | 2 / 失敗 11 |
| db1 正常停止 90秒 | 0 / 失敗 0 | 601 / 失敗 602 | 2 / 失敗 12 |
(表の値は「失敗レスポンスだが保存された数 / 失敗レスポンス数」)
- OpenSearchはクライアントのタイムアウト後に反映された書き込みでした。
- ClickHouse
insert_quorum=2は「クォーラムを満たせなかった」と応答しましたが、1つのレプリカには書き込まれており、相手が復帰するとそのまま複製されました。失敗レスポンスのほぼ全てがこのケースでした。「失敗 = 書き込まれていない」として扱ってはなりません。 - Keeperの過半数を失ったとき(Keeper 3台中2台を停止させたテスト)も同様でした。挿入はエラーなく5秒のタイムアウトでハングし(60秒間で13件)、Keeperを復旧させると13件が最終的にすべて保存されました。
クライアントが失敗を確認して再送すると、同じ行が二重に入ることになります。そのため、 再送する書き込みには冪等キーが必要です。OpenSearchはドキュメントの _id をアプリ側で指定して投入し、ClickHouseは insert_deduplication_token を付与します。アプリの再試行ロジックが「失敗したから存在しないはずだ」という前提になっていないか、あらかじめ確認する必要があります。
4.3 ClickHouse Keeper強制復旧直後のクォーラムウィンドウ
パート1のセクション7.1で見た内容です。たった2台の構成でKeeperを --force-recovery で単一サーバーとして復旧させた直後の約20秒間、相手のレプリカが孤立したままだったにもかかわらず、
insert_quorum=2 挿入98件が成功として応答されました。その直後には TOO_FEW_LIVE_REPLICAS により1346件が失敗しました。復旧したKeeperに、相手の古い「生存」フラグがセッションタイムアウト(30秒)まで残っていたためと推測されます(推測)。 強制復旧後は、セッション時間が経過するまでクォーラム挿入の成功を信用しません。
4.4 OpenSearchは孤立したノードを検知するのに65秒かかった
正常停止の場合はノードが自らクラスターを離脱するため、書き込みの中断は1秒未満でした。孤立の場合は異なっていました。ノードが外れるまで(yellow)64.8秒と65.2秒かかり、その間 そのノードにprimaryシャードがあるインデックスへの書き込みが停止しました。 検索(_count)は孤立テストごとに0回から1回しか失敗しませんでした。
OpenSearchはデフォルトの障害検知設定(1回の確認のタイムアウト10秒、3回連続失敗で障害と判定)でノードを監視します。孤立状態ではこの判定を2段階経るため、約65秒かかったものと考えられます。 cluster.fault_detection.* 値を小さくすればこの時間を短縮できますが、縮めた値での測定は行っていません(未測定)。一時的なネットワークの揺らぎでもノードが切り離されるようになるため、小さくする場合はその値で再測定する必要があります。
4.5 公式 redis:8 イメージとSentinelの再起動ループ
2節の表にある268.8秒の正体はこれです。原因は以下のように複合していました。
- 公式
redis:8イメージのエントリーポイントは、モジュールフォルダーにある.soをすべて--loadmoduleとして付与し、Redisを起動します。 - Sentinelはフェイルオーバー時にRedisに
CONFIG REWRITEを送信します。すると実行中の設定がファイルに再書き込みされ、そのloadmodule行がredis.confに刻み込まれます。 - 次回の起動時に、エントリーポイントが付与したモジュールとファイルに書き込まれたモジュールが重複し、同じモジュールを二重に読み込もうとして
Can't load module ... server abortingでクラッシュします。コンテナは再起動し、再びクラッシュします。
Sentinelを使用し、フェイルオーバーを1度経験し、そのRedisを再起動したときにのみ表面化するため、セットアップ直後のテストでは見つかりません。モジュールを使わないのであれば解決は簡単です。モジュールフォルダーを空のtmpfsで上書きして一切ロードしないようにします。既に書き込まれている行は削除します。
redis:
image: redis:8.8.4
command: ["redis-server", "/conf/redis.conf"]
tmpfs:
- /usr/local/lib/redis/modules # 진입점이 붙일 모듈이 없게 한다
sed -i '/^loadmodule /d' /path/to/redis.conf # 이미 박힌 줄 지우기
修正後の同じテスト(db1正常停止)ではRedisは正常に戻り、残りの134.8秒の中断はセクション4.1の min-replicas の代償でした。この問題はすでにアップストリームに報告されており(redis/docker-library-redis#468,
redis/redis#14178)、Redis本体側の修正(redis/redis#16009)が2026-10-29にマージされ、次のバージョンからリリースされる予定です。
同じ系統の小さな落とし穴がもう1つあります。 Sentinelは requirepass をACLハッシュ行に書き換えます。 設定ファイルからパスワードを読み取るヘルスチェックが破損するため、今回は認証前の応答(NOAUTH)も「生存」とみなすようにしました。
4.6 立会人は3つ目の障害ドメインに配置する
今回のテストで、立会人VMはdb2と 同一の物理サーバー 上にありました。VM単位の障害しか投入しなかったため結果に影響はありませんでしたが、その物理サーバーがダウンするとdb2と立会人を同時に失い、3票中2票が消滅します。その結果、2台だけの構成と同様に停止します。
立会人の価値は「どちらのデータノードとも同時に死なない」点にあります。運用では立会人を 3つ目の障害ドメイン、すなわち別のラック、別の建物、別の電源とスイッチ、あるいはクラウドの小さなVMに配置します。立会人の役割は投票を行うことだけなので、小さなVMで十分です。今回の立会人は2 vCPU・2 GiBで、セットアップ直後に1.5 GiBを使用しました。ただしセクション2.3で見たように、立会人もRaftリーダーになる可能性があるため、データノードとの往復遅延が大きすぎるとリーダー再選挙中の通信途絶が長くなる可能性があります(未測定)。
4.7 SeaweedFS:ボリュームサーバーが1台では書き込めないが、即座に失敗もしない
レプリケーション 001 においてボリュームサーバーが1台だけになると、新しい書き込みを配置する場所がなくなります。ここまでは設定どおりです。問題は、失敗がリクエストごとに約10秒間ハングした後にようやく返ってきたことです。2つの要因が重なっていました。
- 生き残ったボリュームサーバーが死亡した古いmasterリーダーへハートビートを送り続け、 54秒 経ってから新しいリーダーに接続しました。その間、新しいリーダーは登録された容量が0であるため「no volume server capacity registered yet」を出力していました。実際の原因とは異なる文言です。
- 孤立していたmasterが復帰すると、問題なく動作していたリーダーを揺さぶり、 6秒から19秒間リーダー不在の状態が発生しました。
どちらの問題もアップストリームに報告しました。ボリュームサーバーが新しいリーダーの検出に時間がかかる問題については修正PR(seaweedfs#12259、再現テストで25秒が4秒に短縮)、復帰したmasterがリーダーを引きずり下ろす問題についてはissues(seaweedfs#12260)を提出しました。同じ実験を -raftHashicorp(PreVoteが有効なRaft)で実行するとリーダーは揺らぎませんでした。運用環境でデータノード2台でSeaweedFSを使用する場合、1台が脱落している間はアップロードが即座に失敗せずハングすることを、アプリケーションのタイムアウトに反映させておきます。ボリュームサーバーのディスクをごっそり失ったケースでは、 volume.fix.replication -apply が30.7秒ですべての24ボリュームを整合させ、その間S3の書き込み失敗は1件、読み込み失敗は0件でした。filerを1台60秒間停止させたテストでは、S3の書き込み748件のうち失敗は0件でした。
4.8 小さな落とし穴のまとめ
| 落とし穴 | 症状 | 対策 |
|---|---|---|
OpenSearch node.roles: ${OS_ROLES}(yml内の環境変数置換) |
リストではなく単なる文字列になってしまい、ノード間でクラスターを形成できない。DEBUGログでしか確認できない | 環境変数 node.roles=cluster_manager,data,ingest(カンマ区切りのリスト)として渡す |
| OpenSearch 3.9 立会人のヒープ320m・上限700m | OOM再起動ループ | ヒープ384m・上限1100m |
| SeaweedFSイメージはuid 1000として実行される | root 600のS3設定ファイルが読み込めずS3がクラッシュする | ファイルを1000の所有者にする |
| SeaweedFS masterの上限128m | 89 MiBまで上昇する | 192m |
| ClickHouse 26.8 HTTP挿入のデフォルト | async_insert=1, wait_for_async_insert=1 として出力された |
クォーラムテストは async_insert=0 で個別に |
| ClickHouse Keeper Raftポート | listen_host を指定しても全アドレス(9234)でオープンした |
ホストファイアウォールで外部インターフェースをブロックする |
iptables -m multiport |
1つのルールにポート15個まで(「too many ports specified」) | ルールを2つに分割する |
コンテナには /dev/watchdog がない |
Patroniのフェンシングが合意ストレージのTTLのみに依存している。プライマリサーバーがダウンせず停止したままTTLを超過すると、一時的にマスターが2つ存在しうる | ホストのwatchdogを渡すかsoftdog(未測定) |
docker stop 猶予30秒 |
猶予を使い切るコンテナがあるためノード全体の一斉停止に30秒かかった | 正常停止手順のタイムバジェットに組み込む |
5. 本測定の限界
数値を参考にする前に条件をご確認ください。
- VM上のDockerです。 KubeVirt VM内でDocker Composeにより実行しました。物理サーバー、別のハイパーバイザー、Kubernetes operator構成では数値が異なる可能性があります。コンテナのメモリ制限も運用環境より小さく設定しています(OpenSearch 1.4gなど)。
- 各テストは1回ずつです。 孤立はノードと長さを変えて3回、正常停止は2回(そのうち1回のRedisの値はセクション4.5の不具合のため別途考慮が必要)、立会人の停止と2台構成での孤立・手動昇格はそれぞれ1回です。同一条件を繰り返した分布や中央値ではありません。
- VMの電源断は行っていません(未測定)。 「孤立」を相手側から見た電源断と同等とみなして代用しています。ディスクキャッシュの消失など、電源断特有の現象はこの測定には含まれていません。
- 立会人がデータノードの1つと同一の物理サーバー上にありました。 VM単位の障害のみを投入したため結果への影響はありませんが、物理サーバーの障害は測定していません。
- 測定していない項目:復旧(停止していたノードの再参加)、libpqのマルチホスト接続(
target_session_attrs=read-write)によるプライマリサーバーの検出、Patronifailsafe_mode、OpenSearchの障害検知値を小さくした場合、2メンバー構成での実際の再構築を行ったKeeper・SeaweedFS master、watchdogフェンシング。 - セキュリティ設定の不備: etcd·PostgreSQL·OpenSearch は独自 CA の TLS を使用していましたが、Redis·Sentinel はパスワード認証のみ、Patroni REST·SeaweedFS·ClickHouse HTTP はセッション網内で平文でした。本番運用ではこれら 3 つにも TLS を追加する必要があります。
- クロックの差は VM 間で ±0.3 秒以内であったため、1 秒未満の差については解釈していません。
6. まとめ: 2 台 + 証人構成を採用する際のチェックリスト
| 確認項目 | 理由(本記事の実測値) |
|---|---|
| 証人をデータノードとは異なる障害ドメインに配置する | 同じ場所で同時に停止した場合、2 台だけの構成と同じになる |
Redis の min-replicas-* を意識して選定する |
(1, 10) は分断時に 22 件消失、ノードを 1 つ 90 秒失うと 105 秒間書き込みが停止 |
| リトライする書き込みには冪等キーを付与する | 失敗の応答だったが保存された書き込みが OpenSearch·ClickHouse·S3 で発生した |
| ClickHouse の強制復旧後、セッション時間中はクォーラムを信用しない | 98 件の偽の成功 |
| OpenSearch の孤立検知時間を予算に組み込む | デフォルト値で 65 秒 |
公式 redis:8 + Sentinel の場合はモジュールフォルダを空にする |
フェイルオーバー後の再起動ループ |
| 手動昇格の手順を事前に記載し、フェンシングから実行する | 2 台のみの場合の唯一の出口。サービスごとに 0.9 秒から 44.2 秒 |
TT からの一言。 証人の友だちができると多数決がうまく機能します。しかし荷物を持つ友だちは相変わらず 2 人だけなので、1 人が休むと「相棒が来たら荷物を下ろすね」と言い出すサービスがあります。そして「失敗しました」という言葉を聞いて再送する時は、必ず名札(冪等キー)を付けてください。同じ荷物が 2 回入ってくると困りますからね!