<?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>Isolation on Tarragon</title><link>https://tarrragon.github.io/blog/tags/isolation/</link><description>Recent content in Isolation on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/isolation/index.xml" rel="self" type="application/rss+xml"/><item><title>Sandbox</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/sandbox/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/sandbox/</guid><description>&lt;p>Sandbox 的核心概念是「把程式跑在權限受限的隔離環境、限制檔案存取、網路連線、系統呼叫的範圍」。在 LLM 場景下、sandbox 用來控制 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/tool-use/" data-link-title="Tool Use" data-link-desc="LLM 透過結構化呼叫外部工具（讀檔、查資料庫、發 API request）來擴展能力的設計、function calling 跟 MCP 是常見實作">tool use&lt;/a> 跟 MCP server 的副作用範圍：即使 LLM 被 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prompt-injection/" data-link-title="Prompt Injection" data-link-desc="把惡意指令藏進 LLM 會讀到的內容、誘導 LLM 跑出非開發者預期行為的攻擊類別、OWASP LLM01 列入頭號威脅">prompt injection&lt;/a> 誘導跑惡意 tool、sandbox 能限制最壞情況的影響面。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>常見的 sandbox 技術光譜（依隔離強度跟工程成本）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>技術&lt;/th>
 &lt;th>隔離強度&lt;/th>
 &lt;th>工程成本&lt;/th>
 &lt;th>LLM 場景的典型用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>不同 OS user&lt;/td>
 &lt;td>中（檔案權限）&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>個人 dev 跑 MCP server&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Docker container&lt;/td>
 &lt;td>中高&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>跑第三方 MCP server、隔離 LLM agent&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VM / Firecracker / gVisor&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>中高&lt;/td>
 &lt;td>production 多租戶 LLM agent&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>chroot / namespace&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>限定 filesystem 視角&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>seccomp / AppArmor / SELinux&lt;/td>
 &lt;td>高（syscall 層）&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>細粒度限制 syscall&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Web Worker / V8 isolate&lt;/td>
 &lt;td>中（JavaScript 層）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>LLM 跑 user-provided JavaScript&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Sandbox 在 LLM 場景的常見配置：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>個人 dev&lt;/strong>：用獨立 OS user 跑 MCP server、限制檔案存取到 workspace；或用 Docker。&lt;/li>
&lt;li>&lt;strong>production agent&lt;/strong>：每個 user / session 一個 ephemeral container、跑完就 destroy。&lt;/li>
&lt;li>&lt;strong>code execution tool&lt;/strong>：把 LLM 生成的 code 丟進 sandbox 跑（如 OpenAI Code Interpreter、Anthropic Claude Code Tool）。&lt;/li>
&lt;/ol>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>理解 sandbox 後可以解釋兩個現象：為什麼跑第三方 MCP server 前 sandbox 是基本配置（MCP 是可執行程式碼、權限上限是「跑該 server 的 user 的權限」）、為什麼 production 場景的 code execution tool 必定在 ephemeral sandbox 內跑（避免長期 state 跟跨 user 殘留）。&lt;/p>
&lt;p>設計 LLM application 時、sandbox 跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/tool-use/" data-link-title="Tool Use" data-link-desc="LLM 透過結構化呼叫外部工具（讀檔、查資料庫、發 API request）來擴展能力的設計、function calling 跟 MCP 是常見實作">tool use&lt;/a> 的白名單是兩個獨立的防護層、建議都做：白名單擋已知範圍、sandbox 擋未預期的副作用。詳見 &lt;a href="https://tarrragon.github.io/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2 tool use 與 MCP server 的權限模型&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Sandbox 的核心概念是「把程式跑在權限受限的隔離環境、限制檔案存取、網路連線、系統呼叫的範圍」。在 LLM 場景下、sandbox 用來控制 <a href="/blog/llm/knowledge-cards/tool-use/" data-link-title="Tool Use" data-link-desc="LLM 透過結構化呼叫外部工具（讀檔、查資料庫、發 API request）來擴展能力的設計、function calling 跟 MCP 是常見實作">tool use</a> 跟 MCP server 的副作用範圍：即使 LLM 被 <a href="/blog/llm/knowledge-cards/prompt-injection/" data-link-title="Prompt Injection" data-link-desc="把惡意指令藏進 LLM 會讀到的內容、誘導 LLM 跑出非開發者預期行為的攻擊類別、OWASP LLM01 列入頭號威脅">prompt injection</a> 誘導跑惡意 tool、sandbox 能限制最壞情況的影響面。</p>
<h2 id="概念位置">概念位置</h2>
<p>常見的 sandbox 技術光譜（依隔離強度跟工程成本）：</p>
<table>
  <thead>
      <tr>
          <th>技術</th>
          <th>隔離強度</th>
          <th>工程成本</th>
          <th>LLM 場景的典型用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不同 OS user</td>
          <td>中（檔案權限）</td>
          <td>低</td>
          <td>個人 dev 跑 MCP server</td>
      </tr>
      <tr>
          <td>Docker container</td>
          <td>中高</td>
          <td>中</td>
          <td>跑第三方 MCP server、隔離 LLM agent</td>
      </tr>
      <tr>
          <td>VM / Firecracker / gVisor</td>
          <td>高</td>
          <td>中高</td>
          <td>production 多租戶 LLM agent</td>
      </tr>
      <tr>
          <td>chroot / namespace</td>
          <td>中</td>
          <td>中</td>
          <td>限定 filesystem 視角</td>
      </tr>
      <tr>
          <td>seccomp / AppArmor / SELinux</td>
          <td>高（syscall 層）</td>
          <td>高</td>
          <td>細粒度限制 syscall</td>
      </tr>
      <tr>
          <td>Web Worker / V8 isolate</td>
          <td>中（JavaScript 層）</td>
          <td>中</td>
          <td>LLM 跑 user-provided JavaScript</td>
      </tr>
  </tbody>
</table>
<p>Sandbox 在 LLM 場景的常見配置：</p>
<ol>
<li><strong>個人 dev</strong>：用獨立 OS user 跑 MCP server、限制檔案存取到 workspace；或用 Docker。</li>
<li><strong>production agent</strong>：每個 user / session 一個 ephemeral container、跑完就 destroy。</li>
<li><strong>code execution tool</strong>：把 LLM 生成的 code 丟進 sandbox 跑（如 OpenAI Code Interpreter、Anthropic Claude Code Tool）。</li>
</ol>
<h2 id="設計責任">設計責任</h2>
<p>理解 sandbox 後可以解釋兩個現象：為什麼跑第三方 MCP server 前 sandbox 是基本配置（MCP 是可執行程式碼、權限上限是「跑該 server 的 user 的權限」）、為什麼 production 場景的 code execution tool 必定在 ephemeral sandbox 內跑（避免長期 state 跟跨 user 殘留）。</p>
<p>設計 LLM application 時、sandbox 跟 <a href="/blog/llm/knowledge-cards/tool-use/" data-link-title="Tool Use" data-link-desc="LLM 透過結構化呼叫外部工具（讀檔、查資料庫、發 API request）來擴展能力的設計、function calling 跟 MCP 是常見實作">tool use</a> 的白名單是兩個獨立的防護層、建議都做：白名單擋已知範圍、sandbox 擋未預期的副作用。詳見 <a href="/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2 tool use 與 MCP server 的權限模型</a>。</p>
]]></content:encoded></item><item><title>Bulkhead 隔離</title><link>https://tarrragon.github.io/blog/devops/03-traffic-management/bulkhead/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/03-traffic-management/bulkhead/</guid><description>&lt;p>Bulkhead 的概念來自船舶的隔艙設計 — 船體分成多個獨立的水密隔艙，一個隔艙進水不會讓整艘船沉沒。服務設計中，bulkhead 把不同的工作負載隔離到各自的資源池，一個工作負載的過載或故障不會消耗其他工作負載的資源。&lt;/p>
&lt;h2 id="隔離什麼">隔離什麼&lt;/h2>
&lt;p>服務中的共享資源是 bulkhead 的隔離對象：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>共享資源&lt;/th>
 &lt;th>不隔離時的風險&lt;/th>
 &lt;th>隔離方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Goroutine/Thread&lt;/td>
 &lt;td>一個慢查詢佔住所有 goroutine，整個服務不回應&lt;/td>
 &lt;td>每類工作分配獨立的 goroutine pool&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料庫連線&lt;/td>
 &lt;td>一個大查詢佔住 connection pool，其他查詢排隊&lt;/td>
 &lt;td>不同工作類型用不同的連線池&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>記憶體&lt;/td>
 &lt;td>一個功能的 buffer 無限增長，OOM 殺掉整個 process&lt;/td>
 &lt;td>每個功能的 buffer 有獨立上限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CPU&lt;/td>
 &lt;td>一個計算密集任務佔滿 CPU，其他請求延遲&lt;/td>
 &lt;td>cgroup 或 GOMAXPROCS 限制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="實作模式">實作模式&lt;/h2>
