<?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>Dax on Tarragon</title><link>https://tarrragon.github.io/blog/tags/dax/</link><description>Recent content in Dax on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 02 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/dax/index.xml" rel="self" type="application/rss+xml"/><item><title>DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &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>。&lt;/p>&lt;/blockquote>
&lt;p>single-table design 上線後第三個月、PM 提了三個新 query 需求：「依商品分類查訂單」、「依 status 查 user」、「依時間 range 取最近活動」。team 第一反應是加 GSI、結果 GSI 從 1 個變 6 個、cost 跟 latency 一起上升。打開 AWS Cost Explorer 一看、GSI 的 storage + WCU 合計已經超過 base table。這時 team 開始懷疑「single-table 是不是錯了」— 那是 &lt;em>誤判&lt;/em>。GSI 多到 cost 超過 base table 通常是 &lt;em>主 PK 沒設計好&lt;/em>、不是 single-table 錯。本文展開 GSI / LSI 的正確補位、projection 的三型選擇、sparse index、以及 DAX 作為讀峰值補位的觸發條件。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DynamoDB workload 適配判讀（基本 4 軸）&lt;/strong>：PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定 — 判讀軸詳見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>。本文聚焦 GSI / LSI 補位操作層、是 &lt;em>已選 DynamoDB + access pattern 已穩定&lt;/em> 的 schema 設計議題。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制gsi-vs-lsi-的工程差異">核心機制：GSI vs LSI 的工程差異&lt;/h2>
&lt;p>DynamoDB 的兩種 secondary index 解的問題不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>屬性&lt;/th>
 &lt;th>GSI（Global Secondary Index）&lt;/th>
 &lt;th>LSI（Local Secondary Index）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Partition&lt;/td>
 &lt;td>獨立 partition、可選新 PK + SK&lt;/td>
 &lt;td>同 base table partition、同 PK&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>建立時機&lt;/td>
 &lt;td>隨時可加 / 移除&lt;/td>
 &lt;td>只能在 create table 時定義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consistency&lt;/td>
 &lt;td>只支援 eventual read&lt;/td>
 &lt;td>支援 strongly consistent read&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Capacity&lt;/td>
 &lt;td>獨立 RCU/WCU、按 base 主表 write 同步收&lt;/td>
 &lt;td>共享 base table capacity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>數量上限&lt;/td>
 &lt;td>vendor 規格、需 cross-verify AWS doc&lt;/td>
 &lt;td>vendor 規格、需 cross-verify&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適用場景&lt;/td>
 &lt;td>跨 PK 查詢、需求變動&lt;/td>
 &lt;td>同 PK 內不同 SK + 需 strong read&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「LSI 數量上限 5 個」、「GSI 數量上限 20」這些具體數字屬 vendor 規格、需在實作時 cross-verify AWS doc 當前數字、本文 case（Disney+ / Capcom / Lemino）沒揭露具體 index 數量。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB</a> overview 的 implementation-layer deep article。寫作參照 <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>。</p></blockquote>
<p>single-table design 上線後第三個月、PM 提了三個新 query 需求：「依商品分類查訂單」、「依 status 查 user」、「依時間 range 取最近活動」。team 第一反應是加 GSI、結果 GSI 從 1 個變 6 個、cost 跟 latency 一起上升。打開 AWS Cost Explorer 一看、GSI 的 storage + WCU 合計已經超過 base table。這時 team 開始懷疑「single-table 是不是錯了」— 那是 <em>誤判</em>。GSI 多到 cost 超過 base table 通常是 <em>主 PK 沒設計好</em>、不是 single-table 錯。本文展開 GSI / LSI 的正確補位、projection 的三型選擇、sparse index、以及 DAX 作為讀峰值補位的觸發條件。</p>
<blockquote>
<p><strong>DynamoDB workload 適配判讀（基本 4 軸）</strong>：PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定 — 判讀軸詳見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>。本文聚焦 GSI / LSI 補位操作層、是 <em>已選 DynamoDB + access pattern 已穩定</em> 的 schema 設計議題。</p></blockquote>
<h2 id="核心機制gsi-vs-lsi-的工程差異">核心機制：GSI vs LSI 的工程差異</h2>
<p>DynamoDB 的兩種 secondary index 解的問題不同：</p>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>GSI（Global Secondary Index）</th>
          <th>LSI（Local Secondary Index）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Partition</td>
          <td>獨立 partition、可選新 PK + SK</td>
          <td>同 base table partition、同 PK</td>
      </tr>
      <tr>
          <td>建立時機</td>
          <td>隨時可加 / 移除</td>
          <td>只能在 create table 時定義</td>
      </tr>
      <tr>
          <td>Consistency</td>
          <td>只支援 eventual read</td>
          <td>支援 strongly consistent read</td>
      </tr>
      <tr>
          <td>Capacity</td>
          <td>獨立 RCU/WCU、按 base 主表 write 同步收</td>
          <td>共享 base table capacity</td>
      </tr>
      <tr>
          <td>數量上限</td>
          <td>vendor 規格、需 cross-verify AWS doc</td>
          <td>vendor 規格、需 cross-verify</td>
      </tr>
      <tr>
          <td>適用場景</td>
          <td>跨 PK 查詢、需求變動</td>
          <td>同 PK 內不同 SK + 需 strong read</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>Scope warning</strong>：「LSI 數量上限 5 個」、「GSI 數量上限 20」這些具體數字屬 vendor 規格、需在實作時 cross-verify AWS doc 當前數字、本文 case（Disney+ / Capcom / Lemino）沒揭露具體 index 數量。</p></blockquote>
