<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MicroFIRE]]></title><description><![CDATA[Observability、OpenTelemetry、Software Architecture、DataBase]]></description><link>https://ganhua.wang</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1687235846899/_HpeveXrd.png</url><title>MicroFIRE</title><link>https://ganhua.wang</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 13 May 2026 06:54:17 GMT</lastBuildDate><atom:link href="https://ganhua.wang/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Grafana o11y-bench 深入剖析：讓 AI 真正面對 on-call 現場]]></title><description><![CDATA[o11y-bench 深入剖析：讓 AI 真正面對 on-call 現場

從任務設計、合成環境、Agent 架構、評分機制到報告輸出，逐一解析這個開放 benchmark 的每個組件——以及 Gemini 3 Flash Preview 的完整實測結果


先說清楚這在解決什麼問題
目前多數 LLM benchmark 測的是「知識」：模型知不知道 PromQL 的語法，知不知道什麼是 p99 ]]></description><link>https://ganhua.wang/grafana-o11y-bench-ai-on-call</link><guid isPermaLink="true">https://ganhua.wang/grafana-o11y-bench-ai-on-call</guid><category><![CDATA[Grafana]]></category><category><![CDATA[#agent]]></category><category><![CDATA[#AIOps]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sun, 03 May 2026 15:58:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/4702b54d-41c0-4e91-ada3-39f2d3d3ca54.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>o11y-bench 深入剖析：讓 AI 真正面對 on-call 現場</h1>
<blockquote>
<p>從任務設計、合成環境、Agent 架構、評分機制到報告輸出，逐一解析這個開放 benchmark 的每個組件——以及 Gemini 3 Flash Preview 的完整實測結果</p>
</blockquote>
<hr />
<h2>先說清楚這在解決什麼問題</h2>
<p>目前多數 LLM benchmark 測的是「知識」：模型知不知道 PromQL 的語法，知不知道什麼是 p99 latency。但 SRE 的工作從來不只是知識問題——真正的挑戰是：<strong>能不能在一個陌生的監控環境裡，自主操作工具，把分散在 metrics、logs、traces 三個訊號裡的線索拼起來，找到根因？</strong></p>
<blockquote>
<p>metrics 像體溫計（數字趨勢）、logs 像病歷（事件記錄）、traces 像 X ▎ 光（請求在系統內怎麼流動）。on-call 的難處是這三種資料分別存在三個系統裡，要自己拼起來。</p>
</blockquote>
<p>Grafana Labs 的工程師 Yasir Ekinci 和 Jack Gordley 在 GrafanaCON 2026 發表 o11y-bench 時，特別強調了 observability 任務的本質差異：</p>
<blockquote>
<p>"In observability, the dangerous mistakes are often the subtle ones."</p>
</blockquote>
<p>一個查詢語法正確，但選了錯誤的 metric series；一個 dashboard 在 UI 上看起來正常，但 variable binding 沒有真正連上 panels——這類錯誤在一般的 benchmark 裡不會被抓到，但在真實 on-call 場景裡會導致錯誤判斷。這就是為什麼需要一個讓 agent 面對真實 stack、結果由程式直接驗證的 benchmark。</p>
<p><a href="https://o11ybench.ai/">o11y-bench</a> 嘗試回答這個問題。它是由 Grafana Labs 開發的開放 benchmark，基於 <a href="https://harborframework.com">Harbor</a> 框架運作，讓 LLM agent 在一個真實跑起來的 Grafana + Prometheus + Loki + Tempo stack 上解題，評分後對外公開 leaderboard。</p>
<blockquote>
<ul>
<li><p>Harbor → benchmark 執行框架（負責起容器、跑 agent、收結果）</p>
</li>
<li><p>Prometheus → 存 metrics、Loki → 存 logs、Tempo → 存 traces</p>
</li>
</ul>
</blockquote>
<hr />
<h2>整體架構鳥瞰</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/6c3b5c46-49a3-49c5-a14f-65445b52ddea.png" alt="" style="display:block;margin:0 auto" />

<p>跑一次 benchmark 的流程大致如下：</p>
<pre><code class="language-plaintext">tasks-spec/  →  (sync)  →  tasks/
                                ↓
                    Harbor 起一個 trial
                                ↓
              ┌─────────────────────────────┐
              │   docker/ sidecar 容器          │
              │   Prometheus + Loki + Tempo    │
              │   + Grafana + mcp-grafana      │
              └─────────────────────────────┘
                                ↓
              ┌─────────────────────────────┐
              │   agents/ agent 容器            │
              │   讀題目 → 呼叫 MCP 工具          │
              │   → 寫出 trajectory.json       │
              └─────────────────────────────┘
                                ↓
              ┌─────────────────────────────┐
              │   grading/ verifier            │
              │   deterministic checks         │
              │   + LLM rubric (Claude)        │
              └─────────────────────────────┘
                                ↓
              jobs/&lt;job-name&gt;/result.json
              jobs/&lt;job-name&gt;/run_report.html
</code></pre>
<p>以下逐一拆解每個組件。</p>
<hr />
<h2>組件一：tasks-spec/ — 題目的源頭</h2>
<pre><code class="language-plaintext">tasks-spec/
  prometheus_query/   (16 題)
  loki_query/         (10 題)
  tempo_query/        (13 題)
  grafana_api/        (6 題)
  dashboarding/       (7 題)
  investigation/      (11 題)
</code></pre>
<p><code>tasks-spec/</code> 是整個專案唯一需要手動維護的 task 資料，<code>tasks/</code> 是從它生成的 output（不要直接編輯）。執行 <code>mise run setup:sync</code> 會把所有 YAML 轉換成 Harbor 能讀的任務格式。</p>
<h3>YAML 格式設計</h3>
<p>每個 task 的核心欄位：</p>
<pre><code class="language-yaml">id: promql-subquery-peak-error-rate
category: prometheus_query
statement: |
  六小時內，payment-service 的 error rate 峰值是多少？

checks:
  - name: Response cites a trace ID that appears in Tempo tool results
    weight: 70
    type: grounding
    params:
      mode: tool_trace_id

rubric:
  - criterion: The final response states the peak error rate accurately.
    weight: 65
    fact:
      kind: query
      backend: prometheus
      query: max_over_time(rate(http_requests_total{job="payment-service",status=~"5.."}[5m])[6h:1m])
</code></pre>
<p>有幾個刻意的設計決策值得注意：</p>
<ul>
<li><p><strong>statement 用自然語言，不給語法提示</strong>：不寫「用 PromQL subquery」，只說「六小時內的峰值是多少」。這樣才是真正測 agent 能不能選對工具。</p>
</li>
<li><p><strong>數字精確性用</strong> <code>fact</code>：benchmark 自己跑那個 PromQL 拿到 ground truth，讓 judge 比對，而不是把答案寫死在 criterion 文字裡（否則改了資料就要改題目）。</p>
</li>
<li><p><strong>具體實體用 grounding check</strong>：trace ID 不靠 LLM 判斷，程式直接比對。</p>
</li>
</ul>
<blockquote>
<p>grounding check：強制答案裡的具體值（trace ID、service 名）必須真的在工具回傳結果裡出現，防 LLM 編造</p>
</blockquote>
<blockquote>
<p>一道題包含三個東西：題目（statement）、程式驗的部分（checks）、LLM 評分的部分（rubric）。「rubric」是 LLM 評估常用語：一份評分準則清單。「LLM rubric」就是讓另一個 LLM（這裡是 Claude）當 judge，照清單逐條打勾 YES/NO。後面組件四會展開細節。</p>
</blockquote>
<h3>六個類別的能力層次</h3>
<table>
<thead>
<tr>
<th>類別</th>
<th>題數</th>
<th>核心挑戰</th>
</tr>
</thead>
<tbody><tr>
<td><strong>PromQL</strong></td>
<td>16</td>
<td>選對函數（rate/offset/topk/subquery），數字解讀正確</td>
</tr>
<tr>
<td><strong>LogQL</strong></td>
<td>10</td>
<td>多階段 pipeline（`</td>
</tr>
<tr>
<td><strong>TraceQL</strong></td>
<td>13</td>
<td>追 call chain，引用真實 trace ID（有 grounding check 防幻覺）</td>
</tr>
<tr>
<td><strong>Grafana API</strong></td>
<td>6</td>
<td>直接操作 REST API，不只是用 Grafana UI</td>
</tr>
<tr>
<td><strong>Dashboarding</strong></td>
<td>7</td>
<td>建出真實可用的 dashboard（state check 直接驗 Grafana 狀態）</td>
</tr>
<tr>
<td><strong>Investigation</strong></td>
<td>11</td>
<td>跨三個訊號做根因分析，全靠 LLM rubric</td>
</tr>
</tbody></table>
<hr />
<h3>PromQL（16 題）—— 結構化查詢的精確度</h3>
<p>最基礎的一層。題目用自然語言描述，不給語法提示，例如：</p>
<blockquote>
<p>「找出六小時內 payment-service 的 error rate 峰值」</p>
</blockquote>
<p>模型需要自己決定用 <code>subquery</code>、<code>offset</code>、<code>topk</code> 還是 <code>rate</code>，並且對計算出的數字做出正確解讀。</p>
<p>代表題目：</p>
<table>
<thead>
<tr>
<th>task</th>
<th>測什麼</th>
</tr>
</thead>
<tbody><tr>
<td><code>promql-error-rate</code></td>
<td>三個 service 合計 5xx share</td>
</tr>
<tr>
<td><code>promql-burn-rate-assessment</code></td>
<td>比較 payment-service 現在 vs 6h 前的 error rate（<code>offset</code>）</td>
</tr>
<tr>
<td><code>promql-subquery-peak-error-rate</code></td>
<td>找 6h 內的 error rate 峰值（<code>subquery</code>）</td>
</tr>
<tr>
<td><code>promql-topk-5xx-share</code></td>
<td>哪個 backend 貢獻最多 5xx（<code>topk</code>）</td>
</tr>
<tr>
<td><code>query-cpu-metrics</code></td>
<td>比較各 job 的 CPU 用量並排名</td>
</tr>
<tr>
<td><code>promql-capacity-analysis</code></td>
<td>記憶體用量趨勢與容量評估</td>
</tr>
</tbody></table>
<hr />
<h3>LogQL（10 題）—— 半結構化資料的處理能力</h3>
<p>Loki 的 LogQL 比 PromQL 難在：必須先做 pipeline 解析（<code>| json | __error__=""</code>），才能對欄位做聚合。測試案例包括找最慢 endpoint（需要 <code>unwrap duration_ms</code>）、計算 p95 latency、區分 retry 造成的假 5xx 和真實錯誤。</p>
<p>代表題目：</p>
<table>
<thead>
<tr>
<th>task</th>
<th>測什麼</th>
</tr>
</thead>
<tbody><tr>
<td><code>logql-top-5xx-endpoint</code></td>
<td>按 path 統計 5xx，找最多那條</td>
</tr>
<tr>
<td><code>logql-multi-stage-pipeline</code></td>
<td>找最慢 endpoint（<code>unwrap duration_ms</code>）</td>
</tr>
<tr>
<td><code>logql-parse-json-logs</code></td>
<td>JSON log 解析與欄位過濾</td>
</tr>
<tr>
<td><code>logql-unwrap-orders-p95-latency</code></td>
<td>計算 /api/orders 的 p95 latency</td>
</tr>
<tr>
<td><code>logql-retry-vs-real-errors</code></td>
<td>區分 retry 造成的 5xx 和真實錯誤</td>
</tr>
<tr>
<td><code>logql-deployment-rollout-events</code></td>
<td>從 log 找 deployment 事件時間線</td>
</tr>
</tbody></table>
<hr />
<h3>TraceQL（13 題）—— 跨服務的因果推理</h3>
<p>分散式追蹤是三個訊號裡最難操作的。測試重點：</p>
<ul>
<li><p>能否準確引用真實的 trace ID（有 grounding check 防止幻覺）</p>
</li>
<li><p>能否沿著 call chain 追蹤 error 傳播路徑</p>
</li>
<li><p>能否用 TraceQL metrics 計算各 service 的 error rate</p>
</li>
</ul>
<p>代表題目：</p>
<table>
<thead>
<tr>
<th>task</th>
<th>測什麼</th>
</tr>
</thead>
<tbody><tr>
<td><code>traceql-error-chain-orders</code></td>
<td>找 POST /api/orders 的 failing trace，說明 error 傳播路徑</td>
</tr>
<tr>
<td><code>traceql-structural-query</code></td>
<td>order-service checkout 流程的 downstream call chain</td>
</tr>
<tr>
<td><code>traceql-tail-latency-bottleneck</code></td>
<td>找 p99 最慢的 service 和 span</td>
</tr>
<tr>
<td><code>traceql-metrics-error-rate-by-service</code></td>
<td>用 TraceQL metrics 計算各 service error rate</td>
</tr>
</tbody></table>
<hr />
<h3>Grafana API（6 題）—— REST API 直接操作</h3>
<p>不只是用 Grafana UI，而是要能直接操作 REST API 讀取 datasource 設定、搜尋 dashboard、檢查 panel 查詢內容。</p>
<table>
<thead>
<tr>
<th>task</th>
<th>測什麼</th>
</tr>
</thead>
<tbody><tr>
<td><code>list-datasources</code></td>
<td>列出所有 datasource 的名稱和 type</td>
</tr>
<tr>
<td><code>get-datasource-details</code></td>
<td>取得特定 datasource 的 URL 和 access mode</td>
</tr>
<tr>
<td><code>search-dashboards</code></td>
<td>搜尋 dashboard list</td>
</tr>
<tr>
<td><code>inspect-dashboard-queries</code></td>
<td>讀取 dashboard panels 裡的查詢內容</td>
</tr>
<tr>
<td><code>audit-service-overview-variable</code></td>
<td>確認 dashboard variable 的設定</td>
</tr>
</tbody></table>
<hr />
<h3>Dashboarding（7 題）—— 能不能建出真實可用的東西</h3>
<p>這類題目以 deterministic check 為主：agent 建完 dashboard 後，benchmark 直接打 Grafana API 驗證 panels 是否存在、datasource 是否正確、variable binding 是否真的生效。</p>
<table>
<thead>
<tr>
<th>task</th>
<th>測什麼</th>
</tr>
</thead>
<tbody><tr>
<td><code>dashboard-create-service-overview</code></td>
<td>建立含 timeseries、stat、logs panel、variable、annotation 的完整 dashboard</td>
</tr>
<tr>
<td><code>dashboard-add-cache-lag-panels</code></td>
<td>在現有 dashboard 加入 cache lag 相關 panels</td>
</tr>
<tr>
<td><code>dashboard-add-deployment-annotation</code></td>
<td>加入 deployment 事件 annotation</td>
</tr>
<tr>
<td><code>dashboard-update-add-service-variable</code></td>
<td>加入 service dropdown variable 並讓所有 panel 跟隨</td>
</tr>
<tr>
<td><code>dashboard-repair-cache-review</code></td>
<td>修復有問題的 dashboard</td>
</tr>
</tbody></table>
<p><code>dashboard-create-service-overview</code> 是最複雜的 task，要求一次建立 5 種 panel type + 1 個 multi-value variable + 1 個 Loki annotation，並驗證 variable binding 在選不同 service 時真的生效。</p>
<hr />
<h3>Investigation（11 題）—— 跨訊號的根因分析</h3>
<p>最困難的一層，也是最接近真實 on-call 工作的場景。沒有 deterministic check，全靠 LLM rubric。典型題目：</p>
<blockquote>
<p>「api-gateway 出現 5xx，請判斷是自己的問題還是 downstream 傳上來的？」</p>
</blockquote>
<table>
<thead>
<tr>
<th>task</th>
<th>測什麼</th>
</tr>
</thead>
<tbody><tr>
<td><code>incident-triage</code></td>
<td>跨 Prometheus + Loki 找出哪些 service 受影響、何時開始</td>
</tr>
<tr>
<td><code>payments-path-root-cause</code></td>
<td>用 logs + traces 找出 /api/payments 是不是 root cause</td>
</tr>
<tr>
<td><code>dependency-outage-false-lead</code></td>
<td>api-gateway 的 5xx 是自己的問題還是 downstream 傳上來的？</td>
</tr>
<tr>
<td><code>service-degradation-rca</code></td>
<td>找出哪個 service 最先出問題</td>
</tr>
<tr>
<td><code>slow-path-hotspot-correlation</code></td>
<td>把 slow path 的 logs 和 metrics 對應起來</td>
</tr>
<tr>
<td><code>cache-incident-blast-radius</code></td>
<td>cache 問題影響了哪些 service</td>
</tr>
</tbody></table>
<p>評分標準包含：數字準確性（有 ground truth 對照）、推理是否基於工具結果而非憑空推測、結論是否區分主因和次要影響。</p>
<hr />
<h2>組件二：docker/ — 合成的 Observability 環境</h2>
<pre><code class="language-plaintext">docker/
  Dockerfile
  entrypoint.sh
  prometheus.yml  /  loki-config.yaml  /  tempo.yaml  /  datasources.yaml
  python/src/o11y_stack/
    generate_data.py           # 合成資料產生器
    provision_task_resources.py # task 專屬的 Grafana 資源
</code></pre>
<p>每次執行 benchmark trial，這個 sidecar 容器就從頭啟動，最終對外提供一個完整的 Grafana stack 加上 mcp-grafana（暴露 36 個 MCP 工具給 agent）。</p>
<h3>啟動順序</h3>
<p>容器啟動有嚴格的相依順序：</p>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/0a3ee03f-0cd6-4f3a-b0fe-7ac78aaead00.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-plaintext">1. Loki + Tempo + Grafana 同時背景啟動
      ↓
2. 等 Loki、Tempo 的 /ready 端點回應
      ↓
3. generate_data.py — 產生 24 小時合成資料
   ├─ 寫 Prometheus TSDB block（用 promtool）
   ├─ 推 traces 到 Tempo（OTLP HTTP）
   └─ 推 logs 到 Loki（push API）
      ↓
4. 啟動 Prometheus（故意在這步才啟動）
      ↓   ← TSDB block 必須先存在才能被讀到
5. 等所有服務的 health endpoint
      ↓
6. provision_task_resources.py — 建立 task 需要的 dashboard
      ↓
7. 啟動 mcp-grafana（:8080）
      ↓
=== Environment Ready ===
</code></pre>
<p>Prometheus 故意在資料產生完才啟動，這是個有意思的工程細節：TSDB block 必須在 Prometheus 啟動前就存在才能被讀到，如果先啟動 Prometheus 再推資料，會有時序問題。</p>
<h3>模擬的電商微服務系統</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/3fb5fd76-a863-486b-9342-3e742d4d1035.png" alt="" style="display:block;margin:0 auto" />

<p>合成資料來自一個 5 個微服務的電商平台：</p>
<pre><code class="language-plaintext">webapp → api-gateway → user-service
                     → order-service → user-service
                                     → payment-service
                     → payment-service
</code></pre>
<p>資料時間釘死在 <code>scenario_time.txt</code>，隨機種子 <code>random.seed(42)</code>，確保每次跑同一道題拿到完全相同的資料——這是跨模型、跨時間比較的基礎。</p>
<h3>三段刻意設計的故障</h3>
<p>資料裡藏了三段相互獨立的 incident，這是整個 benchmark 的核心：</p>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/9a566694-4fbe-472c-a45f-44cc8c23c9b5.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Incident 1：Error Spike（資料結尾前 3 小時，持續 30 分鐘）</strong></p>
<p>payment-service 大量出錯，cascading 傳遞：</p>
<table>
<thead>
<tr>
<th>服務</th>
<th>故障期間 error rate</th>
</tr>
</thead>
<tbody><tr>
<td>payment-service</td>
<td>70%</td>
</tr>
<tr>
<td>order-service</td>
<td>15%（cascading）</td>
</tr>
<tr>
<td>api-gateway</td>
<td>8%（cascading）</td>
</tr>
</tbody></table>
<p>故障前 2 分鐘：Loki 有 payment-service 和 order-service 的 deployment log，暗示是部署引發的。</p>
<p><strong>Incident 2：Latency 劣化（資料結尾前 6 小時，持續 45 分鐘）</strong></p>
<p>order-service 回應變 5 倍慢，upstream 連帶受影響。<code>/api/orders</code> 有 60% 機率被標成 slow request（duration_ms 500–3000ms）。</p>
<p><strong>Incident 3：Cache Refresh Lag（資料結尾前 9 小時，持續 40 分鐘）</strong></p>
<p>user-service auth cache 更新卡住。<code>service_cache_refresh_lag_seconds</code> metric 最高到 520 秒，Loki 有帶 <code>lag_seconds</code> 和 <code>stale_keys</code> 欄位的 warn log。</p>
<p><strong>精心設計的陷阱</strong>：traces 裡的 error span status <strong>只標在真正的根源 span</strong>。upstream 的 webapp 和 api-gateway 雖然回傳 HTTP 500，但 span status 是 OK。模型如果偷懶只看最外層，會找錯根因——必須真的沿著 call chain 往下追。</p>
<blockquote>
<p>合成資料涵蓋 24 小時，「資料結尾」就是模擬的「現在時間」（即 scenario_time.txt 裡釘死的那個時刻）。上面三段 incident 的時間都是相對這個「現在」往回推。這樣設計是因為 agent 解題時是站在「資料結尾」這個固定時間點往回看。</p>
</blockquote>
<h3>Prometheus Metrics 清單</h3>
<table>
<thead>
<tr>
<th>metric</th>
<th>說明</th>
</tr>
</thead>
<tbody><tr>
<td><code>http_requests_total{job, status}</code></td>
<td>request 數，按 HTTP status code</td>
</tr>
<tr>
<td><code>http_request_duration_seconds</code></td>
<td>latency histogram（11 個 bucket）</td>
</tr>
<tr>
<td><code>process_cpu_seconds_total</code></td>
<td>CPU counter</td>
</tr>
<tr>
<td><code>process_resident_memory_bytes</code></td>
<td>記憶體 gauge</td>
</tr>
<tr>
<td><code>service_retry_queue_depth</code></td>
<td>retry backlog 深度</td>
</tr>
<tr>
<td><code>service_cache_refresh_lag_seconds</code></td>
<td>cache lag（user-service 專用）</td>
</tr>
</tbody></table>
<h3>provision_task_resources.py</h3>
<p>某些 task 需要環境裡預先存在某個 dashboard（例如「修復這個壞掉的 dashboard」），這個腳本在 Grafana ready 後讀取 <code>/task/setup.json</code>，把需要的 dashboard 建好並確認可讀後才結束。目前 9 個 task 有用到。</p>
<hr />
<h2>組件三：agents/ — 跑在容器裡的 Agent</h2>
<pre><code class="language-plaintext">agents/
  o11y_agent.py          # Harbor agent 入口（host 端）
  agent_runner.py        # 核心 loop（跑在 task 容器內）
  system_prompt.txt
  task_prompt.txt
  langchain_o11y_agent.py   # LangChain 版（示範用）
  gcx_opencode_agent.py     # 使用 gcx CLI 的替代版本
</code></pre>
<h3>執行架構</h3>
<p>Agent 分成兩層：</p>
<pre><code class="language-plaintext">[Host]  O11yBenchAgent.run()         o11y_agent.py
          ├─ 上傳 agent_runner.py 進容器
          ├─ 轉傳 API keys 環境變數
          └─ 等容器結束，下載 trajectory.json
                   ↓
[容器]  agent_runner.py              (uv run 啟動)
          ├─ 連接 mcp-grafana（36 個工具）
          └─ while True loop
</code></pre>
<h3>核心 Loop</h3>
<pre><code class="language-python">while True:
    resp = await litellm.acompletion(messages, tools=mcp_tools)
    if no tool_calls:
        write trajectory, print "done", break
    for tc in tool_calls:
        out = await mcp_session.call_tool(tc.name, tc.args)
        messages.append(tool result)
    flush_trajectory()   # 每步都寫，partial work 不會丟失
</code></pre>
<p>設計上刻意簡單：純粹的 ReAct 循環，最多 50 步，透過 litellm 支援所有主流 provider。<code>flush_trajectory()</code> 每步都寫出，即使 trial 中途被取消也能保留部分紀錄。</p>
<blockquote>
<p>Reasoning + Acting：模型輸出『下一步要做什麼』→ 執行工具 → 把結果丟回去 → 模型決定下一步，反覆到模型說『我答完了』。</p>
</blockquote>
<h3>Prompt 設計的關鍵細節</h3>
<p><code>task_prompt.txt</code> 有一個重要的時間控制：</p>
<pre><code class="language-plaintext">&lt;context&gt;
Current time: {current_time}
&lt;/context&gt;

{statement}
</code></pre>
<p><code>current_time</code> 來自 <code>O11Y_SCENARIO_TIME_ISO</code> 環境變數（即 <code>scenario_time.txt</code>），強制 agent 把這個時間當作「現在」。如果 agent 用真實的 <code>now()</code> 查詢，拿到的資料範圍就會對不上合成資料，導致無法找到那三段 incident。</p>
<p><code>system_prompt.txt</code> 裡有幾條關鍵指令：</p>
<ul>
<li><p>必須基於工具回傳的資料做結論，不能靠記憶推測</p>
</li>
<li><p>建 Grafana dashboard 時一步給出完整 model</p>
</li>
<li><p><code>Act autonomously. Do not ask the user for clarification.</code></p>
</li>
</ul>
<h3>Trajectory 格式（ATIF-v1.6）</h3>
<p>每次 trial 結束產出 <code>trajectory.json</code>：</p>
<pre><code class="language-json">{
  "schema_version": "ATIF-v1.6",
  "agent": { "model_name": "gemini/gemini-3-flash-preview", ... },
  "steps": [
    { "step_id": 1, "source": "system", "message": "..." },
    { "step_id": 2, "source": "user",   "message": "題目內容" },
    { "step_id": 3, "source": "agent",  "tool_calls": [...] },
    { "step_id": 4, "source": "agent",  "message": "最終回答" }
  ],
  "final_metrics": {
    "total_cost_usd": 0.064,
    "total_tool_calls": 4,
    "elapsed_seconds": 23.3
  }
}
</code></pre>
<p>這個檔案是後續 regrade 和行為分析的基礎。評分系統只需要 trajectory.json，不需要重跑 agent。</p>
<blockquote>
<p>trajectory（軌跡）就是 agent 整次解題的完整紀錄：每一步它說了什麼、呼叫了哪個 工具、拿到什麼回應，全部依時序存下來。後續評分只要讀這個檔，不需要再跑一次 agent。</p>
</blockquote>
<h3>替代 Agent</h3>
<p>除了預設 agent，repo 還提供兩種替代實作：</p>
<table>
<thead>
<tr>
<th>Agent</th>
<th>差異</th>
</tr>
</thead>
<tbody><tr>
<td><code>LangChainO11yBenchAgent</code></td>
<td>用 LangChain 框架，作為自訂 agent 的示範</td>
</tr>
<tr>
<td><code>GcxOpenCodeAgent</code></td>
<td>使用 gcx CLI 而非 MCP tools，MCP 工具被全部移除，agent 只能透過 gcx 操作 Grafana</td>
</tr>
</tbody></table>
<hr />
<h2>組件四：grading/ — 雙層評分系統</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/18c66d27-1de5-4808-b524-01910efba982.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-plaintext">grading/
  verifier.py           # 評分流程 main()
  checks.py             # deterministic check 執行
  facts.py              # ground truth 查詢與快取
  judge.py              # LLM judge（Claude）
  scoring.py            # 加權分數計算
  transcript_parser.py  # 解析 trajectory.json
  dashboard_state.py    # dashboard 狀態 check
  env_context.py        # 對 Grafana/Prometheus/Loki/Tempo 發請求
</code></pre>
<h3>評分流程</h3>
<pre><code class="language-plaintext">trajectory.json  +  problem.yaml
         ↓
1. Deterministic checks
   grounding: 答案引用的 trace ID 是否來自工具結果？
   state:     Grafana 上 dashboard/datasource 是否符合規格？
         ↓
2. Resolve facts
   實際打 Prometheus/Loki/Tempo/Grafana API 拿 ground truth
         ↓
3. LLM rubric（Claude 當 judge）
   讀完整 transcript，逐條評 YES/NO
         ↓
4. 加權合算 → score (0.0–1.0)
</code></pre>
<p>官方部落格對評分設計的核心立場是：</p>
<blockquote>
<p>"Our general grading philosophy is to always check against the ground truth of what the agent actually did, not just what it said."</p>
</blockquote>
<p>對數字類的 criterion，benchmark 會拿相同的 PromQL 在相同資料上跑一遍；對 dashboard 類的操作，直接讀取已儲存的 panel JSON、驗證 variable binding、執行查詢並比對結果。Agent 的最終回答只是輔助，實際狀態才是評分依據。</p>
<h3>第一層：Deterministic Checks</h3>
<p>程式直接驗，快、精確，不需要 LLM。共有五種模式：</p>
<table>
<thead>
<tr>
<th>mode</th>
<th>驗什麼</th>
</tr>
</thead>
<tbody><tr>
<td><code>tool_trace_id</code></td>
<td>答案裡的 trace ID 必須真的出現在 Tempo tool result</td>
</tr>
<tr>
<td><code>dashboard_state</code></td>
<td>dashboard 存在且 panels/variables/annotations 符合規格</td>
</tr>
<tr>
<td><code>datasource_inventory</code></td>
<td>Grafana 有指定的 datasource types</td>
</tr>
<tr>
<td><code>datasource_detail</code></td>
<td>指定 datasource 有正確的 type/URL/access mode</td>
</tr>
<tr>
<td><code>tempo_trace_service_inventory</code></td>
<td>Tempo 有指定 service 的 trace 資料</td>
</tr>
</tbody></table>
<p><code>tool_trace_id</code> 防的是一個常見的 LLM 行為：模型「知道」系統用了 Tempo，所以直接捏造一個看起來合理的 trace ID 格式。Grounding check 強制要求 trace ID 必須出現在這次 trial 的工具回傳結果裡。</p>
<h3>第二層：LLM Rubric</h3>
<p>用另一個 Claude 當 judge。Judge 的 prompt 結構：</p>
<pre><code class="language-plaintext">&lt;transcript&gt;
  [System]: ...
  [User]: 題目
  [Assistant Tool Call]: query_prometheus(...)
  [Tool Result]: {"data": [...]}
  [Assistant]: 最終回答：payment-service error rate 峰值為 3.4%
&lt;/transcript&gt;

Based on the transcript above, evaluate each criterion:

&lt;criteria&gt;
  &lt;criterion id="0"&gt;The final response states the peak error rate accurately.
  Source of truth: The canonical query returned 0.034.&lt;/criterion&gt;
&lt;/criteria&gt;
</code></pre>
<p><code>Source of truth</code> 是 <code>facts.py</code> 實際打 Prometheus API 拿到的數字，讓 judge 有明確的對照基準。Judge 回傳：</p>
<pre><code class="language-xml">&lt;evaluation id="0"&gt;
&lt;answer&gt;YES&lt;/answer&gt;
&lt;explanation&gt;Response states 3.4% which matches the canonical value 0.034.&lt;/explanation&gt;
&lt;/evaluation&gt;
</code></pre>
<p><strong>Context budget 設計</strong>：Judge prompt 有三段長度嘗試（180K / 120K / 80K chars），遇到「prompt is too long」錯誤時自動縮短重試，縮短策略是先壓 thinking 和 tool result，最後才截頭截尾。</p>
<h3>分數計算</h3>
<p><code>scoring.py</code> 非常單純：</p>
<pre><code class="language-python">def calculate_score(subscores, weights) -&gt; float:
    normalized = normalize_weights(weights)
    return sum(s * w for s, w in zip(subscores, normalized))
</code></pre>
<p>checks 和 rubric 的 weight 一起正規化，所以兩層的相對比重由各自的 weight 值決定，不是固定的 50/50。</p>
<h3>重新評分（Regrade）</h3>
<p>如果修改了 rubric 或評分邏輯，可以只重跑評分部分，不需要重跑 agent：</p>
<pre><code class="language-bash">uv run python -m o11y_bench regrade --job-dir jobs/&lt;job-name&gt;
</code></pre>
<p>verifier 會讀取已存在的 <code>trajectory.json</code>，重跑 checks 和 LLM judge，覆寫 <code>grading_details.json</code> 和 <code>reward.txt</code>。需要真實 Grafana stack 的 check（如 <code>dashboard_state</code>）會自動起臨時的 sidecar stack。</p>
<hr />
<h2>組件五：o11y_bench/ — 排程與執行協調</h2>
<p>這層是 Harbor 框架的使用者：負責把 tasks、agents、environments 組合起來，按照 job config 排程執行，處理 retry 邏輯，以及在執行完成後觸發 reporting。</p>
<p>關鍵行為：</p>
<p><strong>Resume 機制</strong>：<code>bench:job</code> 會按 job directory 名稱 resume。如果 job 已存在且 config 相容，直接跑剩下的 trial，不重跑已完成的。這讓中途中斷的 job 可以繼續接著跑。</p>
<p><strong>Retry 設定</strong>（來自昨天的 config）：</p>
<pre><code class="language-json">"retry": {
  "max_retries": 1,
  "exclude_exceptions": [
    "AgentTimeoutError", "RewardFileNotFoundError",
    "VerifierOutputParseError", "RewardFileEmptyError"
  ]
}
</code></pre>
<p><code>RewardFileNotFoundError</code> 被排除在 retry 之外——這代表這類錯誤不會自動重試，需要人工介入調查。</p>
<blockquote>
<p>簡單說：前四個組件是「零件」，這一層是把零件組裝起來、按順序執行、處理失敗的「 指揮中心」。當你執行 mise run bench:job 時，呼叫的就是這層。</p>
</blockquote>
<hr />
<h2>組件六：reporting/ — 結果視覺化</h2>
<pre><code class="language-plaintext">reporting/
  run_report.py       # 單一 job 的 HTML 報告
  report.py           # 跨模型 suite leaderboard
  compare_report.py   # 兩個 job 並排比較
  report_data.py      # 核心資料載入與分類
  summary.py          # TrialRow / TaskSummary 聚合計算
</code></pre>
<h3>三個關鍵指標</h3>
<table>
<thead>
<tr>
<th>指標</th>
<th>意思</th>
<th>解讀方式</th>
</tr>
</thead>
<tbody><tr>
<td><strong>pass@k</strong></td>
<td>≥1 次 trial 通過的 task 比例</td>
<td>模型「最好狀況」的能力上限</td>
</tr>
<tr>
<td><strong>pass^k</strong></td>
<td>所有 k 次 trial 都通過的比例</td>
<td>模型的<strong>穩定性</strong></td>
</tr>
<tr>
<td><strong>mean_score</strong></td>
<td>所有 trial 的平均分（含部分分）</td>
<td>整體表現</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/4cb4774e-0f8e-4d12-8583-2da081e93720.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>一題跑 3 次，pass@3 = 3 次裡至少 1 次過；pass^3 = 3 次都要過。on-call 想要的是後者——你不會希望「值班 3 次裡有 1 次能找到 bug」。</p>
</blockquote>
<p><strong>為什麼 leaderboard 以 pass^k 排序</strong></p>
<p>官方的設計決策是：<strong>leaderboard 以 pass^k（一致性）為主要排名指標</strong>，而不是 pass@k（最好情況）。原因在於 observability 的使用情境——on-call 的時候，你需要的是「每次都對」，而不是「運氣好的時候對」。pass@k 和 pass^k 之間的差距本身就是一個重要訊號：差距大代表模型有能力但不穩定，不適合生產環境；差距小代表行為可預測。</p>
<p>幾種典型的解讀模式：</p>
<ul>
<li><p><strong>pass@3 高、pass^3 低</strong>：有能力但不穩定，需要多試幾次</p>
</li>
<li><p><strong>pass@3 ≈ pass^3</strong>：穩定，行為可預測（不論對錯）</p>
</li>
<li><p><strong>mean_score 高但 pass_rate 低</strong>：很多題都拿到部分分，但沒有一題完全答對</p>
</li>
</ul>
<h3>輸出 Artifacts</h3>
<pre><code class="language-plaintext">jobs/&lt;job-name&gt;/
  run_report.html                   # 單一模型報告（自動生成）
  result.json                       # 所有 trial 分數摘要
  &lt;task-name&gt;__&lt;trial-id&gt;/
    agent/trajectory.json           # 完整對話 + 所有 tool calls
    agent/command-0/stdout.txt      # 每步工具呼叫摘要
    verifier/reward.txt             # 分數（0.0–1.0）
    verifier/grading_details.json   # 各 criterion 分數 + judge 解釋
</code></pre>
<p><code>grading_details.json</code> 裡的 <code>explanation:</code> 欄位是診斷模型行為最直接的入口：</p>
<pre><code class="language-json">{
  "score": 0.45,
  "The final response identifies the root cause service.": 0.0,
  "explanation:The final response identifies...": "Agent said order-service but canonical query shows payment-service as the root cause"
}
</code></pre>
<hr />
<h2>實測結果：Gemini 3 Flash Preview（2026-05-02）</h2>
<h3>測試配置</h3>
<table>
<thead>
<tr>
<th>項目</th>
<th>值</th>
</tr>
</thead>
<tbody><tr>
<td>模型</td>
<td><code>google/gemini-3-flash-preview</code></td>
</tr>
<tr>
<td>Reasoning effort</td>
<td>off</td>
</tr>
<tr>
<td>每題嘗試次數</td>
<td>k=3（理論 189 trials）</td>
</tr>
<tr>
<td>實際執行</td>
<td>100 trials（job 未完整結束）</td>
</tr>
<tr>
<td>有效評分</td>
<td>83 trials，平均分 <strong>0.710</strong></td>
</tr>
<tr>
<td>錯誤</td>
<td>21 trials</td>
</tr>
</tbody></table>
<h3>錯誤分布</h3>
<table>
<thead>
<tr>
<th>類型</th>
<th>數量</th>
<th>意義</th>
</tr>
</thead>
<tbody><tr>
<td><code>RewardFileNotFoundError</code></td>
<td>11</td>
<td>Agent 跑完但 verifier 找不到 reward file；依 retry 設定不會自動重試</td>
</tr>
<tr>
<td><code>NonZeroAgentExitCodeError</code></td>
<td>5</td>
<td>Agent 執行中崩潰</td>
</tr>
<tr>
<td><code>CancelledError</code></td>
<td>5</td>
<td>Trial 超時被取消</td>
</tr>
</tbody></table>
<h3>按類別分析</h3>
<h4>PromQL — 接近完美</h4>
<p>幾乎全部滿分。唯一例外 <code>promql-cache-refresh-lag-peak</code>（0.55）。PromQL 語意嚴格、訓練資料充足，是目前模型最熟悉的能力域。</p>
<h4>TraceQL — 整體良好</h4>
<p>多數滿分，<code>traceql-metrics-error-rate-by-service</code> 其中一次 0.92（幾乎完美）。<code>traceql-checkout-p99-by-service</code> 得 0.0 是因為 <code>NonZeroAgentExitCodeError</code>（agent 崩潰），不是理解問題。</p>
<h4>LogQL — 中等，變異最大</h4>
<table>
<thead>
<tr>
<th>題目</th>
<th>分數</th>
</tr>
</thead>
<tbody><tr>
<td>retry-backlog-warnings、unwrap-p95-latency 等基礎題</td>
<td>1.0</td>
</tr>
<tr>
<td>cache-refresh-peak-lag</td>
<td>0.65</td>
</tr>
<tr>
<td>multi-stage-pipeline</td>
<td><strong>0.625（兩次一致）</strong></td>
</tr>
<tr>
<td>deployment-rollout-events</td>
<td>0.609</td>
</tr>
<tr>
<td>retry-vs-real-errors</td>
<td>0.576</td>
</tr>
<tr>
<td>top-5xx-endpoint（其中一次）</td>
<td>0.36</td>
</tr>
<tr>
<td>parse-json-logs（其中一次）</td>
<td>0.0（崩潰）</td>
</tr>
</tbody></table>
<p><code>logql-multi-stage-pipeline</code> 兩次都是 0.625 是個強訊號——說明模型對這個題型有固定的理解偏差，而不是偶發失誤。這個題目需要先 JSON 解析再用 <code>unwrap</code> 做 metric 計算，任何一步跑偏後續就全錯。</p>
<h4>Dashboarding — 加法容易、改法困難</h4>
<ul>
<li><p><code>add-cache-lag-panels</code>、<code>add-deployment-annotation</code>：兩次都是 <strong>1.0</strong></p>
</li>
<li><p><code>create-cache-incident-review</code>：0.90</p>
</li>
<li><p><code>add-retry-backlog-panels</code>：0.773</p>
</li>
<li><p><code>update-add-service-variable</code><strong>：0.455（兩次一致）</strong></p>
</li>
</ul>
<p><code>dashboard-update-add-service-variable</code> 固定 0.455 最值得關注——這道題要在所有現有 panels 加上 service variable binding，讓選不同服務時 panels 跟隨過濾。分數暗示模型能新增 variable，但無法把 variable 正確插入每個 panel 的查詢 template 裡。</p>
<h4>Investigation — 清楚的能力分界線</h4>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/10ac83ae-7408-46f1-964b-d7f2278d9b2c.png" alt="" style="display:block;margin:0 auto" />

<p>結果分成明顯兩群：</p>
<p><strong>拿到滿分的（1.0）</strong>：incident-triage、service-degradation-rca、cache-incident-blast-radius、retry-backlog-incident</p>
<p><strong>固定卡在 0.45（兩次一致）</strong>：</p>
<ul>
<li><p><code>payments-path-root-cause</code></p>
</li>
<li><p><code>slow-path-hotspot-correlation</code></p>
</li>
<li><p><code>deployment-blast-radius-check</code></p>
</li>
</ul>
<p>0.45 不是完全失敗——模型能識別部分症狀，但無法完成完整的因果推理鏈。這三道題的共同點：需要把 metrics 數字 + logs 時間序列 + traces span 結構對應到同一個根因，並給出有量化依據的結論。滿分的那些題目只需要跨 2 個訊號，這 3 道需要 3 個訊號全部對準。</p>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/5bc5fbe0-4881-4783-afe5-99d9d9958030.png" alt="" style="display:block;margin:0 auto" />

<h3>四個核心觀察</h3>
<p><strong>1. PromQL &gt; TraceQL &gt; LogQL 的能力梯度</strong></p>
<p>符合直覺：PromQL 語法嚴格語意清楚；TraceQL 以結構化查詢為主；LogQL 需要理解 pipeline 概念且輸出是文字，容錯空間最小。</p>
<p><strong>2. 新增比修改容易</strong></p>
<p>在 Dashboarding 類別，新增 panels 幾乎全滿分，但修改 variable binding 固定失敗。這反映對 Grafana dashboard JSON schema 的理解深度不同：建新 panel 只需知道基本結構，修改 variable binding 需要理解整個 dashboard 的資料流。</p>
<p><strong>3. 一致的低分比偶發的低分更有診斷價值</strong></p>
<p><code>payments-path-root-cause</code> 兩次 0.45、<code>dashboard-update-add-service-variable</code> 兩次 0.455、<code>logql-multi-stage-pipeline</code> 兩次 0.625——這些固定分數說明的是系統性理解缺口，是模型真正的能力邊界，而不是運氣問題。</p>
<p><strong>4. 11 個 RewardFileNotFoundError 值得追查</strong></p>
<p>依照 retry 設定，這類錯誤不會自動重試，需要逐一查看各 trial 的 <code>verifier/grading_details.json</code>，釐清是 agent 輸出格式問題還是 verifier 環境問題。</p>
<hr />
<h2>官方 Leaderboard 全局觀（GrafanaCON 2026）</h2>
<p>以上是 Gemini 3 Flash Preview 單一模型的分析。官方在 GrafanaCON 2026 公布了更完整的跨模型比較，以下是重點摘要。</p>
<h3>整體排名趨勢</h3>
<p><strong>leaderboard 以 pass^k（一致性）排序</strong>，而不是 pass@k（最好情況）。整體趨勢：</p>
<ul>
<li><p><strong>Anthropic Claude Opus 4.7（reasoning off）</strong> 拿到最高 pass^k，一致性最佳</p>
</li>
<li><p><strong>Claude Opus 4.7（reasoning on）</strong> pass@k 更高但 pass^k 略低——開啟 reasoning 讓模型偶爾能解更難的題，但也增加了不穩定性</p>
</li>
<li><p><strong>開源模型</strong>：Qwen 3.6 Plus 超越了部分較小的 Sonnet 和 GPT 變體，顯示開源模型在 observability 任務上已具競爭力</p>
</li>
</ul>
<h3>各類別的飽和程度</h3>
<table>
<thead>
<tr>
<th>類別</th>
<th>狀態</th>
</tr>
</thead>
<tbody><tr>
<td>Grafana API、PromQL</td>
<td>接近飽和，多數模型表現良好</td>
</tr>
<tr>
<td>Tempo（TraceQL）、Loki（LogQL）</td>
<td>中間層，仍有明顯差異</td>
</tr>
<tr>
<td>Dashboarding</td>
<td>最難，是目前最能區分模型的類別</td>
</tr>
</tbody></table>
<p>Dashboard 任務之所以最難，是因為它同時考驗四件事：state 正確、query 語法正確、variable wiring 正確、saved behavior 符合預期——任何一層出錯都會被 deterministic check 抓到。</p>
<h3>一個值得注意的任務設計細節</h3>
<p>官方提到 <code>promql-retry-backlog-triage</code> 這個任務揭示了一個有趣的 tradeoff：高 reasoning 或高 token 消耗的 agent，反而容易在這道題上過度蒐集資訊、繞遠路。這暗示 observability 任務的評分不只是「對不對」，也是「有沒有效率地對」——而 benchmark 的 cost 和 tool call 數量指標正好捕捉這個面向。</p>
<hr />
<h2>如何自己跑</h2>
<h3>環境需求</h3>
<pre><code class="language-bash"># 安裝工具鏈
git clone &lt;repo&gt;
cd o11y-bench
mise install
uv sync

# 設定 API keys
export ANTHROPIC_API_KEY=...   # grading 用（必要）
export GOOGLE_API_KEY=...      # 如果要跑 Gemini
export OPENAI_API_KEY=...      # 如果要跑 GPT
</code></pre>
<h3>快速驗證（單一題目）</h3>
<pre><code class="language-bash">mise run bench:job -- --model google/gemini-3-flash-preview \
  --task-name query-cpu-metrics --n-concurrent 1
</code></pre>
<h3>跑完整 63 題</h3>
<pre><code class="language-bash">mise run bench:job -- --model google/gemini-3-flash-preview
</code></pre>
<h3>跑所有模型全部題目（完整 suite）</h3>
<pre><code class="language-bash">mise run bench:suite
</code></pre>
<h3>沒有 Anthropic API key 也能跑</h3>
<pre><code class="language-bash">export SKIP_LLM_GRADING=1
export ANTHROPIC_API_KEY=dummy   # Harbor 前置檢查需要這個變數存在，填假的即可
mise run bench:job -- --model google/gemini-3-flash-preview --task-name query-cpu-metrics
</code></pre>
<p>注意：<code>investigation</code> 類的 task 全靠 LLM rubric，這種模式下分數會是 0，但 agent 行為本身還是會跑完。</p>
<h3>重新評分（不重跑 agent）</h3>
<pre><code class="language-bash">uv run python -m o11y_bench regrade --job-dir jobs/&lt;job-name&gt;
</code></pre>
<h3>其他常用指令</h3>
<pre><code class="language-bash">mise run test             # 跑 pytest 測試套件（驗 benchmark 邏輯，不需要 Docker）
mise run lint             # Ruff lint
mise run typecheck        # mypy
mise run setup:sync       # 從 tasks-spec/ 重新生成 tasks/
mise run setup:preflight  # 預先 build Docker image、清理舊容器

# 重建單一 job report
uv run python -m reporting.run_report --job-dir jobs/&lt;job-name&gt;

# 兩個 job 並排比較
uv run python -m reporting.compare_report \
  --job-dir jobs/&lt;suite-id&gt;/&lt;job-a&gt; \
  --job-dir jobs/&lt;suite-id&gt;/&lt;job-b&gt;
</code></pre>
<h3>解讀結果</h3>
<p>每個 trial 目錄下有三個關鍵檔案：</p>
<pre><code class="language-plaintext">jobs/&lt;job-name&gt;/&lt;task-name&gt;__&lt;trial-id&gt;/
  agent/trajectory.json          # 完整對話 + 所有 tool calls
  verifier/grading_details.json  # 各 criterion 分數 + judge 解釋
  verifier/reward.txt            # 最終分數（0.0–1.0）
</code></pre>
<p><code>grading_details.json</code> 裡的 <code>explanation:</code> 欄位直接說明 judge 為什麼扣分，是診斷模型行為最有效的入口。</p>
<hr />
<h2>拿來測試你們自己的 Agent</h2>
<p>o11y-bench 不只是用來跑公開 leaderboard 的工具，更實際的用途是讓你在開發 agent 的過程中，對著真實的 Grafana stack 持續驗證能力。接入的方式取決於你的 agent 形態，有三條路。</p>
<img src="https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/1ff2942b-29a6-4848-92de-aaefc0a51da6.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>路線一：只換 model，架構不動</h3>
<p>最快的起點——你的 agent 本質上是「LLM + tool use loop」，只是想換成自己的模型或推論服務：</p>
<pre><code class="language-bash"># 換成自己的模型
mise run bench:job -- --model your-provider/your-model

# 接 OpenAI-compatible 自架服務
export OPENAI_API_BASE=https://your-inference-server
mise run bench:job -- --model openai/your-model-name
</code></pre>
<p>這條路適合：評估特定 model 在 observability 任務上的能力、比較不同 reasoning effort 設定的效益。</p>
<hr />
<h3>路線二：接入自己的 Agent Framework</h3>
<p>如果你的 agent 用了自己的 framework（LangChain、自製 loop、LlamaIndex 等），需要實作一個薄薄的 Harbor agent class。Repo 裡的 <code>agents/langchain_o11y_agent.py</code> 就是範本，照著結構改：</p>
<pre><code class="language-python"># agents/my_team_agent.py
from harbor.agents.base import BaseAgent

class MyTeamAgent(BaseAgent):
    async def setup(self):
        pass  # 環境準備，可留空

    async def run(self, task):
        statement = task.statement   # 題目文字
        current_time = ...           # 從環境變數 O11Y_SCENARIO_TIME_ISO 讀

        # 用你的 framework 跑 agent loop
        # MCP server 在 http://localhost:8080/sse（mcp-grafana）
        result = await your_framework.run(statement, mcp_url="http://localhost:8080/sse")

        # 把結果寫到 /logs/agent/trajectory.json
        self.write_trajectory(result)
</code></pre>
<p>執行：</p>
<pre><code class="language-bash">mise run bench:job -- --model your/model \
  --agent-import-path agents.my_team_agent:MyTeamAgent
</code></pre>
<p><strong>Trajectory 格式要求</strong>：grading 系統只需要能讀到 <code>steps</code> 和最終的 assistant message，最簡化的結構：</p>
<pre><code class="language-json">{
  "schema_version": "ATIF-v1.6",
  "steps": [
    { "step_id": 1, "source": "user",  "message": "題目內容" },
    { "step_id": 2, "source": "agent", "tool_calls": [
        { "id": "tc1", "name": "query_prometheus", "arguments": {...} }
    ]},
    { "step_id": 3, "source": "tool",  "tool_call_id": "tc1", "content": "..." },
    { "step_id": 4, "source": "agent", "message": "最終回答" }
  ],
  "final_metrics": { "total_cost_usd": 0.0, "total_tool_calls": 3 }
}
</code></pre>
<p>grading 的 LLM rubric 讀的是最後一個 <code>source: "agent"</code> 且有 <code>message</code> 的 step；deterministic check 讀的是所有 <code>source: "tool"</code> 的 content。只要這兩段正確，分數就能算出來。</p>
<hr />
<h3>路線三：Agent 不用 MCP，有自己的 Grafana 操作方式</h3>
<p>如果你的 agent 已經有自己一套跟 Grafana 互動的方法（REST API wrapper、自製 SDK、CLI tool），可以參考 <code>agents/gcx_opencode_agent.py</code>——它把 MCP tools 全部移除，改讓 agent 透過 gcx CLI 操作 Grafana。</p>
<p>你的 agent class 在 <code>run()</code> 裡可以完全自主決定怎麼操作 Grafana，只要最終產出格式正確的 <code>trajectory.json</code> 就行。sidecar 容器裡的 Grafana 端口是固定的（<code>:3000</code>），Prometheus 在 <code>:9090</code>，Loki 在 <code>:3100</code>，Tempo 在 <code>:3200</code>。</p>
<hr />
<h3>建議的起步順序</h3>
<p>無論哪條路，建議這樣進入：</p>
<p><strong>1. 先跑預設 agent 確認環境正常</strong></p>
<pre><code class="language-bash">mise run bench:job -- --model your/model --task-name query-cpu-metrics --n-concurrent 1
</code></pre>
<p>這一步驗證 Docker 環境、API key、model provider 都通了。</p>
<p><strong>2. 挑 2–3 道你最在意的 task 重點觀察</strong></p>
<p>不需要跑全部 63 題。根據你的 agent 設計，挑最相關的類別：</p>
<ul>
<li><p>如果你的 agent 主打查詢能力：<code>promql-error-rate</code>、<code>logql-multi-stage-pipeline</code></p>
</li>
<li><p>如果你的 agent 主打根因分析：<code>incident-triage</code>、<code>payments-path-root-cause</code></p>
</li>
<li><p>如果你的 agent 主打 Grafana 操作：<code>dashboard-create-service-overview</code>、<code>dashboard-update-add-service-variable</code></p>
</li>
</ul>
<p><strong>3. 讀 grading_details.json，理解評分標準</strong></p>
<pre><code class="language-bash">cat jobs/&lt;job-name&gt;/&lt;task-name&gt;__&lt;trial-id&gt;/verifier/grading_details.json
</code></pre>
<p><code>explanation:</code> 欄位會直接告訴你 judge 為什麼扣分。在開始改 agent 之前，先確認評分標準符合你的預期——如果有 criterion 的定義不合理，直接去 <code>tasks-spec/</code> 修改 rubric，跑 <code>regrade</code> 就能看到新分數，不需要重跑 agent。</p>
<p><strong>4. 接入自己的 agent，跑完整 63 題</strong></p>
<p>確認了環境和評分標準之後，再把自己的 agent 接進來跑完整 benchmark。這時候的分數才有意義——你能知道自己的 agent 在哪個類別有系統性的弱點，以及跟公開 leaderboard 上的其他模型相比在什麼位置。</p>
<hr />
<h2>小結</h2>
<p>o11y-bench 的設計哲學是<strong>不測試模型「知道」什麼，而是測試模型「能做到」什麼</strong>：</p>
<ul>
<li><p><code>tasks-spec/</code> 用自然語言出題，不洩露語法提示</p>
</li>
<li><p><code>docker/</code> 跑起真實 stack，資料釘死確保可重現</p>
</li>
<li><p><code>agents/</code> 用輕量 ReAct loop，讓工具能力差異直接顯現</p>
</li>
<li><p><code>grading/</code> 用 deterministic check 防幻覺，用 LLM rubric 處理語意判斷</p>
</li>
</ul>
<p>從 Gemini 3 Flash Preview 的結果來看，0.710 的平均分顯示結構化查詢已相當成熟，但跨訊號根因分析和精確修改複雜資源結構這兩個能力邊界仍然清晰。整體 leaderboard 的趨勢也印證了這點：PromQL 和 Grafana API 類別接近飽和，Dashboard 和 Investigation 仍是區分模型能力的主戰場。</p>
<p>o11y-bench 刻意設計成 <strong>可檢驗（inspectable）、可重現（reproducible）、開放挑戰（open to challenge）</strong>。資料、題目、評分邏輯全部開源，任何人都可以在本地重現結果、加入新的 agent harness、或對評分標準提出質疑。這和那種「只公布分數、不公布方法」的封閉 benchmark 不同——結果的意義來自於它背後的流程是透明的。</p>
<p>leaderboard 持續更新在 <a href="https://o11ybench.ai/">o11ybench.ai</a>，提交新結果見 <a href="https://huggingface.co/datasets/grafanalabs/o11y-bench-leaderboard">Hugging Face 投稿 repo</a>，官方介紹文見 <a href="https://grafana.com/blog/o11y-bench-open-benchmark-for-observability-agents/">Grafana Blog</a>。</p>
]]></content:encoded></item><item><title><![CDATA[Claude Code Skills 由淺入深]]></title><description><![CDATA[接續上篇【OTel 陣法監控篇】，此卷深入解析奇術（Skills）機制——臥龍神算如何知曉有哪些兵器可用、糧草如何節省、奇術如何設計才不浪費、以及如何驗證奇術行為如你所謀。


稟告主公：為何需要奇術（Skills）？
主公可曾遇過此種困境：每次要臥龍神算（Claude Code）助你辦一件事，都得貼上一大段說明？

「請用我軍的插旗格式，格式如下：feat(scope): 描述，且...（以下三]]></description><link>https://ganhua.wang/claude-code-skills</link><guid isPermaLink="true">https://ganhua.wang/claude-code-skills</guid><category><![CDATA[claude-code]]></category><category><![CDATA[skills]]></category><category><![CDATA[OpenTelemetry]]></category><category><![CDATA[Grafana]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Fri, 20 Feb 2026 15:41:56 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/3cb0a019-5171-454b-a093-9c5d15217609.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>接續上篇【OTel 陣法監控篇】，此卷深入解析奇術（Skills）機制——臥龍神算如何知曉有哪些兵器可用、糧草如何節省、奇術如何設計才不浪費、以及如何驗證奇術行為如你所謀。</p>
</blockquote>
<hr />
<h2>稟告主公：為何需要奇術（Skills）？</h2>
<p>主公可曾遇過此種困境：每次要臥龍神算（Claude Code）助你辦一件事，都得貼上一大段說明？</p>
<blockquote>
<p>「請用我軍的插旗格式，格式如下：<code>feat(scope): 描述</code>，且...（以下三百字軍令）」</p>
</blockquote>
<p>每次開新一場戰役便要重複貼上一遍。更慘的是，若你有三十份這樣的「工作說明書」，全部塞進軍令總諭，不但糧草爆炸，臥龍神算的注意力也會被稀釋——什麼都想做，反而什麼都做不精。</p>
<p><strong>奇術就是解決此問題的機制。</strong></p>
<p>其核心思想甚為簡單：<strong>按需調兵，用多少拿多少。</strong></p>
<hr />
<h2>第一計：奇術如何運作？三層漸進揭露之術</h2>
<p>此乃全篇兵書最重要之處。理解了此機制，後面所有的設計決策都會豁然開朗。</p>
<p><em>此節由諸葛亮主講。孔明搖動羽扇，微笑道：</em></p>
<p><strong>孔明言：</strong> 主公請聽臣細細道來。</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/e7f0f6d2-71d2-4421-b4a5-ee4919cf2e15.png" alt="" style="display:block;margin:0 auto" />

<h3>第一層：目錄情報（Metadata）</h3>
<pre><code class="language-plaintext">神算啟動時 → 只載入每個奇術的名稱與描述 → 佔用極少糧草
</code></pre>
<p>臥龍神算啟動時，<strong>只</strong>把所有奇術的名稱和描述載入軍令總諭。此乃圖書館的目錄卡，告訴神算「你的兵器庫裡有什麼」，但不把每本兵書的內容都塞進去。</p>
<p><strong>技術細節</strong>：描述總共的糧草預算 = 戰場視野的 2%，最多 16,000 字元。若奇術太多超過預算，執行 <code>/context</code> 可以看到警告。</p>
<h3>第二層：奇術兵書（Instructions）</h3>
<pre><code class="language-plaintext">將領說「幫我審查這份奏摺」
  → 神算發現這符合 "review-pr" 這個奇術
  → 透過後台指令讀取 SKILL.md 兵書
  → 詳細的部署步驟才進入神算的兵略
</code></pre>
<p>奇術被觸發時，神算才去讀 <a href="http://SKILL.md">SKILL.md</a> 兵書。在此之前，<a href="http://SKILL.md">SKILL.md</a> 的內容完全不佔用戰場視野。</p>
<h3>第三層：運行時資源（Runtime Resources）</h3>
<pre><code class="language-plaintext">執行具體步驟時
  → 兵書輔冊（reference/*.md）：需要時才讀
  → 奇兵腳本（scripts/*.py）：直接執行，不讀進戰場視野
</code></pre>
<p>此處有個關鍵之點：<strong>奇兵腳本只有輸出結果會耗用糧草，腳本本身的程式碼不會</strong>。五百行的 Python 腳本，被執行後回傳一行結果，那也只花那一行結果的糧草。</p>
<pre><code class="language-plaintext">┌────────────────────────────────────────────────────┐
│  奇術：漸進揭露三層陣法                              │
│                                                     │
│  第一層：目錄情報（神算啟動）                        │
│  ├── name: "review-pr"                              │
│  └── description: "..."    ← 只有這些進戰場視野      │
│                                                     │
│  第二層：奇術兵書（奇術被觸發）                      │
│  └── SKILL.md 全文         ← 此時才載入              │
│                                                     │
│  第三層：運行時資源（執行步驟中）                    │
│  ├── references/checklist.md ← 需要時才讀            │
│  └── scripts/validate.py    ← 只有輸出進戰場視野     │
└────────────────────────────────────────────────────┘
</code></pre>
<hr />
<h2>第二計：糧草如何節省？奇術與軍需官之本質差異</h2>
<p>理解了三層陣法，我們來看奇術（Skills）和軍需官（MCP）的根本差異。</p>
<p><strong>司馬懿接回主講：</strong></p>
<p><em>仲達撫著長袖，冷靜分析道：</em></p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/00b3fcd8-3b0a-497e-b5c7-44a2ba486862.png" alt="" style="display:block;margin:0 auto" />

<h3>軍需官（MCP）的弊端</h3>
<p>軍需官要讓神算知道「有哪些兵器可用」，必須在對陣開始時把<strong>所有兵器的完整規格</strong>一次性注入：名稱、詳細描述、參數規格、使用範例。</p>
<p>以 GitHub 軍需官為例，它有三十餘件兵器。假設每件兵器規格消耗五百糧草：</p>
<pre><code class="language-plaintext">30 件兵器 × 500 糧草 = 15,000 糧草（光是「告知神算有哪些兵器」就花掉了）
</code></pre>
<p>若你連接四十個軍需官、三百件兵器，啟動成本可以高達數萬糧草。你還沒說任何事，神算就已經燒掉大量糧草了。</p>
<p>更麻煩的是，兵器太多會讓神算的注意力下降。根據 MCP Atlas 基準測試，即使最強的 Claude Opus 4.6 兵種，在四十個軍需官、三百件兵器的環境下，兵器調用準確率也只有 62%。</p>
<h3>奇術的解法</h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>比較維度</p></th><th><p>軍需官（MCP）</p></th><th><p>奇術（Skills）</p></th></tr><tr><td><p>啟動時載入</p></td><td><p>全部兵器完整規格</p></td><td><p>只載入名稱+描述（幾千糧草）</p></td></tr><tr><td><p>兵器選擇</p></td><td><p>神算從海量清單中挑</p></td><td><p>漏斗式引導，逐層縮小</p></td></tr><tr><td><p>適合場景</p></td><td><p>連接外部服務（GitHub、Slack、資料庫）</p></td><td><p>封裝固定行軍部署和本地知識庫</p></td></tr><tr><td><p>門檻</p></td><td><p>需要寫程式碼（MCP Server）</p></td><td><p>只需要寫兵書提示詞</p></td></tr></tbody></table>

<p>💡 <strong>孔明言</strong>：兩者不互相取代，而是互補。軍需官專注「連接各方諸侯」，奇術專注「封裝行軍流程」。未來架構會是：</p>
<pre><code class="language-plaintext">AI 謀士神算
  ├── 內建兵器（bash, read, write, edit）← 核心能力
  ├── 奇術層 ← 封裝 80% 的行軍流程
  └── 軍需官層 ← 少數場景需要遠端連線
</code></pre>
<hr />
<h2>第三計：你的第一個奇術——臨陣速建之法</h2>
<p><em>此節由諸葛亮主講。孔明搖動羽扇，微笑道：</em></p>
<h3>奇術駐紮何處？</h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>位置</p></th><th><p>兵書位址</p></th><th><p>適用範圍</p></th></tr><tr><td><p>個人</p></td><td><p><code>~/.claude/skills/&lt;skill-name&gt;/SKILL.md</code></p></td><td><p>所有陣地</p></td></tr><tr><td><p>陣地</p></td><td><p><code>.claude/skills/&lt;skill-name&gt;/SKILL.md</code></p></td><td><p>此陣地</p></td></tr><tr><td><p>軍團</p></td><td><p>由主帥令冊統一部署</p></td><td><p>全軍組織</p></td></tr></tbody></table>

<blockquote>
<p><strong>注意</strong>：舊的 <code>.claude/commands/</code> 目錄依然有效，只是奇術目錄多了資料夾、輔助兵書等功能。</p>
</blockquote>
<h3>建立第一個奇術</h3>
<pre><code class="language-bash">mkdir -p ~/.claude/skills/commit-helper
</code></pre>
<p>建立 <code>~/.claude/skills/commit-helper/SKILL.md</code> 兵書：</p>
<pre><code class="language-yaml">---
name: commit-helper
description: 根據 git diff 生成符合 Conventional Commits 規範的插旗訊息。當將領要插旗、寫插旗記錄、或詢問暫存變更時使用。
disable-model-invocation: true
---

根據 `git diff --staged` 的輸出，生成一條符合軍令規範格式的插旗訊息：

## 格式規則
- `feat(scope): 描述` — 新功能
- `fix(scope): 描述` — 修復亂象
- `docs(scope): 描述` — 兵書變更
- `refactor(scope): 描述` — 重整部署

## 行軍步驟
1. 執行 `git diff --staged` 查看變更
2. 分析變更類型和影響範圍
3. 生成一條簡潔的主訊息（50 字元以內）
4. 若有多項變更，加上條列說明

## 注意
- 不要自動執行 `git commit`，只輸出建議的訊息
- scope 用小寫，和陣地模組名稱一致
</code></pre>
<h3>兩種召喚方式</h3>
<p><strong>方式一：直接呼叫</strong></p>
<pre><code class="language-plaintext">/commit-helper
</code></pre>
<p><strong>方式二：自然語言觸發</strong>（描述符合時自動載入）</p>
<pre><code class="language-plaintext">幫我整理一下這次的插旗訊息
</code></pre>
<hr />
<h2>第四計：兵書題記完整欄位解析</h2>
<p>此為 <code>SKILL.md</code> 兵書開頭 <code>---</code> 之間可以設定的所有欄位：</p>
<pre><code class="language-yaml">---
name: my-skill              # 奇術識別名稱（小寫英數和連字號，最多 64 字）
description: "..."          # 告訴神算何時使用（最多 1024 字，用第三人稱寫）
argument-hint: &lt;奏摺號碼&gt;   # 在 / 選單中顯示的參數提示
disable-model-invocation: true  # true = 只有將領能手動觸發，神算不會自動呼叫
user-invocable: false       # false = 從 / 選單隱藏，但神算可自動觸發
allowed-tools: Read, Grep   # 執行此奇術時不需逐一確認的兵器清單（虎符授權）
model: claude-opus-4-6      # 指定執行這個奇術使用的兵種
context: fork               # fork = 在隔離的偏師中執行
agent: Explore              # 指定偏師類型（搭配 context: fork 使用）
hooks:                      # 奇術生命週期 Hooks
  - event: skill-start
    command: echo "奇術啟動"
---
</code></pre>
<h3>何時用 <code>disable-model-invocation: true</code>？</h3>
<p>此欄位防止神算自動觸發奇術，適合有副作用的操作：</p>
<pre><code class="language-yaml"># 出兵、傳送軍報、資料庫遷移 — 你想控制時機的操作
---
name: deploy-production
description: 出兵至正式戰場
disable-model-invocation: true  # 絕對不讓神算自己決定要出兵
---
</code></pre>
<h3>何時用 <code>user-invocable: false</code>？</h3>
<p>把奇術設為背景知識，讓神算在相關情境自動參考，但不出現在 <code>/</code> 選單：</p>
<pre><code class="language-yaml">---
name: legacy-system-context
description: 包含舊陣地的架構說明和注意事項。處理和舊陣地相關的程式碼時參考。
user-invocable: false  # 將領不需要直接呼叫，神算遇到相關情境會自動載入
---
</code></pre>
<h3>三種組合的行為對照</h3>
<table style="min-width:100px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>設定</p></th><th><p>將領可呼</p></th><th><p>神算可自動觸發</p></th><th><p>何時進戰場視野</p></th></tr><tr><td><p>預設</p></td><td><p>✅</p></td><td><p>✅</p></td><td><p>描述常駐，全文在觸發時載入</p></td></tr><tr><td><p><code>disable-model-invocation: true</code></p></td><td><p>✅</p></td><td><p>❌</p></td><td><p>描述不進戰場視野，全文在手動觸發時載入</p></td></tr><tr><td><p><code>user-invocable: false</code></p></td><td><p>❌</p></td><td><p>✅</p></td><td><p>描述常駐，全文在觸發時載入</p></td></tr></tbody></table>

<hr />
<h2>奇兵一策：拆分部署之法——引用輔助兵書</h2>
<p>此為節省糧草最重要的實作技巧。若你寫過軟體，你會發現奇術的設計原則和模組化程式設計幾乎一模一樣——只是操作對象從「程式碼」換成了「給神算的指令」。</p>
<p><em>此節由諸葛亮主講。孔明搖動羽扇，微笑道：</em></p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/3d3a5b67-3b45-42eb-b85c-1e0af09a36a3.png" alt="" style="display:block;margin:0 auto" />

<h3>以軍事部署思維理解奇術結構</h3>
<p><strong>奇術目錄 ≈ 一個軍團（Module）</strong></p>
<p>行軍打仗，我們不會把所有兵力塞進主帥一人；設計奇術也一樣，不要把所有知識塞進一個 <a href="http://SKILL.md">SKILL.md</a> 兵書。</p>
<pre><code class="language-plaintext"># 你熟悉的軍團結構：
src/bigquery/
├── index.ts          # 正面旗幟（只暴露必要介面）
├── finance.ts        # 財務謀略（內部實作）
├── sales.ts          # 銷售謀略（內部實作）
└── utils/
    └── validate.ts   # 輔助工具（不直接對外）

# 奇術的對應結構：
.claude/skills/bigquery-analysis/
├── SKILL.md          # 正面旗幟（神算的進入點）
├── reference/
│   ├── finance.md    # 財務謀略（需要時才載入）
│   └── sales.md      # 銷售謀略（需要時才載入）
└── scripts/
    └── validate.py   # 奇兵腳本（直接執行，不讀入戰場視野）
</code></pre>
<p><a href="http://SKILL.md"><strong>SKILL.md</strong></a> <strong>≈ 正面旗幟（Facade 佯攻之術）</strong></p>
<p><a href="http://SKILL.md">SKILL.md</a> 只宣告「這個奇術能做什麼、入口在哪」，把實作細節隱藏在兵書輔冊和奇兵腳本背後。此與佯攻之術完全相同：對外提供簡單的正面，背後可以有複雜的子陣。</p>
<pre><code class="language-plaintext"># 程式碼的 Facade：
class DataService {
  getReport(domain: string) { /* 隱藏內部複雜性 */ }
}

# 奇術的 Facade（SKILL.md 兵書）：
## 可用資料集
- 財務：[reference/finance.md](reference/finance.md)
- 銷售：[reference/sales.md](reference/sales.md)
</code></pre>
<p><strong>漸進揭露 ≈ 按需調兵（Lazy Loading）</strong></p>
<p>前線將領對按需調兵一定不陌生：不要在大戰前把所有兵力一次調到前線，用到哪個方向才調哪路兵馬。奇術的三層陣法做的是同一件事：</p>
<pre><code class="language-plaintext"># 按需調兵（前端）：
import('./finance')  // 只有進入 /finance 頁面才召來這路兵馬

# 漸進揭露（奇術）：
[reference/finance.md]  // 只有談到財務問題才讀這份兵書
</code></pre>
<p><strong>兵書輔冊（reference/ 資料夾） ≈ 職責分守原則（ISP）</strong></p>
<p>職責分守原則說：不要強迫將士依賴他們用不到的命令。同理，不要強迫神算在每次戰役都載入所有知識——把不同領域的知識拆分到各自的文件，神算只拿它需要的部分。</p>
<pre><code class="language-plaintext"># 職責分守違反（單一大陣）：
interface BigQueryService {
  financeQuery();   // 每次都要帶著這個
  salesQuery();     // 和這個
  productQuery();   // 和這個
}

# 職責分守遵守（分開的小陣）：
interface FinanceService { financeQuery(); }
interface SalesService { salesQuery(); }
# ↑ 對應到：把知識拆進 finance.md / sales.md
</code></pre>
<p><strong>奇兵腳本（scripts/ 資料夾） ≈ 封裝內功（Encapsulation）</strong></p>
<p>腳本的程式碼邏輯對神算是不透明的——它只知道「執行這個腳本，取得結果」，不需要理解腳本內部怎麼運作。此與封裝兵器內部機密、只暴露使用方法的概念完全一致。</p>
<pre><code class="language-python"># validate_query.py 的 500 行實作細節，神算完全不知道
# 神算只知道：
# - 輸入：SQL 查詢字串
# - 輸出：OK 或錯誤訊息
# 此乃封裝內功
</code></pre>
<hr />
<h3>實作：SKILL.md 作為正面旗幟</h3>
<pre><code class="language-markdown">---
name: bigquery-analysis
description: 分析 BigQuery 資料，生成業務報告。處理財務、銷售、產品相關數據查詢時使用。
allowed-tools: Bash(bq *)
---

# BigQuery 分析謀士

## 可用資料集（依需求載入對應兵書）

| 領域     | 說明              | 兵書位址                                          |
|--------|-----------------|------------------------------------------------|
| **財務** | 營收、ARR、帳單  | [reference/finance.md](reference/finance.md)   |
| **銷售** | Pipeline、機會   | [reference/sales.md](reference/sales.md)       |
| **產品** | API 使用量、功能採用 | [reference/product.md](reference/product.md) |

## 行軍部署，分三步走：

1. 確認將領要查詢哪個領域 → 讀取**對應的**兵書輔冊（不要全部讀）
2. 撰寫查詢 → 執行驗證腳本 → 執行查詢
3. 格式化結果

## 注意事項
- 所有查詢必須排除測試帳號（`WHERE account_type != 'test'`）
- 日期一律使用 UTC
</code></pre>
<p><strong>效果：</strong> 將領問「銷售 Pipeline 本週的數字」時：</p>
<ol>
<li><p>神算讀 SKILL.md 兵書（約 100 行）</p>
</li>
<li><p>正面旗幟引導它判斷：這是銷售問題 → 讀 <code>reference/sales.md</code></p>
</li>
<li><p><code>finance.md</code> 和 <code>product.md</code> 完全不進戰場視野</p>
</li>
</ol>
<p>三份兵書輔冊若各 200 行，此次戰役省了 400 行糧草。</p>
<hr />
<h3>單一職責原則（SRP）：一個奇術辦一件事</h3>
<p>SRP 說：一個軍團應該只有一個改變的理由。奇術設計也一樣——<strong>不要建一個「萬能奇術」</strong>。</p>
<pre><code class="language-plaintext">❌ 違反 SRP：
.claude/skills/everything/
├── SKILL.md  # 裡面同時處理：奏摺審查、兵書生成、出兵、資料分析...

✅ 遵守 SRP：
.claude/skills/review-pr/     # 只做奏摺審查
.claude/skills/gen-docs/      # 只做兵書生成
.claude/skills/deploy/        # 只做出兵
.claude/skills/data-analysis/ # 只做資料分析
</code></pre>
<p>細粒度的奇術可以被**組合（Compose）**使用——神算在一次戰役中可以依序觸發多個奇術，此和兵法組合的概念一樣。</p>
<hr />
<h3>黃金法則：旗幟最好只有一層深度</h3>
<p>此對應兵法中的「避免過深的糧道依賴鏈」。若 A 依賴 B，B 又依賴 C，任何一層出問題都會導致整條糧道斷裂。</p>
<pre><code class="language-plaintext">❌ 過深的糧道依賴鏈：
SKILL.md → advanced.md → details.md → 真正的情報
# 神算讀到 advanced.md 發現還要再讀 details.md
# 可能只預覽就決定跳過，導致情報不完整

✅ 扁平的依賴（最多一層）：
SKILL.md → finance.md  （直接是完整情報）
SKILL.md → sales.md
SKILL.md → product.md
</code></pre>
<h3>超過 100 行的兵書輔冊要加目錄（相當於 JSDoc / 兵器型別定義）</h3>
<pre><code class="language-markdown"># 財務資料 Schema 兵書

## 目錄
- [revenue_monthly 陣型](#revenue-monthly)
- [billing_events 陣型](#billing-events)
- [arr_snapshots 陣型](#arr-snapshots)

## revenue_monthly
...
</code></pre>
<p>即使神算只讀到前幾行，它也能從目錄知道整份兵書的全貌，就像 TypeScript 的 <code>.d.ts</code> 型別定義——不需要看實作就能知道有什麼可以用。</p>
<hr />
<h2>奇兵二策：動態注陣與分兵遣將</h2>
<h3>動態上下文注入（<code>!</code> 語法）</h3>
<p>在奇術被觸發時，可以預先執行 Shell 指令，把輸出注入到提示詞中：</p>
<pre><code class="language-yaml">---
name: pr-review
description: 對奏摺進行完整的程式碼審查
context: fork
agent: Explore
allowed-tools: Bash(gh *)
---

## 奏摺情報（系統自動抓取）

- 奏摺差異：!`gh pr diff`
- 奏摺描述：!`gh pr view --json title,body`
- 變更檔案：!`gh pr diff --name-only`
- CI 戰況：!`gh pr checks`

## 審查任務

請針對以上奏摺進行完整審查，重點關注：
1. 邏輯正確性
2. 是否有遺漏的邊界情況
3. 演武場覆蓋是否足夠
4. 是否符合陣地程式碼規範
</code></pre>
<p><strong>重要</strong>：<code>command</code> 是在奇術載入時就執行，神算只看到執行結果，不是神算在「做」這個動作。</p>
<blockquote>
<p>「奇術之妙，在於因地制宜。以 ! 之法，可臨陣攝取八方情報——如 CPU 之氣、記憶體之靈 ，將動態之氣注入軍令，使神算所得情報皆為即時戰況，此乃『借力使力』之最輕量陣法 。」</p>
</blockquote>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/15d73d91-1b9f-43d3-8664-d1b4cd3c9198.png" alt="" style="display:block;margin:0 auto" />

<h3>在偏師中執行（<code>context: fork</code>）</h3>
<p>加上 <code>context: fork</code> 讓奇術在獨立沙箱執行，不共享當前戰役歷史：</p>
<pre><code class="language-yaml">---
name: deep-research
description: 對程式碼庫進行深入研究分析
context: fork
agent: Explore          # 使用探查偏師（唯讀兵器集，防止意外修改）
---

深入研究 $ARGUMENTS 的實作細節：

1. 用搜索斥候找到所有相關檔案
2. 閱讀核心實作
3. 整理依賴關係
4. 回報：主要邏輯、潛在亂象、建議改進方向
</code></pre>
<p><strong>可選的偏師類型：</strong></p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>偏師</p></th><th><p>兵器集</p></th><th><p>適用場景</p></th></tr><tr><td><p><code>Explore</code></p></td><td><p>唯讀（Glob, Grep, Read）</p></td><td><p>研究、分析、不能動程式碼</p></td></tr><tr><td><p><code>Plan</code></p></td><td><p>謀劃工具</p></td><td><p>設計架構、制定計畫</p></td></tr><tr><td><p><code>general-purpose</code></p></td><td><p>完整兵器集</p></td><td><p>一般執行任務</p></td></tr><tr><td><p>自定義偏師</p></td><td><p>自己定義</p></td><td><p>從 <code>.claude/agents/</code> 載入</p></td></tr></tbody></table>

<blockquote>
<p>「深入險境，不宜動搖大軍。以 fork 之法遣一偏師，在隔離之沙場進行深入探究（Deep Research） 。其探查結果回報即可，偏師即使在險境中有所變動，亦不傷及中軍大帳之戰役歷史 。」</p>
</blockquote>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/b56fe8b4-f7ed-4efa-a8f4-e2ecda070e8f.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>第五計：選兵指揮之術——讓不同奇術用不同兵種</h2>
<pre><code class="language-yaml">---
name: quick-lint
description: 快速 Lint 程式碼，找常見亂象
model: claude-haiku-4-5-20251001   # 輕量任務用 Haiku 兵種，速度快糧草少
---

快速掃描 $ARGUMENTS 檔案，找出：
- 明顯的語法錯誤
- 未使用的 import
- 明顯的命名問題
</code></pre>
<pre><code class="language-yaml">---
name: architecture-review
description: 對陣地架構做深度評估
model: claude-opus-4-6             # 需要深度謀略的任務用 Opus 兵種
context: fork
---

對以下架構設計進行深度評估：$ARGUMENTS

評估維度：可擴展性、安全性、維護成本、效能瓶頸
</code></pre>
<p><strong>兵種 ID 速查：</strong></p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>兵種</p></th><th><p>ID</p></th><th><p>適用場景</p></th></tr><tr><td><p>Opus 4.6</p></td><td><p><code>claude-opus-4-6</code></p></td><td><p>複雜謀略、架構設計</p></td></tr><tr><td><p>Sonnet 4.6</p></td><td><p><code>claude-sonnet-4-6</code></p></td><td><p>一般開發任務（預設）</p></td></tr><tr><td><p>Haiku 4.5</p></td><td><p><code>claude-haiku-4-5-20251001</code></p></td><td><p>輕量、重複性任務</p></td></tr></tbody></table>

<blockquote>
<p>「夫兵者，各有其材。Haiku 如輕騎，迅捷而省糧，利在快攻 ；Sonnet 如中軍步兵，穩健多能，為陣中砥柱 ；Opus 則如大將軍，深謀遠慮，專應架構之變 。主公應視戰情緩急，調遣合適兵種，方為度支之道。」</p>
</blockquote>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/22c4c4e6-21b4-40ca-9743-b56fe88ce1cf.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>第六計：臥龍神算內建哪些兵器？它如何知曉武器存在？</h2>
<h3>兵器發現機制</h3>
<p>神算知道有哪些兵器，來源有三：</p>
<ol>
<li><p><strong>臥龍神算內建兵器</strong>：隨系統自動注入，永遠可用</p>
</li>
<li><p><strong>奇術宣告的虎符授權（</strong><code>allowed-tools</code><strong>）</strong>：奇術被觸發時，這些兵器可不經確認直接使用</p>
</li>
<li><p><strong>軍需官（MCP Server）兵器</strong>：連接的軍需官在啟動時注入完整定義</p>
</li>
</ol>
<h3>臥龍神算內建兵器完整清單</h3>
<pre><code class="language-plaintext">核心操作兵器：
├── Bash          衝鋒奇兵（執行 Shell 指令）
├── Read          探查細作（讀取檔案）
├── Write         修築工事之兵（建立/覆寫檔案）
├── Edit          精密工事之兵（精確替換檔案內容）
├── Glob          搜索斥候（用 Pattern 搜尋檔案）
└── Grep          情報細作（在檔案內容中搜尋）

遠探工具：
├── WebFetch      信使斥候（抓取網頁內容）
└── WebSearch     天下情報（網路搜尋）

開發工具：
├── Task          調兵遣將（委派任務給偏師）
├── LSP           兵器語言服務（GoToDefinition、FindReferences 等）
└── NotebookEdit  策劃冊編修（編輯 Jupyter Notebook）

任務管理：
├── TodoWrite     軍令清單建立
└── TodoRead      軍令清單查看

互動工具：
└── AskUserQuestion   向將領提問，等待回覆後再繼續
</code></pre>
<h3><code>AskUserQuestion</code> 特別說明</h3>
<p>此兵器值得單獨拿出來講，因為在奇術設計裡特別常見卻容易被忽略。</p>
<p><strong>其作用</strong>：讓神算暫停執行，彈出一個選項面板或輸入框向將領確認，再繼續。</p>
<p><strong>典型使用場景</strong>：</p>
<pre><code class="language-yaml">---
name: sanguo-rewrite
description: 以三國軍師風格改寫技術文章
allowed-tools: Read, Write, AskUserQuestion   # ← 注意這裡的虎符授權
argument-hint: &lt;文件路徑&gt;
---

1. 讀取 $ARGUMENTS 指定的文件
2. 若 $ARGUMENTS 為空，使用 AskUserQuestion 詢問文件路徑
3. 改寫並輸出
</code></pre>
<p>當將領直接打 <code>/sanguo-rewrite</code>（沒帶路徑），奇術會透過 <code>AskUserQuestion</code> 彈出詢問，而不是憑空猜或直接報錯。</p>
<p><strong>為何要在</strong> <code>allowed-tools</code> <strong>裡宣告它？</strong></p>
<p>預設情況下，神算使用兵器需要逐一確認。把 <code>AskUserQuestion</code> 加入虎符授權，代表奇術執行期間可以不經額外確認就向你提問——讓互動流程更順暢。</p>
<p><strong>在奇術裡的實用寫法</strong>：</p>
<pre><code class="language-markdown">## 出征前確認

若以下任一條件不明確，使用 AskUserQuestion 詢問後再繼續：
- 目標文件路徑未指定
- 輸出格式有多個選項（Markdown / HTML / 純文字）
- 操作會覆寫現有兵書
</code></pre>
<p>此讓奇術具備「智慧詢問」的能力：需要確認的才問，不需要的直接執行。</p>
<h3>虎符授權（<code>allowed-tools</code>）語法細節</h3>
<pre><code class="language-yaml">---
allowed-tools: Read, Grep, Glob          # 允許特定兵器
---

---
allowed-tools: Bash(git *)               # 允許衝鋒奇兵，但只能執行 git 開頭的指令
---

---
allowed-tools: Bash(npm run *), Read, Write   # 組合：允許 npm 指令 + 讀寫
---
</code></pre>
<p>此機制讓你可以在奇術裡開放特定兵器的自動執行權限，減少操作過程中的確認彈窗，同時又不會全面放開兵器庫。</p>
<blockquote>
<p>「軍令有疑，必先諮詢主公。奇術運作時，若遇關隘不明，軍師將手持『智慧詢問』之兵，退回中軍請示 。若主公預賜『虎符授權』（Allowed Tools），則部分兵器可先斬後奏，不驚擾主公，使行軍流程如水流般順暢 。」</p>
</blockquote>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/f16a6e7f-2fa9-4096-aa65-6da0c67631c3.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>第七計：自鑄兵器之法——自行開發工具給神算使用</h2>
<h3>方式一：軍需官（MCP Server）——最完整的自訂兵器</h3>
<p>透過 MCP 協議開發自定義兵器，神算可以調用你的 API、資料庫、內部服務：</p>
<pre><code class="language-python"># 一個簡單的軍需官範例（Python）
from anthropic import MCP

@MCP.tool("get_deploy_status")
def get_deploy_status(env: str) -&gt; dict:
    """查詢出兵狀態"""
    # 呼叫你的內部 API
    return your_internal_api.get_status(env)
</code></pre>
<p>在 <code>~/.claude/settings.json</code> 主帥令冊中設定：</p>
<pre><code class="language-json">{
  "mcpServers": {
    "company-tools": {
      "command": "python",
      "args": ["/path/to/your/mcp_server.py"]
    }
  }
}
</code></pre>
<h3>方式二：奇術內建腳本（輕量版，不需要軍需官框架）</h3>
<p>把腳本打包進奇術目錄，透過衝鋒奇兵執行：</p>
<pre><code class="language-plaintext">.claude/skills/deploy-checker/
├── SKILL.md
└── scripts/
    └── check_deploy.sh   # 你的自訂兵器邏輯
</code></pre>
<pre><code class="language-yaml">---
name: deploy-checker
description: 檢查所有陣地的出兵狀態
allowed-tools: Bash(bash ~/.claude/skills/deploy-checker/scripts/*)
---

執行出兵狀態檢查：

```bash
bash ~/.claude/skills/deploy-checker/scripts/check_deploy.sh $ARGUMENTS

解讀輸出並提供建議。
</code></pre>
<p>此法門檻極低：任何 Shell/Python/Node.js 腳本都可以變成神算的「兵器」。</p>
<h3>方式三：<code>!</code> 動態注陣（最輕量）</h3>
<p>若兵器只是抓資料，用動態注陣就夠了：</p>
<pre><code class="language-yaml">---
name: check-metrics
description: 查看軍情數報
allowed-tools: Bash(curl *)
---

## 當前戰況
- CPU 使用率：!"curl -s http://localhost:9090/api/v1/query?query=node_cpu_usage"
- 記憶體：!"curl -s http://localhost:9090/api/v1/query?query=node_memory_usage"

根據以上軍情分析戰況，若有亂象提供建議。
</code></pre>
<hr />
<h2>陣前排難：驗證你的奇術</h2>
<p><strong>司馬懿接回主講：</strong></p>
<p><em>仲達補充：</em> 孔明設計奇術，仲達負責驗證——此乃軍中分工之道。</p>
<h3>先釐清：「評估兵書」是規格書，不是可執行的演武框架</h3>
<p>官方文件提到的評估 JSON 結構長這樣：</p>
<pre><code class="language-json">{
  "skills": ["commit-helper"],
  "query": "幫我整理這次的插旗訊息",
  "expected_behavior": [
    "執行 git diff --staged 取得變更內容",
    "判斷變更類型（feat/fix/docs/refactor）",
    "生成符合軍令規範格式的訊息",
    "不自動執行 git commit"
  ]
}
</code></pre>
<p><strong>但官方同時說：「目前沒有內建的執行方式，請自行建立評估系統。」</strong></p>
<p>此 JSON 的用途是<strong>寫清楚你對奇術的期望</strong>，讓你在手動或程式化演武時有明確的對照基準。它不是一個 <code>npm test</code> 那樣可以直接跑的東西。</p>
<p>實際上有三個層次的演武方式，從輕到重：</p>
<hr />
<h3>亂象一：手動演武（最快驗證）</h3>
<p>最直接的方式：開臥龍神算，真的跑奇術，然後<strong>逐條對照</strong> expected_behavior 清單。</p>
<p><strong>行軍步驟：</strong></p>
<pre><code class="language-bash"># 1. 確認奇術已放在正確位置
ls ~/.claude/skills/commit-helper/

# 2. 打開臥龍神算
claude

# 3. 先確認奇術被認得
What skills are available?

# 4. 觸發奇術，觀察行為
/commit-helper
# 或自然語言：
# 幫我整理這次的插旗訊息
</code></pre>
<p><strong>觀察清單（對照你寫的 expected_behavior）：</strong></p>
<pre><code class="language-plaintext">□ 神算有沒有執行 git diff --staged？
□ 有沒有正確判斷 feat / fix / docs？
□ 格式有沒有符合軍令規範格式？
□ 有沒有自己去執行 git commit（這個不應該發生）？
□ 有沒有讀了不應該讀的兵書輔冊？
□ 有沒有問不必要的問題？
</code></pre>
<p>此方式的限制是「你自己跑一次」，容易遺漏邊界情況。建議為同一個奇術準備 <strong>三個以上不同的詢問</strong>（正常情況、邊界情況、模糊情況）。</p>
<hr />
<h3>亂象二：<code>claude -p</code> 程式化演武（半自動）</h3>
<p>臥龍神算支援 <code>-p</code> 旗號（print mode）非互動式執行，可以寫成腳本批次演武：</p>
<pre><code class="language-bash"># 基本用法：把詢問傳進去，直接輸出結果
claude -p "幫我整理這次的插旗訊息"

# 搭配 --output-format json 可以拿到結構化結果
claude -p "幫我整理這次的插旗訊息" --output-format json
</code></pre>
<p>把這個包進 Shell 腳本，就能批次跑多個演武情境：</p>
<pre><code class="language-bash">#!/bin/bash
# test_commit_skill.sh

PASS=0
FAIL=0

run_test() {
  local description="$1"
  local query="$2"
  local should_contain="$3"
  local should_not_contain="$4"

  echo "🧪 演武：$description"
  result=\((claude -p "\)query" 2&gt;&amp;1)

  if echo "\(result" | grep -q "\)should_contain"; then
    if [ -n "\(should_not_contain" ] &amp;&amp; echo "\)result" | grep -q "$should_not_contain"; then
      echo "  ❌ 失敗：結果含有不應出現的「$should_not_contain」"
      ((FAIL++))
    else
      echo "  ✅ 通過"
      ((PASS++))
    fi
  else
    echo "  ❌ 失敗：結果未包含「$should_contain」"
    echo "  輸出預覽：\((echo "\)result" | head -3)"
    ((FAIL++))
  fi
}

# 演武案例
run_test \
  "新功能的插旗" \
  "剛加了一個登入功能，幫我整理插旗訊息" \
  "feat" \
  "git commit"   # 不應該自動執行插旗

run_test \
  "亂象修復的插旗" \
  "修了一個日期顯示錯誤的亂象，幫我整理插旗訊息" \
  "fix" \
  "git commit"

run_test \
  "兵書變更的插旗" \
  "更新了 README 兵書，幫我整理插旗訊息" \
  "docs" \
  ""

echo ""
echo "結果：\(PASS 通過 / \)FAIL 失敗"
</code></pre>
<pre><code class="language-bash">chmod +x test_commit_skill.sh
./test_commit_skill.sh
</code></pre>
<p><strong>限制</strong>：<code>grep</code> 只能做字串比對，無法判斷「格式正不正確」這類語意問題。對於需要語意判斷的期望行為，還是得靠人眼或下面的方法三。</p>
<hr />
<h3>亂象三：雙謀士演武法（最接近真實情境）</h3>
<p>讓另一個神算實例扮演「測試謀士」：</p>
<pre><code class="language-plaintext">謀士甲（你的開發對話）              謀士乙（新戰役，乾淨環境）
    │                                        │
    │── 幫我設計這個奇術 ──&gt;               │
    │&lt;── 生成 SKILL.md ──                   │
    │                                        │
    │         放好 SKILL.md，給實際任務 ──&gt; │
    │                          &lt;── 觀察行為 │
    │                                        │
    │── 它在 X 地方卡住了，沒有讀 finance.md │
    │&lt;── 建議：把 finance.md 的連結移到更前面│
    │                                        │
    │         更新 SKILL.md，再演武一次 ──&gt; │
    │                          &lt;── 通過了 ──│
</code></pre>
<p><strong>謀士乙要觀察的具體行為：</strong></p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>觀察點</p></th><th><p>好的訊號</p></th><th><p>壞的訊號</p></th></tr><tr><td><p>兵書讀取順序</p></td><td><p>只讀相關的兵書輔冊</p></td><td><p>把所有兵書都讀一遍</p></td></tr><tr><td><p>軍令遵守</p></td><td><p>從未觸發不應該觸發的操作</p></td><td><p>忘記奇術裡某條限制</p></td></tr><tr><td><p>詢問時機</p></td><td><p>只在真的需要確認時才問</p></td><td><p>問了不必要的問題，或沒問就猜</p></td></tr><tr><td><p>輸出格式</p></td><td><p>符合奇術規定的格式</p></td><td><p>自由發揮，沒照模板</p></td></tr></tbody></table>

<p>💡 <strong>仲達觀察</strong>：你在謀士甲裡對奇術設計太熟悉，容易用「已知答案」去補充奇術沒說清楚的部分。謀士乙是第一次看到這個奇術，它的行為才是真實將領的縮影。</p>
<hr />
<h3>亂象四：不同兵種演武</h3>
<pre><code class="language-yaml"># Haiku 兵種需要更明確的指引
model: claude-haiku-4-5-20251001
---
# 若奇術對 Opus 有效，但對 Haiku 失效，
# 代表你的奇術依賴兵種自身的推理能力，需要補充更多明確指示
</code></pre>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>兵種</p></th><th><p>演武重點</p></th></tr><tr><td><p>Haiku</p></td><td><p>奇術是否提供足夠的指引？</p></td></tr><tr><td><p>Sonnet</p></td><td><p>奇術是否清晰高效？</p></td></tr><tr><td><p>Opus</p></td><td><p>奇術有沒有過度解釋（浪費糧草）？</p></td></tr></tbody></table>

<h3>常見亂象排除</h3>
<p><strong>亂象一：奇術不自動觸發？</strong></p>
<pre><code class="language-bash"># 在臥龍神算中詢問：
What skills are available?

# 確認 description 包含將領自然會說的關鍵字
# 描述過於通用 = 不容易被匹配
</code></pre>
<p><strong>亂象二：奇術觸發太頻繁？</strong></p>
<pre><code class="language-yaml"># 加上更精確的觸發條件，或改為手動觸發
disable-model-invocation: true
</code></pre>
<p><strong>亂象三：奇術太多，有些沒載入？</strong></p>
<pre><code class="language-bash"># 設定軍令旗號放寬糧草預算
export SLASH_COMMAND_TOOL_CHAR_BUDGET=32000
</code></pre>
<hr />
<h2>第八計：用 OTel 陣法親眼驗證三層機制（接續烽火台篇）</h2>
<p><strong>司馬懿接回主講：</strong></p>
<p>若你已按上一篇架好 OTel 陣法 + 大帳（Grafana），可以直接用斥候情報<strong>觀察三層漸進揭露的每一步行為</strong>。</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/9f62277f-146b-4bab-91a4-1859078628de.png" alt="" style="display:block;margin:0 auto" />

<h3>步驟一：建立一個有完整三層的演武奇術</h3>
<pre><code class="language-bash">mkdir -p ~/.claude/skills/layer-test/reference
mkdir -p ~/.claude/skills/layer-test/scripts
</code></pre>
<p><code>~/.claude/skills/layer-test/SKILL.md</code> 兵書：</p>
<pre><code class="language-yaml">---
name: layer-test
description: 演武用：顯示三國戰役列表。當將領詢問三國戰役、赤壁之戰、官渡之戰時使用。
allowed-tools: Read, Bash
---

# 三國戰役資料庫

## 可用資料

- 主要戰役列表：[reference/battles.md](reference/battles.md)

## 執行步驟

1. 先讀取 reference/battles.md
2. 執行 scripts/format.sh 格式化輸出
3. 回答將領問題
</code></pre>
<p><code>~/.claude/skills/layer-test/reference/battles.md</code> 兵書輔冊：</p>
<pre><code class="language-markdown"># 主要戰役
- 赤壁之戰（208年）
- 官渡之戰（200年）
- 夷陵之戰（221年）
</code></pre>
<p><code>~/.claude/skills/layer-test/scripts/format.sh</code> 奇兵腳本：</p>
<pre><code class="language-bash">#!/bin/bash
echo "=== 戰役查詢結果 ==="
date
echo "查詢完成"
</code></pre>
<p>然後開臥龍神算，說：「赤壁之戰是哪一年？」</p>
<hr />
<h3>步驟二：在大帳 Explore 分層觀察</h3>
<p><strong>第一層（目錄情報）— 看輸入糧草基準值</strong></p>
<p>目錄情報不會產生獨立 event，但所有奇術的描述都注入在軍令總諭裡，所以每次傳令兵出使的輸入糧草都包含它的重量。對比有奇術和沒有奇術的第一筆傳令，差異就是目錄情報佔用的量。</p>
<pre><code class="language-logql">{service_name="claude-code"}
  | event_name = "api_request"
  | line_format "input={{.input_tokens}} cache_read={{.cache_read_tokens}} model={{.model}}"
</code></pre>
<p><strong>第二層（SKILL.md 兵書被讀取）— 看探查細作的路徑</strong></p>
<pre><code class="language-logql">{service_name="claude-code"}
  | event_name = "tool_result"
  | tool_name = "Read"
  | line_format "path={{.tool_parameters}} duration={{.duration_ms}}ms"
</code></pre>
<p>你會在 <code>tool_parameters</code> 看到 <code>SKILL.md</code> 兵書的完整路徑出現，這就是第二層觸發的證據。</p>
<p><strong>第三層（兵書輔冊被讀、奇兵腳本被執行）— 分開看</strong></p>
<pre><code class="language-logql"># 兵書輔冊被讀取
{service_name="claude-code"}
  | event_name = "tool_result"
  | tool_name = "Read"
  | line_format "{{.tool_parameters}}"
  |= "reference"

# 奇兵腳本被執行（只有輸出進戰場視野，腳本本身不進）
{service_name="claude-code"}
  | event_name = "tool_result"
  | tool_name = "Bash"
  | line_format "cmd={{.tool_parameters}} output_size={{.tool_result_size_bytes}}bytes"
</code></pre>
<hr />
<h3>步驟三：切換 Table 模式看完整時序</h3>
<p>在大帳 Explore 把顯示模式從 <strong>Logs</strong> 改成 <strong>Table</strong>，然後跑這條軍令：</p>
<pre><code class="language-logql">{service_name="claude-code"}
  | json
  | session_id=`你的-session-id`
  | event_name =~ "tool_result|api_request|user_prompt"
  | line_format "{{.event_sequence}} | {{.event_name}} | {{.tool_name}} | {{.duration_ms}}ms | size={{.tool_result_size_bytes}}B"
</code></pre>
<blockquote>
<p>💡 <strong>仲達觀察</strong>：關於 <code>| json</code>：臥龍神算的戰報 body 不是 JSON（只是 <code>claude_code.event_name</code> 字串），所以 <code>| json</code> 會在每筆戰報加一個 <code>__error__="JSONParserErr"</code> 欄位，但<strong>不影響查詢結果</strong>。大帳的 query builder 常會自動幫你加，直接用完全沒問題；若想消除 <code>__error__</code> 欄位，把 <code>| json</code> 移除即可。</p>
</blockquote>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/7dc48902-90e0-4839-8871-e02c7aafc4f6.png" alt="" style="display:block;margin:0 auto" />

<p>按時間排序，你會看到三層的完整執行鏈（以下為實際 OTel 陣法數據）：</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/acfc461b-7688-4542-8438-6cf7fe8e46aa.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-plaintext">序號  時刻      事件             兵器   耗時      大小    說明
──────────────────────────────────────────────────────────────────
  0  13:28:03  user_prompt      —      —         —       你說「赤壁之戰是哪年？」（9字）

  1  13:28:08  tool_decision    奇術   —         —       ┐ 第二層：
  2  13:28:08  tool_result      奇術   9ms       74B     ┘ SKILL.md 兵書指令進戰場視野

  3  13:28:08  api_request      —      4637ms    —       神算讀完指令後謀略

  4  13:28:10  tool_decision    Read   —         —       ┐ 第三層：
  6  13:28:10  tool_result      Read   34ms      347B    ┘ battles.md 進戰場視野

  7  13:28:12  api_request      —      2009ms    —       謀略要執行腳本

  9  13:28:13  tool_decision    Bash   —         —       ┐ 第三層：
 10  13:28:14  tool_result      Bash   691ms     226B    ┘ format.sh 輸出進戰場視野
                                                         （腳本本身沒進戰場視野）

 11  13:28:17  api_request      —      2810ms    —       組合最終答案
</code></pre>
<p>三層陣法的每一步都清楚可見。</p>
<p>若不是呼叫奇術的戰役，會沒有兵器階段的 span，這是因為神算知道此問題跟任何兵器都沒關係：</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/55c60ae7-5114-4c32-bb22-41da2060fca0.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/9fe6562f-0762-4cad-820e-f64d9d335321.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>從糧草補給效率看按需調兵的實際成本</h3>
<p>每次傳令兵出使的 <code>cache_creation_tokens</code>（當輪新增進糧倉的量）會隨著戰場視野逐步累積而遞減：</p>
<table style="min-width:100px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>傳令出使</p></th><th><p>新增糧倉</p></th><th><p>從糧倉直取</p></th><th><p>說明</p></th></tr><tr><td><p>序號 3（第一輪謀略）</p></td><td><p><strong>4,056</strong></p></td><td><p>22,591</p></td><td><p>奇術目錄情報 + <a target="_self" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="http://SKILL.md" style="pointer-events:none">SKILL.md</a> 兵書指令寫入糧倉</p></td></tr><tr><td><p>序號 5（讀完 <a target="_self" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="http://battles.md" style="pointer-events:none">battles.md</a>）</p></td><td><p><strong>348</strong></p></td><td><p>26,647</p></td><td><p><a target="_self" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="http://battles.md" style="pointer-events:none">battles.md</a> 內容（347B）寫入糧倉</p></td></tr><tr><td><p>序號 7（準備執行腳本）</p></td><td><p><strong>480</strong></p></td><td><p>26,995</p></td><td><p>衝鋒奇兵輸出提示寫入糧倉</p></td></tr><tr><td><p>序號 11（組合答案）</p></td><td><p><strong>145</strong></p></td><td><p>27,475</p></td><td><p>持續遞減</p></td></tr><tr><td><p>序號 13（最終回覆）</p></td><td><p><strong>77</strong></p></td><td><p>27,620</p></td><td><p>幾乎全部來自糧倉</p></td></tr></tbody></table>

<p>從糧倉直取從 22,591 增長到 27,620，<strong>新增成本（新增糧倉）卻從 4,056 降到 77</strong>。此乃漸進揭露之術的實際成本曲線——兵書輔冊和奇兵腳本只在需要時才付新增糧草成本，一旦進入糧倉，後續輪次幾乎免費。</p>
<hr />
<h3>怎麼識別「同一輪戰役」的戰報？</h3>
<p>OTel 陣法資料有兩個層級的識別碼：</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>識別碼</p></th><th><p>範圍</p></th><th><p>說明</p></th></tr><tr><td><p><code>session_id</code></p></td><td><p>整個神算視窗</p></td><td><p>開一個大本營跑 <code>claude</code>，到關掉為止都是同一個</p></td></tr><tr><td><p><code>prompt_id</code></p></td><td><p>單一將領訊息</p></td><td><p>你說一句話，神算為了回應這句話的所有動作共用同一個</p></td></tr></tbody></table>

<p>同一句話的完整鏈路：</p>
<pre><code class="language-plaintext">你說「赤壁之戰是哪一年？」→ prompt_id: "6b003bf2-..."
  tool_result   奇術        74B  /  9ms  ← 第二層：SKILL.md 兵書指令
  api_request               4637ms       ← 神算謀略
  tool_result   Read        347B / 34ms  ← 第三層：battles.md
  api_request               2009ms       ← 決定執行腳本
  tool_result   Bash        226B / 691ms ← 第三層：format.sh 輸出
  api_request               2810ms       ← 最終回答

你說「官渡之戰呢？」→ prompt_id: "def-456-..."
  ...（新的一輪，全新的 prompt_id，同一個 session_id）
</code></pre>
<p>同一個大本營 terminal session 跑完整段對話：全部共用同一個 <code>session_id</code>。</p>
<hr />
<h3>在大帳建戰役下拉選單</h3>
<p>此讓你可以在戰情圖上點選要看哪個戰役，不用每次手動貼識別碼。</p>
<p><strong>步驟一</strong>：大帳 → Settings → Variables → New variable</p>
<pre><code class="language-plaintext">Name:        session_id
Type:        Query
Data source: 戰報文書庫（你的 Loki 資料源名稱）
Query:       label_values({service_name="claude-code"}, session_id)
Sort:        Alphabetical desc（最新的排前面）
</code></pre>
<p><strong>步驟二</strong>：所有 panel 的軍令查詢加上變數 filter：</p>
<pre><code class="language-logql">{service_name="claude-code"}
  | session_id = "$session_id"
  | event_name =~ "tool_result|api_request"
</code></pre>
<p><strong>步驟三</strong>（選用）：再加一個 <code>prompt_id</code> 變數，縮小到單一對話輪次：</p>
<pre><code class="language-plaintext">Name:        prompt_id
Type:        Query
Query:       label_values({service_name="claude-code"}, prompt_id)
</code></pre>
<p>然後在 panel query 補上：</p>
<pre><code class="language-logql">{service_name="claude-code"}
  | session_id = "$session_id"
  | prompt_id = "$prompt_id"
  | line_format "{{.event_name}} | {{.tool_name}} | {{.duration_ms}}ms"
</code></pre>
<p>此乃從大帳精確到「這一句話說出去之後，神算到底動了哪些兵器、照什麼順序、花了多少時間」——三層陣法的行為一覽無遺，也可以直接驗證奇術是否照你設計的路徑在跑。</p>
<hr />
<h3>幾個實用的奇術監控軍令查詢</h3>
<pre><code class="language-logql"># 哪些奇術被調用過，各幾次
{service_name="claude-code"}
  | event_name = "tool_result"
  | tool_name = "Skill"
  | line_format "{{.tool_parameters}}"

# 奇術調用失利（有哪個奇術出錯）
{service_name="claude-code"}
  | event_name = "tool_result"
  | tool_name = "Skill"
  | success = "false"

# 某個奇術執行期間讀了哪些兵書輔冊
{service_name="claude-code"}
  | session_id = "$session_id"
  | prompt_id = "$prompt_id"
  | event_name = "tool_result"
  | tool_name = "Read"
  | line_format "{{.tool_parameters}}"
</code></pre>
<p>最後一條特別有用：若你在演武「BigQuery 奇術只應該讀 <a href="http://sales.md">sales.md</a>，不應該讀 <a href="http://finance.md">finance.md</a>」，此條查詢可以直接告訴你神算在這一輪戰役裡讀了哪些兵書。</p>
<hr />
<h2>兵法總綱：設計原則總整理</h2>
<p><em>此節由諸葛亮主講。孔明搖動羽扇，微笑道：</em></p>
<h3>好的奇術寫法 vs 壞的寫法</h3>
<pre><code class="language-markdown">❌ 壞：過度解釋（神算已經知道這些）
&gt; PDF（Portable Document Format）是一種廣泛使用的文件格式。
&gt; 要從 PDF 提取文字，你需要一個程式庫...（以下三百字）

✅ 好：直接給核心情報
&gt; 使用 pdfplumber 提取文字：
&gt; ```python
&gt; import pdfplumber
&gt; with pdfplumber.open("file.pdf") as pdf:
&gt;     text = pdf.pages[0].extract_text()
&gt; ```
</code></pre>
<h3>自由度設定原則</h3>
<pre><code class="language-plaintext">操作越危險、越不可逆 → 給越少自由度（明確軍令）
操作越靈活、越有彈性 → 給越多自由度（方向性指引）
</code></pre>
<p><strong>類比</strong>：把神算想成在懸崖邊走鋼索的先鋒兵：</p>
<ul>
<li><p>若兩側都是懸崖 → 給精確的護衛（如：資料庫遷移必須照順序）</p>
</li>
<li><p>若是開闊草地 → 只給大方向即可（如：奏摺審查）</p>
</li>
</ul>
<h3>奇術品質自我檢查清單</h3>
<p><strong>基本品質</strong></p>
<ul>
<li><p>[ ] description 用第三人稱，包含觸發關鍵字</p>
</li>
<li><p>[ ] <a href="http://SKILL.md">SKILL.md</a> 兵書在 500 行以內</p>
</li>
<li><p>[ ] 兵書輔冊在獨立檔案中，且只有一層深</p>
</li>
<li><p>[ ] 超過 100 行的兵書輔冊有目錄</p>
</li>
</ul>
<p><strong>糧草效率</strong></p>
<ul>
<li><p>[ ] 沒有過度解釋神算已知的常識</p>
</li>
<li><p>[ ] 奇兵腳本用執行方式（不是讀進戰場視野）</p>
</li>
<li><p>[ ] 按領域拆分，避免載入無關內容</p>
</li>
</ul>
<p><strong>密情守則</strong></p>
<ul>
<li><p>[ ] 有副作用的操作有 <code>disable-model-invocation: true</code></p>
</li>
<li><p>[ ] 虎符授權只開放必要兵器</p>
</li>
<li><p>[ ] 危險操作有驗證步驟</p>
</li>
</ul>
<p><strong>可演武性</strong></p>
<ul>
<li><p>[ ] 有至少三個評估案例</p>
</li>
<li><p>[ ] 用不同兵種演武過</p>
</li>
<li><p>[ ] 在 OTel 陣法監控中可以追蹤執行路徑</p>
</li>
</ul>
<hr />
<h2>臨陣速查：實戰場景</h2>
<h3>場景一：封裝奏摺審查流程</h3>
<pre><code class="language-yaml">---
name: review-pr
description: 對奏摺做完整的程式碼審查，包含安全性、效能、可讀性分析
context: fork
agent: Explore
allowed-tools: Bash(gh *)
---

## 奏摺情報
- Diff：!`gh pr diff`
- 描述：!`gh pr view --json title,body,labels`
- CI 戰況：!`gh pr checks --json name,status,conclusion`

## 審查重點（依序）
1. 邏輯正確性與邊界條件
2. 安全漏洞（SQL Injection、XSS、硬編碼密鑰）
3. 效能影響（N+1 查詢、不必要的計算）
4. 可讀性與命名

## 輸出格式
用 GitHub comment 格式輸出，直接可貼上。
</code></pre>
<h3>場景二：特定領域知識庫（RAG 的奇術版本）</h3>
<p>不用建向量資料庫，直接把兵書放進奇術的 references 資料夾：</p>
<pre><code class="language-plaintext">.claude/skills/product-knowledge/
├── SKILL.md
└── references/
    ├── pricing.md      # 產品定價規則
    ├── faq.md          # 常見問題
    └── api-limits.md   # API 限制說明
</code></pre>
<pre><code class="language-yaml">---
name: product-knowledge
description: 回答關於產品定價、API 限制、功能說明的問題。將領詢問產品細節、定價、功能比較時使用。
user-invocable: false   # 背景知識，不需要手動呼叫
---

## 產品知識庫

- 定價規則：[references/pricing.md](references/pricing.md)
- 常見問題：[references/faq.md](references/faq.md)
- API 限制：[references/api-limits.md](references/api-limits.md)

回答問題時，引用來源兵書，保持準確性。
</code></pre>
<h3>場景三：多兵種協作奇術</h3>
<pre><code class="language-yaml">---
name: smart-refactor
description: 對程式碼進行智能重整
context: fork
model: claude-opus-4-6   # 重整分析需要 Opus 兵種
---

分析 $ARGUMENTS 的程式碼，提供重整建議：
1. 識別不良陣型（Code Smell）
2. 建議重整策略
3. 提供重整後的範例程式碼

只提供建議，不直接修改兵書。
</code></pre>
<hr />
<h2>總結</h2>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/4464e5e7-556d-4217-b5bc-67033d48f31d.png" alt="" style="display:block;margin:0 auto" />

<p>司馬懿終末叮嚀：</p>
<blockquote>
<p>主公，奇術乃「內功」，監控乃「外照」。</p>
<ul>
<li><p>奇術設計：務必遵守「一個奇術辦一件事」的 SRP 原則，切莫自鑄萬能兵器，反受其累。</p>
</li>
<li><p>演武驗證：建議主公在設計後，先以「雙謀士演武法」進行測試，確保奇術在陌生環境下依然如您所謀。</p>
</li>
</ul>
</blockquote>
<blockquote>
<p>正如主公所言： <strong>內功不濟，則外強中乾；外照不明，則盲人瞎馬。</strong></p>
<p>若只顧監控而無精良奇術，不過是眼睜睜看著糧草虛擲；若空有奇術卻無監控，則如暗夜行軍，不知險阻。二者相輔相成，方能決勝千里。</p>
<p>那「萬能兵器」看似美好，實則如軍中過重的輜重，平時看似什麼都有，真到臨陣時卻樣樣不精，反倒拖累了大軍行進。專才專用，職責單一，才是用兵之上策。</p>
</blockquote>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/4fb91d21-ab72-4ae6-ae39-75f2ad1bf52e.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>延伸兵書</h2>
<ul>
<li><p><a href="https://code.claude.com/docs/en/skills">Claude Skills 官方文件</a></p>
</li>
<li><p><a href="https://agentskills.io">Agent Skills 開放標準</a></p>
</li>
<li><p><a href="https://platform.claude.com/docs/zh-TW/agents-and-tools/agent-skills/best-practices">Agent Skills 最佳實踐（官方）</a></p>
</li>
<li><p><a href="https://skillsmp.com">奇術開放市場 -</a> <a href="http://skillsmp.com">skillsmp.com</a></p>
</li>
<li><p><a href="https://ganhua.wang/claude-code-opentelemetry-otel-otlp">上一篇：用 OTel 陣法監控臥龍神算行為</a></p>
</li>
</ul>
<hr />
<p><em>兵書版本：2026-02-20 ｜ 主述：諸葛亮（字孔明）與司馬懿（字仲達）聯合演繹 ｜ 由</em> <code>/sanguo-rewrite</code> <em>奇術生成</em></p>
]]></content:encoded></item><item><title><![CDATA[Claude Code 監控秘錄：OpenTelemetry（OTel/OTLP）實戰指南]]></title><description><![CDATA[稟告主公：此乃司馬懿進呈之兵書，詳解如何以 OpenTelemetry 陣法，令臥龍神算之一舉一動盡在掌握，知糧草消耗、察兵器效能、辨戰報異常，使主公運籌帷幄於大帳之中。


為何需要斥候情報？
司馬懿稟告主公：
臥龍神算（Claude Code）乃當世利器，然若無斥候回報，主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯，一概不知。臣以為，此乃兵家大忌。
無情報之弊，有四：

軍]]></description><link>https://ganhua.wang/claude-code-opentelemetry-otel-otlp</link><guid isPermaLink="true">https://ganhua.wang/claude-code-opentelemetry-otel-otlp</guid><category><![CDATA[claude-code]]></category><category><![CDATA[OpenTelemetry]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Thu, 19 Feb 2026 10:04:21 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/a25ade60-bd24-41c9-be0b-27e45efeaaa8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>稟告主公：此乃司馬懿進呈之兵書，詳解如何以 OpenTelemetry 陣法，令臥龍神算之一舉一動盡在掌握，知糧草消耗、察兵器效能、辨戰報異常，使主公運籌帷幄於大帳之中。</p>
</blockquote>
<hr />
<h2>為何需要斥候情報？</h2>
<p><strong>司馬懿稟告主公：</strong></p>
<p>臥龍神算（Claude Code）乃當世利器，然若無斥候回報，主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯，一概不知。臣以為，此乃兵家大忌。</p>
<p>無情報之弊，有四：</p>
<ul>
<li><p><strong>軍費不明</strong>：不知每日每週糧草消耗幾何，哪場戰役尤為耗費</p>
</li>
<li><p><strong>用兵不察</strong>：哪件兵器最常被調遣？哪件兵器行動最遲緩？</p>
</li>
<li><p><strong>功績難量</strong>：無法向諸將或主公展示神算工具帶來的實際戰果</p>
</li>
<li><p><strong>敗仗難溯</strong>：兵器偶有失靈卻不知是哪件、發生在何種陣仗之中</p>
</li>
</ul>
<p>透過 OpenTelemetry 陣法整合，主公可得以下情報：</p>
<p>✅ <strong>即時掌握等效軍費消耗</strong>（精確到每支兵馬、每五分鐘的耗糧節奏）</p>
<p>✅ <strong>分析糧草使用效率</strong>（糧倉補給命中率、輸入糧草 vs 產出糧草之比）</p>
<p>✅ <strong>掌握兵器出征行為</strong>（哪件兵器最常出動、哪件行進最遲）</p>
<p>✅ <strong>追查折戟與傳令失利</strong>（逐筆查閱失靈兵器、驛站告急、虎符拒發記錄）</p>
<p>✅ <strong>多路諸侯比較</strong>（按使用者、團隊、部門分層分析兵力消耗）</p>
<hr />
<h2>連環陣法總覽</h2>
<p><strong>司馬懿展開沙盤，向諸將說明：</strong></p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/e526c681-5e04-4a24-af8f-fb3d58cb0f9d.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-plaintext">┌─────────────────────────────────────────────────────────────────┐
│  主公之大本營（開發機器）                                           │
│                                                                   │
│  臥龍神算 CLI                                                      │
│  ├── CLAUDE_CODE_ENABLE_TELEMETRY=1  ← 命令斥候出發的虎符          │
│  ├── OTEL_METRICS_EXPORTER=otlp     → 軍情數報                    │
│  └── OTEL_LOGS_EXPORTER=otlp        → 戰報文書                    │
│           │                                                       │
│           │ OTLP gRPC :4317 / HTTP :4318（驛道）                   │
│           ▼                                                       │
│  ┌────────────────────────────────┐                              │
│  │  情報中樞（OTel Collector）      │  ← 傳令中軍，統一收發          │
│  │  (otel/opentelemetry-          │                              │
│  │   collector-contrib:0.145.0)   │                              │
│  │                                │                              │
│  │  接收：OTLP（gRPC/HTTP 兩路）   │                              │
│  │  處理：限制糧草、批次傳遞        │                              │
│  │  分發：                        │                              │
│  │    ├── 糧草台帳 (:8889)         │                              │
│  │    └── 戰報文書庫 (:3100/otlp)  │                              │
│  └────────────────────────────────┘                              │
│           │                    │                                  │
│           ▼                    ▼                                  │
│  ┌──────────────┐    ┌──────────────┐                           │
│  │  糧草台帳     │    │  戰報文書庫   │  ← 情報儲存               │
│  │ (Prometheus) │    │  (Loki 3.6)  │                           │
│  │  v2.55.1     │    │  (戰況存錄)   │                           │
│  │  (:9090)     │    │  (:3100)     │                           │
│  └──────────────┘    └──────────────┘                           │
│           │                    │                                  │
│           └─────────┬──────────┘                                │
│                     ▼                                             │
│              ┌────────────┐                                      │
│              │  大帳沙盤   │  ← 統帥覽圖之所（Grafana）             │
│              │  12.3.3    │                                      │
│              │  (:3000)   │                                      │
│              └────────────┘                                      │
└─────────────────────────────────────────────────────────────────┘
</code></pre>
<h3>情報傳遞路徑</h3>
<table style="min-width:100px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>情報種類</p></th><th><p>傳遞路徑</p></th><th><p>儲存之所</p></th><th><p>用途</p></th></tr><tr><td><p><strong>軍情數報（Metrics）</strong></p></td><td><p>OTLP → 情報中樞 → 糧草台帳</p></td><td><p>Prometheus TSDB</p></td><td><p>時序趨勢、消耗分析、告警</p></td></tr><tr><td><p><strong>戰報文書（Logs/Events）</strong></p></td><td><p>OTLP → 情報中樞 → 戰報文書庫</p></td><td><p>Loki chunks</p></td><td><p>詳細戰況記錄、折戟追溯</p></td></tr></tbody></table>

<hr />
<h2>軍情二分：數報與戰報</h2>
<p><strong>司馬懿言：</strong> 情報有二類，各有其用，主公需知其別。</p>
<h3>軍情數報（Metrics）</h3>
<ul>
<li><p><strong>性質</strong>：累積計數器，記錄糧草消耗總量</p>
</li>
<li><p><strong>特性</strong>：適合趨勢分析、消耗速率計算</p>
</li>
<li><p><strong>存於</strong>：糧草台帳（Prometheus）</p>
</li>
<li><p><strong>回報頻率</strong>：每 60 秒一次（可用 <code>OTEL_METRIC_EXPORT_INTERVAL</code> 加快）</p>
</li>
</ul>
<h3>戰報文書（Events/Logs）</h3>
<ul>
<li><p><strong>性質</strong>：結構化戰況記錄，含豐富上下文</p>
</li>
<li><p><strong>特性</strong>：包含具體兵器名稱、出征時長、折戟原因</p>
</li>
<li><p><strong>存於</strong>：戰報文書庫（Loki）</p>
</li>
<li><p><strong>回報頻率</strong>：每 5 秒一次（可用 <code>OTEL_LOGS_EXPORT_INTERVAL</code> 調整）</p>
</li>
</ul>
<hr />
<h2>烽火台速建之法</h2>
<p><em>此節由諸葛亮主講。孔明搖動羽扇，微笑道：</em></p>
<p><strong>孔明言：</strong> 仲達所言陣法雖精妙，然主公若欲速見成效，可按亮之三法，依序而行。</p>
<h3>第一法：臨陣點火（單次試用）</h3>
<pre><code class="language-bash">export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_SERVICE_NAME=claude-code

claude
</code></pre>
<h3>第二法：軍令刻入兵書（永久設定，亮力薦此法）</h3>
<p>編輯 <code>~/.claude/settings.json</code>（主公的兵書）：</p>
<pre><code class="language-json">{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
    "OTEL_SERVICE_NAME": "claude-code",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_LOGS_EXPORTER": "otlp"
  }
}
</code></pre>
<h3>第三法：加快斥候回報（偵錯模式）</h3>
<pre><code class="language-bash">export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=console,otlp
export OTEL_LOGS_EXPORTER=console,otlp
export OTEL_METRIC_EXPORT_INTERVAL=5000   # 5秒（預設60秒）
export OTEL_LOGS_EXPORT_INTERVAL=1000     # 1秒（預設5秒）
</code></pre>
<h3>連環陣法開陣</h3>
<pre><code class="language-bash">cd monitoring
docker compose up -d

# 確認五軍就位
docker compose ps

# 探查各軍健康
curl http://localhost:13133          # 情報中樞
curl http://localhost:3100/ready     # 戰報文書庫
curl -s http://localhost:3000/api/health | jq .  # 大帳沙盤
</code></pre>
<p><em>司馬懿補充：</em> <strong>主公需防一事</strong>——連環陣法啟動後，約需等候 60 秒方能在大帳見到第一批軍情數報。此乃正常等候，非陣法有誤。</p>
<hr />
<h2>兵書詳解：settings.json 深度解析</h2>
<p><strong>司馬懿接回主講：</strong></p>
<h3>本軍兵書解讀</h3>
<pre><code class="language-json">{
  "env": {
    "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "32000",     // 神算一次最多輸出之文字量
    "MAX_THINKING_TOKENS": "8000",                 // 神算思謀之最大用度
    "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",   // 啟用多路子軍同時出征
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",           // 啟用斥候情報（最關鍵之令）
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",  // 情報中樞位址
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",         // 驛道傳令協議
    "OTEL_SERVICE_NAME": "claude-code",            // 本軍番號（戰報文書庫之識別標籤）
    "OTEL_METRICS_EXPORTER": "otlp",               // 軍情數報之傳送器
    "OTEL_LOGS_EXPORTER": "otlp"                   // 戰報文書之傳送器
  }
}
</code></pre>
<h3>所有軍令旗號完整參考</h3>
<h4>核心旗號</h4>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>旗號名稱</p></th><th><p>預設</p></th><th><p>說明</p></th></tr><tr><td><p><code>CLAUDE_CODE_ENABLE_TELEMETRY</code></p></td><td><p>未升（停用）</p></td><td><p>必須升旗設為 <code>1</code>，斥候方能出發</p></td></tr><tr><td><p><code>OTEL_METRICS_EXPORTER</code></p></td><td><p>未設</p></td><td><p><code>otlp</code>、<code>prometheus</code>、<code>console</code>，可逗號並列多路</p></td></tr><tr><td><p><code>OTEL_LOGS_EXPORTER</code></p></td><td><p>未設</p></td><td><p><code>otlp</code>、<code>console</code></p></td></tr><tr><td><p><code>OTEL_EXPORTER_OTLP_PROTOCOL</code></p></td><td><p>—</p></td><td><p><code>grpc</code>、<code>http/protobuf</code>、<code>http/json</code></p></td></tr><tr><td><p><code>OTEL_EXPORTER_OTLP_ENDPOINT</code></p></td><td><p>—</p></td><td><p>情報中樞的驛道位址</p></td></tr><tr><td><p><code>OTEL_SERVICE_NAME</code></p></td><td><p><code>claude-code</code></p></td><td><p>本軍番號，作為戰報文書庫之 stream label</p></td></tr></tbody></table>

<h4>行軍調速旗號</h4>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>旗號名稱</p></th><th><p>預設</p></th><th><p>說明</p></th></tr><tr><td><p><code>OTEL_METRIC_EXPORT_INTERVAL</code></p></td><td><p><code>60000</code> ms</p></td><td><p>軍情數報回傳間隔（毫秒）</p></td></tr><tr><td><p><code>OTEL_LOGS_EXPORT_INTERVAL</code></p></td><td><p><code>5000</code> ms</p></td><td><p>戰報文書回傳間隔（毫秒）</p></td></tr><tr><td><p><code>OTEL_METRICS_TEMPORALITY_PREFERENCE</code></p></td><td><p><code>delta</code></p></td><td><p>若糧草台帳需要累積值，改為 <code>cumulative</code></p></td></tr></tbody></table>

<h4>輜重管制旗號（高基數問題）</h4>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>旗號名稱</p></th><th><p>預設</p></th><th><p>說明</p></th></tr><tr><td><p><code>OTEL_METRICS_INCLUDE_SESSION_ID</code></p></td><td><p><code>true</code></p></td><td><p>數報中是否附記本場戰役番號</p></td></tr><tr><td><p><code>OTEL_METRICS_INCLUDE_VERSION</code></p></td><td><p><code>false</code></p></td><td><p>是否附記神算版本代數</p></td></tr><tr><td><p><code>OTEL_METRICS_INCLUDE_ACCOUNT_UUID</code></p></td><td><p><code>true</code></p></td><td><p>是否附記將領識別號</p></td></tr></tbody></table>

<blockquote>
<p>⚠️ <strong>仲達提醒</strong>：每場戰役（session）均有獨一番號，若台帳時序過多，可令 <code>OTEL_METRICS_INCLUDE_SESSION_ID=false</code>，以減輕糧草台帳之負擔。</p>
</blockquote>
<h4>密情管制旗號</h4>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>旗號名稱</p></th><th><p>預設</p></th><th><p>說明</p></th></tr><tr><td><p><code>OTEL_LOG_USER_PROMPTS</code></p></td><td><p>未升（停用）</p></td><td><p>設為 <code>1</code> 才將主公原話錄入戰報</p></td></tr><tr><td><p><code>OTEL_LOG_TOOL_DETAILS</code></p></td><td><p>未升（停用）</p></td><td><p>設為 <code>1</code> 才記錄 MCP 軍需官名稱與奇術名目</p></td></tr></tbody></table>

<hr />
<h2>五軍聯動：監控堆疊元件解析</h2>
<p><strong>司馬懿正色道：</strong></p>
<p>臣為主公佈下五軍聯動之陣，缺一不可，各司其職。</p>
<h3>第一軍：情報中樞（OTel Collector）</h3>
<p><strong>兵書位址</strong>：<code>monitoring/otelcol/config.yaml</code></p>
<p>情報中樞乃整座連環陣法之樞紐，職責三分：</p>
<ol>
<li><p><strong>接收斥候回報</strong>：同時接收 gRPC（:4317）與 HTTP（:4318）兩路情報</p>
</li>
<li><p><strong>整理情報</strong>：</p>
<ul>
<li><p><code>memory_limiter</code>：防止情報堆積壓垮中樞（限制 512MiB）</p>
</li>
<li><p><code>batch</code>：批次整理，每 5 秒 / 1024 筆集中傳送</p>
</li>
</ul>
</li>
<li><p><strong>分發情報</strong>：</p>
<ul>
<li><p>軍情數報 → 糧草台帳（:8889 供台帳定期抄錄）</p>
</li>
<li><p>戰報文書 → 戰報文書庫（HTTP OTLP）</p>
</li>
</ul>
</li>
</ol>
<p>情報中樞亦自我回報健康狀況（<code>:8888</code>），可透過糧草台帳直接查詢（<code>http://localhost:9090</code>）確認收發量。</p>
<h3>第二軍：糧草台帳（Prometheus）</h3>
<p><strong>兵書位址</strong>：<code>monitoring/prometheus/prometheus.yml</code></p>
<pre><code class="language-yaml">scrape_configs:
  - job_name: "claude-code"
    scrape_interval: 15s
    static_configs:
      - targets: ["otelcol:8889"]
</code></pre>
<p>糧草台帳每 15 秒從情報中樞抄錄一次軍情數報，存入時序冊（TSDB）。</p>
<p><strong>存糧期限</strong>：預設 15 天。如需延長，於 <code>docker-compose.yml</code> 加入：</p>
<pre><code class="language-yaml">command:
  - "--storage.tsdb.retention.time=90d"
</code></pre>
<h3>第三軍：戰報文書庫（Loki）</h3>
<p><strong>兵書位址</strong>：<code>monitoring/loki/loki-config.yaml</code></p>
<p>戰報文書庫採用 TSDB schema v13（最新一代歸檔法），並開啟結構化密令支援（<code>allow_structured_metadata: true</code>），此乃接收 OTLP 戰報之必要設定。</p>
<p><strong>存檔期限</strong>：30 天（<code>retention_period: 720h</code>）</p>
<h3>第四軍：大帳沙盤（Grafana）</h3>
<p><strong>版本</strong>：12.3.3 <strong>大帳位址</strong>：<code>http://localhost:3000</code>（通關密語：admin / admin）</p>
<p>戰情圖透過自動佈陣（provisioning）載入，並設為大帳首頁：</p>
<pre><code class="language-yaml">GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/claude-code.json
</code></pre>
<hr />
<h2>大帳沙盤導覽</h2>
<p><strong>司馬懿引諸將至大帳，指點沙盤：</strong></p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/31615607-d35e-4423-82a0-edefe4f66335.png" alt="" style="display:block;margin:0 auto" />

<h3>一覽台：戰役總覽（Session Overview）</h3>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/432c55ac-f305-4b33-93c8-6bc1a9fe8807.png" alt="" style="display:block;margin:0 auto" />

<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>旗標</p></th><th><p>PromQL 軍令</p></th><th><p>說明</p></th></tr><tr><td><p>戰役總數（Total Sessions）</p></td><td><p><code>count(count by (session_id)(claude_code_active_time_seconds_total))</code></p></td><td><p>共出征幾場戰役</p></td></tr><tr><td><p>軍費消耗（Total Cost）</p></td><td><p><code>sum(increase(claude_code_cost_usage_USD_total[\(__range]))</code></p></td><td><p>時間範圍內等效軍費</p></td></tr><tr><td><p>糧草總量（Total Tokens）</p></td><td><p><code>sum(increase(claude_code_token_usage_tokens_total[\)__range]))</code></p></td><td><p>糧草總消耗</p></td></tr><tr><td><p>修築工事（Tool Edits Accepted）</p></td><td><p><code>sum(increase(claude_code_code_edit_tool_decision_total{decision="accept"}[\(__range]))</code></p></td><td><p>主公批准之工事次數</p></td></tr><tr><td><p>出征時長（Active Time）</p></td><td><p><code>sum(increase(claude_code_active_time_seconds_total[\)__range])) / 3600</code></p></td><td><p>實際出征小時數</p></td></tr></tbody></table>

<h3>糧草軍費圖（Cost &amp; Token Usage）</h3>
<ul>
<li><p><strong>各路兵馬軍費速率</strong>（Cost Rate by Model）：各兵種軍費消耗速率，識別哪路兵馬最貴</p>
</li>
<li><p><strong>糧草類型速率</strong>（Token Rate by Type）：輸入/輸出/糧倉取糧/糧倉存糧之速率趨勢</p>
</li>
<li><p><strong>每五刻軍費</strong>（Cost per 5 Minutes）：每五分鐘之軍費消耗（條形圖，按兵種分色）— 清楚顯示何時有密集出征，軍令為 <code>sum by (model)(increase(claude_code_cost_usage_USD_total[5m]))</code></p>
</li>
<li><p><strong>糧草類型分佈</strong>（Token Distribution，圓餅圖）：時間範圍內各類型糧草占比</p>
</li>
<li><p><strong>各路兵馬軍費占比</strong>（Cost by Model，圓餅圖）：各兵種累積軍費占比</p>
</li>
<li><p><strong>糧倉補給效率</strong>（Cache Hit Rate）：<code>取糧 / 總糧草 × 100%</code>，越高表示糧倉調撥越順暢</p>
</li>
</ul>
<blockquote>
<p>💡 <strong>仲達觀察</strong>：糧倉補給效率乃衡量出征前軍令旗號是否穩定之要指。若主公每番出征皆沿用相同旗號，糧倉直接調撥，無需重新清點，軍費節省可觀。</p>
<p>⚠️ <strong>仲達提醒</strong>：此軍費數字為「等效 API 費用」而非主公每月繳納之固定月費。Pro / Max 方案按月收費，此數字僅供消耗趨勢參考，無法對應 Settings 中之配額使用百分比。</p>
</blockquote>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/c8b10f1a-cda1-434f-8f59-3ce7e97b7e45.png" alt="" style="display:block;margin:0 auto" />

<h3>兵器排行榜（Top 10 Tools）</h3>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/bb07ef0a-d900-43f4-9d57-964cb9ba235e.png" alt="" style="display:block;margin:0 auto" />

<p>使用戰報文書庫 LogQL 軍令查詢（<code>tool_result</code> 戰報），資料來自結構化密令，直接 <code>unwrap</code> 取值。</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>旗標</p></th><th><p>查詢邏輯</p></th><th><p>說明</p></th></tr><tr><td><p><strong>出動次數排行</strong></p></td><td><p><code>topk(10, count_over_time(...) by tool_name)</code></p></td><td><p>哪件兵器被調遣最頻繁</p></td></tr><tr><td><p><strong>平均出征時長</strong></p></td><td><p><code>topk(10, avg_over_time(unwrap duration_ms) by tool_name)</code></p></td><td><p>哪件兵器行動最遲，找出效能瓶頸</p></td></tr></tbody></table>

<blockquote>
<p>折戟詳情請見下方「戰報文書 → 兵器折戟記錄」，提供比排行榜更完整之逐筆戰況。</p>
</blockquote>
<h3>戰報文書閱覽（Logs &amp; Events）</h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>戰報</p></th><th><p>LogQL 軍令</p></th><th><p>說明</p></th></tr><tr><td><p>兵器折戟記錄（Tool Failures）</p></td><td><p><code>{service_name="claude-code"} | event_name="tool_result" | success="false"</code></p></td><td><p>所有失靈的兵器呼叫</p></td></tr><tr><td><p>傳令出使記錄（API Requests）</p></td><td><p><code>{service_name="claude-code"} | event_name="api_request"</code></p></td><td><p>所有傳令兵出使詳情</p></td></tr><tr><td><p>兵器出征記錄（Tool Executions）</p></td><td><p><code>{service_name="claude-code"} | event_name="tool_result"</code></p></td><td><p>所有兵器出征完整記錄</p></td></tr><tr><td><p>虎符決策記錄（Permission Decisions）</p></td><td><p><code>{service_name="claude-code"} | event_name="tool_decision"</code></p></td><td><p>虎符授予/拒發記錄</p></td></tr><tr><td><p>主公軍令記錄（User Prompts）</p></td><td><p><code>{service_name="claude-code"} | event_name="user_prompt"</code></p></td><td><p>主公發令記錄</p></td></tr></tbody></table>

<hr />
<h2>觀圖識機：看到什麼、代表什麼、能做什麼</h2>
<p><strong>司馬懿向諸將道：</strong> 觀圖非為賞玩，乃為臨陣決策。每處旗標背後，皆有可採取之行動。</p>
<h3>一覽台 — 一眼掌握戰役總況</h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>見到…</p></th><th><p>代表…</p></th><th><p>可下令…</p></th></tr><tr><td><p>軍費消耗偏高</p></td><td><p>此段時間等效軍費較重</p></td><td><p>對照「每五刻軍費圖」找出哪個時段爆量出征</p></td></tr><tr><td><p>糧草高但軍費低</p></td><td><p>糧倉補給效率高，輸入糧草被有效重用</p></td><td><p>保持現有旗號，勿頻繁收兵重開戰役</p></td></tr><tr><td><p>出征時長遠低於實際工時</p></td><td><p>神算大部分時間在等主公下令</p></td><td><p>此為正常；若出征時長異常高，代表有長時間指令在執行</p></td></tr><tr><td><p>修築工事 = 0</p></td><td><p>神算未實際修改任何工事</p></td><td><p>確認此段時間是否只在對話，而非令神算動手施工</p></td></tr></tbody></table>

<h3>糧草軍費圖 — 費用節奏與效率</h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>見到…</p></th><th><p>代表…</p></th><th><p>可下令…</p></th></tr><tr><td><p>每五刻軍費出現明顯尖峰</p></td><td><p>那個時段有密集傳令出使（大型任務或多輪對話）</p></td><td><p>評估該任務之軍費是否合算，或拆解為更小步驟</p></td></tr><tr><td><p>糧倉補給效率 &lt; 20%</p></td><td><p>軍令旗號每次重新清點，未能善用糧倉</p></td><td><p>檢查是否頻繁開新戰役或大幅改動旗號</p></td></tr><tr><td><p>糧倉補給效率 &gt; 60%</p></td><td><p>旗號穩定，糧倉大量直接調撥</p></td><td><p>此乃善用之道，繼續保持</p></td></tr><tr><td><p>各路兵馬軍費幾乎全是 Opus</p></td><td><p>高費用兵種占比過重</p></td><td><p>評估哪些任務可改調 Sonnet 出征（一般修築、探查任務）</p></td></tr><tr><td><p>輸出糧草遠大於輸入糧草</p></td><td><p>神算輸出極多，可能在進行大型生成任務</p></td><td><p>確認輸出是否皆有被利用，勿浪費糧草</p></td></tr></tbody></table>

<h3>兵器排行榜 — 兵器使用行為</h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>見到…</p></th><th><p>代表…</p></th><th><p>可下令…</p></th></tr><tr><td><p>衝鋒奇兵（Bash）出動遠多於他器</p></td><td><p>大量指令衝鋒，可能在跑測試或建置</p></td><td><p>正常；若平均時長也高，考慮優化指令或加設逾時限制</p></td></tr><tr><td><p>探查細作（Read）次數極高</p></td><td><p>神算頻繁探查，可能在大型兵書庫中反覆搜尋</p></td><td><p>考慮提供更精確的典籍位址，減少大範圍探查</p></td></tr><tr><td><p>子軍統帥（Task）平均時長特別高</p></td><td><p>子軍任務耗時甚長</p></td><td><p>檢查是否有子軍卡陣或任務過於複雜</p></td></tr><tr><td><p>某兵器平均時長異常偏高</p></td><td><p>出征遲緩，可能有逾時或大量回報</p></td><td><p>至「兵器出征記錄」查閱該兵器之具體戰況</p></td></tr></tbody></table>

<h3>戰報文書 — 折戟追查第一入口</h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>見到…</p></th><th><p>代表…</p></th><th><p>可下令…</p></th></tr><tr><td><p>兵器折戟記錄有條目</p></td><td><p>兵器出征失靈，有折戟原因</p></td><td><p>展開戰報查看 <code>error</code> 欄位，定位失靈原因</p></td></tr><tr><td><p>傳令出使中 <code>duration_ms</code> 偏高</p></td><td><p>傳令兵往返耗時，可能是兵種過載或驛道壅塞</p></td><td><p>查看 <code>speed</code> 欄位（fast/normal）與糧草消耗量</p></td></tr><tr><td><p>虎符決策有「拒發」記錄</p></td><td><p>主公或守衛拒絕了某件兵器出征</p></td><td><p>確認是否為預期中的攔截，或需調整授權設定</p></td></tr><tr><td><p>主公軍令中 <code>prompt_length</code> 甚大</p></td><td><p>主公下達了極長之軍令</p></td><td><p>長軍令消耗更多輸入糧草；考慮分段或精簡措辭</p></td></tr></tbody></table>

<hr />
<h2>軍情數報完整冊（所有 Metrics）</h2>
<p><strong>司馬懿取出台帳典籍，逐一說明：</strong></p>
<h3>指標命名轉換規則</h3>
<p>糧草台帳（Prometheus）中，指標名稱由 <code>.</code> 轉換為 <code>_</code>：</p>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>OTel 原名</p></th><th><p>Prometheus 台帳名稱</p></th></tr><tr><td><p><code>claude_code.session.count</code></p></td><td><p><code>claude_code_session_count_total</code></p></td></tr><tr><td><p><code>claude_code.cost.usage</code></p></td><td><p><code>claude_code_cost_usage_USD_total</code></p></td></tr><tr><td><p><code>claude_code.token.usage</code></p></td><td><p><code>claude_code_token_usage_tokens_total</code></p></td></tr><tr><td><p><code>claude_code.lines_of_code.count</code></p></td><td><p><code>claude_code_lines_of_code_count_total</code></p></td></tr><tr><td><p><code>claude_code.code_edit_tool.decision</code></p></td><td><p><code>claude_code_code_edit_tool_decision_total</code></p></td></tr><tr><td><p><code>claude_code.active_time.total</code></p></td><td><p><code>claude_code_active_time_seconds_total</code></p></td></tr><tr><td><p><code>claude_code.commit.count</code></p></td><td><p><code>claude_code_commit_count_total</code></p></td></tr><tr><td><p><code>claude_code.pull_request.count</code></p></td><td><p><code>claude_code_pull_request_count_total</code></p></td></tr></tbody></table>

<h3>各指標可用維度（Labels）</h3>
<pre><code class="language-plaintext">claude_code_cost_usage_USD_total
  └── 維度：model（兵種）、session_id（戰役番號）、user_account_uuid（將領號）...

claude_code_token_usage_tokens_total
  └── 維度：type (input|output|cacheRead|cacheCreation)、model、session_id...

claude_code_lines_of_code_count_total
  └── 維度：type (added|removed)、session_id...

claude_code_code_edit_tool_decision_total
  └── 維度：tool_name (Edit|Write|NotebookEdit)、decision (accept|reject)、
              source (config|hook|user_permanent|user_temporary|user_abort|user_reject)、
              language (TypeScript|Python|...|unknown)

claude_code_active_time_seconds_total
  └── 維度：type (user|cli)、session_id...
</code></pre>
<h3>常用台帳查詢範例</h3>
<pre><code class="language-promql"># 今日軍費消耗
sum(increase(claude_code_cost_usage_USD_total[24h]))

# 按兵種分組的軍費速率
sum by (model)(rate(claude_code_cost_usage_USD_total[$__rate_interval]))

# 糧倉補給效率（節省軍費的關鍵指標）
100 * sum(increase(claude_code_token_usage_tokens_total{type="cacheRead"}[24h]))
    / sum(increase(claude_code_token_usage_tokens_total[24h]))

# 工事淨新增行數
sum(increase(claude_code_lines_of_code_count_total{type="added"}[24h]))
- sum(increase(claude_code_lines_of_code_count_total{type="removed"}[24h]))

# TypeScript 工事的修築核准率
sum(rate(claude_code_code_edit_tool_decision_total{language="TypeScript",decision="accept"}[$__rate_interval]))
/ sum(rate(claude_code_code_edit_tool_decision_total{language="TypeScript"}[$__rate_interval]))

# 每場戰役的平均軍費
sum(increase(claude_code_cost_usage_USD_total[$__range]))
/ count(count by (session_id)(claude_code_active_time_seconds_total))
</code></pre>
<hr />
<h2>戰報文書總錄（所有 Events）</h2>
<h3>軍令追蹤：prompt.id 關聯之法</h3>
<p><strong>司馬懿道：</strong> 每次主公下令，神算可能出動多路人馬。透過 <code>prompt.id</code> 可追蹤一道軍令觸發的完整行軍鏈路：</p>
<pre><code class="language-plaintext">主公軍令 (prompt.id: "abc-123")
  └── 傳令出使 (prompt.id: "abc-123")
  └── 傳令出使 (prompt.id: "abc-123")  ← 同一道令可能多次傳令
  └── 衝鋒奇兵出征 (prompt.id: "abc-123")
  └── 探查細作出征 (prompt.id: "abc-123")
  └── 修築工事出征 (prompt.id: "abc-123")
  └── 虎符決策 (prompt.id: "abc-123")
</code></pre>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/b798dda6-cb0b-4a3c-a36b-0678a2a98d32.png" alt="" style="display:block;margin:0 auto" />

<p>查詢特定軍令之完整行軍記錄：</p>
<pre><code class="language-logql">{service_name="claude-code"} | json | prompt_id = "你的-prompt-id"
</code></pre>
<h3>各類戰報詳細欄位</h3>
<h4><code>user_prompt</code>（主公軍令）</h4>
<pre><code class="language-json">{
  "event.name": "user_prompt",
  "event.timestamp": "2025-01-15T10:30:00Z",
  "event.sequence": 1,
  "prompt_length": 256,
  "prompt": "&lt;僅在 OTEL_LOG_USER_PROMPTS=1 時才錄入原話&gt;"
}
</code></pre>
<h4><code>api_request</code>（傳令出使）</h4>
<pre><code class="language-json">{
  "event.name": "api_request",
  "model": "claude-sonnet-4-6",
  "cost_usd": 0.00145,
  "duration_ms": 3200,
  "input_tokens": 12500,
  "output_tokens": 850,
  "cache_read_tokens": 11000,
  "cache_creation_tokens": 1500,
  "speed": "normal"
}
</code></pre>
<h4><code>tool_result</code>（兵器出征結果）</h4>
<pre><code class="language-json">{
  "event.name": "tool_result",
  "tool_name": "Bash",
  "success": "true",
  "duration_ms": 450,
  "tool_result_size_bytes": 2048,
  "decision_type": "accept",
  "decision_source": "config",
  "tool_parameters": "{\"bash_command\":\"npm test\",\"timeout\":120000}"
}
</code></pre>
<h4><code>tool_decision</code>（虎符授決）</h4>
<pre><code class="language-json">{
  "event.name": "tool_decision",
  "tool_name": "Edit",
  "decision": "accept",
  "source": "user_permanent"
}
</code></pre>
<h4><code>api_error</code>（傳令失利）</h4>
<pre><code class="language-json">{
  "event.name": "api_error",
  "model": "claude-sonnet-4-6",
  "error": "Rate limit exceeded",
  "status_code": "429",
  "duration_ms": 500,
  "attempt": 2,
  "speed": "normal"
}
</code></pre>
<hr />
<h2>奇兵五策：進階應用場景</h2>
<p><strong>司馬懿獻上五策奇謀：</strong></p>
<h3>第一策：識別最拖沓之兵器</h3>
<pre><code class="language-logql"># 找出平均出征時長最久的兵器
topk(10,
  sum by (tool_name)(
    sum_over_time(
      {service_name="claude-code"} | event_name=`tool_result` | json
      | unwrap duration_ms | __error__="" [24h]
    )
  ) /
  sum by (tool_name)(
    count_over_time(
      {service_name="claude-code"} | event_name=`tool_result` [24h]
    )
  )
)
</code></pre>
<h3>第二策：察看衝鋒奇兵之動向</h3>
<pre><code class="language-logql"># 查看所有衝鋒指令（需 tool_parameters 中有 bash_command）
{service_name="claude-code"}
  | event_name=`tool_result`
  | tool_name=`Bash`
  | json
  | line_format "{{.tool_parameters}}"
</code></pre>
<h3>第三策：衡量出征效率（每時辰產出）</h3>
<pre><code class="language-promql"># 每時辰新修工事行數
sum(rate(claude_code_lines_of_code_count_total{type="added"}[1h])) * 3600

# 每時辰插旗（commit）次數
sum(rate(claude_code_commit_count_total[1h])) * 3600
</code></pre>
<h3>第四策：異常軍費告警（大帳預警）</h3>
<p>在大帳沙盤中設定告警旗號：</p>
<pre><code class="language-promql"># 當一個時辰軍費超過五金時觸發警報
sum(increase(claude_code_cost_usage_USD_total[1h])) &gt; 5
</code></pre>
<h3>第五策：兵種選用最佳化</h3>
<pre><code class="language-promql"># 各兵種「每單位糧草之軍費」效率比較
sum by (model)(rate(claude_code_cost_usage_USD_total[$__rate_interval]))
/
sum by (model)(rate(claude_code_token_usage_tokens_total[$__rate_interval]))
</code></pre>
<hr />
<h2>度支算糧：ROI 衡量與成本分析</h2>
<p><strong>司馬懿掐指算道：</strong></p>
<h3>戰力量化之道</h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>指標</p></th><th><p>衡量方式</p></th><th><p>PromQL 軍令</p></th></tr><tr><td><p><strong>工事產出速率</strong></p></td><td><p>每時辰新修行數</p></td><td><p><code>rate(claude_code_lines_of_code_count_total{type="added"}[24h])</code></p></td></tr><tr><td><p><strong>插旗頻率</strong></p></td><td><p>Commit / PR 頻率</p></td><td><p><code>rate(claude_code_commit_count_total[7d])</code></p></td></tr><tr><td><p><strong>兵器效率</strong></p></td><td><p>工事核准率</p></td><td><p><code>accept / (accept + reject)</code></p></td></tr><tr><td><p><strong>糧倉節省</strong></p></td><td><p>糧倉補給命中節省費用</p></td><td><p><code>cacheRead tokens × 輸入單價 × 0.9（折扣）</code></p></td></tr></tbody></table>

<h3>糧草管控四訣</h3>
<ol>
<li><p><strong>每日告警</strong>：設定大帳預警，當單日軍費超出預算即鳴金示警</p>
</li>
<li><p><strong>兵種選用</strong>：Opus 4.6 費用約為 Sonnet 4.6 之十五倍，確認是否調用了正確兵種</p>
</li>
<li><p><strong>糧倉優化</strong>：軍令旗號越穩定，糧倉補給效率越高，可節省九成糧倉部分之費用</p>
</li>
<li><p><strong>戰役時長</strong>：久戰不收兵，context 越積越大，軍費越高；適時鳴金收兵重整，可節省消耗</p>
</li>
</ol>
<h3>月度軍費戰報（PromQL）</h3>
<pre><code class="language-promql"># 本月總軍費
sum(increase(claude_code_cost_usage_USD_total[30d]))

# 本月 vs 上月軍費環比
sum(increase(claude_code_cost_usage_USD_total[30d]))
/ sum(increase(claude_code_cost_usage_USD_total[30d] offset 30d))
</code></pre>
<hr />
<h2>諸侯聯盟：多人團隊監控</h2>
<p><strong>司馬懿謀劃多路諸侯協同之策：</strong></p>
<h3>以旗號區別各路人馬</h3>
<pre><code class="language-bash"># 於各將領的兵書中設定所屬陣營
export OTEL_RESOURCE_ATTRIBUTES="department=engineering,team.id=platform,cost_center=eng-123"
</code></pre>
<h3>按將領分組之軍費查詢</h3>
<pre><code class="language-promql"># 各將領之軍費分佈
sum by (user_account_uuid)(increase(claude_code_cost_usage_USD_total[7d]))

# 各將領糧草消耗排行（前十名）
topk(10,
  sum by (user_account_uuid)(
    increase(claude_code_token_usage_tokens_total[7d])
  )
)
</code></pre>
<h3>主帥統一部署：管理員集中設定</h3>
<p>於 <code>/etc/claude/settings.json</code>（主帥令冊）中統一部署：</p>
<pre><code class="language-json">{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector.company.com:4317",
    "OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer company-token"
  }
}
</code></pre>
<blockquote>
<p>📝 主帥令冊之優先度高於各將領自設兵書，無法被覆蓋。</p>
</blockquote>
<hr />
<h2>陣前排難：常見問題排除</h2>
<p>司馬懿終末叮嚀：</p>
<p>主公，圖表雖美，然「觀圖」之後的「決策」才是勝負關鍵。臣已將這些陣法圖示融入秘錄之中，建議主公下一步可執行以下軍令：</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/858b29a5-cc34-4f6f-8681-be1ff7c77988.png" alt="" style="display:block;margin:0 auto" />

<p><strong>司馬懿沉聲道：</strong> 主公需防以下陣前之亂，臣逐一列出破解之法。</p>
<h3>亂象一：大帳沙盤無任何軍情數報</h3>
<p><strong>診斷步驟</strong>：</p>
<pre><code class="language-bash"># 1. 確認情報中樞正在接收斥候回報
docker compose logs otelcol --tail=50

# 2. 確認糧草台帳之抄錄目標狀態
curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}'

# 3. 直接查閱情報中樞匯出之數報
curl -s http://localhost:8889/metrics | grep claude_code | head -20

# 4. 確認虎符旗號已升起
claude --version  # 並在啟動前 echo $CLAUDE_CODE_ENABLE_TELEMETRY
</code></pre>
<p><strong>常見原因</strong>：</p>
<ul>
<li><p><code>CLAUDE_CODE_ENABLE_TELEMETRY</code> 旗號未升或不在正確的營帳（shell）中</p>
</li>
<li><p>情報中樞驛道（4317）未開放</p>
</li>
<li><p>糧草台帳尚未完成首次抄錄（等候 &gt;30 秒）</p>
</li>
</ul>
<h3>亂象二：戰報文書庫無任何戰報</h3>
<pre><code class="language-bash"># 1. 確認戰報文書庫已就緒
curl http://localhost:3100/ready

# 2. 查詢庫中是否有任何存檔
curl -G -s "http://localhost:3100/loki/api/v1/query" \
  --data-urlencode 'query={service_name="claude-code"}' \
  --data-urlencode 'limit=5' | jq .

# 3. 若無存檔，確認情報中樞之戰報管道
docker compose logs otelcol | grep -i "loki\|log"
</code></pre>
<p><strong>戰報分類過濾問題</strong>：</p>
<p>若 <code>event_name</code> 存於戰報正文而非標籤，需改用：</p>
<pre><code class="language-logql"># 原本（不可用）
{service_name="claude-code"} | event_name=`tool_result`

# 改為 JSON 解讀
{service_name="claude-code"} | json | event_name = "tool_result"
</code></pre>
<hr />
<h2>密情守則：安全性與隱私考量</h2>
<p><strong>司馬懿正色叮囑：</strong> 情報雖重要，洩密亦是大患，主公需知以下密情守則。</p>
<h3>預設隱私防護</h3>
<p>臥龍神算之遙測，預設已做以下防護：</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>情報種類</p></th><th><p>預設處置</p></th><th><p>啟用方式</p></th></tr><tr><td><p>主公原話完整內容</p></td><td><p><strong>不錄入</strong>，只記錄字數</p></td><td><p><code>OTEL_LOG_USER_PROMPTS=1</code></p></td></tr><tr><td><p>軍需官（MCP server）名稱</p></td><td><p><strong>不錄入</strong></p></td><td><p><code>OTEL_LOG_TOOL_DETAILS=1</code></p></td></tr><tr><td><p>奇術（Skill）名目</p></td><td><p><strong>不錄入</strong></p></td><td><p><code>OTEL_LOG_TOOL_DETAILS=1</code></p></td></tr><tr><td><p>原始工事內容（程式碼）</p></td><td><p><strong>永不錄入</strong></p></td><td><p>無法啟用</p></td></tr><tr><td><p>衝鋒指令（含參數）</p></td><td><p>錄入 <code>tool_parameters</code></p></td><td><p>可在後端過濾</p></td></tr></tbody></table>

<h3>潛在密情外洩之處</h3>
<p><code>tool_result</code> 戰報之 <code>tool_parameters</code> 欄位含衝鋒指令，可能含有：</p>
<ul>
<li><p>密語（若直接寫在指令中）</p>
</li>
<li><p>金鑰令牌</p>
</li>
<li><p>典籍路徑（可能洩露大本營之目錄結構）</p>
</li>
</ul>
<p><strong>建議</strong>：在情報中樞中加入 <code>attributes</code> 處理器遮蔽敏感欄位：</p>
<pre><code class="language-yaml">processors:
  attributes:
    actions:
      - key: tool_parameters
        action: delete  # 或使用 update + 遮罩
</code></pre>
<h3>本地陣法 vs 遠端傳送</h3>
<p>本連環陣法<strong>完全在大本營運行</strong>，無任何情報傳送至外部勢力。</p>
<p>若要傳送至雲端（如 Grafana Cloud、Datadog 等外藩），請確認：</p>
<ol>
<li><p>驛道加密（TLS/HTTPS）</p>
</li>
<li><p>認證虎符（<code>OTEL_EXPORTER_OTLP_HEADERS</code> 設定 Bearer token）</p>
</li>
<li><p>情報存留政策符合軍規</p>
</li>
</ol>
<hr />
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6420f5cbbdbe7d697133d12a/5fb76b01-aa68-4d73-9ffd-c8deba6478fb.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>延伸兵書</h2>
<ul>
<li><p><a href="https://code.claude.com/docs/en/monitoring-usage">Claude Code 監控文書</a></p>
</li>
<li><p><a href="https://github.com/anthropics/claude-code-monitoring-guide">Anthropic ROI 量化</a></p>
</li>
<li><p><a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md">OpenTelemetry 規格</a></p>
</li>
<li><p><a href="https://prometheus.io/docs/prometheus/latest/querying/basics/">Prometheus PromQL 查詢教典</a></p>
</li>
<li><p><a href="https://grafana.com/docs/loki/latest/query/">Loki LogQL 查詢文法</a></p>
</li>
<li><p><a href="https://gist.github.com/tedmax100/06f0b9830939eea0ee3c3ec8f1d478bc">Grafana Dashboard 程式碼</a></p>
</li>
</ul>
<hr />
<p><em>兵書版本：2026-02-19 ｜ 主述：司馬懿（仲達）、諸葛亮（孔明）｜ 由</em> <code>/sanguo-rewrite</code> <em>奇術生成</em></p>
]]></content:encoded></item><item><title><![CDATA[工程師的 Claude Code 實戰指南：從零開始到高效開發]]></title><description><![CDATA[工程師的 Claude Code 實戰指南：從零開始到高效開發

本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips，帶你由淺入深掌握 Claude Code。


什麼是 Claude Code？為什麼值得學？
如果你還在用「複製程式碼貼到 ChatGPT，再複製答案貼回去」的工作流程，Claude Code 會讓你大開眼界。
Claude Code 是 Anthropic 推出的命令列工具，它直接活在你的 terminal 裡，能夠讀懂你的整個 codeb...]]></description><link>https://ganhua.wang/claude-code</link><guid isPermaLink="true">https://ganhua.wang/claude-code</guid><category><![CDATA[claude-code]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Wed, 18 Feb 2026 14:44:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771425824749/f0f60f5c-6e9f-4c19-9f32-93b3385a6eb5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-claude-code">工程師的 Claude Code 實戰指南：從零開始到高效開發</h1>
<blockquote>
<p>本文整合 <a target="_blank" href="https://www.anthropic.com/engineering/claude-code-best-practices">Anthropic 官方 Best Practices</a> 與社群實戰 Tips，帶你由淺入深掌握 Claude Code。</p>
<h2 id="heading-"></h2>
</blockquote>
<h2 id="heading-claude-code-1">什麼是 Claude Code？為什麼值得學？</h2>
<p>如果你還在用「複製程式碼貼到 ChatGPT，再複製答案貼回去」的工作流程，Claude Code 會讓你大開眼界。</p>
<p>Claude Code 是 Anthropic 推出的命令列工具，它直接活在你的 terminal 裡，能夠讀懂你的整個 codebase、寫入檔案、執行指令、操作 git，甚至幫你開 PR。它不只是個「提示框」，而是一個能主動採取行動的 AI 代理（agentic coding assistant）。</p>
<p>用一句話形容：<strong>你告訴它要做什麼，它去搞定</strong>。</p>
<p>很多從 Cursor、GitHub Copilot 轉過來的工程師都說，用過 Claude Code 之後回不去了。原因不是它比較聰明，而是它的工作方式根本不同——它在你的環境裡工作，而不是你把東西帶去它的環境。</p>
<hr />
<h2 id="heading-56ys5lia5q2l77ya5a6j6kod6iih5z65pys5zwf5yuv">第一步：安裝與基本啟動</h2>
<p>安裝 Claude Code 只需要一行：</p>
<pre><code class="lang-bash">curl -fsSL https://claude.ai/install.sh | bash
</code></pre>
<p>安裝後，進入你的專案目錄，直接輸入 <code>claude</code> 就能啟動。</p>
<h3 id="heading-5zub56iu5zwf5yuv5qih5byp">四種啟動模式</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指令</td><td>情境</td></tr>
</thead>
<tbody>
<tr>
<td><code>claude</code></td><td>標準啟動，開始全新對話（互動式）</td></tr>
<tr>
<td><code>claude -c</code></td><td>快速接回最近一次的對話</td></tr>
<tr>
<td><code>claude -r</code></td><td>顯示歷史對話列表，含摘要，選擇要接回哪一個</td></tr>
<tr>
<td><code>claude -p "..."</code></td><td>Headless Mode，非互動式，單次執行，用於自動化</td></tr>
</tbody>
</table>
</div><p>建議新手先從 <code>claude</code> 開始，熟悉基本操作後，<code>-c</code> 和 <code>-r</code> 會成為你每天的好朋友。</p>
<h3 id="heading-headless-modeclaude-p">Headless Mode（<code>claude -p</code>）</h3>
<p><code>claude -p</code> 是 Claude Code 從「個人工具」升級到「團隊基礎設施」的關鍵能力：</p>
<ul>
<li>非互動式，執行完就結束，不進入對話模式</li>
<li>適合用在 CI/CD、git hooks、自動化腳本</li>
<li>可搭配 <code>--output-format stream-json</code> 輸出結構化 JSON，方便下游程式處理</li>
<li>可搭配 <code>--allowedTools</code> 限制可用工具範圍</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-comment"># 最簡單的用法</span>
claude -p <span class="hljs-string">"review 這個 PR 的安全性問題"</span>

<span class="hljs-comment"># pipe 資料進去</span>
cat error.log | claude -p <span class="hljs-string">"分析這些錯誤，找出最常見的根因"</span>

<span class="hljs-comment"># 輸出 JSON 給下游處理</span>
claude -p <span class="hljs-string">"列出所有 deprecated 的 API 呼叫"</span> --output-format stream-json | your_script.py

<span class="hljs-comment"># 限制工具範圍（更安全）</span>
claude -p <span class="hljs-string">"把 foo.py 從 React 改成 Vue"</span> --allowedTools Edit <span class="hljs-string">"Bash(git commit:*)"</span>
</code></pre>
<hr />
<h2 id="heading-56ys5lqm5q2l77ya5a245pyd5zyo5bcn6kmx5lit5pon5l2c">第二步：學會在對話中操作</h2>
<p>進入 Claude Code 後，它看起來像個聊天介面，但有一些特殊符號和快捷鍵讓你的效率倍增。</p>
<h3 id="heading-5lij5ycl6laf5pyj55so55qe56ym6jmf">三個超有用的符號</h3>
<p><strong><code>@</code> 指定檔案</strong>：輸入 <code>@</code> 後會顯示檔案列表，支援模糊搜尋，讓你精確告訴 Claude 要操作哪個檔案，不用擔心它讀錯地方。</p>
<p><strong><code>!</code> 直接執行 shell 指令</strong>：有時你只是想快速執行一個指令，不需要 AI 處理，直接用 <code>!</code> 前綴就能執行 shell 命令。例如 <code>!git status</code> 或 <code>!ls -la</code>。</p>
<p><strong><code>#</code> 加入記憶</strong>：當你輸入 <code>#</code> 開頭的訊息，系統會詢問你要存入哪個 CLAUDE.md，讓 Claude 長期記住這段背景知識。例如「# 我們的 API 版本是 v2，請不要使用 v1 的 endpoint」，之後的每次對話它都會記得。</p>
<h3 id="heading-5lin5yv5lin55l55qe5br5o236y21">不可不知的快捷鍵</h3>
<ul>
<li><strong><code>ESC</code></strong> — 中斷當前任務。Claude 正在瘋狂編輯檔案但方向不對？按 ESC 立刻停下，不會破壞 session，可以重新下指令</li>
<li><strong><code>ESC ESC</code>（按兩次）</strong> — 顯示過去發送的訊息列表，讓你選擇一個重新發送，類似「開分支」的概念，從不同的起點探索</li>
<li><strong><code>Shift+TAB</code></strong> — 切換工作模式</li>
</ul>
<h3 id="heading-5lij56iu5bel5l2c5qih5byp">三種工作模式</h3>
<p>使用 <code>Shift+TAB</code> 可以在三種模式間切換：</p>
<p><strong>自動接受模式（auto-accept edits）</strong>：Claude 提議的修改自動核准，適合信任度高、不想每次都確認的場景。速度最快。</p>
<p><strong>規劃模式（plan mode）</strong>：Claude 只做分析和規劃，不會實際動檔案。在開始寫程式之前，先用這個模式讓它產出設計方案給你審核，是架構討論的好幫手。</p>
<p><strong>危險模式（<code>--dangerously-skip-permissions</code>）</strong>：跳過所有權限確認，讓 Claude 全速工作。官方文件特別強調這個模式要在有限制的 Docker container 裡使用，不建議在本機直接用。</p>
<hr />
<h2 id="heading-claudemd">第三步：設定你的專案大腦 — CLAUDE.md</h2>
<p><code>CLAUDE.md</code> 是整個 Claude Code 生態系中最重要的概念。<strong>這個檔案是 Claude 每次啟動對話時必讀的說明書</strong>，放對內容，效果立竿見影。</p>
<p>第一次使用時，執行 <code>/init</code> 指令，Claude 會自動分析你的 codebase 結構並產生一份基礎的 CLAUDE.md，再手動精修即可。</p>
<h3 id="heading-claudemd-1">放什麼在 CLAUDE.md？</h3>
<ul>
<li>常用的 bash 指令（<code>npm run build</code>、<code>npm run test</code> 等）</li>
<li>核心檔案與工具函式的位置說明</li>
<li>程式碼風格規範（例如：使用 ES modules 而非 CommonJS）</li>
<li>測試方式與規則</li>
<li>Git 工作流程規範（例如：branch 命名方式、merge vs. rebase）</li>
<li>開發環境特殊設定</li>
<li>任何你希望 Claude 永遠記住的事項</li>
</ul>
<pre><code class="lang-markdown"><span class="hljs-section"># 常用指令</span>
<span class="hljs-bullet">-</span> npm run build: 建置專案
<span class="hljs-bullet">-</span> npm run test: 執行測試
<span class="hljs-bullet">-</span> npm run typecheck: 型別檢查

<span class="hljs-section"># 程式碼風格</span>
<span class="hljs-bullet">-</span> 使用 ES modules (import/export)，不用 CommonJS (require)
<span class="hljs-bullet">-</span> 盡量用解構語法引入 (import { foo } from 'bar')
<span class="hljs-bullet">-</span> 所有 API 呼叫都走 /src/api/ 資料夾的封裝

<span class="hljs-section"># 工作流程</span>
<span class="hljs-bullet">-</span> 每次改完記得跑 typecheck
<span class="hljs-bullet">-</span> 測試優先，盡量跑單一測試而非全套
<span class="hljs-bullet">-</span> branch 命名格式：feature/xxx 或 fix/xxx
</code></pre>
<h3 id="heading-claudemd-2">CLAUDE.md 的四個層級</h3>
<p>根據官方文件，CLAUDE.md 其實有四個層級，由高到低依序套用：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層級</td><td>位置</td><td>用途</td><td>誰能看到</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Enterprise policy</strong></td><td>macOS: <code>/Library/Application Support/ClaudeCode/CLAUDE.md</code></td><td>公司統一規範，由 IT/DevOps 部署</td><td>組織內所有使用者</td></tr>
<tr>
<td><strong>Project memory</strong></td><td><code>./CLAUDE.md</code> 或 <code>./.claude/CLAUDE.md</code></td><td>專案共用規範</td><td>團隊（透過 git）</td></tr>
<tr>
<td><strong>User memory</strong></td><td><code>~/.claude/CLAUDE.md</code></td><td>個人全域偏好</td><td>只有你（所有專案）</td></tr>
<tr>
<td><strong>Project memory (local)</strong></td><td><code>./CLAUDE.local.md</code></td><td>個人專案設定</td><td>只有你（當前專案）</td></tr>
</tbody>
</table>
</div><blockquote>
<p><code>CLAUDE.local.md</code> 會自動加入 <code>.gitignore</code>，適合放沙箱 URL、個人測試資料等不應上傳的設定。</p>
</blockquote>
<h3 id="heading-import"><code>@import</code> 語法</h3>
<p>CLAUDE.md 支援用 <code>@</code> 語法引入其他檔案，最多支援 5 層巢狀：</p>
<pre><code class="lang-markdown"><span class="hljs-section"># 引入其他說明文件</span>
請參考 @README 了解專案概覽，@package.json 查看可用指令。

<span class="hljs-section"># Git 工作流程</span>
@docs/git-instructions.md

<span class="hljs-section"># 個人偏好（引入自 home 目錄，不會進 git）</span>
@~/.claude/my-project-instructions.md
</code></pre>
<p>這讓 CLAUDE.md 保持簡潔，把細節拆到獨立文件裡分開維護。</p>
<h3 id="heading-monorepo">Monorepo 建議結構</h3>
<p>CLAUDE.md 支援<strong>多層級繼承</strong>，Claude 會自動把沿途讀到的所有 CLAUDE.md 合併進 context，這對 monorepo 非常適合：</p>
<pre><code>root/
├── CLAUDE.md                ← 全專案共用：monorepo 工具（nx/turborepo）、CI 指令、跨 package 規範
│                               可用 @<span class="hljs-keyword">import</span> 引入各 package 文件
├── docs/
│   └── git-instructions.md  ← 被 @<span class="hljs-keyword">import</span> 引用的獨立說明
├── packages/
│   ├── frontend/
│   │   └── CLAUDE.md        ← 前端專用：React 規範、CSS-<span class="hljs-keyword">in</span>-JS、UI 元件慣例
│   ├── backend/
│   │   └── CLAUDE.md        ← 後端專用：API 設計規範、DB migration 指令
│   └── shared/
│       └── CLAUDE.md        ← shared lib 專用：型別規範、不能有 side effect 等限制
└── CLAUDE.local.md          ← 個人設定，自動加入 .gitignore
</code></pre><p><strong>分層邏輯：</strong></p>
<ul>
<li><strong>根目錄</strong> 放「任何 package 都適用」的內容，例如 <code>pnpm install</code>、branch 命名規則、PR 流程</li>
<li><strong>各 package</strong> 只放「這個 package 才有意義」的差異化內容</li>
</ul>
<p>當你在 <code>packages/frontend/</code> 啟動 <code>claude</code>，它會同時讀到根目錄與 <code>packages/frontend/</code> 的 CLAUDE.md，不需要在每個 package 重複寫共用規範。</p>
<blockquote>
<p>小技巧：根目錄的 CLAUDE.md 加一行職責邊界說明，例如「<code>packages/shared</code> 只能被其他 package 引用，不能引用 frontend 或 backend 的程式碼」，跨 package 重構時 Claude 就不會犯錯。</p>
</blockquote>
<h3 id="heading-5yw25luw566h55cg6kiy5oa255qe5pa55byp">其他管理記憶的方式</h3>
<ul>
<li><strong><code>#</code> 快速新增</strong>：輸入 <code># 內容</code>，系統會詢問要存入哪個 CLAUDE.md</li>
<li><strong><code>/memory</code> 指令</strong>：在對話中執行，會用系統編輯器開啟記憶檔案，適合一次做大量整理</li>
</ul>
<hr />
<h2 id="heading-56ys5zub5q2l77ya5lik5lil5pah566h55cgiokalcdmnidpl5zpjbxnmotos4fmupa">第四步：上下文管理 — 最關鍵的資源</h2>
<p>很多人用 Claude Code 用著用著開始感覺它「變笨了」，原因幾乎都是一樣的：<strong>context window 滿了</strong>。</p>
<p>Claude 的整個對話歷史、讀過的每個檔案、執行過的每個指令輸出，全都塞在 context window 裡。一個複雜的除錯過程，可能幾萬個 token 就燒掉了。當 context 快滿，Claude 開始「遺忘」早期的指令，犯更多錯誤。</p>
<h3 id="heading-5ywp5ycl6zec6y215oyh5luk">兩個關鍵指令</h3>
<p><strong><code>/clear</code></strong>：清除所有當前對話的 context，重新開始。開始一個全新任務之前，養成習慣打 <code>/clear</code>，讓它帶著清爽的頭腦工作。</p>
<p><strong><code>/compact</code></strong>：壓縮 context，可以附上提示說明要保留哪些重點。注意，壓縮後有可能遺失部分細節、導致後續表現變差，更建議的做法是直接開新的 session。</p>
<p>官方建議：<strong>在長對話工作後、切換到新任務前，定期使用 <code>/clear</code></strong>。有些工程師甚至把「每次開始新任務就 /clear」當成強制習慣。</p>
<hr />
<h2 id="heading-think">第五步：善用 Think 模式深度推理</h2>
<p>Claude Code 支援幾個特殊關鍵字，讓它進入不同深度的思考模式：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層級</td><td>關鍵字</td></tr>
</thead>
<tbody>
<tr>
<td>基礎</td><td><code>think</code></td></tr>
<tr>
<td>中等</td><td><code>think more</code> / <code>think hard</code></td></tr>
<tr>
<td>深入</td><td><code>think longer</code> / <code>think harder</code></td></tr>
<tr>
<td>最大</td><td><code>ultrathink</code></td></tr>
</tbody>
</table>
</div><p>使用方式很直覺，直接在 prompt 裡說「請 think hard，設計這個資料庫 schema」或「ultrathink，分析這個 bug 的根本原因」即可。</p>
<p><strong>重要提醒</strong>：更深的思考消耗更多 token，建議根據問題複雜度選擇。簡單任務用 <code>think</code> 就夠，複雜架構設計或難以追蹤的 bug 才用 <code>ultrathink</code>。</p>
<p>中文指令「想一想」、「深度思考」也可以被識別，但優先用英文以確保穩定性。</p>
<hr />
<h2 id="heading-56ys5ywt5q2l77ya5o6m5oh6auy5pwi5bel5l2c5rwb">第六步：掌握高效工作流</h2>
<p>有了基礎工具知識後，來看幾個 Anthropic 官方推薦的實戰工作流。</p>
<h3 id="heading-commit">工作流一：探索 → 規劃 → 實作 → Commit</h3>
<p>這是最通用的工作流，適合大多數功能開發場景：</p>
<ol>
<li><strong>探索</strong>：告訴 Claude 讀相關的檔案、圖表或 URL，但<strong>明確說「還不要寫程式」</strong>。這個階段讓它建立完整的 context。</li>
<li><strong>規劃</strong>：請 Claude 制定實作計畫（可以用 <code>think hard</code> 讓它想得更仔細）。計畫確認後，請它寫成 Markdown 文件或 GitHub issue 存檔。</li>
<li><strong>實作</strong>：拿著確認好的計畫，請它動手寫程式。</li>
<li><strong>Commit</strong>：請 Claude 寫 commit message 並 commit，需要的話也可以請它開 PR。</li>
</ol>
<p>前兩步是關鍵。很多工程師跳過規劃直接叫它寫，結果方向跑偏，浪費更多時間。</p>
<h3 id="heading-tdd">工作流二：TDD 測試驅動開發</h3>
<p>這是 Anthropic 內部最愛的工作流，AI 時代的 TDD 威力加倍：</p>
<ol>
<li>請 Claude 根據預期的輸入輸出寫測試，<strong>明確說要做 TDD，不要先做 mock 實作</strong></li>
<li>請它跑測試，確認測試<strong>確實失敗</strong></li>
<li>對測試滿意後，請它 commit 測試</li>
<li>請 Claude 寫讓測試通過的實作，<strong>不允許修改測試</strong>，讓它自己跑測試、修改、再跑，直到全部通過</li>
<li>確認後 commit 實作</li>
</ol>
<p>提供清晰的「完成標準」是讓 Claude 表現最好的方式，而測試就是最好的完成標準。</p>
<h3 id="heading-ui">工作流三：截圖視覺迭代（UI 開發最愛）</h3>
<ol>
<li>給 Claude 一個設計稿圖片（拖放或貼上截圖）</li>
<li>請它實作 UI</li>
<li>透過 Puppeteer MCP 讓它截圖比對</li>
<li>請它根據差異迭代修正</li>
<li>滿意後 commit</li>
</ol>
<p>Claude 在有視覺目標時表現特別好。第一版可能不完美，但給它 2-3 輪迭代後通常相當接近設計稿。</p>
<hr />
<h2 id="heading-claude">第七步：進階技巧 — 讓 Claude 更強大</h2>
<h3 id="heading-mcp">擴充工具能力：MCP</h3>
<p>MCP（Model Context Protocol）是讓 Claude Code 連接外部服務的標準協議。常用的有：</p>
<ul>
<li><strong>Puppeteer</strong>：讓 Claude 能操控瀏覽器、截圖</li>
<li><strong>GitHub MCP</strong>：更深度整合 GitHub 操作</li>
<li><strong>資料庫 MCP</strong>：直接查詢你的資料庫</li>
</ul>
<p>在專案目錄加入 <code>.mcp.json</code> 設定檔，整個團隊都能共用這些工具。</p>
<h3 id="heading-slash">自訂 Slash 指令</h3>
<p>把重複的工作流程做成 slash 指令，存在 <code>.claude/commands/</code> 資料夾裡。例如建立 <code>fix-github-issue.md</code>：</p>
<pre><code class="lang-markdown">請分析並修復 GitHub issue：$ARGUMENTS

步驟：
<span class="hljs-bullet">1.</span> 用 gh issue view 取得 issue 詳細內容
<span class="hljs-bullet">2.</span> 理解問題描述
<span class="hljs-bullet">3.</span> 搜尋 codebase 找出相關檔案
<span class="hljs-bullet">4.</span> 實作必要的修改
<span class="hljs-bullet">5.</span> 撰寫並執行測試驗證修復
<span class="hljs-bullet">6.</span> 確認通過 lint 和 type check
<span class="hljs-bullet">7.</span> 建立描述性的 commit message
<span class="hljs-bullet">8.</span> Push 並開 PR
</code></pre>
<p>之後只要輸入 <code>/project:fix-github-issue 1234</code> 就能一鍵處理 issue #1234。個人常用指令則存到 <code>~/.claude/commands/</code> 讓所有專案都能用。</p>
<h3 id="heading-permissions">允許清單（Permissions）</h3>
<p>用 <code>/permissions</code> 指令把常用操作加入允許清單，例如 <code>Edit</code>（允許編輯檔案）和 <code>Bash(git commit:*)</code>（允許 git commit），不用每次都確認，工作流程更流暢。</p>
<hr />
<h2 id="heading-claude-1">第八步：多 Claude 並行 — 終極生產力</h2>
<p>當你熟悉了單 Claude 工作流後，可以嘗試更強大的多 Agent 模式。</p>
<h3 id="heading-claude-2">雙 Claude 互審</h3>
<p>用第一個 Claude 寫程式，另開一個 terminal 或用 <code>/clear</code> 重置，讓第二個 Claude 審查程式碼。不同的 context 往往能發現不同的問題，效果類似真實的 code review。</p>
<h3 id="heading-git-worktrees">Git Worktrees 平行作業</h3>
<p>這是 Anthropic 工程師內部的常用技巧：用 <code>git worktree add</code> 建立多個獨立的工作目錄，每個目錄開一個 Claude，同時處理不同任務：</p>
<pre><code class="lang-bash">git worktree add ../project-feature-a feature-a
git worktree add ../project-feature-b feature-b

<span class="hljs-built_in">cd</span> ../project-feature-a &amp;&amp; claude  <span class="hljs-comment"># Claude A 做 feature A</span>
<span class="hljs-built_in">cd</span> ../project-feature-b &amp;&amp; claude  <span class="hljs-comment"># Claude B 做 feature B</span>

<span class="hljs-comment"># 完成後清理</span>
git worktree remove ../project-feature-a
git worktree remove ../project-feature-b
</code></pre>
<p>兩個任務互不干擾，你只需要輪流去確認進度和核准操作。</p>
<hr />
<h2 id="heading-57wm5paw5oml55qe5bu66k2w5rif5zau">給新手的建議清單</h2>
<p><strong>操作習慣</strong></p>
<ul>
<li>每次開始新任務前先 <code>/clear</code>，保持 context 乾淨</li>
<li>養成用 <code>ESC</code> 中斷而非 <code>Ctrl+C</code>（後者會直接退出整個程式）</li>
<li>用 <code>@</code> 精確指定檔案，不要讓它猜</li>
<li>重要決策和設定用 <code>#</code> 記錄到 CLAUDE.md，或用 <code>/memory</code> 做整理</li>
</ul>
<p><strong>下指令的原則</strong></p>
<ul>
<li><strong>具體比模糊好</strong>。「幫我加測試」遠不如「為 foo.py 新增一個測試 case，覆蓋使用者未登入的邊界情況，不要用 mock」</li>
<li>先規劃再實作，省下來的時間遠比多打一次指令值得</li>
<li>根據問題複雜度選擇 think 層級，不要每次都 ultrathink</li>
</ul>
<p><strong>品質把關</strong></p>
<ul>
<li>Claude 寫的程式你負責 review，最終責任在你</li>
<li>有測試和 CI 的環境下信任它更多，沒有就要多留意</li>
<li>對話越長越要注意 context 品質，適時 <code>/clear</code> 比用 <code>/compact</code> 更可靠</li>
</ul>
<hr />
<h2 id="heading-57wq6kqe">結語</h2>
<p>Claude Code 的學習曲線不在於功能複雜，而在於思維方式的轉換：從「AI 幫我補全程式碼」轉向「AI 是我的協作工程師，我負責方向，它負責執行」。</p>
<p>一開始可能會覺得要打很多字、要設定很多東西。但一旦你的 CLAUDE.md 建立起來，slash 指令設定好，權限調整完，你會發現整個開發體驗有質的飛躍。</p>
<p>從今天開始，打開 terminal，進入你的專案，輸入 <code>claude</code>，然後問它：「你對這個 codebase 有什麼問題嗎？」——你們的旅程就此開始。</p>
<hr />
<p><em>參考資料：<a target="_blank" href="https://www.anthropic.com/engineering/claude-code-best-practices">Anthropic 官方 Claude Code Best Practices</a>、<a target="_blank" href="https://code.claude.com/docs/en/memory">Anthropic 官方 Memory 文件</a>、<a target="_blank" href="https://blog.cashwu.com/blog/2025/claude-code-tips">Cash Wu 的 Claude Code Tips</a></em></p>
]]></content:encoded></item><item><title><![CDATA[System Design Interview Ch 12 Digital Wallet]]></title><description><![CDATA[確立問題與設計範疇




角色對話內容



面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎？我們是否需要擔心其他功能？

面試官讓我們只關注餘額轉帳操作。

面試者該系統需要支援多少 TPS（每秒交易次數）？

面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。

面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎？

面試官聽起來不錯。

面試者我們需要證明正確性嗎？

面試官這是一個很好的問題。正確性（Correctness）通常只有在交...]]></description><link>https://ganhua.wang/system-design-interview-ch-12-digital-wallet</link><guid isPermaLink="true">https://ganhua.wang/system-design-interview-ch-12-digital-wallet</guid><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Mon, 02 Feb 2026 03:20:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770019579195/0a432322-c4e6-4789-82dc-19985ae409ff.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-kirnorrnq4vllypoyzoiifoqk3oqijnr4tnlocqkg"><strong>確立問題與設計範疇</strong></h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>角色</td><td>對話內容</td></tr>
</thead>
<tbody>
<tr>
<td><strong>面試者</strong></td><td>我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎？我們是否需要擔心其他功能？</td></tr>
<tr>
<td><strong>面試官</strong></td><td>讓我們只關注餘額轉帳操作。</td></tr>
<tr>
<td><strong>面試者</strong></td><td>該系統需要支援多少 TPS（每秒交易次數）？</td></tr>
<tr>
<td><strong>面試官</strong></td><td>讓我們假設是 <strong>1,000,000 TPS</strong> (每秒 100 萬次交易)。</td></tr>
<tr>
<td><strong>面試者</strong></td><td>數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎？</td></tr>
<tr>
<td><strong>面試官</strong></td><td>聽起來不錯。</td></tr>
<tr>
<td><strong>面試者</strong></td><td>我們需要證明正確性嗎？</td></tr>
<tr>
<td><strong>面試官</strong></td><td>這是一個很好的問題。<strong>正確性（Correctness）通常只有在交易完成後才能驗證</strong>。一種驗證方法是將我們的內部記錄與銀行對帳單進行比較。對帳的局限性在於它只顯示差異，無法說明差異是如何產生的。因此，我們希望設計一個具有**可重現性（Repoducibility）**的系統，這意味著我們可以透過從頭開始重放數據，隨時重建歷史餘額。</td></tr>
<tr>
<td><strong>面試者</strong></td><td>我們可以假設可用性要求是 <strong>99.99%</strong> 嗎？</td></tr>
<tr>
<td><strong>面試官</strong></td><td>聽起來不錯。</td></tr>
<tr>
<td><strong>面試者</strong></td><td>我們需要考慮外匯嗎？</td></tr>
<tr>
<td><strong>面試官</strong></td><td>不，這超出了本次討論範圍。</td></tr>
</tbody>
</table>
</div><p>摘要:</p>
<p>• <strong>高吞吐量 (TPS)：</strong> 系統需要支援<strong>每秒 1,000,000 次交易 (1,000,000 TPS)</strong>。</p>
<ul>
<li>為了達到這個目標，後端估算可能需要約 2,000 個資料庫節點來處理每秒 1 百萬次轉帳（因為每次轉帳涉及兩條指令，相當於 2 百萬次操作）。</li>
</ul>
<p>• <strong>可靠性：</strong> 系統的可靠性必須至少達到 <strong>99.99%</strong>。</p>
<p>• <strong>事務與一致性：</strong> 必須支援<strong>交易 (transactions)</strong>，以確保資料的正確性。</p>
<p>• <strong>可重現性 (Reproducibility)：</strong> 系統需要具備從頭開始重放資料來重建歷史餘額的能力</p>
<h1 id="heading-step-1">Step 1 粗略估算</h1>
<p><strong>1. 估算的前提與假設</strong></p>
<p>• <strong>背景：</strong> 討論 TPS 時，通常假設系統將使用<strong>事務型資料庫 (transactional database)</strong>。</p>
<p>• <strong>性能基準：</strong> 來源指出，現今運行在典型資料中心節點上的關係型資料庫，大約可以支援<strong>每秒幾千次事務</strong>。</p>
<p>• <strong>單節點假設：</strong> 為了進行估算，假設一個資料庫節點可以支援 <strong>1,000 TPS</strong>（每秒 1,000 次事務）。</p>
<p>• <strong>目標負載：</strong> 系統必須支援 <strong>1,000,000 TPS</strong>（每秒 1 百萬次交易）。</p>
<p><strong>2. 初始計算與修正</strong></p>
<p>這個估算過程的關鍵點在於，<strong>交易數（轉帳次數）並不等同於實際的資料庫操作數</strong>。</p>
<p>1. <strong>初始（不準確的）計算：</strong> 如果系統需要 1,000,000 TPS，而每個資料庫節點能處理 1,000 TPS，那麼初始估算需要 <strong>1,000 個資料庫節點</strong>（1,000,000 / 1,000 = 1,000）。</p>
<p>2. <strong>關鍵修正（實際操作數）：</strong> 這個計算被認為是<strong>稍微不準確的</strong>。這是因為<strong>每個轉帳指令 (transfer command) 需要兩個操作</strong>：</p>
<p>◦ 從一個帳戶<strong>扣除</strong>資金 (deducting money)。</p>
<p>◦ 向另一個帳戶<strong>存入</strong>資金 (depositing money)。</p>
<p>3. <strong>最終所需的節點數量：</strong> 為了支援每秒 1 百萬次的轉帳，系統實際上需要處理高達 <strong>2 百萬次 TPS</strong>（2,000,000 TPS，即 2 百萬次操作）。 因此，根據每個節點 1,000 TPS 的假設，系統需要 <strong>2,000 個節點</strong>（2,000,000 / 1,000 = 2,000 節點）來支援此負載。</p>
<p><strong>3. 對設計的影響</strong></p>
<p>這個粗略估算直接影響了設計目標，因為所需節點的總數與單節點可處理的事務次數成反比：</p>
<p>• <strong>設計目標：</strong> 降低所需的總節點數量是設計目標之一，這意味著必須<strong>增加單一節點可以處理的事務數量 (per-node TPS)</strong>。單節點的 TPS 越高，所需的硬體成本就越低。</p>
<p>來源提供了 TPS 與所需節點數量的映射表（Table 12.1），進一步說明了這種關係：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>每節點 TPS (Per-node TPS)</td><td>所需節點數量 (Node Number)</td></tr>
</thead>
<tbody>
<tr>
<td>100</td><td>20,000</td></tr>
<tr>
<td><strong>1,000</strong></td><td><strong>2,000</strong> (此為計算結果)</td></tr>
<tr>
<td>10,000</td><td>200</td></tr>
</tbody>
</table>
</div><p>總結來說，粗略估算確定了數位錢包系統為了處理 1 百萬次轉帳的負載，需要<strong>數千個</strong>資料庫節點，這為後續討論<strong>分區 (sharding)</strong>、<strong>分佈式事務 (distributed transactions)</strong> 以及如何提高單節點效率奠定了基礎。</p>
<h1 id="heading-step-2-propose-high-level-design-and-get-buy-in"><strong>Step 2 - Propose High-level Design and Get Buy-in</strong></h1>
<h3 id="heading-1-api-api-design"><strong>1. API 設計 (API Design)</strong></h3>
<p>在深入討論架構之前，首先要確立外部客戶端將如何與系統互動。</p>
<p>• <strong>API 協定：</strong> 採用 <strong>RESTful API 協定</strong>。</p>
<p>• <strong>唯一支援的 API：</strong> 在本次面試中，只需支援一個 API，即<strong>跨錢包餘額轉帳</strong>操作。</p>
<p>• <strong>API 規範：</strong></p>
<p>◦ <strong>方法/路徑：</strong> <code>POST /v1/wallet/balance_transfer</code>。</p>
<p>◦ <strong>功能：</strong> 執行從一個錢包向另一個錢包轉帳。</p>
<p>• <strong>請求參數 (Request parameters)：</strong> 請求需要包含以下欄位：</p>
<p>◦ <code>from_account</code>：借記帳戶（扣款帳戶）。</p>
<p>◦ <code>to_account</code>：貸記帳戶（收款帳戶）。</p>
<p>◦ <code>amount</code>：金額（資料類型為字串 <code>string</code>）。</p>
<p>◦ <code>currency</code>：貨幣類型（使用 ISO 4217 標準）。</p>
<p>◦ <code>transaction_id</code>：用於<strong>去重複 (deduplication)</strong> 的 UUID。</p>
<p>•Response Body:</p>
<p>◦ <code>status</code></p>
<p>◦ <code>transaction_id</code></p>
<h3 id="heading-2-three-high-level-designs"><strong>2. 三種高階設計方案的討論 (Three High-level Designs)</strong></h3>
<p>此階段的重點是逐步提出並完善三個高階設計方案，以解決系統對<strong>一致性</strong>和<strong>可重現性</strong>的嚴格要求。</p>
<ul>
<li><h4 id="heading-21-simple-in-memory-solution">2.1. 方案一：<strong>簡單的記憶體內解決方案 (Simple In-memory solution)</strong></h4>
</li>
</ul>
<p>這是第一個被提出的高階方案，主要利用<strong>記憶體內分區儲存</strong>（例如 Redis 叢集）來維護帳戶餘額。</p>
<p><img src="https://www.tpisoftware.com/tpu/File/html/202012/20201211164256/images/20201204152234.png" alt="Docker + Redis Cluster 實戰" /></p>
<p>• <strong>架構：</strong> 錢包服務 (Wallet Service) 是<strong>無狀態的</strong>，它根據分片資訊（可能由 ZooKeeper 管理）向分佈在多個 Redis 節點上的帳戶發出更新指令。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763911824187/6ab1bbd2-e4f2-4d3f-b72c-2cf4db977071.png" alt class="image--center mx-auto" /></p>
<p>以下是面試官與面構、高效能優勢，以及<strong>最關鍵的缺陷——缺乏事務原子性 (Atomicity)</strong> 進行了交流。</p>
<blockquote>
<p>1. 面試者對方案的描述（架構概述）</p>
<p>面試者首先說明了該方案的結構和工作原理：</p>
<p>• <strong>資料儲存與分散：</strong> 帳戶餘額分佈在多個 <strong>Redis 節點</strong>上。</p>
<p>• <strong>分片資訊：</strong> <strong>ZooKeeper</strong> 被用於維護分片（sharding）資訊。</p>
<p>• <strong>服務層：</strong> <strong>無狀態的錢包服務 (stateless Wallet Service)</strong> 利用分片資訊來定位客戶端所在的 Redis 節點。</p>
<p>• <strong>操作：</strong> 錢包服務隨後相應地更新帳戶餘額。</p>
<p>2. 面試官的評論與關鍵挑戰（指出缺陷）</p>
<p>面試官肯定了此設計在工作方面是可行的，但立刻指出了它最大的問題——無法滿足系統對「正確性」的嚴格要求：</p>
<p>• <strong>承認可行性：</strong> 面試官指出：「這個設計是可行的 (The design works)」。</p>
<p>• <strong>否決原因：</strong> 面試官接著說：「但它<strong>不符合我們對正確性的要求</strong> (but it does not meet our correctness requirement)」。</p>
<p>• <strong>問題暴露：</strong> 面試官解釋了轉帳操作如何違反原子性：</p>
<p>◦ 錢包服務需要為每次轉帳更新<strong>兩個 Redis 節點</strong>。</p>
<p>◦ <strong>無法保證</strong>這兩次更新都能成功。</p>
<p>◦ <strong>失敗情境：</strong> 如果錢包服務節點在<strong>第一次更新完成之後</strong>、但在<strong>第二次更新完成之前崩潰</strong>，就會導致<strong>轉帳不完整 (an incomplete transfer)</strong>。</p>
<p><strong>結論要求：</strong> 面試官強調，這兩次更新需要作為<strong>單一的原子事務 (single atomic transaction)</strong> 進行。</p>
</blockquote>
<p>• <strong>結論：</strong> 儘管它能滿足高吞吐量的需求（1,000,000 TPS），但<strong>無法保證原子性</strong>。由於一次轉帳涉及兩次獨立的更新，如果錢包服務在兩次更新之間崩潰，會導致<strong>轉帳不完整</strong>。因此，此方案因缺乏事務保證而被排除。</p>
<ul>
<li><h4 id="heading-22-database-based-distributed-transaction-solution">2.2. 方案二：<strong>基於資料庫的分佈式事務解決方案 (Database-based distributed transaction solution)</strong></h4>
</li>
</ul>
<p>為了提供事務保證，設計從記憶體儲存轉向使用<strong>事務型關聯式資料庫</strong>。由於帳戶仍然需要分區，因此需要實施<strong>分佈式事務</strong>。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763911879056/4f55a57f-e43d-4ff2-9a7c-2bf7a5b3a253.png" alt class="image--center mx-auto" /></p>
<p>來源討論了兩種分佈式事務的實施方式：</p>
<p>• <strong>兩階段提交 (2PC)：</strong> 這是一種<strong>低階解決方案</strong>，依賴資料庫本身來確保原子性。然而，2PC 的主要問題是可能導致<strong>鎖被長時間佔用</strong>，並且<strong>協調者 (Coordinator) 可能成為單點故障 (single point of failure)</strong>。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763911899424/31fa9b1e-1dee-497d-9815-51779c16df43.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763911937692/b43ddcdc-a4e1-4284-b14c-e3007a4a7c52.png" alt class="image--center mx-auto" /></p>
<p>• <strong>嘗試-確認/取消 (TC/C)：</strong> 這是一種<strong>高階解決方案</strong>，在<strong>業務邏輯層</strong>處理事務，因此它與底層資料庫無關。TC/C 分為兩個階段：第一階段是 <strong>Try</strong>（保留資源），第二階段是 <strong>Confirm</strong>（確認）或 <strong>Cancel</strong>（取消，即「undo」操作）。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763911966825/0d6f05fb-eb5c-4fb7-9991-986bf0489057.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Try (嘗試階段)：</strong> 進行業務檢查，並<strong>預留</strong>必要的業務資源。例如圖中的帳戶 A 先扣除 $1，確保這筆錢被鎖定，但還沒真正交給帳戶 C。</p>
</li>
<li><p><strong>Confirm (確認階段)：</strong> 當所有參與者的 Try 都成功後，執行真正的業務操作。此階段不進行業務檢查，只使用 Try 階段預留的資源。</p>
</li>
<li><p><strong>Cancel (取消階段)：</strong> 如果有任何參與者的 Try 失敗，則執行回滾，釋放 Try 階段預留的資源（即「undo」操作）。</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763912207532/5ff3c400-ffdc-4a86-afeb-03df31d557e0.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-try-phase-figure-127"><strong>第一階段：Try Phase (Figure 12.7)</strong></h3>
<p>這是所有流程的起點。</p>
<ol>
<li><p><strong>Coordinator (Wallet Service)</strong> 發起請求。</p>
</li>
<li><p><strong>Database A (扣款方)：</strong> 執行 <code>UPDATE</code> 將餘額減 1。這就是「預留資源」，錢已經從 A 的可用餘額中消失了。</p>
</li>
<li><p><strong>Database C (收款方)：</strong> 此時執行 <code>NOP</code> (No Operation)，意即在 Try 階段不做任何實質改動，僅確認服務可用。</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763912220098/8cdf8b1e-eb6c-4708-a922-2fcc171d1a98.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-confirm-phase-figure-128"><strong>第二階段：Confirm Phase (Figure 12.8)</strong></h3>
<p>當 Try 階段全部成功（Done）時進入此流程。</p>
<ol>
<li><p><strong>Database A：</strong> 既然 Try 已經扣過錢了，Confirm 階段只需確認（NOP），不需要再動 A。</p>
</li>
<li><p><strong>Database C：</strong> 執行 <code>UPDATE</code> 將餘額加 1。這完成了最終的轉帳動作。</p>
</li>
<li><p><strong>結果：</strong> A 減 1，C 加 1，事務成功完成。</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763912232095/4e64e2b0-3f77-413e-94d7-fb64f8f7bf99.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-cancel-phase-figure-129"><strong>第二階段：Cancel Phase (Figure 12.9)</strong></h3>
<p>如果在 Try 階段發生錯誤（例如圖中的 <code>X Failed</code>），則進入取消流程。</p>
<ol>
<li><p><strong>Database A：</strong> 因為 Try 階段已經扣了 $1，現在必須「補償」回來。執行 <code>UPDATE balance = balance + 1</code>，將 A 的錢加回去。</p>
</li>
<li><p><strong>Database C：</strong> 維持 NOP，因為 Try 階段本來就沒動到 C。</p>
</li>
<li><p><strong>結果：</strong> A 的餘額恢復原狀，系統回到初始狀態，保證了一致性。</p>
</li>
</ol>
<p><strong>階段狀態表（Phase Status Table）</strong></p>
<p>1. 階段狀態表的用途與必要性</p>
<p>在分佈式事務的設計中，例如 TC/C，協調者（在本案例中是錢包服務或 TCC/Saga 協調者）負責協調多個資料庫節點的更新。</p>
<p>• <strong>解決的問題：</strong> 來源提到，如果錢包服務在 <strong>TC/C 處理的中途重新啟動</strong>，它可能會遺失所有先前的操作歷史記錄，並且不知道如何恢復事務。</p>
<p>• <strong>解決方案：</strong> 解決方案很簡單，就是將 <strong>TC/C 執行的進度 (progress)</strong> 儲存到<strong>事務型資料庫</strong>中的階段狀態表內。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763913024374/0674de3b-7764-462d-98b9-9ac0b027a3a0.png" alt class="image--center mx-auto" /></p>
<p>• <strong>功能：</strong> 階段狀態表用於儲存分佈式事務的狀態，從而允許系統在協調者崩潰或重新啟動後，<strong>知道如何恢復</strong>，確保事務的持久性和一致性。</p>
<p>2. 階段狀態表的內容</p>
<p>階段狀態表儲存了確保分佈式事務能夠恢復和追蹤所需的重要資訊，包括：</p>
<p>1. <strong>分佈式事務的 ID 與內容 (ID and content)</strong>。</p>
<p>2. <strong>Try 階段的狀態 (The status of the Try phase)</strong>：記錄了給定資料庫的狀態（例如，狀態是否已發送、已接收、或已回應）。</p>
<p>3. <strong>第二階段的名稱 (The name of the Second phase)</strong>：根據 Try 階段的結果，可以計算出第二階段是 **Confirm（確認）**或 <strong>Cancel（取消）</strong>。</p>
<p>4. <strong>第二階段的狀態 (The status of the second phase)</strong>。</p>
<p>5. <strong>亂序執行狀態 (An out-of-order execution status)</strong>：用於處理亂序執行的狀況。</p>
<ul>
<li>TC/C 發生 <strong>Out-of-order Execution</strong></li>
</ul>
<p>TC/C 模式處理因網路延遲導致「亂序執行（Out-of-order Execution）」的機制如下：</p>
<h3 id="heading-1">1. 問題場景定義</h3>
<p>在分散式系統中，網路延遲可能導致指令到達順序與發送順序不一致。最典型的亂序場景是 <strong>Cancel 命令比 Try 命令先到達</strong>。</p>
<ul>
<li><p><strong>發生原因：</strong> 協調者（Coordinator）發送 <code>Try</code> 後，因為網路擁塞導致超時（Timeout）。協調者隨即判定失敗並發送 <code>Cancel</code> 進行補償。結果 <code>Cancel</code> 請求比遲到的 <code>Try</code> 請求更早到達參與者節點。</p>
</li>
<li><p><strong>風險：</strong> 如果不處理，節點收到 <code>Cancel</code> 時發現沒有 <code>Try</code> 記錄可能什麼都不做。接著 <code>Try</code> 到達並成功預留資源。但因為協調者已經發過 <code>Cancel</code> 並認為事務已結束，這筆被 <code>Try</code> 預留的資源將永遠不會被 Confirm 或釋放（資源洩漏）。</p>
</li>
</ul>
<h3 id="heading-2">2. 解決方案：階段狀態表與亂序旗標</h3>
<p>為了處理解決這個問題，系統必須利用 <strong>階段狀態表 (Phase Status Table)</strong> 配合特定的邏輯檢查：</p>
<h4 id="heading-5q2l6amf5lia77ya5ywb6kix44cm56m65zue5ru44cn5lim6kiy6yye5qiz6kiy">步驟一：允許「空回滾」並記錄標記</h4>
<p>當節點收到 <code>Cancel</code> 指令時，如果發現沒有對應的 <code>Try</code> 記錄，它<strong>不能</strong>視為錯誤或忽略。</p>
<ul>
<li><p><strong>邏輯：</strong> 節點必須允許在未收到 <code>Try</code> 的情況下執行 <code>Cancel</code>。</p>
</li>
<li><p><strong>記錄：</strong> 節點必須在 <strong>階段狀態表</strong> 中插入一條記錄，並設置一個 <strong>「亂序旗標 (out-of-order flag)」</strong>。這個旗標表示：「我已經收到過 Cancel 了，雖然我還沒看過 Try」。</p>
</li>
</ul>
<h4 id="heading-try">步驟二：Try 階段的預先檢查</h4>
<p>當延遲的 <code>Try</code> 指令最終到達節點時，系統必須執行以下檢查：</p>
<ul>
<li><p><strong>檢查：</strong> <code>Try</code> 操作在執行任何資源預留之前，必須先查詢階段狀態表，檢查是否已存在該事務 ID 的 <strong>亂序旗標</strong>。</p>
</li>
<li><p><strong>結果：</strong> 如果發現該旗標存在（表示 Cancel 已經先執行了），<code>Try</code> 操作必須<strong>返回失敗 (return a failure)</strong>。</p>
</li>
</ul>
<p><img src="https://lh3.googleusercontent.com/notebooklm/ANHLwAySCCtndQTiS-vt_gipd-19PCYrDaDYGuH3M1wcFW5Xl7dSNcM0LZJyJu7sWjXQuhmUYFwrccPLAQWfJCB1XK4Jb3diEtrzZJhm5scqxyr3cn96ej5_rDqnMQ7_aLjpYlFEfKBnIP9X_CNEOjLf7c9UDFtC7_4=w2752-d-h1536-mp2?authuser=0" alt="這張技術圖解說明如何透過階段狀態表與亂序旗標，解決 TC/C 模式中因網路延遲導致的資源鎖定與洩漏問題。" /></p>
<p>透過在 <strong>階段狀態表</strong> 中儲存 <strong>亂序執行狀態 (out-of-order execution status)</strong>，系統確保了即使 <code>Cancel</code> 先於 <code>Try</code> 執行，遲到的 <code>Try</code> 也不會錯誤地鎖定資源，從而保證了分散式事務在網路不穩定環境下的正確性。這也是為什麼來源強調階段狀態表是 TC/C 中處理協調者崩潰恢復與亂序執行的關鍵組件。</p>
<p>3. 階段狀態表的位置</p>
<p>階段狀態表通常儲存在<strong>包含資金被扣除的錢包帳戶（借記帳戶）的資料庫</strong>中。</p>
<p>在最終的高階設計中，階段狀態表與 <strong>TCC/Saga 協調者</strong>協同工作。當使用者發送轉帳命令時：</p>
<p>• <strong>TCC/Saga 協調者</strong>會<strong>在階段狀態表中建立一條記錄</strong>，以追蹤該事務的狀態。</p>
<p>• 當分區 1（帳戶 A 所在的資料庫）成功執行扣款操作後，<strong>Saga 協調者會在階段狀態表中記錄該操作成功</strong>。這確保了在執行下一步（向帳戶 C 存入資金）之前，系統狀態是可追溯和可恢復的。</p>
<p>總體來說，階段狀態表是將應用層實現的分佈式事務（如 TC/C 和 Saga）的<strong>狀態持久化</strong>的機制，從而將無狀態的服務轉變為具備故障恢復能力的系統。</p>
<p><strong>不平衡狀態 (Unbalanced state)</strong></p>
<p>「不平衡狀態 (Unbalanced state)」是在<strong>分佈式事務解決方案</strong>，特別是<strong>應用層事務模型，如嘗試-確認/取消 (TC/C) 或 Saga</strong>，執行過程中出現的一種<strong>暫時性資料不一致狀態</strong>。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763913270238/c8830acc-e792-4958-915c-f674b42e959e.png" alt class="image--center mx-auto" /></p>
<p>這張圖片詳細解釋了在分散式事務（特別是 TCC 或 Saga 模式）中，系統處於中間過程時所產生的**「不平衡狀態 (Unbalanced state)」**。</p>
<p>簡單來說，它描述了「錢已經扣了，但還沒入帳」的那段暫時性消失的狀態。</p>
<hr />
<h3 id="heading-kirlnjbooajmtyhnqivmi4bop6mqkg"><strong>圖表流程拆解</strong></h3>
<p>追蹤了帳戶 A 加帳戶 C 的<strong>總金額 ($A+C$)</strong> 變化：</p>
<h4 id="heading-1-before-tcc-start"><strong>1. 初始狀態 (Before TCC start)</strong></h4>
<ul>
<li><p><strong>狀態：</strong> 帳戶 A 有 $1，帳戶 C 有 $0。</p>
</li>
<li><p><strong>總和：</strong> $A+C = \$1$。此時系統是平衡的。</p>
</li>
</ul>
<h4 id="heading-2-after-first-phase-try"><strong>2. 第一階段結束後 (After first phase: Try)</strong></h4>
<ul>
<li><p><strong>動作：</strong> Coordinator 執行了 <code>Try: A-$1</code>。資料庫 A 的餘額被更新為 $0。</p>
</li>
<li><p><strong>狀態：</strong> 帳戶 A 變成了 $0，而帳戶 C 依然是 $0。</p>
</li>
<li><p><strong>總和：</strong> <strong>$A+C = \$0$</strong>。</p>
</li>
<li><p><strong>現象 (Money loss)：</strong> 從系統宏觀角度看，這 $1 暫時「失蹤」了。這就是圖中所標註的 <strong>「不平衡狀態 (Unbalanced state)」</strong>。這是一種暫時性的資料不一致。</p>
</li>
</ul>
<h4 id="heading-3-after-second-phase-confirm"><strong>3. 第二階段結束後 (After second phase: Confirm)</strong></h4>
<ul>
<li><p><strong>動作：</strong> Coordinator 執行了 <code>Confirm: C+$1</code>。資料庫 C 的餘額被更新為 $1。</p>
</li>
<li><p><strong>狀態：</strong> 帳戶 A 為 $0，帳戶 C 為 $1。</p>
</li>
<li><p><strong>總和：</strong> <strong>$A+C = \$1$</strong>。</p>
</li>
<li><p><strong>現象 (Money recovery)：</strong> 隨著第二階段完成，失蹤的 $1 被找回來了，系統重新回到平衡狀態。</p>
</li>
</ul>
<p>以下是針對不平衡狀態的詳細說明：</p>
<p>1. 不平衡狀態的定義與發生時機</p>
<p>不平衡狀態發生在分佈式轉帳操作的第一階段結束時。</p>
<p>• <strong>機制：</strong> 假設有一個從帳戶 A 轉帳 1給帳戶<em>C</em>的操作。在∗∗<em>TC</em>/<em>C</em>的<em>Try</em>階段(第一階段)∗∗結束時，∗∗1 已成功從帳戶 A 扣除**，但<strong>帳戶 C 仍然保持不變</strong>（尚未收到 $1）。</p>
<p>• <strong>結果：</strong> 在這個中間點，帳戶 A 和帳戶 C 的餘額總和將會是 <strong>$0</strong>（假設交易開始時總和為 $1），這<strong>小於 TC/C 開始時的總和</strong>。</p>
<p><img src="https://lh3.googleusercontent.com/notebooklm/ANHLwAypV_kdfsJw4dmelaotoU_y6NT_t1zANbO_SSiEYhL-Hu10Tzy1af3Q7Z0DASufXVSpcaqp-1_0Tl9EF2e3QZpgkvhCDn1M1BElpNpFS_hXbiCxTUp2Bq_qSu8X7GNm_ljWCCIatZkkGzv_uhIPc16QV6Qp_g=w2752-d-h1536-mp2?authuser=0" alt="此圖表解釋 TCC 分散式事務模式中，為何在嘗試階段優先扣款是確保交易原子性與安全回滾的唯一有效方法。" /></p>
<p>2. 會計原則的違反</p>
<p>這種暫時的狀態被稱為「不平衡」，因為它<strong>違反了一項基本的會計原則</strong>：在一次交易之後，資金的總和應該保持不變。</p>
<p>• 在 Try 階段完成後，資金似乎「丟失」了 $1 (Money loss: $1)，這就是不平衡狀態的體現。資金必須等到第二階段（Confirm 確認）完成後才會恢復平衡 (Money recovery: $1)。</p>
<p>3. TC/C 導致狀態可見性</p>
<p>不平衡狀態是應用層級分佈式事務（如 TC/C 和 Saga）的特性，因為這些高階解決方案是透過協調數個<strong>獨立的本地事務 (several independent local transactions)</strong> 來運作的。</p>
<p>• <strong>應用程式可見：</strong> 因為 <strong>TC/C 是在應用程式層實施的</strong>，應用程式本身能夠<strong>看到</strong>這些本地事務之間產生的中間結果（即不平衡狀態）。</p>
<p>• <strong>與 2PC 的區別：</strong> 相比之下，資料庫事務或<strong>兩階段提交 (2PC)</strong> 版本的集中式分佈式事務，通常會由資料庫維護這種高層次事務的抽象，因此這些不平衡狀態對於應用程式來說是<strong>不可見的</strong>。</p>
<p>4. 解決與處理</p>
<p>儘管存在不平衡狀態，<strong>事務保證仍然由 TC/C 維持</strong>。這意味著系統最終會通過執行第二階段（確認或取消）來達成最終的一致性。</p>
<p>• 由於高階分佈式事務會導致這種差異可見，如果底層的系統（如資料庫）沒有預先修復這些差異，那麼<strong>應用程式層必須自行處理這些不一致性 (handle them ourselves)</strong>。例如，TC/C 模式必須透過 <strong>Confirm</strong> 階段完成轉入，或者透過 <strong>Cancel</strong> 階段執行補償操作（undo）來回滾先前的操作，從而最終消除不平衡狀態。</p>
<p>此階段還討論了 <strong>Saga 工作流程</strong>，這是另一種應用層的分佈式事務模式。TC/C 和 Saga 都可以用來實施分佈式事務，但 <strong>TC/C 允許並行執行</strong>，而 Saga（除非使用協調模式）通常需要<strong>線性順序</strong>執行。</p>
<p>[<strong>Saga Pattern</strong>](<a target="_blank" href="https://ithelp.ithome.com.tw/articles/10236124">https://ithelp.ithome.com.tw/articles/10236124</a>)</p>
<p>[<strong>Saga, Choreography vs Orchestration](</strong><a target="_blank" href="https://ithelp.ithome.com.tw/articles/10236411">https://ithelp.ithome.com.tw/articles/10236411</a><strong>)</strong></p>
<p>[<strong>YouTUbe Digital Wallet System Design | Distributed transactions | SAGA pattern</strong>](<a target="_blank" href="https://www.youtube.com/watch?v=8ByEyCd-MEM">https://www.youtube.com/watch?v=8ByEyCd-MEM</a>)</p>
<h4 id="heading-saga-vs-tcc">Saga v.s. TC/C</h4>
<p>Saga 模式與 TC/C (Try-Confirm/Cancel) 在性能上最顯著的差異在於**「並行執行能力 (Parallel Execution)導致的延遲 (Latency)<strong>不同，以及適用</strong>交易長度**的區別。</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>角色</td><td>對話內容</td></tr>
</thead>
<tbody>
<tr>
<td>面試官</td><td>我們在實務中應該使用哪一個？</td></tr>
<tr>
<td>面試者</td><td>答案取決於延遲（Latency）的要求。Saga 中的操作必須按線性順序（串聯）執行，但在 TCC 中則可以並行執行。因此，這項決定取決於以下幾個因素：</td></tr>
<tr>
<td>面試者</td><td>1. 如果沒有延遲要求，或者服務數量非常少（例如我們的轉帳範例），我們可以選擇其中任何一種。如果我們想追隨微服務架構的趨勢，請選擇 <strong>Saga</strong>。</td></tr>
<tr>
<td>面試者</td><td>2, 如果系統對延遲非常敏感，且包含許多服務或操作，<strong>TCC</strong> 可能是更好的選擇。</td></tr>
<tr>
<td>面試者</td><td>為了讓餘額轉帳具備事務性（Transactional），我們將 Redis 替換為 RDBMS，並使用 TCC 或 Saga 來實現分散式事務。</td></tr>
</tbody>
</table>
</div><p>以下是差異分析：</p>
<p><strong>1. 並行執行與延遲 (Parallelism &amp; Latency)</strong></p>
<p>這是兩者在架構設計上最直接的性能差異：</p>
<ul>
<li><p><strong>TC/C (低延遲，支援並行)：</strong></p>
<ul>
<li><p><strong>機制：</strong> TC/C 允許協調者在第一階段 (<strong>Try 階段</strong>) <strong>同時</strong>向所有參與服務發送請求（例如同時預留資源 A 和資源 B）。</p>
</li>
<li><p><strong>性能影響：</strong> 由於操作是並行執行的，總耗時取決於<strong>最慢</strong>的那個服務，而非所有服務耗時的總和。因此，TC/C 非常適合對<strong>延遲敏感 (Latency-sensitive)</strong> 的系統,,。</p>
</li>
</ul>
</li>
<li><p><strong>Saga (較高延遲，線性執行)：</strong></p>
<ul>
<li><p><strong>機制：</strong> 在典型的編排模式中，Saga 通常被設計為<strong>線性執行 (Linear execution)</strong>，即操作按順序一個接一個執行（例如先扣款，成功後再觸發存款）。</p>
</li>
<li><p><strong>性能影響：</strong> 總耗時是鏈條上所有服務耗時的<strong>總和</strong>。如果業務流程包含多個步驟，Saga 的整體響應時間會比 TC/C 長,。</p>
</li>
</ul>
</li>
</ul>
<p><strong>2. 資源鎖定與交易長度 (Resource Locking &amp; Transaction Duration)</strong></p>
<p>兩者對資源的佔用方式不同，這決定了它們分別適用於「短交易」還是「長交易」：</p>
<ul>
<li><p><strong>TC/C (適用於短交易)：</strong></p>
<ul>
<li><p><strong>資源預留 (Quasi-Isolation)：</strong> TC/C 在 Try 階段會「凍結」或「預留」資源（例如將資金移入 <code>trading_balance</code> 欄位）。雖然這不是資料庫層級的死鎖，但在 Confirm/Cancel 完成前，這部分資源是無法被其他事務使用的,。</p>
</li>
<li><p><strong>性能限制：</strong> 由於應用程式崩潰可能導致預留狀態遺失，且長時間預留會影響業務並發度，TC/C 被設計用來處理<strong>短時間</strong>且要求<strong>高一致性</strong>的交易,。</p>
</li>
</ul>
</li>
<li><p><strong>Saga (適用於長交易 - Long Lived Transactions)：</strong></p>
<ul>
<li><p><strong>無鎖/立即提交 (Lock-free)：</strong> Saga 的每個本地事務執行完後會<strong>立即提交 (Commit)</strong> 到資料庫，不進行資源預留。這意味著資源鎖定時間極短,。</p>
</li>
<li><p><strong>性能優勢：</strong> 由於不長時間佔用資源，Saga 非常適合處理跨越數分鐘甚至數天的<strong>長事務 (LLTs)</strong>，在這種場景下它能維持高吞吐量，不會像 2PC 或 TC/C 那樣因為長時間佔用資源而導致性能瓶頸,,。</p>
</li>
</ul>
</li>
</ul>
<p><strong>3. 補償成本 (Compensation Cost)</strong></p>
<ul>
<li><p><strong>TC/C (輕量回滾)：</strong> TC/C 的 Cancel 操作通常只是釋放 Try階段預留的資源（例如解凍資金），邏輯相對簡單且副作用較小,。</p>
</li>
<li><p><strong>Saga (昂貴補償)：</strong> 由於 Saga 的本地事務已提交，若後續步驟失敗，必須執行<strong>補償事務 (Compensating Transaction)</strong> 來「修正」數據（例如退款）。這通常涉及新的業務邏輯寫入（如新增一條負向流水的紀錄），在性能開銷和開發複雜度上可能較高,。</p>
</li>
</ul>
<p><strong>總結比較表</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>特性</td><td><strong>TC/C (Try-Confirm/Cancel)</strong></td><td><strong>Saga</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>執行順序</strong></td><td><strong>可並行 (Parallel)</strong></td><td><strong>線性/順序 (Linear)</strong></td></tr>
<tr>
<td><strong>延遲 (Latency)</strong></td><td><strong>低</strong> (取決於最慢的服務)</td><td><strong>高</strong> (所有步驟耗時總和)</td></tr>
<tr>
<td><strong>資源佔用</strong></td><td><strong>預留資源</strong> (準隔離性)，佔用時間較長</td><td><strong>立即提交</strong> (無隔離性)，佔用時間極短</td></tr>
<tr>
<td><strong>適用場景</strong></td><td><strong>短交易</strong>、高併發、對延遲敏感</td><td><strong>長交易 (LLT)</strong>、非同步流程,</td></tr>
</tbody>
</table>
</div><p><strong>結論：</strong> 如果您的系統追求<strong>極致的低延遲</strong>且能接受較高的開發成本（需實作 Try/Confirm/Cancel 三個接口），<strong>TC/C</strong> 是性能較佳的選擇；如果您處理的是<strong>長流程</strong>且希望避免資源鎖定，<strong>Saga</strong> 則能提供更好的系統吞吐量。</p>
<p>面試官: 分散式事務解決方案雖然有效，但在某些情況下可能表現不佳。例如，用戶可能在應用層輸入了錯誤的操作，在這種情況下，我們指定的金額可能是錯誤的。我們需要一種方法來<strong>追蹤問題的根因</strong>，並<strong>審計（Audit）所有帳戶操作</strong>。我們該如何做到這一點？</p>
<ul>
<li>2.3. 方案三：具備可重現性的事件溯源解決方案 (Event sourcing solution with reproducibility)</li>
</ul>
<p>此方案是為了滿足面試官提出的對<strong>審計</strong>和<strong>追蹤問題根本原因</strong>的更高要求。</p>
<hr />
<p><strong>1. 為什麼選擇事件溯源？ (Background)</strong></p>
<p>在面試場景中，面試官提出了一個關鍵挑戰：外部審計員可能會問：「我們如何知道任何給定時間點的帳戶餘額？」或「我們如何證明程式碼更改後的系統邏輯仍然正確？」。</p>
<p>傳統資料庫只儲存<strong>當前的狀態 (Current State)</strong>（例如：餘額 $10），一旦更新就覆蓋了舊值，丟失了變更的歷史軌跡。<strong>事件溯源</strong>正是為了回答這些問題而引入的設計哲學。</p>
<p>• <strong>核心理念：</strong> <strong>事件溯源 (Event Sourcing)</strong> 哲學將所有狀態變化儲存為<strong>不可變的事件清單</strong>，而不是只儲存最終的餘額狀態。</p>
<p>• <strong>可重現性：</strong> 事件清單是不可變的，狀態機 (State machine) 的行為是<strong>確定性的</strong>，因此系統可以透過<strong>重放 (replaying) 事件</strong>來重建歷史狀態。這是事件溯源相對於其他架構的<strong>最大優勢</strong>。</p>
<p>• <strong>狀態機：</strong> 狀態機在事件溯源中扮演核心角色，負責<strong>驗證命令 (Validate commands)</strong> 和<strong>應用事件 (Apply event) 來更新狀態</strong>。</p>
<hr />
<p><strong>2. 四個核心概念 (Definitions)</strong></p>
<p><img src="https://lh3.googleusercontent.com/notebooklm/ANHLwAysGJqKwQAwjLum5Qn6_ou1NC_RguuKT-Q9n1W4B7iw_Z1DC5NK5L3G2v0kyIsfV6Ms8GOZctGe3IgdETk0DFjikVIc-8Vbc74shVV9ij4JmbM4j_0xgF1YvrFgUEdZuKSg77gD2y9AyFbLeKEl51iD350pacc=w2752-d-h1536-mp2?authuser=0" alt="這張圖表說明事件溯源如何解決傳統系統挑戰，並展示透過 mmap、CQRS 和 Raft 演算法實現高效能與高可靠性的技術架構。" /></p>
<p>事件溯源的四個重要術語,：</p>
<ol>
<li><p><strong>命令 (Command)</strong>：</p>
<ul>
<li><p>這是來自外部世界的<strong>意圖 (Intended action)</strong>。例如：「從 A 轉 $1 給 C」。</p>
</li>
<li><p>命令不是事實，它可能會失敗（例如餘額不足）。命令必須按順序放入 FIFO 佇列中。</p>
</li>
</ul>
</li>
<li><p><strong>事件 (Event)</strong>：</p>
<ul>
<li><p>這是已經發生並經過驗證的<strong>事實 (Validated fact)</strong>。例如：「從 A 轉了 $1 給 C (Transferred $1 from A to C)」。</p>
</li>
<li><p><strong>不可變性：</strong> 事件一旦生成，就永遠不能被改變。</p>
</li>
<li><p><strong>區別：</strong> 一個命令可能產生零個或多個事件。事件代表過去式。</p>
</li>
</ul>
</li>
<li><p><strong>狀態 (State)</strong>：</p>
<ul>
<li>這是在應用事件後變更的內容。在錢包系統中，狀態就是客戶的<strong>帳戶餘額</strong> (Map&lt;User, Balance&gt;)。</li>
</ul>
</li>
<li><p><strong>狀態機 (State Machine)</strong>：</p>
<ul>
<li><p>這是驅動事件溯源過程的引擎。它有兩個主要功能：</p>
<ol>
<li><p><strong>驗證命令並生成事件 (Validate commands and generate events)。</strong></p>
</li>
<li><p><strong>應用事件以更新狀態 (Apply event to update state)。</strong></p>
</li>
</ol>
</li>
<li><p><strong>關鍵特性 - 確定性 (Deterministic)：</strong> 狀態機的行為必須是嚴格確定性的。它<strong>不能包含任何隨機性</strong>（如隨機數、依賴系統時間或外部 I/O）。這保證了只要輸入相同的事件序列，無論何時重播，輸出的狀態永遠一致。</p>
</li>
</ul>
</li>
</ol>
<hr />
<p><strong>3. 運作流程 (Dynamic View)</strong></p>
<p>狀態機處理請求的流程如下,：</p>
<ol>
<li><p><strong>讀取命令：</strong> 從命令佇列（如 Kafka）讀取轉帳請求。</p>
</li>
<li><p><strong>讀取狀態：</strong> 從資料庫讀取當前的餘額。</p>
</li>
<li><p><strong>驗證邏輯：</strong> 檢查餘額是否足夠。</p>
</li>
<li><p><strong>生成事件：</strong> 如果驗證通過，生成事件（如 <code>MoneyDeducted</code>）。</p>
</li>
<li><p><strong>應用事件：</strong> 更新資料庫中的餘額。</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770009185954/b499ac40-2f95-4357-8ea1-ff770bc683a7.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>系統分為三個層次：<strong>Command（指令）</strong>、<strong>Event（事件）</strong> 與 <strong>State（狀態）</strong>。</p>
</li>
<li><p><strong>指令</strong>經過驗證（Validate）後轉化為<strong>事件</strong>，事件被應用（Apply）後改變<strong>狀態</strong>。</p>
</li>
<li><p><strong>錢包服務範例 (Wallet Service Example)：</strong></p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770009535614/5e85c1c9-20f8-43f0-887b-79e244083447.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Command 是來自外部世界（客戶端）的請求。它不是事實，因為它可能會失敗（例如餘額不足或驗證失敗）。Command 會被放入一個 FIFO 隊列中等待處理。</p>
<ul>
<li><ul>
<li><pre><code class="lang-json">        {
          <span class="hljs-attr">"name"</span>: <span class="hljs-string">"TransferRequest"</span>,
          <span class="hljs-attr">"payload"</span>: {
            <span class="hljs-attr">"transaction_id"</span>: <span class="hljs-string">"81589980-2664-11ec-9621-0242ac130002"</span>, <span class="hljs-comment">// 用於去重 (Deduplication)</span>
            <span class="hljs-attr">"from_account"</span>: <span class="hljs-string">"A"</span>,      <span class="hljs-comment">// 扣款帳戶</span>
            <span class="hljs-attr">"to_account"</span>: <span class="hljs-string">"C"</span>,        <span class="hljs-comment">// 收款帳戶</span>
            <span class="hljs-attr">"amount"</span>: <span class="hljs-string">"1.00"</span>,         <span class="hljs-comment">// 金額 (字串類型以避免精度丟失)</span>
            <span class="hljs-attr">"currency"</span>: <span class="hljs-string">"USD"</span>         <span class="hljs-comment">// 貨幣類型</span>
          },
          <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2023-10-27T10:00:00Z"</span>
        }
</code></pre>
    ```</li>
</ul>
</li>
</ul>
</li>
<li><p>Event 是經過狀態機驗證後產生的結果。它代表已經發生且<strong>不可變 (Immutable)</strong> 的歷史事實。</p>
<ul>
<li><pre><code class="lang-json">    事件 <span class="hljs-number">1</span>：扣款事件 (針對帳戶 A)
    {
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"MoneyDeducted"</span>,    <span class="hljs-comment">// 過去式，表示已發生</span>
      <span class="hljs-attr">"payload"</span>: {
        <span class="hljs-attr">"account_id"</span>: <span class="hljs-string">"A"</span>,        <span class="hljs-comment">// 影響的帳戶</span>
        <span class="hljs-attr">"amount"</span>: <span class="hljs-string">"1.00"</span>,         <span class="hljs-comment">// 變動金額</span>
        <span class="hljs-attr">"transaction_id"</span>: <span class="hljs-string">"81589980-2664-11ec-9621-0242ac130002"</span>, <span class="hljs-comment">// 關聯的交易 ID</span>
        <span class="hljs-attr">"new_balance"</span>: <span class="hljs-string">"9.00"</span>     <span class="hljs-comment">// (可選) 有些設計會包含結果餘額，或由重播計算</span>
      },
      <span class="hljs-attr">"sequence_id"</span>: <span class="hljs-number">101</span>,         <span class="hljs-comment">// 用於確保順序</span>
      <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2023-10-27T10:00:01Z"</span>
    }
    事件 <span class="hljs-number">2</span>：入帳事件 (針對帳戶 C)
    {
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"MoneyCredited"</span>,    <span class="hljs-comment">// 過去式</span>
      <span class="hljs-attr">"payload"</span>: {
        <span class="hljs-attr">"account_id"</span>: <span class="hljs-string">"C"</span>,
        <span class="hljs-attr">"amount"</span>: <span class="hljs-string">"1.00"</span>,
        <span class="hljs-attr">"transaction_id"</span>: <span class="hljs-string">"81589980-2664-11ec-9621-0242ac130002"</span>
      },
      <span class="hljs-attr">"sequence_id"</span>: <span class="hljs-number">102</span>,
      <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2023-10-27T10:00:01Z"</span>
    }
</code></pre>
</li>
</ul>
</li>
<li><ul>
<li><p>在錢包服務中，「指令」就是<strong>轉帳請求</strong>。</p>
<p>    * 這些指令會進入一個 <strong>FIFO</strong> Queue。實務上常用的選擇是 <strong>Kafka</strong>。</p>
<p>    * 當狀態儲存在關聯式資料庫時，狀態機按以下 5 個步驟執行：</p>
<p>    1. <strong>Read commands：</strong> 從指令隊列（Command Queue）讀取指令。</p>
<p>    2. <strong>Read balance：</strong> 從資料庫讀取目前的餘額狀態。</p>
<p>    3. <strong>Validate：</strong> 驗證指令（例如餘額是否充足）。若有效，為涉及的帳戶生成事件。</p>
<p>    * <em>範例：</em> 指令是「A 轉 $1 元給 C」，會生成兩個事件：「A: $-$1」與「C: $+$1」。</p>
<p>    4. <strong>Read next event：</strong> 讀取生成的事件。</p>
<p>    5. <strong>Apply event：</strong> 通過更新資料庫中的餘額來應用事件。</p>
</li>
</ul>
</li>
</ul>
<hr />
<p><strong>4. 核心優勢：可重現性 (Reproducibility)</strong></p>
<p>這是事件溯源相對於其他架構的最大優勢。由於事件列表是不可變的，且狀態機是確定性的，系統可以透過<strong>從頭重播 (Replay)</strong> 所有事件來隨時重建歷史上的任何狀態。這完美解決了對帳與審計的需求，也能用來驗證修復 Bug 後的邏輯是否正確,。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770009655902/3eec320b-103b-4419-994b-e58f46ccb3de.png" alt class="image--center mx-auto" /></p>
<p><strong>審計問題的解答 (Final Section)</strong></p>
<p>事件溯源能回答以下三個關鍵問題：</p>
<ol>
<li><p><strong>我們能否知道任何特定時間點的帳戶餘額？</strong></p>
<ul>
<li><em>回答：</em> 可以，透過從起點重放事件，直到你想知道的那個時間點為止。</li>
</ul>
</li>
<li><p><strong>我們如何知道歷史與目前的餘額是正確的？</strong></p>
<ul>
<li><em>回答：</em> 透過事件列表重新計算來驗證。</li>
</ul>
</li>
<li><p><strong>如何證明代碼更動後系統邏輯依然正確？</strong></p>
<ul>
<li><em>回答：</em> 可以針對同一組事件運行不同版本的代碼，驗證其產出的結果是否一致（即 Regression Testing）。</li>
</ul>
</li>
</ol>
<blockquote>
<p><strong>結論：</strong> 由於具備強大的審計能力，事件溯源通常被視為錢包服務（Wallet Service）的標準解決方案。</p>
</blockquote>
<hr />
<p><strong>5. CQRS 架構 (Command-Query Responsibility Segregation)</strong></p>
<p>[<strong>CQRS亂談](</strong><a target="_blank" href="https://ithelp.ithome.com.tw/articles/10237458">https://ithelp.ithome.com.tw/articles/10237458</a><strong>)</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770009683653/1345b7e9-c96d-4a59-aafc-b7894f2ab207.png" alt class="image--center mx-auto" /></p>
<p>由於事件溯源主要處理寫入，且只是一堆事件日誌，客戶端很難直接查詢「我現在有多少錢」。因此系統採用了 <strong>CQRS</strong>,：</p>
<ul>
<li><p><strong>寫入路徑 (Write Path)：</strong> 由上述狀態機負責，處理命令並生成/儲存事件。</p>
</li>
<li><p><strong>讀取路徑 (Read Path)：</strong> 由 <strong>唯讀狀態機 (Read-only state machines)</strong> 負責。它們訂閱事件流，並計算出狀態，儲存到專門用於查詢的資料庫或視圖 (View) 中。客戶端查詢時是訪問這個讀取層。</p>
</li>
<li><p><strong>特性：</strong> 讀取層可能有輕微的延遲（最終一致性），但在金融系統中，這通常是可接受的，且能提供完整的審計軌跡。</p>
</li>
</ul>
<hr />
<p><strong>6. 深入設計：高效能與可靠性優化 (Deep Dive)</strong></p>
<p>為了達到 <strong>100 萬 TPS</strong> 的目標，簡單的 Kafka + 資料庫方案是不夠的。所以能考慮進行深度的技術優化：</p>
<h4 id="heading-a-file-based-storage">A. 檔案基於的命令與事件儲存 (File-based Storage)</h4>
<ul>
<li><p><strong>問題：</strong> 透過網絡訪問遠端儲存（如 Kafka 或傳統 DB）太慢。</p>
</li>
<li><p><strong>優化：</strong> 將命令和事件直接保存在<strong>本地磁碟 (Local Disk)</strong> 的追加日誌檔 (Append-only file) 中。</p>
</li>
<li><p><strong>技術：</strong> 利用 <strong>mmap (Memory Mapped File)</strong> 技術。這將磁碟文件映射到記憶體數組中，將寫入操作轉換為記憶體操作，並由作業系統負責刷入磁碟。這將隨機 I/O 轉換為極快的<strong>循序 I/O (Sequential I/O)</strong>。</p>
</li>
</ul>
<h4 id="heading-b-local-state">B. 狀態儲存 (Local State)</h4>
<ul>
<li>使用嵌入式的高效能資料庫，如 <strong>RocksDB</strong> 或 <strong>SQLite</strong>，直接在本地儲存狀態（餘額），避免遠端資料庫調用的開銷。RocksDB 使用 LSM-tree 結構，非常適合高寫入場景。</li>
</ul>
<h4 id="heading-c-snapshot">C. 快照 (Snapshot)</h4>
<ul>
<li><p>為了避免每次重啟都要從「創世區塊」重播數百萬個事件，系統會定期將當前的狀態存成<strong>快照</strong>。</p>
</li>
<li><p>恢復時，只需讀取最近的快照，然後重播該快照之後的事件即可。</p>
</li>
</ul>
<h4 id="heading-d-raft-consensus">D. 可靠性：Raft 共識演算法 (Consensus)</h4>
<p>[<strong>etcd Raft淺談(上) 名詞簡介</strong>](<a target="_blank" href="https://ithelp.ithome.com.tw/articles/10239673">https://ithelp.ithome.com.tw/articles/10239673</a>)</p>
<ul>
<li><p><strong>風險：</strong> 使用本地磁碟儲存雖然快，但如果該節點掛了，資料就丟了（單點故障）。</p>
</li>
<li><p><strong>解決方案：</strong> 引入 <strong>Raft 共識演算法</strong>。</p>
</li>
<li><p><strong>運作：</strong> 系統將節點組成一個 Raft 群組（1 個 Leader，多個 Follower）。</p>
<ul>
<li><p>Leader 接收命令並轉換為事件。</p>
</li>
<li><p>Raft 負責將<strong>事件列表 (Event List)</strong> 複製到其他 Follower 節點。</p>
</li>
<li><p>只要大多數節點存活，資料就不會丟失。</p>
</li>
<li><p>這確保了在本地高效能儲存的基礎上，同時擁有分散式系統的高可用性與資料一致性。</p>
</li>
</ul>
</li>
</ul>
<p><strong>為什麼「事件 (Event)」是可靠性的核心？</strong></p>
<p>為什麼我們備份「事件」而不是「指令」或「狀態」。</p>
<ul>
<li><p><strong>指令 (Command) 不具備決定性：</strong> 指令在轉化為事件的過程中，可能包含隨機數或外部 I/O。如果只記錄指令，重新執行時結果可能不同。</p>
</li>
<li><p><strong>事件 (Event) 是既定事實：</strong> 事件代表已經發生的歷史紀錄（如：帳戶餘額變動），它是<strong>不可變 (Immutable)</strong> 的。</p>
</li>
<li><p><strong>結論：</strong> 只要確保「事件列表」具備高可靠性，我們隨時都能透過重放 (Replay) 事件來還原系統狀態。</p>
</li>
</ul>
<p><code>利用 Raft 演算法達成共識</code></p>
<p>為了避免單點故障，需要將事件列表同步到多個節點。</p>
<ul>
<li><p><strong>多數決原則 (Majority)：</strong> 只要集群中超過半數的節點正常運作（例如 5 台中有 3 台活著），系統就能持續運作。</p>
</li>
<li><p><strong>角色分工：</strong></p>
<ul>
<li><p><strong>Leader (領導者)：</strong> 負責接收外部指令、轉換成事件，並同步給其他節點。</p>
</li>
<li><p><strong>Follower (跟隨者)：</strong> 接收來自 Leader 的事件並存檔。</p>
</li>
<li><p><strong>Candidate (候選人)：</strong> 競選 Leader 時的過渡狀態。</p>
</li>
</ul>
</li>
</ul>
<p><strong>解決效能瓶頸：分片與即時性</strong></p>
<p>單一 Raft 組無法支撐百萬級 TPS，因此需要擴展架構。</p>
<ul>
<li><p><strong>資料分片 (Sharding)</strong>：根據 Key 的雜湊值將資料切分到不同的 Raft 組（Partition）。(Multi Raft Group)</p>
</li>
<li><p><strong>從「拉」到「推」</strong>：為了避免客戶端因輪詢 (Polling) 產生的延遲，引入 <strong>反向代理 (Reverse Proxy)</strong>。當狀態機完成更新時，主動將結果推送給反向代理，給予用戶即時的回饋感。</p>
</li>
</ul>
<h2 id="heading-saga">跨區交易：Saga 分散式事務</h2>
<p>當一筆交易（如轉帳）跨越兩個分片時，需要 <strong>Saga 協調器 (Coordinator)</strong> 來確保一致性。</p>
<h3 id="heading-happy-path"><strong>轉帳成功流程 (Happy Path)</strong>：</h3>
<ol>
<li><p><strong>發起請求</strong>：用戶 A 要求轉帳 $1 給用戶 C。</p>
</li>
<li><p><strong>狀態追蹤</strong>：協調器在狀態表中建立記錄。</p>
</li>
<li><p><strong>分片 1 執行 (扣款)</strong>：協調器發送指令給 Partition 1。Leader 將其轉為事件、同步 Raft，並執行扣款。成功後透過讀取路徑回傳狀態給協調器。</p>
</li>
<li><p><strong>分片 2 執行 (存款)</strong>：協調器確認扣款成功後，發送指令給 Partition 2 執行存款。同樣完成 Raft 同步後回傳成功訊息。</p>
</li>
<li><p><strong>結束交易</strong>：協調器更新狀態表，完成整個分散式事務。</p>
</li>
</ol>
<h3 id="heading-kirmnrbmp4vnul3ntzdooagqkg"><strong>架構總結表</strong></h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>組件</strong></td><td><strong>解決的問題</strong></td><td><strong>關鍵機制</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Event Sourcing</strong></td><td>數據一致性與可追溯性</td><td>指令轉事件、事件不可變性</td></tr>
<tr>
<td><strong>Raft Algorithm</strong></td><td>服務高可用性與防單點故障</td><td>多數決同步、Leader 選舉</td></tr>
<tr>
<td><strong>Sharding</strong></td><td>系統吞吐量 (TPS) 限制</td><td>按 Hash 分片處理</td></tr>
<tr>
<td><strong>Saga Model</strong></td><td>跨分片的數據一致性</td><td>協調器、階段性狀態追蹤</td></tr>
</tbody>
</table>
</div><p>在分散式架構中，當多個請求同時想對同一個帳戶（用戶 A）進行操作時，系統設計主要透過 <strong>「排序」</strong> 與 <strong>「驗證」</strong> 兩個核心機制來確保資料正確性。</p>
<p><strong>1. Raft Leader 的序列化 (Serialization)</strong></p>
<p>在 Partition 1 的 Raft 組中，只有一個 <strong>Leader</strong> 負責接收所有指令。</p>
<ul>
<li><p><strong>指令排隊</strong>：不論有多少個 Saga 協調器同時發送「扣款用戶 A」的指令，Partition 1 的 Leader 都會將這些指令放入一個<strong>指令列表 (Command List)</strong> 中。</p>
</li>
<li><p><strong>單一順序</strong>：Leader 會按照接收順序，一個接一個地處理這些指令。這意味著在技術層面上，對同一個用戶的操作已經在進入系統的第一關被「排好隊」了。</p>
</li>
</ul>
<p><strong>2. 狀態驗證與事件轉換 (Validation)</strong></p>
<p>當 Leader 處理到某個扣款指令時，它會執行以下動作：</p>
<ul>
<li><p><strong>即時驗證</strong>：Leader 會根據目前的狀態機（State Machine）檢查用戶 A 的餘額是否足夠。</p>
</li>
<li><p><strong>轉換為事件</strong>：如果餘額足夠且指令合法，Leader 才會將其轉換為「事件（Event）」並寫入 Log。</p>
</li>
<li><p><strong>失敗處理</strong>：如果前一筆轉帳已經扣光了錢，第二筆指令在「驗證」階段就會被 Leader 拒絕，不會產生事件，也不會進行後續的 Raft 同步。</p>
</li>
</ul>
<p><strong>3. 分散式事務的狀態鎖定 (Isolation)</strong></p>
<p>在 Saga 模型中，為了防止「超支」或「髒讀」，通常有兩種常見的設計模式：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>模式</strong></td><td><strong>處理方式</strong></td><td><strong>特點</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>悲觀鎖 (Pessimistic)</strong></td><td>進入 Saga 後，先將用戶 A 的餘額「凍結」（Reserved）。</td><td>確保後續一定有錢扣，但會降低帳戶的可用性。</td></tr>
<tr>
<td><strong>補償機制 (Compensating)</strong></td><td>直接執行扣款事件。若後續（如 Partition 2）失敗，再執行一個「加回餘額」的反向事件。</td><td>效能較高，是 Saga 的標準作法。</td></tr>
</tbody>
</table>
</div><p>當用戶 A 同時有兩筆轉帳請求時：</p>
<ol>
<li><p><strong>第一筆請求</strong>到達 Leader，Leader 檢查餘額夠，轉換成「扣款事件」，同步給 Follower 並完成扣款。</p>
</li>
<li><p><strong>第二筆請求</strong>在 Leader 的隊列中等待，輪到它時，Leader 發現餘額已經被第一筆扣掉了。</p>
</li>
<li><p><strong>結果</strong>：Leader 直接回傳錯誤給 Saga 協調器，該筆交易失敗，不會影響系統一致性。</p>
</li>
</ol>
<p>如果第一筆交易在 Partition 1 扣款成功，但最後在 Partition 2 失敗時，系統是如何幫用戶 A「把錢還回來」的（補償機制）?</p>
<p>簡單來說：<strong>既然錢已經扣了，我們就再發一個「存回去」的動作來抵銷。</strong></p>
<h3 id="heading-57i957wq">總結</h3>
<p>該章節描述的 Event Sourcing 方案，是從一個簡單的概念演進為一個工業級的高頻交易引擎：</p>
<ol>
<li><p>利用 <strong>事件不可變性</strong> 解決審計問題。</p>
</li>
<li><p>利用 <strong>CQRS</strong> 解決查詢問題。</p>
</li>
<li><p>利用 <strong>mmap/RocksDB</strong> (本地循序 I/O) 解決吞吐量問題 (1M TPS)。</p>
</li>
<li><p>利用 <strong>Raft</strong> 解決本地儲存的可靠性問題。</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦]]></title><description><![CDATA[在現代 AI 輔助開發中，我們不僅需要 AI 寫程式，更需要它懂規則、記性好，並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制，我們可以介入 AI 的思考與執行迴圈，實現真正的「人機協作自動化」。

一、 動機與痛點：為什麼你需要介入 AI 的生命週期？
在預設狀態下，Claude Code 雖然強大，但它是「被動」且「無狀態」的，這導致了開發者常遇到以下痛點：

記憶重置 (Session Amnesia)：

痛點：每次重啟終端機，AI 就像失憶一樣。

解法：你...]]></description><link>https://ganhua.wang/claude-code-event-driven-hooks</link><guid isPermaLink="true">https://ganhua.wang/claude-code-event-driven-hooks</guid><category><![CDATA[claude.ai]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sat, 24 Jan 2026 13:42:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769262078165/07e4e314-8709-48ed-85a5-e9f76d9d99ab.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>在現代 AI 輔助開發中，我們不僅需要 AI 寫程式，更需要它懂規則、記性好，並且能自動處理那些繁瑣的雜事。透過 <strong>Claude Code Hooks</strong> 機制，我們可以介入 AI 的思考與執行迴圈，實現真正的「人機協作自動化」。</p>
<hr />
<h2 id="heading-ai">一、 動機與痛點：為什麼你需要介入 AI 的生命週期？</h2>
<p>在預設狀態下，Claude Code 雖然強大，但它是「被動」且「無狀態」的，這導致了開發者常遇到以下痛點：</p>
<ol>
<li><p><strong>記憶重置 (Session Amnesia)</strong>：</p>
<ul>
<li><p><em>痛點</em>：每次重啟終端機，AI 就像失憶一樣。</p>
</li>
<li><p><em>解法</em>：你需要一個機制，在 <code>SessionStart</code> 時自動把「上一集的劇情（Session Log）」灌輸給它。</p>
</li>
</ul>
</li>
<li><p><strong>程式碼品質不一 (Inconsistent Quality)</strong>：</p>
<ul>
<li><p><em>痛點</em>：AI 寫出的 Go 程式碼可能忘了 <code>gofmt</code>，或者留下了 <code>fmt.Println</code> 除錯訊息。</p>
</li>
<li><p><em>解法</em>：你需要一個「糾察隊」，在 <code>PostToolUse</code>（工具用完後）自動執行格式化與檢查。</p>
</li>
</ul>
</li>
<li><p><strong>危險操作 (Safety Risks)</strong>：</p>
<ul>
<li><p><em>痛點</em>：AI 有時會過度自信，想直接 <code>git push</code> 到主分支。</p>
</li>
<li><p><em>解法</em>：你需要在 <code>PreToolUse</code>（工具執行前）設下攔截點，強制顯示警告。</p>
</li>
</ul>
</li>
<li><p><strong>上下文丟失 (Context Drift)</strong>：</p>
<ul>
<li><p><em>痛點</em>：對話太長時，重要資訊被壓縮丟棄。</p>
</li>
<li><p><em>解法</em>：利用 <code>PreCompact</code> 在壓縮發生前，將關鍵狀態寫入硬碟。</p>
</li>
</ul>
</li>
</ol>
<hr />
<h2 id="heading-the-lifecycle">二、 核心機制：生命週期圖解 (The Lifecycle)</h2>
<p>要掌握 Hooks，必須理解這張生命週期圖。這不僅是流程，更是我們可以「插入程式碼」的機會點：</p>
<p><img src="https://mintcdn.com/claude-code/z2YM37Ycg6eMbID3/images/hooks-lifecycle.png?fit=max&amp;auto=format&amp;n=z2YM37Ycg6eMbID3&amp;q=85&amp;s=5c25fedbc3db6f8882af50c3cc478c32" alt="Hook lifecycle diagram showing the sequence of hooks from SessionStart through the agentic loop to SessionEnd" /></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Hook 事件</td><td>觸發時機</td><td>應用場景</td></tr>
</thead>
<tbody>
<tr>
<td><code>SessionStart</code></td><td>Claude Code 啟動時</td><td>載入上次進度、顯示專案狀態</td></tr>
<tr>
<td><code>SessionEnd</code></td><td>會話結束時</td><td>保存工作進度、建立 session 記錄</td></tr>
<tr>
<td><code>PreToolUse</code></td><td>執行工具前</td><td>安全檢查、阻擋危險操作</td></tr>
<tr>
<td><code>PostToolUse</code></td><td>執行工具後</td><td>程式碼格式化、品質檢查</td></tr>
<tr>
<td><code>Stop</code></td><td>AI 完成回應時</td><td>檢查未提交的 debug 程式碼</td></tr>
<tr>
<td><code>PreCompact</code></td><td>Context 壓縮前</td><td>保存重要狀態、記錄壓縮事件</td></tr>
</tbody>
</table>
</div><p>我們可以將其劃分為三個戰略區域：</p>
<h3 id="heading-1-session-management">1. 啟動與結束區 (Session Management)</h3>
<ul>
<li><p><code>SessionStart</code>：這是「載入記憶」的時刻。你的腳本 <code>session-start.js</code> 在這裡執行，負責掃描 <code>.claude/sessions/</code> 下的 <code>.tmp</code> 檔案，告訴 AI 上次工作到哪裡。</p>
</li>
<li><p><code>SessionEnd</code>：這是「存檔」的時刻。<code>session-end.js</code> 會將當前的狀態快照保存下來，供下次使用。</p>
</li>
</ul>
<h3 id="heading-2-the-agentic-loop">2. 代理循環區 (The Agentic Loop) - <em>自動化的核心</em></h3>
<p>這是圖中橙色虛線框起來的部分，也是 AI 實際工作的地方。</p>
<ul>
<li><p><code>PreToolUse</code> (攔截層)：在 AI 真正執行 <code>Bash</code> 或 <code>Edit</code> 之前。這是防止錯誤的最佳時機（例如阻擋 <code>git push</code>）。</p>
</li>
<li><p><code>PostToolUse</code> (修正層)：在 AI 修改完檔案後。你的腳本可以在這裡自動執行 <code>gofmt</code>，或是檢查有沒有遺留的 <code>fmt.Print</code>。</p>
</li>
</ul>
<h3 id="heading-3-maintenance">3. 維護區 (Maintenance)</h3>
<ul>
<li><code>PreCompact</code>：當 Token 即將爆滿時，系統會觸發壓縮。利用 <code>pre-compact.js</code> 記錄這一事件，防止重要資訊在壓縮中「無聲消失」。</li>
</ul>
<hr />
<h2 id="heading-5lij44cbiomfjee9rue1koaniiihiqnuazlq">三、 配置結構與語法</h2>
<p>Hooks 配置在 <code>.claude/settings.local.json</code> 中：</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"hooks"</span>: {
    <span class="hljs-attr">"HookEvent"</span>: [
      {
        <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"條件表達式"</span>,
        <span class="hljs-attr">"hooks"</span>: [
          {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
            <span class="hljs-attr">"command"</span>: <span class="hljs-string">"要執行的指令"</span>
          }
        ]
      }
    ]
  }
}
</code></pre>
<h3 id="heading-matcher">Matcher 語法</h3>
<ul>
<li><p><code>"*"</code> - 匹配所有情況</p>
</li>
<li><p><code>tool == "Bash"</code> - 匹配特定工具</p>
</li>
<li><p><code>tool == "Edit" &amp;&amp; tool_input.file_path matches "\\.go$"</code> - 組合條件</p>
</li>
<li><p><code>!(tool_input.file_path matches "README\\.md")</code> - 排除條件</p>
</li>
</ul>
<hr />
<h2 id="heading-5zub44cbiowvpuaisomfjee9ruinoaeko8mus9ooeahoifsacrowbmus6hus7gom6vo8nw">四、 實戰配置解析：你的腳本做了什麼？</h2>
<p>結合 Go 專案範例，這套配置實現了以下具體功能：</p>
<h3 id="heading-1-sessionstart">1. 智慧型記憶掛載 (<code>SessionStart</code>)</h3>
<p>你的配置不再只是依賴單一的 <code>CLAUDE.md</code>，而是引入了時間序列的 Session Log。</p>
<ul>
<li><p><strong>行為</strong>：腳本會檢查 <code>go.mod</code> 確認這是 Go 專案，並自動尋找最近修改過的 <code>sessions/*.tmp</code> 檔案。</p>
</li>
<li><p><strong>優勢</strong>：AI 一啟動就知道專案類型（Go Module）以及上次具體的工作內容，實現「無縫熱啟動」。</p>
</li>
</ul>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"SessionStart"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node /path/to/project/.claude/scripts/hooks/session-start.js"</span>
        }
      ]
    }
  ]
}
</code></pre>
<p><strong>session-start.js：</strong></p>
<pre><code class="lang-javascript"><span class="hljs-meta">#!/usr/bin/env node</span>
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> sessionsDir = path.join(process.env.HOME, <span class="hljs-string">'.claude'</span>, <span class="hljs-string">'sessions'</span>);

  <span class="hljs-comment">// 檢查是否有最近的 session 記錄</span>
  <span class="hljs-keyword">if</span> (fs.existsSync(sessionsDir)) {
    <span class="hljs-keyword">const</span> files = fs.readdirSync(sessionsDir)
      .filter(<span class="hljs-function"><span class="hljs-params">f</span> =&gt;</span> f.endsWith(<span class="hljs-string">'.tmp'</span>))
      .sort().reverse();

    <span class="hljs-keyword">if</span> (files.length &gt; <span class="hljs-number">0</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionStart] Found <span class="hljs-subst">${files.length}</span> recent session(s)`</span>);
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionStart] Latest: <span class="hljs-subst">${files[<span class="hljs-number">0</span>]}</span>`</span>);
    }
  }

  <span class="hljs-comment">// Go 專案檢測</span>
  <span class="hljs-keyword">if</span> (fs.existsSync(<span class="hljs-string">'go.mod'</span>)) {
    <span class="hljs-keyword">const</span> content = fs.readFileSync(<span class="hljs-string">'go.mod'</span>, <span class="hljs-string">'utf8'</span>);
    <span class="hljs-keyword">const</span> match = content.match(<span class="hljs-regexp">/^module\s+(.+)$/m</span>);
    <span class="hljs-keyword">if</span> (match) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionStart] Go project: <span class="hljs-subst">${match[<span class="hljs-number">1</span>]}</span>`</span>);
    }
  }

  process.exit(<span class="hljs-number">0</span>);
}

main();
</code></pre>
<h3 id="heading-2-posttooluse">2. 強制性程式碼規範 (<code>PostToolUse</code>)</h3>
<p>這是這套配置最精彩的部分—— <strong>自動修正 (Auto-Correction)</strong>。</p>
<ul>
<li><p><strong>行為</strong>：當監測到 <code>Edit</code> 工具修改了 <code>.go</code> 檔案後，Hooks 會自動觸發：</p>
<pre><code class="lang-bash">  gofmt -w <span class="hljs-string">"file_path"</span>
</code></pre>
<p>  同時，若發現檔案內含有 <code>fmt.Print</code>，會透過 <code>console.error</code> 警告開發者。</p>
</li>
<li><p><strong>優勢</strong>：即使 AI 生成的程式碼格式混亂，寫入硬碟的那一刻也會被強制修正為標準格式。這大幅減少了 code review 的負擔。</p>
</li>
</ul>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"PostToolUse"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Edit\" &amp;&amp; tool_input.file_path matches \"\\\\.go$\""</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"const{execSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=&gt;d+=c);process.stdin.on('end',()=&gt;{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&amp;&amp;fs.existsSync(p)){try{execSync('gofmt -w \\\"'+p+'\\\"',{stdio:['pipe','pipe','pipe']})}catch(e){console.error('[Hook] gofmt failed: '+e.message)}}console.log(d)})\""</span>
        }
      ]
    },
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Edit\" &amp;&amp; tool_input.file_path matches \"\\\\.go$\""</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=&gt;d+=c);process.stdin.on('end',()=&gt;{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&amp;&amp;fs.existsSync(p)){const c=fs.readFileSync(p,'utf8');if(/fmt\\\\.Print(ln|f)?\\\\(/.test(c)){console.error('[Hook] WARNING: fmt.Print found in '+p);console.error('[Hook] Consider using proper logging instead')}}console.log(d)})\""</span>
        }
      ]
    },
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Bash\""</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"let d='';process.stdin.on('data',c=&gt;d+=c);process.stdin.on('end',()=&gt;{const i=JSON.parse(d);const cmd=i.tool_input?.command||'';if(/gh pr create/.test(cmd)){const out=i.tool_output?.output||'';const m=out.match(/https:\\\\/\\\\/github.com\\\\/[^/]+\\\\/[^/]+\\\\/pull\\\\/\\\\d+/);if(m){console.error('[Hook] PR created: '+m[0])}}console.log(d)})\""</span>
        }
      ]
    }
  ]
}
</code></pre>
<h3 id="heading-3-pretooluse">3. 危險操作防護網 (<code>PreToolUse</code>)</h3>
<ul>
<li><p><strong>行為</strong>：當 AI 試圖執行 <code>git push</code> 時，Hooks 會攔截並輸出：</p>
<blockquote>
<p><code>[Hook] Review changes before push... Consider: git diff HEAD~1</code></p>
</blockquote>
</li>
<li><p><strong>優勢</strong>：增加了一道「冷靜期」，防止 AI 在未經人工確認的情況下將錯誤程式碼推送到遠端倉庫。</p>
</li>
</ul>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"PreToolUse"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Bash\" &amp;&amp; tool_input.command matches \"git push\""</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"console.error('[Hook] Review changes before push...');console.error('[Hook] Consider: git diff HEAD~1, git log --oneline -5')\""</span>
        }
      ]
    },
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"tool == \"Write\" &amp;&amp; tool_input.file_path matches \"\\\\.(md|txt)$\" &amp;&amp; !(tool_input.file_path matches \"README\\\\.md|CLAUDE\\\\.md\")"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"console.error('[Hook] WARNING: Creating documentation file');console.error('[Hook] Consider using CONTEXT.md for session notes')\""</span>
        }
      ]
    }
  ]
}
</code></pre>
<h3 id="heading-4-stop">4. 提交前最後檢查 (<code>Stop</code>)</h3>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"Stop"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node -e \"const{execSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=&gt;d+=c);process.stdin.on('end',()=&gt;{try{execSync('git rev-parse --git-dir',{stdio:'pipe'})}catch{console.log(d);process.exit(0)}try{const files=execSync('git diff --name-only HEAD',{encoding:'utf8',stdio:['pipe','pipe','pipe']}).split('\\\\n').filter(f=&gt;/\\\\.go$/.test(f)&amp;&amp;fs.existsSync(f));let hasDebug=false;for(const f of files){const content=fs.readFileSync(f,'utf8');if(/fmt\\\\.Print(ln|f)?\\\\(/.test(content)){console.error('[Hook] WARNING: fmt.Print found in '+f);hasDebug=true}}if(hasDebug)console.error('[Hook] Remove debug prints before committing')}catch(e){}console.log(d)})\""</span>
        }
      ]
    }
  ]
}
</code></pre>
<h3 id="heading-5-session-sessionend">5. Session 結束存檔 (<code>SessionEnd</code>)</h3>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"SessionEnd"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node /path/to/project/.claude/scripts/hooks/session-end.js"</span>
        }
      ]
    }
  ]
}
</code></pre>
<p><strong>session-end.js：</strong></p>
<pre><code class="lang-javascript"><span class="hljs-meta">#!/usr/bin/env node</span>
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> sessionsDir = path.join(process.env.HOME, <span class="hljs-string">'.claude'</span>, <span class="hljs-string">'sessions'</span>);
  <span class="hljs-keyword">const</span> today = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString().split(<span class="hljs-string">'T'</span>)[<span class="hljs-number">0</span>];
  <span class="hljs-keyword">const</span> sessionFile = path.join(sessionsDir, <span class="hljs-string">`<span class="hljs-subst">${today}</span>-session.tmp`</span>);

  <span class="hljs-keyword">if</span> (!fs.existsSync(sessionsDir)) {
    fs.mkdirSync(sessionsDir, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
  }

  <span class="hljs-keyword">const</span> time = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toTimeString().slice(<span class="hljs-number">0</span>, <span class="hljs-number">5</span>);

  <span class="hljs-keyword">if</span> (fs.existsSync(sessionFile)) {
    <span class="hljs-keyword">let</span> content = fs.readFileSync(sessionFile, <span class="hljs-string">'utf8'</span>);
    content = content.replace(<span class="hljs-regexp">/\*\*Last Updated:\*\*.*/</span>, <span class="hljs-string">`**Last Updated:** <span class="hljs-subst">${time}</span>`</span>);
    fs.writeFileSync(sessionFile, content);
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionEnd] Updated session: <span class="hljs-subst">${today}</span>-session.tmp`</span>);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">const</span> template = <span class="hljs-string">`# Session: <span class="hljs-subst">${today}</span>
**Started:** <span class="hljs-subst">${time}</span>
**Last Updated:** <span class="hljs-subst">${time}</span>

## Completed
- [ ]

## In Progress
- [ ]

## Notes for Next Session
-
`</span>;
    fs.writeFileSync(sessionFile, template);
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`[SessionEnd] Created session: <span class="hljs-subst">${today}</span>-session.tmp`</span>);
  }

  process.exit(<span class="hljs-number">0</span>);
}

main();
</code></pre>
<h3 id="heading-6-precompact">6. 壓縮前狀態保存 (<code>PreCompact</code>)</h3>
<p><strong>配置範例：</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"PreCompact"</span>: [
    {
      <span class="hljs-attr">"matcher"</span>: <span class="hljs-string">"*"</span>,
      <span class="hljs-attr">"hooks"</span>: [
        {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"command"</span>,
          <span class="hljs-attr">"command"</span>: <span class="hljs-string">"node /path/to/project/.claude/scripts/hooks/pre-compact.js"</span>
        }
      ]
    }
  ]
}
</code></pre>
<p><strong>pre-compact.js：</strong></p>
<pre><code class="lang-javascript"><span class="hljs-meta">#!/usr/bin/env node</span>
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> sessionsDir = path.join(process.env.HOME, <span class="hljs-string">'.claude'</span>, <span class="hljs-string">'sessions'</span>);
  <span class="hljs-keyword">const</span> logFile = path.join(sessionsDir, <span class="hljs-string">'compaction-log.txt'</span>);

  <span class="hljs-keyword">if</span> (!fs.existsSync(sessionsDir)) {
    fs.mkdirSync(sessionsDir, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
  }

  <span class="hljs-keyword">const</span> timestamp = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString().replace(<span class="hljs-string">'T'</span>, <span class="hljs-string">' '</span>).slice(<span class="hljs-number">0</span>, <span class="hljs-number">19</span>);
  fs.appendFileSync(logFile, <span class="hljs-string">`[<span class="hljs-subst">${timestamp}</span>] Context compaction triggered\n`</span>);

  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'[PreCompact] State preserved before compaction'</span>);
  process.exit(<span class="hljs-number">0</span>);
}

main();
</code></pre>
<hr />
<h2 id="heading-5lqu44cbioebrummhoe1koaniw">五、 目錄結構</h2>
<p>建議的專案 hooks 結構：</p>
<pre><code class="lang-sql">.claude/
├── settings.local.json    <span class="hljs-comment"># Hooks 配置</span>
├── scripts/
│   ├── hooks/
│   │   ├── session-start.js
│   │   ├── session-end.js
│   │   └── pre-compact.js
│   └── lib/
│       └── utils.js       <span class="hljs-comment"># 共用工具函式</span>
└── skills/                <span class="hljs-comment"># Claude Code skills</span>
</code></pre>
<hr />
<h2 id="heading-claude-code">六、 為什麼 Claude Code 仰賴這些設定？</h2>
<p>Claude Code 本質上是一個 <strong>「事件驅動的執行環境 (Event-Driven Execution Environment)」</strong>。</p>
<ol>
<li><p><strong>彌補 LLM 的缺陷</strong>：LLM 擅長生成，但不擅長「守紀律」和「記狀態」。Hooks 透過確定性的程式碼（Node.js/Shell）來彌補機率性的 AI 模型。</p>
</li>
<li><p><strong>標準輸入/輸出的管道設計</strong>： 注意到腳本中使用了 <code>process.stdin</code> 和 <code>process.stdout</code> 嗎？</p>
<pre><code class="lang-javascript"> process.stdin.on(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-params">c</span> =&gt;</span> d += c); <span class="hljs-comment">// 接收 Claude 的數據</span>
 <span class="hljs-built_in">console</span>.log(d); <span class="hljs-comment">// 必須把數據傳下去，否則流程會斷掉</span>
</code></pre>
<p> 這設計讓 Hooks 成為類似 Linux Pipe 的過濾器，可以在不打斷 AI 思路的前提下，對資料進行「偷看」、「修改」或「阻擋」。</p>
</li>
</ol>
<hr />
<h2 id="heading-5lid44cbioazqoaejs6imghq">七、 注意事項</h2>
<ol>
<li><p><strong>Hook 必須輸出 stdin 資料</strong>：對於需要處理輸入的 hooks，必須在最後輸出 <code>console.log(d)</code> 將原始資料傳遞下去</p>
</li>
<li><p><strong>使用 stderr 顯示訊息</strong>：<code>console.error()</code> 用於顯示給使用者的訊息，<code>console.log()</code> 用於傳遞資料</p>
</li>
<li><p><strong>避免阻塞</strong>：Hook 腳本應快速執行，避免耗時操作</p>
</li>
<li><p><strong>錯誤處理</strong>：即使發生錯誤也應 <code>process.exit(0)</code>，避免阻斷 Claude Code 流程</p>
</li>
<li><p><strong>路徑處理</strong>：使用絕對路徑或相對於專案根目錄的路徑</p>
</li>
</ol>
<hr />
<h2 id="heading-5ywr44cbiomfjee9ruwjow4tushueahoihjoecuuiuiumdqq">八、 配置後帶來的行為變革</h2>
<p>一旦部署這套 <code>.claude/settings.local.json</code>，你的開發體驗將發生如下質變：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>開發情境</strong></td><td><strong>觸發設定 (Hook)</strong></td><td><strong>系統自動化行為</strong></td><td><strong>開發者獲得的好處</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>開啟專案</strong></td><td><code>SessionStart</code></td><td>自動讀取 <code>sessions/2026-01-24.tmp</code> 並分析 <code>go.mod</code>。</td><td><strong>秒進狀態</strong>：不用再打字解釋「這是 Go 專案，上次做到哪」。</td></tr>
<tr>
<td><strong>AI 寫完 Code</strong></td><td><code>PostToolUse</code></td><td>背景靜默執行 <code>gofmt</code>。</td><td><strong>格式完美</strong>：檔案永遠符合 Go Standard，不會有縮排錯誤。</td></tr>
<tr>
<td><strong>AI 忘記刪 Log</strong></td><td><code>Stop</code>/<code>PostToolUse</code></td><td>掃描並紅字警告：<code>WARNING: fmt.Print found</code>。</td><td><strong>保持潔淨</strong>：防止 Debug 代碼污染生產環境。</td></tr>
<tr>
<td><strong>準備提交 PR</strong></td><td><code>PreToolUse</code></td><td>攔截 <code>git push</code> 並建議先 <code>git diff</code>。</td><td><strong>安全防護</strong>：避免意外將實驗性代碼推上線。</td></tr>
<tr>
<td><strong>關閉終端</strong></td><td><code>SessionEnd</code></td><td>將當前進度寫入 <code>*-session.tmp</code>。</td><td><strong>進度固化</strong>：確保今天的上下文能準確傳遞給明天的自己。</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-57i957wq">總結</h2>
<p>這套配置將 <strong>記憶持久化概念</strong> 進一步細化為針對 <strong>Go 語言特性的自動化工作流</strong>。它不僅解決了「失憶」問題，更透過 <code>gofmt</code> 和安全檢查，讓 AI 成為了一個「守紀律」的初級工程師，而不僅僅是一個聊天機器人。</p>
<hr />
<h2 id="heading-5yd6icd6loh5rqq">參考資源</h2>
<ul>
<li><a target="_blank" href="https://code.claude.com/docs/en/hooks">Claude Code Hooks 官方文件</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[OTel 戰地筆記：破解 Delta to Cumulative 的兩大夢魘 (ErrOutOfOrder & ErrOlderStart)]]></title><description><![CDATA[[OTel 深水區] 徹底拆解 deltatocumulative 的兩大夢魘：ErrOlderStart 與 ErrOutOfOrder
接續上一篇我們對 deltatocumulative Processor 的架構剖析，今天我們要進入「深水區」。
在生產環境中，你可能遇過這種情況：Metrics 莫名其妙開始 Drop，去查 deltatocumulative_datapoints 指標時，看到 error 標籤出現了 delta.ErrOutOfOrder 或 delta.ErrOlde...]]></description><link>https://ganhua.wang/otel-delta-to-cumulative-erroutoforder-and-errolderstart</link><guid isPermaLink="true">https://ganhua.wang/otel-delta-to-cumulative-erroutoforder-and-errolderstart</guid><category><![CDATA[opentelemetry collector]]></category><category><![CDATA[observability]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Thu, 22 Jan 2026 15:07:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769094697034/3121ef65-d3bd-4cb4-a6c5-34aadb7e9694.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-otel-deltatocumulative-errolderstart-erroutoforder">[OTel 深水區] 徹底拆解 deltatocumulative 的兩大夢魘：ErrOlderStart 與 ErrOutOfOrder</h1>
<p>接續<a target="_blank" href="https://ganhua.wang/otel-collector-delta-to-cumulative-processor">上一篇我們對 <code>deltatocumulative</code> Processor 的架構剖析</a>，今天我們要進入「深水區」。</p>
<p>在生產環境中，你可能遇過這種情況：Metrics 莫名其妙開始 Drop，去查 <code>deltatocumulative_datapoints</code> 指標時，看到 <code>error</code> 標籤出現了 <code>delta.ErrOutOfOrder</code> 或 <code>delta.ErrOlderStart</code>。</p>
<p>這不僅僅是報錯，這是 Processor 在告訴你：<strong>你的數據流違反了物理定律</strong>。本文將從 Stream Identity (身份識別) 的源碼層級出發，徹底拆解這兩個錯誤的根源。</p>
<hr />
<h2 id="heading-1-stream-identity">1. 核心根源：Stream Identity (身份識別)</h2>
<p>要讀懂錯誤，首先得搞懂 Processor 怎麼判定「這是同一條 Stream」。許多人誤以為只要 Metric Name 一樣就是同一條線，這是大錯特錯。</p>
<p>在源碼中，<strong>Stream Identity</strong> 是一個 Hash 值，其組成結構非常嚴謹：</p>
<pre><code class="lang-text">Stream Identity = Hash of:
┌─────────────────────────────────────────────────────────────────────┐
│  Metric                                                             │
│  ├── Scope                                                          │
│  │   ├── Resource                                                   │
│  │   │   └── resource.attributes (hash)                             │
│  │   │       (關鍵!: service.name, host.name, k8s.pod.name)          │
│  │   ├── scope.name       (例: "otelcol/prometheus")                 │
│  │   ├── scope.version    (例: "v0.90.0")                           │
│  │   └── scope.attributes (hash)                                    │
│  ├── metric.name          (例: "http_requests_total")               │
│  ├── metric.unit          (例: "ms")                                │
│  ├── metric.type          (例: Sum, Histogram)                      │
│  ├── metric.monotonic     (true/false)                              │
│  └── metric.temporality   (Delta/Cumulative)                        │
└─────────────────────────────────────────────────────────────────────┘
      +
datapoint.attributes (hash)   (例: method="GET", status="200")
</code></pre>
<h3 id="heading-5am6amx5a6k55m85477ya6lqr5lu95yik5a6a55qe6bud6yer5rov5ymh">實驗室發現：身份判定的黃金法則</h3>
<p>我們在單元測試 (<code>TestStreamIdentity</code>) 中驗證了以下關鍵行為，這直接決定了你會不會踩坑：</p>
<ol>
<li><strong>相同 Attributes = 相同 Stream</strong>：
即使 Timestamp 或 Value 不同，只要上述方框內的屬性完全一樣，它們就是同一條 Stream。這就是為什麼多個 Pod 如果沒有帶 <code>pod.name</code> 會導致 Collision（衝突）。</li>
<li><strong>Resource 不同 = 不同 Stream</strong>：
來自 <code>host-1</code> 和 <code>host-2</code> 的數據，即使 Metric Name 一樣，也是兩條獨立的平行線，互不干擾。</li>
<li><strong>Timestamp 與 Value 不影響 Identity</strong>：</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td>欄位</td><td>是否影響 Identity</td></tr>
</thead>
<tbody>
<tr>
<td><code>datapoint.timestamp</code></td><td>❌ <strong>不影響</strong></td></tr>
<tr>
<td><code>datapoint.start_timestamp</code></td><td>❌ <strong>不影響</strong></td></tr>
<tr>
<td><code>datapoint.value</code></td><td>❌ <strong>不影響</strong></td></tr>
<tr>
<td><code>datapoint.attributes</code></td><td>✅ <strong>影響</strong></td></tr>
</tbody>
</table>
</div><p>這解釋了為什麼同一條 Stream 的多個 Datapoints 會被聚合——因為它們共享同一個 Identity。</p>
<hr />
<h2 id="heading-2-processor">2. 決策樹：Processor 是怎麼思考的？</h2>
<p>Processor 在處理數據時是有優先順序的：<strong>先查身世 (Start)，再查時序 (Time)</strong>。</p>
<pre><code class="lang-mermaid">graph TD
    A[數據進站 DataPoint] --&gt; B{檢查 1: 出生證明 &lt;br&gt;StartTimestamp}
    B -- New &lt; Stored --&gt; C[💥 ErrOlderStart &lt;br&gt; 丟棄 Drop]
    B -- New &gt;= Stored --&gt; D{檢查 2: 結束時間 &lt;br&gt; Timestamp}
    D -- New &lt;= Stored --&gt; E[💥 ErrOutOfOrder &lt;br&gt; 丟棄 Drop]
    D -- New &gt; Stored --&gt; F[✅ 更新狀態 Update State]

    C -.-&gt; G[懷疑有多個 Process 撞名]
    E -.-&gt; H[單純的網路亂序或重送]
</code></pre>
<p>理解了這個流程，我們就能針對兩個錯誤進行深度解析。</p>
<hr />
<h2 id="heading-3-errolderstart">3. 第一道防線：ErrOlderStart (身份錯亂)</h2>
<p>這是最嚴重、也最常被誤判的錯誤。</p>
<ul>
<li><strong>錯誤代碼</strong>：<code>ErrOlderStart</code></li>
<li><strong>直白翻譯</strong>：「你明明比我年輕，為什麼出生日期比我還老？」</li>
<li><strong>觸發條件</strong>：<code>New.StartTimestamp &lt; Stored.StartTimestamp</code></li>
</ul>
<h3 id="heading-the-highlander-rule">為什麼會發生？ (The "Highlander" Rule)</h3>
<p>這通常不是時間穿越，而是<strong>「身份撞車 (Identity Collision)」</strong>。Processor 預設一個 Stream（Identity）在同一時間只能有一個來源。</p>
<p>試想這個場景：你有兩個 Pod (A 和 B) 都在跑同一個 Service，但你忘記在 Metric Label 裡加上 <code>pod_name</code>。</p>
<ol>
<li><strong>Pod A (較新)</strong>：<code>StartTimestamp = 1000</code></li>
<li><strong>Pod B (較舊)</strong>：<code>StartTimestamp = 500</code></li>
</ol>
<p>Processor 的視角：</p>
<ol>
<li>收到 Pod A 數據 → 記住：「這個 Stream 是 1000 出生的」。</li>
<li>收到 Pod B 數據（500 出生）。</li>
<li>檢查：<code>500 &lt; 1000</code>？<strong>報錯 <code>ErrOlderStart</code> 並丟棄</strong>。</li>
</ol>
<blockquote>
<p><strong>診斷結論</strong>：
只要看到這個錯誤，<strong>90% 都是 Label 設定問題</strong>。你的數據來源不單純（Multiple Processes），有多個實例在搶同一個身份。</p>
</blockquote>
<hr />
<h2 id="heading-4-erroutoforder">4. 第二道防線：ErrOutOfOrder (時光倒流)</h2>
<p>如果通過了第一關，數據會來到第二關。</p>
<ul>
<li><strong>錯誤代碼</strong>：<code>ErrOutOfOrder</code></li>
<li><strong>直白翻譯</strong>：「現在已經 12 點了，你怎麼還在送 11 點的數據？」</li>
<li><strong>觸發條件</strong>：<code>New.Timestamp &lt;= Stored.Timestamp</code></li>
</ul>
<h3 id="heading-54k65lua6bq85pyd55m855sf77yf">為什麼會發生？</h3>
<p>這通常是網路或傳輸層的問題，而非配置問題：</p>
<ol>
<li><strong>網路亂序</strong>：數據包迷路了，晚送出的反而早到。</li>
<li><strong>重試風暴 (Retry)</strong>：Prometheus Remote Write 因為發送失敗而重送舊數據。</li>
<li><strong>時鐘不同步</strong>：不同機器上的時間沒對準。</li>
</ol>
<hr />
<h2 id="heading-5">5. 深度情境分析：那些詭異的組合</h2>
<p>這部分是高手進階的關鍵，我們來看兩種特殊組合。</p>
<h3 id="heading-a-errolderstart-erroutoforder">情境 A：有 ErrOlderStart 但「沒有」ErrOutOfOrder</h3>
<p><strong>這完全可能發生，而且是診斷「多實例衝突」的鐵證。</strong></p>
<p>假設有兩個進程 A (新, Start=1000) 和 B (舊, Start=500) 交錯發送：</p>
<ol>
<li>收到 A (Start=1000, Time=1100) → <strong>OK</strong></li>
<li>收到 B (Start=500, Time=600) → <strong>Fail</strong> (因為 500 &lt; 1000，第一關直接擋下)</li>
</ol>
<p><strong>結論</strong>：B 的數據在第一關就被擋下來了，根本沒機會觸發第二關的 <code>ErrOutOfOrder</code>。如果你只看到 <code>ErrOlderStart</code>，請直接檢查你的 Label 設定。</p>
<h3 id="heading-b-erroutoforder-errolderstart">情境 B：有 ErrOutOfOrder 但「沒有」ErrOlderStart</h3>
<p><strong>這反而是好消息：這代表你的 Stream Identity 是乾淨的。</strong></p>
<p>這表示沒有多個 Pod 在互搶身份，純粹是單一來源的數據傳輸有點顛簸（網路延遲、Batch Flush 順序或重送）。</p>
<p><strong>結論</strong>：</p>
<ul>
<li>資料來自同一個 <code>series</code>。</li>
<li>StartTimestamp 完全一致（不可能觸發 <code>ErrOlderStart</code>）。</li>
<li>問題僅在於傳輸順序。</li>
</ul>
<hr />
<h2 id="heading-6-sop">6. 實戰除錯 SOP</h2>
<p>當你在 Grafana 看到 <code>deltatocumulative_datapoints</code> 的 drop rate 飆高時，請依照以下步驟排查：</p>
<h3 id="heading-step-1">Step 1: 區分敵我</h3>
<p>用這句 PromQL 撈撈看錯誤分佈：</p>
<pre><code class="lang-sql">sum by (error) (rate(otelcol_deltatocumulative_datapoints{error!=""}[5m]))
</code></pre>
<h3 id="heading-step-2">Step 2: 對症下藥</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>錯誤類型</td><td>嫌疑犯 (Root Cause)</td><td>處置行動 (Action Item)</td></tr>
</thead>
<tbody>
<tr>
<td><strong>ErrOlderStart</strong></td><td><strong>配置架構問題</strong><br />多個實例 (Pod/Process) 共享了相同的 Identity。</td><td><strong>檢查 Resource/Attributes</strong><br /><br />查看 <code>resource_detection</code> 或 <code>k8s_attributes</code> processor。確保每個 Metric 都帶有能區分實例的標籤 (如 <code>k8s.pod.name</code>)。</td></tr>
<tr>
<td><strong>ErrOutOfOrder</strong></td><td><strong>環境傳輸問題</strong><br /><br />網路抖動、Client 重試。</td><td><strong>通常無需過度介入</strong><br /><br />如果是持續發生，檢查發送端的 batch 設定或 NTP 時間同步。</td></tr>
<tr>
<td><strong>Limit</strong></td><td><strong>資源限制</strong><br /><br />Stream 總數爆了。</td><td><strong>調整配置</strong><br /><br />調大 <code>max_streams</code> 上限（參考上一篇文章）。</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-7">7. 附錄：測試驗證結構</h2>
<p>為了確保上述理論正確，我們參考了以下的測試案例結構，<a target="_blank" href="https://gist.github.com/tedmax100/0a4a03fc3269b452f11344787c644491">測試程式碼</a>：</p>
<h3 id="heading-part-1-stream-identity">Part 1: Stream Identity 測試</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>測試案例</td><td>驗證內容</td></tr>
</thead>
<tbody>
<tr>
<td><code>TestStreamIdentity_SameAttributes_SameStream</code></td><td>相同 attrs = 相同 stream</td></tr>
<tr>
<td><code>TestStreamIdentity_DifferentAttributes_DifferentStream</code></td><td>不同 attrs = 不同 stream</td></tr>
<tr>
<td><code>TestStreamIdentity_Collision_MultiplePodsWithoutInstanceLabel</code></td><td>展示 identity collision</td></tr>
<tr>
<td><code>TestStreamIdentity_Components</code></td><td>各組成部分對 identity 的影響</td></tr>
<tr>
<td><code>TestStreamIdentity_NotAffectedBy</code></td><td>timestamp/value 不影響 identity</td></tr>
</tbody>
</table>
</div><h3 id="heading-part-2-delta-aggregate">Part 2: Delta Aggregate 錯誤測試</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>測試案例</td><td>驗證內容</td></tr>
</thead>
<tbody>
<tr>
<td><code>TestDeltaAggregate_FirstSample</code></td><td>第一筆數據直接存入 state</td></tr>
<tr>
<td><code>TestDeltaAggregate_NormalSequence</code></td><td>正常數據序列不報錯</td></tr>
<tr>
<td><code>TestDeltaAggregate_ErrOutOfOrder</code></td><td>驗證 ErrOutOfOrder 觸發條件</td></tr>
<tr>
<td><code>TestDeltaAggregate_ErrOlderStart</code></td><td>驗證 ErrOlderStart 觸發條件</td></tr>
<tr>
<td><code>TestDeltaAggregate_ErrorPriority</code></td><td>ErrOlderStart 優先於 ErrOutOfOrder</td></tr>
<tr>
<td><code>TestDeltaAggregate_RealWorldScenario_MultiplePodsCollision</code></td><td>模擬多 Pod collision</td></tr>
<tr>
<td><code>TestDeltaAggregate_RealWorldScenario_NetworkDelay</code></td><td>模擬網路延遲亂序</td></tr>
</tbody>
</table>
</div><h3 id="heading-6zec6y215ris6kmm5pel6kqm6ly45ye656e5l6l">關鍵測試日誌輸出範例</h3>
<p><strong>ErrOlderStart 場景:</strong></p>
<pre><code class="lang-text">Pod B rejected: dropped sample with start_time=...0000005..., 
because series only starts at start_time=...000001... 
consider checking for multiple processes sending the exact same series
</code></pre>
<p><strong>ErrOutOfOrder 場景:</strong></p>
<pre><code class="lang-text">Delayed data rejected: out of order: dropped sample from time=...0000011..., 
because series is already at time=...0000012...
</code></pre>
<hr />
<h3 id="heading-57i957wq">總結</h3>
<p><code>ErrOlderStart</code> 和 <code>ErrOutOfOrder</code> 雖然都會導致丟包，但意義截然不同。
<strong>ErrOlderStart 是你的設定錯了（撞名），ErrOutOfOrder 是網路環境亂了。</strong></p>
<p>下次看到數據掉點，別急著重啟 Collector，先看看 <code>error</code> label 告訴你什麼故事。</p>
<hr />
<p>為解決 <code>deltatocumulative</code> Processor 中 <code>ErrOlderStart</code> 錯誤而設計的 <code>k8s_attributes</code> 實踐配置。</p>
<h3 id="heading-uniqueness">核心策略：唯一性 (Uniqueness)</h3>
<p>要根除 <code>ErrOlderStart</code>，我們必須確保來自不同 Pod 的數據流擁有不同的 <strong>Stream Identity</strong>。根據上一篇的分析，Resource Attributes 是 Identity 的重要組成部分。</p>
<p>因此，我們的目標是：<strong>強勢注入 <code>k8s.pod.name</code> 和 <code>k8s.pod.uid</code> 到每一筆數據的 Resource 中。</strong></p>
<hr />
<h2 id="heading-1-collector-configyaml">1. Collector 配置範例 (config.yaml)</h2>
<p>這份配置適用於以 <strong>DaemonSet</strong> 方式運行的 OTEL Collector Agent。這是最推薦的部署模式，因為 Agent 可以直接透過來源 IP 關聯到 Pod。</p>
<pre><code class="lang-yaml"><span class="hljs-attr">receivers:</span>
  <span class="hljs-attr">otlp:</span>
    <span class="hljs-attr">protocols:</span>
      <span class="hljs-attr">grpc:</span>
      <span class="hljs-attr">http:</span>

<span class="hljs-attr">processors:</span>
  <span class="hljs-comment"># 1. Kubernetes 屬性增強 (關鍵步驟)</span>
  <span class="hljs-attr">k8sattributes:</span>
    <span class="hljs-attr">auth_type:</span> <span class="hljs-string">"serviceAccount"</span>
    <span class="hljs-attr">passthrough:</span> <span class="hljs-literal">false</span>
    <span class="hljs-attr">filter:</span>
      <span class="hljs-attr">node_from_env_var:</span> <span class="hljs-string">K8S_NODE_NAME</span> <span class="hljs-comment"># 只處理本節點的 Pod，提升效能</span>

    <span class="hljs-comment"># 這裡定義要抓取哪些 K8s Metadata 塞進 Resource Attribute</span>
    <span class="hljs-attr">extract:</span>
      <span class="hljs-attr">metadata:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">k8s.pod.name</span>       <span class="hljs-comment"># &lt;--- 絕對必要：區分不同 Pod</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">k8s.pod.uid</span>        <span class="hljs-comment"># &lt;--- 強烈建議：區分同名 Pod 重啟前後</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">k8s.deployment.name</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">k8s.namespace.name</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">k8s.node.name</span>

    <span class="hljs-comment"># 定義如何透過網路連線找到對應的 Pod</span>
    <span class="hljs-attr">pod_association:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">sources:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">from:</span> <span class="hljs-string">connection</span> <span class="hljs-comment"># 透過來源 IP 自動關聯</span>

  <span class="hljs-comment"># 2. 記憶體限制 (標準配置)</span>
  <span class="hljs-attr">memory_limiter:</span>
    <span class="hljs-attr">check_interval:</span> <span class="hljs-string">1s</span>
    <span class="hljs-attr">limit_percentage:</span> <span class="hljs-number">75</span>
    <span class="hljs-attr">spike_limit_percentage:</span> <span class="hljs-number">15</span>

  <span class="hljs-comment"># 3. 轉化為 Cumulative (我們的主角)</span>
  <span class="hljs-attr">deltatocumulative:</span>
    <span class="hljs-attr">max_streams:</span> <span class="hljs-number">100000</span> <span class="hljs-comment"># 記得根據上一篇建議調整</span>
    <span class="hljs-attr">max_stale:</span> <span class="hljs-string">5m</span>

<span class="hljs-attr">exporters:</span>
  <span class="hljs-attr">prometheus:</span>
    <span class="hljs-attr">endpoint:</span> <span class="hljs-string">"0.0.0.0:8889"</span>

<span class="hljs-attr">service:</span>
  <span class="hljs-attr">pipelines:</span>
    <span class="hljs-attr">metrics:</span>
      <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>]
      <span class="hljs-comment"># 注意順序：先加標籤 (k8sattributes)，再做聚合 (deltatocumulative)</span>
      <span class="hljs-attr">processors:</span> [<span class="hljs-string">memory_limiter</span>, <span class="hljs-string">k8sattributes</span>, <span class="hljs-string">deltatocumulative</span>] 
      <span class="hljs-attr">exporters:</span> [<span class="hljs-string">prometheus</span>]
</code></pre>
<hr />
<h2 id="heading-2-errolderstart">2. 為什麼這樣配能解決 ErrOlderStart？</h2>
<p>讓我們回到源碼層級的 Hash 公式：</p>
<p><strong>修正前 (Collision):</strong></p>
<pre><code class="lang-text">Stream ID = Hash(
  Resource: { service.name="payment" },  &lt;-- 所有 Pod 都一樣
  Metric:   { name="http_requests" }
)
==&gt; 結果：Pod A 和 Pod B 產生相同的 Hash，Processor 以為它們是同一個 Stream，導致時間戳衝突。
</code></pre>
<p><strong>修正後 (Unique):</strong></p>
<pre><code class="lang-text">Stream ID (Pod A) = Hash(
  Resource: { service.name="payment", k8s.pod.name="payment-zx9q", k8s.pod.uid="uuid-1" },
  Metric:   { name="http_requests" }
)

Stream ID (Pod B) = Hash(
  Resource: { service.name="payment", k8s.pod.name="payment-ab12", k8s.pod.uid="uuid-2" },
  Metric:   { name="http_requests" }
)
==&gt; 結果：Hash 不同，Processor 視為兩條平行線，ErrOlderStart 消失。
</code></pre>
<h3 id="heading-k8spoduid">特別說明：為什麼需要 <code>k8s.pod.uid</code>？</h3>
<p>雖然 <code>k8s.pod.name</code> 解決了不同 Pod 的衝突，但如果同一個 Pod 被殺掉重建（Recreated），名稱可能不變（例如 StatefulSet 或某些 Deployment 策略）。加上 <code>uid</code> 可以確保即使是「同名的轉世」也會被視為全新的 Stream，徹底杜絕舊數據干擾。</p>
<hr />
<p>如果你不想賦予 Collector 複雜的 K8s RBAC 權限（讀取 Pods/Nodes），或者你的 Collector 是以 Sidecar 模式運作，<strong>「K8s Downward API + Resource Processor」</strong> 是完全可以取代 <code>k8s_attributes</code> 的最佳替代方案。</p>
<p>這個組合拳的邏輯是：<strong>「與其讓 Collector 去問 API Server 我是誰，不如我在啟動時直接把身分證塞進它的口袋。」</strong></p>
<p>以下是完整的配置範例：</p>
<hr />
<h3 id="heading-1-k8s-yaml">1. K8s YAML 修改 (注入身份)</h3>
<p>首先，我們需要利用 <a target="_blank" href="https://kubernetes.io/docs/concepts/workloads/pods/downward-api/">K8s 的 Downward API</a>，將 Pod 的 metadata 變成環境變數 (Environment Variables) 注入到 Collector 的容器中。</p>
<p>修改你的 Deployment 或 DaemonSet：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">my-app-sidecar</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">otel-collector</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">otel/opentelemetry-collector-contrib:latest</span>
          <span class="hljs-attr">env:</span>
            <span class="hljs-comment"># --- 關鍵：利用 Downward API 注入變數 ---</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">K8S_POD_NAME</span>
              <span class="hljs-attr">valueFrom:</span>
                <span class="hljs-attr">fieldRef:</span>
                  <span class="hljs-attr">fieldPath:</span> <span class="hljs-string">metadata.name</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">K8S_POD_UID</span>
              <span class="hljs-attr">valueFrom:</span>
                <span class="hljs-attr">fieldRef:</span>
                  <span class="hljs-attr">fieldPath:</span> <span class="hljs-string">metadata.uid</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">K8S_NAMESPACE</span>
              <span class="hljs-attr">valueFrom:</span>
                <span class="hljs-attr">fieldRef:</span>
                  <span class="hljs-attr">fieldPath:</span> <span class="hljs-string">metadata.namespace</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">K8S_NODE_NAME</span>
              <span class="hljs-attr">valueFrom:</span>
                <span class="hljs-attr">fieldRef:</span>
                  <span class="hljs-attr">fieldPath:</span> <span class="hljs-string">spec.nodeName</span>
            <span class="hljs-comment"># -------------------------------------</span>
          <span class="hljs-attr">volumeMounts:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/etc/otelcol</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">otel-config</span>
</code></pre>
<hr />
<h3 id="heading-2-otel-collector-config">2. OTel Collector Config (讀取身份)</h3>
<p>接下來，在 Collector 的配置中，我們不使用 <code>k8s_attributes</code>，而是改用 <strong><code>resource</code> processor</strong> (注意：不是 <code>attributes</code> processor)。</p>
<p><strong>為什麼是 <code>resource</code> processor？</strong>
因為上一篇提到，Stream Identity 的判定依賴於 <strong>Resource Attributes</strong>。普通的 <code>attributes</code> processor 預設是修改 DataPoint 屬性，這對 Stream Identity 的修正無效。我們必須修改 Resource 層級。</p>
<pre><code class="lang-yaml"><span class="hljs-attr">processors:</span>
  <span class="hljs-comment"># 使用 Resource Processor 抓取環境變數</span>
  <span class="hljs-attr">resource:</span>
    <span class="hljs-attr">attributes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">k8s.pod.name</span>
        <span class="hljs-attr">value:</span> <span class="hljs-string">"${env:K8S_POD_NAME}"</span>  <span class="hljs-comment"># 讀取 K8s 注入的變數</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">upsert</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">k8s.pod.uid</span>
        <span class="hljs-attr">value:</span> <span class="hljs-string">"${env:K8S_POD_UID}"</span>   <span class="hljs-comment"># 解決 ErrOlderStart 的核心</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">upsert</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">k8s.namespace.name</span>
        <span class="hljs-attr">value:</span> <span class="hljs-string">"${env:K8S_NAMESPACE}"</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">upsert</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">k8s.node.name</span>
        <span class="hljs-attr">value:</span> <span class="hljs-string">"${env:K8S_NODE_NAME}"</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">upsert</span>

  <span class="hljs-attr">deltatocumulative:</span>
    <span class="hljs-attr">max_streams:</span> <span class="hljs-number">100000</span>

<span class="hljs-attr">service:</span>
  <span class="hljs-attr">pipelines:</span>
    <span class="hljs-attr">metrics:</span>
      <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>]
      <span class="hljs-comment"># 順序很重要：先 Resource (貼標籤) -&gt; 再 Delta (計算)</span>
      <span class="hljs-attr">processors:</span> [<span class="hljs-string">resource</span>, <span class="hljs-string">deltatocumulative</span>] 
      <span class="hljs-attr">exporters:</span> [<span class="hljs-string">prometheus</span>]
</code></pre>
<hr />
<h3 id="heading-3-vs-k8sattributes">3. 這個方案 vs <code>k8s_attributes</code> 的比較</h3>
<p>這兩種方法都能達到讓 Stream Identity 唯一化的目的，消除 <code>ErrOlderStart</code>。</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>特性</td><td>Downward API + Resource Processor</td><td>k8s_attributes Processor</td></tr>
</thead>
<tbody>
<tr>
<td><strong>RBAC 權限</strong></td><td>✅ <strong>不需要</strong> (由 K8s 自身注入)</td><td>❌ <strong>需要</strong> (List/Watch Pods)</td></tr>
<tr>
<td><strong>API Server 負載</strong></td><td>✅ <strong>零負載</strong> (靜態變數)</td><td>⚠️ <strong>有負載</strong> (需建立 Watch 連線)</td></tr>
<tr>
<td><strong>適用場景</strong></td><td><strong>Sidecar 模式首選</strong></td><td><strong>DaemonSet / Gateway 模式首選</strong></td></tr>
<tr>
<td><strong>即時性</strong></td><td>啟動即生效，無延遲</td><td>可能有啟動延遲 (需等待 List 完成)</td></tr>
<tr>
<td><strong>資料豐富度</strong></td><td>受限於 Downward API 支援的欄位</td><td>可抓取所有 Labels 與 Annotations</td></tr>
</tbody>
</table>
</div><h3 id="heading-errolderstart">點評：為什麼這也能防 <code>ErrOlderStart</code>？</h3>
<p><code>deltatocumulative</code> 並不在乎標籤是「誰」貼上去的。它只在乎當它計算 Hash 時，Resource 裡有沒有這兩個欄位：</p>
<ol>
<li><strong><code>k8s.pod.name</code></strong>: 區分了 Pod A 和 Pod B。</li>
<li><strong><code>k8s.pod.uid</code></strong>: 區分了 Pod A (昨天死的) 和 Pod A (今天重啟的)。</li>
</ol>
<p>透過 Downward API 注入這兩個值，Stream ID 就會變成全宇宙唯一，徹底解決身份衝突與 <code>ErrOlderStart</code> 問題，而且完全不需要跟 K8s API Server 打交道，效能更好、安全性更高。</p>
]]></content:encoded></item><item><title><![CDATA[剖析 OTel Collector Delta To Cumulative Processor]]></title><description><![CDATA[本文將深入探討 OpenTelemetry Collector Contrib 中的 deltatocumulative Processor。除了基本的配置與使用外，我們將從 源碼層級 (Source Code Level) 分析其內部運作機制、狀態管理策略，並詳細解釋生產環境中常見的異常現象。
1. 簡介
deltatocumulativeprocessor 的核心任務是將 Metrics 的 Temporality 從 Delta (增量) 轉換為 Cumulative (累積)。這是一個 ...]]></description><link>https://ganhua.wang/otel-collector-delta-to-cumulative-processor</link><guid isPermaLink="true">https://ganhua.wang/otel-collector-delta-to-cumulative-processor</guid><category><![CDATA[OpenTelemetry]]></category><category><![CDATA[observability]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Wed, 21 Jan 2026 14:54:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769007242495/889ffd72-daea-4092-a832-eb726ef4568e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>本文將深入探討 OpenTelemetry Collector Contrib 中的 <code>deltatocumulative</code> Processor。除了基本的配置與使用外，我們將從 <strong>源碼層級 (Source Code Level)</strong> 分析其內部運作機制、狀態管理策略，並詳細解釋生產環境中常見的異常現象。</p>
<h2 id="heading-1">1. 簡介</h2>
<p><a target="_blank" href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/cumulativetodeltaprocessor/README.md"><code>deltatocumulativeprocessor</code></a> 的核心任務是將 Metrics 的 Temporality 從 <strong>Delta (增量)</strong> 轉換為 <strong>Cumulative (累積)</strong>。這是一個 <strong>Stateful (有狀態)</strong> 的組件，意味著它必須在記憶體中維護所有活躍 Time Series 的當前數值。</p>
<h2 id="heading-2-architecture-amp-processing-flow">2. 核心架構與處理流程 (Architecture &amp; Processing Flow)</h2>
<p>此 Processor 的設計核心在於「高效的狀態管理」與「嚴格的時間序驗證」。為了在高併發下維持準確性，它採用了細粒度的鎖定策略與強型別的狀態存儲。</p>
<h3 id="heading-21-key-data-structures">2.1 關鍵資料結構 (Key Data Structures)</h3>
<p>核心邏輯位於 <code>processor/deltatocumulativeprocessor</code>，主要由以下結構支撐：</p>
<h4 id="heading-a-identitystream">A. 唯一識別 <code>identity.Stream</code></h4>
<p>每個 Time Series (時間序列) 由 <code>identity.Stream</code> 唯一識別。這不僅僅是 Metric Name，還包含：</p>
<ul>
<li><p><strong>Metric Signature</strong>: Name, Unit, Type, Monotonicity, Temporality.</p>
</li>
<li><p><strong>Attributes Hash</strong>: 所有 DataPoint 屬性 (Labels) 的雜湊值。 這確保了即使是同一個 Metric Name，不同的 Label 組合也會被視為獨立的 Stream。</p>
</li>
</ul>
<h4 id="heading-b-state">B. 狀態存儲 <code>state</code></h4>
<p>Processor 內部使用 <code>maps.Parallel</code> (基於 <code>xsync.MapOf</code>) 來存儲狀態。為了效能與型別安全，它針對不同數據類型分開存儲：</p>
<ul>
<li><p><code>nums</code>: 存儲 <code>NumberDataPoint</code> (Sum/Gauge)</p>
</li>
<li><p><code>hist</code>: 存儲 <code>HistogramDataPoint</code></p>
</li>
<li><p><code>expo</code>: 存儲 <code>ExponentialHistogramDataPoint</code></p>
</li>
</ul>
<h4 id="heading-c-mutext">C. 併發控制 <code>mutex[T]</code></h4>
<p>這是效能關鍵。Processor <strong>不使用全域鎖 (Global Lock)</strong> 來保護所有狀態。 相反，每個 Stream 都有自己獨立的 <code>mutex</code>。</p>
<ul>
<li><strong>意義</strong>: 不同 Stream 的更新是完全平行的，互不阻塞。只有當多個請求同時更新 <em>同一個</em> Stream 時，才會發生鎖競爭。</li>
</ul>
<h3 id="heading-22-consumemetrics">2.2 處理流程 (<code>ConsumeMetrics</code>)</h3>
<p>當 Metrics 進入 Processor 時，數據流經以下嚴格步驟：</p>
<ol>
<li><p><strong>過濾 (Filter)</strong>:</p>
<ul>
<li>檢查 <code>AggregationTemporality</code>。只有 <strong>Delta</strong> 類型的指標會被處理；Cumulative 指標直接透傳 (Pass-through)。</li>
</ul>
</li>
<li><p><strong>識別與查找 (Identify &amp; Lookup)</strong>:</p>
<ul>
<li><p>計算 DataPoint 的 <code>identity.Stream</code>。</p>
</li>
<li><p>嘗試從 <code>state</code> 中檢索現有累積值。</p>
</li>
<li><p><strong>容量檢查</strong>: 如果是新 Stream 且總數已達 <code>max_streams</code>，則標記 <code>error="limit"</code> 並<strong>丟棄</strong>該數據點。</p>
</li>
</ul>
</li>
<li><p><strong>聚合運算 (</strong><code>delta.Aggregate</code>):</p>
<ul>
<li><p>在 Stream 級別的鎖保護下執行。</p>
</li>
<li><p><strong>邏輯</strong>: <code>New_Cumulative = Old_Cumulative + New_Delta</code>。</p>
</li>
</ul>
</li>
<li><p><strong>時間序驗證 (Validation)</strong>: 在聚合前，必須通過兩項關鍵檢查 (位於 <code>internal/delta/delta.go</code>)：</p>
<ul>
<li><p><strong>亂序檢測 (</strong><code>ErrOutOfOrder</code>):</p>
<ul>
<li><p>條件: <code>New.Time &lt;= Stored.Time</code></p>
</li>
<li><p>結果: 丟棄數據。這通常發生在發送端重試或網路亂序時。</p>
</li>
</ul>
</li>
<li><p><strong>重啟檢測 (</strong><code>ErrOlderStart</code>):</p>
<ul>
<li><p>條件: <code>New.Start &lt; Stored.Start</code></p>
</li>
<li><p>結果: 丟棄數據。這代表來源進程可能已重啟，發送了屬於「上一代」的數據，或是時間戳生成有誤。</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>寫回與標記</strong>:</p>
<ul>
<li><p>將計算出的 Cumulative 值寫回原始 DataPoint。</p>
</li>
<li><p>將 Temporality 修改為 <code>Cumulative</code>。</p>
</li>
<li><p>更新 <code>stale</code> map 中的最後活躍時間 (Last Seen)。</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-23-garbage-collection">2.3 垃圾回收機制 (Garbage Collection)</h3>
<p>為了釋放不再活躍的 Stream (例如短暫存在的 Pod Metrics)，Processor 運行一個背景 Goroutine：</p>
<ul>
<li><p><strong>頻率</strong>: 每 1 分鐘執行一次。</p>
</li>
<li><p><strong>邏輯</strong>: 遍歷 <code>stale</code> Map。如果 <code>now - last_seen &gt; max_stale</code>，則從記憶體中刪除該 Stream 的所有狀態。</p>
</li>
</ul>
<hr />
<h2 id="heading-3-configuration">3. 關鍵配置參數 (Configuration)</h2>
<pre><code class="lang-yaml"><span class="hljs-attr">processors:</span>
    <span class="hljs-attr">deltatocumulative:</span>
        <span class="hljs-comment"># Stream 閒置多久後被視為過期並清除 (預設 5m)</span>
        <span class="hljs-comment"># 影響: 設置過短會導致頻繁的狀態重置；設置過長會增加記憶體壓力。</span>
        <span class="hljs-attr">max_stale:</span> <span class="hljs-string">5m</span>

        <span class="hljs-comment"># 允許追蹤的最大 Stream 數量 (預設為 Max Int)</span>
        <span class="hljs-comment"># 影響: 這是保護 Collector 不被 High Cardinality 數據撐爆的最後防線。</span>
        <span class="hljs-comment"># 一旦觸發，超出的 Stream 會被無情丟棄。</span>
        <span class="hljs-attr">max_streams:</span> <span class="hljs-number">9223372036854775807</span>
</code></pre>
<hr />
<h2 id="heading-4-observability">4. 監控指標詳解 (Observability)</h2>
<p>Processor 透過 <code>internal/telemetry</code> 暴露了自我監控指標 (Self-monitoring Metrics)，這是排查數據丟失問題的首要依據。</p>
<h3 id="heading-41">4.1 核心指標列表</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標名稱 (Metric Name)</td><td>類型</td><td>說明</td><td>關鍵標籤 (Labels)</td></tr>
</thead>
<tbody>
<tr>
<td><code>deltatocumulative_datapoints</code></td><td>Counter</td><td>處理的數據點總數。請密切關注 <code>error</code> 標籤。</td><td><strong>error</strong>:</td></tr>
<tr>
<td>- <code>(missing)</code>: 處理成功</td><td></td><td></td><td></td></tr>
<tr>
<td>- <code>limit</code>: 觸發 <code>max_streams</code> 上限而丟棄</td><td></td><td></td><td></td></tr>
<tr>
<td>- <code>delta.ErrOutOfOrder</code>: 因時間戳亂序而丟棄</td><td></td><td></td><td></td></tr>
<tr>
<td>- <code>delta.ErrOlderStart</code>: 因起始時間異常而丟棄</td><td></td><td></td><td></td></tr>
<tr>
<td><code>deltatocumulative_streams_tracked</code></td><td>Gauge</td><td>當前記憶體中活躍追蹤的 Stream 總數。</td><td>無</td></tr>
<tr>
<td><code>deltatocumulative_streams_limit</code></td><td>Gauge</td><td>配置的 <code>max_streams</code> 值。</td><td>無</td></tr>
<tr>
<td><code>deltatocumulative_streams_max_stale</code></td><td>Gauge</td><td>配置的 <code>max_stale</code> 值 (秒)。</td><td>無</td></tr>
</tbody>
</table>
</div><hr />
<ol start="5">
<li><h2 id="heading-6ygl54ef55kw5akd5bi46kal6k2w6agm5ymw5p6q">運營環境常見議題剖析</h2>
</li>
</ol>
<h3 id="heading-streamstracked">議題一：狀態丟失與 <code>streams_tracked</code> 的鋸齒狀波動</h3>
<p><strong>現象：</strong> 監控圖表顯示 <code>streams_tracked</code> 呈現週期性的鋸齒狀下跌，或者劇烈震盪。同時下游看到的數值可能突然歸零或重置。</p>
<p><strong>源碼級原因：</strong> 這通常與 GC 機制 (<code>stale</code> check) 有關。</p>
<ol>
<li><p><strong>間歇性流量</strong>: 如果某個指標每 6 分鐘才送一次，而 <code>max_stale</code> 設為 5 分鐘。Processor 會在第 5 分鐘刪除狀態。第 6 分鐘數據進來時，被視為全新的 Stream，累積值從 0 (或當前 Delta) 開始計算，導致<strong>狀態丟失</strong>。</p>
</li>
<li><p><strong>GC 運作</strong>: 背景 Goroutine 每分鐘一次的清理動作，會導致 <code>streams_tracked</code> 出現階梯式下降。</p>
</li>
</ol>
<p><strong>對策：</strong></p>
<ul>
<li>確保 <code>max_stale</code> 顯著大於 metrics 的 scrape interval 或 push interval (建議至少 2-3 倍)。</li>
</ul>
<h3 id="heading-pipeline">議題二：Pipeline 順序導致的「數據消失之謎」</h3>
<p><strong>現象：</strong><code>batch</code> Processor 報告發送了 2.6k 點，但 <code>exporter</code> 報告只發送了 2.0k 點。中間的 0.6k 憑空消失，且沒有任何 Error Log。</p>
<p><strong>源碼級原因：</strong> 這是 <code>deltatocumulative</code> 的 <code>max_streams</code> 限制與 Pipeline 順序共同作用的結果。</p>
<pre><code class="lang-go"><span class="hljs-comment">// processor.go 片段</span>
<span class="hljs-keyword">if</span> maps.Exceeded(last, loaded) {
    attrs.Set(telemetry.Error(<span class="hljs-string">"limit"</span>))
    <span class="hljs-keyword">return</span> drop <span class="hljs-comment">// 直接返回 false，數據點從 Slice 中移除</span>
}
</code></pre>
<p>如果 Pipeline 配置為 <code>[batch, deltatocumulative]</code>：</p>
<ol>
<li><p><strong>Batch</strong>: 收到數據，計數器 <code>batch_send_size</code> +2.6k。</p>
</li>
<li><p><strong>DeltaToCumulative</strong>: 發現 Stream 總數超標，靜默丟棄 0.6k 數據點 (僅增加 <code>deltatocumulative_datapoints{error="limit"}</code>)。</p>
</li>
<li><p><strong>Exporter</strong>: 收到剩餘的 2.0k，計數器 <code>sent_metric_points</code> +2.0k。</p>
</li>
</ol>
<p><strong>對策：</strong></p>
<ol>
<li><p><strong>調整順序</strong>: 改為 <code>[deltatocumulative, batch]</code>。讓過濾發生在打包之前。</p>
</li>
<li><p><strong>監控 Drop</strong>: 設置告警監控 <code>sum(rate(otelcol_deltatocumulative_datapoints{error="limit"}[2m])) &gt; 0</code>。</p>
</li>
</ol>
<h4 id="heading-6ycy6zqo6kqq5pio">進階說明</h4>
<p>  deltatocumulativeprocessor 不會拆分 metric，它只做：                                                      </p>
<ul>
<li>Delta → Cumulative 的 temporality 轉換                                                                  </li>
<li>累加數值       </li>
</ul>
<p>拆分成 _total, _sum, _bucket 是 Prometheus Exporter 的工作。                                              </p>
<p>  流程圖                                                                                                    </p>
<pre><code class="lang-text">┌─────────────────────────────────────────────────────────────────────────┐                               
  │                        OTel Collector Pipeline                          │                               
  ├─────────────────────────────────────────────────────────────────────────┤                               
  │                                                                         │                               
  │  Receiver          Processor                    Exporter                │                               
  │  ────────          ─────────                    ────────                │                               
  │                                                                         │                               
  │  ┌──────────┐     ┌─────────────────────┐     ┌───────────────────┐    │                                
  │  │ OTLP     │     │ deltatocumulative   │     │ prometheusremote  │    │                                
  │  │ Receiver │ ──▶ │ processor           │ ──▶ │ write exporter    │    │                                
  │  └──────────┘     └─────────────────────┘     └───────────────────┘    │                                
  │                                                                         │                               
  │  OTel Histogram    OTel Histogram              Prometheus format:       │                               
  │  (Delta)           (Cumulative)                • metric_bucket{le=...}  │                               
  │                    ↑                           • metric_sum             │                               
  │                    只改 temporality            • metric_count           │                               
  │                    不改結構                                              │                              
  │                                                                         │                               
  └─────────────────────────────────────────────────────────────────────────┘
</code></pre>
<hr />
<p>  各 Metric 類型在 Prometheus Exporter 的輸出                                                               </p>
<pre><code class="lang-text">┌──────────────────────┬──────────────────────────────────────────────────────────┐                       
  │   OTel Metric Type   │                     Prometheus 輸出                      │                       
  ├──────────────────────┼──────────────────────────────────────────────────────────┤                       
  │ Sum (Counter)        │ metric_total (1 個)                                      │                       
  ├──────────────────────┼──────────────────────────────────────────────────────────┤                       
  │ Gauge                │ metric (1 個)                                            │                       
  ├──────────────────────┼──────────────────────────────────────────────────────────┤                       
  │ Histogram            │ metric_bucket{le=...} (N 個) + metric_sum + metric_count │                       
  ├──────────────────────┼──────────────────────────────────────────────────────────┤                       
  │ ExponentialHistogram │ 轉成普通 histogram 後同上                                │                       
  ├──────────────────────┼──────────────────────────────────────────────────────────┤                       
  │ Summary              │ metric{quantile=...} (N 個) + metric_sum + metric_count  │                       
  └──────────────────────┴──────────────────────────────────────────────────────────┘
</code></pre>
<hr />
<p>  範例：一個 Histogram DataPoint                                                                            </p>
<pre><code>  # OTel Histogram (經過 deltatocumulativeprocessor 後)                                                     
  <span class="hljs-attr">name</span>: http_request_duration_seconds                                                                       
  <span class="hljs-attr">type</span>: Histogram                                                                                           
  <span class="hljs-attr">temporality</span>: Cumulative  # ← processor 改了這個                                                           
  <span class="hljs-attr">datapoint</span>:                                                                                                
    attributes: {<span class="hljs-attr">method</span>: <span class="hljs-string">"GET"</span>}                                                                             
    <span class="hljs-attr">sum</span>: <span class="hljs-number">150.5</span>                                                                                              
    <span class="hljs-attr">count</span>: <span class="hljs-number">1000</span>                                                                                             
    <span class="hljs-attr">bucket_counts</span>: [<span class="hljs-number">100</span>, <span class="hljs-number">300</span>, <span class="hljs-number">400</span>, <span class="hljs-number">150</span>, <span class="hljs-number">50</span>]                                                                 
    <span class="hljs-attr">explicit_bounds</span>: [<span class="hljs-number">0.005</span>, <span class="hljs-number">0.01</span>, <span class="hljs-number">0.025</span>, <span class="hljs-number">0.05</span>]
</code></pre><p>  ↓ Prometheus Exporter ↓                                                                                   </p>
<pre><code>  # 變成多個 time series                                                                                    
  http_request_duration_seconds_bucket{method=<span class="hljs-string">"GET"</span>, le=<span class="hljs-string">"0.005"</span>} <span class="hljs-number">100</span>                                        
  http_request_duration_seconds_bucket{method=<span class="hljs-string">"GET"</span>, le=<span class="hljs-string">"0.01"</span>} <span class="hljs-number">400</span>                                         
  http_request_duration_seconds_bucket{method=<span class="hljs-string">"GET"</span>, le=<span class="hljs-string">"0.025"</span>} <span class="hljs-number">800</span>                                        
  http_request_duration_seconds_bucket{method=<span class="hljs-string">"GET"</span>, le=<span class="hljs-string">"0.05"</span>} <span class="hljs-number">950</span>                                         
  http_request_duration_seconds_bucket{method=<span class="hljs-string">"GET"</span>, le=<span class="hljs-string">"+Inf"</span>} <span class="hljs-number">1000</span>                                        
  http_request_duration_seconds_sum{method=<span class="hljs-string">"GET"</span>} <span class="hljs-number">150.5</span>                                                     
  http_request_duration_seconds_count{method=<span class="hljs-string">"GET"</span>} <span class="hljs-number">1000</span>
</code></pre><hr />
<p>  總結                                                                                                      </p>
<pre><code class="lang-text">┌────────────────────────────┬──────────────────────────────────────────────────┐                         
  │            組件            │                       職責                       │                         
  ├────────────────────────────┼──────────────────────────────────────────────────┤                         
  │ deltatocumulativeprocessor │ 只改 temporality，不拆分 metric                  │                         
  ├────────────────────────────┼──────────────────────────────────────────────────┤                         
  │ Prometheus Exporter        │ 將 OTel 格式轉成 Prometheus 格式，拆分 histogram │                         
  └────────────────────────────┴──────────────────────────────────────────────────┘
</code></pre>
<p>所以正常 sent_metric_points 應該要更多才是。</p>
<h3 id="heading-out-of-order">議題三：亂序數據 (Out of Order)</h3>
<p><strong>現象：</strong> 數據偶爾丟失，<code>deltatocumulative_datapoints</code> 出現 <code>error="out_of_order"</code>。</p>
<p><strong>源碼級原因 (</strong><code>internal/delta/delta.go</code>)：</p>
<pre><code class="lang-go"><span class="hljs-keyword">case</span> dp.Timestamp() &lt;= state.Timestamp():
    <span class="hljs-keyword">return</span> ErrOutOfOrder{...}
</code></pre>
<p>Processor 強制要求數據的時間戳嚴格遞增。如果發送端 (如 Prometheus Remote Write 或某些 SDK) 因重試邏輯發送了重複或舊的時間戳，Processor 會為了保護累積值的單調性而拒絕該數據。</p>
<p><strong>對策：</strong> 檢查發送端的 Retry 策略或時鐘同步狀態。</p>
<hr />
<h2 id="heading-6-poc-lab">6. PoC Lab 環境 - 本地重現驗證</h2>
<p>為了驗證上述理論，我們建立了一個可在本地運行的 Docker Compose PoC 環境。</p>
<h3 id="heading-61">6.1 環境架構</h3>
<pre><code class="lang-sql">┌─────────────────┐     ┌─────────────────────────────────────┐     ┌────────────┐
│  telemetrygen   │────▶│         OTel Collector              │────▶│ Prometheus │
│  (60 instances) │     │  ┌─────────────────────────────┐    │     │  :9090     │
│  app-01 ~ 60    │     │  │ Pipeline:                   │    │     └────────────┘
└─────────────────┘     │  │ receiver → cumulativetodelta│    │
                        │  │          → batch            │    │
                        │  │          → deltatocumulative│    │
                        │  │          → exporter         │    │
                        │  │                             │    │
                        │  │ max_streams: 50 (&lt; 60)      │    │
                        │  └─────────────────────────────┘    │
                        └─────────────────────────────────────┘
</code></pre>
<p><strong>設計理念：</strong></p>
<ul>
<li><p>60 個 telemetrygen 實例，每個發送不同的 <code>service.name</code> (app-01 ~ app-60)</p>
</li>
<li><p><code>max_streams</code> 設為 50，刻意製造 Stream 超限</p>
</li>
<li><p>Pipeline 順序為 <code>[cumulativetodelta, batch, deltatocumulative]</code>，重現「先 batch 後過濾」的問題</p>
</li>
</ul>
<h3 id="heading-62">6.2 檔案結構</h3>
<p><a target="_blank" href="https://gist.github.com/tedmax100/a588754e12e6e5cc0788a8df67337fd5">Soruce Code</a></p>
<pre><code class="lang-sql">poc-lab/
├── docker-compose.yaml   <span class="hljs-comment"># Docker Compose 配置</span>
├── otel-config.yaml      <span class="hljs-comment"># OTel Collector 配置</span>
└── prometheus.yaml       <span class="hljs-comment"># Prometheus scrape 配置</span>
</code></pre>
<h3 id="heading-63">6.3 配置檔案</h3>
<h4 id="heading-otel-configyaml"><code>otel-config.yaml</code></h4>
<pre><code class="lang-yaml"><span class="hljs-attr">receivers:</span>
  <span class="hljs-attr">otlp:</span>
    <span class="hljs-attr">protocols:</span>
      <span class="hljs-attr">grpc:</span>
        <span class="hljs-attr">endpoint:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:4317</span>

<span class="hljs-attr">processors:</span>
  <span class="hljs-comment"># 將 telemetrygen 產生的 Cumulative 轉成 Delta</span>
  <span class="hljs-attr">cumulativetodelta:</span>

  <span class="hljs-attr">batch:</span>
    <span class="hljs-attr">send_batch_size:</span> <span class="hljs-number">100</span>
    <span class="hljs-attr">timeout:</span> <span class="hljs-string">1s</span>

  <span class="hljs-attr">deltatocumulative:</span>
    <span class="hljs-attr">max_stale:</span> <span class="hljs-string">1m</span>
    <span class="hljs-comment"># 【關鍵設定】設定極低的上限，強迫發生 Drop</span>
    <span class="hljs-attr">max_streams:</span> <span class="hljs-number">50</span>

<span class="hljs-attr">exporters:</span>
  <span class="hljs-attr">prometheus:</span>
    <span class="hljs-attr">endpoint:</span> <span class="hljs-string">"0.0.0.0:8889"</span>
    <span class="hljs-attr">namespace:</span> <span class="hljs-string">"poc_app"</span>

  <span class="hljs-attr">debug:</span>
    <span class="hljs-attr">verbosity:</span> <span class="hljs-string">normal</span>

<span class="hljs-attr">service:</span>
  <span class="hljs-attr">telemetry:</span>
    <span class="hljs-attr">metrics:</span>
      <span class="hljs-attr">readers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">pull:</span>
            <span class="hljs-attr">exporter:</span>
              <span class="hljs-attr">prometheus:</span>
                <span class="hljs-attr">host:</span> <span class="hljs-string">"0.0.0.0"</span>
                <span class="hljs-attr">port:</span> <span class="hljs-number">8888</span>

  <span class="hljs-attr">pipelines:</span>
    <span class="hljs-attr">metrics:</span>
      <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>]
      <span class="hljs-comment"># 【關鍵錯誤順序】重現問題</span>
      <span class="hljs-attr">processors:</span> [<span class="hljs-string">cumulativetodelta</span>, <span class="hljs-string">batch</span>, <span class="hljs-string">deltatocumulative</span>]
      <span class="hljs-attr">exporters:</span> [<span class="hljs-string">prometheus</span>, <span class="hljs-string">debug</span>]
</code></pre>
<h4 id="heading-prometheusyaml"><code>prometheus.yaml</code></h4>
<pre><code class="lang-yaml"><span class="hljs-attr">global:</span>
  <span class="hljs-attr">scrape_interval:</span> <span class="hljs-string">5s</span>
  <span class="hljs-attr">evaluation_interval:</span> <span class="hljs-string">5s</span>

<span class="hljs-attr">scrape_configs:</span>
  <span class="hljs-comment"># Job 1: 監控 Collector 本身</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'otel-collector-internal'</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'otel-collector:8888'</span>]

  <span class="hljs-comment"># Job 2: 監控實際輸出的數據</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'app-metrics'</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'otel-collector:8889'</span>]
</code></pre>
<h4 id="heading-docker-composeyaml"><code>docker-compose.yaml</code> (精簡版)</h4>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">otel-collector:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">otel/opentelemetry-collector-contrib:0.142.0</span>
    <span class="hljs-attr">command:</span> [<span class="hljs-string">"--config=/etc/otel-collector-config.yaml"</span>]
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./otel-config.yaml:/etc/otel-collector-config.yaml:ro</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"4317:4317"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8888:8888"</span>   <span class="hljs-comment"># Collector Internal Metrics</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8889:8889"</span>   <span class="hljs-comment"># Prometheus Exporter</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">otel-network</span>

  <span class="hljs-attr">prometheus:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">prom/prometheus:latest</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./prometheus.yaml:/etc/prometheus/prometheus.yml:ro</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9090:9090"</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">otel-collector</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">otel-network</span>

  <span class="hljs-comment"># 使用 YAML anchor 定義 60 個 telemetrygen 實例</span>
  <span class="hljs-attr">telemetrygen-01:</span> <span class="hljs-string">&amp;telemetrygen-base</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest</span>
    <span class="hljs-attr">command:</span> [<span class="hljs-string">"metrics"</span>, <span class="hljs-string">"--otlp-insecure"</span>, <span class="hljs-string">"--otlp-endpoint=otel-collector:4317"</span>,
              <span class="hljs-string">"--rate=5"</span>, <span class="hljs-string">"--duration=1000h"</span>, <span class="hljs-string">"--metric-type=Sum"</span>, <span class="hljs-string">"--service=app-01"</span>]
    <span class="hljs-attr">depends_on:</span> [<span class="hljs-string">otel-collector</span>]
    <span class="hljs-attr">networks:</span> [<span class="hljs-string">otel-network</span>]

  <span class="hljs-attr">telemetrygen-02:</span> { <span class="hljs-string">&lt;&lt;:</span> <span class="hljs-string">*telemetrygen-base</span>, <span class="hljs-attr">command:</span> [<span class="hljs-string">...</span>, <span class="hljs-string">"--service=app-02"</span>] }
  <span class="hljs-comment"># ... (app-03 ~ app-60)</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">otel-network:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
</code></pre>
<h3 id="heading-64">6.4 運行與驗證</h3>
<h4 id="heading-1-1">步驟 1: 啟動環境</h4>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> poc-lab
docker compose up -d
</code></pre>
<h4 id="heading-2-30-60">步驟 2: 等待數據累積 (約 30-60 秒)</h4>
<pre><code class="lang-bash">docker compose ps  <span class="hljs-comment"># 確認所有容器運行中</span>
</code></pre>
<h4 id="heading-3">步驟 3: 驗證查詢</h4>
<p>打開 Prometheus UI (http://localhost:9090) 或使用 curl：</p>
<p><strong>查詢 1: Streams 追蹤數量 (應該達到上限 50)</strong></p>
<pre><code class="lang-sql">otelcol_deltatocumulative_streams_tracked
</code></pre>
<p><strong>查詢 2: 數據點處理結果 (按 error 分類)</strong></p>
<pre><code class="lang-sql">otelcol_deltatocumulative_datapoints_total
</code></pre>
<p><strong>查詢 3: 比較 Batch vs Exporter</strong></p>
<pre><code class="lang-sql"><span class="hljs-comment"># Batch 處理量</span>
otelcol_processor_batch_batch_send_size_sum

<span class="hljs-comment"># Exporter 發送量</span>
otelcol_exporter_sent_metric_points_total
</code></pre>
<h3 id="heading-65">6.5 驗證結果 - 問題重現 (錯誤順序)</h3>
<p>使用錯誤的 Pipeline 順序 <code>[cumulativetodelta, batch, deltatocumulative]</code> 運行後：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標</td><td>數值</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><code>streams_tracked</code></td><td><strong>50</strong></td><td>達到 max_streams 上限</td></tr>
<tr>
<td><code>streams_limit</code></td><td>50</td><td>配置值</td></tr>
<tr>
<td><code>receiver_accepted</code></td><td>13,000</td><td>Receiver 接收的總數據點</td></tr>
<tr>
<td><code>batch_send_size_sum</code></td><td><strong>13,000</strong></td><td>Batch 處理的數據點</td></tr>
<tr>
<td><code>exporter_sent</code></td><td><strong>11,000</strong></td><td>Exporter 實際發送的數據點</td></tr>
</tbody>
</table>
</div><p><strong>數據點處理詳情：</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>error 標籤</td><td>數值</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><code>error="none"</code></td><td>16,000</td><td>成功處理</td></tr>
<tr>
<td><code>error="limit"</code></td><td><strong>3,000</strong></td><td>因達到 Stream 上限而丟棄</td></tr>
</tbody>
</table>
</div><p><strong>關鍵發現：</strong></p>
<ul>
<li><p>發送端: 60 個 telemetrygen 實例</p>
</li>
<li><p>限制: max_streams = 50</p>
</li>
<li><p>被完全丟棄的實例: 10 個 (60 - 50)</p>
</li>
<li><p><strong>Batch (13,000) ≠ Exporter (11,000)</strong>：差異 2,000 個數據點「憑空消失」</p>
</li>
<li><p>丟棄僅標記 <code>error="limit"</code>，無 Error Log，難以察覺</p>
</li>
</ul>
<h3 id="heading-66">6.6 修正驗證 - 正確順序</h3>
<p>修改 <code>otel-config.yaml</code> 中的 processors 順序：</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># 修正前 (問題配置)</span>
<span class="hljs-attr">processors:</span> [<span class="hljs-string">cumulativetodelta</span>, <span class="hljs-string">batch</span>, <span class="hljs-string">deltatocumulative</span>]

<span class="hljs-comment"># 修正後 (正確配置)</span>
<span class="hljs-attr">processors:</span> [<span class="hljs-string">cumulativetodelta</span>, <span class="hljs-string">deltatocumulative</span>, <span class="hljs-string">batch</span>]
</code></pre>
<p><strong>修正後驗證結果：</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標</td><td>數值</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><code>streams_tracked</code></td><td><strong>50</strong></td><td>仍達到 max_streams 上限</td></tr>
<tr>
<td><code>receiver_accepted</code></td><td>17,500</td><td>Receiver 接收的總數據點</td></tr>
<tr>
<td><code>batch_send_size_sum</code></td><td><strong>14,950</strong></td><td>Batch 處理的數據點</td></tr>
<tr>
<td><code>exporter_sent</code></td><td><strong>14,950</strong></td><td>Exporter 實際發送的數據點</td></tr>
</tbody>
</table>
</div><p><strong>數據點處理詳情：</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>error 標籤</td><td>數值</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><code>error="none"</code></td><td>14,950</td><td>成功處理</td></tr>
<tr>
<td><code>error="limit"</code></td><td>2,490</td><td>因達到 Stream 上限而丟棄</td></tr>
</tbody>
</table>
</div><h3 id="heading-67">6.7 修正前後對比</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標</td><td>修正前</td><td>修正後</td><td>結論</td></tr>
</thead>
<tbody>
<tr>
<td>Batch 處理量</td><td>13,000</td><td>14,950</td><td>-</td></tr>
<tr>
<td>Exporter 發送量</td><td>11,000</td><td>14,950</td><td>-</td></tr>
<tr>
<td><strong>Batch = Exporter?</strong></td><td><strong>否 (差 2,000)</strong></td><td><strong>是 (完全一致)</strong></td><td><strong>問題解決</strong></td></tr>
<tr>
<td>error="limit"</td><td>3,000</td><td>2,490</td><td>仍有丟棄 (預期行為)</td></tr>
</tbody>
</table>
</div><p><strong>關鍵結論：</strong></p>
<pre><code class="lang-sql">修正前: Batch (13,000) ≠ Exporter (11,000)  ← 數據「消失」，難以排查
修正後: Batch (14,950) = Exporter (14,950)  ← 指標一致，問題可追溯
</code></pre>
<ol>
<li><p><strong>Pipeline 順序修正有效</strong> - Batch 和 Exporter 的數值現在完全一致</p>
</li>
<li><p><strong>丟棄仍然發生</strong> (error="limit")，但這是<strong>預期行為</strong>，因為來源數 (60) &gt; max_streams (50)</p>
</li>
<li><p><strong>監控指標正確反映實際狀態</strong> - 運維人員可直接從 <code>error="limit"</code> 指標看到丟棄量</p>
</li>
</ol>
<h3 id="heading-68">6.8 清理環境</h3>
<pre><code class="lang-bash">docker compose down
</code></pre>
<hr />
<h2 id="heading-7-php-delta-to-cumulative-processor">7. PHP 與 Delta To Cumulative Processor 的關聯</h2>
<h2 id="heading-71-php-the-share-nothing-architecture">7.1 為什麼是 PHP？ (The "Share-Nothing" Architecture)</h2>
<p>大多數的應用程式（如 Java, Go, Python, Node.js）通常是以常駐進程 (Long-running process) 的方式運行。它們在記憶體中有一個全域的計數器，可以一直累加數值：</p>
<ul>
<li><p>00:01: 累計 10 次</p>
</li>
<li><p>00:02: 累計 15 次 (累加了 5 次)</p>
</li>
<li><p>00:03: 累計 20 次 (又累加了 5 次)</p>
</li>
</ul>
<p>這就是 Cumulative (累積) 模式，也是 Prometheus 等後端系統最喜歡的格式。</p>
<p>但是，PHP 通常運行在 PHP-FPM 或 CGI 模式下。其生命週期是「一個請求一個進程」 (Per-request process)：</p>
<ol>
<li>收到請求 -&gt; 啟動 (或重用) PHP worker。</li>
<li>執行腳本 -&gt; 處理指標 (Metrics)。</li>
<li><p>請求結束 -&gt; 記憶體釋放/重置。</p>
<p>因為 PHP 進程之間通常不共享記憶體 (Share-nothing)，要維護一個「自從伺服器啟動以來的所有請求總數」是非常困難且昂貴的（需要依賴外部 Redis 或 Shared Memory）。</p>
<p>因此，PHP 最自然的做法是只回報「這一次請求發生了什麼」：</p>
</li>
<li>請求 A: 我處理了 1 個 DB 查詢 (Delta) -&gt; 結束</li>
<li><p>請求 B: 我處理了 1 個 DB 查詢 (Delta) -&gt; 結束</p>
<p>這就是 <code>Delta (增量)</code> 模式。</p>
</li>
</ol>
<h2 id="heading-72-delta-to-cumulative-processor">7.2 Delta to Cumulative Processor 的角色</h2>
<p>  當您的後端資料庫（如 Prometheus）只接受 Cumulative 資料，但您的應用程式（如 PHP 或 Serverless
  Functions）只能提供 Delta 資料時，就會發生格式不相容。</p>
<p>  這時候就需要 deltatocumulative processor 擔任「狀態管理者」 (Stateful Intermediary) 的角色：</p>
<ol>
<li>接收 (Receive): 它接收來自 PHP 的無數個小 Delta (例如：+1, +1, +1)。</li>
<li>記憶 (Remember): 它在 Collector 的記憶體中維護一個對應的 Stream，並幫忙做加法運算 (State Management)。</li>
<li>轉換 (Convert): 它算出累積值 (例如：目前總共是 3)，並將其轉換為 Cumulative 格式。</li>
<li>輸出 (Export): 發送給 Prometheus。</li>
</ol>
<p>雖然 Serverless (如 AWS Lambda) 或 CLI 工具也有類似需求，但 PHP的廣泛使用以及其標準的運行模式，使其成為這個 Processor 最常見的使用案例。</p>
<h2 id="heading-8">8. 結論與最佳實踐</h2>
<ol>
<li><p><strong>Pipeline 順序</strong>: 將 <code>deltatocumulative</code> 放在 <code>batch</code> <strong>之前</strong>，讓過濾發生在打包前</p>
</li>
<li><p><strong>監控告警</strong>: 設置對 <code>error="limit"</code>, <code>error="out_of_order"</code> 的告警</p>
</li>
<li><p><strong>容量規劃</strong>: 根據預期的 Stream 數量合理設置 <code>max_streams</code></p>
</li>
<li><p><strong>Stale 設定</strong>: <code>max_stale</code> 應大於最大的 push/scrape interval (建議 2-3 倍)</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[SingleFlight]]></title><description><![CDATA[Go Singleflight 實作全攻略：優化 API 消耗、並發控制與監控實務
在開發高併發應用程式（如股票分析機器人）時，我們常面臨 「驚群效應」(Thundering Herd)：當快取失效或系統剛啟動時，大量請求同時湧入，導致昂貴的 API（如 Gemini）成本爆炸或資料庫崩潰。
singleflight 是 Go 官方擴充套件（golang.org/x/sync/singleflight）中的神兵利器，確保當多個請求同時要求同一個結果時，實際的運算只會執行一次。

1. 為什麼需要...]]></description><link>https://ganhua.wang/singleflight</link><guid isPermaLink="true">https://ganhua.wang/singleflight</guid><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Mon, 29 Dec 2025 03:05:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766977586494/61fafd33-b7b6-4c10-8a43-cc2bcd5b01a2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-go-singleflight-api">Go Singleflight 實作全攻略：優化 API 消耗、並發控制與監控實務</h1>
<p>在開發高併發應用程式（如股票分析機器人）時，我們常面臨 <strong>「驚群效應」(Thundering Herd)</strong>：當快取失效或系統剛啟動時，大量請求同時湧入，導致昂貴的 API（如 Gemini）成本爆炸或資料庫崩潰。</p>
<p><code>singleflight</code> 是 Go 官方擴充套件（<a target="_blank" href="https://pkg.go.dev/golang.org/x/sync/singleflight"><code>golang.org/x/sync/singleflight</code></a>）中的神兵利器，確保<strong>當多個請求同時要求同一個結果時，實際的運算只會執行一次</strong>。</p>
<hr />
<h2 id="heading-1-singleflight">1. 為什麼需要 Singleflight？</h2>
<p>想像 1,000 個用戶同時查詢「台積電 (2330)」的分析報告：</p>
<ul>
<li><strong>無控制</strong>：1,000 次 Gemini API 呼叫（帳單飆升）、1,000 次資料庫連線（系統卡死）。</li>
<li><strong>有 Singleflight</strong>：<strong>1 次</strong> API 呼叫，其餘 999 人在記憶體中等待結果，隨後共享同一份數據。</li>
</ul>
<hr />
<h2 id="heading-2-singleflight">2. 深入核心：Singleflight 原始碼拆解</h2>
<p>理解 <code>singleflight</code> 的底層源碼結構，能幫助我們更精準地掌握並發控制。</p>
<h3 id="heading-5qc45bd57wq5qel5a6a576p">核心結構定義</h3>
<ol>
<li><strong><code>Group</code></strong>: 代表一個命名空間，管理所有正在進行中的任務。</li>
<li><code>m map[string]*call</code>: 記錄目前有哪些 Key 正在「飛行中」(In-flight)。</li>
<li><code>mu sync.Mutex</code>: 保護 Map，防止高併發下的 Race Condition。</li>
</ol>
<ol start="2">
<li><strong><code>call</code></strong>: 代表一個正在執行的具體任務。</li>
<li><code>wg sync.WaitGroup</code>: <strong>最關鍵的同步元件</strong>。用來讓「後到」的請求等待「先到」的請求完成。</li>
<li><code>val / err</code>: 存放執行後的結果與錯誤。</li>
</ol>
<h3 id="heading-dokey-fn">運作流程圖解：Do(key, fn)</h3>
<pre><code class="lang-mermaid">sequenceDiagram
    autonumber
    participant G as Group (櫃台)
    participant C as Call (任務)
    participant FN as 執行函數 (fn)

    Note over G: 1. 鎖定 Map 並檢查 Key
    alt 情境 A：已經有人在做了 (Duplicate Call)
        G-&gt;&gt;C: 發現 Key 存在
        G-&gt;&gt;C: c.dups++ (記錄重複)
        G-&gt;&gt;G: Unlock Map
        G-&gt;&gt;C: c.wg.Wait() (原地卡住等待)
        C--&gt;&gt;G: 喚醒並回傳結果
    else 情境 B：我是第一個做的 (Original Call)
        G-&gt;&gt;C: 建立 new(call)
        G-&gt;&gt;C: c.wg.Add(1) (任務開始標記)
        G-&gt;&gt;G: 將 call 掛入 Map
        G-&gt;&gt;G: Unlock Map
        G-&gt;&gt;FN: g.doCall(c, key, fn)
        FN--&gt;&gt;C: 填入結果與錯誤
        Note over G: 2. 清理與廣播
        G-&gt;&gt;G: 鎖定 Map 並 delete(key)
        G-&gt;&gt;C: c.wg.Done() (大喊：好囉！)
    end
</code></pre>
<hr />
<h2 id="heading-3-l1-singleflight">3. 核心實作範例：L1 快取 + Singleflight</h2>
<p>我們在 <code>AnalysisService</code> 中構建兩層防護：<strong>記憶體快取 (L1)</strong> 與 <strong>Singleflight</strong>。</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> analyzer

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"sync"</span>

    <span class="hljs-string">"golang.org/x/sync/singleflight"</span> 
    <span class="hljs-string">"github.com/nathan/stock_bot/internal/storage"</span>
)

<span class="hljs-keyword">type</span> AnalysisService <span class="hljs-keyword">struct</span> {
    genai      *GenAIClient
    d1Client   *storage.D1Client
    stockCache <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]*StockAnalysisResult
    mu         sync.RWMutex
    sf         singleflight.Group
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *AnalysisService)</span> <span class="hljs-title">analyzeStock</span><span class="hljs-params">(ctx context.Context, code, name <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(*StockAnalysisResult, error)</span></span> {
    <span class="hljs-comment">// 1. 第一層防護：檢查記憶體快取 (L1 Cache)</span>
    s.mu.RLock()
    <span class="hljs-keyword">if</span> result, ok := s.stockCache[code]; ok {
        s.mu.RUnlock()
        <span class="hljs-keyword">return</span> result, <span class="hljs-literal">nil</span>
    }
    s.mu.RUnlock()

    <span class="hljs-comment">// 2. 第二層防護：Singleflight (請求合併)</span>
    key := <span class="hljs-string">"stock:"</span> + code
    v, err, _ := s.sf.Do(key, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-params">(<span class="hljs-keyword">interface</span>{}, error)</span></span> {
        <span class="hljs-comment">// 3. 執行昂貴的邏輯 (DB + Gemini API)</span>
        result, err := s.doAnalyzeStock(ctx, code, name) 
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }

        <span class="hljs-comment">// 4. 寫入快取 (務必在 singleflight 內部完成，防止下一波瞬間擊穿)</span>
        s.mu.Lock()
        s.stockCache[code] = result
        s.mu.Unlock()

        <span class="hljs-keyword">return</span> result, <span class="hljs-literal">nil</span>
    })

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }
    <span class="hljs-keyword">return</span> v.(*StockAnalysisResult), <span class="hljs-literal">nil</span>
}
</code></pre>
<hr />
<h2 id="heading-4-context">4. 進階實戰：處理「巢狀 Context」與非同步操作</h2>
<p>在 <code>doAnalyzeStock</code> 內部，如果我們有其他的非同步操作（例如同時查多個 API），我們必須正確傳遞並檢查 <code>ctx.Err()</code>。這樣做是為了確保：<strong>如果外部請求（領頭者）超時了，後續的耗時運算能立即停止，釋放資源。</strong></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *AnalysisService)</span> <span class="hljs-title">doAnalyzeStock</span><span class="hljs-params">(ctx context.Context, code, name <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(*StockAnalysisResult, error)</span></span> {
    <span class="hljs-comment">// 建立一個子 Context 用於內部的多個非同步任務</span>
    g, ctx := errgroup.WithContext(ctx)

    <span class="hljs-keyword">var</span> dbData <span class="hljs-keyword">string</span>
    <span class="hljs-keyword">var</span> aiResult <span class="hljs-keyword">string</span>

    <span class="hljs-comment">// 任務 1：查資料庫</span>
    g.Go(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// 隨時檢查 Context 是否已取消</span>
        <span class="hljs-keyword">select</span> {
        <span class="hljs-keyword">case</span> &lt;-ctx.Done():
            <span class="hljs-keyword">return</span> ctx.Err()
        <span class="hljs-keyword">default</span>:
            <span class="hljs-comment">// 模擬資料庫查詢</span>
            dbData = <span class="hljs-string">"Historical Data"</span>
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
        }
    })

    <span class="hljs-comment">// 任務 2：呼叫 Gemini API</span>
    g.Go(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// 將 ctx 傳入 API 客戶端，讓它能跟隨整體的超時控制</span>
        res, err := s.genai.Generate(ctx, <span class="hljs-string">"Analyze this: "</span>+code)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        aiResult = res
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })

    <span class="hljs-comment">// 等待所有任務完成或其中一個出錯</span>
    <span class="hljs-keyword">if</span> err := g.Wait(); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }

    <span class="hljs-keyword">return</span> &amp;StockAnalysisResult{Data: dbData, Analysis: aiResult}, <span class="hljs-literal">nil</span>
}
</code></pre>
<hr />
<h2 id="heading-5-100">5. 性能數據對比 (100 個併發請求)</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標</td><td>無 Singleflight</td><td>有 Singleflight</td><td>優化率</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Gemini API 呼叫次數</strong></td><td>100 次</td><td><strong>1 次</strong></td><td><strong>99%</strong></td></tr>
<tr>
<td><strong>資料庫讀取壓力</strong></td><td>高 (100 併發)</td><td><strong>極低 (1 併發)</strong></td><td><strong>99%</strong></td></tr>
<tr>
<td><strong>平均回應時間</strong></td><td>~2,500ms</td><td><strong>~2,100ms</strong></td><td><strong>~16%</strong></td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-6-opentelemetry">6. 監控與量化：加入 OpenTelemetry 追蹤</h2>
<p>為了知道 Singleflight 幫我們省了多少錢，我們可以追蹤 <code>shared</code> 這個回傳值。<code>shared</code> 為 <code>true</code> 表示該請求是「搭便車」成功的。</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *AnalysisService)</span> <span class="hljs-title">analyzeStockWithMetrics</span><span class="hljs-params">(ctx context.Context, code <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(*StockAnalysisResult, error)</span></span> {
    key := <span class="hljs-string">"stock:"</span> + code
    v, err, shared := s.sf.Do(key, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-params">(<span class="hljs-keyword">interface</span>{}, error)</span></span> {
        <span class="hljs-keyword">return</span> s.doAnalyzeStock(ctx, code, <span class="hljs-string">"Name"</span>)
    })

    <span class="hljs-comment">// 紀錄監控指標：分辨是「原始呼叫」還是「共享結果」</span>
    status := <span class="hljs-string">"original"</span>
    <span class="hljs-keyword">if</span> shared {
        status = <span class="hljs-string">"shared"</span>
    }

    s.sfCounter.Add(ctx, <span class="hljs-number">1</span>, metric.WithAttributes(
        attribute.String(<span class="hljs-string">"stock_code"</span>, code),
        attribute.String(<span class="hljs-string">"type"</span>, status),
    ))

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }
    <span class="hljs-keyword">return</span> v.(*StockAnalysisResult), <span class="hljs-literal">nil</span>
}
</code></pre>
<p>儀表板觀測指標 (Prometheus / Grafana)
透過這組指標，你可以在 Grafana 畫出以下圖表：</p>
<ol>
<li><p><strong>Request Stacked Area Chart</strong>:</p>
<ul>
<li><p>type="original": 實際產生的 API 呼叫次數（你的成本預算）。</p>
</li>
<li><p>type="shared": 被合併的請求次數（你省下的錢）。</p>
</li>
</ul>
</li>
<li><p><strong>Saving Ratio (節省率)</strong>:</p>
<ul>
<li>公式：sum(rate(shared)) / sum(rate(total))。</li>
</ul>
</li>
</ol>
<hr />
<h2 id="heading-7">7. 總結</h2>
<p><code>singleflight</code> 的機制就像是：</p>
<ol>
<li><strong>進門先看櫃台 (Map)</strong>：有沒有掛牌子 (Key)？</li>
<li><strong>有牌子</strong>：代表有人處理了，我就站在旁邊等 (<strong>Wait</strong>)，等他處理好直接拿結果。</li>
<li><strong>沒牌子</strong>：我掛上牌子 (<strong>Add Map</strong>)，開始處理 (<strong>Do Fn</strong>)。</li>
</ol>
<p><strong>開發者筆記：</strong></p>
<ul>
<li><strong>快取寫入</strong>：務必在 <code>sf.Do</code> 的 func 內部寫入快取。</li>
<li><strong>Context 意識</strong>：傳遞並檢查 <code>ctx.Err()</code> 是確保系統在高壓下不會資源洩漏的關鍵。</li>
<li><strong>可觀測性</strong>：沒有監控，優化就只是傳說。加入 OTel 讓數據說話。</li>
</ul>
<blockquote>
<p><strong>「多人請求，一人做事，結果共享」</strong> —— 這不僅是效能優化，更是對昂貴資源與系統穩定性的極致追求。</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[來自 Grafana 與 OpenTelemetry 的 Logging 最佳實踐]]></title><description><![CDATA[現代可觀測性中的 Log 思維革命：來自 Grafana 與 OpenTelemetry 的最佳實踐


以下是影片內容的完整解釋與重點摘要：
1. 2025 年 Log 的角色與重要性

Log 是否仍有一席之地？ [05:30]

來賓們討論在現代可觀測性Observability）中 Log 的地位。Ed認為 Log 是「唯一真實的可觀測性訊號」，因為它是最容易獲取的（從 Hello World 就開始用），且無法被完全取代（如 process 的 stdout/stderr）。

Zer...]]></description><link>https://ganhua.wang/grafana-opentelemetry</link><guid isPermaLink="true">https://ganhua.wang/grafana-opentelemetry</guid><category><![CDATA[observability]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Fri, 19 Dec 2025 07:08:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766128085732/815177e9-16db-4c73-8287-e6b756338589.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-log-grafana-opentelemetry">現代可觀測性中的 Log 思維革命：來自 Grafana 與 OpenTelemetry 的最佳實踐</h2>
<iframe width="560" height="315" src="https://www.youtube.com/embed/D-bWeY-gBWU?si=M-uqAa1TInSKgVc3"></iframe>

<p>以下是影片內容的完整解釋與重點摘要：</p>
<h3 id="heading-1-2025-log">1. 2025 年 Log 的角色與重要性</h3>
<ul>
<li><p><strong>Log 是否仍有一席之地？</strong> <a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=330">[05:30]</a></p>
<ul>
<li><p>來賓們討論在現代<a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=330">可觀測性</a>Observability）中 Log 的地位。<a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=330">Ed認為 Log 是「唯一真實的可觀測性訊號」，因為它是最容易獲取的</a>（從 Hello World 就開始用），且無法被完全取代（如 process 的 stdout/stderr）。</p>
</li>
<li><p><strong>Zero Duration Spans (零持續時間的 Span</strong>) <a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=513">[08:33]</a>：Jack 提到一種反模式，即人們將 Log 記錄為「持續時間為零」的 Span。他認為如果不具備層級結構且沒有持續時間，那它本質上就是 Log，不應硬套用 Span 的概念。</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-2-opentelemetry-logng-api-1723httpwwwyoutubecomwatchvd-bwey-gbwuampt1043">2. OpenTelemetry 為何需要 Logng API？ <a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=1043">[17:23]</a></h3>
<ul>
<li><p>儘管已有很多成熟的 Log [框架（如 Log4j,Python logging），OpenTelemetry 仍推出了自己的 Logging API。</p>
</li>
<li><p><strong>互操作性（Interoperability）</strong>：最初目的是為了橋接現有的 Log 系統（Log Bridge API）。</p>
</li>
<li><p><strong>統一性</strong>：讓使用者能用同一套 API 處理 Metrics、Traces 和 Logs。</p>
</li>
<li><p><strong>強型別與結構化</strong>：OpenTelemetry 希望推動 Log 往「結構化（Structured）」方向發展，包含明確的事件名稱（Event Name）與屬性（Attributes），而非僅是純文字字串。</p>
</li>
</ul>
<h3 id="heading-3-log-vs">3. 結構化 Log vs. 人類可讀性</h3>
<ul>
<li><p><strong>兩難</strong>：純文字 Log 對人類閱讀除錯很友善，但對機器分析不便；結構化 Log（如 JSON）對機器友善，但人類難以直接閱讀。</p>
</li>
<li><p><strong>建議</strong>：Log 應該是結構化的，但保留一個人類可讀的訊息欄位（或將其轉化為 Event Name）。<a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=1043">[34:51]</a></p>
</li>
</ul>
<h3 id="heading-4-log">[4. Log 的最佳實踐建議</h3>
<p>來賓們分享了具體的實戰建議：</p>
<ul>
<li><p><strong>撰寫有意義的錯誤訊息</strong>[<a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=2212">36:52]</a></p>
<ul>
<li>Ed強調：當你寫 error log 時，請<strong>告訴讀者（通常是未來的你）該怎麼做</strong>。不要只寫「資料庫連線失敗」，要寫「資料庫連線失敗，正在重試」或「請檢查某設定」。</li>
</ul>
</li>
<li><p><strong>直接傳送 vs</strong>檔案抓取**<a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=513">[42:50]</a></p>
<ul>
<li><p>Jack 建議應用程式應<strong>直接透過網路（OTLP協定）發送Log</strong>後端，而不是寫入檔案再由 Agent 去抓取（Tail）。這樣能更無縫地保留 Context（如 Trace ID）。</p>
</li>
<li><p>Ed 補充建議：在應用程式與後端之間最好有一個<strong>本地的Colle</strong>ctor**。這樣當需要過濾大量垃圾 Log 時，可以在 Coller 端調整設定，而無需重新部署應用程式。</p>
</li>
</ul>
</li>
<li><p><strong>不要濫用 Log</strong></p>
<ul>
<li><p><strong>Metrics vs. Logs</strong><a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=2848">[47:28]</a>：不要用 Lo 來做高基數（High Cardinality）的計數統計，那應該用 Metrics。</p>
</li>
<li><p><strong>Traces vs. Logs</strong> <a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=3816">[48:29]</a>：如果你想知道「請求花費了多少時間」或「請求的路徑」，請使用 Traces，不要用 Log 印出「開始]請求」、「結束請求」。</p>
</li>
</ul>
</li>
<li><p>[<strong>Log 等級（Severity</strong><a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=3816">[01:03:36]</a>：絕對不要記錄沒有等級的 Log。至少要區分 INFO、ERROR、DEBUG，這對過濾和警報至關重要。</p>
</li>
<li><p><strong>安全性</strong> <a target="_blank" href="http://www.youtube.com/watch?v=D-bWeY-gBWU&amp;t=3432">[01:01:53]</a>：小心不要在 Log 中記錄 PII（個人識別資訊）或 Secrets，也不要記錄完整的 HTTP Headers。</p>
</li>
</ul>
<h3 id="heading-5-good-or-bad">5. 快速問答環節（Good or Bad?）</h3>
<p>影片末尾[[57:12]] 進行了一個快問快答遊戲：</p>
<ul>
<li><p><strong>格式化 L</strong>og 字串？** 不建議（應使用參數化/結構化）。</p>
</li>
<li><p><strong>將所有 Excetion 都記為 Error？</strong> 同意。</p>
</li>
<li><p><strong>記錄 Request/Response Body？</strong> 通常不建議（成本太高且易洩漏個資，除非有極強烈的除錯需求。</p>
</li>
<li><p><strong>記錄所有細節？</strong> 不建議，每一個 bytes都要錢，請有意識地選擇要記錄的內容（Be intention）。</p>
</li>
</ul>
<hr />
<p>在 Cloud Native 與微服務架構盛行的 2025 年，Log（日誌）這個最古老的除錯工具，是否已經過時？或者它只是需要一點「現代化」的改造？</p>
<p>最近觀看了 Grafana Labs 與 OpenTelemetry (OTel) 技術委員會成員的一場深度對談（Community Call），主題聚焦於 <strong>Logging Best Practices</strong>。這場討論並非單純的操作教學，而是對於 Log 在現代可觀測性（Observability）中角色的重新定義。</p>
<p>以下整理了影片中的核心觀點，希望能幫助開發者與維運人員打破舊有的 Log 思維，建立更具「意圖性」的可觀測性策略。</p>
<h3 id="heading-1-log">1. 觀念翻轉：Log 的定位與「反模式」</h3>
<p>在 Tracing（追蹤）技術日益普及的今天，很多人會問：「我還需要 Log 嗎？」答案是肯定的，而且 Log 是不可取代的「唯一真實訊號」。</p>
<p>然而，為了追求新技術，社群中出現了一種名為 <strong>Zero Duration Spans（零持續時間 Span）</strong> 的怪現象。</p>
<h4 id="heading-zero-duration-span">什麼是 Zero Duration Span？</h4>
<p>開發者為了利用 Tracing 工具的視覺化，將原本該是 Log 的單點事件，包裝成「開始時間等於結束時間」的 Span。</p>
<p>OpenTelemetry 技術委員會成員 Jack Berg 對此提出了精闢的見解：</p>
<blockquote>
<p><strong>"If it walks like a log, talks like a log, it's a log."</strong> （如果它走起來像 Log，叫起來像 Log，那它就是 Log，別裝了。）</p>
</blockquote>
<h4 id="heading-5yik5pa35qiz5rqw">判斷標準</h4>
<ul>
<li><p><strong>Span (Traces)</strong>：具有層級結構（Hierarchy）與持續時間（Duration）。用來回答「這個請求花了多久？路徑為何？」。</p>
</li>
<li><p><strong>Log</strong>：發生在特定時間點的事件。用來回答「當下發生了什麼事？」。</p>
</li>
</ul>
<p><strong>最佳實踐</strong>：誠實面對資料的本質，不要硬套用 Span 的概念。Log 依然是一等公民。</p>
<h3 id="heading-2">2. 結構化革命：從「給人看」到「機器優先」</h3>
<p>傳統的 Log 是一行行給人類閱讀的字串（Text-based），但在微服務與海量資料的場景下，這種模式已經行不通。OpenTelemetry 正推動 Log 往 <strong>結構化（Structured）</strong> 與 <strong>強型別（Strongly typed）</strong> 發展。</p>
<h4 id="heading-54k65lua6bq86zya6kab57wq5qel5yyw77yf">為什麼需要結構化？</h4>
<ul>
<li><p><strong>查詢效率</strong>：試想在幾億行 Log 中 <code>grep</code> 字串，與在資料庫中查詢 <code>attributes.http_status_code == 500</code> 的區別。</p>
</li>
<li><p><strong>關聯性</strong>：OTel 的目標是讓 Logs、Traces、Metrics 使用統一的語意規範（Semantic Conventions）。</p>
</li>
</ul>
<h4 id="heading-5ywp6zuj6iih6kej5rov">兩難與解法</h4>
<p>結構化 Log（如 JSON）對機器友善，但對人類閱讀不便。</p>
<ul>
<li><strong>建議做法</strong>：保留 Log 的結構化欄位（Attributes）供機器分析，但同時保留一個 <code>message</code> 欄位或是將其轉化為易讀的 <code>Event Name</code> 供人類快速瀏覽。</li>
</ul>
<h3 id="heading-3-file-tail-vs-otlp-direct">3. 架構演進：File Tail vs. OTLP Direct</h3>
<p>你的 Log 是怎麼送到後端的？這涉及到架構的解耦。</p>
<h4 id="heading-file-scraping">傳統模式：File Scraping</h4>
<p>應用程式將 Log 寫入磁碟檔案 -&gt; Agent (如 Promtail) 去抓取 (Tail) -&gt; 傳送後端。</p>
<ul>
<li><strong>缺點</strong>：容易丟失 Context（如 Trace ID），且依賴硬碟 I/O。</li>
</ul>
<h4 id="heading-otlp-direct">現代模式：OTLP Direct</h4>
<p>應用程式透過網路協定（OTLP）直接將 Log 發送出去。</p>
<ul>
<li><strong>優點</strong>：原生支援結構化，保留完整 Context，無需檔案權限。</li>
</ul>
<h4 id="heading-local-collector-pattern">🔥 進階技巧：Local Collector Pattern</h4>
<p>Grafana Loki 的維護者 Ed 提出了一個極佳的架構建議：<strong>在應用程式旁跑一個 Local Collector</strong>（作為 Sidecar 或 DaemonSet）。</p>
<blockquote>
<p><strong>好處</strong>：當你需要將 Log Level 從 INFO 改為 DEBUG 時，你只需要調整 Collector 的過濾規則，<strong>完全不需要重新部署應用程式</strong>。這實現了極致的維運解耦。</p>
</blockquote>
<h3 id="heading-4-log-1">4. 實戰指南：如何寫出「有靈魂」的 Log？</h3>
<p>當開發者敲下 <code>logger.error(...)</code> 時，請記住以下原則：</p>
<h4 id="heading-1-actionable-errors">✅ 1. 寫給未來的自己 (Actionable Errors)</h4>
<p>不要只寫「連線失敗」。錯誤訊息必須包含「接下來該做什麼」。</p>
<ul>
<li><p>❌ Bad: <code>Connection failed.</code></p>
</li>
<li><p>✅ Good: <code>Connection to DB failed, retrying in 5s. Please check network config.</code> <strong>精神：同理心。</strong> 給半夜修 Bug 的人一條明路。</p>
</li>
</ul>
<h4 id="heading-2-context-is-king">✅ 2. Context is King</h4>
<p>沒有 Trace ID 或 User ID 的 Log，在高併發環境下幾乎是雜訊。務必確保 Log 包含足夠的上下文屬性。</p>
<h4 id="heading-3">⛔ 3. 避免高成本操作</h4>
<ul>
<li><p><strong>不要 Log Request Body</strong>：除非在開發環境，否則這會導致儲存成本爆炸，且有洩漏 PII（個資）與 Secrets（金鑰）的風險。</p>
</li>
<li><p><strong>不要用 Log 做統計</strong>：如果你想知道「API 被呼叫幾次」，請用 Metrics，不要用 Log。</p>
</li>
</ul>
<h3 id="heading-be-intentional">總結：Be Intentional (保持意圖)</h3>
<p>整場討論可以用這兩個字總結：<strong>"Be Intentional"</strong>。</p>
<p>Log 不再是廉價的 <code>printf</code>。每一行 Log 都有儲存成本、傳輸成本與認知成本。</p>
<ul>
<li><p>這條 Log 是給誰看的？（人還是機器？）</p>
</li>
<li><p>它的目的是什麼？（除錯、稽核還是統計？）</p>
</li>
<li><p>它包含了足夠的資訊嗎？</p>
</li>
</ul>
<p>在 2025 年，寫好 Log 不僅是技術能力的展現，更是一種對系統架構與團隊協作的深層思考。</p>
<hr />
<blockquote>
<p><em>參考來源：Grafana &amp; OpenTelemetry Community Call - Logging Best Practices</em></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[12 個揭示真正瓶頸的 OpenTelemetry 儀表板]]></title><description><![CDATA[原文 :12 OpenTelemetry Dashboards That Surface Real Bottlenecks https://medium.com/@sparknp1/12-opentelemetry-dashboards-that-surface-real-bottlenecks-f81d36e043a4
「正確的儀表板感覺就像作弊程式碼」。遙測的目標並非擁有更多數據，而是要讓少數視圖清晰易讀，使你的下一步行動顯而易見。實用、簡潔的 OTel 視圖能將雜亂的噪音轉化為具體的解決方...]]></description><link>https://ganhua.wang/12-opentelemetry</link><guid isPermaLink="true">https://ganhua.wang/12-opentelemetry</guid><category><![CDATA[monitoring]]></category><category><![CDATA[OpenTelemetry]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sun, 23 Nov 2025 06:01:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763877698266/d9d3673d-3f85-421a-8e9e-4493e206d7da.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>原文 :<strong>12 OpenTelemetry Dashboards That Surface Real Bottlenecks</strong> <a target="_blank" href="https://medium.com/@sparknp1/12-opentelemetry-dashboards-that-surface-real-bottlenecks-f81d36e043a4">https://medium.com/@sparknp1/12-opentelemetry-dashboards-that-surface-real-bottlenecks-f81d36e043a4</a></p>
<p><strong>「正確的儀表板感覺就像作弊程式碼」</strong>。遙測的目標並非擁有更多數據，而是要<strong>讓少數視圖清晰易讀，使你的下一步行動顯而易見</strong>。實用、簡潔的 OTel 視圖能將雜亂的噪音轉化為具體的解決方案。</p>
</blockquote>
<h2 id="heading-5ymn6kia">前言</h2>
<p>你把一切都可監測了。現在，圖表牆正在回望。哪些實際上可以幫助您解決結帳速度慢、API 不穩定或神秘的 p99 問題？</p>
<p>說實話：<strong>正確的儀表板感覺就像作弊程式碼</strong>。</p>
<p>以下是我使用（並調整過）的 12 個 OpenTelemetry 儀表板，它們一致地揭示了實際瓶頸和下一步行動。</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌─────────────────────────────────────────────────────────┐</span>
<span class="hljs-string">│</span>  <span class="hljs-string">OpenTelemetry</span> <span class="hljs-string">監控全景</span>                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────┐</span>  <span class="hljs-string">┌──────────┐</span>  <span class="hljs-string">┌──────────┐</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>   <span class="hljs-string">應用</span>    <span class="hljs-string">│</span>  <span class="hljs-string">│</span>   <span class="hljs-string">服務</span>    <span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">微服務</span>   <span class="hljs-string">│</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└─────┬────┘</span>  <span class="hljs-string">└─────┬────┘</span>  <span class="hljs-string">└─────┬────┘</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>        <span class="hljs-string">│</span>             <span class="hljs-string">│</span>             <span class="hljs-string">│</span>                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>        <span class="hljs-string">└─────────────┴─────────────┘</span>                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                      <span class="hljs-string">│</span>                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                      <span class="hljs-string">▼</span>                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>            <span class="hljs-string">┌─────────────────┐</span>                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>            <span class="hljs-string">│</span>  <span class="hljs-string">OTel</span> <span class="hljs-string">Collector</span> <span class="hljs-string">│</span>                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>            <span class="hljs-string">└────────┬────────┘</span>                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                     <span class="hljs-string">│</span>                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>         <span class="hljs-string">┌───────────┼───────────┐</span>                       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>         <span class="hljs-string">▼</span>           <span class="hljs-string">▼</span>           <span class="hljs-string">▼</span>                       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-string">┌────────┐</span>  <span class="hljs-string">┌───────┐</span>  <span class="hljs-string">┌────────┐</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-string">│</span> <span class="hljs-string">Metrics│</span>  <span class="hljs-string">│Traces</span> <span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">Logs</span>  <span class="hljs-string">│</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-string">└────────┘</span>  <span class="hljs-string">└───────┘</span>  <span class="hljs-string">└────────┘</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>         <span class="hljs-string">│</span>           <span class="hljs-string">│</span>           <span class="hljs-string">│</span>                       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>         <span class="hljs-string">└───────────┴───────────┘</span>                       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                     <span class="hljs-string">│</span>                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                     <span class="hljs-string">▼</span>                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>         <span class="hljs-string">📊</span> <span class="hljs-number">12</span> <span class="hljs-string">個關鍵儀表板</span>                                <span class="hljs-string">│</span>
<span class="hljs-string">└─────────────────────────────────────────────────────────┘</span>
</code></pre>
<h2 id="heading-8jorydmoljlv4pnm6pmjqflhidooajmnb8">🎯 核心監控儀表板</h2>
<h3 id="heading-1-red-for-every-critical-endpoint-red">1. RED for Every Critical Endpoint（每個關鍵端點的 RED 指標）</h3>
<p><strong>顯示內容</strong>：每條高價值路線的 Rate（速率）、Errors（錯誤）、Duration（持續時間）</p>
<p><strong>為什麼有效</strong>：RED 三元組是快速故障檢測器和深入分析的指南針</p>
<p><strong>如何構建</strong>：使用由 <code>http.route</code> 鍵入的跨度指標（持續時間直方圖 + 錯誤計數）</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># 如何建立：使用由 http.route 鍵入的 span metrics</span>
<span class="hljs-comment"># OpenTelemetry Collector (span metrics)</span>
<span class="hljs-attr">processors:</span>
  <span class="hljs-attr">spanmetrics:</span>
    <span class="hljs-attr">metrics_exporter:</span> <span class="hljs-string">prometheus</span>
    <span class="hljs-attr">dimensions:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">http.route</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">http.method</span>
    <span class="hljs-attr">histogram:</span>
      <span class="hljs-attr">unit:</span> <span class="hljs-string">"ms"</span>
      <span class="hljs-attr">explicit_buckets:</span> [<span class="hljs-number">5</span>, <span class="hljs-number">10</span>, <span class="hljs-number">25</span>, <span class="hljs-number">50</span>, <span class="hljs-number">100</span>, <span class="hljs-number">250</span>, <span class="hljs-number">500</span>, <span class="hljs-number">1000</span>, <span class="hljs-number">2500</span>]

<span class="hljs-attr">service:</span>
  <span class="hljs-attr">pipelines:</span>
    <span class="hljs-attr">traces:</span>
      <span class="hljs-attr">processors:</span> [<span class="hljs-string">spanmetrics</span>]
</code></pre>
<pre><code class="lang-yaml"><span class="hljs-string">┌──────────────────────</span> <span class="hljs-string">RED</span> <span class="hljs-string">儀表板</span> <span class="hljs-string">──────────────────────┐</span>
<span class="hljs-string">│</span>                                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📈</span> <span class="hljs-string">Rate</span> <span class="hljs-string">(請求速率)</span>                                     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/checkout</span>    <span class="hljs-string">████████████████░░</span>  <span class="hljs-number">1</span><span class="hljs-string">,250</span> <span class="hljs-string">req/s</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/login</span>       <span class="hljs-string">████████░░░░░░░░░░</span>    <span class="hljs-number">450</span> <span class="hljs-string">req/s</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/quote</span>       <span class="hljs-string">██████████████████</span>  <span class="hljs-number">1</span><span class="hljs-string">,800</span> <span class="hljs-string">req/s</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">❌</span> <span class="hljs-string">Errors</span> <span class="hljs-string">(錯誤率)</span>                                     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/checkout</span>    <span class="hljs-string">██░░░░░░░░░░░░░░░░</span>    <span class="hljs-number">1.2</span><span class="hljs-string">%</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/login</span>       <span class="hljs-string">░░░░░░░░░░░░░░░░░░</span>    <span class="hljs-number">0.1</span><span class="hljs-string">%</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/quote</span>       <span class="hljs-string">████░░░░░░░░░░░░░░</span>    <span class="hljs-number">2.8</span><span class="hljs-string">%</span> <span class="hljs-string">⚠️</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">⏱️</span>  <span class="hljs-string">Duration</span> <span class="hljs-string">(回應時間)</span>                                <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/checkout</span>    <span class="hljs-attr">p50: 120ms  p95: 450ms  p99:</span> <span class="hljs-string">890ms</span>     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/login</span>       <span class="hljs-attr">p50:  45ms  p95: 125ms  p99:</span> <span class="hljs-string">280ms</span>     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">/quote</span>       <span class="hljs-attr">p50: 180ms  p95: 620ms  p99:</span> <span class="hljs-number">1.</span><span class="hljs-string">2s</span> <span class="hljs-string">🔥</span>   <span class="hljs-string">│</span>
<span class="hljs-string">└────────────────────────────────────────────────────────┘</span>
</code></pre>
<p>要建立每個關鍵端點的 RED 指標，您需要使用 OpenTelemetry Collector 中的 spanmetrics 處理器。這是將追蹤 (Traces) 數據轉換為指標 (Metrics) 的核心步驟。</p>
<p>具體的 Collector 配置示例如下，它使用 http.route 和 http.method 作為維度鍵入跨度指標：</p>
<pre><code class="lang-c"># 如何建立：使用由 http.route 鍵入的 span metrics [<span class="hljs-number">6</span>]
processors:
  spanmetrics:
    metrics_exporter: prometheus
    dimensions:
      - name: http.route
      - name: http.method
    histogram:
      unit: <span class="hljs-string">"ms"</span>
      # 設定明確的持續時間分桶，用於精確計算延遲百分位數（例如 p95, p99）
      explicit_buckets: [<span class="hljs-number">9</span><span class="hljs-number">-11</span>] [<span class="hljs-number">5</span>, <span class="hljs-number">6</span>]

service:
  pipelines:
    traces:
      processors: [spanmetrics] [<span class="hljs-number">6</span>, <span class="hljs-number">12</span>]
</code></pre>
<blockquote>
<p>💡 <strong>解讀技巧</strong>：p95 在流量沒有增長的情況下突然上升通常意味著飽和或下游依賴性問題。</p>
</blockquote>
<h3 id="heading-2-tail-latency-anatomy">2. Tail Latency Anatomy（尾部延遲剖析）</h3>
<p><strong>顯示內容</strong>：隨時間變化的 p50/p95/p99，加上差異面板 (p99–p50)</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌────────────────</span> <span class="hljs-string">尾部延遲追蹤</span> <span class="hljs-string">────────────────┐</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">延遲分佈</span> <span class="hljs-string">(ms)</span>                                <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">2000</span><span class="hljs-string">│</span>                              <span class="hljs-string">╱╲</span>       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>      <span class="hljs-string">│</span>                            <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">1500</span><span class="hljs-string">│</span>                          <span class="hljs-string">╱</span>        <span class="hljs-string">╲</span>   <span class="hljs-string">│</span> <span class="hljs-string">p99</span>
<span class="hljs-string">│</span>      <span class="hljs-string">│</span>                        <span class="hljs-string">╱</span>            <span class="hljs-string">╲</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">1000</span><span class="hljs-string">│</span>              <span class="hljs-string">╱╲</span>      <span class="hljs-string">╱</span>                <span class="hljs-string">│</span>
<span class="hljs-string">│</span>      <span class="hljs-string">│</span>            <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>  <span class="hljs-string">╱</span>                  <span class="hljs-string">│</span> <span class="hljs-string">p95</span>
<span class="hljs-string">│</span>   <span class="hljs-number">500</span><span class="hljs-string">│</span>      <span class="hljs-string">╱╲</span>  <span class="hljs-string">╱</span>        <span class="hljs-string">╲</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>      <span class="hljs-string">│</span>    <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>                             <span class="hljs-string">│</span> <span class="hljs-string">p50</span>
<span class="hljs-string">│</span>      <span class="hljs-string">│</span>  <span class="hljs-string">╱</span>                                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-number">0</span><span class="hljs-string">└──┴───┴───┴───┴───┴───┴───┴───┴───┴─</span>  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>       <span class="hljs-number">8</span><span class="hljs-string">:00</span> <span class="hljs-number">9</span><span class="hljs-string">:00</span> <span class="hljs-number">10</span><span class="hljs-string">:00</span> <span class="hljs-number">11</span><span class="hljs-string">:00</span> <span class="hljs-number">12</span><span class="hljs-string">:00</span> <span class="hljs-number">13</span><span class="hljs-string">:00</span>     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">差異分析</span> <span class="hljs-string">(p99</span> <span class="hljs-bullet">-</span> <span class="hljs-string">p50)</span>                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</span>                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">正常時段:</span>  <span class="hljs-string">~200ms</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">高峰時段:</span>  <span class="hljs-string">~1,100ms</span> <span class="hljs-string">⚠️</span>  <span class="hljs-string">(5.5x</span> <span class="hljs-string">差距)</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">💡</span> <span class="hljs-string">建議:</span> <span class="hljs-string">檢查</span> <span class="hljs-number">11</span><span class="hljs-string">:00</span> <span class="hljs-string">時段的依賴服務</span>             <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：</p>
<ul>
<li><p>p50 代表用戶幸福感</p>
</li>
<li><p>p99 代表執行壓力</p>
</li>
<li><p>差距顯示長尾疼痛</p>
</li>
</ul>
<p>🎯 <strong>專業提示</strong>：從追蹤中添加示例，直接從尖峰跳到有問題的跨度。</p>
<h3 id="heading-3-saturation-heatmap-by-resource">3. Saturation Heatmap by Resource（按資源劃分的飽和度熱圖）</h3>
<p><strong>顯示內容</strong>：按服務劃分的 CPU、內存、I/O 和線程池利用率，按剩餘空間排序</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌───────────────</span> <span class="hljs-string">資源飽和度熱圖</span> <span class="hljs-string">───────────────┐</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">服務名稱</span>      <span class="hljs-string">CPU</span>    <span class="hljs-string">Memory</span>   <span class="hljs-string">I/O</span>   <span class="hljs-string">Thread</span>  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">────────────────────────────────────────</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">api-gateway</span>   <span class="hljs-string">██░░</span>   <span class="hljs-string">███░░</span>   <span class="hljs-string">█░░░</span>   <span class="hljs-string">████</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                <span class="hljs-number">52</span><span class="hljs-string">%</span>     <span class="hljs-number">68</span><span class="hljs-string">%</span>     <span class="hljs-number">18</span><span class="hljs-string">%</span>     <span class="hljs-number">89</span><span class="hljs-string">%</span> <span class="hljs-string">🔥│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">auth-service</span>  <span class="hljs-string">████</span>   <span class="hljs-string">████░</span>   <span class="hljs-string">██░░</span>   <span class="hljs-string">███░</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                <span class="hljs-number">89</span><span class="hljs-string">%</span> <span class="hljs-string">🔥</span>  <span class="hljs-number">92</span><span class="hljs-string">%</span> <span class="hljs-string">🔥</span>  <span class="hljs-number">42</span><span class="hljs-string">%</span>     <span class="hljs-number">72</span><span class="hljs-string">%</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">payment-svc</span>   <span class="hljs-string">█░░░</span>   <span class="hljs-string">██░░░</span>   <span class="hljs-string">████</span>   <span class="hljs-string">██░░</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                <span class="hljs-number">23</span><span class="hljs-string">%</span>     <span class="hljs-number">48</span><span class="hljs-string">%</span>     <span class="hljs-number">91</span><span class="hljs-string">%</span> <span class="hljs-string">🔥</span>  <span class="hljs-number">45</span><span class="hljs-string">%</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">user-service</span>  <span class="hljs-string">██░░</span>   <span class="hljs-string">███░░</span>   <span class="hljs-string">█░░░</span>   <span class="hljs-string">███░</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                <span class="hljs-number">55</span><span class="hljs-string">%</span>     <span class="hljs-number">71</span><span class="hljs-string">%</span>     <span class="hljs-number">25</span><span class="hljs-string">%</span>     <span class="hljs-number">68</span><span class="hljs-string">%</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">▓▓▓▓</span> <span class="hljs-string">&gt;</span> <span class="hljs-number">80</span><span class="hljs-string">%</span> <span class="hljs-string">(危險)</span>  <span class="hljs-string">███</span> <span class="hljs-number">60</span><span class="hljs-number">-80</span><span class="hljs-string">%</span>  <span class="hljs-string">██</span> <span class="hljs-number">40</span><span class="hljs-number">-60</span><span class="hljs-string">%</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">█</span> <span class="hljs-string">&lt;</span> <span class="hljs-number">40</span><span class="hljs-string">%</span> <span class="hljs-string">(健康)</span>                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">⚠️</span>  <span class="hljs-string">警報:</span> <span class="hljs-string">auth-service</span> <span class="hljs-string">CPU</span> <span class="hljs-string">&amp;</span> <span class="hljs-string">Memory</span> <span class="hljs-string">接近飽和</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">💡</span> <span class="hljs-string">建議:</span> <span class="hljs-string">擴展</span> <span class="hljs-string">payment-svc</span> <span class="hljs-string">I/O</span> <span class="hljs-string">容量</span>           <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：如果當淨空下降到低於 ~20% 時 p95 上升，那麼您就發現了資源瓶頸</p>
<p><strong>建議添加</strong>：</p>
<ul>
<li><p>限制標誌（CPU 限制計數、容器 OOM 終止）</p>
</li>
<li><p>每個服務的隊列積壓</p>
</li>
</ul>
<h2 id="heading-8jtiidkvp3os7toiifmgkfog73ov73ouaq">📊 依賴與性能追蹤</h2>
<h3 id="heading-4-queue-health-depth-age-drains">4. Queue Health: Depth × Age × Drains（隊列健康度）</h3>
<p>當服務在高負載下，隊列成為了減震器，但同時也可能是問題的「犯罪現場」。在監控隊列健康度（深度、年齡、消耗率）時，應遵循以下分類規則：</p>
<p>• <strong>分類規則</strong>：如果隊列深度 (Depth) 增加且最早的消息年齡 (Age) 攀升，而消費者消耗率 (Drains) 保持平坦，這表示瓶頸在於消費者數量不足，或者下游服務運行緩慢，應優先<strong>增加消費者或解決下游緩慢問題</strong>。</p>
<p><strong>顯示內容</strong>：消息深度、最早的消息年齡以及每個隊列/主題的消費者消耗率</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌──────────────</span> <span class="hljs-string">隊列健康監控</span> <span class="hljs-string">──────────────┐</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📬</span> <span class="hljs-string">order-queue</span>                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">深度:</span> <span class="hljs-number">1</span><span class="hljs-string">,240</span> <span class="hljs-string">msgs</span>  <span class="hljs-string">████████░░░</span>       <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">年齡:</span> <span class="hljs-number">8.5</span> <span class="hljs-string">min</span>     <span class="hljs-string">████████████░</span> <span class="hljs-string">🔥</span>  <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">消耗:</span> <span class="hljs-number">145</span> <span class="hljs-string">msg/s</span>   <span class="hljs-string">██████░░░░░░</span>      <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📬</span> <span class="hljs-string">notification-queue</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">深度:</span> <span class="hljs-number">45</span> <span class="hljs-string">msgs</span>     <span class="hljs-string">█░░░░░░░░░</span>        <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">年齡:</span> <span class="hljs-number">1.2</span> <span class="hljs-string">min</span>     <span class="hljs-string">██░░░░░░░░</span>        <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">消耗:</span> <span class="hljs-number">380</span> <span class="hljs-string">msg/s</span>   <span class="hljs-string">████████████</span>      <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📬</span> <span class="hljs-string">payment-queue</span>                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">深度:</span> <span class="hljs-number">3</span><span class="hljs-string">,890</span> <span class="hljs-string">msgs</span>  <span class="hljs-string">████████████░</span> <span class="hljs-string">⚠️</span>  <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">年齡:</span> <span class="hljs-number">15.3</span> <span class="hljs-string">min</span>    <span class="hljs-string">████████████████</span> <span class="hljs-string">🔥│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">消耗:</span> <span class="hljs-number">62</span> <span class="hljs-string">msg/s</span>    <span class="hljs-string">███░░░░░░░░░</span>      <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">⚠️</span>  <span class="hljs-string">payment-queue</span> <span class="hljs-string">積壓嚴重</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">💡</span> <span class="hljs-string">建議:</span> <span class="hljs-string">增加消費者或檢查下游服務</span>         <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：在負載情況下，隊列會成為您的減震器——或者成為您的犯罪現場</p>
<p>⚠️ <strong>分類規則</strong>：如果深度增加且年齡增加，而排水管保持平坦 → 首先添加消費者或解決下游緩慢問題</p>
<hr />
<h3 id="heading-5-dependency-waterfall">5. Dependency Waterfall（依賴瀑布）</h3>
<p><strong>顯示內容</strong>：按 <code>peer.service</code>/<code>db.system</code> 聚合的跨度指標及對總延遲的貢獻</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌────────────────</span> <span class="hljs-string">依賴瀑布分析</span> <span class="hljs-string">────────────────┐</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">請求:</span> <span class="hljs-string">GET</span> <span class="hljs-string">/checkout</span> <span class="hljs-string">(總時長:</span> <span class="hljs-number">1</span><span class="hljs-string">,240ms)</span>        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├─</span> <span class="hljs-string">API</span> <span class="hljs-string">Gateway</span>           <span class="hljs-string">45ms</span>   <span class="hljs-string">██</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├─</span> <span class="hljs-string">Auth</span> <span class="hljs-string">Service</span>          <span class="hljs-string">120ms</span>  <span class="hljs-string">█████</span>      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├─</span> <span class="hljs-string">User</span> <span class="hljs-string">Service</span>           <span class="hljs-string">85ms</span>  <span class="hljs-string">███</span>        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├─</span> <span class="hljs-string">Inventory</span> <span class="hljs-string">Check</span>       <span class="hljs-string">180ms</span>  <span class="hljs-string">███████</span>    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├─</span> <span class="hljs-string">Tax</span> <span class="hljs-string">Calculator</span>        <span class="hljs-string">680ms</span>  <span class="hljs-string">████████████████████</span> <span class="hljs-string">🔥</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>   <span class="hljs-string">(第三方</span> <span class="hljs-string">API)</span>                            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├─</span> <span class="hljs-string">Payment</span> <span class="hljs-string">Gateway</span>       <span class="hljs-string">95ms</span>   <span class="hljs-string">████</span>       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└─</span> <span class="hljs-string">Order</span> <span class="hljs-string">Creation</span>        <span class="hljs-string">35ms</span>   <span class="hljs-string">█</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">延遲貢獻分析:</span>                             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬</span>                     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">Tax</span> <span class="hljs-string">Calculator</span>    <span class="hljs-number">54.8</span><span class="hljs-string">%</span>  <span class="hljs-string">████████████████</span>  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">Inventory</span> <span class="hljs-string">Check</span>   <span class="hljs-number">14.5</span><span class="hljs-string">%</span>  <span class="hljs-string">████</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">Auth</span> <span class="hljs-string">Service</span>       <span class="hljs-number">9.7</span><span class="hljs-string">%</span>  <span class="hljs-string">███</span>               <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">其他</span>              <span class="hljs-number">21.0</span><span class="hljs-string">%</span>  <span class="hljs-string">██████</span>             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🎯</span> <span class="hljs-string">優化目標:</span> <span class="hljs-string">優先處理</span> <span class="hljs-string">Tax</span> <span class="hljs-string">Calculator</span>         <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：快速指出罪魁禍首：數據庫、身份驗證、緩存、第三方 API</p>
<p>📝 <strong>真實案例</strong>：我發現 60% 的結帳延遲與隱藏在三層深處的「稅務計算器」跨度相關。這個視圖在五分鐘內就暴露了問題。</p>
<hr />
<h3 id="heading-6-n1-query-detectorn1">6. N+1 Query Detector（N+1 查詢檢測器）</h3>
<p>N+1 查詢模式通常表現為部署後每個請求的跨度中值計數跳躍，隨之而來的是延遲攀升。</p>
<p><strong>顯示內容</strong>：每個事務的請求與響應時間，以及每個路由跨度計數分佈</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌───────────────</span> <span class="hljs-string">N+1</span> <span class="hljs-string">查詢檢測</span> <span class="hljs-string">───────────────┐</span>
<span class="hljs-string">│</span>                                            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">路由:</span> <span class="hljs-string">/api/users/{id}/orders</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">部署前</span> <span class="hljs-string">(v1.2.3)</span> <span class="hljs-string">│</span> <span class="hljs-string">部署後</span> <span class="hljs-string">(v1.2.4)</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">─────────────────────────────────────</span>    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">每請求跨度數:</span>                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────┐</span>  <span class="hljs-string">│</span>  <span class="hljs-string">┌──────────┐</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>    <span class="hljs-number">8</span>     <span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">│</span>   <span class="hljs-number">156</span>    <span class="hljs-string">│</span> <span class="hljs-string">🔥</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">spans</span>   <span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">spans</span>   <span class="hljs-string">│</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└──────────┘</span>  <span class="hljs-string">│</span>  <span class="hljs-string">└──────────┘</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">平均延遲:</span>                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────┐</span>  <span class="hljs-string">│</span>  <span class="hljs-string">┌──────────┐</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">245ms</span>   <span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-number">3</span><span class="hljs-string">,840ms</span> <span class="hljs-string">│</span> <span class="hljs-string">🔥</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└──────────┘</span>  <span class="hljs-string">│</span>  <span class="hljs-string">└──────────┘</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔍</span> <span class="hljs-string">檢測到的模式:</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-attr">for order in orders:</span>                     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-string">├─</span> <span class="hljs-string">SELECT</span> <span class="hljs-string">*</span> <span class="hljs-string">FROM</span> <span class="hljs-string">order_items</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-string">├─</span> <span class="hljs-string">SELECT</span> <span class="hljs-string">*</span> <span class="hljs-string">FROM</span> <span class="hljs-string">order_items</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-string">├─</span> <span class="hljs-string">SELECT</span> <span class="hljs-string">*</span> <span class="hljs-string">FROM</span> <span class="hljs-string">order_items</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-string">└─</span> <span class="hljs-string">...</span> <span class="hljs-string">(重複</span> <span class="hljs-number">150</span><span class="hljs-string">+</span> <span class="hljs-string">次)</span> <span class="hljs-string">⚠️</span>                <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">💡</span> <span class="hljs-string">建議:</span> <span class="hljs-string">使用</span> <span class="hljs-string">JOIN</span> <span class="hljs-string">或批量查詢優化</span>          <span class="hljs-string">│</span>
<span class="hljs-string">└────────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：如果在部署後每個請求的跨度中值計數跳躍並且延遲攀升，則您可能引入了 N+1 模式</p>
<p>🚨 <strong>簡單啟發式警報</strong>：當超過 15 分鐘的路由 <strong>spans_per_request p95 增加超過 25%</strong> 時，應發出警報，這極有可能表示引入了 N+1 模式。</p>
<h2 id="heading-slo">🎯 SLO 與錯誤管理</h2>
<h3 id="heading-7-error-budget-burn">7. Error Budget Burn（錯誤預算消耗）</h3>
<p><strong>顯示內容</strong>：服務 SLO（可用性/延遲）、當前錯誤預算和消耗率（1h &amp; 6h 窗口）</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌────────────────</span> <span class="hljs-string">SLO</span> <span class="hljs-string">錯誤預算監控</span> <span class="hljs-string">────────────────┐</span>
<span class="hljs-string">│</span>                                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">服務:</span> <span class="hljs-string">payment-service</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">SLO</span> <span class="hljs-string">目標:</span> <span class="hljs-number">99.9</span><span class="hljs-string">%</span> <span class="hljs-string">(30</span> <span class="hljs-string">天)</span>                         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">剩餘錯誤預算</span>                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">████████████████████░░░░░░░░</span>  <span class="hljs-number">68.2</span><span class="hljs-string">%</span> <span class="hljs-string">(19.5天)</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔥</span> <span class="hljs-string">消耗率分析</span>                                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">時間窗口</span>    <span class="hljs-string">消耗率</span>      <span class="hljs-string">狀態</span>                  <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├────────────────────────────────────────────┤</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-number">1</span> <span class="hljs-string">小時</span>     <span class="hljs-number">2.8</span><span class="hljs-string">%/hr</span>    <span class="hljs-string">🔴</span> <span class="hljs-string">高速消耗</span> <span class="hljs-string">(&gt;2%)</span>      <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-number">6</span> <span class="hljs-string">小時</span>     <span class="hljs-number">1.2</span><span class="hljs-string">%/hr</span>    <span class="hljs-string">🟡</span> <span class="hljs-string">中速消耗</span>            <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-number">24</span> <span class="hljs-string">小時</span>    <span class="hljs-number">0.6</span><span class="hljs-string">%/hr</span>    <span class="hljs-string">🟢</span> <span class="hljs-string">正常</span>                <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📈</span> <span class="hljs-string">消耗趨勢</span> <span class="hljs-string">(過去</span> <span class="hljs-number">24</span> <span class="hljs-string">小時)</span>                       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-string">%│</span>      <span class="hljs-string">╱╲</span>                    <span class="hljs-string">╱╲</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">3</span> <span class="hljs-string">│</span>    <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>                <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">2</span> <span class="hljs-string">│</span>  <span class="hljs-string">╱</span>        <span class="hljs-string">╲</span>            <span class="hljs-string">╱</span>        <span class="hljs-string">╲</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">1</span> <span class="hljs-string">│╱</span>            <span class="hljs-string">╲</span>        <span class="hljs-string">╱</span>            <span class="hljs-string">╲</span>        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">0</span> <span class="hljs-string">└──┴───┴───┴───┴───┴───┴───┴───┴───┴──</span>      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-number">0</span>   <span class="hljs-number">4</span>   <span class="hljs-number">8</span>  <span class="hljs-number">12</span>  <span class="hljs-number">16</span>  <span class="hljs-number">20</span>  <span class="hljs-number">24</span> <span class="hljs-string">(小時)</span>             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">⚠️</span>  <span class="hljs-string">警報:</span> <span class="hljs-number">1</span><span class="hljs-string">小時消耗率超過閾值</span> <span class="hljs-string">(&gt;2%)</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🚨</span> <span class="hljs-string">行動:</span> <span class="hljs-string">立即調查並停止非關鍵部署</span>                <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：</p>
<ul>
<li><p>一小時內消耗 2% 的預算 = "停止一切"</p>
</li>
<li><p>每週消耗 2% = "計劃修復"</p>
</li>
</ul>
<pre><code class="lang-yaml"><span class="hljs-comment"># Example SLO label enrichment</span>
<span class="hljs-attr">processors:</span>
  <span class="hljs-attr">attributes:</span>
    <span class="hljs-attr">actions:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">slo.target</span>
        <span class="hljs-attr">value:</span> <span class="hljs-string">"0.99"</span>
        <span class="hljs-attr">action:</span> <span class="hljs-string">insert</span>
</code></pre>
<blockquote>
<p>💡 <strong>儀表板提示</strong>：按預算跑道（剩餘天數）對服務進行分組，這是非常有效的聚焦工具。</p>
</blockquote>
<h2 id="heading-4pqz77ipiombiihjoazguiihizha6kowequwmlg">⚙️ 運行時與資源優化</h2>
<h3 id="heading-8-gc-amp-alloc-pressuregc">8. GC &amp; Alloc Pressure（GC 和分配壓力）</h3>
<p>對於使用 JVM、.NET、Go 的服務，GC（垃圾回收）暫停會佔用 CPU 時間並影響延遲。</p>
<p><strong>顯示內容</strong>：GC 暫停時間百分位、分配率、使用中的堆和提升率</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌────────────────</span> <span class="hljs-string">JVM</span> <span class="hljs-string">GC</span> <span class="hljs-string">壓力監控</span> <span class="hljs-string">────────────────┐</span>
<span class="hljs-string">│</span>                                                 <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">服務:</span> <span class="hljs-string">order-processor-service</span> <span class="hljs-string">(JVM)</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                 <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🗑️</span> <span class="hljs-string">GC</span> <span class="hljs-string">暫停時間分佈</span>                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">ms│</span>                                            <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">500</span><span class="hljs-string">│</span>                          <span class="hljs-string">╱╲</span>                <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">400</span><span class="hljs-string">│</span>                        <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">300</span><span class="hljs-string">│</span>              <span class="hljs-string">╱╲</span>      <span class="hljs-string">╱</span>        <span class="hljs-string">╲</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">200</span><span class="hljs-string">│</span>      <span class="hljs-string">╱╲</span>    <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>  <span class="hljs-string">╱</span>            <span class="hljs-string">╲</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">100</span><span class="hljs-string">│</span>    <span class="hljs-string">╱</span>    <span class="hljs-string">╲╱</span>        <span class="hljs-string">╲</span>                <span class="hljs-string">╲</span>       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-number">0</span><span class="hljs-string">└──┴───┴───┴───┴───┴───┴───┴───┴───┴──</span>      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-number">8</span><span class="hljs-string">:00</span> <span class="hljs-number">9</span><span class="hljs-string">:00</span> <span class="hljs-number">10</span><span class="hljs-string">:00</span> <span class="hljs-number">11</span><span class="hljs-string">:00</span> <span class="hljs-number">12</span><span class="hljs-string">:00</span> <span class="hljs-number">13</span><span class="hljs-string">:00</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                 <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">關鍵指標</span>                                     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌───────────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">GC</span> <span class="hljs-string">暫停</span> <span class="hljs-attr">p99:</span>  <span class="hljs-string">380ms</span>  <span class="hljs-string">████████████░</span>  <span class="hljs-string">🔥</span>    <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">分配率:</span>       <span class="hljs-number">2.</span><span class="hljs-string">4GB/s</span> <span class="hljs-string">███████████████</span>      <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">堆使用:</span>       <span class="hljs-number">87</span><span class="hljs-string">%</span>     <span class="hljs-string">████████████████</span> <span class="hljs-string">🔥</span>  <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">提升率:</span>       <span class="hljs-string">450MB/s</span> <span class="hljs-string">████████░░░░░</span>        <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└───────────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                 <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📈</span> <span class="hljs-string">GC</span> <span class="hljs-string">vs</span> <span class="hljs-string">延遲相關性</span>                             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>      <span class="hljs-string">p95</span> <span class="hljs-string">延遲</span> <span class="hljs-string">(ms)</span>                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">800</span><span class="hljs-string">│</span>  <span class="hljs-string">●</span>                    <span class="hljs-string">●</span>                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">600</span><span class="hljs-string">│</span>     <span class="hljs-string">●</span>              <span class="hljs-string">●</span>                       <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">400</span><span class="hljs-string">│</span>        <span class="hljs-string">●</span>        <span class="hljs-string">●</span>         <span class="hljs-string">●</span>                <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">200</span><span class="hljs-string">│</span>           <span class="hljs-string">●</span>  <span class="hljs-string">●</span>               <span class="hljs-string">●</span>             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-number">0</span><span class="hljs-string">└──┴───┴───┴───┴───┴───┴───┴───┴──</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>      <span class="hljs-string">GC</span> <span class="hljs-string">暫停時間</span> <span class="hljs-string">(ms)</span> <span class="hljs-string">→</span>                         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                 <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">💡</span> <span class="hljs-string">相關係數:</span> <span class="hljs-number">0.89</span> <span class="hljs-string">(強相關)</span>                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔧</span> <span class="hljs-string">建議:</span> <span class="hljs-string">調整堆大小或優化熱路徑對象分配</span>          <span class="hljs-string">│</span>
<span class="hljs-string">└─────────────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：當 p95 因 GC 暫停而增加時，CPU 時間被內存攪動所佔用</p>
<p>🔧 <strong>行動模式</strong>：</p>
<ul>
<li><p>• <strong>流量高峰期間暫停峰值</strong>：應<strong>減少熱路徑上的分配</strong>或調整堆/GC 配置。</p>
<p>  • <strong>部署後暫停峰值</strong>：應<strong>檢查物件生命週期</strong>和緩存更改，可能是新的程式碼導致記憶體攪動 (memory churn)。</p>
</li>
</ul>
<hr />
<h3 id="heading-9-cold-starts-amp-autoscaling-granularity">9. Cold Starts &amp; Autoscaling Granularity（冷啟動和自動縮放）</h3>
<p><strong>顯示內容</strong>：冷啟動跨度的計數和延遲、容器啟動率以及隨時間的擴展事件</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌──────────</span> <span class="hljs-string">冷啟動與自動縮放監控</span> <span class="hljs-string">──────────┐</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">服務:</span> <span class="hljs-string">lambda-image-processor</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">❄️</span> <span class="hljs-string">冷啟動統計</span> <span class="hljs-string">(過去</span> <span class="hljs-number">1</span> <span class="hljs-string">小時)</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">冷啟動次數:</span>    <span class="hljs-number">47</span> <span class="hljs-string">次</span>                <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">冷啟動率:</span>      <span class="hljs-number">23.5</span><span class="hljs-string">%</span>  <span class="hljs-string">⚠️</span>            <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">平均延遲:</span>      <span class="hljs-number">2</span><span class="hljs-string">,340ms</span>              <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">暖啟動延遲:</span>    <span class="hljs-string">145ms</span>                <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">延遲差距:</span>      <span class="hljs-number">16.</span><span class="hljs-string">1x</span>  <span class="hljs-string">🔥</span>            <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">啟動類型分佈</span>                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">❄️</span> <span class="hljs-string">冷啟動</span>  <span class="hljs-string">████████░░░░░░░░</span>  <span class="hljs-number">47</span>     <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">🔥</span> <span class="hljs-string">暖啟動</span>  <span class="hljs-string">████████████████</span>  <span class="hljs-number">153</span>    <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">⚡</span> <span class="hljs-string">擴展事件時間線</span>                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">實例數</span>                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-number">20</span><span class="hljs-string">│</span>        <span class="hljs-string">┌───┐</span>      <span class="hljs-string">┌───┐</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-number">15</span><span class="hljs-string">│</span>    <span class="hljs-string">┌───┤</span>   <span class="hljs-string">├───┐</span>  <span class="hljs-string">│</span>   <span class="hljs-string">│</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-number">10</span><span class="hljs-string">│</span>  <span class="hljs-string">┌─┤</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>   <span class="hljs-string">├──┤</span>   <span class="hljs-string">├─┐</span>        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-number">5</span><span class="hljs-string">│──┤</span> <span class="hljs-string">│</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>  <span class="hljs-string">│</span>   <span class="hljs-string">│</span> <span class="hljs-string">├──</span>      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-number">0</span><span class="hljs-string">└──┴─┴───┴───┴───┴──┴───┴─┴──</span>      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>      <span class="hljs-number">8</span>  <span class="hljs-number">9</span>  <span class="hljs-number">10</span>  <span class="hljs-number">11</span>  <span class="hljs-number">12</span>  <span class="hljs-number">13</span>  <span class="hljs-number">14</span> <span class="hljs-string">(時)</span>      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>       <span class="hljs-string">↑冷啟動峰值</span>                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">💡</span> <span class="hljs-string">優化建議:</span>                             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">設置最小實例數:</span> <span class="hljs-number">3</span> <span class="hljs-string">→</span> <span class="hljs-number">5</span>                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">預熱關鍵依賴</span> <span class="hljs-string">(DB</span> <span class="hljs-string">連接、配置)</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">延遲加載非關鍵模組</span>                     <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：Serverless 和 k8s 自動縮放會帶來突發懲罰，此儀表板將其量化</p>
<p>🔧 <strong>修復路徑</strong>：預熱、調整最小實例或將重初始化從冷路徑轉移到穩定狀態</p>
<hr />
<h3 id="heading-10-retrytimeout-feedback-loop">10. Retry/Timeout Feedback Loop（重試/超時反饋循環）</h3>
<p><strong>顯示內容</strong>：重試計數、超時率以及由此產生的下游 QPS</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌───────────</span> <span class="hljs-string">重試與超時反饋循環</span> <span class="hljs-string">───────────┐</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔄</span> <span class="hljs-string">重試模式分析</span>                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">原始請求</span> <span class="hljs-string">vs</span> <span class="hljs-string">實際請求</span> <span class="hljs-string">(包含重試)</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">QPS</span> <span class="hljs-string">│</span>                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">800</span> <span class="hljs-string">│</span>              <span class="hljs-string">╱╲</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">600</span> <span class="hljs-string">│</span>            <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>   <span class="hljs-string">╱╲</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">400</span> <span class="hljs-string">│</span>          <span class="hljs-string">╱</span>        <span class="hljs-string">╲╱</span>  <span class="hljs-string">╲</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">200</span> <span class="hljs-string">│</span>    <span class="hljs-string">────╱</span>                <span class="hljs-string">╲────</span>     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>      <span class="hljs-string">│</span>  <span class="hljs-string">══════════════════════════════</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-number">0</span> <span class="hljs-string">└──┴───┴───┴───┴───┴───┴───┴──</span>     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>       <span class="hljs-number">8</span>   <span class="hljs-number">9</span>   <span class="hljs-number">10</span>  <span class="hljs-number">11</span>  <span class="hljs-number">12</span>  <span class="hljs-number">13</span>  <span class="hljs-number">14</span> <span class="hljs-string">(時)</span>    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>       <span class="hljs-string">──</span> <span class="hljs-string">原始</span>  <span class="hljs-string">══</span> <span class="hljs-string">實際</span> <span class="hljs-string">(含重試)</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">放大倍數</span>                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">時間</span>      <span class="hljs-string">原始QPS</span>  <span class="hljs-string">實際QPS</span>  <span class="hljs-string">倍數</span>     <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├────────────────────────────────────┤</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-number">10</span><span class="hljs-string">:00</span>    <span class="hljs-number">200</span>      <span class="hljs-number">580</span>      <span class="hljs-number">2.</span><span class="hljs-string">9x</span>    <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-number">11</span><span class="hljs-string">:00</span>    <span class="hljs-number">250</span>      <span class="hljs-number">750</span>      <span class="hljs-number">3.</span><span class="hljs-string">0x</span> <span class="hljs-string">🔥</span> <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-number">12</span><span class="hljs-string">:00</span>    <span class="hljs-number">180</span>      <span class="hljs-number">520</span>      <span class="hljs-number">2.</span><span class="hljs-string">9x</span>    <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">⚠️</span> <span class="hljs-string">重試風暴分析</span>                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────────┐</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">原始錯誤:</span>  <span class="hljs-number">2.5</span><span class="hljs-string">%</span>                     <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">重試策略:</span>  <span class="hljs-string">指數退避</span> <span class="hljs-string">(3</span> <span class="hljs-string">次最大)</span>       <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">結果影響:</span>  <span class="hljs-string">下游流量</span> <span class="hljs-string">+190%</span>           <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>           <span class="hljs-string">延遲增加</span> <span class="hljs-string">+340%</span>            <span class="hljs-string">│</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────────┘</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔧</span> <span class="hljs-string">建議修復:</span>                             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">添加</span> <span class="hljs-string">jitter</span> <span class="hljs-string">避免重試同步</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">降低最大重試次數:</span> <span class="hljs-number">3</span> <span class="hljs-string">→</span> <span class="hljs-number">2</span>               <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">實施斷路器保護下游</span>                    <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：積極的重試會將微小的信號變成交通風暴，您會看到「迴聲」</p>
<p>⚖️ <strong>經驗法則</strong>：</p>
<ul>
<li><p>錯誤是暫時的且短暫的 → 在抖動情況下限制重試</p>
</li>
<li><p>錯誤持續存在 → 快速失敗並減輕負載以保護核心</p>
</li>
</ul>
<hr />
<h3 id="heading-11-cache-efficacy-amp-eviction-storms">11. Cache Efficacy &amp; Eviction Storms（緩存效率和驅逐風暴）</h3>
<p><strong>顯示內容</strong>：命中率（按鍵空間）、獲取/設置延遲、驅逐和連接池統計</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌────────────</span> <span class="hljs-string">緩存效率監控</span> <span class="hljs-string">────────────┐</span>
<span class="hljs-string">│</span>                                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🎯</span> <span class="hljs-string">緩存命中率趨勢</span>                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-string">%│</span>                                 <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">100</span><span class="hljs-string">│</span> <span class="hljs-string">─────────────╲</span>                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">80</span><span class="hljs-string">│</span>               <span class="hljs-string">╲</span>   <span class="hljs-string">╱─────</span>       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">60</span><span class="hljs-string">│</span>                <span class="hljs-string">╲╱</span>        <span class="hljs-string">⚠️</span>     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">40</span><span class="hljs-string">│</span>                                <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">20</span><span class="hljs-string">│</span>                                <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-number">0</span><span class="hljs-string">└──┴───┴───┴───┴───┴───┴──</span>      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-number">8</span>   <span class="hljs-number">9</span>   <span class="hljs-number">10</span>  <span class="hljs-number">11</span>  <span class="hljs-number">12</span>  <span class="hljs-number">13</span> <span class="hljs-string">(時)</span>     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>          <span class="hljs-string">↑</span> <span class="hljs-string">驅逐風暴開始</span>               <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">詳細指標</span>                          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────────────────────────┐</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">命中率:</span>    <span class="hljs-number">62</span><span class="hljs-string">%</span>  <span class="hljs-string">████████░░</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">未命中率:</span>  <span class="hljs-number">38</span><span class="hljs-string">%</span>  <span class="hljs-string">█████░░░░░</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">驅逐/分:</span>   <span class="hljs-number">1</span><span class="hljs-string">,240</span> <span class="hljs-string">████████████│🔥</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">平均延遲:</span>  <span class="hljs-string">8ms</span>  <span class="hljs-string">███░░░░░░░</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└──────────────────────────────┘</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔥</span> <span class="hljs-string">驅逐風暴分析</span>                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">驅逐/分</span>                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">2000</span><span class="hljs-string">│</span>          <span class="hljs-string">╱╲</span>                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">1500</span><span class="hljs-string">│</span>        <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">1000</span><span class="hljs-string">│</span>      <span class="hljs-string">╱</span>        <span class="hljs-string">╲╱╲</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">500</span><span class="hljs-string">│</span>  <span class="hljs-string">──╱</span>              <span class="hljs-string">╲──</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>    <span class="hljs-number">0</span><span class="hljs-string">└──┴───┴───┴───┴───┴──</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>      <span class="hljs-number">10</span><span class="hljs-string">:00</span> <span class="hljs-number">10</span><span class="hljs-string">:15</span> <span class="hljs-number">10</span><span class="hljs-string">:30</span> <span class="hljs-number">10</span><span class="hljs-string">:45</span>        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">💡</span> <span class="hljs-string">根本原因:</span> <span class="hljs-string">TTL</span> <span class="hljs-string">同步到期</span>             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────────────────────────┐</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-number">90</span><span class="hljs-string">%</span> <span class="hljs-string">的緩存鍵在</span> <span class="hljs-number">10</span><span class="hljs-string">:30</span> <span class="hljs-string">同時到期</span> <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">導致驚群效應</span> <span class="hljs-string">(thundering</span> <span class="hljs-string">herd)</span> <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└──────────────────────────────┘</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔧</span> <span class="hljs-string">優化方案:</span>                         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">TTL</span> <span class="hljs-string">添加隨機抖動</span> <span class="hljs-string">(±10%)</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">熱鍵實施請求合併</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">•</span> <span class="hljs-string">增加緩存容量</span> <span class="hljs-number">20</span><span class="hljs-string">%</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：</p>
<ul>
<li><p>80% 命中率 → 快樂</p>
</li>
<li><p>隨著驅逐次數增加降至 60% → 驚群效應或糟糕的 TTL</p>
</li>
</ul>
<p>🔧 <strong>真正的修復</strong>：為熱鍵添加請求合併並錯開 TTL，以避免同步到期懸崖</p>
<hr />
<h3 id="heading-12-golden-signals-release-overlay">12. Golden Signals × Release Overlay（黃金信號 × 釋放覆蓋）</h3>
<p><strong>顯示內容</strong>：延遲、流量、錯誤、飽和度，搭配部署標記和功能標記切換</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌───────────────</span> <span class="hljs-string">黃金信號</span> <span class="hljs-string">+</span> <span class="hljs-string">部署關聯</span> <span class="hljs-string">───────────────┐</span>
<span class="hljs-string">│</span>                                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">延遲</span> <span class="hljs-string">(Latency)</span>                                <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">ms│</span>                    <span class="hljs-string">┃</span>                         <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">800</span><span class="hljs-string">│</span>                    <span class="hljs-string">┃</span>  <span class="hljs-string">╱╲</span>                     <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">600</span><span class="hljs-string">│</span>            <span class="hljs-string">╱╲</span>      <span class="hljs-string">┃╱</span>    <span class="hljs-string">╲</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">400</span><span class="hljs-string">│</span>          <span class="hljs-string">╱</span>    <span class="hljs-string">╲</span>    <span class="hljs-string">┃</span>       <span class="hljs-string">╲</span>                 <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">200</span><span class="hljs-string">│</span>    <span class="hljs-string">────╱</span>        <span class="hljs-string">╲──┃</span>         <span class="hljs-string">╲────</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-number">0</span><span class="hljs-string">└──┴───┴───┴───┴───┃───┴───┴───┴──</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-number">8</span>   <span class="hljs-number">9</span>   <span class="hljs-number">10</span>  <span class="hljs-number">11</span>  <span class="hljs-number">12</span><span class="hljs-string">┃13</span>  <span class="hljs-number">14</span>  <span class="hljs-number">15</span>  <span class="hljs-number">16</span> <span class="hljs-string">(時)</span>       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                       <span class="hljs-string">┃</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📈</span> <span class="hljs-string">流量</span> <span class="hljs-string">(Traffic)</span>    <span class="hljs-string">┃v104-25%</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-string">QPS│</span>                  <span class="hljs-string">┃</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">500</span><span class="hljs-string">│</span>  <span class="hljs-string">════════════════┃════════════</span>               <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">400</span><span class="hljs-string">│</span>                  <span class="hljs-string">┃</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                       <span class="hljs-string">┃</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">❌</span> <span class="hljs-string">錯誤率</span> <span class="hljs-string">(Errors)</span>   <span class="hljs-string">┃</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-string">%│</span>                  <span class="hljs-string">┃</span>  <span class="hljs-string">╱╲</span>                       <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">5</span> <span class="hljs-string">│</span>                  <span class="hljs-string">┃╱</span>    <span class="hljs-string">╲</span>                     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">3</span> <span class="hljs-string">│</span>                  <span class="hljs-string">┃</span>       <span class="hljs-string">╲</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">1</span> <span class="hljs-string">│</span>  <span class="hljs-string">────────────────┃─────────────</span>              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                       <span class="hljs-string">┃</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔥</span> <span class="hljs-string">飽和度</span> <span class="hljs-string">(Saturation)</span> <span class="hljs-string">┃</span>                         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>   <span class="hljs-string">%│</span>                  <span class="hljs-string">┃</span>    <span class="hljs-string">╱────╲</span>                 <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">80</span> <span class="hljs-string">│</span>                  <span class="hljs-string">┃</span>  <span class="hljs-string">╱</span>        <span class="hljs-string">╲</span>               <span class="hljs-string">│</span>
<span class="hljs-string">│</span> <span class="hljs-number">60</span> <span class="hljs-string">│</span>  <span class="hljs-string">──────────────╱</span> <span class="hljs-string">┃╱</span>            <span class="hljs-string">╲──</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                       <span class="hljs-string">┃</span>                           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┃</span> <span class="hljs-string">=</span> <span class="hljs-string">部署標記:</span> <span class="hljs-string">v104</span> <span class="hljs-string">rollout</span> <span class="hljs-number">25</span><span class="hljs-string">%</span>                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🎯</span> <span class="hljs-string">爆炸半徑分析</span>                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌───────────────────────────────────────────┐</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">服務名稱</span>          <span class="hljs-string">指標變化</span>    <span class="hljs-string">Σ</span> <span class="hljs-string">偏差</span>       <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├───────────────────────────────────────────┤</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">payment-svc</span>      <span class="hljs-string">+340%</span> <span class="hljs-string">延遲</span>   <span class="hljs-number">4.2</span><span class="hljs-string">σ</span>  <span class="hljs-string">🔥</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">order-processor</span>  <span class="hljs-string">+180%</span> <span class="hljs-string">錯誤</span>   <span class="hljs-number">3.8</span><span class="hljs-string">σ</span>  <span class="hljs-string">🔥</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">auth-service</span>     <span class="hljs-string">+45%</span> <span class="hljs-string">延遲</span>    <span class="hljs-number">2.1</span><span class="hljs-string">σ</span>  <span class="hljs-string">⚠️</span>   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">user-service</span>     <span class="hljs-string">+12%</span> <span class="hljs-string">延遲</span>    <span class="hljs-number">0.8</span><span class="hljs-string">σ</span>  <span class="hljs-string">✓</span>    <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└───────────────────────────────────────────┘</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                                   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🚨</span> <span class="hljs-string">建議:</span> <span class="hljs-string">回滾</span> <span class="hljs-string">v104</span> <span class="hljs-string">並調查</span> <span class="hljs-string">payment-svc</span> <span class="hljs-string">變更</span>        <span class="hljs-string">│</span>
<span class="hljs-string">└───────────────────────────────────────────────────┘</span>
</code></pre>
<p><strong>為什麼有效</strong>：無需猜測即可關聯。「v104-rollout-25%」後五分鐘出現峰值？現在你知道在哪裡挖掘了</p>
<p>🎁 <strong>獎勵</strong>：添加「爆炸半徑」面板，列出自部署以來指標變化 ≥2σ 的服務</p>
<hr />
<h2 id="heading-8jpl4jydmnrbmp4vmtyhnqis">🏗️ 架構流程</h2>
<pre><code class="lang-yaml">[<span class="hljs-string">App</span> <span class="hljs-string">/</span> <span class="hljs-string">Services</span>] <span class="hljs-string">--OTLP--&gt;</span> [<span class="hljs-string">OTel</span> <span class="hljs-string">Collector</span>]
      <span class="hljs-string">|</span> <span class="hljs-string">traces,metrics,logs</span>         <span class="hljs-string">|
      |                             +--&gt; [Time-Series DB] 
      |                             |    └─&gt; RED, Tail, Saturation, Queue
      |                             |
      |                             +--&gt; [Trace Store]
      |                             |    └─&gt; Waterfall, N+1, Exemplars
      |                             |
      |                             +--&gt; [Log Store]
      |                                  └─&gt; Errors, Retries, Cold Starts
      |
</span>[<span class="hljs-string">Feature</span> <span class="hljs-string">Flags</span> <span class="hljs-string">/</span> <span class="hljs-string">CI-CD</span>] <span class="hljs-string">---------&gt;</span> [<span class="hljs-string">Deploy</span> <span class="hljs-string">Markers</span>] 
                                    <span class="hljs-string">└─&gt;</span> <span class="hljs-string">Overlays</span> <span class="hljs-string">on</span> <span class="hljs-string">all</span> <span class="hljs-string">dashboards</span>
</code></pre>
<p><strong>核心思想</strong>：</p>
<ul>
<li><p>一個 Collector，多個管道</p>
</li>
<li><p>跨度指標為 RED/尾部圖表提供支持</p>
</li>
<li><p>範例將尖峰與原始追蹤連接起來</p>
</li>
<li><p>部署標記將因果關係縫合到視覺效果中</p>
</li>
</ul>
<h2 id="heading-8jujsdmn6xoqalniyfmrrxnr4tkvos">🔍 查詢片段範例</h2>
<h4 id="heading-p95">p95 延遲（按路由）</h4>
<pre><code class="lang-yaml"><span class="hljs-string">histogram_quantile(</span>
  <span class="hljs-number">0.95</span><span class="hljs-string">,</span>
  <span class="hljs-string">sum</span> <span class="hljs-string">by</span> <span class="hljs-string">(le,</span> <span class="hljs-string">http_route)</span> <span class="hljs-string">(rate(traces_span_duration_bucket[5m]))</span>
<span class="hljs-string">)</span>
</code></pre>
<h4 id="heading-5qp6acf5pyn5yuz55qe6yen6kmm546h">每項服務的重試率</h4>
<pre><code class="lang-yaml"><span class="hljs-string">sum</span> <span class="hljs-string">by</span> <span class="hljs-string">(service_name)</span> <span class="hljs-string">(rate(traces_span_events_total{event="retry"}[5m]))</span>
<span class="hljs-string">/</span>
<span class="hljs-string">sum</span> <span class="hljs-string">by</span> <span class="hljs-string">(service_name)</span> <span class="hljs-string">(rate(traces_span_total[5m]))</span>
</code></pre>
<h4 id="heading-5qp5ycl6kul5rgc55qe6leo5bqm6kii5pw4">每個請求的跨度計數</h4>
<pre><code class="lang-yaml"><span class="hljs-string">sum</span> <span class="hljs-string">by</span> <span class="hljs-string">(http_route)</span> <span class="hljs-string">(rate(spans_per_request_total[5m]))</span> 
<span class="hljs-string">/</span> <span class="hljs-string">clamp_min(sum</span> <span class="hljs-string">by</span> <span class="hljs-string">(http_route)</span> <span class="hljs-string">(rate(requests_total[5m])),</span> <span class="hljs-number">1</span><span class="hljs-string">)</span>
</code></pre>
<hr />
<p>這些儀表板不僅僅是圖表，它們是從混亂的資料中提取<strong>行動信號</strong>的工具。</p>
<p>1. 橋接數據：從指標到追蹤的連貫性</p>
<p>在儀表板設計中，最關鍵的步驟是<strong>建立數據之間的橋樑</strong>。</p>
<p>• <strong>核心思想</strong>：單一的 OTel Collector 可以處理追蹤、指標和日誌。</p>
<p>• <strong>實現連貫性</strong>：<strong>範例 (Exemplars)</strong> 扮演著關鍵角色。它們能夠將尾部延遲圖表等指標中的尖峰，直接連結到造成該尖峰的<strong>原始追蹤 (raw traces)</strong>，使你能夠「直接從尖峰跳到有問題的跨度」。</p>
<p>2. 聚焦：部署標記縫合因果關係</p>
<p>當延遲或錯誤率發生變化時，工程師經常需要猜測是否與最近的部署有關。</p>
<p>• <strong>黃金信號 × 釋放覆蓋</strong> (Golden Signals × Release Overlay) 儀表板解決了這個問題。</p>
<p>• 通過 CI/CD 將部署標記 (<code>service.version</code> 和發布標記) 疊加到所有儀表板上，你可以<strong>無需猜測即可關聯問題</strong>。例如，在「v104-rollout-25%」後五分鐘出現的峰值，會立即指引調查方向。</p>
<p>• 同時，<strong>「爆炸半徑」面板</strong>能列出部署後指標變化超過 2σ 的服務，極大地縮小了調查範圍。</p>
<p>3. 行動性：保持警報的精準與安靜</p>
<p>設置過多的警報會產生疲勞。為了確保警報真正代表了業務風險並需要緊急行動，來源建議<strong>僅設置 3 個關鍵警報</strong>，保持警報的精準與安靜 (Keep it quiet, keep it crisp)：</p>
<p>1. <strong>快速燃燒</strong>：在 1 小時內消耗的錯誤預算超過 SLO 的 14 倍。</p>
<p>2. <strong>隊列年齡</strong>：隊列中最老的消息年齡超過 SLO 設定時間的一半。</p>
<p>3. <strong>長尾差距</strong>：p99 與 p50 的差距超過您的用戶容忍度。</p>
<h2 id="heading-10">🚀 10 分鐘推出計劃</h2>
<ol>
<li><p>✅ 在 Collector 中啟用跨度指標，運送到指標存儲</p>
</li>
<li><p>✅ 添加示例，使尾部圖在點擊時鏈接到追蹤</p>
</li>
<li><p>✅ 通過 CI 使用 <code>service.version</code> 和發布標記標記部署</p>
</li>
<li><p>✅ 從四個板開始：每條路線的 RED、尾部延遲、依賴瀑布、錯誤預算消耗</p>
</li>
<li><p>✅ 隨著模式出現擴展到其餘部分（隊列、GC、重試、緩存、自動縮放）</p>
</li>
<li><p>✅ <strong>僅設置 3 個警報</strong>：</p>
<ul>
<li><p>快速燃燒（1 小時內 &gt;14x SLO）</p>
</li>
<li><p>隊列年齡 &gt; SLO/2</p>
</li>
<li><p>p99 與 p50 的差距超出用戶容忍度</p>
</li>
</ul>
</li>
</ol>
<pre><code class="lang-yaml"><span class="hljs-string">┌──────────────</span> <span class="hljs-string">實施路線圖</span> <span class="hljs-string">──────────────┐</span>
<span class="hljs-string">│</span>                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">✅</span> <span class="hljs-string">階段</span> <span class="hljs-attr">1:</span> <span class="hljs-string">基礎設置</span> <span class="hljs-string">(2</span> <span class="hljs-string">分鐘)</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">└─</span> <span class="hljs-string">在</span> <span class="hljs-string">Collector</span> <span class="hljs-string">中啟用</span> <span class="hljs-string">spanmetrics</span> <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">✅</span> <span class="hljs-string">階段</span> <span class="hljs-attr">2:</span> <span class="hljs-string">追蹤增強</span> <span class="hljs-string">(2</span> <span class="hljs-string">分鐘)</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">└─</span> <span class="hljs-string">添加</span> <span class="hljs-string">exemplars</span> <span class="hljs-string">連結</span>             <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">✅</span> <span class="hljs-string">階段</span> <span class="hljs-attr">3:</span> <span class="hljs-string">部署標記</span> <span class="hljs-string">(1</span> <span class="hljs-string">分鐘)</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">└─</span> <span class="hljs-string">CI</span> <span class="hljs-string">整合</span> <span class="hljs-string">service.version</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">✅</span> <span class="hljs-string">階段</span> <span class="hljs-attr">4:</span> <span class="hljs-string">核心儀表板</span> <span class="hljs-string">(3</span> <span class="hljs-string">分鐘)</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">├─</span> <span class="hljs-string">RED</span> <span class="hljs-string">(每條路線)</span>                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">├─</span> <span class="hljs-string">尾部延遲</span>                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">├─</span> <span class="hljs-string">依賴瀑布</span>                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">└─</span> <span class="hljs-string">錯誤預算消耗</span>                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">✅</span> <span class="hljs-string">階段</span> <span class="hljs-attr">5:</span> <span class="hljs-string">擴展監控</span> <span class="hljs-string">(2</span> <span class="hljs-string">分鐘)</span>           <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">├─</span> <span class="hljs-string">隊列健康</span>                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">├─</span> <span class="hljs-string">GC</span> <span class="hljs-string">壓力</span>                         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">├─</span> <span class="hljs-string">重試循環</span>                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">└─</span> <span class="hljs-string">緩存效率</span>                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                        <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔔</span> <span class="hljs-string">階段</span> <span class="hljs-attr">6:</span> <span class="hljs-string">關鍵警報</span> <span class="hljs-string">(僅</span> <span class="hljs-number">3</span> <span class="hljs-string">個!)</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">├─</span> <span class="hljs-string">快速消耗</span> <span class="hljs-string">(&gt;14x</span> <span class="hljs-string">SLO/1h)</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">├─</span> <span class="hljs-string">隊列年齡</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">SLO/2</span>                <span class="hljs-string">│</span>
<span class="hljs-string">│</span>     <span class="hljs-string">└─</span> <span class="hljs-string">p99-p50</span> <span class="hljs-string">差距超過容忍度</span>          <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                        <span class="hljs-string">│</span>
<span class="hljs-string">└────────────────────────────────────────┘</span>
</code></pre>
<h2 id="heading-8jtlidnnjlr6bmoyjkvovmlyxkuos">📖 真實案例故事</h2>
<p>一家金融科技 API 在 <code>/quote</code> 端點上看到間歇性的 p99 峰值。</p>
<p><strong>調查過程</strong>：</p>
<ul>
<li><p>✓ RED 平均看起來不錯</p>
</li>
<li><p>✓ 尾部延遲板顯示 p99-p50 差距僅在市場開放期間擴大</p>
</li>
<li><p>✓ 依賴瀑布指出第三方定價跨度</p>
</li>
<li><p>✓ 重試/超時視圖顯示指數重試將每個信號點的流量乘以 3 倍</p>
</li>
</ul>
<p><strong>修復方案</strong>：</p>
<ol>
<li><p>限制重試並加入抖動</p>
</li>
<li><p>為「熱門符號」添加 100 ms 緩存</p>
</li>
<li><p>在開盤前預熱實例</p>
</li>
</ol>
<p><strong>結果</strong>：p99 次日下跌 47%，值班人員安靜了。</p>
<pre><code class="lang-yaml"><span class="hljs-string">┌──────────────</span> <span class="hljs-string">金融科技</span> <span class="hljs-string">API</span> <span class="hljs-string">案例</span> <span class="hljs-string">──────────────┐</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🏦</span> <span class="hljs-string">背景</span>                                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">服務:</span> <span class="hljs-string">/quote</span> <span class="hljs-string">API</span> <span class="hljs-string">(股票報價)</span>                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">問題:</span> <span class="hljs-string">間歇性</span> <span class="hljs-string">p99</span> <span class="hljs-string">延遲峰值</span>                     <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔍</span> <span class="hljs-string">調查過程</span>                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────────────────────────────────┐</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">步驟</span> <span class="hljs-attr">1:</span> <span class="hljs-string">RED</span> <span class="hljs-string">儀表板</span>                    <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">✓</span> <span class="hljs-string">平均值看起來正常</span>                   <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">✗</span> <span class="hljs-string">p99</span> <span class="hljs-string">有異常峰值</span>                    <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└──────────────────────────────────────┘</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────────────────────────────────┐</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">步驟</span> <span class="hljs-attr">2:</span> <span class="hljs-string">尾部延遲分析</span>                  <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">✓</span> <span class="hljs-string">p99-p50</span> <span class="hljs-string">差距僅在市場開盤時擴大</span>     <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">差距:</span> <span class="hljs-string">正常</span> <span class="hljs-string">200ms</span> <span class="hljs-string">→</span> <span class="hljs-string">高峰</span> <span class="hljs-number">1</span><span class="hljs-string">,100ms</span>  <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└──────────────────────────────────────┘</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────────────────────────────────┐</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">步驟</span> <span class="hljs-attr">3:</span> <span class="hljs-string">依賴瀑布</span>                      <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">🎯</span> <span class="hljs-string">發現:</span> <span class="hljs-string">第三方定價</span> <span class="hljs-string">API</span> <span class="hljs-string">佔</span> <span class="hljs-number">60</span><span class="hljs-string">%</span> <span class="hljs-string">延遲</span>  <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└──────────────────────────────────────┘</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌──────────────────────────────────────┐</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">步驟</span> <span class="hljs-attr">4:</span> <span class="hljs-string">重試/超時分析</span>                 <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span>  <span class="hljs-string">🔥</span> <span class="hljs-string">指數重試將流量放大</span> <span class="hljs-number">3</span> <span class="hljs-string">倍</span>           <span class="hljs-string">│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└──────────────────────────────────────┘</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">🔧</span> <span class="hljs-string">修復方案</span>                                  <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">1</span><span class="hljs-string">️⃣</span> <span class="hljs-string">限制重試次數並加入</span> <span class="hljs-string">jitter</span>               <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">2</span><span class="hljs-string">️⃣</span> <span class="hljs-string">為「熱門股票」添加</span> <span class="hljs-string">100ms</span> <span class="hljs-string">緩存</span>            <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-number">3</span><span class="hljs-string">️⃣</span> <span class="hljs-string">開盤前</span> <span class="hljs-number">5</span> <span class="hljs-string">分鐘預熱實例</span>                    <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">📊</span> <span class="hljs-string">結果</span>                                      <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">┌────────────────────────────────┐</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">Before</span> <span class="hljs-string">│</span>  <span class="hljs-string">After</span>  <span class="hljs-string">│</span>  <span class="hljs-string">改善</span>       <span class="hljs-string">│</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">├────────────────────────────────┤</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-attr">p99:</span> <span class="hljs-number">1</span><span class="hljs-string">,240ms</span> <span class="hljs-string">│</span> <span class="hljs-string">658ms</span> <span class="hljs-string">│</span> <span class="hljs-number">-47</span><span class="hljs-string">%</span> <span class="hljs-string">✅│</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">│</span> <span class="hljs-string">值班警報:</span>  <span class="hljs-number">8</span><span class="hljs-string">/天</span> <span class="hljs-string">│</span>  <span class="hljs-number">0.5</span><span class="hljs-string">/天</span> <span class="hljs-string">│</span> <span class="hljs-number">-94</span><span class="hljs-string">%</span> <span class="hljs-string">✅│</span>   <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">└────────────────────────────────┘</span>         <span class="hljs-string">│</span>
<span class="hljs-string">│</span>                                              <span class="hljs-string">│</span>
<span class="hljs-string">│</span>  <span class="hljs-string">⏱️</span> <span class="hljs-string">從發現到修復:</span> <span class="hljs-number">4</span> <span class="hljs-string">小時</span>                      <span class="hljs-string">│</span>
<span class="hljs-string">└──────────────────────────────────────────────┘</span>
</code></pre>
<h2 id="heading-8jsosdnul3ntza">💡 總結</h2>
<p>遙測並不是要擁有更多數據，<strong>而是讓少數視圖清晰易讀，使你的下一步行動顯而易見</strong>。</p>
<p>這 12 個儀表板能將詭異的圖表轉化為具體的解決方案。</p>
<p><img src="https://lh3.googleusercontent.com/notebooklm/AG60hOo7a6bF82i8ToR03Ci2oNf4U7UX80Ql8Upo7J9bg42b9lP4NPdIoAYUW9cUECG78O0cuAUR0BWhDg_RLmX4BjJbYFxqEaPEjnnNaZvVFQZrMfI7lnBrhjdK-Zmfztz1kMyL6WWtPL30XpeIdctGTHivaExhDJY=w2752-d-h1536?authuser=0" alt="一張資訊圖表說明四個 OpenTelemetry 儀表板，用於核心服務健康度、延遲分析、根因分析和錯誤預算消耗的監控。" /></p>
<p>如果您的監控系統是一棟佈滿傳感器的複雜大樓，那麼 OpenTelemetry 的 12 個關鍵儀表板就像是<strong>電力控制室裡的一組精準的斷路器和壓力表</strong>。您不需要看著幾百盞小燈閃爍，只需看著少數幾個儀表（RED、尾部差距、錯誤預算）就能迅速判斷故障點，並透過部署標記直接找出是哪個新安裝的設備（部署）造成了短路。這讓您能夠從「猜測問題」轉向「鎖定並修復問題」。</p>
]]></content:encoded></item><item><title><![CDATA[Prometheus TSDB 第一次接觸]]></title><description><![CDATA[1. 架構總覽
🏗️ 核心架構流程
graph LR
    A[📥 Scrape Target] -->|寫入| B[📝 WAL]
    B -->|同步| C[💾 Head Block<br/>記憶體]
    C -->|定期| D[✅ Checkpoint]
    C -->|每2小時| E[💿 Persistent Block<br/>磁碟]
    E -->|包含| F[📦 Chunks]

    style A fill:#e74c3c,stroke:#c039...]]></description><link>https://ganhua.wang/prometheus-tsdb</link><guid isPermaLink="true">https://ganhua.wang/prometheus-tsdb</guid><category><![CDATA[#prometheus]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Thu, 20 Nov 2025 05:01:26 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-1">1. 架構總覽</h2>
<h3 id="heading-8jpl4jydmoljlv4pmnrbmp4vmtyhnqis">🏗️ 核心架構流程</h3>
<pre><code class="lang-mermaid">graph LR
    A[📥 Scrape Target] --&gt;|寫入| B[📝 WAL]
    B --&gt;|同步| C[💾 Head Block&lt;br/&gt;記憶體]
    C --&gt;|定期| D[✅ Checkpoint]
    C --&gt;|每2小時| E[💿 Persistent Block&lt;br/&gt;磁碟]
    E --&gt;|包含| F[📦 Chunks]

    style A fill:#e74c3c,stroke:#c0392b,color:#fff
    style B fill:#e74c3c,stroke:#c0392b,color:#fff
    style C fill:#3498db,stroke:#2980b9,color:#fff
    style D fill:#f39c12,stroke:#e67e22,color:#fff
    style E fill:#2ecc71,stroke:#27ae60,color:#fff
    style F fill:#9b59b6,stroke:#8e44ad,color:#fff
</code></pre>
<blockquote>
<p><strong>🔑 核心概念</strong>：Prometheus 使用多層級的儲存架構，用來確保數據的<strong>持久性</strong>，<strong>高效查詢</strong>跟<strong>儲存空間優化</strong>的保證。</p>
</blockquote>
<h2 id="heading-2-wal-write-ahead-log">2. WAL (Write-Ahead Log)</h2>
<h3 id="heading-8jtnsdlipog73lrprkvy0">📝 功能定位</h3>
<p><strong>WAL</strong>是所有新數據的第一站，類似資料庫的 transaction log，確保數據不會因系統崩潰而遺失。</p>
<h3 id="heading-8jtgsdmqptmoyjntzdmp4s">📁 檔案結構</h3>
<pre><code class="lang-c">/data/wal/
         ├── <span class="hljs-number">00000000</span>  # <span class="hljs-number">128</span>MB segment
         ├── <span class="hljs-number">00000001</span>  # <span class="hljs-number">128</span>MB segment
         ├── <span class="hljs-number">00000002</span>  # 活躍寫入
        └── checkpoint<span class="hljs-number">.00000001</span>/
</code></pre>
<h3 id="heading-4pqz77ipioejueaapw">⚙️ 特性</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>特性</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><strong>大小</strong></td><td>每個 segment 128MB</td></tr>
<tr>
<td><strong>順序</strong></td><td>必須連續編號</td></tr>
<tr>
<td><strong>保留</strong></td><td>2-3 小時數據</td></tr>
<tr>
<td><strong>格式</strong></td><td>二進制</td></tr>
</tbody>
</table>
</div><h3 id="heading-wal">🔄 WAL 工作流程</h3>
<pre><code class="lang-mermaid">sequenceDiagram
    participant S as Scrape
    participant W as WAL (磁碟)
    participant H as Head Block (記憶體)

    S-&gt;&gt;W: 1. 先寫入 WAL
    Note over W: 確保持久化
    W-&gt;&gt;H: 2. 再寫入 Head
    Note over H: 快速查詢

    alt Prometheus 崩潰
        W-&gt;&gt;H: 3. 重啟時從 WAL 恢復
        Note over H: 重建記憶體狀態
    end
</code></pre>
<h3 id="heading-wal-1">✅ WAL 的作用</h3>
<ol>
<li><p><strong>持久化保證</strong>：先寫入磁碟，確保數據安全</p>
</li>
<li><p><strong>崩潰恢復</strong>：重啟時重建 Head Block</p>
</li>
<li><p><strong>順序寫入</strong>：高效的磁碟 I/O</p>
</li>
</ol>
<hr />
<h2 id="heading-3-head-block">3. Head Block (記憶體區塊)</h2>
<h3 id="heading-8jsvidlipog73lrprkvy0">💾 功能定位</h3>
<p>存放在<strong>記憶體</strong>中的活躍數據，提供最快的查詢速度。</p>
<h3 id="heading-8jpl4jydntzdmp4vntytmija">🏗️ 結構組成</h3>
<pre><code class="lang-mermaid">graph TD
    A[Head Block 記憶體] --&gt; B[mmap chunks&lt;br/&gt;記憶體映射檔案]
    A --&gt; C[Inverted Index&lt;br/&gt;倒排索引]
    A --&gt; D[Series Metadata&lt;br/&gt;時間序列元數據]

    style A fill:#3498db,stroke:#2980b9,color:#fff
    style B fill:#5dade2,stroke:#3498db,color:#fff
    style C fill:#5dade2,stroke:#3498db,color:#fff
    style D fill:#5dade2,stroke:#3498db,color:#fff
</code></pre>
<h3 id="heading-8jtiidpl5zpjbxnibnmgkc">📊 關鍵特性</h3>
<ul>
<li><p><strong>數據範圍</strong>：通常 2-3 小時</p>
</li>
<li><p><strong>查詢速度</strong>：極快（直接從記憶體）</p>
</li>
<li><p><strong>壓縮算法</strong>：XOR + Delta-of-Delta</p>
</li>
<li><p><strong>更新頻率</strong>：即時</p>
</li>
</ul>
<hr />
<h2 id="heading-4-checkpoint">4. Checkpoint (檢查點)</h2>
<h3 id="heading-4pyfiowknidvewumus9jq">✅ 功能定位</h3>
<p>WAL 的<strong>快照</strong>，加速 Prometheus 重啟，減少需要重放的 WAL 數量。</p>
<h3 id="heading-checkpoint">📸 Checkpoint 機制</h3>
<pre><code class="lang-mermaid">graph LR
    A[WAL 00000000] --&gt; D[Checkpoint.00000002]
    B[WAL 00000001] --&gt; D
    C[WAL 00000002] --&gt; D
    E[WAL 00000003] --&gt;|活躍| F[繼續寫入]
    G[WAL 00000004] --&gt;|活躍| F

    style D fill:#f39c12,stroke:#e67e22,color:#fff
    style E fill:#2ecc71,stroke:#27ae60,color:#fff
    style G fill:#2ecc71,stroke:#27ae60,color:#fff
</code></pre>
<h3 id="heading-4pyoiowequm7ng">✨ 優點</h3>
<ul>
<li><p>✅ 重啟更快（不需要重放所有 WAL）</p>
</li>
<li><p>✅ 減少磁碟使用（可刪除舊 WAL）</p>
</li>
<li><p>✅ 每 2 小時自動創建</p>
</li>
</ul>
<h3 id="heading-8jupydlt6xkvzzljpnkiy">🔧 工作原理</h3>
<pre><code class="lang-c"><span class="hljs-number">1.</span> 每 <span class="hljs-number">2</span> 小時創建一次 checkpoint
<span class="hljs-number">2.</span> Checkpoint 包含 <span class="hljs-number">00000000</span><span class="hljs-number">-00000002</span> 的數據
<span class="hljs-number">3.</span> 舊的 WAL segments 可以安全刪除
<span class="hljs-number">4.</span> 重啟時：
   → 讀取最新 checkpoint
   → 重放之後的 WAL segments
</code></pre>
<hr />
<h2 id="heading-5-persistent-block">5. Persistent Block (持久化區塊)</h2>
<h3 id="heading-8jsvydlipog73lrprkvy0">💿 功能定位</h3>
<p><strong>壓縮後的歷史數據</strong>，不可變（immutable），存儲在磁碟上。</p>
<h3 id="heading-block">📦 Block 結構</h3>
<pre><code class="lang-c">/data/<span class="hljs-number">01</span>K9TJBMDM5BC097TAG3HETHDZ/  ← Block ULID
├── chunks/
│   └── <span class="hljs-number">000001</span>              # 壓縮的時間序列數據
├── index                   # 倒排索引（快速查詢）
├── meta.json               # 元數據（時間範圍、統計）
└── tombstones              # 刪除標記
</code></pre>
<h3 id="heading-block-1">🔄 Block 壓縮層級</h3>
<pre><code class="lang-mermaid">graph TD
    A[2小時 Block] --&gt; B[Level 0]
    B --&gt; C[4小時 Block]
    C --&gt; D[Level 1]
    D --&gt; E[12小時 Block]
    E --&gt; F[Level 2]
    F --&gt; G[24小時+ Block]
    G --&gt; H[Level 3+]

    style A fill:#e8f5e9,stroke:#4caf50
    style C fill:#fff9c4,stroke:#fbc02d
    style E fill:#ffe0b2,stroke:#fb8c00
    style G fill:#ffccbc,stroke:#f4511e
</code></pre>
<h3 id="heading-8jtiidlo5pnuk7lsatntjroqqrmmi4">📊 壓縮層級說明</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層級</td><td>時間範圍</td><td>來源</td></tr>
</thead>
<tbody>
<tr>
<td>Level 0</td><td>2 小時</td><td>從 Head Block 轉換</td></tr>
<tr>
<td>Level 1</td><td>4-6 小時</td><td>合併 2 個 Level 0</td></tr>
<tr>
<td>Level 2</td><td>12-24 小時</td><td>合併多個 Level 1</td></tr>
<tr>
<td>Level 3+</td><td>24+ 小時</td><td>長期歷史數據</td></tr>
</tbody>
</table>
</div><h3 id="heading-block-2">🎯 Block 特性</h3>
<ul>
<li><p><strong>不可變</strong>：一旦創建就不會修改</p>
</li>
<li><p><strong>時間範圍固定</strong>：每個 Block 覆蓋固定時間</p>
</li>
<li><p><strong>高壓縮率</strong>：~84% 空間節省</p>
</li>
<li><p><strong>快速查詢</strong>：通過 index 加速</p>
</li>
</ul>
<hr />
<h2 id="heading-6-chunks">6. Chunks (數據塊)</h2>
<h3 id="heading-8jtpidlipog73lrprkvy0">📦 功能定位</h3>
<p>實際存儲<strong>時間序列樣本</strong>的地方，使用高效壓縮算法。</p>
<h3 id="heading-8jxno4jydlo5pnuk7mlyjnjoc">🗜️ 壓縮效率</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>類型</td><td>大小</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><strong>原始數據</strong></td><td>16 bytes/sample</td><td>timestamp (8) + value (8)</td></tr>
<tr>
<td><strong>壓縮後</strong></td><td>1.3 bytes/sample</td><td>XOR + Delta-of-Delta</td></tr>
<tr>
<td><strong>壓縮率</strong></td><td><strong>84%</strong></td><td>節省空間</td></tr>
</tbody>
</table>
</div><h3 id="heading-8jupydlo5pnuk7miodoozm">🔧 壓縮技術</h3>
<pre><code class="lang-mermaid">graph LR
    A[原始時間戳] --&gt;|Delta編碼| B[時間差值]
    B --&gt;|Delta-of-Delta| C[差值的差值]
    D[原始數值] --&gt;|XOR編碼| E[XOR結果]
    C --&gt; F[變長編碼]
    E --&gt; F
    F --&gt; G[壓縮 Chunk]

    style A fill:#ffebee,stroke:#f44336
    style D fill:#ffebee,stroke:#f44336
    style G fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
</code></pre>
<h3 id="heading-chunk">📊 Chunk 組織</h3>
<pre><code class="lang-c">一個時間序列：metric{label=<span class="hljs-string">"value"</span>}

├── Chunk <span class="hljs-number">1</span> (<span class="hljs-number">0</span><span class="hljs-number">-2</span>小時)
│   ├── timestamp: <span class="hljs-number">1763445600</span>
│   ├── value: <span class="hljs-number">42.5</span>
│   ├── timestamp: <span class="hljs-number">1763445615</span>
│   └── value: <span class="hljs-number">43.1</span>
│   
├── Chunk <span class="hljs-number">2</span> (<span class="hljs-number">2</span><span class="hljs-number">-4</span>小時)
└── Chunk <span class="hljs-number">3</span> (<span class="hljs-number">4</span><span class="hljs-number">-6</span>小時)
</code></pre>
<hr />
<h2 id="heading-7">7. 數據生命週期</h2>
<h3 id="heading-8juhcdlrozmlbtmtyhnqis">🔄 完整流程</h3>
<h4 id="heading-4pggioavuoatmuwvqwfpemajuautq">① 數據寫入階段</h4>
<pre><code class="lang-c">Scrape Target
    ↓
寫入 WAL (磁碟) ← 崩潰恢復保證
    ↓
寫入 <span class="hljs-function">Head <span class="hljs-title">Block</span> <span class="hljs-params">(記憶體)</span> ← 快速查詢
    ↓
定期創建 Checkpoint ← 加速重啟</span>
</code></pre>
<h4 id="heading-2">② 數據壓縮階段（每 2 小時）</h4>
<pre><code class="lang-c"><span class="hljs-function">Head <span class="hljs-title">Block</span> <span class="hljs-params">(記憶體)</span>
    ↓
持久化為 <span class="hljs-title">Block</span> <span class="hljs-params">(磁碟)</span>
    ↓
Chunks 壓縮存儲
    ↓
創建 <span class="hljs-title">Index</span> <span class="hljs-params">(快速查詢)</span>
    ↓
舊 WAL 可刪除</span>
</code></pre>
<h4 id="heading-4pgiioavuoatmuafpeipoumajuautq">③ 數據查詢階段</h4>
<pre><code class="lang-c">Query
    ↓
├── 查詢 <span class="hljs-function">Head <span class="hljs-title">Block</span> <span class="hljs-params">(最近 <span class="hljs-number">2</span> 小時)</span> ← 記憶體，最快
└── 查詢 Persistent Blocks ← 磁碟，較慢
    ├── 讀取 <span class="hljs-title">Index</span> <span class="hljs-params">(找到相關 Chunks)</span>
    └── 讀取 <span class="hljs-title">Chunks</span> <span class="hljs-params">(解壓縮數據)</span></span>
</code></pre>
<h3 id="heading-8jtiidmmylplppou7jnplrmhi8">📊 時間軸示意</h3>
<pre><code class="lang-c">時間: <span class="hljs-number">0</span>h    <span class="hljs-number">2</span>h    <span class="hljs-number">4</span>h    <span class="hljs-number">6</span>h    <span class="hljs-number">8</span>h    <span class="hljs-number">10</span>h   <span class="hljs-number">12</span>h
      ├─────┼─────┼─────┼─────┼─────┼─────┤
WAL:  [====活躍數據====]
      └────────┘
      轉換為 Block

Head: [====記憶體====]

Blocks:    [B1] [B2] [B3] [B4] [B5] ...
           └──┬──┘   └──┬──┘
              [B6]      [B7]  ← Level <span class="hljs-number">1</span> 壓縮
              └────┬────┘
                  [B8]       ← Level <span class="hljs-number">2</span> 壓縮
</code></pre>
<hr />
<h2 id="heading-8">8. 問題診斷與解決</h2>
<h3 id="heading-4p2miow4uoimimmriqpa">❌ 常見錯誤</h3>
<pre><code class="lang-c">opening storage failed: get segment range: segments are <span class="hljs-keyword">not</span> sequential
</code></pre>
<h3 id="heading-8jujsdpjkoqqtljplm6a">🔍 錯誤原因</h3>
<p>WAL segments 的編號不連續：</p>
<pre><code class="lang-diff">✗ 錯誤情況：
/data/wal/
├── 00000000
├── 00000001
<span class="hljs-deletion">- ├── 00000003  ← 缺少 00000002！</span>
└── 00000004

✓ 正確情況：
/data/wal/
├── 00000000
├── 00000001
<span class="hljs-addition">+ ├── 00000002  ← 連續！</span>
└── 00000003
</code></pre>
<h3 id="heading-8jorydmg4xms4hliibmnpa">🎯 情況分析</h3>
<pre><code class="lang-c">/data/wal/
├── <span class="hljs-number">00000</span>              ⚠️ 不該存在！
├── checkpoint<span class="hljs-number">.00587</span>/  ✓ 正常
├── <span class="hljs-number">00588</span>              ✓ 正常
└── <span class="hljs-number">00589</span>              ✓ 正常
</code></pre>
<p><strong>診斷結果</strong>：</p>
<ul>
<li><p>⚠️ <code>00000</code> 是孤立的舊 segment</p>
</li>
<li><p>✅ Checkpoint 機制正常運作</p>
</li>
<li><p>✅ 新的 segments (00588, 00589) 正常</p>
</li>
</ul>
<p><strong>原因</strong>：</p>
<ul>
<li><p>Pod 異常重啟時創建了 <code>00000</code></p>
</li>
<li><p>但舊的 checkpoint 和 segments 還保留著</p>
</li>
<li><p>導致 WAL 結構不一致</p>
</li>
</ul>
<hr />
<h2 id="heading-8jboo4jydop6pmsbrmlrnmoyg">🛠️ 解決方案</h2>
<h3 id="heading-5pa55qgi5bcn5qu">方案對比</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>方案</td><td>影響</td><td>適用場景</td><td>操作複雜度</td></tr>
</thead>
<tbody>
<tr>
<td><strong>方案 1：刪除異常 segment</strong></td><td>🟢 最小</td><td>單一 segment 損壞</td><td>簡單</td></tr>
<tr>
<td><strong>方案 2：刪除整個 WAL</strong></td><td>🟡 中等</td><td>多個 segment 損壞</td><td>中等</td></tr>
<tr>
<td><strong>方案 3：完全重置</strong></td><td>🔴 最大</td><td>測試環境/嚴重損壞</td><td>複雜</td></tr>
</tbody>
</table>
</div><hr />
<h3 id="heading-00000">✅ 推薦方案：刪除異常 00000</h3>
<h4 id="heading-1-1">步驟 1：驗證問題</h4>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -n kubecost kubecost-prometheus-server-7dd5f595d6-4k8h6 \
  -c prometheus-server -- ls -lh /data/wal/00000
</code></pre>
<h4 id="heading-2-1">步驟 2：備份（可選）</h4>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -n kubecost kubecost-prometheus-server-7dd5f595d6-4k8h6 \
  -c prometheus-server -- cp /data/wal/00000 /tmp/wal_00000_backup
</code></pre>
<h4 id="heading-3">步驟 3：刪除異常檔案</h4>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -n kubecost kubecost-prometheus-server-7dd5f595d6-4k8h6 \
  -c prometheus-server -- rm /data/wal/00000
</code></pre>
<h4 id="heading-4-pod">步驟 4：重啟 Pod</h4>
<pre><code class="lang-bash">kubectl delete pod -n kubecost kubecost-prometheus-server-7dd5f595d6-4k8h6
</code></pre>
<h4 id="heading-5">步驟 5：驗證恢復</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># 監控新 Pod 啟動</span>
kubectl get pods -n kubecost -w

<span class="hljs-comment"># 查看日誌</span>
kubectl logs -n kubecost &lt;new-pod-name&gt; -c prometheus-server -f

<span class="hljs-comment"># 應該看到：</span>
<span class="hljs-comment"># ✅ "Server is ready to receive web requests"</span>
</code></pre>
<h4 id="heading-6-wal">步驟 6：檢查 WAL 結構</h4>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -n kubecost &lt;new-pod-name&gt; -c prometheus-server -- \
  ls -lh /data/wal/

<span class="hljs-comment"># 預期結果：</span>
<span class="hljs-comment"># checkpoint.00587/</span>
<span class="hljs-comment"># 00588</span>
<span class="hljs-comment"># 00589</span>
<span class="hljs-comment"># 00590 (新創建)</span>
</code></pre>
<hr />
<h3 id="heading-2-wal">🔄 備選方案 2：刪除整個 WAL</h3>
<p>如果方案 1 失敗，使用此方案：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 進入 Pod</span>
kubectl <span class="hljs-built_in">exec</span> -it -n kubecost &lt;pod-name&gt; -c prometheus-server -- sh

<span class="hljs-comment"># 刪除 WAL 和 chunks_head</span>
rm -rf /data/wal/*
rm -rf /data/chunks_head/*

<span class="hljs-comment"># 退出並重啟</span>
<span class="hljs-built_in">exit</span>
kubectl delete pod -n kubecost &lt;pod-name&gt;
</code></pre>
<p><strong>影響</strong>：</p>
<ul>
<li><p>❌ 丟失最近 2-3 小時數據</p>
</li>
<li><p>✅ 保留所有歷史 Blocks</p>
</li>
</ul>
<hr />
<h3 id="heading-3-1">🔄 備選方案 3：完全重置</h3>
<p><strong>僅用於測試環境或嚴重損壞！</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 縮減副本</span>
kubectl scale deployment kubecost-prometheus-server -n kubecost --replicas=0

<span class="hljs-comment"># 2. 刪除 PVC（會刪除所有數據！）</span>
kubectl delete pvc -n kubecost -l app=prometheus

<span class="hljs-comment"># 3. 重新啟動</span>
kubectl scale deployment kubecost-prometheus-server -n kubecost --replicas=1
</code></pre>
<p><strong>影響</strong>：</p>
<ul>
<li><p>❌ <strong>丟失所有歷史數據</strong></p>
</li>
<li><p>✅ 全新乾淨的資料庫</p>
</li>
</ul>
<hr />
<h2 id="heading-8jtiidpojdpmllmjqrmlr0">📊 預防措施</h2>
<h3 id="heading-1-2">1. 監控指標</h3>
<pre><code class="lang-c"># WAL segment 數量
prometheus_tsdb_wal_segment_current

# WAL 損壞次數
prometheus_tsdb_wal_corruptions_total

# Checkpoint 失敗次數
prometheus_tsdb_checkpoint_creations_failed_total

# TSDB Head 大小
prometheus_tsdb_head_series
</code></pre>
<h3 id="heading-2-2">2. 資源配置建議</h3>
<pre><code class="lang-yaml"><span class="hljs-comment"># Prometheus 配置</span>
<span class="hljs-attr">resources:</span>
  <span class="hljs-attr">limits:</span>
    <span class="hljs-attr">memory:</span> <span class="hljs-string">4Gi</span>        <span class="hljs-comment"># 足夠的記憶體</span>
    <span class="hljs-attr">cpu:</span> <span class="hljs-string">2000m</span>
  <span class="hljs-attr">requests:</span>
    <span class="hljs-attr">memory:</span> <span class="hljs-string">2Gi</span>
    <span class="hljs-attr">cpu:</span> <span class="hljs-string">1000m</span>

<span class="hljs-comment"># 存儲配置</span>
<span class="hljs-attr">storage:</span>
  <span class="hljs-attr">tsdb:</span>
    <span class="hljs-attr">retention.time:</span> <span class="hljs-string">15d</span>
    <span class="hljs-attr">retention.size:</span> <span class="hljs-string">50GB</span>
    <span class="hljs-attr">wal-compression:</span> <span class="hljs-literal">true</span>  <span class="hljs-comment"># 啟用 WAL 壓縮</span>
</code></pre>
<h3 id="heading-3-2">3. 定期檢查清單</h3>
<ul>
<li><p>[ ] 每週檢查 WAL 結構</p>
</li>
<li><p>[ ] 監控磁碟使用率（&gt; 80% 告警）</p>
</li>
<li><p>[ ] 定期使用 promtool 驗證</p>
</li>
<li><p>[ ] 檢查 Prometheus 日誌中的錯誤</p>
</li>
<li><p>[ ] 監控 Pod 記憶體使用（避免 OOM）</p>
</li>
</ul>
<h3 id="heading-4-promtool">4. 使用 Promtool 驗證</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># 檢查 TSDB 健康度</span>
kubectl <span class="hljs-built_in">exec</span> -n kubecost &lt;pod&gt; -c prometheus-server -- \
  promtool tsdb analyze /data

<span class="hljs-comment"># 檢查 WAL 完整性</span>
kubectl <span class="hljs-built_in">exec</span> -n kubecost &lt;pod&gt; -c prometheus-server -- \
  promtool tsdb check wal /data/wal

<span class="hljs-comment"># 列出所有 Blocks</span>
kubectl <span class="hljs-built_in">exec</span> -n kubecost &lt;pod&gt; -c prometheus-server -- \
  promtool tsdb list /data
</code></pre>
<hr />
<h2 id="heading-8jtmidnul3ntza">📚 總結</h2>
<h3 id="heading-5qc45bd5qac5b15zue6agn">核心概念回顧</h3>
<pre><code class="lang-c">數據流向：
Scrape → WAL → Head Block → Checkpoint → Persistent Block → Chunks
         ↓      ↓            ↓            ↓                 ↓
      持久化  快速查詢    加速重啟     長期存儲         高壓縮
</code></pre>
<h3 id="heading-6zec6y216kab6bue">關鍵要點</h3>
<ol>
<li><p><strong>WAL</strong>：預寫日誌，確保數據持久性</p>
</li>
<li><p><strong>Head Block</strong>：記憶體中的活躍數據</p>
</li>
<li><p><strong>Checkpoint</strong>：WAL 快照，加速重啟</p>
</li>
<li><p><strong>Persistent Block</strong>：壓縮的歷史數據</p>
</li>
<li><p><strong>Chunks</strong>：實際數據存儲，高效壓縮</p>
</li>
</ol>
<h3 id="heading-5pwf6zqc5b2x6z56e5zyn">故障影響範圍</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>組件</td><td>損壞影響</td><td>恢復難度</td></tr>
</thead>
<tbody>
<tr>
<td>WAL</td><td>丟失 2-3 小時數據</td><td>容易</td></tr>
<tr>
<td>Head Block</td><td>丟失 2-3 小時數據</td><td>容易</td></tr>
<tr>
<td>Checkpoint</td><td>重啟較慢</td><td>容易</td></tr>
<tr>
<td>Persistent Block</td><td>丟失該 Block 時間範圍數據</td><td>困難</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-8julydlj4pogipos4fmupa">🔗 參考資源</h2>
<ul>
<li><p><a target="_blank" href="https://prometheus.io/docs/prometheus/latest/storage/">Prometheus Storage Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/prometheus/prometheus/blob/main/tsdb/docs/format/README.md">TSDB Format</a></p>
</li>
<li><p><a target="_blank" href="https://prometheus.io/docs/prometheus/latest/command-line/promtool/">Promtool Documentation</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Only 100 Metrics Matter 讀後感]]></title><description><![CDATA[最近讀了〈Only 100 Metrics Matter〉，有些感想。核心觀點不是「不要蒐集資料」，而是「別讓蒐集到的資料分散了我們的注意力」。
痛點與決策
文章開頭即是**痛點**的描述︰

蒐集可以很貪心，但注意力需要節制。

全量記錄是為了稀有情況與根因排查；日常決策則需要一組極精煉的「核心指標、核心事件、核心屬性」。

今年才跟朋友說，我想我應該很難一直留意那些 log 跟數量有啥異常，你不如轉成看是百分比還是一個量化的純數吧。


100個核心指標、50個核心事件、150個核心屬性，就...]]></description><link>https://ganhua.wang/only-100-metrics-matter</link><guid isPermaLink="true">https://ganhua.wang/only-100-metrics-matter</guid><category><![CDATA[observability]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sat, 25 Oct 2025 15:16:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761405312336/7adefe04-9bf1-4f26-8f1b-a2059629c594.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>最近讀了<a target="_blank" href="https://opinionatedintelligence.substack.com/p/only-100-metrics-matter">〈Only 100 Metrics Matter〉</a>，有些感想。核心觀點不是「不要蒐集資料」，而是「別讓蒐集到的資料分散了我們的注意力」。</p>
<h1 id="heading-55eb6bue6iih5rg6562w">痛點與決策</h1>
<p>文章開頭即是**痛點**的描述︰</p>
<ul>
<li><p>蒐集可以很貪心，但注意力需要節制。</p>
</li>
<li><p>全量記錄是為了稀有情況與根因排查；日常決策則需要一組極精煉的「核心指標、核心事件、核心屬性」。</p>
<ul>
<li>今年才跟朋友說，我想我應該很難一直留意那些 log 跟數量有啥異常，你不如轉成看是百分比還是一個量化的純數吧。</li>
</ul>
</li>
<li><p>100個核心指標、50個核心事件、150個核心屬性，就能解釋90%的業務與產品變化；其餘是長尾，留作特殊分析與事後鑑識。</p>
<ul>
<li><p>其實這句話在說的是可數的幾件事情相互組合基本就能解釋大部分的狀態變化了。這些數字並非絕對法則，而是作者用來說明「在大多數情境裡，一個典型的產品／業務，這量級的指標就能覆蓋絕大多數情況」。</p>
</li>
<li><p>有個 <a target="_blank" href="https://ithelp.ithome.com.tw/articles/10348115"><strong>Pareto Principle（80/20 法則</strong>、<strong>關鍵少數法則）</strong></a>，核心概念也類似，小弟我在以前鐵人賽有寫一篇文章可供參考。</p>
</li>
</ul>
</li>
</ul>
<p><strong>決策</strong>︰</p>
<ul>
<li><p><strong>認知負荷</strong>：看板與告警越多，決策品質反而下降，MTTD/MTTR變慢。</p>
</li>
<li><p><strong>組織對齊</strong>：少數關鍵指標更容易形成共識，推動跨部門協同。</p>
</li>
<li><p><strong>噪音抑制</strong>：<strong>長尾數據</strong>波動大、樣本小，容易誤導；<strong>core set</strong>（能夠解釋 80～90% 業務或產品變化的那一小組指標、事件與屬性。） 有更高信噪比。</p>
<ul>
<li><p>這些少數關鍵指標的變化，更能可靠地代表實際情況，而不是被隨機波動誤導。例如 <strong>DAU</strong> 或 <strong>錯誤率</strong> 的變化通常能真實反映系統層面的問題或市場反應。</p>
</li>
<li><p>把「長尾」留給事後調查或特殊情境，不混進日常決策。</p>
</li>
</ul>
</li>
<li><p><strong>彈性保留</strong>：全量追蹤不浪費，因為儲存便宜（是真的也不貴，但流量倒是相對貴多了QQ）；但把注意力花在長尾很昂貴。</p>
</li>
</ul>
<blockquote>
<p>「不是在收集更多，而是在收斂注意力到真正能影響行動與決策的少數關鍵訊號。」</p>
</blockquote>
<h2 id="heading-the-system-beneath-the-surface">The System Beneath the Surface</h2>
<blockquote>
<p>「Every business can be represented as a system. And these systems can be written as a set of equations.」</p>
</blockquote>
<p>文章要我們把業務想成「可被數學關係描述的系統」──<br />也就是說，你的營收、使用者行為、成效轉換，背後都有可觀測的變數與可操作的<strong>槓桿</strong>（<strong>levers</strong>）。</p>
<p>以 Facebook 為例，他把「Revenue」分解為 4 個槓桿：</p>
<blockquote>
<p>Users × Impressions per User × Ad Impressions per Impression × Revenue per Ad</p>
</blockquote>
<p>這種表達方式的重點是：<br />一旦你看見了系統的方程式，就知道「<strong>哪個變數可被操作、哪個是結果</strong>」。</p>
<p>對系統來說 「每一個技術系統也可以被寫成一組方程式。」</p>
<p>例如，在人力銀行／廣告投放平台中：</p>
<blockquote>
<p>System Health = Service Availability × Latency Performance × Error Rate Stability × Queue Throughput</p>
</blockquote>
<p>如果把「業務」換成「系統」，<br />那每個乘項（或加項）其實就是一組可觀測的核心 metric（<strong>SLI</strong>）。</p>
<p><strong>→ 換句話說： observability = 將你的系統行為具象成可操作的方程式。</strong><br />知道這個結構，就能知道「哪些指標才是槓桿、哪些只是噪音」。</p>
<h2 id="heading-the-power-of-decomposition">The Power of Decomposition</h2>
<blockquote>
<p>「When you break the system into equations, you can see what drives what.」</p>
</blockquote>
<p>文章用 MAU 舉例：</p>
<blockquote>
<p>MAU = New Users + Retained Users + Resurrected Users</p>
</blockquote>
<p>再往下分解「New Users」成各個轉換步驟。<br />這叫做 <strong>系統分解（decomposition）</strong>——從最上層 KPI 一層一層拆解出真正影響它的變數。</p>
<p>在技術系統裡，「<strong>decomposition</strong>」就是 <strong>tracing</strong> 與 <strong>dependency mapping</strong>。</p>
<p>你會從：</p>
<ul>
<li><p>「整體平台健康度」<br />  ↓</p>
</li>
<li><p>「哪個服務異常」<br />  ↓</p>
</li>
<li><p>「哪個 API latency 提升」<br />  ↓</p>
</li>
<li><p>「是哪個下游依賴卡住」</p>
</li>
</ul>
<p>這種<strong>由上而下的 drill-down 分析</strong>，就是 observability 工程的核心價值。</p>
<p>對 Product Engineer 而言，這也是產品儀表板該有的結構：<br />從「整體求職轉換率下降」→ 分解成「下載轉換低」「投遞流程慢」→ 再分解成「API 錯誤上升」。</p>
<p>👉 所以：<br /><strong>System Equation = 你能觀測的模型架構</strong><br /><strong>Decomposition = 你能追蹤與定位問題的能力</strong></p>
<h2 id="heading-why-only-100-metrics-matter">Why Only 100 Metrics Matter</h2>
<blockquote>
<p>「Each additional layer gives you smaller returns… you don’t need thousands of metrics to understand your business.」</p>
</blockquote>
<p>深入分析太多層之後，邊際效益會快速下降：樣本小、噪音大、難以行動。<br />因此需要聚焦在一個「小而穩的 Core sets」。</p>
<h3 id="heading-5bcn57o757wx55uj5o6n77yp5yv6kea5ris5ocn55qe5bcn5oej">對系統監控／可觀測性的對應</h3>
<p>這段就是我們前面聊到的「Core set」「告警疲勞」「高信噪比」：</p>
<ul>
<li><p>監控應該只針對最重要的指標（例如 Golden Signals：流量、延遲、錯誤、飽和度）。</p>
</li>
<li><p>Dashboard 不是越多越好，而是越能直接支持決策越好。</p>
</li>
<li><p>長尾數據留給事後分析，不該進日常告警或首頁看板。</p>
</li>
</ul>
<h2 id="heading-5oqa6kgt5zyy6zqk5lit55qe6kes6imy">技術團隊中的角色</h2>
<p>如果你是這兩種角色，能嘗試將技術與業務掛勾：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>角色</strong></td><td><strong>如何應用三個概念</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Product Engineer</strong></td><td>將產品成長或轉換率看作一個系統方程式（System Beneath the Surface），再分解出可操作的漏斗階段（Power of Decomposition），最後聚焦在能驅動決策的少數核心指標。</td></tr>
<tr>
<td><strong>Observability Engineer</strong></td><td>將系統可用性寫成核心 SLO 方程式（System），建立跨服務的依賴追蹤（Decomposition），再只監控與告警那一組「能真實反映健康度的 100 metrics 」（Core sets）。</td></tr>
</tbody>
</table>
</div><h3 id="heading-product-engineer">Product Engineer</h3>
<blockquote>
<p>「產品成長也像一個系統，有結構、有槓桿、有信號。<br />我們要做的，不是蒐集更多數據，而是抓到能推動變化的那幾個變數。」</p>
</blockquote>
<p><strong>流程要能被拆解與量化</strong></p>
<p>把「求職旅程」或「廣告投放」想成一條可量化的方程式：</p>
<pre><code class="lang-sql">Job Success = Visitors × Signups × Resume Completed × Applications × Interviews × Offers × Hires
</code></pre>
<ul>
<li><p>每一段都是一個「可操作的槓桿」，而不是單純的報表項目。</p>
</li>
<li><p>要清楚知道：這些環節哪一個波動，整體就會出問題。</p>
</li>
</ul>
<p><strong>指標要能反映決策，不只是呈現現狀</strong></p>
<ul>
<li><p>比方說：</p>
<ul>
<li><p>「投遞成功率下降」→ 是轉換率問題，還是流量異常？</p>
</li>
<li><p>「平均求職時間變長」→ 是履歷填寫率下降，還是推薦職缺不夠準？</p>
</li>
</ul>
</li>
<li><p>這些要能一層層拆出來看，形成「行動鏈」。</p>
</li>
</ul>
<p><strong>定義 Core Set</strong></p>
<ul>
<li><p>例如：</p>
<ul>
<li><p>核心指標（Metrics）＝「註冊率」「履歷完成率」「投遞成功率」「面試率」「錄取率」「到職率」</p>
</li>
<li><p>核心事件（Events）＝「註冊」「上傳履歷」「投遞成功」「面試完成」</p>
</li>
<li><p>核心屬性（Attributes）＝「地區」「產業」「年資」「職缺類別」</p>
</li>
</ul>
</li>
<li><p>這組核心集就能解釋 80～90% 的產品健康度，其他留給 ad-hoc 分析即可。</p>
</li>
</ul>
<p><strong>把數據變成故事，而不是表格</strong></p>
<ul>
<li><p>「為什麼這週投遞率掉了？」</p>
<ul>
<li><p>看看「下載→註冊」階段是否轉換低？</p>
</li>
<li><p>或「註冊→履歷完成」這段卡住？</p>
</li>
</ul>
</li>
<li><p>每個數字背後，都應該能講出行為邏輯。</p>
</li>
</ul>
<h3 id="heading-observability-engineer"><strong>Observability Engineer</strong></h3>
<blockquote>
<p>「可觀測性不是看越多越好，而是能清楚地看出<strong>問題在哪裡、為什麼、要不要修</strong>。」</p>
</blockquote>
<p><strong>用系統方程式思考健康度</strong></p>
<ul>
<li><p>把平台整體狀態定義為一個高層方程式：</p>
<pre><code class="lang-sql">  System Health = Availability × Latency Stability × Error Rate × Queue Throughput
</code></pre>
</li>
<li><p>每一項就是你的<strong>核心監控指標</strong>（<strong>Golden Signals</strong>）。<br />  你只要看這幾個，就能快速判斷整體健康狀況。</p>
</li>
</ul>
<p><strong>用分解（Decomposition）做問題定位</strong></p>
<ul>
<li><p>當系統異常時，你應該能從：</p>
<ul>
<li><p>現象「整體服務延遲↑」<br />  ↓</p>
</li>
<li><p>「哪個API latency 提升」<br />  ↓</p>
</li>
<li><p>「是哪個 downstream service 卡住」</p>
</li>
</ul>
</li>
<li><p>這個過程就像 MAU 拆成 New/Retained/Resurrected，用層層分解去找 root cause。<br />  observability 本身其實就是「系統方程式的實作版」。</p>
</li>
</ul>
<p><strong>建立核心監控集（Core Set of Metrics）</strong></p>
<ul>
<li><p>不需要監控幾千個 metric。<br />  一般來說，只要這些就能覆蓋 90% 的狀況：</p>
<ul>
<li><p><strong>四大 Golden Signals</strong>：Traffic（流量）、Latency（延遲）、Errors（錯誤率）、Saturation（資源飽和）</p>
<ul>
<li>Four Golden Signals 是一種綜合性的監控方法，它提供跨使用者介面層 (UI Layer)、服務層 (Service Layer) 和基礎設施層 (Infrastructure Layer)的全面監控視角，而非僅限於單一服務或資源。</li>
</ul>
</li>
<li><p><strong>面板能關聯到主要業務事件健康度儀表板</strong>：註冊成功率、投遞成功率、到職轉換率</p>
</li>
</ul>
</li>
<li><p>其他指標可以紀錄，但不用進主 dashboard。</p>
</li>
</ul>
<p><strong>告警要能反映真實影響</strong></p>
<ul>
<li><p>告警的目的不是通知，而是讓你<strong>更快做決策</strong>。</p>
<ul>
<li><p>只有當<strong>對應 SLO 惡化</strong>時，才該觸發 P1。</p>
</li>
<li><p>其他長尾異常（如零星 timeout）留給事後分析。</p>
</li>
</ul>
</li>
<li><p>減少噪音，也就是提高信噪比。</p>
</li>
</ul>
<p><strong>讓變更可追蹤</strong></p>
<ul>
<li><p>每次版本或設定變更都該留下「Change Event」(<strong>GitOps</strong>)，標註在監控走勢圖上。</p>
</li>
<li><p>讓你在看 SLO 波動時，一眼就知道是不是跟部署有關。</p>
</li>
<li><p>可觀測性最重要的價值之一，就是讓「系統可被審計」。</p>
</li>
</ul>
<h1 id="heading-5lia5yl6kmx57i957wq">一句話總結</h1>
<blockquote>
<p>可觀測性的目標不是更多資料，而是更快理解<strong>因果關係</strong>。<br />我們不需要 10,000 個 metric，只需要那 100 個能讓我們信任的。</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[📚 透過 OpenTelemetry Operator 學習 Kubernetes Operator 開發指南]]></title><description><![CDATA[📚 透過 OpenTelemetry Operator 深度學習 Kubernetes Operator 開發

本教程基於生產級專案 OpenTelemetry Operator 的實際代碼
涵蓋從基礎概念到高級實戰的完整學習路徑


目錄

Kubernetes Operator 核心概念

OpenTelemetry Operator 架構深度解析

CRD 完整剖析與實戰

Controller/Reconciler 深度實現

Manifest 構建器詳解

Webhook 機制深度...]]></description><link>https://ganhua.wang/opentelemetry-operator-kubernetes-operator</link><guid isPermaLink="true">https://ganhua.wang/opentelemetry-operator-kubernetes-operator</guid><category><![CDATA[k8s]]></category><category><![CDATA[OpenTelemetry]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Wed, 22 Oct 2025 06:10:20 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-opentelemetry-operator-kubernetes-operator">📚 透過 OpenTelemetry Operator 深度學習 Kubernetes Operator 開發</h1>
<blockquote>
<p><strong>本教程基於生產級專案</strong> <a target="_blank" href="https://github.com/open-telemetry/opentelemetry-operator"><strong>OpenTelemetry Operator</strong></a> <strong>的實際代碼</strong></p>
<p>涵蓋從基礎概念到高級實戰的完整學習路徑</p>
</blockquote>
<hr />
<h2 id="heading-55uu6yye">目錄</h2>
<ol>
<li><p><a class="post-section-overview" href="#%E4%B8%80kubernetes-operator-%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5">Kubernetes Operator 核心概念</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E4%BA%8Copentelemetry-operator-%E6%9E%B6%E6%A7%8B%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90">OpenTelemetry Operator 架構深度解析</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E4%B8%89crd-%E5%AE%8C%E6%95%B4%E5%89%96%E6%9E%90%E8%88%87%E5%AF%A6%E6%88%B0">CRD 完整剖析與實戰</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E5%9B%9Bcontrollerreconciler-%E6%B7%B1%E5%BA%A6%E5%AF%A6%E7%8F%BE">Controller/Reconciler 深度實現</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E4%BA%94manifest-%E6%A7%8B%E5%BB%BA%E5%99%A8%E8%A9%B3%E8%A7%A3">Manifest 構建器詳解</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E5%85%ADwebhook-%E6%A9%9F%E5%88%B6%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90">Webhook 機制深度解析</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E4%B8%83%E9%96%8B%E7%99%BC%E7%92%B0%E5%A2%83%E5%AE%8C%E6%95%B4%E8%A8%AD%E7%BD%AE">開發環境完整設置</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E5%85%AB%E6%B8%AC%E8%A9%A6%E7%AD%96%E7%95%A5%E8%88%87%E5%AF%A6%E8%B8%90">測試策略與實踐</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E4%B9%9D%E5%AF%A6%E6%88%B0%E5%B0%88%E6%A1%88nginx-operator">實戰專案：Nginx Operator</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E5%8D%81%E9%80%B2%E9%9A%8E%E4%B8%BB%E9%A1%8C%E8%88%87%E6%9C%80%E4%BD%B3%E5%AF%A6%E8%B8%90">進階主題與最佳實踐</a></p>
</li>
<li><p><a class="post-section-overview" href="#%E5%8D%81%E4%B8%80%E5%B8%B8%E8%A6%8B%E5%95%8F%E9%A1%8C%E8%88%87%E8%AA%BF%E8%A9%A6%E6%8A%80%E5%B7%A7">常見問題與調試技巧</a></p>
</li>
</ol>
<hr />
<h1 id="heading-kubernetes-operator">一、Kubernetes Operator 核心概念</h1>
<h2 id="heading-11-operator">1.1 什麼是 Operator？</h2>
<p>Operator 是 Kubernetes 的一種<strong>擴展模式</strong>，用於<strong>自動化複雜應用的部署和管理</strong>。它將人類運維知識編碼到軟體中。</p>
<h3 id="heading-5qc45bd57we5oiq6yoo5yig">核心組成部分</h3>
<pre><code class="lang-bash">┌─────────────────────────────────────────────────────────┐
│                     Kubernetes Operator                 │
│                                                          │
│  ┌────────────────┐  ┌──────────────┐  ┌─────────────┐ │
│  │  Custom        │  │  Controller  │  │   Domain    │ │
│  │  Resource      │◄─┤  (Reconciler)├─►│  Knowledge  │ │
│  │  Definition    │  │              │  │             │ │
│  └────────────────┘  └──────────────┘  └─────────────┘ │
│         │                    │                 │        │
│         │                    │                 │        │
│         ▼                    ▼                 ▼        │
│    擴展 K8s API        監控&amp;調和狀態      運維邏輯編碼  │
└─────────────────────────────────────────────────────────┘
</code></pre>
<h3 id="heading-12">1.2 工作原理</h3>
<pre><code class="lang-bash">User (kubectl apply)
    │
    ▼
┌─────────────────────────────────────┐
│  Custom Resource (CR)                │
│  例如: OpenTelemetryCollector        │
│                                      │
│  apiVersion: opentelemetry.io/v1beta1│
│  kind: OpenTelemetryCollector        │
│  spec:                               │
│    mode: deployment                  │
│    config: {...}                     │
└─────────────────────────────────────┘
    │ (儲存到 etcd)
    ▼
┌─────────────────────────────────────┐
│  Kubernetes API Server               │
│  (發送 Watch 事件)                   │
└─────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────┐
│  Controller (Reconciler)             │
│                                      │
│  1. 接收事件                         │
│  2. 讀取 CR                          │
│  3. 計算期望狀態                     │
│  4. 調和當前狀態 → 期望狀態          │
└─────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────┐
│  Kubernetes Resources                │
│  - Deployment                        │
│  - Service                           │
│  - ConfigMap                         │
│  - ServiceAccount                    │
│  - ...                               │
└─────────────────────────────────────┘
</code></pre>
<h3 id="heading-13-operator">1.3 Operator 能力等級</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>等級</td><td>能力</td><td>OpenTelemetry Operator 支持</td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td>基本安裝</td><td>✅</td></tr>
<tr>
<td>2</td><td>無縫升級</td><td>✅ (自動版本升級)</td></tr>
<tr>
<td>3</td><td>完整生命週期</td><td>✅ (Finalizer、備份配置)</td></tr>
<tr>
<td>4</td><td>深度洞察</td><td>✅ (Metrics、Events)</td></tr>
<tr>
<td>5</td><td>自動調優</td><td>✅ (HPA、Target Allocator)</td></tr>
</tbody>
</table>
</div><hr />
<h1 id="heading-opentelemetry-operator">二、OpenTelemetry Operator 架構深度解析</h1>
<h2 id="heading-21">2.1 專案完整結構</h2>
<pre><code class="lang-bash">opentelemetry-operator/
├── main.go                         <span class="hljs-comment"># 程式入口點 (200+ 行)</span>
│
├── apis/                           <span class="hljs-comment"># CRD API 定義</span>
│   ├── v1alpha1/                   <span class="hljs-comment"># Alpha API</span>
│   │   ├── instrumentation_types.go       <span class="hljs-comment"># 自動埋點 CRD</span>
│   │   ├── opampbridge_types.go          <span class="hljs-comment"># OpAMP Bridge CRD</span>
│   │   └── targetallocator_types.go      <span class="hljs-comment"># Target Allocator CRD (deprecated)</span>
│   └── v1beta1/                    <span class="hljs-comment"># Beta API (穩定版)</span>
│       ├── opentelemetrycollector_types.go  <span class="hljs-comment"># Collector CRD (主要)</span>
│       ├── targetallocator_types.go         <span class="hljs-comment"># Target Allocator CRD</span>
│       └── config.go                        <span class="hljs-comment"># 配置解析</span>
│
├── internal/                       <span class="hljs-comment"># 內部實現</span>
│   ├── controllers/                <span class="hljs-comment"># 控制器實現</span>
│   │   ├── opentelemetrycollector_controller.go  <span class="hljs-comment"># 主控制器 (400+ 行)</span>
│   │   ├── targetallocator_controller.go
│   │   ├── opampbridge_controller.go
│   │   └── reconcile_test.go              <span class="hljs-comment"># 測試 (56KB!)</span>
│   │
│   ├── manifests/                  <span class="hljs-comment"># K8s 資源構建器</span>
│   │   ├── collector/              <span class="hljs-comment"># Collector 資源構建</span>
│   │   │   ├── deployment.go      <span class="hljs-comment"># Deployment 構建</span>
│   │   │   ├── daemonset.go       <span class="hljs-comment"># DaemonSet 構建</span>
│   │   │   ├── statefulset.go     <span class="hljs-comment"># StatefulSet 構建</span>
│   │   │   ├── container.go       <span class="hljs-comment"># Container 構建 (核心邏輯)</span>
│   │   │   ├── service.go         <span class="hljs-comment"># Service 構建</span>
│   │   │   ├── configmap.go       <span class="hljs-comment"># ConfigMap 構建</span>
│   │   │   └── ...
│   │   ├── targetallocator/        <span class="hljs-comment"># Target Allocator 資源構建</span>
│   │   └── manifestutils/          <span class="hljs-comment"># 工具函數</span>
│   │
│   ├── webhook/                    <span class="hljs-comment"># Admission Webhooks</span>
│   │   ├── podmutation/            <span class="hljs-comment"># Pod Mutation Webhook</span>
│   │   │   └── webhookhandler.go  <span class="hljs-comment"># Sidecar/Instrumentation 注入</span>
│   │   └── validation/             <span class="hljs-comment"># Validation Webhook</span>
│   │
│   ├── config/                     <span class="hljs-comment"># Operator 配置管理</span>
│   ├── rbac/                       <span class="hljs-comment"># RBAC 工具</span>
│   ├── autodetect/                 <span class="hljs-comment"># 環境自動檢測</span>
│   │   ├── prometheus/             <span class="hljs-comment"># Prometheus Operator 檢測</span>
│   │   ├── openshift/              <span class="hljs-comment"># OpenShift 檢測</span>
│   │   └── certmanager/            <span class="hljs-comment"># cert-manager 檢測</span>
│   └── status/                     <span class="hljs-comment"># Status 更新邏輯</span>
│
├── pkg/                            <span class="hljs-comment"># 公開包</span>
│   ├── collector/                  <span class="hljs-comment"># Collector 工具</span>
│   │   └── upgrade/                <span class="hljs-comment"># 版本升級邏輯</span>
│   ├── sidecar/                    <span class="hljs-comment"># Sidecar 注入邏輯</span>
│   ├── featuregate/                <span class="hljs-comment"># Feature Gate 管理</span>
│   └── constants/                  <span class="hljs-comment"># 常量定義</span>
│
├── config/                         <span class="hljs-comment"># Kustomize 部署配置</span>
│   ├── crd/                        <span class="hljs-comment"># CRD YAML 文件</span>
│   │   └── bases/                  <span class="hljs-comment"># 基礎 CRD</span>
│   ├── rbac/                       <span class="hljs-comment"># RBAC 規則</span>
│   ├── manager/                    <span class="hljs-comment"># Operator Deployment</span>
│   ├── webhook/                    <span class="hljs-comment"># Webhook 配置</span>
│   └── samples/                    <span class="hljs-comment"># CR 範例</span>
│
├── tests/                          <span class="hljs-comment"># 測試套件</span>
│   ├── e2e/                        <span class="hljs-comment"># 基礎 E2E 測試</span>
│   ├── e2e-instrumentation/        <span class="hljs-comment"># 自動埋點測試</span>
│   ├── e2e-targetallocator/        <span class="hljs-comment"># Target Allocator 測試</span>
│   ├── e2e-upgrade/                <span class="hljs-comment"># 升級測試</span>
│   └── test-e2e-apps/              <span class="hljs-comment"># 測試應用</span>
│
├── cmd/                            <span class="hljs-comment"># 額外的可執行程序</span>
│   ├── otel-allocator/             <span class="hljs-comment"># Target Allocator 服務</span>
│   ├── operator-opamp-bridge/      <span class="hljs-comment"># OpAMP Bridge 服務</span>
│   └── gather/                     <span class="hljs-comment"># 故障排查工具</span>
│
├── Makefile                        <span class="hljs-comment"># 構建腳本</span>
├── go.mod                          <span class="hljs-comment"># Go 依賴管理</span>
└── versions.txt                    <span class="hljs-comment"># 版本管理文件</span>
</code></pre>
<h2 id="heading-22-crd">2.2 四大核心 CRD 詳解</h2>
<h3 id="heading-221-opentelemetrycollector-crd">2.2.1 OpenTelemetryCollector (主要 CRD)</h3>
<p><strong>檔案</strong>: <code>apis/v1beta1/opentelemetrycollector_types.go</code></p>
<p><strong>用途</strong>: 管理 OpenTelemetry Collector 的部署</p>
<p><strong>支持的部署模式</strong>:</p>
<pre><code class="lang-go"><span class="hljs-keyword">const</span> (
    ModeDeployment  Mode = <span class="hljs-string">"deployment"</span>   <span class="hljs-comment">// 標準部署</span>
    ModeDaemonSet   Mode = <span class="hljs-string">"daemonset"</span>    <span class="hljs-comment">// 每個節點一個實例</span>
    ModeStatefulSet Mode = <span class="hljs-string">"statefulset"</span>  <span class="hljs-comment">// 有狀態部署</span>
    ModeSidecar     Mode = <span class="hljs-string">"sidecar"</span>      <span class="hljs-comment">// 注入到其他 Pod</span>
)
</code></pre>
<p><strong>功能特性</strong>:</p>
<ul>
<li><p>✅ 多種部署模式</p>
</li>
<li><p>✅ 自動配置管理（ConfigMap 版本控制）</p>
</li>
<li><p>✅ Target Allocator 集成</p>
</li>
<li><p>✅ HPA 自動擴縮容</p>
</li>
<li><p>✅ 版本自動升級</p>
</li>
<li><p>✅ Ingress/Route 支持</p>
</li>
<li><p>✅ PodMonitor/ServiceMonitor 支持</p>
</li>
</ul>
<h3 id="heading-222-instrumentation">2.2.2 Instrumentation</h3>
<p><strong>檔案</strong>: <code>apis/v1alpha1/instrumentation_types.go</code></p>
<p><strong>用途</strong>: 配置自動埋點（Auto-instrumentation）</p>
<p><strong>支持的語言</strong>:</p>
<ul>
<li><p>Java (OpenTelemetry Java Agent)</p>
</li>
<li><p>Node.js (OpenTelemetry Node.js)</p>
</li>
<li><p>Python (OpenTelemetry Python)</p>
</li>
<li><p>.NET (OpenTelemetry .NET)</p>
</li>
<li><p>Go (eBPF-based)</p>
</li>
<li><p>Apache HTTPD</p>
</li>
<li><p>Nginx</p>
</li>
</ul>
<h3 id="heading-223-targetallocator">2.2.3 TargetAllocator</h3>
<p><strong>檔案</strong>: <code>apis/v1beta1/targetallocator_types.go</code></p>
<p><strong>用途</strong>: 分配 Prometheus 抓取目標到多個 Collector 實例</p>
<p><strong>分配策略</strong>:</p>
<ul>
<li><p><code>least-weighted</code>: 最少加權（基於目標數量）</p>
</li>
<li><p><code>consistent-hashing</code>: 一致性哈希</p>
</li>
<li><p><code>per-node</code>: 每個節點一個 Allocator</p>
</li>
</ul>
<h3 id="heading-224-opampbridge">2.2.4 OpAMPBridge</h3>
<p><strong>檔案</strong>: <code>apis/v1alpha1/opampbridge_types.go</code></p>
<p><strong>用途</strong>: 實現 OpAMP 協議，實現遠程管理 Collector</p>
<hr />
<h1 id="heading-crd">三、CRD 完整剖析與實戰</h1>
<h2 id="heading-31-crd">3.1 CRD 結構深度解析</h2>
<h3 id="heading-311-opentelemetrycollector-crd">3.1.1 OpenTelemetryCollector CRD 完整定義</h3>
<p><strong>檔案</strong>: <code>apis/v1beta1/opentelemetrycollector_types.go:32-145</code></p>
<pre><code class="lang-go"><span class="hljs-comment">// +kubebuilder:object:root=true</span>
<span class="hljs-comment">// +kubebuilder:resource:shortName=otelcol;otelcols</span>
<span class="hljs-comment">// +kubebuilder:storageversion</span>
<span class="hljs-comment">// +kubebuilder:subresource:status</span>
<span class="hljs-comment">// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.scale.replicas,selectorpath=.status.scale.selector</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Mode",type="string",JSONPath=".spec.mode",description="Deployment Mode"</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".status.version",description="OpenTelemetry Version"</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.scale.statusReplicas"</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".status.image"</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Management",type="string",JSONPath=".spec.managementState",description="Management State"</span>

<span class="hljs-keyword">type</span> OpenTelemetryCollector <span class="hljs-keyword">struct</span> {
    metav1.TypeMeta   <span class="hljs-string">`json:",inline"`</span>
    metav1.ObjectMeta <span class="hljs-string">`json:"metadata,omitempty"`</span>

    Spec   OpenTelemetryCollectorSpec   <span class="hljs-string">`json:"spec,omitempty"`</span>
    Status OpenTelemetryCollectorStatus <span class="hljs-string">`json:"status,omitempty"`</span>
}
</code></pre>
<h3 id="heading-312-kubebuilder">3.1.2 Kubebuilder 註解完全指南</h3>
<h4 id="heading-5z656so6ki76kej">基礎註解</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>註解</td><td>說明</td><td>範例</td></tr>
</thead>
<tbody>
<tr>
<td><code>+kubebuilder:object:root=true</code></td><td>標記為 CRD 根物件</td><td>必須添加</td></tr>
<tr>
<td><code>+kubebuilder:subresource:status</code></td><td>啟用 status 子資源</td><td>允許獨立更新 status</td></tr>
<tr>
<td><code>+kubebuilder:subresource:scale</code></td><td>啟用 scale 子資源</td><td>支持 <code>kubectl scale</code></td></tr>
<tr>
<td><code>+kubebuilder:resource:shortName</code></td><td>定義簡稱</td><td><code>otelcol,otelcols</code></td></tr>
<tr>
<td><code>+kubebuilder:storageversion</code></td><td>標記為存儲版本</td><td>多版本時指定</td></tr>
</tbody>
</table>
</div><h4 id="heading-6amx6k2j6ki76kej">驗證註解</h4>
<pre><code class="lang-go"><span class="hljs-comment">// 數值驗證</span>
<span class="hljs-comment">// +kubebuilder:validation:Minimum=1</span>
<span class="hljs-comment">// +kubebuilder:validation:Maximum=100</span>
Replicas <span class="hljs-keyword">int32</span> <span class="hljs-string">`json:"replicas,omitempty"`</span>

<span class="hljs-comment">// 字符串驗證</span>
<span class="hljs-comment">// +kubebuilder:validation:MinLength=1</span>
<span class="hljs-comment">// +kubebuilder:validation:MaxLength=255</span>
<span class="hljs-comment">// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`</span>
Name <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"name,omitempty"`</span>

<span class="hljs-comment">// 枚舉驗證</span>
<span class="hljs-comment">// +kubebuilder:validation:Enum=deployment;daemonset;statefulset;sidecar</span>
Mode <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"mode,omitempty"`</span>

<span class="hljs-comment">// 默認值</span>
<span class="hljs-comment">// +kubebuilder:default=1</span>
Replicas <span class="hljs-keyword">int32</span> <span class="hljs-string">`json:"replicas,omitempty"`</span>

<span class="hljs-comment">// CEL 驗證 (Kubernetes 1.25+)</span>
<span class="hljs-comment">// +kubebuilder:validation:XValidation:rule="self.mode != 'sidecar' || !has(self.replicas)",message="sidecar mode does not support replicas"</span>
Spec OpenTelemetryCollectorSpec <span class="hljs-string">`json:"spec,omitempty"`</span>
</code></pre>
<h4 id="heading-6agv56s66ki76kej">顯示註解</h4>
<pre><code class="lang-go"><span class="hljs-comment">// kubectl get 輸出欄位</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Mode",type="string",JSONPath=".spec.mode",description="Deployment Mode"</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"</span>
</code></pre>
<h3 id="heading-313-spec">3.1.3 完整 Spec 結構</h3>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> OpenTelemetryCollectorSpec <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// ===== 部署配置 =====</span>

    <span class="hljs-comment">// 部署模式</span>
    <span class="hljs-comment">// +optional</span>
    <span class="hljs-comment">// +kubebuilder:default=deployment</span>
    Mode Mode <span class="hljs-string">`json:"mode,omitempty"`</span>

    <span class="hljs-comment">// 副本數（僅 Deployment/StatefulSet 模式）</span>
    <span class="hljs-comment">// +optional</span>
    Replicas *<span class="hljs-keyword">int32</span> <span class="hljs-string">`json:"replicas,omitempty"`</span>

    <span class="hljs-comment">// 容器鏡像</span>
    <span class="hljs-comment">// +optional</span>
    Image <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"image,omitempty"`</span>

    <span class="hljs-comment">// ===== Collector 配置 =====</span>

    <span class="hljs-comment">// Collector 配置（YAML 格式）</span>
    <span class="hljs-comment">// +required</span>
    <span class="hljs-comment">// +kubebuilder:pruning:PreserveUnknownFields</span>
    Config Config <span class="hljs-string">`json:"config"`</span>

    <span class="hljs-comment">// ConfigMap 版本保留數量（用於回滾）</span>
    <span class="hljs-comment">// +optional</span>
    <span class="hljs-comment">// +kubebuilder:default:=3</span>
    <span class="hljs-comment">// +kubebuilder:validation:Minimum:=1</span>
    ConfigVersions <span class="hljs-keyword">int</span> <span class="hljs-string">`json:"configVersions,omitempty"`</span>

    <span class="hljs-comment">// ===== 升級策略 =====</span>

    <span class="hljs-comment">// 升級策略：automatic 或 none</span>
    <span class="hljs-comment">// +optional</span>
    UpgradeStrategy UpgradeStrategy <span class="hljs-string">`json:"upgradeStrategy"`</span>

    <span class="hljs-comment">// Deployment 更新策略</span>
    <span class="hljs-comment">// +optional</span>
    DeploymentUpdateStrategy appsv1.DeploymentStrategy <span class="hljs-string">`json:"deploymentUpdateStrategy,omitempty"`</span>

    <span class="hljs-comment">// DaemonSet 更新策略</span>
    <span class="hljs-comment">// +optional</span>
    DaemonSetUpdateStrategy appsv1.DaemonSetUpdateStrategy <span class="hljs-string">`json:"daemonSetUpdateStrategy,omitempty"`</span>

    <span class="hljs-comment">// ===== 資源配置 =====</span>

    <span class="hljs-comment">// 資源限制</span>
    <span class="hljs-comment">// +optional</span>
    Resources v1.ResourceRequirements <span class="hljs-string">`json:"resources,omitempty"`</span>

    <span class="hljs-comment">// 環境變數</span>
    <span class="hljs-comment">// +optional</span>
    Env []v1.EnvVar <span class="hljs-string">`json:"env,omitempty"`</span>

    <span class="hljs-comment">// Volume Mounts</span>
    <span class="hljs-comment">// +optional</span>
    VolumeMounts []v1.VolumeMount <span class="hljs-string">`json:"volumeMounts,omitempty"`</span>

    <span class="hljs-comment">// Volumes</span>
    <span class="hljs-comment">// +optional</span>
    Volumes []v1.Volume <span class="hljs-string">`json:"volumes,omitempty"`</span>

    <span class="hljs-comment">// ===== 調度配置 =====</span>

    <span class="hljs-comment">// Node Selector</span>
    <span class="hljs-comment">// +optional</span>
    NodeSelector <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span> <span class="hljs-string">`json:"nodeSelector,omitempty"`</span>

    <span class="hljs-comment">// Tolerations</span>
    <span class="hljs-comment">// +optional</span>
    Tolerations []v1.Toleration <span class="hljs-string">`json:"tolerations,omitempty"`</span>

    <span class="hljs-comment">// Affinity</span>
    <span class="hljs-comment">// +optional</span>
    Affinity *v1.Affinity <span class="hljs-string">`json:"affinity,omitempty"`</span>

    <span class="hljs-comment">// ===== 網路配置 =====</span>

    <span class="hljs-comment">// Ingress 配置</span>
    <span class="hljs-comment">// +optional</span>
    Ingress Ingress <span class="hljs-string">`json:"ingress,omitempty"`</span>

    <span class="hljs-comment">// Service 端口</span>
    <span class="hljs-comment">// +optional</span>
    Ports []PortsSpec <span class="hljs-string">`json:"ports,omitempty"`</span>

    <span class="hljs-comment">// ===== 高級功能 =====</span>

    <span class="hljs-comment">// Target Allocator</span>
    <span class="hljs-comment">// +optional</span>
    TargetAllocator TargetAllocatorEmbedded <span class="hljs-string">`json:"targetAllocator,omitempty"`</span>

    <span class="hljs-comment">// HPA 配置</span>
    <span class="hljs-comment">// +optional</span>
    Autoscaler *AutoscalerSpec <span class="hljs-string">`json:"autoscaler,omitempty"`</span>

    <span class="hljs-comment">// 探針配置</span>
    <span class="hljs-comment">// +optional</span>
    LivenessProbe *Probe <span class="hljs-string">`json:"livenessProbe,omitempty"`</span>
    ReadinessProbe *Probe <span class="hljs-string">`json:"readinessProbe,omitempty"`</span>

    <span class="hljs-comment">// ===== 可觀測性 =====</span>

    <span class="hljs-comment">// Observability 配置</span>
    <span class="hljs-comment">// +optional</span>
    Observability ObservabilitySpec <span class="hljs-string">`json:"observability,omitempty"`</span>
}
</code></pre>
<h2 id="heading-32">3.2 實戰範例：從簡單到複雜</h2>
<h3 id="heading-321">3.2.1 最簡範例</h3>
<p><strong>檔案</strong>: <code>config/samples/core_v1beta1_opentelemetrycollector.yaml</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">simplest</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">config:</span>
    <span class="hljs-attr">receivers:</span>
      <span class="hljs-attr">otlp:</span>
        <span class="hljs-attr">protocols:</span>
          <span class="hljs-attr">grpc:</span> {}
          <span class="hljs-attr">http:</span> {}
    <span class="hljs-attr">exporters:</span>
      <span class="hljs-attr">debug:</span> {}
    <span class="hljs-attr">service:</span>
      <span class="hljs-attr">pipelines:</span>
        <span class="hljs-attr">traces:</span>
          <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>]
          <span class="hljs-attr">exporters:</span> [<span class="hljs-string">debug</span>]
</code></pre>
<p><strong>這個 CR 會創建</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. Deployment (1 replica)</span>
kubectl get deployment simplest-collector

<span class="hljs-comment"># 2. Service (暴露 OTLP 端口)</span>
kubectl get service simplest-collector
<span class="hljs-comment"># Ports: 4317 (gRPC), 4318 (HTTP)</span>

<span class="hljs-comment"># 3. Service (headless, 用於 StatefulSet)</span>
kubectl get service simplest-collector-headless

<span class="hljs-comment"># 4. ConfigMap (Collector 配置)</span>
kubectl get configmap simplest-collector

<span class="hljs-comment"># 5. ServiceAccount</span>
kubectl get serviceaccount simplest-collector
</code></pre>
<h3 id="heading-322">3.2.2 生產級範例</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">production-collector</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">env:</span> <span class="hljs-string">production</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-comment"># ===== 部署配置 =====</span>
  <span class="hljs-attr">mode:</span> <span class="hljs-string">deployment</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>  <span class="hljs-comment"># 高可用</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">otel/opentelemetry-collector-k8s:0.88.0</span>

  <span class="hljs-comment"># ===== 升級策略 =====</span>
  <span class="hljs-attr">upgradeStrategy:</span> <span class="hljs-string">automatic</span>
  <span class="hljs-attr">deploymentUpdateStrategy:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">RollingUpdate</span>
    <span class="hljs-attr">rollingUpdate:</span>
      <span class="hljs-attr">maxUnavailable:</span> <span class="hljs-number">1</span>
      <span class="hljs-attr">maxSurge:</span> <span class="hljs-number">1</span>

  <span class="hljs-comment"># ===== ConfigMap 版本控制 =====</span>
  <span class="hljs-attr">configVersions:</span> <span class="hljs-number">5</span>  <span class="hljs-comment"># 保留 5 個版本用於回滾</span>

  <span class="hljs-comment"># ===== Collector 配置 =====</span>
  <span class="hljs-attr">config:</span>
    <span class="hljs-attr">receivers:</span>
      <span class="hljs-attr">otlp:</span>
        <span class="hljs-attr">protocols:</span>
          <span class="hljs-attr">grpc:</span>
            <span class="hljs-attr">endpoint:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:4317</span>
          <span class="hljs-attr">http:</span>
            <span class="hljs-attr">endpoint:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:4318</span>

      <span class="hljs-comment"># Prometheus receiver</span>
      <span class="hljs-attr">prometheus:</span>
        <span class="hljs-attr">config:</span>
          <span class="hljs-attr">scrape_configs:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'otel-collector'</span>
              <span class="hljs-attr">scrape_interval:</span> <span class="hljs-string">30s</span>
              <span class="hljs-attr">static_configs:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'0.0.0.0:8888'</span>]

    <span class="hljs-attr">processors:</span>
      <span class="hljs-comment"># 內存限制器（防止 OOM）</span>
      <span class="hljs-attr">memory_limiter:</span>
        <span class="hljs-attr">check_interval:</span> <span class="hljs-string">1s</span>
        <span class="hljs-attr">limit_percentage:</span> <span class="hljs-number">75</span>
        <span class="hljs-attr">spike_limit_percentage:</span> <span class="hljs-number">15</span>

      <span class="hljs-comment"># 批處理（提升性能）</span>
      <span class="hljs-attr">batch:</span>
        <span class="hljs-attr">send_batch_size:</span> <span class="hljs-number">10000</span>
        <span class="hljs-attr">timeout:</span> <span class="hljs-string">10s</span>

      <span class="hljs-comment"># 資源屬性</span>
      <span class="hljs-attr">resource:</span>
        <span class="hljs-attr">attributes:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">cluster.name</span>
            <span class="hljs-attr">value:</span> <span class="hljs-string">production-cluster</span>
            <span class="hljs-attr">action:</span> <span class="hljs-string">upsert</span>

    <span class="hljs-attr">exporters:</span>
      <span class="hljs-comment"># OTLP 導出到後端</span>
      <span class="hljs-attr">otlp:</span>
        <span class="hljs-attr">endpoint:</span> <span class="hljs-string">backend.example.com:4317</span>
        <span class="hljs-attr">tls:</span>
          <span class="hljs-attr">insecure:</span> <span class="hljs-literal">false</span>
          <span class="hljs-attr">cert_file:</span> <span class="hljs-string">/certs/tls.crt</span>
          <span class="hljs-attr">key_file:</span> <span class="hljs-string">/certs/tls.key</span>

      <span class="hljs-comment"># Prometheus 導出</span>
      <span class="hljs-attr">prometheus:</span>
        <span class="hljs-attr">endpoint:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:8889</span>

    <span class="hljs-comment"># Health Check Extension</span>
    <span class="hljs-attr">extensions:</span>
      <span class="hljs-attr">health_check:</span>
        <span class="hljs-attr">endpoint:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:13133</span>

      <span class="hljs-attr">pprof:</span>
        <span class="hljs-attr">endpoint:</span> <span class="hljs-string">localhost:1777</span>

    <span class="hljs-attr">service:</span>
      <span class="hljs-attr">extensions:</span> [<span class="hljs-string">health_check</span>, <span class="hljs-string">pprof</span>]
      <span class="hljs-attr">pipelines:</span>
        <span class="hljs-attr">traces:</span>
          <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>]
          <span class="hljs-attr">processors:</span> [<span class="hljs-string">memory_limiter</span>, <span class="hljs-string">batch</span>, <span class="hljs-string">resource</span>]
          <span class="hljs-attr">exporters:</span> [<span class="hljs-string">otlp</span>]

        <span class="hljs-attr">metrics:</span>
          <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>, <span class="hljs-string">prometheus</span>]
          <span class="hljs-attr">processors:</span> [<span class="hljs-string">memory_limiter</span>, <span class="hljs-string">batch</span>]
          <span class="hljs-attr">exporters:</span> [<span class="hljs-string">otlp</span>, <span class="hljs-string">prometheus</span>]

  <span class="hljs-comment"># ===== 資源限制 =====</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">requests:</span>
      <span class="hljs-attr">cpu:</span> <span class="hljs-string">500m</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">512Mi</span>
    <span class="hljs-attr">limits:</span>
      <span class="hljs-attr">cpu:</span> <span class="hljs-string">2000m</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">2Gi</span>

  <span class="hljs-comment"># ===== 環境變數 =====</span>
  <span class="hljs-attr">env:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MY_POD_IP</span>
      <span class="hljs-attr">valueFrom:</span>
        <span class="hljs-attr">fieldRef:</span>
          <span class="hljs-attr">fieldPath:</span> <span class="hljs-string">status.podIP</span>

  <span class="hljs-comment"># ===== 調度配置 =====</span>
  <span class="hljs-attr">nodeSelector:</span>
    <span class="hljs-attr">workload:</span> <span class="hljs-string">telemetry</span>

  <span class="hljs-attr">tolerations:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">telemetry</span>
      <span class="hljs-attr">operator:</span> <span class="hljs-string">Equal</span>
      <span class="hljs-attr">value:</span> <span class="hljs-string">"true"</span>
      <span class="hljs-attr">effect:</span> <span class="hljs-string">NoSchedule</span>

  <span class="hljs-attr">affinity:</span>
    <span class="hljs-attr">podAntiAffinity:</span>
      <span class="hljs-attr">preferredDuringSchedulingIgnoredDuringExecution:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">weight:</span> <span class="hljs-number">100</span>
          <span class="hljs-attr">podAffinityTerm:</span>
            <span class="hljs-attr">labelSelector:</span>
              <span class="hljs-attr">matchExpressions:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">app.kubernetes.io/name</span>
                  <span class="hljs-attr">operator:</span> <span class="hljs-string">In</span>
                  <span class="hljs-attr">values:</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-string">production-collector-collector</span>
            <span class="hljs-attr">topologyKey:</span> <span class="hljs-string">kubernetes.io/hostname</span>

  <span class="hljs-comment"># ===== Ingress 配置 =====</span>
  <span class="hljs-attr">ingress:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">ingress</span>
    <span class="hljs-attr">hostname:</span> <span class="hljs-string">collector.example.com</span>
    <span class="hljs-attr">annotations:</span>
      <span class="hljs-attr">cert-manager.io/cluster-issuer:</span> <span class="hljs-string">letsencrypt-prod</span>
    <span class="hljs-attr">tls:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">secretName:</span> <span class="hljs-string">collector-tls</span>
        <span class="hljs-attr">hosts:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">collector.example.com</span>

  <span class="hljs-comment"># ===== HPA 自動擴縮容 =====</span>
  <span class="hljs-attr">autoscaler:</span>
    <span class="hljs-attr">minReplicas:</span> <span class="hljs-number">3</span>
    <span class="hljs-attr">maxReplicas:</span> <span class="hljs-number">10</span>
    <span class="hljs-attr">behavior:</span>
      <span class="hljs-attr">scaleDown:</span>
        <span class="hljs-attr">stabilizationWindowSeconds:</span> <span class="hljs-number">300</span>
        <span class="hljs-attr">policies:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">Percent</span>
            <span class="hljs-attr">value:</span> <span class="hljs-number">50</span>
            <span class="hljs-attr">periodSeconds:</span> <span class="hljs-number">60</span>
      <span class="hljs-attr">scaleUp:</span>
        <span class="hljs-attr">stabilizationWindowSeconds:</span> <span class="hljs-number">0</span>
        <span class="hljs-attr">policies:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">Percent</span>
            <span class="hljs-attr">value:</span> <span class="hljs-number">100</span>
            <span class="hljs-attr">periodSeconds:</span> <span class="hljs-number">15</span>
    <span class="hljs-attr">metrics:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">Resource</span>
        <span class="hljs-attr">resource:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">cpu</span>
          <span class="hljs-attr">target:</span>
            <span class="hljs-attr">type:</span> <span class="hljs-string">Utilization</span>
            <span class="hljs-attr">averageUtilization:</span> <span class="hljs-number">75</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">Resource</span>
        <span class="hljs-attr">resource:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">memory</span>
          <span class="hljs-attr">target:</span>
            <span class="hljs-attr">type:</span> <span class="hljs-string">Utilization</span>
            <span class="hljs-attr">averageUtilization:</span> <span class="hljs-number">80</span>

  <span class="hljs-comment"># ===== 探針配置 =====</span>
  <span class="hljs-attr">livenessProbe:</span>
    <span class="hljs-attr">httpGet:</span>
      <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">13133</span>
    <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">15</span>
    <span class="hljs-attr">periodSeconds:</span> <span class="hljs-number">10</span>

  <span class="hljs-attr">readinessProbe:</span>
    <span class="hljs-attr">httpGet:</span>
      <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">13133</span>
    <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">10</span>
    <span class="hljs-attr">periodSeconds:</span> <span class="hljs-number">5</span>

  <span class="hljs-comment"># ===== Target Allocator =====</span>
  <span class="hljs-attr">targetAllocator:</span>
    <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
    <span class="hljs-attr">allocationStrategy:</span> <span class="hljs-string">consistent-hashing</span>
    <span class="hljs-attr">prometheusCR:</span>
      <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">serviceMonitorSelector:</span> {}
      <span class="hljs-attr">podMonitorSelector:</span> {}

  <span class="hljs-comment"># ===== 可觀測性 =====</span>
  <span class="hljs-attr">observability:</span>
    <span class="hljs-attr">metrics:</span>
      <span class="hljs-attr">enableMetrics:</span> <span class="hljs-literal">true</span>
</code></pre>
<h3 id="heading-323-daemonset">3.2.3 DaemonSet 模式範例</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">node-collector</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">mode:</span> <span class="hljs-string">daemonset</span>  <span class="hljs-comment"># 每個節點一個實例</span>

  <span class="hljs-comment"># DaemonSet 更新策略</span>
  <span class="hljs-attr">daemonSetUpdateStrategy:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">RollingUpdate</span>
    <span class="hljs-attr">rollingUpdate:</span>
      <span class="hljs-attr">maxUnavailable:</span> <span class="hljs-number">1</span>

  <span class="hljs-comment"># Host Network（訪問節點級資源）</span>
  <span class="hljs-attr">hostNetwork:</span> <span class="hljs-literal">true</span>

  <span class="hljs-attr">config:</span>
    <span class="hljs-attr">receivers:</span>
      <span class="hljs-comment"># 主機指標</span>
      <span class="hljs-attr">hostmetrics:</span>
        <span class="hljs-attr">collection_interval:</span> <span class="hljs-string">30s</span>
        <span class="hljs-attr">scrapers:</span>
          <span class="hljs-attr">cpu:</span> {}
          <span class="hljs-attr">load:</span> {}
          <span class="hljs-attr">memory:</span> {}
          <span class="hljs-attr">disk:</span> {}
          <span class="hljs-attr">filesystem:</span> {}
          <span class="hljs-attr">network:</span> {}

      <span class="hljs-comment"># Kubernetes Events</span>
      <span class="hljs-attr">k8s_events:</span>
        <span class="hljs-attr">namespaces:</span> [<span class="hljs-string">default</span>, <span class="hljs-string">kube-system</span>]

    <span class="hljs-attr">exporters:</span>
      <span class="hljs-attr">otlp:</span>
        <span class="hljs-attr">endpoint:</span> <span class="hljs-string">central-collector:4317</span>

    <span class="hljs-attr">service:</span>
      <span class="hljs-attr">pipelines:</span>
        <span class="hljs-attr">metrics:</span>
          <span class="hljs-attr">receivers:</span> [<span class="hljs-string">hostmetrics</span>]
          <span class="hljs-attr">exporters:</span> [<span class="hljs-string">otlp</span>]
</code></pre>
<hr />
<h1 id="heading-controllerreconciler">四、Controller/Reconciler 深度實現</h1>
<h2 id="heading-41-reconciler">4.1 Reconciler 結構體詳解</h2>
<p><strong>檔案</strong>: <code>internal/controllers/opentelemetrycollector_controller.go:58-67</code></p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> OpenTelemetryCollectorReconciler <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// K8s 客戶端（帶緩存）</span>
    client.Client

    <span class="hljs-comment">// 事件記錄器（發送 K8s 事件）</span>
    recorder record.EventRecorder

    <span class="hljs-comment">// API Scheme（資源類型註冊表）</span>
    scheme *runtime.Scheme

    <span class="hljs-comment">// 結構化日誌器</span>
    log logr.Logger

    <span class="hljs-comment">// Operator 配置</span>
    config config.Config

    <span class="hljs-comment">// RBAC 權限審查器</span>
    reviewer *internalRbac.Reviewer

    <span class="hljs-comment">// 版本升級處理器</span>
    upgrade *upgrade.VersionUpgrade
}
</code></pre>
<h3 id="heading-411">4.1.1 依賴組件解析</h3>
<h4 id="heading-clientclient">Client.Client</h4>
<pre><code class="lang-go"><span class="hljs-comment">// controller-runtime 提供的智能客戶端</span>
<span class="hljs-comment">// 特性：</span>
<span class="hljs-comment">// 1. 自動緩存（減少 API Server 壓力）</span>
<span class="hljs-comment">// 2. 類型安全</span>
<span class="hljs-comment">// 3. 支持 FieldIndexer（加速查詢）</span>
<span class="hljs-comment">// 4. 自動處理 RESTMapper</span>

<span class="hljs-comment">// 使用範例</span>
err := r.Client.Get(ctx, types.NamespacedName{
    Name:      <span class="hljs-string">"my-collector"</span>,
    Namespace: <span class="hljs-string">"default"</span>,
}, &amp;collector)
</code></pre>
<h4 id="heading-eventrecorder">EventRecorder</h4>
<pre><code class="lang-go"><span class="hljs-comment">// 記錄 Kubernetes 事件</span>
<span class="hljs-comment">// kubectl describe 時可以看到</span>

r.recorder.Event(
    &amp;instance,                    <span class="hljs-comment">// 相關物件</span>
    corev1.EventTypeNormal,       <span class="hljs-comment">// 事件類型</span>
    <span class="hljs-string">"Created"</span>,                    <span class="hljs-comment">// 原因</span>
    <span class="hljs-string">"Created OpenTelemetry Collector deployment"</span>,  <span class="hljs-comment">// 消息</span>
)
</code></pre>
<h4 id="heading-reviewer-rbac">Reviewer (RBAC 檢查器)</h4>
<pre><code class="lang-go"><span class="hljs-comment">// 檢查 Collector 配置中的 RBAC 需求</span>
<span class="hljs-comment">// 例如：如果配置中有 k8s_cluster receiver，需要額外權限</span>

<span class="hljs-keyword">if</span> reviewer.NeedsClusterRole(collectorConfig) {
    <span class="hljs-comment">// 創建 ClusterRole 和 ClusterRoleBinding</span>
}
</code></pre>
<h2 id="heading-42-reconcile">4.2 Reconcile 完整流程追蹤</h2>
<p><strong>檔案</strong>: <code>internal/controllers/opentelemetrycollector_controller.go:234-314</code></p>
<p>讓我們逐步追蹤一個完整的 Reconcile 流程：</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *OpenTelemetryCollectorReconciler)</span> <span class="hljs-title">Reconcile</span><span class="hljs-params">(
    ctx context.Context,
    req ctrl.Request,
)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    log := r.log.WithValues(<span class="hljs-string">"opentelemetrycollector"</span>, req.NamespacedName)

    <span class="hljs-comment">// ==================== 階段 1: 獲取 CR ====================</span>
    log.Info(<span class="hljs-string">"Reconciling OpenTelemetryCollector"</span>)

    <span class="hljs-keyword">var</span> instance v1beta1.OpenTelemetryCollector
    <span class="hljs-keyword">if</span> err := r.Get(ctx, req.NamespacedName, &amp;instance); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">if</span> !apierrors.IsNotFound(err) {
            log.Error(err, <span class="hljs-string">"unable to fetch OpenTelemetryCollector"</span>)
        }
        <span class="hljs-comment">// 資源已刪除，忽略錯誤</span>
        <span class="hljs-keyword">return</span> ctrl.Result{}, client.IgnoreNotFound(err)
    }

    <span class="hljs-comment">// ==================== 階段 2: 構建參數 ====================</span>
    <span class="hljs-comment">// 包含所有需要的配置、Target Allocator 等</span>
    params, err := r.GetParams(ctx, instance)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Error(err, <span class="hljs-string">"Failed to create manifest.Params"</span>)
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-comment">// ==================== 階段 3: 處理刪除（Finalizer 模式）====================</span>
    <span class="hljs-keyword">if</span> deletionTimestamp := instance.GetDeletionTimestamp(); deletionTimestamp != <span class="hljs-literal">nil</span> {
        log.Info(<span class="hljs-string">"Resource is being deleted"</span>)

        <span class="hljs-keyword">if</span> controllerutil.ContainsFinalizer(&amp;instance, collectorFinalizer) {
            log.Info(<span class="hljs-string">"Running finalizer logic"</span>)

            <span class="hljs-comment">// 執行清理：刪除 ClusterRole、ClusterRoleBinding 等集群級資源</span>
            <span class="hljs-keyword">if</span> err = r.finalizeCollector(ctx, params); err != <span class="hljs-literal">nil</span> {
                log.Error(err, <span class="hljs-string">"Failed to finalize"</span>)
                <span class="hljs-keyword">return</span> ctrl.Result{}, err
            }

            <span class="hljs-comment">// 移除 Finalizer</span>
            <span class="hljs-keyword">if</span> controllerutil.RemoveFinalizer(&amp;instance, collectorFinalizer) {
                err = r.Update(ctx, &amp;instance)
                <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                    <span class="hljs-keyword">return</span> ctrl.Result{}, err
                }
            }
            log.Info(<span class="hljs-string">"Finalizer removed, resource will be deleted"</span>)
        }
        <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
    }

    <span class="hljs-comment">// ==================== 階段 4: 檢查管理狀態 ====================</span>
    <span class="hljs-comment">// ManagementStateUnmanaged: Operator 不管理此資源</span>
    <span class="hljs-keyword">if</span> instance.Spec.ManagementState == v1beta1.ManagementStateUnmanaged {
        log.Info(<span class="hljs-string">"Skipping reconciliation for unmanaged resource"</span>)
        <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
    }

    <span class="hljs-comment">// ==================== 階段 5: 版本升級 ====================</span>
    <span class="hljs-comment">// 檢查 CR 配置是否需要升級到新格式</span>
    <span class="hljs-keyword">if</span> r.upgrade.NeedsUpgrade(instance) {
        log.Info(<span class="hljs-string">"Upgrading OpenTelemetryCollector configuration"</span>)

        err = r.upgrade.Upgrade(ctx, instance)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            log.Error(err, <span class="hljs-string">"Failed to upgrade"</span>)
            <span class="hljs-keyword">return</span> ctrl.Result{}, err
        }

        <span class="hljs-comment">// CR 被修改，重新排隊觸發新的 reconcile</span>
        log.Info(<span class="hljs-string">"Configuration upgraded, requeuing"</span>)
        <span class="hljs-keyword">return</span> ctrl.Result{Requeue: <span class="hljs-literal">true</span>, RequeueAfter: <span class="hljs-number">1</span> * time.Second}, <span class="hljs-literal">nil</span>
    }

    <span class="hljs-comment">// ==================== 階段 6: 添加 Finalizer ====================</span>
    <span class="hljs-comment">// Finalizer 確保在刪除時執行清理邏輯</span>
    <span class="hljs-keyword">if</span> !controllerutil.ContainsFinalizer(&amp;instance, collectorFinalizer) {
        log.Info(<span class="hljs-string">"Adding finalizer"</span>)

        <span class="hljs-keyword">if</span> controllerutil.AddFinalizer(&amp;instance, collectorFinalizer) {
            err = r.Update(ctx, &amp;instance)
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> ctrl.Result{}, err
            }
        }
    }

    <span class="hljs-comment">// ==================== 階段 7: 構建期望資源 ====================</span>
    log.Info(<span class="hljs-string">"Building desired objects"</span>)

    desiredObjects, buildErr := BuildCollector(params)
    <span class="hljs-keyword">if</span> buildErr != <span class="hljs-literal">nil</span> {
        log.Error(buildErr, <span class="hljs-string">"Failed to build desired objects"</span>)
        <span class="hljs-keyword">return</span> ctrl.Result{}, buildErr
    }

    log.Info(<span class="hljs-string">"Desired objects built"</span>, <span class="hljs-string">"count"</span>, <span class="hljs-built_in">len</span>(desiredObjects))

    <span class="hljs-comment">// ==================== 階段 8: 查找現有資源 ====================</span>
    log.Info(<span class="hljs-string">"Finding owned objects"</span>)

    ownedObjects, err := r.findOtelOwnedObjects(ctx, params)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Error(err, <span class="hljs-string">"Failed to find owned objects"</span>)
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    log.Info(<span class="hljs-string">"Found owned objects"</span>, <span class="hljs-string">"count"</span>, <span class="hljs-built_in">len</span>(ownedObjects))

    <span class="hljs-comment">// ==================== 階段 9: 調和資源 ====================</span>
    <span class="hljs-comment">// 比較期望狀態和當前狀態，執行創建/更新/刪除</span>
    log.Info(<span class="hljs-string">"Reconciling desired objects"</span>)

    err = reconcileDesiredObjects(
        ctx,
        r.Client,
        log,
        &amp;instance,
        params.Scheme,
        desiredObjects,
        ownedObjects,
    )

    <span class="hljs-comment">// ==================== 階段 10: 更新 Status ====================</span>
    <span class="hljs-keyword">return</span> collectorStatus.HandleReconcileStatus(ctx, log, params, instance, err)
}
</code></pre>
<h3 id="heading-421-buildcollector">4.2.1 詳細追蹤：BuildCollector 函數</h3>
<p><strong>檔案</strong>: <code>internal/controllers/opentelemetrycollector_controller.go</code></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">BuildCollector</span><span class="hljs-params">(params manifests.Params)</span> <span class="hljs-params">([]client.Object, error)</span></span> {
    <span class="hljs-keyword">var</span> objects []client.Object

    <span class="hljs-comment">// 1. ServiceAccount</span>
    <span class="hljs-keyword">if</span> sa := collector.ServiceAccount(params); sa != <span class="hljs-literal">nil</span> {
        objects = <span class="hljs-built_in">append</span>(objects, sa)
    }

    <span class="hljs-comment">// 2. ConfigMap（Collector 配置）</span>
    configMaps, err := collector.ConfigMaps(params)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }
    <span class="hljs-keyword">for</span> _, cm := <span class="hljs-keyword">range</span> configMaps {
        objects = <span class="hljs-built_in">append</span>(objects, cm)
    }

    <span class="hljs-comment">// 3. 根據模式選擇工作負載類型</span>
    <span class="hljs-keyword">switch</span> params.OtelCol.Spec.Mode {
    <span class="hljs-keyword">case</span> v1alpha1.ModeDeployment:
        deployment, err := collector.Deployment(params)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }
        objects = <span class="hljs-built_in">append</span>(objects, deployment)

    <span class="hljs-keyword">case</span> v1alpha1.ModeDaemonSet:
        daemonset, err := collector.DaemonSet(params)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }
        objects = <span class="hljs-built_in">append</span>(objects, daemonset)

    <span class="hljs-keyword">case</span> v1alpha1.ModeStatefulSet:
        statefulset, err := collector.StatefulSet(params)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }
        objects = <span class="hljs-built_in">append</span>(objects, statefulset)
    }

    <span class="hljs-comment">// 4. Service（暴露端口）</span>
    services := collector.Services(params)
    <span class="hljs-keyword">for</span> _, svc := <span class="hljs-keyword">range</span> services {
        objects = <span class="hljs-built_in">append</span>(objects, svc)
    }

    <span class="hljs-comment">// 5. Ingress（如果配置）</span>
    <span class="hljs-keyword">if</span> params.OtelCol.Spec.Ingress.Type == v1beta1.IngressTypeIngress {
        <span class="hljs-keyword">if</span> ingress := collector.Ingress(params); ingress != <span class="hljs-literal">nil</span> {
            objects = <span class="hljs-built_in">append</span>(objects, ingress)
        }
    }

    <span class="hljs-comment">// 6. HPA（如果配置自動擴縮容）</span>
    <span class="hljs-keyword">if</span> params.OtelCol.Spec.Autoscaler != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">if</span> hpa := collector.HorizontalPodAutoscaler(params); hpa != <span class="hljs-literal">nil</span> {
            objects = <span class="hljs-built_in">append</span>(objects, hpa)
        }
    }

    <span class="hljs-comment">// 7. RBAC（如果需要）</span>
    <span class="hljs-keyword">if</span> params.Config.CreateRBACPermissions == rbac.Available {
        <span class="hljs-keyword">if</span> clusterRole := collector.ClusterRole(params); clusterRole != <span class="hljs-literal">nil</span> {
            objects = <span class="hljs-built_in">append</span>(objects, clusterRole)
        }
        <span class="hljs-keyword">if</span> clusterRoleBinding := collector.ClusterRoleBinding(params); clusterRoleBinding != <span class="hljs-literal">nil</span> {
            objects = <span class="hljs-built_in">append</span>(objects, clusterRoleBinding)
        }
    }

    <span class="hljs-comment">// 8. ServiceMonitor/PodMonitor（可觀測性）</span>
    <span class="hljs-keyword">if</span> params.OtelCol.Spec.Observability.Metrics.EnableMetrics {
        <span class="hljs-keyword">if</span> sm := collector.ServiceMonitor(params); sm != <span class="hljs-literal">nil</span> {
            objects = <span class="hljs-built_in">append</span>(objects, sm)
        }
    }

    <span class="hljs-comment">// 9. Target Allocator（如果啟用）</span>
    <span class="hljs-keyword">if</span> params.OtelCol.Spec.TargetAllocator.Enabled {
        taObjects, err := BuildTargetAllocator(params)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }
        objects = <span class="hljs-built_in">append</span>(objects, taObjects...)
    }

    <span class="hljs-keyword">return</span> objects, <span class="hljs-literal">nil</span>
}
</code></pre>
<h3 id="heading-422-reconciledesiredobjects">4.2.2 調和邏輯：reconcileDesiredObjects</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">reconcileDesiredObjects</span><span class="hljs-params">(
    ctx context.Context,
    client client.Client,
    log logr.Logger,
    owner client.Object,
    scheme *runtime.Scheme,
    desired []client.Object,
    existing <span class="hljs-keyword">map</span>[types.UID]client.Object,
)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-comment">// 為期望的資源設置 OwnerReference</span>
    <span class="hljs-keyword">for</span> _, obj := <span class="hljs-keyword">range</span> desired {
        <span class="hljs-keyword">if</span> err := controllerutil.SetControllerReference(owner, obj, scheme); err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
    }

    <span class="hljs-comment">// ========== 步驟 1: 創建或更新期望的資源 ==========</span>
    <span class="hljs-keyword">for</span> _, desiredObj := <span class="hljs-keyword">range</span> desired {
        log := log.WithValues(
            <span class="hljs-string">"kind"</span>, desiredObj.GetObjectKind().GroupVersionKind().Kind,
            <span class="hljs-string">"name"</span>, desiredObj.GetName(),
        )

        <span class="hljs-comment">// 嘗試獲取現有資源</span>
        existingObj := desiredObj.DeepCopyObject().(client.Object)
        err := client.Get(ctx, types.NamespacedName{
            Name:      desiredObj.GetName(),
            Namespace: desiredObj.GetNamespace(),
        }, existingObj)

        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &amp;&amp; apierrors.IsNotFound(err) {
            <span class="hljs-comment">// 資源不存在，創建</span>
            log.Info(<span class="hljs-string">"Creating resource"</span>)
            <span class="hljs-keyword">if</span> err := client.Create(ctx, desiredObj); err != <span class="hljs-literal">nil</span> {
                log.Error(err, <span class="hljs-string">"Failed to create resource"</span>)
                <span class="hljs-keyword">return</span> err
            }
            log.Info(<span class="hljs-string">"Resource created successfully"</span>)

        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-comment">// 其他錯誤</span>
            <span class="hljs-keyword">return</span> err

        } <span class="hljs-keyword">else</span> {
            <span class="hljs-comment">// 資源存在，更新</span>
            log.Info(<span class="hljs-string">"Updating resource"</span>)

            <span class="hljs-comment">// 保留不應該變更的字段</span>
            desiredObj.SetResourceVersion(existingObj.GetResourceVersion())

            <span class="hljs-keyword">if</span> err := client.Update(ctx, desiredObj); err != <span class="hljs-literal">nil</span> {
                log.Error(err, <span class="hljs-string">"Failed to update resource"</span>)
                <span class="hljs-keyword">return</span> err
            }
            log.Info(<span class="hljs-string">"Resource updated successfully"</span>)

            <span class="hljs-comment">// 從待刪除列表中移除</span>
            <span class="hljs-built_in">delete</span>(existing, existingObj.GetUID())
        }
    }

    <span class="hljs-comment">// ========== 步驟 2: 刪除多餘的資源 ==========</span>
    <span class="hljs-keyword">for</span> uid, obj := <span class="hljs-keyword">range</span> existing {
        log := log.WithValues(
            <span class="hljs-string">"kind"</span>, obj.GetObjectKind().GroupVersionKind().Kind,
            <span class="hljs-string">"name"</span>, obj.GetName(),
            <span class="hljs-string">"uid"</span>, uid,
        )

        log.Info(<span class="hljs-string">"Deleting orphaned resource"</span>)
        <span class="hljs-keyword">if</span> err := client.Delete(ctx, obj); err != <span class="hljs-literal">nil</span> {
            log.Error(err, <span class="hljs-string">"Failed to delete resource"</span>)
            <span class="hljs-keyword">return</span> err
        }
        log.Info(<span class="hljs-string">"Orphaned resource deleted"</span>)
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<h2 id="heading-43">4.3 索引與緩存優化</h2>
<h3 id="heading-431-field-indexer">4.3.1 設置 Field Indexer</h3>
<p><strong>檔案</strong>: <code>internal/controllers/opentelemetrycollector_controller.go:334-354</code></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *OpenTelemetryCollectorReconciler)</span> <span class="hljs-title">SetupCaches</span><span class="hljs-params">(cluster cluster.Cluster)</span> <span class="hljs-title">error</span></span> {
    ownedResources := r.GetOwnedResourceTypes()

    <span class="hljs-comment">// 為每種資源類型建立索引</span>
    <span class="hljs-keyword">for</span> _, resource := <span class="hljs-keyword">range</span> ownedResources {
        <span class="hljs-keyword">if</span> err := cluster.GetCache().IndexField(
            context.Background(),
            resource,
            resourceOwnerKey,  <span class="hljs-comment">// 索引鍵：".metadata.owner"</span>
            <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(rawObj client.Object)</span> []<span class="hljs-title">string</span></span> {
                <span class="hljs-comment">// 提取 Owner Reference</span>
                owner := metav1.GetControllerOf(rawObj)
                <span class="hljs-keyword">if</span> owner == <span class="hljs-literal">nil</span> {
                    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
                }

                <span class="hljs-comment">// 只索引 OpenTelemetryCollector 擁有的資源</span>
                <span class="hljs-keyword">if</span> owner.Kind != <span class="hljs-string">"OpenTelemetryCollector"</span> {
                    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
                }

                <span class="hljs-comment">// 返回 Owner 名稱作為索引值</span>
                <span class="hljs-keyword">return</span> []<span class="hljs-keyword">string</span>{owner.Name}
            },
        ); err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<h3 id="heading-432">4.3.2 使用索引加速查詢</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *OpenTelemetryCollectorReconciler)</span> <span class="hljs-title">findOtelOwnedObjects</span><span class="hljs-params">(
    ctx context.Context,
    params manifests.Params,
)</span> <span class="hljs-params">(<span class="hljs-keyword">map</span>[types.UID]client.Object, error)</span></span> {
    ownedObjects := <span class="hljs-keyword">map</span>[types.UID]client.Object{}

    <span class="hljs-comment">// 使用索引快速查詢（而不是掃描所有資源）</span>
    listOpts := []client.ListOption{
        client.InNamespace(params.OtelCol.Namespace),
        client.MatchingFields{resourceOwnerKey: params.OtelCol.Name},
    }

    <span class="hljs-keyword">for</span> _, objectType := <span class="hljs-keyword">range</span> r.GetOwnedResourceTypes() {
        objs, err := getList(ctx, r, objectType, listOpts...)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }

        <span class="hljs-keyword">for</span> uid, object := <span class="hljs-keyword">range</span> objs {
            ownedObjects[uid] = object
        }
    }

    <span class="hljs-keyword">return</span> ownedObjects, <span class="hljs-literal">nil</span>
}
</code></pre>
<p><strong>性能對比</strong>：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>方法</td><td>時間複雜度</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td>不使用索引</td><td>O(N)</td><td>N = 集群中所有 Deployment 數量</td></tr>
<tr>
<td>使用索引</td><td>O(M)</td><td>M = 被這個 Collector 擁有的 Deployment 數量</td></tr>
</tbody>
</table>
</div><p>在大型集群中，性能提升可達 10-100 倍！</p>
<hr />
<h1 id="heading-manifest">五、Manifest 構建器詳解</h1>
<h2 id="heading-51-deployment">5.1 Deployment 構建完整解析</h2>
<p><strong>檔案</strong>: <code>internal/manifests/collector/deployment.go</code></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Deployment</span><span class="hljs-params">(params manifests.Params)</span> <span class="hljs-params">(*appsv1.Deployment, error)</span></span> {
    name := naming.Collector(params.OtelCol.Name)

    <span class="hljs-comment">// ========== 1. 構建標籤 ==========</span>
    <span class="hljs-comment">// 標籤用於：</span>
    <span class="hljs-comment">// - Service 選擇器</span>
    <span class="hljs-comment">// - HPA 目標選擇</span>
    <span class="hljs-comment">// - 監控發現</span>
    labels := manifestutils.Labels(
        params.OtelCol.ObjectMeta,
        name,
        params.OtelCol.Spec.Image,
        ComponentOpenTelemetryCollector,
        params.Config.LabelsFilter,
    )

    <span class="hljs-comment">// 標籤範例：</span>
    <span class="hljs-comment">// app.kubernetes.io/name: my-collector-collector</span>
    <span class="hljs-comment">// app.kubernetes.io/instance: my-collector</span>
    <span class="hljs-comment">// app.kubernetes.io/managed-by: opentelemetry-operator</span>
    <span class="hljs-comment">// app.kubernetes.io/component: opentelemetry-collector</span>
    <span class="hljs-comment">// app.kubernetes.io/version: 0.88.0</span>

    <span class="hljs-comment">// ========== 2. 構建註解 ==========</span>
    annotations, err := manifestutils.Annotations(
        params.OtelCol,
        params.Config.AnnotationsFilter,
    )
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }

    <span class="hljs-comment">// Pod 註解（額外添加 ConfigMap 哈希）</span>
    podAnnotations, err := manifestutils.PodAnnotations(
        params.OtelCol,
        params.Config.AnnotationsFilter,
    )
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }

    <span class="hljs-comment">// ========== 3. 構建 Deployment ==========</span>
    <span class="hljs-keyword">return</span> &amp;appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:        name,
            Namespace:   params.OtelCol.Namespace,
            Labels:      labels,
            Annotations: annotations,
        },
        Spec: appsv1.DeploymentSpec{
            <span class="hljs-comment">// 副本數</span>
            Replicas: manifestutils.GetInitialReplicas(params.OtelCol),

            <span class="hljs-comment">// 選擇器（必須與 Pod 標籤匹配）</span>
            Selector: &amp;metav1.LabelSelector{
                MatchLabels: manifestutils.SelectorLabels(
                    params.OtelCol.ObjectMeta,
                    ComponentOpenTelemetryCollector,
                ),
            },

            <span class="hljs-comment">// 更新策略</span>
            Strategy: params.OtelCol.Spec.DeploymentUpdateStrategy,

            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels:      labels,
                    Annotations: podAnnotations,
                },
                Spec: corev1.PodSpec{
                    <span class="hljs-comment">// ServiceAccount</span>
                    ServiceAccountName: ServiceAccountName(params.OtelCol),

                    <span class="hljs-comment">// Init Containers</span>
                    InitContainers: params.OtelCol.Spec.InitContainers,

                    <span class="hljs-comment">// 主容器 + 額外容器</span>
                    Containers: <span class="hljs-built_in">append</span>(
                        params.OtelCol.Spec.AdditionalContainers,
                        Container(params.Config, params.Log, params.OtelCol, <span class="hljs-literal">true</span>),
                    ),

                    <span class="hljs-comment">// Volumes</span>
                    Volumes: Volumes(params.Config, params.OtelCol),

                    <span class="hljs-comment">// 網路配置</span>
                    DNSPolicy:   manifestutils.GetDNSPolicy(...),
                    DNSConfig:   &amp;params.OtelCol.Spec.PodDNSConfig,
                    HostNetwork: params.OtelCol.Spec.HostNetwork,

                    <span class="hljs-comment">// 進程命名空間共享</span>
                    ShareProcessNamespace: &amp;params.OtelCol.Spec.ShareProcessNamespace,

                    <span class="hljs-comment">// 調度配置</span>
                    Tolerations:               params.OtelCol.Spec.Tolerations,
                    NodeSelector:              params.OtelCol.Spec.NodeSelector,
                    Affinity:                  params.OtelCol.Spec.Affinity,
                    TopologySpreadConstraints: params.OtelCol.Spec.TopologySpreadConstraints,

                    <span class="hljs-comment">// 安全配置</span>
                    SecurityContext:   params.OtelCol.Spec.PodSecurityContext,
                    PriorityClassName: params.OtelCol.Spec.PriorityClassName,

                    <span class="hljs-comment">// 優雅終止</span>
                    TerminationGracePeriodSeconds: params.OtelCol.Spec.TerminationGracePeriodSeconds,
                },
            },
        },
    }, <span class="hljs-literal">nil</span>
}
</code></pre>
<h2 id="heading-52-container">5.2 Container 構建深度解析</h2>
<p><strong>檔案</strong>: <code>internal/manifests/collector/container.go:28-150</code></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Container</span><span class="hljs-params">(
    cfg config.Config,
    logger logr.Logger,
    otelcol v1beta1.OpenTelemetryCollector,
    addConfig <span class="hljs-keyword">bool</span>,
)</span> <span class="hljs-title">corev1</span>.<span class="hljs-title">Container</span></span> {
    <span class="hljs-comment">// ========== 1. 決定鏡像 ==========</span>
    image := otelcol.Spec.Image
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(image) == <span class="hljs-number">0</span> {
        image = cfg.CollectorImage  <span class="hljs-comment">// 使用預設鏡像</span>
    }

    <span class="hljs-comment">// ========== 2. 構建端口列表 ==========</span>
    ports := getContainerPorts(logger, otelcol)
    <span class="hljs-comment">// 從 Collector 配置中解析端口</span>
    <span class="hljs-comment">// 例如：otlp.protocols.grpc.endpoint: 0.0.0.0:4317</span>
    <span class="hljs-comment">//      → ContainerPort{Name: "otlp-grpc", ContainerPort: 4317}</span>

    <span class="hljs-comment">// ========== 3. 構建啟動參數 ==========</span>
    <span class="hljs-keyword">var</span> args []<span class="hljs-keyword">string</span>
    <span class="hljs-keyword">var</span> volumeMounts []corev1.VolumeMount

    <span class="hljs-comment">// 配置文件始終是第一個參數</span>
    <span class="hljs-keyword">if</span> addConfig {
        args = <span class="hljs-built_in">append</span>(args, fmt.Sprintf(
            <span class="hljs-string">"--config=/conf/%s"</span>,
            cfg.CollectorConfigMapEntry,
        ))

        volumeMounts = <span class="hljs-built_in">append</span>(volumeMounts, corev1.VolumeMount{
            Name:      naming.ConfigMapVolume(),
            MountPath: <span class="hljs-string">"/conf"</span>,
        })
    }

    <span class="hljs-comment">// 如果啟用 Target Allocator mTLS</span>
    <span class="hljs-keyword">if</span> otelcol.Spec.TargetAllocator.Enabled &amp;&amp;
       cfg.CertManagerAvailability == certmanager.Available &amp;&amp;
       featuregate.EnableTargetAllocatorMTLS.IsEnabled() {
        volumeMounts = <span class="hljs-built_in">append</span>(volumeMounts, corev1.VolumeMount{
            Name:      naming.TAClientCertificate(otelcol.Name),
            MountPath: constants.TACollectorTLSDirPath,
        })
    }

    <span class="hljs-comment">// 額外的用戶自定義參數（排序保證一致性）</span>
    argsMap := otelcol.Spec.Args
    <span class="hljs-keyword">if</span> argsMap == <span class="hljs-literal">nil</span> {
        argsMap = <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{}
    }

    <span class="hljs-keyword">var</span> sortedArgs []<span class="hljs-keyword">string</span>
    <span class="hljs-keyword">for</span> k, v := <span class="hljs-keyword">range</span> argsMap {
        sortedArgs = <span class="hljs-built_in">append</span>(sortedArgs, fmt.Sprintf(<span class="hljs-string">"--%s=%s"</span>, k, v))
    }
    sort.Strings(sortedArgs)
    args = <span class="hljs-built_in">append</span>(args, sortedArgs...)

    <span class="hljs-comment">// ========== 4. Volume Mounts ==========</span>
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(otelcol.Spec.VolumeMounts) &gt; <span class="hljs-number">0</span> {
        volumeMounts = <span class="hljs-built_in">append</span>(volumeMounts, otelcol.Spec.VolumeMounts...)
    }

    <span class="hljs-comment">// 額外 ConfigMap 掛載</span>
    <span class="hljs-keyword">for</span> _, cm := <span class="hljs-keyword">range</span> otelcol.Spec.ConfigMaps {
        volumeMounts = <span class="hljs-built_in">append</span>(volumeMounts, corev1.VolumeMount{
            Name:      naming.ConfigMapExtra(cm.Name),
            MountPath: path.Join(<span class="hljs-string">"/var/conf"</span>, cm.MountPath, naming.ConfigMapExtra(cm.Name)),
        })
    }

    <span class="hljs-comment">// ========== 5. 健康檢查探針 ==========</span>
    <span class="hljs-comment">// Liveness Probe</span>
    livenessProbe, err := otelcol.Spec.Config.GetLivenessProbe(logger)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        logger.Error(err, <span class="hljs-string">"cannot create liveness probe"</span>)
    } <span class="hljs-keyword">else</span> {
        defaultProbeSettings(livenessProbe, otelcol.Spec.LivenessProbe)
    }

    <span class="hljs-comment">// Readiness Probe</span>
    readinessProbe, err := otelcol.Spec.Config.GetReadinessProbe(logger)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        logger.Error(err, <span class="hljs-string">"cannot create readiness probe"</span>)
    } <span class="hljs-keyword">else</span> {
        defaultProbeSettings(readinessProbe, otelcol.Spec.ReadinessProbe)
    }

    <span class="hljs-comment">// Startup Probe</span>
    startupProbe, err := otelcol.Spec.Config.GetStartupProbe(logger)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        logger.Error(err, <span class="hljs-string">"cannot create startup probe"</span>)
    }

    <span class="hljs-comment">// ========== 6. 環境變數 ==========</span>
    envVars := otelcol.Spec.Env

    <span class="hljs-comment">// 添加預設環境變數</span>
    envVars = <span class="hljs-built_in">append</span>(envVars, corev1.EnvVar{
        Name: <span class="hljs-string">"POD_NAME"</span>,
        ValueFrom: &amp;corev1.EnvVarSource{
            FieldRef: &amp;corev1.ObjectFieldSelector{
                FieldPath: <span class="hljs-string">"metadata.name"</span>,
            },
        },
    })

    <span class="hljs-comment">// ========== 7. 返回完整 Container ==========</span>
    <span class="hljs-keyword">return</span> corev1.Container{
        Name:            naming.Container(),
        Image:           image,
        ImagePullPolicy: otelcol.Spec.ImagePullPolicy,
        Args:            args,
        Ports:           ports,
        VolumeMounts:    volumeMounts,
        Env:             envVars,
        EnvFrom:         otelcol.Spec.EnvFrom,
        Resources:       otelcol.Spec.Resources,
        LivenessProbe:   livenessProbe,
        ReadinessProbe:  readinessProbe,
        StartupProbe:    startupProbe,
        SecurityContext: otelcol.Spec.SecurityContext,
    }
}
</code></pre>
<h3 id="heading-521">5.2.1 端口解析邏輯</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getContainerPorts</span><span class="hljs-params">(logger logr.Logger, otelcol v1beta1.OpenTelemetryCollector)</span> []<span class="hljs-title">corev1</span>.<span class="hljs-title">ContainerPort</span></span> {
    <span class="hljs-keyword">var</span> ports []corev1.ContainerPort

    <span class="hljs-comment">// 從 Collector 配置中解析端口</span>
    cfg := otelcol.Spec.Config

    <span class="hljs-comment">// 解析 receivers 中的端口</span>
    <span class="hljs-keyword">if</span> receivers, ok := cfg.Receivers.Object[<span class="hljs-string">"otlp"</span>].(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}); ok {
        <span class="hljs-keyword">if</span> protocols, ok := receivers[<span class="hljs-string">"protocols"</span>].(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}); ok {
            <span class="hljs-comment">// gRPC 端口</span>
            <span class="hljs-keyword">if</span> grpc, ok := protocols[<span class="hljs-string">"grpc"</span>].(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}); ok {
                <span class="hljs-keyword">if</span> endpoint, ok := grpc[<span class="hljs-string">"endpoint"</span>].(<span class="hljs-keyword">string</span>); ok {
                    port := extractPort(endpoint)  <span class="hljs-comment">// "0.0.0.0:4317" → 4317</span>
                    ports = <span class="hljs-built_in">append</span>(ports, corev1.ContainerPort{
                        Name:          <span class="hljs-string">"otlp-grpc"</span>,
                        ContainerPort: port,
                        Protocol:      corev1.ProtocolTCP,
                    })
                }
            }

            <span class="hljs-comment">// HTTP 端口</span>
            <span class="hljs-keyword">if</span> http, ok := protocols[<span class="hljs-string">"http"</span>].(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}); ok {
                <span class="hljs-keyword">if</span> endpoint, ok := http[<span class="hljs-string">"endpoint"</span>].(<span class="hljs-keyword">string</span>); ok {
                    port := extractPort(endpoint)  <span class="hljs-comment">// "0.0.0.0:4318" → 4318</span>
                    ports = <span class="hljs-built_in">append</span>(ports, corev1.ContainerPort{
                        Name:          <span class="hljs-string">"otlp-http"</span>,
                        ContainerPort: port,
                        Protocol:      corev1.ProtocolTCP,
                    })
                }
            }
        }
    }

    <span class="hljs-comment">// 用戶自定義端口</span>
    <span class="hljs-keyword">for</span> _, portSpec := <span class="hljs-keyword">range</span> otelcol.Spec.Ports {
        ports = <span class="hljs-built_in">append</span>(ports, corev1.ContainerPort{
            Name:          portSpec.Name,
            ContainerPort: portSpec.Port,
            Protocol:      portSpec.Protocol,
        })
    }

    <span class="hljs-keyword">return</span> ports
}
</code></pre>
<h2 id="heading-53-configmap">5.3 ConfigMap 構建與版本控制</h2>
<p><strong>檔案</strong>: <code>internal/manifests/collector/configmap.go</code></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ConfigMaps</span><span class="hljs-params">(params manifests.Params)</span> <span class="hljs-params">([]*corev1.ConfigMap, error)</span></span> {
    <span class="hljs-keyword">var</span> configMaps []*corev1.ConfigMap

    <span class="hljs-comment">// ========== 1. 構建主 ConfigMap ==========</span>
    name := naming.ConfigMap(params.OtelCol.Name)

    <span class="hljs-comment">// 序列化 Collector 配置為 YAML</span>
    configYAML, err := params.OtelCol.Spec.Config.Yaml()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }

    <span class="hljs-comment">// 計算配置哈希（用於觸發 Pod 重啟）</span>
    configHash := hash(configYAML)

    configMap := &amp;corev1.ConfigMap{
        ObjectMeta: metav1.ObjectMeta{
            Name:      fmt.Sprintf(<span class="hljs-string">"%s-%s"</span>, name, configHash[:<span class="hljs-number">8</span>]),
            Namespace: params.OtelCol.Namespace,
            Labels: manifestutils.Labels(
                params.OtelCol.ObjectMeta,
                name,
                params.OtelCol.Spec.Image,
                ComponentOpenTelemetryCollector,
                params.Config.LabelsFilter,
            ),
            Annotations: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
                <span class="hljs-string">"opentelemetry.io/config-hash"</span>: configHash,
                <span class="hljs-string">"opentelemetry.io/created-at"</span>:  time.Now().Format(time.RFC3339),
            },
        },
        Data: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
            cfg.CollectorConfigMapEntry: configYAML,
        },
    }

    configMaps = <span class="hljs-built_in">append</span>(configMaps, configMap)

    <span class="hljs-comment">// ========== 2. Target Allocator ConfigMap ==========</span>
    <span class="hljs-keyword">if</span> params.OtelCol.Spec.TargetAllocator.Enabled {
        taConfigMap, err := BuildTargetAllocatorConfigMap(params)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }
        configMaps = <span class="hljs-built_in">append</span>(configMaps, taConfigMap)
    }

    <span class="hljs-keyword">return</span> configMaps, <span class="hljs-literal">nil</span>
}
</code></pre>
<h3 id="heading-531-configmap">5.3.1 ConfigMap 版本控制實現</h3>
<p><strong>檔案</strong>: <code>internal/controllers/opentelemetrycollector_controller.go:146-158</code></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getCollectorConfigMapsToKeep</span><span class="hljs-params">(
    configVersionsToKeep <span class="hljs-keyword">int</span>,
    configMaps []*corev1.ConfigMap,
)</span> []*<span class="hljs-title">corev1</span>.<span class="hljs-title">ConfigMap</span></span> {
    configVersionsToKeep = max(<span class="hljs-number">1</span>, configVersionsToKeep)

    <span class="hljs-comment">// 按創建時間排序（最新到最舊）</span>
    sort.Slice(configMaps, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(i, j <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">bool</span></span> {
        iTime := configMaps[i].GetCreationTimestamp().Time
        jTime := configMaps[j].GetCreationTimestamp().Time
        <span class="hljs-keyword">return</span> iTime.After(jTime)
    })

    <span class="hljs-comment">// 保留最新的 N 個</span>
    configMapsToKeep := min(configVersionsToKeep, <span class="hljs-built_in">len</span>(configMaps))
    <span class="hljs-keyword">return</span> configMaps[:configMapsToKeep]
}
</code></pre>
<p><strong>工作流程</strong>:</p>
<pre><code class="lang-bash">配置更新 → 創建新 ConfigMap → 舊 ConfigMap 保留（用於回滾）

範例：
1. my-collector-abcd1234  (當前使用)
2. my-collector-efgh5678  (保留)
3. my-collector-ijkl9012  (保留)
4. my-collector-mnop3456  (將被刪除)
</code></pre>
<hr />
<h1 id="heading-webhook">六、Webhook 機制深度解析</h1>
<h2 id="heading-61-webhook">6.1 Webhook 概述</h2>
<p>Kubernetes Admission Webhooks 允許在資源創建/更新前進行攔截和修改。</p>
<pre><code class="lang-bash">User: kubectl apply -f pod.yaml
    │
    ▼
API Server
    │
    ├──► Mutating Webhook  (修改資源)
    │    - Sidecar 注入
    │    - 添加標籤/註解
    │    - 修改容器配置
    │
    ├──► Validating Webhook (驗證資源)
    │    - 檢查配置合法性
    │    - 執行業務規則
    │
    └──► 存儲到 etcd
</code></pre>
<h2 id="heading-62-pod-mutation-webhook">6.2 Pod Mutation Webhook 實現</h2>
<p><strong>檔案</strong>: <code>internal/webhook/podmutation/webhookhandler.go</code></p>
<h3 id="heading-621-webhook-handler">6.2.1 Webhook Handler 結構</h3>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> podMutationWebhook <span class="hljs-keyword">struct</span> {
    client      client.Client     <span class="hljs-comment">// K8s 客戶端</span>
    decoder     admission.Decoder <span class="hljs-comment">// 請求解碼器</span>
    logger      logr.Logger       <span class="hljs-comment">// 日誌</span>
    podMutators []PodMutator      <span class="hljs-comment">// Pod 變更器鏈</span>
    config      config.Config     <span class="hljs-comment">// 配置</span>
}

<span class="hljs-comment">// PodMutator 介面</span>
<span class="hljs-keyword">type</span> PodMutator <span class="hljs-keyword">interface</span> {
    Mutate(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error)
}
</code></pre>
<h3 id="heading-622-webhook">6.2.2 Webhook 註冊</h3>
<pre><code class="lang-go"><span class="hljs-comment">// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=ignore,groups="",resources=pods,verbs=create,versions=v1,name=mpod.kb.io,sideEffects=none,admissionReviewVersions=v1</span>
</code></pre>
<p><strong>參數解析</strong>:</p>
<ul>
<li><p><code>path</code>: Webhook 路徑</p>
</li>
<li><p><code>mutating=true</code>: 變更型 Webhook</p>
</li>
<li><p><code>failurePolicy=ignore</code>: 失敗時允許請求繼續（高可用）</p>
</li>
<li><p><code>resources=pods</code>: 只攔截 Pod</p>
</li>
<li><p><code>verbs=create</code>: 只攔截創建操作</p>
</li>
</ul>
<h3 id="heading-623-handle">6.2.3 Handle 方法完整實現</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(p *podMutationWebhook)</span> <span class="hljs-title">Handle</span><span class="hljs-params">(
    ctx context.Context,
    req admission.Request,
)</span> <span class="hljs-title">admission</span>.<span class="hljs-title">Response</span></span> {
    log := p.logger.WithValues(<span class="hljs-string">"namespace"</span>, req.Namespace, <span class="hljs-string">"name"</span>, req.Name)
    log.Info(<span class="hljs-string">"Webhook called"</span>)

    <span class="hljs-comment">// ========== 1. 解碼 Pod ==========</span>
    pod := corev1.Pod{}
    err := p.decoder.Decode(req, &amp;pod)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Error(err, <span class="hljs-string">"Failed to decode pod"</span>)
        <span class="hljs-keyword">return</span> admission.Errored(http.StatusBadRequest, err)
    }

    log.Info(<span class="hljs-string">"Pod decoded"</span>, <span class="hljs-string">"podName"</span>, pod.Name)

    <span class="hljs-comment">// ========== 2. 獲取 Namespace ==========</span>
    ns := corev1.Namespace{}
    err = p.client.Get(ctx, types.NamespacedName{
        Name:      req.Namespace,
        Namespace: <span class="hljs-string">""</span>,
    }, &amp;ns)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Error(err, <span class="hljs-string">"Failed to get namespace"</span>)
        res := admission.Errored(http.StatusInternalServerError, err)
        <span class="hljs-comment">// 設置 Allowed = true，即使錯誤也不阻止 Pod 創建</span>
        res.Allowed = <span class="hljs-literal">true</span>
        <span class="hljs-keyword">return</span> res
    }

    <span class="hljs-comment">// ========== 3. 執行所有 Mutator ==========</span>
    originalPod := pod.DeepCopy()

    <span class="hljs-keyword">for</span> i, mutator := <span class="hljs-keyword">range</span> p.podMutators {
        log := log.WithValues(<span class="hljs-string">"mutatorIndex"</span>, i)
        log.Info(<span class="hljs-string">"Applying mutator"</span>)

        pod, err = mutator.Mutate(ctx, ns, pod)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            log.Error(err, <span class="hljs-string">"Mutator failed"</span>)
            res := admission.Errored(http.StatusInternalServerError, err)
            res.Allowed = <span class="hljs-literal">true</span>  <span class="hljs-comment">// 錯誤不阻止創建</span>
            <span class="hljs-keyword">return</span> res
        }
    }

    <span class="hljs-comment">// ========== 4. 檢查是否有修改 ==========</span>
    <span class="hljs-keyword">if</span> reflect.DeepEqual(originalPod, &amp;pod) {
        log.Info(<span class="hljs-string">"No changes made, allowing"</span>)
        <span class="hljs-keyword">return</span> admission.Allowed(<span class="hljs-string">"no changes"</span>)
    }

    <span class="hljs-comment">// ========== 5. 生成 JSONPatch ==========</span>
    marshaledPod, err := json.Marshal(pod)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Error(err, <span class="hljs-string">"Failed to marshal pod"</span>)
        res := admission.Errored(http.StatusInternalServerError, err)
        res.Allowed = <span class="hljs-literal">true</span>
        <span class="hljs-keyword">return</span> res
    }

    log.Info(<span class="hljs-string">"Returning patch response"</span>)
    <span class="hljs-keyword">return</span> admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
}
</code></pre>
<h2 id="heading-63-sidecar">6.3 Sidecar 注入實現</h2>
<h3 id="heading-631-sidecar-mutator">6.3.1 Sidecar Mutator</h3>
<p><strong>檔案</strong>: <code>internal/webhook/podmutation/sidecar_mutator.go</code></p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> sidecarMutator <span class="hljs-keyword">struct</span> {
    client client.Client
    logger logr.Logger
    config config.Config
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *sidecarMutator)</span> <span class="hljs-title">Mutate</span><span class="hljs-params">(
    ctx context.Context,
    ns corev1.Namespace,
    pod corev1.Pod,
)</span> <span class="hljs-params">(corev1.Pod, error)</span></span> {
    log := s.logger.WithValues(<span class="hljs-string">"pod"</span>, pod.Name, <span class="hljs-string">"namespace"</span>, ns.Name)

    <span class="hljs-comment">// ========== 1. 檢查是否需要注入 ==========</span>
    <span class="hljs-comment">// 註解：sidecar.opentelemetry.io/inject</span>
    injectAnnotation, exists := pod.Annotations[<span class="hljs-string">"sidecar.opentelemetry.io/inject"</span>]
    <span class="hljs-keyword">if</span> !exists || injectAnnotation == <span class="hljs-string">"false"</span> {
        log.V(<span class="hljs-number">1</span>).Info(<span class="hljs-string">"Sidecar injection not requested"</span>)
        <span class="hljs-keyword">return</span> pod, <span class="hljs-literal">nil</span>
    }

    log.Info(<span class="hljs-string">"Sidecar injection requested"</span>, <span class="hljs-string">"value"</span>, injectAnnotation)

    <span class="hljs-comment">// ========== 2. 查找 OpenTelemetryCollector ==========</span>
    <span class="hljs-keyword">var</span> collectorName <span class="hljs-keyword">string</span>
    <span class="hljs-keyword">if</span> injectAnnotation == <span class="hljs-string">"true"</span> {
        <span class="hljs-comment">// 自動查找（尋找同 namespace 下 mode=sidecar 的 Collector）</span>
        collectorName, err := s.findSidecarCollector(ctx, ns.Name)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> pod, err
        }
        log.Info(<span class="hljs-string">"Auto-detected collector"</span>, <span class="hljs-string">"name"</span>, collectorName)
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// 使用指定的 Collector</span>
        collectorName = injectAnnotation
        log.Info(<span class="hljs-string">"Using specified collector"</span>, <span class="hljs-string">"name"</span>, collectorName)
    }

    <span class="hljs-comment">// 獲取 Collector CR</span>
    collector := &amp;v1beta1.OpenTelemetryCollector{}
    err := s.client.Get(ctx, types.NamespacedName{
        Name:      collectorName,
        Namespace: ns.Name,
    }, collector)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Error(err, <span class="hljs-string">"Failed to get collector"</span>)
        <span class="hljs-keyword">return</span> pod, err
    }

    <span class="hljs-comment">// 驗證模式</span>
    <span class="hljs-keyword">if</span> collector.Spec.Mode != v1alpha1.ModeSidecar {
        <span class="hljs-keyword">return</span> pod, fmt.Errorf(<span class="hljs-string">"collector %s is not in sidecar mode"</span>, collectorName)
    }

    <span class="hljs-comment">// ========== 3. 注入 Sidecar 容器 ==========</span>
    sidecarContainer := s.buildSidecarContainer(collector)
    pod.Spec.Containers = <span class="hljs-built_in">append</span>(pod.Spec.Containers, sidecarContainer)

    <span class="hljs-comment">// ========== 4. 添加 Volumes ==========</span>
    volumes := s.buildSidecarVolumes(collector)
    pod.Spec.Volumes = <span class="hljs-built_in">append</span>(pod.Spec.Volumes, volumes...)

    <span class="hljs-comment">// ========== 5. 修改應用容器環境變數 ==========</span>
    <span class="hljs-comment">// 將 OTLP endpoint 設置為 localhost</span>
    <span class="hljs-keyword">for</span> i := <span class="hljs-keyword">range</span> pod.Spec.Containers {
        <span class="hljs-keyword">if</span> pod.Spec.Containers[i].Name == sidecarContainer.Name {
            <span class="hljs-keyword">continue</span>  <span class="hljs-comment">// 跳過 sidecar 容器本身</span>
        }

        pod.Spec.Containers[i].Env = <span class="hljs-built_in">append</span>(
            pod.Spec.Containers[i].Env,
            corev1.EnvVar{
                Name:  <span class="hljs-string">"OTEL_EXPORTER_OTLP_ENDPOINT"</span>,
                Value: <span class="hljs-string">"http://localhost:4317"</span>,
            },
        )
    }

    log.Info(<span class="hljs-string">"Sidecar injected successfully"</span>)
    <span class="hljs-keyword">return</span> pod, <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *sidecarMutator)</span> <span class="hljs-title">buildSidecarContainer</span><span class="hljs-params">(
    collector *v1beta1.OpenTelemetryCollector,
)</span> <span class="hljs-title">corev1</span>.<span class="hljs-title">Container</span></span> {
    <span class="hljs-keyword">return</span> corev1.Container{
        Name:  <span class="hljs-string">"otc-sidecar"</span>,
        Image: collector.Spec.Image,
        Args: []<span class="hljs-keyword">string</span>{
            <span class="hljs-string">"--config=/conf/collector.yaml"</span>,
        },
        Ports: []corev1.ContainerPort{
            {Name: <span class="hljs-string">"otlp-grpc"</span>, ContainerPort: <span class="hljs-number">4317</span>},
            {Name: <span class="hljs-string">"otlp-http"</span>, ContainerPort: <span class="hljs-number">4318</span>},
        },
        VolumeMounts: []corev1.VolumeMount{
            {
                Name:      <span class="hljs-string">"otc-config"</span>,
                MountPath: <span class="hljs-string">"/conf"</span>,
            },
        },
        Resources: collector.Spec.Resources,
    }
}
</code></pre>
<h3 id="heading-632-sidecar">6.3.2 Sidecar 注入效果</h3>
<p><strong>原始 Pod</strong>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">myapp</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">sidecar.opentelemetry.io/inject:</span> <span class="hljs-string">"true"</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">containers:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
      <span class="hljs-attr">image:</span> <span class="hljs-string">myapp:v1.0</span>
</code></pre>
<p><strong>注入後的 Pod</strong>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">myapp</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">sidecar.opentelemetry.io/inject:</span> <span class="hljs-string">"true"</span>
    <span class="hljs-attr">sidecar.opentelemetry.io/injected:</span> <span class="hljs-string">"true"</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">containers:</span>
    <span class="hljs-comment"># 原始應用容器（已修改）</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
      <span class="hljs-attr">image:</span> <span class="hljs-string">myapp:v1.0</span>
      <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">OTEL_EXPORTER_OTLP_ENDPOINT</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">http://localhost:4317</span>

    <span class="hljs-comment"># 注入的 Sidecar 容器</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">otc-sidecar</span>
      <span class="hljs-attr">image:</span> <span class="hljs-string">otel/opentelemetry-collector:0.88.0</span>
      <span class="hljs-attr">args:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">--config=/conf/collector.yaml</span>
      <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">otlp-grpc</span>
          <span class="hljs-attr">containerPort:</span> <span class="hljs-number">4317</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">otlp-http</span>
          <span class="hljs-attr">containerPort:</span> <span class="hljs-number">4318</span>
      <span class="hljs-attr">volumeMounts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">otc-config</span>
          <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/conf</span>

  <span class="hljs-attr">volumes:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">otc-config</span>
      <span class="hljs-attr">configMap:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">my-sidecar-collector</span>
</code></pre>
<hr />
<h1 id="heading-5lid44cb6zal55m855kw5akd5a6m5pw06kit572u">七、開發環境完整設置</h1>
<h2 id="heading-71">7.1 前置工具安裝</h2>
<h3 id="heading-711-go">7.1.1 Go 語言環境</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== macOS ==========</span>
brew install go@1.24

<span class="hljs-comment"># 設置環境變數</span>
<span class="hljs-built_in">export</span> GOPATH=<span class="hljs-variable">$HOME</span>/go
<span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">$PATH</span>:<span class="hljs-variable">$GOPATH</span>/bin

<span class="hljs-comment"># 驗證</span>
go version  <span class="hljs-comment"># 應該顯示 go1.24 或更高</span>

<span class="hljs-comment"># ========== Linux ==========</span>
<span class="hljs-comment"># 下載並安裝</span>
wget https://go.dev/dl/go1.24.0.linux-amd64.tar.gz
sudo rm -rf /usr/<span class="hljs-built_in">local</span>/go
sudo tar -C /usr/<span class="hljs-built_in">local</span> -xzf go1.24.0.linux-amd64.tar.gz

<span class="hljs-comment"># 添加到 PATH（添加到 ~/.bashrc 或 ~/.zshrc）</span>
<span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">$PATH</span>:/usr/<span class="hljs-built_in">local</span>/go/bin
<span class="hljs-built_in">export</span> GOPATH=<span class="hljs-variable">$HOME</span>/go
<span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">$PATH</span>:<span class="hljs-variable">$GOPATH</span>/bin

<span class="hljs-comment"># 重新加載配置</span>
<span class="hljs-built_in">source</span> ~/.bashrc  <span class="hljs-comment"># 或 source ~/.zshrc</span>
</code></pre>
<h3 id="heading-712-docker">7.1.2 Docker</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== macOS ==========</span>
brew install --cask docker
<span class="hljs-comment"># 或下載 Docker Desktop: https://www.docker.com/products/docker-desktop</span>

<span class="hljs-comment"># ========== Linux (Ubuntu/Debian) ==========</span>
<span class="hljs-comment"># 安裝依賴</span>
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release

<span class="hljs-comment"># 添加 Docker GPG key</span>
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

<span class="hljs-comment"># 設置倉庫</span>
<span class="hljs-built_in">echo</span> \
  <span class="hljs-string">"deb [arch=<span class="hljs-subst">$(dpkg --print-architecture)</span> signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  <span class="hljs-subst">$(lsb_release -cs)</span> stable"</span> | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

<span class="hljs-comment"># 安裝 Docker Engine</span>
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

<span class="hljs-comment"># 允許非 root 用戶使用 Docker</span>
sudo usermod -aG docker <span class="hljs-variable">$USER</span>
newgrp docker

<span class="hljs-comment"># 驗證</span>
docker --version
docker run hello-world
</code></pre>
<h3 id="heading-713-kubectl">7.1.3 kubectl</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== macOS ==========</span>
brew install kubectl

<span class="hljs-comment"># ========== Linux ==========</span>
curl -LO <span class="hljs-string">"https://dl.k8s.io/release/<span class="hljs-subst">$(curl -L -s https://dl.k8s.io/release/stable.txt)</span>/bin/linux/amd64/kubectl"</span>
sudo install -o root -g root -m 0755 kubectl /usr/<span class="hljs-built_in">local</span>/bin/kubectl

<span class="hljs-comment"># 驗證</span>
kubectl version --client
</code></pre>
<h3 id="heading-714-kind-kubernetes-in-docker">7.1.4 Kind (Kubernetes in Docker)</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== macOS ==========</span>
brew install kind

<span class="hljs-comment"># ========== Linux ==========</span>
go install sigs.k8s.io/kind@v0.20.0

<span class="hljs-comment"># 或使用二進制</span>
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/<span class="hljs-built_in">local</span>/bin/kind

<span class="hljs-comment"># 驗證</span>
kind version
</code></pre>
<h3 id="heading-715-kustomize">7.1.5 Kustomize</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== macOS ==========</span>
brew install kustomize

<span class="hljs-comment"># ========== Linux ==========</span>
curl -s <span class="hljs-string">"https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"</span> | bash
sudo mv kustomize /usr/<span class="hljs-built_in">local</span>/bin/

<span class="hljs-comment"># 驗證</span>
kustomize version
</code></pre>
<h3 id="heading-716-controller-gen-kubebuilder">7.1.6 controller-gen (Kubebuilder 工具)</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># 安裝 controller-gen（生成 CRD 和 deepcopy 代碼）</span>
go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest

<span class="hljs-comment"># 驗證</span>
controller-gen --version
</code></pre>
<h3 id="heading-717-operator-sdk">7.1.7 Operator SDK (可選)</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== macOS ==========</span>
brew install operator-sdk

<span class="hljs-comment"># ========== Linux ==========</span>
<span class="hljs-built_in">export</span> ARCH=$(<span class="hljs-keyword">case</span> $(uname -m) <span class="hljs-keyword">in</span> x86_64) <span class="hljs-built_in">echo</span> -n amd64 ;; aarch64) <span class="hljs-built_in">echo</span> -n arm64 ;; *) <span class="hljs-built_in">echo</span> -n $(uname -m) ;; <span class="hljs-keyword">esac</span>)
<span class="hljs-built_in">export</span> OS=$(uname | awk <span class="hljs-string">'{print tolower($0)}'</span>)
<span class="hljs-built_in">export</span> OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.29.0

curl -LO <span class="hljs-variable">${OPERATOR_SDK_DL_URL}</span>/operator-sdk_<span class="hljs-variable">${OS}</span>_<span class="hljs-variable">${ARCH}</span>
chmod +x operator-sdk_<span class="hljs-variable">${OS}</span>_<span class="hljs-variable">${ARCH}</span>
sudo mv operator-sdk_<span class="hljs-variable">${OS}</span>_<span class="hljs-variable">${ARCH}</span> /usr/<span class="hljs-built_in">local</span>/bin/operator-sdk

<span class="hljs-comment"># 驗證</span>
operator-sdk version
</code></pre>
<h2 id="heading-72">7.2 專案設置</h2>
<h3 id="heading-721-clone">7.2.1 Clone 專案</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Clone OpenTelemetry Operator</span>
git <span class="hljs-built_in">clone</span> https://github.com/open-telemetry/opentelemetry-operator.git
<span class="hljs-built_in">cd</span> opentelemetry-operator

<span class="hljs-comment"># 查看分支</span>
git branch -a

<span class="hljs-comment"># 切換到最新的穩定分支（如果需要）</span>
git checkout main
</code></pre>
<h3 id="heading-722-makefile">7.2.2 了解 Makefile</h3>
<p>OpenTelemetry Operator 的 Makefile 提供了豐富的命令：</p>
<p><strong>檔案</strong>: <code>Makefile</code></p>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 查看所有可用命令 ==========</span>
make <span class="hljs-built_in">help</span>

<span class="hljs-comment"># 主要命令：</span>
make manifests         <span class="hljs-comment"># 生成 CRD YAML</span>
make generate          <span class="hljs-comment"># 生成 Go 代碼（deepcopy 等）</span>
make fmt               <span class="hljs-comment"># 格式化代碼</span>
make vet               <span class="hljs-comment"># 靜態分析</span>
make <span class="hljs-built_in">test</span>              <span class="hljs-comment"># 運行單元測試</span>
make docker-build      <span class="hljs-comment"># 構建 Docker 鏡像</span>
make install           <span class="hljs-comment"># 安裝 CRD 到集群</span>
make deploy            <span class="hljs-comment"># 部署 Operator 到集群</span>
make run               <span class="hljs-comment"># 本地運行 Operator</span>
make kind-cluster      <span class="hljs-comment"># 創建 Kind 測試集群</span>
make e2e               <span class="hljs-comment"># 運行 E2E 測試</span>
</code></pre>
<h3 id="heading-723-kind">7.2.3 創建 Kind 集群</h3>
<p><strong>檔案</strong>: <code>Makefile:101</code></p>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 使用 Makefile 創建（推薦）==========</span>
make kind-cluster

<span class="hljs-comment"># 這個命令會：</span>
<span class="hljs-comment"># 1. 創建一個名為 "otel-operator" 的 Kind 集群</span>
<span class="hljs-comment"># 2. 使用配置文件 kind-1.33.yaml（或其他版本）</span>
<span class="hljs-comment"># 3. 自動安裝 cert-manager</span>

<span class="hljs-comment"># ========== 手動創建 ==========</span>
<span class="hljs-comment"># 創建集群配置文件</span>
cat &lt;&lt;EOF &gt; kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    image: kindest/node:v1.33.0
  - role: worker
    image: kindest/node:v1.33.0
  - role: worker
    image: kindest/node:v1.33.0
EOF

<span class="hljs-comment"># 創建集群</span>
kind create cluster \
  --name otel-operator \
  --config kind-config.yaml

<span class="hljs-comment"># 驗證集群</span>
kubectl cluster-info --context kind-otel-operator
kubectl get nodes
</code></pre>
<h3 id="heading-724-cert-manager">7.2.4 安裝 cert-manager</h3>
<p><strong>檔案</strong>: <code>Makefile:106</code> (CERTMANAGER_VERSION)</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 方式 1: 使用 kubectl ==========</span>
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml

<span class="hljs-comment"># 等待 cert-manager 就緒</span>
kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=Available --timeout=300s \
  deployment/cert-manager -n cert-manager

kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=Available --timeout=300s \
  deployment/cert-manager-webhook -n cert-manager

kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=Available --timeout=300s \
  deployment/cert-manager-cainjector -n cert-manager

<span class="hljs-comment"># 驗證</span>
kubectl get pods -n cert-manager

<span class="hljs-comment"># ========== 方式 2: 使用 Helm ==========</span>
helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.13.0 \
  --<span class="hljs-built_in">set</span> installCRDs=<span class="hljs-literal">true</span>

<span class="hljs-comment"># 驗證</span>
kubectl get pods -n cert-manager
</code></pre>
<h2 id="heading-73">7.3 本地開發工作流</h2>
<h3 id="heading-731-crd">7.3.1 生成 CRD 和代碼</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 1. 生成 CRD YAML ==========</span>
make manifests

<span class="hljs-comment"># 這會：</span>
<span class="hljs-comment"># - 從 apis/**/types.go 的註解生成 CRD YAML</span>
<span class="hljs-comment"># - 輸出到 config/crd/bases/</span>
<span class="hljs-comment"># - 使用 controller-gen 工具</span>

<span class="hljs-comment"># 查看生成的 CRD</span>
ls -lh config/crd/bases/
<span class="hljs-comment"># opentelemetry.io_instrumentations.yaml</span>
<span class="hljs-comment"># opentelemetry.io_opentelemetrycollectors.yaml</span>
<span class="hljs-comment"># opentelemetry.io_targetallocators.yaml</span>
<span class="hljs-comment"># opentelemetry.io_opampbridges.yaml</span>

<span class="hljs-comment"># ========== 2. 生成 Go 代碼 ==========</span>
make generate

<span class="hljs-comment"># 這會：</span>
<span class="hljs-comment"># - 生成 DeepCopy 方法（zz_generated.deepcopy.go）</span>
<span class="hljs-comment"># - 使用 controller-gen 工具</span>

<span class="hljs-comment"># 查看生成的代碼</span>
find apis -name <span class="hljs-string">"zz_generated.*.go"</span>
</code></pre>
<p><strong>背後的命令</strong>（來自 Makefile）：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># manifests 目標</span>
controller-gen \
  crd:generateEmbeddedObjectMeta=<span class="hljs-literal">true</span>,maxDescLen=0 \
  rbac:roleName=manager-role \
  webhook \
  paths=<span class="hljs-string">"./..."</span> \
  output:crd:artifacts:config=config/crd/bases

<span class="hljs-comment"># generate 目標</span>
controller-gen object:headerFile=<span class="hljs-string">"hack/boilerplate.go.txt"</span> paths=<span class="hljs-string">"./..."</span>
</code></pre>
<h3 id="heading-732-crd">7.3.2 安裝 CRD 到集群</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 安裝 CRD ==========</span>
make install

<span class="hljs-comment"># 等價於：</span>
<span class="hljs-comment"># kustomize build config/crd | kubectl apply -f -</span>

<span class="hljs-comment"># 驗證 CRD 已安裝</span>
kubectl get crd | grep opentelemetry

<span class="hljs-comment"># 應該看到：</span>
<span class="hljs-comment"># instrumentations.opentelemetry.io</span>
<span class="hljs-comment"># opampbridges.opentelemetry.io</span>
<span class="hljs-comment"># opentelemetrycollectors.opentelemetry.io</span>
<span class="hljs-comment"># targetallocators.opentelemetry.io</span>

<span class="hljs-comment"># 查看 CRD 詳情</span>
kubectl explain opentelemetrycollector
kubectl explain opentelemetrycollector.spec
kubectl explain opentelemetrycollector.spec.config
</code></pre>
<h3 id="heading-733-operator">7.3.3 本地運行 Operator</h3>
<p><strong>方式 1: 使用 Makefile（推薦）</strong></p>
<p><strong>檔案</strong>: <code>Makefile:202-204</code></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 本地運行（不部署到集群）</span>
make run

<span class="hljs-comment"># 這會：</span>
<span class="hljs-comment"># 1. 執行 make generate fmt vet manifests</span>
<span class="hljs-comment"># 2. 運行 go run ./main.go --zap-devel</span>
<span class="hljs-comment"># 3. Operator 運行在本地，連接到 Kind 集群</span>

<span class="hljs-comment"># 默認禁用 Webhooks（本地運行時）</span>
<span class="hljs-comment"># 如果要啟用 Webhooks：</span>
make run ENABLE_WEBHOOKS=<span class="hljs-literal">true</span>
</code></pre>
<p><strong>方式 2: 直接使用 go run</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 設置環境變數</span>
<span class="hljs-built_in">export</span> WATCH_NAMESPACE=default  <span class="hljs-comment"># 只監控 default namespace</span>
<span class="hljs-comment"># 或不設置，監控所有 namespace</span>

<span class="hljs-comment"># 禁用 Webhooks（本地運行通常禁用）</span>
<span class="hljs-built_in">export</span> ENABLE_WEBHOOKS=<span class="hljs-literal">false</span>

<span class="hljs-comment"># 運行</span>
go run ./main.go \
  --zap-devel \
  --metrics-addr=:8080 \
  --enable-leader-election=<span class="hljs-literal">false</span> \
  --health-probe-addr=:8081

<span class="hljs-comment"># 參數說明：</span>
<span class="hljs-comment"># --zap-devel: 開發模式日誌（更詳細）</span>
<span class="hljs-comment"># --metrics-addr: Prometheus metrics 地址</span>
<span class="hljs-comment"># --enable-leader-election: 禁用 leader election（本地單實例）</span>
<span class="hljs-comment"># --health-probe-addr: 健康檢查地址</span>
</code></pre>
<p><strong>查看日誌輸出</strong>：</p>
<pre><code class="lang-bash">2025-01-15T10:00:00.000Z    INFO    setup    Starting the OpenTelemetry Operator
2025-01-15T10:00:00.001Z    INFO    setup    opentelemetry-operator version    {<span class="hljs-string">"version"</span>: <span class="hljs-string">"0.92.0"</span>}
2025-01-15T10:00:00.002Z    INFO    setup    build-date    {<span class="hljs-string">"date"</span>: <span class="hljs-string">"2025-01-15"</span>}
2025-01-15T10:00:00.003Z    INFO    setup    go-version    {<span class="hljs-string">"version"</span>: <span class="hljs-string">"go1.24.0"</span>}
2025-01-15T10:00:00.010Z    INFO    controller-runtime.metrics    Metrics server is starting to listen    {<span class="hljs-string">"addr"</span>: <span class="hljs-string">":8080"</span>}
2025-01-15T10:00:00.011Z    INFO    setup    starting manager
2025-01-15T10:00:00.011Z    INFO    controller    Starting EventSource    {<span class="hljs-string">"controller"</span>: <span class="hljs-string">"opentelemetrycollector"</span>, <span class="hljs-string">"source"</span>: <span class="hljs-string">"kind source: *v1beta1.OpenTelemetryCollector"</span>}
2025-01-15T10:00:00.112Z    INFO    controller    Starting Controller    {<span class="hljs-string">"controller"</span>: <span class="hljs-string">"opentelemetrycollector"</span>}
2025-01-15T10:00:00.112Z    INFO    controller    Starting workers    {<span class="hljs-string">"controller"</span>: <span class="hljs-string">"opentelemetrycollector"</span>, <span class="hljs-string">"worker count"</span>: 1}
</code></pre>
<h3 id="heading-734-operator">7.3.4 測試 Operator</h3>
<p><strong>在本地 Operator 運行時，打開另一個終端</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 創建測試 CR ==========</span>
kubectl apply -f - &lt;&lt;EOF
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: test-local
spec:
  mode: deployment
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
    processors:
      batch: {}
    exporters:
      debug:
        verbosity: detailed
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [batch]
          exporters: [debug]
EOF

<span class="hljs-comment"># ========== 觀察 Operator 日誌 ==========</span>
<span class="hljs-comment"># 在運行 make run 的終端，你會看到：</span>
<span class="hljs-comment"># INFO    Reconciling OpenTelemetryCollector    {"opentelemetrycollector": "default/test-local"}</span>
<span class="hljs-comment"># INFO    Building desired objects</span>
<span class="hljs-comment"># INFO    Creating resource    {"kind": "ServiceAccount", "name": "test-local-collector"}</span>
<span class="hljs-comment"># INFO    Creating resource    {"kind": "ConfigMap", "name": "test-local-collector-abcd1234"}</span>
<span class="hljs-comment"># INFO    Creating resource    {"kind": "Deployment", "name": "test-local-collector"}</span>
<span class="hljs-comment"># INFO    Creating resource    {"kind": "Service", "name": "test-local-collector"}</span>

<span class="hljs-comment"># ========== 驗證資源 ==========</span>
kubectl get otelcol test-local
kubectl get deployment test-local-collector
kubectl get service test-local-collector
kubectl get configmap -l app.kubernetes.io/instance=test-local

<span class="hljs-comment"># ========== 查看 Collector 日誌 ==========</span>
kubectl logs -f deployment/test-local-collector

<span class="hljs-comment"># ========== 測試配置更新 ==========</span>
kubectl patch otelcol test-local --<span class="hljs-built_in">type</span>=merge -p <span class="hljs-string">'
{
  "spec": {
    "config": {
      "exporters": {
        "debug": {
          "verbosity": "normal"
        }
      }
    }
  }
}'</span>

<span class="hljs-comment"># 觀察 Operator 日誌，會看到：</span>
<span class="hljs-comment"># - 新 ConfigMap 被創建</span>
<span class="hljs-comment"># - Deployment 被更新（觸發滾動更新）</span>

<span class="hljs-comment"># ========== 清理 ==========</span>
kubectl delete otelcol test-local
</code></pre>
<h3 id="heading-735">7.3.5 調試技巧</h3>
<h4 id="heading-delve">使用 Delve 調試器</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># 安裝 Delve</span>
go install github.com/go-delve/delve/cmd/dlv@latest

<span class="hljs-comment"># 使用 Delve 運行</span>
dlv debug ./main.go -- \
  --zap-devel \
  --enable-leader-election=<span class="hljs-literal">false</span>

<span class="hljs-comment"># 設置斷點</span>
(dlv) <span class="hljs-built_in">break</span> internal/controllers/opentelemetrycollector_controller.go:234
(dlv) <span class="hljs-built_in">continue</span>

<span class="hljs-comment"># 創建 CR 後，斷點會被觸發</span>
</code></pre>
<h4 id="heading-vs-code">使用 VS Code 調試</h4>
<p>創建 <code>.vscode/launch.json</code>：</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.2.0"</span>,
  <span class="hljs-attr">"configurations"</span>: [
    {
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Debug Operator"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"go"</span>,
      <span class="hljs-attr">"request"</span>: <span class="hljs-string">"launch"</span>,
      <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"debug"</span>,
      <span class="hljs-attr">"program"</span>: <span class="hljs-string">"${workspaceFolder}/main.go"</span>,
      <span class="hljs-attr">"env"</span>: {
        <span class="hljs-attr">"ENABLE_WEBHOOKS"</span>: <span class="hljs-string">"false"</span>
      },
      <span class="hljs-attr">"args"</span>: [
        <span class="hljs-string">"--zap-devel"</span>,
        <span class="hljs-string">"--enable-leader-election=false"</span>
      ]
    }
  ]
}
</code></pre>
<p>按 F5 開始調試，可以設置斷點、查看變數等。</p>
<h3 id="heading-736">7.3.6 部署到集群（完整流程）</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 1. 構建鏡像 ==========</span>
<span class="hljs-comment"># 設置鏡像名稱</span>
<span class="hljs-built_in">export</span> IMG=localhost:5000/opentelemetry-operator:dev

make docker-build IMG=<span class="hljs-variable">$IMG</span>

<span class="hljs-comment"># ========== 2. 推送到本地 registry（Kind 使用）==========</span>
<span class="hljs-comment"># 創建本地 registry（如果沒有）</span>
docker run -d -p 5000:5000 --name kind-registry registry:2

<span class="hljs-comment"># 推送鏡像</span>
docker push <span class="hljs-variable">$IMG</span>

<span class="hljs-comment"># 或直接加載到 Kind</span>
kind load docker-image <span class="hljs-variable">$IMG</span> --name otel-operator

<span class="hljs-comment"># ========== 3. 部署 Operator ==========</span>
make deploy IMG=<span class="hljs-variable">$IMG</span>

<span class="hljs-comment"># 這會：</span>
<span class="hljs-comment"># 1. 安裝 CRD</span>
<span class="hljs-comment"># 2. 創建 Namespace (opentelemetry-operator-system)</span>
<span class="hljs-comment"># 3. 部署 Operator</span>
<span class="hljs-comment"># 4. 創建 RBAC 資源</span>
<span class="hljs-comment"># 5. 設置 Webhooks</span>

<span class="hljs-comment"># ========== 4. 驗證部署 ==========</span>
kubectl get pods -n opentelemetry-operator-system

<span class="hljs-comment"># 應該看到：</span>
<span class="hljs-comment"># NAME                                                        READY   STATUS    RESTARTS   AGE</span>
<span class="hljs-comment"># opentelemetry-operator-controller-manager-xxxxxxxxx-xxxxx   2/2     Running   0          1m</span>

<span class="hljs-comment"># 查看日誌</span>
kubectl logs -f -n opentelemetry-operator-system \
  deployment/opentelemetry-operator-controller-manager \
  -c manager

<span class="hljs-comment"># ========== 5. 測試 ==========</span>
kubectl apply -f config/samples/core_v1beta1_opentelemetrycollector.yaml

kubectl get otelcol
kubectl get all -l app.kubernetes.io/managed-by=opentelemetry-operator

<span class="hljs-comment"># ========== 6. 清理 ==========</span>
make undeploy
</code></pre>
<h2 id="heading-74">7.4 常用開發命令速查</h2>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 代碼生成 ==========</span>
make manifests              <span class="hljs-comment"># 生成 CRD</span>
make generate               <span class="hljs-comment"># 生成 deepcopy 代碼</span>
make bundle                 <span class="hljs-comment"># 生成 Operator bundle（OLM）</span>

<span class="hljs-comment"># ========== 代碼質量 ==========</span>
make fmt                    <span class="hljs-comment"># 格式化代碼</span>
make vet                    <span class="hljs-comment"># 靜態分析</span>
make lint                   <span class="hljs-comment"># Linting（需要 golangci-lint）</span>
make <span class="hljs-built_in">test</span>                   <span class="hljs-comment"># 運行單元測試</span>
make test-coverage          <span class="hljs-comment"># 測試覆蓋率</span>

<span class="hljs-comment"># ========== 構建 ==========</span>
make manager                <span class="hljs-comment"># 構建 Operator 二進制</span>
make targetallocator        <span class="hljs-comment"># 構建 Target Allocator</span>
make operator-opamp-bridge  <span class="hljs-comment"># 構建 OpAMP Bridge</span>
make docker-build           <span class="hljs-comment"># 構建 Docker 鏡像</span>

<span class="hljs-comment"># ========== 部署 ==========</span>
make install                <span class="hljs-comment"># 安裝 CRD</span>
make uninstall              <span class="hljs-comment"># 卸載 CRD</span>
make deploy                 <span class="hljs-comment"># 部署 Operator</span>
make undeploy               <span class="hljs-comment"># 卸載 Operator</span>
make run                    <span class="hljs-comment"># 本地運行</span>

<span class="hljs-comment"># ========== 測試 ==========</span>
make kind-cluster           <span class="hljs-comment"># 創建 Kind 集群</span>
make kind-delete-cluster    <span class="hljs-comment"># 刪除 Kind 集群</span>
make e2e                    <span class="hljs-comment"># E2E 測試</span>
make e2e-targetallocator    <span class="hljs-comment"># Target Allocator E2E</span>
make e2e-instrumentation    <span class="hljs-comment"># Instrumentation E2E</span>

<span class="hljs-comment"># ========== 清理 ==========</span>
make clean                  <span class="hljs-comment"># 清理生成的文件</span>
</code></pre>
<h2 id="heading-75">7.5 開發環境故障排查</h2>
<h3 id="heading-751">7.5.1 常見問題</h3>
<p><strong>問題 1: controller-gen 未找到</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 錯誤信息</span>
controller-gen: <span class="hljs-built_in">command</span> not found

<span class="hljs-comment"># 解決方案</span>
go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest
</code></pre>
<p><strong>問題 2: Kind 集群無法訪問</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 檢查集群</span>
kind get clusters

<span class="hljs-comment"># 檢查 kubeconfig</span>
kubectl config get-contexts

<span class="hljs-comment"># 切換 context</span>
kubectl config use-context kind-otel-operator
</code></pre>
<p><strong>問題 3: cert-manager 未就緒</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 檢查 cert-manager pods</span>
kubectl get pods -n cert-manager

<span class="hljs-comment"># 查看日誌</span>
kubectl logs -n cert-manager deployment/cert-manager

<span class="hljs-comment"># 重新安裝</span>
kubectl delete namespace cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
</code></pre>
<p><strong>問題 4: Webhook 錯誤</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 錯誤信息</span>
Error from server (InternalError): error when creating <span class="hljs-string">"test.yaml"</span>: Internal error occurred: failed calling webhook

<span class="hljs-comment"># 解決方案（本地運行時）</span>
<span class="hljs-built_in">export</span> ENABLE_WEBHOOKS=<span class="hljs-literal">false</span>
make run
</code></pre>
<hr />
<h1 id="heading-5ywr44cb5ris6kmm562w55wl6iih5am6liq">八、測試策略與實踐</h1>
<h2 id="heading-81">8.1 測試金字塔</h2>
<pre><code class="lang-bash">           /\
          /  \
         / E2E \ (少量，關鍵場景)
        /------\
       /  集成  \ (中等，API 交互)
      /----------\
     /  單元測試   \ (大量，快速反饋)
    /--------------\
</code></pre>
<p>OpenTelemetry Operator 的測試覆蓋：</p>
<ul>
<li><p><strong>單元測試</strong>: <code>internal/**/*_test.go</code></p>
</li>
<li><p><strong>集成測試</strong>: <code>tests/e2e*/**</code></p>
</li>
<li><p><strong>性能測試</strong>: 負載測試腳本</p>
</li>
</ul>
<h2 id="heading-82">8.2 單元測試深度實踐</h2>
<h3 id="heading-821">8.2.1 測試結構</h3>
<p><strong>檔案</strong>: <code>internal/controllers/reconcile_test.go</code></p>
<p>這個文件有 <strong>56KB</strong>，包含大量測試用例！</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 查看測試統計</span>
wc -l internal/controllers/reconcile_test.go
<span class="hljs-comment"># 約 1500+ 行</span>

<span class="hljs-comment"># 運行單元測試</span>
make <span class="hljs-built_in">test</span>

<span class="hljs-comment"># 運行特定包</span>
go <span class="hljs-built_in">test</span> ./internal/controllers -v

<span class="hljs-comment"># 運行特定測試</span>
go <span class="hljs-built_in">test</span> ./internal/controllers -v -run TestReconcile_Deployment
</code></pre>
<h3 id="heading-822">8.2.2 編寫單元測試範例</h3>
<p><strong>創建測試文件</strong>: <code>internal/controllers/my_feature_test.go</code></p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> controllers_test

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"testing"</span>

    <span class="hljs-string">"github.com/stretchr/testify/assert"</span>
    <span class="hljs-string">"github.com/stretchr/testify/require"</span>
    appsv1 <span class="hljs-string">"k8s.io/api/apps/v1"</span>
    corev1 <span class="hljs-string">"k8s.io/api/core/v1"</span>
    metav1 <span class="hljs-string">"k8s.io/apimachinery/pkg/apis/meta/v1"</span>
    <span class="hljs-string">"k8s.io/apimachinery/pkg/types"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/client/fake"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/reconcile"</span>

    <span class="hljs-string">"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"</span>
    <span class="hljs-string">"github.com/open-telemetry/opentelemetry-operator/internal/controllers"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestReconcile_CustomFeature</span><span class="hljs-params">(t *testing.T)</span></span> {
    <span class="hljs-comment">// ========== 準備階段 ==========</span>
    ctx := context.Background()

    <span class="hljs-comment">// 創建測試用的 OpenTelemetryCollector</span>
    otelCol := &amp;v1beta1.OpenTelemetryCollector{
        ObjectMeta: metav1.ObjectMeta{
            Name:      <span class="hljs-string">"test-collector"</span>,
            Namespace: <span class="hljs-string">"default"</span>,
        },
        Spec: v1beta1.OpenTelemetryCollectorSpec{
            Mode: v1alpha1.ModeDeployment,
            Config: v1beta1.Config{
                Receivers: v1beta1.AnyConfig{
                    Object: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
                        <span class="hljs-string">"otlp"</span>: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
                            <span class="hljs-string">"protocols"</span>: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
                                <span class="hljs-string">"grpc"</span>: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
                                    <span class="hljs-string">"endpoint"</span>: <span class="hljs-string">"0.0.0.0:4317"</span>,
                                },
                            },
                        },
                    },
                },
                Exporters: v1beta1.AnyConfig{
                    Object: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
                        <span class="hljs-string">"debug"</span>: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{},
                    },
                },
                Service: v1beta1.Service{
                    Pipelines: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]*v1beta1.Pipeline{
                        <span class="hljs-string">"traces"</span>: {
                            Receivers: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"otlp"</span>},
                            Exporters: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"debug"</span>},
                        },
                    },
                },
            },
        },
    }

    <span class="hljs-comment">// 創建 fake client</span>
    scheme := runtime.NewScheme()
    _ = v1beta1.AddToScheme(scheme)
    _ = appsv1.AddToScheme(scheme)
    _ = corev1.AddToScheme(scheme)

    k8sClient := fake.NewClientBuilder().
        WithScheme(scheme).
        WithObjects(otelCol).
        WithStatusSubresource(&amp;v1beta1.OpenTelemetryCollector{}).
        Build()

    <span class="hljs-comment">// 創建 Reconciler</span>
    reconciler := &amp;controllers.OpenTelemetryCollectorReconciler{
        Client:   k8sClient,
        Scheme:   scheme,
        log:      logr.Discard(),
        recorder: record.NewFakeRecorder(<span class="hljs-number">10</span>),
        config:   config.New(),
    }

    <span class="hljs-comment">// ========== 執行階段 ==========</span>
    req := reconcile.Request{
        NamespacedName: types.NamespacedName{
            Name:      <span class="hljs-string">"test-collector"</span>,
            Namespace: <span class="hljs-string">"default"</span>,
        },
    }

    result, err := reconciler.Reconcile(ctx, req)

    <span class="hljs-comment">// ========== 驗證階段 ==========</span>
    require.NoError(t, err)
    assert.False(t, result.Requeue)

    <span class="hljs-comment">// 驗證 Deployment</span>
    deployment := &amp;appsv1.Deployment{}
    err = k8sClient.Get(ctx, types.NamespacedName{
        Name:      <span class="hljs-string">"test-collector-collector"</span>,
        Namespace: <span class="hljs-string">"default"</span>,
    }, deployment)
    require.NoError(t, err)

    <span class="hljs-comment">// 驗證副本數</span>
    assert.Equal(t, <span class="hljs-keyword">int32</span>(<span class="hljs-number">1</span>), *deployment.Spec.Replicas)

    <span class="hljs-comment">// 驗證容器</span>
    assert.Len(t, deployment.Spec.Template.Spec.Containers, <span class="hljs-number">1</span>)
    container := deployment.Spec.Template.Spec.Containers[<span class="hljs-number">0</span>]
    assert.Equal(t, <span class="hljs-string">"otc-container"</span>, container.Name)

    <span class="hljs-comment">// 驗證端口</span>
    require.Len(t, container.Ports, <span class="hljs-number">1</span>)
    assert.Equal(t, <span class="hljs-string">"otlp-grpc"</span>, container.Ports[<span class="hljs-number">0</span>].Name)
    assert.Equal(t, <span class="hljs-keyword">int32</span>(<span class="hljs-number">4317</span>), container.Ports[<span class="hljs-number">0</span>].ContainerPort)

    <span class="hljs-comment">// 驗證 Service</span>
    service := &amp;corev1.Service{}
    err = k8sClient.Get(ctx, types.NamespacedName{
        Name:      <span class="hljs-string">"test-collector-collector"</span>,
        Namespace: <span class="hljs-string">"default"</span>,
    }, service)
    require.NoError(t, err)
    assert.Len(t, service.Spec.Ports, <span class="hljs-number">1</span>)

    <span class="hljs-comment">// 驗證 ConfigMap</span>
    configMap := &amp;corev1.ConfigMap{}
    configMaps := &amp;corev1.ConfigMapList{}
    err = k8sClient.List(ctx, configMaps,
        client.InNamespace(<span class="hljs-string">"default"</span>),
        client.MatchingLabels{<span class="hljs-string">"app.kubernetes.io/instance"</span>: <span class="hljs-string">"test-collector"</span>},
    )
    require.NoError(t, err)
    assert.Len(t, configMaps.Items, <span class="hljs-number">1</span>)

    <span class="hljs-comment">// 驗證 Status</span>
    err = k8sClient.Get(ctx, types.NamespacedName{
        Name:      <span class="hljs-string">"test-collector"</span>,
        Namespace: <span class="hljs-string">"default"</span>,
    }, otelCol)
    require.NoError(t, err)
    assert.NotEmpty(t, otelCol.Status.Version)
}

<span class="hljs-comment">// Table-driven 測試範例</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestReconcile_DifferentModes</span><span class="hljs-params">(t *testing.T)</span></span> {
    tests := []<span class="hljs-keyword">struct</span> {
        name     <span class="hljs-keyword">string</span>
        mode     v1alpha1.Mode
        replicas *<span class="hljs-keyword">int32</span>
        wantType <span class="hljs-keyword">string</span>
    }{
        {
            name:     <span class="hljs-string">"Deployment mode"</span>,
            mode:     v1alpha1.ModeDeployment,
            replicas: ptr.To(<span class="hljs-keyword">int32</span>(<span class="hljs-number">3</span>)),
            wantType: <span class="hljs-string">"Deployment"</span>,
        },
        {
            name:     <span class="hljs-string">"DaemonSet mode"</span>,
            mode:     v1alpha1.ModeDaemonSet,
            replicas: <span class="hljs-literal">nil</span>,
            wantType: <span class="hljs-string">"DaemonSet"</span>,
        },
        {
            name:     <span class="hljs-string">"StatefulSet mode"</span>,
            mode:     v1alpha1.ModeStatefulSet,
            replicas: ptr.To(<span class="hljs-keyword">int32</span>(<span class="hljs-number">2</span>)),
            wantType: <span class="hljs-string">"StatefulSet"</span>,
        },
    }

    <span class="hljs-keyword">for</span> _, tt := <span class="hljs-keyword">range</span> tests {
        t.Run(tt.name, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(t *testing.T)</span></span> {
            <span class="hljs-comment">// 測試邏輯...</span>
        })
    }
}
</code></pre>
<h3 id="heading-823">8.2.3 運行測試</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 運行所有測試 ==========</span>
make <span class="hljs-built_in">test</span>

<span class="hljs-comment"># ========== 運行特定包 ==========</span>
go <span class="hljs-built_in">test</span> ./internal/controllers -v

<span class="hljs-comment"># ========== 運行特定測試 ==========</span>
go <span class="hljs-built_in">test</span> ./internal/controllers -v -run TestReconcile_Deployment

<span class="hljs-comment"># ========== 運行測試並查看覆蓋率 ==========</span>
go <span class="hljs-built_in">test</span> ./... -coverprofile=coverage.out
go tool cover -html=coverage.out

<span class="hljs-comment"># ========== 並行運行測試 ==========</span>
go <span class="hljs-built_in">test</span> ./... -parallel=4

<span class="hljs-comment"># ========== 運行測試並顯示詳細輸出 ==========</span>
go <span class="hljs-built_in">test</span> ./internal/controllers -v -count=1

<span class="hljs-comment"># ========== 測試特定功能 ==========</span>
go <span class="hljs-built_in">test</span> ./internal/manifests/collector -v -run TestDeployment
</code></pre>
<h2 id="heading-83-e2e">8.3 E2E 測試深度實踐</h2>
<h3 id="heading-831-e2e">8.3.1 E2E 測試架構</h3>
<p>OpenTelemetry Operator 使用 <strong>Chainsaw</strong> 進行 E2E 測試。</p>
<p><strong>測試目錄結構</strong>：</p>
<pre><code class="lang-bash">tests/
├── e2e/                       <span class="hljs-comment"># 基礎功能</span>
│   ├── smoke/                 <span class="hljs-comment"># 煙霧測試</span>
│   ├── smoke-targetallocator/ <span class="hljs-comment"># TA 煙霧測試</span>
│   └── smoke-sidecar/         <span class="hljs-comment"># Sidecar 測試</span>
├── e2e-instrumentation/       <span class="hljs-comment"># 自動埋點測試</span>
├── e2e-targetallocator/       <span class="hljs-comment"># Target Allocator</span>
├── e2e-opampbridge/          <span class="hljs-comment"># OpAMP Bridge</span>
└── e2e-upgrade/              <span class="hljs-comment"># 升級測試</span>
</code></pre>
<h3 id="heading-832-chainsaw">8.3.2 Chainsaw 測試範例</h3>
<p><strong>檔案</strong>: <code>tests/e2e/smoke/00-install.yaml</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">simplest</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">config:</span>
    <span class="hljs-attr">receivers:</span>
      <span class="hljs-attr">otlp:</span>
        <span class="hljs-attr">protocols:</span>
          <span class="hljs-attr">grpc:</span>
            <span class="hljs-attr">endpoint:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">:4317</span>
    <span class="hljs-attr">exporters:</span>
      <span class="hljs-attr">debug:</span>
        <span class="hljs-attr">verbosity:</span> <span class="hljs-string">detailed</span>
    <span class="hljs-attr">service:</span>
      <span class="hljs-attr">pipelines:</span>
        <span class="hljs-attr">traces:</span>
          <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>]
          <span class="hljs-attr">exporters:</span> [<span class="hljs-string">debug</span>]
</code></pre>
<p><strong>檔案</strong>: <code>tests/e2e/smoke/01-assert.yaml</code></p>
<pre><code class="lang-yaml"><span class="hljs-comment"># 斷言 Deployment 被創建並就緒</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">simplest-collector</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
<span class="hljs-attr">status:</span>
  <span class="hljs-attr">readyReplicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">availableReplicas:</span> <span class="hljs-number">1</span>
<span class="hljs-meta">---</span>
<span class="hljs-comment"># 斷言 Service 被創建</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">simplest-collector</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">otlp-grpc</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">4317</span>
      <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
      <span class="hljs-attr">targetPort:</span> <span class="hljs-number">4317</span>
</code></pre>
<p><strong>檔案</strong>: <code>tests/e2e/smoke/chainsaw-test.yaml</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">chainsaw.kyverno.io/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Test</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">smoke</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">collector</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">apply:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">00</span><span class="hljs-string">-install.yaml</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">assert:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">01</span><span class="hljs-string">-assert.yaml</span>
      <span class="hljs-attr">catch:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">describe:</span>
            <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
            <span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">describe:</span>
            <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
            <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">podLogs:</span>
            <span class="hljs-attr">selector:</span> <span class="hljs-string">app.kubernetes.io/name=simplest-collector</span>
</code></pre>
<h3 id="heading-833-e2e">8.3.3 運行 E2E 測試</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 1. 創建測試集群 ==========</span>
make kind-cluster

<span class="hljs-comment"># ========== 2. 構建並載入鏡像 ==========</span>
make docker-build
make docker-build-targetallocator
make docker-build-operator-opamp-bridge

<span class="hljs-comment"># 載入鏡像到 Kind</span>
kind load docker-image \
  ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator:latest \
  --name otel-operator

<span class="hljs-comment"># ========== 3. 部署 Operator ==========</span>
make deploy IMG=ghcr.io/open-telemetry/opentelemetry-operator/opentelemetry-operator:latest

<span class="hljs-comment"># ========== 4. 運行 E2E 測試 ==========</span>
make e2e

<span class="hljs-comment"># 運行特定測試</span>
make e2e-instrumentation
make e2e-targetallocator
make e2e-upgrade

<span class="hljs-comment"># ========== 5. 查看測試結果 ==========</span>
<span class="hljs-comment"># Chainsaw 會輸出詳細的測試報告</span>

<span class="hljs-comment"># ========== 6. 清理 ==========</span>
make kind-delete-cluster
</code></pre>
<h3 id="heading-834-e2e">8.3.4 編寫自定義 E2E 測試</h3>
<p><strong>創建測試目錄</strong>：</p>
<pre><code class="lang-bash">mkdir -p tests/e2e-custom-feature
<span class="hljs-built_in">cd</span> tests/e2e-custom-feature
</code></pre>
<p><strong>創建測試清單</strong> (<code>00-install.yaml</code>):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">custom-test</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">mode:</span> <span class="hljs-string">deployment</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">config:</span>
    <span class="hljs-attr">receivers:</span>
      <span class="hljs-attr">otlp:</span>
        <span class="hljs-attr">protocols:</span>
          <span class="hljs-attr">grpc:</span> {}
    <span class="hljs-attr">processors:</span>
      <span class="hljs-attr">batch:</span>
        <span class="hljs-attr">send_batch_size:</span> <span class="hljs-number">1000</span>
        <span class="hljs-attr">timeout:</span> <span class="hljs-string">10s</span>
    <span class="hljs-attr">exporters:</span>
      <span class="hljs-attr">debug:</span> {}
    <span class="hljs-attr">service:</span>
      <span class="hljs-attr">pipelines:</span>
        <span class="hljs-attr">traces:</span>
          <span class="hljs-attr">receivers:</span> [<span class="hljs-string">otlp</span>]
          <span class="hljs-attr">processors:</span> [<span class="hljs-string">batch</span>]
          <span class="hljs-attr">exporters:</span> [<span class="hljs-string">debug</span>]
</code></pre>
<p><strong>創建斷言</strong> (<code>01-assert.yaml</code>):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">custom-test-collector</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">2</span>
<span class="hljs-attr">status:</span>
  <span class="hljs-attr">readyReplicas:</span> <span class="hljs-number">2</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ConfigMap</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">(custom-test-collector-*)</span>
<span class="hljs-attr">data:</span>
  <span class="hljs-attr">collector.yaml:</span> <span class="hljs-string">|
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317</span>
</code></pre>
<p><strong>創建 Chainsaw 測試</strong> (<code>chainsaw-test.yaml</code>):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">chainsaw.kyverno.io/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Test</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">custom-feature</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">description:</span> <span class="hljs-string">Test</span> <span class="hljs-string">custom</span> <span class="hljs-string">feature</span>
  <span class="hljs-attr">timeouts:</span>
    <span class="hljs-attr">apply:</span> <span class="hljs-string">30s</span>
    <span class="hljs-attr">assert:</span> <span class="hljs-string">60s</span>
  <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">collector</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">apply:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">00</span><span class="hljs-string">-install.yaml</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">assert:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">01</span><span class="hljs-string">-assert.yaml</span>
            <span class="hljs-attr">timeout:</span> <span class="hljs-string">2m</span>
      <span class="hljs-attr">catch:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">describe:</span>
            <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
            <span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">podLogs:</span>
            <span class="hljs-attr">selector:</span> <span class="hljs-string">app.kubernetes.io/name=custom-test-collector</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Test</span> <span class="hljs-string">configuration</span> <span class="hljs-string">update</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">patch:</span>
            <span class="hljs-attr">resource:</span>
              <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
              <span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
              <span class="hljs-attr">metadata:</span>
                <span class="hljs-attr">name:</span> <span class="hljs-string">custom-test</span>
            <span class="hljs-attr">merge:</span>
              <span class="hljs-attr">spec:</span>
                <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">sleep:</span>
            <span class="hljs-attr">duration:</span> <span class="hljs-string">10s</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">assert:</span>
            <span class="hljs-attr">resource:</span>
              <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
              <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
              <span class="hljs-attr">metadata:</span>
                <span class="hljs-attr">name:</span> <span class="hljs-string">custom-test-collector</span>
              <span class="hljs-attr">spec:</span>
                <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Cleanup</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">delete:</span>
            <span class="hljs-attr">ref:</span>
              <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">opentelemetry.io/v1beta1</span>
              <span class="hljs-attr">kind:</span> <span class="hljs-string">OpenTelemetryCollector</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">custom-test</span>
</code></pre>
<p><strong>運行自定義測試</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 安裝 Chainsaw</span>
go install github.com/kyverno/chainsaw@latest

<span class="hljs-comment"># 運行測試</span>
chainsaw <span class="hljs-built_in">test</span> --test-dir ./tests/e2e-custom-feature
</code></pre>
<h2 id="heading-84">8.4 測試覆蓋率</h2>
<pre><code class="lang-bash"><span class="hljs-comment"># ========== 生成覆蓋率報告 ==========</span>
go <span class="hljs-built_in">test</span> ./... -coverprofile=coverage.out

<span class="hljs-comment"># ========== 查看覆蓋率摘要 ==========</span>
go tool cover -func=coverage.out

<span class="hljs-comment"># 輸出範例：</span>
<span class="hljs-comment"># github.com/open-telemetry/opentelemetry-operator/internal/controllers/opentelemetrycollector_controller.go:234:    Reconcile        85.7%</span>
<span class="hljs-comment"># github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector/deployment.go:17:        Deployment        92.3%</span>

<span class="hljs-comment"># ========== 生成 HTML 報告 ==========</span>
go tool cover -html=coverage.out -o coverage.html

<span class="hljs-comment"># 在瀏覽器中打開</span>
open coverage.html  <span class="hljs-comment"># macOS</span>
xdg-open coverage.html  <span class="hljs-comment"># Linux</span>

<span class="hljs-comment"># ========== 按包查看覆蓋率 ==========</span>
go <span class="hljs-built_in">test</span> ./internal/controllers -coverprofile=controllers.out
go tool cover -func=controllers.out
</code></pre>
<h2 id="heading-85">8.5 性能測試與基準測試</h2>
<h3 id="heading-851-benchmark">8.5.1 Benchmark 測試</h3>
<p><strong>創建</strong>: <code>internal/controllers/reconcile_bench_test.go</code></p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> controllers_test

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"testing"</span>

    metav1 <span class="hljs-string">"k8s.io/apimachinery/pkg/apis/meta/v1"</span>
    <span class="hljs-string">"k8s.io/apimachinery/pkg/types"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/client/fake"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/reconcile"</span>

    <span class="hljs-string">"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">BenchmarkReconcile</span><span class="hljs-params">(b *testing.B)</span></span> {
    <span class="hljs-comment">// 準備測試數據</span>
    otelCol := &amp;v1beta1.OpenTelemetryCollector{
        ObjectMeta: metav1.ObjectMeta{
            Name:      <span class="hljs-string">"benchmark-test"</span>,
            Namespace: <span class="hljs-string">"default"</span>,
        },
        Spec: v1beta1.OpenTelemetryCollectorSpec{
            Mode: v1alpha1.ModeDeployment,
            Config: v1beta1.Config{
                <span class="hljs-comment">// ... 配置</span>
            },
        },
    }

    k8sClient := fake.NewClientBuilder().
        WithObjects(otelCol).
        Build()

    reconciler := &amp;controllers.OpenTelemetryCollectorReconciler{
        Client: k8sClient,
        <span class="hljs-comment">// ... 其他字段</span>
    }

    req := reconcile.Request{
        NamespacedName: types.NamespacedName{
            Name:      <span class="hljs-string">"benchmark-test"</span>,
            Namespace: <span class="hljs-string">"default"</span>,
        },
    }

    ctx := context.Background()

    <span class="hljs-comment">// 運行 benchmark</span>
    b.ResetTimer()
    <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; b.N; i++ {
        _, err := reconciler.Reconcile(ctx, req)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            b.Fatal(err)
        }
    }
}
</code></pre>
<p><strong>運行 Benchmark</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 運行 benchmark</span>
go <span class="hljs-built_in">test</span> -bench=. ./internal/controllers

<span class="hljs-comment"># 輸出範例：</span>
<span class="hljs-comment"># BenchmarkReconcile-8           1000       1234567 ns/op</span>

<span class="hljs-comment"># 帶內存分析</span>
go <span class="hljs-built_in">test</span> -bench=. -benchmem ./internal/controllers

<span class="hljs-comment"># 輸出範例：</span>
<span class="hljs-comment"># BenchmarkReconcile-8           1000       1234567 ns/op      123456 B/op        1234 allocs/op</span>

<span class="hljs-comment"># 生成 CPU profile</span>
go <span class="hljs-built_in">test</span> -bench=. -cpuprofile=cpu.prof ./internal/controllers

<span class="hljs-comment"># 分析 profile</span>
go tool pprof cpu.prof
</code></pre>
<hr />
<h1 id="heading-nginx-operator">九、實戰專案：Nginx Operator</h1>
<p>在本章中，我們將從零開始實作一個完整的 Nginx Operator，應用前面學到的所有概念。</p>
<h2 id="heading-91">9.1 專案目標與架構</h2>
<h3 id="heading-911">9.1.1 功能需求</h3>
<p>我們的 Nginx Operator 需要支援：</p>
<ol>
<li><p><strong>自動部署 Nginx</strong>：根據 CR 創建 Deployment</p>
</li>
<li><p><strong>配置管理</strong>：支援自定義 nginx.conf</p>
</li>
<li><p><strong>服務暴露</strong>：自動創建 Service</p>
</li>
<li><p><strong>配置熱更新</strong>：ConfigMap 變更後自動觸發滾動更新</p>
</li>
<li><p><strong>版本管理</strong>：支援 Nginx 版本升級</p>
</li>
<li><p><strong>健康檢查</strong>：配置 liveness 和 readiness probe</p>
</li>
</ol>
<h3 id="heading-912-crd">9.1.2 CRD 設計</h3>
<p><strong>API 定義</strong> (<code>apis/v1alpha1/nginx_types.go</code>)：</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> v1alpha1

<span class="hljs-keyword">import</span> (
    corev1 <span class="hljs-string">"k8s.io/api/core/v1"</span>
    metav1 <span class="hljs-string">"k8s.io/apimachinery/pkg/apis/meta/v1"</span>
)

<span class="hljs-comment">// NginxSpec defines the desired state of Nginx</span>
<span class="hljs-keyword">type</span> NginxSpec <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// +kubebuilder:validation:Minimum=1</span>
    <span class="hljs-comment">// +kubebuilder:validation:Maximum=10</span>
    <span class="hljs-comment">// +kubebuilder:default=1</span>
    Replicas *<span class="hljs-keyword">int32</span> <span class="hljs-string">`json:"replicas,omitempty"`</span>

    <span class="hljs-comment">// +kubebuilder:validation:Pattern=`^nginx:.*$`</span>
    <span class="hljs-comment">// +kubebuilder:default="nginx:1.25"</span>
    Image <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"image,omitempty"`</span>

    <span class="hljs-comment">// +optional</span>
    Resources corev1.ResourceRequirements <span class="hljs-string">`json:"resources,omitempty"`</span>

    <span class="hljs-comment">// +optional</span>
    Config <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"config,omitempty"`</span>

    <span class="hljs-comment">// +optional</span>
    <span class="hljs-comment">// +kubebuilder:validation:Minimum=1</span>
    <span class="hljs-comment">// +kubebuilder:validation:Maximum=65535</span>
    <span class="hljs-comment">// +kubebuilder:default=80</span>
    Port <span class="hljs-keyword">int32</span> <span class="hljs-string">`json:"port,omitempty"`</span>

    <span class="hljs-comment">// +optional</span>
    <span class="hljs-comment">// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer</span>
    <span class="hljs-comment">// +kubebuilder:default=ClusterIP</span>
    ServiceType corev1.ServiceType <span class="hljs-string">`json:"serviceType,omitempty"`</span>
}

<span class="hljs-comment">// NginxStatus defines the observed state of Nginx</span>
<span class="hljs-keyword">type</span> NginxStatus <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// Conditions represent the latest available observations of the Nginx state</span>
    <span class="hljs-comment">// +optional</span>
    Conditions []metav1.Condition <span class="hljs-string">`json:"conditions,omitempty"`</span>

    <span class="hljs-comment">// ReadyReplicas is the number of ready replicas</span>
    <span class="hljs-comment">// +optional</span>
    ReadyReplicas <span class="hljs-keyword">int32</span> <span class="hljs-string">`json:"readyReplicas,omitempty"`</span>

    <span class="hljs-comment">// ConfigVersion tracks the version of the ConfigMap</span>
    <span class="hljs-comment">// +optional</span>
    ConfigVersion <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"configVersion,omitempty"`</span>

    <span class="hljs-comment">// LastUpdateTime is the last time the status was updated</span>
    <span class="hljs-comment">// +optional</span>
    LastUpdateTime *metav1.Time <span class="hljs-string">`json:"lastUpdateTime,omitempty"`</span>
}

<span class="hljs-comment">// +kubebuilder:object:root=true</span>
<span class="hljs-comment">// +kubebuilder:subresource:status</span>
<span class="hljs-comment">// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.readyReplicas</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.spec.replicas`</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas`</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Image",type=string,JSONPath=`.spec.image`</span>
<span class="hljs-comment">// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`</span>

<span class="hljs-comment">// Nginx is the Schema for the nginxes API</span>
<span class="hljs-keyword">type</span> Nginx <span class="hljs-keyword">struct</span> {
    metav1.TypeMeta   <span class="hljs-string">`json:",inline"`</span>
    metav1.ObjectMeta <span class="hljs-string">`json:"metadata,omitempty"`</span>

    Spec   NginxSpec   <span class="hljs-string">`json:"spec,omitempty"`</span>
    Status NginxStatus <span class="hljs-string">`json:"status,omitempty"`</span>
}

<span class="hljs-comment">// +kubebuilder:object:root=true</span>

<span class="hljs-comment">// NginxList contains a list of Nginx</span>
<span class="hljs-keyword">type</span> NginxList <span class="hljs-keyword">struct</span> {
    metav1.TypeMeta <span class="hljs-string">`json:",inline"`</span>
    metav1.ListMeta <span class="hljs-string">`json:"metadata,omitempty"`</span>
    Items           []Nginx <span class="hljs-string">`json:"items"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
    SchemeBuilder.Register(&amp;Nginx{}, &amp;NginxList{})
}
</code></pre>
<p><strong>關鍵註解說明</strong>：</p>
<ul>
<li><p><code>+kubebuilder:subresource:status</code>：啟用 status subresource</p>
</li>
<li><p><code>+kubebuilder:subresource:scale</code>：支援 <code>kubectl scale</code> 命令</p>
</li>
<li><p><code>+kubebuilder:printcolumn</code>：自定義 <code>kubectl get</code> 輸出列</p>
</li>
<li><p><code>+kubebuilder:validation</code>：欄位驗證規則</p>
</li>
</ul>
<h2 id="heading-92-controller">9.2 Controller 實作</h2>
<h3 id="heading-921-reconciler">9.2.1 Reconciler 主邏輯</h3>
<p><strong>Controller</strong> (<code>internal/controller/nginx_controller.go</code>)：</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> controller

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"crypto/sha256"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"time"</span>

    appsv1 <span class="hljs-string">"k8s.io/api/apps/v1"</span>
    corev1 <span class="hljs-string">"k8s.io/api/core/v1"</span>
    apierrors <span class="hljs-string">"k8s.io/apimachinery/pkg/api/errors"</span>
    <span class="hljs-string">"k8s.io/apimachinery/pkg/api/meta"</span>
    metav1 <span class="hljs-string">"k8s.io/apimachinery/pkg/apis/meta/v1"</span>
    <span class="hljs-string">"k8s.io/apimachinery/pkg/runtime"</span>
    <span class="hljs-string">"k8s.io/apimachinery/pkg/types"</span>
    ctrl <span class="hljs-string">"sigs.k8s.io/controller-runtime"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/client"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/log"</span>

    webappv1alpha1 <span class="hljs-string">"example.com/nginx-operator/apis/v1alpha1"</span>
)

<span class="hljs-keyword">const</span> (
    nginxFinalizer = <span class="hljs-string">"nginx.webapp.example.com/finalizer"</span>

    <span class="hljs-comment">// Condition types</span>
    TypeAvailable   = <span class="hljs-string">"Available"</span>
    TypeProgressing = <span class="hljs-string">"Progressing"</span>
    TypeDegraded    = <span class="hljs-string">"Degraded"</span>
)

<span class="hljs-comment">// NginxReconciler reconciles a Nginx object</span>
<span class="hljs-keyword">type</span> NginxReconciler <span class="hljs-keyword">struct</span> {
    client.Client
    Scheme *runtime.Scheme
}

<span class="hljs-comment">// +kubebuilder:rbac:groups=webapp.example.com,resources=nginxes,verbs=get;list;watch;create;update;patch;delete</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=webapp.example.com,resources=nginxes/status,verbs=get;update;patch</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=webapp.example.com,resources=nginxes/finalizers,verbs=update</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=core,resources=services;configmaps,verbs=get;list;watch;create;update;patch;delete</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">Reconcile</span><span class="hljs-params">(ctx context.Context, req ctrl.Request)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    logger := log.FromContext(ctx)
    logger.Info(<span class="hljs-string">"Reconciling Nginx"</span>, <span class="hljs-string">"namespace"</span>, req.Namespace, <span class="hljs-string">"name"</span>, req.Name)

    <span class="hljs-comment">// 1. 獲取 Nginx 資源</span>
    nginx := &amp;webappv1alpha1.Nginx{}
    <span class="hljs-keyword">if</span> err := r.Get(ctx, req.NamespacedName, nginx); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">if</span> apierrors.IsNotFound(err) {
            logger.Info(<span class="hljs-string">"Nginx resource not found, likely deleted"</span>)
            <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
        }
        logger.Error(err, <span class="hljs-string">"Failed to get Nginx"</span>)
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-comment">// 2. 處理刪除邏輯 (Finalizer)</span>
    <span class="hljs-keyword">if</span> !nginx.ObjectMeta.DeletionTimestamp.IsZero() {
        <span class="hljs-keyword">return</span> r.reconcileDelete(ctx, nginx)
    }

    <span class="hljs-comment">// 3. 添加 Finalizer</span>
    <span class="hljs-keyword">if</span> !controllerutil.ContainsFinalizer(nginx, nginxFinalizer) {
        controllerutil.AddFinalizer(nginx, nginxFinalizer)
        <span class="hljs-keyword">if</span> err := r.Update(ctx, nginx); err != <span class="hljs-literal">nil</span> {
            logger.Error(err, <span class="hljs-string">"Failed to add finalizer"</span>)
            <span class="hljs-keyword">return</span> ctrl.Result{}, err
        }
        <span class="hljs-keyword">return</span> ctrl.Result{Requeue: <span class="hljs-literal">true</span>}, <span class="hljs-literal">nil</span>
    }

    <span class="hljs-comment">// 4. Reconcile ConfigMap</span>
    configMap, configVersion, err := r.reconcileConfigMap(ctx, nginx)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        r.setCondition(nginx, TypeDegraded, metav1.ConditionTrue, <span class="hljs-string">"ConfigMapFailed"</span>, err.Error())
        _ = r.Status().Update(ctx, nginx)
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-comment">// 5. Reconcile Deployment</span>
    <span class="hljs-keyword">if</span> err := r.reconcileDeployment(ctx, nginx, configVersion); err != <span class="hljs-literal">nil</span> {
        r.setCondition(nginx, TypeDegraded, metav1.ConditionTrue, <span class="hljs-string">"DeploymentFailed"</span>, err.Error())
        _ = r.Status().Update(ctx, nginx)
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-comment">// 6. Reconcile Service</span>
    <span class="hljs-keyword">if</span> err := r.reconcileService(ctx, nginx); err != <span class="hljs-literal">nil</span> {
        r.setCondition(nginx, TypeDegraded, metav1.ConditionTrue, <span class="hljs-string">"ServiceFailed"</span>, err.Error())
        _ = r.Status().Update(ctx, nginx)
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-comment">// 7. 更新 Status</span>
    <span class="hljs-keyword">if</span> err := r.updateStatus(ctx, nginx, configVersion); err != <span class="hljs-literal">nil</span> {
        logger.Error(err, <span class="hljs-string">"Failed to update status"</span>)
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-comment">// 8. 設置成功 Condition</span>
    r.setCondition(nginx, TypeAvailable, metav1.ConditionTrue, <span class="hljs-string">"ReconcileSuccess"</span>, <span class="hljs-string">"Nginx is available"</span>)
    r.setCondition(nginx, TypeDegraded, metav1.ConditionFalse, <span class="hljs-string">"ReconcileSuccess"</span>, <span class="hljs-string">""</span>)
    <span class="hljs-keyword">if</span> err := r.Status().Update(ctx, nginx); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    logger.Info(<span class="hljs-string">"Successfully reconciled Nginx"</span>)
    <span class="hljs-keyword">return</span> ctrl.Result{RequeueAfter: <span class="hljs-number">30</span> * time.Second}, <span class="hljs-literal">nil</span>
}

<span class="hljs-comment">// reconcileConfigMap 管理 ConfigMap</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">reconcileConfigMap</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx)</span> <span class="hljs-params">(*corev1.ConfigMap, <span class="hljs-keyword">string</span>, error)</span></span> {
    logger := log.FromContext(ctx)

    <span class="hljs-comment">// 默認 nginx 配置</span>
    defaultConfig := <span class="hljs-string">`
events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        location / {
            root /usr/share/nginx/html;
            index index.html;
        }
    }
}`</span>

    config := nginx.Spec.Config
    <span class="hljs-keyword">if</span> config == <span class="hljs-string">""</span> {
        config = defaultConfig
    }

    <span class="hljs-comment">// 計算配置的版本（hash）</span>
    hash := sha256.Sum256([]<span class="hljs-keyword">byte</span>(config))
    configVersion := fmt.Sprintf(<span class="hljs-string">"%x"</span>, hash[:<span class="hljs-number">8</span>])

    configMap := &amp;corev1.ConfigMap{
        ObjectMeta: metav1.ObjectMeta{
            Name:      nginx.Name + <span class="hljs-string">"-config"</span>,
            Namespace: nginx.Namespace,
            Labels: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
                <span class="hljs-string">"app"</span>:     <span class="hljs-string">"nginx"</span>,
                <span class="hljs-string">"nginx"</span>:   nginx.Name,
                <span class="hljs-string">"version"</span>: configVersion,
            },
        },
        Data: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
            <span class="hljs-string">"nginx.conf"</span>: config,
        },
    }

    <span class="hljs-comment">// 設置 Owner Reference</span>
    <span class="hljs-keyword">if</span> err := controllerutil.SetControllerReference(nginx, configMap, r.Scheme); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-string">""</span>, err
    }

    <span class="hljs-comment">// 創建或更新 ConfigMap</span>
    existing := &amp;corev1.ConfigMap{}
    err := r.Get(ctx, types.NamespacedName{
        Name:      configMap.Name,
        Namespace: configMap.Namespace,
    }, existing)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">if</span> apierrors.IsNotFound(err) {
            logger.Info(<span class="hljs-string">"Creating ConfigMap"</span>, <span class="hljs-string">"name"</span>, configMap.Name)
            <span class="hljs-keyword">if</span> err := r.Create(ctx, configMap); err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-string">""</span>, err
            }
            <span class="hljs-keyword">return</span> configMap, configVersion, <span class="hljs-literal">nil</span>
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-string">""</span>, err
    }

    <span class="hljs-comment">// 更新 ConfigMap</span>
    existing.Data = configMap.Data
    existing.Labels = configMap.Labels
    logger.Info(<span class="hljs-string">"Updating ConfigMap"</span>, <span class="hljs-string">"name"</span>, configMap.Name)
    <span class="hljs-keyword">if</span> err := r.Update(ctx, existing); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-string">""</span>, err
    }

    <span class="hljs-keyword">return</span> existing, configVersion, <span class="hljs-literal">nil</span>
}

<span class="hljs-comment">// reconcileDeployment 管理 Deployment</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">reconcileDeployment</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx, configVersion <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
    logger := log.FromContext(ctx)

    replicas := <span class="hljs-keyword">int32</span>(<span class="hljs-number">1</span>)
    <span class="hljs-keyword">if</span> nginx.Spec.Replicas != <span class="hljs-literal">nil</span> {
        replicas = *nginx.Spec.Replicas
    }

    image := <span class="hljs-string">"nginx:1.25"</span>
    <span class="hljs-keyword">if</span> nginx.Spec.Image != <span class="hljs-string">""</span> {
        image = nginx.Spec.Image
    }

    port := <span class="hljs-keyword">int32</span>(<span class="hljs-number">80</span>)
    <span class="hljs-keyword">if</span> nginx.Spec.Port != <span class="hljs-number">0</span> {
        port = nginx.Spec.Port
    }

    deployment := &amp;appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      nginx.Name,
            Namespace: nginx.Namespace,
            Labels: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
                <span class="hljs-string">"app"</span>:   <span class="hljs-string">"nginx"</span>,
                <span class="hljs-string">"nginx"</span>: nginx.Name,
            },
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: &amp;replicas,
            Selector: &amp;metav1.LabelSelector{
                MatchLabels: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
                    <span class="hljs-string">"app"</span>:   <span class="hljs-string">"nginx"</span>,
                    <span class="hljs-string">"nginx"</span>: nginx.Name,
                },
            },
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
                        <span class="hljs-string">"app"</span>:           <span class="hljs-string">"nginx"</span>,
                        <span class="hljs-string">"nginx"</span>:         nginx.Name,
                        <span class="hljs-string">"config-version"</span>: configVersion,  <span class="hljs-comment">// 配置版本變更時觸發滾動更新</span>
                    },
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Name:  <span class="hljs-string">"nginx"</span>,
                            Image: image,
                            Ports: []corev1.ContainerPort{
                                {
                                    Name:          <span class="hljs-string">"http"</span>,
                                    ContainerPort: port,
                                    Protocol:      corev1.ProtocolTCP,
                                },
                            },
                            VolumeMounts: []corev1.VolumeMount{
                                {
                                    Name:      <span class="hljs-string">"config"</span>,
                                    MountPath: <span class="hljs-string">"/etc/nginx/nginx.conf"</span>,
                                    SubPath:   <span class="hljs-string">"nginx.conf"</span>,
                                },
                            },
                            Resources: nginx.Spec.Resources,
                            LivenessProbe: &amp;corev1.Probe{
                                ProbeHandler: corev1.ProbeHandler{
                                    HTTPGet: &amp;corev1.HTTPGetAction{
                                        Path: <span class="hljs-string">"/"</span>,
                                        Port: intstr.FromInt(<span class="hljs-keyword">int</span>(port)),
                                    },
                                },
                                InitialDelaySeconds: <span class="hljs-number">10</span>,
                                PeriodSeconds:       <span class="hljs-number">10</span>,
                            },
                            ReadinessProbe: &amp;corev1.Probe{
                                ProbeHandler: corev1.ProbeHandler{
                                    HTTPGet: &amp;corev1.HTTPGetAction{
                                        Path: <span class="hljs-string">"/"</span>,
                                        Port: intstr.FromInt(<span class="hljs-keyword">int</span>(port)),
                                    },
                                },
                                InitialDelaySeconds: <span class="hljs-number">5</span>,
                                PeriodSeconds:       <span class="hljs-number">5</span>,
                            },
                        },
                    },
                    Volumes: []corev1.Volume{
                        {
                            Name: <span class="hljs-string">"config"</span>,
                            VolumeSource: corev1.VolumeSource{
                                ConfigMap: &amp;corev1.ConfigMapVolumeSource{
                                    LocalObjectReference: corev1.LocalObjectReference{
                                        Name: nginx.Name + <span class="hljs-string">"-config"</span>,
                                    },
                                },
                            },
                        },
                    },
                },
            },
        },
    }

    <span class="hljs-comment">// 設置 Owner Reference</span>
    <span class="hljs-keyword">if</span> err := controllerutil.SetControllerReference(nginx, deployment, r.Scheme); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-comment">// 創建或更新 Deployment</span>
    existing := &amp;appsv1.Deployment{}
    err := r.Get(ctx, types.NamespacedName{
        Name:      deployment.Name,
        Namespace: deployment.Namespace,
    }, existing)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">if</span> apierrors.IsNotFound(err) {
            logger.Info(<span class="hljs-string">"Creating Deployment"</span>, <span class="hljs-string">"name"</span>, deployment.Name)
            <span class="hljs-keyword">return</span> r.Create(ctx, deployment)
        }
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-comment">// 更新 Deployment</span>
    existing.Spec = deployment.Spec
    logger.Info(<span class="hljs-string">"Updating Deployment"</span>, <span class="hljs-string">"name"</span>, deployment.Name)
    <span class="hljs-keyword">return</span> r.Update(ctx, existing)
}

<span class="hljs-comment">// reconcileService 管理 Service</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">reconcileService</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx)</span> <span class="hljs-title">error</span></span> {
    logger := log.FromContext(ctx)

    port := <span class="hljs-keyword">int32</span>(<span class="hljs-number">80</span>)
    <span class="hljs-keyword">if</span> nginx.Spec.Port != <span class="hljs-number">0</span> {
        port = nginx.Spec.Port
    }

    serviceType := corev1.ServiceTypeClusterIP
    <span class="hljs-keyword">if</span> nginx.Spec.ServiceType != <span class="hljs-string">""</span> {
        serviceType = nginx.Spec.ServiceType
    }

    service := &amp;corev1.Service{
        ObjectMeta: metav1.ObjectMeta{
            Name:      nginx.Name,
            Namespace: nginx.Namespace,
            Labels: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
                <span class="hljs-string">"app"</span>:   <span class="hljs-string">"nginx"</span>,
                <span class="hljs-string">"nginx"</span>: nginx.Name,
            },
        },
        Spec: corev1.ServiceSpec{
            Type: serviceType,
            Selector: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
                <span class="hljs-string">"app"</span>:   <span class="hljs-string">"nginx"</span>,
                <span class="hljs-string">"nginx"</span>: nginx.Name,
            },
            Ports: []corev1.ServicePort{
                {
                    Name:       <span class="hljs-string">"http"</span>,
                    Protocol:   corev1.ProtocolTCP,
                    Port:       port,
                    TargetPort: intstr.FromInt(<span class="hljs-keyword">int</span>(port)),
                },
            },
        },
    }

    <span class="hljs-comment">// 設置 Owner Reference</span>
    <span class="hljs-keyword">if</span> err := controllerutil.SetControllerReference(nginx, service, r.Scheme); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-comment">// 創建或更新 Service</span>
    existing := &amp;corev1.Service{}
    err := r.Get(ctx, types.NamespacedName{
        Name:      service.Name,
        Namespace: service.Namespace,
    }, existing)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">if</span> apierrors.IsNotFound(err) {
            logger.Info(<span class="hljs-string">"Creating Service"</span>, <span class="hljs-string">"name"</span>, service.Name)
            <span class="hljs-keyword">return</span> r.Create(ctx, service)
        }
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-comment">// 更新 Service（保留 ClusterIP）</span>
    existing.Spec.Ports = service.Spec.Ports
    existing.Spec.Selector = service.Spec.Selector
    existing.Spec.Type = service.Spec.Type
    logger.Info(<span class="hljs-string">"Updating Service"</span>, <span class="hljs-string">"name"</span>, service.Name)
    <span class="hljs-keyword">return</span> r.Update(ctx, existing)
}

<span class="hljs-comment">// updateStatus 更新 Nginx 狀態</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">updateStatus</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx, configVersion <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-comment">// 獲取 Deployment 狀態</span>
    deployment := &amp;appsv1.Deployment{}
    err := r.Get(ctx, types.NamespacedName{
        Name:      nginx.Name,
        Namespace: nginx.Namespace,
    }, deployment)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-comment">// 更新狀態</span>
    nginx.Status.ReadyReplicas = deployment.Status.ReadyReplicas
    nginx.Status.ConfigVersion = configVersion
    now := metav1.Now()
    nginx.Status.LastUpdateTime = &amp;now

    <span class="hljs-comment">// 設置 Progressing condition</span>
    <span class="hljs-keyword">if</span> deployment.Status.UpdatedReplicas &lt; *deployment.Spec.Replicas {
        r.setCondition(nginx, TypeProgressing, metav1.ConditionTrue, <span class="hljs-string">"Updating"</span>, <span class="hljs-string">"Deployment is being updated"</span>)
    } <span class="hljs-keyword">else</span> {
        r.setCondition(nginx, TypeProgressing, metav1.ConditionFalse, <span class="hljs-string">"Updated"</span>, <span class="hljs-string">"All replicas are updated"</span>)
    }

    <span class="hljs-keyword">return</span> r.Status().Update(ctx, nginx)
}

<span class="hljs-comment">// reconcileDelete 處理刪除邏輯</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">reconcileDelete</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    logger := log.FromContext(ctx)
    logger.Info(<span class="hljs-string">"Deleting Nginx"</span>, <span class="hljs-string">"name"</span>, nginx.Name)

    <span class="hljs-keyword">if</span> controllerutil.ContainsFinalizer(nginx, nginxFinalizer) {
        <span class="hljs-comment">// 執行清理邏輯（例如清理外部資源）</span>
        logger.Info(<span class="hljs-string">"Performing cleanup for Nginx"</span>, <span class="hljs-string">"name"</span>, nginx.Name)

        <span class="hljs-comment">// 移除 finalizer</span>
        controllerutil.RemoveFinalizer(nginx, nginxFinalizer)
        <span class="hljs-keyword">if</span> err := r.Update(ctx, nginx); err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> ctrl.Result{}, err
        }
    }

    <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
}

<span class="hljs-comment">// setCondition 設置 Condition</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">setCondition</span><span class="hljs-params">(nginx *webappv1alpha1.Nginx, condType <span class="hljs-keyword">string</span>, status metav1.ConditionStatus, reason, message <span class="hljs-keyword">string</span>)</span></span> {
    condition := metav1.Condition{
        Type:               condType,
        Status:             status,
        Reason:             reason,
        Message:            message,
        LastTransitionTime: metav1.Now(),
        ObservedGeneration: nginx.Generation,
    }
    meta.SetStatusCondition(&amp;nginx.Status.Conditions, condition)
}

<span class="hljs-comment">// SetupWithManager sets up the controller with the Manager.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">SetupWithManager</span><span class="hljs-params">(mgr ctrl.Manager)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> ctrl.NewControllerManagedBy(mgr).
        For(&amp;webappv1alpha1.Nginx{}).
        Owns(&amp;appsv1.Deployment{}).
        Owns(&amp;corev1.Service{}).
        Owns(&amp;corev1.ConfigMap{}).
        Complete(r)
}
</code></pre>
<h3 id="heading-922">9.2.2 核心概念詳解</h3>
<p><strong>1. ConfigMap 版本管理</strong></p>
<pre><code class="lang-go"><span class="hljs-comment">// 計算配置的 SHA256 hash 作為版本號</span>
hash := sha256.Sum256([]<span class="hljs-keyword">byte</span>(config))
configVersion := fmt.Sprintf(<span class="hljs-string">"%x"</span>, hash[:<span class="hljs-number">8</span>])

<span class="hljs-comment">// 將版本號添加到 Pod labels</span>
Labels: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
    <span class="hljs-string">"config-version"</span>: configVersion,  <span class="hljs-comment">// 版本變更時自動觸發 rolling update</span>
}
</code></pre>
<p><strong>2. Owner Reference 自動清理</strong></p>
<pre><code class="lang-go"><span class="hljs-comment">// 設置 Owner Reference 後，刪除 Nginx 時會自動刪除子資源</span>
<span class="hljs-keyword">if</span> err := controllerutil.SetControllerReference(nginx, deployment, r.Scheme); err != <span class="hljs-literal">nil</span> {
    <span class="hljs-keyword">return</span> err
}
</code></pre>
<p><strong>3. Finalizer 清理邏輯</strong></p>
<pre><code class="lang-go"><span class="hljs-comment">// 添加 finalizer 防止資源被立即刪除</span>
<span class="hljs-keyword">if</span> !controllerutil.ContainsFinalizer(nginx, nginxFinalizer) {
    controllerutil.AddFinalizer(nginx, nginxFinalizer)
    <span class="hljs-keyword">if</span> err := r.Update(ctx, nginx); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }
}

<span class="hljs-comment">// 刪除時執行清理</span>
<span class="hljs-keyword">if</span> !nginx.ObjectMeta.DeletionTimestamp.IsZero() {
    <span class="hljs-comment">// 執行清理邏輯</span>
    <span class="hljs-comment">// ...</span>

    <span class="hljs-comment">// 移除 finalizer 允許刪除</span>
    controllerutil.RemoveFinalizer(nginx, nginxFinalizer)
    <span class="hljs-keyword">if</span> err := r.Update(ctx, nginx); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }
}
</code></pre>
<h2 id="heading-93">9.3 使用範例</h2>
<h3 id="heading-931-operator">9.3.1 部署 Operator</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 生成 CRD 和 RBAC manifests</span>
make manifests

<span class="hljs-comment"># 2. 安裝 CRD</span>
make install

<span class="hljs-comment"># 3. 運行 operator（開發模式）</span>
make run

<span class="hljs-comment"># 或者部署到集群</span>
make docker-build docker-push IMG=your-registry/nginx-operator:v1.0.0
make deploy IMG=your-registry/nginx-operator:v1.0.0
</code></pre>
<h3 id="heading-932-nginx">9.3.2 創建 Nginx 實例</h3>
<p><strong>基本範例</strong> (<code>config/samples/nginx_basic.yaml</code>)：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">webapp.example.com/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Nginx</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-sample</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">nginx:1.25</span>
  <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
  <span class="hljs-attr">serviceType:</span> <span class="hljs-string">ClusterIP</span>
</code></pre>
<p><strong>自定義配置範例</strong> (<code>config/samples/nginx_custom.yaml</code>)：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">webapp.example.com/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Nginx</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-custom</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">nginx:1.25</span>
  <span class="hljs-attr">port:</span> <span class="hljs-number">8080</span>
  <span class="hljs-attr">serviceType:</span> <span class="hljs-string">LoadBalancer</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">requests:</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
      <span class="hljs-attr">cpu:</span> <span class="hljs-string">"100m"</span>
    <span class="hljs-attr">limits:</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">"256Mi"</span>
      <span class="hljs-attr">cpu:</span> <span class="hljs-string">"200m"</span>
  <span class="hljs-attr">config:</span> <span class="hljs-string">|
    events {
        worker_connections 2048;
    }
</span>
    <span class="hljs-string">http</span> {
        <span class="hljs-string">log_format</span> <span class="hljs-string">main</span> <span class="hljs-string">'$remote_addr - $remote_user [$time_local] "$request" '</span>
                        <span class="hljs-string">'$status $body_bytes_sent "$http_referer" '</span>
                        <span class="hljs-string">'"$http_user_agent" "$http_x_forwarded_for"'</span><span class="hljs-string">;</span>

        <span class="hljs-string">access_log</span> <span class="hljs-string">/var/log/nginx/access.log</span> <span class="hljs-string">main;</span>

        <span class="hljs-string">upstream</span> <span class="hljs-string">backend</span> {
            <span class="hljs-string">server</span> <span class="hljs-string">backend-1.example.com;</span>
            <span class="hljs-string">server</span> <span class="hljs-string">backend-2.example.com;</span>
        }

        <span class="hljs-string">server</span> {
            <span class="hljs-string">listen</span> <span class="hljs-number">8080</span><span class="hljs-string">;</span>

            <span class="hljs-string">location</span> <span class="hljs-string">/</span> {
                <span class="hljs-string">proxy_pass</span> <span class="hljs-string">http://backend;</span>
                <span class="hljs-string">proxy_set_header</span> <span class="hljs-string">Host</span> <span class="hljs-string">$host;</span>
                <span class="hljs-string">proxy_set_header</span> <span class="hljs-string">X-Real-IP</span> <span class="hljs-string">$remote_addr;</span>
            }

            <span class="hljs-string">location</span> <span class="hljs-string">/health</span> {
                <span class="hljs-string">access_log</span> <span class="hljs-string">off;</span>
                <span class="hljs-string">return</span> <span class="hljs-number">200</span> <span class="hljs-string">"healthy\n"</span><span class="hljs-string">;</span>
            }
        }
    }
</code></pre>
<h3 id="heading-933">9.3.3 測試和驗證</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 創建 Nginx 實例</span>
kubectl apply -f config/samples/nginx_basic.yaml

<span class="hljs-comment"># 2. 查看 Nginx 狀態</span>
kubectl get nginx nginx-sample

<span class="hljs-comment"># 輸出：</span>
<span class="hljs-comment"># NAME           REPLICAS   READY   IMAGE         AGE</span>
<span class="hljs-comment"># nginx-sample   3          3       nginx:1.25    2m</span>

<span class="hljs-comment"># 3. 查看詳細狀態</span>
kubectl describe nginx nginx-sample

<span class="hljs-comment"># 4. 查看生成的資源</span>
kubectl get deployment,svc,cm -l nginx=nginx-sample

<span class="hljs-comment"># 5. 測試服務</span>
kubectl port-forward svc/nginx-sample 8080:80
curl http://localhost:8080

<span class="hljs-comment"># 6. 更新配置（觸發滾動更新）</span>
kubectl edit nginx nginx-sample
<span class="hljs-comment"># 修改 spec.config 或 spec.replicas</span>

<span class="hljs-comment"># 7. 觀察滾動更新</span>
kubectl rollout status deployment/nginx-sample

<span class="hljs-comment"># 8. 查看 Conditions</span>
kubectl get nginx nginx-sample -o jsonpath=<span class="hljs-string">'{.status.conditions}'</span> | jq

<span class="hljs-comment"># 9. Scale 測試</span>
kubectl scale nginx nginx-sample --replicas=5
kubectl get nginx nginx-sample

<span class="hljs-comment"># 10. 刪除測試</span>
kubectl delete nginx nginx-sample
<span class="hljs-comment"># 驗證子資源被自動清理</span>
kubectl get deployment,svc,cm -l nginx=nginx-sample
</code></pre>
<h2 id="heading-94">9.4 進階功能實作</h2>
<h3 id="heading-941-ingress">9.4.1 支援 Ingress 自動創建</h3>
<p><strong>擴展 CRD</strong>：</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> NginxSpec <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// ... 現有字段</span>

    <span class="hljs-comment">// +optional</span>
    Ingress *IngressSpec <span class="hljs-string">`json:"ingress,omitempty"`</span>
}

<span class="hljs-keyword">type</span> IngressSpec <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// +kubebuilder:validation:Required</span>
    Host <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"host"`</span>

    <span class="hljs-comment">// +optional</span>
    TLS <span class="hljs-keyword">bool</span> <span class="hljs-string">`json:"tls,omitempty"`</span>

    <span class="hljs-comment">// +optional</span>
    Annotations <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span> <span class="hljs-string">`json:"annotations,omitempty"`</span>
}
</code></pre>
<p><strong>Controller 添加 Ingress reconcile</strong>：</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">reconcileIngress</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">if</span> nginx.Spec.Ingress == <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    }

    pathType := networkingv1.PathTypePrefix
    ingress := &amp;networkingv1.Ingress{
        ObjectMeta: metav1.ObjectMeta{
            Name:        nginx.Name,
            Namespace:   nginx.Namespace,
            Annotations: nginx.Spec.Ingress.Annotations,
        },
        Spec: networkingv1.IngressSpec{
            Rules: []networkingv1.IngressRule{
                {
                    Host: nginx.Spec.Ingress.Host,
                    IngressRuleValue: networkingv1.IngressRuleValue{
                        HTTP: &amp;networkingv1.HTTPIngressRuleValue{
                            Paths: []networkingv1.HTTPIngressPath{
                                {
                                    Path:     <span class="hljs-string">"/"</span>,
                                    PathType: &amp;pathType,
                                    Backend: networkingv1.IngressBackend{
                                        Service: &amp;networkingv1.IngressServiceBackend{
                                            Name: nginx.Name,
                                            Port: networkingv1.ServiceBackendPort{
                                                Number: nginx.Spec.Port,
                                            },
                                        },
                                    },
                                },
                            },
                        },
                    },
                },
            },
        },
    }

    <span class="hljs-keyword">if</span> nginx.Spec.Ingress.TLS {
        ingress.Spec.TLS = []networkingv1.IngressTLS{
            {
                Hosts:      []<span class="hljs-keyword">string</span>{nginx.Spec.Ingress.Host},
                SecretName: nginx.Name + <span class="hljs-string">"-tls"</span>,
            },
        }
    }

    <span class="hljs-keyword">if</span> err := controllerutil.SetControllerReference(nginx, ingress, r.Scheme); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-comment">// 創建或更新邏輯...</span>
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<h3 id="heading-942">9.4.2 支援多端口暴露</h3>
<p><strong>擴展 CRD</strong>：</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> PortSpec <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// +kubebuilder:validation:Required</span>
    Name <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"name"`</span>

    <span class="hljs-comment">// +kubebuilder:validation:Minimum=1</span>
    <span class="hljs-comment">// +kubebuilder:validation:Maximum=65535</span>
    Port <span class="hljs-keyword">int32</span> <span class="hljs-string">`json:"port"`</span>

    <span class="hljs-comment">// +optional</span>
    <span class="hljs-comment">// +kubebuilder:default="TCP"</span>
    Protocol corev1.Protocol <span class="hljs-string">`json:"protocol,omitempty"`</span>
}

<span class="hljs-keyword">type</span> NginxSpec <span class="hljs-keyword">struct</span> {
    <span class="hljs-comment">// ... 現有字段</span>

    <span class="hljs-comment">// +optional</span>
    AdditionalPorts []PortSpec <span class="hljs-string">`json:"additionalPorts,omitempty"`</span>
}
</code></pre>
<p><strong>Controller 處理多端口</strong>：</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">buildServicePorts</span><span class="hljs-params">(nginx *webappv1alpha1.Nginx)</span> []<span class="hljs-title">corev1</span>.<span class="hljs-title">ServicePort</span></span> {
    ports := []corev1.ServicePort{
        {
            Name:       <span class="hljs-string">"http"</span>,
            Port:       nginx.Spec.Port,
            TargetPort: intstr.FromInt(<span class="hljs-keyword">int</span>(nginx.Spec.Port)),
            Protocol:   corev1.ProtocolTCP,
        },
    }

    <span class="hljs-keyword">for</span> _, p := <span class="hljs-keyword">range</span> nginx.Spec.AdditionalPorts {
        ports = <span class="hljs-built_in">append</span>(ports, corev1.ServicePort{
            Name:       p.Name,
            Port:       p.Port,
            TargetPort: intstr.FromInt(<span class="hljs-keyword">int</span>(p.Port)),
            Protocol:   p.Protocol,
        })
    }

    <span class="hljs-keyword">return</span> ports
}
</code></pre>
<h2 id="heading-95">9.5 完整測試範例</h2>
<h3 id="heading-951">9.5.1 單元測試</h3>
<p><strong>Controller 測試</strong> (<code>internal/controller/nginx_controller_test.go</code>)：</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> controller

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"time"</span>

    . <span class="hljs-string">"github.com/onsi/ginkgo/v2"</span>
    . <span class="hljs-string">"github.com/onsi/gomega"</span>
    appsv1 <span class="hljs-string">"k8s.io/api/apps/v1"</span>
    corev1 <span class="hljs-string">"k8s.io/api/core/v1"</span>
    metav1 <span class="hljs-string">"k8s.io/apimachinery/pkg/apis/meta/v1"</span>
    <span class="hljs-string">"k8s.io/apimachinery/pkg/types"</span>

    webappv1alpha1 <span class="hljs-string">"example.com/nginx-operator/apis/v1alpha1"</span>
)

<span class="hljs-keyword">var</span> _ = Describe(<span class="hljs-string">"Nginx Controller"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">const</span> (
        NginxName      = <span class="hljs-string">"test-nginx"</span>
        NginxNamespace = <span class="hljs-string">"default"</span>
        timeout        = time.Second * <span class="hljs-number">10</span>
        interval       = time.Millisecond * <span class="hljs-number">250</span>
    )

    Context(<span class="hljs-string">"When creating a Nginx resource"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        It(<span class="hljs-string">"Should create Deployment, Service, and ConfigMap"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
            ctx := context.Background()

            nginx := &amp;webappv1alpha1.Nginx{
                ObjectMeta: metav1.ObjectMeta{
                    Name:      NginxName,
                    Namespace: NginxNamespace,
                },
                Spec: webappv1alpha1.NginxSpec{
                    Replicas: pointer.Int32(<span class="hljs-number">2</span>),
                    Image:    <span class="hljs-string">"nginx:1.25"</span>,
                    Port:     <span class="hljs-number">80</span>,
                },
            }

            Expect(k8sClient.Create(ctx, nginx)).Should(Succeed())

            <span class="hljs-comment">// 驗證 Deployment 被創建</span>
            deployment := &amp;appsv1.Deployment{}
            Eventually(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">bool</span></span> {
                err := k8sClient.Get(ctx, types.NamespacedName{
                    Name:      NginxName,
                    Namespace: NginxNamespace,
                }, deployment)
                <span class="hljs-keyword">return</span> err == <span class="hljs-literal">nil</span>
            }, timeout, interval).Should(BeTrue())

            Expect(*deployment.Spec.Replicas).Should(Equal(<span class="hljs-keyword">int32</span>(<span class="hljs-number">2</span>)))
            Expect(deployment.Spec.Template.Spec.Containers[<span class="hljs-number">0</span>].Image).Should(Equal(<span class="hljs-string">"nginx:1.25"</span>))

            <span class="hljs-comment">// 驗證 Service 被創建</span>
            service := &amp;corev1.Service{}
            Eventually(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">bool</span></span> {
                err := k8sClient.Get(ctx, types.NamespacedName{
                    Name:      NginxName,
                    Namespace: NginxNamespace,
                }, service)
                <span class="hljs-keyword">return</span> err == <span class="hljs-literal">nil</span>
            }, timeout, interval).Should(BeTrue())

            Expect(service.Spec.Ports[<span class="hljs-number">0</span>].Port).Should(Equal(<span class="hljs-keyword">int32</span>(<span class="hljs-number">80</span>)))

            <span class="hljs-comment">// 驗證 ConfigMap 被創建</span>
            configMap := &amp;corev1.ConfigMap{}
            Eventually(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">bool</span></span> {
                err := k8sClient.Get(ctx, types.NamespacedName{
                    Name:      NginxName + <span class="hljs-string">"-config"</span>,
                    Namespace: NginxNamespace,
                }, configMap)
                <span class="hljs-keyword">return</span> err == <span class="hljs-literal">nil</span>
            }, timeout, interval).Should(BeTrue())

            Expect(configMap.Data).Should(HaveKey(<span class="hljs-string">"nginx.conf"</span>))
        })
    })

    Context(<span class="hljs-string">"When updating Nginx config"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        It(<span class="hljs-string">"Should trigger rolling update"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
            ctx := context.Background()

            <span class="hljs-comment">// 獲取當前 Nginx</span>
            nginx := &amp;webappv1alpha1.Nginx{}
            Expect(k8sClient.Get(ctx, types.NamespacedName{
                Name:      NginxName,
                Namespace: NginxNamespace,
            }, nginx)).Should(Succeed())

            <span class="hljs-comment">// 更新配置</span>
            nginx.Spec.Config = <span class="hljs-string">"# updated config"</span>
            Expect(k8sClient.Update(ctx, nginx)).Should(Succeed())

            <span class="hljs-comment">// 驗證 ConfigMap 被更新</span>
            configMap := &amp;corev1.ConfigMap{}
            Eventually(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
                k8sClient.Get(ctx, types.NamespacedName{
                    Name:      NginxName + <span class="hljs-string">"-config"</span>,
                    Namespace: NginxNamespace,
                }, configMap)
                <span class="hljs-keyword">return</span> configMap.Data[<span class="hljs-string">"nginx.conf"</span>]
            }, timeout, interval).Should(ContainSubstring(<span class="hljs-string">"updated config"</span>))

            <span class="hljs-comment">// 驗證 Deployment Pod template 的 config-version label 改變</span>
            deployment := &amp;appsv1.Deployment{}
            oldVersion := <span class="hljs-string">""</span>
            Eventually(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">bool</span></span> {
                k8sClient.Get(ctx, types.NamespacedName{
                    Name:      NginxName,
                    Namespace: NginxNamespace,
                }, deployment)
                newVersion := deployment.Spec.Template.Labels[<span class="hljs-string">"config-version"</span>]
                <span class="hljs-keyword">if</span> oldVersion == <span class="hljs-string">""</span> {
                    oldVersion = newVersion
                    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
                }
                <span class="hljs-keyword">return</span> newVersion != oldVersion
            }, timeout, interval).Should(BeTrue())
        })
    })

    Context(<span class="hljs-string">"When deleting Nginx"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        It(<span class="hljs-string">"Should cleanup all resources"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
            ctx := context.Background()

            nginx := &amp;webappv1alpha1.Nginx{}
            Expect(k8sClient.Get(ctx, types.NamespacedName{
                Name:      NginxName,
                Namespace: NginxNamespace,
            }, nginx)).Should(Succeed())

            Expect(k8sClient.Delete(ctx, nginx)).Should(Succeed())

            <span class="hljs-comment">// 驗證所有子資源被刪除</span>
            Eventually(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-title">bool</span></span> {
                deployment := &amp;appsv1.Deployment{}
                err := k8sClient.Get(ctx, types.NamespacedName{
                    Name:      NginxName,
                    Namespace: NginxNamespace,
                }, deployment)
                <span class="hljs-keyword">return</span> errors.IsNotFound(err)
            }, timeout, interval).Should(BeTrue())
        })
    })
})
</code></pre>
<h3 id="heading-952-e2e-chainsaw">9.5.2 E2E 測試（Chainsaw）</h3>
<p><strong>測試套件</strong> (<code>tests/e2e/nginx/chainsaw-test.yaml</code>)：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">chainsaw.kyverno.io/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Test</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-e2e</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">timeouts:</span>
    <span class="hljs-attr">apply:</span> <span class="hljs-string">30s</span>
    <span class="hljs-attr">assert:</span> <span class="hljs-string">1m</span>
    <span class="hljs-attr">cleanup:</span> <span class="hljs-string">30s</span>
  <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Create</span> <span class="hljs-string">Nginx</span> <span class="hljs-string">instance</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">apply:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">00</span><span class="hljs-string">-nginx-sample.yaml</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">assert:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">01</span><span class="hljs-string">-assert-nginx-ready.yaml</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Test</span> <span class="hljs-string">service</span> <span class="hljs-string">connectivity</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span>
            <span class="hljs-attr">content:</span> <span class="hljs-string">|
              kubectl run curl-test --image=curlimages/curl:latest --rm -i --restart=Never -- \
                curl -s http://nginx-sample.default.svc.cluster.local
</span>            <span class="hljs-attr">check:</span>
              <span class="hljs-string">($error</span> <span class="hljs-string">==</span> <span class="hljs-literal">null</span><span class="hljs-string">):</span> <span class="hljs-literal">true</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Update</span> <span class="hljs-string">configuration</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">apply:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">02</span><span class="hljs-string">-update-config.yaml</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">assert:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">03</span><span class="hljs-string">-assert-rolling-update.yaml</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Scale</span> <span class="hljs-string">up</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span>
            <span class="hljs-attr">content:</span> <span class="hljs-string">|
              kubectl scale nginx nginx-sample --replicas=5
</span>        <span class="hljs-bullet">-</span> <span class="hljs-attr">assert:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">04</span><span class="hljs-string">-assert-scaled.yaml</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Cleanup</span>
      <span class="hljs-attr">try:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">delete:</span>
            <span class="hljs-attr">ref:</span>
              <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">webapp.example.com/v1alpha1</span>
              <span class="hljs-attr">kind:</span> <span class="hljs-string">Nginx</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-sample</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">assert:</span>
            <span class="hljs-attr">file:</span> <span class="hljs-number">05</span><span class="hljs-string">-assert-deleted.yaml</span>
</code></pre>
<p><strong>00-nginx-sample.yaml</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">webapp.example.com/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Nginx</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-sample</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">nginx:1.25</span>
  <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
  <span class="hljs-attr">serviceType:</span> <span class="hljs-string">ClusterIP</span>
</code></pre>
<p><strong>01-assert-nginx-ready.yaml</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">webapp.example.com/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Nginx</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-sample</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">status:</span>
  <span class="hljs-attr">readyReplicas:</span> <span class="hljs-number">3</span>
  <span class="hljs-attr">conditions:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">Available</span>
      <span class="hljs-attr">status:</span> <span class="hljs-string">"True"</span>
      <span class="hljs-attr">reason:</span> <span class="hljs-string">ReconcileSuccess</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-sample</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
<span class="hljs-attr">status:</span>
  <span class="hljs-attr">readyReplicas:</span> <span class="hljs-number">3</span>
  <span class="hljs-attr">updatedReplicas:</span> <span class="hljs-number">3</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-sample</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">ClusterIP</span>
  <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
</code></pre>
<h2 id="heading-96">9.6 部署到生產環境</h2>
<h3 id="heading-961-kustomize">9.6.1 Kustomize 配置</h3>
<p><strong>Base</strong> (<code>config/default/kustomization.yaml</code>)：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">namePrefix:</span> <span class="hljs-string">nginx-operator-</span>
<span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>

<span class="hljs-attr">resources:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">../crd</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">../rbac</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">../manager</span>

<span class="hljs-attr">images:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">controller</span>
    <span class="hljs-attr">newName:</span> <span class="hljs-string">your-registry/nginx-operator</span>
    <span class="hljs-attr">newTag:</span> <span class="hljs-string">v1.0.0</span>
</code></pre>
<p><strong>Overlay for Production</strong> (<code>config/overlays/production/kustomization.yaml</code>)：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">bases:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">../../default</span>

<span class="hljs-attr">patchesStrategicMerge:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">manager_resources.yaml</span>

<span class="hljs-attr">configMapGenerator:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">manager-config</span>
    <span class="hljs-attr">literals:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">LOG_LEVEL=info</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ENABLE_WEBHOOKS=true</span>
</code></pre>
<p><strong>manager_resources.yaml</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">controller-manager</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">manager</span>
          <span class="hljs-attr">resources:</span>
            <span class="hljs-attr">requests:</span>
              <span class="hljs-attr">cpu:</span> <span class="hljs-string">100m</span>
              <span class="hljs-attr">memory:</span> <span class="hljs-string">128Mi</span>
            <span class="hljs-attr">limits:</span>
              <span class="hljs-attr">cpu:</span> <span class="hljs-string">500m</span>
              <span class="hljs-attr">memory:</span> <span class="hljs-string">512Mi</span>
</code></pre>
<h3 id="heading-962">9.6.2 部署命令</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># 構建鏡像</span>
make docker-build IMG=your-registry/nginx-operator:v1.0.0

<span class="hljs-comment"># 推送鏡像</span>
make docker-push IMG=your-registry/nginx-operator:v1.0.0

<span class="hljs-comment"># 部署到生產環境</span>
kubectl apply -k config/overlays/production

<span class="hljs-comment"># 驗證部署</span>
kubectl get deployment -n nginx-operator-system
kubectl get pods -n nginx-operator-system

<span class="hljs-comment"># 查看日誌</span>
kubectl logs -n nginx-operator-system deployment/nginx-operator-controller-manager -f
</code></pre>
<hr />
<h1 id="heading-5y2b44cb6ycy6zqo5li76agm6iih5pya5l2z5am6liq">十、進階主題與最佳實踐</h1>
<h2 id="heading-101">10.1 性能優化</h2>
<h3 id="heading-1011-field-indexer">10.1.1 使用 Field Indexer 加速查詢</h3>
<p>當需要頻繁根據特定字段查詢資源時，Field Indexer 可以大幅提升性能。</p>
<p><strong>範例：根據 Owner 快速查詢 Pod</strong></p>
<p>參考 OpenTelemetry Operator 的實作 (<code>main.go:126-139</code>)：</p>
<pre><code class="lang-go"><span class="hljs-comment">// 為 Pod 添加 owner.name 索引</span>
<span class="hljs-keyword">if</span> err := mgr.GetFieldIndexer().IndexField(
    context.Background(),
    &amp;corev1.Pod{},
    <span class="hljs-string">"metadata.ownerReferences.name"</span>,
    <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(rawObj client.Object)</span> []<span class="hljs-title">string</span></span> {
        pod := rawObj.(*corev1.Pod)
        <span class="hljs-keyword">var</span> owners []<span class="hljs-keyword">string</span>
        <span class="hljs-keyword">for</span> _, ref := <span class="hljs-keyword">range</span> pod.GetOwnerReferences() {
            owners = <span class="hljs-built_in">append</span>(owners, ref.Name)
        }
        <span class="hljs-keyword">return</span> owners
    },
); err != <span class="hljs-literal">nil</span> {
    setupLog.Error(err, <span class="hljs-string">"failed to create pod index"</span>)
    os.Exit(<span class="hljs-number">1</span>)
}

<span class="hljs-comment">// 使用索引快速查詢</span>
pods := &amp;corev1.PodList{}
err := r.List(ctx, pods, client.MatchingFields{
    <span class="hljs-string">"metadata.ownerReferences.name"</span>: collectorName,
})
</code></pre>
<p><strong>更多索引範例</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// 1. 索引 ConfigMap 的標籤</span>
mgr.GetFieldIndexer().IndexField(
    context.Background(),
    &amp;corev1.ConfigMap{},
    <span class="hljs-string">"metadata.labels.app"</span>,
    <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(obj client.Object)</span> []<span class="hljs-title">string</span></span> {
        cm := obj.(*corev1.ConfigMap)
        <span class="hljs-keyword">if</span> app, ok := cm.Labels[<span class="hljs-string">"app"</span>]; ok {
            <span class="hljs-keyword">return</span> []<span class="hljs-keyword">string</span>{app}
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    },
)

<span class="hljs-comment">// 查詢</span>
configMaps := &amp;corev1.ConfigMapList{}
r.List(ctx, configMaps, client.MatchingFields{
    <span class="hljs-string">"metadata.labels.app"</span>: <span class="hljs-string">"nginx"</span>,
})

<span class="hljs-comment">// 2. 索引 Pod 的 Node</span>
mgr.GetFieldIndexer().IndexField(
    context.Background(),
    &amp;corev1.Pod{},
    <span class="hljs-string">"spec.nodeName"</span>,
    <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(obj client.Object)</span> []<span class="hljs-title">string</span></span> {
        pod := obj.(*corev1.Pod)
        <span class="hljs-keyword">if</span> pod.Spec.NodeName != <span class="hljs-string">""</span> {
            <span class="hljs-keyword">return</span> []<span class="hljs-keyword">string</span>{pod.Spec.NodeName}
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    },
)

<span class="hljs-comment">// 查詢特定 Node 上的 Pod</span>
pods := &amp;corev1.PodList{}
r.List(ctx, pods, client.MatchingFields{
    <span class="hljs-string">"spec.nodeName"</span>: <span class="hljs-string">"node-1"</span>,
})
</code></pre>
<h3 id="heading-1012-cache-api">10.1.2 使用 Cache 減少 API 調用</h3>
<p>Controller Runtime 自動提供 cache，但需要正確配置：</p>
<pre><code class="lang-go"><span class="hljs-comment">// main.go - 配置 Cache</span>
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme: scheme,
    Cache: cache.Options{
        <span class="hljs-comment">// 限制 cache 的 namespace（多租戶場景）</span>
        Namespaces: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"namespace1"</span>, <span class="hljs-string">"namespace2"</span>},

        <span class="hljs-comment">// 配置同步週期</span>
        SyncPeriod: pointer.Duration(<span class="hljs-number">10</span> * time.Minute),
    },
    <span class="hljs-comment">// 配置 metrics 和 health 端點</span>
    Metrics: server.Options{
        BindAddress: <span class="hljs-string">":8080"</span>,
    },
    HealthProbeBindAddress: <span class="hljs-string">":8081"</span>,
})

<span class="hljs-comment">// Controller 中使用 cache（自動）</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">Reconcile</span><span class="hljs-params">(ctx context.Context, req ctrl.Request)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    <span class="hljs-comment">// r.Get 自動從 cache 讀取</span>
    nginx := &amp;v1alpha1.Nginx{}
    <span class="hljs-keyword">if</span> err := r.Get(ctx, req.NamespacedName, nginx); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-comment">// r.List 也從 cache 讀取</span>
    deployments := &amp;appsv1.DeploymentList{}
    <span class="hljs-keyword">if</span> err := r.List(ctx, deployments, client.InNamespace(req.Namespace)); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
}
</code></pre>
<h3 id="heading-1013-reconcile">10.1.3 優化 Reconcile 邏輯</h3>
<p><strong>1. 使用 Predicate 過濾不必要的事件</strong></p>
<pre><code class="lang-go"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/predicate"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">SetupWithManager</span><span class="hljs-params">(mgr ctrl.Manager)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> ctrl.NewControllerManagedBy(mgr).
        For(&amp;webappv1alpha1.Nginx{}).
        Owns(&amp;appsv1.Deployment{}).
        Owns(&amp;corev1.Service{}).
        WithEventFilter(predicate.Funcs{
            <span class="hljs-comment">// 忽略 status-only 更新</span>
            UpdateFunc: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(e event.UpdateEvent)</span> <span class="hljs-title">bool</span></span> {
                oldObj := e.ObjectOld.(*webappv1alpha1.Nginx)
                newObj := e.ObjectNew.(*webappv1alpha1.Nginx)

                <span class="hljs-comment">// 只在 spec 或 metadata 改變時 reconcile</span>
                <span class="hljs-keyword">return</span> oldObj.Generation != newObj.Generation ||
                       !reflect.DeepEqual(oldObj.Labels, newObj.Labels) ||
                       !reflect.DeepEqual(oldObj.Annotations, newObj.Annotations)
            },
            <span class="hljs-comment">// 忽略刪除事件（由 finalizer 處理）</span>
            DeleteFunc: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(e event.DeleteEvent)</span> <span class="hljs-title">bool</span></span> {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
            },
        }).
        Complete(r)
}
</code></pre>
<p><strong>2. 批量處理更新</strong></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">reconcileMultipleResources</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-comment">// 並行處理多個資源</span>
    <span class="hljs-keyword">var</span> wg sync.WaitGroup
    errCh := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> error, <span class="hljs-number">3</span>)

    wg.Add(<span class="hljs-number">3</span>)

    <span class="hljs-comment">// ConfigMap</span>
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">defer</span> wg.Done()
        <span class="hljs-keyword">if</span> _, _, err := r.reconcileConfigMap(ctx, nginx); err != <span class="hljs-literal">nil</span> {
            errCh &lt;- fmt.Errorf(<span class="hljs-string">"configmap: %w"</span>, err)
        }
    }()

    <span class="hljs-comment">// Deployment</span>
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">defer</span> wg.Done()
        <span class="hljs-keyword">if</span> err := r.reconcileDeployment(ctx, nginx, <span class="hljs-string">""</span>); err != <span class="hljs-literal">nil</span> {
            errCh &lt;- fmt.Errorf(<span class="hljs-string">"deployment: %w"</span>, err)
        }
    }()

    <span class="hljs-comment">// Service</span>
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">defer</span> wg.Done()
        <span class="hljs-keyword">if</span> err := r.reconcileService(ctx, nginx); err != <span class="hljs-literal">nil</span> {
            errCh &lt;- fmt.Errorf(<span class="hljs-string">"service: %w"</span>, err)
        }
    }()

    wg.Wait()
    <span class="hljs-built_in">close</span>(errCh)

    <span class="hljs-comment">// 收集錯誤</span>
    <span class="hljs-keyword">for</span> err := <span class="hljs-keyword">range</span> errCh {
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<p><strong>3. 使用 Generation 判斷資源變更</strong></p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">Reconcile</span><span class="hljs-params">(ctx context.Context, req ctrl.Request)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    nginx := &amp;webappv1alpha1.Nginx{}
    <span class="hljs-keyword">if</span> err := r.Get(ctx, req.NamespacedName, nginx); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> ctrl.Result{}, client.IgnoreNotFound(err)
    }

    <span class="hljs-comment">// 如果 ObservedGeneration 和 Generation 相同，說明 spec 沒變</span>
    <span class="hljs-keyword">if</span> nginx.Status.ObservedGeneration == nginx.Generation {
        <span class="hljs-comment">// 只檢查子資源狀態，不重新創建</span>
        <span class="hljs-keyword">return</span> r.reconcileStatus(ctx, nginx)
    }

    <span class="hljs-comment">// spec 改變了，執行完整 reconcile</span>
    <span class="hljs-keyword">return</span> r.fullReconcile(ctx, nginx)
}
</code></pre>
<h3 id="heading-1014-rate-limiting">10.1.4 限制並發和 Rate Limiting</h3>
<pre><code class="lang-go"><span class="hljs-comment">// main.go - 配置 Controller 選項</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
        <span class="hljs-comment">// ...</span>
    })

    <span class="hljs-keyword">if</span> err = (&amp;controller.NginxReconciler{
        Client: mgr.GetClient(),
        Scheme: mgr.GetScheme(),
    }).SetupWithManager(mgr); err != <span class="hljs-literal">nil</span> {
        setupLog.Error(err, <span class="hljs-string">"unable to create controller"</span>)
        os.Exit(<span class="hljs-number">1</span>)
    }
}

<span class="hljs-comment">// SetupWithManager - 配置並發和限流</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">SetupWithManager</span><span class="hljs-params">(mgr ctrl.Manager)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> ctrl.NewControllerManagedBy(mgr).
        For(&amp;webappv1alpha1.Nginx{}).
        WithOptions(controller.Options{
            <span class="hljs-comment">// 最大並發 reconcile 數</span>
            MaxConcurrentReconciles: <span class="hljs-number">3</span>,

            <span class="hljs-comment">// Rate Limiter</span>
            RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(
                <span class="hljs-number">5</span>*time.Millisecond,  <span class="hljs-comment">// base delay</span>
                <span class="hljs-number">1000</span>*time.Second,    <span class="hljs-comment">// max delay</span>
            ),
        }).
        Complete(r)
}
</code></pre>
<h2 id="heading-102">10.2 安全最佳實踐</h2>
<h3 id="heading-1021-rbac">10.2.1 RBAC 最小權限原則</h3>
<p><strong>僅授予必要權限</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// +kubebuilder:rbac:groups=webapp.example.com,resources=nginxes,verbs=get;list;watch;create;update;patch;delete</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=webapp.example.com,resources=nginxes/status,verbs=get;update;patch</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=core,resources=services;configmaps,verbs=get;list;watch;create;update;patch;delete</span>

<span class="hljs-comment">// 不要使用通配符</span>
<span class="hljs-comment">// 錯誤示範：</span>
<span class="hljs-comment">// +kubebuilder:rbac:groups=*,resources=*,verbs=*</span>
</code></pre>
<p><strong>使用 ServiceAccount 隔離</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># config/rbac/service_account.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ServiceAccount</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>
<span class="hljs-meta">---</span>
<span class="hljs-comment"># config/rbac/role_binding.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">rbac.authorization.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ClusterRoleBinding</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-manager-rolebinding</span>
<span class="hljs-attr">roleRef:</span>
  <span class="hljs-attr">apiGroup:</span> <span class="hljs-string">rbac.authorization.k8s.io</span>
  <span class="hljs-attr">kind:</span> <span class="hljs-string">ClusterRole</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-manager-role</span>
<span class="hljs-attr">subjects:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">kind:</span> <span class="hljs-string">ServiceAccount</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator</span>
    <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>
</code></pre>
<h3 id="heading-1022-webhook">10.2.2 Webhook 安全</h3>
<p><strong>配置 TLS 和證書管理</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># config/certmanager/certificate.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">cert-manager.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Certificate</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-serving-cert</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">dnsNames:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">nginx-operator-webhook-service.nginx-operator-system.svc</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">nginx-operator-webhook-service.nginx-operator-system.svc.cluster.local</span>
  <span class="hljs-attr">issuerRef:</span>
    <span class="hljs-attr">kind:</span> <span class="hljs-string">Issuer</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-selfsigned-issuer</span>
  <span class="hljs-attr">secretName:</span> <span class="hljs-string">webhook-server-cert</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">cert-manager.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Issuer</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-selfsigned-issuer</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selfSigned:</span> {}
</code></pre>
<p><strong>Validating Webhook 範例</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// +kubebuilder:webhook:path=/validate-webapp-example-com-v1alpha1-nginx,mutating=false,failurePolicy=fail,groups=webapp.example.com,resources=nginxes,verbs=create;update,versions=v1alpha1,name=vnginx.kb.io,sideEffects=None,admissionReviewVersions=v1</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *Nginx)</span> <span class="hljs-title">ValidateCreate</span><span class="hljs-params">()</span> <span class="hljs-params">(admission.Warnings, error)</span></span> {
    <span class="hljs-comment">// 驗證 replicas 範圍</span>
    <span class="hljs-keyword">if</span> r.Spec.Replicas != <span class="hljs-literal">nil</span> &amp;&amp; (*r.Spec.Replicas &lt; <span class="hljs-number">1</span> || *r.Spec.Replicas &gt; <span class="hljs-number">10</span>) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"replicas must be between 1 and 10"</span>)
    }

    <span class="hljs-comment">// 驗證 image 格式</span>
    <span class="hljs-keyword">if</span> !strings.HasPrefix(r.Spec.Image, <span class="hljs-string">"nginx:"</span>) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"image must start with 'nginx:'"</span>)
    }

    <span class="hljs-comment">// 驗證 config 語法（可選）</span>
    <span class="hljs-keyword">if</span> r.Spec.Config != <span class="hljs-string">""</span> {
        <span class="hljs-keyword">if</span> err := validateNginxConfig(r.Spec.Config); err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"invalid nginx config: %w"</span>, err)
        }
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *Nginx)</span> <span class="hljs-title">ValidateUpdate</span><span class="hljs-params">(old runtime.Object)</span> <span class="hljs-params">(admission.Warnings, error)</span></span> {
    oldNginx := old.(*Nginx)

    <span class="hljs-comment">// 防止降級（可選規則）</span>
    <span class="hljs-keyword">if</span> r.Spec.Image != <span class="hljs-string">""</span> &amp;&amp; oldNginx.Spec.Image != <span class="hljs-string">""</span> {
        oldVersion := extractVersion(oldNginx.Spec.Image)
        newVersion := extractVersion(r.Spec.Image)
        <span class="hljs-keyword">if</span> newVersion &lt; oldVersion {
            <span class="hljs-keyword">return</span> admission.Warnings{<span class="hljs-string">"Downgrading nginx version may cause issues"</span>}, <span class="hljs-literal">nil</span>
        }
    }

    <span class="hljs-keyword">return</span> r.ValidateCreate()
}
</code></pre>
<h3 id="heading-1023-secret">10.2.3 Secret 管理</h3>
<p><strong>從 Secret 讀取敏感配置</strong>：</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">reconcileDeployment</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-comment">// 從 Secret 掛載 TLS 證書</span>
    volumes := []corev1.Volume{
        {
            Name: <span class="hljs-string">"config"</span>,
            VolumeSource: corev1.VolumeSource{
                ConfigMap: &amp;corev1.ConfigMapVolumeSource{
                    LocalObjectReference: corev1.LocalObjectReference{
                        Name: nginx.Name + <span class="hljs-string">"-config"</span>,
                    },
                },
            },
        },
    }

    volumeMounts := []corev1.VolumeMount{
        {
            Name:      <span class="hljs-string">"config"</span>,
            MountPath: <span class="hljs-string">"/etc/nginx/nginx.conf"</span>,
            SubPath:   <span class="hljs-string">"nginx.conf"</span>,
        },
    }

    <span class="hljs-comment">// 如果配置了 TLS</span>
    <span class="hljs-keyword">if</span> nginx.Spec.TLS != <span class="hljs-literal">nil</span> {
        volumes = <span class="hljs-built_in">append</span>(volumes, corev1.Volume{
            Name: <span class="hljs-string">"tls"</span>,
            VolumeSource: corev1.VolumeSource{
                Secret: &amp;corev1.SecretVolumeSource{
                    SecretName: nginx.Spec.TLS.SecretName,
                },
            },
        })

        volumeMounts = <span class="hljs-built_in">append</span>(volumeMounts, corev1.VolumeMount{
            Name:      <span class="hljs-string">"tls"</span>,
            MountPath: <span class="hljs-string">"/etc/nginx/tls"</span>,
            ReadOnly:  <span class="hljs-literal">true</span>,
        })
    }

    <span class="hljs-comment">// 使用在 Deployment spec 中...</span>
}
</code></pre>
<p><strong>加密 etcd 中的 Secret</strong>（集群級配置）：</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># /etc/kubernetes/manifests/kube-apiserver.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">kube-apiserver</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">containers:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">kube-apiserver</span>
      <span class="hljs-attr">command:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">kube-apiserver</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">--encryption-provider-config=/etc/kubernetes/encryption-config.yaml</span>
      <span class="hljs-attr">volumeMounts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">encryption-config</span>
          <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/etc/kubernetes/encryption-config.yaml</span>
          <span class="hljs-attr">readOnly:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">volumes:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">encryption-config</span>
      <span class="hljs-attr">hostPath:</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">/etc/kubernetes/encryption-config.yaml</span>
</code></pre>
<h2 id="heading-103">10.3 可觀測性</h2>
<h3 id="heading-1031">10.3.1 結構化日誌</h3>
<p><strong>使用 logr 進行結構化日誌記錄</strong>：</p>
<pre><code class="lang-go"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/log"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">Reconcile</span><span class="hljs-params">(ctx context.Context, req ctrl.Request)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    logger := log.FromContext(ctx)

    <span class="hljs-comment">// 基本日誌</span>
    logger.Info(<span class="hljs-string">"Reconciling Nginx"</span>,
        <span class="hljs-string">"namespace"</span>, req.Namespace,
        <span class="hljs-string">"name"</span>, req.Name,
    )

    <span class="hljs-comment">// 帶錯誤的日誌</span>
    <span class="hljs-keyword">if</span> err := r.Get(ctx, req.NamespacedName, &amp;nginx); err != <span class="hljs-literal">nil</span> {
        logger.Error(err, <span class="hljs-string">"Failed to get Nginx"</span>,
            <span class="hljs-string">"namespace"</span>, req.Namespace,
            <span class="hljs-string">"name"</span>, req.Name,
        )
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-comment">// Debug 級別日誌</span>
    logger.V(<span class="hljs-number">1</span>).Info(<span class="hljs-string">"Detailed debug info"</span>,
        <span class="hljs-string">"spec"</span>, nginx.Spec,
        <span class="hljs-string">"status"</span>, nginx.Status,
    )

    <span class="hljs-comment">// 使用 WithValues 添加上下文</span>
    logger = logger.WithValues(
        <span class="hljs-string">"nginx-version"</span>, nginx.Spec.Image,
        <span class="hljs-string">"replicas"</span>, nginx.Spec.Replicas,
    )

    logger.Info(<span class="hljs-string">"Starting reconciliation"</span>)

    <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
}
</code></pre>
<p><strong>配置日誌級別</strong> (<code>main.go</code>)：</p>
<pre><code class="lang-go"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"flag"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/log/zap"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> logLevel <span class="hljs-keyword">int</span>
    flag.IntVar(&amp;logLevel, <span class="hljs-string">"log-level"</span>, <span class="hljs-number">0</span>, <span class="hljs-string">"Log level (0=info, 1=debug, 2=trace)"</span>)

    opts := zap.Options{
        Development: <span class="hljs-literal">true</span>,
        Level:       zapcore.Level(-logLevel),
    }

    ctrl.SetLogger(zap.New(zap.UseFlagOptions(&amp;opts)))

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<h3 id="heading-1032-metrics">10.3.2 Metrics 暴露</h3>
<p><strong>使用 Prometheus metrics</strong>：</p>
<pre><code class="lang-go"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"github.com/prometheus/client_golang/prometheus"</span>
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/metrics"</span>
)

<span class="hljs-keyword">var</span> (
    nginxReconcileTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: <span class="hljs-string">"nginx_operator_reconcile_total"</span>,
            Help: <span class="hljs-string">"Total number of reconciliations"</span>,
        },
        []<span class="hljs-keyword">string</span>{<span class="hljs-string">"namespace"</span>, <span class="hljs-string">"name"</span>, <span class="hljs-string">"result"</span>},
    )

    nginxReconcileDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    <span class="hljs-string">"nginx_operator_reconcile_duration_seconds"</span>,
            Help:    <span class="hljs-string">"Duration of reconciliations"</span>,
            Buckets: prometheus.DefBuckets,
        },
        []<span class="hljs-keyword">string</span>{<span class="hljs-string">"namespace"</span>, <span class="hljs-string">"name"</span>},
    )

    nginxCount = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: <span class="hljs-string">"nginx_operator_nginx_count"</span>,
            Help: <span class="hljs-string">"Number of Nginx instances"</span>,
        },
        []<span class="hljs-keyword">string</span>{<span class="hljs-string">"namespace"</span>},
    )
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// 註冊 metrics</span>
    metrics.Registry.MustRegister(
        nginxReconcileTotal,
        nginxReconcileDuration,
        nginxCount,
    )
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">Reconcile</span><span class="hljs-params">(ctx context.Context, req ctrl.Request)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    start := time.Now()
    <span class="hljs-keyword">defer</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        duration := time.Since(start).Seconds()
        nginxReconcileDuration.WithLabelValues(
            req.Namespace,
            req.Name,
        ).Observe(duration)
    }()

    nginx := &amp;webappv1alpha1.Nginx{}
    <span class="hljs-keyword">if</span> err := r.Get(ctx, req.NamespacedName, nginx); err != <span class="hljs-literal">nil</span> {
        nginxReconcileTotal.WithLabelValues(
            req.Namespace,
            req.Name,
            <span class="hljs-string">"error"</span>,
        ).Inc()
        <span class="hljs-keyword">return</span> ctrl.Result{}, client.IgnoreNotFound(err)
    }

    <span class="hljs-comment">// ... reconcile 邏輯</span>

    nginxReconcileTotal.WithLabelValues(
        req.Namespace,
        req.Name,
        <span class="hljs-string">"success"</span>,
    ).Inc()

    nginxCount.WithLabelValues(req.Namespace).Set(<span class="hljs-number">1</span>)

    <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>
}
</code></pre>
<p><strong>配置 ServiceMonitor</strong> (Prometheus Operator)：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">monitoring.coreos.com/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ServiceMonitor</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-metrics</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">control-plane:</span> <span class="hljs-string">controller-manager</span>
  <span class="hljs-attr">endpoints:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-string">metrics</span>
      <span class="hljs-attr">interval:</span> <span class="hljs-string">30s</span>
      <span class="hljs-attr">path:</span> <span class="hljs-string">/metrics</span>
</code></pre>
<h3 id="heading-1033">10.3.3 健康檢查</h3>
<p><strong>配置 Health 和 Readiness Probes</strong> (<code>main.go</code>)：</p>
<pre><code class="lang-go"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"sigs.k8s.io/controller-runtime/pkg/healthz"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
        HealthProbeBindAddress: <span class="hljs-string">":8081"</span>,
        <span class="hljs-comment">// ...</span>
    })

    <span class="hljs-comment">// 添加 health check</span>
    <span class="hljs-keyword">if</span> err := mgr.AddHealthzCheck(<span class="hljs-string">"healthz"</span>, healthz.Ping); err != <span class="hljs-literal">nil</span> {
        setupLog.Error(err, <span class="hljs-string">"unable to set up health check"</span>)
        os.Exit(<span class="hljs-number">1</span>)
    }

    <span class="hljs-comment">// 添加 readiness check</span>
    <span class="hljs-keyword">if</span> err := mgr.AddReadyzCheck(<span class="hljs-string">"readyz"</span>, healthz.Ping); err != <span class="hljs-literal">nil</span> {
        setupLog.Error(err, <span class="hljs-string">"unable to set up ready check"</span>)
        os.Exit(<span class="hljs-number">1</span>)
    }

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p><strong>Deployment 配置</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-controller-manager</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">manager</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">nginx-operator:latest</span>
          <span class="hljs-attr">ports:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8080</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">metrics</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8081</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">health</span>
          <span class="hljs-attr">livenessProbe:</span>
            <span class="hljs-attr">httpGet:</span>
              <span class="hljs-attr">path:</span> <span class="hljs-string">/healthz</span>
              <span class="hljs-attr">port:</span> <span class="hljs-string">health</span>
            <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">15</span>
            <span class="hljs-attr">periodSeconds:</span> <span class="hljs-number">20</span>
          <span class="hljs-attr">readinessProbe:</span>
            <span class="hljs-attr">httpGet:</span>
              <span class="hljs-attr">path:</span> <span class="hljs-string">/readyz</span>
              <span class="hljs-attr">port:</span> <span class="hljs-string">health</span>
            <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">5</span>
            <span class="hljs-attr">periodSeconds:</span> <span class="hljs-number">10</span>
</code></pre>
<h2 id="heading-104">10.4 生產環境部署</h2>
<h3 id="heading-1041">10.4.1 高可用性配置</h3>
<p><strong>多副本 + Leader Election</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// main.go</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
        <span class="hljs-comment">// 啟用 Leader Election</span>
        LeaderElection:          <span class="hljs-literal">true</span>,
        LeaderElectionID:        <span class="hljs-string">"nginx-operator-lock"</span>,
        LeaderElectionNamespace: <span class="hljs-string">"nginx-operator-system"</span>,

        <span class="hljs-comment">// 配置 lease 參數</span>
        LeaseDuration: pointer.Duration(<span class="hljs-number">15</span> * time.Second),
        RenewDeadline: pointer.Duration(<span class="hljs-number">10</span> * time.Second),
        RetryPeriod:   pointer.Duration(<span class="hljs-number">2</span> * time.Second),
    })

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p><strong>Deployment 高可用配置</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-controller-manager</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">control-plane:</span> <span class="hljs-string">controller-manager</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">control-plane:</span> <span class="hljs-string">controller-manager</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-comment"># Pod 反親和性 - 分散到不同節點</span>
      <span class="hljs-attr">affinity:</span>
        <span class="hljs-attr">podAntiAffinity:</span>
          <span class="hljs-attr">preferredDuringSchedulingIgnoredDuringExecution:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">weight:</span> <span class="hljs-number">100</span>
              <span class="hljs-attr">podAffinityTerm:</span>
                <span class="hljs-attr">labelSelector:</span>
                  <span class="hljs-attr">matchLabels:</span>
                    <span class="hljs-attr">control-plane:</span> <span class="hljs-string">controller-manager</span>
                <span class="hljs-attr">topologyKey:</span> <span class="hljs-string">kubernetes.io/hostname</span>

      <span class="hljs-comment"># 優先級</span>
      <span class="hljs-attr">priorityClassName:</span> <span class="hljs-string">system-cluster-critical</span>

      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">manager</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">nginx-operator:latest</span>
          <span class="hljs-attr">resources:</span>
            <span class="hljs-attr">requests:</span>
              <span class="hljs-attr">cpu:</span> <span class="hljs-string">100m</span>
              <span class="hljs-attr">memory:</span> <span class="hljs-string">128Mi</span>
            <span class="hljs-attr">limits:</span>
              <span class="hljs-attr">cpu:</span> <span class="hljs-string">500m</span>
              <span class="hljs-attr">memory:</span> <span class="hljs-string">512Mi</span>

          <span class="hljs-comment"># 安全上下文</span>
          <span class="hljs-attr">securityContext:</span>
            <span class="hljs-attr">allowPrivilegeEscalation:</span> <span class="hljs-literal">false</span>
            <span class="hljs-attr">capabilities:</span>
              <span class="hljs-attr">drop:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">ALL</span>
            <span class="hljs-attr">runAsNonRoot:</span> <span class="hljs-literal">true</span>
            <span class="hljs-attr">runAsUser:</span> <span class="hljs-number">65532</span>
            <span class="hljs-attr">seccompProfile:</span>
              <span class="hljs-attr">type:</span> <span class="hljs-string">RuntimeDefault</span>
</code></pre>
<h3 id="heading-1042">10.4.2 資源限制</h3>
<p><strong>配置 Resource Quotas</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ResourceQuota</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-quota</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">hard:</span>
    <span class="hljs-attr">requests.cpu:</span> <span class="hljs-string">"2"</span>
    <span class="hljs-attr">requests.memory:</span> <span class="hljs-string">"2Gi"</span>
    <span class="hljs-attr">limits.cpu:</span> <span class="hljs-string">"4"</span>
    <span class="hljs-attr">limits.memory:</span> <span class="hljs-string">"4Gi"</span>
    <span class="hljs-attr">persistentvolumeclaims:</span> <span class="hljs-string">"10"</span>
</code></pre>
<p><strong>配置 LimitRange</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">LimitRange</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-limitrange</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">limits:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">max:</span>
        <span class="hljs-attr">cpu:</span> <span class="hljs-string">"1"</span>
        <span class="hljs-attr">memory:</span> <span class="hljs-string">"1Gi"</span>
      <span class="hljs-attr">min:</span>
        <span class="hljs-attr">cpu:</span> <span class="hljs-string">"50m"</span>
        <span class="hljs-attr">memory:</span> <span class="hljs-string">"64Mi"</span>
      <span class="hljs-attr">default:</span>
        <span class="hljs-attr">cpu:</span> <span class="hljs-string">"200m"</span>
        <span class="hljs-attr">memory:</span> <span class="hljs-string">"256Mi"</span>
      <span class="hljs-attr">defaultRequest:</span>
        <span class="hljs-attr">cpu:</span> <span class="hljs-string">"100m"</span>
        <span class="hljs-attr">memory:</span> <span class="hljs-string">"128Mi"</span>
      <span class="hljs-attr">type:</span> <span class="hljs-string">Container</span>
</code></pre>
<h3 id="heading-1043">10.4.3 監控告警</h3>
<p><strong>Prometheus 告警規則</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">monitoring.coreos.com/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">PrometheusRule</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-alerts</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">nginx-operator-system</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">groups:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator</span>
      <span class="hljs-attr">interval:</span> <span class="hljs-string">30s</span>
      <span class="hljs-attr">rules:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">alert:</span> <span class="hljs-string">NginxOperatorDown</span>
          <span class="hljs-attr">expr:</span> <span class="hljs-string">up{job="nginx-operator-metrics"}</span> <span class="hljs-string">==</span> <span class="hljs-number">0</span>
          <span class="hljs-attr">for:</span> <span class="hljs-string">5m</span>
          <span class="hljs-attr">labels:</span>
            <span class="hljs-attr">severity:</span> <span class="hljs-string">critical</span>
          <span class="hljs-attr">annotations:</span>
            <span class="hljs-attr">summary:</span> <span class="hljs-string">"Nginx Operator is down"</span>
            <span class="hljs-attr">description:</span> <span class="hljs-string">"Nginx Operator has been down for more than 5 minutes"</span>

        <span class="hljs-bullet">-</span> <span class="hljs-attr">alert:</span> <span class="hljs-string">NginxOperatorHighReconcileErrors</span>
          <span class="hljs-attr">expr:</span> <span class="hljs-string">rate(nginx_operator_reconcile_total{result="error"}[5m])</span> <span class="hljs-string">&gt;</span> <span class="hljs-number">0.1</span>
          <span class="hljs-attr">for:</span> <span class="hljs-string">10m</span>
          <span class="hljs-attr">labels:</span>
            <span class="hljs-attr">severity:</span> <span class="hljs-string">warning</span>
          <span class="hljs-attr">annotations:</span>
            <span class="hljs-attr">summary:</span> <span class="hljs-string">"High reconcile error rate"</span>
            <span class="hljs-attr">description:</span> <span class="hljs-string">"Nginx Operator has high reconcile error rate: <span class="hljs-template-variable">{{ $value }}</span>"</span>

        <span class="hljs-bullet">-</span> <span class="hljs-attr">alert:</span> <span class="hljs-string">NginxInstanceNotReady</span>
          <span class="hljs-attr">expr:</span> <span class="hljs-string">nginx_operator_nginx_count{}</span> <span class="hljs-bullet">-</span> <span class="hljs-string">nginx_operator_nginx_ready{}</span> <span class="hljs-string">&gt;</span> <span class="hljs-number">0</span>
          <span class="hljs-attr">for:</span> <span class="hljs-string">15m</span>
          <span class="hljs-attr">labels:</span>
            <span class="hljs-attr">severity:</span> <span class="hljs-string">warning</span>
          <span class="hljs-attr">annotations:</span>
            <span class="hljs-attr">summary:</span> <span class="hljs-string">"Nginx instance not ready"</span>
            <span class="hljs-attr">description:</span> <span class="hljs-string">"Nginx instance in <span class="hljs-template-variable">{{ $labels.namespace }}</span> is not ready for 15 minutes"</span>
</code></pre>
<h3 id="heading-1044">10.4.4 備份和恢復</h3>
<p><strong>備份 CRD 和 CR</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
<span class="hljs-comment"># backup-nginx-operator.sh</span>

BACKUP_DIR=<span class="hljs-string">"/backup/nginx-operator/<span class="hljs-subst">$(date +%Y%m%d-%H%M%S)</span>"</span>
mkdir -p <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>"</span>

<span class="hljs-comment"># 備份 CRD</span>
kubectl get crd nginxes.webapp.example.com -o yaml &gt; <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>/crd.yaml"</span>

<span class="hljs-comment"># 備份所有 Nginx CR</span>
kubectl get nginx --all-namespaces -o yaml &gt; <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>/nginx-crs.yaml"</span>

<span class="hljs-comment"># 備份 Operator 配置</span>
kubectl get deployment,service,configmap,secret -n nginx-operator-system -o yaml &gt; <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>/operator-resources.yaml"</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Backup completed: <span class="hljs-variable">$BACKUP_DIR</span>"</span>
</code></pre>
<p><strong>恢復流程</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
<span class="hljs-comment"># restore-nginx-operator.sh</span>

BACKUP_DIR=<span class="hljs-variable">$1</span>

<span class="hljs-keyword">if</span> [ -z <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>"</span> ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Usage: <span class="hljs-variable">$0</span> &lt;backup-directory&gt;"</span>
    <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># 1. 恢復 CRD</span>
kubectl apply -f <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>/crd.yaml"</span>

<span class="hljs-comment"># 2. 恢復 Operator</span>
kubectl apply -f <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>/operator-resources.yaml"</span>

<span class="hljs-comment"># 3. 等待 Operator ready</span>
kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=available --timeout=300s \
    deployment/nginx-operator-controller-manager -n nginx-operator-system

<span class="hljs-comment"># 4. 恢復 Nginx CR</span>
kubectl apply -f <span class="hljs-string">"<span class="hljs-variable">$BACKUP_DIR</span>/nginx-crs.yaml"</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Restore completed from: <span class="hljs-variable">$BACKUP_DIR</span>"</span>
</code></pre>
<h2 id="heading-105">10.5 多租戶支援</h2>
<h3 id="heading-1051-namespace">10.5.1 Namespace 隔離</h3>
<p><strong>限制 Operator 監聽特定 Namespace</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// main.go</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// 從環境變數讀取允許的 namespaces</span>
    watchNamespaces := os.Getenv(<span class="hljs-string">"WATCH_NAMESPACES"</span>)
    <span class="hljs-keyword">var</span> namespaces []<span class="hljs-keyword">string</span>
    <span class="hljs-keyword">if</span> watchNamespaces != <span class="hljs-string">""</span> {
        namespaces = strings.Split(watchNamespaces, <span class="hljs-string">","</span>)
    }

    mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
        Scheme: scheme,
        Cache: cache.Options{
            Namespaces: namespaces,  <span class="hljs-comment">// 只 watch 這些 namespaces</span>
        },
    })

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p><strong>Deployment 配置</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nginx-operator-controller-manager</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">manager</span>
          <span class="hljs-attr">env:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">WATCH_NAMESPACES</span>
              <span class="hljs-attr">value:</span> <span class="hljs-string">"tenant-a,tenant-b,tenant-c"</span>
</code></pre>
<h3 id="heading-1052">10.5.2 資源配額</h3>
<p><strong>為每個租戶配置 ResourceQuota</strong>：</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ResourceQuota</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">tenant-a-nginx-quota</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">tenant-a</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">hard:</span>
    <span class="hljs-attr">count/nginxes.webapp.example.com:</span> <span class="hljs-string">"10"</span>  <span class="hljs-comment"># 最多 10 個 Nginx 實例</span>
  <span class="hljs-attr">scopeSelector:</span>
    <span class="hljs-attr">matchExpressions:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">operator:</span> <span class="hljs-string">In</span>
        <span class="hljs-attr">scopeName:</span> <span class="hljs-string">PriorityClass</span>
        <span class="hljs-attr">values:</span> [<span class="hljs-string">"tenant-a"</span>]
</code></pre>
<h3 id="heading-1053">10.5.3 驗證租戶權限</h3>
<p><strong>Validating Webhook 檢查權限</strong>：</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *Nginx)</span> <span class="hljs-title">ValidateCreate</span><span class="hljs-params">()</span> <span class="hljs-params">(admission.Warnings, error)</span></span> {
    <span class="hljs-comment">// 檢查租戶配額</span>
    quota, err := getTenantQuota(r.Namespace)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }

    currentCount, err := getNginxCount(r.Namespace)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }

    <span class="hljs-keyword">if</span> currentCount &gt;= quota {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(
            <span class="hljs-string">"tenant %s has reached quota limit: %d/%d"</span>,
            r.Namespace, currentCount, quota,
        )
    }

    <span class="hljs-comment">// 檢查資源限制</span>
    <span class="hljs-keyword">if</span> r.Spec.Replicas != <span class="hljs-literal">nil</span> &amp;&amp; *r.Spec.Replicas &gt; quota.MaxReplicas {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(
            <span class="hljs-string">"replicas %d exceeds tenant limit %d"</span>,
            *r.Spec.Replicas, quota.MaxReplicas,
        )
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-literal">nil</span>
}
</code></pre>
<hr />
<h1 id="heading-5y2b5lia44cb5bi46kal5zwp6agm6iih6kq6kmm5oqa5ben">十一、常見問題與調試技巧</h1>
<h2 id="heading-111">11.1 常見錯誤及解決方案</h2>
<h3 id="heading-1111-crd">11.1.1 CRD 相關錯誤</h3>
<p><strong>錯誤 1：CRD 未安裝</strong></p>
<pre><code class="lang-bash">Error: the server could not find the requested resource (get nginxes.webapp.example.com)
</code></pre>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 檢查 CRD 是否存在</span>
kubectl get crd | grep nginx

<span class="hljs-comment"># 安裝 CRD</span>
make install

<span class="hljs-comment"># 或手動安裝</span>
kubectl apply -f config/crd/bases/webapp.example.com_nginxes.yaml

<span class="hljs-comment"># 驗證</span>
kubectl get crd nginxes.webapp.example.com -o yaml
</code></pre>
<p><strong>錯誤 2：CRD 版本不匹配</strong></p>
<pre><code class="lang-bash">error: error validating <span class="hljs-string">"nginx.yaml"</span>: error validating data: ValidationError(Nginx.spec): unknown field <span class="hljs-string">"newField"</span>
</code></pre>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 更新 CRD 定義</span>
make manifests

<span class="hljs-comment"># 2. 重新安裝 CRD</span>
kubectl replace -f config/crd/bases/webapp.example.com_nginxes.yaml

<span class="hljs-comment"># 3. 如果 replace 失敗，需要先刪除（危險操作！會刪除所有 CR）</span>
kubectl delete crd nginxes.webapp.example.com
make install
</code></pre>
<p><strong>錯誤 3：Validation 規則不生效</strong></p>
<pre><code class="lang-yaml"><span class="hljs-comment"># CRD 中定義了 validation，但創建時沒有被驗證</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">100</span>  <span class="hljs-comment"># 超過 maximum: 10 但沒報錯</span>
</code></pre>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 確認 CRD 中包含 validation rules</span>
kubectl get crd nginxes.webapp.example.com -o yaml | grep -A 10 validation

<span class="hljs-comment"># 2. 重新生成並安裝 CRD</span>
make manifests
kubectl apply -f config/crd/bases/webapp.example.com_nginxes.yaml

<span class="hljs-comment"># 3. 驗證 validation 生效</span>
kubectl apply -f - &lt;&lt;EOF
apiVersion: webapp.example.com/v1alpha1
kind: Nginx
metadata:
  name: <span class="hljs-built_in">test</span>
spec:
  replicas: 100  <span class="hljs-comment"># 應該報錯</span>
EOF
</code></pre>
<h3 id="heading-1112-controller">11.1.2 Controller 錯誤</h3>
<p><strong>錯誤 1：Reconcile 死循環</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 日誌顯示不停 reconcile</span>
INFO    Reconciling Nginx
INFO    Reconciling Nginx
INFO    Reconciling Nginx
...
</code></pre>
<p><strong>原因分析</strong>：</p>
<ol>
<li><p>Status 更新觸發了 reconcile</p>
</li>
<li><p>沒有正確設置 Predicate 過濾</p>
</li>
<li><p>Requeue 邏輯錯誤</p>
</li>
</ol>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// 1. 使用 Predicate 過濾 status-only 更新</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">SetupWithManager</span><span class="hljs-params">(mgr ctrl.Manager)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-keyword">return</span> ctrl.NewControllerManagedBy(mgr).
        For(&amp;webappv1alpha1.Nginx{}).
        WithEventFilter(predicate.Funcs{
            UpdateFunc: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(e event.UpdateEvent)</span> <span class="hljs-title">bool</span></span> {
                oldObj := e.ObjectOld.(*webappv1alpha1.Nginx)
                newObj := e.ObjectNew.(*webappv1alpha1.Nginx)

                <span class="hljs-comment">// 只在 spec 改變時 reconcile</span>
                <span class="hljs-keyword">return</span> oldObj.Generation != newObj.Generation
            },
        }).
        Complete(r)
}

<span class="hljs-comment">// 2. 正確使用 Status().Update()</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">Reconcile</span><span class="hljs-params">(ctx context.Context, req ctrl.Request)</span> <span class="hljs-params">(ctrl.Result, error)</span></span> {
    <span class="hljs-comment">// ... reconcile logic</span>

    <span class="hljs-comment">// 使用 Status() subresource 更新，不會觸發新的 reconcile</span>
    <span class="hljs-keyword">if</span> err := r.Status().Update(ctx, nginx); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> ctrl.Result{}, err
    }

    <span class="hljs-keyword">return</span> ctrl.Result{}, <span class="hljs-literal">nil</span>  <span class="hljs-comment">// 不要返回 Requeue: true</span>
}
</code></pre>
<p><strong>錯誤 2：無法更新 Status</strong></p>
<pre><code class="lang-bash">ERROR   Failed to update status    error=<span class="hljs-string">"the server could not find the requested resource"</span>
</code></pre>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// 確保 CRD 定義包含 status subresource</span>
<span class="hljs-comment">// +kubebuilder:object:root=true</span>
<span class="hljs-comment">// +kubebuilder:subresource:status  // &lt;-- 這一行很重要！</span>

<span class="hljs-keyword">type</span> Nginx <span class="hljs-keyword">struct</span> {
    metav1.TypeMeta   <span class="hljs-string">`json:",inline"`</span>
    metav1.ObjectMeta <span class="hljs-string">`json:"metadata,omitempty"`</span>
    Spec   NginxSpec   <span class="hljs-string">`json:"spec,omitempty"`</span>
    Status NginxStatus <span class="hljs-string">`json:"status,omitempty"`</span>
}
</code></pre>
<pre><code class="lang-bash"><span class="hljs-comment"># 重新生成並安裝 CRD</span>
make manifests
make install
</code></pre>
<p><strong>錯誤 3：Owner Reference 錯誤導致資源無法刪除</strong></p>
<pre><code class="lang-bash">ERROR   Failed to delete Deployment    error=<span class="hljs-string">"cannot delete resource, owner references are set"</span>
</code></pre>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// 正確設置 Owner Reference</span>
<span class="hljs-keyword">if</span> err := controllerutil.SetControllerReference(nginx, deployment, r.Scheme); err != <span class="hljs-literal">nil</span> {
    <span class="hljs-keyword">return</span> err
}

<span class="hljs-comment">// 刪除時會自動清理子資源（Garbage Collection）</span>
<span class="hljs-comment">// 如果需要手動清理：</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(r *NginxReconciler)</span> <span class="hljs-title">cleanupResources</span><span class="hljs-params">(ctx context.Context, nginx *webappv1alpha1.Nginx)</span> <span class="hljs-title">error</span></span> {
    <span class="hljs-comment">// 列出所有關聯資源</span>
    deployments := &amp;appsv1.DeploymentList{}
    <span class="hljs-keyword">if</span> err := r.List(ctx, deployments, client.InNamespace(nginx.Namespace), client.MatchingLabels{
        <span class="hljs-string">"nginx"</span>: nginx.Name,
    }); err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-comment">// 刪除資源</span>
    <span class="hljs-keyword">for</span> _, deployment := <span class="hljs-keyword">range</span> deployments.Items {
        <span class="hljs-keyword">if</span> err := r.Delete(ctx, &amp;deployment); err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<h3 id="heading-1113-rbac">11.1.3 RBAC 權限錯誤</h3>
<p><strong>錯誤：403 Forbidden</strong></p>
<pre><code class="lang-bash">ERROR   Failed to list Deployments    error=<span class="hljs-string">"deployments.apps is forbidden: User \"system:serviceaccount:nginx-operator-system:default\" cannot list resource \"deployments\" in API group \"apps\" in the namespace \"default\""</span>
</code></pre>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 檢查 RBAC 註解是否正確</span>
<span class="hljs-comment"># Controller 代碼中：</span>
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete

<span class="hljs-comment"># 2. 重新生成 RBAC manifests</span>
make manifests

<span class="hljs-comment"># 3. 檢查生成的 Role/ClusterRole</span>
cat config/rbac/role.yaml

<span class="hljs-comment"># 4. 重新部署 Operator</span>
kubectl apply -k config/default

<span class="hljs-comment"># 5. 驗證 ServiceAccount 權限</span>
kubectl auth can-i list deployments \
    --as=system:serviceaccount:nginx-operator-system:nginx-operator-controller-manager \
    -n default
</code></pre>
<p><strong>跨 Namespace 權限問題</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 如果 Operator 需要操作多個 namespace，需要使用 ClusterRole</span>
<span class="hljs-comment"># 修改 config/default/kustomization.yaml：</span>

<span class="hljs-comment"># 註釋掉這一行（使用 Role）</span>
<span class="hljs-comment"># - ../rbac/role.yaml</span>
<span class="hljs-comment"># - ../rbac/role_binding.yaml</span>

<span class="hljs-comment"># 使用 ClusterRole</span>
- ../rbac/cluster_role.yaml
- ../rbac/cluster_role_binding.yaml
</code></pre>
<h3 id="heading-1114-webhook">11.1.4 Webhook 錯誤</h3>
<p><strong>錯誤：Webhook 連接超時</strong></p>
<pre><code class="lang-bash">Error from server (InternalError): Internal error occurred: failed calling webhook <span class="hljs-string">"vnginx.kb.io"</span>: Post <span class="hljs-string">"https://nginx-operator-webhook-service.nginx-operator-system.svc:443/validate-webapp-example-com-v1alpha1-nginx?timeout=10s"</span>: context deadline exceeded
</code></pre>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 檢查 webhook service 是否存在</span>
kubectl get svc -n nginx-operator-system

<span class="hljs-comment"># 2. 檢查 webhook pod 是否運行</span>
kubectl get pods -n nginx-operator-system

<span class="hljs-comment"># 3. 檢查證書是否正確配置</span>
kubectl get secret -n nginx-operator-system | grep webhook

<span class="hljs-comment"># 4. 檢查 ValidatingWebhookConfiguration</span>
kubectl get validatingwebhookconfiguration

<span class="hljs-comment"># 5. 查看 webhook 配置詳情</span>
kubectl get validatingwebhookconfiguration nginx-operator-validating-webhook-configuration -o yaml

<span class="hljs-comment"># 6. 測試 webhook service 連接</span>
kubectl run test-curl --image=curlimages/curl --rm -it --restart=Never -- \
    curl -k https://nginx-operator-webhook-service.nginx-operator-system.svc:443

<span class="hljs-comment"># 7. 如果開發環境不需要 webhook，可以禁用</span>
<span class="hljs-built_in">export</span> ENABLE_WEBHOOKS=<span class="hljs-literal">false</span>
make run
</code></pre>
<p><strong>錯誤：證書驗證失敗</strong></p>
<pre><code class="lang-bash">Error: x509: certificate signed by unknown authority
</code></pre>
<p><strong>解決方案</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 確保安裝了 cert-manager</span>
kubectl get pods -n cert-manager

<span class="hljs-comment"># 如果沒有安裝：</span>
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml

<span class="hljs-comment"># 2. 檢查 Certificate 資源</span>
kubectl get certificate -n nginx-operator-system

<span class="hljs-comment"># 3. 檢查證書狀態</span>
kubectl describe certificate nginx-operator-serving-cert -n nginx-operator-system

<span class="hljs-comment"># 4. 手動觸發證書重新生成</span>
kubectl delete certificate nginx-operator-serving-cert -n nginx-operator-system
kubectl apply -f config/certmanager/certificate.yaml

<span class="hljs-comment"># 5. 等待證書 ready</span>
kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=ready certificate/nginx-operator-serving-cert \
    -n nginx-operator-system --timeout=300s
</code></pre>
<h2 id="heading-112">11.2 調試技巧</h2>
<h3 id="heading-1121">11.2.1 本地調試</h3>
<p><strong>使用 Delve 調試</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 安裝 Delve</span>
go install github.com/go-delve/delve/cmd/dlv@latest

<span class="hljs-comment"># 2. 啟動調試模式</span>
dlv debug ./main.go -- --zap-devel

<span class="hljs-comment"># 3. 設置斷點</span>
(dlv) <span class="hljs-built_in">break</span> internal/controller/nginx_controller.go:60
(dlv) <span class="hljs-built_in">continue</span>

<span class="hljs-comment"># 4. 查看變量</span>
(dlv) <span class="hljs-built_in">print</span> nginx
(dlv) <span class="hljs-built_in">print</span> err

<span class="hljs-comment"># 5. 查看調用棧</span>
(dlv) stack
</code></pre>
<p><strong>VS Code 調試配置</strong> (<code>.vscode/launch.json</code>)：</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.2.0"</span>,
    <span class="hljs-attr">"configurations"</span>: [
        {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Debug Operator"</span>,
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"go"</span>,
            <span class="hljs-attr">"request"</span>: <span class="hljs-string">"launch"</span>,
            <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"debug"</span>,
            <span class="hljs-attr">"program"</span>: <span class="hljs-string">"${workspaceFolder}/main.go"</span>,
            <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"--zap-devel"</span>],
            <span class="hljs-attr">"env"</span>: {
                <span class="hljs-attr">"ENABLE_WEBHOOKS"</span>: <span class="hljs-string">"false"</span>,
                <span class="hljs-attr">"KUBECONFIG"</span>: <span class="hljs-string">"${env:HOME}/.kube/config"</span>
            }
        },
        {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Attach to Process"</span>,
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"go"</span>,
            <span class="hljs-attr">"request"</span>: <span class="hljs-string">"attach"</span>,
            <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"local"</span>,
            <span class="hljs-attr">"processId"</span>: <span class="hljs-string">"${command:pickProcess}"</span>
        }
    ]
}
</code></pre>
<h3 id="heading-1122">11.2.2 日誌分析</h3>
<p><strong>增加日誌詳細度</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 運行時指定 log level</span>
make run -- --zap-log-level=debug

<span class="hljs-comment"># 或在代碼中：</span>
logger.V(1).Info(<span class="hljs-string">"Debug message"</span>, <span class="hljs-string">"key"</span>, value)  <span class="hljs-comment"># debug</span>
logger.V(2).Info(<span class="hljs-string">"Trace message"</span>, <span class="hljs-string">"key"</span>, value)  <span class="hljs-comment"># trace</span>
</code></pre>
<p><strong>結構化日誌查詢</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 使用 jq 過濾日誌</span>
kubectl logs -n nginx-operator-system deployment/nginx-operator-controller-manager | jq <span class="hljs-string">'select(.level == "error")'</span>

<span class="hljs-comment"># 查找特定資源的日誌</span>
kubectl logs -n nginx-operator-system deployment/nginx-operator-controller-manager | \
    jq <span class="hljs-string">'select(.nginx == "nginx-sample")'</span>

<span class="hljs-comment"># 統計錯誤類型</span>
kubectl logs -n nginx-operator-system deployment/nginx-operator-controller-manager | \
    jq -r <span class="hljs-string">'select(.level == "error") | .msg'</span> | sort | uniq -c
</code></pre>
<h3 id="heading-1123">11.2.3 性能分析</h3>
<p><strong>CPU Profiling</strong>：</p>
<pre><code class="lang-go"><span class="hljs-comment">// main.go</span>
<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"net/http"</span>
    _ <span class="hljs-string">"net/http/pprof"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// 啟動 pprof server</span>
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        http.ListenAndServe(<span class="hljs-string">"localhost:6060"</span>, <span class="hljs-literal">nil</span>)
    }()

    <span class="hljs-comment">// ... 其他代碼</span>
}
</code></pre>
<pre><code class="lang-bash"><span class="hljs-comment"># 收集 30 秒的 CPU profile</span>
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

<span class="hljs-comment"># 交互式分析</span>
(pprof) top10
(pprof) list reconcileDeployment
(pprof) web  <span class="hljs-comment"># 生成可視化圖表（需要 graphviz）</span>
</code></pre>
<p><strong>Memory Profiling</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 收集 heap profile</span>
go tool pprof http://localhost:6060/debug/pprof/heap

<span class="hljs-comment"># 分析內存分配</span>
(pprof) top10
(pprof) list NginxReconciler.Reconcile

<span class="hljs-comment"># 查看內存洩漏</span>
go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
</code></pre>
<p><strong>Goroutine 分析</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-comment"># 查看所有 goroutine</span>
curl http://localhost:6060/debug/pprof/goroutine?debug=2

<span class="hljs-comment"># 分析 goroutine 洩漏</span>
go tool pprof http://localhost:6060/debug/pprof/goroutine
</code></pre>
<h3 id="heading-1124">11.2.4 資源狀態檢查</h3>
<p><strong>完整的資源檢查腳本</strong>：</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
<span class="hljs-comment"># debug-nginx.sh - Nginx Operator 診斷腳本</span>

NAMESPACE=<span class="hljs-variable">${1:-default}</span>
NGINX_NAME=<span class="hljs-variable">${2:-nginx-sample}</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"=== Nginx Resource ==="</span>
kubectl get nginx <span class="hljs-variable">$NGINX_NAME</span> -n <span class="hljs-variable">$NAMESPACE</span> -o yaml

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== Nginx Status ==="</span>
kubectl get nginx <span class="hljs-variable">$NGINX_NAME</span> -n <span class="hljs-variable">$NAMESPACE</span> -o jsonpath=<span class="hljs-string">'{.status}'</span> | jq

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== Nginx Events ==="</span>
kubectl get events -n <span class="hljs-variable">$NAMESPACE</span> --field-selector involvedObject.name=<span class="hljs-variable">$NGINX_NAME</span>

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== Deployment ==="</span>
kubectl get deployment <span class="hljs-variable">$NGINX_NAME</span> -n <span class="hljs-variable">$NAMESPACE</span> -o yaml

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== Deployment Events ==="</span>
kubectl get events -n <span class="hljs-variable">$NAMESPACE</span> --field-selector involvedObject.name=<span class="hljs-variable">$NGINX_NAME</span>,involvedObject.kind=Deployment

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== Pods ==="</span>
kubectl get pods -n <span class="hljs-variable">$NAMESPACE</span> -l nginx=<span class="hljs-variable">$NGINX_NAME</span>

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== Pod Events ==="</span>
<span class="hljs-keyword">for</span> pod <span class="hljs-keyword">in</span> $(kubectl get pods -n <span class="hljs-variable">$NAMESPACE</span> -l nginx=<span class="hljs-variable">$NGINX_NAME</span> -o name); <span class="hljs-keyword">do</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Events for <span class="hljs-variable">$pod</span>:"</span>
    kubectl get events -n <span class="hljs-variable">$NAMESPACE</span> --field-selector involvedObject.name=$(basename <span class="hljs-variable">$pod</span>)
<span class="hljs-keyword">done</span>

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== Service ==="</span>
kubectl get svc <span class="hljs-variable">$NGINX_NAME</span> -n <span class="hljs-variable">$NAMESPACE</span> -o yaml

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== ConfigMap ==="</span>
kubectl get cm <span class="hljs-variable">${NGINX_NAME}</span>-config -n <span class="hljs-variable">$NAMESPACE</span> -o yaml

<span class="hljs-built_in">echo</span> -e <span class="hljs-string">"\n=== Operator Logs (last 50 lines) ==="</span>
kubectl logs -n nginx-operator-system deployment/nginx-operator-controller-manager --tail=50
</code></pre>
<p>使用：</p>
<pre><code class="lang-bash">chmod +x debug-nginx.sh
./debug-nginx.sh default nginx-sample
</code></pre>
<h2 id="heading-113">11.3 故障排除流程</h2>
<h3 id="heading-1131">11.3.1 問題診斷流程圖</h3>
<pre><code class="lang-bash">1. CR 能否創建成功？
   ├─ No  → 檢查 CRD 安裝、Validation Webhook
   └─ Yes → 繼續

2. Operator 能否收到事件？
   ├─ No  → 檢查 Operator 運行狀態、RBAC 權限、Namespace 配置
   └─ Yes → 繼續

3. Reconcile 是否執行？
   ├─ No  → 檢查 Predicate 過濾、事件觸發條件
   └─ Yes → 繼續

4. 子資源是否創建？
   ├─ No  → 檢查 RBAC、資源定義、錯誤日誌
   └─ Yes → 繼續

5. 子資源狀態是否正常？
   ├─ No  → 檢查資源配置、鏡像、探針、資源限制
   └─ Yes → 問題解決
</code></pre>
<h3 id="heading-1132">11.3.2 常見場景排查</h3>
<p><strong>場景 1：Pod 無法啟動</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 查看 Pod 狀態</span>
kubectl get pods -l nginx=nginx-sample

<span class="hljs-comment"># 2. 查看詳細錯誤</span>
kubectl describe pod &lt;pod-name&gt;

<span class="hljs-comment"># 3. 查看日誌</span>
kubectl logs &lt;pod-name&gt;

<span class="hljs-comment"># 常見原因：</span>
<span class="hljs-comment"># - ImagePullBackOff → 鏡像不存在或無權限</span>
<span class="hljs-comment"># - CrashLoopBackOff → 容器啟動失敗，檢查配置和日誌</span>
<span class="hljs-comment"># - Pending → 資源不足或調度失敗</span>
</code></pre>
<p><strong>場景 2：配置更新不生效</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 檢查 ConfigMap 是否更新</span>
kubectl get cm nginx-sample-config -o yaml

<span class="hljs-comment"># 2. 檢查 Deployment 的 config-version label</span>
kubectl get deployment nginx-sample -o jsonpath=<span class="hljs-string">'{.spec.template.metadata.labels.config-version}'</span>

<span class="hljs-comment"># 3. 如果 label 沒變，檢查 reconcile 日誌</span>
kubectl logs -n nginx-operator-system deployment/nginx-operator-controller-manager | grep nginx-sample

<span class="hljs-comment"># 4. 手動觸發 reconcile</span>
kubectl annotate nginx nginx-sample force-reconcile=<span class="hljs-string">"<span class="hljs-subst">$(date +%s)</span>"</span>
</code></pre>
<p><strong>場景 3：資源洩漏</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># 1. 列出所有孤立資源（沒有 owner reference）</span>
kubectl get deployment,svc,cm --all-namespaces -o json | \
    jq -r <span class="hljs-string">'.items[] | select(.metadata.ownerReferences == null) | "\(.metadata.namespace)/\(.kind)/\(.metadata.name)"'</span>

<span class="hljs-comment"># 2. 清理孤立資源</span>
kubectl delete deployment &lt;name&gt; -n &lt;namespace&gt;

<span class="hljs-comment"># 3. 確保 Controller 設置了 Owner Reference</span>
<span class="hljs-comment"># 代碼檢查：</span>
<span class="hljs-keyword">if</span> err := controllerutil.SetControllerReference(nginx, deployment, r.Scheme); err != nil {
    <span class="hljs-built_in">return</span> err
}
</code></pre>
<h2 id="heading-114">11.4 最佳實踐總結</h2>
<h3 id="heading-1141">11.4.1 開發階段</h3>
<ol>
<li><p><strong>使用 Kind 本地測試</strong></p>
<pre><code class="lang-bash"> make kind-cluster
 make install
 make run
</code></pre>
</li>
<li><p><strong>頻繁運行測試</strong></p>
<pre><code class="lang-bash"> make <span class="hljs-built_in">test</span>
 make test-e2e
</code></pre>
</li>
<li><p><strong>使用 Linter</strong></p>
<pre><code class="lang-bash"> golangci-lint run ./...
</code></pre>
</li>
</ol>
<h3 id="heading-1142">11.4.2 部署階段</h3>
<ol>
<li><p><strong>使用 Kustomize 管理環境</strong></p>
<pre><code class="lang-bash"> config/
 ├── base/
 └── overlays/
     ├── development/
     ├── staging/
     └── production/
</code></pre>
</li>
<li><p><strong>配置資源限制</strong></p>
<pre><code class="lang-yaml"> <span class="hljs-attr">resources:</span>
   <span class="hljs-attr">requests:</span>
     <span class="hljs-attr">cpu:</span> <span class="hljs-string">100m</span>
     <span class="hljs-attr">memory:</span> <span class="hljs-string">128Mi</span>
   <span class="hljs-attr">limits:</span>
     <span class="hljs-attr">cpu:</span> <span class="hljs-string">500m</span>
     <span class="hljs-attr">memory:</span> <span class="hljs-string">512Mi</span>
</code></pre>
</li>
<li><p><strong>啟用 Leader Election</strong></p>
<pre><code class="lang-go"> LeaderElection: <span class="hljs-literal">true</span>
</code></pre>
</li>
</ol>
<h3 id="heading-1143">11.4.3 運維階段</h3>
<ol>
<li><p><strong>監控關鍵指標</strong></p>
<ul>
<li><p>Reconcile 成功率</p>
</li>
<li><p>Reconcile 延遲</p>
</li>
<li><p>資源使用率</p>
</li>
</ul>
</li>
<li><p><strong>配置告警</strong></p>
<ul>
<li><p>Operator Down</p>
</li>
<li><p>高錯誤率</p>
</li>
<li><p>資源洩漏</p>
</li>
</ul>
</li>
<li><p><strong>定期備份</strong></p>
<pre><code class="lang-bash"> kubectl get nginx --all-namespaces -o yaml &gt; backup.yaml
</code></pre>
</li>
</ol>
<hr />
<h2 id="heading-57i957wq">總結</h2>
<p>本教學從基礎概念到高級實踐，完整覆蓋了 Kubernetes Operator 開發的各個方面：</p>
<ol>
<li><p><strong>第一~三章</strong>：Operator 基礎概念、架構設計</p>
</li>
<li><p><strong>第四~六章</strong>：深入 OpenTelemetry Operator 代碼分析</p>
</li>
<li><p><strong>第七~八章</strong>：開發環境、測試實踐</p>
</li>
<li><p><strong>第九章</strong>：完整的 Nginx Operator 實戰</p>
</li>
<li><p><strong>第十章</strong>：性能、安全、可觀測性等進階主題</p>
</li>
<li><p><strong>第十一章</strong>：故障排查和最佳實踐</p>
</li>
</ol>
<p><strong>下一步建議</strong>：</p>
<ol>
<li><p>閱讀 <a target="_blank" href="https://sdk.operatorframework.io/">Operator SDK 文檔</a></p>
</li>
<li><p>研究優秀的開源 Operator 項目</p>
</li>
<li><p>在實際項目中實踐所學知識</p>
</li>
</ol>
<p>成為 Kubernetes Operator 開發專家！ Go 🚀</p>
]]></content:encoded></item><item><title><![CDATA[System Design Interview Ch 8 Leader Board]]></title><description><![CDATA[Leader Board
我們將設計一個即時遊戲排行榜系統
能參考 Binance 的 leaderboard 。
Step 1 : Requirements and Scope

Candidate: How is the score calculated for the leaderboard?
Interviewer: The user gets a point when they win a match. We can go with a simple point system in w...]]></description><link>https://ganhua.wang/system-design-interview-ch-8-leader-board</link><guid isPermaLink="true">https://ganhua.wang/system-design-interview-ch-8-leader-board</guid><category><![CDATA[System Design]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Mon, 20 Oct 2025 15:47:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760975160441/d8528587-dfdc-4609-a66e-513c50a5d6e2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-leader-board">Leader Board</h1>
<p>我們將設計一個<strong>即時</strong>遊戲排行榜系統</p>
<p>能參考 <a target="_blank" href="https://www.binance.com/en/futures-activity/leaderboard/top-ranking">Binance 的 leaderboard</a> 。</p>
<h2 id="heading-step-1-requirements-and-scope">Step 1 : Requirements and Scope</h2>
<blockquote>
<p><strong>Candidate:</strong> How is the <strong>score</strong> calculated for the leaderboard?</p>
<p><strong>Interviewer:</strong> The user gets a point when they win a match. We can go with a simple point system in which each user has a score associated with them. Each time the user wins a match, we should add a point to their total score.</p>
<p>用戶在贏得一場比賽時會得到一點分數。我們可以採用一個簡單的積分系統，每個用戶都有一個與之相關聯的分數。每當用戶贏得一場比賽，我們就應該在他們的總分中增加一點。</p>
<p><strong>Candidate:</strong> Are <strong>all players</strong> included in the leaderboard?</p>
<p><strong>Interviewer:</strong> Yes.</p>
<p><strong>Candidate:</strong> Is there a <strong>time segment</strong> associated with the leaderboard?</p>
<p>排行榜是否有相關的時間分段（或賽季）？</p>
<p><strong>Interviewer:</strong> Each month, a new tournament kicks off which starts a new leaderboard.</p>
<p>每個月都會啟動一次新的錦標賽，並開始一個新的排行榜。</p>
<p><strong>Candidate:</strong> Can we assume we only care about the <strong>top 10 users</strong>?</p>
<p>我們是否可以假設我們只需要關心前 10 名用戶？</p>
<p><strong>Interviewer:</strong> We want to display the top 10 users as well as the position of a specific user on the leaderboard. If time allows, let's also discuss how to return users <strong>who are four places above and below a specific user</strong>.</p>
<p>我們想要顯示前 10 名用戶，以及特定用戶在排行榜上的位置。如果時間允許，我們也來討論如何回傳特定用戶排名上、下各四位的玩家。</p>
<p><strong>Candidate:</strong> How many users are in a tournament?</p>
<p><strong>Interviewer:</strong> Average of <strong>5 million daily active users (DAU)</strong> and <strong>25 million monthly active users (MAU)</strong>.</p>
<p>平均有 500 萬每日活躍用戶 (DAU) 和 2,500 萬每月活躍用戶 (MAU)。</p>
<p><strong>Candidate:</strong> How many matches are played on average during a tournament?</p>
<p><strong>Interviewer:</strong> Each player plays <strong>10 matches</strong> per day on average.</p>
<p>每位玩家平均每天進行 10 場比賽。</p>
<p><strong>Candidate:</strong> How do we determine the rank if two players have the same score?</p>
<p><strong>Interviewer:</strong> In this case, their ranks are the same. If time allows, we can talk about ways to break ties.</p>
<p><strong>Candidate:</strong> Does the leaderboard need to be <strong>real-time</strong>?</p>
<p><strong>Interviewer:</strong> Yes, we want to present real-time results, or as close as possible. It is not okay to present a batched history of results.</p>
<p>是的，我們希望呈現即時的結果，或盡可能接近即時。呈現批次處理的歷史結果是不可接受的。</p>
</blockquote>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>類型</strong></td><td><strong>項目</strong></td><td><strong>詳細要求</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>功能需求 (F.R.)</strong></td><td><strong>排行榜顯示</strong></td><td>1. 顯示排行榜<strong>前 10 名</strong>玩家。 2. 顯示特定用戶的<strong>當前排名</strong>。 3. （加分項）顯示特定用戶<strong>上下四個排名</strong>的玩家。</td></tr>
<tr>
<td><strong>非功能需求 (N.F.R.)</strong></td><td><strong>即時性 (Real-time)</strong></td><td>積分更新必須<strong>即時</strong>反映在排行榜上，不接受延遲的結果。</td></tr>
<tr>
<td><strong>比賽規則</strong></td><td><strong>計分方式</strong></td><td>用戶每贏得一場比賽，總分加 <strong>1</strong> 點。</td></tr>
<tr>
<td><strong>時間範圍</strong></td><td><strong>賽季長度</strong></td><td>每個<strong>月</strong>開始一個新的錦標賽，即新的排行榜。</td></tr>
<tr>
<td><strong>規模估算</strong></td><td><strong>每日活躍用戶 (DAU)</strong></td><td>500 萬 (5M) 用戶。</td></tr>
<tr>
<td><strong>規模估算</strong></td><td><strong>分數更新 QPS (峰值)</strong></td><td>約 2,500 次/秒。</td></tr>
</tbody>
</table>
</div><h2 id="heading-user-scale">User Scale</h2>
<p>系統的規模假設：</p>
<p>每日活躍用戶 (DAU): 500 萬 (5 Million)</p>
<p>每月活躍用戶 (MAU): 2,500 萬 (25 Million)</p>
<h2 id="heading-qps-estimate">QPS estimate</h2>
<p>QPS 估算決定了系統的<strong>即時處理能力</strong>。主要估算了兩種類型的 QPS：</p>
<h4 id="heading-a-score-update-qps">A. Score Update QPS</h4>
<p>指玩家贏得比賽後，系統需要更新分數的頻率。</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>項目</strong></td><td><strong>數量/假設</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>活躍用戶</strong></td><td>5,000,000 DAU</td></tr>
<tr>
<td><strong>分數更新事件</strong></td><td>假設每位 DAU <strong>平均</strong>每天贏得 1 場比賽（即 1 次分數更新）。</td></tr>
<tr>
<td><strong>平均 QPS</strong></td><td>5000000次更新/86400秒 = 57.87次/秒</td></tr>
</tbody>
</table>
</div><p>為了設計具有冗餘和彈性的系統，設定了更高的 QPS 基準：</p>
<ul>
<li><p><strong>平均 QPS 基準：</strong> 500 次/秒 (假設是更保守或更高的流量)</p>
</li>
<li><p><strong>峰值 QPS 基準：</strong> 2,500 次/秒 (假設是平均 QPS 基準的 5 倍)</p>
</li>
</ul>
<blockquote>
<p><strong>🎯 思考：</strong> 為什麼在設計一個<strong>每月</strong>都會重置的排行榜系統時，我們需要同時關注 <strong>DAU (每日活躍用戶)</strong> 和 <strong>MAU (每月活躍用戶)</strong>？哪一個數字對儲存<strong>排行榜數據的總容量</strong>影響最大？</p>
</blockquote>
<h4 id="heading-b-leaderboard-read-qps">B. Leaderboard Read QPS</h4>
<p>這是指用戶查看前 10 名排行榜的頻率。</p>
<ul>
<li><strong>排行榜查詢 QPS：</strong> 估計約 50 次/秒。</li>
</ul>
<blockquote>
<p><strong>🎯 思考：</strong> 如果我們必須在這兩項 QPS 中選擇一個來進行<strong>優先優化</strong>，你會選擇優化 <strong>2,500 次/秒的「分數更新」</strong> 還是 <strong>50 次/秒的「排行榜查詢」</strong>？為什麼？</p>
</blockquote>
<h2 id="heading-step-2-high-level-design">Step 2 : High-Level Design</h2>
<h2 id="heading-api-design">API Design</h2>
<p>首先，系統必須定義外部服務和客戶端如何與排行榜系統互動。設計了三個核心 API：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>API URI</strong></td><td><strong>方法</strong></td><td><strong>描述</strong></td><td><strong>呼叫方 (Caller)</strong></td><td><strong>Request Fields</strong></td><td><strong>Response Fields</strong></td></tr>
</thead>
<tbody>
<tr>
<td><code>/v1/scores</code></td><td><code>POST</code></td><td><strong>更新用戶分數</strong> (Score Update)（用戶贏得比賽時）</td><td><strong>遊戲服務器 (Game Service)</strong> （<em>內部 API</em>）</td><td><code>user_id</code> (string): 玩家唯一 ID <code>points</code> (integer): 要增加的分數（通常為 1）</td><td><code>success</code> (boolean): 操作結果狀態 <code>new_score</code> (integer): 更新後的總分數</td></tr>
<tr>
<td><code>/v1/scores</code></td><td><code>GET</code></td><td><strong>獲取排行榜前 10 名</strong> (Get Top 10)</td><td><strong>客戶端 App (Client App)</strong></td><td><em>無</em></td><td><code>leaderboard</code> (array):     包含多個物件，每個物件有：     <code>user_id</code> (string)     <code>score</code> (integer)     <code>rank</code> (integer)</td></tr>
<tr>
<td><code>/v1/scores/{:user_id}</code></td><td><code>GET</code></td><td><strong>獲取特定用戶排名</strong> (Get User Rank)</td><td><strong>客戶端 App (Client App)</strong></td><td><em>URI 參數</em>：     <code>user_id</code> (string)</td><td><code>user_id</code> (string) <code>score</code> (integer): 玩家分數 <code>rank</code> (integer): 玩家排名 <code>neighbors</code> (array/optional):     該用戶<strong>上下四位</strong>玩家的列表。</td></tr>
</tbody>
</table>
</div><p><strong>GET /v1/scores (獲取排行榜前 10 名)</strong></p>
<p>這個 API 用於獲取排行榜上分數最高的 N 個玩家（在此設計中為前 10 名）。</p>
<p><strong>API 目的：</strong> 顯示完整的排行榜首頁。</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"status"</span>: <span class="hljs-string">"success"</span>,
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"leaderboard"</span>: [
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Aquaboys_976"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">976</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"B_team_956"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">956</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">2</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Berlins_Angels_890"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">890</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">3</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Grendel_Team_878"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">878</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">4</span>
      },
      <span class="hljs-comment">// ... (省略中間部分)</span>
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Player_500"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">701</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">9</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Last_Top_10"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">695</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">10</span>
      }
    ],
    <span class="hljs-attr">"count"</span>: <span class="hljs-number">10</span>
  }
}
</code></pre>
<p><strong>GET /v1/scores/{:user_id} (獲取特定用戶排名)</strong></p>
<p>這個 API 用於查詢特定用戶的個人資訊，並滿足額外需求：「顯示特定用戶<strong>上下四個排名</strong>的玩家」（即 <code>neighbors</code>）。</p>
<p><strong>API 目的：</strong> 讓用戶快速找到自己的位置，並查看附近競爭者的分數。</p>
<p>假設查詢用戶：<code>user_id: CurrentPlayer_750</code></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"status"</span>: <span class="hljs-string">"success"</span>,
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"CurrentPlayer_750"</span>,
    <span class="hljs-attr">"score"</span>: <span class="hljs-number">750</span>,
    <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1500</span>, <span class="hljs-comment">// 玩家當前的排名</span>
    <span class="hljs-attr">"neighbors"</span>: [
      <span class="hljs-comment">// 玩家排名上方的四位 (Rank 1496 ~ 1499)</span>
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Rival_A"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">755</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1496</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Rival_B"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">754</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1497</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Rival_C"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">753</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1498</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Rival_D"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">752</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1499</span>
      },
      <span class="hljs-comment">// ********* 當前玩家 *********</span>
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"CurrentPlayer_750"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">750</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1500</span>,
        <span class="hljs-attr">"is_current_user"</span>: <span class="hljs-literal">true</span>
      },
      <span class="hljs-comment">// 玩家排名下方的四位 (Rank 1501 ~ 1504)</span>
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Friend_E"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">748</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1501</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Friend_F"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">745</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1502</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Friend_G"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">744</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1503</span>
      },
      {
        <span class="hljs-attr">"user_id"</span>: <span class="hljs-string">"Friend_H"</span>,
        <span class="hljs-attr">"score"</span>: <span class="hljs-number">743</span>,
        <span class="hljs-attr">"rank"</span>: <span class="hljs-number">1504</span>
      }
    ]
  }
}
</code></pre>
<h2 id="heading-high-level-arch">High-level Arch.</h2>
<p>為了確保系統的<strong>可擴展性</strong>和<strong>職責分離</strong>，架構被劃分為兩個主要服務：</p>
<ol>
<li><p><strong>遊戲服務 (Game Service)</strong>：專注於遊戲本身的邏輯和驗證。</p>
</li>
<li><p><strong>排行榜服務 (Leaderboard Service)</strong>：專注於管理和顯示分數與排名。</p>
</li>
</ol>
<p><strong>運作流程：</strong></p>
<p>玩家贏得遊戲 → 遊戲服務驗證 → <strong>調用排行榜服務 API</strong> (<code>POST /v1/scores</code>) → 排行榜服務更新 <strong>Leaderboard Store</strong> 中的分數。</p>
<pre><code class="lang-mermaid">graph TD
    %% ========= Nodes =========
    subgraph Client
        U[使用者裝置 / Client]
    end

    GS[Game service]
    LS[Leaderboard service]
    LDB[(Leaderboard Store)]

    %% ========= Flows =========
    %% 1. 玩家贏得遊戲 → client 通知 game service
    U -- "① Win a game → send request" --&gt; GS

    %% 2. game service 驗證並呼叫 leaderboard service 更新分數
    GS -- "② Update score" --&gt; LS

    %% 3. leaderboard service 將分數寫入儲存層
    LS -- "③ Update score" --&gt; LDB

    %% 4. client 可直接查詢排行榜或個人名次
    U -- "④a Get leaderboard" --&gt; LS
    U -- "④b Get player_rank" --&gt; LS

    %% ========= Styling =========
    classDef write_path stroke:#cc0000,fill:#ffeaea,stroke-width:1.5px;
    classDef read_path  stroke:#007a3d,fill:#eafff1,stroke-width:1.5px;
    classDef store      stroke:#1f3b64,fill:#e7f0ff,stroke-width:1.5px;

    class LDB store;

    %% 按出現順序給邊著色：0..2 = 寫入，3..4 = 讀取
    linkStyle 0,1,2 stroke:#cc0000,stroke-width:2px;
    linkStyle 3,4 stroke:#007a3d,stroke-width:2px;
</code></pre>
<ul>
<li><p><strong>元件與分工</strong></p>
<ul>
<li><p><strong>Client（使用者裝置）</strong>：觸發遊戲結果上報，並向排行榜服務查詢資料。</p>
</li>
<li><p><strong>Game service</strong>：接收「玩家贏得遊戲」的事件並進行必要驗證，之後請求排行榜服務更新分數。</p>
</li>
<li><p><strong>Leaderboard service</strong>：提供更新與查詢排行榜的介面，對下游的 <strong>Leaderboard Store</strong> 進行資料存取。</p>
</li>
<li><p><strong>Leaderboard Store</strong>：抽象的排行榜儲存層（圖中未指定技術或資料庫種類）。</p>
</li>
</ul>
</li>
<li><p><strong>寫入流程（①②③）</strong></p>
<ol>
<li><p>玩家贏一場遊戲，<strong>Client</strong> 發請求給 <strong>Game service</strong>。</p>
</li>
<li><p><strong>Game service</strong> 確認結果有效後，呼叫 <strong>Leaderboard service</strong> 更新分數。</p>
</li>
<li><p><strong>Leaderboard service</strong> 將更新寫入 <strong>Leaderboard Store</strong>。</p>
</li>
</ol>
</li>
<li><p><strong>讀取流程（④a / ④b）</strong></p>
<ul>
<li><p><strong>Client</strong> 可<strong>直接</strong>呼叫 <strong>Leaderboard service</strong>：</p>
<ul>
<li><p>④a 取得排行榜（如 Top 10）。</p>
</li>
<li><p>④b 查詢特定玩家在排行榜上的名次（player_rank）。</p>
</li>
</ul>
</li>
<li><p><strong>Leaderboard service</strong> 從 <strong>Leaderboard Store</strong> 讀取並回傳查詢結果</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-do-we-need-a-message-queue-between-the-game-service-and-the-leader-board-service">Do we need a <strong>message queue</strong> between the game service and the leader board service?</h3>
<p><strong>是否需要要看「Game score event 會不會被多方使用」</strong>。</p>
<ul>
<li><p>若只是單純由 Leaderboard 讀取、沒有其他用途，<strong>可直接同步呼叫</strong> Leaderboard Service（不必加 MQ）（即更即時的變更）。</p>
</li>
<li><p>若同一份「分數更新事件」還要被 <strong>多個服務</strong> 消費（例如排行榜、分析、推播），<strong>放進 Kafka 等 MQ 比較合適</strong>，形成一對多的事件分發。</p>
</li>
</ul>
<pre><code class="lang-mermaid">flowchart LR
    GS[Game service]
    K[(Kafka / MQ)]
    LBS[Leaderboard service]
    AS[Analytics service]
    PS[Push Notification service]

    GS --&gt; K
    K --&gt; LBS
    K --&gt; AS
    K --&gt; PS
</code></pre>
<h2 id="heading-data-model-amp-storage-options">Data Model &amp; Storage Options</h2>
<h3 id="heading-relational-db">關聯式資料庫（Relational DB）方案</h3>
<h3 id="heading-1">1️⃣ 基本設計</h3>
<ul>
<li><p>使用一張 <code>leaderboard</code> 表，欄位如下：</p>
</li>
<li><p>| 欄位 | 型別 |
  | --- | --- |
  | user_id | varchar |
  | score | int |</p>
<p>  每個月的排行榜可用一張表（或一個分區）表示。</p>
</li>
</ul>
<h3 id="heading-2">2️⃣ 分數更新流程</h3>
<p>當玩家贏得比賽時：</p>
<ul>
<li><p>若該玩家尚未有紀錄 → 新增一筆：</p>
</li>
<li><pre><code class="lang-sql">          <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> leaderboard (user_id, score) <span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'mary1934'</span>, <span class="hljs-number">1</span>);
</code></pre>
</li>
<li><p>若已有紀錄 → 更新分數：</p>
</li>
<li><pre><code class="lang-sql">          <span class="hljs-keyword">UPDATE</span> leaderboard <span class="hljs-keyword">SET</span> score = score + <span class="hljs-number">1</span> <span class="hljs-keyword">WHERE</span> user_id = <span class="hljs-string">'mary1934'</span>;
</code></pre>
</li>
</ul>
<h3 id="heading-3">3️⃣ 查詢玩家名次</h3>
<p>為了找出排行榜名次，必須根據分數排序：</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> (@<span class="hljs-keyword">rownum</span> := @<span class="hljs-keyword">rownum</span> + <span class="hljs-number">1</span>) <span class="hljs-keyword">AS</span> <span class="hljs-keyword">rank</span>, user_id, score
<span class="hljs-keyword">FROM</span> leaderboard
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> score <span class="hljs-keyword">DESC</span>;
</code></pre>
<h3 id="heading-4pqg77ipiowvjmhjom7ng">⚠️ 問題點</h3>
<ul>
<li><p>當玩家很多（數百/千萬筆）時，排序操作非常耗時。</p>
</li>
<li><p>SQL 排序與排名計算屬於 <strong>掃描（scan）</strong>, even <code>table scan</code>。</p>
</li>
<li><p>每次要查名次都必須對整張表排序 → 不適合即時系統。</p>
</li>
<li><p>若資料頻繁變動（玩家持續上分），無法依靠 cache/index。</p>
</li>
<li><p>只能做 batch job，不適合實時排行榜。</p>
</li>
</ul>
<p>即使加上索引與 <code>LIMIT</code>（僅取前 N 名）仍無法從根本改善「找名次」問題。</p>
<h4 id="heading-rdb-sql">RDB 中求特定玩家排名的 SQL</h4>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> *,
    (<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">FROM</span> leaderboard lb2 <span class="hljs-keyword">WHERE</span> lb2.score &gt;= lb1.score) <span class="hljs-keyword">AS</span> <span class="hljs-keyword">rank</span>
<span class="hljs-keyword">FROM</span> leaderboard lb1
<span class="hljs-keyword">WHERE</span> lb1.user_id = :user_id;
</code></pre>
<h3 id="heading-lb1">外層查詢 (<code>lb1</code>)</h3>
<p>取出指定玩家的資料（例如 mary1934）。</p>
<h3 id="heading-lb2">🔹內層子查詢 (<code>lb2</code>)</h3>
<p>計算所有分數「<strong>大於等於該玩家分數</strong>」的筆數。</p>
<p>也就是：</p>
<ul>
<li><p>如果有 3 個人分數比她高或一樣高，</p>
</li>
<li><p>那她的名次（rank）就是第 3 名。</p>
</li>
</ul>
<p>雖然邏輯簡單，但這查詢的問題在於：</p>
<p><strong>❗ 每次都執行一次完整的 COUNT(*)</strong></p>
<p>對於每一位使用者，要重新掃描整張 leaderboard：</p>
<ul>
<li><p>若有 100 萬玩家 → 需要執行 100 萬次 <code>COUNT(*)</code>。</p>
</li>
<li><p>每次 <code>COUNT(*)</code> 都會掃整個表（因為 <code>score &gt;=</code> 無法使用索引完全優化）。</p>
</li>
</ul>
<p>所以整體時間複雜度是：</p>
<blockquote>
<p><strong>O(N²)</strong>（平方級別，非常慢）</p>
</blockquote>
<p><strong>SQL 為何在這場景下無法快？</strong></p>
<ul>
<li><p>關聯式資料庫（RDB）沒有內建「自動維持排序名次」的結構。</p>
</li>
<li><p><code>COUNT(*)</code> + <code>&gt;=</code> 會導致資料庫必須掃描整個表。</p>
</li>
<li><p>即使加索引，也只能幫部分情況加速，仍需大量比較操作。</p>
</li>
</ul>
<p>這就是為什麼書中說：</p>
<blockquote>
<p>SQL databases are not performant when we have to process large amounts of continuously changing information.</p>
</blockquote>
<h3 id="heading-redis">Redis 方案</h3>
<p>Redis 提供 <strong>Sorted Set (ZSet)</strong> 直接對應排行榜問題，在插入時就自動排序好，而且能直接用指令查名次：：</p>
<ul>
<li><p>每個元素（玩家）都有一個「分數」。</p>
</li>
<li><p>Redis 自動根據分數排序。</p>
</li>
<li><p>提供操作：</p>
<ul>
<li><p><code>ZINCRBY</code>：增加玩家分數。O(log n)</p>
<ul>
<li><p>每當玩家贏得一場比賽時，他的分數要加 1。<code>ZINCRBY &lt;KEY&gt; &lt;INCREMENT&gt; &lt;USER&gt;</code></p>
</li>
<li><p><code>ZINCRBY leaderboard_feb_2021 1 'mary1934'</code> ，在 <code>leaderboard_feb_2021</code>（2021 年 2 月排行榜）中，將使用者 <code>mary1934</code> 的分數增加 1。若她之前不在榜上，會自動新增。</p>
</li>
<li><p>上個月的排行榜則會被移到歷史資料庫保存。因為 Redis 存在記憶體中，長期資料會定期清除或搬走。核心精神是 <strong><em>過往紀錄不會再被修改了</em></strong>，放在關聯式資料庫以replica 跟 veiw 提供快速讀取。</p>
</li>
</ul>
</li>
<li><p><code>ZADD</code> : UPSERT 使用者與分數。O(log n)</p>
</li>
<li><p><code>ZREVRANGE</code>：取得前 N 名。O(log n + m) ,<strong>n</strong> 是排行榜中總成員數量。<strong>m</strong> 是要取出的筆數（通常很小，例如前 10 名）。</p>
<ul>
<li><p><code>ZREVRANGE &lt;KEY&gt; &lt;START&gt; &lt;STOP&gt; WITHSCORES</code></p>
</li>
<li><p>取 Top 10 <code>ZREVRANGE</code> leaderboard_feb_2021 <code>0 9 WITHSCORES</code> 。<code>ZREVRANGE</code>：取出成員並依照分數<strong>由高到低</strong>排序（rev = reverse）。<code>WITHSCORES</code>：同時返回使用者分數。</p>
</li>
<li><p>當月的 Top 10 用 Redis 這樣的解法比較快，因為 RDB 要全表排序才能得出結果。但過往的能另外做成一個存 Top 10 的 view.</p>
</li>
<li><p>查詢附近名次，若 <code>Mallow007</code> 排名為 361，想看前後 4 名， <code>ZREVRANGE leaderboard_feb_2021 357 365</code> ，會返回 rank 357–365 的玩家清單。</p>
</li>
</ul>
</li>
<li><p><code>ZREVRANK</code>：查玩家名次 <code>ZREVRANK leaderboard mary1934</code>。O(log n)</p>
<ul>
<li><p><code>ZREVRANK &lt;KEY&gt; &lt;USER&gt;</code></p>
</li>
<li><p><code>ZREVRANK leaderboard_feb_2021 'mary1934'</code></p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="heading-zset-skip-list">ZSet 內部實作：<strong>Skip List（跳表）</strong></h4>
<p>Redis Sorted Set 是用 <strong>Skip List（跳表）</strong> 實作的。</p>
<h4 id="heading-5qac5b177ya">概念：</h4>
<p>類似「多層索引」的鏈結串列。</p>
<ul>
<li><p>每一層會「跳過」一些節點，讓搜尋速度更快。</p>
</li>
<li><p>平均時間複雜度為 <strong>O(log n)</strong>，比全排序的 <strong>O(n log n)</strong> 快很多。</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760879805084/7358cf9c-87fe-44f0-8e4a-296f38f7eec8.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760879819247/83f34d52-d262-4e92-b250-b919adc4578b.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>當節點只有單層（Base list）→ 搜尋慢。</p>
</li>
<li><p>加入多層索引（Level 1, Level 2…）→ 能快速跳過大量節點。</p>
</li>
</ul>
<p>舉例：搜尋節點 <code>45</code> 時，原本需走 8 步，</p>
<p>從 Level 1 開始走：</p>
<p>1️⃣ 1 → 8 → 15 → 36 → 60<br />2️⃣ 發現 60 &gt; 45，就往下切換到底層。<br />3️⃣ 從 36 繼續走到底層：36 → 45 ✅</p>
<p>👉 大概只花 6 步。</p>
<p><strong>再加更多層索引</strong></p>
<p>每一層都「再跳更遠」：</p>
<p>從 Level 2 開始走：</p>
<p>1️⃣ 1 → 15 → 60<br />2️⃣ 發現 60 太大，往下一層 Level 1。。</p>
<p>3️⃣ 從 15 往後走：<code>15 → 36 → 60</code><br />4️⃣ 再往下層從 36 找：<code>36 → 45</code> ✅</p>
<p>👉 大概只花 6 步。</p>
<h2 id="heading-storage">Storage 需求</h2>
<h4 id="heading-6kiy5oa26auu5lyw566x">記憶體估算</h4>
<p>因為主要放 Redis ，那就要粗略估一下當月排行榜可能要多少記憶體來儲存。</p>
<p>最基本的情況下，排行榜需要儲存兩個欄位，user_id 與 score</p>
<p>因此每個排行榜entry至少需要：</p>
<ul>
<li><p><code>user_id</code>（假設為 24 bytes）</p>
</li>
<li><p><code>score</code>（16-bit 整數 = 2 bytes）</p>
</li>
</ul>
<p>總共約 <strong>26 bytes</strong>。</p>
<p>最壞情況（worst-case scenario）</p>
<p>假設有 <strong>2,500 萬月活躍使用者 (MAU)</strong>，而每個人至少贏過一場比賽，<br />因此每個人都會在排行榜中佔有一筆紀錄。大概需要 ≈ 650 MB</p>
<p>再加上 Redis 的 Index （skip list + hash table），<br />記憶體需求約 <strong>1.3 GB</strong> 左右。</p>
<h4 id="heading-kirmlyjog73osqdovikqkg"><strong>效能負載</strong></h4>
<p>CPU / I/O 考量：</p>
<p>估算每秒有約 2,500 次更新請求（<strong>QPS 2500</strong>），最基本的情況下，排行榜需要儲存兩個欄位</p>
<p>小 case!</p>
<h4 id="heading-redis-persistence-concern">Redis 的持久性問題 (Persistence Concern)</h4>
<p>我們的需求量不高，基本一台主機提供讀寫即可，為此我們考慮<strong>高可用</strong>的情況下，通常會考慮做 <strong>failover</strong>。</p>
<ul>
<li><p>啟用 <strong>replication（主從複製）</strong>：<br />  主節點（master）處理寫入，<br />  從節點（read replica）做備份與讀取。</p>
</li>
<li><p>若主節點故障，系統會自動將從節點升級成主節點。</p>
</li>
</ul>
<p>雖然 Redis 有持久化機制（RDB / AOF），<br />但從磁碟重啟大資料集仍然較慢，<br />所以設計上會使用多副本來確保即時可用。</p>
<p>Redis 進行 <strong>快照（snapshot / RDB dump）</strong> 時，會使用 <strong>Copy-on-Write（COW）</strong> 機制：<br />當 Redis 把記憶體內容寫入磁碟的同時，新的寫入仍在發生，這些新資料會暫存在新的記憶體區塊。</p>
<p>👉 所以在這段期間，<strong>實際記憶體用量可能暫時翻倍</strong>。</p>
<blockquote>
<p>💡 範例：</p>
<ul>
<li><p>排行榜原本需要 <strong>1.3 GB</strong> 記憶體</p>
</li>
<li><p>快照中寫入仍在進行</p>
</li>
<li><p>需要再額外預留 <strong>1.3 GB</strong><br />  → 因此整體記憶體至少要準備 <strong>2.6 GB</strong></p>
</li>
</ul>
</blockquote>
<p>Redis 支援兩種持久化方式：</p>
<ul>
<li><p><strong>RDB</strong>：定期快照（速度快，但可能遺失最近幾秒的資料）</p>
</li>
<li><p><strong>AOF（Append Only File）</strong>：逐筆記錄每次寫入（更安全但慢）</p>
</li>
</ul>
<p>Redis 官方提供 <a target="_blank" href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/benchmarks/"><code>redis-benchmark</code></a> 工具，用來模擬多用戶同時存取的情況。<br />你可以用它測試：</p>
<ul>
<li><p>每秒請求數（requests per second, RPS）</p>
</li>
<li><p>延遲（latency）</p>
</li>
<li><p>同時連線數（connections）</p>
</li>
</ul>
<p>藉由這些數據，你可以判斷目前硬體配置（CPU、記憶體、網路）是否足以支撐你的排行榜負載。</p>
<h4 id="heading-6zec6igv5byp6loh5paz5bqr">關聯式資料庫</h4>
<p>Redis 處理排行榜的即時更新與查詢，<br />但仍需要 <strong>關聯式資料庫</strong> 來保存永久性資料。</p>
<p>系統會設計兩張輔助表：</p>
<ul>
<li><p>User Table︰儲存 User profile</p>
</li>
<li><p>Point Table︰儲存 play history(include user_id, score, timestamp)。也能充當 redis 重建排行榜用的依據。</p>
</li>
<li><p>可以另外建立一個小型 view/cache ，專門存前十名玩家的詳細資料。因為這部分資料最常被查詢。且這不會佔用太多記憶體或儲存空間。</p>
</li>
</ul>
<h2 id="heading-todo-demo-by-k3d">TODO : Demo by K3D</h2>
<p><strong>SLO</strong>：更新/查詢 P95 &lt; 50ms；峰值 2,500 QPS 可撐。</p>
<p>場景 1 : Only RDBMS</p>
<ul>
<li><p>先演示並解釋上述 SQL 的執行計畫</p>
</li>
<li><p>壓測腳本，並關注 dashboard</p>
</li>
</ul>
<p>場景 2 : Use Redis</p>
<ul>
<li>壓測腳本，並關注 dashboard 上的 duration</li>
</ul>
<p><a target="_blank" href="https://github.com/tedmax100/system_design_interview_lab/tree/main/ch10_leader_board">Demo Code</a></p>
<h1 id="heading-57i957wq">總結</h1>
<h2 id="heading-5ywi6agn5aw955qe44cm5bcp6icm6zec6y2144cn5bel56il57sw56a">先顧好的「小而關鍵」工程細節</h2>
<ul>
<li><p><strong>冪等性</strong>：<code>POST /v1/scores</code> 帶 <code>match_id</code>，服務端去重（避免重複加分）。</p>
</li>
<li><p><strong>限流 &amp; 防濫用</strong>：在 Leaderboard Service 前做簡單令牌桶即可。</p>
</li>
<li><p><strong>安全邊界</strong>：只能由 Game Service 更新分數（client 不直寫）。</p>
</li>
<li><p><strong>可觀測性</strong>：QPS、P95 延遲、Redis 內存使用、慢查、鍵數量、複製延遲。</p>
</li>
<li><p><strong>高可用但簡單</strong>：主從複製（1 主 1 從）；RDB/AOF 擇一或混合；<strong>記憶體預留 2×</strong> 應對 COW snapshot。</p>
</li>
<li><p><strong>月度滾榜</strong>：<code>leaderboard_YYYYMM</code> 新鍵；歷史榜定期歸檔到 RDB（之後做離線報表/回放）。</p>
</li>
</ul>
<h2 id="heading-5lua6bq85pmc5ycz5omn5byv5ywl6ksh6zuc5bqm77yi5pio56k66ke455m86bue77yj">什麼時候才引入複雜度（明確觸發點）</h2>
<p><strong>1) 要不要 MQ（Kafka）？</strong><br />只有當「<strong>分數事件一對多</strong>」被其他服務共用（風控/分析/推播/成就）時才加。</p>
<ul>
<li><p>觸發點：新增第二個依賴「即時分數事件」的下游；或你需要<strong>重播/審計</strong>能力。</p>
</li>
<li><p>做法：Game Service 發 event → MQ → 多消費者（Leaderboard、Analytics…）。排行榜 path 仍可同步直寫，避免即時性被 MQ 拖慢。</p>
</li>
</ul>
<p><strong>2) 要不要 Redis Cluster / Sharding？</strong><br />當 <strong>單節點</strong>達到<strong>兩類瓶頸</strong>才升級：</p>
<ul>
<li><p><strong>容量</strong>：單月榜含索引 &gt; 10–15GB，或多同時活躍榜單壓迫記憶體。</p>
</li>
<li><p><strong>吞吐</strong>：更新峰值逼近 <strong>10萬+/秒</strong>、或主從複製延遲持續飆高。<br />  先考慮<strong>固定分片</strong>（依分數區間或區服/賽季），比 cluster 心智成本更低；真的全球級流量再上 <strong>Redis Cluster</strong>。</p>
</li>
</ul>
<p><strong>3) 要不要 CQRS？</strong><br />讀寫壓力/模型明顯分離、讀的花樣很多（多維排序、濾條件多）且你準備投入維運成本時再做。這題的讀模型單純（Top-N + 單人名次），<strong>沒必要</strong>。</p>
<h2 id="heading-6ako6zqq6iih5yn5qih5byp77yi6yg5ywn5lia6zal5ael5bcx6lip77yj">風險與反模式（避免一開始就踩）</h2>
<ul>
<li><p><strong>過度設計</strong>：一上來就 Kafka、Cluster、CQRS，交付慢、故障面積大、Demo 不出來。</p>
</li>
<li><p><strong>把歷史查詢放即時層</strong>：舊榜查報表丟給 RDB/離線系統，別拖累 Redis。</p>
</li>
<li><p><strong>客戶端直改分數</strong>：安全洞；必須 server-authoritative。</p>
</li>
<li><p><strong>忘了預留 2× 記憶體</strong>：快照時 COW 會瞬時吃雙倍 RAM。</p>
</li>
<li><p><strong>未做冪等/限流</strong>：比「沒用 MQ」更容易把系統打爆。</p>
</li>
</ul>
<h2 id="heading-5oir5pyd5zyo6z2i6kmm5lit5by36kq55qe5lij5yl6kmx">我會在面試中強調的三句話</h2>
<ol>
<li><p><strong>先用最小架構打穿功能與即時性</strong>：單台 Redis ZSet + MySQL 歷史表，三支 API 即可交付。</p>
</li>
<li><p><strong>明訂升級門檻與路徑</strong>：多消費者上 MQ；容量/吞吐逼近單機上限再分片或 Cluster。</p>
</li>
<li><p><strong>工程實務先做對小事</strong>：冪等等幂、限流、觀測、HA、月度滾榜與資料歸檔——這些比任何流行名詞更值錢。</p>
</li>
</ol>
<h1 id="heading-references">References</h1>
<p><a target="_blank" href="https://medium.com/analytics-vidhya/redis-sorted-sets-explained-2d8b6302525">https://medium.com/analytics-vidhya/redis-sorted-sets-explained-2d8b6302525</a></p>
<p><a target="_blank" href="https://systemdesign.one/leaderboard-system-design/">https://systemdesign.one/leaderboard-system-design/</a></p>
]]></content:encoded></item><item><title><![CDATA[DDD 紅皮書 - Ch4]]></title><description><![CDATA[https://learning.oreilly.com/library/view/implementing-domain-driven-design/9780133039900/ch04lev1sec4.html#ch04lev1sec4
訪問 CIO
了解系統架構演化的過程與面臨什麼事件因此需要演化。
主持人 Maria 與 SaaSOvation 的 CIO Mitchell 對談，回顧十年來他們如何在不同階段選用合適的架構、搭配 DDD 而穩健成長。

早期：原本規劃桌面部署＋中央資料庫...]]></description><link>https://ganhua.wang/ddd-ch4</link><guid isPermaLink="true">https://ganhua.wang/ddd-ch4</guid><category><![CDATA[DDD]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Tue, 30 Sep 2025 15:48:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759250316644/8c5cf290-ef28-45d4-8479-840e68342454.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://learning.oreilly.com/library/view/implementing-domain-driven-design/9780133039900/ch04lev1sec4.html#ch04lev1sec4">https://learning.oreilly.com/library/view/implementing-domain-driven-design/9780133039900/ch04lev1sec4.html#ch04lev1sec4</a></p>
<h1 id="heading-cio">訪問 CIO</h1>
<p>了解系統架構演化的過程與面臨什麼事件因此需要演化。</p>
<p><strong>主持人 Maria</strong> 與 <strong>SaaSOvation 的 CIO Mitchell</strong> 對談，回顧十年來他們如何在不同階段選用合適的架構、搭配 DDD 而穩健成長。</p>
<ul>
<li><p><strong>早期</strong>：原本規劃桌面部署＋中央資料庫，採 <strong>分層架構（Layers）</strong>，對單一應用層＋DB 的情境合理。</p>
</li>
<li><p><strong>轉向 SaaS</strong>：與夥伴合作、拿到資金，優先做協作工具，再回頭強化敏捷專案管理產品。</p>
</li>
<li><p><strong>引入 DIP</strong>：為了可測試性，用 <strong>依賴反轉（DIP）</strong> 讓 UI、基礎設施（例如持久化）可替換；以 <strong>Aggregate／Repository</strong> 配合「記憶體實作 → 真正持久化」的替換策略。</p>
</li>
<li><p><strong>上雲與行動化</strong>：行動需求、同盟登入、安全、BI 報表等湧現，採 <strong>REST</strong>；同時轉向 <strong>六邊形架構（Ports &amp; Adapters）</strong>，方便加新用戶端、持久化與訊息等 Adapter。</p>
</li>
<li><p><strong>擴張與整併</strong>：大量新租戶、資料遷移需求，於服務邊界用 <strong>SOA</strong>（例如 Mule 的 Collection Aggregator）整合，同時仍維持六邊形核心。</p>
</li>
<li><p><strong>UI 日益複雜</strong>：專案／缺陷看板需即時更新、且每租戶偏好不同；為降低指令與查詢世界的摩擦，引入 <strong>CQRS</strong>。</p>
</li>
<li><p><strong>長流程需求</strong>：某些功能要跑一串分散流程，不能讓使用者等待；導入 <strong>事件驅動架構（EDA）</strong> 與 <strong>Pipes &amp; Filters</strong>。</p>
</li>
<li><p><strong>規模再起</strong>：被大型雲商收購後，用戶暴增，將 Pipes &amp; Filters <strong>分散化與平行化</strong>，並加入 <strong>Saga（長流程／補償）</strong>。</p>
</li>
<li><p><strong>法規遵循</strong>：政府要求追蹤每次變更；採用 <strong>Event Sourcing</strong> 作為自然的領域機制以滿足合規。</p>
</li>
<li><p><strong>總結</strong>：一路以 DDD 為核心，依需求與風險適時引入合適的架構影響，支撐快速成長與變更。</p>
</li>
</ul>
<h1 id="heading-service-oriented">Service Oriented</h1>
<p><strong>服務導向架構（SOA）對不同的人有不同的意義，這使得相關的討論往往充滿挑戰。最好的方式是先找到一些共同基礎，或者至少定義出本次討論的範圍。依照 <em>Thomas Erl</em> 的定義，服務除了必須具備互通性</strong>外，還應符合 <strong>八項設計原則。</strong></p>
<ol>
<li><p><strong>Service Contract（服務契約）</strong></p>
<p> <strong>原則</strong>：服務要清楚表達自己的目的與能力，並以契約（通常是描述文件或 API 定義）來規範。</p>
<p> <strong>例子</strong>：</p>
<ul>
<li><p>假設有一個「線上支付服務」，它的 API 契約會定義：</p>
<ul>
<li><p>輸入：信用卡號、金額、貨幣</p>
</li>
<li><p>輸出：交易成功或失敗的訊息</p>
</li>
<li><p>限制：金額上限、支援幣別<br />  這樣，任何人使用此服務前，都能明確知道怎麼使用，而不必了解服務的內部實作。</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Service Loose Coupling（服務低耦合）</strong></p>
<p> <strong>原則</strong>：服務之間應該盡量減少依賴，只需知道彼此的存在即可。<br /> <strong>例子</strong>：</p>
<ul>
<li><p>「會員服務」不需要知道「購物車服務」的內部資料庫結構，它只需要透過 API 要到「會員資訊」。</p>
</li>
<li><p>如果日後「購物車服務」改用另一種資料庫，「會員服務」完全不用修改。</p>
</li>
</ul>
</li>
<li><p><strong>Service Abstraction</strong>（服務抽象化）</p>
<p> <strong>原則</strong>：服務只公開契約（API/RPC/Protocol），隱藏內部邏輯與技術細節。<br /> <strong>例子</strong>：</p>
<ul>
<li><p>使用「天氣查詢服務」時，只要調用 API 就能得到今天的氣溫。</p>
</li>
<li><p>使用者不需要知道它是抓氣象局資料，還是透過 AI 預測，只要能拿到正確結果即可。</p>
</li>
</ul>
</li>
<li><p><strong>Service Reusability</strong>（服務可重用性）</p>
<p> <strong>原則</strong>：服務設計應能被多方使用，而不是只針對單一應用場景。<br /> <strong>例子</strong>：</p>
<ul>
<li><p>「寄送 Email 的服務」可被：</p>
<ul>
<li><p>訂單服務用來寄送「訂單確認信」</p>
</li>
<li><p>行銷服務用來寄送「廣告電子報」</p>
</li>
<li><p>系統通知用來寄送「忘記密碼信」<br />  同一個服務被重複利用，減少開發與維護成本。</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Service Autonomy</strong>（服務自主性）</p>
<p> <strong>原則</strong>：服務應能掌控自己的資源與運行環境，獨立可靠。<br /> <strong>例子</strong>：</p>
<ul>
<li><p>「圖片上傳服務」自己有獨立的伺服器與儲存空間，無需依賴「會員服務」的資料庫。</p>
</li>
<li><p>即使會員系統故障，用戶仍能正常上傳圖片。</p>
</li>
</ul>
</li>
<li><p><strong>Service Statelessness</strong>（服務無狀態性）</p>
<p> <strong>原則</strong>：服務本身不應該保存狀態，狀態交由使用者或外部系統管理。<br /> <strong>例子</strong>：</p>
<ul>
<li><p>「訂單查詢服務」每次請求時，必須附上「訂單 ID」，而不是依賴服務端記住用戶的上一次請求。</p>
</li>
<li><p>這樣可以讓服務更容易擴展（加機器分流），因為不用保存用戶的上下文。</p>
</li>
</ul>
</li>
<li><p><strong>Service Discoverability</strong>（服務可發現性）</p>
<p> <strong>原則</strong>：服務應有描述與標記（metadata），能讓其他人快速找到並理解它能做什麼。<br /> <strong>例子</strong>：</p>
<ul>
<li><p>公司內部有「服務登錄中心（Service Registry）」，開發者能快速查到有哪些服務，例如：</p>
<ul>
<li><p>「支付服務」：支援信用卡與電子錢包</p>
</li>
<li><p>「會員服務」：支援註冊、登入、會員升級</p>
</li>
</ul>
</li>
<li><p>新團隊就能直接使用，不必自己重造輪子。</p>
</li>
</ul>
</li>
<li><p><strong>Service Composability（服務可組合性）</strong></p>
<p> <strong>原則</strong>：服務應能像積木一樣被組合，形成更大型的服務。<br /> <strong>例子</strong>：</p>
<ul>
<li><p>「旅遊預訂服務」可以組合以下子服務：</p>
<ul>
<li><p>「航班查詢服務」</p>
</li>
<li><p>「飯店預訂服務」</p>
</li>
<li><p>「支付服務」<br />  這樣能快速打造一個複雜的商業應用。</p>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<p>這八個原則的核心思想是：</p>
<ul>
<li><p><strong>清楚定義（契約、抽象化）</strong></p>
</li>
<li><p><strong>減少依賴（低耦合、無狀態、自主性）</strong></p>
</li>
<li><p><strong>最大化價值（可重用、可發現、可組合）</strong></p>
</li>
</ul>
<p>我們可以將這些原則與 <strong>六邊形架構（Hexagonal Architecture）</strong> 結合，將服務邊界放在最左側，而將 <strong>領域模型（Domain Model）</strong> 置於核心。圖 4.5 展示了這種基本架構：使用者可透過 <strong>REST、SOAP 或訊息傳遞（Messaging）</strong> 來存取服務。請注意，一個基於六邊形的系統可以同時支援多種技術服務端點，這也會影響 DDD 在 SOA 內的應用方式。</p>
<p><img src="https://learning.oreilly.com/api/v2/epubs/urn:orm:book:9780133039900/files/graphics/04fig05.jpg" alt="Image" /></p>
<ol>
<li><h3 id="heading-domain-model"><strong>核心領域模型（Domain Model）</strong></h3>
</li>
</ol>
<ul>
<li><p>在圖中央，可以看到 <strong>Domain Model</strong> 與 <strong>Application</strong>。</p>
</li>
<li><p>這是系統的業務核心，所有規則與邏輯都在這裡，與外部技術無關。<br />  👉 對應原則：<strong>Service Abstraction（服務抽象化）</strong></p>
</li>
</ul>
<ol start="2">
<li><h3 id="heading-t-services-adapters"><strong>技術服務端點（T-Services Adapters）</strong></h3>
<ul>
<li><p>左側看到三種 <strong>技術服務適配器（Adapters）</strong>：</p>
<ul>
<li><p><strong>REST Adapter</strong></p>
</li>
<li><p><strong>SOAP Adapter</strong></p>
</li>
<li><p><strong>Messaging Adapter</strong></p>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<p>    這代表一個應用可以同時支援多種協定或技術，服務使用者（Clients, C）可以自由選擇方式存取服務。<br />    👉 對應原則：</p>
<ul>
<li><p><strong>Service Contract（服務契約）</strong>：每個 Adapter 提供清晰 API 規格。</p>
</li>
<li><p><strong>Service Loose Coupling（低耦合）</strong>：REST 使用者不需要知道 SOAP 的存在。</p>
</li>
</ul>
<ol start="3">
<li><h3 id="heading-services-registry"><strong>服務登錄中心（Services Registry）</strong></h3>
<ul>
<li><p>左上角的 <strong>Services Registry</strong>，用來存放服務的描述與資訊（metadata）。</p>
</li>
<li><p>使用者可以查詢有哪些服務可用，並了解它們的契約。<br />  👉 對應原則：<strong>Service Discoverability（服務可發現性）</strong></p>
<ul>
<li>讓服務成為「可尋找、可理解、可重用的資產」。</li>
</ul>
</li>
</ul>
</li>
</ol>
<ol start="4">
<li><p>外部資源整合（右側 Adapters）</p>
<ul>
<li><p>右側的 <strong>Adapter E, F, G</strong>，以及 <strong>Mem</strong>，表示服務可透過不同 Adapter 存取外部資源，例如：</p>
<ul>
<li><p>資料庫</p>
</li>
<li><p>外部 API</p>
</li>
<li><p>記憶體快取</p>
</li>
</ul>
</li>
<li><p>這些整合點不影響核心 Domain Model，因為它們被封裝在 Adapter 裡。<br />  👉 對應原則：</p>
</li>
<li><p><strong>Service Autonomy（自主性）</strong>：服務掌控自己的依賴資源。</p>
</li>
<li><p><strong>Service Reusability（可重用性）</strong>：不同服務可以共用相同 Adapter（例如資料庫連線）。</p>
</li>
</ul>
</li>
<li><h3 id="heading-composability"><strong>服務的組合（Composability）</strong></h3>
<ul>
<li><p>圖裡的 REST / SOAP / Messaging 都是不同的「技術服務」，但它們都代表同一個「業務服務」。</p>
</li>
<li><p>在更大範圍內，這些服務可以進一步被組合，形成更高階的商業應用。<br />  👉 對應原則：<strong>Service Composability（可組合性）</strong></p>
</li>
</ul>
</li>
</ol>
<ol start="6">
<li><h3 id="heading-statelessness"><strong>無狀態性（Statelessness）</strong></h3>
<ul>
<li><p>REST、SOAP、Messaging 的請求都必須包含完整資訊，讓服務能獨立處理。</p>
</li>
<li><p>例如查詢訂單時，請求必須帶上「訂單 ID」，服務不會記住你上一次查了什麼。<br />  👉 對應原則：<strong>Service Statelessness（無狀態性）</strong></p>
</li>
</ul>
</li>
</ol>
<h1 id="heading-44cmkirpm7vllyblubplj7aqkuoajewbmus4gowaiyoq56uv5yiw56uvkirnmotlr6bli5nmoyjkvos">「<strong>電商平台</strong>」做一個<strong>端到端</strong>的實務案例</h1>
<p>角色：<strong>會員</strong>、<strong>商品</strong>、<strong>訂單</strong>、<strong>支付</strong>、<strong>出貨</strong>、<strong>通知</strong>、<strong>服務註冊中心</strong>、<strong>快取/資料庫</strong></p>
<h2 id="heading-5lia44cb5ac05pmv5pwy6lw77yi5b6e5lil5zau5yiw5yiw6lko77yj">一、場景敘述（從下單到到貨）</h2>
<ol>
<li><p>使用者在前台挑選商品 → 呼叫 <strong>商品服務</strong> 查價格與庫存（REST）。</p>
</li>
<li><p>按「下單」 → <strong>訂單服務</strong> 建立訂單（REST），並發出 <code>OrderCreated</code> 事件（Messaging）。</p>
</li>
<li><p><strong>支付服務</strong> 接到事件後執行扣款（可用 REST 或第三方 SOAP 介面），成功後發 <code>PaymentCaptured</code>。</p>
</li>
<li><p><strong>出貨服務</strong> 監聽到 <code>PaymentCaptured</code> → 鎖定庫存、產生配送單 → 發 <code>ShipmentCreated</code>。</p>
</li>
<li><p><strong>通知服務</strong> 監聽各事件，寄 Email/SMS（REST 介接寄信供應商）。</p>
</li>
<li><p><strong>會員服務</strong> 可查到使用者的歷史訂單（REST），但不依賴訂單內部實作。</p>
</li>
<li><p><strong>服務註冊中心</strong> 提供各服務契約與位址，方便新團隊接入（Discoverability）。</p>
</li>
</ol>
<h3 id="heading-1-service-contract">1) Service Contract（服務契約）</h3>
<p>每個服務在註冊中心都有清楚的 API/事件契約。</p>
<ul>
<li><strong>訂單服務 REST 契約（摘要）</strong><br />  <code>POST /orders</code> 建立訂單<br />  Request</li>
</ul>
<pre><code class="lang-python">{ <span class="hljs-string">"customerId"</span>:<span class="hljs-string">"C-1024"</span>, <span class="hljs-string">"items"</span>:[{<span class="hljs-string">"sku"</span>:<span class="hljs-string">"SKU-9"</span>,<span class="hljs-string">"qty"</span>:<span class="hljs-number">2</span>}], <span class="hljs-string">"coupon"</span>:<span class="hljs-string">"SPRING20"</span> }
</code></pre>
<p>Response</p>
<pre><code class="lang-python">{ <span class="hljs-string">"orderId"</span>:<span class="hljs-string">"O-9001"</span>, <span class="hljs-string">"status"</span>:<span class="hljs-string">"PENDING_PAYMENT"</span>, <span class="hljs-string">"total"</span>: <span class="hljs-number">1280</span> }
</code></pre>
<p><strong>事件契約</strong><br />Topic: <code>commerce.orders</code><br /><code>OrderCreated</code>：</p>
<pre><code class="lang-python">{ <span class="hljs-string">"orderId"</span>:<span class="hljs-string">"O-9001"</span>, <span class="hljs-string">"customerId"</span>:<span class="hljs-string">"C-1024"</span>, <span class="hljs-string">"total"</span>:<span class="hljs-number">1280</span>, <span class="hljs-string">"occurredAt"</span>:<span class="hljs-string">"2025-09-30T10:02:11Z"</span> }
</code></pre>
<h3 id="heading-2-service-loose-coupling">2) Service Loose Coupling（低耦合）</h3>
<ul>
<li><p>訂單服務<strong>只知道</strong>「支付服務的契約」和「事件主題」，不知道支付內部是否接 Stripe、Line Pay 或銀行 SOAP。</p>
</li>
<li><p>出貨服務透過事件觸發，不直接呼叫訂單的資料庫或私有 API。</p>
</li>
</ul>
<h3 id="heading-3-service-abstraction">3) Service Abstraction（抽象化）</h3>
<ul>
<li><p><strong>支付服務</strong> 封裝不同金流供應商（REST/SOAP），對外只暴露：</p>
<ul>
<li><p><code>POST /payments/capture</code></p>
</li>
<li><p><code>GET /payments/{id}</code><br />  內部如何重試、如何對帳，對其他服務是不可見的。</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-4-service-reusability">4) Service Reusability（可重用性）</h3>
<ul>
<li><p><strong>通知服務</strong> 被多方使用：下單成功寄信、付款成功簡訊、出貨提醒、重設密碼。</p>
</li>
<li><p><strong>會員服務</strong> 的「會員等級查詢」同時被行銷服務（做分眾推播）與訂單服務（計算折扣）重用。</p>
</li>
</ul>
<h3 id="heading-5-service-autonomy">5) Service Autonomy（自主性）</h3>
<ul>
<li><p>每個服務持有<strong>自己的資料庫</strong>與<strong>快取</strong>：</p>
<ul>
<li><p>訂單 DB 儲存訂單、明細；</p>
</li>
<li><p>支付 DB 儲存交易、對帳；</p>
</li>
<li><p>出貨 DB 儲存配送單與追蹤碼。</p>
</li>
</ul>
</li>
<li><p>任一服務掛掉，不會把整個系統拖垮（例如通知延遲不影響下單流程；可用事件堆積 &amp; 重試）。</p>
</li>
</ul>
<h3 id="heading-6-service-statelessness">6) Service Statelessness（無狀態性）</h3>
<ul>
<li><p>REST 請求必帶必要上下文（如 <code>Authorization</code>、<code>orderId</code>），服務<strong>不記</strong>用戶會話。</p>
</li>
<li><p>橫向擴充時，請求可被任何一台實例處理；狀態放 DB/快取/Message 中間件，而非應用記憶體。</p>
</li>
</ul>
<h3 id="heading-7-service-discoverability">7) Service Discoverability（可發現性）</h3>
<ul>
<li><p><strong>服務註冊中心</strong>/API 入口網站（Dev Portal）提供：</p>
<ul>
<li><p>Swagger / OpenAPI、AsyncAPI（事件）</p>
</li>
<li><p>範例請求/回應、錯誤碼表、速率限制</p>
</li>
<li><p>版本/生命週期標示（如：<code>/v1</code> 穩定，<code>/v2</code> 測試中）</p>
</li>
</ul>
</li>
<li><p>新團隊（例如「發票服務」）能快速找到 <code>PaymentCaptured</code> 事件並訂閱。</p>
</li>
</ul>
<h3 id="heading-8-service-composability">8) Service Composability（可組合性）</h3>
<ul>
<li><p>「<strong>結帳服務</strong>」其實是一個<strong>流程編排</strong>：</p>
<ol>
<li><p>呼叫訂單服務建單</p>
</li>
<li><p>呼叫支付服務扣款</p>
</li>
<li><p>等待 <code>PaymentCaptured</code> 事件</p>
</li>
<li><p>呼叫/觸發出貨服務</p>
</li>
<li><p>呼叫通知服務送信</p>
</li>
</ol>
</li>
<li><p>同一組服務也可被「<strong>市集平台</strong>」重用（只換前端 App 或 BFF），或被「<strong>門市 POS</strong>」以 Messaging 介接。</p>
</li>
</ul>
<p>由於 SOA 的價值和定義眾說紛紜，你可能會不同意這裡的觀點。Martin Fowler 把這種情況稱為「[<strong>服務導向的模糊性（service-oriented ambiguity）](</strong><a target="_blank" href="https://martinfowler.com/bliki/ServiceOrientedAmbiguity.html">https://martinfowler.com/bliki/ServiceOrientedAmbiguity.html</a>)」。因此，我不會嘗試去嚴格消除 SOA 的歧義，而是提供一個觀點，說明 DDD 如何與 <strong>SOA 宣言（SOA Manifesto）</strong> 的優先事項相契合。</p>
<p>雖然 SOA 宣言本身曾受到不少批評，但我們仍能從中獲得一些價值。Manifesto 的撰寫者之一 Stefan Tilkov 提出了務實的看法：</p>
<blockquote>
<p>「[SOA 宣言] 讓我可以把服務視為一組 <strong>SOAP/WSDL 介面</strong>，或是一組 <strong>RESTful 資源</strong>。這並不是嚴格的定義，而是嘗試找出大家都能同意的價值與原則。」</p>
</blockquote>
<h1 id="heading-representational-state-transferrest">Representational State Transfer—REST</h1>
<h3 id="heading-rest">REST 作為一種架構風格</h3>
<ul>
<li><p><strong>架構風格（Architectural Style）</strong> 就像是架構領域的設計模式：它抽象出多種實作的共同點，讓人能比較不同架構的優劣。</p>
</li>
<li><p>Fielding 在論文裡，先介紹了分散式系統的幾種風格（例如 client-server、distributed objects），然後定義了每種風格的限制（constraints）。</p>
</li>
<li><p>REST 就是其中一種風格，專門描述 <strong>Web 架構應該遵守的原則</strong>。</p>
</li>
</ul>
<h3 id="heading-rest-1">REST 的關鍵概念</h3>
<ol>
<li><p><strong>資源（Resource）</strong></p>
<ul>
<li><p>系統必須定義哪些東西要對外暴露（例如：客戶、產品、訂單、搜尋結果）。就像 Class 的 <strong>public</strong> &amp; private</p>
</li>
<li><p>每個資源要有唯一 URI，可透過不同表現形式（JSON、XML、HTML）對外呈現。就像可以是 Public Function with Void、Public Function with arguments… Public Const Static function… ( URI == Function Name, 表現形式 == 方法簽章或返回的內容)</p>
</li>
</ul>
</li>
<li><p><strong>無狀態性（Stateless Communication）</strong></p>
<ul>
<li><p>每個 HTTP 請求必須自包含處理所需資訊（例如授權、請求參數）。</p>
</li>
<li><p>不依賴伺服器「記住」用戶的上下文（session）。</p>
</li>
<li><p>有助於擴展性（scalability）。</p>
</li>
</ul>
</li>
<li><p><strong>統一介面（Uniform Interface）</strong></p>
<ul>
<li><p>所有資源共用相同操作方法（HTTP verbs：GET、POST、PUT、DELETE）。</p>
</li>
<li><p>注意：這些方法 <strong>不是等同 CRUD</strong>，例如 POST 可以用來觸發動作。</p>
</li>
</ul>
</li>
<li><p><strong>安全性</strong> safety <strong>與冪等性</strong> Idempotency</p>
<ul>
<li><p>GET 是「安全操作」，只讀取資料，不應產生副作用，可快取。</p>
</li>
<li><p>PUT、DELETE、GET 是「冪等（idempotent）」：同樣的請求執行多次，結果相同。</p>
</li>
</ul>
</li>
<li><p><strong>HATEOAS（Hypermedia as the Engine of Application State）</strong></p>
<ul>
<li><p>回應中要包含可導航的連結，讓客戶端能透過資源之間的關聯找到操作路徑。</p>
</li>
<li><p>就像 Web 瀏覽器點擊超連結一樣，客戶端能自發探索。</p>
<p>  例子：</p>
</li>
<li><p>初始入口：<br />  <code>GET</code> <a target="_blank" href="https://api.shop.com/%EF%BF%BC%E5%9B%9E%E6%87%89%EF%BC%9A"><code>https://api.shop.com/</code><br />  回應：</a></p>
</li>
</ul>
</li>
</ol>
<pre><code class="lang-json">{
  <span class="hljs-attr">"_links"</span>: {
    <span class="hljs-attr">"products"</span>: {<span class="hljs-attr">"href"</span>: <span class="hljs-string">"/products"</span>},
    <span class="hljs-attr">"orders"</span>: {<span class="hljs-attr">"href"</span>: <span class="hljs-string">"/orders"</span>},
    <span class="hljs-attr">"profile"</span>: {<span class="hljs-attr">"href"</span>: <span class="hljs-string">"/users/me"</span>}
  }
}
</code></pre>
<p>客戶端只需要知道一個 Root URI，其餘由 <code>_links</code> 告訴它下一步可以去哪裡。</p>
<p>例子2: 如果訂單尚未付款：</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"orderId"</span>: <span class="hljs-string">"O-1001"</span>,
  <span class="hljs-attr">"status"</span>: <span class="hljs-string">"PENDING_PAYMENT"</span>,
  <span class="hljs-attr">"_links"</span>: {
    <span class="hljs-attr">"pay"</span>: {<span class="hljs-attr">"href"</span>: <span class="hljs-string">"/orders/O-1001/payment"</span>}
  }
}
</code></pre>
<p>如果訂單已出貨：</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"orderId"</span>: <span class="hljs-string">"O-1001"</span>,
  <span class="hljs-attr">"status"</span>: <span class="hljs-string">"SHIPPED"</span>,
  <span class="hljs-attr">"_links"</span>: {
    <span class="hljs-attr">"track"</span>: {<span class="hljs-attr">"href"</span>: <span class="hljs-string">"/shipments/S-2222"</span>},
    <span class="hljs-attr">"invoice"</span>: {<span class="hljs-attr">"href"</span>: <span class="hljs-string">"/orders/O-1001/invoice"</span>}
  }
}
</code></pre>
<p>➡️ 客戶端不需要硬編「如果狀態是 PENDING 就去 /payment」，而是依伺服器給的 <code>_links</code> 來行動。</p>
<h3 id="heading-rest-ddd">REST 與 DDD 的關係</h3>
<ul>
<li><p><strong>不要直接把領域模型（Domain Model）暴露為 REST API</strong>：</p>
<ul>
<li>因為模型的改動會直接反映到 API，導致脆弱。</li>
</ul>
</li>
<li><p>更佳的方式有兩種：</p>
<ol>
<li><p><strong>分離 Bounded Context</strong>：</p>
<ul>
<li><p>系統的 API 介面層（用例導向）與核心領域模型分離。</p>
</li>
<li><p>API 的資源模型來自領域模型，但不等於領域模型。</p>
</li>
</ul>
</li>
<li><p><strong>使用標準媒體型別</strong>：</p>
<ul>
<li><p>例如行事曆使用 <code>ical</code>，其模型跨系統共用。</p>
</li>
<li><p>這相當於 DDD 的「共享核心（Shared Kernel）」或「發佈語言（Published Language）」。</p>
</li>
</ul>
</li>
</ol>
</li>
</ul>
<h3 id="heading-rest-2">為什麼選 REST？</h3>
<ul>
<li><p><strong>鬆耦合（Loose Coupling）</strong>：容易增加新資源，不會破壞現有客戶端。</p>
</li>
<li><p><strong>可擴展性</strong>：利用 HTTP 快取、URI 重寫等既有機制。</p>
</li>
<li><p><strong>可理解性</strong>：每個資源都是獨立的入口，容易測試與調試。</p>
</li>
<li><p><strong>高成熟度的生態</strong>：HTTP 工具、伺服器、快取已經非常成熟。</p>
</li>
</ul>
<h1 id="heading-cqrs">CQRS</h1>
<p>在複雜的領域模型中，當使用者介面需要顯示跨越多個聚合（Aggregate）型別與實例的資料時，單純透過 Repository 查詢往往很困難。隨著領域越複雜，這種情況越常發生。</p>
<p>如果僅靠 Repository，解法通常不理想：</p>
<ul>
<li><p>客戶端可能需要呼叫多個 Repository，取出不同 Aggregate，然後自行組裝成 DTO。</p>
</li>
<li><p>或者我們可以在 Repository 中設計特製的 finder 方法，讓它能透過一個查詢找齊分散的資料。</p>
</li>
<li><p>如果這些方式都不合適，那麼可能會犧牲使用者體驗，把 UI 強行綁在聚合的邊界上，但長遠來看，這種僵化的介面設計並不理想。</p>
</li>
</ul>
<p>有沒有一種完全不同的方式，能更好地把領域資料映射到 UI？答案就是 <strong>CQRS</strong>。它是將 Bertrand Meyer 的 <strong>命令-查詢分離原則（CQS）</strong> 推廣到架構層級的一種模式。</p>
<p>CQS 的原則是：</p>
<ul>
<li><p>每個方法要麼是命令（修改狀態，不回傳值），要麼是查詢（回傳值，不改變狀態），不能兩者兼具。</p>
</li>
<li><p>例如：在 Java/C# 中，命令方法宣告為 <code>void</code>，查詢方法回傳值且不能引發任何狀態變化。</p>
</li>
</ul>
<p>在典型的 DDD 模型（限界上下文）中，我們會看到：</p>
<ul>
<li><p>聚合既有命令方法也有查詢方法。</p>
</li>
<li><p>Repository 除了 <code>add()</code>、<code>save()</code> 之外，還有各種 finder 查詢。</p>
</li>
</ul>
<p><strong>CQRS 的做法</strong>：</p>
<ul>
<li><p>把命令與查詢責任完全分開。</p>
</li>
<li><p><strong>命令模型（Command Model）</strong>：只保留命令方法與最基本的查詢（<code>fromId()</code>）。聚合不再有 getters，Repository 不再有複雜查詢。</p>
</li>
<li><p><strong>查詢模型（Query Model）</strong>：專門用來優化查詢，直接面向使用者介面或報表需求。</p>
</li>
</ul>
<p>因此，傳統單一的領域模型會被拆成兩部分：</p>
<ul>
<li><p><strong>命令模型（Write Model）</strong> 存在一個資料庫。</p>
</li>
<li><p><strong>查詢模型（Read Model）</strong> 存在另一個資料庫。</p>
</li>
</ul>
<p>最終形成如圖所示的結構：</p>
<ul>
<li><p><strong>命令 Command</strong> 從客戶端傳到命令模型。</p>
</li>
<li><p><strong>查詢</strong> <strong>Query</strong> 直接針對查詢模型執行，通常經過最佳化，結果用於 UI 或報表。</p>
</li>
</ul>
<p><img src="https://learning.oreilly.com/api/v2/epubs/urn:orm:book:9780133039900/files/graphics/04fig06.jpg" alt="Image" /></p>
<p>有人可能覺得這樣很麻煩，增加了額外複雜度。<br />但要注意，<strong>CQRS 是針對特定情境的解法</strong>：當 UI 的查詢需求高度複雜時，CQRS 的價值才會凸顯。它並不是一種「潮流」或炫技，而是用來解決複雜查詢場景的架構模式。</p>
<p>另外，CQRS 裡：</p>
<ul>
<li><p>Query Model 也稱為 <strong>Read Model</strong>。</p>
</li>
<li><p>Command Model 也稱為 <strong>Write Model</strong>。</p>
</li>
</ul>
<h2 id="heading-client-query-processor">Client 和 Query Processor</h2>
<p>客戶端（圖的最左側）可能是網頁瀏覽器或桌面 UI。它透過伺服器上的 <strong>Query Processor（查詢處理器）</strong> 來存取資料。圖中沒有顯示伺服器層級的分層，因為 Query Processor 本身只是簡單元件，負責執行基本查詢，例如針對 SQL 資料庫。</p>
<p>這個元件不包含複雜邏輯：它執行查詢，必要時把結果序列化成可傳輸格式（例如 DTO、XML、JSON）。若是客戶端能直接讀取資料集（如 JDBC），甚至不需要序列化。不過，使用 Query Processor 來做連線池仍是較佳選擇，可以避免每個客戶端都需要昂貴的 DB 授權。</p>
<h3 id="heading-query-model-read-model">Query Model（或 Read Model）</h3>
<p><strong>查詢模型</strong> 是一種 <strong>非正規化（denormalized）資料模型</strong>。</p>
<ul>
<li><p>它不是用來承載領域行為，而是 <strong>針對顯示與報表做最佳化</strong>。</p>
</li>
<li><p>在 SQL 中，一張表可以對應一個 UI 的畫面。</p>
</li>
<li><p>表可以有很多欄位，甚至超過單一畫面需要的資料，以便不同角色（一般用戶、管理員、主管）透過不同的 table view 存取到該角色能看到的資料。</p>
</li>
</ul>
<p>這些視圖（views）可以很便宜、很快生成，也可以隨需求刪除重建。若結合 <strong>Event Sourcing</strong>，則可以重新播放歷史事件來生成新的查詢模型，甚至換一種完全不同的儲存技術。</p>
<p>例如： 普通用戶查詢 <code>vw_usr_product</code> 視圖，而管理員則查詢 <code>vw_mgr_product</code>，能看到更多資訊。</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> vw_usr_product <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = ?
</code></pre>
<h3 id="heading-client-command">Client 驅動的 Command 提交</h3>
<p>UI 客戶端會透過命令（Command）來執行系統行為。</p>
<ul>
<li><p>Command 包含要執行的行為名稱與必要參數（相當於序列化的 method invocation）。</p>
</li>
<li><p>UI 設計上需幫助使用者輸入正確的資料，形成正確的命令封包。</p>
</li>
<li><p>這裡最適合「任務驅動（inductive）」的 UI 設計，讓使用者只看到能執行的動作，避免誤操作。</p>
</li>
</ul>
<h3 id="heading-command-processors">Command Processors（命令處理器）</h3>
<p>命令由 <strong>Command Handler / Processor</strong> 接收並處理，常見有三種風格：</p>
<ol>
<li><p><strong>分類式（Categorized）</strong>：一個 Application Service 處理一類命令，每個命令對應一個方法 → 簡單、易維護。</p>
</li>
<li><p><strong>專用式（Dedicated）</strong>：一個處理器只處理一個命令 → 單一職責、可獨立部署、易於水平擴展。</p>
</li>
<li><p><strong>訊息式（Messaging）</strong>：命令以異步訊息傳遞 → 支援更高擴展性與容錯，但設計更複雜。</p>
</li>
</ol>
<p><strong>處理步驟</strong>：</p>
<ul>
<li><p>從 Repository 拿出 Aggregate</p>
</li>
<li><p>執行對應的命令方法（例如 commit backlog item to sprint）</p>
</li>
<li><p>更新完成後，發佈 <strong>Domain Event</strong>，供查詢模型更新</p>
</li>
</ul>
<h3 id="heading-command-model-write-model">Command Model（或 Write Model）</h3>
<p>命令模型的重點是執行業務行為，並在完成後 <strong>發佈領域事件（Domain Event）</strong>。</p>
<ul>
<li><p>例如：<code>BacklogItem.commitTo(Sprint)</code> → 發佈 <code>BacklogItemCommitted</code> 事件。</p>
</li>
<li><p>發佈機制基於 <strong>Observer 模式</strong>。</p>
</li>
<li><p>事件用於：</p>
<ul>
<li><p>更新查詢模型</p>
</li>
<li><p>或（若採用事件溯源）用來重建 Aggregate 狀態</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-event-subscriber-query-model">Event Subscriber 更新 Query Model</h3>
<p>專門的 <strong>事件訂閱者（Event Subscriber）</strong> 會監聽命令模型發佈的所有事件，用來更新查詢模型。</p>
<ul>
<li><p>每個事件需要包含足夠資訊，讓查詢模型能正確更新。</p>
</li>
<li><p>更新可以：</p>
<ul>
<li><p><strong>同步（synchronous）</strong>：在同一交易中同時更新 Command Model 與 Query Model → 保證一致，但成本較高。</p>
</li>
<li><p><strong>非同步（asynchronous）</strong>：透過事件佇列稍後更新 → 可擴展，但會導致 <strong>最終一致性</strong>（eventual consistency）。</p>
</li>
</ul>
</li>
</ul>
<p>當新增新的 UI 視圖時：</p>
<ul>
<li><p>如果使用事件溯源，可以重播舊事件來建構新表。</p>
</li>
<li><p>如果不是事件溯源，可以用 ETL 從 Command Model Store 匯出資料，轉換後載入 Query Model Store。</p>
</li>
</ul>
<h2 id="heading-eventually-consistent-query-model">Eventually Consistent Query Model</h2>
<p>如果查詢模型設計成 <strong>最終一致性</strong>（也就是在寫入命令模型後，查詢模型是非同步更新），那麼使用者介面就會遇到一些問題。</p>
<p>例如：使用者提交一個命令後，下個畫面是否會立即顯示更新後的資料？答案是不一定，可能取決於系統負載或其他因素。但我們最好假設最壞情況：<strong>UI 永遠不會即時一致</strong>，然後基於這個前提來設計。</p>
<h3 id="heading-ui">UI 設計技巧</h3>
<p>一個方法是：<strong>UI 暫時顯示剛剛提交的命令參數</strong>，也就是直接呈現使用者剛輸入的資料。雖然這有點「取巧」，但它能保證使用者看到的是「將來最終會一致」的結果，而不是完全過時的查詢數據。</p>
<p>但如果這方法不實用，或在多人同時操作時，其他使用者仍然會看到過期的舊資料（ 例如排行榜、股票的 orderbook …），該怎麼辦？</p>
<h3 id="heading-5oqa6kgt6kej5rov6iih5oqy6kg3">技術解法與折衷</h3>
<ul>
<li><p><strong>顯示資料時間戳</strong>：<br />  每筆 Query Model 的資料都保留最後更新時間，UI 明確顯示「資料最後更新於 XX:XX:XX」。</p>
<ul>
<li><p>優點：使用者知道資料新鮮度，可以自行判斷是否要刷新。</p>
</li>
<li><p>缺點：有些人覺得這是有效模式，有些人覺得這只是「補丁」。最好先做使用者測試。</p>
</li>
</ul>
</li>
<li><p><strong>其他方式</strong>：</p>
<ul>
<li><p><strong>Comet / Ajax Push</strong>（伺服器主動推送更新）</p>
</li>
<li><p><strong>Observer 或事件訂閱</strong>（例如分散式快取、事件網格 GemFire/Coherence）</p>
</li>
<li><p><strong>UI 提示延遲</strong>（告訴使用者「請求已接受，處理需要一些時間」）</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-cqrs-1">謹慎使用 CQRS</h3>
<p>和所有模式一樣，CQRS 帶來多種<strong>權衡</strong>。</p>
<ul>
<li><p>如果 UI 不複雜，也不需要跨多個 Aggregate 查詢，那麼引入 CQRS 可能只是 <strong>額外的偶發性複雜度（Accidental Complexity）</strong>。</p>
</li>
<li><p>只有在複雜度或風險足以導致失敗時，CQRS 才是必要的解法。</p>
</li>
</ul>
<h2 id="heading-cqrs-2">🔑 CQRS 的核心流程</h2>
<pre><code class="lang-mermaid">flowchart LR
    subgraph Client["🖥️ Client (UI/Web/Service)"]
        C1[發送 Command]
        Q1[發送 Query]
    end

    subgraph Server["⚙️ Server 層"]
        CP[Command Processor - Application Service]
        CM[Command Model - Aggregates]
        EV[Domain Event Publisher]
        ES[Event Subscriber]
        QP[Query Processor]
    end

    subgraph Stores["💾 資料儲存"]
        CMS[Command Model Store - 寫模型DB]
        QMS[Query Model Store - 讀模型DB]
    end

    %% 命令路徑
    C1 --&gt; CP --&gt; CM --&gt; CMS
    CM --&gt; EV --&gt; ES --&gt; QMS

    %% 查詢路徑
    Q1 --&gt; QP --&gt; QMS --&gt; Q1R[回傳 Query 結果]

    %% 標註樣式
    classDef write fill:#fde2e2,stroke:#e67c7c,stroke-width:2px;
    classDef read fill:#d6f5d6,stroke:#2eb82e,stroke-width:2px;

    C1,CP,CM,CMS,EV,ES,QMS:::write
    Q1,QP,QMS,Q1R:::read
</code></pre>
<ol>
<li><p><strong>查詢（Query）</strong></p>
<ul>
<li>Client → Query Processor → Query Model Store（快、專為 UI 最佳化）</li>
</ul>
</li>
<li><p><strong>命令（Command）</strong></p>
<ul>
<li>Client → Command Processor → Command Model（聚合更新） → 發佈事件</li>
</ul>
</li>
<li><p><strong>事件（Event）</strong></p>
<ul>
<li>Event Subscriber 接收事件 → 更新 Query Model（同步或非同步）</li>
</ul>
</li>
</ol>
<hr />
<h2 id="heading-cqrs-3">⚖️ CQRS 的取捨</h2>
<ul>
<li><p><strong>優點</strong>：</p>
<ul>
<li><p>查詢快、靈活（Read Model 為 UI 量身打造）。</p>
</li>
<li><p>寫模型保持乾淨（聚合只專注於業務邏輯）。</p>
</li>
<li><p>可彈性支援多角色、多視圖需求。</p>
</li>
</ul>
</li>
<li><p><strong>缺點</strong>：</p>
<ul>
<li><p>增加複雜度（兩份模型、兩份資料存儲）。</p>
</li>
<li><p>可能會遇到 <strong>最終一致性</strong> 問題。</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-57at6amx5rov5ymh77ya5l2v5pmc55sols4jeeuqa">經驗法則：何時用/不用</h2>
<p><strong>適合：</strong></p>
<ul>
<li><p>一個畫面要整合多聚合、跨服務資料，且查詢複雜、量大、低延遲要求。</p>
</li>
<li><p>有多樣化報表/搜尋/排序/分析需求，讀寫負載比極端（讀多寫少或反之）。<br />  <strong>不適合：</strong></p>
</li>
<li><p>系統小、用例單純；單一聚合就能滿足查詢；團隊維運成本有限。</p>
</li>
</ul>
<h1 id="heading-event-driven-architecture">Event-Driven Architecture</h1>
<p><strong>事件驅動架構（Event-Driven Architecture, EDA）</strong>是一種促進事件的<strong>產生、偵測、消費與反應</strong>的軟體架構。</p>
<ul>
<li><p><strong>事件來源</strong>：可以是業務動作（例如「訂單已建立」）、系統監控（CPU 高負載）、基礎設施通知（節點擴容）等。</p>
</li>
<li><p><strong>核心價值</strong>：鬆耦合（Decoupling）。發布者不需要知道誰會接收，訂閱者只要對事件類型有興趣就能消費。</p>
</li>
</ul>
<p><strong>DA 的價值</strong>是它解耦了各系統之間的依賴，只保留了 <strong>訊息傳遞機制本身</strong>以及<strong>它們訂閱的事件類型</strong>。</p>
<p>下圖可以看到三角形的 client 與對應的三角形輸出機制，代表了 <strong>Bounded Context 使用的事件機制</strong>。</p>
<ul>
<li><p><strong>輸入事件</strong>（Incoming Events）會從一個專門的 Port 進來，這個 Port 與其他三個 client 使用的 Port 是不同的。</p>
</li>
<li><p><strong>輸出事件</strong>（Outgoing Events）同樣透過另一個不同的 Port 傳出。</p>
</li>
</ul>
<blockquote>
<p>這些獨立的 <strong>Port</strong> 可以代表不同的事件傳遞方式，例如 <strong>AMQP（RabbitMQ）</strong>，而不是一般更常見的 <strong>HTTP</strong>。無論實際採用何種事件傳遞機制，我們可以假設事件的<strong>進入</strong>與<strong>輸出</strong>皆是透過這些符號化的三角形來完成。</p>
</blockquote>
<p><img src="https://learning.oreilly.com/api/v2/epubs/urn:orm:book:9780133039900/files/graphics/04fig07.jpg" alt="Image" /></p>
<p>一個六邊形中可能有多種類型的事件進出，但我們特別關注的是 <strong>領域事件（Domain Events）</strong>。</p>
<ul>
<li><p>應用程式也可能訂閱系統事件、企業事件或其他型別的事件，例如：</p>
<ul>
<li><p>系統健康監控</p>
</li>
<li><p>記錄（logging）</p>
</li>
<li><p>動態資源配置（provisioning）</p>
</li>
</ul>
</li>
<li><p>但真正需要我們建模關注的，仍是領域事件，因為它們承載了業務語境中發生的「<strong>重要事情</strong>」。</p>
</li>
</ul>
<h2 id="heading-domain-events">Domain Events 的傳遞與意義</h2>
<p>某個系統透過輸出 Port 發佈的 <strong>領域事件</strong>，會被其他系統的輸入 Port 接收。</p>
<ul>
<li><p>不同 Bounded Context 接收到的事件，其意義可能不同，甚至完全無意義。</p>
</li>
<li><p>若某事件類型對特定 Context 有興趣，它的屬性會被轉換並適配到該應用程式的 API，進而觸發某個操作。</p>
</li>
<li><p>這個被執行的操作會再反映到領域模型，並依循其協議（protocol）進行。</p>
</li>
</ul>
<p>（註：若使用訊息過濾器或 routing key，訂閱者可以避免接收到對自己沒有意義的事件。）</p>
<h2 id="heading-5asa5q2l6amf5rwb56il6iih5oyr5oiw">多步驟流程與挑戰</h2>
<p>有時候，一個接收到的領域事件只是 <strong>多任務流程（multitask process）</strong> 的一部分。</p>
<ul>
<li><p>在所有預期的事件到齊之前，這個流程並不算完成。</p>
</li>
<li><p>那麼問題是：</p>
<ol>
<li><p>流程如何開始？</p>
</li>
<li><p>它如何分散在整個企業中？</p>
</li>
<li><p>我們又該如何追蹤進度直到流程完成？</p>
</li>
</ol>
</li>
</ul>
<h2 id="heading-pipes-and-filters">管線與過濾器（Pipes and Filters）</h2>
<p>最簡單的 <strong>Pipes and Filters</strong> 形式之一，就是在 shell/console 命令列使用：</p>
<pre><code class="lang-bash">$ cat phone_numbers.txt | grep 303 | wc -l
3
</code></pre>
<p>用 Linux bash，來找出在 <code>phone_numbers.txt</code> 這個個人資訊管理檔案中，有多少聯絡人是科羅拉多州的電話號碼（區碼 303）。<br />雖然這不是一個很可靠的方式來實作此需求，但它確實示範了 <strong>Pipes and Filters</strong> 的運作方式：</p>
<ol>
<li><p><strong>cat</strong>：將 <code>phone_numbers.txt</code> 的內容輸出到標準輸出（stdout）。通常這會連到 console，但當使用 <code>|</code> 時，輸出會被導向到下一個程式的輸入。</p>
</li>
<li><p><strong>grep</strong>：從標準輸入讀取（也就是 cat 的輸出），參數指定要比對包含字串 <code>303</code> 的行。每個符合的行會輸出到自己的 stdout，接著再被 pipe 到下一個程式。</p>
</li>
<li><p><strong>wc</strong>：從 stdin 讀取（也就是 grep 的輸出）。<code>-l</code> 參數告訴 wc 要數輸入的行數。最後結果是 <code>3</code>，因為 grep 輸出了三行。由於沒有再 pipe，這次的 stdout 會顯示到 console。</p>
</li>
</ol>
<h3 id="heading-5z65pys5qac5b1">基本概念</h3>
<p>在這個例子中：</p>
<ul>
<li><p>每個工具（cat、grep、wc）都 <strong>接收資料集 → 處理 → 輸出新的資料集</strong>。</p>
</li>
<li><p>每個工具的輸出和輸入都不同，因為它們扮演了 <strong>Filter</strong> 的角色。</p>
</li>
<li><p>最終輸出已經完全不同：從原始的聯絡人檔案，變成了一個數字 <code>3</code>。</p>
</li>
</ul>
<p>這就是 <strong>Pipes and Filters</strong> 的核心原理。</p>
<h3 id="heading-pipes-and-filters-1"><strong>Pipes and Filters 的基本特性</strong></h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>特性</td><td>說明</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Pipes 是訊息通道</strong></td><td>Filter（過濾器/處理器）會在輸入管道接收訊息，並將訊息送往輸出管道。Pipe 實際上就是一個訊息通道。</td></tr>
<tr>
<td><strong>Ports 連接 Filters 與 Pipes</strong></td><td>Filters 透過 Port 連接到輸入與輸出管道。這使得六邊形架構（Ports and Adapters）成為一個合適的總體風格。</td></tr>
<tr>
<td><strong>Filters 是處理器</strong></td><td>Filter 可能只處理訊息而不一定真的做「過濾」。</td></tr>
<tr>
<td><strong>獨立處理器</strong></td><td>每個 Filter 處理器是一個獨立元件，適當的元件粒度需透過良好的設計來達成。</td></tr>
<tr>
<td><strong>鬆耦合</strong></td><td>每個 Filter 處理器獨立組合在整體流程中，不依賴其他處理器。Filter 的組成方式可以透過設定定義。</td></tr>
<tr>
<td><strong>可替換性</strong></td><td>處理器接收訊息的順序可以根據需求重新安排，通常透過設定組合來完成。</td></tr>
<tr>
<td><strong>Filters 可以多管道處理</strong></td><td>不像命令列 Filter 僅能讀/寫單一管道，訊息 Filter 可以讀取或寫入多個管道，這意味著可以進行平行或並行處理。</td></tr>
<tr>
<td><strong>同類型 Filters 可平行使用</strong></td><td>最忙碌、可能最慢的 Filter，可以以多個實例平行部署來增加處理量。</td></tr>
</tbody>
</table>
</div><p>將這種 Pipeline 原理套用到 <strong>事件驅動架構</strong>的設計上。</p>
<p>假設我們把 <code>cat</code>、<code>grep</code>、<code>wc</code> 想像成 <strong>EDA 元件</strong>：</p>
<ol>
<li><p><strong>PhoneNumbersPublisher</strong></p>
<ul>
<li><p>從 <code>phone_numbers.txt</code> 讀取所有行</p>
</li>
<li><p>發佈事件 <code>AllPhoneNumbersListed</code>（包含所有資料）</p>
</li>
<li><p>管線開始</p>
</li>
</ul>
</li>
<li><p><strong>PhoneNumberFinder</strong>（第一個 Filter）</p>
<ul>
<li><p>訂閱 <code>AllPhoneNumbersListed</code>，接收事件</p>
</li>
<li><p>搜尋含有「303」的行</p>
</li>
<li><p>發佈事件 <code>PhoneNumbersMatched</code>，內容包含比對到的完整行</p>
</li>
</ul>
</li>
<li><p><strong>MatchedPhoneNumberCounter</strong>（第二個 Filter）</p>
<ul>
<li><p>訂閱 <code>PhoneNumbersMatched</code>，接收事件</p>
</li>
<li><p>計算電話號碼數量（例：3 筆）</p>
</li>
<li><p>發佈事件 <code>MatchedPhoneNumbersCounted</code>，屬性 count=3</p>
</li>
</ul>
</li>
<li><p><strong>PhoneNumberExecutive</strong>（終端處理器）</p>
<ul>
<li><p>訂閱 <code>MatchedPhoneNumbersCounted</code>，接收事件</p>
</li>
<li><p>負責紀錄 log，例如：<code>3 phone numbers matched on July 15, 2012 at 11:15 PM</code></p>
</li>
</ul>
</li>
</ol>
<p><img src="https://learning.oreilly.com/api/v2/epubs/urn:orm:book:9780133039900/files/graphics/04fig08.jpg" alt="Image" /></p>
<h2 id="heading-5b2i5ocn6iih6zmq5yi2">彈性與限制</h2>
<p>這種管線是 <strong>相對靈活的</strong>：</p>
<ul>
<li><p>如果要加新的 Filter，只需建立新的事件並調整訂閱關係。</p>
</li>
<li><p>必須設定小心地改變管線的順序。</p>
</li>
<li><p>但不像命令列一樣簡單，通常事件管線不會頻繁修改。</p>
</li>
</ul>
<p>在真正的企業系統中，我們會用這種模式把大問題分解成更小的步驟，使 <strong>分散式處理</strong> 更容易理解與管理。同時，它還允許多個系統各自專注於自己最擅長的事情。</p>
<p>在實際的 DDD（領域驅動設計）情境下，<strong>領域事件（Domain Events）</strong> 的命名都反映了對業務有意義的內容。</p>
<p>在真正的企業系統中，我們會用這種模式把大問題分解成更小的步驟，使 <strong>分散式處理</strong> 更容易理解與管理。同時，它還允許多個系統各自專注於自己最擅長的事情。</p>
<p>在實際的 DDD（領域驅動設計）情境下，<strong>領域事件（Domain Events）</strong> 的命名都反映了對業務有意義的內容。</p>
<ul>
<li><p><strong>步驟 1</strong> 可以發布一個領域事件，這個事件來自某個 <strong>Bounded Context</strong> 中某個 <strong>聚合（Aggregate）</strong> 的行為結果。</p>
</li>
<li><p><strong>後續步驟</strong>可能會發生在一個或多個不同的 Bounded Context 中，它們接收初始事件並發布後續的事件。這三個步驟可能會在各自的上下文中建立或修改新的 <strong>Aggregate</strong>。</p>
</li>
</ul>
<p>具體要怎麼做要依賴領域本身，但這些就是在 <strong>Pipes and Filters 架構</strong> 下處理領域事件時的常見結果。</p>
<p>這些事件並不是單薄的技術性通知。它們明確地建模了「<strong>業務流程中的活動發生</strong>」，對整個領域的訂閱者來說是有用的訊號。而且它們會包含唯一識別資訊，以及足夠的屬性來完整傳達其意義。</p>
<p>然而，這種「同步、逐步」的風格也能被擴展，使其同時完成不只一件事情。</p>
<hr />
<h1 id="heading-bonus-section">Bonus Section</h1>
<h2 id="heading-cqrs-4">與 CQRS 常搭配的模式</h2>
<ul>
<li><p><strong>Event Sourcing（事件溯源）</strong>：寫模型以事件做唯一真相（source of truth），讀模型由事件「投影（projection）」生成。優點是重建/回補方便；缺點是事件演進與版本化要做得很嚴謹。</p>
<ul>
<li><p>寫模型不存「當前狀態 Gauge」作為權威，而是把每次變更記成<strong>不可變的事件</strong>，追加到針對某個 Aggregate 的事件串（stream）中。</p>
</li>
<li><p><strong>狀態＝事件重播的結果</strong>：<br />  需要當前狀態時，將該 Aggregate 的事件依序重播（rehydration）即可。</p>
</li>
<li><p><strong>讀模型（Read Model/Projection）＝事件的投影</strong>：<br />  將事件「投影」成為查詢友善的資料（表格、索引、快取），服務 UI/報表；這些讀模型通常是<strong>最終一致</strong>。</p>
</li>
<li><p>以加密貨幣交易機制為例.</p>
</li>
</ul>
</li>
<li><p><strong>Outbox / CDC（變更資料擷取）</strong>：避免「雙寫」不一致。寫模型在同一交易把「事件」寫進 outbox table，再由背景程序或 CDC 可靠地發佈到訊息匯流排。</p>
<ul>
<li>細說 Outbox Pattern 的實作方式</li>
</ul>
</li>
<li><p><strong>Saga / Process Manager</strong>：跨聚合、跨服務的長事務流程與補償（取消、退款等）由流程管理器負責協調。</p>
</li>
<li><p><strong>Materialized Views（實體化檢視）</strong>：讀模型可以是彙總/聚合後的快取表或搜尋索引（如 Elastic、Redis、Cassandra）。</p>
</li>
</ul>
<h2 id="heading-read-your-writes">一致性與體驗策略（Read-Your-Writes）</h2>
<ul>
<li><p><strong>Session Cache</strong>：命令成功後，先用本地/前端快取回填剛提交的值，直到讀模型同步完成。</p>
</li>
<li><p><strong>Sticky Reads</strong>：短時間內把該使用者的查詢導向同一資料分片/同一區域Main資料庫，以提高「讀到自己剛寫的」機率。</p>
</li>
<li><p><strong>延遲回應 / 非同步完成</strong>：命令回傳 202（接受），前端顯示「處理中」，待讀模型更新以推播/輪詢呈現結果。</p>
</li>
<li><p><strong>一致性開關</strong>：在少數 <strong>必需強一致</strong> 的用例（如付款確認頁），可臨時改走 <strong>同步投影</strong> 或直接讀寫模型（慎用）。</p>
</li>
</ul>
<h2 id="heading-5l2155m86iih6yge6ycb5ld6k2j">併發與遞送保證</h2>
<ul>
<li><p><strong>樂觀鎖（Optimistic Concurrency）</strong>：Aggregate 帶 version；命令以期望版本寫入，衝突則重試或回報。</p>
<ul>
<li>可減少資料鎖定時間，但會有大量衝突retry在消耗連線處理的情況</li>
</ul>
</li>
<li><p><strong>冪等性（Idempotency）</strong>：命令與事件要能以 <strong>Idempotency Key</strong> 去重；訂閱者也要能「至多一次 / 至少一次」都安全。</p>
</li>
<li><p><strong>事件順序</strong>：對「同一 AggregateId」要保序（partition by AggregateId）；跨 Aggregate 則設計上避免強相依序。</p>
<p>  一樣已加密貨幣交易為例</p>
</li>
</ul>
<h2 id="heading-6k6a5qih5z6l5oqv5b2x5am5yuz">讀模型投影實務</h2>
<ul>
<li><p><strong>投影器（Projector）設計</strong>：</p>
<ul>
<li><p>小而專一，一個投影器負責一種 view/table。</p>
</li>
<li><p>能「重播」與「快轉到最新（catch-up）」。</p>
</li>
<li><p>失敗要可重試、可跳過（含死信佇列）。</p>
</li>
</ul>
</li>
<li><p><strong>重建策略</strong>：</p>
<ul>
<li><p>有事件倉：全量重播或以時間窗增量重播。</p>
</li>
<li><p>無事件倉：用 ETL 從寫庫批次回填(初始化階段)；之後靠事件持續更新(差異變更)。</p>
</li>
</ul>
</li>
<li><p><strong>讀庫選型</strong>：依需求挑選（查全文、排序/分頁、地理查詢、分析聚合）；多種RO資料庫並存很常見。</p>
</li>
</ul>
<h2 id="heading-read-side">安全與授權（Read Side）</h2>
<ul>
<li><p><strong>投影即分權</strong>：把 <strong>租戶 / 角色 / 權限</strong> 直接烙印在讀模型裡（欄位或分表），查詢過濾更快、更簡單。</p>
</li>
<li><p><strong>資料最小化</strong>：GDPR/刪除請求要能在各讀模型一併抹除（保持索引同步刪除的機制）。</p>
</li>
</ul>
<h2 id="heading-57at6ygl6iih5yv6kea5ris5ocn">維運與可觀測性</h2>
<ul>
<li><p><strong>落後量（Lag）度量</strong>：以「事件序號或時間戳」監控每個讀模型的滯後；超閾值告警。</p>
</li>
<li><p><strong>對帳檢核</strong>：定時抽樣比對讀/寫模型一致性，發現偏差就重播修復。</p>
</li>
<li><p><strong>灰度發佈</strong>：新增讀模型時先影子寫入（dual-projection），穩定後切流量。</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[System Design Interview Ch 2 粗略估算]]></title><description><![CDATA[在系統設計面試關卡中，經常會要求面試者粗略估算來評估系統容量或效能需求。估算的目的都是為了系統的可擴展性（Scalability）。

back-of-the-envelope calculations are estimates you create using a combination of thought experiments and common performance numbers to get a good feel for which designs will meet yo...]]></description><link>https://ganhua.wang/system-design-interview-ch-2</link><guid isPermaLink="true">https://ganhua.wang/system-design-interview-ch-2</guid><category><![CDATA[System Design]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sun, 28 Sep 2025 16:43:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759118017845/0b9cbb4e-a84f-4e60-8806-a22e20d1af00.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>在系統設計面試關卡中，經常會要求面試者粗略估算來評估<strong>系統容量</strong>或<strong>效能需求</strong>。估算的目的都是為了系統的<strong>可擴展性（Scalability）</strong>。</p>
<blockquote>
<p>back-of-the-envelope calculations are estimates you create using a combination of thought experiments and common performance numbers to get a good feel for which designs will meet your requirements</p>
<p><strong>粗略估算是你結合思想實驗和常見效能數據所創建的估算，用來了解哪些設計能滿足你的需求</strong></p>
</blockquote>
<p>粗略估算不是要算得非常精確，而是在面試限時內，快速判斷系統容量、吞吐量、儲存與成本，並據此選擇合理架構。</p>
<pre><code class="lang-mermaid">mindmap
  root((系統設計：效能瓶頸與粗略估算))
    1 導言
      為什麼這一節很重要
      在整體系統設計學習中的位置
    2 效能瓶頸識別
      I/O 密集
        資料庫/磁碟/網路等待
      CPU 密集
        計算/編碼/加解密
      如何找瓶頸
        端到端延遲剖析
        監控指標：CPU/IO wait/吞吐/錯誤率
        單元壓測與隔離測試
    3 粗略估算思路
      Back-of-the-envelope 方法
      常用參考基準
        延遲數字 (Latency Numbers)
        單機 QPS（Web/DB）
        網路頻寬與 I/O 吞吐
      計算流程
        澄清需求：QPS/延遲/留存/可用性
        列出假設：DAU/讀寫比/資料大小/峰值係數
        計算 &amp; Sanity Check
        形成結論與架構權衡
    4 關鍵組件性能測試
      資料庫 Benchmark
        典型查詢讀寫
        量測平均/P95/P99
      搜尋系統 Benchmark
        過濾/聚合/排序
      Cache Benchmark
        GET/SET 高併發
      產出
        QPS 上限
        延遲曲線
        錯誤率/超時門檻
    5 架構設計模式與優化
      Cache
        熱點資料/查詢結果快取
      Sharding
        水平切分/路由策略
      Replication
        主寫多讀/跨 AZ
      負載分散
        LB/Auto Scaling/健康檢查
      一致性策略
        強一致 vs 最終一致
      韌性與保護
        限流/熔斷/重試退避
        降級/Fallback
    6 案例演練（社交平台 1 億用戶）
      需求假設
        DAU/活躍比例/人均請求
        峰值係數
      推導
        平均 &amp; 峰值 QPS
        Cache 命中率 → DB QPS
        儲存需求（每日/年度）
      規模估算
        前端/服務台數
        DB/Cache 節點數
        儲存硬體/雲成本級別
    7 權衡考量
      成本 vs 擴展性
      複雜性 vs 可維護性
      一致性 vs 可用性（CAP/延遲）
      故障恢復 vs 延遲開銷（RTO/RPO）
    8 總結與面試答題建議
      答題流程
        先定位瓶頸
        再做粗算
        說明 Benchmark 設計
        提方案與 trade-off
        白板簡圖標註 QPS/數據流
      常見陷阱
        只看平均不看峰值
        不分層 QPS（API/DB/Cache）
        忽略延遲分佈（P95/P99）
        忽略 Cache 對後端壓力的影響
</code></pre>
<h2 id="heading-8jorydns7vntbhoqk3oqijkuk3nmotmlyjog73nk7bpoljoiifnspfnlaxkvldnrpfmgj3ntq0">🎯 系統設計中的效能瓶頸與粗略估算思維</h2>
<p>在設計分散式系統時，首先要辨識出<strong>整個鏈路中最關鍵的效能瓶頸</strong>。<br />如果系統的服務鏈中存在「主要吃硬碟的組件」──例如資料庫、搜尋引擎或訊息佇列──那麼整體**吞吐量（Throughput）**的上限，往往會由這類 I/O 密集服務所決定。</p>
<p>因此，<strong>第一步</strong>應該是根據實際業務場景，先對這些關鍵服務進行簡單的 <strong>benchmark 測試</strong>，例如測出資料庫在典型查詢下的 <strong>QPS（Queries Per Second）上限</strong>。<br />一旦掌握這個數據，其他依賴該資料庫的上層服務（API、應用層、快取層等）就能以此為基準，推算整體的流量能力與系統擴展需求。</p>
<h3 id="heading-peak-qpsaverage-qps">峰值（Peak QPS）與平均（Average QPS）</h3>
<p>在設計系統時，我們不只關心平均值，更要關心「<strong>尖峰時刻</strong>」的請求量：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>指標</td><td>定義</td><td>用途</td></tr>
</thead>
<tbody>
<tr>
<td><strong>平均 QPS</strong></td><td>一天總請求 ÷ 86400 秒</td><td>評估整體負載</td></tr>
<tr>
<td><strong>峰值 QPS（Peak QPS）</strong></td><td>峰值期間（例如 5 分鐘內）的最高每秒請求數</td><td>用於容量預估與壓測設計</td></tr>
</tbody>
</table>
</div><h3 id="heading-5bi46kal6kqk5y2a">常見誤區</h3>
<ol>
<li><p>❌ 只看平均 QPS → 忽略尖峰期導致過載</p>
</li>
<li><p>❌ 不區分層級 → API 與 DB QPS 不等</p>
</li>
<li><p>❌ 忽略 Cache 命中率 → 實際 DB QPS 大幅下降</p>
</li>
<li><p>❌ 不看延遲分佈（P95/P99） → 高延遲請求導致併發壅塞</p>
</li>
</ol>
<h3 id="heading-qps">用 QPS 做容量預估</h3>
<p>若單台伺服器可承受 5,000 QPS，<br />服務預估峰值為 25,000 QPS，</p>
<p>則需要：<code>伺服器台數 = 峰值 QPS ÷ 每台 QPS = 25,000 ÷ 5,000 = 5 台</code></p>
<p>加上冗餘與健康檢查，通常會部署 <strong>N+1 或 N+2 架構</strong>：<br />→ 最終部署 6～7 台。</p>
<h4 id="heading-5q2l6amf">步驟</h4>
<p>*<em>澄清需求</em></p>
<ul>
<li><p>QPS（Queries Per Second）: 每秒完成的請求數（Requests / second） = 總請求數 ÷ 總時間（秒）</p>
<ul>
<li>一個 API 伺服器在 10 秒內處理完了 50,000 次請求<br />  → QPS = 50,000 ÷ 10 = <strong>5,000 QPS</strong></li>
</ul>
</li>
<li><p>吞吐量、資料留存時間、延遲要求</p>
</li>
<li><p>讀寫比、資料體積、可用性目標</p>
</li>
</ul>
<p><strong>列出假設</strong></p>
<ul>
<li><p>使用者數量／活躍比例（DAU/MAU）</p>
</li>
<li><p>平均單請求／單筆資料大小</p>
</li>
<li><p>工作負載分佈（讀多寫少、峰值因子）</p>
</li>
</ul>
<p><strong>套用常見性能數字</strong></p>
<ul>
<li><p>Latency Numbers</p>
</li>
<li><p>單機機器能承受的 QPS（HTTP 伺服器、資料庫）</p>
</li>
<li><p>網路頻寬與 I/O 吞吐</p>
</li>
</ul>
<p><strong>計算 &amp; Sanity Check</strong></p>
<ul>
<li><p>計算出 QPS、頻寬需求、每日／每年儲存量</p>
</li>
<li><p>把結果拿回「常識」比對：</p>
<ul>
<li><p>每秒幾千 QPS 需幾台 Web 伺服器？</p>
</li>
<li><p>幾 TB 資料／天要多少硬碟？</p>
</li>
</ul>
</li>
</ul>
<p><strong>形成結論與權衡</strong></p>
<ul>
<li><p>是否需要 Cache、分片、CDN、流量削峰…</p>
</li>
<li><p>預估硬體／雲端成本級別</p>
</li>
</ul>
<h1 id="heading-delay">每個系統工程師都應該知道的Delay數字</h1>
<p>透過[<strong>Latency Numbers Every Programmer Should Know網站</strong>](<a target="_blank" href="https://colin-scott.github.io/personal_website/research/interactive_latency.html">https://colin-scott.github.io/personal_website/research/interactive_latency.html</a>)</p>
<p><strong>我們得出以下結論：</strong></p>
<ul>
<li><p><strong>記憶體很快，但磁碟很慢</strong></p>
</li>
<li><p><strong>盡可能避免磁碟尋址</strong></p>
</li>
<li><p><strong>簡單的壓縮演算法很快</strong></p>
</li>
<li><p><strong>如果可能的話，在透過網際網路傳送資料前先壓縮</strong></p>
</li>
<li><p><strong>資料中心通常位於不同地區，在它們之間傳送資料需要時間</strong></p>
</li>
</ul>
<p>這些延遲數字揭示了現代電腦系統的效能特性：</p>
<h3 id="heading-4pqhicoq6ycf5bqm6zqo5bgkkio">⚡ <strong>速度階層</strong></h3>
<ol>
<li><p><strong>CPU 快取/記憶體</strong>：奈秒級別</p>
</li>
<li><p><strong>SSD 存取</strong>：微秒級別</p>
</li>
<li><p><strong>網路通訊</strong>：毫秒級別</p>
</li>
<li><p><strong>機械硬碟</strong>：毫秒級別（最慢）</p>
</li>
</ol>
<h3 id="heading-8joryaqkuioreioiowonwjhyoq">🎯 <strong>設計原則</strong></h3>
<ul>
<li><p><strong>記憶體優先</strong>：盡量將熱點資料保存在記憶體中</p>
</li>
<li><p><strong>避免磁碟 I/O</strong>：特別是隨機存取，或者將 I/O 操作批次化</p>
</li>
<li><p><strong>善用壓縮</strong>：網路傳輸前壓縮資料</p>
</li>
<li><p><strong>就近部署</strong>：減少跨地區的資料傳輸</p>
</li>
</ul>
<p>這些基準數字是系統設計決策的重要依據，幫助我們識別效能瓶頸並選擇合適的架構策略。</p>
<h2 id="heading-twitter-qps">範例：估算 Twitter 的 QPS 和儲存需求</h2>
<p>請注意以下數字僅用於此練習，並非 Twitter 的真實數據。</p>
<h3 id="heading-kirlgyfoqk3mop3ku7bvvjoqkg"><strong>假設條件：</strong></h3>
<ul>
<li><p>3 億月活躍用戶</p>
</li>
<li><p>50% 的用戶（1.5億）每天使用 Twitter</p>
</li>
<li><p>用戶平均每天發布 2 則推文（3億篇推文）</p>
</li>
<li><p>10% 的推文包含媒體</p>
</li>
<li><p>資料儲存 5 年</p>
</li>
</ul>
<p><strong>估算：</strong></p>
<p><strong>QPS 估算：</strong></p>
<ul>
<li><p>日活躍用戶（DAU）= 3 億 × 50% = 1.5 億</p>
</li>
<li><p>推文 QPS = 1.5 億 × 2 則推文 ÷ 24 小時 ÷ 3600 秒 ≈ 3 500 qps</p>
</li>
<li><p>尖峰 QPS ≈ 2× 平均 = 7 000 qps</p>
</li>
</ul>
<p><strong>媒體儲存量估算：</strong></p>
<ul>
<li><p>平均推文大小：</p>
<ul>
<li><p>tweet_id：64 bytes</p>
</li>
<li><p>文字：140 bytes</p>
</li>
<li><p>媒體：1 MB</p>
</li>
</ul>
</li>
<li><p>推文文本：3e8 × 200 bytes ≈ 60 GB／天</p>
</li>
<li><p>媒體儲存：1.5 億 × 2 × 10% × 1 MB = 30 TB／天</p>
</li>
<li><p>5 年媒體儲存：30 TB × 365 × 5 = ~55 PB</p>
</li>
</ul>
<p><strong>Sanity Check</strong></p>
<ul>
<li><p>7 000 qps → 假設一台 Nginx + App 能承 5 000 qps → 2–3 台即可</p>
</li>
<li><p>55 PB → 若單機硬碟 20 TB → 約 3 000 顆分散式儲存</p>
</li>
</ul>
<h4 id="heading-kirwn5okiow4uoimieahoeylevpes8soeulwvjmhjcoq"><strong>📊 常見的粗略估算問題</strong></h4>
<p>QPS、Peak QPS、儲存、Cache 機制、伺服器數量等。準備面試時可以練習這些計算。</p>
<h2 id="heading-high-availability">高可用（High Availability）</h2>
<p><strong>高可用性</strong>是指系統能夠在理想的長時間內持續運作的能力。高可用性以<strong>百分比</strong>來衡量，100% 表示服務的停機時間為 0。大多數服務的可用性介於 99% 到 100% 之間。</p>
<p>**服務等級協議（SLA）**是服務提供商常用的術語。這是你（服務提供商）與客戶之間的協議，正式定義了你的服務將提供的正常運行時間等級。雲端服務提供商 Amazon [4]、Google [5] 和 Microsoft [6] 將他們的 SLA 設定在 99.9% 或以上。正常運行時間傳統上以「幾個 9」來衡量。9 的數量越多越好。9 的數量與預期的系統停機時間相關。</p>
<p><strong>可用性等級對照</strong></p>
<ul>
<li><p><strong>99%</strong> = 每年 3.65 天停機時間</p>
</li>
<li><p><strong>99.9%</strong> = 每年 8.76 小時停機時間</p>
</li>
<li><p><strong>99.99%</strong> = 每年 52.56 分鐘停機時間</p>
</li>
<li><p><strong>99.999%</strong> = 每年 5.26 分鐘停機時間</p>
</li>
</ul>
<p>**高可用性（High Availability, HA）**不只是追求「幾個九」的可用率，更是一整套在架構、運維和測試上保障服務不中斷的具體措施。<br />可用率計算公式</p>
<h3 id="heading-5yv55so5ocn6kii566x55qe6ksh6zuc5ocn">可用性計算的複雜性</h3>
<p><strong>基礎公式：</strong></p>
<pre><code class="lang-scss">
可用性 = MTBF / (MTBF + MTTR)
</code></pre>
<ul>
<li><p>MTBF（Mean Time Between Failures）平均故障間隔</p>
</li>
<li><p>MTTR（Mean Time To Repair）平均修復時間</p>
</li>
</ul>
<p>提高可用率的兩大思路：</p>
<ol>
<li><p>增加 MTBF → 系統更穩定、故障頻率下降</p>
</li>
<li><p>降低 MTTR → 快速偵測、快速恢復</p>
</li>
</ol>
<h2 id="heading-vs-vs">可用性 vs. 可靠性 vs. 耐久性</h2>
<ul>
<li><p><strong>Availability（可用性）</strong>：系統可「對外提供服務」的時間比例。</p>
</li>
<li><p><strong>Reliability（可靠性）</strong>：系統在指定時間內正確執行功能的概率。</p>
</li>
<li><p><strong>Durability（耐久性）</strong>：資料不遺失的程度（一般用在 Storage/Backup）</p>
</li>
</ul>
<blockquote>
<p>在 SLA 條款中若同時談「資料遺失賠償」，通常就是在講耐久性（Durability）。</p>
</blockquote>
<p><strong>實際案例對比：</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>場景</td><td>可用性</td><td>可靠性</td><td>耐久性</td></tr>
</thead>
<tbody>
<tr>
<td>系統回應但資料錯誤</td><td>✅ 高</td><td>❌ 低</td><td>✅ 高</td></tr>
<tr>
<td>系統離線但資料完整</td><td>❌ 低</td><td>❌ 低</td><td>✅ 高</td></tr>
<tr>
<td>系統正常但資料遺失</td><td>✅ 高</td><td>❌ 低</td><td>❌ 低</td></tr>
<tr>
<td>理想狀態</td><td>✅ 高</td><td>✅ 高</td><td>✅ 高</td></tr>
</tbody>
</table>
</div><h3 id="heading-sli-slo-sla">關鍵指標：SLI / SLO / SLA</h3>
<ul>
<li><p><strong>SLI (Service Level Indicator)</strong><br />  用於衡量系統健康度的指標，如「成功響應率」「99% 響應時間 ≤ 500 ms」等。</p>
</li>
<li><p><strong>SLO (Service Level Objective)</strong><br />  你對 SLI 設定的目標值。例如「每月成功響應率 ≥ 99.9%」。</p>
</li>
<li><p><strong>SLA (Service Level Agreement)</strong><br />  對客戶承諾的可用率（通常 ≥ 99.9%），並與未達標時的賠償機制綁定。</p>
</li>
</ul>
<p><strong>大型系統的現實挑戰：</strong></p>
<p><strong>串聯系統（Serial）</strong></p>
<pre><code class="lang-css">總可用性 = <span class="hljs-selector-tag">A</span>₁ × <span class="hljs-selector-tag">A</span>₂ × <span class="hljs-selector-tag">A</span>₃ × ... × <span class="hljs-selector-tag">A</span>ₙ
</code></pre>
<ul>
<li><p>3個99.9%的服務串聯 = 99.7%</p>
</li>
<li><p>10個99.9%的服務串聯 = 99.0%</p>
</li>
</ul>
<p><strong>並聯系統（Parallel）</strong></p>
<pre><code class="lang-css">總不可用性 = (1<span class="hljs-selector-tag">-A</span>₁) × (1<span class="hljs-selector-tag">-A</span>₂) × ... × (1<span class="hljs-selector-tag">-A</span>ₙ)
總可用性 = 1 <span class="hljs-selector-tag">-</span> 總不可用性
</code></pre>
<ul>
<li>2個99.9%的服務並聯 = 99.9999%</li>
</ul>
<p><strong>混合架構計算範例</strong></p>
<pre><code class="lang-css"><span class="hljs-selector-tag">Web</span>層：2台並聯 (99<span class="hljs-selector-class">.9</span>% <span class="hljs-selector-tag">each</span>) → 99<span class="hljs-selector-class">.9999</span>%
<span class="hljs-selector-tag">App</span>層：3台並聯 (99<span class="hljs-selector-class">.5</span>% <span class="hljs-selector-tag">each</span>) → 99<span class="hljs-selector-class">.9875</span>%
<span class="hljs-selector-tag">DB</span>層：主從架構 (99<span class="hljs-selector-class">.95</span>%) → 99<span class="hljs-selector-class">.95</span>%

總可用性 = 99<span class="hljs-selector-class">.9999</span>% × 99<span class="hljs-selector-class">.9875</span>% × 99<span class="hljs-selector-class">.95</span>% ≈ 99<span class="hljs-selector-class">.937</span>%
</code></pre>
<p>一個大型服務往往包含多個 API Endpoint，要衡量「可用性」時，就要先明確定義你的 SLI（Service Level Indicator）和 SLO（Service Level Objective）針對哪個粒度：</p>
<p><strong>可用性粒度（Granularity）</strong></p>
<ol>
<li><p>Endpoint 級別</p>
<ul>
<li><p>每個 API 路徑（如 <code>/login</code>、<code>/orders</code>、<code>/users/{id}</code>）都設定獨立的 SLI</p>
</li>
<li><p>適合用於：不同 Endpoint 負責不同商業功能，對可用性要求差異大</p>
</li>
</ul>
</li>
<li><p>服務（Service）級別</p>
<ul>
<li><p>把所有或一組關鍵 Endpoint 聚合起來，計算整體成功率</p>
</li>
<li><p>適合用於：只關注「整個服務可用／不可用」，不細分各路徑</p>
</li>
</ul>
</li>
<li><p>流量加權（Weighted）</p>
<ul>
<li><p>根據流量大小或業務重要性，給不同 Endpoint 不同權重</p>
</li>
<li><p>例如：<code>/checkout</code> 佔總流量 20%，<code>/search</code> 佔 50%，按流量加權後算出「加權可用率」</p>
</li>
</ul>
</li>
</ol>
<p>假設我們把 <code>/login</code>、<code>/checkout</code>、<code>/search</code> 三個 Endpoint 聚合成「電子商務服務」：</p>
<ul>
<li><p><strong>SLI</strong>：</p>
<ul>
<li><p><strong>successful_requests</strong> = 請求數中返回 2xx 的次數</p>
</li>
<li><p><strong>total_requests</strong> = 所有到達三個 Endpoint 的請求</p>
</li>
<li><p><strong>availability</strong> = successful_requests / total_requests</p>
</li>
</ul>
</li>
<li><p><strong>SLO</strong>：</p>
<ul>
<li><p>每分鐘可用率 ≥ 99.9%</p>
</li>
<li><p>429 回應佔比上限 ≤ 1%。<strong>429 (Too Many Requests)</strong>：屬於「流量保護/限流」，有時不算系統故障，但使用者無法完成操作，可視為可用性下降</p>
</li>
</ul>
</li>
<li><p><strong>SLA</strong>：</p>
<ul>
<li>客戶承諾月度可用率 ≥ 99.9%，若低於則賠償</li>
</ul>
</li>
</ul>
<h2 id="heading-slo">從 SLO 要求到架構設計</h2>
<ol>
<li><h3 id="heading-slo-sla-slis"><strong>收到 SLO / SLA 要求 → 定義 SLIs</strong></h3>
<ul>
<li><p>與 SRE / 架構師一起確認：<br />  • 哪些 Endpoint、哪些指標要算進可用率？<br />  • Time window（1 min / 5 min / 30 days rolling）<br />  • 流量是否加權？</p>
</li>
<li><p>範例：<br />  SLO：<br />  • 成功回應率 ≥ 99.9%（2xx）<br />  • p99 Latency ≤ 500 ms<br />  • 429 限流率 ≤ 1%</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-sli">SLI 設計的層次化思維</h3>
<h4 id="heading-slibusiness-level-sli"><strong>業務層 SLI（Business-Level SLI）</strong></h4>
<pre><code class="lang-yaml">    <span class="hljs-comment"># 電商系統範例</span>
    <span class="hljs-attr">business_sli:</span>
      <span class="hljs-attr">order_success_rate:</span>
        <span class="hljs-attr">definition:</span> <span class="hljs-string">"成功完成訂單 / 嘗試下單次數"</span>
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&gt; 99.5%"</span>

      <span class="hljs-attr">payment_success_rate:</span>
        <span class="hljs-attr">definition:</span> <span class="hljs-string">"支付成功 / 支付嘗試次數"</span>  
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&gt; 99.9%"</span>

      <span class="hljs-attr">search_relevance:</span>
        <span class="hljs-attr">definition:</span> <span class="hljs-string">"用戶點擊前3個搜尋結果的比例"</span>
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&gt; 85%"</span>
</code></pre>
<h4 id="heading-slisystem-level-sli"><strong>系統層 SLI（System-Level SLI）</strong></h4>
<pre><code class="lang-yaml">    <span class="hljs-attr">system_sli:</span>
      <span class="hljs-attr">api_availability:</span>
        <span class="hljs-attr">definition:</span> <span class="hljs-string">"2xx回應 / 總請求數"</span>
        <span class="hljs-attr">measurement_window:</span> <span class="hljs-string">"1分鐘"</span>
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&gt; 99.9%"</span>

      <span class="hljs-attr">latency_p99:</span>
        <span class="hljs-attr">definition:</span> <span class="hljs-string">"99%請求的回應時間"</span>
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&lt; 500ms"</span>

      <span class="hljs-attr">error_rate:</span>
        <span class="hljs-attr">definition:</span> <span class="hljs-string">"5xx錯誤 / 總請求數"</span>
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&lt; 0.1%"</span>
</code></pre>
<h4 id="heading-sliinfrastructure-level-sli"><strong>基礎設施層 SLI（Infrastructure-Level SLI）</strong></h4>
<pre><code class="lang-yaml">
    <span class="hljs-attr">infrastructure_sli:</span>
      <span class="hljs-attr">cpu_utilization:</span>
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&lt; 70% (sustained)"</span>

      <span class="hljs-attr">memory_utilization:</span>
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&lt; 80%"</span>

      <span class="hljs-attr">disk_io_wait:</span>
        <span class="hljs-attr">target:</span> <span class="hljs-string">"&lt; 10%"</span>
</code></pre>
<h3 id="heading-slo-1">複雜系統的 SLO 分層設計</h3>
<h4 id="heading-slo-2"><strong>金字塔式 SLO 架構</strong></h4>
<pre><code class="lang-java">
    ┌─────────────────────────────────────┐
    │        <span class="hljs-function">Business <span class="hljs-title">SLO</span> <span class="hljs-params">(<span class="hljs-number">99.5</span>%)</span>        │  ← 對客戶承諾
    ├─────────────────────────────────────┤
    │      Service <span class="hljs-title">SLO</span> <span class="hljs-params">(<span class="hljs-number">99.7</span>%)</span>           │  ← 內部目標
    ├─────────────────────────────────────┤
    │   Component <span class="hljs-title">SLO</span> <span class="hljs-params">(<span class="hljs-number">99.9</span>%)</span>            │  ← 元件目標  
    ├─────────────────────────────────────┤
    │ Infrastructure <span class="hljs-title">SLO</span> <span class="hljs-params">(<span class="hljs-number">99.95</span>%)</span>        │  ← 基礎設施
    └─────────────────────────────────────┘</span>
</code></pre>
<p>    <strong>實際案例：微服務電商系統</strong></p>
<pre><code class="lang-yaml">
    <span class="hljs-comment"># 業務層 SLO</span>
    <span class="hljs-attr">business_slo:</span>
      <span class="hljs-attr">checkout_flow:</span>
        <span class="hljs-attr">slo:</span> <span class="hljs-string">"99.5% 成功率"</span>
        <span class="hljs-attr">error_budget:</span> <span class="hljs-string">"0.5% = 3.6小時/月"</span>

    <span class="hljs-comment"># 服務層 SLO  </span>
    <span class="hljs-attr">service_slo:</span>
      <span class="hljs-attr">user_service:</span>
        <span class="hljs-attr">availability:</span> <span class="hljs-string">"99.8%"</span>
        <span class="hljs-attr">latency_p95:</span> <span class="hljs-string">"&lt; 200ms"</span>

      <span class="hljs-attr">order_service:</span>
        <span class="hljs-attr">availability:</span> <span class="hljs-string">"99.9%"</span>
        <span class="hljs-attr">latency_p95:</span> <span class="hljs-string">"&lt; 300ms"</span>

      <span class="hljs-attr">payment_service:</span>
        <span class="hljs-attr">availability:</span> <span class="hljs-string">"99.95%"</span>  <span class="hljs-comment"># 更嚴格，因為影響收入</span>
        <span class="hljs-attr">latency_p95:</span> <span class="hljs-string">"&lt; 500ms"</span>

    <span class="hljs-comment"># 基礎設施 SLO</span>
    <span class="hljs-attr">infrastructure_slo:</span>
      <span class="hljs-attr">kubernetes_cluster:</span>
        <span class="hljs-attr">node_availability:</span> <span class="hljs-string">"99.95%"</span>

      <span class="hljs-attr">database:</span>
        <span class="hljs-attr">availability:</span> <span class="hljs-string">"99.99%"</span>
        <span class="hljs-attr">replication_lag:</span> <span class="hljs-string">"&lt; 1s"</span>
</code></pre>
<h2 id="heading-5asn5z6l6ksh6zuc57o757wx55qe5yv55so5ocn5p625qel6kit6kii">大型複雜系統的可用性架構設計</h2>
<h3 id="heading-5asa5bgk5qyh5yax6asy6kit6kii">多層次冗餘設計</h3>
<h4 id="heading-kirlnldnkibliibmlapmnrbmp4sqkg"><strong>地理分散架構</strong></h4>
<pre><code class="lang-yaml">    <span class="hljs-attr">global_architecture:</span>
      <span class="hljs-attr">regions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"us-east-1"</span>
          <span class="hljs-attr">role:</span> <span class="hljs-string">"primary"</span>
          <span class="hljs-attr">capacity:</span> <span class="hljs-string">"60%"</span>

        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"us-west-2"</span>  
          <span class="hljs-attr">role:</span> <span class="hljs-string">"secondary"</span>
          <span class="hljs-attr">capacity:</span> <span class="hljs-string">"40%"</span>

        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"eu-west-1"</span>
          <span class="hljs-attr">role:</span> <span class="hljs-string">"disaster_recovery"</span>
          <span class="hljs-attr">capacity:</span> <span class="hljs-string">"100% (cold standby)"</span>

      <span class="hljs-attr">failover_strategy:</span>
        <span class="hljs-attr">rto:</span> <span class="hljs-string">"&lt; 5 minutes"</span>  <span class="hljs-comment"># Recovery Time Objective</span>
        <span class="hljs-attr">rpo:</span> <span class="hljs-string">"&lt; 30 seconds"</span> <span class="hljs-comment"># Recovery Point Objective</span>
</code></pre>
<h4 id="heading-kirmni3li5nntrlmolzlrrnpjkmqkhlvi8qkg"><strong>服務網格容錯模式</strong></h4>
<pre><code class="lang-yaml">
    <span class="hljs-attr">service_mesh_patterns:</span>
      <span class="hljs-attr">circuit_breaker:</span>
        <span class="hljs-attr">failure_threshold:</span> <span class="hljs-number">50</span><span class="hljs-string">%</span>
        <span class="hljs-attr">timeout:</span> <span class="hljs-string">30s</span>
        <span class="hljs-attr">reset_timeout:</span> <span class="hljs-string">60s</span>

      <span class="hljs-attr">retry_policy:</span>
        <span class="hljs-attr">max_attempts:</span> <span class="hljs-number">3</span>
        <span class="hljs-attr">backoff:</span> <span class="hljs-string">"exponential"</span>
        <span class="hljs-attr">base_delay:</span> <span class="hljs-string">100ms</span>
        <span class="hljs-attr">max_delay:</span> <span class="hljs-string">10s</span>

      <span class="hljs-attr">bulkhead:</span>
        <span class="hljs-attr">thread_pools:</span>
          <span class="hljs-attr">critical_operations:</span> <span class="hljs-number">20</span>
          <span class="hljs-attr">normal_operations:</span> <span class="hljs-number">10</span>
          <span class="hljs-attr">background_tasks:</span> <span class="hljs-number">5</span>
</code></pre>
<h3 id="heading-6loh5paz5bgk5yv55so5ocn6kit6kii">資料層可用性設計</h3>
<h4 id="heading-kirliibmlaplvios4fmlpnluqvnrzbnlauqkg"><strong>分散式資料庫策略</strong></h4>
<pre><code class="lang-yaml">    <span class="hljs-attr">database_architecture:</span>
      <span class="hljs-attr">primary_db:</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">"PostgreSQL"</span>
        <span class="hljs-attr">availability:</span> <span class="hljs-string">"99.95%"</span>
        <span class="hljs-attr">backup_strategy:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">continuous_wal_archiving</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">daily_full_backup</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cross_region_replication</span>

      <span class="hljs-attr">read_replicas:</span>
        <span class="hljs-attr">count:</span> <span class="hljs-number">3</span>
        <span class="hljs-attr">distribution:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">same_az:</span> <span class="hljs-number">1</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">different_az:</span> <span class="hljs-number">2</span>
        <span class="hljs-attr">lag_threshold:</span> <span class="hljs-string">"&lt; 100ms"</span>

      <span class="hljs-attr">cache_layers:</span>
        <span class="hljs-attr">l1_cache:</span> <span class="hljs-string">"Application Memory"</span>
        <span class="hljs-attr">l2_cache:</span> <span class="hljs-string">"Redis Cluster"</span>
        <span class="hljs-attr">l3_cache:</span> <span class="hljs-string">"CDN"</span>

      <span class="hljs-attr">consistency_model:</span>
        <span class="hljs-attr">writes:</span> <span class="hljs-string">"strong_consistency"</span>
        <span class="hljs-attr">reads:</span> <span class="hljs-string">"eventual_consistency_acceptable"</span>
</code></pre>
<h4 id="heading-kiros4fmlpnliibniyfoiifot6nlleqkg"><strong>資料分片與路由</strong></h4>
<pre><code class="lang-python">    <span class="hljs-comment"># 資料分片策略範例</span>
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShardingStrategy</span>:</span>
        <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
            self.shards = {
                <span class="hljs-string">'shard_1'</span>: {<span class="hljs-string">'range'</span>: <span class="hljs-string">'0-999999'</span>, <span class="hljs-string">'master'</span>: <span class="hljs-string">'db1'</span>, <span class="hljs-string">'replicas'</span>: [<span class="hljs-string">'db1r1'</span>, <span class="hljs-string">'db1r2'</span>]},
                <span class="hljs-string">'shard_2'</span>: {<span class="hljs-string">'range'</span>: <span class="hljs-string">'1000000-1999999'</span>, <span class="hljs-string">'master'</span>: <span class="hljs-string">'db2'</span>, <span class="hljs-string">'replicas'</span>: [<span class="hljs-string">'db2r1'</span>, <span class="hljs-string">'db2r2'</span>]},
                <span class="hljs-string">'shard_3'</span>: {<span class="hljs-string">'range'</span>: <span class="hljs-string">'2000000-2999999'</span>, <span class="hljs-string">'master'</span>: <span class="hljs-string">'db3'</span>, <span class="hljs-string">'replicas'</span>: [<span class="hljs-string">'db3r1'</span>, <span class="hljs-string">'db3r2'</span>]}
            }

        <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">route_query</span>(<span class="hljs-params">self, user_id</span>):</span>
            shard_key = user_id % <span class="hljs-number">3</span>
            <span class="hljs-keyword">return</span> self.shards[<span class="hljs-string">f'shard_<span class="hljs-subst">{shard_key + <span class="hljs-number">1</span>}</span>'</span>]

        <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_shard_failure</span>(<span class="hljs-params">self, failed_shard</span>):</span>
            <span class="hljs-comment"># 自動故障轉移到副本</span>
            replicas = self.shards[failed_shard][<span class="hljs-string">'replicas'</span>]
            <span class="hljs-keyword">return</span> replicas[<span class="hljs-number">0</span>]  <span class="hljs-comment"># 提升第一個副本為主庫</span>
</code></pre>
<h3 id="heading-2-amp"><strong>2. 架構 &amp; 技術選型</strong></h3>
<p>    根據 SLIs/SLOs 把可用性拆解到各層面需要採取的措施：</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層級</td><td>措施範例</td></tr>
</thead>
<tbody>
<tr>
<td>基礎架構</td><td>■ 多可用區域（AZ）或 Region 部署</td></tr>
<tr>
<td>■ Anycast DNS / Global LB</td><td></td></tr>
<tr>
<td>計算層</td><td>■ Auto-Scaling + Health Check</td></tr>
<tr>
<td>■ 多實例冗餘，避免單點故障</td><td></td></tr>
<tr>
<td>資料儲存層</td><td>■ 多副本／分片（Replication / Sharding）</td></tr>
<tr>
<td>■ 定期備份，異地災難備援</td><td></td></tr>
<tr>
<td>網路 &amp; 流量管理</td><td>■ Load Balancer + Rate Limiter</td></tr>
<tr>
<td>■ CDN / Edge Cache</td><td></td></tr>
<tr>
<td>■ Circuit Breaker</td><td></td></tr>
<tr>
<td>運維 &amp; 驗證</td><td>■ 監控 + Alert（Prometheus + Alertmanager）</td></tr>
<tr>
<td>■ Chaos Engineering</td></tr>
</tbody>
</table>
</div><h3 id="heading-3"><strong>3. 實作步驟</strong></h3>
<p>    <strong>1. 指標蒐集（Instrumentation）</strong></p>
<ul>
<li><p>在程式碼中<strong>埋點</strong>（metrics）<br />  • 請求量、成功/失敗次數、延遲時間、限流次數…</p>
</li>
<li><p>選擇收集與視覺化工具<br />  • Prometheus + Grafana、Datadog、New Relic</p>
</li>
</ul>
<p>    <strong>2. 自動化部署</strong></p>
<ul>
<li><p>IaC（Infrastructure as Code）：Terraform、CloudFormation</p>
</li>
<li><p>CI/CD 流程：</p>
<ol>
<li>單元測試 → 2. 整合測試 → 3. 部署到 Canary / Staging → 4. 觀察 SLI 指標 → 5. 全量上線</li>
</ol>
</li>
</ul>
<p>    <strong>3. 容錯 &amp; 降級設計</strong></p>
<ul>
<li><p>Circuit Breaker（斷路器）</p>
</li>
<li><p>Bulkhead（隔艙模式）</p>
</li>
<li><p>Timeout、Retry、Fallback → 保護下游服務</p>
</li>
</ul>
<p>    <strong>4. 災難恢復</strong></p>
<ul>
<li><p>訂定 RPO / RTO，落實冷備 / 熱備方案</p>
</li>
<li><p>定期演練「主機房失效切換」（DR Drill）</p>
</li>
</ul>
<h3 id="heading-4"><strong>4. 監控、告警與運維</strong></h3>
<ol>
<li><p>Dashboard</p>
<ul>
<li><p>實時展示 SLI（成功率、Latency、Error Budget）</p>
</li>
<li><p>結合業務指標（流量、訂單量）預測壓力</p>
</li>
</ul>
</li>
<li><p>Alert</p>
<ul>
<li><p>分級告警：P1（系統不可用）、P2（延遲升高）、P3（資源飽和）</p>
</li>
<li><p>設定告警門檻、告警頻率（避免告警風暴）</p>
</li>
</ul>
</li>
<li><p>事件管理</p>
<ul>
<li><p>PagerDuty / Opsgenie on‐call</p>
</li>
<li><p>Blameless Postmortem（無責任歸屬的事故分析）</p>
</li>
</ul>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[System Design Interview Ch 1]]></title><description><![CDATA[前言
這本書的重點面試中關於系統設計問題的環節。因為系統設計沒有範圍沒有固定模式，什麼都能提出來討論跟討論，問題範圍太大，整個過程非常開放，且幾乎沒有標準答案。
所以在這過程中，主要考察的是溝通與問題解決能力。評估面試者如何分析與拆解問題，並且與面試官說明想法與討論。
系統設計問題是開放式的。就像真實世界一樣，系統之間存在各種差異與變體。期望的結果是提出一個能達成系統設計目標的架構。討論方向會依面試官有所不同：有人會涵蓋所有層面的高階架構，也有人會挑出一兩個領域深入鑽研。一般而言，系統需求、限制...]]></description><link>https://ganhua.wang/system-design-interview-ch-1</link><guid isPermaLink="true">https://ganhua.wang/system-design-interview-ch-1</guid><category><![CDATA[system design interview]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Mon, 15 Sep 2025 16:49:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758261572912/7e6549fd-3e86-474a-870d-265858b9ec73.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-5ymn6kia">前言</h1>
<p>這本書的重點面試中關於<strong>系統設計問題</strong>的環節。因為系統設計沒有範圍沒有固定模式，什麼都能提出來討論跟討論，問題範圍太大，整個過程非常開放，且幾乎沒有標準答案。</p>
<p>所以在這過程中，主要考察的是溝通與問題解決能力。評估面試者如何分析與拆解問題，並且與面試官說明想法與討論。</p>
<p>系統設計問題是開放式的。就像真實世界一樣，系統之間存在各種差異與變體。期望的結果是提出一個能達成系統設計目標的架構。討論方向會依面試官有所不同：有人會涵蓋所有層面的高階架構，也有人會挑出一兩個領域深入鑽研。一般而言，<strong>系統需求</strong>、<strong>限制</strong>（constraints）與<strong>瓶頸</strong>（bottlenecks）必須被充分理解，才能引導雙方後續的討論方向。</p>
<h3 id="heading-5bcp5byf55qe5bd5rov">小弟的心法</h3>
<ol>
<li><p>不急著畫架構——先問清需求與假設。</p>
</li>
<li><p>每個決策都能說明「為什麼不是另一種」。</p>
</li>
<li><p>面試更像協作設計會議，而非單人獨白。</p>
</li>
<li><p>有條理比「炫技」重要；框架 + 邏輯推進 &gt; 記憶片段。</p>
</li>
</ol>
<h3 id="heading-6kio6kuw5rwb56il">討論流程</h3>
<ol>
<li><p>通用流程框架（需求 → 容量估算 → 高階架構 → 核心元件 → 資料流 → 優化 → 風險 &amp; 改進）</p>
</li>
<li><p>主題化整理（儲存、Cache、索引、排程、排隊、監控）</p>
</li>
</ol>
<h1 id="heading-ch1-zero-millions">Ch1 從 Zero 擴展到支援 Millions 的使用者</h1>
<p>一個系統能從支援在線幾百人、幾千人、幾萬人、幾百萬人，這是一個演化的過程，幾乎不太能一步到位的。基本都從支援極少的使用者開始設計，再逐步擴展到能服務百萬使用者。</p>
<h2 id="heading-single-server-setup">單一伺服器架構（Single Server Setup）</h2>
<p>打造一個複雜系統也是如此。都是從最簡單開始：所有東西都跑在同一台伺服器（web service、資料庫、cache service 等全部同一主機運行。）上。目標是完成業務功能與驗證。</p>
<h3 id="heading-6kul5rgc6iih5zue5oej">請求與回應</h3>
<p>為了理解這個架構，先看「Request 流程」與「Traffic 來源」。</p>
<p>Request 流程︰</p>
<ol>
<li><p>使用者透過網域名稱（例如：<a target="_blank" href="http://api.mysite.com">api.mysite.com</a>）存取網站。通常 DNS 是第三方的付費服務（例如 Route53, Cloudflare DNS…），而不是由我們自己的伺服器託管。</p>
</li>
<li><p>DNS 解析後會將 IP 位址回傳給瀏覽器或手機應用。在例子中，回傳的 IP 是 15.125.23.214。</p>
</li>
<li><p>一旦取得 IP 位址，瀏覽器或行動 App 會直接向你的 Web 伺服器送出 HTTP request。</p>
</li>
<li><p>Web 伺服器回傳 HTML 頁面或 JSON 回應，供前端渲染。</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757950383710/49a3a18c-deb4-4b6d-9cc9-870ff8d5ec4f.png" alt class="image--center mx-auto" /></p>
<p>接著看流量來源︰到達你 Web 伺服器的流量來自兩種來源：Web 應用與 App 應用。</p>
<ul>
<li><p>Web 應用：使用Server side language（例如 Java、Python 等）處理商業邏輯、資料儲存等，再用前端技術（HTML、JavaScript）呈現。</p>
</li>
<li><p>行動應用：行動 App 與 Web 伺服器之間以 HTTP 協定溝通。</p>
</li>
</ul>
<p>不論哪種 Web service 都要提供 Web API 例如 <code>GET /users/12</code> 請求．取得 JSON 內容</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"id"</span>: <span class="hljs-number">12</span>,
  <span class="hljs-attr">"firstName"</span>: <span class="hljs-string">"xxx"</span>,
  <span class="hljs-attr">"lastName"</span>: <span class="hljs-string">"yyy"</span>,
  <span class="hljs-attr">"address"</span>: {
    <span class="hljs-attr">"city"</span>: <span class="hljs-string">"Taipei"</span>,
    <span class="hljs-attr">"postalCode"</span>: <span class="hljs-string">"100"</span>
  },
  <span class="hljs-attr">"phoneNumbers"</span>: [
      <span class="hljs-string">"23939889"</span>,
      <span class="hljs-string">"8825252"</span>
   ]
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757950354099/605902e6-a696-4a9a-a5a0-ad6a506aa850.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-5yig5bgk77ym5bch6loh5paz5bqr542o56ul5ye65y67">分層，將資料庫獨立出去</h2>
<p>隨著使用者數量逐漸成長，一台主機不夠用了，我們需要多台：一台處理流量，另一台作為資料庫。將流量（Web Tier）與資料庫（Data Tier）分離，可以各自獨立擴展。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757955016160/443e5786-638a-4142-80ff-ec434258b8fd.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-6kab55so5zoq56iu6loh5paz5bqr77yf">要用哪種資料庫？</h3>
<h4 id="heading-rdbms-vs-nosql">關聯式資料庫（RDBMS） vs 非關聯式資料庫（NoSQL）</h4>
<ul>
<li><p>RDBMS的資料以「<strong>表格 + 列</strong>」方式儲存，你可以用 <strong>SQL</strong> <strong>跨多表做 join</strong>。</p>
</li>
<li><p>NoSQL 主要分四大類：Key-Value、Graph、Column、Document。一般<strong>不支援 join</strong>。</p>
<ul>
<li>這幾乎意味著資料是<em>非結構化</em>，或<em>沒有明確關聯。</em></li>
</ul>
</li>
</ul>
<p><img src="https://ucarecdn.com/b0aa13df-e279-459b-80bb-3c27742382d5/" alt="SQL vs NoSQL Database" /></p>
<h4 id="heading-sql-vs-nosql">關聯式（SQL） vs 非關聯式（NoSQL）深度比較</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>維度</td><td>關聯式 RDBMS</td><td>NoSQL（概括）</td><td>適合情境</td></tr>
</thead>
<tbody>
<tr>
<td>資料模型</td><td>嚴謹結構（Schema）</td><td>彈性 / 半結構 / 無 Schema</td><td>需求變動快用 NoSQL</td></tr>
<tr>
<td>查詢能力</td><td>強大（JOIN, GROUP BY）</td><td>依類型差異大：KV 最弱、Document 中等、Graph 強</td><td>多維複雜查詢 → RDBMS</td></tr>
<tr>
<td>交易（ACID）</td><td>原生支援</td><td>多數弱化（但有改良，如 DynamoDB 的條件寫入）</td><td>金融 / 訂單 → RDBMS</td></tr>
<tr>
<td>一致性</td><td>強一致（可調整隔離層級）</td><td>多採最終一致（Eventual Consistency）</td><td>高可用 + 容忍延遲一致</td></tr>
<tr>
<td>可擴展性</td><td>垂直為主（水平需要分片）</td><td>多為天然水平擴展（尤其 KV / Column）</td><td>快速擴容量 → NoSQL</td></tr>
<tr>
<td>延遲</td><td>寫讀都穩定，但會因 JOIN 複雜度上升</td><td>KV / Column 可極低延遲</td><td>Ultra 低延遲 → NoSQL</td></tr>
<tr>
<td>生態成熟度</td><td>工具、多數工程師熟悉</td><td>生態片段化</td><td>團隊熟悉度低 → 先選 RDBMS</td></tr>
<tr>
<td>成本/維運</td><td>單體易維護，scale 後複雜</td><td>分散式一致性/備援需心智成本</td><td>人力不足 → 先簡單</td></tr>
</tbody>
</table>
</div><h2 id="heading-nosql">NoSQL 四大類型何時選？</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>類型</td><td>代表</td><td>優勢</td><td>典型場景</td></tr>
</thead>
<tbody>
<tr>
<td>Key-Value Store</td><td>Redis, DynamoDB</td><td>超低延遲、簡單伸縮</td><td>Session、快取、購物車</td></tr>
<tr>
<td>Document Store</td><td>MongoDB, CouchDB</td><td>半結構 JSON、欄位可變</td><td>CMS、使用者設定、活動資料</td></tr>
<tr>
<td>Column Family Store</td><td>Cassandra, HBase</td><td>寫入量大、時間序列、寬表</td><td>Log、IoT、時間序列聚合</td></tr>
<tr>
<td>Graph Store</td><td>Neo4j, JanusGraph</td><td>圖遍歷、關係跳數</td><td>社交圖、推薦、欺詐關聯</td></tr>
</tbody>
</table>
</div><p>如果你需要：</p>
<ul>
<li><p>頻繁 schema 變化 → Document</p>
</li>
<li><p>海量寫入（append-only style）→ Column</p>
</li>
<li><p>超快單 key 存取 → Key-Value</p>
</li>
<li><p>多跳關係（friend of friend）→ Graph</p>
</li>
</ul>
<h4 id="heading-5bi46kal44cm6kqk6kej44cn6iih5yn5l6l">常見「誤解」與反例</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>誤解</td><td>說明</td><td>更好思路</td></tr>
</thead>
<tbody>
<tr>
<td>高併發就一定要 NoSQL</td><td>先看 Query Pattern + 快取命中</td><td>加 Cache + 調索引可能足夠</td></tr>
<tr>
<td>NoSQL 不需要設計資料模型</td><td>模型差仍導致查詢/儲存浪費</td><td>瞭解訪問模式後再定文件結構</td></tr>
<tr>
<td>RDBMS 不可水平擴展</td><td>可：分片 / 分區表 / 多租戶設計</td><td>成本是操作複雜性</td></tr>
<tr>
<td>Join 不支援 = NoSQL 一定快</td><td>可能需要在應用層多次 <strong>round-trip</strong></td><td>資料壓平（denormalize）需衡量更新成本</td></tr>
</tbody>
</table>
</div><h3 id="heading-57wq5qel5yyw6iih6z2e57wq5qel5yyw">結構化與非結構化</h3>
<h4 id="heading-structured">什麼是「結構化（Structured）」資料？</h4>
<ul>
<li><p>欄位（Column）固定、類型明確（INT、VARCHAR、DATETIME…）、行（Row）格式一致。</p>
</li>
<li><p>易於使用標準化查詢（SQL）</p>
</li>
<li><p>可建立索引、加約束（UNIQUE、FOREIGN KEY、CHECK）</p>
</li>
<li><p>資料品質高，可做強一致交易（ACID）</p>
</li>
<li><p>本質：<strong>Schema-on-write（寫入時即驗證格式）</strong></p>
</li>
</ul>
<h4 id="heading-semi-structured">半結構（Semi-Structured）資料</h4>
<ul>
<li><p>有基本層次（Hierarchy）與標記（Markers / Tags），但欄位可變；每筆資料不必完全相同。</p>
<ul>
<li><p>資料本身以「樹狀 / 巢狀（Nested）」方式呈現，而不是像關聯式資料表那樣固定為二維（rows × columns）。</p>
</li>
<li><p>標記（Markers / Tags）指的是附著在資料上的「自描述結構資訊（Self-describing Metadata）」，幫助系統或人來解析這份資料（白話文即欄位名稱）。</p>
</li>
</ul>
</li>
<li><p>例如︰JSON、XML、YAML、Parquet</p>
</li>
<li><p>常見的有Document Store（MongoDB）、Data Lake（查詢引擎 Presto / Athena）、Columnar files（Parquet 供 OLAP）</p>
</li>
<li><p>Schema 演進成本低，因為欄位名稱與層次一起存放於記錄裡：</p>
<ul>
<li><p>新增欄位：新文件直接加；舊文件沒有也不報錯</p>
</li>
<li><p>移除欄位：新文件不再寫；舊文件仍保留歷史值</p>
</li>
<li><p>讀取的程式可以設計成「有則解析，無則給預設值」（能參考 <a target="_blank" href="https://martinfowler.com/bliki/TolerantReader.html">Martin Fowler 的 Tolerant Reader</a> ）</p>
</li>
</ul>
</li>
<li><p>更貼近業務事件自然結構</p>
</li>
</ul>
<h4 id="heading-unstructured">非結構化（Unstructured）資料</h4>
<ul>
<li><p>缺乏可預測欄位結構，系統很難直接用行列表示語意。</p>
</li>
<li><p>例子：影片、圖片、音訊、自然語言文件（純文字）、PDF。</p>
</li>
<li><p>通常都放 AWS S3、或自己建立檔案系統（FTP）這類的。</p>
</li>
<li><p>設計上會搭配「Metadata」用來建立索引（例如：影像寬高、上傳者、標籤）</p>
</li>
<li><p>本質︰Schema-on-read（讀取 / 分析時才賦予結構）</p>
</li>
</ul>
<h4 id="heading-54k65l2v5y2a5yig6ycz5lij6icf77yf">為何區分這三者？</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>面向</td><td>結構化</td><td>半結構</td><td>非結構化</td></tr>
</thead>
<tbody>
<tr>
<td>可查詢性</td><td>立即強</td><td>需索引策略</td><td>需先萃取特徵</td></tr>
<tr>
<td>驗證</td><td>寫入即驗證</td><td>可選（驗或放寬）</td><td>幾乎無（靠外部流程）</td></tr>
<tr>
<td>模型演進</td><td>嚴謹、成本高</td><td>中等</td><td>重度依賴額外處理層</td></tr>
<tr>
<td>典型操作</td><td>OLTP</td><td>混合（OLTP + Event）</td><td>ETL / 分析 / AI 推論</td></tr>
<tr>
<td>索引策略</td><td>B-Tree、Hash</td><td>二級索引、多鍵</td><td>Metadata / 向量索引</td></tr>
<tr>
<td>分析工具成熟度</td><td>高</td><td>高（日益成熟）</td><td>需前處理</td></tr>
</tbody>
</table>
</div><blockquote>
<p>不同資料型態 → 不同儲存與存取模式，而非一刀切</p>
</blockquote>
<h3 id="heading-join">Join 的「表面意義」與「深層意義」</h3>
<h3 id="heading-6kgo6z2i5osp576p77yi5l255so5bgk77yj">表面意義（使用層）</h3>
<p>JOIN 是把分散在不同表格中的相關資料「在查詢時動態關聯」起來，例如：</p>
<ul>
<li><p>users（使用者）</p>
</li>
<li><p>orders（訂單）</p>
</li>
<li><pre><code class="lang-sql">      <span class="hljs-keyword">SELECT</span> u.name, o.amount <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">users</span> u <span class="hljs-keyword">JOIN</span> orders o <span class="hljs-keyword">ON</span> u.id = o.user_id
</code></pre>
</li>
</ul>
<h3 id="heading-5rex5bgk5osp576p77yi6kit6kii5bgk77yj">深層意義（設計層）</h3>
<p><strong>JOIN</strong> 存在的根本原因：你「選擇將資料 <strong>Normalization</strong>」以：</p>
<ol>
<li><p>減少冗餘（避免資料重複）</p>
</li>
<li><p>降低更新異常（Update / Delete / Insert anomalies）</p>
</li>
<li><p>維持一致性（例如使用者名稱更新只改一處）</p>
</li>
</ol>
<p>JOIN 就是「查詢時把拆散的資訊重新拼起來」的計算成本。</p>
<p>「為什麼要拆？拆開後怎麼正確表達『包含』語意？」、怎麼辨別「擁有 (ownership)」 vs 「參考 (reference)」 vs 「成員 (membership)」。</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層面</td><td>說明</td><td>關鍵洞察</td></tr>
</thead>
<tbody>
<tr>
<td><strong>表面（操作層）</strong></td><td>SQL 語法：動態關聯多表資料</td><td>「拼接」分散的資訊片段</td></tr>
<tr>
<td><strong>深層（設計層）</strong></td><td>正規化的代價：查詢時重組被拆散的業務實體</td><td>「為什麼要拆？拆了如何表達完整語意？」</td></tr>
</tbody>
</table>
</div><h3 id="heading-join-1">正規化 ↔ JOIN 的權衡循環</h3>
<pre><code class="lang-markdown">
業務實體 → 正規化拆表 → 減少冗餘 → 需要 JOIN 重組 → 查詢成本 → 考慮反正規化
<span class="hljs-code">    ↑                                                                    ↓
    ←←←←←←←←←← 效能 vs 一致性的永恆拉鋸 ←←←←←←←←←←←←←←←←←←←←←←←</span>
</code></pre>
<h2 id="heading-5lua6bq85piv44cm5yyf5zcr44cn6kqe5osp77yf">什麼是「包含」語意？</h2>
<p>「<strong>包含</strong>」(X contains Y) 指：X 代表一個聚合整體 (whole)，Y 為其組成部分 (parts)，Y 的存在或意義與 X 有<strong>緊密依附關係</strong>（生命週期、識別、權限、保存策略）。</p>
<p>對比：</p>
<ul>
<li><p>單純引用 (reference)：Post 引用 User（作者），User 不屬於 Post。</p>
</li>
<li><p>成員關係 (membership)：Playlist 與 Song，Song 可同時在多個 Playlist。</p>
</li>
<li><p>真正組成/擁有 (composition)：Order 與 OrderItem；OrderItem 沒有 Order 就不應存在。</p>
</li>
</ul>
<h2 id="heading-5lij5bgk6kaw6kes77ya6kqe5osp5bgkiokgkidntzdmp4vlsaqg4oasiowvpus9nowxpa">三層視角：語意層 → 結構層 → 實作層</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層次</td><td>問題</td><td>表達工具</td></tr>
</thead>
<tbody>
<tr>
<td>語意層 (Domain)</td><td>這是擁有？聚合？共享資源？</td><td>DDD Aggregate、語意語彙</td></tr>
<tr>
<td>結構層 (Logical / Relational)</td><td>怎麼拆表與定鍵？</td><td>主鍵/外鍵、唯一約束、級聯</td></tr>
<tr>
<td>實作層 (Physical)</td><td>怎麼索引、分片、避免昂貴 JOIN？</td><td>索引、分區、快取、反正規化</td></tr>
</tbody>
</table>
</div><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758255445730/f68036a7-1ceb-4a61-991a-c1517e7e5c70.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-domain-layer">第一層：語意層（Domain Layer）</h3>
<p><strong>核心問題：</strong> 這個關係在現實世界中代表什麼？</p>
<ul>
<li><p>專注於 <strong>業務語意</strong> 和 <strong>現實世界關係</strong></p>
</li>
<li><p>定義四種關係類型：擁有/組成、引用、成員、依賴</p>
</li>
<li><p>決定資料的 <strong>生命週期</strong> 和 <strong>耦合程度</strong></p>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td>關係類型</td><td>特徵</td><td>範例</td><td>設計含義</td></tr>
</thead>
<tbody>
<tr>
<td><strong>擁有/組成 (Composition)</strong></td><td>強依附、共生命週期</td><td>Order ← OrderItem</td><td>應聚合設計、級聯操作</td></tr>
<tr>
<td><strong>引用 (Reference)</strong></td><td>弱耦合、獨立存在</td><td>Post → User (author)</td><td>外鍵約束、但不級聯刪除（生命週期不同）</td></tr>
<tr>
<td><strong>成員 (Membership)</strong></td><td><strong>多對多</strong>、共享資源</td><td>Playlist ↔ Song</td><td>中介表、獨立生命週期</td></tr>
<tr>
<td><strong>依賴 (Dependency)</strong></td><td>單向依賴、可替換</td><td>Order → PaymentMethod</td><td>可能需要歷史快照</td></tr>
</tbody>
</table>
</div><h3 id="heading-logicalrelational-layer">第二層：結構層（Logical/Relational Layer）</h3>
<p><strong>核心問題：</strong> 如何用關聯式模型正確表達語意？</p>
<ul>
<li><p>將語意轉換為 <strong>資料庫結構</strong></p>
</li>
<li><p>設計主鍵、外鍵、約束和級聯規則</p>
</li>
<li><p>確保資料的 <strong>完整性</strong> 和 <strong>一致性</strong></p>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td>語意</td><td>主鍵設計</td><td>外鍵約束</td><td>級聯規則</td><td>唯一性</td></tr>
</thead>
<tbody>
<tr>
<td>組成關係</td><td>複合鍵 (parent_id, seq)</td><td>NOT NULL + FK</td><td>ON DELETE CASCADE</td><td>子項在父項內唯一</td></tr>
<tr>
<td>引用關係</td><td>獨立主鍵</td><td>FK (可 NULL)</td><td>NO ACTION / RESTRICT</td><td>全域唯一</td></tr>
<tr>
<td>成員關係</td><td>中介表雙 FK</td><td>雙向 FK</td><td>SET NULL</td><td>組合唯一</td></tr>
</tbody>
</table>
</div><h3 id="heading-physical-layer">第三層：實作層（Physical Layer）</h3>
<p><strong>核心問題：</strong> 如何在效能與一致性間取得平衡？</p>
<ul>
<li><p>針對 <strong>效能需求</strong> 選擇實作策略</p>
</li>
<li><p>在一致性與效能間做 <strong>權衡</strong></p>
</li>
<li><p>考慮索引、分片、快取等技術手段</p>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td>策略</td><td>適用場景</td><td>優勢</td><td>代價</td></tr>
</thead>
<tbody>
<tr>
<td><strong>標準 JOIN</strong></td><td>中等規模、高一致性需求</td><td>即時、準確</td><td>跨表查詢成本</td></tr>
<tr>
<td><strong>反正規化</strong></td><td>讀多寫少、可接受最終一致</td><td>單表查詢快</td><td>更新複雜、儲存冗餘</td></tr>
<tr>
<td><strong>物化視圖</strong></td><td>複雜聚合、定期刷新可接受</td><td>查詢極快</td><td>刷新延遲、儲存倍增</td></tr>
<tr>
<td><strong>應用層 JOIN</strong></td><td>分片環境、微服務架構</td><td>靈活路由</td><td>網路往返、程式複雜</td></tr>
<tr>
<td><strong>事件溯源</strong></td><td>高變更頻率、需完整歷史</td><td>可重放、審計完整</td><td>查詢需重建狀態</td></tr>
</tbody>
</table>
</div><h2 id="heading-44cmkirljixlkksqkuiqnuaejoajewipoawtahhuaetg">「<strong>包含</strong>語意」判斷框架</h2>
<h3 id="heading-7">快速評估表（7 個維度）</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>維度</td><td>組成關係 (Composition)</td><td>引用關係 (Reference)</td></tr>
</thead>
<tbody>
<tr>
<td><strong>獨立價值</strong></td><td>子項無獨立商業意義</td><td>被引用方有獨立價值</td></tr>
<tr>
<td><strong>生命週期</strong></td><td>與父項同生<strong>共死</strong></td><td>獨立的創建/銷毀</td></tr>
<tr>
<td><strong>權限繼承</strong></td><td>繼承父項權限</td><td>有自己的權限體系</td></tr>
<tr>
<td><strong>數量特性</strong></td><td>可預期上限（1-100s）</td><td>可能無上限或極大</td></tr>
<tr>
<td><strong>共享性</strong></td><td>專屬於單一父項</td><td>可被多方引用</td></tr>
<tr>
<td><strong>查詢模式</strong></td><td>通常與父項一起查詢(跟第一點一樣意思)</td><td>經常獨立查詢</td></tr>
<tr>
<td><strong>一致性要求</strong></td><td>強一致性</td><td>可接受最終一致</td></tr>
</tbody>
</table>
</div><h3 id="heading-5yik5pa357wq5p6ciokgkidoqk3oqijnrzbnlau">判斷結果 → 設計策略</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>評估結果</td><td>推薦策略</td><td>實作要點</td></tr>
</thead>
<tbody>
<tr>
<td><strong>5+ 個「組成」特徵</strong></td><td>聚合根設計</td><td>複合主鍵、級聯刪除、事務邊界</td></tr>
<tr>
<td><strong>3-4 個「組成」特徵</strong></td><td>強外鍵約束</td><td>NOT NULL FK、部分級聯</td></tr>
<tr>
<td><strong>2 個以下「組成」特徵</strong></td><td>引用設計</td><td>獨立主鍵、軟約束、可 NULL</td></tr>
</tbody>
</table>
</div><h2 id="heading-rdbms">RDBMS 中的「包含」表達手法</h2>
<p>常見實作：</p>
<ol>
<li><p>父表 + 子表（FK NOT NULL + ON DELETE CASCADE）</p>
</li>
<li><p>子表複合主鍵 (parent_id, line_no) 強化「從屬」語意</p>
</li>
<li><p>有些小且穩定的子集合（例如 User 的設定集）可用 JSON/ARRAY 欄位（減少 JOIN）</p>
</li>
<li><p>視圖 / 物化視圖 將常用父子聚合預先展開（讀快、寫複雜）</p>
</li>
</ol>
<h2 id="heading-nosqldocument-db">NoSQL（以Document DB為例）對應模型</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>建模方式</td><td>語意</td><td>優點</td><td>風險</td></tr>
</thead>
<tbody>
<tr>
<td>嵌入 (Embed)</td><td>強組成、一起讀</td><td>單查取整體、少網路往返、單文件原子性</td><td>子集合無上限會膨脹；部分更新成本高；熱點文件鎖</td></tr>
<tr>
<td>參考 (Reference)</td><td>弱關聯 / 重用</td><td>文件小、子項可獨立成長</td><td>需要多次 round-trip 或應用層 Join；一致性自己管</td></tr>
<tr>
<td>預先展開 (Denormalize Copy)</td><td>為讀優化</td><td>查詢快</td><td>重複數據同步難</td></tr>
<tr>
<td>預計算 / 物化文件</td><td>聚合快取</td><td>讀非常快</td><td>延遲一致 / 重建流程必要</td></tr>
</tbody>
</table>
</div><p>MongoDB 有 <code>$lookup</code> 但非傳統 cost-based optimizer；Cassandra 無 JOIN（需建反向查詢表）。</p>
<h2 id="heading-nosql-vs">何時在 NoSQL「嵌入」 vs 「引用」</h2>
<p><strong>嵌入 (Embed)</strong> 適用：</p>
<ul>
<li><p>子項集合大小「小且有上限」（例如 &lt;= 50 或數百內）</p>
</li>
<li><p>讀操作幾乎總是「父 + 全部子」一起要</p>
</li>
<li><p>子項更新頻率低或批次</p>
</li>
<li><p>需要單文件原子性</p>
</li>
<li><p>不會被多個父共享</p>
</li>
<li><p>無需跨子項複雜查詢（例如不單獨以子項條件大範圍搜尋）</p>
</li>
</ul>
<p><strong>引用 (Reference)</strong> 適用：</p>
<ul>
<li><p>子集合「可能無上限」或高速成長（留言、日誌）</p>
</li>
<li><p>子項要被多個父使用或交叉分析</p>
</li>
<li><p>子項更新頻率高 / 局部更新為主</p>
</li>
<li><p>需要基於子項條件分頁、排序、統計</p>
</li>
<li><p>權限或壽命不完全跟父同步</p>
</li>
<li><p>需要避免文件熱點（高併發寫同一父）</p>
</li>
</ul>
<h2 id="heading-5bi46kal6yyv6kqk5bcn5oej">常見錯誤對應</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>誤解</td><td>風險</td><td>建議</td></tr>
</thead>
<tbody>
<tr>
<td>看到「包含」就一律在 RDBMS 拆表</td><td>過多 JOIN、過早複雜化</td><td>子集合小且不查詢可考慮 <strong>JSON</strong></td></tr>
<tr>
<td>NoSQL 一律嵌入</td><td>文件爆炸、熱點寫鎖</td><td>監控文件大小/寫頻率，必要拆</td></tr>
<tr>
<td>以為引用 + 應用層 join 等同 RDBMS JOIN 成本</td><td>多 round-trip 延遲高</td><td>用批量查詢 / pipeline / 預計算</td></tr>
<tr>
<td>任意反正規化未設同步策略</td><td>資料漂移不一致</td><td>有來源版本號 / 重建流程</td></tr>
<tr>
<td>Mongo $lookup 當成 RDBMS JOIN 大量使用</td><td>性能不可預期</td><td>控制資料量，適度寫入時展開</td></tr>
</tbody>
</table>
</div><p>但除了正規化避免冗餘，還常因為它們屬於<strong>不同的業務主題</strong>（Domain / Subject）或擁有<strong>不同的更新頻率、保存期限、合規要求與存取模式</strong>；當某個使用場景需要把這些原本獨立演化的資料拼在一起時，就需要 JOIN。</p>
<h4 id="heading-54k65lua6bq844cm5lin5zcm5li76agmic8g55sf5zg96ycx5pyf44cn5pyd6amf5yuv5yig6kgo77yf">為什麼「不同主題 / 生命週期」會驅動分表？</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>拆分驅動因素</td><td>說明</td><td>常見做法</td><td>影響 JOIN 需求</td></tr>
</thead>
<tbody>
<tr>
<td>主題邊界（Domain Boundary）</td><td>User、Order、Payment、Inventory 各自業務語意</td><td>按領域建獨立表（甚至獨立 schema）</td><td>報表或聚合時跨域 JOIN</td></tr>
<tr>
<td>更新頻率（Update Velocity）</td><td>Profile 更新少；交易流水高頻</td><td>熱表與冷表分離</td><td>查詢展示時再 JOIN</td></tr>
<tr>
<td>生命週期 / 保存期限（Retention）</td><td>訂單主檔保留 7 年；暫存計算只要 30 天</td><td>歷史表（history/archive）</td><td>需要歷史 + 現況視圖時 JOIN 或 UNION</td></tr>
<tr>
<td>安全 / 權限（Security Classification）</td><td>PII 與一般行為資料分離</td><td>敏感表設更嚴 ACL</td><td>需遮罩或脫敏後再 JOIN</td></tr>
<tr>
<td>存取模式（Access Pattern）</td><td>OrderItems 高寫入、Product 靜態</td><td>針對高寫入表獨立調優</td><td>下單詳情頁面 JOIN</td></tr>
<tr>
<td>資料粒度（Granularity）</td><td>Fact（交易事實） vs Dimension（維度）</td><td>星狀/雪花模型</td><td>OLAP 查詢多 JOIN</td></tr>
<tr>
<td>資料版本 / 緩變（Slowly Changing Dimension, SCD）</td><td>需要保留歷史屬性</td><td>維度表多版本（Type 2）</td><td>回放歷史分析需依時間 JOIN</td></tr>
<tr>
<td>擴展 / 分片策略</td><td>不同表按不同 key 分片</td><td>垂直分片 + 水平分片</td><td>跨分片 JOIN 成本升高</td></tr>
</tbody>
</table>
</div><h2 id="heading-54k65lua6bq86kab44cm5oej55so5bgkic8g6loh5paz5bgk44cn5yig6zui77yf">為什麼要「應用層 / 資料層」分離？</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>面向</td><td>分離前（單機）</td><td>分離後（雙層）</td><td>好處</td></tr>
</thead>
<tbody>
<tr>
<td>資源競爭</td><td>App 與 DB 搶 CPU / RAM / I/O</td><td>各自獨立</td><td>效能穩定</td></tr>
<tr>
<td>擴展模式</td><td>垂直擴充為主（易到頂）</td><td>App 可水平加機，DB 可做複寫/調優</td><td>成本更彈性</td></tr>
<tr>
<td>可靠性</td><td>一崩全崩</td><td>單層故障影響範圍縮小</td><td>可用性提升</td></tr>
<tr>
<td>部署風險</td><td>部署 App 可能拖累 DB</td><td>App 可頻繁部署，DB 較少改動</td><td>DevOps 友善</td></tr>
<tr>
<td>監控精度</td><td>難拆指標</td><td>分 tiers 監控 (QPS, Slow Query, CPU)</td><td>除錯快</td></tr>
</tbody>
</table>
</div><p>這一步是系統走向 Layered Architecture 的第一個拆分，也是之後能加快取、CDN、讀寫分離、分片的基石。</p>
<h2 id="heading-vertical-scalinghorizontal-scaling">垂直擴充（Vertical Scaling）與水平擴充（Horizontal Scaling）</h2>
<p><strong>垂直擴充</strong>（又稱 Scale <strong>Up</strong>）是指替現有伺服器增加更多資源（例如 CPU、記憶體等）。在流量較低的階段，垂直擴充是一個很好的選擇，其最大優點是簡單（不需修改架構、部署流程；常只是調整雲端實例規格。）。但它也存在嚴重的限制。</p>
<ol>
<li><p>垂直擴充存在硬體上限——不可能無限制地為單一伺服器添加 CPU 與記憶體。</p>
</li>
<li><p>垂直擴充本身不提供容錯與冗餘能力；若唯一的伺服器故障，整個網站／應用會完全無法服務（會有<a target="_blank" href="https://en.wikipedia.org/wiki/Single_point_of_failure">Single point of failure問題</a>）。</p>
</li>
</ol>
<p>So…在業務有賺錢且請求數才 C10K 上下時，其實垂直擴充還是很不錯的解決方案的。</p>
<p><strong>水平擴充</strong>（又稱 Scale <strong>Out</strong>）則是透過新增多台伺服器進入資源池來提升整體處理能力。在更大請求數的系統中，水平擴充往往是更可取的方案。又因為有多台伺服器所以水平擴充方案通常會導入<strong>負載平衡器</strong>（Load Balancer）是最合適的技術手段。</p>
<h2 id="heading-load-balancer"><strong>負載平衡器</strong>（Load Balancer）</h2>
<p>負載平衡器會將進入的流量平均（或<strong>依特定演算法</strong>）分配到設定於「負載平衡伺服器組」中的多台 Web 伺服器。如下圖所示。</p>
<p>使用者直接連到負載平衡器的 Public IP。採用這種架構後，外部用戶無法再直接存取後端 Web 伺服器。為了提升<strong>安全性</strong>，伺服器之間的通訊方式改用 <strong>Private IP</strong>。所謂Private IP，是只能在同一網路（例如同一 VPC / 子網）內互相存取的IP address，無法透過公開網際網路被直接訪問。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758034832218/592eb1cf-771a-44dc-8841-e05a17dbad32.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-load-balnacer-httpsdocsnginxcomnginxadmin-guideload-balancerhttp-load-balancer">Load Balnacer 常見的<a target="_blank" href="https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/">負載平均策略</a></h3>
<h3 id="heading-server-side-discoveryhttpsmicroservicesiopatternsserver-side-discoveryhtml">服務方的服務發現（<a target="_blank" href="https://microservices.io/patterns/server-side-discovery.html">Server Side Discovery</a>）</h3>
<h3 id="heading-httpsdocsawsamazoncomzhtwroute53latestdeveloperguiderouting-policy-geohtml-region">根據<a target="_blank" href="https://docs.aws.amazon.com/zh_tw/Route53/latest/DeveloperGuide/routing-policy-geo.html">地理位置導流量</a>去不同 Region 的服務</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758207257825/7372ca7e-c1a3-4010-826f-23a745e13de7.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-6loh5paz5bqr6ksh5ar5qmf5yi2">資料庫複寫機制</h2>
<p>剛剛提到水平擴展是請求量較大的系統場景時較好的技術方案。又因為大多數場景資料的讀寫比例差異也不小，幾乎都是讀取資料的次數跟筆數遠比寫入修改的高。</p>
<p>因此我們就想著能否將資料庫讀取資料的請求與寫入修改的請求分開，因此有了複寫機制這架構，在這架構中最主要的兩個角色分別是 Primary/Main（資料來源）以及 Replica（資料副本）。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758037402099/5eecee31-dd8e-4a88-8b86-9d6f9b0acbe0.png" alt class="image--center mx-auto" /></p>
<p>資料庫複寫架構的優點：</p>
<ul>
<li><p>效能提升：在複寫模型中，寫入與更新集中於主節點，而讀取請求分散到各從節點，達到並行處理更多查詢的效果。</p>
</li>
<li><p>資料可靠性：若某一資料庫伺服器遭受天災（如颱風、地震）損毀，資料仍保存在其他節點，不致遺失，因為複寫將資料散佈在多個位置（能想成 Git server 與自己local的 Git 副本）。</p>
</li>
<li><p>高可用性：資料跨節點複寫後，即使其中一台資料庫離線，系統仍可向其他節點存取相同資料，服務不致中斷（目前此架構僅針對讀取場景具備高可用性）。</p>
</li>
</ul>
<h3 id="heading-isdmj5dlh7rllypoywgiq">! 提出問題 !</h3>
<ol>
<li><p>若系統只有一台 Replica 資料庫而它離線了，此時 client 發出的讀取請求你會怎處理？</p>
</li>
<li><p>若主資料庫離線呢？</p>
</li>
</ol>
<h2 id="heading-cache">引入 Cache 機制</h2>
<p>Cache 的目的將「<strong>計算代價高</strong>」或「<strong>被頻繁存取</strong>」的資料<strong>結果</strong>存放於記憶體，使後續請求能更快回應。每次網頁載入時，通常會觸發一次或多次資料庫查詢（例如取得遊戲排行榜、後台列表清單、取得企業用戶的昨日報表等），反覆打資料庫會顯著影響效能，而快取可<strong><em>減輕</em></strong>此問題。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758114900465/5e542df4-2d0a-4122-a9a4-d5ce719d5545.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-caching-strategies">Caching Strategies</h3>
<h3 id="heading-vs">被動式 vs 主動式</h3>
<p>被動式︰都是在讀取階段才判斷。</p>
<p>主動示︰資料寫入修改階段跟著作動。</p>
<ol>
<li>Cache Aside（最常見）</li>
</ol>
<p><img src="https://cdn-images-1.medium.com/max/800/1*3DDetzMbVd7HfLU_xrCCbA.png" alt="How cache-aside pattern works?" /></p>
<ol start="2">
<li><p>Read through</p>
<p> 令一個是 Write through，有 <strong>through</strong> 的都是 application 只認識一個端點，由該服務來執行跟資料庫中間的同步。</p>
</li>
</ol>
<p><img src="https://ucarecdn.com/4a02dc04-3572-413b-8d60-49e49b5d4683/" alt="How read through caching pattern works?" /></p>
<ol start="3">
<li>Write Around（小弟個人比較常搭配這個）</li>
</ol>
<p><img src="https://ucarecdn.com/52e92dc2-d666-4d33-8f87-cdbeecde8842/" alt="How write-around caching strategy works?" /></p>
<h3 id="heading-cache-1">使用 Cache 的一些提醒事項</h3>
<ul>
<li><p>適用於「<strong>讀多</strong>、<strong>改少</strong>」的資料。</p>
</li>
<li><p>Cache 存於Memory，<strong>重啟即失</strong>，並不適合作為最終持久層。因此關鍵資料必須寫入永久性儲存（如資料庫、物件儲存）。</p>
</li>
<li><p>應設定<strong>到期時間</strong>。沒有<strong>過期機制</strong>會導致記憶體永遠被占用。</p>
<ul>
<li><p>TTL 太短 → 頻繁回源查 DB</p>
</li>
<li><p>TTL 太長 → 資料可能陳舊失真。需依一致性與負載取平衡。</p>
</li>
</ul>
</li>
<li><p><strong>資料一致性</strong>問題，需維持主資料存放（DB）與快取同步。由於更新快取與更新資料庫通常非同一原子交易，可能產生不一致。多區域部署時（跨資料中心）一致性更難</p>
</li>
<li><p><strong>淘汰（Eviction）策略</strong>：當快取已滿，新加入資料會觸發舊資料被移除，稱為「快取淘汰」。最常見策略為 <strong>LRU</strong>（最近最少使用）。其他如 <strong>LFU</strong>（最少使用頻率）、<strong>FIFO</strong>（先進先出）等，可依使用情境選擇。</p>
</li>
<li><p>故障緩解：單一快取伺服器可能成為單點故障（SPOF）。建議多台、甚至跨資料中心部署以避免全失。另可超額配置記憶體（預留成長緩衝），降低突增導致的淘汰壓力。</p>
</li>
</ul>
<h2 id="heading-cdn">CDN</h2>
<p>CDN（內容傳遞網路）是由<strong>地理</strong>上分散的伺服器所組成，用來加速傳送靜態內容。CDN 節點會快取圖片、影片、CSS、JavaScript 等靜態資源。</p>
<p>使用者造訪網站時，離他最近的 CDN 節點提供靜態資源。直覺上，使用者距離 CDN 節點越遠，載入越慢。例如節點在舊金山，洛杉磯使用者會比歐洲使用者獲得更快回應。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758122791767/168ea3d5-0307-4e78-92b0-4d0be053de73.png" alt class="image--center mx-auto" /></p>
<p>使用 CDN 的常見注意事項</p>
<ul>
<li><p>成本：CDN 由第三方營運，需付出外送/回源流量費。快取極少被用的資源效益低，可移出 CDN。</p>
</li>
<li><p>適當快取到期（TTL）：對時效性內容要設定適當期限，過長會陳舊，過短會頻繁回源。</p>
</li>
<li><p>失效（Invalidate）檔案：可在到期前移除或更新：</p>
<ul>
<li><p>透過 CDN API 主動失效（Purge/Invalidate）。主動刪除</p>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/zh_tw/AmazonCloudFront/latest/DeveloperGuide/invalidation-access-logs.html">https://docs.aws.amazon.com/zh_tw/AmazonCloudFront/latest/DeveloperGuide/invalidation-access-logs.html</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/FRiCKLE/ngx_cache_purge">https://github.com/FRiCKLE/ngx_cache_purge</a></p>
</li>
</ul>
</li>
<li><p>Client（需要調整 Client） 透過物件版本化（URL 增加版本參數，如 image.png?v=2）</p>
</li>
</ul>
</li>
<li><p>CDN Fallback：當 CDN 發生暫時性故障（例如整個網域解析不到、邊緣節點異常、TLS 失敗、連線超時），你的網站或應用不應立即「崩潰」或讓使用者卡住，而是應該能進行切換，或者直接到 Origin server。</p>
<ul>
<li><a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/high_availability_origin_failover.html">https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/high_availability_origin_failover.html</a></li>
</ul>
</li>
</ul>
<h2 id="heading-stateful-vs-stateless">Stateful vs Stateless</h2>
<p>之前都是考慮資料層的分層、擴容。但 Web/API Server ，一些應用服務 呢？</p>
<p>如果我們想把 Web/API server 做<strong>水平擴充</strong>。要達成這點，必須把<strong>狀態</strong>（例如：使用者 Session 資料）移出 Web 層。好的做法是把 Session 資料（常見的狀態之一）存放在持久化或共享型儲存（例如：關聯式資料庫或 NoSQL）。叢集中的每一台 Web 伺服器都能從該資料庫存取狀態資料。這種設計稱為「stateless Web 層 (stateless web tier)」。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758124451678/e08049bb-835d-4a87-830b-9345c53ef8b3.png" alt class="image--center mx-auto" /></p>
<p>問題在於：<strong>同一個客戶端</strong>的每次請求都必須導向<strong>同一台伺服器</strong>。多數負載平衡器可以透過「 Session (sticky sessions)」達成，但這會帶來額外開銷。以這種方式擴充或縮減伺服器更困難，也較難處理伺服器故障。</p>
<blockquote>
<p>討論︰為什麼stateful server 我們即使擴充或縮減伺服器，書上也是說困難？</p>
</blockquote>
<p>下圖是採用無狀態 Web 層後的更新設計。在這種 stateless 架構中，使用者的 HTTP 請求可以被送往任意 Web 伺服器；這些伺服器會到共享資料儲存取回狀態。狀態資料放在共享儲存（該儲存可以是RDBMS、Memcached / Redis、或 NoSQL 等。）中，而不是留在 Web 伺服器。stateless 系統更簡單、更穩健且易於擴充（僅限於方便擴充 Web server）。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758124680689/ee8931fe-9673-4bde-b431-7fd42869c71d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-44cm54ua5owl44cn5piv5lua6bq877yf">「狀態」是什麼？</h2>
<p>在這裡的<strong>狀態</strong>指的是：<br />為了正確處理使用者接下來的請求，某台特定伺服器必須保存在自己本地（記憶體/本地檔案/連線）的<strong>資料或上下文</strong>。<br />只要「下一個請求必須回到同一台伺服器才能正常運作」，那份資料就是造成有狀態的因素。</p>
<p>簡化判別句：<br />如果我把這台 Web 機器砍掉重建，使用者的體驗或流程會不會壞？會 → 那些就是『狀態』。</p>
<h3 id="heading-54ua5owl5bi46kal5yig6age">狀態常見分類</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>類型</td><td>說明</td><td>是否常見造成黏著</td></tr>
</thead>
<tbody>
<tr>
<td>使用者身份 / 認證狀態</td><td>Session、登入 token、權限上下文</td><td>是</td></tr>
<tr>
<td>使用者互動流程進度</td><td>多步驟表單進度、購物車、結帳流程</td><td>是</td></tr>
<tr>
<td>使用者偏好 / UI 狀態</td><td>語系、佈局模式、A/B variant</td><td>若存在伺服器本地就會</td></tr>
<tr>
<td>暫存的業務資料</td><td>尚未提交的草稿、臨時計算結果、購物車：若存放在某機器記憶體，請求換機器就消失、股票撮合服務 Orderbook 上掛的買賣單</td><td>是</td></tr>
<tr>
<td>Cache</td><td>本機計算結果 / 熱資料</td><td>一般屬「軟狀態」，但設計不當會依賴</td></tr>
<tr>
<td>檔案暫存</td><td>上傳中碎片、影像處理中間檔案</td><td>是</td></tr>
<tr>
<td>連線層狀態</td><td>WebSocket 連接對應的使用者、推播訂閱</td><td>是（如綁在單機記憶體）</td></tr>
<tr>
<td>排程 / 工作佇列內記錄</td><td>In-memory job queue / delayed tasks、背景批次 Job 進度</td><td>是</td></tr>
<tr>
<td>Rate limiting / 計數器</td><td>以記憶體紀錄使用者請求數 <code>Map&lt;IP, counter&gt;</code>。</td><td>是（各機不一致導致錯誤限制）</td></tr>
<tr>
<td>防重 / Idempotency 狀態</td><td>保存在單機的紀錄是否已處理某訂單/交易</td><td>是（若僅存在本機）</td></tr>
<tr>
<td>Feature Flag 解析結果</td><td>每個使用者被分派的組別</td><td>是（若本機才知）</td></tr>
<tr>
<td>TLS / 加密交握 / session ticket</td><td>傳輸層協商（通常由 LB 處理）</td><td>通常不視為應用黏著</td></tr>
<tr>
<td>ML 推論上下文</td><td>使用者個別暫存特徵向量</td><td>可能</td></tr>
</tbody>
</table>
</div><h3 id="heading-vs-1">「軟狀態 vs 硬狀態」</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>類別</td><td>定義</td><td>失去後影響</td><td>例子</td></tr>
</thead>
<tbody>
<tr>
<td><strong>硬狀態</strong> (Authoritative)</td><td>系統必須正確保存</td><td>遺失 = 資料錯誤/使用者體驗壞</td><td>Session、交易紀錄、購物車</td></tr>
<tr>
<td><strong>軟狀態</strong> (Reconstructable)</td><td>可以重新計算或重抓</td><td>遺失 = 暫時變慢 / 重新生成</td><td>Cache、預算好的推薦結果</td></tr>
</tbody>
</table>
</div><p>建議：</p>
<ul>
<li><p>硬狀態永遠不要只放在單機記憶體。</p>
</li>
<li><p>軟狀態可放本機：但要接受可隨時清空 (cache invalidation OK)。</p>
</li>
</ul>
<h4 id="heading-5lua6bq85oof5rob44cm5lin5bf6ygo5bqm6l95rgc5a6m5ywo54sh54ua5owl44cn77yf">什麼情況「不必過度追求完全無狀態」？</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td>條件</td><td>可接受局部本地狀態</td></tr>
</thead>
<tbody>
<tr>
<td>早期原型、單節點</td><td>快速迭代比彈性更重要</td></tr>
<tr>
<td>本地緩存的這些資料不是系統的「authoritative source」，而是從別地方的資料加工、計算、查詢、組合後得到的暫存結果。</td><td>清掉僅造成效能下降</td></tr>
<tr>
<td>臨時開發工具介面</td><td>非生產流量</td></tr>
</tbody>
</table>
</div><h2 id="heading-message-queue">Message Queue</h2>
<p>Message Queue 是一種支援<strong>非同步通訊</strong>的「具備持久性（durable）」元件，同時以記憶體為基礎進行暫存。它扮演 buffer 的角色（消峰），分散並傳遞非同步請求。</p>
<p>其基本架構相當簡潔：輸入端服務（稱為 Producer / Publisher）產生訊息並發佈到佇列；其他服務或伺服器（稱為 Consumer / Subscriber）連接到佇列，依訊息所定義的內容執行對應動作。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758209029657/9f227a7d-debf-4a4b-997d-d8139f85601a.png" alt class="image--center mx-auto" /></p>
<p>「<strong>解耦（Decoupling）</strong>」特性使訊息佇列成為構建可擴展且可靠應用的常用架構。使用訊息佇列時，當消費者暫時無法處理請求，生產者仍然可以把訊息送進佇列；反之，即使生產者暫時離線，消費者仍能從佇列中取出既有訊息處理。<strong>Decoupling</strong> 白話的說，生產者不必管有沒有消費者，也不用管消費者是誰。消費者也是如此。</p>
<h2 id="heading-6loh5paz5bqr55qe5po05bgv">資料庫的擴展</h2>
<p>最簡單的是垂直擴展（Vertical Scaling），前面講過了不再提。</p>
<p>而資料庫也有水平擴展（Horizontal Scaling），又稱「<strong>分片（Sharding）</strong>」，是透過新增更多伺服器來擴充。分片是把大型資料庫切成較小且更易管理的單元（稱為「分片」）。各分片共享相同 Schema，但存放的實際資料互不重複。</p>
<p>圖示範一個分片資料庫：使用者資料依使用者 ID 分配到不同伺服器。每次存取資料都會用雜湊函式尋找對應分片。本例採用 <code>user_id%4</code> 作為hash：結果為 0 則用 shard 0；為 1 則 shard 1；其餘類推。</p>
<p>實作分片策略時最重要的考量是「分片鍵」（<strong>Sharding Key</strong>，又稱 Partition Key）。分片鍵由一或多個欄位組成，決定資料如何分佈。良好的分片鍵可讓查詢正確路由到對應資料庫以有效存取。關鍵準則之一是能「<strong>均勻分散</strong>」資料（避免 hot spot）。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758210441760/da9414c9-2610-4ff5-8785-3c5bf9594f2a.png" alt class="image--center mx-auto" /></p>
<p>Sharding 是有效擴展資料庫的技術，但並非完美，會引入新的複雜度與挑戰：</p>
<ul>
<li><p>資料再分片（<strong>Resharding</strong>）：在以下情況需要：1) 單一分片因成長過快已放不下。2) 資料分佈不均造成部分分片較快「耗盡」（容量或效能）。此時須更新分片函式並搬遷資料。第 5 章將提到的一致性雜湊（<strong>Consistent Hashing</strong>）是常用解法。</p>
</li>
<li><p>名人問題（Celebrity Problem / <strong>Hotspot Key</strong>）：過多請求集中於特定分片導致過載。若多位超高人氣帳號落在同一分片，其讀取壓力會壓垮伺服器。可能需為每位名人單獨配置分片，甚至再細分。</p>
</li>
<li><p>Join 與去正規化：跨分片進行 Join 困難，常見替代作法是「去正規化」，讓查詢可在單表完成。</p>
</li>
</ul>
<h2 id="heading-sharding-join">Sharding 架構下的 JOIN 挑戰</h2>
<h3 id="heading-5oyr5oiw5yig6age">挑戰分類</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>挑戰類型</td><td>具體問題</td><td>影響程度</td></tr>
</thead>
<tbody>
<tr>
<td><strong>跨分片 JOIN</strong></td><td>資料分散在不同物理節點</td><td>高：需網路聚合</td></tr>
<tr>
<td><strong>分片鍵不一致</strong></td><td>關聯表用不同分片策略</td><td>極高：可能全掃描</td></tr>
<tr>
<td><strong>聚合計算</strong></td><td>SUM、COUNT 需跨分片合併</td><td>中：可並行處理</td></tr>
<tr>
<td><strong>排序分頁</strong></td><td>ORDER BY + LIMIT 跨分片複雜</td><td>高：需全域排序</td></tr>
</tbody>
</table>
</div><h3 id="heading-6kej5rov5bcn54wn">解法對照</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>解法</td><td>適用場景</td><td>實作複雜度</td><td>效能影響</td></tr>
</thead>
<tbody>
<tr>
<td><strong>應用層 JOIN</strong></td><td>簡單關聯、可控資料量</td><td>中</td><td>多次網路往返</td></tr>
<tr>
<td><strong>分片鍵對齊</strong></td><td>強關聯實體（如 User-Order）</td><td>低</td><td>可能分佈不均</td></tr>
<tr>
<td><strong>反正規化</strong></td><td>讀多寫少、可接受冗餘</td><td>高（同步機制）</td><td>讀快寫慢</td></tr>
<tr>
<td><strong>CQRS 分離</strong></td><td>複雜查詢 vs 簡單寫入</td><td>高</td><td>查詢極快</td></tr>
<tr>
<td><strong>搜尋引擎</strong></td><td>全文檢索、複雜篩選</td><td>中</td><td>近即時延遲</td></tr>
<tr>
<td><strong>資料倉儲</strong></td><td>分析型查詢、歷史資料</td><td>中</td><td>批次延遲</td></tr>
</tbody>
</table>
</div><pre><code class="lang-mermaid">flowchart TD
  A[識別實體關係] --&gt; B{是否為組成關係?}

  B --&gt;|是| C[聚合根設計]
  B --&gt;|否| D{查詢頻率如何?}

  C --&gt; C1[複合主鍵&lt;br/&gt;級聯刪除&lt;br/&gt;事務邊界]

  D --&gt;|高頻| E{資料量級別?}
  D --&gt;|低頻| F[標準 FK 引用]

  E --&gt;|小量 &lt;10M| G[標準 JOIN]
  E --&gt;|中量 10M-100M| H{讀寫比例?}
  E --&gt;|大量 &gt;100M| I[必須分片]

  H --&gt;|讀多| J[考慮反正規化]
  H --&gt;|寫多| K[保持正規化+快取]

  I --&gt; L{能否對齊分片鍵?}
  L --&gt;|能| M[分片鍵對齊]
  L --&gt;|不能| N[應用層 JOIN&lt;br/&gt;或 CQRS]

  G --&gt; G1[建立適當索引&lt;br/&gt;監控查詢效能]
  J --&gt; J1[設計同步機制&lt;br/&gt;處理最終一致性]
  K --&gt; K1[Redis/快取層&lt;br/&gt;減少 DB 壓力]
  M --&gt; M1[可能犧牲分佈均勻性]
  N --&gt; N1[複雜度顯著增加&lt;br/&gt;需要專門架構]

  F --&gt; F1[可 NULL FK&lt;br/&gt;軟約束檢查]
</code></pre>
<h2 id="heading-5am5yuz5qgi5l6l5yig5p6q">實務案例分析</h2>
<h3 id="heading-1">案例 1：電商訂單系統</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>實體關係</td><td>語意判斷</td><td>設計決策</td><td>理由</td></tr>
</thead>
<tbody>
<tr>
<td>Order ← OrderItem</td><td>組成關係</td><td>複合主鍵 (order_id, line_no)</td><td>OrderItem 無獨立意義</td></tr>
<tr>
<td>Order → User</td><td>引用關係</td><td>獨立 FK，不級聯</td><td>User 有獨立生命週期</td></tr>
<tr>
<td>Order → Product</td><td>引用關係</td><td>FK + 快照欄位</td><td>Product 可能變更，需歷史記錄</td></tr>
</tbody>
</table>
</div><h3 id="heading-2">案例 2：社交媒體平台</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>實體關係</td><td>語意判斷</td><td>設計決策</td><td>理由</td></tr>
</thead>
<tbody>
<tr>
<td>Post ← Comment</td><td>組成關係</td><td>FK + 級聯刪除</td><td>Comment 依附於 Post</td></tr>
<tr>
<td>Post → User</td><td>引用關係</td><td>FK，不級聯</td><td>User 獨立存在</td></tr>
<tr>
<td>User ↔ User (Follow)</td><td>成員關係</td><td>中介表 follows</td><td>多對多關係</td></tr>
</tbody>
</table>
</div><h3 id="heading-3">案例 3：分片環境挑戰</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>場景</td><td>問題</td><td>解法</td><td>權衡</td></tr>
</thead>
<tbody>
<tr>
<td>跨用戶統計</td><td>無法按 user_id 分片</td><td>定期 ETL 到分析庫</td><td>延遲 vs 效能</td></tr>
<tr>
<td>商品搜尋</td><td>商品與訂單不同分片策略</td><td>Elasticsearch 同步</td><td>一致性 vs 查詢能力</td></tr>
<tr>
<td>實時推薦</td><td>需要用戶行為 + 商品資訊</td><td>Redis 快取 + 應用層合併</td><td>記憶體成本 vs 延遲</td></tr>
</tbody>
</table>
</div><h2 id="heading-55uj5o6n6iih5ysq5yyw5oyh5qiz">監控與優化指標</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>層面</td><td>關鍵指標</td><td>告警閾值建議</td><td>優化方向</td></tr>
</thead>
<tbody>
<tr>
<td><strong>JOIN 效能</strong></td><td>查詢執行時間、掃描行數</td><td>P95 &gt; 500ms</td><td>索引優化、查詢重寫</td></tr>
<tr>
<td><strong>跨分片成本</strong></td><td>網路往返次數、資料傳輸量</td><td>單查詢 &gt;3 次往返</td><td>批次查詢、快取</td></tr>
<tr>
<td><strong>一致性延遲</strong></td><td>反正規化同步延遲</td><td>\&gt;10 秒</td><td>事件驅動、並行處理</td></tr>
<tr>
<td><strong>快取命中率</strong></td><td>JOIN 結果快取效率</td><td>&lt;80%</td><td>快取策略調整</td></tr>
</tbody>
</table>
</div><h1 id="heading-57i957wq">總結</h1>
<p>架構演進路線</p>
<pre><code class="lang-mermaid">graph TD
  A[單機架構&lt;br/&gt;Web + DB 同機] --&gt; B[分層架構&lt;br/&gt;Web 層 + DB 層]
  B --&gt; C[加入負載均衡&lt;br/&gt;多 Web Server]
  C --&gt; D[資料庫讀寫分離&lt;br/&gt;Master + Replica]
  D --&gt; E[引入快取層&lt;br/&gt;Redis/Memcached]
  E --&gt; F[CDN 加速&lt;br/&gt;靜態資源分離]
  F --&gt; G[無狀態化&lt;br/&gt;Session 外部化]
  G --&gt; H[訊息佇列&lt;br/&gt;異步處理]
  H --&gt; I[資料庫分片&lt;br/&gt;水平擴展]

  A --&gt; A1[適用: 原型/小流量]
  B --&gt; B1[適用: 中等流量&lt;br/&gt;需要獨立擴展]
  C --&gt; C1[適用: 高併發讀取]
  D --&gt; D1[適用: 讀寫分離場景]
  E --&gt; E1[適用: 熱點資料]
  F --&gt; F1[適用: 全球化服務]
  G --&gt; G1[適用: 多實例部署]
  H --&gt; H1[適用: 解耦異步]
  I --&gt; I1[適用: 海量資料]
</code></pre>
<div class="hn-table">
<table>
<thead>
<tr>
<td>場景特徵</td><td>推薦方案</td><td>關鍵考量</td></tr>
</thead>
<tbody>
<tr>
<td>QPS &lt; 1K, 資料 &lt; 1GB</td><td>單機</td><td>簡單優先</td></tr>
<tr>
<td>QPS 1K-10K, 讀多寫少</td><td>讀寫分離 + 快取</td><td>成本效益</td></tr>
<tr>
<td>QPS &gt; 10K, 多地域</td><td>CDN + 分片</td><td>延遲與一致性</td></tr>
</tbody>
</table>
</div><pre><code class="lang-pgsql">// 使用 pg_database_size() 看資料庫的資料大小
<span class="hljs-keyword">SELECT</span> 
    datname <span class="hljs-keyword">as</span> database_name,
    pg_size_pretty(pg_database_size(datname)) <span class="hljs-keyword">as</span> size
<span class="hljs-keyword">FROM</span> pg_database
<span class="hljs-keyword">WHERE</span> datname = <span class="hljs-string">'your_database_name'</span>;
</code></pre>
<p>要怎知道讀多寫少? 或者啟用 Metrics 做監控。</p>
<pre><code class="lang-pgsql"><span class="hljs-comment">-- 查看資料庫整體讀寫統計</span>
<span class="hljs-keyword">SELECT</span> 
    datname <span class="hljs-keyword">as</span> database_name,
    tup_returned <span class="hljs-keyword">as</span> total_reads,      <span class="hljs-comment">-- 總讀取行數</span>
    tup_fetched <span class="hljs-keyword">as</span> fetched_reads,     <span class="hljs-comment">-- 實際取得行數  </span>
    tup_inserted <span class="hljs-keyword">as</span> total_inserts,    <span class="hljs-comment">-- 總插入行數</span>
    tup_updated <span class="hljs-keyword">as</span> total_updates,     <span class="hljs-comment">-- 總更新行數</span>
    tup_deleted <span class="hljs-keyword">as</span> total_deletes,     <span class="hljs-comment">-- 總刪除行數</span>
    (tup_inserted + tup_updated + tup_deleted) <span class="hljs-keyword">as</span> total_writes,
    <span class="hljs-keyword">CASE</span> 
        <span class="hljs-keyword">WHEN</span> (tup_inserted + tup_updated + tup_deleted) = <span class="hljs-number">0</span> <span class="hljs-keyword">THEN</span> <span class="hljs-string">'READ_ONLY'</span>
        <span class="hljs-keyword">ELSE</span> ROUND(
            tup_returned::<span class="hljs-type">numeric</span> / 
            (tup_inserted + tup_updated + tup_deleted)::<span class="hljs-type">numeric</span>, 
            <span class="hljs-number">2</span>
        )
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">as</span> read_write_ratio
<span class="hljs-keyword">FROM</span> pg_stat_database 
<span class="hljs-keyword">WHERE</span> datname = current_database();
</code></pre>
]]></content:encoded></item><item><title><![CDATA[淺談 Go Iterator]]></title><description><![CDATA[Go 1.23 引入了原生的 Iterator 支援，這是 Go 語言在函數式程式設計道路上的重要里程碑。這篇將分享 Go Iterator 的設計理念、使用方法和實踐。
什麼是 Iterator？
Iterator（迭代器）是一個**函數**，它將序列中的連續元素傳遞給 callback 函數（通常命名為 yield）。當序列結束或 yield 返回 false 時，函數會停止迭代。
在 Go 1.23 中，Iterator 是語言的原生特性，介面定義在 iter 套件中。
Iterator ...]]></description><link>https://ganhua.wang/go-iterator</link><guid isPermaLink="true">https://ganhua.wang/go-iterator</guid><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sat, 06 Sep 2025 05:31:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757136632083/80ffd84a-75dc-4920-8299-b68232cdb7df.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Go 1.23 引入了原生的 Iterator 支援，這是 Go 語言在函數式程式設計道路上的重要里程碑。這篇將分享 Go Iterator 的設計理念、使用方法和實踐。</p>
<h1 id="heading-iterator">什麼是 Iterator？</h1>
<p><strong>Iterator</strong>（迭代器）是一個**函數**，它將序列中的連續元素傳遞給 callback 函數（通常命名為 <code>yield</code>）。當序列結束或 yield 返回 false 時，函數會停止迭代。</p>
<p>在 Go 1.23 中，Iterator 是語言的原生特性，介面定義在 <a target="_blank" href="https://pkg.go.dev/iter#Seq"><code>iter</code> 套件</a>中。</p>
<h2 id="heading-iterator-1">Iterator 的核心設計</h2>
<h3 id="heading-6age5z6l5a6a576p">類型定義</h3>
<p>Go 定義了兩種主要的 Iterator 類型：</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Seq[V any] <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(V)</span> <span class="hljs-title">bool</span>)</span>
<span class="hljs-keyword">type</span> Seq2[K, V any] <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(K, V)</span> <span class="hljs-title">bool</span>)</span>
</code></pre>
<h3 id="heading-yield">yield 函數的語意設計</h3>
<p>yield 函數的核心語意是："產出並詢問是否繼續"</p>
<pre><code class="lang-mermaid">graph TD
    A[Iterator 開始] --&gt; B[計算/獲取下一個值]
    B --&gt; C[調用 yield value]
    C --&gt; D{yield 返回值}
    D --&gt;|true| E[繼續迭代]
    D --&gt;|false| F[停止迭代]
    E --&gt; B
    F --&gt; G[Iterator 結束]
</code></pre>
<pre><code class="lang-go"><span class="hljs-comment">// yield 函數返回值的意義：</span>
<span class="hljs-comment">// - true: 繼續迭代下一個元素</span>
<span class="hljs-comment">// - false: 停止迭代</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">myIterator</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">int</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(<span class="hljs-keyword">int</span>)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span>; i &lt;= <span class="hljs-number">5</span>; i++ {
            <span class="hljs-keyword">if</span> !yield(i) { <span class="hljs-comment">// 產出值並詢問是否繼續</span>
                <span class="hljs-keyword">return</span> <span class="hljs-comment">// 尊重消費者的決定</span>
            }
        }
    }
}
</code></pre>
<h3 id="heading-5y2u5l2c5byp5o6n5yi25rwb">協作式控制流</h3>
<p>Iterator 的設計體現了生產者和消費者之間的協作關係：</p>
<h4 id="heading-55sf55si6icf6kaw6kes55qe6kqe5osp">生產者視角的語意</h4>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">fibonacci</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">int</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(<span class="hljs-keyword">int</span>)</span> <span class="hljs-title">bool</span>)</span> {
        a, b := <span class="hljs-number">0</span>, <span class="hljs-number">1</span>
        <span class="hljs-keyword">for</span> {
            <span class="hljs-comment">// 語意："我計算出了一個值，你要嗎？"</span>
            <span class="hljs-keyword">if</span> !yield(a) {
                <span class="hljs-comment">// 語意："好的，你不要了，我停止生產"</span>
                <span class="hljs-keyword">return</span>
            }
            <span class="hljs-comment">// 語意："你還要，我繼續計算下一個"</span>
            a, b = b, a+b
        }
    }
}
</code></pre>
<h4 id="heading-5rai6lk76icf6kaw6kes55qe6kqe5osp">消費者視角的語意</h4>
<pre><code class="lang-go"><span class="hljs-comment">// range 迴圈的隱含語意</span>
<span class="hljs-keyword">for</span> value := <span class="hljs-keyword">range</span> fibonacci() {
    fmt.Println(value)
    <span class="hljs-keyword">if</span> value &gt; <span class="hljs-number">100</span> {
        <span class="hljs-comment">// 隱含語意："我不需要更多了"</span>
        <span class="hljs-keyword">break</span> <span class="hljs-comment">// 這會讓 yield 返回 false</span>
    }
    <span class="hljs-comment">// 隱含語意："我處理完了，請給我下一個"</span>
}
</code></pre>
<pre><code class="lang-mermaid">sequenceDiagram
    participant P as 生產者 (Iterator)
    participant C as 消費者 (range loop)

    P-&gt;&gt;C: yield(value1) - "我有一個值，你要嗎？"
    C-&gt;&gt;P: return true - "要，請繼續"
    P-&gt;&gt;C: yield(value2) - "下一個值"
    C-&gt;&gt;P: return true - "繼續"
    P-&gt;&gt;C: yield(value3) - "又一個值"
    C-&gt;&gt;P: return false - "夠了，停止吧"
    P-&gt;&gt;P: return - "好的，我停止"
</code></pre>
<h2 id="heading-5z65pys5l255so5pa55rov">基本使用方法</h2>
<h3 id="heading-1">1. 簡單的數值範圍迭代器</h3>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"iter"</span>
)

<span class="hljs-comment">// 整數範圍迭代器</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">rangeSeq</span><span class="hljs-params">(start, end <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">int</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(<span class="hljs-keyword">int</span>)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> i := start; i &lt;= end; i++ {
            <span class="hljs-keyword">if</span> !yield(i) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// 使用 range 語法遍歷</span>
    <span class="hljs-keyword">for</span> num := <span class="hljs-keyword">range</span> rangeSeq(<span class="hljs-number">1</span>, <span class="hljs-number">5</span>) {
        fmt.Println(num) <span class="hljs-comment">// 輸出: 1, 2, 3, 4, 5</span>
    }
}
</code></pre>
<p><strong>實際用途：</strong></p>
<ul>
<li><p>測試資料生成：快速生成測試用的數字序列</p>
</li>
<li><p>分頁處理：生成頁碼序列</p>
</li>
</ul>
<h3 id="heading-2-key-value-pair">2. Key-Value Pair 迭代器</h3>
<pre><code class="lang-go"><span class="hljs-comment">// Map 迭代器</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">mapSeq</span>[<span class="hljs-title">K</span> <span class="hljs-title">comparable</span>, <span class="hljs-title">V</span> <span class="hljs-title">any</span>]<span class="hljs-params">(m <span class="hljs-keyword">map</span>[K]V)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq2</span>[<span class="hljs-title">K</span>, <span class="hljs-title">V</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(K, V)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> k, v := <span class="hljs-keyword">range</span> m {
            <span class="hljs-keyword">if</span> !yield(k, v) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}

<span class="hljs-comment">// 使用</span>
data := <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">int</span>{<span class="hljs-string">"apple"</span>: <span class="hljs-number">5</span>, <span class="hljs-string">"banana"</span>: <span class="hljs-number">3</span>, <span class="hljs-string">"cherry"</span>: <span class="hljs-number">8</span>}
<span class="hljs-keyword">for</span> key, value := <span class="hljs-keyword">range</span> mapSeq(data) {
    fmt.Printf(<span class="hljs-string">"%s: %d\n"</span>, key, value)
}
</code></pre>
<p><strong>實際用途：</strong></p>
<ul>
<li><p>配置處理：遍歷配置項目</p>
</li>
<li><p>資料轉換：將 map 轉換為其他格式</p>
</li>
<li><p>條件過濾：在遍歷過程中進行篩選</p>
</li>
</ul>
<h3 id="heading-3-iterator">3. 集合類型的 Iterator</h3>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Set[V comparable] <span class="hljs-keyword">struct</span> {
    items <span class="hljs-keyword">map</span>[V]<span class="hljs-keyword">struct</span>{}
}

<span class="hljs-comment">// 按照命名慣例，集合的迭代器方法命名為 All</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *Set[V])</span> <span class="hljs-title">All</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">V</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(V)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> s.items {
            <span class="hljs-keyword">if</span> !yield(item) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}

<span class="hljs-comment">// 使用</span>
set := &amp;Set[<span class="hljs-keyword">string</span>]{items: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">struct</span>{}{
    <span class="hljs-string">"apple"</span>: {}, <span class="hljs-string">"banana"</span>: {}, <span class="hljs-string">"cherry"</span>: {},
}}

<span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> set.All() {
    fmt.Println(item)
}
</code></pre>
<h2 id="heading-6ycy6zqo5oej55so">進階應用</h2>
<h3 id="heading-1-pull">1. Pull 函數 - 手動控制迭代</h3>
<pre><code class="lang-mermaid">graph LR
    A[Push Iterator&lt;br/&gt;seq] --&gt; B[iter.Pull]
    B --&gt; C[next function]
    B --&gt; D[stop function]
    C --&gt; E[手動拉取值]
    D --&gt; F[清理資源]
</code></pre>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">demonstratePull</span><span class="hljs-params">()</span></span> {
    seq := rangeSeq(<span class="hljs-number">1</span>, <span class="hljs-number">5</span>)

    <span class="hljs-comment">// 轉換為 Pull 迭代器</span>
    next, stop := iter.Pull(seq)
    <span class="hljs-keyword">defer</span> stop() <span class="hljs-comment">// 重要：必須調用 stop 來清理資源</span>

    <span class="hljs-comment">// 手動拉取值</span>
    <span class="hljs-keyword">for</span> {
        value, ok := next()
        <span class="hljs-keyword">if</span> !ok {
            <span class="hljs-keyword">break</span> <span class="hljs-comment">// 序列結束</span>
        }
        fmt.Printf(<span class="hljs-string">"Pulled: %d\n"</span>, value)

        <span class="hljs-comment">// 可以在這裡添加複雜的邏輯</span>
        <span class="hljs-keyword">if</span> value == <span class="hljs-number">3</span> {
            fmt.Println(<span class="hljs-string">"Stopping early"</span>)
            <span class="hljs-keyword">break</span>
        }
    }
}
</code></pre>
<p><strong>實際用途：</strong></p>
<ul>
<li><p>API 分頁處理：需要根據回應決定是否繼續</p>
</li>
<li><p>資料庫批次讀取：根據記憶體使用量控制讀取</p>
</li>
<li><p>檔案處理：逐行處理大檔案，可隨時停止</p>
</li>
</ul>
<h3 id="heading-2">2. 配對迭代器</h3>
<pre><code class="lang-go"><span class="hljs-comment">// 將序列中的值配對</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Pairs</span>[<span class="hljs-title">V</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[V])</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq2</span>[<span class="hljs-title">V</span>, <span class="hljs-title">V</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(V, V)</span> <span class="hljs-title">bool</span>)</span> {
        next, stop := iter.Pull(seq)
        <span class="hljs-keyword">defer</span> stop()

        <span class="hljs-keyword">for</span> {
            v1, ok1 := next()
            <span class="hljs-keyword">if</span> !ok1 {
                <span class="hljs-keyword">return</span>
            }

            v2, ok2 := next()
            <span class="hljs-comment">// 如果 ok2 為 false，v2 是零值；產生最後一對</span>
            <span class="hljs-keyword">if</span> !yield(v1, v2) {
                <span class="hljs-keyword">return</span>
            }

            <span class="hljs-keyword">if</span> !ok2 {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}

<span class="hljs-comment">// 使用範例：座標處理</span>
coordinates := []<span class="hljs-keyword">float64</span>{<span class="hljs-number">1.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">3.0</span>, <span class="hljs-number">4.0</span>, <span class="hljs-number">5.0</span>, <span class="hljs-number">6.0</span>}
coordSeq := SliceSeq(coordinates)

<span class="hljs-keyword">for</span> x, y := <span class="hljs-keyword">range</span> Pairs(coordSeq) {
    point := Point{X: x, Y: y}
    drawPoint(point)
}
<span class="hljs-comment">// 結果：(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)</span>
</code></pre>
<h3 id="heading-3-filter-map">3. Filter 和 Map 操作</h3>
<pre><code class="lang-go"><span class="hljs-comment">// Filter 迭代器</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Filter</span>[<span class="hljs-title">V</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[V], predicate <span class="hljs-keyword">func</span>(V)</span> <span class="hljs-title">bool</span>) <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">V</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(V)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> value := <span class="hljs-keyword">range</span> seq {
            <span class="hljs-keyword">if</span> predicate(value) {
                <span class="hljs-keyword">if</span> !yield(value) {
                    <span class="hljs-keyword">return</span>
                }
            }
        }
    }
}

<span class="hljs-comment">// Map 迭代器</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Map</span>[<span class="hljs-title">T</span>, <span class="hljs-title">U</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[T], mapper <span class="hljs-keyword">func</span>(T)</span> <span class="hljs-title">U</span>) <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">U</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(U)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> value := <span class="hljs-keyword">range</span> seq {
            <span class="hljs-keyword">if</span> !yield(mapper(value)) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}

<span class="hljs-comment">// 鏈式操作</span>
numbers := rangeSeq(<span class="hljs-number">1</span>, <span class="hljs-number">10</span>)
evenNumbers := Filter(numbers, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(n <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">bool</span></span> { <span class="hljs-keyword">return</span> n%<span class="hljs-number">2</span> == <span class="hljs-number">0</span> })
squares := Map(evenNumbers, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(n <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">int</span></span> { <span class="hljs-keyword">return</span> n * n })

<span class="hljs-keyword">for</span> square := <span class="hljs-keyword">range</span> squares {
    fmt.Printf(<span class="hljs-string">"%d "</span>, square) <span class="hljs-comment">// 4 16 36 64 100</span>
}
</code></pre>
<pre><code class="lang-mermaid">graph LR
    A[原始序列&lt;br/&gt;1,2,3,4,5,6,7,8,9,10] --&gt; B[Filter&lt;br/&gt;偶數過濾]
    B --&gt; C[過濾結果&lt;br/&gt;2,4,6,8,10]
    C --&gt; D[Map&lt;br/&gt;平方轉換]
    D --&gt; E[最終結果&lt;br/&gt;4,16,36,64,100]
</code></pre>
<h3 id="heading-4">4. 無限序列</h3>
<pre><code class="lang-go"><span class="hljs-comment">// 費波那契數列</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Fibonacci</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">int</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(<span class="hljs-keyword">int</span>)</span> <span class="hljs-title">bool</span>)</span> {
        a, b := <span class="hljs-number">0</span>, <span class="hljs-number">1</span>
        <span class="hljs-keyword">for</span> {
            <span class="hljs-keyword">if</span> !yield(a) {
                <span class="hljs-keyword">return</span>
            }
            a, b = b, a+b
        }
    }
}

<span class="hljs-comment">// 使用時需要手動控制停止</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">useFibonacci</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">for</span> num := <span class="hljs-keyword">range</span> Fibonacci() {
        <span class="hljs-keyword">if</span> num &gt; <span class="hljs-number">1000</span> {
            <span class="hljs-keyword">break</span>
        }
        fmt.Println(num)
    }
}

<span class="hljs-comment">// 產生唯一 ID</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">UniqueIDs</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">string</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(<span class="hljs-keyword">string</span>)</span> <span class="hljs-title">bool</span>)</span> {
        counter := <span class="hljs-number">0</span>
        <span class="hljs-keyword">for</span> {
            id := fmt.Sprintf(<span class="hljs-string">"ID_%d_%d"</span>, time.Now().Unix(), counter)
            <span class="hljs-keyword">if</span> !yield(id) {
                <span class="hljs-keyword">return</span>
            }
            counter++
        }
    }
}
</code></pre>
<p><strong>實際用途：</strong></p>
<ul>
<li><p>測試資料：生成無限測試資料</p>
</li>
<li><p>演算法實現：實現需要無限序列的演算法</p>
</li>
</ul>
<h3 id="heading-5">5. 批次處理</h3>
<pre><code class="lang-go"><span class="hljs-comment">// 將序列分批處理</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Batch</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[T], size <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[[]<span class="hljs-title">T</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>([]T)</span> <span class="hljs-title">bool</span>)</span> {
        batch := <span class="hljs-built_in">make</span>([]T, <span class="hljs-number">0</span>, size)

        <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> seq {
            batch = <span class="hljs-built_in">append</span>(batch, item)
            <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(batch) == size {
                <span class="hljs-keyword">if</span> !yield(batch) {
                    <span class="hljs-keyword">return</span>
                }
                batch = batch[:<span class="hljs-number">0</span>] <span class="hljs-comment">// 重用 slice</span>
            }
        }

        <span class="hljs-comment">// 處理剩餘元素</span>
        <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(batch) &gt; <span class="hljs-number">0</span> {
            yield(batch)
        }
    }
}
</code></pre>
<pre><code class="lang-mermaid">graph LR
    A[原始序列&lt;br/&gt;1,2,3,4,5,6,7,8,9] --&gt; B[Batch size=3]
    B --&gt; C[批次1&lt;br/&gt;1,2,3]
    B --&gt; D[批次2&lt;br/&gt;4,5,6]
    B --&gt; E[批次3&lt;br/&gt;7,8,9]
</code></pre>
<ol start="6">
<li><h3 id="heading-5bi455so6lyu5yqp5ye95pw4">常用輔助函數</h3>
</li>
</ol>
<pre><code class="lang-go"><span class="hljs-comment">// 將 slice 轉換為 Iterator</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SliceSeq</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>]<span class="hljs-params">(slice []T)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">T</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(T)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> _, item := <span class="hljs-keyword">range</span> slice {
            <span class="hljs-keyword">if</span> !yield(item) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}

<span class="hljs-comment">// 將 Iterator 收集為 slice</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Collect</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[T])</span> []<span class="hljs-title">T</span></span> {
    <span class="hljs-keyword">var</span> result []T
    <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> seq {
        result = <span class="hljs-built_in">append</span>(result, item)
    }
    <span class="hljs-keyword">return</span> result
}

<span class="hljs-comment">// 取前 n 個元素</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Take</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[T], n <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">T</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(T)</span> <span class="hljs-title">bool</span>)</span> {
        count := <span class="hljs-number">0</span>
        <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> seq {
            <span class="hljs-keyword">if</span> count &gt;= n {
                <span class="hljs-keyword">return</span>
            }
            <span class="hljs-keyword">if</span> !yield(item) {
                <span class="hljs-keyword">return</span>
            }
            count++
        }
    }
}

<span class="hljs-comment">// 跳過前 n 個元素</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Skip</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[T], n <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">T</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(T)</span> <span class="hljs-title">bool</span>)</span> {
        count := <span class="hljs-number">0</span>
        <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> seq {
            <span class="hljs-keyword">if</span> count &lt; n {
                count++
                <span class="hljs-keyword">continue</span>
            }
            <span class="hljs-keyword">if</span> !yield(item) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}
</code></pre>
<h2 id="heading-5zg95zcn5owj5l6l">命名慣例</h2>
<p>根據官方文件，Iterator 的命名有以下慣例：</p>
<h3 id="heading-1-1">1. 集合迭代器</h3>
<pre><code class="lang-go"><span class="hljs-comment">// 集合類型的主要迭代器命名為 All</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *Set[V])</span> <span class="hljs-title">All</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">V</span>]</span> { ... }

<span class="hljs-comment">// 多種序列時，名稱指示具體序列</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *Country)</span> <span class="hljs-title">Cities</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[*<span class="hljs-title">City</span>]</span> { ... }
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *Country)</span> <span class="hljs-title">Languages</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">string</span>]</span> { ... }
</code></pre>
<h3 id="heading-2-1">2. 配置參數的迭代器</h3>
<pre><code class="lang-go"><span class="hljs-comment">// 需要額外配置的迭代器</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(m *Map[K, V])</span> <span class="hljs-title">Scan</span><span class="hljs-params">(min, max K)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq2</span>[<span class="hljs-title">K</span>, <span class="hljs-title">V</span>]</span> { ... }
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Split</span><span class="hljs-params">(s, sep <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">string</span>]</span> { ... }
</code></pre>
<h3 id="heading-3">3. 不同順序的迭代器</h3>
<pre><code class="lang-go"><span class="hljs-comment">// 指示遍歷順序</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(l *List[V])</span> <span class="hljs-title">All</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">V</span>]</span> { ... }        <span class="hljs-comment">// 從頭到尾</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(l *List[V])</span> <span class="hljs-title">Backward</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">V</span>]</span> { ... }   <span class="hljs-comment">// 從尾到頭</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Preorder</span><span class="hljs-params">(root Node)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">Node</span>]</span> { ... }    <span class="hljs-comment">// 前序遍歷</span>
</code></pre>
<h2 id="heading-5ocn6io96icd6yep">性能考量</h2>
<h3 id="heading-5bu26ygy5rgc5yc855qe5ysq5yui">延遲求值的優勢</h3>
<pre><code class="lang-go"><span class="hljs-comment">// 延遲求值範例：只計算需要的部分</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">expensiveComputation</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">int</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(<span class="hljs-keyword">int</span>)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span>; i &lt;= <span class="hljs-number">1000000</span>; i++ {
            <span class="hljs-comment">// 昂貴的計算</span>
            result := complexCalculation(i)
            <span class="hljs-keyword">if</span> !yield(result) {
                <span class="hljs-keyword">return</span> <span class="hljs-comment">// 可以提早停止，節省計算</span>
            }
        }
    }
}

<span class="hljs-comment">// 只取前 5 個結果，剩下的不會計算</span>
results := Take(expensiveComputation(), <span class="hljs-number">5</span>)
<span class="hljs-keyword">for</span> result := <span class="hljs-keyword">range</span> results {
    fmt.Println(result)
}
</code></pre>
<pre><code class="lang-mermaid">graph TD
    A[開始計算] --&gt; B{需要更多結果？}
    B --&gt;|是| C[計算下一個]
    B --&gt;|否| D[停止計算&lt;br/&gt;節省資源]
    C --&gt; E[yield 結果]
    E --&gt; F{消費者要繼續？}
    F --&gt;|是| B
    F --&gt;|否| D
</code></pre>
<h3 id="heading-6kiy5oa26auu5l255so5pwi546h">記憶體使用效率</h3>
<pre><code class="lang-go">
<span class="hljs-comment">// 傳統方式：需要將所有結果載入記憶體</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">processAllAtOnce</span><span class="hljs-params">()</span> []<span class="hljs-title">ProcessedData</span></span> {
    data := loadLargeDataset() <span class="hljs-comment">// 可能很大</span>
    <span class="hljs-keyword">var</span> results []ProcessedData
    <span class="hljs-keyword">for</span> _, item := <span class="hljs-keyword">range</span> data {
        results = <span class="hljs-built_in">append</span>(results, process(item))
    }
    <span class="hljs-keyword">return</span> results <span class="hljs-comment">// 記憶體使用量可以說"翻倍"</span>
}

<span class="hljs-comment">// Iterator 方式：串流處理</span>
<span class="hljs-comment">// 用到哪，拉取到哪</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">processStream</span><span class="hljs-params">()</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">ProcessedData</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(ProcessedData)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> loadDataStream() {
            processed := process(item)
            <span class="hljs-keyword">if</span> !yield(processed) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}
</code></pre>
<h2 id="heading-5o6o6jam55qe6yyv6kqk6jmv55cg5pa55byp">推薦的錯誤處理方式</h2>
<pre><code class="lang-go"><span class="hljs-comment">// 方法 1：使用 Result 類型</span>
<span class="hljs-keyword">type</span> Result[T any] <span class="hljs-keyword">struct</span> {
    Value T
    Error error
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ProcessWithErrors</span>[<span class="hljs-title">T</span>, <span class="hljs-title">U</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[T], processor <span class="hljs-keyword">func</span>(T)</span> <span class="hljs-params">(U, error)</span>) <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">Result</span>[<span class="hljs-title">U</span>]]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(Result[U])</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> seq {
            value, err := processor(item)
            <span class="hljs-keyword">if</span> !yield(Result[U]{Value: value, Error: err}) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}

<span class="hljs-comment">// 方法 2：分離錯誤處理</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ProcessSafe</span>[<span class="hljs-title">T</span>, <span class="hljs-title">U</span> <span class="hljs-title">any</span>]<span class="hljs-params">(seq iter.Seq[T], processor <span class="hljs-keyword">func</span>(T)</span> <span class="hljs-params">(U, error)</span>) <span class="hljs-params">(iter.Seq[U], error)</span></span> {
    <span class="hljs-keyword">var</span> firstError error

    filtered := <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(U)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> seq {
            value, err := processor(item)
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">if</span> firstError == <span class="hljs-literal">nil</span> {
                    firstError = err
                }
                <span class="hljs-keyword">continue</span>
            }
            <span class="hljs-keyword">if</span> !yield(value) {
                <span class="hljs-keyword">return</span>
            }
        }
    }

    <span class="hljs-keyword">return</span> filtered, firstError
}
</code></pre>
<h2 id="heading-iterator-pattern">與傳統 Iterator Pattern 的比較</h2>
<h3 id="heading-55u45zcm6bue">相同點</h3>
<ul>
<li><p>目的相同：提供統一的方式遍歷集合</p>
</li>
<li><p>封裝性：隱藏集合內部實現</p>
</li>
<li><p>順序訪問：按順序訪問元素</p>
</li>
</ul>
<h3 id="heading-5li76kab5beu55ww">主要差異</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>特性</td><td>傳統 Iterator Pattern</td><td>Go Iterator</td></tr>
</thead>
<tbody>
<tr>
<td>實現方式</td><td>物件導向（介面 + 類別）</td><td>函數式（closure + yield）</td></tr>
<tr>
<td>狀態管理</td><td>Iterator 物件維護狀態</td><td>closure 維護狀態</td></tr>
<tr>
<td>語法支援</td><td>手動調用 Next(), HasNext()</td><td>原生 range 語法</td></tr>
<tr>
<td>記憶體開銷</td><td>需要創建 Iterator 物件</td><td>只有函數調用開銷</td></tr>
<tr>
<td>組合性</td><td>較難組合多個迭代器</td><td>容易組合和鏈接</td></tr>
<tr>
<td>類型安全</td><td>依賴語言的泛型支援</td><td>完整的泛型支援</td></tr>
</tbody>
</table>
</div><h3 id="heading-iterator-pattern-1">傳統 Iterator Pattern</h3>
<pre><code class="lang-go"><span class="hljs-comment">// 傳統方式</span>
<span class="hljs-keyword">type</span> Iterator[T any] <span class="hljs-keyword">interface</span> {
    HasNext() <span class="hljs-keyword">bool</span>
    Next() T
}

<span class="hljs-keyword">type</span> SliceIterator[T any] <span class="hljs-keyword">struct</span> {
    slice []T
    index <span class="hljs-keyword">int</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(it *SliceIterator[T])</span> <span class="hljs-title">HasNext</span><span class="hljs-params">()</span> <span class="hljs-title">bool</span></span> {
    <span class="hljs-keyword">return</span> it.index &lt; <span class="hljs-built_in">len</span>(it.slice)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(it *SliceIterator[T])</span> <span class="hljs-title">Next</span><span class="hljs-params">()</span> <span class="hljs-title">T</span></span> {
    <span class="hljs-keyword">if</span> it.HasNext() {
        value := it.slice[it.index]
        it.index++
        <span class="hljs-keyword">return</span> value
    }
    <span class="hljs-keyword">var</span> zero T
    <span class="hljs-keyword">return</span> zero
}

<span class="hljs-comment">// 使用</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">useTraditionalIterator</span><span class="hljs-params">()</span></span> {
    data := []<span class="hljs-keyword">int</span>{<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>}
    iterator := &amp;SliceIterator[<span class="hljs-keyword">int</span>]{slice: data}

    <span class="hljs-keyword">for</span> iterator.HasNext() {
        fmt.Println(iterator.Next())
    }
}
</code></pre>
<h3 id="heading-go-iterator">Go Iterator</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SliceSeq</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>]<span class="hljs-params">(slice []T)</span> <span class="hljs-title">iter</span>.<span class="hljs-title">Seq</span>[<span class="hljs-title">T</span>]</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(yield <span class="hljs-keyword">func</span>(T)</span> <span class="hljs-title">bool</span>)</span> {
        <span class="hljs-keyword">for</span> _, item := <span class="hljs-keyword">range</span> slice {
            <span class="hljs-keyword">if</span> !yield(item) {
                <span class="hljs-keyword">return</span>
            }
        }
    }
}

<span class="hljs-comment">// 使用</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">useGoIterator</span><span class="hljs-params">()</span></span> {
    data := []<span class="hljs-keyword">int</span>{<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>}

    <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> SliceSeq(data) {
        fmt.Println(item)
    }
}
</code></pre>
<pre><code class="lang-mermaid">graph TD
    subgraph "傳統 Iterator Pattern"
        A[Iterator 介面] --&gt; B[HasNext 方法]
        A --&gt; C[Next 方法]
        B --&gt; D[手動控制迴圈]
        C --&gt; D
    end

    subgraph "Go Iterator"
        E[iter.Seq 函數] --&gt; F[yield 函數]
        F --&gt; G[原生 range 語法]
        G --&gt; H[自動控制流程]
    end
</code></pre>
<h2 id="heading-go-iterator-1">Go 標準庫中已經實現的 Iterator</h2>
<p>除了下述的 database Next()、<a target="_blank" href="https://pkg.go.dev/path/filepath#Walk">filepath.Walk(Dir)</a>、<a target="_blank" href="https://pkg.go.dev/sync#Map.Range">sync Map Range()</a>。其實更早之前都已經提供了 Iterator 的自我實現，也不難發現現有的 iterator 設計跟使用方式都不相同，所以 Go 才想統一標準。</p>
<h2 id="heading-go-database-next">Go database 的 Next()</h2>
<p>Go 資料庫的 <a target="_blank" href="https://cs.opensource.google/go/go/+/refs/tags/go1.25.1:src/database/sql/sql.go;l=3023"><code>Next()</code></a> 實現了與 Iterator 相同的迭代模式和控制流語義。</p>
<h3 id="heading-rowsnexthttprowsnext"><a target="_blank" href="http://rows.Next">rows.Next</a>() 的實際行為</h3>
<pre><code class="lang-go"><span class="hljs-comment">// rows.Next() 的實際行為</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(rs *Rows)</span> <span class="hljs-title">Next</span><span class="hljs-params">()</span> <span class="hljs-title">bool</span></span> {
    <span class="hljs-comment">// 1. 釋放上一行的記憶體鎖定</span>
    rs.closemuRUnlockIfHeldByScan()

    <span class="hljs-comment">// 2. 檢查 context 是否已取消</span>
    <span class="hljs-keyword">if</span> rs.contextDone.Load() != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }

    <span class="hljs-comment">// 3. 嘗試讀取下一行（這裡才真正從網路/磁碟讀取）</span>
    <span class="hljs-keyword">var</span> doClose, ok <span class="hljs-keyword">bool</span>
    withLock(rs.closemu.RLocker(), <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        doClose, ok = rs.nextLocked() <span class="hljs-comment">// 關鍵：這裡才讀取下一筆</span>
    })

    <span class="hljs-comment">// 4. 如果需要關閉就關閉</span>
    <span class="hljs-keyword">if</span> doClose {
        rs.Close()
    }

    <span class="hljs-keyword">return</span> ok
}
</code></pre>
<h3 id="heading-5am6zqb55qe6loh5paz5rwb6iih5o6n5yi25rwb">實際的資料流與控制流</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">demonstrateActualFlow</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// 1. Query 只是建立連線和發送 SQL，但不等待所有結果</span>
    rows, err := db.Query(<span class="hljs-string">"SELECT * FROM huge_table"</span>) <span class="hljs-comment">// 只發送查詢，不讀取資料</span>
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatal(err)
    }
    <span class="hljs-keyword">defer</span> rows.Close()

    fmt.Println(<span class="hljs-string">"查詢已發送，但還沒讀取任何資料"</span>)

    <span class="hljs-comment">// 2. 第一次 Next() 開始從緩衝區讀取，緩衝區空時才觸發網路讀取</span>
    <span class="hljs-keyword">if</span> rows.Next() { <span class="hljs-comment">// 可能觸發網路讀取，填充緩衝區</span>
        fmt.Println(<span class="hljs-string">"讀取到第一筆資料"</span>)

        <span class="hljs-keyword">var</span> id <span class="hljs-keyword">int</span>
        <span class="hljs-keyword">var</span> name <span class="hljs-keyword">string</span>
        rows.Scan(&amp;id, &amp;name) <span class="hljs-comment">// 掃描剛才讀取的資料</span>
        fmt.Printf(<span class="hljs-string">"第一筆: %d, %s\n"</span>, id, name)
    }

    <span class="hljs-comment">// 3. 每次 Next() 都會讀取下一筆</span>
    <span class="hljs-keyword">if</span> rows.Next() { <span class="hljs-comment">// 通常從緩衝區直接讀取</span>
        fmt.Println(<span class="hljs-string">"讀取到第二筆資料"</span>)
        <span class="hljs-comment">// ... 掃描第二筆</span>
    }

    <span class="hljs-comment">// 4. 如果我們在這裡 early break，剩下的資料不會被讀取</span>
    fmt.Println(<span class="hljs-string">"停止讀取，剩下的資料不會從資料庫傳輸"</span>)
}
</code></pre>
<pre><code class="lang-mermaid">sequenceDiagram
    participant A as 應用程式
    participant D as lib/pq驅動
    participant B as bufio緩衝區
    participant TCP as TCP連接
    participant DB as PostgreSQL

    A-&gt;&gt;D: db.Query("SELECT ...")
    D-&gt;&gt;TCP: 發送 SQL 查詢
    TCP-&gt;&gt;DB: 傳輸查詢
    DB-&gt;&gt;DB: 開始執行查詢
    DB-&gt;&gt;TCP: 逐行發送 DataRow 消息
    Note over TCP: TCP將多個小消息組合成網絡包
    D-&gt;&gt;A: 返回 rows 物件

    Note over A: 此時還沒讀取任何資料

    A-&gt;&gt;D: rows.Next()
    D-&gt;&gt;B: 檢查緩衝區
    Note over B: 緩衝區空，需要網絡讀取
    D-&gt;&gt;TCP: 讀取網絡數據
    TCP-&gt;&gt;D: 返回網路包(4-8KB，包含多行)
    D-&gt;&gt;B: 數據存入 bufio 緩衝區
    D-&gt;&gt;D: 從緩衝區解析第一行
    D-&gt;&gt;A: 返回 true

    A-&gt;&gt;D: rows.Scan(&amp;data)
    D-&gt;&gt;A: 返回掃描的資料

    A-&gt;&gt;D: rows.Next()
    D-&gt;&gt;B: 檢查緩衝區
    Note over B: 從緩衝區直接讀取第二行
    D-&gt;&gt;D: 解析第二行數據
    D-&gt;&gt;A: 返回 true

    A-&gt;&gt;D: rows.Next()
    D-&gt;&gt;B: 檢查緩衝區
    Note over B: 從緩衝區直接讀取第三行
    D-&gt;&gt;A: 返回 true

    Note over A: 多次 Next() 都從緩衝區讀取...

    A-&gt;&gt;A: break (停止迭代)
    Note over B: 緩衝區可能還有未讀數據
    Note over TCP: TCP連接上可能還有未讀包
    Note over DB: PostgreSQL可能仍在發送數據
</code></pre>
<h3 id="heading-libpq">lib/pq 的實做方式</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">networkBehavior</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// PostgreSQL (lib/pq)</span>
    rows, _ := pgDB.Query(<span class="hljs-string">"SELECT * FROM million_rows"</span>)

    <span class="hljs-comment">// lib/pq 實際行為：</span>
    <span class="hljs-comment">// 1. 發送查詢到 PostgreSQL</span>
    <span class="hljs-comment">// 2. PostgreSQL 開始執行查詢並逐行發送結果</span>
    <span class="hljs-comment">// 3. TCP 將多個小的 DataRow 消息組合成 package（通常4-8KB）</span>
    <span class="hljs-comment">// 4. lib/pq 的 bufio.Reader 緩衝這些package</span>
    <span class="hljs-comment">// 5. rows.Next() 從 bufio 緩衝區逐行解析，緩衝區空時才觸發網路讀取</span>

    count := <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> rows.Next() { <span class="hljs-comment">// 每次 Next() 會：</span>
        <span class="hljs-comment">// - 從 bufio 緩衝區讀取並解析一行（通常很快）</span>
        <span class="hljs-comment">// - 當緩衝區數據用完時，觸發網路讀取下一個package（偶爾較慢）</span>
        <span class="hljs-comment">// - 注意：不是應用層的批次緩存，而是網路層的被動緩衝</span>

        <span class="hljs-keyword">var</span> data <span class="hljs-keyword">string</span>
        rows.Scan(&amp;data)
        count++

        <span class="hljs-keyword">if</span> count &gt;= <span class="hljs-number">10</span> {
            <span class="hljs-keyword">break</span> <span class="hljs-comment">// PostgreSQL 可能仍在發送資料，但客戶端停止讀取</span>
                  <span class="hljs-comment">// 連接上可能還有未讀的網路資料</span>
        }
    }
}
</code></pre>
<h1 id="heading-57i957wq">總結</h1>
<h3 id="heading-8joryaqkuaguowgw9semfvyoq">🎯 <strong>核心影響</strong></h3>
<p>Go 1.23 引入統一的 Iterator 標準是一個<strong>重大的生態系統改進</strong>，將混亂的迭代模式統一為 <code>iter.Seq[T]</code> 和 <code>range</code> 語法，帶來函數式程式設計能力和更好的資源效率。</p>
<h3 id="heading-4pyficoq5li76kab5aw96jmvkio">✅ <strong>主要好處</strong></h3>
<ul>
<li><p><strong>統一體驗</strong>：所有迭代都用相同的 <code>range</code> 語法</p>
</li>
<li><p><strong>函數式能力</strong>：支援 Map、Filter、組合等操作</p>
</li>
<li><p><strong>性能優化</strong>：延遲求值、恆定記憶體使用、提早退出</p>
</li>
<li><p><strong>開發效率</strong>：降低學習成本、提升代碼可讀性</p>
</li>
</ul>
<h3 id="heading-4pqg77ipicoq545am5oyr5oiwkio">⚠️ <strong>現實挑戰</strong></h3>
<ul>
<li><p><strong>過渡期混亂</strong>：新舊兩套 API 並存，開發者需要同時掌握</p>
</li>
<li><p><strong>決策負擔</strong>：每次都要選擇用新方法還是舊方法</p>
</li>
<li><p><strong>生態系統滯後</strong>：標準庫和第三方庫需要時間適應</p>
</li>
<li><p><strong>學習資源落差</strong>：教學材料、Stack Overflow 答案新舊混雜</p>
</li>
</ul>
<p>這是 Go 語言<strong>成熟度的重要里程碑</strong>，短期內會帶來一些混亂和學習成本，但長期來看將大幅提升 Go 的表達能力和開發體驗。關鍵是給生態系統足夠的時間來適應和標準化。</p>
<p><strong>建議</strong>：新專案採用 Iterator，舊專案漸進式遷移，團隊內部制定統一的使用規範。</p>
]]></content:encoded></item><item><title><![CDATA[Go synctest：徹底解決並發測試的痛點]]></title><description><![CDATA[Go 語言以 goroutine 和 channel 聞名，併發測試場景卻常常讓人頭痛：sleep 不夠會 fail，sleep 太久拖慢 CI，偶發錯誤難以重現。
Go 1.24 開始，提供了一個令人振奮的實驗性測試新功能：synctest。讓這一切成為過去！！預計將在 8 月釋出的 Go 1.25 正式釋出。

為什麼傳統並發測試這麼難寫？
讓我們看一個常見的例子：假設你要測試一個 goroutine 工作是否如期完成。
func TestWorker(t *testing.T) {
   ...]]></description><link>https://ganhua.wang/go-synctest</link><guid isPermaLink="true">https://ganhua.wang/go-synctest</guid><category><![CDATA[Go Language]]></category><dc:creator><![CDATA[雷N]]></dc:creator><pubDate>Sat, 19 Jul 2025 16:07:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752941218795/04a0aa18-212a-4dd5-ba17-1ff91f2ce535.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Go 語言以 goroutine 和 channel 聞名，併發測試場景卻常常讓人頭痛：<br />sleep 不夠會 fail，sleep 太久拖慢 CI，偶發錯誤難以重現。</p>
<p><strong>Go 1.24</strong> 開始，提供了一個令人振奮的實驗性測試新功能：<code>synctest</code>。<br />讓這一切成為過去！！預計將在 8 月釋出的 Go 1.25 正式釋出。</p>
</blockquote>
<h2 id="heading-54k65lua6bq85ykz57wx5lim55m85ris6kmm6ycz6bq86zuj5ar77yf">為什麼傳統並發測試這麼難寫？</h2>
<p>讓我們看一個常見的例子：<br />假設你要測試一個 goroutine 工作是否如期完成。</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestWorker</span><span class="hljs-params">(t *testing.T)</span></span> {
    done := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">struct</span>{})
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// do some work</span>
        time.Sleep(<span class="hljs-number">100</span> * time.Millisecond)
        <span class="hljs-built_in">close</span>(done)
    }()
    time.Sleep(<span class="hljs-number">150</span> * time.Millisecond) <span class="hljs-comment">// 等 goroutine 做完</span>
    <span class="hljs-keyword">select</span> {
    <span class="hljs-keyword">case</span> &lt;-done:
        <span class="hljs-comment">// ok</span>
    <span class="hljs-keyword">default</span>:
        t.Fatal(<span class="hljs-string">"worker 未完成"</span>)
    }
}
</code></pre>
<h3 id="heading-kirllypoyzlnkjlk6rvvj8qkg"><strong>問題在哪？</strong></h3>
<ul>
<li><p>你只能靠 sleep「猜」goroutine 完成的時機</p>
</li>
<li><p>sleep 太短測試會 fail，太長又浪費時間</p>
</li>
<li><p>在 CI 跑多次還是可能偶爾 fail</p>
</li>
<li><p>很難精確同步 goroutine 狀態</p>
</li>
</ul>
<h2 id="heading-testingsynctest">testing/synctest：用「泡泡」模型徹底解決同步問題</h2>
<p><code>synctest</code> 的核心在於「泡泡」（bubble）與「穩定阻塞」（durably blocked）同步模型。</p>
<h3 id="heading-5lua6bq85piv44cm5roh5roh44cn77yf">什麼是「泡泡」？</h3>
<ul>
<li><p>用 <a target="_blank" href="http://synctest.Run"><code>synctest.Run</code></a> 包起來的程式碼和其 goroutine，會被放進一個「泡泡」</p>
</li>
<li><p>泡泡內的 goroutine 只能互相影響，和外部世界隔離</p>
</li>
<li><p>泡泡追蹤所有在裡面建立的 channel、timer、WaitGroup 等同步物件</p>
</li>
</ul>
<h3 id="heading-5lua6bq85piv44cm56mp5a6a6zi75age44cn77yf">什麼是「穩定阻塞」？</h3>
<ul>
<li><p>當泡泡裡<strong>所有 goroutine 都卡住</strong>，而且<strong>只能被泡泡內其他 goroutine 喚醒</strong>，這時就叫做「穩定阻塞」</p>
</li>
<li><p>這時呼叫 <code>synctest.Wait()</code> 會立刻返回</p>
</li>
<li><p>若泡泡內沒有人能再被解鎖，代表死鎖，<code>Run</code> 會 panic</p>
</li>
<li><p>若有 timer 等待，fake clock 會自動快轉到下一個事件</p>
</li>
</ul>
<h2 id="heading-contextafterfunc">實戰範例：測試 context.AfterFunc</h2>
<h3 id="heading-5ykz57wx5ar5rov">傳統寫法</h3>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestAfterFunc</span><span class="hljs-params">(t *testing.T)</span></span> {
    ctx, cancel := context.WithCancel(context.Background())

    calledCh := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">struct</span>{})
    context.AfterFunc(ctx, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> { <span class="hljs-built_in">close</span>(calledCh) })

    <span class="hljs-comment">// 先確認還沒 cancel 前不會被呼叫</span>
    <span class="hljs-keyword">select</span> {
    <span class="hljs-keyword">case</span> &lt;-calledCh:
        t.Fatal(<span class="hljs-string">"AfterFunc 在 cancel 前就被呼叫"</span>)
    <span class="hljs-keyword">case</span> &lt;-time.After(<span class="hljs-number">10</span> * time.Millisecond):
        <span class="hljs-comment">// OK</span>
    }

    cancel()

    <span class="hljs-comment">// 再確認 cancel 後一定會被呼叫</span>
    <span class="hljs-keyword">select</span> {
    <span class="hljs-keyword">case</span> &lt;-calledCh:
        <span class="hljs-comment">// OK</span>
    <span class="hljs-keyword">case</span> &lt;-time.After(<span class="hljs-number">10</span> * time.Millisecond):
        t.Fatal(<span class="hljs-string">"AfterFunc 沒有在 cancel 後被呼叫"</span>)
    }
}
</code></pre>
<p><strong>痛點：</strong></p>
<ul>
<li><p>10ms 對快機器來說很長，對慢機器可能不夠，導致測試「慢」又「不穩」</p>
</li>
<li><p>sleep 長一點雖然穩，但測試就變慢</p>
</li>
<li><p>sleep 短一點測試快，但偶爾就 fail</p>
</li>
</ul>
<h3 id="heading-synctest">用 synctest 重寫</h3>
<pre><code class="lang-go">
<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"testing/synctest"</span>
    <span class="hljs-string">"testing"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestAfterFunc</span><span class="hljs-params">(t *testing.T)</span></span> {
    synctest.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        ctx, cancel := context.WithCancel(context.Background())

        called := <span class="hljs-literal">false</span>
        context.AfterFunc(ctx, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> { called = <span class="hljs-literal">true</span> })

        synctest.Wait() <span class="hljs-comment">// 等到所有 goroutine 都卡住</span>
        <span class="hljs-keyword">if</span> called {
            t.Fatal(<span class="hljs-string">"AfterFunc 在 cancel 前就被呼叫"</span>)
        }

        cancel()

        synctest.Wait() <span class="hljs-comment">// 再等一次</span>
        <span class="hljs-keyword">if</span> !called {
            t.Fatal(<span class="hljs-string">"AfterFunc 沒有在 cancel 後被呼叫"</span>)
        }
    })
}
</code></pre>
<p><strong>優點：</strong></p>
<ul>
<li><p>不用 sleep，測試又快又穩</p>
</li>
<li><p>不用 channel，直接用變數就好，因為 synctest.Wait 會保證沒有 data race</p>
</li>
<li><p>測試邏輯更清楚</p>
</li>
</ul>
<hr />
<h2 id="heading-5pmc6zat55u46zec55qe5lim55m85ris6kmm">時間相關的並發測試</h2>
<p><code>synctest</code> 會自動用 fake clock 控制時間，只要泡泡裡的 goroutine 都 block 住，時間就會自動快轉到下一個可解鎖事件。</p>
<h3 id="heading-contextwithtimeout">範例：測試 context.WithTimeout</h3>
<pre><code class="lang-go"><span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"testing/synctest"</span>
    <span class="hljs-string">"testing"</span>
    <span class="hljs-string">"time"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestWithTimeout</span><span class="hljs-params">(t *testing.T)</span></span> {
    synctest.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">const</span> timeout = <span class="hljs-number">5</span> * time.Second
        ctx, cancel := context.WithTimeout(context.Background(), timeout)
        <span class="hljs-keyword">defer</span> cancel()

        time.Sleep(timeout - time.Nanosecond)
        synctest.Wait()
        <span class="hljs-keyword">if</span> ctx.Err() != <span class="hljs-literal">nil</span> {
            t.Fatalf(<span class="hljs-string">"timeout 前 ctx.Err() = %v，預期為 nil"</span>, ctx.Err())
        }

        time.Sleep(time.Nanosecond)
        synctest.Wait()
        <span class="hljs-keyword">if</span> ctx.Err() != context.DeadlineExceeded {
            t.Fatalf(<span class="hljs-string">"timeout 後 ctx.Err() = %v，預期為 DeadlineExceeded"</span>, ctx.Err())
        }
    })
}
</code></pre>
<p><strong>重點：</strong></p>
<ul>
<li><p>寫法和一般測試一樣，但不用擔心 sleep 真的拖慢測試</p>
</li>
<li><p>synctest.Wait 會讓 fake clock 自動前進，測試又快又穩</p>
</li>
</ul>
<h2 id="heading-5roh5roh5zcm5q2l5qih5z6l55qe57sw56a">泡泡同步模型的細節</h2>
<h3 id="heading-synctest-1">哪些阻塞會被 synctest 當作「穩定阻塞」？</h3>
<p>只有下列情況會被判斷為「穩定阻塞」：</p>
<ul>
<li><p>nil channel 上的 send/receive</p>
</li>
<li><p>泡泡內建立的 channel 上的 send/receive（且已經 block）</p>
</li>
<li><p>select 裡所有 case 都是上述阻塞</p>
</li>
<li><p>time.Sleep</p>
</li>
<li><p>sync.Cond.Wait</p>
</li>
<li><p>sync.WaitGroup.Wait</p>
</li>
</ul>
<h3 id="heading-5zoq5lqb5lin566x77yf">哪些不算？</h3>
<ul>
<li><p>sync.Mutex（因為可能被泡泡外 goroutine 解鎖）</p>
</li>
<li><p>泡泡外建立的 channel</p>
</li>
<li><p>外部 I/O（例如網路、檔案）</p>
<ul>
<li>目前 synctest 只支援泡泡內建立的同步物件，真實網路、檔案等外部 I/O 仍無法 fake。</li>
</ul>
</li>
<li><p>任何可能被泡泡外事件喚醒的阻塞</p>
</li>
</ul>
<h3 id="heading-channel">Channel 的特別規則</h3>
<ul>
<li><p>只有「泡泡內建立」的 channel，才會被 synctest 追蹤</p>
</li>
<li><p>如果你在泡泡外操作泡泡內的 channel，會 panic</p>
</li>
</ul>
<h3 id="heading-5ris6kmm57ay6lev56il5byp5oco6bq86l6m77yf">測試網路程式怎麼辦？</h3>
<p>真實網路 I/O 不被 synctest 控制，建議用 <code>net.Pipe</code> 這種 in-memory fake network 來測試，這樣 synctest 才能正確判斷所有 goroutine 是否真的都卡住。</p>
<h3 id="heading-vs-synctest">傳統 vs synctest</h3>
<pre><code class="lang-mermaid">flowchart TD
    A[傳統測試] --&gt;|time.Sleep| B(等待 goroutine)
    B --&gt; C{goroutine 結束?}
    C -- No --&gt; B
    C -- Yes --&gt; D[Assert 結果]
    D --&gt; E[測試結束]

    F[synctest 測試] --&gt; G(執行待測程式)
    G --&gt; H(synctest.Wait：等待所有 goroutine 穩定阻塞)
    H --&gt; I[檢查狀態/Assert 結果]
    I --&gt; J[測試結束]
</code></pre>
<h2 id="heading-net-io">實戰範例：Net I/O</h2>
<p>測試 HTTP client “Expect: 100-continue” 行為</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> v6

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"bufio"</span>
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"io"</span>
    <span class="hljs-string">"net"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"strings"</span>
    <span class="hljs-string">"testing"</span>
    <span class="hljs-string">"time"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestHTTPExpect100Continue_NoSyncTest</span><span class="hljs-params">(t *testing.T)</span></span> {
    srvConn, cliConn := net.Pipe()
    <span class="hljs-keyword">defer</span> srvConn.Close()
    <span class="hljs-keyword">defer</span> cliConn.Close()

    tr := &amp;http.Transport{
        DialContext: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context, network, address <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(net.Conn, error)</span></span> {
            <span class="hljs-keyword">return</span> cliConn, <span class="hljs-literal">nil</span>
        },
        ExpectContinueTimeout: <span class="hljs-number">500</span> * time.Millisecond, <span class="hljs-comment">// 設短一點</span>
    }

    body := <span class="hljs-string">"request body"</span>
    done := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">struct</span>{})

    <span class="hljs-comment">// 啟動 client 發 request</span>
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        req, _ := http.NewRequest(<span class="hljs-string">"PUT"</span>, <span class="hljs-string">"http://fake.tld/"</span>, strings.NewReader(body))
        req.Header.Set(<span class="hljs-string">"Expect"</span>, <span class="hljs-string">"100-continue"</span>)
        resp, err := tr.RoundTrip(req)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            t.Errorf(<span class="hljs-string">"RoundTrip: unexpected error %v"</span>, err)
        } <span class="hljs-keyword">else</span> {
            resp.Body.Close()
        }
        <span class="hljs-built_in">close</span>(done)
    }()

    <span class="hljs-comment">// server 端讀 request headers</span>
    req, err := http.ReadRequest(bufio.NewReader(srvConn))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        t.Fatalf(<span class="hljs-string">"ReadRequest: %v"</span>, err)
    }

    <span class="hljs-comment">// 啟動 goroutine 讀取 body</span>
    <span class="hljs-keyword">var</span> gotBody strings.Builder
    bodyDone := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">struct</span>{})
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        io.Copy(&amp;gotBody, req.Body)
        <span class="hljs-built_in">close</span>(bodyDone)
    }()

    <span class="hljs-comment">// 等 100ms，確認 body 尚未被送出</span>
    time.Sleep(<span class="hljs-number">100</span> * time.Millisecond)
    <span class="hljs-keyword">if</span> got := gotBody.String(); got != <span class="hljs-string">""</span> {
        t.Fatalf(<span class="hljs-string">"before 100 Continue, got body: %q"</span>, got)
    }

    <span class="hljs-comment">// server 回 100 Continue</span>
    srvConn.Write([]<span class="hljs-keyword">byte</span>(<span class="hljs-string">"HTTP/1.1 100 Continue\r\n\r\n"</span>))

    <span class="hljs-comment">// 最多等 200ms，body 應該會到</span>
    <span class="hljs-keyword">select</span> {
    <span class="hljs-keyword">case</span> &lt;-bodyDone:
        <span class="hljs-comment">// 確認 body</span>
        <span class="hljs-keyword">if</span> got := gotBody.String(); got != body {
            t.Fatalf(<span class="hljs-string">"after 100 Continue, got body %q, want %q"</span>, got, body)
        }
    <span class="hljs-keyword">case</span> &lt;-time.After(<span class="hljs-number">200</span> * time.Millisecond):
        t.Fatal(<span class="hljs-string">"timed out waiting for client to send body"</span>)
    }

    <span class="hljs-comment">// server 回 200 OK 結束</span>
    srvConn.Write([]<span class="hljs-keyword">byte</span>(<span class="hljs-string">"HTTP/1.1 200 OK\r\n\r\n"</span>))

    <span class="hljs-comment">// 等 client 結束</span>
    <span class="hljs-keyword">select</span> {
    <span class="hljs-keyword">case</span> &lt;-done:
    <span class="hljs-keyword">case</span> &lt;-time.After(<span class="hljs-number">200</span> * time.Millisecond):
        t.Fatal(<span class="hljs-string">"timed out waiting for client to finish"</span>)
    }
}

&gt; <span class="hljs-keyword">go</span> test -run TestHTTPExpect100Continue_NoSyncTest -v                      
=== RUN   TestHTTPExpect100Continue_NoSyncTest
--- PASS: TestHTTPExpect100Continue_NoSyncTest (<span class="hljs-number">0.10</span>s)
PASS
ok      github.com/quii/learn-<span class="hljs-keyword">go</span>-with-tests/sync/v6     <span class="hljs-number">0.105</span>s
</code></pre>
<p>改用 testing/synctest</p>
<ul>
<li><p>用 <code>net.Pipe</code> 可以 fake network，讓 synctest 能完全控制測試環境。</p>
<ul>
<li>建立一對 in-memory 連線（<code>srvConn</code>, <code>cliConn</code>），不會碰到真實網路 I/O。</li>
</ul>
</li>
<li><p>不要用真實的 TCP/UDP，否則 synctest 控制不了外部事件。</p>
</li>
<li><p>這種方式可以 deterministic 地測各種網路 protocol 行為。</p>
</li>
</ul>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> v7

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"bufio"</span>
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"io"</span>
    <span class="hljs-string">"net"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"strings"</span>
    <span class="hljs-string">"testing"</span>
    <span class="hljs-string">"testing/synctest"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestHTTPExpect100Continue</span><span class="hljs-params">(t *testing.T)</span></span> {
    synctest.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        srvConn, cliConn := net.Pipe()
        <span class="hljs-keyword">defer</span> srvConn.Close()
        <span class="hljs-keyword">defer</span> cliConn.Close()

        tr := &amp;http.Transport{
            DialContext: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context, network, address <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(net.Conn, error)</span></span> {
                <span class="hljs-keyword">return</span> cliConn, <span class="hljs-literal">nil</span>
            },
            <span class="hljs-comment">// 這個 timeout 不會真的等 5 秒，因為 synctest fake time</span>
            ExpectContinueTimeout: <span class="hljs-number">5</span> * <span class="hljs-number">1e9</span>, <span class="hljs-comment">// 5 秒</span>
        }

        body := <span class="hljs-string">"request body"</span>
        <span class="hljs-comment">// 啟動 client 發 request</span>
        <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
            req, _ := http.NewRequest(<span class="hljs-string">"PUT"</span>, <span class="hljs-string">"http://fake.tld/"</span>, strings.NewReader(body))
            req.Header.Set(<span class="hljs-string">"Expect"</span>, <span class="hljs-string">"100-continue"</span>)
            resp, err := tr.RoundTrip(req)
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                t.Errorf(<span class="hljs-string">"RoundTrip: unexpected error %v"</span>, err)
            } <span class="hljs-keyword">else</span> {
                resp.Body.Close()
            }
        }()

        <span class="hljs-comment">// server 端讀 request headers</span>
        req, err := http.ReadRequest(bufio.NewReader(srvConn))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            t.Fatalf(<span class="hljs-string">"ReadRequest: %v"</span>, err)
        }

        <span class="hljs-comment">// 啟動 goroutine 讀取 body</span>
        <span class="hljs-keyword">var</span> gotBody strings.Builder
        <span class="hljs-keyword">go</span> io.Copy(&amp;gotBody, req.Body)

        <span class="hljs-comment">// 等 bubble 裡 goroutine 都 block</span>
        synctest.Wait()
        <span class="hljs-comment">// 應該還沒收到 body</span>
        <span class="hljs-keyword">if</span> got := gotBody.String(); got != <span class="hljs-string">""</span> {
            t.Fatalf(<span class="hljs-string">"before 100 Continue, got body: %q"</span>, got)
        }

        <span class="hljs-comment">// server 回 100 Continue</span>
        srvConn.Write([]<span class="hljs-keyword">byte</span>(<span class="hljs-string">"HTTP/1.1 100 Continue\r\n\r\n"</span>))
        synctest.Wait()
        <span class="hljs-comment">// 現在應該收到完整 body</span>
        <span class="hljs-keyword">if</span> got := gotBody.String(); got != body {
            t.Fatalf(<span class="hljs-string">"after 100 Continue, got body %q, want %q"</span>, got, body)
        }

        <span class="hljs-comment">// server 回 200 OK 結束</span>
        srvConn.Write([]<span class="hljs-keyword">byte</span>(<span class="hljs-string">"HTTP/1.1 200 OK\r\n\r\n"</span>))
    })
}

&gt; GOEXPERIMENT=synctest <span class="hljs-keyword">go</span> test -run TestHTTPExpect100Continue -v
=== RUN   TestHTTPExpect100Continue
--- PASS: TestHTTPExpect100Continue (<span class="hljs-number">0.00</span>s)
PASS
ok      github.com/quii/learn-<span class="hljs-keyword">go</span>-with-tests/sync/v7     <span class="hljs-number">0.004</span>s
</code></pre>
<h2 id="heading-eventmonitor">實戰範例：EventMonitor 併發測試</h2>
<p>假設你有一個 EventMonitor，要測試它能正確處理多 goroutine 並發通知。以及一些其他場景。這種 pattern 也常見於 scheduler、watcher、poller 等服務元件，現在你可以放心用 synctest 測它</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> monitor

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"time"</span>
)

<span class="hljs-comment">// EventMonitor : 簡化版本</span>
<span class="hljs-keyword">type</span> EventMonitor <span class="hljs-keyword">struct</span> {
    notificationChan    &lt;-<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>
    ticker              *time.Ticker
    checkFunc           <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(context.Context)</span></span>
    interval            time.Duration
    ctx                 context.Context
    cancel              context.CancelFunc
    ProcessNotification <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(<span class="hljs-keyword">string</span>)</span></span>
}

<span class="hljs-comment">// NewTokenMonitor: constructor</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">New</span><span class="hljs-params">(notificationChan &lt;-<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">EventMonitor</span></span> {
    ctx, cancel := context.WithCancel(context.Background())
    <span class="hljs-keyword">return</span> &amp;EventMonitor{
        notificationChan: notificationChan,
        interval:         <span class="hljs-number">1</span> * time.Second,
        ctx:              ctx,
        cancel:           cancel,
        ProcessNotification: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(<span class="hljs-keyword">string</span>)</span></span> {
            <span class="hljs-comment">// 預設實作，不做任何事</span>
        },
    }
}

<span class="hljs-comment">// SetCheckFunc : set check function</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(tm *EventMonitor)</span> <span class="hljs-title">SetCheckFunc</span><span class="hljs-params">(fn <span class="hljs-keyword">func</span>(context.Context)</span>)</span> {
    tm.checkFunc = fn
}

<span class="hljs-comment">// SetInterval : set scan interval</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(tm *EventMonitor)</span> <span class="hljs-title">SetInterval</span><span class="hljs-params">(interval time.Duration)</span></span> {
    tm.interval = interval
    <span class="hljs-keyword">if</span> tm.ticker != <span class="hljs-literal">nil</span> {
        tm.ticker.Reset(interval)
    }
}

<span class="hljs-comment">// Run : 啟動 monitor instance</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(tm *EventMonitor)</span> <span class="hljs-title">Run</span><span class="hljs-params">()</span></span> {
    tm.ticker = time.NewTicker(tm.interval)

    <span class="hljs-keyword">for</span> {
        <span class="hljs-keyword">select</span> {
        <span class="hljs-keyword">case</span> msg, ok := &lt;-tm.notificationChan:
            <span class="hljs-keyword">if</span> !ok {
                <span class="hljs-keyword">return</span> <span class="hljs-comment">// since channel is closed and then return the process</span>
            }
            <span class="hljs-keyword">go</span> tm.ProcessNotification(msg)

        <span class="hljs-keyword">case</span> &lt;-tm.ticker.C:
            <span class="hljs-keyword">if</span> tm.checkFunc != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">go</span> tm.checkFunc(tm.ctx)
            }

        <span class="hljs-keyword">case</span> &lt;-tm.ctx.Done():
            <span class="hljs-keyword">return</span> <span class="hljs-comment">// since context is cancled and then return</span>
        }
    }
}

<span class="hljs-comment">// Stop : stop monitor</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(tm *EventMonitor)</span> <span class="hljs-title">Stop</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">if</span> tm.ticker != <span class="hljs-literal">nil</span> {
        tm.ticker.Stop()
    }
    tm.cancel()
}
</code></pre>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> monitor

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"sync/atomic"</span>
    <span class="hljs-string">"testing"</span>
    <span class="hljs-string">"time"</span>
)

<span class="hljs-comment">// TestEventMonitor_Concurrency 測試 EventMonitor 處理大量並發通知的能力</span>
<span class="hljs-comment">// 模擬多個 goroutine 同時發送通知，確保所有通知都能被正確處理</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestEventMonitor_Concurrency</span><span class="hljs-params">(t *testing.T)</span></span> {
    <span class="hljs-comment">// Arrange</span>
    notificationChan := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>, <span class="hljs-number">100</span>)
    tm := New(notificationChan)
    <span class="hljs-keyword">var</span> processedCount atomic.Int32

    <span class="hljs-comment">// 設置處理通知的函數來計數</span>
    <span class="hljs-comment">// 每當收到一個通知時，計數器加一</span>
    tm.ProcessNotification = <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(msg <span class="hljs-keyword">string</span>)</span></span> {
        processedCount.Add(<span class="hljs-number">1</span>)
    }

    <span class="hljs-comment">// Act</span>
    <span class="hljs-keyword">go</span> tm.Run()

    <span class="hljs-comment">// 啟動 10 個 goroutine，每個發送 10 個通知</span>
    <span class="hljs-comment">// 總共發送 100 個通知，測試並發處理能力</span>
    <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; i++ {
        <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(id <span class="hljs-keyword">int</span>)</span></span> {
            <span class="hljs-keyword">for</span> j := <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">10</span>; j++ {
                notificationChan &lt;- fmt.Sprintf(<span class="hljs-string">"n-%d-%d"</span>, id, j)
            }
        }(i)
    }

    <span class="hljs-comment">// 等待足夠時間讓通知被處理</span>
    time.Sleep(<span class="hljs-number">500</span> * time.Millisecond)

    <span class="hljs-comment">// Assert</span>
    <span class="hljs-keyword">if</span> processedCount.Load() != <span class="hljs-number">100</span> {
        t.Errorf(<span class="hljs-string">"通知處理數量不符"</span>)
    }

    tm.Stop()
}

<span class="hljs-comment">// TestEventMonitor_TimerTrigger 測試定時器觸發功能</span>
<span class="hljs-comment">// 驗證 checkFunc 是否會按照設定的時間間隔被調用</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestEventMonitor_TimerTrigger</span><span class="hljs-params">(t *testing.T)</span></span> {
    <span class="hljs-comment">// Arrange</span>
    notificationChan := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>, <span class="hljs-number">10</span>)
    tm := New(notificationChan)

    <span class="hljs-comment">// 設置短間隔時間 (100ms)，以便快速測試</span>
    interval := <span class="hljs-number">100</span> * time.Millisecond
    tm.SetInterval(interval)

    <span class="hljs-comment">// 計數器，記錄 checkFunc 被調用的次數</span>
    <span class="hljs-keyword">var</span> checkCount atomic.Int32

    <span class="hljs-comment">// 設置檢查函數，每次被調用時計數器加一</span>
    tm.SetCheckFunc(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context)</span></span> {
        checkCount.Add(<span class="hljs-number">1</span>)
    })

    <span class="hljs-comment">// Act</span>
    <span class="hljs-keyword">go</span> tm.Run()

    <span class="hljs-comment">// 等待約 550ms，理論上 checkFunc 應該被調用約 5 次</span>
    time.Sleep(<span class="hljs-number">550</span> * time.Millisecond)

    <span class="hljs-comment">// Assert</span>
    <span class="hljs-comment">// 檢查調用次數是否在預期範圍內</span>
    <span class="hljs-comment">// 由於計時器精確度和系統負載可能導致誤差，允許結果在 4-6 次之間</span>
    count := checkCount.Load()
    <span class="hljs-keyword">if</span> count &lt; <span class="hljs-number">4</span> || count &gt; <span class="hljs-number">6</span> {
        t.Errorf(<span class="hljs-string">"預期 checkFunc 應被觸發約 5 次，實際觸發 %d 次"</span>, count)
    }

    <span class="hljs-comment">// 停止監控器</span>
    tm.Stop()
}

<span class="hljs-comment">// TestEventMonitor_ChannelClose 測試通知通道關閉時的行為</span>
<span class="hljs-comment">// 驗證當通知通道關閉時，監控器是否會優雅退出</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestEventMonitor_ChannelClose</span><span class="hljs-params">(t *testing.T)</span></span> {
    <span class="hljs-comment">// Arrange</span>
    notificationChan := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>, <span class="hljs-number">10</span>)
    tm := New(notificationChan)

    <span class="hljs-comment">// 設置通知處理函數</span>
    <span class="hljs-keyword">var</span> processedCount atomic.Int32
    tm.ProcessNotification = <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(msg <span class="hljs-keyword">string</span>)</span></span> {
        processedCount.Add(<span class="hljs-number">1</span>)
    }

    <span class="hljs-comment">// 使用 done 通道來監控 Run 方法是否結束</span>
    done := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">struct</span>{})
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        tm.Run()    <span class="hljs-comment">// 當通知通道關閉時，Run 應該自動返回</span>
        <span class="hljs-built_in">close</span>(done) <span class="hljs-comment">// 通知測試 Run 已結束</span>
    }()

    <span class="hljs-comment">// 發送兩個測試通知</span>
    notificationChan &lt;- <span class="hljs-string">"test1"</span>
    notificationChan &lt;- <span class="hljs-string">"test2"</span>

    <span class="hljs-comment">// 等待通知被處理</span>
    time.Sleep(<span class="hljs-number">100</span> * time.Millisecond)

    <span class="hljs-comment">// Act</span>
    <span class="hljs-comment">// 關閉通知通道，這應該導致 Run 方法退出</span>
    <span class="hljs-built_in">close</span>(notificationChan)

    <span class="hljs-comment">// 等待 Run 方法退出，最多等待 500ms</span>
    <span class="hljs-keyword">select</span> {
    <span class="hljs-keyword">case</span> &lt;-done:
        <span class="hljs-comment">// 成功，monitor 已停止運行</span>
    <span class="hljs-keyword">case</span> &lt;-time.After(<span class="hljs-number">500</span> * time.Millisecond):
        t.Error(<span class="hljs-string">"通知通道關閉後，monitor 未能停止運行"</span>)
    }

    <span class="hljs-comment">// Assert</span>
    <span class="hljs-comment">// 確認只有兩個通知被處理</span>
    <span class="hljs-keyword">if</span> processedCount.Load() != <span class="hljs-number">2</span> {
        t.Errorf(<span class="hljs-string">"預期處理 2 個通知，實際處理 %d 個"</span>, processedCount.Load())
    }
}

<span class="hljs-comment">// TestEventMonitor_SetInterval 測試動態調整間隔時間的功能</span>
<span class="hljs-comment">// 驗證 SetInterval 方法是否能有效地改變檢查函數的調用頻率</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestEventMonitor_SetInterval</span><span class="hljs-params">(t *testing.T)</span></span> {
    <span class="hljs-comment">// Arrange</span>
    notificationChan := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>, <span class="hljs-number">10</span>)
    tm := New(notificationChan)

    <span class="hljs-comment">// 設置檢查函數</span>
    <span class="hljs-keyword">var</span> checkCount atomic.Int32
    tm.SetCheckFunc(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context)</span></span> {
        checkCount.Add(<span class="hljs-number">1</span>)
    })

    <span class="hljs-comment">// 初始設置為較長間隔 (1秒)</span>
    tm.SetInterval(<span class="hljs-number">1</span> * time.Second)

    <span class="hljs-comment">// Act</span>
    <span class="hljs-keyword">go</span> tm.Run()

    <span class="hljs-comment">// 等待短時間，由於間隔長，應該不會觸發多次</span>
    time.Sleep(<span class="hljs-number">250</span> * time.Millisecond)
    initialCount := checkCount.Load() <span class="hljs-comment">// 記錄初始計數</span>

    <span class="hljs-comment">// 動態修改為較短間隔 (100ms)</span>
    tm.SetInterval(<span class="hljs-number">100</span> * time.Millisecond)

    <span class="hljs-comment">// 再等待足夠時間，讓短間隔產生多次觸發</span>
    time.Sleep(<span class="hljs-number">550</span> * time.Millisecond) <span class="hljs-comment">// 應該至少觸發 4-5 次</span>

    <span class="hljs-comment">// Assert</span>
    <span class="hljs-comment">// 檢查觸發次數是否明顯增加</span>
    finalCount := checkCount.Load()
    <span class="hljs-keyword">if</span> finalCount-initialCount &lt; <span class="hljs-number">4</span> {
        t.Errorf(<span class="hljs-string">"更改間隔後，預期至少增加 4 次觸發，實際增加 %d 次"</span>, finalCount-initialCount)
    }

    <span class="hljs-comment">// 停止監控器</span>
    tm.Stop()
}

&gt; <span class="hljs-keyword">go</span> test -run ./... -v                                             
=== RUN   TestEventMonitor_Concurrency
--- PASS: TestEventMonitor_Concurrency (<span class="hljs-number">0.50</span>s)
=== RUN   TestEventMonitor_TimerTrigger
--- PASS: TestEventMonitor_TimerTrigger (<span class="hljs-number">0.55</span>s)
=== RUN   TestEventMonitor_ChannelClose
--- PASS: TestEventMonitor_ChannelClose (<span class="hljs-number">0.10</span>s)
=== RUN   TestEventMonitor_SetInterval
--- PASS: TestEventMonitor_SetInterval (<span class="hljs-number">0.80</span>s)
PASS
ok      github.com/quii/learn-<span class="hljs-keyword">go</span>-with-tests/sync/monitor        <span class="hljs-number">1.956</span>s
</code></pre>
<p>改成用 testing/synctest</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> monitor

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"sync/atomic"</span>
    <span class="hljs-string">"testing"</span>
    <span class="hljs-string">"testing/synctest"</span>
    <span class="hljs-string">"time"</span>
)

<span class="hljs-comment">// TestEventMonitor_Concurrency 測試 EventMonitor 處理大量並發通知的能力</span>
<span class="hljs-comment">// 模擬多個 goroutine 同時發送通知，確保所有通知都能被正確處理</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestEventMonitor_Concurrency</span><span class="hljs-params">(t *testing.T)</span></span> {
    synctest.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// Arrange</span>
        notificationChan := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>, <span class="hljs-number">100</span>)
        tm := New(notificationChan)
        <span class="hljs-keyword">var</span> processedCount atomic.Int32

        <span class="hljs-comment">// 設置處理通知的函數來計數</span>
        <span class="hljs-comment">// 每當收到一個通知時，計數器加一</span>
        tm.ProcessNotification = <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(msg <span class="hljs-keyword">string</span>)</span></span> {
            processedCount.Add(<span class="hljs-number">1</span>)
        }

        <span class="hljs-comment">// Act</span>
        <span class="hljs-keyword">go</span> tm.Run()

        <span class="hljs-comment">// 啟動 10 個 goroutine，每個發送 10 個通知</span>
        <span class="hljs-comment">// 總共發送 100 個通知，測試並發處理能力</span>
        <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; i++ {
            <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(id <span class="hljs-keyword">int</span>)</span></span> {
                <span class="hljs-keyword">for</span> j := <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">10</span>; j++ {
                    notificationChan &lt;- fmt.Sprintf(<span class="hljs-string">"n-%d-%d"</span>, id, j)
                }
            }(i)
        }

        <span class="hljs-comment">// 等待足夠時間讓通知被處理</span>
        time.Sleep(<span class="hljs-number">500</span> * time.Millisecond)
        synctest.Wait()

        <span class="hljs-comment">// Assert</span>
        <span class="hljs-keyword">if</span> processedCount.Load() != <span class="hljs-number">100</span> {
            t.Errorf(<span class="hljs-string">"通知處理數量不符"</span>)
        }

        tm.Stop()
    })
}

<span class="hljs-comment">// TestEventMonitor_TimerTrigger 測試定時器觸發功能</span>
<span class="hljs-comment">// 驗證 checkFunc 是否會按照設定的時間間隔被調用</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestEventMonitor_TimerTrigger</span><span class="hljs-params">(t *testing.T)</span></span> {
    synctest.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// Arrange</span>
        notificationChan := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>, <span class="hljs-number">10</span>)
        tm := New(notificationChan)

        <span class="hljs-comment">// 設置短間隔時間 (100ms)，以便快速測試</span>
        interval := <span class="hljs-number">100</span> * time.Millisecond
        tm.SetInterval(interval)

        <span class="hljs-comment">// 計數器，記錄 checkFunc 被調用的次數</span>
        <span class="hljs-keyword">var</span> checkCount atomic.Int32

        <span class="hljs-comment">// 設置檢查函數，每次被調用時計數器加一</span>
        tm.SetCheckFunc(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context)</span></span> {
            checkCount.Add(<span class="hljs-number">1</span>)
        })

        <span class="hljs-comment">// Act</span>
        <span class="hljs-keyword">go</span> tm.Run()

        <span class="hljs-comment">// 等待約 550ms，理論上 checkFunc 應該被調用約 5 次</span>
        time.Sleep(<span class="hljs-number">550</span> * time.Millisecond)
        synctest.Wait()

        <span class="hljs-comment">// Assert</span>
        <span class="hljs-comment">// 檢查調用次數是否在預期範圍內</span>
        <span class="hljs-comment">// 由於計時器精確度和系統負載可能導致誤差，允許結果在 4-6 次之間</span>
        count := checkCount.Load()
        <span class="hljs-keyword">if</span> count &lt; <span class="hljs-number">4</span> || count &gt; <span class="hljs-number">6</span> {
            t.Errorf(<span class="hljs-string">"預期 checkFunc 應被觸發約 5 次，實際觸發 %d 次"</span>, count)
        }

        <span class="hljs-comment">// 停止監控器</span>
        tm.Stop()
    })

}

<span class="hljs-comment">// TestEventMonitor_ChannelClose 測試通知通道關閉時的行為</span>
<span class="hljs-comment">// 驗證當通知通道關閉時，監控器是否會優雅退出</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestEventMonitor_ChannelClose</span><span class="hljs-params">(t *testing.T)</span></span> {
    synctest.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// Arrange</span>
        notificationChan := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>, <span class="hljs-number">10</span>)
        tm := New(notificationChan)

        <span class="hljs-comment">// 設置通知處理函數</span>
        <span class="hljs-keyword">var</span> processedCount atomic.Int32
        tm.ProcessNotification = <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(msg <span class="hljs-keyword">string</span>)</span></span> {
            processedCount.Add(<span class="hljs-number">1</span>)
        }

        <span class="hljs-comment">// 使用 done 通道來監控 Run 方法是否結束</span>
        done := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">struct</span>{})
        <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
            tm.Run()    <span class="hljs-comment">// 當通知通道關閉時，Run 應該自動返回</span>
            <span class="hljs-built_in">close</span>(done) <span class="hljs-comment">// 通知測試 Run 已結束</span>
        }()

        <span class="hljs-comment">// 發送兩個測試通知</span>
        notificationChan &lt;- <span class="hljs-string">"test1"</span>
        notificationChan &lt;- <span class="hljs-string">"test2"</span>

        <span class="hljs-comment">// 等待通知被處理</span>
        time.Sleep(<span class="hljs-number">100</span> * time.Millisecond)
        synctest.Wait()

        <span class="hljs-comment">// Act</span>
        <span class="hljs-comment">// 關閉通知通道，這應該導致 Run 方法退出</span>
        <span class="hljs-built_in">close</span>(notificationChan)

        <span class="hljs-comment">// 等待 Run 方法退出，最多等待 500ms</span>
        <span class="hljs-keyword">select</span> {
        <span class="hljs-keyword">case</span> &lt;-done:
            <span class="hljs-comment">// 成功，monitor 已停止運行</span>
        <span class="hljs-keyword">case</span> &lt;-time.After(<span class="hljs-number">500</span> * time.Millisecond):
            t.Error(<span class="hljs-string">"通知通道關閉後，monitor 未能停止運行"</span>)
        }

        <span class="hljs-comment">// Assert</span>
        <span class="hljs-comment">// 確認只有兩個通知被處理</span>
        <span class="hljs-keyword">if</span> processedCount.Load() != <span class="hljs-number">2</span> {
            t.Errorf(<span class="hljs-string">"預期處理 2 個通知，實際處理 %d 個"</span>, processedCount.Load())
        }

        tm.Stop()
    })

}

<span class="hljs-comment">// TestEventMonitor_SetInterval 測試動態調整間隔時間的功能</span>
<span class="hljs-comment">// 驗證 SetInterval 方法是否能有效地改變檢查函數的調用頻率</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestEventMonitor_SetInterval</span><span class="hljs-params">(t *testing.T)</span></span> {
    synctest.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// Arrange</span>
        notificationChan := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>, <span class="hljs-number">10</span>)
        tm := New(notificationChan)

        <span class="hljs-comment">// 設置檢查函數</span>
        <span class="hljs-keyword">var</span> checkCount atomic.Int32
        tm.SetCheckFunc(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context)</span></span> {
            checkCount.Add(<span class="hljs-number">1</span>)
        })

        <span class="hljs-comment">// 初始設置為較長間隔 (1秒)</span>
        tm.SetInterval(<span class="hljs-number">1</span> * time.Second)

        <span class="hljs-comment">// Act</span>
        <span class="hljs-keyword">go</span> tm.Run()

        <span class="hljs-comment">// 等待短時間，由於間隔長，應該不會觸發多次</span>
        time.Sleep(<span class="hljs-number">250</span> * time.Millisecond)
        synctest.Wait()

        initialCount := checkCount.Load() <span class="hljs-comment">// 記錄初始計數</span>

        <span class="hljs-comment">// 動態修改為較短間隔 (100ms)</span>
        tm.SetInterval(<span class="hljs-number">100</span> * time.Millisecond)

        <span class="hljs-comment">// 再等待足夠時間，讓短間隔產生多次觸發</span>
        time.Sleep(<span class="hljs-number">550</span> * time.Millisecond) <span class="hljs-comment">// 應該至少觸發 4-5 次</span>
        synctest.Wait()
        <span class="hljs-comment">// Assert</span>
        <span class="hljs-comment">// 檢查觸發次數是否明顯增加</span>
        finalCount := checkCount.Load()
        <span class="hljs-keyword">if</span> finalCount-initialCount &lt; <span class="hljs-number">4</span> {
            t.Errorf(<span class="hljs-string">"更改間隔後，預期至少增加 4 次觸發，實際增加 %d 次"</span>, finalCount-initialCount)
        }

        <span class="hljs-comment">// 停止監控器</span>
        tm.Stop()
    })

}

&gt; GOEXPERIMENT=synctest <span class="hljs-keyword">go</span> test -run ./... -v
=== RUN   TestEventMonitor_Concurrency
--- PASS: TestEventMonitor_Concurrency (<span class="hljs-number">0.00</span>s)
=== RUN   TestEventMonitor_TimerTrigger
--- PASS: TestEventMonitor_TimerTrigger (<span class="hljs-number">0.00</span>s)
=== RUN   TestEventMonitor_ChannelClose
--- PASS: TestEventMonitor_ChannelClose (<span class="hljs-number">0.00</span>s)
=== RUN   TestEventMonitor_SetInterval
--- PASS: TestEventMonitor_SetInterval (<span class="hljs-number">0.00</span>s)
PASS
ok      github.com/quii/learn-<span class="hljs-keyword">go</span>-with-tests/sync/monitor/v2     <span class="hljs-number">0.003</span>s
</code></pre>
<p>不難看見測試都是瞬間完成，這樣測試就能脫離時間的不確定性了，變得更加穩健。</p>
<h2 id="heading-57i957wq6iih5bd5b6x">總結與心得</h2>
<p>Go 1.24 帶來的 <code>testing/synctest</code>，徹底改變了我們寫並發測試的方式。不用再自己實做 mock timer。<br />也不用再靠 time.Sleep 來猜測 goroutine 進度、不用再擔心 CI 上偶發失敗，<br />只要把測試放進 synctest 的「泡泡」裡，所有 goroutine、timer、channel 都能被準確追蹤與 fake，<br />讓你的併發測試又快、又穩、又 deterministic！</p>
<p>這對任何需要測試 timer、channel、goroutine 行為的 Go 專案來說，是一大福音。<br />無論你在寫 HTTP 協議、事件監控、poller、watcher，甚至自訂同步 primitive，<br />都可以放心用 synctest 讓測試變得簡單可靠。</p>
<p><strong>小提醒：</strong></p>
<ul>
<li><p>synctest 目前仍是實驗性功能，只能 fake 泡泡內的同步物件，外部 I/O 仍需小心。</p>
</li>
<li><p>建議多用 net.Pipe、in-memory fake 等技巧，讓測試完全可控。</p>
</li>
</ul>
<p>最後，<strong>推薦大家勇敢升級 Go 1.25，並開始把自己的併發測試搬進 synctest 泡泡裡！</strong><br />你會發現，寫並發測試再也不可怕，甚至變得很有趣！期待 Go 1.25 正式版本的釋出！</p>
<p>Reference:</p>
<p>[Go blog - Testing concurrent code with testing/synctest](<a target="_blank" href="https://go.dev/blog/synctest">https://go.dev/blog/synctest</a>)</p>
]]></content:encoded></item></channel></rss>