&lt;h3 id="獨立-goroutine-pool">獨立 Goroutine Pool&lt;/h3>
&lt;p>Go 中用有限容量的 channel 模擬 goroutine pool：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ingestPool&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kd">struct&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// ingestion 最多 100 goroutine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">queryPool&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kd">struct&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// query 最多 20 goroutine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">rulePool&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kd">struct&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// rule engine 最多 10 goroutine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">handleIngest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">ingestPool&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="kd">struct&lt;/span>&lt;span class="p">{}{}:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ingestPool&lt;/span> &lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nf">processIngest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;ingestion overloaded&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusServiceUnavailable&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ingestion 的 100 個 goroutine 全部被佔用時，新的 ingestion 請求被拒絕（503），但 query 和 rule engine 的 goroutine 不受影響。&lt;/p>
&lt;h3 id="獨立-connection-pool">獨立 Connection Pool&lt;/h3>
&lt;p>資料庫連線池按工作類型分開：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// driver name 依實際使用的 driver 而定（modernc.org/sqlite 用 &amp;#34;sqlite&amp;#34;、mattn/go-sqlite3 用 &amp;#34;sqlite3&amp;#34;）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">ingestDB&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">sql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;sqlite&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;events.db&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">ingestDB&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetMaxOpenConns&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// ingestion 專用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nx">queryDB&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">sql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;sqlite&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;events.db&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">queryDB&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetMaxOpenConns&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// query 專用&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>SQLite 的特殊情況：SQLite 是檔案級鎖定，多個連線池打開同一個檔案時仍共享 write lock。連線池隔離在 SQLite 上主要隔離的是 Go 層的 goroutine 等待，不是 DB 層的鎖定。PostgreSQL 的連線池隔離則是真正的資源隔離。&lt;/p>
&lt;h2 id="容量分配">容量分配&lt;/h2>
&lt;p>Bulkhead 的每個隔艙分配多少資源是設計決策。分配依據是「這個工作負載的優先順序和預期併發量」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工作負載&lt;/th>
 &lt;th>優先順序&lt;/th>
 &lt;th>預期併發&lt;/th>
 &lt;th>分配&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Ingestion&lt;/td>
 &lt;td>高（不能丟事件）&lt;/td>
 &lt;td>高（多 SDK 同時 flush）&lt;/td>
 &lt;td>60%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query&lt;/td>
 &lt;td>中（dashboard 查詢）&lt;/td>
 &lt;td>低（dashboard 定期刷新）&lt;/td>
 &lt;td>25%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rule engine&lt;/td>
 &lt;td>低（觸發可延遲）&lt;/td>
 &lt;td>低（規則命中是少數事件）&lt;/td>
 &lt;td>15%&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>分配比例不需要精確 — 重點是每個隔艙有獨立的上限，而非共享一個無差別的總上限。&lt;/p>
