<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Failover on Tarragon</title><link>https://tarrragon.github.io/blog/tags/failover/</link><description>Recent content in Failover on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/failover/index.xml" rel="self" type="application/rss+xml"/><item><title>模組六：高可用與災難復原</title><link>https://tarrragon.github.io/blog/devops/06-high-availability/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/06-high-availability/</guid><description>&lt;p>回答「一個節點掛了服務怎麼不中斷」。高可用的核心是冗餘 — 每個單點故障都有替代路徑。&lt;/p>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 單點故障盤點（服務實例 / DB / LB / DNS — 哪些掛了整個系統就掛）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 冗餘設計模式（active-passive / active-active / multi-region）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Failover 機制（自動 vs 手動、failover 時間、資料一致性）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Disaster recovery 策略（RPO / RTO 目標、備份恢復演練）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 高可用的成本（冗餘 = 至少 2x 資源成本 — 值不值得）&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend 可靠性&lt;/a>：Backend 的可靠性設計&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/devops/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">devops 模組四 服務探活&lt;/a>：探活是 failover 的觸發條件&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/05-core-services/stateful-protection-dependency/" data-link-title="Stateful 資源保護與跨服務依賴表達" data-link-desc="stateful 資源的保護策略（multi-AZ、備份、刪除保護）、stateful 與 stateless 的操作差異，以及用 output 與 data source 表達服務間依賴">Infra 核心服務上 IaC — Stateful 資源保護&lt;/a>：multi-AZ 是 infra 層的可用區冗餘能力，本模組的 HA 策略（健康檢查、自動恢復、failover 機制）建立在這個能力之上&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">Infra 網路地基&lt;/a>：跨可用區的 subnet 與 NAT 冗餘設計是 HA 的網路前提&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「一個節點掛了服務怎麼不中斷」。高可用的核心是冗餘 — 每個單點故障都有替代路徑。</p>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input disabled="" type="checkbox"> 單點故障盤點（服務實例 / DB / LB / DNS — 哪些掛了整個系統就掛）</li>
<li><input disabled="" type="checkbox"> 冗餘設計模式（active-passive / active-active / multi-region）</li>
<li><input disabled="" type="checkbox"> Failover 機制（自動 vs 手動、failover 時間、資料一致性）</li>
<li><input disabled="" type="checkbox"> Disaster recovery 策略（RPO / RTO 目標、備份恢復演練）</li>
<li><input disabled="" type="checkbox"> 高可用的成本（冗餘 = 至少 2x 資源成本 — 值不值得）</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend 可靠性</a>：Backend 的可靠性設計</li>
<li>→ <a href="/blog/devops/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">devops 模組四 服務探活</a>：探活是 failover 的觸發條件</li>
<li>→ <a href="/blog/infra/05-core-services/stateful-protection-dependency/" data-link-title="Stateful 資源保護與跨服務依賴表達" data-link-desc="stateful 資源的保護策略（multi-AZ、備份、刪除保護）、stateful 與 stateless 的操作差異，以及用 output 與 data source 表達服務間依賴">Infra 核心服務上 IaC — Stateful 資源保護</a>：multi-AZ 是 infra 層的可用區冗餘能力，本模組的 HA 策略（健康檢查、自動恢復、failover 機制）建立在這個能力之上</li>
<li>→ <a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">Infra 網路地基</a>：跨可用區的 subnet 與 NAT 冗餘設計是 HA 的網路前提</li>
</ul>
]]></content:encoded></item><item><title>AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a> overview 的 implementation-layer deep article。選型層（為何用 managed、engine 選擇、跟自管取捨）見 overview；本文只處理「決定用 ElastiCache 後，哪些是 AWS 的責任、哪些仍是你的」。CLI 與計費以 &lt;a href="https://docs.aws.amazon.com/elasticache/">AWS ElastiCache 官方文件&lt;/a>、&lt;a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價&lt;/a> 為準、最後檢查日 2026-06-16（managed 服務的引數與價格會變、以官方為準）。&lt;/p>&lt;/blockquote>
&lt;h2 id="managed-不等於-hands-off">managed 不等於 hands-off&lt;/h2>
&lt;p>把 cache 換成 ElastiCache 之後，最危險的心態是「現在 AWS 全包了」。AWS 確實接走了一大塊運維——它幫你做 failover、patching、snapshot、跨 AZ 複製，你不用再自己部署 Sentinel、不用半夜起來手動切 master。但有一類問題 ElastiCache 一個都沒幫你解，而且因為「以為 AWS 會處理」，這些問題在 managed 環境反而更容易被忽略到上線才爆。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>跑在 ElastiCache for Valkey 上、4700 萬月活、sub-millisecond 延遲——這證明 managed 撐得起極大規模，但 Tinder 仍要自己設計 key、處理 cache miss、控制 client 行為。ElastiCache for Redis 7.1 在 r7g.4xlarge 上單 node 可達約 100 萬 RPS、單 cluster 約 5 億 RPS（引自 &lt;a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog&lt;/a>）——這個吞吐是 AWS 給的，但用不用得好取決於你的 key 分布與 client 設計。&lt;/p>
&lt;p>理解 ElastiCache 就是劃清這條責任邊界。本文按 shared responsibility 展開：AWS 管什麼、你管什麼、邊界上的踩坑在哪。&lt;/p>
&lt;h2 id="核心概念shared-responsibility-的兩側">核心概念：shared responsibility 的兩側&lt;/h2>
&lt;p>ElastiCache 的責任劃分可以列成一張清楚的表，這張表是判讀所有 ElastiCache 事故的起點：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>AWS 的責任（managed）&lt;/th>
 &lt;th>你的責任（仍要自己做）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>硬體 / OS / patching&lt;/td>
 &lt;td>全包&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>failover&lt;/td>
 &lt;td>自動偵測 + replica 晉升&lt;/td>
 &lt;td>client 要有 reconnect 邏輯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 AZ 複製&lt;/td>
 &lt;td>Multi-AZ 自動複製&lt;/td>
 &lt;td>接受非同步複製的 stale window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>snapshot / backup&lt;/td>
 &lt;td>自動 + 手動 snapshot&lt;/td>
 &lt;td>決定保留策略、驗證能還原&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>eviction&lt;/td>
 &lt;td>提供 maxmemory-policy 參數&lt;/td>
 &lt;td>選對 policy、設對 TTL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cache stampede&lt;/td>
 &lt;td>不管&lt;/td>
 &lt;td>client-side jitter / singleflight 自己做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>key 設計 / hot key&lt;/td>
 &lt;td>不管&lt;/td>
 &lt;td>key 分布、hot key 兩層 cache 自己處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>連線管理&lt;/td>
 &lt;td>提供 endpoint&lt;/td>
 &lt;td>連線池、socket timeout 自己設&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>左欄是用 managed 換到的，右欄是用 managed 換不掉的。&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 cache stampede&lt;/a> 的雪崩、&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線風暴&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 選錯&lt;/a> 在 ElastiCache 上跟自管 Redis 一模一樣會發生——因為這些是 cache 使用方式的問題，不是運維的問題。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a> overview 的 implementation-layer deep article。選型層（為何用 managed、engine 選擇、跟自管取捨）見 overview；本文只處理「決定用 ElastiCache 後，哪些是 AWS 的責任、哪些仍是你的」。CLI 與計費以 <a href="https://docs.aws.amazon.com/elasticache/">AWS ElastiCache 官方文件</a>、<a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價</a> 為準、最後檢查日 2026-06-16（managed 服務的引數與價格會變、以官方為準）。</p></blockquote>
<h2 id="managed-不等於-hands-off">managed 不等於 hands-off</h2>
<p>把 cache 換成 ElastiCache 之後，最危險的心態是「現在 AWS 全包了」。AWS 確實接走了一大塊運維——它幫你做 failover、patching、snapshot、跨 AZ 複製，你不用再自己部署 Sentinel、不用半夜起來手動切 master。但有一類問題 ElastiCache 一個都沒幫你解，而且因為「以為 AWS 會處理」，這些問題在 managed 環境反而更容易被忽略到上線才爆。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>跑在 ElastiCache for Valkey 上、4700 萬月活、sub-millisecond 延遲——這證明 managed 撐得起極大規模，但 Tinder 仍要自己設計 key、處理 cache miss、控制 client 行為。ElastiCache for Redis 7.1 在 r7g.4xlarge 上單 node 可達約 100 萬 RPS、單 cluster 約 5 億 RPS（引自 <a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog</a>）——這個吞吐是 AWS 給的，但用不用得好取決於你的 key 分布與 client 設計。</p>
<p>理解 ElastiCache 就是劃清這條責任邊界。本文按 shared responsibility 展開：AWS 管什麼、你管什麼、邊界上的踩坑在哪。</p>
<h2 id="核心概念shared-responsibility-的兩側">核心概念：shared responsibility 的兩側</h2>
<p>ElastiCache 的責任劃分可以列成一張清楚的表，這張表是判讀所有 ElastiCache 事故的起點：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>AWS 的責任（managed）</th>
          <th>你的責任（仍要自己做）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>硬體 / OS / patching</td>
          <td>全包</td>
          <td>—</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自動偵測 + replica 晉升</td>
          <td>client 要有 reconnect 邏輯</td>
      </tr>
      <tr>
          <td>跨 AZ 複製</td>
          <td>Multi-AZ 自動複製</td>
          <td>接受非同步複製的 stale window</td>
      </tr>
      <tr>
          <td>snapshot / backup</td>
          <td>自動 + 手動 snapshot</td>
          <td>決定保留策略、驗證能還原</td>
      </tr>
      <tr>
          <td>eviction</td>
          <td>提供 maxmemory-policy 參數</td>
          <td>選對 policy、設對 TTL</td>
      </tr>
      <tr>
          <td>cache stampede</td>
          <td>不管</td>
          <td>client-side jitter / singleflight 自己做</td>
      </tr>
      <tr>
          <td>key 設計 / hot key</td>
          <td>不管</td>
          <td>key 分布、hot key 兩層 cache 自己處理</td>
      </tr>
      <tr>
          <td>連線管理</td>
          <td>提供 endpoint</td>
          <td>連線池、socket timeout 自己設</td>
      </tr>
  </tbody>
</table>
<p>左欄是用 managed 換到的，右欄是用 managed 換不掉的。<a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 cache stampede</a> 的雪崩、<a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線風暴</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 選錯</a> 在 ElastiCache 上跟自管 Redis 一模一樣會發生——因為這些是 cache 使用方式的問題，不是運維的問題。</p>
<h3 id="engine-選擇與-cluster-mode">engine 選擇與 cluster mode</h3>
<p>ElastiCache 的兩個結構性決策：</p>
<p><strong>engine</strong>：2024 起 default 是 Valkey（成本約低 20%、OSI 開源、Redis 7.2.4 fork、API 相容）；Redis OSS 仍可選但 AWS 不推；Memcached 是另一條線（純 KV、無 cluster mode 概念）。新部署或既有 Redis 遷移都走 Valkey（相容、便宜），純 cache 才考慮 Memcached。</p>
<p><strong>cluster mode</strong>：disabled 是 1 primary + 最多 5 replica、單 shard、上限約 340GB；enabled 是多 shard（最多 500）、自動 sharding、橫向擴展。判讀：dataset &lt; 300GB 且不需 sharding 用 disabled（簡單），&gt; 300GB 或要橫向擴展用 enabled（但 client 要 cluster-aware）。</p>
<h2 id="配置建立與治理的設定路徑">配置：建立與治理的設定路徑</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 建立 Valkey replication group（Multi-AZ、auto failover、cluster mode disabled）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws elasticache create-replication-group <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --replication-group-id prod-cache <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --replication-group-description <span class="s2">&#34;prod cache&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --engine valkey <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --cache-node-type cache.r7g.large <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --num-cache-clusters <span class="m">3</span> <span class="se">\ </span>          <span class="c1"># 1 primary + 2 replica</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  --automatic-failover-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --multi-az-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --snapshot-retention-limit <span class="m">7</span> <span class="se">\ </span>    <span class="c1"># 自動 snapshot 保留 7 天</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  --at-rest-encryption-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --transit-encryption-enabled
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 自訂 parameter group（maxmemory-policy 等仍是你的責任）</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">aws elasticache create-cache-parameter-group <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --cache-parameter-group-name prod-params <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --cache-parameter-group-family valkey8 <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>  --description <span class="s2">&#34;prod cache params&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">aws elasticache modify-cache-parameter-group <span class="se">\
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="se"></span>  --cache-parameter-group-name prod-params <span class="se">\
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="se"></span>  --parameter-name-values <span class="s2">&#34;ParameterName=maxmemory-policy,ParameterValue=allkeys-lru&#34;</span></span></span></code></pre></div><p>配置判讀：</p>
<ul>
<li><code>--automatic-failover-enabled</code> + <code>--multi-az-enabled</code> 是 HA 的核心，把 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel 那條 failover 時序鏈</a>託管掉</li>
<li><code>maxmemory-policy</code> 透過 parameter group 設定——AWS 給旋鈕、選哪個是你的責任（見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 調校</a>）</li>
<li><code>--transit-encryption-enabled</code> 加 TLS，但 TLS 增加 client 建連成本，連線池更重要</li>
<li>IAM authentication（Redis 7+）取代 AUTH password，對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">security 模組</a></li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1failover-期間-client-持續-error">Case 1：failover 期間 client 持續 error</h3>
<p><strong>徵兆</strong>：ElastiCache 觸發 failover（看 <code>describe-events</code>），AWS 端 replica 晉升完成，但 application 持續 30 秒到幾分鐘大量連線 error。</p>
<p><strong>根因</strong>：failover 時 primary endpoint 的 DNS 切到新 primary，但 client 的連線池還握著舊 primary 的連線、DNS 也可能有快取。AWS 完成了 failover，但 client 重連是你的責任——ElastiCache 不會幫你的 application 重連。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 用支援自動重連的 library，設合理的 socket timeout 與 retry（見 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線調校</a>）</li>
<li>連到 primary endpoint（會跟著 failover 更新 DNS），不要連到特定 node 的 endpoint</li>
<li>縮短 client 的 DNS 快取 TTL，讓 failover 後的 DNS 切換更快被看到</li>
<li>failover 期間的寫入中斷無法完全避免（非同步複製 + 重連時間），latency-sensitive 服務要設計降級</li>
</ol>
<h3 id="case-2跨-az-replication-lag-造成-stale-read">Case 2：跨 AZ replication lag 造成 stale read</h3>
<p><strong>徵兆</strong>：寫入 primary 後立刻從 replica 讀，偶爾讀到舊值；CloudWatch 的 <code>ReplicationLag</code> 在高寫入時段上升。</p>
<p><strong>根因</strong>：ElastiCache 的跨 AZ 複製是非同步的，replica 有 lag。AWS 保證複製會發生，但不保證即時——read-from-replica 在寫後立即讀的場景會看到 stale window。這跟自管 Redis 的 replica 行為一致，managed 沒有消除它。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>寫後需要立即一致讀的路徑，強制 read from primary</li>
<li>監控 CloudWatch <code>ReplicationLag</code>，持續高代表寫入超過複製能力，要 scale up node 或降寫入</li>
<li>接受 cache 的最終一致性——這是 cache copy 的本質，不是 bug（見 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a>）</li>
<li>需要強一致 + durability 走 MemoryDB（見本文 Capacity / cost 邊界段）</li>
</ol>
<h3 id="case-3serverless-計費超出預期">Case 3：Serverless 計費超出預期</h3>
<p><strong>徵兆</strong>：用了 ElastiCache Serverless 想省容量規劃，月底帳單遠超預期。</p>
<p><strong>根因</strong>：Serverless 按 ECPU（運算）+ storage 計費，流量尖峰或低效的 access pattern（大量小命令、大 value）會推高 ECPU 消耗。Serverless 解的是「不想規劃容量」，不是「一定更便宜」——可預測的穩態流量用 node-based + Reserved Instance 通常更省。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>流量可預測、穩態高的 workload 用 node-based + Reserved Instance（1/3 年承諾、折扣約 30-60%）</li>
<li>流量不可預測、有大量閒置時段的才適合 Serverless</li>
<li>監控 ECPU 消耗，找出推高成本的 access pattern（用 pipeline 合併小命令降 ECPU）</li>
<li>成本模型對比要算實際 workload，不要假設 Serverless 一定划算</li>
</ol>
<h3 id="case-4cluster-mode-enabled-但-client-不是-cluster-aware">Case 4：cluster mode enabled 但 client 不是 cluster-aware</h3>
<p><strong>徵兆</strong>：建了 cluster mode enabled 的 cluster，application 連線報 <code>MOVED</code> redirect 或連不上某些 key。</p>
<p><strong>根因</strong>：cluster mode enabled 把 keyspace 分到多 shard，client 必須 cluster-aware（懂 <code>CLUSTER SLOTS</code>、處理 <code>MOVED</code>/<code>ASK</code> redirect）才能正確路由。普通 standalone client 連 cluster mode enabled 會失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cluster mode enabled 一律用 cluster-aware client（連 configuration endpoint 不是單一 node）</li>
<li>確認 application 的多 key 操作用 hash tag 把相關 key co-locate 同 slot（見 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">cluster re-sharding</a>）</li>
<li>dataset &lt; 300GB 且不需 sharding，用 cluster mode disabled 省掉這層複雜度</li>
<li>從 disabled 升 enabled 是有成本的架構變更，初期規劃就要決定</li>
</ol>
<h3 id="case-5snapshot-期間記憶體尖峰node-不穩">Case 5：snapshot 期間記憶體尖峰、node 不穩</h3>
<p><strong>徵兆</strong>：自動 snapshot 時段 node 延遲上升、<code>DatabaseMemoryUsagePercentage</code> 衝高，偶爾 snapshot 失敗。</p>
<p><strong>根因</strong>：Redis engine 的 snapshot 靠 fork（見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a>），fork 期間 copy-on-write 推高記憶體。如果 node 記憶體已吃緊，snapshot 的 fork 把它推爆。AWS 託管了 snapshot 排程，但 fork 的記憶體成本仍在 engine 層存在。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>node 記憶體留 headroom（不要長期 &gt; 80%），給 snapshot 的 fork copy-on-write 空間</li>
<li>snapshot window 設在低流量時段，減少 fork 期間被改的 page</li>
<li>監控 CloudWatch <code>DatabaseMemoryUsagePercentage</code>，&gt; 80% 考慮 scale up node type</li>
<li>Valkey engine 繼承 Redis 的 fork 模型，這個成本換 engine 到 Valkey 也還在（fork-less 要 DragonflyDB、但 ElastiCache 不提供）</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>ElastiCache 的容量判讀，混合了 AWS 的 metric 與 engine 層的行為：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>DatabaseMemoryUsagePercentage</code></td>
          <td>&lt; 80%</td>
          <td>&gt; 80% → scale up node 或調 maxmemory-policy</td>
      </tr>
      <tr>
          <td><code>ReplicationLag</code></td>
          <td>&lt; 1 秒</td>
          <td>持續高 → 寫入超過複製能力</td>
      </tr>
      <tr>
          <td><code>CurrConnections</code></td>
          <td>遠低於 node 上限</td>
          <td>接近上限 → client 連線池問題</td>
      </tr>
      <tr>
          <td><code>CacheHitRate</code></td>
          <td>&gt; 90%（多數 cache）</td>
          <td>下滑 → TTL / eviction / key 設計問題</td>
      </tr>
      <tr>
          <td>Serverless ECPU</td>
          <td>對齊預算</td>
          <td>暴衝 → access pattern 低效、用 pipeline 合併</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要 source-of-truth 的 Redis API（不是 cache）</strong>：ElastiCache 是 cache 語意（資料可重建）。需要 durability 走 <strong>AWS MemoryDB</strong>——Redis-compatible 但有 multi-AZ transaction log、提供 source-of-truth 語意，成本約 ElastiCache 的 2-3 倍。判讀：<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi 把 feature store 從 ScyllaDB 遷到 ElastiCache</a> 的前提是「feature 可重新計算」——可重建選 ElastiCache，不可重建選 MemoryDB 或 database。</li>