<p><strong>Projection type</strong> 決定 GSI 儲存哪些 attribute：</p>
<ul>
<li><code>KEYS_ONLY</code>：只存 PK + SK + base key、最省 storage、但讀取後通常還要回 base table 撈 attribute</li>
<li><code>INCLUDE</code>：除了 key、再存指定的 attribute；常用 sweet spot、storage 跟 query 效率平衡</li>
<li><code>ALL</code>：複製 base table 所有 attribute；最方便、最貴</li>
</ul>
<p>讀路徑差異：</p>
<ul>
<li>GSI eventual read：跨 partition、不支援 strong；base table write → GSI replication 通常 &lt; 1s 但無 SLA</li>
<li>LSI strong read：同 partition quorum 內成立、read-your-write 場景適用</li>
</ul>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a>、<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>。</p>
<h2 id="dax-作為讀峰值補位">DAX 作為讀峰值補位</h2>
<p>DAX（DynamoDB Accelerator）不是 GSI / LSI 同層方案、不是 DynamoDB 預設配置、是「讀峰值持續高時的補位」。寫進你的設計前先看觸發條件：</p>
<p><strong><code>9.C29 Lemino</code> 揭露</strong>（case fact）：「DAX 是 DynamoDB 讀 cache 的標準解法」、觸發條件是「當讀峰值持續高、加 DAX 減少 DynamoDB 讀次數、降低成本」（熱門節目首播時段、共用 metadata）。Lemino 是 case 直接揭露使用 DAX。</p>
<p><strong><code>9.C19 Capcom</code> 是判讀層 derive、不是 case fact</strong>：原 finding 從「single-digit ms」latency 反推 Capcom 必須用 sub-region cache + DynamoDB DAX、不能單靠 DynamoDB；但 <code>9.C19</code> case <em>沒有公開揭露</em> 使用 DAX。引用 Capcom 時要明示「DAX 是作者判讀層推論、Capcom 沒公開使用」、避免把推論寫成 case 揭露。</p>
<p><strong>跟 GSI / LSI 的職責分離</strong>：</p>
<ul>
<li>GSI / LSI 解「無法用主 PK 查」的問題（access pattern 補位）</li>
<li>DAX 解「同 query 重複打 DynamoDB 太貴或太慢」的問題（讀路徑加速）</li>
<li>兩者不互斥、但解不同問題；不要把 DAX 當 GSI 替代品</li>
</ul>
<p><strong>DAX 適用觸發條件</strong>：</p>
<ul>
<li>讀峰值持續高（熱門節目 / 共用 leaderboard / 全平台共享 metadata / read:write ratio &gt; 10:1）</li>
<li>cache 命中率可預期高（重複讀同一組 key）</li>
</ul>
<p><strong>DAX 不適用情境</strong>：</p>
<ul>
<li>寫密集 workload（cache invalidation 開銷 &gt; cache 收益）</li>
<li>每次讀都不同 key（cache hit rate &lt; 30%、加 DAX 等於白花錢）</li>
<li>read-your-write 場景（DAX 仍是 eventual cache、staleness 視 cache TTL 而定）</li>
</ul>
<h2 id="設計流程">設計流程</h2>
<p>從 access pattern 補位到 DAX 評估的 6 步流程。</p>
<h4 id="step-1標記最小成本路徑">Step 1：標記最小成本路徑</h4>
<p>每個 access pattern 標記能用最便宜路徑解：</p>
<ul>
<li>能用主表 PK/SK 直接 <code>GetItem</code> / <code>Query</code> → 主表（最便宜）</li>
<li>同 PK 內不同 SK 排序 + 需要 strong read → LSI（同 partition、strong）</li>
<li>跨 PK 或 base table 已建好 → GSI（額外 storage + WCU）</li>
</ul>
<h4 id="step-2選-lsi-還是-gsi">Step 2：選 LSI 還是 GSI</h4>
<p>LSI 只能在 create table 時定義、不能後加。team 經常踩雷：上線後想加 strongly consistent 索引、發現只能重建 table。建 table 前列完 access pattern、不確定走 GSI 不走 LSI 是保守選擇（GSI 隨時可加可移）。</p>
<h4 id="step-3projection-設計">Step 3：projection 設計</h4>
<p>每個 GSI 單獨設 projection、不要全用 <code>ALL</code>：</p>
<ul>
<li>query 只要回 key → <code>KEYS_ONLY</code></li>
<li>query 需要常見 3-5 個欄位 → <code>INCLUDE</code>（列出實際 column、storage 跟 query 效率平衡）</li>
<li>用 GSI 直接顯示資料（不回 base table） → <code>ALL</code>（storage 跟 WCU 都翻倍、慎用）</li>
</ul>
<h4 id="step-4sparse-index-pattern">Step 4：sparse index pattern</h4>
<p>GSI PK 只在某 attribute 存在時填、自動「只索引子集」、節省 storage：</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="k">def</span> <span class="nf">write_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">status</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">item</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ORDER#</span><span class="si">{</span><span class="n">order_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">,</span> <span class="s2">&#34;status&#34;</span><span class="p">:</span> <span class="n">status</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1"># sparse index: 只有 active order 進 GSI</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">if</span> <span class="n">status</span> <span class="o">==</span> <span class="s2">&#34;active&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">item</span><span class="p">[</span><span class="s2">&#34;GSI1PK&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;STATUS#active&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">item</span><span class="p">[</span><span class="s2">&#34;GSI1SK&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">order_id</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">table</span><span class="o">.</span><span class="n">put_item</span><span class="p">(</span><span class="n">Item</span><span class="o">=</span><span class="n">item</span><span class="p">)</span></span></span></code></pre></div><p>GSI1 只索引 active order、archive order 不進 GSI。當 active order 是 10%、storage 節省約 90%。</p>
<blockquote>
<p><strong>Scope warning</strong>：「50-90% storage 節省」具體節省比例屬通用工程估算、依 active subset 比例變動、case 未揭露 sparse index 具體數字。</p></blockquote>
<h4 id="step-5驗證點">Step 5：驗證點</h4>