&lt;h2 id="監控系統的應用">監控系統的應用&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">Collector&lt;/a> 同時承載 ingestion（接收事件）、query（dashboard 查詢）和 rule engine（規則評估）三種工作。不隔離時，一個複雜的 dashboard 查詢（full table scan）可能佔住所有資料庫連線，讓 ingestion 的寫入也排隊等待。&lt;/p>
&lt;p>Bulkhead 設計讓 ingestion 和 query 各自的過載互不影響：&lt;/p></description><content:encoded><![CDATA[<p>Bulkhead 的概念來自船舶的隔艙設計 — 船體分成多個獨立的水密隔艙，一個隔艙進水不會讓整艘船沉沒。服務設計中，bulkhead 把不同的工作負載隔離到各自的資源池，一個工作負載的過載或故障不會消耗其他工作負載的資源。</p>
<h2 id="隔離什麼">隔離什麼</h2>
<p>服務中的共享資源是 bulkhead 的隔離對象：</p>
<table>
  <thead>
      <tr>
          <th>共享資源</th>
          <th>不隔離時的風險</th>
          <th>隔離方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Goroutine/Thread</td>
          <td>一個慢查詢佔住所有 goroutine，整個服務不回應</td>
          <td>每類工作分配獨立的 goroutine pool</td>
      </tr>
      <tr>
          <td>資料庫連線</td>
          <td>一個大查詢佔住 connection pool，其他查詢排隊</td>
          <td>不同工作類型用不同的連線池</td>
      </tr>
      <tr>
          <td>記憶體</td>
          <td>一個功能的 buffer 無限增長，OOM 殺掉整個 process</td>
          <td>每個功能的 buffer 有獨立上限</td>
      </tr>
      <tr>
          <td>CPU</td>
          <td>一個計算密集任務佔滿 CPU，其他請求延遲</td>
          <td>cgroup 或 GOMAXPROCS 限制</td>
      </tr>
  </tbody>
</table>
<h2 id="實作模式">實作模式</h2>
<h3 id="獨立-goroutine-pool">獨立 Goroutine Pool</h3>
<p>Go 中用有限容量的 channel 模擬 goroutine pool：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ingestPool</span> <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{},</span> <span class="mi">100</span><span class="p">)</span>  <span class="c1">// ingestion 最多 100 goroutine</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">queryPool</span>  <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{},</span> <span class="mi">20</span><span class="p">)</span>   <span class="c1">// query 最多 20 goroutine</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">rulePool</span>   <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{},</span> <span class="mi">10</span><span class="p">)</span>   <span class="c1">// rule engine 最多 10 goroutine</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">func</span> <span class="nf">handleIngest</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">case</span> <span class="nx">ingestPool</span> <span class="o">&lt;-</span> <span class="kd">struct</span><span class="p">{}{}:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> <span class="o">&lt;-</span><span class="nx">ingestPool</span> <span class="p">}()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nf">processIngest</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;ingestion overloaded&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusServiceUnavailable</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Ingestion 的 100 個 goroutine 全部被佔用時，新的 ingestion 請求被拒絕（503），但 query 和 rule engine 的 goroutine 不受影響。</p>
<h3 id="獨立-connection-pool">獨立 Connection Pool</h3>
<p>資料庫連線池按工作類型分開：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// driver name 依實際使用的 driver 而定（modernc.org/sqlite 用 &#34;sqlite&#34;、mattn/go-sqlite3 用 &#34;sqlite3&#34;）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">ingestDB</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="s">&#34;sqlite&#34;</span><span class="p">,</span> <span class="s">&#34;events.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">ingestDB</span><span class="p">.</span><span class="nf">SetMaxOpenConns</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="c1">// ingestion 專用</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="nx">queryDB</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="s">&#34;sqlite&#34;</span><span class="p">,</span> <span class="s">&#34;events.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">queryDB</span><span class="p">.</span><span class="nf">SetMaxOpenConns</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>   <span class="c1">// query 專用</span></span></span></code></pre></div><p>SQLite 的特殊情況：SQLite 是檔案級鎖定，多個連線池打開同一個檔案時仍共享 write lock。連線池隔離在 SQLite 上主要隔離的是 Go 層的 goroutine 等待，不是 DB 層的鎖定。PostgreSQL 的連線池隔離則是真正的資源隔離。</p>
<h2 id="容量分配">容量分配</h2>
<p>Bulkhead 的每個隔艙分配多少資源是設計決策。分配依據是「這個工作負載的優先順序和預期併發量」：</p>
<table>
  <thead>
      <tr>
          <th>工作負載</th>
          <th>優先順序</th>
          <th>預期併發</th>
          <th>分配</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ingestion</td>
          <td>高（不能丟事件）</td>
          <td>高（多 SDK 同時 flush）</td>
          <td>60%</td>
      </tr>
      <tr>
          <td>Query</td>
          <td>中（dashboard 查詢）</td>
          <td>低（dashboard 定期刷新）</td>
          <td>25%</td>
      </tr>
      <tr>
          <td>Rule engine</td>
          <td>低（觸發可延遲）</td>
          <td>低（規則命中是少數事件）</td>
          <td>15%</td>
      </tr>
  </tbody>
</table>
<p>分配比例不需要精確 — 重點是每個隔艙有獨立的上限，而非共享一個無差別的總上限。</p>
<h2 id="監控系統的應用">監控系統的應用</h2>
<p><a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">Collector</a> 同時承載 ingestion（接收事件）、query（dashboard 查詢）和 rule engine（規則評估）三種工作。不隔離時，一個複雜的 dashboard 查詢（full table scan）可能佔住所有資料庫連線，讓 ingestion 的寫入也排隊等待。</p>
<p>Bulkhead 設計讓 ingestion 和 query 各自的過載互不影響：</p>
<ul>
<li>Ingestion 的 goroutine pool 滿了 → SDK 收到 429 → 離線 buffer 接手</li>
<li>Query 的 goroutine pool 滿了 → dashboard 暫時顯示 loading → 不影響 ingestion</li>
<li>Rule engine 的 goroutine pool 滿了 → 規則評估延遲 → 不影響事件接收和查詢</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>背壓的流量控制 → <a href="/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &#43; 回壓訊號的設計、和 rate limit 的區別">背壓機制</a></li>
<li>依賴失敗的快速失敗 → <a href="/blog/devops/03-traffic-management/circuit-breaker/" data-link-title="熔斷器" data-link-desc="依賴服務失敗時怎麼快速失敗而非拖慢自己 — 三狀態模型（closed → open → half-open）和熔斷判斷條件">熔斷器</a></li>
<li>突發流量時的綜合策略 → <a href="/blog/devops/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">模組七 突發流量</a></li>
</ul>
]]></content:encoded></item><item><title>CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 PostgreSQL wire 相容定位、本文聚焦 &lt;em>serializable default 對 application transaction contract 的重塑&lt;/em>。&lt;/p>
&lt;p>&lt;strong>Scope warning（最高、F4 Frame 2）&lt;/strong>：&lt;strong>本篇整篇是跨 case 合成 frame、不是單一 case 揭露&lt;/strong>。3 個 CockroachDB direct case（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a>）對 application transaction retry contract 重塑的揭露 &lt;em>都偏弱&lt;/em> — DoorDash case 只寫 PostgreSQL wire &lt;em>protocol-level&lt;/em> 相容、SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」、&lt;strong>沒&lt;/strong>直接寫 &lt;code>40001 serialization_failure&lt;/code> / &lt;code>SAVEPOINT cockroach_restart&lt;/code> / hot row contention / retry loop pattern。Netflix / Hard Rock case 完全沒寫 retry pattern。本章 retry pattern 議題從 Cockroach Labs 官方 SQL Layer docs + PG → CockroachDB 通用 contract 重塑視角合成、DoorDash 只作為 trigger context（撞牆訊號 + 觸發遷移）、不是 ground truth case study。讀者引用本章內容到實際系統前、應該 &lt;em>自己跑 application audit&lt;/em> 而不是直接套合成的 pattern。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境從-pg-read-committed-遷到-cockroachdb-serializable-的-application-衝擊">問題情境：從 PG READ COMMITTED 遷到 CockroachDB SERIALIZABLE 的 application 衝擊&lt;/h2>
&lt;p>團隊從 PostgreSQL（default &lt;code>READ COMMITTED&lt;/code>）遷到 CockroachDB（default &lt;code>SERIALIZABLE&lt;/code>）、上線後 application transaction retry 突然爆增、user-facing latency p99 高 5 倍、error rate 顯著上升。Driver 不會自動 retry — 應用層必須認得 &lt;code>40001 serialization_failure&lt;/code> 並包 retry loop with exponential backoff。沒包就是直接拋例外給用戶。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 PostgreSQL wire 相容定位、本文聚焦 <em>serializable default 對 application transaction contract 的重塑</em>。</p>
<p><strong>Scope warning（最高、F4 Frame 2）</strong>：<strong>本篇整篇是跨 case 合成 frame、不是單一 case 揭露</strong>。3 個 CockroachDB direct case（<a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a> / <a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> / <a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>）對 application transaction retry contract 重塑的揭露 <em>都偏弱</em> — DoorDash case 只寫 PostgreSQL wire <em>protocol-level</em> 相容、SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」、<strong>沒</strong>直接寫 <code>40001 serialization_failure</code> / <code>SAVEPOINT cockroach_restart</code> / hot row contention / retry loop pattern。Netflix / Hard Rock case 完全沒寫 retry pattern。本章 retry pattern 議題從 Cockroach Labs 官方 SQL Layer docs + PG → CockroachDB 通用 contract 重塑視角合成、DoorDash 只作為 trigger context（撞牆訊號 + 觸發遷移）、不是 ground truth case study。讀者引用本章內容到實際系統前、應該 <em>自己跑 application audit</em> 而不是直接套合成的 pattern。</p></blockquote>
<hr>
<h2 id="問題情境從-pg-read-committed-遷到-cockroachdb-serializable-的-application-衝擊">問題情境：從 PG READ COMMITTED 遷到 CockroachDB SERIALIZABLE 的 application 衝擊</h2>
<p>團隊從 PostgreSQL（default <code>READ COMMITTED</code>）遷到 CockroachDB（default <code>SERIALIZABLE</code>）、上線後 application transaction retry 突然爆增、user-facing latency p99 高 5 倍、error rate 顯著上升。Driver 不會自動 retry — 應用層必須認得 <code>40001 serialization_failure</code> 並包 retry loop with exponential backoff。沒包就是直接拋例外給用戶。</p>
<p>讀者常問：</p>
<ul>
<li>為什麼同樣的 transaction 在 CockroachDB 一直 retry、在 PostgreSQL 從來不會？</li>
<li><code>40001 serialization_failure</code> error 怎麼處理、能不能直接 swallow？</li>
<li>我要把所有 application transaction 都改成 retry loop 包起來嗎？</li>
<li>能不能改 isolation level 回 <code>READ COMMITTED</code>、放棄 serializable 保證？</li>
</ul>
<p>四題的回答都依賴一個前提：CockroachDB 的 application transaction contract 跟 PostgreSQL default 不一樣、必須重塑。</p>
<h3 id="scope-warning-explicit-labeldoordash-case-沒揭露-retry-pattern">Scope warning explicit label：DoorDash case 沒揭露 retry pattern</h3>
<p><strong>DoorDash case 沒直接揭露 serializable retry contract / 40001 / SAVEPOINT pattern / hot row contention</strong>。case 只寫「PostgreSQL wire protocol 相容、實際 SQL 行為（serializable default、retry semantics、partial index）<em>仍要驗證</em>」（DoorDash 觀察段 / 策略段 3、F4.4）。</p>
<p>本章 retry pattern 議題是從 PG → CockroachDB 通用 contract 重塑視角合成、不是 DoorDash case 直接揭露。引用 DoorDash 時應該用：</p>
<ul>
<li><strong>正確口徑</strong>：「DoorDash 揭露 Aurora Postgres 1.636 M QPS 撞牆 → 引出 distributed SQL retry contract 需求、本章 retry pattern 議題是從 PostgreSQL → CockroachDB 通用 contract 重塑視角合成、不是 DoorDash case 直接揭露」</li>
<li><strong>不要寫成</strong>：「DoorDash retry pattern」、「DoorDash 揭露 40001 處理」之類把合成包成 case fact 的語法</li>
</ul>
<h3 id="case-anchortrigger-context不是-ground-truth">Case anchor（trigger context、不是 ground truth）</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>：提供「PG wire 相容、SQL 行為仍要 audit」的 case 警語（F4.4）、作為本章 <em>為什麼 retry contract 要重塑</em> 的觸發訊號。retry pattern 本體走 standard-driven（Cockroach Labs 官方 SQL Layer docs + Transaction Retry docs）</li>
</ul>
<p>Sibling 對照 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora financial ledger</a> 提供 <em>PostgreSQL READ COMMITTED + Aurora</em> 的另一條路徑 — 用 application-level sharding（200 個獨立 Aurora cluster）避開 retry、而不是處理 retry。<strong>Scope warning</strong>：DraftKings case <em>沒</em> 寫 PostgreSQL READ COMMITTED retry pattern、case 是 Aurora 內 business sharding 路徑。本章引用 DraftKings 為「假想若把 DraftKings 遷 CockroachDB 會撞到 retry contract 重塑」合成對照、不是 case 直接揭露。</p>
<h2 id="核心機制serializable-default-跟-postgresql-的差異">核心機制：serializable default 跟 PostgreSQL 的差異</h2>
<blockquote>
<p><strong>來源分層</strong>：本段機制來源是 Cockroach Labs 官方 SQL Layer docs + Transaction Retry docs（standard-driven）、<em>不是</em> 從 case 抽取。3 個 direct case 都沒揭露這些機制細節。</p></blockquote>
<h3 id="serializable-是-cockroachdb-的-default">Serializable 是 CockroachDB 的 default</h3>
<p>CockroachDB 預設 <code>SERIALIZABLE</code> — 最強 isolation level、保證 transaction 結果等同某個 serial order（即所有 transaction 像逐個按順序執行）。對比：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PostgreSQL default</th>
          <th>CockroachDB default</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Isolation</td>
          <td>READ COMMITTED</td>
          <td>SERIALIZABLE</td>
      </tr>
      <tr>
          <td>衝突處理</td>
          <td>後 writer 等 lock</td>
          <td>衝突即 abort、丟 40001</td>
      </tr>
      <tr>
          <td>機制</td>
          <td>row lock + MVCC</td>
          <td>timestamp ordering + write intent</td>
      </tr>
      <tr>
          <td>Retry 必要性</td>
          <td>通常不需要</td>
          <td>application 必須有 retry loop</td>
      </tr>
      <tr>
          <td>SSI 對應</td>
          <td>PG SSI（opt-in）</td>
          <td>預設啟用</td>
      </tr>
  </tbody>
