<?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>Redis on Tarragon</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/</link><description>Recent content in Redis on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 01 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/index.xml" rel="self" type="application/rss+xml"/><item><title>Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &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>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type B drop-in&lt;/strong>（全維度 Low），結構走 6-section + 相容性 audit 前置。實機驗證於 valkey/valkey:8（valkey_version 8.1.8、redis_version 7.2.4）、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="同一份程式碼不同授權">同一份程式碼、不同授權&lt;/h2>
&lt;p>多數 migration 的工作量在「source 跟 target 不一樣」——schema 要翻譯、API 要改、資料要轉。Redis → Valkey 幾乎沒有這個問題：Valkey 是 2024 年從 Redis 7.2.4 直接 fork 出來的，那一刻它跟 Redis 是 bit-for-bit 同一份程式碼。RDB 與 AOF 檔案格式相同（可以直接把 Redis 的資料目錄拷給 Valkey 載入）、RESP 協定相同、所有 Redis client library 不改一行就能連。技術上，這是 cache 領域最容易的遷移。&lt;/p>
&lt;p>那為什麼要寫一篇 playbook？因為這個遷移的工作量不在資料層，在兩個別的地方。第一是&lt;strong>授權&lt;/strong>——Redis 2024 改成 RSALv2 / SSPL（非 OSI 認可），Valkey 是 BSD 3-clause（OSI、Linux Foundation 治理），這個遷移的整個 driver 是授權合規，而合規驗證有它自己的流程。第二是&lt;strong>fork 後的分歧&lt;/strong>——fork 那一刻兩者相同，但之後各自演進：Redis 加了 7.4+ 的新功能、Valkey 加了自己的（如 8.x 多執行緒），用到 fork 之後 Redis 新功能的部署會有相容缺口。&lt;/p>
&lt;p>&lt;code>INFO server&lt;/code> 上看得到這個「同源但分歧」的事實：&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">valkey-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|valkey_version&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:7.2.4 ← fork 點、client 以此判斷相容性（裝成 Redis 7.2.4）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># valkey_version:8.1.8 ← Valkey 自己的演進線&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>redis_version:7.2.4&lt;/code> 是相容性的保證（client 看到就以 Redis 7.2.4 行為運作）；&lt;code>valkey_version&lt;/code> 是分歧的證據。這篇 playbook 處理的就是「資料層幾乎零工作、工作在授權與分歧盤點」的 drop-in 遷移。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit為什麼是-type-b">6 維 diff dimension audit：為什麼是 Type B&lt;/h2>
&lt;p>跑 diff dimension audit，Redis → Valkey 全維度 Low：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>同 Redis 7.2.4（fork 同源）、RESP 協定一致&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>同 redis.conf、同監控指標、同 CLI 命令&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>完全相同（同一份 code base 演進）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>1 → 1（單服務換單服務）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>零（所有 Redis client library 直接相容）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>RDB / AOF 檔案相容、可直接拷資料目錄&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>全 Low → &lt;strong>Type B drop-in&lt;/strong>（6-section + 相容性 audit 前置、週期 1-4 週）。跟同模組的 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> 對照：DragonflyDB 是 C++ 重寫（drop-in 但 Lua / encoding / module 有差異），Valkey 是 fork（同源、連 RDB 檔都相容）——Valkey 的相容度比 DragonflyDB 更高，是 Type B 裡最純粹的一端。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <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>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。跑 6 維 diff dimension audit 後判定為 <strong>Type B drop-in</strong>（全維度 Low），結構走 6-section + 相容性 audit 前置。實機驗證於 valkey/valkey:8（valkey_version 8.1.8、redis_version 7.2.4）、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="同一份程式碼不同授權">同一份程式碼、不同授權</h2>
<p>多數 migration 的工作量在「source 跟 target 不一樣」——schema 要翻譯、API 要改、資料要轉。Redis → Valkey 幾乎沒有這個問題：Valkey 是 2024 年從 Redis 7.2.4 直接 fork 出來的，那一刻它跟 Redis 是 bit-for-bit 同一份程式碼。RDB 與 AOF 檔案格式相同（可以直接把 Redis 的資料目錄拷給 Valkey 載入）、RESP 協定相同、所有 Redis client library 不改一行就能連。技術上，這是 cache 領域最容易的遷移。</p>
<p>那為什麼要寫一篇 playbook？因為這個遷移的工作量不在資料層，在兩個別的地方。第一是<strong>授權</strong>——Redis 2024 改成 RSALv2 / SSPL（非 OSI 認可），Valkey 是 BSD 3-clause（OSI、Linux Foundation 治理），這個遷移的整個 driver 是授權合規，而合規驗證有它自己的流程。第二是<strong>fork 後的分歧</strong>——fork 那一刻兩者相同，但之後各自演進：Redis 加了 7.4+ 的新功能、Valkey 加了自己的（如 8.x 多執行緒），用到 fork 之後 Redis 新功能的部署會有相容缺口。</p>
<p><code>INFO server</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">valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← fork 點、client 以此判斷相容性（裝成 Redis 7.2.4）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># valkey_version:8.1.8   ← Valkey 自己的演進線</span></span></span></code></pre></div><p><code>redis_version:7.2.4</code> 是相容性的保證（client 看到就以 Redis 7.2.4 行為運作）；<code>valkey_version</code> 是分歧的證據。這篇 playbook 處理的就是「資料層幾乎零工作、工作在授權與分歧盤點」的 drop-in 遷移。</p>
<h2 id="6-維-diff-dimension-audit為什麼是-type-b">6 維 diff dimension audit：為什麼是 Type B</h2>
<p>跑 diff dimension audit，Redis → Valkey 全維度 Low：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Redis 7.2.4（fork 同源）、RESP 協定一致</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 redis.conf、同監控指標、同 CLI 命令</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>完全相同（同一份 code base 演進）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>1 → 1（單服務換單服務）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>零（所有 Redis client library 直接相容）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>RDB / AOF 檔案相容、可直接拷資料目錄</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>全 Low → <strong>Type B drop-in</strong>（6-section + 相容性 audit 前置、週期 1-4 週）。跟同模組的 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 對照：DragonflyDB 是 C++ 重寫（drop-in 但 Lua / encoding / module 有差異），Valkey 是 fork（同源、連 RDB 檔都相容）——Valkey 的相容度比 DragonflyDB 更高，是 Type B 裡最純粹的一端。</p>
<p>這個遷移的特殊之處是 driver 在資料層之外：它是<strong>授權 / 合規驅動</strong>。依 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的漏類處理，政策 / 合規驅動的遷移資料層仍走 Type B，但 audit 重點多一塊<strong>授權驗證與證據收集</strong>。</p>
<h2 id="相容性-auditcutover-前要確認的清單">相容性 audit：cutover 前要確認的清單</h2>
<p>Valkey 號稱 100% 相容 Redis 7.2.4，但「100%」的邊界在 fork 之後的分歧。Pre-migration 必跑的 audit：</p>
<table>
  <thead>
      <tr>
          <th>Redis feature</th>
          <th>Valkey 相容程度</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Core data types / commands / RESP</td>
          <td>完全相容（fork 自 7.2.4）</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RDB / AOF 檔案格式</td>
          <td>完全相容（可直接拷資料目錄）</td>
          <td>無需轉檔</td>
      </tr>
      <tr>
          <td>Eviction / persistence / pub-sub</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>Client libraries</td>
          <td>完全相容（透過 redis_version 協商）</td>
          <td>無需改 code</td>
      </tr>
      <tr>
          <td>Cluster / Sentinel</td>
          <td>完全相容（同 Redis 模型）</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>Redis 7.4+ 新功能（fork 後新增）</td>
          <td>Valkey 不一定跟進</td>
          <td>盤點是否用到、確認 Valkey 對應</td>
      </tr>
      <tr>
          <td>Redis Stack 商業 module（JSON/Search）</td>
          <td>不相容（Valkey 有 valkey-search / valkey-bloom）</td>
          <td>盤點 module 使用、確認替代或改寫</td>
      </tr>
      <tr>
          <td>RedisInsight 等 Redis Inc 監控工具</td>
          <td>部分 vendor-specific 命令缺</td>
          <td>改通用工具（valkey-cli / redis_exporter）</td>
      </tr>
  </tbody>
</table>
<p><strong>audit 的關鍵 output</strong>：兩份清單——(1) 用到的 Redis 7.4+ 功能（fork 後新增、Valkey 可能沒有）、(2) 載入的 Redis Stack module。這兩塊是僅有的相容風險，其餘資料層零工作。盤點方法：</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"># 盤點載入的 module（最大相容風險）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli MODULE LIST
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 盤點是否用到 7.4+ 功能（抓 production traffic 對照 Redis 7.4 changelog）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">redis-cli MONITOR    <span class="c1"># 限時抓樣、grep 可疑的新命令</span></span></span></code></pre></div><h2 id="step-by-step-cutover">Step-by-step cutover</h2>
<p>因為 RDB 檔案相容，cutover 比 DragonflyDB 更簡單（無版本轉換風險）：</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. 部署 Valkey（同 Redis 配置、可直接沿用 redis.conf）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name valkey -p 6380:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v /data/valkey:/data <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  valkey/valkey:8 valkey-server /etc/valkey/valkey.conf
</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"># 2. Redis 端 BGSAVE 產生 RDB</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli -h redis-primary BGSAVE
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">redis-cli -h redis-primary INFO Persistence <span class="p">|</span> grep rdb_last_save_time
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 3. 把 dump.rdb 拷給 Valkey（檔案格式相容、無需轉換）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">scp redis-primary:/var/lib/redis/dump.rdb valkey-host:/data/valkey/
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 4. 重啟 Valkey 載入 RDB</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">docker restart valkey
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 5. 驗證資料一致 + 版本</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">valkey-cli -h valkey-host -p <span class="m">6380</span> DBSIZE          <span class="c1"># 對齊 Redis DBSIZE</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">valkey-cli -h valkey-host -p <span class="m">6380</span> INFO server <span class="p">|</span> grep redis_version  <span class="c1"># 7.2.4</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 6. 替代方案（零停機）：用 replicaof 讓 Valkey 當 Redis 的 replica、即時同步後 promote</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">#    valkey-cli -h valkey-host REPLICAOF redis-primary 6379</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1">#    重要邊界：此路徑只在 source 是 Redis 7.2 或更早版本時成立。</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1">#    Redis 7.4+（Community Edition）改了複製格式、Valkey 無法當其 replica</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1">#    → source 為 7.4+ 時改走上面的 RDB 拷貝路徑（步驟 2-4）。</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># 7. Cutover：client 配置切到 Valkey endpoint、Redis 留 standby</span></span></span></code></pre></div><p>關鍵時間點：</p>
<ul>
<li><strong>RDB 拷貝 + load</strong>：100GB 約 5-15 分鐘（無版本轉換、比 DragonflyDB 少一道風險）</li>
<li><strong>replicaof 路徑</strong>：要零停機可讓 Valkey 當 Redis replica 即時同步、確認 lag 趨零後 promote + 切 client（僅限 source 為 Redis 7.2 或更早；7.4+ 複製格式已分歧、不適用、改走 RDB 拷貝）</li>
<li><strong>Cutover</strong>：client 配置切換（單次完成、硬邊界）、Redis 留 standby 1-2 週</li>
<li><strong>Decom</strong>：無相容問題後關閉 Redis</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1用到-redis-74-功能valkey-沒有">Case 1：用到 Redis 7.4+ 功能、Valkey 沒有</h3>
<p><strong>徵兆</strong>：cutover 後某功能報 <code>unknown command</code> 或行為不同，命令是 Redis 在 7.4 之後（fork 點之後）才加的。</p>
<p><strong>根因</strong>：Valkey fork 自 Redis 7.2.4，Redis 7.4+ 新增的功能 Valkey 不一定跟進。pre-migration audit 漏掉了這些 fork 後的新功能。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration 對照 Redis 7.4+ changelog 盤點用到的新功能（audit 清單第一項）</li>
<li>Valkey 有對應就確認版本、沒有就評估改寫或留在 Redis 商業版</li>
<li>多數標準 cache 用法不碰 7.4+ 新功能，這個風險集中在用了較新進階功能的部署</li>
<li>Valkey 自己的 roadmap（valkey.io）會逐步補上 Redis 新功能，可追蹤</li>
</ol>
<h3 id="case-2載入了-redis-stack-商業-module">Case 2：載入了 Redis Stack 商業 module</h3>
<p><strong>徵兆</strong>：cutover 後 <code>JSON.SET</code> / <code>FT.SEARCH</code> 報 <code>unknown command</code>，application 部分功能失效。</p>
<p><strong>根因</strong>：用了 Redis Stack 的商業 module（RedisJSON / RedisSearch），這些不在 fork 範圍。Valkey 有自己的 valkey-search / valkey-bloom，但不是同一套命令、要另外安裝。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration <code>MODULE LIST</code> 盤點所有載入的 module（audit 清單第二項）</li>
<li>確認 Valkey 對應替代（valkey-search 對 RedisSearch）、確認命令相容度</li>
<li>沒有對應的評估改 module-free 設計（JSON 操作拉回 application 層）或留在 Redis Inc 商業版</li>
<li>對應 <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 相容性 deep article</a> 的三層相容邊界</li>
</ol>
<h3 id="case-3以為換-valkey-解決了記憶體--fork-問題">Case 3：以為換 Valkey 解決了記憶體 / fork 問題</h3>
<p><strong>徵兆</strong>：因為 Redis 的 OOM 或 fork 延遲尖峰而遷 Valkey，遷完發現同樣問題還在。</p>
<p><strong>根因</strong>：Valkey fork 自 Redis 7.2.4，繼承了完全相同的記憶體模型、eviction 演算法、AOF/RDB fork 機制。這些行為在 Valkey 上一模一樣——遷移沒有改變它們。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>記憶體 / fork 調校在 Valkey 上跟 Redis 完全相同，直接套用 <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></li>
<li>遷 Valkey 的理由應是授權合規 / 多執行緒吞吐 / managed 成本，不是記憶體問題</li>
<li>fork 尖峰要根治走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 fork-less，不是換 Valkey</li>
<li>遷移前釐清痛點是授權（Valkey 解）還是架構（Valkey 不解）</li>
</ol>
<h3 id="case-4授權合規驗證沒做完整合規卡關">Case 4：授權合規驗證沒做完整、合規卡關</h3>
<p><strong>徵兆</strong>：技術遷移完成、但法務 / 合規 review 要求證明「不再使用 RSALv2 / SSPL 授權的軟體」，缺少證據。</p>
<p><strong>根因</strong>：這個遷移的 driver 是授權合規，但團隊只做了技術 cutover、沒收集合規證據。Redis 的 binary / image / 相依套件若還殘留在某些環境，合規目標沒真正達成。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>盤點所有環境（dev / staging / prod / CI）的 Redis binary / image / 相依，確認全部換成 Valkey</li>
<li>收集合規證據：image SBOM、套件清單、部署 manifest 顯示 Valkey BSD 授權</li>
<li>把「不再使用非 OSI 授權 cache」寫成可驗證的 CI 檢查（掃 image / 依賴）</li>
<li>依 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的合規驅動漏類，audit 重點就是 evidence collection</li>
</ol>
<h3 id="case-5監控-dashboard-部分指標斷掉">Case 5：監控 dashboard 部分指標斷掉</h3>
<p><strong>徵兆</strong>：cutover 後 RedisInsight 或某監控 dashboard 部分面板空白、vendor-specific 命令回錯。</p>
<p><strong>根因</strong>：RedisInsight 等 Redis Inc 工具有部分偏商業版的命令，Valkey 不一定實作。核心指標通用，但進階面板可能缺。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>監控改用通用工具：valkey-cli INFO、Prometheus + redis_exporter（相容 Valkey）、Grafana</li>
<li>核心指標（used_memory / keyspace_hits / connected_clients）在 Valkey 完全相容、覆蓋不受影響</li>
<li>把監控相容性納入 cutover 前驗證、不要遷完才發現面板空白</li>
<li>RedisInsight 連 Valkey 多數仍可用、只是部分 vendor 進階面板缺</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis（self-managed）</th>
          <th>Valkey（self-managed）</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>授權</td>
          <td>RSALv2 / SSPL（非 OSI）</td>
          <td>BSD 3-clause（OSI、Linux Foundation）</td>
          <td>Valkey 對合規敏感場景是決定性優勢</td>
      </tr>
      <tr>
          <td>核心效能</td>
          <td>baseline</td>
          <td>同 Redis 7.2.4 + 8.x 多執行緒選項</td>
          <td>Valkey 多核 workload 可更高（依 workload）</td>
      </tr>
      <tr>
          <td>相容度</td>
          <td>原生</td>
          <td>100%（fork、檔案相容）</td>
          <td>平手（同源）</td>
      </tr>
      <tr>
          <td>記憶體 / fork</td>
          <td>baseline</td>
          <td>完全相同（同源）</td>
          <td>平手（遷移不改變這層）</td>
      </tr>
      <tr>
          <td>7.4+ 新功能</td>
          <td>有</td>
          <td>不一定跟進</td>
          <td>Redis 領先（用到才在意）</td>
      </tr>
      <tr>
          <td>Redis Stack module</td>
          <td>RedisJSON / Search / Graph</td>
          <td>valkey-search / valkey-bloom（不同套）</td>
          <td>Redis 商業 module 較全</td>
      </tr>
      <tr>
          <td>managed 選項</td>
          <td>ElastiCache for Redis（legacy）</td>
          <td>ElastiCache for Valkey（AWS default、約低 20%）</td>
          <td>Valkey 在 AWS 生態成本優勢</td>
      </tr>
      <tr>
          <td>遷移成本</td>
          <td>—</td>
          <td>極低（drop-in + 檔案相容）</td>
          <td>Valkey 是最容易的遷移目標</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：合規敏感（公部門 / 企業 OSI 政策）或想降 managed 成本 → 遷 Valkey（drop-in、風險集中在 module / 7.4+ 盤點）；重度依賴 Redis Stack 商業 module → 留 Redis Inc 商業版。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-elasticache-for-valkey-對位">跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache for Valkey</a> 對位</h3>
