<?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>Write-Sharding on Tarragon</title><link>https://tarrragon.github.io/blog/tags/write-sharding/</link><description>Recent content in Write-Sharding on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 27 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/write-sharding/index.xml" rel="self" type="application/rss+xml"/><item><title>DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/</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>售票網站開賣前一小時把 DynamoDB capacity 從 200 WCU 拉到 5000、心想「容量加 25 倍應該夠」。開賣瞬間還是看到 &lt;code>ThrottledRequests&lt;/code> 拉警報、CloudWatch 顯示總 capacity 才用了 1500 WCU。打開 partition-level metric 才看到某一個 partition 已經達到 1000 WCU 上限、其他 partition 閒置 — &lt;code>event_id&lt;/code> 當 PK、單一熱門場次把所有寫入集中到同一個 partition。Capacity 加再多都救不了，因為單 partition 上限是 1000 WCU / 3000 RCU、跟 table 總容量無關。這就是 hot partition 的本質：partition key 設計問題、不是 capacity 不夠。&lt;/p>
&lt;p>本文展開 partition key 反模式的識別、composite key / write sharding 兩種修法、mode × partition 在 provisioned / on-demand 下的不同表現、以及 9.C15 拓元 6750x IOPS 擴展案例的工程細節。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DynamoDB 適用度前置判讀&lt;/strong>：本篇假設 workload 已通過 DynamoDB 適用度 4 軸（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>、本篇不重複展開。Partition key 反模式是 &lt;em>已選 DynamoDB 後&lt;/em> 的 schema 修補議題；若 4 軸不成立、改回 SQL 比補 composite key 更合理。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>跨 vendor 可逆性對照 SSoT&lt;/strong>：MongoDB / DynamoDB / Cosmos DB 三家 partition key 可逆性不在同一光譜（DynamoDB 走 backfill 到新 table、屬中度可逆）、跨 vendor 對照 SSoT 主寫位置在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/db3-vendor-selection/#%e4%b8%89-vendor-%e5%b0%8d%e6%af%94-10-%e8%bb%b8" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &amp;#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">DB3 entry — 三 vendor 對比 10 軸&lt;/a> + 對應的&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/db3-vendor-selection/#%e8%bb%b8%e7%9a%84%e5%bb%b6%e4%bc%b8%e5%ad%90%e6%ae%b5" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &amp;#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">軸的延伸子段&lt;/a>。本篇聚焦 DynamoDB 內部如何識別 partition key 反模式 + composite key / write sharding 修法、不重複跨 vendor 比較。&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>售票網站開賣前一小時把 DynamoDB capacity 從 200 WCU 拉到 5000、心想「容量加 25 倍應該夠」。開賣瞬間還是看到 <code>ThrottledRequests</code> 拉警報、CloudWatch 顯示總 capacity 才用了 1500 WCU。打開 partition-level metric 才看到某一個 partition 已經達到 1000 WCU 上限、其他 partition 閒置 — <code>event_id</code> 當 PK、單一熱門場次把所有寫入集中到同一個 partition。Capacity 加再多都救不了，因為單 partition 上限是 1000 WCU / 3000 RCU、跟 table 總容量無關。這就是 hot partition 的本質：partition key 設計問題、不是 capacity 不夠。</p>
<p>本文展開 partition key 反模式的識別、composite key / write sharding 兩種修法、mode × partition 在 provisioned / on-demand 下的不同表現、以及 9.C15 拓元 6750x IOPS 擴展案例的工程細節。</p>
<blockquote>
<p><strong>DynamoDB 適用度前置判讀</strong>：本篇假設 workload 已通過 DynamoDB 適用度 4 軸（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>、本篇不重複展開。Partition key 反模式是 <em>已選 DynamoDB 後</em> 的 schema 修補議題；若 4 軸不成立、改回 SQL 比補 composite key 更合理。</p></blockquote>
<blockquote>
<p><strong>跨 vendor 可逆性對照 SSoT</strong>：MongoDB / DynamoDB / Cosmos DB 三家 partition key 可逆性不在同一光譜（DynamoDB 走 backfill 到新 table、屬中度可逆）、跨 vendor 對照 SSoT 主寫位置在 <a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e4%b8%89-vendor-%e5%b0%8d%e6%af%94-10-%e8%bb%b8" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">DB3 entry — 三 vendor 對比 10 軸</a> + 對應的<a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e8%bb%b8%e7%9a%84%e5%bb%b6%e4%bc%b8%e5%ad%90%e6%ae%b5" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">軸的延伸子段</a>。本篇聚焦 DynamoDB 內部如何識別 partition key 反模式 + composite key / write sharding 修法、不重複跨 vendor 比較。</p></blockquote>
<h2 id="核心機制partition-上限是工程硬天花板">核心機制：partition 上限是工程硬天花板</h2>
<p>DynamoDB 把 capacity 抽象成 RCU / WCU、但底下仍是物理 partition。理解 partition 的 4 條硬規則：</p>
<ul>
<li><strong>單 partition 上限</strong>：3000 RCU、1000 WCU、10GB storage；超過任一個觸發 partition split</li>
<li><strong>總容量公式</strong>：<code>partition 數量 × 每 partition 上限</code>、partition 數量由 vendor 自動管理</li>
<li><strong>Adaptive Capacity</strong>：跨 partition 重新分配閒置容量、但 <em>單 partition 仍硬上限</em>；不解 single-key 集中</li>
<li><strong>Splitting on heat</strong>：vendor 偵測 hot partition 後自動 split、有分鐘級延遲；突發流量來不及 split 就先 throttle</li>
</ul>
<p><code>9.C5 Amazon Ads</code> 揭露同一 frame：「容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 是工程天花板」。Amazon Ads 90M reads/sec 不是把單 partition 推到極限、是 <em>partition key 設計讓流量散到極多 partition</em>、每個 partition 都在合理區間。</p>
<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/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database-sharding</a>。</p>
<h2 id="mode--partition-交叉判讀">Mode × Partition 交叉判讀</h2>
<p>Hot partition 在 capacity mode 不同下表現不同、但根因都是 schema。這是 single-table / partition-key / capacity-mode 三篇 deep article 的交叉軸 — mode 切換不解 partition 設計問題、partition 設計也不解 mode 選擇問題。</p>
<table>
  <thead>
      <tr>
          <th>表現面</th>
          <th>Provisioned 模式</th>
          <th>On-demand 模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Throttle 可見性</td>
          <td><code>WriteThrottleEvents</code> 立即可見、CloudWatch 直接抓</td>
          <td>不顯示 throttle event、表現為 <code>SuccessfulRequestLatency</code> p99 突然跳高</td>
      </tr>
      <tr>
          <td>Application 表現</td>
          <td><code>ProvisionedThroughputExceededException</code> 立即拋</td>
          <td>timeout / retry 加劇、看起來像「DynamoDB 變慢」</td>
      </tr>
      <tr>
          <td>工程誤判風險</td>
          <td>低（exception 明顯）</td>
          <td>高（latency spike 容易被誤判成網路 / 應用層 / 下游服務問題）</td>
      </tr>
      <tr>
          <td>解法</td>
          <td>改 PK schema（composite key / write sharding）</td>
          <td>改 PK schema（同左、不是切 mode）</td>
      </tr>
  </tbody>