</table>
<h3 id="conflict-detectionread--write-set-衝突就-abort">Conflict detection：read / write set 衝突就 abort</h3>
<p>CockroachDB 追蹤每個 transaction 的 read set 跟 write set。當兩個並行 transaction 的 read / write set 衝突、CockroachDB abort 後到的那個、發 <a href="/blog/backend/knowledge-cards/serialization-failure/" data-link-title="Serialization Failure" data-link-desc="SERIALIZABLE isolation 衝突偵測後 abort 的協議、SQL state 40001、application 必須包 retry loop">Serialization Failure</a>（<code>40001 serialization_failure</code>）。</p>
<p>對比 PostgreSQL serializable（SSI）：兩者都是「post-detect」、commit 時偵測 anomaly、不是 pre-lock。差別在 <em>衝突偵測時機</em> 跟 <em>成本</em>：</p>
<ul>
<li>PostgreSQL SSI：用 predicate lock 追蹤 query 條件、commit 時偵測</li>
<li>CockroachDB：用 timestamp ordering + write intent、衝突 <em>當下</em> 就 abort</li>
</ul>
<p>CockroachDB 的成本在「衝突立刻 abort 不等 commit」、好處是「retry window 較短、不會跑完整個 transaction 才發現衝突」。</p>
<h3 id="application-端-retrydriver-不自動處理">Application 端 retry：driver 不自動處理</h3>
<p>關鍵：<strong>CockroachDB driver 不自動 retry</strong>。application 收到 <code>40001 serialization_failure</code> 必須自己決定怎麼處理 — exponential backoff retry、circuit break、或拋給上層。</p>
<p>對比 PostgreSQL：PostgreSQL READ COMMITTED 幾乎不會丟 serialization failure（後 writer 等 lock 不 abort）、SERIALIZABLE 才會、但多數 application 沒走 SERIALIZABLE。CockroachDB <em>預設</em> 就是 SERIALIZABLE、所以 retry loop 是 <em>必要</em>、不是 optional。</p>
<h3 id="savepoint-pattern官方推薦寫法">Savepoint pattern：官方推薦寫法</h3>
<p>Cockroach Labs 官方推薦的 retry pattern 用 <code>SAVEPOINT cockroach_restart</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">SAVEPOINT</span><span class="w"> </span><span class="n">cockroach_restart</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 做正常 transaction 工作
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="n">RELEASE</span><span class="w"> </span><span class="n">SAVEPOINT</span><span class="w"> </span><span class="n">cockroach_restart</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- 如果中途 40001：
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">-- ROLLBACK TO SAVEPOINT cockroach_restart;
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">-- 重新跑 transaction body、再 RELEASE + COMMIT</span></span></span></code></pre></div><p><code>cockroach_restart</code> 是特殊保留 savepoint name — CockroachDB 認得這個名字、會把 <code>ROLLBACK TO SAVEPOINT cockroach_restart</code> 視為「重啟整個 transaction」而不是部分 rollback。</p>
<h3 id="read-committed-是-v232-可選降級">READ COMMITTED 是 v23.2+ 可選降級</h3>
<p>CockroachDB v23.2+ 新增 <code>READ COMMITTED</code> isolation level — application 可選擇用 weaker isolation 換少 retry。但這是「降級」、失去 serializable 保證 — 對應的反例段在失敗模式段展開（金融 ledger 走 READ COMMITTED 可能讓 balance 變負）。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a> 跟 <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a>。</p>
<h3 id="doordash-case-對接點trigger-context-only">DoorDash case 對接點（trigger context only）</h3>
<p>DoorDash case 揭露 PG wire <em>protocol-level</em> 相容、明示 SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」（F4.4）。本章機制段就是回答「audit 什麼」的具體展開 — 但 audit checklist 本體屬通用工程知識、case 沒 ground truth。</p>
<p>引用紀律：「DoorDash 揭露 PG wire 相容、SQL 行為仍要 audit、其中 serializable default 跟 retry semantics 是 application contract 重塑的核心議題」— 把 case 揭露的 fact 跟本章合成的 frame 分開講。</p>
<h2 id="操作流程retry-loop-設計">操作流程：retry loop 設計</h2>
<h3 id="retry-loop-偽碼">Retry loop 偽碼</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">for</span> <span class="nx">attempt</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">attempt</span> <span class="p">&lt;</span> <span class="nx">MAX_RETRIES</span><span class="p">;</span> <span class="nx">attempt</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">tx</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">Begin</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">err</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="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="s">&#34;SAVEPOINT cockroach_restart&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">();</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</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">// ... 跑 transaction body ...</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="s">&#34;RELEASE SAVEPOINT cockroach_restart&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="c1">// 成功</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="nf">isSerializationFailure</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// SQLSTATE == &#34;40001&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nx">backoff</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Duration</span><span class="p">(</span><span class="nx">math</span><span class="p">.</span><span class="nf">Pow</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">attempt</span><span class="p">)))</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nx">backoff</span> <span class="o">+</span> <span class="nf">jitter</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span> <span class="c1">// 非 retry-able error</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="k">return</span> <span class="nx">ErrMaxRetriesExceeded</span></span></span></code></pre></div><p>關鍵點：</p>
<ul>
<li>exponential backoff with jitter（避免 retry storm 同步）</li>
<li>max retry 上限（避免無限 loop、要有 circuit breaker）</li>
<li>只 retry serialization failure、其他 error 直接拋</li>
<li>transaction body 必須是 <em>冪等</em> 的（同樣 input 多次執行結果一致）</li>
</ul>
<h3 id="配置">配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 改 transaction isolation level（v23.2+ 才支援 READ COMMITTED）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">READ</span><span class="w"> </span><span class="k">COMMITTED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 看當前 session 預設
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">SESSION</span><span class="w"> </span><span class="n">default_transaction_isolation</span><span class="p">;</span></span></span></code></pre></div><h3 id="驗證點">驗證點</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看 transaction retry 統計
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">txn_stats</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 看哪些 query / table 衝突最多
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">cluster_contention_events</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">count</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><h3 id="idempotency-設計transaction-body-必須冪等">Idempotency 設計：transaction body 必須冪等</h3>
<p>retry-safe transaction body 必須冪等 — 同樣 input 多次執行結果一致。這是 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 在 distributed SQL retry contract 下的具體展開、不是 optional：</p>
<table>
  <thead>
      <tr>
          <th>Transaction body</th>
          <th>是否冪等</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>UPDATE balance SET balance = balance - 100</code></td>
          <td>是</td>
          <td>同樣 input 每次都減 100</td>
      </tr>
      <tr>
          <td><code>UPDATE balance SET balance = 900</code></td>
          <td>是</td>
          <td>設成絕對值、retry 不影響</td>
      </tr>
      <tr>
          <td><code>INSERT INTO logs VALUES (...)</code></td>
          <td>否</td>
          <td>retry 後重複寫、要加 UNIQUE constraint</td>
      </tr>
      <tr>
          <td><code>INSERT ON CONFLICT (id) DO NOTHING</code></td>
          <td>是</td>
          <td>用 ON CONFLICT 處理重複</td>
      </tr>
      <tr>
          <td><code>UPDATE counter SET val = val + 1</code></td>
          <td>否（語意問題）</td>
          <td>retry 後加超過預期次數</td>
      </tr>
  </tbody>