<p>AWS 已把 ElastiCache default engine 設為 Valkey（約低 Redis 20%）。自管 Redis → ElastiCache for Valkey 是「換授權 + 轉 managed」一次到位，但要同時處理 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">managed 責任邊界</a>（failover / cluster mode / client 重連）。</p>
<h3 id="跟-client--監控整合">跟 client / 監控整合</h3>
<p>client library 零改（透過 redis_version 協商）；監控把 exporter 指向 Valkey 即可（redis_exporter 相容）、RedisInsight 部分面板需換通用工具。</p>
<h3 id="跟-valkey-8-多執行緒對位">跟 Valkey 8 多執行緒對位</h3>
<p>遷移後可評估開 Valkey 8 的 io-threads 榨多核吞吐（Redis 7.2.4 沒有的能力），見 <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 相容性與 io-threads deep article</a>。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>反向遷移</strong>（Valkey → Redis）：僅在重度依賴 Redis 7.4+ 功能或 Stack 商業 module 時需要、同樣 drop-in</li>
<li><strong>跨雲 managed Valkey</strong>：GCP Memorystore / Azure Cache 的 Valkey 支援陸續推出、評估 vendor boundary</li>
<li><strong>授權合規 CI 化</strong>：把「不使用非 OSI 授權 cache」寫成持續檢查</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source 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>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>平行 migration playbook：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>（重寫型 drop-in）、<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（Type B drop-in + 合規驅動漏類）</li>
</ul>
]]></content:encoded></item><item><title>Redis → DragonflyDB：drop-in 相容下的容量躍升 + 5 個踩雷</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &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>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a>（target）。跟前一篇 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security&lt;/a> 的 6-phase playbook 對照、Redis → DragonflyDB 是 &lt;em>drop-in 相容&lt;/em> 形態的 migration、結構更接近 &lt;a href="https://tarrragon.github.io/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 deep article methodology&lt;/a> 的 6-section flow + 一段「相容性驗證」前置。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼遷cost--single-thread--multi-tenancy-三條-driver">為什麼遷：cost / single-thread / multi-tenancy 三條 driver&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>觸發場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Memory cost&lt;/strong>&lt;/td>
 &lt;td>Redis 6.x cluster 跑 1-10 TB 時、機器成本爆；DragonflyDB 記憶體效率提升 ~30%、相同 dataset 少 30% RAM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Single-thread bottleneck&lt;/strong>&lt;/td>
 &lt;td>Redis 主執行緒在單一 hot key 寫入時是瓶頸、scale-up 受限；DragonflyDB 多執行緒 + shared-nothing 設計、單機 throughput 號稱 25x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Multi-tenancy&lt;/strong>&lt;/td>
 &lt;td>Redis Cluster 多 namespace 需要 cluster-per-tenant、運維成本爆；DragonflyDB 設計上 namespace 隔離成本低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向 driver（DragonflyDB → Redis）也存在 — 主要是 &lt;em>Redis Modules 依賴&lt;/em>（RedisJSON / RedisSearch / RedisGraph）DragonflyDB 不支援、或 &lt;em>Lua script 用了 redis.call 進階 API&lt;/em>。&lt;/p>
&lt;h2 id="跟-phased-migration-的對照drop-in-不需要-phased">跟 phased migration 的對照：drop-in 不需要 phased&lt;/h2>
&lt;p>跟前一篇 Splunk → Elastic 的 6-phase playbook 不同、Redis → DragonflyDB 的 migration &lt;em>結構接近 standard deep article&lt;/em>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <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>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（target）。跟前一篇 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> 的 6-phase playbook 對照、Redis → DragonflyDB 是 <em>drop-in 相容</em> 形態的 migration、結構更接近 <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 deep article methodology</a> 的 6-section flow + 一段「相容性驗證」前置。</p></blockquote>
<h2 id="為什麼遷cost--single-thread--multi-tenancy-三條-driver">為什麼遷：cost / single-thread / multi-tenancy 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Memory cost</strong></td>
          <td>Redis 6.x cluster 跑 1-10 TB 時、機器成本爆；DragonflyDB 記憶體效率提升 ~30%、相同 dataset 少 30% RAM</td>
      </tr>
      <tr>
          <td><strong>Single-thread bottleneck</strong></td>
          <td>Redis 主執行緒在單一 hot key 寫入時是瓶頸、scale-up 受限；DragonflyDB 多執行緒 + shared-nothing 設計、單機 throughput 號稱 25x</td>
      </tr>
      <tr>
          <td><strong>Multi-tenancy</strong></td>
          <td>Redis Cluster 多 namespace 需要 cluster-per-tenant、運維成本爆；DragonflyDB 設計上 namespace 隔離成本低</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（DragonflyDB → Redis）也存在 — 主要是 <em>Redis Modules 依賴</em>（RedisJSON / RedisSearch / RedisGraph）DragonflyDB 不支援、或 <em>Lua script 用了 redis.call 進階 API</em>。</p>
<h2 id="跟-phased-migration-的對照drop-in-不需要-phased">跟 phased migration 的對照：drop-in 不需要 phased</h2>
<p>跟前一篇 Splunk → Elastic 的 6-phase playbook 不同、Redis → DragonflyDB 的 migration <em>結構接近 standard deep article</em>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Splunk → Elastic（phased）</th>
          <th>Redis → DragonflyDB（drop-in）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema 對位</td>
          <td>需要（SPL ↔ KQL / CIM ↔ ECS）</td>
          <td>不需要（RESP protocol 相容）</td>
      </tr>
      <tr>
          <td>Rule translation</td>
          <td>4-12 週 SOC engineering 工作</td>
          <td>不需要（command 直接相容）</td>
      </tr>
      <tr>
          <td>Parallel run</td>
          <td>4-8 週 dual-SIEM 跑</td>
          <td>1-7 天 dual-write 觀察</td>
      </tr>
      <tr>
          <td>Cutover 邊界</td>
          <td>軟邊界（routing 切換、可逆 30 分鐘）</td>
          <td>硬邊界（client 配置切換、單次完成）</td>
      </tr>
      <tr>
          <td>不可逆 cleanup</td>
          <td>1 年後 archive</td>
          <td>立刻（DragonflyDB 接管後 Redis 可關）</td>
      </tr>
      <tr>
          <td>整體週期</td>
          <td>4-9 個月</td>
          <td>1-4 週</td>
      </tr>
  </tbody>
</table>
<p><strong>判斷依據</strong>：migration 結構由 <em>source 跟 target 的 schema / protocol 差異程度</em> 決定、不是 universal phased playbook。本批第 2 篇驗證 <em>deep article methodology 的 6-section 框架</em> 在 drop-in migration 仍適用（只需前置 <em>相容性驗證</em> 段、其他 6 段對位）。</p>
<h2 id="相容性驗證在-cutover-前要確認的清單">相容性驗證：在 cutover 前要確認的清單</h2>
<p>DragonflyDB 號稱 Redis drop-in、但「drop-in」涵蓋範圍依 Redis feature 使用程度而定。Pre-migration 必跑的相容性 audit：</p>
<table>
  <thead>
      <tr>
          <th>Redis feature</th>
          <th>DragonflyDB 支援程度</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Basic data types (String / Hash / List / Set / ZSet)</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RESP protocol v2 / v3</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RDB load</td>
          <td>Redis 6.x RDB 完全相容；7.x 部分 feature 待測</td>
          <td>用 BGSAVE → 切換 → load 驗證</td>
      </tr>
      <tr>
          <td>AOF</td>
          <td>DragonflyDB 不用 AOF、改 <em>snapshotting</em> 模式</td>
          <td>不直接 import AOF、需經 RDB 中介</td>
      </tr>
      <tr>
          <td>Lua scripts</td>
          <td>90% 相容、部分 redis.call API + EVAL 邊界 case 差異</td>
          <td>Lua script audit 必跑、不能假設全相容</td>
      </tr>
      <tr>
          <td>Pub/Sub</td>
          <td>相容、但 message fanout 行為差異（多 thread 處理）</td>
          <td>高 fanout pub/sub 場景需測 latency</td>
      </tr>
      <tr>
          <td>Cluster mode</td>
          <td>DragonflyDB <em>單機</em> 即可達 cluster throughput、不必 cluster；emulated cluster mode 部分相容</td>
          <td>評估是否仍需 cluster</td>
      </tr>
      <tr>
          <td>Sentinel HA</td>
          <td>不直接支援、用 DragonflyDB 自家 replication</td>
          <td>HA 架構重設計</td>
      </tr>
      <tr>
          <td>Redis Modules (RedisJSON / Search / Graph)</td>
          <td><strong>不支援</strong></td>
          <td>必須前置改寫 application</td>
      </tr>
      <tr>
          <td>Streams</td>
          <td>相容、但 consumer group 行為部分差異</td>
          <td>Stream consumer 跑 dual-write 觀察</td>
      </tr>
      <tr>
          <td>Keyspace notifications</td>
          <td>相容</td>
          <td>無需處理</td>
      </tr>
  </tbody>