<li><strong>跨雲 / 不在 AWS 生態</strong>：ElastiCache 綁 AWS，跨雲走自管 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a> 或 GCP Memorystore / Azure Cache。</li>
<li><strong>極端單機 throughput</strong>：要榨單機多核走自管 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（ElastiCache 不提供 Dragonfly engine）。</li>
<li><strong>跨 region active-passive DR</strong>：ElastiCache 的 Global Datastore（1 primary region + 多 secondary read replica、跨 region lag &lt; 1 秒），不支援 active-active multi-master。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>ElastiCache 的 deep article 本質是「劃清 managed 邊界」，它跟 engine 層的調校知識緊密相連：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis 全系列 deep article</a></strong>：eviction、persistence/fork、連線的調校在 ElastiCache 上仍適用（engine 是 Redis/Valkey），AWS 託管的是 failover/patching/snapshot 排程，不是這些 engine 行為。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性</a></strong>：ElastiCache 的 default engine 就是 Valkey，相容性與 io-threads 的判讀直接適用。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">Netflix EVCache</a></strong>：EVCache 是 Netflix 自管的 Memcached-based 全域 cache，對照 ElastiCache for Memcached + Global Datastore——展示了自管跨區 vs managed 跨區的取捨。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder</a> / <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi</a></strong>：兩個 ElastiCache 規模化案例，一個是 sub-ms 配對引擎、一個是 ML feature store p99&lt;10ms，都展示了「AWS 給吞吐、你給設計」的邊界。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>engine 層 deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel 與 failover 時序</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性</a></li>
<li>上游能力：<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/patroni-ha/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/patroni-ha/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 PostgreSQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>Patroni-based HA&lt;/em> 的 lifecycle 設計 — 從正常運作到 failover 完成的 5 段、每段配置 + failure mode + recovery。&lt;/p>&lt;/blockquote>
&lt;h2 id="failover-lifecycle5-段不是一條曲線">Failover lifecycle：5 段不是一條曲線&lt;/h2>
&lt;p>PostgreSQL 原生沒有 auto-failover；primary 掛了、application 卡死、SRE 手動 promote standby — 整個過程通常 5-30 分鐘。Patroni 把這條鏈拆成 &lt;em>自動化的 5 段 lifecycle&lt;/em>、每段有自己的 trigger、配置、失敗模式：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>段&lt;/th>
 &lt;th>觸發&lt;/th>
 &lt;th>動作&lt;/th>
 &lt;th>失敗模式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>1. Detection&lt;/strong>&lt;/td>
 &lt;td>Leader heartbeat 在 DCS（etcd / Consul）失聯&lt;/td>
 &lt;td>Standby 們開始觀察、累積失聯時間到 TTL&lt;/td>
 &lt;td>DCS 本身分裂 → false detection 啟動失敗 failover&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>2. Election&lt;/strong>&lt;/td>
 &lt;td>TTL 過、DCS 開放 leader lock&lt;/td>
 &lt;td>Standby 競爭寫 leader key（DCS quorum-based）&lt;/td>
 &lt;td>Network partition → 兩邊都自認 leader（split-brain）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>3. Promotion&lt;/strong>&lt;/td>
 &lt;td>新 leader 寫 DCS key 成功&lt;/td>
 &lt;td>跑 &lt;code>pg_ctl promote&lt;/code>、停 streaming replication、開始接寫&lt;/td>
 &lt;td>Standby 落後太多 → 拒 promote 或承接時資料缺&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>4. Reconfiguration&lt;/strong>&lt;/td>
 &lt;td>Patroni REST API 通知 routing 層&lt;/td>
 &lt;td>HAProxy / PgBouncer 切流量到新 leader&lt;/td>
 &lt;td>Routing 層 health check 慢 → 流量持續打舊 leader&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>5. Recovery&lt;/strong>&lt;/td>
 &lt;td>舊 leader 恢復（手動 / 自動）&lt;/td>
 &lt;td>跑 &lt;code>pg_rewind&lt;/code> + 重接 streaming replication 為 standby&lt;/td>
 &lt;td>WAL divergence 太大 → 必須重 base backup&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每段都有獨立配置、不是「設一個 timeout 就好」。後面分段展開。&lt;/p>
&lt;h2 id="stage-1detection--dcs-heartbeat-跟-ttl">Stage 1：Detection — DCS heartbeat 跟 TTL&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c"># patroni.yml 核心配置&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">scope&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">myapp-pg-cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/db/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">pg-node-1 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 跟 hostname 一致&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">etcd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">etcd1:2379,etcd2:2379,etcd3:2379 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># DCS quorum&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">bootstrap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dcs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ttl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># leader lock TTL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">loop_wait&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># patroni 主循環間隔&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retry_timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># DCS retry 上限&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maximum_lag_on_failover&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1048576&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># standby 落後 1MB 內才能 promote&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">synchronous_mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># async / sync 取捨&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵直覺：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> overview 的 implementation-layer deep article。Overview 已說明 PostgreSQL 在 OLTP 譜系的定位、本文聚焦 <em>Patroni-based HA</em> 的 lifecycle 設計 — 從正常運作到 failover 完成的 5 段、每段配置 + failure mode + recovery。</p></blockquote>
<h2 id="failover-lifecycle5-段不是一條曲線">Failover lifecycle：5 段不是一條曲線</h2>
<p>PostgreSQL 原生沒有 auto-failover；primary 掛了、application 卡死、SRE 手動 promote standby — 整個過程通常 5-30 分鐘。Patroni 把這條鏈拆成 <em>自動化的 5 段 lifecycle</em>、每段有自己的 trigger、配置、失敗模式：</p>
<table>
  <thead>
      <tr>
          <th>段</th>
          <th>觸發</th>
          <th>動作</th>
          <th>失敗模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>1. Detection</strong></td>
          <td>Leader heartbeat 在 DCS（etcd / Consul）失聯</td>
          <td>Standby 們開始觀察、累積失聯時間到 TTL</td>
          <td>DCS 本身分裂 → false detection 啟動失敗 failover</td>
      </tr>
      <tr>
          <td><strong>2. Election</strong></td>
          <td>TTL 過、DCS 開放 leader lock</td>
          <td>Standby 競爭寫 leader key（DCS quorum-based）</td>
          <td>Network partition → 兩邊都自認 leader（split-brain）</td>
      </tr>
      <tr>
          <td><strong>3. Promotion</strong></td>
          <td>新 leader 寫 DCS key 成功</td>
          <td>跑 <code>pg_ctl promote</code>、停 streaming replication、開始接寫</td>
          <td>Standby 落後太多 → 拒 promote 或承接時資料缺</td>
      </tr>
      <tr>
          <td><strong>4. Reconfiguration</strong></td>
          <td>Patroni REST API 通知 routing 層</td>
          <td>HAProxy / PgBouncer 切流量到新 leader</td>
          <td>Routing 層 health check 慢 → 流量持續打舊 leader</td>
      </tr>
      <tr>
          <td><strong>5. Recovery</strong></td>
          <td>舊 leader 恢復（手動 / 自動）</td>
          <td>跑 <code>pg_rewind</code> + 重接 streaming replication 為 standby</td>
          <td>WAL divergence 太大 → 必須重 base backup</td>
      </tr>
  </tbody>