<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="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">query</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">KeyConditionExpression</span><span class="o">=</span><span class="n">Key</span><span class="p">(</span><span class="s2">&#34;GSI1PK&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">eq</span><span class="p">(</span><span class="s2">&#34;STATUS#active&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">IndexName</span><span class="o">=</span><span class="s2">&#34;GSI1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ReturnConsumedCapacity</span><span class="o">=</span><span class="s2">&#34;INDEXES&#34;</span>  <span class="c1"># 看每個 query 走 GSI 還是主表</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;ConsumedCapacity&#34;</span><span class="p">])</span></span></span></code></pre></div><p>CloudWatch GSI metric：看每個 GSI 的 WCU usage 跟主表的比例；GSI WCU &gt; base table WCU 通常是設計訊號。</p>
<h4 id="step-6dax-評估">Step 6：DAX 評估</h4>
<p>讀峰值持續高 + cache hit rate 可預期、才加 DAX；不要把 DAX 當預設配置（Lemino 揭露的觸發條件）。先觀察 base 路徑的 read pattern、判斷 cache hit rate 預期值、再決定加 DAX。</p>
<p><strong>Rollback boundary</strong>：GSI 可隨時刪、但 deletion 是 async 且不可逆；建議先 application 切回 base table query、觀察 1 週再刪 GSI。DAX 可隨時 detach、application 端把 DAX endpoint 換回 DynamoDB endpoint 即可。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>7 個 production 常見踩雷：</p>
<h4 id="case-1gsi-寫入-throttle-拖累主表-write">Case 1：GSI 寫入 throttle 拖累主表 write</h4>
<p>GSI 用了集中型 PK（如 <code>STATUS#active</code> 所有 active order 集中）、單 partition 上限 1000 WCU 撞牆、GSI replication 失敗、主表 write retry、整體 latency 上升。修法：GSI PK 設計獨立 review、不可繼承主表 PK 的均勻假設（base PK 均勻 ≠ GSI PK 均勻）；GSI PK 也要做 <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition key 均勻度判讀</a>。</p>
<h4 id="case-2gsi-eventual-read-餵錯資料">Case 2：GSI eventual read 餵錯資料</h4>
<p>application 用 GSI 讀「user 最新 status」、code 假設 strong 一致；實際 100-500ms staleness 導致 UI 顯示舊狀態。修法：read-your-write 場景改回主表 query（主表支援 strong）、或加 application-side write-through cache。</p>
<blockquote>
<p><strong>Scope warning</strong>：「100-500ms staleness」具體數字屬通用工程估算、case 未揭露 GSI replication latency 具體 p99 數字。</p></blockquote>
<h4 id="case-3projection-all-把-cost-翻倍">Case 3：projection ALL 把 cost 翻倍</h4>
<p>圖省事所有 GSI 用 <code>ALL</code>、實際 query 只需要 3 個 column；storage + WCU 都浪費。修法：每個 GSI 單獨設 projection、<code>INCLUDE</code> 列出實際 column；只在「用 GSI 直接顯示資料、不回主表」場景才用 <code>ALL</code>。</p>
<blockquote>
<p><strong>Scope warning</strong>：「cost 翻 3 倍」具體數字屬通用工程估算、case 未揭露具體 cost ratio。</p></blockquote>
<h4 id="case-4lsi-用完了才發現要的是-gsi">Case 4：LSI 用完了才發現要的是 GSI</h4>
<p>LSI 上限受 vendor 規格限制（建議 cross-verify AWS doc 當前數字）且建 table 時定、半年後想加 strongly consistent 索引發現要重建 table。修法：建 table 前列完 access pattern、不確定就走 GSI（隨時可加可移）；LSI 留給「明確需要同 PK + strong read」場景。</p>
<h4 id="case-5gsi-反向-scan-取代-query">Case 5：GSI 反向 scan 取代 query</h4>
<p>application 用 GSI 做 <code>Scan</code> 而非 <code>Query</code>、全 GSI 掃過去、cost 跟 latency 都炸。修法：<code>Scan</code> 是 <em>程式碼錯誤訊號</em>、不是 capacity 不夠；review code 看 GSI 為什麼沒被當 query 路徑用、通常是 GSI PK 設計沒對齊 access pattern。</p>
<h4 id="case-6把-dax-當預設配置">Case 6：把 DAX 當預設配置</h4>
<p>寫密集 workload / cache hit rate 低的場景加 DAX、cache invalidation 成本超過 cache 收益、cost 上升 latency 沒降。修法：DAX 是「讀峰值持續高」的補位、不是預設（Lemino 揭露的觸發條件、Capcom 是 derive 不是 case fact）；先觀察 read pattern + 評估 cache hit rate 預期、再決定。</p>
<h4 id="case-7gsi-capacity-mode-跟-base-table-不一致">Case 7：GSI capacity mode 跟 base table 不一致</h4>
<p>GSI 的 capacity mode 跟 base table 是 <em>獨立</em> 設定、不會自動繼承 — base table 是 provisioned + auto-scaling、開新 GSI 預設仍是 provisioned 但 WCU / RCU 預設值跟 base table 不同步、或誤把某個 GSI 切 on-demand 而 base table 維持 provisioned、實際 production 寫入 throttle / 成本失衡都會出現。屬通用工程議題、case 未直接揭露具體 mode 錯配狀況。</p>
<p>徵兆：</p>
<ul>
<li>Base table <code>ConsumedWriteCapacityUnits</code> 健康、卻看到 GSI <code>WriteThrottleEvents</code> 持續觸發、application 端寫入 latency p99 拉高</li>
<li>GSI 切 on-demand 後成本「不知為何」翻 X 倍、查 Cost Explorer 才發現 GSI WCU 計費跟 base table 的 provisioned 是完全不同帳單路徑</li>
<li>Auto-scaling policy 只設了 base table、GSI 沒設、流量上來時 base table 自動擴、GSI 卻 throttle</li>
</ul>
<p>修法：</p>
<ul>
<li>建 GSI 時把 capacity mode 當成獨立決策、不要假設「base 怎麼設、GSI 跟著走」</li>
<li>流量穩定 workload 同時把 base + GSI 都設 provisioned + auto-scaling、auto-scaling target 對齊</li>
<li>Spiky workload 改 on-demand 時整批切（base table + 全部 GSI 同時切）、避免單側切換造成 partial throttle</li>
<li>CloudWatch alarm 對每個 GSI 獨立設 <code>WriteThrottleEvents</code> / <code>ReadThrottleEvents</code>、不要只盯 base table</li>
<li>詳細 mode 切換時機看 sibling <a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/" data-link-title="DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover" data-link-desc="capacity mode 選擇不是單軸 peak/avg ratio；本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 暫時 vs 永久 baseline / predictable-peak vs flash-sale / DBA 工時釋放 / vendor vs 自管 cost crossover），含 Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload 等 case 分軸 anchor">on-demand vs provisioned</a></li>
</ul>
<p><strong>Anti-recommendation</strong>：access pattern &lt; 3 個、主表 PK 已能覆蓋 → 不要預先建 GSI；GSI 從少到多容易、從多到少要 application 端配合 cutover。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li>每個 GSI 獨立 <code>ConsumedReadCapacityUnits</code> / <code>ConsumedWriteCapacityUnits</code></li>
<li><code>ReplicationLatency</code>：GSI async replication 延遲、p99 通常 &lt; 1s（無 SLA）</li>
<li>DAX：<code>CacheHits</code> / <code>CacheMisses</code> / <code>CacheHitRate</code>、<code>ItemCacheHits</code> / <code>QueryCacheHits</code></li>
</ul>
<p><code>ReturnConsumedCapacity</code> flag：query 時帶 <code>INDEXES</code> 看 GSI consumption；<code>TOTAL</code> 看 base + GSI 合計、debug 時切換用。</p>
<p><strong>Cost monitoring</strong>：</p>
<ul>
<li>每個 GSI 都重複收 storage + WCU；GSI 多時 cost 容易超過 base table</li>
<li>用 AWS Cost Explorer 按 GSI 維度看、不是只看 table-level 總 cost</li>
<li>DAX cost 是 instance-hour 計、不是 per-request；只在 read peak 持續高才划算</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「GSI 多時 cost 超過 base table」屬通用工程知識、<code>9.C27 Disney+</code> / <code>9.C19 Capcom</code> case 沒揭露具體 GSI cost ratio。</p></blockquote>
<p><strong>DAX 觀測重點</strong>（新增）：</p>
<ul>
<li><code>CacheHitRate</code> &lt; 70% 應重新評估 DAX 是否該存在</li>
<li>cache size utilization 看 DAX instance class 是否足夠</li>
<li>觀察 cache miss 後 fallback 到 DynamoDB 的 latency、確認 DAX 真的減少 base 路徑壓力</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「70% hit rate 閾值」屬通用工程估算、case 未揭露具體閾值。</p></blockquote>
<p>接回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 的 NoSQL index cost section、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="disney--capcom-的-access-pattern-對照">Disney+ / Capcom 的 access pattern 對照</h3>
<p><code>9.C27 Disney+</code> 跟 <code>9.C19 Capcom</code> 是兩種 GSI 用法：</p>
<ul>
<li>Disney+ watchlist + 播放進度 + cross-device sync 全用主表 + 少量 GSI、避免 GSI 爆炸；cross-device sync 透過 <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">Global Tables</a> 處理、不是 GSI</li>
<li>Capcom 玩家 leaderboard / 戰績用 GSI 反向查詢（跨遊戲共用平台、player_id 為 base PK、game_id 為 GSI PK）；leaderboard 是否該走 GSI 還是 Redis sorted set 是另一個取捨</li>
</ul>
<p>兩個 case 都 <em>沒有公開揭露</em> 具體 GSI 數量、projection 配置、DAX 是否使用。引用 case 時要分層 — 概念是 case 揭露、實作數字是通用工程估算。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — GSI 是 single-table 沒覆蓋的 access pattern 補位</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — GSI 自己也會 hot partition、GSI PK 設計獨立 review</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — GSI 強制 eventual、對應 consistency 軸</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/" data-link-title="DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover" data-link-desc="capacity mode 選擇不是單軸 peak/avg ratio；本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 暫時 vs 永久 baseline / predictable-peak vs flash-sale / DBA 工時釋放 / vendor vs 自管 cost crossover），含 Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload 等 case 分軸 anchor">on-demand-vs-provisioned</a> — GSI 多時 cost 跟 mode 互動</li>
<li>替代路由：access pattern 變動頻繁 → 考慮 OpenSearch / Aurora、單純 search 不要拿 GSI 當 inverted index</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom 9.C19</a> 互引：leaderboard 用 GSI vs Redis sorted set 的選擇；DAX 是 derive 不是 case fact、引用要明示</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino 9.C29</a> 互引：DAX 作為讀峰值補位的 case 揭露</li>
</ul>
]]></content:encoded></item><item><title>DynamoDB DAX 快取策略：cluster 架構、item/query cache、write-through 與 invalidation 邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/dax-caching-strategy/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/dax-caching-strategy/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &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>。&lt;/p>&lt;/blockquote>
&lt;p>熱門節目首播時段、application 對同一批 metadata item 的讀取 latency p99 從 5ms 尖到 40ms、下游 timeout 連鎖。team 加了 DAX、p99 壓回個位數毫秒。三個月後另一個 service 也「照抄」加 DAX、結果 cost 上升、latency 沒降 — 那個 service 是寫密集、每次讀的 key 都不同、cache hit rate 不到 20%。同一個工具、在一個 workload 壓回 p99 延遲、在另一個只增加成本卻不降延遲。DAX 的價值取決於 read pattern 跟一致性需求是否匹配。本文展開 DAX 的 cluster 架構、兩種快取的不同失效語意、以及 write-through 跟 strongly consistent read 的邊界。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DAX 觸發條件 SSoT&lt;/strong>：DAX 「該不該存在」的觸發條件（讀峰值持續高 / cache hit rate 可預期 / read:write ratio 高）主寫於 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design 的 DAX 段&lt;/a>、含 &lt;code>9.C29 Lemino&lt;/code> case fact 跟 &lt;code>9.C19 Capcom&lt;/code> derive 分層。本文承接「已決定要用 DAX」之後的機制、配置與失效邊界、不重複展開觸發判讀。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制dax-cluster-與兩種快取">核心機制：DAX cluster 與兩種快取&lt;/h2>
&lt;p>DAX（DynamoDB Accelerator）是 DynamoDB 前面的 in-memory write-through cache、提供 microsecond 級讀取（DynamoDB 本身是 single-digit ms）。它 API 相容 — application 把 DynamoDB client 換成 DAX client、API call 不變、讀寫自動經過 cache 層。&lt;/p>
&lt;p>&lt;strong>cluster 拓樸&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>一個 DAX cluster 由多個 node 組成、一個 primary（接受寫）+ 多個 read replica&lt;/li>
&lt;li>跨多 AZ 部署、primary 故障時 replica 接手&lt;/li>
&lt;li>application 透過 DAX endpoint 連 cluster、SDK 自動分散讀取到 replica&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>兩種快取、不同生命週期&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB</a> overview 的 implementation-layer deep article。寫作參照 <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>。</p></blockquote>
<p>熱門節目首播時段、application 對同一批 metadata item 的讀取 latency p99 從 5ms 尖到 40ms、下游 timeout 連鎖。team 加了 DAX、p99 壓回個位數毫秒。三個月後另一個 service 也「照抄」加 DAX、結果 cost 上升、latency 沒降 — 那個 service 是寫密集、每次讀的 key 都不同、cache hit rate 不到 20%。同一個工具、在一個 workload 壓回 p99 延遲、在另一個只增加成本卻不降延遲。DAX 的價值取決於 read pattern 跟一致性需求是否匹配。本文展開 DAX 的 cluster 架構、兩種快取的不同失效語意、以及 write-through 跟 strongly consistent read 的邊界。</p>
<blockquote>
<p><strong>DAX 觸發條件 SSoT</strong>：DAX 「該不該存在」的觸發條件（讀峰值持續高 / cache hit rate 可預期 / read:write ratio 高）主寫於 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design 的 DAX 段</a>、含 <code>9.C29 Lemino</code> case fact 跟 <code>9.C19 Capcom</code> derive 分層。本文承接「已決定要用 DAX」之後的機制、配置與失效邊界、不重複展開觸發判讀。</p></blockquote>
<h2 id="核心機制dax-cluster-與兩種快取">核心機制：DAX cluster 與兩種快取</h2>
<p>DAX（DynamoDB Accelerator）是 DynamoDB 前面的 in-memory write-through cache、提供 microsecond 級讀取（DynamoDB 本身是 single-digit ms）。它 API 相容 — application 把 DynamoDB client 換成 DAX client、API call 不變、讀寫自動經過 cache 層。</p>
<p><strong>cluster 拓樸</strong>：</p>
<ul>
<li>一個 DAX cluster 由多個 node 組成、一個 primary（接受寫）+ 多個 read replica</li>
<li>跨多 AZ 部署、primary 故障時 replica 接手</li>
<li>application 透過 DAX endpoint 連 cluster、SDK 自動分散讀取到 replica</li>
</ul>
<p><strong>兩種快取、不同生命週期</strong>：</p>
<table>
  <thead>
      <tr>
          <th>快取類型</th>
          <th>內容</th>
          <th>寫入如何影響</th>
          <th>失效方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Item cache</td>
          <td><code>GetItem</code> / <code>BatchGetItem</code> 的單筆結果</td>
          <td>write-through 寫入時同步更新對應 item</td>
          <td>item TTL + write-through</td>
      </tr>
      <tr>
          <td>Query cache</td>
          <td><code>Query</code> / <code>Scan</code> 的結果集</td>
          <td>單筆 write <em>不會</em> 失效對應 query 結果集</td>
          <td>只靠 query TTL</td>
      </tr>
  </tbody>