</table>
<p><strong>Audit 的關鍵 output</strong>：列「不相容功能」清單 + 對應 application code 修改範圍；若 Modules 在 production 使用、migration <em>退役</em>。</p>
<h2 id="step-by-step-cutover">Step-by-step cutover</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"># 1. 部署 DragonflyDB</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name dragonfly -p 6380:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v /data/dragonfly:/data <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  docker.dragonflydb.io/dragonflydb/dragonfly:latest <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --logtostderr --requirepass<span class="o">=</span>&lt;your_password&gt;
</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"># 2. Redis 端 BGSAVE</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">redis-cli -h redis-primary BGSAVE
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 等到 BGSAVE 完成</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli -h redis-primary INFO Persistence <span class="p">|</span> grep rdb_last_save_time
</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"># 3. 把 dump.rdb 拷到 DragonflyDB</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">scp redis-primary:/var/lib/redis/dump.rdb dragonfly-host:/data/dragonfly/
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 4. 重啟 DragonflyDB 載入 RDB</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">docker restart dragonfly
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># 5. 驗證資料一致</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">redis-cli -h dragonfly-host -p <span class="m">6380</span> DBSIZE
</span></span><span class="line"><span class="ln">20</span><span class="cl">redis-cli -h redis-primary DBSIZE
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># 兩端 key 數對齊</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># 6. Dual-write 1-7 天（application 同時寫兩端）</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"># 7. Read 切換到 DragonflyDB、Redis 端只寫不讀</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># 8. Write 切換、Redis 端 standby</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># 9. 觀察 1-2 週、無異常後 Redis decommission</span></span></span></code></pre></div><p>關鍵時間點：</p>
<ul>
<li><strong>BGSAVE → load</strong>：100GB RDB 約 5-15 分鐘、跨網路 SCP 時間另算</li>
<li><strong>Dual-write window</strong>：1-7 天觀察、application 寫兩端、read 仍走 Redis</li>
<li><strong>Cutover</strong>：read switch → write switch、每步間隔 24 小時</li>
<li><strong>Decom</strong>：Redis 保留 standby 1-2 週、無異常後關閉</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1rdb-版本差dragonflydb-load-失敗">Case 1：RDB 版本差，DragonflyDB load 失敗</h3>
<p><strong>徵兆</strong>：Redis 7.2 端 BGSAVE 出的 <code>dump.rdb</code> 在 DragonflyDB load 時報 <code>Unsupported RDB version</code>、DragonflyDB 啟動失敗。</p>
<p><strong>根因</strong>：Redis 7.2 RDB version 11 含新 feature（function library / sharded pubsub）DragonflyDB 當前 release 沒支援；版本相容性需逐 release 確認。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration 版本相容矩陣 audit</strong>：DragonflyDB release note 對照 Redis version、確認 RDB version 支援</li>
<li><strong>降級 BGSAVE</strong>：Redis 端設 <code>rdb-version 9</code>（Redis 6.x 兼容版本）、犧牲 Redis 7.x 新 feature</li>
<li><strong>替代方案</strong>：用 <code>redis-cli --scan</code> + <code>MIGRATE</code> 命令 incremental 搬、不用 RDB；速度慢 100x 但相容性好</li>
</ol>
<h3 id="case-2lua-script-跑進-eval-不一致">Case 2：Lua script 跑進 EVAL 不一致</h3>
<p><strong>徵兆</strong>：dual-write 階段、發現某些 EVAL script 在 Redis 跟 DragonflyDB 結果不同；具體是某個 <code>redis.call(&quot;OBJECT&quot;, &quot;ENCODING&quot;, key)</code> 在 DragonflyDB 回不一樣的 encoding 字串。</p>
<p><strong>根因</strong>：DragonflyDB 內部不用 Redis 的 ziplist / listpack encoding（dashtable 不需要）、<code>OBJECT ENCODING</code> 返回值不對等；script 邏輯依賴 encoding 來決定行為、結果不同。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Audit Lua script</strong>：grep 所有 <code>redis.call(&quot;OBJECT&quot;</code>、列出依賴 encoding 的 script</li>
<li><strong>改寫 application</strong>：不依賴 encoding、改用 <code>MEMORY USAGE</code> 或 high-level check</li>
<li><strong>接受差異</strong>：DragonflyDB 不會回 encoding 但 functional 結果對等、SOC review 確認可接受</li>
</ol>
<h3 id="case-3pubsub-fanout-高負載-latency">Case 3：Pub/Sub fanout 高負載 latency</h3>
<p><strong>徵兆</strong>：production 切到 DragonflyDB 後、Pub/Sub 訂閱端 latency p99 從 5ms 漲到 20-50ms；topic fanout &gt;10K subscriber 場景。</p>
<p><strong>根因</strong>：DragonflyDB 多 thread 設計、Pub/Sub message 在 thread 間 dispatch 需要 routing；Redis single-thread 沒這個 overhead。高 fanout 是 DragonflyDB 設計取捨。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>架構</strong>：高 fanout Pub/Sub 不用 DragonflyDB、改 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> / Redis Streams + consumer group</li>
<li><strong>DragonflyDB 配置調整</strong>：<code>--proactor_threads</code> 對 Pub/Sub 影響大、調到符合 CPU 核心數</li>
<li><strong>接受 latency</strong>：&lt; 10K subscriber 差異可忽略、不必動</li>
</ol>
<h3 id="case-4cluster-mode-看似相容但-slot-routing-行為差">Case 4：Cluster mode 看似相容但 slot routing 行為差</h3>
<p><strong>徵兆</strong>：application 用 Redis Cluster client（lettuce / Jedis cluster mode）連 DragonflyDB emulated cluster、運行幾天後 <code>MOVED</code> redirect 異常、key 找不到。</p>
<p><strong>根因</strong>：DragonflyDB emulated cluster mode 是 <em>single node 模擬</em>、CLUSTER SLOTS 返回固定 mapping；某些 client 端 cluster topology cache 跟實際 routing 不對齊、發 redirect。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application 改 standalone client</strong>：DragonflyDB single node 已能達 cluster 級 throughput、不必用 cluster client</li>
<li><strong>Client config</strong>：lettuce 端 <code>clusterTopologyRefreshOptions(...)</code> 設較長 refresh、減少 redirect 機會</li>
<li><strong>長期</strong>：等 DragonflyDB cluster 正式 GA 後再評估</li>
</ol>
<h3 id="case-5modules-用了沒注意migration-卡住">Case 5：Modules 用了沒注意，migration 卡住</h3>
<p><strong>徵兆</strong>：cutover 後幾天、application 某個功能完全壞、log 顯示 <code>ERR unknown command 'JSON.SET'</code>；DragonflyDB 不支援 RedisJSON。</p>
<p><strong>根因</strong>：Pre-migration audit 漏掉 application 用了 RedisJSON（透過某 client library 抽象）；DragonflyDB 不支援該 Module 命令、application 直接壞。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration audit 必跑</strong>：<code>MONITOR</code> 抓 1 小時 production traffic、grep 非 standard command（<code>JSON.*</code> / <code>FT.*</code> / <code>GRAPH.*</code>）</li>
<li><strong>應急回退</strong>：Redis standby 還在、application client config 切回</li>
<li><strong>長期</strong>：JSON 改用 standard Hash + serialization、Search 改 Elasticsearch / Meilisearch、Graph 改 Neo4j</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis（self-managed）</th>
          <th>DragonflyDB</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-node throughput</td>
          <td>~100K-200K ops/s</td>
          <td>~2-5M ops/s（號稱 25x）</td>
          <td>DragonflyDB 領先、實測依 workload 而定</td>
      </tr>
      <tr>
          <td>Memory efficiency</td>
          <td>baseline</td>
          <td>-30% 平均、依資料分佈</td>
          <td>DragonflyDB 領先</td>
      </tr>
      <tr>
          <td>Persistence</td>
          <td>RDB / AOF 雙模式</td>
          <td>Snapshotting 為主、不用 AOF</td>
          <td>Redis 對 durability 要求高的 workload 仍領先</td>
      </tr>
      <tr>
          <td>HA / Replication</td>
          <td>Sentinel + Cluster 成熟</td>
          <td>自家 replication、HA 文件相對少</td>
          <td>Redis 領先</td>
      </tr>
      <tr>
          <td>Modules ecosystem</td>
          <td>RedisJSON / Search / Graph / TimeSeries</td>
          <td>不支援</td>
          <td>Redis 領先</td>
      </tr>
      <tr>
          <td>Cluster scaling</td>
          <td>Cluster mode 成熟</td>
          <td>單機效能高、cluster 仍 emerging</td>
          <td>Redis 領先、但 DragonflyDB 單機已能 cover 多數 use case</td>
      </tr>
      <tr>
          <td>Total cost (10TB cache)</td>
          <td>$8-15K USD / month</td>
          <td>$2-5K USD / month</td>
          <td>DragonflyDB 顯著便宜</td>
      </tr>
      <tr>
          <td>Operational maturity</td>
          <td>高（10+ 年 production）</td>
          <td>中（2022+、production 案例 1000+）</td>
          <td>Redis 領先</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：cache use case 簡單（pure cache / session store）走 DragonflyDB；複雜 use case（Modules / Pub/Sub fanout / strict durability）保留 Redis。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-client-library-整合">跟 client library 整合</h3>
<p>主流 Redis client（lettuce / Jedis / redis-py / node-redis / go-redis）都直接相容 DragonflyDB；唯一例外是 <em>cluster client</em> 模式行為差（見 Case 4）。</p>
<h3 id="跟-monitoring-整合">跟 monitoring 整合</h3>
<p>DragonflyDB exporter 提供 Prometheus metric、跟 Redis exporter 對應 metric 名稱 80% 相同；grafana dashboard 需小改：</p>
<ul>
<li><code>redis_memory_used_bytes</code> → <code>dragonfly_memory_used_bytes</code></li>
<li><code>redis_commands_processed_total</code> → <code>dragonfly_commands_processed_total</code></li>
</ul>
<h3 id="跟-redis-sentinel-ha-對位">跟 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Redis Sentinel HA</a> 對位</h3>
<p>DragonflyDB 不直接支援 Sentinel、HA 走自家 <em>master-replica</em> + DNS-based failover：</p>
<ol>
<li>DragonflyDB primary + replica</li>
<li>K8s 用 StatefulSet + Service + readiness probe</li>
<li>失敗 failover 比 Sentinel 慢（30s-2min vs 5-15s）</li>
</ol>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>DragonflyDB Cluster GA</strong>：正式 cluster mode 出來後重評估</li>
<li><strong>Stream + consumer group 細節</strong>：dual-write 期間驗證每個 consumer pattern</li>
<li><strong>Modules 替代方案</strong>：JSON / Search / Graph 各自的 cloud-native 替代評估</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source 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>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</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>Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &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> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Paradigm = High（multi-paradigm → pure cache）→ Type E paradigm shift&lt;/em>；本文是 &lt;em>paradigm reduction&lt;/em>（downgrade 方向）的 dogfood。&lt;/p>&lt;/blockquote>
&lt;h2 id="memcached-不是-simpler-redis是-cache-paradigm">Memcached 不是 simpler Redis、是 cache paradigm&lt;/h2>
&lt;p>把 Redis → Memcached 當「移除 Redis 功能」是最常見的誤判：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>Redis&lt;/th>
 &lt;th>Memcached&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>核心 paradigm&lt;/td>
 &lt;td>Multi-paradigm（KV + 資料結構 + pub/sub + script）&lt;/td>
 &lt;td>Pure cache（KV + TTL）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Value 類型&lt;/td>
 &lt;td>String / Hash / List / Set / Sorted Set / Stream / Bitmap / HyperLogLog&lt;/td>
 &lt;td>byte string only&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Atomic operations&lt;/td>
 &lt;td>100+（INCR / LPUSH / ZADD / &amp;hellip;）&lt;/td>
 &lt;td>INCR / DECR / APPEND / CAS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Server-side scripting&lt;/td>
 &lt;td>Lua scripts (&lt;code>EVAL&lt;/code>)&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pub/Sub&lt;/td>
 &lt;td>Native&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Persistence&lt;/td>
 &lt;td>RDB / AOF&lt;/td>
 &lt;td>無（restart 全失）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication&lt;/td>
 &lt;td>Async / sync replication&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cluster&lt;/td>
 &lt;td>Redis Cluster + Sentinel HA&lt;/td>
 &lt;td>Memcached cluster（client-side sharding）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Eviction policy&lt;/td>
 &lt;td>8 種（LRU / LFU / random / &amp;hellip;）&lt;/td>
 &lt;td>LRU only&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Expiration accuracy&lt;/td>
 &lt;td>TTL 精確到 ms&lt;/td>
 &lt;td>TTL 精確到 second、lazy expiration&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心差異不在「Memcached 少了 Redis 功能」、在「Memcached 是不同的 cache paradigm」。&lt;/strong> Redis 的 features（hash / sorted set / pub/sub）多數 &lt;em>不該移除&lt;/em>、是 &lt;em>重新分配到對應 specialized service&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>Hash / sorted set → application 端用 JSON + 自管 index&lt;/li>
&lt;li>Pub/Sub → message queue（NATS / Redis Streams / Kafka）&lt;/li>
&lt;li>Lua scripts → application code&lt;/li>
&lt;li>Persistence → 真正需要的 data 該存 DB、不是 cache&lt;/li>
&lt;li>Replication / cluster → Memcached 自己 cluster strategy&lt;/li>
&lt;/ul>
&lt;h2 id="為什麼遷simplification--cost--ops-三條-driver">為什麼遷：simplification / cost / ops 三條 driver&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Operational simplification&lt;/strong>：Memcached 沒 persistence / replication / cluster mode、ops surface 縮小、團隊不用懂 Redis 25+ command family&lt;/li>
&lt;li>&lt;strong>Cost&lt;/strong>：對 &lt;em>純 cache use case&lt;/em> 而言、Memcached 每 GB 比 Redis 便宜（memory efficiency 略勝 + 無 persistence overhead）&lt;/li>
&lt;li>&lt;strong>Strict cache discipline&lt;/strong>：Memcached &lt;em>逼&lt;/em> application code 把「真正的 cache」跟「半 persistent state」分開、避免 Redis 變 &lt;em>poor man&amp;rsquo;s database&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>反向 driver（Memcached → Redis）：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <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> 跟 <a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Paradigm = High（multi-paradigm → pure cache）→ Type E paradigm shift</em>；本文是 <em>paradigm reduction</em>（downgrade 方向）的 dogfood。</p></blockquote>
<h2 id="memcached-不是-simpler-redis是-cache-paradigm">Memcached 不是 simpler Redis、是 cache paradigm</h2>
<p>把 Redis → Memcached 當「移除 Redis 功能」是最常見的誤判：</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>Redis</th>
          <th>Memcached</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心 paradigm</td>
          <td>Multi-paradigm（KV + 資料結構 + pub/sub + script）</td>
          <td>Pure cache（KV + TTL）</td>
      </tr>
      <tr>
          <td>Value 類型</td>
          <td>String / Hash / List / Set / Sorted Set / Stream / Bitmap / HyperLogLog</td>
          <td>byte string only</td>
      </tr>
      <tr>
          <td>Atomic operations</td>
          <td>100+（INCR / LPUSH / ZADD / &hellip;）</td>
          <td>INCR / DECR / APPEND / CAS</td>
      </tr>
      <tr>
          <td>Server-side scripting</td>
          <td>Lua scripts (<code>EVAL</code>)</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Pub/Sub</td>
          <td>Native</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Persistence</td>
          <td>RDB / AOF</td>
          <td>無（restart 全失）</td>
      </tr>
      <tr>
          <td>Replication</td>
          <td>Async / sync replication</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Cluster</td>
          <td>Redis Cluster + Sentinel HA</td>
          <td>Memcached cluster（client-side sharding）</td>
      </tr>
      <tr>
          <td>Eviction policy</td>
          <td>8 種（LRU / LFU / random / &hellip;）</td>
          <td>LRU only</td>
      </tr>
      <tr>
          <td>Expiration accuracy</td>
          <td>TTL 精確到 ms</td>
          <td>TTL 精確到 second、lazy expiration</td>
      </tr>
  </tbody>
</table>
<p><strong>核心差異不在「Memcached 少了 Redis 功能」、在「Memcached 是不同的 cache paradigm」。</strong> Redis 的 features（hash / sorted set / pub/sub）多數 <em>不該移除</em>、是 <em>重新分配到對應 specialized service</em>：</p>
<ul>
<li>Hash / sorted set → application 端用 JSON + 自管 index</li>
<li>Pub/Sub → message queue（NATS / Redis Streams / Kafka）</li>
<li>Lua scripts → application code</li>
<li>Persistence → 真正需要的 data 該存 DB、不是 cache</li>
<li>Replication / cluster → Memcached 自己 cluster strategy</li>
</ul>
<h2 id="為什麼遷simplification--cost--ops-三條-driver">為什麼遷：simplification / cost / ops 三條 driver</h2>
<ul>
<li><strong>Operational simplification</strong>：Memcached 沒 persistence / replication / cluster mode、ops surface 縮小、團隊不用懂 Redis 25+ command family</li>
<li><strong>Cost</strong>：對 <em>純 cache use case</em> 而言、Memcached 每 GB 比 Redis 便宜（memory efficiency 略勝 + 無 persistence overhead）</li>
<li><strong>Strict cache discipline</strong>：Memcached <em>逼</em> application code 把「真正的 cache」跟「半 persistent state」分開、避免 Redis 變 <em>poor man&rsquo;s database</em></li>
</ul>
<p>反向 driver（Memcached → Redis）：</p>
<ul>
<li>Application 寫到 Memcached 後發現需要 <em>atomic counter / leaderboard / queue / lock</em>、應該升 Redis（不是繼續 wrap Memcached）</li>
</ul>
<h2 id="跑-6-維-audit">跑 6 維 audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Redis 命令集 → Memcached 命令集、相容度 &lt; 20%</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>兩者都簡單、Memcached 略簡單</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Multi-paradigm → pure cache</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 1 個 cache service</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>必改（任何 hash / list / sorted set / pubsub 用法）</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同 single instance / cluster</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>3 維 High（Schema / Paradigm / Application change）多軸高、主導維度 = Paradigm → <strong>Type E paradigm shift</strong>；Schema + Application change 抽獨立段補充。</p>
<h2 id="結構類-type-e--paradigm-reduction-分配路線">結構：類 Type E + paradigm reduction 分配路線</h2>





<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">1. Memcached 不是 simpler Redis（concept reverse 開頭）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 為什麼遷
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">3. 6 維 audit
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">4. Paradigm reduction 路線（Redis features 對應的 specialized service）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">5. Schema 差段（Redis vs Memcached command set）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">6. Application 重設計（per-call-site refactor）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">7. Migration 流程（漸進、部分 use case 切）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">8. Production 故障演練
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">9. Capacity / cost
</span></span><span class="line"><span class="ln">10</span><span class="cl">10. 整合 / 下一步</span></span></code></pre></div><p>10 章節、220-260 行。比 Type E（<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a>）多 <em>paradigm reduction 路線</em> 段。</p>
<h2 id="paradigm-reduction-路線">Paradigm reduction 路線</h2>
<p>Redis features 對應的 specialized service：</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">Redis Hash           → Application 端 JSON.stringify + Memcached SET
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">                       (or 直接存 DB + Memcached cache layer)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Redis List (queue)   → NATS / Kafka / RabbitMQ / SQS
</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">Redis List (stack)   → Application 端用 array + 自管 LIFO
</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">Redis Set            → Application 端用 array + dedup OR 用 DB unique index
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">Redis Sorted Set     → Application 端用 ordered list + comparator
</span></span><span class="line"><span class="ln">11</span><span class="cl">                       OR PostgreSQL + index
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">Redis Stream         → Kafka / Redis Streams (保留) / NATS JetStream
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">Redis Pub/Sub        → NATS Core / Redis Streams / Kafka
</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">Redis Lua script     → Application code（避免 atomic 假設）
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">Redis distributed lock → Consul / etcd / DB advisory lock / Redis (保留)
</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">Redis Bitmap         → DB bit column / 應用端 bitset
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">Redis HyperLogLog    → DB approx_count_distinct / 應用端 cardinality estimator</span></span></code></pre></div><p>Migration scope 包含 <em>每個 Redis-specific feature use case 對應的 service 評估</em>；不是「移除」、是「重新分配」。</p>
<h2 id="application-重設計">Application 重設計</h2>





<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="c1"># Before: Redis hash</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">hset</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="s1">&#39;email&#39;</span><span class="p">,</span> <span class="s1">&#39;a@b.com&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">hset</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">,</span> <span class="s1">&#39;Alice&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">hgetall</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">)</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"># After: Memcached + JSON</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">user_data</span> <span class="o">=</span> <span class="p">{</span><span class="s1">&#39;email&#39;</span><span class="p">:</span> <span class="s1">&#39;a@b.com&#39;</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Alice&#39;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">user_data</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">mc</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">)</span> <span class="ow">or</span> <span class="s1">&#39;</span><span class="si">{}</span><span class="s1">&#39;</span><span class="p">)</span></span></span></code></pre></div>




<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="c1"># Before: Redis sorted set (leaderboard)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">zadd</span><span class="p">(</span><span class="s1">&#39;leaderboard&#39;</span><span class="p">,</span> <span class="p">{</span><span class="s1">&#39;alice&#39;</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s1">&#39;bob&#39;</span><span class="p">:</span> <span class="mi">95</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">top_10</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">zrevrange</span><span class="p">(</span><span class="s1">&#39;leaderboard&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="n">withscores</span><span class="o">=</span><span class="kc">True</span><span class="p">)</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"># After: PostgreSQL + index + Memcached cache</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># Persistent: write to DB</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># Cache: pre-compute top 10 in DB query, cache in Memcached</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">&#39;leaderboard:top10&#39;</span><span class="p">,</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="s1">&#39;SELECT user, score FROM scores ORDER BY score DESC LIMIT 10&#39;</span><span class="p">)))</span></span></span></code></pre></div>




<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="c1"># Before: Redis distributed lock</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">with</span> <span class="n">redis</span><span class="o">.</span><span class="n">lock</span><span class="p">(</span><span class="s1">&#39;resource:1&#39;</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">process_resource</span><span class="p">()</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"># After: PostgreSQL advisory lock OR Consul session</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">with</span> <span class="n">db</span><span class="o">.</span><span class="n">advisory_lock</span><span class="p">(</span><span class="n">resource_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">process_resource</span><span class="p">()</span></span></span></code></pre></div><p>每個 Redis-specific pattern 都要 per-call-site refactor、不是 SDK 換。</p>
<h2 id="migration-流程">Migration 流程</h2>
<p>跟 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 同 <em>partial migration</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">1. Audit application code、列所有 Redis call site + feature 使用
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 按 feature 分類處理 plan:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - Pure KV (GET/SET/DEL/TTL): 切 Memcached 直接
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - Hash → JSON + Memcached: per-call-site refactor
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   - List/Sorted Set: 評估是 queue / leaderboard / 其他用途、對應 service
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   - Pub/Sub: 移到 message queue
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - Lock: 移到 DB 或保留 Redis
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">3. 部分 application 先切（純 KV use case）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">4. 複雜 use case 逐步 refactor 到對應 service
</span></span><span class="line"><span class="ln">10</span><span class="cl">5. Memcached 跑 production 後、Redis 可降為 *narrow scope*（只跑剩餘 Redis-specific feature）
</span></span><span class="line"><span class="ln">11</span><span class="cl">   或完全退役（如果 application 已 refactor 乾淨）
</span></span><span class="line"><span class="ln">12</span><span class="cl">6. 長期混合架構：Memcached cache layer + DB persistent state + 可選的 Redis（locks / specialty）</span></span></code></pre></div><p>整體 3-12 個月、依 Redis-specific feature 使用深度。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1hash--json-後-getset-round-trip-變-n1">Case 1：Hash → JSON 後 GET/SET round-trip 變 N+1</h3>
<p><strong>徵兆</strong>：cutover 後 application latency p99 從 5ms 漲到 50ms；profiling 顯示「為了改 user.email、要先 GET user object → modify → SET」、原本 Redis <code>HSET</code> 1 個 round-trip 現在 2 個。</p>
<p><strong>根因</strong>：JSON-encoded value 不能 partial update、每次改一欄都要 read-modify-write。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application 端 cache JSON object in memory</strong>：read-modify-write 仍 1 個 SET、但 read 是 memory</li>
<li><strong>Compare-and-swap (CAS)</strong>：Memcached CAS 防止 concurrent update lost</li>
<li><strong>Field-level cache key</strong>：把 hash 拆成 N 個 Memcached key（<code>user:123:email</code> / <code>user:123:name</code>）、避開 JSON</li>
</ol>
<h3 id="case-2sorted-set-leaderboard-退化recomputation-cost-爆">Case 2：Sorted set leaderboard 退化、recomputation cost 爆</h3>
<p><strong>徵兆</strong>：原本 Redis leaderboard <code>ZADD</code> + <code>ZREVRANGE</code> &lt; 1ms；切 DB-backed leaderboard 後 <code>SELECT ... ORDER BY ... LIMIT 10</code> 在 1M+ row 跑 100-500ms。</p>
<p><strong>根因</strong>：Memcached 不支援 sorted set、leaderboard 必須在 DB 算、N 大時 sort 慢。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Cache pre-computed top N</strong>：DB scheduled job 每分鐘算 top 100、寫 Memcached、application 讀 cache 不直查 DB</li>
<li><strong>Materialized view + index</strong>：DB 端用 materialized view + index、毫秒級 query</li>
<li><strong>保留 Redis sorted set</strong>：leaderboard 是 Redis 強項、不該退到 Memcached、走混合架構</li>
</ol>
<h3 id="case-3pubsub-移除缺-fan-out-機制">Case 3：Pub/Sub 移除、缺 fan-out 機制</h3>
<p><strong>徵兆</strong>：原本 Redis Pub/Sub 跑 cache invalidation broadcast、N 個 application instance 都收 invalidation msg；切 Memcached 後失去 broadcast、cache stale。</p>
<p><strong>根因</strong>：Memcached 沒 Pub/Sub；application 需要外部 fan-out 機制。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>NATS / Redis Streams + consumer group</strong>：each application instance 是 consumer、收 invalidation</li>
<li><strong>Database trigger + LISTEN/NOTIFY</strong>：PostgreSQL <code>LISTEN/NOTIFY</code> 對中型 fan-out 足夠</li>
<li><strong>Architecture rethink</strong>：是否真需要 broadcast invalidation？通常用 <em>TTL-based cache</em> + <em>cache key versioning</em> 就能 cover 多數 invalidation use case</li>
</ol>
<h3 id="case-4atomic-incr-沒對等race-condition">Case 4：Atomic INCR 沒對等、race condition</h3>
<p><strong>徵兆</strong>：rate limiter / counter pattern 切 Memcached、<code>mc.incr(key)</code> 在 key 不存在時 return None（不 auto-init 為 0）；application 端 <code>if None: mc.set(key, 1)</code> race condition、低機率 counter reset。</p>
<p><strong>根因</strong>：Memcached INCR 對 missing key 不像 Redis 自動 init；application 端 init logic 容易 race。</p>
<p><strong>修法</strong>：</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="c1"># 用 ADD（atomic put-if-absent）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>  <span class="c1"># only sets if missing</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">incr</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>    <span class="c1"># always works after add</span></span></span></code></pre></div><p><code>ADD</code> + <code>INCR</code> 兩個 atomic operation 合起來 race-free。</p>
<h3 id="case-5eviction-policy-差異production-cache-hit-rate-降">Case 5：Eviction policy 差異、production cache hit rate 降</h3>
<p><strong>徵兆</strong>：cutover 後 cache hit rate 從 95% 降到 80%；profiling 發現「重要 key 沒在 cache」、新 key 一直擠走熱 key。</p>
<p><strong>根因</strong>：Redis 預設 <code>allkeys-lfu</code> (least frequently used)、長期熱 key 不被擠；Memcached 只有 LRU、單純按 access time、burst access 的 cold key 擠走 long-tail hot key。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Memory headroom</strong>：Memcached memory 限制拉高 30-50%、避免 eviction pressure</li>
<li><strong>Application-side cache priority</strong>：critical key 用 <em>no-expiration set</em> + 主動 refresh</li>
<li><strong>保留 Redis for LFU workload</strong>：long-tail hot key 場景 Redis LFU 更合適、不該退 Memcached</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis</th>
          <th>Memcached</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Memory efficiency</td>
          <td>baseline</td>
          <td>+10-20%（無 metadata overhead）</td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td>~100K ops/s single-thread</td>
          <td>~500K-1M ops/s multi-threaded</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>1-3ms</td>
          <td>0.5-1ms</td>
      </tr>
      <tr>
          <td>Persistence overhead</td>
          <td>5-15% CPU</td>
          <td>0</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8</td>
          <td>0.1-0.3</td>
      </tr>
      <tr>
          <td>Application complexity</td>
          <td>Low（feature 豐富）</td>
          <td>Higher（feature 移到 application）</td>
      </tr>
      <tr>
          <td>Cost per GB memory</td>
          <td>baseline</td>
          <td>略低（無 persistence I/O / replication overhead）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：純 cache use case 走 Memcached 省 ops + 略省 cost；application 已用 Redis-specific feature 不該切；混合架構是 long-term default。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-redis--dragonflydb-對比">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 對比</h3>
<p>兩條路：</p>
<ul>
<li>DragonflyDB：保留 Redis paradigm、優化 throughput + memory；application 不用改</li>
<li>Memcached：退到 pure cache paradigm、application 必須改、但 ops 簡化</li>
</ul>
<p>選擇取決於 <em>是否真的需要 Redis multi-paradigm features</em>：用得到就 DragonflyDB / Redis、用不到就 Memcached。</p>
<h3 id="跟-nats-整合">跟 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> 整合</h3>
<p>Redis Pub/Sub 移除後、應用端 fan-out / messaging 需求轉到 NATS / Redis Streams / Kafka；本文 cross-link migration playbook <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 有 paradigm shift 流程參考。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Memcached Cluster strategy</strong>：client-side consistent hashing vs server-side cluster mode、ops 簡化 vs scalability 取捨</li>
<li><strong>Long-term mixed architecture</strong>：80% Memcached + 20% Redis 是常見 stable state、不一定要完全消除 Redis</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source 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>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>平行 migration playbook (Type E)：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB</a></li>
<li>平行 Type B 對照：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>（保留 paradigm）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/</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。本文是 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> 「何時不該套」段的第 3 項實證（容量重新規劃 / re-sharding）— source / target 同 vendor 同 cluster、但 &lt;em>data topology 重劃&lt;/em>、不在 5 type 內。&lt;/p>&lt;/blockquote>
&lt;h2 id="source--target但-topology-重劃">Source = Target，但 topology 重劃&lt;/h2>
&lt;p>Migration 通常假設 &lt;em>source 跟 target 是不同 cluster / vendor&lt;/em>；re-sharding 是 &lt;em>同 cluster 內的 slot 重分配&lt;/em>、source 跟 target 是 &lt;em>同一個 Redis Cluster 的不同 state&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">Before re-shard:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> Cluster A: [node1: slots 0-5460] [node2: slots 5461-10921] [node3: slots 10922-16383]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> ~ 33% load ~ 50% load ~ 17% load (heavy imbalance)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">After re-shard:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> Cluster A: [node1: slots 0-4095] [node2: slots 4096-8191] [node3: slots 8192-12287] [node4: slots 12288-16383]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> ~ 25% load ~ 25% load ~ 25% load ~ 25% load&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>source 跟 target 是 &lt;em>同 cluster&lt;/em>、區別在 &lt;em>slot 對 node 的 mapping&lt;/em>。Application connection string 不變、cluster API 不變、data model 不變。但 &lt;em>slot migration 期間&lt;/em> application 行為跟 &lt;em>normal operation&lt;/em> 差很多 — 這是 re-sharding 主要工作。&lt;/p>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit&lt;/a> 對 Redis cluster re-sharding：&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。本文是 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> 「何時不該套」段的第 3 項實證（容量重新規劃 / re-sharding）— source / target 同 vendor 同 cluster、但 <em>data topology 重劃</em>、不在 5 type 內。</p></blockquote>
<h2 id="source--target但-topology-重劃">Source = Target，但 topology 重劃</h2>
<p>Migration 通常假設 <em>source 跟 target 是不同 cluster / vendor</em>；re-sharding 是 <em>同 cluster 內的 slot 重分配</em>、source 跟 target 是 <em>同一個 Redis Cluster 的不同 state</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">Before re-shard:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  Cluster A: [node1: slots 0-5460] [node2: slots 5461-10921] [node3: slots 10922-16383]
</span></span><span class="line"><span class="ln">3</span><span class="cl">              ~ 33% load           ~ 50% load              ~ 17% load (heavy imbalance)
</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">After re-shard:
</span></span><span class="line"><span class="ln">6</span><span class="cl">  Cluster A: [node1: slots 0-4095] [node2: slots 4096-8191] [node3: slots 8192-12287] [node4: slots 12288-16383]
</span></span><span class="line"><span class="ln">7</span><span class="cl">              ~ 25% load           ~ 25% load              ~ 25% load              ~ 25% load</span></span></code></pre></div><p>source 跟 target 是 <em>同 cluster</em>、區別在 <em>slot 對 node 的 mapping</em>。Application connection string 不變、cluster API 不變、data model 不變。但 <em>slot migration 期間</em> application 行為跟 <em>normal operation</em> 差很多 — 這是 re-sharding 主要工作。</p>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a> 對 Redis cluster re-sharding：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Redis、無變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 Redis Cluster、operational 不變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>同 Redis Cluster、無 paradigm 差</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>同 1 個（cluster）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>多數不改、client cluster mode 自處理</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>重劃</strong> — slot mapping 跟 node 數</td>
          <td><strong>New axis</strong></td>
      </tr>
  </tbody>
</table>
<p>5 維皆 Low、對映 Type B drop-in；但 <em>data topology</em> 是 5 type 沒有的 <em>第 6 維度</em>。本文採用 <em>re-sharding-specific 結構</em>、不是 5 type 任一個。</p>
<h2 id="4-種-re-sharding-driver">4 種 re-sharding driver</h2>
<p>不同 driver 對應不同 re-sharding 策略：</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
          <th>對應 re-sharding 操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slot imbalance</td>
          <td>業務熱點打到部分 slot、單 node CPU / memory 80%+</td>
          <td>Rebalance（slot 重分配、不加 node）</td>
      </tr>
      <tr>
          <td>Capacity expansion</td>
          <td>整 cluster memory / throughput 上限快到、要加 node</td>
          <td>Add node + slot migration（從現有 node 搬部分 slot 過去）</td>
      </tr>
      <tr>
          <td>Node decommission</td>
          <td>老 node 硬體淘汰 / cloud instance 換代</td>
          <td>Drain（該 node 的 slot 全搬走）+ remove</td>
      </tr>
      <tr>
          <td>Hash tag refactor</td>
          <td>業務 access pattern 變、需要 co-located key 群重分組</td>
          <td>Application-side migration（不是 cluster-level）</td>
      </tr>
  </tbody>
</table>
<p>前 3 種是 cluster-internal、用 <code>redis-cli --cluster</code> 工具完成；第 4 種需要 application 端 dual-write + migration、本文不展開。</p>
<h2 id="slot-migration-機制">Slot migration 機制</h2>
<p>Redis Cluster 16384 個 slot、每個 key 經 <code>CRC16(key) % 16384</code> 對應 slot。Slot migration 過程：</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">Source node:     [slot N: MIGRATING to dest]
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Dest node:       [slot N: IMPORTING from source]
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">                 ↓
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Source node:     SCAN slot N → for each key:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                 1. DUMP key (serialize value)
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                 2. send to dest via MIGRATE command
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                 3. dest RESTORE key
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                 4. source DEL key
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                 ↓
</span></span><span class="line"><span class="ln">10</span><span class="cl">Source node:     [slot N: OWNED by dest]
</span></span><span class="line"><span class="ln">11</span><span class="cl">Dest node:       [slot N: OWNED]
</span></span><span class="line"><span class="ln">12</span><span class="cl">                 ↓
</span></span><span class="line"><span class="ln">13</span><span class="cl">跨 cluster broadcast: slot N 屬於 dest</span></span></code></pre></div><p>期間 client 行為：</p>
<ul>
<li>Key 在 source 端（未 migrate）：source 直接 serve</li>
<li>Key 在 dest 端（已 migrate）：source 回 <code>-ASK</code> redirect、client 重發到 dest</li>
<li>寫入 MIGRATING slot 的新 key：source serve、之後也會 migrate</li>
<li>Application 不需要改 code、cluster-aware client 自動處理 <code>-ASK</code> redirect</li>
</ul>
<h2 id="redis-cli-cluster-工具">redis-cli &ndash;cluster 工具</h2>
<p>production 用 official tool、不要手寫 slot migration：</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. Rebalance（slot 重分配、適合 imbalance）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">redis-cli --cluster rebalance 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --cluster-use-empty-masters <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --cluster-threshold <span class="m">5</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"># 2. Reshard（指定來源 → 目標、適合 capacity expansion）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli --cluster reshard 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --cluster-from &lt;source-node-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --cluster-to &lt;dest-node-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --cluster-slots <span class="m">4096</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --cluster-yes
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 3. Add-node（加新 node 進 cluster）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">redis-cli --cluster add-node 10.0.0.4:6379 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --cluster-master-id &lt;existing-master-id&gt;
</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"># 4. Del-node（移除 node、需先 drain slot）</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">redis-cli --cluster del-node 10.0.0.1:6379 &lt;node-to-remove&gt;</span></span></code></pre></div><p>關鍵：</p>
<ul>
<li><code>--cluster-threshold 5</code>：load 差異超過 5% 才 rebalance、避免反覆觸發</li>
<li><code>--cluster-slots</code>：一次 migrate 多少 slot；太大 lock 久、太小步驟多</li>
<li>Rebalance / reshard 過程 cluster 仍 serve traffic、但 <em>latency 升高</em>（migration overhead）</li>
</ul>
<h2 id="5-段執行流程">5 段執行流程</h2>





<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">1. Pre-resharding analysis
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   - 當前 slot 分佈跟 load
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - Hot key 識別（CLUSTER COUNTKEYSINSLOT）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - 預估 migration 時間
</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">2. Backup checkpoint
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - BGSAVE on all master
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   - 確認 replica 跟得上（replication offset diff &lt; 10MB）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">3. Execute re-sharding
</span></span><span class="line"><span class="ln">11</span><span class="cl">   - 用 redis-cli --cluster 工具
</span></span><span class="line"><span class="ln">12</span><span class="cl">   - Monitor cluster health（CLUSTER INFO + CLUSTER NODES）
</span></span><span class="line"><span class="ln">13</span><span class="cl">   - Migration 期間 application 端 latency baseline 比對
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">4. Verify
</span></span><span class="line"><span class="ln">16</span><span class="cl">   - Slot distribution 對 expected mapping
</span></span><span class="line"><span class="ln">17</span><span class="cl">   - Application traffic pattern 對 baseline
</span></span><span class="line"><span class="ln">18</span><span class="cl">   - 跑 cross-node sanity check
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">5. Cleanup
</span></span><span class="line"><span class="ln">21</span><span class="cl">   - 舊 node（若 decommission）reset / 釋放
</span></span><span class="line"><span class="ln">22</span><span class="cl">   - Monitoring dashboard 更新 (Prometheus target / Grafana panel)
</span></span><span class="line"><span class="ln">23</span><span class="cl">   - Document new topology</span></span></code></pre></div><p>整體 1-7 天、依 cluster 大小（10GB ~ 1 小時、TB 級 1-3 天）。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1cluster-busy-期間-application-timeout">Case 1：Cluster busy 期間 application timeout</h3>
<p><strong>徵兆</strong>：re-sharding 跑到一半、application 端開始大量 <code>CLUSTER BUSY</code> error / <code>OOM</code> warning / latency p99 從 5ms 跳到 200-2000ms；某些 batch operation 完全失敗。</p>
<p><strong>根因</strong>：MIGRATE command 對單 key 是 <em>blocking</em>（DUMP + send + RESTORE + DEL atomic）— 大 value（HASH / SORTED SET / LIST 含 100K+ entry）migration 可能 lock node 數秒；同期間其他 query 阻塞。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding audit</strong>：<code>MEMORY USAGE</code> 跑 sample key、找 &gt; 1MB 的 <em>fat key</em>、列出單獨處理</li>
<li><strong>MIGRATE timeout 調</strong>：<code>redis.conf</code> 設 <code>cluster-migration-timeout 10000</code>（10s）、避免單 key migration 卡爆 cluster</li>
<li><strong>降低並行</strong>：<code>--cluster-pipeline 1</code> 一次只搬一個 slot（預設 10）、減少 CPU 壓力</li>
<li><strong>Fat key refactor</strong>：production 不該有 1M+ entry 的 collection、refactor 拆分</li>
</ol>
<h3 id="case-2replica-lag-during-re-sharding">Case 2：Replica lag during re-sharding</h3>
<p><strong>徵兆</strong>：reshard 完成後、replica 顯示 stale data 數分鐘、application 端 read from replica 拿到舊值。</p>
<p><strong>根因</strong>：master 端 slot migration 產生大量 <code>DEL</code> + <code>RESTORE</code> 命令、replication stream 量爆、replica 跟不上、accumulated lag。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding 確認 replica lag &lt; 5MB</strong>、否則先 fix replica issue 再開始</li>
<li><strong>Throttle migration</strong>：用 <code>--cluster-replace</code> + lower pipeline、放慢 master 寫入速度</li>
<li><strong>Application 端 read-write split policy</strong>：reshard 期間強制 read from master、暫時放棄 replica read</li>
<li><strong>預備計畫</strong>：若 lag &gt; 30s 撐了 5+ 分鐘、考慮暫停 reshard、wait replica catch up</li>
</ol>
<h3 id="case-3client-side-topology-cache-stale">Case 3：Client-side topology cache stale</h3>
<p><strong>徵兆</strong>：reshard 完、application 端持續報 <code>MOVED &lt;slot&gt; &lt;new-node&gt;</code> redirect、但隔 30s 又 redirect 一次；某些 client 直接 connection refused（連到已 decommission node）。</p>
<p><strong>根因</strong>：cluster-aware client（lettuce / Jedis cluster mode）有 <em>topology cache</em>、reshard 後不主動 refresh；遇 MOVED 後 refresh 一次、但 cache TTL 內可能繼續用舊 mapping。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Client config</strong>：lettuce <code>clusterTopologyRefreshOptions(...)</code> 設較短 refresh interval（60s）+ <code>enablePeriodicRefresh()</code></li>
<li><strong>Reshard 完後 trigger refresh</strong>：application 端可主動發 <code>CLUSTER NODES</code> 拿最新 topology、不依賴 client lib 自動 refresh</li>
<li><strong>Graceful client shutdown / restart</strong>：對 latency-sensitive 服務、reshard 完 rolling restart application pod、避免 stale cache</li>
<li><strong>Decommissioned node 保留 5 分鐘</strong>：不立刻 stop node、給 stale client 自然 retry 機會</li>
</ol>
<h3 id="case-4cross-slot-transaction-失敗">Case 4：Cross-slot transaction 失敗</h3>
<p><strong>徵兆</strong>：application 用 <code>MULTI/EXEC</code> 跨多 key、reshard 期間部分 transaction 報 <code>MOVED</code> error、整個 transaction 失敗、business logic 不一致。</p>
<p><strong>根因</strong>：Redis Cluster transaction 要求 <em>所有 key 在同 slot</em>（用 hash tag <code>{user:123}</code>）；reshard 期間如果 transaction 內某 key migrate 到 dest、cluster topology 暫時 inconsistent、transaction 拒絕。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding audit</strong>：grep application code 找 MULTI / pipeline 使用、確認所有都用 hash tag co-locate</li>
<li><strong>Reshard 期間 application 端加 retry</strong>：transaction failure 後 backoff retry、cluster stabilize 後成功</li>
<li><strong>架構</strong>：transaction-heavy 場景考慮不用 Redis Cluster、用 Redis Sentinel single master（無 slot 概念）</li>
</ol>
<h3 id="case-5monitor-visibility-gap-during-reshard">Case 5：Monitor visibility gap during reshard</h3>
<p><strong>徵兆</strong>：reshard 期間 Prometheus dashboard 對某 node 的 metric 突然顯示 <em>錯位</em> — load = 95% 但 slot count 顯示 6% slot；SOC 不知道 node 健康狀況。</p>
<p><strong>根因</strong>：Prometheus exporter 對 <em>slot count</em> 跟 <em>traffic load</em> 分開計算；reshard 期間 slot count 已 migrate 但流量仍打 source node（client cache stale）— metric 看似矛盾。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Reshard 期間關 alert</strong>：knownmaintenance window、Prometheus silence alert</li>
<li><strong>加 reshard-aware metric</strong>：用 <code>redis_cluster_migration_slots</code> 量化 in-flight migration</li>
<li><strong>Dashboard 加註解</strong>：reshard 期間 SOC 看 dashboard 知道是 <em>normal anomaly</em></li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slot migration 速度</td>
          <td>1-10K key / sec（依 key size + network）</td>
          <td>TB 級 10K key / sec → 1 天</td>
      </tr>
      <tr>
          <td>Application latency impact</td>
          <td>p99 +50-200% during migration</td>
          <td>設 latency budget、超出暫停</td>
      </tr>
      <tr>
          <td>Memory / node</td>
          <td>不變、但 temporary 雙寫期間 +5-15%</td>
          <td>不能在 memory 90%+ 時 reshard</td>
      </tr>
      <tr>
          <td>Network bandwidth</td>
          <td>跨 node 大流量、~100-500 Mbps per migration stream</td>
          <td>跨 AZ reshard egress cost 注意</td>
      </tr>
      <tr>
          <td>Recovery time</td>
          <td>Reshard 失敗回退 = 反向 reshard（時間相同）</td>
          <td>不能在 incident 期間 reshard</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>跑在 <em>低流量時段</em>（夜間 / 週末）</li>
<li>Throughput 容忍度 &lt; 50% 再 reshard、不要 80%+ 時操作</li>
<li>預留 <em>回退 window</em> — reshard 卡住時能 abort + 恢復原狀</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-redis--dragonflydb-migration-對位">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB migration</a> 對位</h3>
<p>DragonflyDB 設計上 <em>單機效能取代 cluster</em>、re-sharding 議題消失；如果 cluster re-sharding 頻繁觸發、評估直接遷 DragonflyDB 是否更便宜。</p>
<h3 id="跟-sentinel-ha-對比">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Sentinel HA</a> 對比</h3>
<p>Sentinel 模式無 slot 概念、re-sharding 不適用；但 <em>manual sharding by application</em> 場景仍可能需要類似 topology re-layout、application 端要自己處理。</p>
<h3 id="跟-redis-7-function--cluster-v2">跟 Redis 7+ Function / Cluster v2</h3>
<p>Redis 7 推 Cluster v2 跟 Functions、slot migration 機制部分升級；keyspace migration 仍是核心議題、但 API 跟 monitoring 改進。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Auto-rebalance via operator</strong>：Redis Enterprise / Aiven 等 managed Redis 提供自動 rebalance、不需手動觸發</li>
<li><strong>Cross-DC slot migration</strong>：跨 region cluster slot migration 對 latency / cost 影響大、通常用 <em>application-level sharding</em> 取代 cluster-level</li>
<li><strong>Hash tag 治理</strong>：application code grep / lint 強制 hash tag、避免 cross-slot transaction 反模式</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>平行 migration playbook：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>對位 deep article：<a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">PostgreSQL major version upgrade</a>（另一個 5 type 漏類驗證）</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> / <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（本文驗證 <em>容量重劃漏類</em>）</li>
</ul>
]]></content:encoded></item><item><title>Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/</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。選型層（Redis vs Valkey vs Memcached）見 overview；本文只處理「已經選了 Redis、記憶體怎麼配才不會在尖峰爆掉」。配置以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">Redis 官方 memory optimization 文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="你的-redis-會在凌晨三點-oom">你的 Redis 會在凌晨三點 OOM&lt;/h2>
&lt;p>Redis 的記憶體問題很少在有人盯著儀表板時發生。它發生在流量爬升、某個 key 集合悄悄長大、AOF rewrite 剛好撞上 RDB save 的那個瞬間——通常是凌晨三點，沒人盯著。徵兆是 application 端突然一片 &lt;code>OOM command not allowed when used memory &amp;gt; 'maxmemory'&lt;/code>，所有寫入失敗，但讀取還活著，於是監控的「Redis 還在回應」綠燈騙過了 on-call。&lt;/p>
&lt;p>這類事故的根因幾乎都不是「Redis 不夠快」，而是三個記憶體旋鈕在設計時被當成預設值放著沒動：&lt;code>maxmemory&lt;/code> 設多少、&lt;code>maxmemory-policy&lt;/code> 選哪個、以及沒人注意到的記憶體碎片化。這三個旋鈕決定了 Redis 在記憶體壓力下是「優雅地淘汰冷資料繼續服務」還是「拒絕所有寫入直到有人重啟」。本文處理這三者的會計模型、選型判讀，以及它們怎麼被寫成事故。&lt;/p>
&lt;p>對延遲就是業務 KPI 的服務，這個旋鈕的代價更直接。&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>每次滑動要查多個快取（profile、距離、偏好、推薦池），4700 萬月活下 cache 不是 DB 的補救、是主要服務面，cache miss 是邊緣案例。eviction policy 選錯，淘汰掉的若是熱資料，下一次滑動就打回 origin，sub-millisecond 的延遲預算瞬間破表。&lt;/p>
&lt;h2 id="核心概念redis-記憶體的會計模型">核心概念：Redis 記憶體的會計模型&lt;/h2>
&lt;p>要調校記憶體，先要分清楚 &lt;code>used_memory&lt;/code> 這個數字到底由什麼組成。&lt;code>INFO memory&lt;/code> 回報的是幾層疊加的記憶體會計，每一層去處不同：&lt;/p>
&lt;p>&lt;strong>&lt;code>used_memory&lt;/code>&lt;/strong> 是 Redis allocator（預設 jemalloc）配給資料、結構與 buffer 的總量。&lt;strong>&lt;code>used_memory_rss&lt;/code>&lt;/strong> 是作業系統視角看到的 Redis 進程實體記憶體（resident set size），它通常大於 &lt;code>used_memory&lt;/code>——兩者的比值就是 &lt;code>mem_fragmentation_ratio&lt;/code>。&lt;strong>&lt;code>used_memory_dataset&lt;/code>&lt;/strong> 才是純資料的部分，扣掉了 Redis 自身的 overhead。&lt;/p>
&lt;p>理解三個跟 OOM 直接相關的記憶體去處：&lt;/p>
&lt;p>&lt;strong>資料本身的編碼會放大或縮小記憶體&lt;/strong>。一個小 hash（field 數少於 &lt;code>hash-max-listpack-entries&lt;/code>、value 短於 &lt;code>hash-max-listpack-value&lt;/code>）用 listpack 緊湊編碼，記憶體可能只有大 hash 用 hashtable 編碼的幾分之一。同樣的邏輯套用在 list、set、sorted set。一個欄位設計的小決定（把 user object 拆成 200 個獨立 key 還是壓成一個 hash）會讓記憶體差好幾倍。&lt;/p>
&lt;p>&lt;strong>client output buffer 不計入 dataset 但會吃光記憶體&lt;/strong>。慢速 consumer、&lt;code>MONITOR&lt;/code>、大量 pub/sub 訂閱者都會讓 Redis 在 server 端堆積 reply buffer。&lt;code>client-output-buffer-limit&lt;/code> 沒設好，一個讀很慢的 replica 或一個掛著的 &lt;code>MONITOR&lt;/code> 連線就能把記憶體推到 maxmemory。&lt;/p>
&lt;p>&lt;strong>fork 期間記憶體會短暫翻倍&lt;/strong>。RDB save 與 AOF rewrite 都靠 &lt;code>fork()&lt;/code> + copy-on-write，父進程在 fork 後若持續寫入，被改動的 page 會被複製，最壞情況記憶體接近翻倍。這是 maxmemory 必須留 headroom 的核心原因，細節見 &lt;a href="https://tarrragon.github.io/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 deep article&lt;/a>。&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。選型層（Redis vs Valkey vs Memcached）見 overview；本文只處理「已經選了 Redis、記憶體怎麼配才不會在尖峰爆掉」。配置以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">Redis 官方 memory optimization 文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="你的-redis-會在凌晨三點-oom">你的 Redis 會在凌晨三點 OOM</h2>
<p>Redis 的記憶體問題很少在有人盯著儀表板時發生。它發生在流量爬升、某個 key 集合悄悄長大、AOF rewrite 剛好撞上 RDB save 的那個瞬間——通常是凌晨三點，沒人盯著。徵兆是 application 端突然一片 <code>OOM command not allowed when used memory &gt; 'maxmemory'</code>，所有寫入失敗，但讀取還活著，於是監控的「Redis 還在回應」綠燈騙過了 on-call。</p>
<p>這類事故的根因幾乎都不是「Redis 不夠快」，而是三個記憶體旋鈕在設計時被當成預設值放著沒動：<code>maxmemory</code> 設多少、<code>maxmemory-policy</code> 選哪個、以及沒人注意到的記憶體碎片化。這三個旋鈕決定了 Redis 在記憶體壓力下是「優雅地淘汰冷資料繼續服務」還是「拒絕所有寫入直到有人重啟」。本文處理這三者的會計模型、選型判讀，以及它們怎麼被寫成事故。</p>
<p>對延遲就是業務 KPI 的服務，這個旋鈕的代價更直接。<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>每次滑動要查多個快取（profile、距離、偏好、推薦池），4700 萬月活下 cache 不是 DB 的補救、是主要服務面，cache miss 是邊緣案例。eviction policy 選錯，淘汰掉的若是熱資料，下一次滑動就打回 origin，sub-millisecond 的延遲預算瞬間破表。</p>
<h2 id="核心概念redis-記憶體的會計模型">核心概念：Redis 記憶體的會計模型</h2>
<p>要調校記憶體，先要分清楚 <code>used_memory</code> 這個數字到底由什麼組成。<code>INFO memory</code> 回報的是幾層疊加的記憶體會計，每一層去處不同：</p>
<p><strong><code>used_memory</code></strong> 是 Redis allocator（預設 jemalloc）配給資料、結構與 buffer 的總量。<strong><code>used_memory_rss</code></strong> 是作業系統視角看到的 Redis 進程實體記憶體（resident set size），它通常大於 <code>used_memory</code>——兩者的比值就是 <code>mem_fragmentation_ratio</code>。<strong><code>used_memory_dataset</code></strong> 才是純資料的部分，扣掉了 Redis 自身的 overhead。</p>
<p>理解三個跟 OOM 直接相關的記憶體去處：</p>
<p><strong>資料本身的編碼會放大或縮小記憶體</strong>。一個小 hash（field 數少於 <code>hash-max-listpack-entries</code>、value 短於 <code>hash-max-listpack-value</code>）用 listpack 緊湊編碼，記憶體可能只有大 hash 用 hashtable 編碼的幾分之一。同樣的邏輯套用在 list、set、sorted set。一個欄位設計的小決定（把 user object 拆成 200 個獨立 key 還是壓成一個 hash）會讓記憶體差好幾倍。</p>
<p><strong>client output buffer 不計入 dataset 但會吃光記憶體</strong>。慢速 consumer、<code>MONITOR</code>、大量 pub/sub 訂閱者都會讓 Redis 在 server 端堆積 reply buffer。<code>client-output-buffer-limit</code> 沒設好，一個讀很慢的 replica 或一個掛著的 <code>MONITOR</code> 連線就能把記憶體推到 maxmemory。</p>
<p><strong>fork 期間記憶體會短暫翻倍</strong>。RDB save 與 AOF rewrite 都靠 <code>fork()</code> + copy-on-write，父進程在 fork 後若持續寫入，被改動的 page 會被複製，最壞情況記憶體接近翻倍。這是 maxmemory 必須留 headroom 的核心原因，細節見 <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 deep article</a>。</p>
<p><code>maxmemory</code> 框住的是 <code>used_memory</code>，不是 <code>used_memory_rss</code>。所以 maxmemory 設成機器 RAM 的 100% 是錯的——碎片化、fork copy-on-write、client buffer 都在 maxmemory 之外，會把 RSS 推爆系統，觸發 Linux OOM killer 直接砍掉 Redis 進程（比 Redis 自己的 noeviction 更糟，因為是無預警 SIGKILL）。</p>
<h2 id="配置maxmemory-與-policy-的設定路徑">配置：maxmemory 與 policy 的設定路徑</h2>
<p>設定分兩步：先框住記憶體上限，再決定撞到上限時的行為。</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. 設定記憶體上限（留 headroom 給 fork / fragmentation / client buffer）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 機器 RAM 8GB → maxmemory 設 ~5-6GB、留 25-35% headroom</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">redis-cli CONFIG SET maxmemory 6gb
</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"># 2. 設定撞到上限時的淘汰行為</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">redis-cli CONFIG SET maxmemory-policy allkeys-lfu
</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"># 3. 永久化到 redis.conf（CONFIG SET 重啟後失效）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># redis.conf:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">#   maxmemory 6gb</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">#   maxmemory-policy allkeys-lfu</span></span></span></code></pre></div><p>八個 <code>maxmemory-policy</code> 選項分三類，選型靠「資料是不是全部都能淘汰」與「淘汰要靠存取頻率還是 TTL」兩個問題：</p>
<table>
  <thead>
      <tr>
          <th>policy</th>
          <th>淘汰範圍</th>
          <th>淘汰依據</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>noeviction</code></td>
          <td>不淘汰</td>
          <td>寫入直接報錯</td>
          <td>資料是 source-of-truth、不能丟（少見）</td>
      </tr>
      <tr>
          <td><code>allkeys-lru</code></td>
          <td>所有 key</td>
          <td>最近最少使用</td>
          <td>純 cache、無法預判哪些該留</td>
      </tr>
      <tr>
          <td><code>allkeys-lfu</code></td>
          <td>所有 key</td>
          <td>最少使用頻率</td>
          <td>純 cache、有明顯熱資料（多數 cache 場景）</td>
      </tr>
      <tr>
          <td><code>allkeys-random</code></td>
          <td>所有 key</td>
          <td>隨機</td>
          <td>key 存取均勻、省 LRU/LFU 計算</td>
      </tr>
      <tr>
          <td><code>volatile-lru</code></td>
          <td>有 TTL 的 key</td>
          <td>最近最少使用</td>
          <td>cache 與持久資料混存、只淘汰可過期的</td>
      </tr>
      <tr>
          <td><code>volatile-lfu</code></td>
          <td>有 TTL 的 key</td>
          <td>最少使用頻率</td>
          <td>同上、有熱資料</td>
      </tr>
      <tr>
          <td><code>volatile-random</code></td>
          <td>有 TTL 的 key</td>
          <td>隨機</td>
          <td>同上、省計算</td>
      </tr>
      <tr>
          <td><code>volatile-ttl</code></td>
          <td>有 TTL 的 key</td>
          <td>最接近過期的先淘汰</td>
          <td>想讓近期過期的提早讓位</td>
      </tr>
  </tbody>