</table>
<p>每段都有獨立配置、不是「設一個 timeout 就好」。後面分段展開。</p>
<h2 id="stage-1detection--dcs-heartbeat-跟-ttl">Stage 1：Detection — DCS heartbeat 跟 TTL</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># patroni.yml 核心配置</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">scope</span><span class="p">:</span><span class="w"> </span><span class="l">myapp-pg-cluster</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">/db/</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">pg-node-1                               </span><span class="w"> </span><span class="c"># 跟 hostname 一致</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">etcd</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">hosts</span><span class="p">:</span><span class="w"> </span><span class="l">etcd1:2379,etcd2:2379,etcd3:2379      </span><span class="w"> </span><span class="c"># DCS quorum</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="nt">bootstrap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">dcs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">ttl</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">                                     </span><span class="c"># leader lock TTL</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">loop_wait</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">                               </span><span class="c"># patroni 主循環間隔</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">retry_timeout</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">                           </span><span class="c"># DCS retry 上限</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">maximum_lag_on_failover</span><span class="p">:</span><span class="w"> </span><span class="m">1048576</span><span class="w">            </span><span class="c"># standby 落後 1MB 內才能 promote</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">synchronous_mode</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">                     </span><span class="c"># async / sync 取捨</span></span></span></code></pre></div><p>關鍵直覺：</p>
<ul>
<li><strong>TTL (30s) = leader 失聯多久才被視為 dead</strong>。設太短（&lt; 15s）會把 transient network jitter 當 dead；設太長（&gt; 60s）unavailability 拖長</li>
<li><strong>loop_wait + retry_timeout &lt; TTL</strong>：Patroni 必須在 TTL 內成功跟 DCS 互動 N 次、<code>loop_wait=10 + retry_timeout=10</code> 給每個循環 20s buffer</li>
<li><strong>maximum_lag_on_failover</strong>：standby WAL 落後超過這個閾值就 <em>不參與 election</em>；防止「promote 一個落後 5 分鐘的 standby」資料丟失</li>
</ul>
<h2 id="stage-2election--dcs-quorum--watchdog-防-split-brain">Stage 2：Election — DCS quorum + watchdog 防 split-brain</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">watchdog</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">required                               </span><span class="w"> </span><span class="c"># required / automatic / off</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">device</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/watchdog</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">safety_margin</span><span class="p">:</span><span class="w"> </span><span class="m">5</span></span></span></code></pre></div><p>Election 期間最大風險是 <em>split-brain</em> — network partition 下、舊 leader 還活著但跟 DCS 斷線；新 leader 從 standby 升上來、application 同時連兩個 PostgreSQL 寫。資料 divergence 後 <em>無法自動 reconcile</em>。</p>
<p>防護機制兩層：</p>
<ol>
<li><strong>DCS quorum</strong>：etcd / Consul 至少 3 node、過半 quorum 才能寫 leader key — 少數派 partition 無法 elect 新 leader</li>
<li><strong>Watchdog (Linux kernel)</strong>：required mode 強制 — Patroni 必須定期 <em>poke</em> <code>/dev/watchdog</code>、若 Patroni 自己掛或被 OS 凍結、kernel 自動 reboot 整台機器、避免舊 leader 在 DCS 失聯後繼續接寫</li>
</ol>
<p>Watchdog <code>required</code> 是 production-grade 的硬要求 — <code>automatic</code> / <code>off</code> 在 split-brain 場景下無法防護。</p>
<h2 id="stage-3promotion--pg_ctl--replication-slot-切換">Stage 3：Promotion — pg_ctl + replication slot 切換</h2>
<p>新 leader 寫 DCS key 成功後、Patroni 自動執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Patroni 內部、不要手動跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pg_ctl promote -D /var/lib/postgresql/data
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># postgresql.auto.conf 移除 primary_conninfo</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># postgresql.auto.conf 重新計算 timeline ID</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 啟動接寫</span></span></span></code></pre></div><p>Promotion 期間關鍵議題：</p>
<ul>
<li><strong>timeline divergence</strong>：新 leader 開新 timeline ID（從 leader 失聯時的 LSN 開始）；其他 standby 需要 <code>pg_rewind</code> 把自己的 WAL fork 點對齊新 timeline</li>
<li><strong>replication slot 處理</strong>：舊 leader 上的 replication slot 在 DCS 中已 stale、新 leader 重建 slot；如果 logical replication consumer 沒 idempotent、會 replay 部分訊息</li>
<li><strong>promotion latency</strong>：通常 3-10 秒（pg_ctl 本身 &lt; 5s、加 DCS 寫確認）</li>
</ul>
<h2 id="stage-4reconfiguration--client-routing-切換">Stage 4：Reconfiguration — client routing 切換</h2>
<p>PostgreSQL 自己升 leader 還不夠、application 不知道；要靠前端 routing 層轉發。三種典型 pattern：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[client] → [HAProxy / pgBouncer] → [pg-node-1 (leader)]
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                 → [pg-node-2 (standby, read)]
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                 → [pg-node-3 (standby, read)]</span></span></code></pre></div><p>Patroni REST API 暴露 <code>/leader</code> / <code>/replica</code> / <code>/health</code> endpoint、HAProxy 用 <em>health check</em> 跑這些 endpoint：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># haproxy.cfg
</span></span><span class="line"><span class="ln">2</span><span class="cl">backend pg-write
</span></span><span class="line"><span class="ln">3</span><span class="cl">  option httpchk OPTIONS /leader
</span></span><span class="line"><span class="ln">4</span><span class="cl">  http-check expect status 200
</span></span><span class="line"><span class="ln">5</span><span class="cl">  server pg-node-1 pg-node-1:5432 check port 8008
</span></span><span class="line"><span class="ln">6</span><span class="cl">  server pg-node-2 pg-node-2:5432 check port 8008 backup
</span></span><span class="line"><span class="ln">7</span><span class="cl">  server pg-node-3 pg-node-3:5432 check port 8008 backup</span></span></code></pre></div><p>Reconfiguration 期間關鍵延遲：</p>
<ul>
<li>HAProxy health check 間隔（預設 2s）+ failure threshold（預設 3 次）= ~6s 切換感應</li>
<li>PgBouncer 不主動 health check、要靠 application 端 retry 跟 connection drop 觸發重連</li>
<li>整個 reconfiguration 端到端通常 10-20s（含 PostgreSQL promotion 時間）</li>
</ul>
<h2 id="stage-5recovery--pg_rewind-跟-base-backup-取捨">Stage 5：Recovery — pg_rewind 跟 base backup 取捨</h2>
<p>舊 leader 恢復後變 standby，但 WAL 已 divergence — 必須選一條 recovery path：</p>
<ul>
<li><strong><code>pg_rewind</code></strong>：rewind 舊 leader WAL 到分歧點、重新接 streaming replication；條件 = 分歧 WAL 量小（&lt; 幾 GB）且 timeline 可對齊</li>
<li><strong>重 base backup</strong>：用 <code>pg_basebackup</code> 從新 leader 拉完整 base + WAL；條件 = 任何時候都可、但時間長（TB 級 1-4 小時）</li>
</ul>
<p>Patroni 預設嘗試 pg_rewind、失敗才退 base backup。production 配置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">postgresql</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">use_pg_rewind</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">remove_data_directory_on_rewind_failure</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">   </span><span class="c"># rewind 失敗自動清 data dir、再 base backup</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">remove_data_directory_on_diverged_timelines</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div><h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1split-brain-due-to-dcs-partition">Case 1：Split-brain due to DCS partition</h3>
<p><strong>徵兆</strong>：兩個 PostgreSQL node 都在接寫、application 大量寫入 conflict / unique constraint violation。</p>
<p><strong>根因</strong>：DCS（etcd）partition — 兩個 etcd node 在 partition 兩側、都自認 quorum；其實是 split-vote、兩邊都不應該。Patroni 在兩邊各 elect 一個 leader。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>DCS 必須奇數 node（3 / 5 / 7）、過半 quorum 嚴格 enforce</li>
<li>DCS 部署跨 AZ / region 時、quorum size 要考慮 partition 機率（3 AZ 各 1 node 是 production 最低標）</li>
<li>Watchdog <code>required</code> mode 是最後一道閘門 — DCS partition 加 quorum 失靈時、watchdog 強制 reboot 失聯 node</li>
</ol>
<h3 id="case-2standby-落後太多無法-failover">Case 2：Standby 落後太多、無法 failover</h3>
<p><strong>徵兆</strong>：primary 失聯後、Patroni log 顯示 <code>Following members have lag greater than maximum_lag_on_failover</code>、所有 standby 都被拒 promote、cluster unavailable。</p>
<p><strong>根因</strong>：maximum_lag_on_failover 設 1MB、但 standby replication lag 累積到 50MB（write-heavy workload + slow disk on standby）。安全機制觸發、但代價是 <em>無 standby 可升</em>、需要人工降低門檻或等 standby catch up。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預防</strong>：standby 容量 / IO 對齊 primary、避免 lag 累積；prometheus alert <code>pg_replication_lag_bytes &gt; 10MB</code> 觸發前 catch</li>
<li><strong>臨時</strong>：手動 <code>patronictl edit-config</code> 把 maximum_lag_on_failover 暫時拉到 50MB、接受可能丟 50MB worth of writes、換 availability</li>
<li><strong>長期</strong>：sync replication（一個 standby 強制同步）、保證至少一個 standby zero-lag</li>
</ol>
<h3 id="case-3promotion-後-application-connection-storm">Case 3：Promotion 後 application connection storm</h3>
<p><strong>徵兆</strong>：failover 完成後 30-120 秒內、application log 大量 <code>connection refused</code> / <code>password authentication failed</code>、application 自己 retry storm。</p>
<p><strong>根因</strong>：新 leader 剛 promote、PostgreSQL <code>max_connections</code> 容量還在 warm up（shared memory / cache 未 prime）、application 同時湧入大量 connection request；應用 retry 不夠 jitter、queue 堆積。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Application 用 <em>exponential backoff with jitter</em>、不要 immediate retry</li>
<li>PgBouncer / connection pool 限制每 application instance 對 PG 的 connection 上限、不直連 PG</li>
<li>預先在 standby 跑 <code>pg_prewarm</code> 把熱表 cache 預熱、promotion 後 cache miss 不爆</li>
</ol>
<h3 id="case-4pg_rewind-失敗退到-base-backup-沒做">Case 4：pg_rewind 失敗、退到 base backup 沒做</h3>
<p><strong>徵兆</strong>：舊 leader 恢復後、Patroni log 顯示 <code>pg_rewind failed</code>、舊 leader 一直 STARTING、無法重接 cluster；SRE 手動跑 pg_basebackup 才恢復。</p>
<p><strong>根因</strong>：<code>remove_data_directory_on_rewind_failure: false</code>（預設）— rewind 失敗時 Patroni 不主動清 data dir、需要 SRE 手動處理；運維沒 runbook、卡在這步幾小時。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Production 設 <code>remove_data_directory_on_rewind_failure: true</code> + <code>remove_data_directory_on_diverged_timelines: true</code>、讓 Patroni 自動 fallback</li>
<li>data dir 跑在獨立 PV / disk、清掉風險可控（不要跑 root disk）</li>
<li>容量規劃：base backup 時間預估納入 RTO（TB 級 base backup 1-4 小時、不是 RTO 30 分鐘所能承受）</li>
</ol>
<h3 id="case-5watchdog-觸發整機-reboot誤殺">Case 5：Watchdog 觸發整機 reboot、誤殺</h3>
<p><strong>徵兆</strong>：production server 在無故障時 unexpected reboot、<code>dmesg</code> 顯示 <code>watchdog: BUG: soft lockup</code>。</p>
<p><strong>根因</strong>：Patroni 主循環因 etcd 短暫慢回應卡住 60+ 秒、kernel watchdog 觸發 reboot；但實際 PostgreSQL 沒 hang、是 Patroni-watchdog 鏈過敏。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>safety_margin</code> 設大一點（10-15）、給 Patroni loop_wait 抖動空間</li>
<li>etcd 跟 Patroni 部署在低延遲 network 內（同 AZ &lt; 5ms）、跨 region etcd 不建議</li>
<li>watchdog device 用 softdog（軟體模擬）vs 硬體 watchdog、debug 時 softdog 容易觀察</li>
</ol>
<h2 id="容量規劃">容量規劃</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster size</td>
          <td>3-5 node（含 leader + 2-4 standby）</td>
          <td>&lt; 3 不能 HA（單 standby 失敗整 cluster 掛）</td>
      </tr>
      <tr>
          <td>DCS size</td>
          <td>3 / 5 / 7 node（奇數 quorum）</td>
          <td>etcd 5 node 是 prod standard</td>
      </tr>
      <tr>
          <td>TTL</td>
          <td>30s（default 30、production 20-60）</td>
          <td>&lt; 15s 過敏、&gt; 60s 過鈍</td>
      </tr>
      <tr>
          <td>maximum_lag_on_failover</td>
          <td>1MB（default）</td>
          <td>大表 write-heavy 可放 10-100MB</td>
      </tr>
      <tr>
          <td>Synchronous standby</td>
          <td>1 個 sync + N 個 async 是 production 預設</td>
          <td>全 async 容易丟資料、全 sync write latency 爆</td>
      </tr>
      <tr>
          <td>RTO</td>
          <td>10-30 秒（detection 30s 內 + promotion 5-10s + reconfig 5s）</td>
          <td>&gt; 60s 要 audit 鏈路</td>
      </tr>
      <tr>
          <td>RPO</td>
          <td>sync mode 接近 0、async mode 跟 lag 同數量級</td>
          <td>async 在 disk IO 慢時 lag 可能 MB-GB level</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-pgbouncer-整合">跟 <a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">PgBouncer</a> 整合</h3>
<p>PgBouncer 不主動感知 Patroni failover、要靠：</p>
<ol>
<li><strong>HAProxy 在 PgBouncer 上層</strong>：HAProxy 跑 Patroni health check、PgBouncer connection 重新路由</li>
<li><strong>PgBouncer reload</strong>：failover 後 SRE / automation 跑 <code>pgbouncer -R</code>、強制重連 backend</li>
<li><strong>Connection pool drain</strong>：application 端 connection pool 設 <code>pool_lifetime_max=5min</code>、舊 connection 自然汰換</li>
</ol>
<h3 id="跟-cert-managertls-rotation">跟 cert-manager（TLS rotation）</h3>
<p>Patroni REST API 跟 PostgreSQL streaming replication 都用 TLS、cert rotation 不能停服務：</p>
<ol>
<li>cert-manager 自動換證後、Patroni 跟 PostgreSQL 都需要 reload（不是 restart）</li>
<li><code>patronictl reload &lt;cluster&gt;</code> 不會觸發 failover、只 reload config</li>
<li>PostgreSQL <code>pg_ctl reload</code> 是 SIGHUP、平滑載入新 cert</li>
</ol>
<h3 id="跟-backup--pitr">跟 backup / PITR</h3>
<p>Patroni 不管 backup — 但 standby promotion 後、WAL archive 必須跟新 leader 的 timeline 對齊：</p>
<ol>
<li>WAL archive 命令模板含 <code>%t</code>（timeline）：<code>archive_command = 'wal-g wal-push %p'</code></li>
<li>Backup tool（pgBackRest / WAL-G）支援 timeline 切換、archive 不會中斷</li>
<li>詳見 <a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL archiving deep article</a></li>
</ol>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Multi-region Patroni</strong>：跨 region 部署的 DCS quorum 設計、跟單 region 的取捨完全不同</li>
<li><strong>PostgreSQL 16+ streaming replication slot 持久化</strong>：簡化 standby promotion 後 logical consumer 重連</li>
<li><strong>跟 Kubernetes operator 整合</strong>：Patroni 跑在 K8s 時、StatefulSet + pod identity + DCS 部署模式</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></li>
<li>上游 chapter：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">High Concurrency Access</a> — connection / replication / HA 全鏈</li>
<li>平行 deep article：<a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a> / <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault Dynamic Credential</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL Orchestrator Failover：HA 工具自己怎麼 HA？raft cluster + GTID-based promotion 的兩段 paradox</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/orchestrator-failover/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/orchestrator-failover/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 &lt;em>Orchestrator failover&lt;/em> — 自動 HA 的工具雙層架構跟 5 段 decision tree。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;blockquote>
&lt;p>用詞註：Orchestrator 工具命名與 MySQL 5.7- SQL 命令（&lt;code>SHOW SLAVE STATUS&lt;/code> / &lt;code>CHANGE MASTER TO&lt;/code> / &lt;code>STOP SLAVE&lt;/code> 等）沿用 &lt;em>master / slave&lt;/em>。MySQL 8.0+ 改採 &lt;em>primary / replica&lt;/em>、但 SQL syntax 仍保留別名。本文出現 master / slave 處對應 8.0 primary / replica 概念。&lt;/p>&lt;/blockquote>
&lt;p>讀者第一個會問的問題：「Orchestrator 自己會壞嗎？壞了誰 failover Orchestrator？」這個 paradox 是 &lt;em>任何 HA 工具&lt;/em> 的核心議題、PostgreSQL 的 Patroni 用 DCS（etcd / Consul）解決、MySQL 的 Orchestrator 用 &lt;em>內建 raft cluster&lt;/em> 解決：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">被管的 (Layer 1): primary MySQL → replica MySQL → replica MySQL → ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">管理者 (Layer 2): orchestrator instance × 3 (or 5) — 用 raft 自己選 leader
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">管理者狀態存放 (Layer 3): 每個 orchestrator instance 自己有 MySQL backend (state)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Orchestrator 3 個 instance 構成 &lt;em>raft cluster&lt;/em>、自己選 leader。Leader 才有 &lt;em>寫入 state&lt;/em> + &lt;em>發起 failover&lt;/em> 權限、其他 instance follower 同步 state。Leader 失聯 → raft 重新選 leader（&amp;lt; 10 秒）、新 leader 繼續 manage MySQL topology。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &amp;#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni&lt;/a> 不同：Patroni 需要 &lt;em>外部 DCS&lt;/em>（etcd / Consul）作為 source of truth、Patroni 本身 stateless；Orchestrator 內建 raft、不需要外部 DCS、但每個 orchestrator instance 需要 &lt;em>自己的 MySQL backend&lt;/em> 存 state。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 <em>Orchestrator failover</em> — 自動 HA 的工具雙層架構跟 5 段 decision tree。</p></blockquote>
<hr>
<blockquote>
<p>用詞註：Orchestrator 工具命名與 MySQL 5.7- SQL 命令（<code>SHOW SLAVE STATUS</code> / <code>CHANGE MASTER TO</code> / <code>STOP SLAVE</code> 等）沿用 <em>master / slave</em>。MySQL 8.0+ 改採 <em>primary / replica</em>、但 SQL syntax 仍保留別名。本文出現 master / slave 處對應 8.0 primary / replica 概念。</p></blockquote>
<p>讀者第一個會問的問題：「Orchestrator 自己會壞嗎？壞了誰 failover Orchestrator？」這個 paradox 是 <em>任何 HA 工具</em> 的核心議題、PostgreSQL 的 Patroni 用 DCS（etcd / Consul）解決、MySQL 的 Orchestrator 用 <em>內建 raft cluster</em> 解決：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">被管的 (Layer 1):       primary MySQL → replica MySQL → replica MySQL → ...
</span></span><span class="line"><span class="ln">2</span><span class="cl">管理者 (Layer 2):       orchestrator instance × 3 (or 5) — 用 raft 自己選 leader
</span></span><span class="line"><span class="ln">3</span><span class="cl">管理者狀態存放 (Layer 3): 每個 orchestrator instance 自己有 MySQL backend (state)</span></span></code></pre></div><p>Orchestrator 3 個 instance 構成 <em>raft cluster</em>、自己選 leader。Leader 才有 <em>寫入 state</em> + <em>發起 failover</em> 權限、其他 instance follower 同步 state。Leader 失聯 → raft 重新選 leader（&lt; 10 秒）、新 leader 繼續 manage MySQL topology。</p>
<p>跟 <a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni</a> 不同：Patroni 需要 <em>外部 DCS</em>（etcd / Consul）作為 source of truth、Patroni 本身 stateless；Orchestrator 內建 raft、不需要外部 DCS、但每個 orchestrator instance 需要 <em>自己的 MySQL backend</em> 存 state。</p>
<h2 id="orchestrator-雙層架構管-mysql-的-layer-2">Orchestrator 雙層架構：管 MySQL 的 Layer 2</h2>
<p>Layer 1 是 <em>被管的</em> MySQL cluster — primary + replica 群。Layer 2 是 <em>管理者</em> — orchestrator instance 群。Layer 2 監視 Layer 1、Layer 2 自己用 raft 自管。</p>
<p><strong>Layer 1 對 Orchestrator 的需求</strong>：</p>
<ul>
<li>所有 MySQL server 啟用 <code>binlog</code> + <code>log_slave_updates</code>（讓 Orchestrator 看得到 binlog event）</li>
<li>啟用 GTID（Orchestrator failover decision 依賴 GTID 比較進度、不用算 binlog position）</li>
<li>每個 server 有 <em>orchestrator user</em>（<code>GRANT SUPER, REPLICATION CLIENT, REPLICATION SLAVE, PROCESS ON *.* TO 'orchestrator'@'%'</code>）</li>
</ul>
<p><strong>Layer 2 配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># /etc/orchestrator.conf.json (簡化)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="na">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="na">&#34;MySQLOrchestratorHost&#34;: &#34;orchestrator-backend.example.com&#34;,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="na">&#34;MySQLOrchestratorPort&#34;: 3306,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="na">&#34;MySQLOrchestratorDatabase&#34;: &#34;orchestrator&#34;,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1"># 用 backend MySQL（每個 orchestrator instance 自己一個）+ raft 同步</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="na">&#34;RaftEnabled&#34;: true,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="na">&#34;RaftDataDir&#34;: &#34;/var/lib/orchestrator&#34;,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="na">&#34;RaftBind&#34;: &#34;10.0.1.10:10008&#34;,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="na">&#34;RaftNodes&#34;: [</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="na">&#34;orchestrator1.example.com:10008&#34;,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="na">&#34;orchestrator2.example.com:10008&#34;,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="na">&#34;orchestrator3.example.com:10008&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="na">],</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="c1"># Topology discovery</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="na">&#34;DiscoverByShowSlaveHosts&#34;: true,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="na">&#34;InstancePollSeconds&#34;: 5,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="c1"># Failover detection</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="na">&#34;FailureDetectionPeriodBlockMinutes&#34;: 60,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="na">&#34;RecoveryPeriodBlockSeconds&#34;: 3600,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="c1"># Failover automation</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="na">&#34;RecoverMasterClusterFilters&#34;: [&#34;*&#34;],</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="na">&#34;RecoverIntermediateMasterClusterFilters&#34;: [&#34;*&#34;],</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="na">&#34;PreFailoverProcesses&#34;: [&#34;/usr/local/bin/orchestrator-fence-master.sh&#34;],</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="na">&#34;PostFailoverProcesses&#34;: [&#34;/usr/local/bin/orchestrator-notify-proxysql.sh&#34;]</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="na">}</span></span></span></code></pre></div><h2 id="stage-1topology-discovery--自動發現--manual-seed">Stage 1：Topology Discovery — 自動發現 + manual seed</h2>
<p>Orchestrator 啟動後 <em>seed</em> 一個或多個 MySQL server、自動發現整個 topology：</p>
<ul>
<li>連 seed server → <code>SHOW SLAVE HOSTS</code> → 發現所有 replica</li>
<li>對每個 replica 跑 <code>SHOW MASTER STATUS</code> + <code>SHOW SLAVE STATUS</code> → 建立 <em>父子關係 graph</em></li>
<li>持續 poll（<code>InstancePollSeconds=5</code>）每 5 秒更新 topology state</li>
</ul>
<p><strong>Topology graph 的 node</strong>：</p>
<ul>
<li><em>Master</em>：no slave status、被多個 replica 指</li>
<li><em>Intermediate master</em>：有 slave status 也有下游 replica（chained replication）</li>
<li><em>Co-master</em>：互相 replicate（罕見、active-passive failover 場景）</li>
<li><em>Replica</em>：有 slave status、無下游</li>
</ul>
<p>Topology 可視化：Orchestrator UI（web）顯示 cluster 樹狀圖、操作員可手動 drag-and-drop replica 重新 attach。</p>
<h2 id="stage-2failure-detection--區分真壞跟假壞">Stage 2：Failure Detection — 區分真壞跟假壞</h2>
<p>Orchestrator 不是 <em>單一 ping 失敗就 failover</em>、有 <em>holistic detection</em>：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>解讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Master <code>connect fail</code></td>
          <td>可能 network blip、不一定真壞</td>
      </tr>
      <tr>
          <td>Master <code>timeout poll</code></td>
          <td>可能 master loaded、不一定真壞</td>
      </tr>
      <tr>
          <td><strong>Replica 全部 <code>IO error</code></strong></td>
          <td>Master 真的對 replica 不可達、強訊號</td>
      </tr>
      <tr>
          <td>Replica 看到 master 還活著</td>
          <td>Master 對 orchestrator 不可達、可能是 <em>orchestrator network</em> 問題、不是 master</td>
      </tr>
      <tr>
          <td>Replica lag 暴增</td>
          <td>Master 可能還活著但 overload、不一定要 failover</td>
      </tr>
  </tbody>
