<?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>Hot-Partition on Tarragon</title><link>https://tarrragon.github.io/blog/tags/hot-partition/</link><description>Recent content in Hot-Partition 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/hot-partition/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><item><title>Cosmos DB Partition Key Design：synthetic / composite / hierarchical + 不可逆性硬約束</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/partition-key-design/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/partition-key-design/</guid><description>&lt;p>Cosmos DB 的 &lt;em>logical partition 上限是 10,000 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &amp;#43; memory &amp;#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit&lt;/a>/s + 20 GB storage&lt;/em>、partition key 一旦上 production &lt;em>改不了&lt;/em>（要 export → recreate container → import）。partition key 選錯的後果是 Black Friday / 上線日 / VIP 用戶把流量壓在少數 partition、p99 latency 從 50ms 飆到 5s、整體 container 還有 70% RU 剩餘卻全 throttle。Cosmos DB partition key 設計是 &lt;em>selection 階段就要決定的硬約束&lt;/em>、不是「先選錯再改」可承擔的風險 — 這個不可逆性跟 MongoDB（&lt;code>reshardCollection&lt;/code> 線上完成）跟 DynamoDB（建新 table backfill）形成關鍵對比。&lt;/p>
&lt;p>本文不是 Cosmos DB overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁&lt;/a>）— 而是 partition key 設計 + 故障演練的深度展開。Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a>（synthetic partition key 強制分散、AR 遊戲玩家位置）+ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS&lt;/a>（Black Friday 流量分散 + latency budget 拆解）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Cosmos DB 適用度前置判讀&lt;/strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 &lt;a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing&lt;/a>、本篇不重複展開。Partition key 設計是 &lt;em>已選 Cosmos DB 後&lt;/em> 的硬約束議題；若 workload 不適用 Cosmos DB、partition key 設計無法救回 vendor 選錯的不可逆性風險。&lt;/p></description><content:encoded><![CDATA[<p>Cosmos DB 的 <em>logical partition 上限是 10,000 <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a>/s + 20 GB storage</em>、partition key 一旦上 production <em>改不了</em>（要 export → recreate container → import）。partition key 選錯的後果是 Black Friday / 上線日 / VIP 用戶把流量壓在少數 partition、p99 latency 從 50ms 飆到 5s、整體 container 還有 70% RU 剩餘卻全 throttle。Cosmos DB partition key 設計是 <em>selection 階段就要決定的硬約束</em>、不是「先選錯再改」可承擔的風險 — 這個不可逆性跟 MongoDB（<code>reshardCollection</code> 線上完成）跟 DynamoDB（建新 table backfill）形成關鍵對比。</p>
<p>本文不是 Cosmos DB overview（請看 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁</a>）— 而是 partition key 設計 + 故障演練的深度展開。Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a>（synthetic partition key 強制分散、AR 遊戲玩家位置）+ <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a>（Black Friday 流量分散 + latency budget 拆解）。</p>
<blockquote>
<p><strong>Cosmos DB 適用度前置判讀</strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 <a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing</a>、本篇不重複展開。Partition key 設計是 <em>已選 Cosmos DB 後</em> 的硬約束議題；若 workload 不適用 Cosmos DB、partition key 設計無法救回 vendor 選錯的不可逆性風險。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：團隊用 user_id 當 partition key 上 production、平常正常、Black Friday 或 VIP 大客戶上線當天 — application 收到 <code>429 TooManyRequests</code>、p99 從 50ms 飆到 5s；查 portal Metrics 發現 <em>整體 RU 使用率才 30%</em> 但少數 partition 100% 滿、其他 partition 閒置。Cosmos DB 設了 10000 RU/s、實際只能用 2000 就 throttle。</p>
<p>讀者徵兆：</p>
<ul>
<li>「Cosmos DB throughput 我設了 10000 RU、但寫入只有 2000 就 throttle」</li>
<li>「user_id 當 partition key 結果 VIP 用戶全卡在一個 partition」</li>
<li>「Hierarchical partition key 是 2023 後才有的、跟 composite 差在哪」</li>
<li>「partition key 選錯能改嗎」</li>
</ul>
<p>真實壓力：</p>
<ul>
<li>遊戲玩家位置（同伺服器集中同 partition、Minecraft Earth 場景）</li>
<li>IoT 裝置遙測（單一裝置高頻寫入、device_id 不均）</li>
<li>SaaS 多租戶（大客戶 vs 小客戶不均、tenant_id 直接當 partition key 會 hot）</li>
<li>零售商品 catalog（熱門 SKU vs 冷門 SKU 不均）</li>
</ul>
<p>partition key 選錯的隱性成本：要改就是 <em>export → recreate container with new partition key → import</em>、無 in-place migration、production 等於停機窗口 + 全量資料搬移。selection 階段就要決定、不能 phase 後補。</p>
<h2 id="核心機制">核心機制</h2>
<h3 id="partition-模型">Partition 模型</h3>
<p>每個 container 有 N 個 <em>physical partition</em>、每個 physical 上有多個 <em>logical partition</em>。同 partition key value 的所有 document 落到同一個 logical partition。Cosmos DB 動態調整 physical partition 數量（透明 split）、但 logical partition 的歸屬 <em>永遠不變</em>（同 PK value 永遠在同 logical）。</p>
<p>9.C11 Minecraft Earth 案例的平台特性段揭露「partition 動態分裂：透明」 — physical partition 的 split 對 application 透明、不需要 application 重連 / 重新 hash。但這個透明 <em>只解 physical partition 容量</em> 問題、<em>不解 logical partition 熱點</em> — logical partition 由 PK value 決定、application 必須自己均勻散佈 value。</p>
<h3 id="logical-partition-上限">Logical partition 上限</h3>
<p>10,000 RU/s + 20 GB storage、達 limit 後即使 container 還有總 RU、單一 partition key 一樣 throttle。這是 <em>硬上限</em>、不是 soft limit、不能調高。</p>
<p>20 GB storage 限制在小用戶通常碰不到、但對「以 tenant_id 為 PK 的大客戶」、storage 也可能先到上限（單一大客戶 50GB 資料、塞不進一個 logical partition）。</p>
<h3 id="partition-key-設計三種模式">Partition key 設計三種模式</h3>
<h4 id="synthetic人工合成-key">Synthetic（人工合成 key）</h4>
<p>機制：用 <code>{userId}_{random_0_to_99}</code> 把單一 user 的寫入散到 100 個 logical partition。application 端 hash userId + random suffix、寫入時組合成 partition key。</p>
<p>副作用：read 需 fan-out 100 個 partition、單一 query RU 暴漲 100x。適合 <em>write-heavy + 不需精準 read</em> 場景（如 IoT telemetry、log）。</p>
<p>9.C11 Minecraft Earth 用 synthetic partition key 強制分散 — AR 遊戲玩家位置寫入頻繁、partition 分散讓單一玩家不會打爆一個 partition。但 case 沒揭露具體 schema、synthetic 細節屬 outline knowledge 推論。</p>
<h4 id="composite多欄位合成">Composite（多欄位合成）</h4>
<p>機制：用 <code>{tenantId}_{deviceId}</code> 兩個欄位合成（<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> 通用樣式）、避免單一 high-cardinality 欄位 hot。適合 <em>多租戶 SaaS</em>、單一 tenant 內又有多個 device、避免大 tenant 把所有寫入集中。</p>
<p>副作用：read 必須帶兩個欄位、否則 cross-partition query；query API 設計要強制帶 tenant + device。</p>
<h4 id="hierarchical2023-原生支援">Hierarchical（2023+ 原生支援）</h4>
<p>機制：原生支援多層 key（最多 3 層、如 <code>tenantId / deviceId / sessionId</code>）、不用手動合成；query 可指定前綴做 partition scope query（如「拿 tenant X 的所有 device」單一 partition scope）。</p>
<p>適合：多層業務 hierarchy 場景（tenant → user → session、organization → team → project）。比 composite 優勢是 <em>支援 prefix query</em>、composite key 只能完整匹配。</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">az cosmosdb sql container create <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --partition-key-paths <span class="s2">&#34;/tenantId&#34;</span> <span class="s2">&#34;/deviceId&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --partition-key-kind <span class="s2">&#34;MultiHash&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  ...</span></span></code></pre></div><p>設計順序要從 <em>低 cardinality</em> 到 <em>高 cardinality</em>（tenant 少、device 多、session 最多）— 反序會讓 prefix query 無意義。</p>
<h3 id="跟其他-vendor-的可逆性對照本章合成-frame">跟其他 vendor 的可逆性對照（本章合成 frame）</h3>
<blockquote>
<p><strong>跨 vendor 可逆性對照 SSoT</strong>：MongoDB / DynamoDB / Cosmos DB 三家 partition key 可逆性不在同一光譜（Cosmos DB 屬不可改、不可逆性最高）、跨 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>。本段聚焦 Cosmos DB 不可改特性對 selection 階段 access pattern audit 嚴格度的影響、不重複展開三 vendor 全光譜比較。</p></blockquote>
<p>partition / shard key 的可逆性在 vendor 間差異懸殊：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>可逆性</th>
          <th>機制</th>
          <th>工程成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MongoDB</td>
          <td>可改（4.4+ <code>reshardCollection</code>）</td>
          <td>線上完成、cluster 內搬移</td>
          <td>高、但 in-place</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>可改</td>
          <td>建新 table、backfill + dual-write 切換</td>
          <td>中、要 backfill</td>
      </tr>
      <tr>
          <td>Cosmos DB</td>
          <td><em>不可改</em></td>
          <td>必須 export → recreate container → import</td>
          <td>最高、需停機窗口</td>
      </tr>
  </tbody>
</table>
<p><strong>對照表是本章合成 frame、9.C11 Minecraft Earth 沒直接揭露此對比、是從 outline knowledge 跟 MongoDB shard-key-selection 對照得出</strong>。引用時必須明示：Cosmos DB partition key 不可改是 <em>設計選型的硬約束</em>、不是「先選錯再改」可承擔的風險 — 這個約束直接決定 selection 階段的 partition key audit 嚴格度該多高。</p>
<p>對 selection 的意義：若團隊對 access pattern 不確定、不能用「先上 Cosmos DB 再說、不行再改」的心態、要先用 MongoDB / DynamoDB 試 access pattern、確定後再評估 Cosmos DB。</p>
<h3 id="跟-dynamodb-partition-key-對比">跟 DynamoDB partition key 對比</h3>
<ul>
<li><strong>DynamoDB</strong>：partition key + optional sort key、無 hierarchical key、adaptive capacity 自動補 hot partition（部分減緩、不完全解決）</li>
<li><strong>Cosmos DB</strong>：hierarchical key 是 <em>原生功能</em>、不靠 adaptive；單 logical partition 限制嚴格、必須前期設計</li>
</ul>
<p>Cosmos DB 的 <em>硬上限 + 不可逆性</em> 跟 DynamoDB 的 <em>adaptive + 可遷移</em> 是兩種設計哲學 — selection 時要評估團隊能不能負擔前期 design effort。</p>
<p>對應 knowledge cards：<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="操作流程">操作流程</h2>
<h3 id="設定-partition-key">設定 partition key</h3>
<p>建 container 時指定、<em>無法事後修改</em>：</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">az cosmosdb sql container create <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --account-name mycosmos --database-name mydb <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --name mycontainer --resource-group myrg <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --partition-key-path <span class="s2">&#34;/userId&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --partition-key-version <span class="m">2</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --throughput <span class="m">10000</span></span></span></code></pre></div><h3 id="hierarchical-key-設定c-sdk-範例">Hierarchical key 設定（C# SDK 範例）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kt">var</span> <span class="n">properties</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ContainerProperties</span><span class="p">(</span><span class="s">&#34;mycontainer&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="s">&#34;/tenantId&#34;</span><span class="p">,</span> <span class="s">&#34;/deviceId&#34;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">properties</span><span class="p">.</span><span class="n">PartitionKeyDefinitionVersion</span> <span class="p">=</span> <span class="n">PartitionKeyDefinitionVersion</span><span class="p">.</span><span class="n">V2</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kt">var</span> <span class="n">container</span> <span class="p">=</span> <span class="k">await</span> <span class="n">database</span><span class="p">.</span><span class="n">CreateContainerAsync</span><span class="p">(</span><span class="n">properties</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">// 寫入時帶完整 hierarchical key</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kt">var</span> <span class="n">pk</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PartitionKeyBuilder</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">&#34;tenant-123&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">&#34;device-456&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">.</span><span class="n">Build</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="n">CreateItemAsync</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">pk</span><span class="p">);</span>
</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">// Prefix query：拿 tenant-123 的所有 device</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kt">var</span> <span class="n">prefixPk</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PartitionKeyBuilder</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="s">&#34;tenant-123&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">.</span><span class="n">Build</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="kt">var</span> <span class="n">iterator</span> <span class="p">=</span> <span class="n">container</span><span class="p">.</span><span class="n">GetItemQueryIterator</span><span class="p">&lt;</span><span class="n">Item</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="s">&#34;SELECT * FROM c&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">requestOptions</span><span class="p">:</span> <span class="k">new</span> <span class="n">QueryRequestOptions</span> <span class="p">{</span> <span class="n">PartitionKey</span> <span class="p">=</span> <span class="n">prefixPk</span> <span class="p">});</span></span></span></code></pre></div><h3 id="synthetic-key-寫入">Synthetic key 寫入</h3>
<p>application 端 hash + random suffix、寫入時組合成 partition key：</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 class="kn">import</span> <span class="nn">random</span>
</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="k">def</span> <span class="nf">get_partition_key</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">fanout</span><span class="o">=</span><span class="mi">100</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">suffix</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">fanout</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="k">return</span> <span class="sa">f</span><span class="s2">&#34;</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">suffix</span><span class="si">}</span><span class="s2">&#34;</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"># Read 時 fan-out 所有可能 suffix</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">def</span> <span class="nf">read_user_data</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">fanout</span><span class="o">=</span><span class="mi">100</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">10</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">11</span><span class="cl">    <span class="k">for</span> <span class="n">suffix</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">fanout</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</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">user_id</span><span class="si">}</span><span class="s2">_</span><span class="si">{</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">13</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">query_partition</span><span class="p">(</span><span class="n">pk</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="n">results</span></span></span></code></pre></div><p>注意 fanout 的 trade-off：fanout = 100 等於 read 成本 × 100；要在 <em>write 分散</em> 跟 <em>read 效率</em> 間平衡、通常 fanout 10-100 之間。</p>
<h3 id="查-partition-分布">查 partition 分布</h3>
<p>portal Metrics &gt; Storage by partition key、看分布是否均勻；或用 <code>SELECT * FROM c WHERE c.partitionKey = &quot;specific-value&quot;</code> query + diagnostic log 看 RU 分布。</p>
<h3 id="驗證點">驗證點</h3>
<ul>
<li>每個 logical partition 的 RU 消耗 &lt; 80% limit（給 burst 留 20% buffer）</li>
<li>單一 partition 的 storage &lt; 16 GB（給成長預留 4 GB buffer）</li>
<li>p99 latency 在 hot partition 不退化</li>
<li>跨 partition query 比例 &lt; 5%（多數 query 帶 partition key 條件）</li>
</ul>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>partition key 選錯只能 export → recreate container with new partition key → import；無 in-place migration、生產系統等於停機窗口 + dual-write cutover 流程。對應 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> 的遷移模型。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="failure-1user_id-直接當-partition-key">Failure 1：user_id 直接當 partition key</h3>
<p>高活躍用戶（VIP / bot / 大客戶）超過 10,000 RU/s、全 container 被 throttle；徵兆是 <code>429 TooManyRequests</code> 集中在少數 partition、整體 RU 利用率才 30%。</p>
<p>修：</p>
<ul>
<li>短期：把 hot user 拉到獨立 container（合規上有時要這樣做、把 VIP / 企業客戶獨立治理）</li>
<li>長期：換 synthetic key（user_id + random suffix）或 composite key（tenant + user）</li>
<li>selection 階段 audit：access pattern 是否會有「少數 user 主導流量」現象（B2B SaaS、VIP 用戶都有）</li>
</ul>
<h3 id="failure-2時間當-partition-key">Failure 2：時間當 partition key</h3>
<p><code>/createdDate</code> 或 <code>/yyyyMM</code>、新資料全寫入最新 partition、舊 partition 冷掉浪費 — write hot + read 不均。徵兆：最新月份 partition throttle、其他月份 partition 閒置。</p>
<p>修：時間 + 業務維度組合（如 <code>/yyyyMM-userId</code>、<code>/userId-yyyy</code>）、避免純時間維度。time-series workload 該考慮 Azure Time Series Insights 或 Cosmos DB time-series 專屬模式。</p>
<h3 id="failure-3synthetic-key-沒考慮-read-路徑">Failure 3：Synthetic key 沒考慮 read 路徑</h3>
<p>寫入散開但 read 必須 fan-out 100 partition、單一 query RU 暴漲 100x。徵兆：read 成本遠高於估算、<code>RetrievedDocumentCount</code> 跟 <code>OutputDocumentCount</code> 比例 &gt; 50。</p>
<p>修：</p>
<ul>
<li>用 Change Feed 把投影預先寫到 read-optimized container（partition key 用 user_id）、read 走投影</li>
<li>或調 fanout（10 而非 100）、平衡 write 分散跟 read 成本</li>
<li>或重新評估「真的需要 synthetic key 嗎」 — 多數場景用 composite 就夠</li>
</ul>
<h3 id="failure-4hierarchical-key-設計順序顛倒">Failure 4：Hierarchical key 設計順序顛倒</h3>
<p>把 high-cardinality 放第一層、prefix query 變得無意義。如 <code>/userId/tenantId</code> 而非 <code>/tenantId/userId</code> — 想拿「tenant X 的所有 user」變成 cross-partition query、完全失去 hierarchical 優勢。</p>
<p>修：設計順序從 <em>低 cardinality</em> 到 <em>高 cardinality</em>、跟業務 query pattern 對齊。建 container 前畫 access pattern 表、列每個 query 的 hierarchy 順序、再決定 partition key path。</p>
<h3 id="failure-5不監控-partition-分布">Failure 5：不監控 partition 分布</h3>
<p>partition skew 累積幾個月、直到事故才發現。production 上線初期 access pattern 還不明顯、半年後 VIP 客戶開始用、partition 失衡 — 來不及改 partition key、只能在 throttle 中應急。</p>
<p>修：上線第一天就設 alert：</p>
<ul>
<li>單 partition RU 利用 &gt; 80% 持續 5 min</li>
<li>單 partition storage &gt; 16 GB</li>
<li>429 error rate 突增</li>
</ul>
<p>每週看 portal Insights &gt; Top contributors &gt; Partition key range、early detect skew。</p>
<h3 id="failure-6container-之間-partition-設計不一致">Failure 6：Container 之間 partition 設計不一致</h3>
<p>跨 container query 需要 fan-out、cross-partition query 成本爆炸。常見 anti-pattern：訂單 container 用 user_id、商品 container 用 product_id、join 訂單 + 商品時兩邊都 cross-partition。</p>
<p>修：跨 container 的 access pattern 在 selection 階段就要設計、不能各 container 各自決定 partition key。或者用 Change Feed 把跨 container 資料合成 single container 的 materialized view。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：<code>PhysicalPartitionThroughputInfo</code>、<code>NormalizedRUConsumption</code> per partition、<code>StorageDistributionPerPartition</code></li>
<li>Hot partition 偵測：portal Insights &gt; Top contributors &gt; Partition key range</li>
<li>容量估算公式：peak RU per partition × partition 數 + 預留 buffer（一般 30%）= total RU/s</li>
<li>回 <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 skew 當 saturation signal</li>
<li>Alert：單 partition RU 利用 &gt; 80% 持續 5 min；429 error rate 突增</li>
</ul>
<h3 id="latency-budget-拆解vendor-sla-vs-end-to-end-實測">Latency budget 拆解：vendor SLA vs end-to-end 實測</h3>
<p>9.C21 ASOS 觀察「48ms 平均響應 = 全球分散下 Cosmos DB 的代表性數字」段揭露：48ms 包含 <em>網路 + DB + 應用層</em>、DB 本身可能只佔 5-10ms、其他是網路與應用層。引用時不能把 vendor 廣告的 5-10ms p99 當「使用者體驗」、要明示「48ms 是 9.C21 ASOS 案例的 end-to-end 觀察、Cosmos DB 自身可能只佔 5-10ms（case 揭露的拆解推論、不是 case fact）」。</p>
<p>操作上要把 end-to-end latency 拆 budget：</p>
<ul>
<li><strong>DB 端 latency</strong>（vendor SLA、p99 &lt; 10ms 地區內讀、9.C11 揭露）</li>
<li><strong>跨 region replication latency</strong>（multi-region read 從就近 region 拿、不會跨洲、但 cross-region write 不同、見 <a href="../multi-region-write-conflict/">multi-region-write-conflict</a>）</li>
<li><strong>應用層 latency</strong>（serialize / business logic / HTTP overhead）</li>
<li><strong>客戶端網路 latency</strong>（mobile / 跨洲）</li>
</ul>
<p>跟 partition skew 的關係：partition 失衡時即使 vendor 端 SLA 達標、實測 p99 仍會被 hot partition 拉高 — 單一 partition 的 RU consumption 飽和 → 429 retry → 應用層 latency 暴漲 → end-to-end 從 48ms 變 500ms。partition 設計直接影響 end-to-end SLA 鏈路。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（partition skew 直接影響 RU sizing）、<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（partition 失衡時即使設 Strong 也看到 throttle）、<a href="../multi-region-write-conflict/">multi-region-write-conflict</a>（partition key 影響 conflict 分布）、<a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a>（MongoDB shard key → Cosmos DB partition key 翻譯）</li>
<li>跟 <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 vendor</a> 對比：partition key + adaptive capacity vs 不可逆 + hierarchical</li>
<li>跟 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor</a> 對比：<code>reshardCollection</code> 可逆 vs 不可逆</li>
<li>跟 1.x 章節：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a></li>
<li>Knowledge cards：<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></li>
<li>Anti-recommendation：小流量（&lt; 1000 RU/s 預期）不必過度設計 synthetic key、Cosmos DB autoscale + 簡單 partition key 即可；過度 design 比 under-design 更常見的成本浪費</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 partition key design backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth case</a> — synthetic partition key 主案例</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS case</a> — latency budget 拆解 + 全球零售流量分散</li>
<li><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> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/partitioning-overview">Cosmos DB partitioning</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/hierarchical-partition-keys">Hierarchical partition keys</a></li>
</ul>
]]></content:encoded></item></channel></rss>