</table>
<h3 id="lru-跟-lfu-的真實差異">LRU 跟 LFU 的真實差異</h3>
<p><code>allkeys-lru</code> 跟 <code>allkeys-lfu</code> 看起來像同一件事的兩種寫法，但選錯會在特定 workload 下讓 hit rate 掉一截。LRU 看「最後一次被存取是多久以前」，LFU 看「被存取的頻率」。差別在一次性掃描（scan pollution）：某個批次任務一次讀過大量冷 key，LRU 會把這些剛被碰過的冷 key 排到淘汰隊伍最後面，反而把真正的熱 key 擠出去。LFU 因為看頻率，一次性的存取不會讓冷 key 假裝成熱 key。</p>
<p>Redis 4.0 後的 LFU 用的是 probabilistic counter（Morris counter）加 decay，不是精確計數，靠兩個參數調：</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"># lfu-log-factor：counter 增長的對數速度、越大越能區分高頻 key</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET lfu-log-factor <span class="m">10</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># lfu-decay-time：counter 衰減的分鐘數、越小越快遺忘舊熱度</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">redis-cli CONFIG SET lfu-decay-time <span class="m">1</span></span></span></code></pre></div><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>（熱門 profile、熱區域推薦池）的服務，<code>allkeys-lfu</code> 比 <code>allkeys-lru</code> 更能保護熱 key 不被批次掃描或冷流量擠出。</p>
<h3 id="approximate-eviction-的取樣">approximate eviction 的取樣</h3>
<p>Redis 的 LRU/LFU 都是近似演算法，不掃全 keyspace，而是每次取樣 <code>maxmemory-samples</code> 個 key（預設 5）挑最該淘汰的。樣本數越大越接近精確 LRU/LFU，但越吃 CPU。記憶體壓力大、淘汰頻繁時，預設 5 已夠；要更精準可調到 10，代價是淘汰路徑的 CPU 上升。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1noeviction-讓寫入全滅讀取假裝健康">Case 1：noeviction 讓寫入全滅、讀取假裝健康</h3>
<p><strong>徵兆</strong>：application 寫入路徑大量 <code>OOM command not allowed when used memory &gt; 'maxmemory'</code>，但 <code>GET</code> 仍正常、health check（通常打 <code>PING</code> 或 <code>GET</code>）綠燈，on-call 收到的是 application 層的 500、不是 Redis 告警。</p>
<p><strong>根因</strong>：<code>maxmemory-policy</code> 預設是 <code>noeviction</code>。當 Redis 把 cache 當 cache 用，但 policy 留在 <code>noeviction</code>，記憶體一滿，所有會增加記憶體的命令（<code>SET</code>、<code>LPUSH</code>、<code>HSET</code>）直接報錯，唯讀命令照常。health check 若只測讀取，完全偵測不到。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>純 cache 場景把 policy 改成 <code>allkeys-lru</code> 或 <code>allkeys-lfu</code>，讓記憶體壓力自動透過淘汰釋放</li>
<li>health check 加一個寫入探針（<code>SET healthcheck:probe &lt;ts&gt; EX 10</code>），讓 OOM 寫入失敗能被偵測</li>
<li>告警掛在 <code>used_memory / maxmemory &gt; 0.85</code>，不要等 OOM 才反應</li>
<li>若資料真的不能淘汰（誤把 Redis 當 source-of-truth），那不該用 cache 配置，見本文 Capacity / cost 邊界段的路由判斷</li>
</ol>
<h3 id="case-2碎片化吃掉-30-記憶體">Case 2：碎片化吃掉 30% 記憶體</h3>
<p><strong>徵兆</strong>：<code>used_memory</code> 顯示 4GB、但 <code>used_memory_rss</code> 是 5.5GB，<code>mem_fragmentation_ratio</code> 是 1.37，機器 RAM 開始吃緊但資料量沒漲。重啟 Redis 後 RSS 掉回 4GB 出頭。</p>
<p><strong>根因</strong>：大量寫入後刪除、或 value 大小頻繁變動（例如 list 一直 push/pop），jemalloc 的記憶體頁出現空洞——配出去的 page 還佔著 RSS，但裡面只有零星資料。<code>mem_fragmentation_ratio</code> 持續 &gt; 1.5 是明確訊號。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>開 active defrag 讓 Redis 在背景整理（4.0+）：</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-cli CONFIG SET activedefrag yes
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET active-defrag-ignore-bytes 100mb
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET active-defrag-threshold-lower <span class="m">10</span></span></span></code></pre></div></li>
<li>
<p>fragmentation_ratio &lt; 1.0 是另一種警訊——代表 Redis 在用 swap，比碎片化更危險，要立刻降記憶體壓力</p>
</li>
<li>
<p>結構選擇上避免大幅波動的 collection；穩態大小的資料碎片化天然較低</p>
</li>
<li>
<p>計算 maxmemory headroom 時把 1.2-1.4 的 fragmentation 算進去</p>
</li>
</ol>
<h3 id="case-3一個-monitor-連線把記憶體推爆">Case 3：一個 MONITOR 連線把記憶體推爆</h3>
<p><strong>徵兆</strong>：某次 debug 後記憶體莫名持續上升，<code>used_memory_dataset</code> 沒變但 <code>used_memory</code> 一直漲，<code>CLIENT LIST</code> 看到一個連線的 <code>omem</code>（output buffer memory）有幾百 MB。</p>
<p><strong>根因</strong>：有人開了 <code>MONITOR</code> 去看即時命令流、然後忘了關（或 client crash 但連線沒斷）。<code>MONITOR</code> 把每一條命令都推給該連線，高 QPS 下 server 端 output buffer 爆量堆積，計入 <code>used_memory</code> 但不在 dataset。慢速 replica 或大量 pub/sub 訂閱者也會觸發同類問題。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>設定 client output buffer 上限，超過就斷線：</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"># normal client / replica / pubsub 分開設</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET client-output-buffer-limit <span class="s2">&#34;normal 256mb 64mb 60&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET client-output-buffer-limit <span class="s2">&#34;pubsub 32mb 8mb 60&#34;</span></span></span></code></pre></div></li>
<li>
<p><code>MONITOR</code> 在 production 嚴格禁用或限時，它本身也拖慢整個 server</p>
</li>
<li>
<p>監控加 <code>CLIENT LIST</code> 的 <code>omem</code> 巡檢，找出異常 buffer 的連線</p>
</li>
<li>
<p>replica lag 過大時 output buffer 會堆，對應 <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 走的邊界">replication / failover deep article</a></p>
</li>
</ol>
<h3 id="case-4欄位設計讓記憶體多用三倍">Case 4：欄位設計讓記憶體多用三倍</h3>
<p><strong>徵兆</strong>：資料筆數跟預估一致，但 <code>used_memory</code> 是試算的 3 倍。<code>MEMORY USAGE &lt;key&gt;</code> 抽樣發現單筆 object 的記憶體遠超 value 本身的 byte 數。</p>
<p><strong>根因</strong>：把一個有 10 個欄位的 user object 拆成 10 個獨立 string key（<code>user:123:name</code>、<code>user:123:age</code>&hellip;），每個 key 都帶 Redis 的 key overhead（dict entry、expire dict entry、key 字串本身）。10 個 key 的 overhead 是一個 hash 的好幾倍。反過來，超過 <code>hash-max-listpack-entries</code> 的大 hash 從緊湊的 listpack 退化成 hashtable 編碼，也會放大記憶體。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>同一 entity 的欄位用一個 hash 存，共享 key overhead</p>
</li>
<li>
<p>保持 hash 在 listpack 閾值內以用緊湊編碼：</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-cli CONFIG GET hash-max-listpack-entries  <span class="c1"># 預設 128</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG GET hash-max-listpack-value    <span class="c1"># 預設 64</span></span></span></code></pre></div></li>
<li>
<p>用 <code>MEMORY USAGE &lt;key&gt;</code> 跟 <code>redis-cli --bigkeys</code> 抽樣驗證實際記憶體，不靠試算</p>
</li>
<li>
<p><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">Shopify 的 serialization 遷移</a>（Marshal → MessagePack）正是用更省的編碼壓 payload，欄位編碼決策對記憶體與頻寬同時有效</p>
</li>
</ol>
<h3 id="case-5淘汰把熱-key-一起帶走hit-rate-崩">Case 5：淘汰把熱 key 一起帶走、hit rate 崩</h3>
<p><strong>徵兆</strong>：記憶體壓力下開始 eviction（<code>evicted_keys</code> 持續上升），同時 <code>keyspace_hits / (hits + misses)</code> 從 95% 掉到 70%，origin QPS 跟著飆，下游 DB 開始吃緊。</p>
<p><strong>根因</strong>：用了 <code>allkeys-random</code>，或 <code>allkeys-lru</code> 撞上批次掃描污染，淘汰演算法把熱 key 跟冷 key 一視同仁，熱 key 被淘汰後下一個請求 miss、回源、再寫回，形成淘汰與回填的拉鋸，hit rate 持續惡化。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>有明顯熱資料就用 <code>allkeys-lfu</code>，讓頻率高的 key 留下</li>
<li>把 maxmemory-samples 調到 10 提高淘汰精準度</li>
<li>根因常是記憶體真的不夠——<code>evicted_keys</code> 持續高代表 working set 超過 maxmemory，該擴容或分片，不是純調 policy 能解</li>
<li>熱 key 本身過熱（單 key QPS 遠超其他）要走 local cache + Redis 兩層，對應 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>記憶體配置的容量判讀，核心是「working set 對 maxmemory 的比值」與「淘汰是否健康」：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>used_memory / maxmemory</code></td>
          <td>&lt; 80%</td>
          <td>&gt; 85% 告警、&gt; 95% 接近 OOM 或大量淘汰</td>
      </tr>
      <tr>
          <td><code>mem_fragmentation_ratio</code></td>
          <td>1.0 - 1.5</td>
          <td>&gt; 1.5 開 active defrag、&lt; 1.0 在用 swap 要救火</td>
      </tr>
      <tr>
          <td><code>evicted_keys</code> 速率</td>
          <td>接近 0（working set 放得下）</td>
          <td>持續高 → working set 超量、該擴容 / 分片</td>
      </tr>
      <tr>
          <td>hit rate</td>
          <td>&gt; 90%（多數 cache）</td>
          <td>持續下滑 → 淘汰太兇或 TTL 太短</td>
      </tr>
      <tr>
          <td>fork 期間 RSS 峰值</td>
          <td>&lt; 機器 RAM</td>
          <td>接近 RAM → maxmemory headroom 不足、降 maxmemory</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單機記憶體不夠、working set 持續超量</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）">Redis Cluster 分片</a>，把 keyspace 切到多 node。</li>
