<?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>Change-Feed on Tarragon</title><link>https://tarrragon.github.io/blog/tags/change-feed/</link><description>Recent content in Change-Feed 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/change-feed/index.xml" rel="self" type="application/rss+xml"/><item><title>Cosmos DB Change Feed (CDC)：persistent change log、Azure Functions trigger、latest-version vs all-versions-and-deletes 與跟 DynamoDB Streams 對照</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/change-feed-cdc/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/change-feed-cdc/</guid><description>&lt;p>本文是 &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&lt;/a> overview 的 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>。Change Feed 是 Cosmos DB 把 container 內每次寫入按 logical partition 順序持久化成一條可重讀變更序列的能力、對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture&lt;/a> 的概念分層。它讓「寫入後要做的後續工作」（投影、cache 失效、事件發布、跨 store 同步）從 application 寫入路徑解耦出來、由獨立 consumer 按自己的進度消費。本文先講 Change Feed 的精確語義與兩種模式、再進 change feed processor 與 Azure Functions trigger 的操作流程、最後拆失敗模式與跟 DynamoDB Streams 的對照。&lt;/p>
&lt;p>Case anchor 是 &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>（85,000 SKU、每週新增 5,000 件的高更新頻率 catalog、寫入後需要 search index / 推薦排序投影）。ASOS case 本身沒有揭露 Change Feed 的實作細節、本文只取它的 catalog 寫入投影壓力當情境 anchor、機制以 Azure vendor 規格與通用工程展開。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：catalog 寫入 Cosmos DB 後、下游還有一連串工作要做 — 把商品同步到 search index、刷新推薦排序、讓 cache 失效、發 event 給庫存服務。團隊一開始把這些工作塞進寫入 API 的同步路徑、寫一筆商品要等 search index 更新完才返回、寫入 latency 被下游拖垮；高峰時下游 search service 變慢、整條寫入鏈一起阻塞。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>「寫入 API latency 被下游投影工作拖高、想把它非同步化」&lt;/li>
&lt;li>「下游 consumer 掛掉一段時間、重啟後要怎麼補回漏掉的變更」&lt;/li>
&lt;li>「同一筆 document 在短時間內改三次、下游只需要最終狀態還是每次都要」&lt;/li>
&lt;li>「要做 audit / 要知道刪除事件、但 Change Feed 預設讀不到 delete」&lt;/li>
&lt;/ul>
&lt;p>真實壓力：寫入路徑與下游處理耦合會讓寫入 SLA 受制於最不穩的 consumer；而把投影改成「掃全表」的 batch job 又有延遲與成本問題。Change Feed 提供的是 &lt;em>持久、可重讀、按 partition 有序&lt;/em> 的變更來源、讓下游用 pull 或 trigger 模式按自己的進度消費。&lt;/p>
&lt;h2 id="核心機制partition-scoped-persistent-change-log">核心機制：partition-scoped persistent change log&lt;/h2>
&lt;p>Change Feed 是 container 的內建能力、把每個 logical partition 內的寫入按發生順序記錄成一條持久序列。它的關鍵語義有幾個面向。&lt;/p>
&lt;p>順序保證是 &lt;em>per logical partition&lt;/em>、不是 container 全域。同一 partition key 內的變更嚴格有序、跨 partition 之間沒有全域順序 — 這跟 &lt;a href="../partition-key-design/">partition-key-design&lt;/a> 的設計直接相關、consumer 必須假設不同 partition 的事件可能交錯到達。&lt;/p></description><content:encoded><![CDATA[<p>本文是 <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</a> overview 的 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>。Change Feed 是 Cosmos DB 把 container 內每次寫入按 logical partition 順序持久化成一條可重讀變更序列的能力、對應 <a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture</a> 的概念分層。它讓「寫入後要做的後續工作」（投影、cache 失效、事件發布、跨 store 同步）從 application 寫入路徑解耦出來、由獨立 consumer 按自己的進度消費。本文先講 Change Feed 的精確語義與兩種模式、再進 change feed processor 與 Azure Functions trigger 的操作流程、最後拆失敗模式與跟 DynamoDB Streams 的對照。</p>
<p>Case anchor 是 <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>（85,000 SKU、每週新增 5,000 件的高更新頻率 catalog、寫入後需要 search index / 推薦排序投影）。ASOS case 本身沒有揭露 Change Feed 的實作細節、本文只取它的 catalog 寫入投影壓力當情境 anchor、機制以 Azure vendor 規格與通用工程展開。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：catalog 寫入 Cosmos DB 後、下游還有一連串工作要做 — 把商品同步到 search index、刷新推薦排序、讓 cache 失效、發 event 給庫存服務。團隊一開始把這些工作塞進寫入 API 的同步路徑、寫一筆商品要等 search index 更新完才返回、寫入 latency 被下游拖垮；高峰時下游 search service 變慢、整條寫入鏈一起阻塞。</p>
<p>讀者徵兆：</p>
<ul>
<li>「寫入 API latency 被下游投影工作拖高、想把它非同步化」</li>
<li>「下游 consumer 掛掉一段時間、重啟後要怎麼補回漏掉的變更」</li>
<li>「同一筆 document 在短時間內改三次、下游只需要最終狀態還是每次都要」</li>
<li>「要做 audit / 要知道刪除事件、但 Change Feed 預設讀不到 delete」</li>
</ul>
<p>真實壓力：寫入路徑與下游處理耦合會讓寫入 SLA 受制於最不穩的 consumer；而把投影改成「掃全表」的 batch job 又有延遲與成本問題。Change Feed 提供的是 <em>持久、可重讀、按 partition 有序</em> 的變更來源、讓下游用 pull 或 trigger 模式按自己的進度消費。</p>
<h2 id="核心機制partition-scoped-persistent-change-log">核心機制：partition-scoped persistent change log</h2>
<p>Change Feed 是 container 的內建能力、把每個 logical partition 內的寫入按發生順序記錄成一條持久序列。它的關鍵語義有幾個面向。</p>
<p>順序保證是 <em>per logical partition</em>、不是 container 全域。同一 partition key 內的變更嚴格有序、跨 partition 之間沒有全域順序 — 這跟 <a href="../partition-key-design/">partition-key-design</a> 的設計直接相關、consumer 必須假設不同 partition 的事件可能交錯到達。</p>
<p>進度由 continuation token 表達。consumer 讀到哪裡、用一個 continuation token 標記；下次帶 token 回來、從上次的位置繼續。token 是 per partition range 的、container 做 partition split 時 token 要能跟著 range 拆分 — 這是 change feed processor 幫忙處理的部分。</p>
<p>讀取是 pull-based 持久來源、不是 push 通知。Change Feed 不主動推、是 consumer 主動拉。Azure Functions 的 Cosmos DB trigger 看起來像 push、底層仍是 trigger runtime 持續 poll Change Feed。</p>
<h3 id="兩種模式latest-version-vs-all-versions-and-deletes">兩種模式：latest-version vs all-versions-and-deletes</h3>
<p>Change Feed 有兩種模式、語義差很大、選錯會在 audit / 補償場景出問題（模式名稱與可用性屬時間敏感、查 <a href="https://learn.microsoft.com/azure/cosmos-db/change-feed">最新文件</a>）。</p>
<p>Latest-version 模式（過去稱 incremental feed）只給每個 document 的 <em>最新狀態</em>。同一 document 在兩次消費之間改了三次、consumer 只會看到最後一個版本、中間版本看不到；delete 也看不到（document 消失、feed 裡沒有對應的 tombstone）。這個模式適合「我只要把最終狀態投影到下游」的場景 — search index 同步、cache 刷新、物化視圖更新。</p>
<p>All-versions-and-deletes 模式給 <em>每一次</em> 變更、包含中間版本與 delete / TTL 過期事件。同一 document 改三次、feed 給三筆；刪掉給一筆刪除事件。這個模式適合需要完整變更歷史的場景 — audit log、event sourcing、需要對 delete 做反應的跨 store 同步。代價是事件量更大、且這個模式對 retention 與 partition 行為有額外約束（時間敏感、查文件）。</p>
<p>選擇判準：問「我需要中間版本與刪除事件嗎」。投影類工作（只要最終狀態）用 latest-version；audit 與需要對刪除反應的同步用 all-versions-and-deletes。預設選 latest-version、只有明確需要歷史與 delete 時才升級。</p>
<h3 id="change-feed-processor-的角色">change feed processor 的角色</h3>
<p>直接讀 Change Feed 要自己管 partition range、lease、continuation token、failover — 這些 plumbing 用 change feed processor library 處理。它的核心元件是 <em>lease container</em>：一個獨立的 Cosmos DB container、記錄每個 partition range 由哪個 consumer instance 處理、處理到哪個 continuation token。多個 consumer instance 共用同一個 lease container 時、processor 自動把 partition range 分配到不同 instance、達成水平擴展與 failover。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="啟用與確認">啟用與確認</h3>
<p>Change Feed 對 SQL API container 是預設啟用的、不需要額外開關（latest-version 模式）。all-versions-and-deletes 模式需要在 container 層設定、且要設 retention window。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 確認 container 存在、Change Feed 自動可用（latest-version）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">az cosmosdb sql container show <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --account-name mycosmos --resource-group myrg <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --database-name catalog --name products <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;resource.id&#34;</span></span></span></code></pre></div><p>驗證：container 存在即可讀 latest-version feed。要用 all-versions-and-deletes、先確認 account / SDK 版本支援（時間敏感、查文件）並設好 retention。</p>
<h3 id="change-feed-processorc-sdk">change feed processor（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="c1">// lease container 獨立於 monitored container</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">Container</span> <span class="n">monitored</span> <span class="p">=</span> <span class="n">client</span><span class="p">.</span><span class="n">GetContainer</span><span class="p">(</span><span class="s">&#34;catalog&#34;</span><span class="p">,</span> <span class="s">&#34;products&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">Container</span> <span class="n">leases</span> <span class="p">=</span> <span class="n">client</span><span class="p">.</span><span class="n">GetContainer</span><span class="p">(</span><span class="s">&#34;catalog&#34;</span><span class="p">,</span> <span class="s">&#34;leases&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">ChangeFeedProcessor</span> <span class="n">processor</span> <span class="p">=</span> <span class="n">monitored</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">.</span><span class="n">GetChangeFeedProcessorBuilder</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">processorName</span><span class="p">:</span> <span class="s">&#34;search-index-sync&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">onChangesDelegate</span><span class="p">:</span> <span class="n">HandleChangesAsync</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">WithInstanceName</span><span class="p">(</span><span class="n">Environment</span><span class="p">.</span><span class="n">MachineName</span><span class="p">)</span>  <span class="c1">// 每個 instance 唯一</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">.</span><span class="n">WithLeaseContainer</span><span class="p">(</span><span class="n">leases</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</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">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">await</span> <span class="n">processor</span><span class="p">.</span><span class="n">StartAsync</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="kd">async</span> <span class="n">Task</span> <span class="n">HandleChangesAsync</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">IReadOnlyCollection</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;</span> <span class="n">changes</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">product</span> <span class="k">in</span> <span class="n">changes</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="c1">// 投影到 search index — 必須 idempotent</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="k">await</span> <span class="n">searchIndex</span><span class="p">.</span><span class="n">UpsertAsync</span><span class="p">(</span><span class="n">product</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="c1">// delegate 正常返回 = processor 自動推進 lease 的 continuation token</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>驗證：lease container 內會出現每個 partition range 的 lease document、<code>ContinuationToken</code> 欄位隨消費推進；多開一個 instance、觀察 lease 被重新分配到兩個 instance。失敗時 delegate 拋例外、processor 不推進該 range 的 token、下次重讀同一批（at-least-once、所以 handler 要 idempotent）。</p>
<h3 id="azure-functions-trigger消費端最省維運的形態">Azure Functions trigger（消費端最省維運的形態）</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="na">[FunctionName(&#34;SyncSearchIndex&#34;)]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">public</span> <span class="kd">static</span> <span class="kd">async</span> <span class="n">Task</span> <span class="n">Run</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="na">    [CosmosDBTrigger(
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="na">        databaseName: &#34;catalog&#34;,
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="na">        containerName: &#34;products&#34;,
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="na">        Connection = &#34;CosmosConnection&#34;,
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="na">        LeaseContainerName = &#34;leases&#34;,
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="na">        CreateLeaseContainerIfNotExists = true)]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;</span> <span class="n">changes</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">ILogger</span> <span class="n">log</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">p</span> <span class="k">in</span> <span class="n">changes</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">await</span> <span class="n">searchIndex</span><span class="p">.</span><span class="n">UpsertAsync</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>  <span class="c1">// idempotent</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Functions trigger 底層就是 change feed processor、lease 與 scale-out 由 Functions runtime 管。驗證：function 的 invocation count 隨寫入增加、Application Insights 看 <code>changes</code> batch size 與 lag。</p>
<h3 id="rollback-boundary">Rollback boundary</h3>
<p>Change Feed 是讀取側機制、停掉 consumer 不影響寫入。要重放：刪掉 lease container 的對應 lease（或建新 processor name）會從 container 起點或指定時間點重讀。重放前確認下游投影是 idempotent、否則重放會重複寫。</p>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="把-handler-寫成非-idempotent">把 handler 寫成非 idempotent</h3>
<p>Change Feed 是 at-least-once。consumer 在處理一批後、推進 token 前 crash、重啟會重讀同一批。handler 若是「append 一筆 audit row」這種非 idempotent 操作、重放會產生重複。徵兆是下游出現重複事件、且重複數對應 consumer 重啟次數。修法是讓投影用 upsert（以 document id + version 為 key）、audit 用 dedup key、發 event 帶 idempotency key 讓下游去重 — 對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 的設計。</p>
<h3 id="用-latest-version-模式卻期待看到-delete">用 latest-version 模式卻期待看到 delete</h3>
<p>team 用預設 latest-version feed 做跨 store 同步、上線後發現「source 刪掉的 document、target 還在」。latest-version 模式不發 delete 事件、刪除在 feed 裡是「該 document 不再出現」、consumer 無從得知。修法是 audit / 需要刪除反應的場景改 all-versions-and-deletes 模式；或在 application 層用 soft delete（寫一個 <code>deleted: true</code> 的版本、latest-version feed 就看得到這次寫入）。</p>
<h3 id="lease-container-配置不足成為瓶頸">lease container 配置不足成為瓶頸</h3>
<p>lease container 自己也吃 RU、且 processor 對它有頻繁讀寫。lease container RU 配太低、processor 推進 token 被 throttle、表現成 Change Feed 消費 lag 升高、但 monitored container 看起來健康。徵兆是消費 lag 持續增長、診斷發現 429 來自 lease container 而非 source。修法是給 lease container 足夠 RU、把它跟 source container 的容量分開規劃、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>。</p>
<h3 id="假設-change-feed-有跨-partition-全域順序">假設 Change Feed 有跨 partition 全域順序</h3>
<p>consumer 假設事件按全域時間到達、做了依賴順序的邏輯（例如「先建立帳號事件、後消費事件」）。Change Feed 只保證 per logical partition 有序、跨 partition 交錯。徵兆是偶發的「後續事件先到、依賴的前置事件後到」。修法是讓有順序依賴的 document 落在同一 partition key、或在 consumer 端用業務 timestamp / version 做排序與 buffer、不依賴 feed 到達順序。</p>
<h3 id="anti-recommendation不是所有寫入後工作都要-change-feed">Anti-recommendation：不是所有「寫入後工作」都要 Change Feed</h3>
<p>寫入後若只是同一 request 內、同一 partition 的小量同步工作、直接在 application 寫入路徑處理、或用 stored procedure 在 partition 內做（見 <a href="../stored-procedure-trigger/">stored-procedure-trigger</a>）更簡單。Change Feed 的價值在 <em>解耦下游、可重放、水平擴展</em> — 當下游處理慢、會失敗、需要重放、或要被多個獨立 consumer 各自消費時才成立。下游工作輕、不需要重放、強耦合在寫入語義內時、引入 Change Feed + lease container 是多一層維運成本。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：Change Feed 消費 lag（最新寫入時間 vs consumer 已處理位置）、processor 每批 <code>changes</code> 數量、lease container 的 <code>NormalizedRUConsumption</code></li>
<li>consumer 端 throughput 受 partition range 數限制 — 並行度上限約等於 physical partition 數；range 不夠多時加 consumer instance 不會更快</li>
<li>成本：Change Feed 讀取本身吃 RU、all-versions-and-deletes 模式事件量更大、lease container 額外 RU — 三項都進容量公式、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a></li>
<li>回 <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>：把 Change Feed consumer 當獨立 throughput 單位、不要跟 OLTP 寫入共用同一個 RU budget 估算</li>
<li>Alert：消費 lag 持續增長（consumer 跟不上寫入）、lease container 429、handler 例外率上升</li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../stored-procedure-trigger/">stored-procedure-trigger</a>（partition 內同步邏輯 vs Change Feed 的非同步解耦）、<a href="../synapse-link-federation/">synapse-link-federation</a>（分析 workload 用 analytical store、不要用 Change Feed 自己搭 analytics pipeline）、<a href="../partition-key-design/">partition-key-design</a>（per-partition 順序的來源）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（Change Feed + lease container 的 RU 成本）</li>
<li>跟 DynamoDB Streams 對照：兩者都是 partition-ordered 變更 log + at-least-once consumer。差異在 DynamoDB Streams 有固定 24 小時 retention、原生發 INSERT / MODIFY / REMOVE（含 delete）；Cosmos DB latest-version 模式預設不發 delete、要 all-versions-and-deletes 模式才有完整事件與 delete。從 DynamoDB Streams 思維過來的 team 容易假設「delete 一定看得到」、要先確認模式。對照 <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></li>
<li>Knowledge card：<a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture</a> / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a></li>
<li>回 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 overview</a> 的「忽略 Change Feed」常見陷阱</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> — 本文是該頁尾 Change Feed backlog 的深度展開</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> — 高更新頻率 catalog 投影壓力的情境 anchor</li>
<li><a href="../stored-procedure-trigger/">stored-procedure-trigger</a> — partition 內同步邏輯的對照</li>
<li><a href="../partition-key-design/">partition-key-design</a> — per-partition 順序的設計來源</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> — DynamoDB Streams 對照</li>
<li><a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">Change Data Capture 卡片</a> / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/change-feed">Change feed in Azure Cosmos DB</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/nosql/change-feed-processor">Change feed processor</a></li>
</ul>
]]></content:encoded></item></channel></rss>