</table>
<p><code>9.C15 Tixcraft</code> 警惕段明示這個 frame：「DynamoDB 寫入排隊本身就是隱性限流」— provisioned 看得到、on-demand 看不到，但都是同一個 schema 問題。</p>
<p><strong>核心 frame</strong>：on-demand 不是 partition key 設計的逃避路徑。看到 on-demand 模式 latency spike 但 throttle 為零，<em>第一個懷疑就是 hot partition</em>、不是網路或應用層。</p>
<p>跟 <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> 共軸閱讀：本篇從 schema 視角切入、那篇從 mode 選擇視角切入、合起來才是完整判讀。</p>
<h2 id="修復流程">修復流程</h2>
<p>從 access pattern audit 到 composite key 設計的 5 步流程。</p>
<h4 id="step-1識別寫入集中的-logical-key">Step 1：識別寫入集中的 logical key</h4>
<p>審視 access pattern 表、抓出 <em>寫入集中</em> 的 key：</p>
<ul>
<li>單一 event / single user 寫入比例 &gt; 10%（如熱門場次售票、bot 帳號）</li>
<li>時間 bucket（<code>PK = date</code> / <code>PK = hour</code>）— 寫入永遠打當下 partition、舊 partition 閒置</li>
<li>少數枚舉值（<code>PK = status</code> / <code>PK = country</code> 但只有 5-10 個值）</li>
</ul>
<p><code>9.C15 Tixcraft</code> 揭露的具體場景：演唱會某一熱門場次的 <code>event_id</code> 為 PK、開賣瞬間 200K 用戶同時搶該場次、所有寫入集中到單一 partition。</p>
<h4 id="step-2選-shard-數">Step 2：選 shard 數</h4>
<p>把單一 logical key 切成 N 個物理 shard。N 的估算邏輯：</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">單 partition WCU 上限 = 1000
</span></span><span class="line"><span class="ln">2</span><span class="cl">留 20% buffer            = 800
</span></span><span class="line"><span class="ln">3</span><span class="cl">N = 單 logical key 預期峰值 WCU / 800（最小 shard 數）</span></span></code></pre></div><blockquote>
<p><strong>Scope warning</strong>：「shard 數 10-100」、「800 WCU 留 buffer」這些具體數字是通用工程估算、9.C15 case <em>沒有</em> 揭露 Tixcraft 用幾個 shard。case 揭露的是「composite key 分散」概念跟「IOPS 從 20 衝到 135K」的結果、不是具體 shard 數量。寫進你自己的設計時、shard 數依預期單 logical key 峰值估算、不要照搬本文數字。</p></blockquote>
<h4 id="step-3composite-key-設計random-shard">Step 3：composite key 設計（random shard）</h4>
<p><a href="/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key</a> 把 logical key 加上 random suffix、把 hot logical 值分散到多個 partition：</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">random</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="k">def</span> <span class="nf">write_order</span><span class="p">(</span><span class="n">event_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">order_data</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1"># 寫入端：random suffix 分散到 N shard</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">shard</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">N</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">pk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">event_id</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">shard</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">sk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;USER#</span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">timestamp</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">8</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="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="n">pk</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="n">sk</span><span class="p">,</span> <span class="o">**</span><span class="n">order_data</span><span class="p">})</span></span></span></code></pre></div><p>讀取時 fan-out 到所有 shard：</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">query_event_orders</span><span class="p">(</span><span class="n">event_id</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">results</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="n">shard</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">pk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">event_id</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">shard</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">page</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">KeyConditionExpression</span><span class="o">=</span><span class="n">Key</span><span class="p">(</span><span class="s2">&#34;PK&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">eq</span><span class="p">(</span><span class="n">pk</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">results</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">page</span><span class="p">[</span><span class="s2">&#34;Items&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="n">results</span></span></span></code></pre></div><h4 id="step-4calculated-shard讓同-user-仍可預測讀取">Step 4：calculated shard（讓同 user 仍可預測讀取）</h4>
<p>random shard 的代價是讀取要 fan-out N 次。當你需要「同 user 寫入分散、但讀取 <em>該 user</em> 自己的資料時不要 fan-out」、改用 calculated shard：</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">hashlib</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="k">def</span> <span class="nf">shard_for_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">n</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">h</span> <span class="o">=</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">md5</span><span class="p">(</span><span class="n">user_id</span><span class="o">.</span><span class="n">encode</span><span class="p">())</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span> <span class="o">%</span> <span class="n">n</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">write_user_event</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">event_data</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">shard</span> <span class="o">=</span> <span class="n">shard_for_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">N</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">pk</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;USER#</span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">shard</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="c1"># 同一 user_id 永遠拿到同一 shard</span></span></span></code></pre></div><p>讀單一 user 只 query 一個 shard、讀全平台 user 才 fan-out N 個 shard。</p>
<p>選擇：</p>
<ul>
<li><strong>random shard</strong>：寫入完全均勻、但所有讀路徑都要 fan-out；適合 <em>flash-sale / 緩衝層</em>（讀路徑是後端慢消費、不在乎 fan-out latency）</li>
<li><strong>calculated shard</strong>：寫入按 hash 均勻、user-level 讀路徑單 shard；適合 <em>user-facing OLTP</em>（user 讀自己資料延遲敏感）</li>
</ul>
<h4 id="step-5驗證點">Step 5：驗證點</h4>
<ul>
<li>Contributor Insights 看 top-N PK 訪問是否平均分布</li>
<li>CloudWatch partition-level throttle = 0</li>
<li>Application 端 read fan-out latency 在預算內</li>
</ul>
<p><strong>Rollback boundary</strong>：composite key 寫入端可雙寫舊 + 新 key 一段時間（雙寫窗口）、application read 端 fallback 到舊 PK；不可逆動作只在「移除舊 key」階段。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production case 揭露的 5 個踩雷情境：</p>
<h4 id="case-1時間序-pk-集中">Case 1：時間序 PK 集中</h4>
<p><code>PK = date</code> 或 <code>PK = hour</code> — 寫入永遠打當下 partition、舊 partition 閒置。每日凌晨換 partition 時瞬間冷啟動、寫入 latency spike。修法：<code>date#shard</code> 把當下 partition 拆 N 個物理 shard、或改用 event-stream pattern（每個 event 獨立 ID 為 PK）。</p>
<h4 id="case-2bot-user-集中">Case 2：bot user 集中</h4>
<p>PK = <code>user_id</code>、某個 bot 帳號每秒寫 1000 次、單 user_id 達 1000 WCU 上限。修法：</p>
<ul>
<li>偵測高頻 user 後動態加 shard suffix（<code>user_id#shard0</code> … <code>user_id#shardN</code>）</li>
<li>或在 application 層 rate limit、不讓 bot 直接打 DynamoDB</li>
</ul>
<h4 id="case-3composite-key-但-read-端忘記-fan-out">Case 3：composite key 但 read 端忘記 fan-out</h4>
<p>寫入分散到 100 shard、讀取只 query 一個 shard、結果不完整。修法：讀取必須 N 次 query 並 application 端合併、或建反向 GSI（GSI PK = <code>event_id</code>、不加 shard suffix；但 GSI 自己也會 hot partition）。</p>
<h4 id="case-4shard-數選太多-read-fan-out-latency-爆">Case 4：shard 數選太多 read fan-out latency 爆</h4>
<p>N 過大時讀取 fan-out latency 從 5ms 變 200ms（具體數字隨網路延遲跟並行度變動、9.C15 case 未揭露 Tixcraft 用幾個 shard）。修法：shard 數依「單 logical key 預期峰值 / 800」估算、不是越多越好；read latency 跟寫入分散度是 trade-off。</p>
<h4 id="case-5on-demand-模式以為不會-hot-partition">Case 5：on-demand 模式以為不會 hot partition</h4>
<p>on-demand 仍受單 partition 1000 WCU 限制、只是 throttling 表現為 latency spike 而非 exception。team 看到「沒有 ThrottledRequests」就以為沒問題、實際 p99 已經從 5ms 跳到 50ms。修法：on-demand 不是 partition key 設計的逃避路徑、依然要做 composite key；觀測上看 <code>SuccessfulRequestLatency</code> p99 不只看 throttle。跟 <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> 共軸閱讀。</p>
<p><strong>Anti-recommendation</strong>：access pattern 寫入分散自然均勻（如 UUID 為 PK、無 logical hot key），不要預先 sharding；增加 read 端 fan-out 複雜度沒帶來收益。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>WriteThrottleEvents</code> / <code>ReadThrottleEvents</code>：按 table 跟 GSI 分；provisioned 模式直接訊號</li>
<li><code>SuccessfulRequestLatency</code> p99：on-demand 模式下 hot partition 的訊號（throttle 為零但 latency 跳高）</li>
<li>partition-level metric 透過 Contributor Insights 看，不是 CloudWatch 預設 panel</li>
</ul>
<p><strong>Contributor Insights 必開</strong>：top-N partition key by access frequency；每月 cost ~$0.02 per million event、值得開。沒開 Contributor Insights 你看不到 partition-level 分布、只能從總 capacity 跟 throttle 反推。</p>
<p>DynamoDB Streams：可用來抓 hot key debugging — 寫入事件落 Lambda 後統計 PK 頻率。</p>
<p><strong>Mode × partition 觀測差異</strong>（重申交叉判讀）：</p>
<ul>
<li>Provisioned 模式：看 <code>WriteThrottleEvents</code>、立即可見</li>
<li>On-demand 模式：看 <code>SuccessfulRequestLatency</code> p99、看 partition-level Contributor Insights、看 application 端 timeout / retry trend</li>
</ul>
<p>接回 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的 partition 章節。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="9c15-tixcraft-6750x-擴展的工程拆解">9.C15 Tixcraft 6750x 擴展的工程拆解</h3>
<p><code>9.C15 Tixcraft</code> 揭露的數字：IOPS 從 20 衝到 135K（6750 倍）、6 servers 變 800 servers、總成本 $4200、throttle rate 0.26%。但「6750x 擴展」不是 DynamoDB 自己的魔法、是 <em>partition key 均勻分散 + 架構解耦</em> 的組合結果：</p>
<ul>
<li><strong>partition key 均勻</strong>：composite key（<code>event_id</code> 加分散 suffix）把單一熱門場次散到多個 partition、每個 partition 都在合理區間（case 揭露概念、未揭露具體 shard 數）</li>
<li><strong>架構解耦</strong>：DynamoDB 當 durable queue、後端傳統 server（金流 / 票庫）用自己節奏消費、不被前端 130x 流量拖垮（見 <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> 的 durable queue 段）</li>
<li><strong>付款層獨立</strong>：付款不是 DynamoDB、是另一層獨立服務、避免搶票流量影響付款</li>
</ul>
<p>讀者該學的不是「DynamoDB 能撐 6750x」、是「composite key + 架構解耦 + 服務分層」三件事一起做才能撐。</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> — PK 設計上游、本篇是 PK 不天然均勻時的補救</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> — capacity mode 對 hot partition 表現的影響、mode × partition 交叉判讀的另一視角</li>
<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> — GSI 自己也會 hot partition、GSI PK 設計獨立 review</li>
<li>Migration playbook：composite key migration 屬「topology re-layout」、寫入需雙軌；對應 <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>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 9.C15</a> 互引：售票模式的 6750x 擴展細節、composite key 是工程選擇而非 vendor 魔法</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">Amazon Ads 9.C5</a> 互引：容量 = 每 partition 上限 × partition 數量、最熱 partition saturation 是容量天花板</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> 互引：connection-free scale 的另一面是 partition 設計責任</li>
</ul>
]]></content:encoded></item></channel></rss>