<li><strong>想用 Redis API 但要極致單機記憶體效率</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 dashtable 在同 dataset 下通常比 Redis 省 20-40% 記憶體（依資料形狀、以官方 benchmark 為準），且單機多核能撐到 Redis 要靠 cluster 才能達到的規模——若 cluster re-sharding 頻繁觸發，評估直接遷 DragonflyDB 是否更省維運。</li>
<li><strong>資料其實不能淘汰（被當 source-of-truth）</strong>：那它不是 cache，該走 durable store。AWS 生態下用 <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>（Redis-compatible durable），或把正式狀態放回 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a>。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>記憶體與淘汰是 Redis 運維的第一層旋鈕，但它跟其他子系統耦合：</p>
<ul>
<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>：fork 期間的 copy-on-write 是 maxmemory headroom 的主要消耗者，記憶體調校跟持久化調校必須一起看。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 與 eviction 概念</a></strong>：TTL 設計決定哪些 key 帶過期時間，直接影響 <code>volatile-*</code> policy 的淘汰範圍。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">cache stampede</a></strong>：大量 key 同時被淘汰或同時過期會引發回源雪崩，eviction 調校要跟 TTL jitter / singleflight 一起設計。</li>
<li><strong>跟 <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 的 cache vs durable 選型</a></strong>：Tubi 把 ML feature store 從 ScyllaDB 遷到 ElastiCache，前提是「feature 可重新計算」——這個判斷決定了 eviction 是可接受的，記憶體調校才有意義。資料若不可重建，問題不在淘汰 policy，在選錯了儲存層。</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/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/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>上游概念：<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</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>Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/</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。持久化跟&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 走的邊界">記憶體調校&lt;/a>互相耦合（fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者），兩篇建議一起讀。機制以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/">Redis persistence 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="fork-那一瞬間">fork 那一瞬間&lt;/h2>
&lt;p>Redis 是單執行緒處理命令的，這是它延遲可預測的根基——直到它需要把記憶體裡的資料寫到磁碟。RDB snapshot 跟 AOF rewrite 都不能在主執行緒上慢慢做（會凍結所有命令），於是 Redis 的解法是 &lt;code>fork()&lt;/code>：複製出一個子進程，由子進程把當下的記憶體快照寫到磁碟，主進程繼續服務。&lt;/p>
&lt;p>問題在 &lt;code>fork()&lt;/code> 本身不是免費的。Linux 的 &lt;code>fork()&lt;/code> 要複製父進程的分頁表（page table），記憶體越大、分頁表越大，這個複製動作越久——而它發生在主執行緒上，是阻塞的。一個 20GB 的 Redis 實例，fork 可能凍結主執行緒數百毫秒到一秒。在這段時間裡，所有命令排隊，p99 延遲從 1ms 跳到 500ms+。&lt;/p>
&lt;p>更糟的是 fork 之後。&lt;code>fork()&lt;/code> 用 copy-on-write：子進程跟父進程共享實體分頁，直到某一方寫入才複製。子進程只讀（在寫 snapshot），但父進程持續服務寫入，每改一個分頁就觸發一次複製。寫入越密集、snapshot 跑越久，被複製的分頁越多，最壞情況記憶體接近翻倍。這就是為什麼 Redis 的 maxmemory 必須留 headroom——不是給資料，是給 fork 期間的分頁複製。&lt;/p>
&lt;p>理解持久化，本質是理解「fork 的延遲尖峰」與「資料持久性」之間的取捨。本文按這條線展開機制、配置與踩坑。&lt;/p>
&lt;h2 id="核心概念aof-與-rdb-是兩種不同的持久性語意">核心概念：AOF 與 RDB 是兩種不同的持久性語意&lt;/h2>
&lt;p>Redis 的兩種持久化不是「二選一的同類選項」，它們回答的是不同問題。&lt;/p>
&lt;p>&lt;strong>RDB 是某個時間點的記憶體快照&lt;/strong>。它把整個 dataset 序列化成一個緊湊的二進位檔（&lt;code>dump.rdb&lt;/code>）。優點是檔案小、還原快（直接載入記憶體）、fork 一次寫完。缺點是兩次 snapshot 之間的寫入會在崩潰時全部遺失——RDB 的持久性顆粒度是「上一次 save 到現在」，可能是幾分鐘的資料。&lt;/p>
&lt;p>&lt;strong>AOF 是命令的 append-only log&lt;/strong>。每個改變資料的命令（&lt;code>SET&lt;/code>、&lt;code>LPUSH&lt;/code>&amp;hellip;）被追加到 log 檔，還原時重放整個 log。優點是持久性顆粒度細（最多丟 &lt;code>fsync&lt;/code> 策略決定的一小段）。缺點是 log 會無限增長，需要定期 rewrite 壓縮——而 rewrite 也要 fork。&lt;/p>
&lt;p>兩者的 fork 觸發點不同但機制相同：RDB 是 &lt;code>BGSAVE&lt;/code>（手動或 save 規則觸發）fork，AOF 是 &lt;code>BGREWRITEAOF&lt;/code>（log 太大時觸發）fork。兩個若同時跑，記憶體壓力疊加。&lt;/p>
&lt;h3 id="aof-的-fsync-策略決定丟多少資料">AOF 的 fsync 策略決定丟多少資料&lt;/h3>
&lt;p>AOF 寫 log 分兩步：先 write 到 OS 的 page cache，再 fsync 刷到磁碟。&lt;code>appendfsync&lt;/code> 控制 fsync 頻率，這是持久性與延遲的核心旋鈕：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>&lt;code>appendfsync&lt;/code>&lt;/th>
 &lt;th>fsync 時機&lt;/th>
 &lt;th>崩潰最多丟&lt;/th>
 &lt;th>延遲影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>always&lt;/code>&lt;/td>
 &lt;td>每個寫命令&lt;/td>
 &lt;td>幾乎不丟&lt;/td>
 &lt;td>每次寫都等磁碟、延遲最高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>everysec&lt;/code>&lt;/td>
 &lt;td>每秒一次（背景）&lt;/td>
 &lt;td>最多 1 秒&lt;/td>
 &lt;td>多數場景的平衡點（預設）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>no&lt;/code>&lt;/td>
 &lt;td>交給 OS（~30 秒）&lt;/td>
 &lt;td>OS 決定、可能丟很多&lt;/td>
 &lt;td>延遲最低、持久性最弱&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>everysec&lt;/code> 是多數場景的預設選擇——背景執行緒每秒 fsync，主執行緒不等磁碟，崩潰最多丟 1 秒。但要注意：當磁碟 I/O 飽和，背景 fsync 跑超過 1 秒沒完成，主執行緒會被迫等待（避免 buffer 無限堆積），這時延遲尖峰跟 &lt;code>always&lt;/code> 一樣明顯。&lt;/p>