</table>
<p>這張表的第二列是 DAX 最常被誤解的點：<strong>query cache 不會因為底層某筆 item 被改而失效</strong>。item cache 走 write-through、寫入時會更新；但 query cache 存的是「整個結果集」、DAX 無法知道某筆新寫入是否該進某個已快取的 query 結果、所以 query cache 只靠 TTL 過期。這代表 query 結果可能 stale 到一個 TTL 週期。</p>
<blockquote>
<p><strong>Scope warning</strong>：「item cache 預設 TTL 5 分鐘」、「query cache 預設 TTL 5 分鐘」這些預設值屬 AWS vendor 規格、可在 cluster 設定調整、實作時 cross-verify 官方 doc。本文不含 production case 揭露的 DAX TTL 配置數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache-invalidation</a>、<a href="/blog/backend/knowledge-cards/write-through-cache/" data-link-title="Write-Through Cache" data-link-desc="說明寫入時同步更新快取與正式來源的策略">write-through-cache</a>、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl</a>、<a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache-hit-rate</a>。</p>
<h2 id="一致性與-invalidation-邊界">一致性與 invalidation 邊界</h2>
<p>DAX 的一致性語意是它跟「一般 cache-aside」最大的差別、也是踩雷集中區。</p>
<p><strong>write-through 的保證範圍</strong>：</p>
<p>寫入經過 DAX 時、DAX 先寫 DynamoDB、成功後更新自己的 item cache。所以「寫完馬上用 <code>GetItem</code> 讀同一筆」、在 <em>同一個 DAX node</em> 上能讀到新值。但這不是 strong consistency — 多 node cluster 下、寫入只更新 primary 與被路由到的 node、其他 read replica 的 item cache 仍可能 stale 到 TTL。</p>
<p><strong>strongly consistent read 繞過 cache</strong>：</p>
<p>DAX 只服務 eventually consistent read。application 若要求 strongly consistent read（<code>ConsistentRead=True</code>）、DAX 直接 pass through 到 DynamoDB、不經 cache、也享受不到 microsecond latency。這是設計上的取捨 — DAX 換 latency 的代價是放棄 strong consistency。read-your-write 嚴格場景不能靠 DAX。</p>
<p><strong>query cache stale 的真實後果</strong>：</p>
<p>application 用 <code>Query</code> 列「某 user 的 active order」、結果被 query cache 快取；user 新建一筆 order、item cache 更新了該筆 item、但 <em>列表 query 的 cache 沒失效</em>、user 重整頁面在 TTL 內看不到新訂單。修法不是調 DAX、是判斷「這個 query 能不能接受 TTL 內 stale」— 不能接受的、該 query 不要走 DAX（直接打 DynamoDB）、或縮短該類 query 的 TTL。</p>
<blockquote>
<p><strong>Scope warning</strong>：上述一致性語意屬 DAX vendor 規格 + 通用 cache 工程知識、非 production case 揭露；實際 staleness 視 cluster node 數、TTL 配置與讀寫分布而定。</p></blockquote>
<h2 id="操作流程">操作流程</h2>
<p>從 read pattern 評估到上線的 6 步流程。</p>
<h4 id="step-1確認-read-pattern-適配">Step 1：確認 read pattern 適配</h4>
<p>在加 DAX 前、用 CloudWatch 看目標 table 的 read:write ratio 跟 read 的 key 重複度：</p>
<ul>
<li>read:write 高（讀遠多於寫）+ 重複讀同一組 key → 適合</li>
<li>寫密集 / 每次讀不同 key / 大量 strongly consistent read → 不適合（回頭看 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design DAX 觸發條件</a>）</li>
</ul>
<h4 id="step-2cluster-sizing">Step 2：cluster sizing</h4>