</table>
<p><strong>Detection rule</strong>：Master <em>自己連不上</em> + <em>至少一個 replica 也看 master IO error</em> → 判定 <code>DeadMaster</code>。單一 orchestrator 連不上 master 不觸發 — 防 orchestrator network 隔離造成的 false positive failover。</p>
<h2 id="stage-3failover-decision-tree--選哪個-replica-promote">Stage 3：Failover Decision Tree — 選哪個 replica promote</h2>
<p>判定 <code>DeadMaster</code> 後不是 <em>選最近的 replica</em>、用 decision tree：</p>
<ol>
<li><strong>GTID 最新的 replica</strong>：跟舊 master 同步最完整（用 <code>Executed_Gtid_Set</code> 對比）</li>
<li><strong>同 DC / AZ 的 replica</strong>（如果有 multi-DC 配置）</li>
<li><strong>手動指定的 promotion candidate</strong>（<code>promote_rule=must</code> 或 <code>prefer</code>）</li>
<li><strong>Semi-sync ack 的 replica</strong>（如果 semi-sync 啟用）</li>
</ol>
<p>GTID 最新是基本要求。其他規則是 <em>tie-breaker</em>。</p>
<p><strong>Errant transaction 處理</strong>：選出的 candidate replica 如果有 <em>errant GTID</em>（master 沒有但 replica 有的 transaction）、Orchestrator <em>不會 promote 這個 replica</em>（怕 errant transaction 變成 new master state）。改選次優 candidate。</p>
<h2 id="stage-4promote-action--5-步-atomic理想情況">Stage 4：Promote Action — 5 步 atomic（理想情況）</h2>
<p>選好 candidate 後執行：</p>
<ol>
<li><strong>Fence 舊 master</strong>（pre-failover hook）：把舊 master 對外停掉、防 split-brain</li>
<li><strong>STOP SLAVE on candidate</strong>：candidate 不再從舊 master pull binlog</li>
<li><strong>RESET SLAVE ALL on candidate</strong>：candidate 清掉 slave 配置、變成獨立 master</li>
<li><strong>Re-attach 其他 replica</strong>：用 <code>CHANGE MASTER TO MASTER_HOST=&lt;candidate&gt;, MASTER_AUTO_POSITION=1</code>（GTID auto-position）</li>
<li><strong>Post-failover hook</strong>：通知 ProxySQL / HAProxy / DNS 切流量</li>
</ol>
<p>每步任一失敗、Orchestrator 可能停在中間狀態、需要 <em>人工介入</em>。</p>
<h2 id="stage-5recovery--old-master-怎麼處理">Stage 5：Recovery — Old master 怎麼處理</h2>
<p>Failover 完、舊 master 可能：</p>
<ul>
<li><em>真的死了</em>：物理 server 故障 / region outage → 不必處理、未來修好作為新 replica re-attach</li>
<li><em>Network blip 後復活</em>：舊 master 自己 <em>仍認為自己是 master</em>、再次接受寫入會造成 split-brain</li>
</ul>
<p>修法：</p>
<ul>
<li><em>Fencing</em>（必須）：pre-failover hook 把舊 master 對外 firewall 掉、或 force <code>read_only=1</code>、防舊 master 復活後接受寫入</li>
<li><em>Manual reset</em>：舊 master 復活後人工 confirm 是否變成新 master 的 replica（不要自動、自動容易誤判）</li>
</ul>
<p>Orchestrator UI 在偵測到 errant master 時會標 warning、不會自動處理。</p>
<h2 id="5-個-production-踩雷">5 個 Production 踩雷</h2>
<h3 id="1-split-brain--pre-failover-hook-沒-fence-舊-master">1. Split-brain — pre-failover hook 沒 fence 舊 master</h3>
<p>舊 master network blip 後復活、orchestrator 已 promote 新 master、application 部分 instance 連舊 master、部分連新 master、雙寫造成 data divergence。</p>
<p>修法：</p>
<ul>
<li><em>Pre-failover hook 必須 fence</em>（不是可選）：
<ul>
<li>物理 fencing：透過 IPMI 重啟 / 關 server</li>
<li>Network fencing：透過 firewall rule 切斷 server 對外連線</li>
<li>MySQL fencing：<code>SET GLOBAL read_only=1</code> + <code>KILL</code> 所有 active connection</li>
</ul>
</li>
<li>用 <em>VIP / DNS</em> 配合：fence 完才切 VIP / DNS 到新 master、避免 application 連舊 IP</li>
<li>不依賴 application 連線 string 動態變更（DNS TTL 期間仍可能連舊 IP）</li>
</ul>
<h3 id="2-pre-failover-hook-失敗--orchestrator-該停還是該繼續">2. Pre-failover hook 失敗 — Orchestrator 該停還是該繼續</h3>
<p>Pre-failover hook 跑失敗（fence script 因為 SSH 不通、IPMI 沒回應）。Orchestrator 有兩種策略：</p>
<ul>
<li><em>PostponeReplicaRecoveryOnLagMinutes</em>：等 hook 成功才繼續、可能永遠 stuck</li>
<li><em>FailMasterPromotionOnLagMinutes</em>：放棄 promotion、留 cluster degraded（無 master）</li>
</ul>
<p>兩者都不理想。多數 production 選 <em>PostponeReplicaRecoveryOnLagMinutes=10</em>：等 10 分鐘 hook 成功、超時則 alert 人工介入、不繼續 auto-promote（人工 review 才是正確選擇）。</p>
<h3 id="3-anti-flapping-窗口太短--master-抖動-vs-真死">3. Anti-flapping 窗口太短 — Master 抖動 vs 真死</h3>
<p><code>FailureDetectionPeriodBlockMinutes=60</code>：偵測一次 failure 後 60 分鐘內不再 trigger failover（即使再偵測到 failure）。預設 60 分鐘對 <em>第一次 failover 後 master 仍不穩</em> 的場景太長 — 60 分鐘內 master 真的死了第二次、orchestrator 不 failover。預設 60 分鐘對 <em>網路抖動</em> 的場景太短 — 60 分鐘內可能 multiple failover、cluster 一直在 promote。</p>
<p>修法：</p>
<ul>
<li>評估自己 cluster 的 <em>typical recovery time</em>：1-2 小時、設 <code>FailureDetectionPeriodBlockMinutes=120</code></li>
<li>監控 <em>failover 頻率</em>、單週 &gt; 2 次表示底層問題（網路 / hardware）、不是調 anti-flapping window 解決</li>
</ul>
<h3 id="4-gtid-errant-transaction--orchestrator-拒絕-promote-但沒講原因">4. GTID errant transaction — Orchestrator 拒絕 promote 但沒講原因</h3>
<p>Candidate replica 有 <em>errant GTID</em>（從別處 inject 的 transaction）、Orchestrator 拒絕 promote、log 訊息 <code>errant GTID detected</code>、但 <em>沒寫實際是哪個 GTID</em>。On-call 在事故中沒辦法 debug。</p>
<p>修法：</p>
<ul>
<li>平時 <em>監控 errant GTID</em>：定期跑 <code>pt-show-grants</code> + GTID 比對、不要等 failover 才發現</li>
<li>Orchestrator 的 <code>OrchestratorIssuesAGtidPurge</code> 設 true：preview mode 看 errant GTID 的位置</li>
<li>Errant GTID 來源通常是 <em>人為 inject</em>（DBA 直接寫 replica 然後 binlog 出現）、教育 DBA 不要直接連 replica 寫</li>
</ul>
<h3 id="5-vip--proxysql-整合斷層--切流量延遲">5. VIP / ProxySQL 整合斷層 — 切流量延遲</h3>
<p>Post-failover hook 跑完 <em>script 上報</em>「我切完了」、但實際 <em>VIP / DNS / ProxySQL 還沒看到變化</em>。Application 連 stale endpoint 30 秒、寫入失敗。</p>
<p>修法：</p>
<ul>
<li><em>Post-failover hook 不只 trigger 切換、要 wait 切換完成</em>：
<ul>
<li>VIP：等 <code>arping</code> 確認新 IP 已 propagate</li>
<li>ProxySQL：等 <code>mysql_servers</code> runtime table 更新 + 確認 monitor module 看到新 primary</li>
<li>DNS：先把 TTL 降到極短（5 秒）、再切 DNS、等 TTL 過</li>
</ul>
</li>
<li>Orchestrator <code>PostFailoverProcessesFailOnError=true</code>：hook 失敗整個 failover 標記失敗、人工檢查</li>
<li>ProxySQL 用 <code>mysql_replication_hostgroups</code> 自動偵測 read_only flag、可不依賴 hook（推薦）</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>配置建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Orchestrator instance 數量</td>
          <td>3（raft cluster 最小、odd number、容忍 1 個故障）</td>
      </tr>
      <tr>
          <td>每個 instance MySQL backend</td>
          <td>1 個獨立 MySQL（不要共用、不要用被管的 cluster）</td>
      </tr>
      <tr>
          <td>Backend MySQL spec</td>
          <td>t3.small 級別、Orchestrator state ~1 GB</td>
      </tr>
      <tr>
          <td>Network latency</td>
          <td>raft 同 region 內、跨 AZ 可接受（&lt; 5ms）、跨 region 不推薦</td>
      </tr>
      <tr>
          <td>InstancePollSeconds</td>
          <td>5 秒（預設）— 越小越敏感、越大越省連線</td>
      </tr>
  </tbody>