&lt;h3 id="混合持久化rdb-preamble--aof-tail">混合持久化：RDB preamble + AOF tail&lt;/h3>
&lt;p>Redis 4.0 後的 &lt;code>aof-use-rdb-preamble yes&lt;/code>（4.0+ 預設開）把兩者結合：AOF rewrite 時，先寫一段 RDB 格式的快照當前綴，後面接增量命令 log。還原時先快速載入 RDB preamble，再重放尾端的 log。這拿到了 RDB 的還原速度與 AOF 的細顆粒持久性，是目前的建議配置。&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。持久化跟<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>互相耦合（fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者），兩篇建議一起讀。機制以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/">Redis persistence 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="fork-那一瞬間">fork 那一瞬間</h2>
<p>Redis 是單執行緒處理命令的，這是它延遲可預測的根基——直到它需要把記憶體裡的資料寫到磁碟。RDB snapshot 跟 AOF rewrite 都不能在主執行緒上慢慢做（會凍結所有命令），於是 Redis 的解法是 <code>fork()</code>：複製出一個子進程，由子進程把當下的記憶體快照寫到磁碟，主進程繼續服務。</p>
<p>問題在 <code>fork()</code> 本身不是免費的。Linux 的 <code>fork()</code> 要複製父進程的分頁表（page table），記憶體越大、分頁表越大，這個複製動作越久——而它發生在主執行緒上，是阻塞的。一個 20GB 的 Redis 實例，fork 可能凍結主執行緒數百毫秒到一秒。在這段時間裡，所有命令排隊，p99 延遲從 1ms 跳到 500ms+。</p>
<p>更糟的是 fork 之後。<code>fork()</code> 用 copy-on-write：子進程跟父進程共享實體分頁，直到某一方寫入才複製。子進程只讀（在寫 snapshot），但父進程持續服務寫入，每改一個分頁就觸發一次複製。寫入越密集、snapshot 跑越久，被複製的分頁越多，最壞情況記憶體接近翻倍。這就是為什麼 Redis 的 maxmemory 必須留 headroom——不是給資料，是給 fork 期間的分頁複製。</p>
<p>理解持久化，本質是理解「fork 的延遲尖峰」與「資料持久性」之間的取捨。本文按這條線展開機制、配置與踩坑。</p>
<h2 id="核心概念aof-與-rdb-是兩種不同的持久性語意">核心概念：AOF 與 RDB 是兩種不同的持久性語意</h2>
<p>Redis 的兩種持久化不是「二選一的同類選項」，它們回答的是不同問題。</p>
<p><strong>RDB 是某個時間點的記憶體快照</strong>。它把整個 dataset 序列化成一個緊湊的二進位檔（<code>dump.rdb</code>）。優點是檔案小、還原快（直接載入記憶體）、fork 一次寫完。缺點是兩次 snapshot 之間的寫入會在崩潰時全部遺失——RDB 的持久性顆粒度是「上一次 save 到現在」，可能是幾分鐘的資料。</p>
<p><strong>AOF 是命令的 append-only log</strong>。每個改變資料的命令（<code>SET</code>、<code>LPUSH</code>&hellip;）被追加到 log 檔，還原時重放整個 log。優點是持久性顆粒度細（最多丟 <code>fsync</code> 策略決定的一小段）。缺點是 log 會無限增長，需要定期 rewrite 壓縮——而 rewrite 也要 fork。</p>
<p>兩者的 fork 觸發點不同但機制相同：RDB 是 <code>BGSAVE</code>（手動或 save 規則觸發）fork，AOF 是 <code>BGREWRITEAOF</code>（log 太大時觸發）fork。兩個若同時跑，記憶體壓力疊加。</p>
<h3 id="aof-的-fsync-策略決定丟多少資料">AOF 的 fsync 策略決定丟多少資料</h3>
<p>AOF 寫 log 分兩步：先 write 到 OS 的 page cache，再 fsync 刷到磁碟。<code>appendfsync</code> 控制 fsync 頻率，這是持久性與延遲的核心旋鈕：</p>
<table>
  <thead>
      <tr>
          <th><code>appendfsync</code></th>
          <th>fsync 時機</th>
          <th>崩潰最多丟</th>
          <th>延遲影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>always</code></td>
          <td>每個寫命令</td>
          <td>幾乎不丟</td>
          <td>每次寫都等磁碟、延遲最高</td>
      </tr>
      <tr>
          <td><code>everysec</code></td>
          <td>每秒一次（背景）</td>
          <td>最多 1 秒</td>
          <td>多數場景的平衡點（預設）</td>
      </tr>
      <tr>
          <td><code>no</code></td>
          <td>交給 OS（~30 秒）</td>
          <td>OS 決定、可能丟很多</td>
          <td>延遲最低、持久性最弱</td>
      </tr>
  </tbody>
</table>
<p><code>everysec</code> 是多數場景的預設選擇——背景執行緒每秒 fsync，主執行緒不等磁碟，崩潰最多丟 1 秒。但要注意：當磁碟 I/O 飽和，背景 fsync 跑超過 1 秒沒完成，主執行緒會被迫等待（避免 buffer 無限堆積），這時延遲尖峰跟 <code>always</code> 一樣明顯。</p>
<h3 id="混合持久化rdb-preamble--aof-tail">混合持久化：RDB preamble + AOF tail</h3>
<p>Redis 4.0 後的 <code>aof-use-rdb-preamble yes</code>（4.0+ 預設開）把兩者結合：AOF rewrite 時，先寫一段 RDB 格式的快照當前綴，後面接增量命令 log。還原時先快速載入 RDB preamble，再重放尾端的 log。這拿到了 RDB 的還原速度與 AOF 的細顆粒持久性，是目前的建議配置。</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"># --- RDB snapshot 規則（多久 + 多少改動觸發 BGSAVE）---</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># redis.conf:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">#   save 900 1      # 900 秒內有 1 個 key 改動</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">#   save 300 100    # 300 秒內有 100 個改動</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">#   save 60 10000   # 60 秒內有 10000 個改動</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 純 cache 不需要 RDB 可關閉：</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">#   save &#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># --- AOF 設定 ---</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli CONFIG SET appendonly yes
</span></span><span class="line"><span class="ln">11</span><span class="cl">redis-cli CONFIG SET appendfsync everysec
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># AOF rewrite 觸發條件：比上次 rewrite 大 100% 且至少 64MB</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">redis-cli CONFIG SET auto-aof-rewrite-percentage <span class="m">100</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">redis-cli CONFIG SET auto-aof-rewrite-min-size 64mb
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 混合持久化（4.0+ 預設）</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">redis-cli CONFIG SET aof-use-rdb-preamble yes</span></span></code></pre></div><p>降低 fork 衝擊的兩個系統層設定：</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. 關閉 Transparent Huge Pages（THP）——THP 會讓 copy-on-write 以 2MB 為單位複製、放大 fork 後的記憶體與延遲</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> never &gt; /sys/kernel/mm/transparent_hugepage/enabled
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 2. 允許 overcommit memory——fork 時 Linux 預設可能因 overcommit 檢查拒絕 fork、導致 BGSAVE 失敗</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># /etc/sysctl.conf:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#   vm.overcommit_memory = 1</span></span></span></code></pre></div><p>這兩個是 Redis 官方明確建議的系統設定，沒設好會直接讓 fork 失敗或放大延遲尖峰。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1bgsave-那一刻-p99-延遲尖峰">Case 1：BGSAVE 那一刻 p99 延遲尖峰</h3>
<p><strong>徵兆</strong>：監控上每隔一段時間（對齊 save 規則）出現規律的延遲尖峰，p99 從 2ms 跳到 300-800ms，持續一兩秒後恢復。<code>INFO stats</code> 的 <code>latest_fork_usec</code> 顯示某次 fork 花了 700000 微秒（0.7 秒）。</p>
<p><strong>根因</strong>：大記憶體實例的 <code>fork()</code> 要複製分頁表，這個動作阻塞主執行緒。實例越大尖峰越明顯，THP 開著會更嚴重。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 THP 關閉（最常見的放大原因）</li>
<li>把 RDB save 規則放寬或關閉——純 cache 場景靠 AOF 或乾脆不持久化</li>
<li>大實例考慮分片，把單實例記憶體降下來，fork 成本隨之降低</li>
<li>在 replica 上做持久化（master 只服務、replica 負責 BGSAVE），把 fork 尖峰移出服務路徑</li>
</ol>
<h3 id="case-2fork-期間記憶體翻倍觸發-oom">Case 2：fork 期間記憶體翻倍觸發 OOM</h3>
<p><strong>徵兆</strong>：BGSAVE 開始後記憶體快速上升，<code>used_memory_rss</code> 在 snapshot 期間衝高，撞到機器 RAM 上限，Linux OOM killer 把 redis-server 進程 SIGKILL，無預警下線。</p>
<p><strong>根因</strong>：copy-on-write 在寫入密集期間複製大量分頁，maxmemory 沒留足夠 headroom。maxmemory 設成 RAM 的 90%+ 時，fork 期間的分頁複製把 RSS 推爆系統。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>maxmemory 設成 RAM 的 60-70%，留 30-40% 給 fork copy-on-write（寫入越密集留越多）</li>
<li>設 <code>vm.overcommit_memory = 1</code> 避免 fork 直接被拒</li>
<li>在低寫入時段（夜間）排程 BGSAVE，減少 fork 期間被複製的分頁</li>
<li>監控 <code>latest_fork_usec</code> 與 BGSAVE 期間的 RSS 峰值，跟 <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>的 headroom 計算合看</li>
</ol>
<h3 id="case-3aof-everysec-在磁碟飽和時退化成-always">Case 3：AOF everysec 在磁碟飽和時退化成 always</h3>
<p><strong>徵兆</strong>：平常延遲穩定，某段時間（通常伴隨大量寫入或磁碟被其他進程佔用）延遲全面上升，<code>INFO</code> 的 <code>aof_delayed_fsync</code> 計數持續增加。</p>
<p><strong>根因</strong>：<code>everysec</code> 的背景 fsync 應該每秒完成，但磁碟 I/O 飽和時 fsync 跑超過 1 秒。Redis 為了不讓 AOF buffer 無限堆積，會在主執行緒上阻塞等 fsync 完成——<code>everysec</code> 在這個情境下退化成接近 <code>always</code> 的延遲行為。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>用獨立的高 IOPS 磁碟給 AOF（不要跟 OS / log / 其他服務共用 I/O）</li>
<li>監控 <code>aof_delayed_fsync</code>，持續增加代表磁碟跟不上寫入</li>
<li>評估 <code>no-appendfsync-on-rewrite yes</code>——AOF rewrite 期間暫停 fsync，避免 rewrite 的 I/O 跟 fsync 互搶（代價是 rewrite 期間崩潰丟更多）</li>
<li>寫入吞吐超過單磁碟負荷是擴容訊號，不是調 fsync 能解</li>
</ol>
<h3 id="case-4aof-檔尾損壞讓-redis-起不來">Case 4：AOF 檔尾損壞讓 Redis 起不來</h3>
<p><strong>徵兆</strong>：Redis 崩潰後重啟失敗，log 顯示 <code>Bad file format reading the append only file</code>，服務無法載入 AOF。</p>
<p><strong>根因</strong>：崩潰發生在 AOF 寫到一半，最後一條命令只寫了部分 byte，AOF 檔尾不完整。Redis 預設 <code>aof-load-truncated yes</code> 應能容忍尾端截斷，但若損壞在中段（罕見的磁碟錯誤）或設了 <code>aof-load-truncated no</code>，載入直接失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 <code>aof-load-truncated yes</code>（預設），容忍尾端截斷自動修復</li>
<li>中段損壞用 <code>redis-check-aof --fix appendonly.aof</code> 修復（會截掉損壞點之後的內容、有資料遺失）</li>
<li>修復前先備份原 AOF 檔，不要直接覆蓋</li>
<li>混合持久化下還原優先用 RDB preamble，降低純 AOF replay 的損壞風險</li>
</ol>
<h3 id="case-5以為有持久化其實-bgsave-一直在失敗">Case 5：以為有持久化、其實 BGSAVE 一直在失敗</h3>
<p><strong>徵兆</strong>：某次需要從 RDB 還原時發現 <code>dump.rdb</code> 是好幾天前的，期間的資料全沒了。回查 log 發現 BGSAVE 一直報 <code>Can't save in background: fork: Cannot allocate memory</code>。</p>
<p><strong>根因</strong>：<code>vm.overcommit_memory</code> 是預設的 0，Linux 在 fork 時做嚴格的記憶體檢查——當 Redis 已用掉大半 RAM，fork 估算可能需要翻倍記憶體而被拒。BGSAVE 靜默失敗，RDB 停留在最後一次成功的版本，但沒人在看 log。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設 <code>vm.overcommit_memory = 1</code>，讓 fork 在記憶體吃緊時仍能成功（靠 copy-on-write 實際不會真的翻倍）</li>
<li>監控 <code>rdb_last_bgsave_status</code> 與 <code>aof_last_bgrewrite_status</code>，<code>err</code> 要立刻告警</li>
<li>監控 <code>rdb_last_save_time</code>，距今太久代表持久化已停擺</li>
<li>持久化的存在不等於可用——定期演練從備份還原，驗證 RDB / AOF 真的能載入</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>持久化的容量判讀，圍繞 fork 成本與磁碟負荷：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>latest_fork_usec</code></td>
          <td>&lt; 100ms（小實例）</td>
          <td>&gt; 數百 ms → 實例太大、考慮分片或 replica 持久化</td>
      </tr>
      <tr>
          <td>fork 期間 RSS 峰值</td>
          <td>&lt; 機器 RAM</td>
          <td>接近 RAM → maxmemory headroom 不足</td>
      </tr>
      <tr>
          <td><code>aof_delayed_fsync</code></td>
          <td>接近 0</td>
          <td>持續增加 → 磁碟 I/O 跟不上、換高 IOPS 磁碟</td>
      </tr>
      <tr>
          <td><code>rdb_last_bgsave_status</code></td>
          <td><code>ok</code></td>
          <td><code>err</code> → fork 失敗、查 overcommit / 記憶體</td>
      </tr>
      <tr>
          <td>AOF 檔大小 / dataset</td>
          <td>rewrite 後接近 dataset 大小</td>
          <td>遠大於 dataset → rewrite 沒觸發、檢查閾值</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>fork 尖峰無法接受、實例又必須大</strong>：把持久化移到 replica（master 純服務），或走 <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 分片</a>降低單實例記憶體。</li>