<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">node 數 = 讀峰值 throughput / 單 node 容量 + 1（容錯餘量）
</span></span><span class="line"><span class="ln">2</span><span class="cl">node class = 依 working set 大小選（cache 要能裝下熱資料）</span></span></code></pre></div><p>跨至少 2 個 AZ、確保 primary 故障有 replica 接手。</p>
<h4 id="step-3application-切換-client">Step 3：application 切換 client</h4>





<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">amazondax</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 原本：dynamodb = boto3.resource(&#34;dynamodb&#34;)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">dax</span> <span class="o">=</span> <span class="n">amazondax</span><span class="o">.</span><span class="n">AmazonDaxClient</span><span class="o">.</span><span class="n">resource</span><span class="p">(</span><span class="n">endpoint_url</span><span class="o">=</span><span class="s2">&#34;dax://my-cluster.xxx.dax-clusters.region.amazonaws.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">table</span> <span class="o">=</span> <span class="n">dax</span><span class="o">.</span><span class="n">Table</span><span class="p">(</span><span class="s2">&#34;orders&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># API 不變、讀寫自動經過 DAX</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="s2">&#34;ORDER#123&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">})</span></span></span></code></pre></div><h4 id="step-4分流-strongly-consistent-read">Step 4：分流 strongly consistent read</h4>





<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"># 需要 strong 的讀直接走 DynamoDB、不要走 DAX</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">ddb_table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=...</span><span class="p">,</span> <span class="n">ConsistentRead</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>   <span class="c1"># 繞過 cache</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 可接受 eventual 的讀走 DAX</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">dax_table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=...</span><span class="p">)</span>                          <span class="c1"># 走 cache</span></span></span></code></pre></div><p>application 要明確區分哪些讀路徑能接受 stale、哪些不能；不能接受的不走 DAX。</p>
<h4 id="step-5設定-ttl-與監控-hit-rate">Step 5：設定 TTL 與監控 hit rate</h4>
<p>依資料變動頻率設 item / query cache TTL：變動慢的 metadata 可設長 TTL、變動快的設短或不快取。上線後盯 <code>CacheHitRate</code>。</p>
<h4 id="step-6驗證點">Step 6：驗證點</h4>





<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"># 驗證 hit rate 達預期、確認 DAX 真的減少 DynamoDB 讀</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># CloudWatch: DAX CacheHits / (CacheHits + CacheMisses)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 同時看 DynamoDB ConsumedReadCapacityUnits 是否下降</span></span></span></code></pre></div><p><strong>Rollback boundary</strong>：DAX 可隨時 detach — application 端把 DAX endpoint 換回 DynamoDB endpoint 即可、無資料遷移；DAX 只是讀路徑加速層、不持有唯一資料。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1把-dax-當預設配置">Case 1：把 DAX 當預設配置</h4>
<p>寫密集 / 低 hit rate workload 加 DAX、invalidation 開銷 + cluster 成本 &gt; cache 收益。修法：先確認 read pattern 適配（Step 1）、DAX 是讀峰值補位不是預設（觸發條件 SSoT 在 gsi-lsi-design）。</p>
<h4 id="case-2以為-query-cache-會即時反映寫入">Case 2：以為 query cache 會即時反映寫入</h4>
<p>寫入後列表 query 在 TTL 內看不到新資料、被當成 bug 長時間誤查。修法：理解 query cache 只靠 TTL 失效（不是 bug 是設計）；強一致列表需求的 query 不走 DAX、或縮短 TTL。</p>
<h4 id="case-3strongly-consistent-read-全走-dax-還抱怨不快">Case 3：strongly consistent read 全走 DAX 還抱怨不快</h4>
<p>application 全程 <code>ConsistentRead=True</code>、DAX 全部 pass through、等於沒裝 DAX 還多付 cluster 錢。修法：分流 — strong read 直接打 DynamoDB、eventual read 才走 DAX。</p>
<h4 id="case-4cluster-單-az--單-node">Case 4：cluster 單 AZ / 單 node</h4>
<p>省成本只開單 node、primary 故障時讀路徑整個失效、回退到 DynamoDB 瞬間流量尖峰。修法：跨 2+ AZ、primary + replica；DAX 故障的 fallback 路徑（直連 DynamoDB）要先測過。這個 Case 的失敗代價跟其他 Case 不對稱 — 其餘 Case 多是成本浪費或延遲沒降、detach DAX 即可回復；單 AZ / 單 node 故障是讀路徑硬中斷、回退瞬間把原本被 cache 吸收的讀峰值全打回 DynamoDB、若 base table 的 RCU 或 on-demand burst 餘量沒預留、會引發 throttling 連鎖。回退路徑要按「DAX 全失效時的讀峰值」預估 DynamoDB 側容量、而非平時被 cache 削減後的讀量。</p>
<h4 id="case-5working-set-超過-cache-容量">Case 5：working set 超過 cache 容量</h4>
<p>熱資料超過 node memory、cache 不斷 evict、hit rate 掉到沒意義。修法：依 working set 選 node class、或縮小快取範圍（只快取真正熱的 access pattern）。</p>
<p><strong>Anti-recommendation</strong>：read:write ratio 低、或 cache hit rate 預期 &lt; 50% 的 workload、不要上 DAX；application 端的 request-level cache 或根本不快取可能更划算。DAX 是 cluster 常駐成本（instance-hour 計）、只在讀峰值持續高才回本。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>CacheHits</code> / <code>CacheMisses</code> / 算出 <code>CacheHitRate</code> — 核心健康指標</li>
<li><code>ItemCacheHits</code> / <code>QueryCacheHits</code> — 分辨兩種快取各自的命中</li>
<li><code>CPUUtilization</code> / <code>EvictedSize</code> — node 是否過載、cache 是否頻繁 evict</li>
<li>DynamoDB 端 <code>ConsumedReadCapacityUnits</code> — 確認 DAX 真的削減了 base 讀取</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>CacheHitRate</code> &lt; 70% — 重新評估 DAX 是否該存在、或快取範圍是否該收窄</li>
<li><code>EvictedSize</code> 持續高 — working set 超過 cache 容量、要加大 node class</li>
<li>DynamoDB read capacity 沒因 DAX 下降 — read pattern 不適配、DAX 沒發揮作用</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「70% hit rate 閾值」屬通用工程估算、非 case 揭露；實際閾值依 cost 結構與 latency 目標調整。</p></blockquote>
<p>接回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="dax-vs-application-side-cache-vs-elasticache">DAX vs application-side cache vs ElastiCache</h3>
<p>DAX 不是唯一的 DynamoDB 讀加速方案。三者責任不同：</p>
<ul>
<li><strong>DAX</strong>：DynamoDB 專屬、API 相容、write-through、零 application cache 邏輯；綁 DynamoDB</li>
<li><strong>application-side cache</strong>（如 in-process LRU）：最低延遲、但每個 instance 各自一份、一致性難管</li>
<li><strong>ElastiCache（Redis / Valkey）</strong>：通用 cache、可跨資料源、但要自己寫 cache-aside 邏輯與 invalidation</li>
</ul>
<p>當快取需求超出單一 DynamoDB table（跨資料源聚合 / 需要 Redis 資料結構如 sorted set leaderboard）、回 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 評估 ElastiCache；DAX 最適配的情境是「純 DynamoDB 讀加速、且不想自行維護 cache 邏輯」。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a> — DAX 觸發條件 SSoT（讀峰值補位 / Lemino case fact / Capcom derive）在該篇、本篇承接機制層</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/" data-link-title="DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover" data-link-desc="capacity mode 選擇不是單軸 peak/avg ratio；本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 暫時 vs 永久 baseline / predictable-peak vs flash-sale / DBA 工時釋放 / vendor vs 自管 cost crossover），含 Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload 等 case 分軸 anchor">on-demand-vs-provisioned</a> — DAX 削減 base 讀取後、provisioned RCU 規劃要重算</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — strongly consistent read 繞過 DAX、對應 read 一致性軸</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — DAX 不解 hot partition、寫熱點仍打到 DynamoDB</li>
<li>替代路由：跨資料源快取 / Redis 資料結構需求 → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> ElastiCache</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino 9.C29</a> 互引：DAX 讀峰值補位的 case fact</li>
</ul>
]]></content:encoded></item></channel></rss>