</table>
<p>3 instance raft cluster 容忍 1 instance 故障。5 instance 容忍 2 instance 故障但 quorum cost 高、99% 場景 3 個夠用。</p>
<h2 id="跟其他模組整合">跟其他模組整合</h2>
<h3 id="跟-replication-topology">跟 Replication topology</h3>
<p>Orchestrator 100% 依賴 GTID + binlog ROW format（<a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">Replication Topology</a>）。沒 GTID 用 binlog position、failover 時 re-pointing 容易出錯、Orchestrator 強烈建議 GTID。</p>
<h3 id="跟-proxysql">跟 ProxySQL</h3>
<p><a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">ProxySQL</a> 用 <code>mysql_replication_hostgroups</code> 自動偵測 <code>read_only</code> flag — orchestrator 切完新 master 後、ProxySQL monitor module 自動看到新 master 的 <code>read_only=0</code>、自動更新 routing、application 不用改 connection string。</p>
<p>這個 <em>無需 post-failover hook 通知 ProxySQL</em> 的整合是 ProxySQL + Orchestrator 組合的最大優勢、比手動 hook 通知 VIP / DNS 可靠。</p>
<h3 id="跟-patronipostgresql-對應">跟 Patroni（PostgreSQL 對應）</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Orchestrator</th>
          <th>Patroni</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DCS</td>
          <td>內建 raft（不需外部）</td>
          <td>外部（etcd / Consul / ZooKeeper）</td>
      </tr>
      <tr>
          <td>State storage</td>
          <td>每 instance 一個 MySQL backend</td>
          <td>DCS 本身</td>
      </tr>
      <tr>
          <td>Topology discovery</td>
          <td>自動 + manual seed</td>
          <td>自動（透過 DCS）</td>
      </tr>
      <tr>
          <td>Fencing</td>
          <td>Pre-failover hook（自實作）</td>
          <td>Watchdog（內建）</td>
      </tr>
      <tr>
          <td>5+ year 生產驗證</td>
          <td>GitHub / Booking.com / Shopify</td>
          <td>Zalando / 多個歐美企業</td>
      </tr>
  </tbody>