<li><strong>大記憶體下 fork 成本是結構性瓶頸</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 用 fork-less snapshot 機制，大記憶體場景的快照不付 fork 的延遲與記憶體翻倍代價——若 fork 尖峰是主要痛點，這是值得評估的架構替代。</li>
<li><strong>需要真正的 source-of-truth 持久性（不是盡力而為）</strong>：Redis 持久化本質是 cache 的回填保險，不是交易級持久性。要強持久性走 <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）或 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a>。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>持久化決策的起點其實是一個選型問題：這份資料是 cache 還是 source-of-truth。</p>
<ul>
<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>：fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者，兩者必須一起算。</li>
<li><strong>跟 <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 走的邊界">replication / failover</a></strong>：replica 是承接持久化負擔的地方，也是 fork 尖峰的替代執行點。</li>
<li><strong>跟 <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 的 cache vs durable 選型</a></strong>：Tubi 把 ML feature store 從 ScyllaDB（durable）遷到 ElastiCache，判斷是「feature 可重新計算」——這正是「不需要持久化」的判斷，持久化配置應隨之簡化甚至關閉。反過來，若資料不可重建，問題在選錯儲存層，不在持久化調校。</li>
<li><strong>跟 <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></strong>：服務若把 Redis 當主要 serving layer，持久化決定了重啟後是冷啟動回源雪崩還是溫啟動，跟 stampede 防護直接相關。</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/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>、<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>上游概念：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 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>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>Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/</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。連線與往返是 application 端量到的延遲主因，跟 server 端的&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 走的邊界">記憶體&lt;/a>、&lt;a href="https://tarrragon.github.io/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 場景到底要不要持久化的邊界">持久化&lt;/a>調校互補。pipeline 機制以 &lt;a href="https://redis.io/docs/latest/develop/use/pipelining/">Redis pipelining 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="延遲不在-redis在往返">延遲不在 Redis、在往返&lt;/h2>
&lt;p>把單一 &lt;code>GET&lt;/code> 丟進 &lt;code>redis-cli --latency&lt;/code>，會看到 server 端執行時間是微秒級。但 application 端的 APM 量到的 Redis 呼叫卻是 1-3ms。這個差距不是 Redis 變慢了，是網路往返（round-trip time，RTT）——命令從 application 送到 Redis、結果送回來，這趟來回就是毫秒級，而 Redis 的執行只佔其中一小部分。&lt;/p>
&lt;p>這個認知翻轉了 Redis 優化的方向：當你的服務每個請求要打 10 個 Redis 命令，瓶頸不是 Redis 的吞吐，是 10 次 RTT 疊加成 10-30ms。pipelining 常被講成「批次發命令省效能」，但它真正消除的是 RTT 稅——把 10 次往返打包成 1 次往返，server 端執行時間幾乎不變，但 application 端延遲從 10×RTT 降到 1×RTT。&lt;/p>
&lt;p>對每次互動要查多個 cache 的服務，這筆 RTT 稅是延遲預算的主要支出。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &amp;#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 架構下的痛點&lt;/a>正是這個放大版：application 在一個 cloud、cache 在另一個，每次 lookup 多吃 5-30ms 跨 cloud RTT，「5ms × 10 cache lookup = 50ms 額外延遲」。Snap 把 KeyDB 部署到同 cloud 減少跨 cloud RTT，本質就是降低往返稅。本文處理 RTT 的會計、連線池配置與 pipeline 的正確使用。&lt;/p>
&lt;h2 id="核心概念rtt-會計與三種降稅手段">核心概念：RTT 會計與三種降稅手段&lt;/h2>
&lt;p>Redis 一次請求的延遲拆成三段：client 序列化 + 送出、網路往返（RTT）、server 執行。多數 cache 場景下 RTT 是主導項，server 執行可忽略。降低總延遲有三種手段，對應三種「省 RTT」的方式：&lt;/p>
&lt;p>&lt;strong>連線池消除「每次都建連線」的稅&lt;/strong>。建立 TCP 連線（三次握手）本身就是一趟 RTT，若還有 TLS 再加幾趟。每個請求都新建連線等於每次都付建連稅。連線池讓連線重用，把建連成本攤平到接近零。&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。連線與往返是 application 端量到的延遲主因，跟 server 端的<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>、<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 場景到底要不要持久化的邊界">持久化</a>調校互補。pipeline 機制以 <a href="https://redis.io/docs/latest/develop/use/pipelining/">Redis pipelining 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="延遲不在-redis在往返">延遲不在 Redis、在往返</h2>
<p>把單一 <code>GET</code> 丟進 <code>redis-cli --latency</code>，會看到 server 端執行時間是微秒級。但 application 端的 APM 量到的 Redis 呼叫卻是 1-3ms。這個差距不是 Redis 變慢了，是網路往返（round-trip time，RTT）——命令從 application 送到 Redis、結果送回來，這趟來回就是毫秒級，而 Redis 的執行只佔其中一小部分。</p>
<p>這個認知翻轉了 Redis 優化的方向：當你的服務每個請求要打 10 個 Redis 命令，瓶頸不是 Redis 的吞吐，是 10 次 RTT 疊加成 10-30ms。pipelining 常被講成「批次發命令省效能」，但它真正消除的是 RTT 稅——把 10 次往返打包成 1 次往返，server 端執行時間幾乎不變，但 application 端延遲從 10×RTT 降到 1×RTT。</p>
<p>對每次互動要查多個 cache 的服務，這筆 RTT 稅是延遲預算的主要支出。<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 架構下的痛點</a>正是這個放大版：application 在一個 cloud、cache 在另一個，每次 lookup 多吃 5-30ms 跨 cloud RTT，「5ms × 10 cache lookup = 50ms 額外延遲」。Snap 把 KeyDB 部署到同 cloud 減少跨 cloud RTT，本質就是降低往返稅。本文處理 RTT 的會計、連線池配置與 pipeline 的正確使用。</p>
<h2 id="核心概念rtt-會計與三種降稅手段">核心概念：RTT 會計與三種降稅手段</h2>
<p>Redis 一次請求的延遲拆成三段：client 序列化 + 送出、網路往返（RTT）、server 執行。多數 cache 場景下 RTT 是主導項，server 執行可忽略。降低總延遲有三種手段，對應三種「省 RTT」的方式：</p>
<p><strong>連線池消除「每次都建連線」的稅</strong>。建立 TCP 連線（三次握手）本身就是一趟 RTT，若還有 TLS 再加幾趟。每個請求都新建連線等於每次都付建連稅。連線池讓連線重用，把建連成本攤平到接近零。</p>
<p><strong>pipelining 把 N 次 RTT 壓成 1 次</strong>。連續送 N 個命令而不等每個的回應，一次讀回 N 個結果。這要求這 N 個命令彼此無依賴（後一個不需要前一個的結果）。</p>
<p><strong>Lua script / 多 key 命令把多操作合成 1 次往返且原子</strong>。當命令之間有依賴（讀了再決定怎麼寫），pipeline 不適用（後面的命令送出時前面的結果還沒回來），這時用 Lua script 把邏輯放到 server 端一次執行，省 RTT 又拿到原子性。</p>
<h3 id="pipeline-跟-multi-是不同的東西">pipeline 跟 MULTI 是不同的東西</h3>
<p>這兩個常被混淆，但解的問題不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>pipeline</th>
          <th>MULTI / EXEC（transaction）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要目的</td>
          <td>省 RTT（效能）</td>
          <td>原子性（多命令不被打斷）</td>
      </tr>
      <tr>
          <td>原子性</td>
          <td>無——命令間可能插入其他 client</td>
          <td>有——EXEC 內命令連續執行不被插入</td>
      </tr>
      <tr>
          <td>回應時機</td>
          <td>全部送完一次讀回</td>
          <td>EXEC 後一次回所有結果</td>
      </tr>
      <tr>
          <td>失敗處理</td>
          <td>各命令獨立成敗</td>
          <td>入隊期語法錯整批拒、執行期錯不回滾</td>
      </tr>
      <tr>
          <td>適用</td>
          <td>大量無依賴命令的批次讀寫</td>
          <td>需要「一組命令不被其他 client 插隊」</td>
      </tr>
  </tbody>