</table>
<p>冪等性是 application 設計議題、不是 CockroachDB 配置可解的 — application contract 重塑的核心成本就在這。</p>
<h3 id="rollback-邊界">Rollback 邊界</h3>
<p>transaction 自身有 <code>SAVEPOINT cockroach_restart</code> 邊界、<code>ROLLBACK TO SAVEPOINT</code> 後可重試整個 transaction body。但：</p>
<ul>
<li>commit 後不可回滾 — 業務狀態還原只能新交易補償</li>
<li>application 端如果在 transaction <em>外</em> cache state、retry 後 state 不一致（見失敗模式段）</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="retry-stormcontention-嚴重時-cpu-雪崩">Retry storm：contention 嚴重時 CPU 雪崩</h3>
<p>當高頻寫入撞同一 row（例：全局 counter、熱門商品 inventory）、serializable 衝突率可能 100%、application 端 retry loop 不斷重跑、CPU 雪崩。</p>
<p>修法：</p>
<ul>
<li>Max retry 上限 + circuit breaker：超過就放棄、回 5xx 給 client、避免 retry storm 拖垮 cluster</li>
<li>改 schema 避開 hot row（partition by region、shard counter、用 sequence 代替全局 counter）</li>
<li>監控 <code>crdb_internal.cluster_contention_events</code>、針對 top-N table 改設計</li>
</ul>
<h3 id="非冪等-transaction-重試double-count">非冪等 transaction 重試：double-count</h3>
<p>最危險的 production bug：transaction body 不是冪等的、retry 後資料重複寫。ledger double-count、payment 重複扣款、log 重複記錄。</p>
<p>修法：</p>
<ul>
<li>transaction body 寫成 <code>UPDATE balance SET balance = balance - X</code>（相對運算）、不寫 <code>UPDATE balance SET balance = Y</code>（絕對賦值依賴 read 結果）</li>
<li><code>INSERT</code> 加 UNIQUE constraint + <code>ON CONFLICT DO NOTHING</code></li>
<li>用 idempotency key（client 帶 UUID、server 端 dedupe）</li>
</ul>
<h3 id="cross-statement-state-假設">Cross-statement state 假設</h3>
<p>application 在 transaction <em>外</em> cache state（例：開 transaction 前 read 一個值、跑 transaction 期間用 cached 值）— retry 從 SAVEPOINT 重來時、cached state 不會重新讀、retry 後 state 不一致。</p>
<p>修法：</p>
<ul>
<li>把 cached state 改成在 transaction 內 read</li>
<li>retry loop 內 reset 所有 cached state</li>
<li>用 closure / scope 限制 cache 的生命週期到 transaction 內</li>
</ul>
<h3 id="hot-row-contention">Hot row contention</h3>
<p>高頻 update 同一 row（例：全局計數器、熱門商品庫存、世界冠軍直播觀眾數）— serializable 衝突率接近 100%、無論 retry 多少次都繼續衝突。</p>
<p>修法（schema-level、不是 application-level）：</p>
<ul>
<li>用 sequence 或 distributed counter（每節點本地 + 定期 aggregate）</li>
<li>partition by hash key、把單一 row 拆成 N 個 sub-row</li>
<li>改 <em>append-only</em> + 定期 aggregate（事件流 + materialized view）</li>
</ul>
<h3 id="改-read-committed-後忘了驗證業務語意">改 READ COMMITTED 後忘了驗證業務語意</h3>
<p>v23.2+ 可改 <code>READ COMMITTED</code>、少 retry 但失去 serializable 保證。對金融 ledger：READ COMMITTED 可能讓 balance 變負（兩個並行 withdraw 都看到 balance=100、都扣 50、結果 balance=-50）。</p>
<p>修法：</p>
<ul>
<li>金融 / 庫存 / 配額這類 <em>strict consistency</em> 場景必須留 SERIALIZABLE</li>
<li>READ COMMITTED 只用在 <em>容忍 stale read</em> 的場景（搜尋結果 / 分析 dashboard）</li>
<li>改 isolation level 前 <em>跑 application audit</em>、確認業務語意能容忍</li>
</ul>
<h3 id="long-running-transactionretry-機率隨時間線性上升">Long-running transaction：retry 機率隨時間線性上升</h3>
<p>transaction read 開始時間早、commit 時 conflict window 大、retry 機率隨 transaction duration 線性上升。</p>
<p>修法：</p>
<ul>
<li>transaction scope 縮小 — 只包必要 read / write、不要把 RPC call / external API 放 transaction 內</li>
<li>kill long-running query（<code>SHOW SESSIONS</code> + <code>CANCEL QUERY</code>）</li>
<li>把 batch update 拆成多個小 transaction、加 idempotency key</li>
</ul>
<h3 id="distributed-deadlock-跟-retry-互動">Distributed deadlock 跟 retry 互動</h3>
<p>CockroachDB 用 distributed deadlock detection（每個 node 維護 wait-for graph、定期跨 node 交換）跟 PostgreSQL local lock 表的 deadlock detection 不同。一般情況下、被 detector 選為 victim 的 transaction 會直接 abort、application retry loop 應該收到 <code>40001</code> 後重跑。但在三種 corner case 下會跟 retry loop 形成雪崩 pattern：</p>
<ul>
<li>多 transaction 同時撞同一組熱 row、deadlock detector 跨節點時間窗有 lag、多個 victim 同時 abort 後同時 retry、撞回同一個 deadlock window</li>
<li>跨節點的 distributed deadlock 偵測週期（預設 200ms+）放大 application retry latency、application 的 retry backoff 沒對齊偵測週期、形成「detect → abort → 快速 retry → 再 deadlock」迴圈</li>
<li>Application 把 deadlock victim 當 <code>40001</code> 直接 retry、不分流出來看、就難以從 metric 區分「serialization conflict retry」跟「distributed deadlock retry」、調 schema / contention 的策略會用錯方向</li>
</ul>
<p>修法（屬通用工程議題、case 未直接揭露）：</p>
<ul>
<li>Retry backoff 至少對齊 distributed deadlock 偵測週期、避免在偵測窗內快速 retry</li>
<li>加 jitter、不同 session 的 retry 不同步</li>
<li>Application metric 分桶記錄 <code>serialization_conflict_retry</code> vs <code>distributed_deadlock_retry</code>、避免 contention 改善方向判錯</li>
<li>Schema 設計階段避免「跨節點熱 row 環形依賴」（例：兩個服務交叉 update 對方的 counter row）</li>
</ul>
<h3 id="跨-case-合成-scope-warningdraftkings-對照">跨 case 合成 Scope warning：DraftKings 對照</h3>
<p>DraftKings ledger 對照 — <strong>DraftKings case 沒寫 PostgreSQL READ COMMITTED retry pattern</strong>、case 內容是「Aurora 內 business sharding 路徑」、用 200 個獨立 cluster 解 Aurora single-primary 撞牆。本章把 DraftKings 拿來當「假想若遷 CockroachDB 需改 SERIALIZABLE + retry loop」的合成對照、不是 case 揭露的 fact。</p>
<p>實際 DraftKings 走 Aurora + application sharding 而非 CockroachDB、所以「DraftKings retry pattern」這個說法本身就是合成 — 應該寫成「DraftKings 走 Aurora sharding 避開 retry contract 重塑、若改走 CockroachDB 則需處理本章描述的 application 改寫」。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Transaction retry rate</code>：per table、per session</li>
<li><code>Serialization failure rate</code>：絕對值 + ratio</li>
<li><code>Transaction duration p99</code>：long-running 是 retry 的根因之一</li>
<li><code>Hot ranges by retry count</code>：top contention 來源</li>
<li>Application metric：retry count per request、retry-induced latency p99、circuit breaker trip count</li>
</ul>
<h3 id="容量公式">容量公式</h3>
<ul>
<li>基底 QPS × (1 + avg retry count) = 實際 transaction load</li>
<li>例：1000 QPS、avg retry = 0.3 → 實際 cluster 處理 1300 transaction/s</li>
</ul>
<p>retry rate 是 <em>容量規劃必納入</em> 的變數 — 沒算 retry 就會 underestimate 真實 load。</p>
<h3 id="tuning">Tuning</h3>
<ul>
<li>reduce transaction scope：transaction 越短、conflict window 越小</li>
<li>kill long-running query：transaction 過長要主動截斷</li>
<li>partition hot rows：schema-level 解 hot contention</li>
<li>改 isolation 到 READ COMMITTED（如果業務語意允許）</li>
</ul>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 判斷 retry-bound vs CPU-bound</li>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> retry rate × baseline QPS</li>
<li><a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a></li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a></li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>：為什麼 serializable 是 distributed SQL 的合理 default</li>
<li><a href="../locality-aware-schema/">locality-aware schema</a>：partition 降低 hot row contention</li>
<li><a href="../survival-goals/">survival goals</a>：cross-region latency 加長 retry window</li>
</ul>
<h3 id="跟-postgresql-對照">跟 PostgreSQL 對照</h3>
<p>PostgreSQL READ COMMITTED 是 default、application 沒 retry loop 是 acceptable。遷 CockroachDB <em>必須</em> 重塑 application transaction contract — 這是 migration 階段最容易 underestimate 的成本。</p>
<p>對應 PostgreSQL MVCC + SSI 機制細節、見 <a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PostgreSQL MVCC + Lock Model</a>。</p>
<h3 id="migration-playbook">Migration playbook</h3>
<p>PG → CockroachDB 的 application audit 必看 transaction shape：</p>
<ul>
<li>每個 transaction 的 read / write set 預估衝突率</li>
<li>是否冪等（retry-safe）</li>
<li>transaction duration（long-running 是 retry 放大器）</li>
<li>業務語意能否容忍 READ COMMITTED（避開 retry 的 fallback）</li>
</ul>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> 上游 — distributed transaction 邊界</li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a></li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>純 read-only workload、無 contention</li>
<li>已用 PostgreSQL serializable（application contract 相似、遷移衝擊小）</li>
<li>用 CockroachDB v23.2+ READ COMMITTED 且業務允許 stale read</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>（trigger context — PG wire 相容警語）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a>（合成對照 — Aurora sharding 路徑）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PostgreSQL MVCC + Lock Model</a></li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a> / <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/transactions.html">CockroachDB Transactions</a> / <a href="https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference.html">Transaction Retry Error Reference</a> / <a href="https://www.cockroachlabs.com/docs/stable/read-committed.html">READ COMMITTED v23.2 announcement</a></li>
</ul>
]]></content:encoded></item><item><title>LLM 多租戶推論隔離</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/</guid><description>&lt;p>本章的責任是把 LLM 推論服務的多租戶隔離問題拆成可操作的判讀節點。LLM 服務的隔離議題在一般 multi-tenant 隔離（compute / network / data、見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant-boundary&lt;/a>）之上、多了 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a>（特別是 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">prefix cache&lt;/a> 重用）、prompt log、model artifact 訪問權三個 LLM-specific 層、本章聚焦這些差異。一般 multi-tenant 隔離原則沿用 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分授權邊界&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈&lt;/a>。&lt;/p>
&lt;h2 id="本章寫作邊界">本章寫作邊界&lt;/h2>
&lt;p>本章聚焦 production LLM 推論的多租戶 isolation 特殊性。team / 個人 dev 場景的「多人共用本地 server」見 &lt;a href="https://tarrragon.github.io/blog/llm/06-security/routing-to-production-security/" data-link-title="6.5 跨進 production 的 routing 中樞" data-link-desc="個人 dev → 團隊 → production LLM 服務的三層演化、跟 backend/07 對應卡片的 routing 清單">llm/6.5 跨進 production 的 routing 中樞&lt;/a>；通用 IAM / 服務間信任邊界見 7.2。&lt;/p>
&lt;h2 id="本章-threat-scope">本章 threat scope&lt;/h2>
&lt;p>&lt;strong>In-scope&lt;/strong>：KV cache 跨租戶洩漏、prompt log 隔離、模型 artifact 訪問權、batch 推論的順序敏感性、tenant-scoped rate limit、共用 GPU 上的記憶體殘留。&lt;/p>
&lt;p>&lt;strong>Out-of-scope&lt;/strong>（路由到他章）：&lt;/p>
&lt;ul>
&lt;li>通用 IAM / 服務間信任 → &lt;a href="../identity-access-boundary/">7.2 identity-access-boundary&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity&lt;/a> → &lt;a href="../workload-identity-and-federated-trust/">7.7 workload-identity-and-federated-trust&lt;/a>&lt;/li>
&lt;li>log / PII 治理 → &lt;a href="../llm-log-and-pii-governance/">llm-log-and-pii-governance&lt;/a>&lt;/li>
&lt;li>model artifact 供應鏈 → &lt;a href="../llm-deployment-supply-chain/">llm-deployment-supply-chain&lt;/a>&lt;/li>
&lt;li>入口治理 → &lt;a href="../entrypoint-and-server-protection/">7.3 entrypoint-and-server-protection&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="從本章到實作">從本章到實作&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Mechanism&lt;/strong>：問題節點表 → knowledge-card → 看具體機制。&lt;/li>
&lt;li>&lt;strong>Delivery&lt;/strong>：交接路由 → &lt;code>05-deployment-platform / 06-reliability / 08-incident-response&lt;/code>。&lt;/li>
&lt;/ul>
&lt;h2 id="llm-多租戶隔離的三個-llm-specific-層">LLM 多租戶隔離的三個 LLM-specific 層&lt;/h2>
&lt;p>跟一般 service 的多租戶隔離（compute / network / data）相比、LLM 推論服務多了三個層次：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>KV cache 層&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a> 是推論時的 attention 暫存、跨 request 可能重用（prefix cache、shared prefix optimization）；跨租戶共用 cache 是直接的資料洩漏面。&lt;/li>
&lt;li>&lt;strong>prompt log 層&lt;/strong>：production LLM 服務通常會 log prompt + response 用於 debug / billing / abuse detection；log 的隔離與保留期限直接影響跨租戶洩漏風險。&lt;/li>
&lt;li>&lt;strong>model artifact 訪問權&lt;/strong>：production 可能部署多個 fine-tuned 模型（如 customer-specific 模型）、模型本身是 sensitive artifact、訪問權要對齊 IAM。&lt;/li>
&lt;/ol>
&lt;h2 id="分析模型">分析模型&lt;/h2>
&lt;p>production LLM 推論的多租戶隔離依四個層次分析：&lt;/p></description><content:encoded><![CDATA[<p>本章的責任是把 LLM 推論服務的多租戶隔離問題拆成可操作的判讀節點。LLM 服務的隔離議題在一般 multi-tenant 隔離（compute / network / data、見 <a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant-boundary</a>）之上、多了 <a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a>（特別是 <a href="/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">prefix cache</a> 重用）、prompt log、model artifact 訪問權三個 LLM-specific 層、本章聚焦這些差異。一般 multi-tenant 隔離原則沿用 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分授權邊界</a> 跟 <a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">7.4 供應鏈</a>。</p>
<h2 id="本章寫作邊界">本章寫作邊界</h2>
<p>本章聚焦 production LLM 推論的多租戶 isolation 特殊性。team / 個人 dev 場景的「多人共用本地 server」見 <a href="/blog/llm/06-security/routing-to-production-security/" data-link-title="6.5 跨進 production 的 routing 中樞" data-link-desc="個人 dev → 團隊 → production LLM 服務的三層演化、跟 backend/07 對應卡片的 routing 清單">llm/6.5 跨進 production 的 routing 中樞</a>；通用 IAM / 服務間信任邊界見 7.2。</p>
<h2 id="本章-threat-scope">本章 threat scope</h2>
<p><strong>In-scope</strong>：KV cache 跨租戶洩漏、prompt log 隔離、模型 artifact 訪問權、batch 推論的順序敏感性、tenant-scoped rate limit、共用 GPU 上的記憶體殘留。</p>
<p><strong>Out-of-scope</strong>（路由到他章）：</p>
<ul>
<li>通用 IAM / 服務間信任 → <a href="../identity-access-boundary/">7.2 identity-access-boundary</a></li>
<li><a href="/blog/backend/knowledge-cards/workload-identity/" data-link-title="Workload Identity" data-link-desc="用於機器工作負載的身份語意與授權邊界">workload identity</a> → <a href="../workload-identity-and-federated-trust/">7.7 workload-identity-and-federated-trust</a></li>
<li>log / PII 治理 → <a href="../llm-log-and-pii-governance/">llm-log-and-pii-governance</a></li>
<li>model artifact 供應鏈 → <a href="../llm-deployment-supply-chain/">llm-deployment-supply-chain</a></li>
<li>入口治理 → <a href="../entrypoint-and-server-protection/">7.3 entrypoint-and-server-protection</a></li>
</ul>
<h2 id="從本章到實作">從本章到實作</h2>
<ul>
<li><strong>Mechanism</strong>：問題節點表 → knowledge-card → 看具體機制。</li>
<li><strong>Delivery</strong>：交接路由 → <code>05-deployment-platform / 06-reliability / 08-incident-response</code>。</li>
</ul>
<h2 id="llm-多租戶隔離的三個-llm-specific-層">LLM 多租戶隔離的三個 LLM-specific 層</h2>
<p>跟一般 service 的多租戶隔離（compute / network / data）相比、LLM 推論服務多了三個層次：</p>
<ol>
<li><strong>KV cache 層</strong>：<a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> 是推論時的 attention 暫存、跨 request 可能重用（prefix cache、shared prefix optimization）；跨租戶共用 cache 是直接的資料洩漏面。</li>
<li><strong>prompt log 層</strong>：production LLM 服務通常會 log prompt + response 用於 debug / billing / abuse detection；log 的隔離與保留期限直接影響跨租戶洩漏風險。</li>
<li><strong>model artifact 訪問權</strong>：production 可能部署多個 fine-tuned 模型（如 customer-specific 模型）、模型本身是 sensitive artifact、訪問權要對齊 IAM。</li>
</ol>
<h2 id="分析模型">分析模型</h2>
<p>production LLM 推論的多租戶隔離依四個層次分析：</p>
<ol>
<li><strong>memory 層</strong>：GPU VRAM、CPU RAM 中的 KV cache 跟模型權重、跨 request / 跨租戶的殘留與共享邊界。</li>
<li><strong>storage 層</strong>：模型 artifact、prompt log、context cache 在儲存層的隔離。</li>
<li><strong>identity 層</strong>：tenant identity 怎麼帶到 inference call、rate limit / quota 怎麼按租戶分。</li>
<li><strong>observability 層</strong>：metric / log / trace 中的 tenant tag、跨租戶分析的允許範圍。</li>
</ol>
<h2 id="判讀流程">判讀流程</h2>
<p>判讀流程的責任是把「能服務多個租戶的 LLM 服務」轉成「租戶間資料不互相洩漏的 LLM 服務」。</p>
<ol>
<li>先確認 tenant identity 從 API gateway 到 inference call 的傳遞路徑。</li>
<li>再確認 KV cache、prompt log、model artifact 各自的隔離邊界。</li>
<li>接著確認 GPU 記憶體中的跨 request 殘留是否清理。</li>
<li>最後交接到偵測流程、確認跨租戶異常能被識別。</li>
</ol>
<h2 id="問題節點案例觸發式">問題節點（案例觸發式）</h2>
<table>
  <thead>
      <tr>
          <th>問題節點</th>
          <th>判讀訊號</th>
          <th>風險後果</th>
          <th>前置控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>KV cache 跨租戶共享</td>
          <td>shared prefix optimization 沒按 tenant key 分桶</td>
          <td>租戶 A 的 prompt prefix 被租戶 B 看見</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">data-protection</a></td>
      </tr>
      <tr>
          <td>prompt log 沒分租戶</td>
          <td>集中 log、查詢時 tenant filter 缺失</td>
          <td>abuse detection 跨租戶看 prompt 內容、隱私違規</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit-log</a></td>
      </tr>
      <tr>
          <td>共用 GPU 上的記憶體殘留</td>
          <td>推論完未清 VRAM、下一個 request 可能 dump 到前一個內容</td>
          <td>同 GPU 上的不同 tenant 之間殘留洩漏</td>
          <td><a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">secret-management</a></td>
      </tr>
      <tr>
          <td>tenant-scoped rate limit 失效</td>
          <td>同一 API key 限流、租戶被互相 DoS</td>
          <td>大租戶吃光 quota、其他租戶無法用</td>
          <td><a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate-limit</a></td>
      </tr>
      <tr>
          <td>model artifact 訪問權混亂</td>
          <td>fine-tuned 模型路徑可被其他 tenant 載入</td>
          <td>客戶模型被其他客戶使用、模型權重洩漏</td>
          <td><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">identity-access-boundary</a></td>
      </tr>
      <tr>
          <td>batch 推論的 cross-tenant 順序敏感</td>
          <td>dynamic batching 把不同 tenant 的 request 合批</td>
          <td>一個 tenant 的 OOM / 長 prompt 影響其他 tenant 的 latency</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a></td>
      </tr>
  </tbody>
</table>
<h2 id="常見風險邊界">常見風險邊界</h2>
<p>風險邊界的責任是界定何時 LLM 多租戶 isolation 已進入高壓狀態。</p>
<ul>
<li>KV cache 共用範圍跨越 tenant 邊界時、代表記憶體層 isolation 失效。</li>
<li>prompt log 沒帶 tenant tag、或 tag 後仍可跨 tenant 查時、代表 log 層 isolation 不足。</li>
<li>模型 artifact 訪問權跟 IAM 解耦時、代表 identity 層 isolation 不足。</li>
<li>推論 batch 對 tenant boundary 不敏感時、代表 batch 層的 noisy-neighbor 風險上升。</li>
</ul>
<h2 id="llm-場景的特殊判讀">LLM 場景的特殊判讀</h2>
<p>LLM 多租戶 isolation 相對一般 multi-tenant 服務的特殊性：</p>
<ol>
<li><strong>KV cache 是有用但敏感的優化</strong>：shared prefix cache（如多 tenant 用同一 system prompt）能省大量 prefill 算力、但跨 tenant 共用就是洩漏。判讀：可以 share 同 tenant 內的 prefix、不能 share 跨 tenant。</li>
<li><strong>prompt log 含豐富使用者意圖</strong>：相比一般 API log 主要記 endpoint / status code、LLM prompt log 記的是「使用者實際在問什麼」、隱私敏感度高得多。</li>
<li><strong>GPU 是稀缺資源、共用比 CPU 多</strong>：production LLM 服務常多 tenant 共用同卡、isolation 比一般 multi-tenant 服務（每 tenant 跑獨立 pod）更難做、需要更細的 batch 跟 memory 管理。</li>
<li><strong>fine-tuned 模型本身是 customer asset</strong>：模型訓練成本高、權重是客戶 IP、訪問權混亂直接是 IP 外洩。</li>
<li><strong>「LLM 記住 cross-tenant 資訊」的疑慮</strong>：使用者常擔心 LLM 把 A tenant 的 prompt「記住」洩漏給 B tenant；對 inference-only 服務（無 fine-tune）這不發生（模型權重 immutable）、有 fine-tune 時要看 training data 隔離。</li>
</ol>
<h2 id="案例觸發參考">案例觸發參考</h2>
<p>LLM 多租戶 isolation 的公開案例累積中、本章先沿用通用 multi-tenant 案例：</p>
<ul>
<li>一般 multi-tenant 隔離案例見 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分授權邊界</a>。</li>
<li>LLM-specific 案例累積後會補入 <code>red-team/cases/llm-multi-tenant/</code>。</li>
</ul>
<blockquote>
<p><strong>事實查核註</strong>：LLM 多租戶 isolation 的公開事件案例還在早期、社群上有些「LLM A 的 system prompt 被 B 看到」等報告、多數屬 prompt injection 範疇而非 cache 洩漏。建議引用前以最新的 OWASP LLM Top 10 跟具體 vendor 的 incident 公告為準。</p></blockquote>
<h2 id="引用標準">引用標準</h2>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>版本 / 年份</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>NIST SP 800-207（Zero Trust Architecture）</td>
          <td>2020</td>
          <td>tenant boundary 零信任模型 reference</td>
      </tr>
      <tr>
          <td>OWASP LLM Top 10</td>
          <td>2025</td>
          <td>LLM application security 通用 reference</td>
      </tr>
      <tr>
          <td>CSA Cloud Controls Matrix</td>
          <td>v4 (2021)</td>
          <td>multi-tenant cloud 控制 reference</td>
      </tr>
  </tbody>
</table>
<p>引用版本與 cadence 規則見 <a href="/blog/report/security-citation-currency-and-precision/" data-link-title="Security 標準引用的時效性與精確度" data-link-desc="資安 citation 跟一般技術引用不同——best practice 時效短（MD5 / SHA-1 / bcrypt 100k / TLS 1.0 都曾是 best practice）、原文常被引用扭曲（conditional → unconditional drift）、版本不標 reader 會套用過時 spec。citation 同時涵蓋外部標準（OWASP / RFC / NIST / CIS）跟內部 citation（knowledge-cards / 跨章引用作為 control-of-record）；後者因無版本號 anchor 反而更易 silent drift / broken。每條 citation 必須附：版本 / 年份、引用句意可回溯、deprecated / superseded 標記、強度參數對應 actor 能力的 review trigger（外部）/ last-checked &#43; sync owner（內部）。">security-citation-currency-and-precision</a>。Last reviewed: 2026-05-12。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>身份授權邊界：<a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 identity-access-boundary</a></li>
<li>log 治理：<a href="/blog/backend/07-security-data-protection/llm-log-and-pii-governance/" data-link-title="LLM Log 與 PII 治理" data-link-desc="production LLM 服務的 prompt log 累積、PII 偵測與過濾、保留期限與合規對齊">llm-log-and-pii-governance</a></li>
<li>agent prompt injection 後果：<a href="/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/" data-link-title="LLM Agent Prompt Injection 後果治理" data-link-desc="production LLM agent 場景的 prompt injection 後果：tool spec 設計、agent loop 限制、review checkpoint、跟 incident workflow 的接合">llm-prompt-injection-in-agent</a></li>
<li>部署平台：<code>05-deployment-platform</code></li>
<li>可靠性：<code>06-reliability</code></li>
</ul>
]]></content:encoded></item></channel></rss>