</table>
<p>兩者角色相同、設計取捨不同。Patroni 對 DCS 高依賴、Orchestrator 對自己 backend MySQL 高依賴。</p>
<h3 id="跟-rds--aurora-mysql">跟 RDS / Aurora MySQL</h3>
<p>AWS RDS / Aurora 內建 multi-AZ failover、<em>不用 Orchestrator</em>。Aurora failover &lt; 30 秒、RDS failover ~60-120 秒。Aurora 把 replication / failover 整套封進 storage layer、application 看到的是 reader endpoint + writer endpoint。</p>
<p>詳見 <a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor page</a>。</p>
<h3 id="跟-vitess">跟 Vitess</h3>
<p>Vitess shard 內部用 <em>VTOrc</em>（Vitess fork of Orchestrator）— 概念跟 Orchestrator 一致、針對 Vitess topology metadata 適配。</p>
<p>詳見 <em>Vitess sharding 設計</em> 篇（待寫）。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL vendor overview</a></li>
<li><a href="/blog/backend/01-database/vendors/mysql/replication-topology/" data-link-title="MySQL Replication Topology：async / semi-sync / GTID 不是三選一、是三個 trade-off 軸的疊加" data-link-desc="MySQL replication 不是「選 async 還是 semi-sync」、是 *durability / latency / consistency* 三個 trade-off 軸的疊加；GTID 是跨 mode 的 infrastructure layer、不是第三種 mode。本文走 3 軸取捨模型 → async / semi-sync 行為對比 → GTID 替代 binlog-position 的好處 → 配置 step-by-step → 5 production 踩雷（lag 暴衝 / semi-sync 退回 async / GTID gap / Loss-Less semi-sync 真的 loss-less / chained replication 雪崩）→ 跟 Aurora MySQL / Vitess / ProxySQL / Orchestrator 整合">MySQL Replication Topology</a>（GTID 是 Orchestrator pre-requisite）</li>
<li><a href="/blog/backend/01-database/vendors/mysql/proxysql-config/" data-link-title="MySQL ProxySQL 配置：connection / query / route / response 四段 lifecycle 跟 query rule 設計" data-link-desc="ProxySQL 是 MySQL 生態的 connection pool &#43; query routing 標準。本文走 connection → query parse → route → response 四段 lifecycle、query rule engine 的 rule chain 設計、Hostgroup / Server / User 三層 schema、配置 step-by-step（讀寫分離 &#43; replica lag-aware routing）、5 production 踩雷（query rule 順序錯亂 / connection 漂移 / write 路由到 replica / runtime / disk schema drift / mirror traffic 副作用）、跟 Replication / Orchestrator / HAProxy 整合">MySQL ProxySQL 配置</a>（Orchestrator + ProxySQL 自動失效切換組合）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">PostgreSQL Patroni HA</a>（PG sibling、不同 HA 機制）</li>
<li><a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor page</a>（managed MySQL、Orchestrator 不需要）</li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum 卡片</a> / <a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover 卡片</a></li>
<li>官方：<a href="https://github.com/openark/orchestrator">orchestrator GitHub</a> / <a href="https://github.com/openark/orchestrator/tree/master/docs">orchestrator docs</a></li>
</ul>
]]></content:encoded></item><item><title>Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。Sentinel 處理的是「單 master 容量夠、但 master 不能是單點」的 HA 場景；要橫向擴容超過單機記憶體則走 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding&lt;/a>，兩者解的問題不同。機制以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/">Redis Sentinel 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="failover-是一條時序鏈不是一個瞬間">Failover 是一條時序鏈、不是一個瞬間&lt;/h2>
&lt;p>「master 掛了 Sentinel 會自動切換」這句話把 failover 講成一個原子動作，但真正在 production 出事時，問題永遠出在這條鏈的某一段卡住。把 failover 攤開成時序，才看得到延遲跟資料遺失藏在哪：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">T0 master 失去回應
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> ↓ (down-after-milliseconds)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">T1 單一 Sentinel 標記 master 為 SDOWN（主觀下線）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> ↓ (Sentinel 之間互問)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">T2 達到 quorum 數量的 Sentinel 同意 → ODOWN（客觀下線）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> ↓ (Sentinel 之間選出 leader 來主導 failover)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">T3 leader Sentinel 從 replica 中挑一個當新 master
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> ↓ (SLAVEOF NO ONE + 其他 replica 改指向新 master)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">T4 新 master 提升完成
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> ↓ (Sentinel 廣播新 topology、更新 DNS / 通知 client)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">T5 client 發現新 master、重連、恢復寫入&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>從 T0 到 T5 的總時間決定了「寫入中斷多久」。每一段都有對應的旋鈕跟失敗模式：T0→T1 由 &lt;code>down-after-milliseconds&lt;/code> 控制（太短誤判、太長反應慢）；T1→T2 由 quorum 設定控制（太低腦裂風險、太高切不動）；T4→T5 由 client 的 topology 感知能力控制。理解 failover 就是理解這條鏈的每一段。&lt;/p>
&lt;p>對把 cache 當主要 serving layer 的服務，這條鏈的長度直接是業務中斷時間。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>每次滑動讀多個 cache、cache miss 是邊緣案例——failover 期間若寫入中斷十幾秒，新寫入的 profile 互動全部 hang，sub-millisecond 的 SLA 在這幾秒徹底失守。這也是為什麼大規模服務多半走 managed multi-AZ failover（見 ElastiCache）而非自管 Sentinel。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。Sentinel 處理的是「單 master 容量夠、但 master 不能是單點」的 HA 場景；要橫向擴容超過單機記憶體則走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a>，兩者解的問題不同。機制以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/">Redis Sentinel 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="failover-是一條時序鏈不是一個瞬間">Failover 是一條時序鏈、不是一個瞬間</h2>
<p>「master 掛了 Sentinel 會自動切換」這句話把 failover 講成一個原子動作，但真正在 production 出事時，問題永遠出在這條鏈的某一段卡住。把 failover 攤開成時序，才看得到延遲跟資料遺失藏在哪：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">T0   master 失去回應
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">     ↓ (down-after-milliseconds)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">T1   單一 Sentinel 標記 master 為 SDOWN（主觀下線）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">     ↓ (Sentinel 之間互問)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">T2   達到 quorum 數量的 Sentinel 同意 → ODOWN（客觀下線）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">     ↓ (Sentinel 之間選出 leader 來主導 failover)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">T3   leader Sentinel 從 replica 中挑一個當新 master
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">     ↓ (SLAVEOF NO ONE + 其他 replica 改指向新 master)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">T4   新 master 提升完成
</span></span><span class="line"><span class="ln">10</span><span class="cl">     ↓ (Sentinel 廣播新 topology、更新 DNS / 通知 client)
</span></span><span class="line"><span class="ln">11</span><span class="cl">T5   client 發現新 master、重連、恢復寫入</span></span></code></pre></div><p>從 T0 到 T5 的總時間決定了「寫入中斷多久」。每一段都有對應的旋鈕跟失敗模式：T0→T1 由 <code>down-after-milliseconds</code> 控制（太短誤判、太長反應慢）；T1→T2 由 quorum 設定控制（太低腦裂風險、太高切不動）；T4→T5 由 client 的 topology 感知能力控制。理解 failover 就是理解這條鏈的每一段。</p>
<p>對把 cache 當主要 serving layer 的服務，這條鏈的長度直接是業務中斷時間。<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>每次滑動讀多個 cache、cache miss 是邊緣案例——failover 期間若寫入中斷十幾秒，新寫入的 profile 互動全部 hang，sub-millisecond 的 SLA 在這幾秒徹底失守。這也是為什麼大規模服務多半走 managed multi-AZ failover（見 ElastiCache）而非自管 Sentinel。</p>
<h2 id="核心概念sentinel-的判定模型">核心概念：Sentinel 的判定模型</h2>
<p>Sentinel 是獨立於 Redis 資料節點的監控進程，它的判定靠兩層共識避免單一 Sentinel 誤判。</p>
<p><strong>SDOWN（Subjectively Down，主觀下線）</strong>：單一 Sentinel 在 <code>down-after-milliseconds</code> 內收不到 master 的有效回應（<code>PING</code>），就主觀認定它下線。這只是一個 Sentinel 的意見，不觸發 failover。</p>
<p><strong>ODOWN（Objectively Down，客觀下線）</strong>：當標記 SDOWN 的 Sentinel 數量達到 <code>quorum</code> 設定值，master 被客觀認定下線。只有 master 的 ODOWN 才會觸發 failover（replica 的下線只標記不 failover）。</p>
<p><code>quorum</code> 是「多少個 Sentinel 同意才算真的下線」，它跟「多少個 Sentinel 同意才能執行 failover」是兩個不同的數字——後者需要 Sentinel 的多數（majority），確保同時只有一個 leader 主導 failover，避免兩個 Sentinel 各自提升不同 replica 造成腦裂。</p>
<p><strong>為什麼 Sentinel 要部署奇數個且至少三個</strong>：quorum 跟 majority 都需要足夠的 Sentinel 投票。兩個 Sentinel 無法在其中一個故障時達成 majority；三個才能容忍一個故障。Sentinel 應部署在不同故障域（不同 AZ / 機架），且不要跟 Redis 資料節點同生共死。</p>
<p><strong>Sentinel 不是 proxy</strong>：client 不透過 Sentinel 讀寫資料。client 向 Sentinel 查詢「現在的 master 是誰」，拿到地址後直連 Redis。failover 後 client 必須重新向 Sentinel 查詢——這是 T4→T5 的關鍵，client library 要支援 Sentinel 模式才能自動完成。</p>
<h2 id="配置sentinel-的設定路徑">配置：Sentinel 的設定路徑</h2>
<p>最小三 Sentinel 配置，每個 Sentinel 一份 <code>sentinel.conf</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># sentinel.conf</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 監控名為 mymaster 的 master、quorum=2（三個 Sentinel 中兩個同意算 ODOWN）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sentinel monitor mymaster 10.0.0.1 <span class="m">6379</span> <span class="m">2</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 多久收不到回應算 SDOWN（5 秒）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">sentinel down-after-milliseconds mymaster <span class="m">5000</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># failover 後同時最多幾個 replica 去 resync 新 master</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 設 1 = 串行 resync、避免所有 replica 同時 resync 拖垮新 master</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">sentinel parallel-syncs mymaster <span class="m">1</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># failover 整體逾時（三分鐘內沒完成算失敗、可重試）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">sentinel failover-timeout mymaster <span class="m">180000</span></span></span></code></pre></div><p>啟動 Sentinel：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">redis-sentinel /path/to/sentinel.conf
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 或 redis-server /path/to/sentinel.conf --sentinel</span></span></span></code></pre></div><p>client 端要用 Sentinel-aware 連線（以 Python redis-py 為例）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">redis.sentinel</span> <span class="kn">import</span> <span class="n">Sentinel</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">sentinel</span> <span class="o">=</span> <span class="n">Sentinel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">[(</span><span class="s2">&#34;10.0.0.10&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;10.0.0.11&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;10.0.0.12&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">)],</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 寫入走 master（failover 後自動重新發現）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">master</span> <span class="o">=</span> <span class="n">sentinel</span><span class="o">.</span><span class="n">master_for</span><span class="p">(</span><span class="s2">&#34;mymaster&#34;</span><span class="p">,</span> <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">master</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">,</span> <span class="s2">&#34;value&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 讀取可走 replica</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">replica</span> <span class="o">=</span> <span class="n">sentinel</span><span class="o">.</span><span class="n">slave_for</span><span class="p">(</span><span class="s2">&#34;mymaster&#34;</span><span class="p">,</span> <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">replica</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">)</span></span></span></code></pre></div><p>關鍵：client 透過 <code>master_for</code> 拿到的是一個會在 failover 後重新查詢 Sentinel 的連線封裝，不是寫死的 IP。直接寫死 master IP 的 client 在 failover 後會持續連到死掉的舊 master。</p>
<h3 id="防腦裂的兩個-master-端設定">防腦裂的兩個 master 端設定</h3>
<p>Sentinel 選主的同時，要防止舊 master 復活後繼續接受寫入（split-brain）。在 Redis master 端設：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 至少要有 1 個 replica 連著、且 replica lag &lt; 10 秒、master 才接受寫入</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET min-replicas-to-write <span class="m">1</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET min-replicas-max-lag <span class="m">10</span></span></span></code></pre></div><p>這讓被網路隔離的舊 master（連不到 replica）自動停止接受寫入，避免它在隔離期間累積的寫入在復活後跟新 master 衝突。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1down-after-太短網路抖動誤觸-failover">Case 1：down-after 太短、網路抖動誤觸 failover</h3>
<p><strong>徵兆</strong>：master 其實沒死，只是一次短暫的網路抖動或 GC 暫停，Sentinel 卻觸發了 failover，造成一次不必要的中斷；甚至反覆 failover（flapping）。</p>
<p><strong>根因</strong>：<code>down-after-milliseconds</code> 設太短（例如 1000ms），master 一個短暫的 STW GC 或跨 AZ 網路抖動就超過閾值，被誤判 SDOWN→ODOWN。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>down-after-milliseconds</code> 設成能容忍正常抖動的值（5000-10000ms 是常見起點），用實際 RTT 與 GC pause 分布反推</li>
<li>quorum 設成多數而非 1，要求多個 Sentinel 同時看到下線，過濾單一 Sentinel 的網路問題</li>
<li>Sentinel 跟 Redis 不要跨高延遲鏈路放，網路品質直接影響誤判率</li>
<li>監控 failover 觸發頻率，flapping 是調參訊號</li>
</ol>
<h3 id="case-2failover-後-client-連到死掉的舊-master">Case 2：failover 後 client 連到死掉的舊 master</h3>
<p><strong>徵兆</strong>：failover 完成、Sentinel 日誌顯示新 master 已提升，但部分 application 持續寫入失敗或寫到舊 master（資料進黑洞），<code>CLIENT LIST</code> 在新 master 上看不到這些 client。</p>
<p><strong>根因</strong>：client 寫死了 master IP，或用的 client library 不支援 Sentinel 模式，failover 後不會重新向 Sentinel 查詢新 master。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 一律用 Sentinel-aware 連線（<code>master_for</code> / lettuce 的 Sentinel 配置），不寫死 IP</li>
<li>確認 client library 版本支援 Sentinel 且配置正確（連的是 Sentinel port 26379，不是 Redis 6379）</li>
<li>對 latency-sensitive 服務，failover 後可主動 rolling restart application，清掉殘留連線</li>
<li>設 <code>min-replicas-to-write</code> 讓被隔離的舊 master 自動停寫，即使 client 連上去也寫不進，避免資料進黑洞</li>
</ol>
<h3 id="case-3選到-lag-大的-replicafailover-丟資料">Case 3：選到 lag 大的 replica、failover 丟資料</h3>
<p><strong>徵兆</strong>：failover 後發現最近幾秒的寫入不見了，新 master 的資料比預期舊。</p>
<p><strong>根因</strong>：Redis replication 是非同步的，replica 之間 lag 不一。Sentinel 選主會優先選 lag 小的（靠 <code>replica-priority</code> 與複製 offset），但若所有 replica 都 lag 大（master 寫入遠快於複製），無論選哪個都會丟掉未複製的寫入。Sentinel 的 failover 保證可用性，不保證零資料遺失。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設 <code>min-replicas-to-write</code> + <code>min-replicas-max-lag</code>，lag 過大時 master 主動停寫，限制資料遺失窗口</li>
<li>監控 replication lag（<code>master_repl_offset</code> vs replica 的 offset），lag 持續大代表複製跟不上寫入，要降寫入或擴容</li>
<li>用 <code>replica-priority</code> 把不適合當 master 的 replica（例如做備份的、跨區的）設成 0 排除</li>
<li>需要零資料遺失的場景，Sentinel 的非同步複製不夠，走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a> 的 multi-AZ transaction log（強一致持久性）</li>
</ol>
<h3 id="case-4腦裂舊-master-復活後雙寫衝突">Case 4：腦裂——舊 master 復活後雙寫衝突</h3>
<p><strong>徵兆</strong>：網路分區期間 Sentinel 提升了新 master，分區恢復後舊 master 回來，兩個 master 各自接受過寫入，資料出現衝突或舊 master 的寫入被覆蓋遺失。</p>
<p><strong>根因</strong>：舊 master 在分區期間被隔離（連不到 Sentinel 多數），但 client 若還連得到它且它沒設停寫保護，就繼續接受寫入。分區恢復後舊 master 被降為 replica，它在分區期間的寫入被新 master 的資料覆蓋。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>min-replicas-to-write 1</code> + <code>min-replicas-max-lag 10</code> 是核心防護——被隔離的舊 master 連不到 replica，自動停寫</li>
<li>Sentinel 部署在多數能存活的故障域，確保分區時多數 Sentinel 在新 master 那側</li>
<li>接受 Redis 的 CAP 取捨：Sentinel 偏向可用性，極端分區下無法完全避免資料遺失，要強一致走別的儲存層</li>
<li>failover 後監控舊 master 復活的降級流程，確認它正確變成 replica 且 resync</li>
</ol>
<h3 id="case-5parallel-syncs-設太大failover-後新-master-被-resync-拖垮">Case 5：parallel-syncs 設太大、failover 後新 master 被 resync 拖垮</h3>
<p><strong>徵兆</strong>：failover 完成的瞬間新 master 延遲暴增、甚至短暫無回應，所有 replica 同時對它發起全量同步。</p>
<p><strong>根因</strong>：<code>parallel-syncs</code> 設成大於 1（或等於 replica 數），failover 後所有 replica 同時對新 master 做 full resync。full resync 要新 master 做 BGSAVE（fork、見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence deep article</a>）並把 RDB 傳給每個 replica，多個同時進行直接打爆新 master。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>parallel-syncs</code> 設 1，replica 串行 resync，犧牲一點恢復速度換新 master 不被拖垮</li>
<li>確認 master 端 <code>repl-backlog-size</code> 夠大，讓短暫斷線的 replica 走部分同步（partial resync）而非全量</li>
<li>監控 failover 後新 master 的 CPU / 記憶體，resync 期間是脆弱窗口</li>
<li>resync 的 fork 成本跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體 headroom</a> 直接相關，新 master 也要留 fork 空間</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Sentinel 的容量判讀，圍繞 failover 時間與資料遺失窗口：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>failover 總時間（T0→T5）</td>
          <td>數秒到十幾秒</td>
          <td>過長 → 查 down-after / parallel-syncs / client</td>
      </tr>
      <tr>
          <td>failover 觸發頻率</td>
          <td>罕見（真實故障才觸發）</td>
          <td>flapping → down-after 太短、quorum 太低</td>
      </tr>
      <tr>
          <td>replication lag</td>
          <td>&lt; 1 秒</td>
          <td>持續大 → 寫入超過複製能力、failover 會丟資料</td>
      </tr>
      <tr>
          <td>Sentinel 數量</td>
          <td>奇數、≥ 3、跨故障域</td>
          <td>&lt; 3 或同故障域 → 無法容忍 Sentinel 故障</td>
      </tr>
      <tr>
          <td>寫入中斷可容忍時間</td>
          <td>業務定義</td>
          <td>不可容忍 → Sentinel 不夠、走 managed multi-AZ</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單 master 容量不夠（記憶體 / 吞吐超過單機）</strong>：Sentinel 解 HA 不解容量。要橫向擴容走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster</a>，它自帶 sharding 與 per-shard failover。</li>
<li><strong>不想自己運維 Sentinel 與 failover 演練</strong>：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a> 的 Multi-AZ 自動 failover 把這條時序鏈託管，failover ~30 秒到幾分鐘，省掉 Sentinel 部署與調參，代價是 managed premium。</li>
<li><strong>需要零資料遺失的強持久性</strong>：Sentinel 的非同步複製在 failover 時會丟未複製的寫入。要強一致走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a> 的 multi-AZ transaction log。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Sentinel 是 HA 的一層，但它的每一段都跟其他子系統耦合：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></strong>：Sentinel 是「不分片的 HA」，Cluster 是「分片 + 每 shard 自帶 failover」。容量需求決定走哪條，本文是前者。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></strong>：failover 後的 resync 靠 BGSAVE（fork），新 master 的 fork 成本是 resync 期間的脆弱點。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></strong>：新 master 提升後要承接全部寫入並支援 replica resync 的 fork，記憶體 headroom 不能少。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">Meta cache consistency</a></strong>：failover / replica promotion 期間的 stale read 與一致性議題，是大規模 cache 治理的核心，Sentinel 的非同步複製是 stale window 的來源之一。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（managed multi-AZ failover）</li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Failover</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/failover/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/failover/</guid><description>&lt;p>Failover 的核心概念是「主要路徑失效時切換到備援路徑」。備援可以是另一個 instance、另一個 availability zone、另一個資料庫 replica、另一個服務供應商或簡化功能。 可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">Fallback Plan&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Failover 是可用性設計的一部分。它要處理健康判斷、切換觸發、資料一致性、DNS 或 load balancer 更新、連線重建與回切流程。 可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">Fallback Plan&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>系統需要 failover 的訊號是單一節點、單一區域或單一供應商故障會造成停機。付款服務可以在主要供應商中斷時切到備援供應商，但要處理交易查詢、費率、風控與對帳差異。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Failover runbook 要定義觸發條件、切換步驟、資料檢查、回切條件與演練頻率。自動 failover 需要更嚴格的健康訊號，人工 failover 則需要清楚的決策權限。&lt;/p></description><content:encoded><![CDATA[<p>Failover 的核心概念是「主要路徑失效時切換到備援路徑」。備援可以是另一個 instance、另一個 availability zone、另一個資料庫 replica、另一個服務供應商或簡化功能。 可先對照 <a href="/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">Fallback Plan</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Failover 是可用性設計的一部分。它要處理健康判斷、切換觸發、資料一致性、DNS 或 load balancer 更新、連線重建與回切流程。 可先對照 <a href="/blog/backend/knowledge-cards/fallback-plan/" data-link-title="Fallback Plan" data-link-desc="說明變更失敗時如何回到可接受狀態">Fallback Plan</a>。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>系統需要 failover 的訊號是單一節點、單一區域或單一供應商故障會造成停機。付款服務可以在主要供應商中斷時切到備援供應商，但要處理交易查詢、費率、風控與對帳差異。</p>
<h2 id="設計責任">設計責任</h2>
<p>Failover runbook 要定義觸發條件、切換步驟、資料檢查、回切條件與演練頻率。自動 failover 需要更嚴格的健康訊號，人工 failover 則需要清楚的決策權限。</p>
]]></content:encoded></item><item><title>Aurora Cross-AZ Failover：RTO 量測、endpoint routing 與 application reconnect 契約</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/cross-az-failover-rto/</guid><description>&lt;p>Aurora cross-AZ failover 的 RTO 文件數字是「&amp;lt; 30 秒」、但 application 端實測常常看到 60-120 秒 — 這個落差不是 Aurora 慢、是 &lt;em>DNS cache + connection pool + retry policy&lt;/em> 的對齊問題。本文展開 failover lifecycle 三段（detection / promotion / DNS update）、application 端 reconnect 契約、量測真實 RTO 的流程、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 受監管銀行業務為什麼選獨立 cluster 而非 Global Database failover 的合規 driver。&lt;/p>
&lt;p>本文不是 Aurora overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&amp;#43;75% 效能改善的 production 證據">Aurora vendor 頁&lt;/a>）— 而是 failover 流程的實作層教學。前置閱讀建議 &lt;a href="../storage-architecture/">Aurora storage architecture&lt;/a>（理解為什麼 Aurora failover 不需要 data catch-up）。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：DraftKings / Standard Chartered 等級的金融交易服務、AZ-level outage 期間用戶操作不能斷、RTO 預算 &amp;lt; 60 秒、但 application 端看到的 reconnect 行為跟 AWS 文件不一致。&lt;/p>
&lt;p>讀者常見的具體疑問：&lt;/p>
&lt;ul>
&lt;li>「Failover trigger 後新 connection 還連到舊 primary、為什麼？」&lt;/li>
&lt;li>「Writer endpoint DNS 切換了、application 還沒重連、什麼時候會切？」&lt;/li>
&lt;li>「Failover 期間 in-flight transaction 是全 abort 還是部分 commit？」&lt;/li>
&lt;li>「我手動測 failover RTO 量出 90 秒、AWS 文件講 &amp;lt; 30 秒、誰錯？」&lt;/li>
&lt;/ul>
&lt;p>進一步問題：失敗模式分布在 &lt;em>application 端的 connection state&lt;/em>、不只是 Aurora 端的 promotion 流程。Aurora 端的 promotion 在 storage 共享下確實 &amp;lt; 30 秒（不需要等 data catch-up）、但 application reconnect 受 JVM DNS cache、connection pool validation、retry policy 影響、容易把總體 RTO 拉長到 2-3 倍。&lt;/p>
&lt;p>對 Standard Chartered 這種受監管銀行業務、failover 還有合規維度：受監管市場資料 &lt;em>不能跨境複製&lt;/em>、Global Database 在這種場景違反合規、必須用每市場獨立 cluster 的 cross-AZ failover 吸收 RTO 預算。這個 driver 跟一般工程「跨 region failover 更好」的直覺相反。&lt;/p>
&lt;h2 id="核心機制failover-lifecycle-三段">核心機制：failover lifecycle 三段&lt;/h2>
&lt;p>Aurora cross-AZ failover 的 first-class concept 是 &lt;em>failover lifecycle 三段&lt;/em>：detection → promotion → DNS update。每一段有自己的 SLA 跟可調維度。&lt;/p></description><content:encoded><![CDATA[<p>Aurora cross-AZ failover 的 RTO 文件數字是「&lt; 30 秒」、但 application 端實測常常看到 60-120 秒 — 這個落差不是 Aurora 慢、是 <em>DNS cache + connection pool + retry policy</em> 的對齊問題。本文展開 failover lifecycle 三段（detection / promotion / DNS update）、application 端 reconnect 契約、量測真實 RTO 的流程、跟 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 受監管銀行業務為什麼選獨立 cluster 而非 Global Database failover 的合規 driver。</p>
<p>本文不是 Aurora overview（請看 <a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor 頁</a>）— 而是 failover 流程的實作層教學。前置閱讀建議 <a href="../storage-architecture/">Aurora storage architecture</a>（理解為什麼 Aurora failover 不需要 data catch-up）。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：DraftKings / Standard Chartered 等級的金融交易服務、AZ-level outage 期間用戶操作不能斷、RTO 預算 &lt; 60 秒、但 application 端看到的 reconnect 行為跟 AWS 文件不一致。</p>
<p>讀者常見的具體疑問：</p>
<ul>
<li>「Failover trigger 後新 connection 還連到舊 primary、為什麼？」</li>
<li>「Writer endpoint DNS 切換了、application 還沒重連、什麼時候會切？」</li>
<li>「Failover 期間 in-flight transaction 是全 abort 還是部分 commit？」</li>
<li>「我手動測 failover RTO 量出 90 秒、AWS 文件講 &lt; 30 秒、誰錯？」</li>
</ul>
<p>進一步問題：失敗模式分布在 <em>application 端的 connection state</em>、不只是 Aurora 端的 promotion 流程。Aurora 端的 promotion 在 storage 共享下確實 &lt; 30 秒（不需要等 data catch-up）、但 application reconnect 受 JVM DNS cache、connection pool validation、retry policy 影響、容易把總體 RTO 拉長到 2-3 倍。</p>
<p>對 Standard Chartered 這種受監管銀行業務、failover 還有合規維度：受監管市場資料 <em>不能跨境複製</em>、Global Database 在這種場景違反合規、必須用每市場獨立 cluster 的 cross-AZ failover 吸收 RTO 預算。這個 driver 跟一般工程「跨 region failover 更好」的直覺相反。</p>
<h2 id="核心機制failover-lifecycle-三段">核心機制：failover lifecycle 三段</h2>
<p>Aurora cross-AZ failover 的 first-class concept 是 <em>failover lifecycle 三段</em>：detection → promotion → DNS update。每一段有自己的 SLA 跟可調維度。</p>
<p><strong>Detection（10-15 秒）</strong>：</p>
<ul>
<li>AWS 內部 health check 每幾秒檢查 primary writer health</li>
<li>連續失敗到一定閾值才 trigger failover（避免 false positive）</li>
<li>讀者無法直接調 detection 閾值、是 AWS managed</li>
</ul>
<p><strong>Promotion（&lt; 5 秒）</strong>：</p>
<ul>
<li>選 PromotionTier 最低的 read replica 升 primary</li>
<li>Storage 跨 AZ 共享、replica 升 primary <em>不需要 data catch-up</em>（vs 傳統 PostgreSQL streaming replication 要等 WAL apply）</li>
<li>Promotion 本身極快、是 Aurora storage 設計的直接受益</li>
</ul>
<p><strong>DNS update（5-15 秒）</strong>：</p>
<ul>
<li>Cluster endpoint / writer endpoint DNS 切到新 primary</li>
<li>Aurora endpoint DNS TTL 是 5 秒、AWS DNS infrastructure 通常 5-15 秒 propagate 完</li>
<li>但 application 端的 DNS cache 可能 cache 更久 — JVM <code>networkaddress.cache.ttl</code> 預設 -1（cache forever）就會卡在這層</li>
</ul>
<p><strong>Endpoint 類型跟 failover 行為</strong>：</p>
<ul>
<li><strong>Writer endpoint</strong>：跟著 failover 走、DNS 切到新 primary、application 寫操作用這個</li>
<li><strong>Reader endpoint</strong>：load-balance 到所有 replica；failover 期間短暫包含 promoted replica（已升 primary）、reader query 可能打到 primary、引起寫鎖競爭</li>
<li><strong>Custom endpoint</strong>：用戶自定 routing rule、failover 期間行為要驗證、不能假設自動跟隨</li>
</ul>
<p><strong>跟通用 failover 差在哪</strong>：Aurora 不需要 data catch-up phase、failover 主要瓶頸是 DNS propagation + application reconnect、不是 promotion 本身。傳統 PostgreSQL streaming replication failover 要等 replica WAL catch-up（heavy write 期間可能秒級延遲）、Aurora 在 storage 設計下消除這段等待。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">failover</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a>。</p>
<h2 id="step-by-step-配置--量測">Step-by-step 配置 / 量測</h2>
<p><strong>Cluster failover 配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 確認 cluster 至少有一個跨 AZ replica</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws rds describe-db-clusters <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --db-cluster-identifier my-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;DBClusters[0].DBClusterMembers&#39;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 設定 PromotionTier（0 最優先、15 最不優先）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">aws rds modify-db-instance <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --db-instance-identifier my-replica-az-b <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --promotion-tier <span class="m">0</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 跨 region replica 預設 tier 15（不優先升、避免 failover 跨 region）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">aws rds modify-db-instance <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --db-instance-identifier my-cross-region-replica <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --promotion-tier <span class="m">15</span></span></span></code></pre></div><p><strong>Application 端 JVM 設定</strong>（最常踩雷的點）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-properties" data-lang="properties"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># JVM 系統 property、預設 -1 = cache forever、必改</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">networkaddress.cache.ttl</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">networkaddress.cache.negative.ttl</span><span class="o">=</span><span class="s">0</span></span></span></code></pre></div><p><strong>Connection pool 設定</strong>（HikariCP 範例）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">spring.datasource.hikari</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">maximum-pool-size</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">connection-test-query</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SELECT 1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">validation-timeout</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">max-lifetime</span><span class="p">:</span><span class="w"> </span><span class="m">1800000</span><span class="w">      </span><span class="c"># 30 分鐘、強制 recycle connection</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">keepalive-time</span><span class="p">:</span><span class="w"> </span><span class="m">30000</span><span class="w">      </span><span class="c"># 30 秒檢查 idle connection</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">connection-timeout</span><span class="p">:</span><span class="w"> </span><span class="m">30000</span></span></span></code></pre></div><p><strong>Retry policy</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 簡化範例、實際用 Resilience4j 或 Failsafe</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">RetryPolicy</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span><span class="w"> </span><span class="n">retryPolicy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">RetryPolicy</span><span class="p">.</span><span class="na">builder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">handle</span><span class="p">(</span><span class="n">SQLTransientConnectionException</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"> </span><span class="n">SQLNonTransientConnectionException</span><span class="p">.</span><span class="na">class</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">withBackoff</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">1</span><span class="p">),</span><span class="w"> </span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">30</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">withMaxAttempts</span><span class="p">(</span><span class="n">5</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span></span></span></code></pre></div><p><strong>手動觸發 failover 量測 RTO</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 觸發 failover、記錄時間</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">START</span><span class="o">=</span><span class="k">$(</span>date +%s%3N<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">aws rds failover-db-cluster --db-cluster-identifier my-cluster
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Failover triggered at </span><span class="nv">$START</span><span class="s2"> ms&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 用 application heartbeat 寫入時間戳</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># application 端跑 every-second insert、failover 後第一個成功 insert 的時間 - START = RTO</span></span></span></code></pre></div><p><strong>驗證點</strong>：</p>
<ul>
<li>CloudWatch <code>FailoverEvent</code> counter &gt; 0（failover 觸發訊號）</li>
<li><code>DatabaseConnections</code> 在 failover 期間 drop &gt; 50%、之後 spike（reconnect 風暴）</li>
<li>Application metric「first successful write after failover trigger」&lt; 30 秒</li>
</ul>
<p><strong>Rollback boundary</strong>：promotion 不可逆 — 原 primary 變 replica、不會自動 fallback。要切回原 AZ 必須再做一次 failover。</p>
<h2 id="故障模式--邊界-case">故障模式 / 邊界 case</h2>
<h3 id="case-1dns-cache-把-rto-從-30-秒拉到-120-秒">Case 1：DNS cache 把 RTO 從 30 秒拉到 120 秒</h3>
<p>徵兆：手動 failover 後、CloudWatch <code>FailoverEvent</code> 1 秒內出現、但 application log 顯示寫操作 120 秒後才恢復。</p>
<p>原因：JVM <code>networkaddress.cache.ttl</code> 預設 <code>-1</code>（cache forever）、application JVM 把 writer endpoint DNS 永久 cache 到舊 primary IP；只有 connection pool eviction 或 application restart 才會重新 resolve。</p>
<p>修：</p>
<ul>
<li>JVM startup 加 <code>-Dnetworkaddress.cache.ttl=5</code></li>
<li>或在 <code>$JAVA_HOME/lib/security/java.security</code> 改 <code>networkaddress.cache.ttl=5</code></li>
<li>Python application 通常沒這問題（DNS resolve per connection）、但要確認 SQLAlchemy 用 <code>pool_pre_ping=True</code></li>
</ul>
<h3 id="case-2connection-pool-cached-connection-全-stale">Case 2：Connection pool cached connection 全 stale</h3>
<p>徵兆：DNS 切換 OK、但 application 寫操作 timeout 10-30 秒後才觸發 reconnect、p99 latency spike。</p>
<p>原因：connection pool 的 cached connection 還指向舊 primary IP、validation 沒開或 timeout 太長、application 拿到 stale connection 才發現 backend gone。</p>
<p>修：</p>
<ul>
<li>HikariCP：<code>connection-test-query: &quot;SELECT 1&quot;</code> + <code>validation-timeout: 5000</code> + <code>keepalive-time: 30000</code></li>
<li>SQLAlchemy：<code>pool_pre_ping=True</code> + <code>pool_recycle=1800</code></li>
<li>failover 演練後驗證 connection pool 在 30 秒內 evict 完所有 stale connection</li>
</ul>
<h3 id="case-3reader-endpoint-failover-期間打到新-primary">Case 3：Reader endpoint failover 期間打到新 primary</h3>
<p>徵兆：failover 期間 application read query 偶發出現 <code>cannot execute SELECT in a read-only transaction</code> 或寫鎖競爭、用戶看到 inconsistent state。</p>
<p>原因：reader endpoint 是 DNS-based load balance 到所有 replica、failover 期間 <em>短暫</em> 包含已升 primary 的 replica（DNS propagation 期間 reader 跟 writer endpoint 都指向同一台）。Read query 打到 primary 後、跟正在寫的 transaction 競爭。</p>
<p>修：</p>
<ul>
<li>Application 端 read 跟 write data source 拆分、不要假設 reader endpoint 永遠 read-only</li>
<li>Failover 期間 application 端做 SQL error type 偵測、<code>read-only transaction</code> 錯誤觸發 retry</li>
<li>用 custom endpoint group 特定 replica、failover 期間 custom endpoint 行為更可控</li>
</ul>
<h3 id="case-4in-flight-transaction-全-abort">Case 4：In-flight transaction 全 abort</h3>
<p>徵兆：failover 期間正在執行的 transaction <em>全部 abort</em>、application 看到 <code>connection reset</code> 或 <code>server closed connection</code>、commit 沒成功。</p>
<p>原因：Aurora failover 不保留 transaction 狀態、所有 in-flight transaction（包括已執行 BEGIN 但還沒 COMMIT 的）全 abort。Application 沒做 idempotent retry 就會丟失 commit。</p>
<p>修：</p>
<ul>
<li>寫操作必須 idempotent（用 idempotency key、application 端做 deduplication）</li>
<li>在 application 層做 transaction-level retry、不在 connection 層 retry</li>
<li>重要寫入做 <em>write-then-verify</em> 模式：commit 後立刻 SELECT 確認、失敗才 retry</li>
</ul>
<h3 id="case-5promotiontier-配置忽略">Case 5：PromotionTier 配置忽略</h3>
<p>徵兆：failover 後 application latency 暴漲、發現升 primary 的是 cross-region replica。</p>
<p>原因：cross-region replica 預設 PromotionTier 是 1（或忘記改）、failover 時優先升、application 跟新 primary 跨 region、latency 從 5ms 變 100ms+。</p>
<p>修：</p>
<ul>
<li>cross-region replica <code>--promotion-tier 15</code>（不優先升）</li>
<li>同 region 跨 AZ replica <code>--promotion-tier 0</code> 或 <code>1</code></li>
<li>Multi-AZ deployment 至少配 2 個 same-region replica、避免 cross-region 被升</li>
</ul>
<h2 id="standard-chartered-為什麼選獨立-cluster-而非-global-database">Standard Chartered 為什麼選獨立 cluster 而非 Global Database</h2>
<p><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 揭露受監管產業的 failover 設計選擇 — 案例「判讀」段第 1 點：「7 個受監管市場代表 7 個獨立 cluster（資料不能跨境）、容量規劃變成『7 個獨立規劃 × 各自合規門檻』」。</p>
<p><strong>合規 driver</strong>：</p>
<ul>
<li>受監管市場資料 <em>不能跨境複製</em></li>
<li>Aurora Global Database 是跨 region async replication、會把資料推到其他 region</li>
<li>→ Global Database 在這種場景 <em>違反合規</em>、不是 DR 選項</li>
<li>必須用每市場獨立 cluster、各自做 cross-AZ failover、各自吸收 RTO 預算</li>
</ul>
<p><strong>工程含義</strong>：</p>
<ul>
<li>每市場 cross-AZ failover RTO &lt; 30 秒、滿足當地監管 RTO 要求</li>
<li>跨市場 DR 不靠 Global Database、靠應用層的 <em>市場切換</em>（用戶從 A 市場切到 B 市場是業務決策、不是技術 failover）</li>
<li>7 個 cluster 各自獨立、operational surface area × 7（parameter group / backup / IAM / observability fan-out）、但合規要求壓倒運維成本</li>
</ul>
<p><strong>Fleet 拓樸</strong>：合規驅動的 fleet 設計（7 個受監管市場 = 7 個獨立 cluster）詳見 <a href="../read-replica-scaling/">Aurora read replica scaling</a> fleet 治理 SSoT 邊界段。本篇只展開 <em>單 cluster cross-AZ failover</em> 流程、不展開跨 cluster 拓樸決策。</p>
<p><strong>scope warning（必明示、case 自承）</strong>：Standard Chartered case 未公開是 PostgreSQL 還是 MySQL、未公開具體 cost 數字、屬「相關 case study」匿名對照。引用時不能擴寫具體 engine。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p><strong>核心 metric</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">FailoverEvent           # failover 觸發 counter、&gt; 0 立即通知
</span></span><span class="line"><span class="ln">2</span><span class="cl">DatabaseConnections     # failover 期間 drop、之後 spike
</span></span><span class="line"><span class="ln">3</span><span class="cl">AuroraReplicaLag        # failover 前 replica 是否 caught up</span></span></code></pre></div><p><strong>Application 端 metric</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">first_successful_write_after_failover  # 真實 RTO
</span></span><span class="line"><span class="ln">2</span><span class="cl">connection_pool_error_rate              # stale connection 訊號
</span></span><span class="line"><span class="ln">3</span><span class="cl">db_retry_count                          # retry policy 觸發頻率</span></span></code></pre></div><p><strong>量測 RTO 流程</strong>：</p>
<ol>
<li>跑 application 端 every-second heartbeat insert</li>
<li>手動觸發 failover、記錄 trigger 時間戳</li>
<li>從 heartbeat insert log 找 failover 後第一個成功 insert 的時間戳</li>
<li>差值 = 真實 RTO（包含 detection + promotion + DNS + reconnect）</li>
</ol>
<p><strong>Alert</strong>：</p>
<ul>
<li><code>FailoverEvent &gt; 0</code> 立即通知 on-call</li>
<li><code>DatabaseConnections</code> 5 分鐘內 drop &gt; 50% 警告 stale connection</li>
<li><code>db_retry_count</code> 短期內 spike 警告 reconnect 風暴</li>
</ul>
<p><strong>Failover 演練頻率</strong>：</p>
<ul>
<li>Non-critical workload：每季一次 planned failover drill</li>
<li>受監管產業（Standard Chartered 類）：每月一次、有合規 sign-off 記錄</li>
<li>重大版本升級前必跑一次</li>
</ul>
<p><strong>回路徑</strong>：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.x incident response</a> failover playbook、<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 判斷 reconnect-bound vs query-bound。</p>
<h2 id="邊界與整合--下一步">邊界與整合 / 下一步</h2>
<p><strong>Sibling deep articles</strong>：</p>
<ul>
<li><a href="../storage-architecture/">Aurora storage architecture</a> — 理解為什麼 Aurora failover 不需要 data catch-up（storage 跨 AZ 共享）</li>
<li><a href="../read-replica-scaling/">Aurora read replica scaling</a> — replica 升 primary 流程跟 fleet 治理 SSoT</li>
<li><a href="../global-database-multi-region/">Aurora Global Database</a> — 跨 region failover RTO 不同數量級（2-15 分鐘 vs cross-AZ &lt; 30 秒）</li>
</ul>
<p><strong>Migration playbook</strong>：</p>
<ul>
<li><a href="../migrate-from-self-managed-pg-mysql/">PostgreSQL / MySQL → Aurora</a> — HA redesign 是 operational redesign 主項、從 Patroni / Orchestrator 切到 Aurora cluster endpoint</li>
</ul>
<p><strong>1.x 章節互引</strong>：</p>
<ul>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> — failover 期間 in-flight transaction abort 對 application 契約的影響</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.x incident response</a> — failover decision log</li>
</ul>
<p><strong>何時不用本文</strong>：non-critical workload、RTO 預算 &gt; 5 分鐘、Multi-AZ 預設配置足夠時可跳過、看 <a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor overview</a> 即可。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor overview</a> — 服務定位、適用 / 不適用場景</li>
<li><a href="/blog/backend/knowledge-cards/failover/" data-link-title="Failover" data-link-desc="說明主要服務或節點失效時如何切換到備援能力">Failover 卡片</a> — 概念基底</li>
<li><a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO 卡片</a> — RTO 量測判讀</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a> — 本文遵循的 6 規格面寫作模板</li>
<li>官方：<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraHighAvailability.html">Aurora high availability</a></li>
</ul>
]]></content:encoded></item><item><title>MySQL Replication Failover Lab</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/replication-failover-lab/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mysql/hands-on/replication-failover-lab/</guid><description>&lt;p>MySQL replication failover lab 的核心責任是讓讀者觀察 source / replica 拓撲在 promotion 時的資料與 client route。這篇承接 &lt;a href="../../replication-topology/">Replication Topology&lt;/a> 與 &lt;a href="../../orchestrator-failover/">Orchestrator Failover&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能記錄 replication status、lag、promotion timeline、client error sample、validation query 與 incident decision log。&lt;/p>
&lt;h2 id="baseline-replication">Baseline Replication&lt;/h2>
&lt;p>Baseline replication 的核心責任是先保存 source / replica 狀態。實際建立 replication 依 GTID、binlog file position、Docker topology 或 managed service 而異；本文聚焦演練 evidence。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">REPLICA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STATUS&lt;/span>&lt;span class="err">\&lt;/span>&lt;span class="k">G&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">BINARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOG&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STATUS&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Baseline 要記錄：&lt;/p>
&lt;ol>
&lt;li>Source host / replica host。&lt;/li>
&lt;li>GTID executed / retrieved。&lt;/li>
&lt;li>IO thread / SQL thread。&lt;/li>
&lt;li>Seconds behind source。&lt;/li>
&lt;li>Read endpoint / write endpoint。&lt;/li>
&lt;/ol>
&lt;h2 id="client-workload">Client Workload&lt;/h2>
&lt;p>Client workload 的核心責任是讓 failover 對 application 可見。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">while&lt;/span> true&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> mysql -h &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$MYSQL_WRITE_HOST&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -u app_user -papp_pw appdb &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="s2">&amp;#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 1, UUID());&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> sleep &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 synthetic workload 產生成功、timeout、duplicate、read-only 或 connection error。正式演練要避免碰 production side effect。&lt;/p>
&lt;h2 id="promotion-frame">Promotion Frame&lt;/h2>
&lt;p>Promotion frame 的核心責任是把 failover action 寫成可審查步驟。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">failover_start:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">old_source:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">candidate_replica:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">lag_before:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">promotion_method:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">accepted_data_loss:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">operator:&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Managed service、Orchestrator 或手動 promotion 都要留下同樣欄位。工具不同，決策證據一致。&lt;/p>
&lt;h2 id="validation">Validation&lt;/h2>
&lt;p>Validation 的核心責任是確認 promoted instance 可讀寫且資料符合預期。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ledger_entries&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">MAX&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ledger_entries&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARIABLES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;read_only&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SHOW&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VARIABLES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;super_read_only&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>若使用 GTID，還要比較 source / replica 的 GTID set。若有 external side effect，要用 idempotency key 做 reconciliation。&lt;/p>
&lt;h2 id="client-route">Client Route&lt;/h2>
&lt;p>Client route 的核心責任是確認 application、ProxySQL、DNS 或 secret 已指向新 writer。&lt;/p>
&lt;p>檢查項目：&lt;/p>
&lt;ol>
&lt;li>Write endpoint 是否更新。&lt;/li>
&lt;li>ProxySQL writer hostgroup 是否切換。&lt;/li>
&lt;li>Application pool 是否清掉舊連線。&lt;/li>
&lt;li>Retry 是否有 backoff。&lt;/li>
&lt;li>Read replica 是否重新掛到新 source。&lt;/li>
&lt;/ol>
&lt;p>Failover 完成標準包含資料庫 promotion 與 client route 穩定。只 promote 成功，application 仍可能寫到舊 endpoint。&lt;/p></description><content:encoded><![CDATA[<p>MySQL replication failover lab 的核心責任是讓讀者觀察 source / replica 拓撲在 promotion 時的資料與 client route。這篇承接 <a href="../../replication-topology/">Replication Topology</a> 與 <a href="../../orchestrator-failover/">Orchestrator Failover</a>。</p>
<p>本文的驗收標準是：你能記錄 replication status、lag、promotion timeline、client error sample、validation query 與 incident decision log。</p>
<h2 id="baseline-replication">Baseline Replication</h2>
<p>Baseline replication 的核心責任是先保存 source / replica 狀態。實際建立 replication 依 GTID、binlog file position、Docker topology 或 managed service 而異；本文聚焦演練 evidence。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">REPLICA</span><span class="w"> </span><span class="n">STATUS</span><span class="err">\</span><span class="k">G</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="nb">BINARY</span><span class="w"> </span><span class="n">LOG</span><span class="w"> </span><span class="n">STATUS</span><span class="p">;</span></span></span></code></pre></div><p>Baseline 要記錄：</p>
<ol>
<li>Source host / replica host。</li>
<li>GTID executed / retrieved。</li>
<li>IO thread / SQL thread。</li>
<li>Seconds behind source。</li>
<li>Read endpoint / write endpoint。</li>
</ol>
<h2 id="client-workload">Client Workload</h2>
<p>Client workload 的核心責任是讓 failover 對 application 可見。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  mysql -h <span class="s2">&#34;</span><span class="nv">$MYSQL_WRITE_HOST</span><span class="s2">&#34;</span> -u app_user -papp_pw appdb <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    -e <span class="s2">&#34;INSERT INTO ledger_entries(account_id, amount_cents, idempotency_key) VALUES (1, 1, UUID());&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  sleep <span class="m">1</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這個 synthetic workload 產生成功、timeout、duplicate、read-only 或 connection error。正式演練要避免碰 production side effect。</p>
<h2 id="promotion-frame">Promotion Frame</h2>
<p>Promotion frame 的核心責任是把 failover action 寫成可審查步驟。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">failover_start:
</span></span><span class="line"><span class="ln">2</span><span class="cl">old_source:
</span></span><span class="line"><span class="ln">3</span><span class="cl">candidate_replica:
</span></span><span class="line"><span class="ln">4</span><span class="cl">lag_before:
</span></span><span class="line"><span class="ln">5</span><span class="cl">promotion_method:
</span></span><span class="line"><span class="ln">6</span><span class="cl">accepted_data_loss:
</span></span><span class="line"><span class="ln">7</span><span class="cl">operator:</span></span></code></pre></div><p>Managed service、Orchestrator 或手動 promotion 都要留下同樣欄位。工具不同，決策證據一致。</p>
<h2 id="validation">Validation</h2>
<p>Validation 的核心責任是確認 promoted instance 可讀寫且資料符合預期。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">ledger_entries</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">MAX</span><span class="p">(</span><span class="n">created_at</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">ledger_entries</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;read_only&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;super_read_only&#39;</span><span class="p">;</span></span></span></code></pre></div><p>若使用 GTID，還要比較 source / replica 的 GTID set。若有 external side effect，要用 idempotency key 做 reconciliation。</p>
<h2 id="client-route">Client Route</h2>
<p>Client route 的核心責任是確認 application、ProxySQL、DNS 或 secret 已指向新 writer。</p>
<p>檢查項目：</p>
<ol>
<li>Write endpoint 是否更新。</li>
<li>ProxySQL writer hostgroup 是否切換。</li>
<li>Application pool 是否清掉舊連線。</li>
<li>Retry 是否有 backoff。</li>
<li>Read replica 是否重新掛到新 source。</li>
</ol>
<p>Failover 完成標準包含資料庫 promotion 與 client route 穩定。只 promote 成功，application 仍可能寫到舊 endpoint。</p>
<h2 id="incident-log">Incident Log</h2>
<p>Incident log 的核心責任是把演練結果保存。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Drill id:
</span></span><span class="line"><span class="ln">2</span><span class="cl">RTO observed:
</span></span><span class="line"><span class="ln">3</span><span class="cl">RPO / accepted data loss:
</span></span><span class="line"><span class="ln">4</span><span class="cl">Client errors:
</span></span><span class="line"><span class="ln">5</span><span class="cl">Validation:
</span></span><span class="line"><span class="ln">6</span><span class="cl">Follow-up:</span></span></code></pre></div><p>完成本篇後，拓撲設計讀 <a href="../../replication-topology/">Replication Topology</a>；自動化工具讀 <a href="../../orchestrator-failover/">Orchestrator Failover</a>；routing 讀 <a href="../proxysql-routing-lab/">ProxySQL Routing Lab</a>。</p>
]]></content:encoded></item><item><title>PostgreSQL HA Failover Drill</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/ha-failover-drill/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/hands-on/ha-failover-drill/</guid><description>&lt;p>PostgreSQL HA failover drill 的核心責任是讓讀者觀察 primary promotion 對 application、pooler 與 incident decision 的影響。這篇承接 &lt;a href="../../patroni-ha/">Patroni HA&lt;/a> 與 &lt;a href="../../cross-region-dr/">Cross-region DR&lt;/a>。&lt;/p>
&lt;p>本文的驗收標準是：你能記錄 failover timeline、replication lag snapshot、client error sample、data validation query 與 incident decision log entry。實際觸發方式依 Patroni、managed PostgreSQL 或雲平台而異；lab 重點是 evidence。&lt;/p>
&lt;h2 id="pre-failover-baseline">Pre-Failover Baseline&lt;/h2>
&lt;p>Pre-failover baseline 的核心責任是確認 primary / standby 狀態與 client route。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_is_in_recovery&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_current_wal_lsn&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">application_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">state&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">sync_state&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">replay_lag&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_stat_replication&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 standby 查：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_is_in_recovery&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_last_wal_receive_lsn&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_last_wal_replay_lsn&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Baseline 要保存 primary host、standby host、replication lag、application connection string、pooler route 與 current timeline。&lt;/p>
&lt;h2 id="client-workload">Client Workload&lt;/h2>
&lt;p>Client workload 的核心責任是讓 failover 對 application 的影響可見。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">while&lt;/span> true&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> date -u
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> psql &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DATABASE_URL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -c &lt;span class="s2">&amp;#34;INSERT INTO restore_markers(marker) VALUES (&amp;#39;failover-drill&amp;#39;) RETURNING id, created_at;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> sleep &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 loop 會在 failover 期間產生成功、timeout、connection reset 或 read-only error。正式演練要用 synthetic workload，避免影響真實使用者。&lt;/p>
&lt;h2 id="trigger-failover">Trigger Failover&lt;/h2>
&lt;p>Trigger failover 的核心責任是以受控方式促成 promotion。Patroni lab 可以用 &lt;code>patronictl failover&lt;/code>；managed service 則用 provider failover / reboot with failover 功能。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">failover_start_time:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">trigger_method:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">old_primary:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">candidate:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">operator:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">reason:&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Failover 觸發前要先確認這是演練，並且 workload、backup、rollback 與 stakeholder 都已對齊。&lt;/p>
&lt;h2 id="observe-promotion">Observe Promotion&lt;/h2>
&lt;p>Observe promotion 的核心責任是記錄資料庫與 client 的時間線。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>時間點&lt;/th>
 &lt;th>Evidence&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Trigger issued&lt;/td>
 &lt;td>command / provider event&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Old primary down&lt;/td>
 &lt;td>connection error / health check&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>New primary promoted&lt;/td>
 &lt;td>&lt;code>pg_is_in_recovery() = false&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Client reconnect&lt;/td>
 &lt;td>first successful write&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pooler stable&lt;/td>
 &lt;td>pool queue / server connection&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation complete&lt;/td>
 &lt;td>row count / marker sequence&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Promotion timeline 要保留秒級時間戳。這是評估 RTO、client retry 與 pooler behavior 的基礎。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL HA failover drill 的核心責任是讓讀者觀察 primary promotion 對 application、pooler 與 incident decision 的影響。這篇承接 <a href="../../patroni-ha/">Patroni HA</a> 與 <a href="../../cross-region-dr/">Cross-region DR</a>。</p>
<p>本文的驗收標準是：你能記錄 failover timeline、replication lag snapshot、client error sample、data validation query 與 incident decision log entry。實際觸發方式依 Patroni、managed PostgreSQL 或雲平台而異；lab 重點是 evidence。</p>
<h2 id="pre-failover-baseline">Pre-Failover Baseline</h2>
<p>Pre-failover baseline 的核心責任是確認 primary / standby 狀態與 client route。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_is_in_recovery</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">now</span><span class="p">(),</span><span class="w"> </span><span class="n">pg_current_wal_lsn</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">application_name</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">sync_state</span><span class="p">,</span><span class="w"> </span><span class="n">replay_lag</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_replication</span><span class="p">;</span></span></span></code></pre></div><p>在 standby 查：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_is_in_recovery</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">now</span><span class="p">(),</span><span class="w"> </span><span class="n">pg_last_wal_receive_lsn</span><span class="p">(),</span><span class="w"> </span><span class="n">pg_last_wal_replay_lsn</span><span class="p">();</span></span></span></code></pre></div><p>Baseline 要保存 primary host、standby host、replication lag、application connection string、pooler route 與 current timeline。</p>
<h2 id="client-workload">Client Workload</h2>
<p>Client workload 的核心責任是讓 failover 對 application 的影響可見。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  date -u
</span></span><span class="line"><span class="ln">3</span><span class="cl">  psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;INSERT INTO restore_markers(marker) VALUES (&#39;failover-drill&#39;) RETURNING id, created_at;&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  sleep <span class="m">1</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>這個 loop 會在 failover 期間產生成功、timeout、connection reset 或 read-only error。正式演練要用 synthetic workload，避免影響真實使用者。</p>
<h2 id="trigger-failover">Trigger Failover</h2>
<p>Trigger failover 的核心責任是以受控方式促成 promotion。Patroni lab 可以用 <code>patronictl failover</code>；managed service 則用 provider failover / reboot with failover 功能。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">failover_start_time:
</span></span><span class="line"><span class="ln">2</span><span class="cl">trigger_method:
</span></span><span class="line"><span class="ln">3</span><span class="cl">old_primary:
</span></span><span class="line"><span class="ln">4</span><span class="cl">candidate:
</span></span><span class="line"><span class="ln">5</span><span class="cl">operator:
</span></span><span class="line"><span class="ln">6</span><span class="cl">reason:</span></span></code></pre></div><p>Failover 觸發前要先確認這是演練，並且 workload、backup、rollback 與 stakeholder 都已對齊。</p>
<h2 id="observe-promotion">Observe Promotion</h2>
<p>Observe promotion 的核心責任是記錄資料庫與 client 的時間線。</p>
<table>
  <thead>
      <tr>
          <th>時間點</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Trigger issued</td>
          <td>command / provider event</td>
      </tr>
      <tr>
          <td>Old primary down</td>
          <td>connection error / health check</td>
      </tr>
      <tr>
          <td>New primary promoted</td>
          <td><code>pg_is_in_recovery() = false</code></td>
      </tr>
      <tr>
          <td>Client reconnect</td>
          <td>first successful write</td>
      </tr>
      <tr>
          <td>Pooler stable</td>
          <td>pool queue / server connection</td>
      </tr>
      <tr>
          <td>Validation complete</td>
          <td>row count / marker sequence</td>
      </tr>
  </tbody>
</table>
<p>Promotion timeline 要保留秒級時間戳。這是評估 RTO、client retry 與 pooler behavior 的基礎。</p>
<h2 id="data-validation">Data Validation</h2>
<p>Data validation 的核心責任是確認 failover 後資料一致性。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">restore_markers</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">marker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;failover-drill&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">max</span><span class="p">(</span><span class="n">created_at</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">restore_markers</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">status</span><span class="p">;</span></span></span></code></pre></div><p>若 workload 有 idempotency key，還要檢查 duplicate。若外部 side effect 參與交易，例如 payment 或 queue，必須有 reconciliation query。</p>
<h2 id="pooler-and-client-behavior">Pooler and Client Behavior</h2>
<p>Pooler and client behavior 的核心責任是確認 failover 後連線能重新指向新 primary。</p>
<p>檢查項目：</p>
<ol>
<li>Application retry 是否有 backoff / jitter。</li>
<li>PgBouncer / proxy 是否清掉舊 server connection。</li>
<li>DNS / endpoint TTL 是否符合 RTO。</li>
<li>Read-only error 是否被正確分類。</li>
<li>Migration / background job 是否暫停。</li>
</ol>
<p>Failover 的完成標準包含 database promote、client reconnect 與 pooler stable。若 client 長時間連到舊 primary 或 pooler 卡住，服務仍處於 unavailable 狀態。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>Incident decision log 的核心責任是把演練變成可審查紀錄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Incident / drill id:
</span></span><span class="line"><span class="ln">2</span><span class="cl">Decision: promote standby
</span></span><span class="line"><span class="ln">3</span><span class="cl">Reason:
</span></span><span class="line"><span class="ln">4</span><span class="cl">Accepted data loss:
</span></span><span class="line"><span class="ln">5</span><span class="cl">RTO observed:
</span></span><span class="line"><span class="ln">6</span><span class="cl">Client impact:
</span></span><span class="line"><span class="ln">7</span><span class="cl">Validation result:
</span></span><span class="line"><span class="ln">8</span><span class="cl">Follow-up:</span></span></code></pre></div><p>每次 drill 都要產生 follow-up。常見 follow-up 是調整 retry、降低 DNS TTL、補 pooler command、增加 validation query 或改善 monitoring。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>完成本篇後，HA 架構讀 <a href="../../patroni-ha/">Patroni HA</a>；跨區災難復原讀 <a href="../../cross-region-dr/">Cross-region DR</a>；connection retry 與 pooler 行為讀 <a href="../connection-pool-lab/">Connection Pool Lab</a>。</p>
]]></content:encoded></item></channel></rss>