</table>
<p>pipeline 純粹是傳輸層優化，不保證原子性——pipeline 裡的命令在 server 端仍可能跟其他 client 的命令交錯。要原子性用 MULTI/EXEC 或 Lua。兩者也可以組合（在 pipeline 裡送 MULTI&hellip;EXEC）。</p>
<p>注意 Redis 的 MULTI/EXEC 不是關聯式 DB 的 transaction：執行期某命令出錯（例如對 string 做 list 操作）不會回滾已執行的命令，它沒有 rollback。</p>
<h2 id="配置連線池與-pipeline-的設定路徑">配置：連線池與 pipeline 的設定路徑</h2>
<p>連線池配置（以 Python redis-py 為例，多數 client library 概念一致）：</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">import</span> <span class="nn">redis</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">pool</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">ConnectionPool</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">host</span><span class="o">=</span><span class="s2">&#34;10.0.0.1&#34;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">6379</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">max_connections</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span>          <span class="c1"># 池上限、依並發量與 Redis maxclients 反推</span>
</span></span><span class="line"><span class="ln"> 6</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 class="c1"># 單命令逾時（秒）——必設、否則慢命令拖垮 caller</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">socket_connect_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>  <span class="c1"># 建連逾時</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">health_check_interval</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span>    <span class="c1"># 定期檢查連線存活、清掉壞連線</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">Redis</span><span class="p">(</span><span class="n">connection_pool</span><span class="o">=</span><span class="n">pool</span><span class="p">)</span></span></span></code></pre></div><p><code>socket_timeout</code> 是最常被遺漏卻最關鍵的設定——沒設逾時，一個慢命令或網路黑洞會讓 caller 無限等待，連鎖拖垮上游。</p>
<p>pipeline 的使用：</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="c1"># pipeline：N 個無依賴命令、一次往返</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">pipe</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">pipeline</span><span class="p">(</span><span class="n">transaction</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>  <span class="c1"># transaction=False 純 pipeline、不包 MULTI</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">for</span> <span class="n">uid</span> <span class="ow">in</span> <span class="n">user_ids</span><span class="p">:</span>                  <span class="c1"># 假設要拿 100 個 user 的 profile</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">pipe</span><span class="o">.</span><span class="n">hgetall</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;user:</span><span class="si">{</span><span class="n">uid</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">results</span> <span class="o">=</span> <span class="n">pipe</span><span class="o">.</span><span class="n">execute</span><span class="p">()</span>              <span class="c1"># 一次往返拿回 100 個結果</span></span></span></code></pre></div><p>依賴型操作改用 Lua（命令間有讀後寫的依賴，pipeline 不適用）：</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="c1"># 原子的 check-and-set：讀目前值、符合條件才更新——一次往返且原子</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">lua</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s2">local current = redis.call(&#39;GET&#39;, KEYS[1])
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">if current == ARGV[1] then
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">  redis.call(&#39;SET&#39;, KEYS[1], ARGV[2])
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">  return 1
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">end
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">return 0
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">cas</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">register_script</span><span class="p">(</span><span class="n">lua</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">cas</span><span class="p">(</span><span class="n">keys</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;lock:resource&#34;</span><span class="p">],</span> <span class="n">args</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;old_token&#34;</span><span class="p">,</span> <span class="s2">&#34;new_token&#34;</span><span class="p">])</span></span></span></code></pre></div><p><code>MGET</code> / <code>MSET</code> / <code>HMGET</code> 等原生多 key 命令是最簡單的省 RTT 手段——能用多 key 命令就不用 pipeline，更省事且原子。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1每請求新建連線延遲全是建連稅">Case 1：每請求新建連線、延遲全是建連稅</h3>
<p><strong>徵兆</strong>：Redis 呼叫延遲偏高且不穩，<code>INFO stats</code> 的 <code>total_connections_received</code> 速率極高（接近 QPS），Redis 的 <code>connected_clients</code> 反覆上下震盪。</p>
<p><strong>根因</strong>：application 沒用連線池，或每個請求 <code>redis.Redis(...)</code> 重新建立 client。每次請求付一趟 TCP 握手（加 TLS 更多）的 RTT，建連稅疊在每個請求上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>用連線池並重用，client 物件在 application 生命週期內共用，不是每請求建立</li>
<li>短生命週期環境（Lambda / serverless）把連線池放在 handler 外（容器重用時連線存活）</li>
<li>監控 <code>total_connections_received</code> 速率，遠高於合理重連頻率代表沒重用</li>
<li>TLS 場景的建連稅更高，連線重用的收益更大</li>
</ol>
<h3 id="case-2沒設-socket_timeout一個慢命令拖垮整條鏈">Case 2：沒設 socket_timeout、一個慢命令拖垮整條鏈</h3>
<p><strong>徵兆</strong>：某次 Redis 短暫卡頓（fork 尖峰、網路抖動），application 端大量請求 hang 住不回，thread / connection 被耗盡，影響擴散到跟 Redis 無關的請求。</p>
<p><strong>根因</strong>：連線沒設 <code>socket_timeout</code>。Redis 一旦慢回應或網路黑洞，caller 無限等待，佔住 thread 與連線，連鎖拖垮整個服務。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>一律設 <code>socket_timeout</code>（cache 場景通常幾百 ms 就該逾時，cache 本來就該快）</li>
<li>逾時後 application 要有 fallback（回源或降級），不是把逾時當 fatal</li>
<li>連線池 <code>max_connections</code> 設上限，避免無限建連把 Redis 的 <code>maxclients</code> 打滿</li>
<li>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> 的延遲尖峰治理</li>
</ol>
<h3 id="case-3一個巨大-pipeline-把-server-跟-client-都撐爆">Case 3：一個巨大 pipeline 把 server 跟 client 都撐爆</h3>
<p><strong>徵兆</strong>：用 pipeline 批次處理時，某次塞了幾十萬個命令進一個 pipeline，Redis 記憶體尖峰、client 端記憶體爆，甚至 OOM。</p>
<p><strong>根因</strong>：pipeline 把所有命令的 request 跟 response 都 buffer 起來。一次塞太多，server 端要 buffer 全部 reply（計入 <code>used_memory</code>、見 <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> 的 output buffer），client 端要 hold 全部結果，雙邊記憶體尖峰。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pipeline 分批（chunk），每批幾百到幾千命令，不要一個 pipeline 塞無上限</li>
<li>大量資料的掃描用 <code>SCAN</code> 游標分批，不要 <code>KEYS *</code> 一次撈</li>
<li>監控 client output buffer（<code>CLIENT LIST</code> 的 <code>omem</code>），異常大代表有巨型 pipeline 或慢 consumer</li>
<li>批次大小靠 RTT 與記憶體權衡——批次越大省越多 RTT，但記憶體尖峰越高</li>
</ol>
<h3 id="case-4在-cluster-模式對跨-slot-key-開-pipeline--transaction-失敗">Case 4：在 cluster 模式對跨 slot key 開 pipeline / transaction 失敗</h3>
<p><strong>徵兆</strong>：單機 Redis 上運作正常的 pipeline 或 MULTI，搬到 <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> 後報 <code>CROSSSLOT Keys in request don't hash to the same slot</code>。</p>
<p><strong>根因</strong>：Cluster 模式下 MULTI/EXEC 與某些多 key 命令要求所有 key 在同一個 hash slot。pipeline 在 cluster 下也要按 slot 分組送到對應 node——若 client library 不自動處理跨 slot，會失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>同組操作的 key 用 hash tag <code>{...}</code> 強制同 slot（例如 <code>user:{123}:profile</code>、<code>user:{123}:settings</code>）</li>
<li>用支援 cluster pipeline 的 client library，它會自動按 slot 分組</li>
<li>設計階段就考慮 key 的 slot 分布，避免事後重構，對應 cluster re-sharding 的 hash tag 治理</li>
<li>跨 slot 的批次邏輯改用 application 端聚合，不依賴 server 端原子性</li>
</ol>
<h3 id="case-5把-pipeline-當-transaction-用出現資料競態">Case 5：把 pipeline 當 transaction 用、出現資料競態</h3>
<p><strong>徵兆</strong>：用 pipeline 做「讀一個值、根據它決定寫什麼」的邏輯，高並發下偶發資料不一致——兩個 client 讀到同樣的舊值、各自寫入，一方覆蓋另一方。</p>
<p><strong>根因</strong>：把 pipeline 誤當原子操作。pipeline 只是把命令打包傳輸，命令之間 server 端仍可能插入其他 client 的命令——它沒有原子性。讀後寫的依賴邏輯放 pipeline 裡，等於沒有任何併發保護。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>讀後寫的依賴邏輯用 Lua script（server 端原子執行），不用 pipeline</li>
<li>樂觀鎖場景用 <code>WATCH</code> + MULTI/EXEC（watch 的 key 被改則 EXEC 失敗、重試）</li>
<li>分清楚需求：要省 RTT 用 pipeline，要原子性用 Lua / MULTI，兩者目的不同</li>
<li>distributed lock 場景見 <a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a>，Redis 的鎖有自己的正確性陷阱</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>連線與往返的容量判讀，圍繞連線數與每請求往返次數：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>connected_clients</code></td>
          <td>穩定、遠低於 <code>maxclients</code></td>
          <td>接近 maxclients → 池太大或洩漏、調池上限</td>
      </tr>
      <tr>
          <td><code>total_connections_received</code> 速率</td>
          <td>低（連線重用）</td>
          <td>接近 QPS → 沒用連線池、每請求建連</td>
      </tr>
      <tr>
          <td>每請求 Redis 往返次數</td>
          <td>盡量合併（多 key / pipeline）</td>
          <td>多次獨立往返 → 用 pipeline / MGET 合併</td>
      </tr>
      <tr>
          <td>client output buffer (<code>omem</code>)</td>
          <td>小</td>
          <td>大 → 巨型 pipeline 或慢 consumer</td>
      </tr>
      <tr>
          <td>Redis CPU</td>
          <td>有餘裕</td>
          <td>單執行緒 CPU 滿 → 命令太重或 QPS 超單機</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單執行緒 CPU 打滿、命令吞吐到頂</strong>：Redis 主執行緒單線處理命令，pipeline 省 RTT 但不增加 server 端平行度。CPU 到頂走 <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 分片</a>把命令分散到多 node。</li>
<li><strong>想要單機多核平行處理命令</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 shared-nothing 多核架構讓命令在單機就能多核平行，Redis 要靠 cluster 才能達到的吞吐它單機就能撐——高吞吐單機 workload 的替代。</li>
<li><strong>跨 cloud / 跨 region 的 RTT 是結構性瓶頸</strong>：<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 的解法</a>是把 cache 部署到跟 application 同 cloud / 同 region，從根本消除跨區 RTT——這是架構層決策，不是 pipeline 能補的。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>連線與往返是 application 端延遲的主因，但它跟 server 端調校互補：</p>
<ul>
<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>：巨型 pipeline 的 server 端 reply buffer 計入 <code>used_memory</code>、慢 consumer 的 output buffer 是記憶體洩漏源頭。</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>：fork 尖峰是 socket_timeout 必須存在的理由之一——慢源頭不只網路。</li>
<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>：cluster 模式改變 pipeline / transaction 的 key 分布規則，hash tag 治理是前提。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></strong>：高並發下的連線數爆炸與熱 key 是同一組壓力的不同面向，連線池上限與 local cache 兩層都是解法。</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/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>、<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/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>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>、<a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</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>自管 Redis / Valkey → AWS ElastiCache：engine 不變、變的是誰運維</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-elasticache/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-elasticache/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &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> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（source、自管）跟 &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>（target、managed）。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 對映 &lt;strong>Operational model = High（自管 → managed）、其他 Low → Type C operational hybrid&lt;/strong>。ElastiCache 是 managed SaaS、AWS 操作依官方文件（未本機驗證、引數以官方為準）、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="engine-不變變的是誰運維">engine 不變、變的是誰運維&lt;/h2>
&lt;p>多數 vendor 遷移會換掉某個本質的東西——協定、data model、paradigm。自管 Redis/Valkey → ElastiCache 一個都沒換：ElastiCache 跑的就是 Redis 或 Valkey engine，同樣的 RESP 協定、同樣的 data types、同樣的 client library、同樣的命令。application code 幾乎不用動。&lt;/p>
&lt;p>那遷的是什麼？&lt;strong>運維責任的歸屬&lt;/strong>。自管時要自己部署、自己打 patch、自己設 replication、自己半夜處理 failover。ElastiCache 把這些接走——AWS 做 failover、patching、snapshot、跨 AZ 複製。這個遷移的全部工作量集中在「把運維交出去」這件事上：網路（VPC）、安全（IAM / Security Group）、cutover 的資料連續性，以及想清楚&lt;strong>交出運維的同時、交出了哪些控制權&lt;/strong>（不再能 SSH 進機器、不能改任意 config、parameter group 限定可調項）。&lt;/p>
&lt;p>這對映 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論&lt;/a> 的 Type C operational hybrid——operational model 是唯一的 High 維度，其他全 Low。本文展開這個「engine 不變、運維轉移」遷移的實際工作與責任邊界。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>同 engine（Redis/Valkey）、RESP 一致、命令一致&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Operational model&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>自管 → AWS managed（failover/patch/snapshot）&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>完全相同（同 engine）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>1 → 1&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、client 加 reconnect / TLS、其餘不動&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>cache 可重建（re-warm）或 RDB seed / online 複製&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>唯一 High 是 operational model，對映 &lt;strong>Type C operational hybrid&lt;/strong>。Type C 的結構是「operational audit 前置 + drop-in cutover」——因為 engine/API 不變，cutover 本身接近 drop-in（換 endpoint），重點在前置的網路/安全/責任邊界盤點。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <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> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（source、自管）跟 <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>（target、managed）。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 對映 <strong>Operational model = High（自管 → managed）、其他 Low → Type C operational hybrid</strong>。ElastiCache 是 managed SaaS、AWS 操作依官方文件（未本機驗證、引數以官方為準）、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="engine-不變變的是誰運維">engine 不變、變的是誰運維</h2>
<p>多數 vendor 遷移會換掉某個本質的東西——協定、data model、paradigm。自管 Redis/Valkey → ElastiCache 一個都沒換：ElastiCache 跑的就是 Redis 或 Valkey engine，同樣的 RESP 協定、同樣的 data types、同樣的 client library、同樣的命令。application code 幾乎不用動。</p>
<p>那遷的是什麼？<strong>運維責任的歸屬</strong>。自管時要自己部署、自己打 patch、自己設 replication、自己半夜處理 failover。ElastiCache 把這些接走——AWS 做 failover、patching、snapshot、跨 AZ 複製。這個遷移的全部工作量集中在「把運維交出去」這件事上：網路（VPC）、安全（IAM / Security Group）、cutover 的資料連續性，以及想清楚<strong>交出運維的同時、交出了哪些控制權</strong>（不再能 SSH 進機器、不能改任意 config、parameter group 限定可調項）。</p>
<p>這對映 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的 Type C operational hybrid——operational model 是唯一的 High 維度，其他全 Low。本文展開這個「engine 不變、運維轉移」遷移的實際工作與責任邊界。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 engine（Redis/Valkey）、RESP 一致、命令一致</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Operational model</strong></td>
          <td><strong>自管 → AWS managed（failover/patch/snapshot）</strong></td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>完全相同（同 engine）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>1 → 1</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、client 加 reconnect / TLS、其餘不動</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>cache 可重建（re-warm）或 RDB seed / online 複製</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>唯一 High 是 operational model，對映 <strong>Type C operational hybrid</strong>。Type C 的結構是「operational audit 前置 + drop-in cutover」——因為 engine/API 不變，cutover 本身接近 drop-in（換 endpoint），重點在前置的網路/安全/責任邊界盤點。</p>
<h2 id="operational-auditcutover-前要盤點的">operational audit：cutover 前要盤點的</h2>
<p>ElastiCache 把運維接走，但也劃下新的邊界。cutover 前必盤：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>自管時的負責項</th>
          <th>ElastiCache 後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署 / patch</td>
          <td>自己裝、自己升級</td>
          <td>AWS 管（失去任意版本控制、跟 AWS 的 engine 版本走）</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自己設 Sentinel / 手動切</td>
          <td>Multi-AZ 自動（需確保 client 會重連）</td>
      </tr>
      <tr>
          <td>config</td>
          <td>改任意 redis.conf</td>
          <td>只能改 parameter group 開放的項（部分鎖死）</td>
      </tr>
      <tr>
          <td>網路存取</td>
          <td>自己的網路</td>
          <td>只在 VPC 內可達、要設 subnet group / Security Group</td>
      </tr>
      <tr>
          <td>認證</td>
          <td>AUTH password / 自管 TLS</td>
          <td>IAM auth（Redis 7+）/ ElastiCache 管的 TLS</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>自己的 Prometheus 等</td>
          <td>CloudWatch（指標名與自管不同、dashboard 要改）</td>
      </tr>
  </tbody>
</table>
<p><strong>audit 的關鍵 output</strong>：(1) 目前改了哪些 redis.conf 項、ElastiCache parameter group 是否支援；(2) client 是否有 failover reconnect 邏輯（managed failover 不會代為重連）；(3) 監控要從自管工具搬到 CloudWatch。這三項是 Type C 的核心工作。詳細的 managed 責任邊界見 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界 deep article</a>。</p>
<h2 id="cutover資料連續性的兩條路">cutover：資料連續性的兩條路</h2>
<p>因為 engine/API 不變，cutover 接近 drop-in（換 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">路徑 A：re-warm（cache 可重建、最簡單）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  1. 建 ElastiCache cluster（空的、選 Valkey / Redis engine、設 parameter group）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  2. application 雙寫（自管 + ElastiCache）、讀仍走自管
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  3. 讀切到 ElastiCache endpoint、cache miss 回源 warm up
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  4. 命中率追上 → 停寫自管 → 下線自管
</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">路徑 B：RDB seed（要 cache 連續性、避免 warm-up origin 衝擊）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  1. 自管端 BGSAVE 產生 RDB
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  2. RDB 上傳 S3、ElastiCache 從 S3 seed 建 cluster（依官方 restore 流程）
</span></span><span class="line"><span class="ln">10</span><span class="cl">  3. application 換 endpoint cutover
</span></span><span class="line"><span class="ln">11</span><span class="cl">  （ElastiCache 也提供 self-managed Redis online migration、見官方文件）</span></span></code></pre></div><p>判讀：</p>
<ul>
<li>純 cache、能接受短暫 warm-up → 路徑 A（最簡單、無資料遷移）</li>
<li>大 dataset、warm-up 會打爆 origin → 路徑 B（RDB seed 保連續性）</li>
<li>AWS CLI 建 cluster 與 restore 細節依 <a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/">ElastiCache 官方文件</a>（未本機驗證）</li>
<li>engine 選 Valkey（AWS default、約低 Redis 20%）除非有 Redis 商業 module 依賴</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1parameter-group-不支援自管時改的-config">Case 1：parameter group 不支援自管時改的 config</h3>
<p><strong>徵兆</strong>：自管時改了某個 redis.conf 項（例如特定 <code>client-output-buffer-limit</code> 或某個進階參數），遷到 ElastiCache 後該設定無法套用或行為不同。</p>
<p><strong>根因</strong>：ElastiCache 只允許改 parameter group 開放的項，部分 config 被 AWS 鎖死（為了 managed 穩定性）。自管時的任意 config 自由度在 managed 後收窄。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration 列出自管端所有非預設 config，逐項對照 ElastiCache parameter group 支援度</li>
<li>不支援的項要評估影響——有些是 AWS 已用更好的方式處理、有些要調整 application 適應</li>
<li>把這個盤點放在 operational audit（cutover 前），不要遷完才發現</li>
<li>高度依賴特殊 config 調校的場景，managed 可能不適合、留自管</li>
</ol>
<h3 id="case-2failover-後-client-不重連managed-不代為重連">Case 2：failover 後 client 不重連（managed 不代為重連）</h3>
<p><strong>徵兆</strong>：ElastiCache Multi-AZ failover 完成，但 application 持續連舊 primary、寫入失敗。</p>
<p><strong>根因</strong>：ElastiCache 接走了 failover（自動晉升 replica），但 application 的 client 重連仍是 application 端的責任——這是 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">managed 責任邊界</a> 的核心：AWS 換 primary，client 要自己跟上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 連 primary endpoint（會跟著 failover 更新 DNS）、不寫死 node IP</li>
<li>client 設合理 socket timeout + retry + 縮短 DNS 快取</li>
<li>遷移前就驗證 client 有 failover reconnect 行為（自管 Sentinel 時可能靠不同機制）</li>
<li>對應 <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 走的邊界">Redis Sentinel failover 時序</a>：自管與 managed 的 failover 機制不同、client 處理要重驗</li>
</ol>
<h3 id="case-3endpoint-只在-vpc-內cutover-後連不上">Case 3：endpoint 只在 VPC 內、cutover 後連不上</h3>
<p><strong>徵兆</strong>：cutover 後 application 完全連不上 ElastiCache、連線逾時。</p>
<p><strong>根因</strong>：ElastiCache endpoint 只在 VPC 內可達、不對公網開放。Security Group 沒開 6379、subnet group 配置錯、或 application 不在同 VPC / 沒有 VPC peering，就連不上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cutover 前確認 Security Group 開 6379 給 application 的來源、subnet group 正確</li>
<li>application 不在同 VPC 要設 peering / Transit Gateway</li>
<li>從 VPC 內 EC2 先 <code>redis-cli -h &lt;endpoint&gt; ping</code> 驗證連通，再切 application</li>
<li>這是自管（自己的網路）→ managed（AWS VPC 模型）最常見的卡點</li>
</ol>
<h3 id="case-4監控斷層自管工具--cloudwatch">Case 4：監控斷層（自管工具 → CloudWatch）</h3>
<p><strong>徵兆</strong>：cutover 後原本的 Prometheus / Grafana dashboard 全空、告警失效。</p>
<p><strong>根因</strong>：自管時用 redis_exporter + Prometheus，ElastiCache 的指標在 CloudWatch、指標名與維度不同。直接搬 dashboard 不會動。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cutover 前把關鍵告警在 CloudWatch 重建（<code>DatabaseMemoryUsagePercentage</code> / <code>ReplicationLag</code> / <code>CurrConnections</code> 等）</li>
<li>要保留 Grafana 可用 CloudWatch data source 接</li>
<li>把監控遷移納入 operational audit、不要遷完才發現沒監控</li>
<li>核心指標語意相同（記憶體 / 命中 / 連線 / 複製延遲）、只是來源與命名變了</li>
</ol>
<h3 id="case-5以為-managed-就不會-oom--stampede--熱-key">Case 5：以為 managed 就不會 OOM / stampede / 熱 key</h3>
<p><strong>徵兆</strong>：遷到 ElastiCache 後仍然 OOM、cache stampede、熱 key 打爆單 shard。</p>
<p><strong>根因</strong>：ElastiCache 接走的是運維（failover/patch/snapshot），不是 cache 使用方式的問題。記憶體淘汰、stampede、熱 key、key 設計仍是 application 端的責任——managed 不等於 hands-off。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>記憶體 / eviction 調校仍要做（透過 parameter group 設 maxmemory-policy），見 <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>stampede / 熱 key 的 application 端防護（jitter / singleflight / 兩層 cache）照舊</li>
<li>釐清 managed 的責任邊界——左欄 AWS 管、右欄 application 端管，見 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">責任邊界 deep article</a></li>
<li>遷 managed 是減運維、不是免設計</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>自管 Redis / Valkey</th>
          <th>ElastiCache（managed）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>engine / API</td>
          <td>同（Redis / Valkey）</td>
          <td>同（Redis / Valkey engine）</td>
      </tr>
      <tr>
          <td>運維責任</td>
          <td>全部自己扛</td>
          <td>failover / patch / snapshot 交 AWS</td>
      </tr>
      <tr>
          <td>config 自由度</td>
          <td>任意 redis.conf</td>
          <td>parameter group 開放項（部分鎖死）</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自設 Sentinel / Cluster</td>
          <td>Multi-AZ 自動（client 要會重連）</td>
      </tr>
      <tr>
          <td>成本</td>
          <td>機器 + 人力運維</td>
          <td>node 費 + managed premium（省人力）</td>
      </tr>
      <tr>
          <td>控制權</td>
          <td>完全</td>
          <td>受 AWS 邊界限制</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>要極致控制 / 跨雲 / 特殊 config</td>
          <td>AWS 生態 / 要減運維 / 可預測 SLA</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：在 AWS 生態、要把運維交出去、能接受 config 自由度收窄 → 遷 ElastiCache（engine 不變、Type C 低風險）；要極致控制 / 跨雲 / 依賴特殊 config → 留自管。engine 選 Valkey 省約 20%。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>self-managed → ElastiCache 是運維轉移，它跟 managed 邊界與 engine 調校交織：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界 deep article</a></strong>：遷過去後哪些 AWS 管、哪些仍 application 端管，是這個遷移的核心後果。</li>
<li><strong>跟 <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 走的邊界">Redis Sentinel failover</a></strong>：自管 failover（Sentinel）→ managed failover（Multi-AZ），client 重連邏輯要重驗。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></strong>：ElastiCache default engine 是 Valkey，自管 Redis 遷 ElastiCache for Valkey 是「換授權 + 轉 managed」一次到位（見 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/" data-link-title="Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移" data-link-desc="Valkey 是 Redis 7.2.4 的 fork，bit-for-bit 幾乎同源、RDB/AOF 檔案相容、client 一行不改——這是技術上最容易的 cache 遷移。真正的工作不在搬資料，在授權合規驗證與 fork 後分歧（Redis 7.4&#43; 功能、Stack 商業 module）的盤點。本文走 Type B drop-in、相容性 audit 前置、5 個把『最容易的遷移』寫成事故的踩坑">Redis → Valkey 遷移</a>）。</li>
<li><strong>跟<a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">能力級買 vs 建</a></strong>：自管 vs managed 的上層取捨見該章，本文是「決定買（managed）之後」的遷移執行。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source 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> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（自管）</li>
<li>Target 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>對應 deep article：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界</a></li>
<li>相關 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/" data-link-title="Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移" data-link-desc="Valkey 是 Redis 7.2.4 的 fork，bit-for-bit 幾乎同源、RDB/AOF 檔案相容、client 一行不改——這是技術上最容易的 cache 遷移。真正的工作不在搬資料，在授權合規驗證與 fork 後分歧（Redis 7.4&#43; 功能、Stack 商業 module）的盤點。本文走 Type B drop-in、相容性 audit 前置、5 個把『最容易的遷移』寫成事故的踩坑">Redis → Valkey</a>（換授權 + 可同時轉 managed）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（Type C operational hybrid）</li>
</ul>
]]></content:encoded></item></channel></rss>