<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Prometheus on Tarragon</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/</link><description>Recent content in Prometheus on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 01 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/index.xml" rel="self" type="application/rss+xml"/><item><title>Prometheus 容量規劃與故障模式</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「Cardinality 管理」跟「Memory pressure」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Prometheus 的容量模型跟傳統資料庫不同 — 它的容量邊界主要受 active series 數量（cardinality）跟 retention 期決定，而非資料筆數或 disk size。理解 Prometheus 的資源消耗模型，才能判斷什麼時候單機夠用、什麼時候需要 remote write 卸載或遷移到 Mimir / Thanos。&lt;/p>
&lt;h2 id="資源消耗模型">資源消耗模型&lt;/h2>
&lt;h3 id="memory由-active-series-決定">Memory：由 active series 決定&lt;/h3>
&lt;p>Prometheus 把近期的 time series 保存在記憶體（head block）。每個 active series 大約消耗 3-4 KB 記憶體（含 index、chunks、postings；Prometheus TSDB 的業界經驗值，實際依 label 長度與 chunk encoding 而定）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Active series&lt;/th>
 &lt;th>預估 memory（head block）&lt;/th>
 &lt;th>適合的機器規格&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>10 萬&lt;/td>
 &lt;td>~400 MB&lt;/td>
 &lt;td>任何 VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>100 萬&lt;/td>
 &lt;td>~4 GB&lt;/td>
 &lt;td>8 GB VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>500 萬&lt;/td>
 &lt;td>~20 GB&lt;/td>
 &lt;td>32 GB VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1000 萬&lt;/td>
 &lt;td>~40 GB&lt;/td>
 &lt;td>64 GB VM&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這是 head block 的記憶體，不含 query execution 跟 WAL replay 的暫時開銷。Heavy PromQL query（大範圍 aggregation、多 series join）會額外消耗數 GB 的暫時記憶體。&lt;/p>
&lt;p>判讀指標：&lt;code>prometheus_tsdb_head_series&lt;/code> 代表當前 active series 數量，&lt;code>process_resident_memory_bytes&lt;/code> 代表實際記憶體使用。兩者的比值偏離預期時（例如 50 萬 series 但記憶體用了 10 GB），可能是 query 記憶體壓力或 WAL corruption。&lt;/p>
&lt;h3 id="disk由-retention-期與-ingestion-rate-決定">Disk：由 retention 期與 ingestion rate 決定&lt;/h3>
&lt;p>Prometheus 的 disk 消耗 = ingestion rate × retention 期 × 壓縮後每 sample 大小（約 1-2 bytes，Gorilla 壓縮算法下的業界經驗值）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Ingestion rate&lt;/th>
 &lt;th>Retention&lt;/th>
 &lt;th>預估 disk&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>10 萬 samples/sec&lt;/td>
 &lt;td>15 天&lt;/td>
 &lt;td>~130 GB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10 萬 samples/sec&lt;/td>
 &lt;td>30 天&lt;/td>
 &lt;td>~260 GB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>50 萬 samples/sec&lt;/td>
 &lt;td>15 天&lt;/td>
 &lt;td>~650 GB&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Disk I/O 的瓶頸通常在 compaction — Prometheus 定期把 head block 壓縮成 persistent block。Compaction 期間的 disk write 跟 CPU 使用會短暫上升。SSD 環境下 compaction 通常不是問題；HDD 環境下可能造成 scrape timeout。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「Cardinality 管理」跟「Memory pressure」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="定位">定位</h2>
<p>Prometheus 的容量模型跟傳統資料庫不同 — 它的容量邊界主要受 active series 數量（cardinality）跟 retention 期決定，而非資料筆數或 disk size。理解 Prometheus 的資源消耗模型，才能判斷什麼時候單機夠用、什麼時候需要 remote write 卸載或遷移到 Mimir / Thanos。</p>
<h2 id="資源消耗模型">資源消耗模型</h2>
<h3 id="memory由-active-series-決定">Memory：由 active series 決定</h3>
<p>Prometheus 把近期的 time series 保存在記憶體（head block）。每個 active series 大約消耗 3-4 KB 記憶體（含 index、chunks、postings；Prometheus TSDB 的業界經驗值，實際依 label 長度與 chunk encoding 而定）。</p>
<table>
  <thead>
      <tr>
          <th>Active series</th>
          <th>預估 memory（head block）</th>
          <th>適合的機器規格</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬</td>
          <td>~400 MB</td>
          <td>任何 VM</td>
      </tr>
      <tr>
          <td>100 萬</td>
          <td>~4 GB</td>
          <td>8 GB VM</td>
      </tr>
      <tr>
          <td>500 萬</td>
          <td>~20 GB</td>
          <td>32 GB VM</td>
      </tr>
      <tr>
          <td>1000 萬</td>
          <td>~40 GB</td>
          <td>64 GB VM</td>
      </tr>
  </tbody>
</table>
<p>這是 head block 的記憶體，不含 query execution 跟 WAL replay 的暫時開銷。Heavy PromQL query（大範圍 aggregation、多 series join）會額外消耗數 GB 的暫時記憶體。</p>
<p>判讀指標：<code>prometheus_tsdb_head_series</code> 代表當前 active series 數量，<code>process_resident_memory_bytes</code> 代表實際記憶體使用。兩者的比值偏離預期時（例如 50 萬 series 但記憶體用了 10 GB），可能是 query 記憶體壓力或 WAL corruption。</p>
<h3 id="disk由-retention-期與-ingestion-rate-決定">Disk：由 retention 期與 ingestion rate 決定</h3>
<p>Prometheus 的 disk 消耗 = ingestion rate × retention 期 × 壓縮後每 sample 大小（約 1-2 bytes，Gorilla 壓縮算法下的業界經驗值）。</p>
<table>
  <thead>
      <tr>
          <th>Ingestion rate</th>
          <th>Retention</th>
          <th>預估 disk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>15 天</td>
          <td>~130 GB</td>
      </tr>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>30 天</td>
          <td>~260 GB</td>
      </tr>
      <tr>
          <td>50 萬 samples/sec</td>
          <td>15 天</td>
          <td>~650 GB</td>
      </tr>
  </tbody>
</table>
<p>Disk I/O 的瓶頸通常在 compaction — Prometheus 定期把 head block 壓縮成 persistent block。Compaction 期間的 disk write 跟 CPU 使用會短暫上升。SSD 環境下 compaction 通常不是問題；HDD 環境下可能造成 scrape timeout。</p>
<h3 id="cpu由-scrape-數量與-query-負載決定">CPU：由 scrape 數量與 query 負載決定</h3>
<p>Scrape 本身的 CPU 消耗不高（HTTP GET + parse），但 scrape 數量 × scrape 間隔決定了基本的 CPU 基線。1000 個 target × 15 秒間隔 = 每秒 ~67 次 scrape，單核可以處理。</p>
<p>Query 是 CPU 的主要消耗者。Recording rule evaluation、alert rule evaluation、dashboard panel 查詢各自佔 CPU。Recording rule 數量增長到數百條時，evaluation 的 CPU 消耗可能成為瓶頸。</p>
<p>判讀指標：<code>prometheus_rule_evaluation_duration_seconds</code> 的 p99 超過 evaluation interval 時，rule 跑不完、alert 會延遲。</p>
<h2 id="cardinality-失控的判讀">Cardinality 失控的判讀</h2>
<p>Cardinality 是 Prometheus 最常見的容量問題。一個意外的高 cardinality label（user_id、request_id、完整 URL）可以在分鐘內把 series 數從 10 萬推到 100 萬、消耗數 GB 記憶體。</p>
<h3 id="判讀訊號">判讀訊號</h3>
<ul>
<li><code>prometheus_tsdb_head_series</code> 持續成長、斜率陡峭</li>
<li><code>prometheus_tsdb_head_active_appenders</code> 成長（新 series 的寫入速率）</li>
<li>Prometheus 的 memory 持續上升、最終 OOM kill</li>
<li>Query 延遲增加（更多 series 要掃描）</li>
<li>Compaction 時間變長</li>
</ul>
<h3 id="定位方式">定位方式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># 找出哪個 metric name 的 series 最多
</span></span><span class="line"><span class="ln">2</span><span class="cl">topk(10, count by (__name__)({__name__=~&#34;.+&#34;}))
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"># 找出哪個 job（scrape target）的 series 最多
</span></span><span class="line"><span class="ln">5</span><span class="cl">topk(10, count by (job)({__name__=~&#34;.+&#34;}))
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"># 找出某個 metric 的哪個 label 組合在爆
</span></span><span class="line"><span class="ln">8</span><span class="cl">count by (method, status) (http_requests_total)</span></span></code></pre></div><h3 id="修復方向">修復方向</h3>
<ul>
<li><strong>Label 白名單</strong>：在 scrape config 或 relabeling rule 中 drop 高 cardinality label</li>
<li><strong>Metric relabeling</strong>：<code>metric_relabel_configs</code> 在 scrape 後、寫入前移除特定 label</li>
<li><strong>Recording rule 替代</strong>：把高 cardinality metric 聚合成低 cardinality 的 recording rule，下游只讀 recording rule</li>
<li><strong>移到 traces</strong>：user_id / request_id 這類維度放在 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 的 span attribute 而非 metric label</li>
</ul>
<h2 id="常見故障模式">常見故障模式</h2>
<h3 id="oom-kill">OOM Kill</h3>
<p><strong>觸發條件</strong>：active series 超過記憶體容量、或 heavy query 消耗大量暫時記憶體。</p>
<p><strong>表現</strong>：Prometheus process 被 kernel OOM killer 終止。重啟後 WAL replay 可能需要分鐘到十分鐘（取決於 WAL 大小），期間 scrape 跟 query 都不可用。</p>
<p><strong>預防</strong>：設定 memory limit alert（process_resident_memory_bytes / machine memory &gt; 70%）、tracking cardinality growth slope、query timeout 限制。</p>
<h3 id="scrape-timeout-連鎖">Scrape timeout 連鎖</h3>
<p><strong>觸發條件</strong>：target 的 metrics endpoint 回應慢（&gt; scrape_timeout）、或 target 數量超過 Prometheus 的並行 scrape 能力。</p>
<p><strong>表現</strong>：<code>up</code> metric 為 0、<code>scrape_duration_seconds</code> 升高、dashboard 出現資料斷層（missing data points）。大量 target 同時 timeout 時，Prometheus 的 scrape goroutine pool 被佔滿，影響其他健康 target 的 scrape。</p>
<p><strong>修復</strong>：調整 <code>scrape_timeout</code>（預設 10s，太短會造成 false timeout）、把慢 target 移到獨立的 scrape pool、或把 metrics endpoint 的回應最佳化（減少 expose 的 metric 數量）。</p>
<h3 id="wal-corruption">WAL corruption</h3>
<p><strong>觸發條件</strong>：Prometheus process 非正常終止（OOM kill、機器斷電）時，WAL 可能損壞。</p>
<p><strong>表現</strong>：重啟後 WAL replay 失敗、Prometheus 無法啟動。Error log 顯示 <code>WAL corrupted</code> 或 <code>invalid segment</code>。</p>
<p><strong>修復</strong>：刪除損壞的 WAL segment（丟失對應時間段的資料），重啟 Prometheus。嚴重時刪除整個 data 目錄重新開始（丟失所有歷史資料）。WAL 的持久性保證不如資料庫 — Prometheus 設計上允許短暫資料丟失，長期儲存靠 remote write 到 Mimir / Thanos。</p>
<h3 id="recording-rule-evaluation-lag">Recording rule evaluation lag</h3>
<p><strong>觸發條件</strong>：recording rule 數量多且表達式複雜、evaluation 時間超過 evaluation interval。</p>
<p><strong>表現</strong>：<code>prometheus_rule_group_last_duration_seconds</code> 超過 <code>prometheus_rule_group_interval_seconds</code>。Dashboard 讀 recording rule 的 panel 看到的資料落後當前時間。Alert rule 也在同一個 evaluation pipeline 裡，evaluation lag 會讓 alert 延遲觸發。</p>
<p><strong>修復</strong>：把重的 recording rule 拆到獨立的 rule group（各自 evaluation interval）、最佳化 PromQL expression（減少 aggregation 層數、縮小 time range）、或把 recording rule 卸載到 Mimir（ruler component 獨立擴展）。</p>
<h2 id="何時該從單機-prometheus-遷出">何時該從單機 Prometheus 遷出</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Active series &gt; 500 萬、memory 吃緊（32 GB VM 上 head block ~20 GB + query overhead 接近上限）</td>
          <td>Remote write 到 Mimir / Thanos 做長期儲存</td>
      </tr>
      <tr>
          <td>需要跨 region / cluster 查詢</td>
          <td>Thanos query 或 Mimir multi-tenant</td>
      </tr>
      <tr>
          <td>Recording rule evaluation lag 持續</td>
          <td>把 rule evaluation 卸載到 Mimir ruler</td>
      </tr>
      <tr>
          <td>需要 HA（single Prometheus = SPOF）</td>
          <td>兩個 instance + Thanos dedup</td>
      </tr>
      <tr>
          <td>Retention 要 &gt; 90 天但 disk 不夠</td>
          <td>Remote write + 短 local retention</td>
      </tr>
  </tbody>
</table>
<p>遷出的第一步通常是加 remote write — Prometheus 繼續本地 scrape 跟短期查詢，長期資料寫到遠端。這是最低風險的演進路徑，不需要改 scrape config 或 PromQL。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：overview 跟日常操作</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a>：cardinality 治理的完整策略</li>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a>：recording rule 跟 rollup 的查詢面設計</li>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>：Mimir 作為 Prometheus 的長期儲存後端</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：recording rule 在查詢設計中的定位</li>
</ul>
]]></content:encoded></item><item><title>PromQL 與 Recording Rules 實務</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/promql-recording-rules/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/promql-recording-rules/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「PromQL 查詢」跟「Recording rules / Alerting rules」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Recording rules 把昂貴的即時聚合預先計算成低延遲 series，降低 dashboard 查詢成本並穩定 alerting 表達式。三個觸發點會讓團隊需要認真處理 PromQL 與 recording rules：&lt;/p>
&lt;p>Grafana dashboard 的某些 panel 載入超過 10 秒。原因通常是 panel 直接查詢高 cardinality 的原始 metric，每次載入都做一次完整的 range query aggregation。Recording rules 預先計算聚合結果，dashboard 只讀計算好的 series，查詢時間從秒級降到毫秒級。&lt;/p>
&lt;p>Alert 表達式想表達「最近 5 分鐘的 error rate 超過 1% 且持續 2 分鐘」，但寫出來的 PromQL 要麼漏抓（counter reset 時 rate 歸零）、要麼誤報（absent series 觸發 NaN 比較）。這類問題的根源是對 counter vs gauge 的語意差異理解不夠精確。&lt;/p>
&lt;p>Recording rules 堆了上百條但沒有命名慣例，新加的 rule 不確定是否跟既有 rule 重疊、也不確定 evaluation 順序是否正確。缺乏結構化的 rule 管理會讓 rule group 的 evaluation 時間逐漸超過 interval。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="counter-與-gauge-的查詢差異">Counter 與 gauge 的查詢差異&lt;/h3>
&lt;p>Counter 是單調遞增的累計值（total requests、total bytes sent），只在 process 重啟時 reset。Gauge 是瞬時值（temperature、goroutine count、queue depth），隨時上下波動。&lt;/p>
&lt;p>查詢 counter 必須用 &lt;code>rate()&lt;/code> 或 &lt;code>increase()&lt;/code> — 直接讀 counter 的原始值沒有業務意義（「從啟動到現在共 5 百萬個 request」不是有用訊號）。&lt;code>rate()&lt;/code> 回傳每秒平均增量，&lt;code>increase()&lt;/code> 回傳區間內的總增量。兩者都自動處理 counter reset — 當值突然下降時（process restart），rate 不會回傳負值。&lt;/p>
&lt;p>查詢 gauge 直接讀原始值即可，用 &lt;code>avg_over_time()&lt;/code>、&lt;code>max_over_time()&lt;/code> 等做區間統計。&lt;/p>
&lt;p>常見錯誤是對 gauge 用 rate（結果無意義 — 溫度的「每秒變化率」不是有用訊號）、或對 counter 直接取 max_over_time（只拿到 counter 的最大累計值、不是最大 QPS）。&lt;/p>
&lt;h3 id="rate-與-increase-的差異">rate 與 increase 的差異&lt;/h3>
&lt;p>&lt;code>rate(http_requests_total[5m])&lt;/code> 回傳 5 分鐘內的平均每秒 request 數。&lt;code>increase(http_requests_total[5m])&lt;/code> 回傳 5 分鐘內的總增量，等於 &lt;code>rate() * 300&lt;/code>。&lt;/p>
&lt;p>選擇取決於讀者的心智模型：SLI dashboard 用 rate（「每秒多少」直觀）；報表用 increase（「過去一小時多少筆」直觀）。&lt;/p>
&lt;p>Range 的選擇有一個實務邊界：range 至少要涵蓋 2 個 scrape interval。15 秒 scrape interval 搭配 &lt;code>rate(...[30s])&lt;/code> 是最小可用 range；&lt;code>rate(...[15s])&lt;/code> 可能只抓到一個 sample，回傳 NaN。production 常用 &lt;code>[5m]&lt;/code> 作為預設 range — 足夠平滑短暫抖動、又不會過度延遲異常偵測。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「PromQL 查詢」跟「Recording rules / Alerting rules」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Recording rules 把昂貴的即時聚合預先計算成低延遲 series，降低 dashboard 查詢成本並穩定 alerting 表達式。三個觸發點會讓團隊需要認真處理 PromQL 與 recording rules：</p>
<p>Grafana dashboard 的某些 panel 載入超過 10 秒。原因通常是 panel 直接查詢高 cardinality 的原始 metric，每次載入都做一次完整的 range query aggregation。Recording rules 預先計算聚合結果，dashboard 只讀計算好的 series，查詢時間從秒級降到毫秒級。</p>
<p>Alert 表達式想表達「最近 5 分鐘的 error rate 超過 1% 且持續 2 分鐘」，但寫出來的 PromQL 要麼漏抓（counter reset 時 rate 歸零）、要麼誤報（absent series 觸發 NaN 比較）。這類問題的根源是對 counter vs gauge 的語意差異理解不夠精確。</p>
<p>Recording rules 堆了上百條但沒有命名慣例，新加的 rule 不確定是否跟既有 rule 重疊、也不確定 evaluation 順序是否正確。缺乏結構化的 rule 管理會讓 rule group 的 evaluation 時間逐漸超過 interval。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="counter-與-gauge-的查詢差異">Counter 與 gauge 的查詢差異</h3>
<p>Counter 是單調遞增的累計值（total requests、total bytes sent），只在 process 重啟時 reset。Gauge 是瞬時值（temperature、goroutine count、queue depth），隨時上下波動。</p>
<p>查詢 counter 必須用 <code>rate()</code> 或 <code>increase()</code> — 直接讀 counter 的原始值沒有業務意義（「從啟動到現在共 5 百萬個 request」不是有用訊號）。<code>rate()</code> 回傳每秒平均增量，<code>increase()</code> 回傳區間內的總增量。兩者都自動處理 counter reset — 當值突然下降時（process restart），rate 不會回傳負值。</p>
<p>查詢 gauge 直接讀原始值即可，用 <code>avg_over_time()</code>、<code>max_over_time()</code> 等做區間統計。</p>
<p>常見錯誤是對 gauge 用 rate（結果無意義 — 溫度的「每秒變化率」不是有用訊號）、或對 counter 直接取 max_over_time（只拿到 counter 的最大累計值、不是最大 QPS）。</p>
<h3 id="rate-與-increase-的差異">rate 與 increase 的差異</h3>
<p><code>rate(http_requests_total[5m])</code> 回傳 5 分鐘內的平均每秒 request 數。<code>increase(http_requests_total[5m])</code> 回傳 5 分鐘內的總增量，等於 <code>rate() * 300</code>。</p>
<p>選擇取決於讀者的心智模型：SLI dashboard 用 rate（「每秒多少」直觀）；報表用 increase（「過去一小時多少筆」直觀）。</p>
<p>Range 的選擇有一個實務邊界：range 至少要涵蓋 2 個 scrape interval。15 秒 scrape interval 搭配 <code>rate(...[30s])</code> 是最小可用 range；<code>rate(...[15s])</code> 可能只抓到一個 sample，回傳 NaN。production 常用 <code>[5m]</code> 作為預設 range — 足夠平滑短暫抖動、又不會過度延遲異常偵測。</p>
<h3 id="histogram_quantile-的-bucket-設計">histogram_quantile 的 bucket 設計</h3>
<p>Prometheus histogram 使用預定義 bucket 邊界收集觀測值分布。<code>histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))</code> 計算 p95 延遲。</p>
<p>Bucket 邊界的設計直接影響精確度。預設 bucket（0.005, 0.01, 0.025, &hellip; 10）適合 HTTP request 延遲場景。如果服務的 p50 在 200ms 而 bucket 只有 0.1 跟 0.25 兩個相鄰邊界，p50 的計算會在 100ms-250ms 之間做線性內插，精確度受限。</p>
<p>設計 bucket 的判準：p50 和 p99 附近各要有 2-3 個相鄰 bucket，讓內插結果接近真實值。SLO 的 latency threshold 也應該落在某個 bucket 邊界上 — 例如 SLO 是 p95 &lt; 500ms，那 500ms 應該是一個 bucket 邊界。</p>
<p>每個 bucket 是一個 time series。10 個 bucket 的 histogram + 4 個 label 組合 = 40 個 series。Bucket 數量增加到 30 個時，同一個 metric 的 series 數量膨脹 3 倍。Bucket 設計要在精確度與 cardinality 之間取捨。</p>
<h3 id="label-matching-規則">Label matching 規則</h3>
<p>PromQL 的 binary operation（<code>/</code>、<code>+</code>、comparison）預設要求兩邊的 label set 完全一致才做 matching。這會在 error rate 計算時造成問題：<code>rate(http_requests_total{status=~&quot;5..&quot;}[5m])</code> 的 label set 含 status、但 <code>rate(http_requests_total[5m])</code> 的 total 不含 status。</p>
<p>解法是在分子做 aggregation 時 drop 掉 status label：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-promql" data-lang="promql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">sum</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="o">(</span><span class="nv">job</span><span class="p">,</span><span class="w"> </span><span class="nv">method</span><span class="o">)</span><span class="w"> </span><span class="o">(</span><span class="kr">rate</span><span class="o">(</span><span class="nv">http_requests_total</span><span class="p">{</span><span class="nl">status</span><span class="o">=~</span><span class="p">&#34;</span><span class="s">5..</span><span class="p">&#34;}[</span><span class="s">5m</span><span class="p">]</span><span class="o">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="o">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">sum</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="o">(</span><span class="nv">job</span><span class="p">,</span><span class="w"> </span><span class="nv">method</span><span class="o">)</span><span class="w"> </span><span class="o">(</span><span class="kr">rate</span><span class="o">(</span><span class="nv">http_requests_total</span><span class="p">[</span><span class="s">5m</span><span class="p">]</span><span class="o">))</span></span></span></code></pre></div><p><code>on()</code> 和 <code>ignoring()</code> 修飾符可以在不做 aggregation 的前提下控制 matching，但可讀性較差。production 推薦的做法是先用 <code>sum by()</code> 控制輸出的 label set，讓兩邊的 label 對齊。</p>
<h2 id="配置常見-sli-pattern">配置：常見 SLI Pattern</h2>
<h3 id="error-rate">Error rate</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># recording rule: 每 5 分鐘計算一次 error rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">groups</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sli_error_rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_error_rate:ratio_rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sd">          sum by (job) (rate(http_requests_total{status=~&#34;5..&#34;}[5m]))
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">          /
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">          sum by (job) (rate(http_requests_total[5m]))</span></span></span></code></pre></div><p>命名慣例 <code>level:metric:operations</code> 來自 Prometheus 官方建議：<code>job</code> 是聚合的 level、<code>http_request_error_rate</code> 是語意、<code>ratio_rate5m</code> 是操作。遵循慣例讓團隊成員看到 rule 名稱就知道它的聚合粒度與計算方式。</p>
<h3 id="latency-percentile">Latency percentile</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_duration_seconds:p95_rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">          histogram_quantile(0.95,
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="sd">            sum by (job, le) (rate(http_request_duration_seconds_bucket[5m]))
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="sd">          )</span></span></span></code></pre></div><p><code>le</code> label 是 histogram bucket 邊界，<code>sum by (job, le)</code> 把 instance 維度聚合掉、保留 bucket 結構。如果漏掉 <code>le</code>，<code>histogram_quantile</code> 會回傳錯誤結果。</p>
<h3 id="throughput">Throughput</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_requests:rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">sum by (job) (rate(http_requests_total[5m]))</span></span></span></code></pre></div><p>三個 SLI — error rate、latency、throughput — 組成服務的 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">RED metrics</a>（Rate、Errors、Duration）。Recording rules 預先計算後，dashboard 只需讀三個 series。</p>
<h3 id="alerting-rule-搭配-recording-rule">Alerting rule 搭配 recording rule</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sli_alerts</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">      </span>- <span class="nt">alert</span><span class="p">:</span><span class="w"> </span><span class="l">HighErrorRate</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_error_rate:ratio_rate5m &gt; 0.01</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">        </span><span class="nt">for</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">        </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">          </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">page</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">        </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">          </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{ $labels.job }} error rate above 1% for 5 minutes&#34;</span></span></span></code></pre></div><p>Alert 表達式讀 recording rule 而非原始 metric。好處有二：alert evaluation 更快（讀預先計算的 series）、alert 表達式與 dashboard panel 使用同一組 recording rule（確保看到的數字一致）。</p>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="series-churn-導致-absent-判斷失準">Series churn 導致 absent() 判斷失準</h3>
<p><code>absent(up{job=&quot;myapp&quot;})</code> 用來偵測 target 完全消失（沒在 scrape）。但在 K8s 環境，pod 頻繁 rolling update 會造成 series churn — 舊 pod 的 series 消失、新 pod 的 series 出現。短暫的時間窗內 <code>absent()</code> 可能誤觸。</p>
<p>修法：用 <code>absent_over_time(up{job=&quot;myapp&quot;}[5m])</code> 替代，要求整個 5 分鐘區間都沒有 series 才觸發。或用 <code>count(up{job=&quot;myapp&quot;}) == 0</code> 明確檢查 series 數量。</p>
<h3 id="recording-rules-circular-dependency">Recording rules circular dependency</h3>
<p>Rule group A 的 rule 讀 rule group B 的 recording rule、group B 又讀 group A 的結果。Prometheus 按 group name 字母序 evaluate，circular dependency 會讓一方讀到上一輪的 stale 結果。</p>
<p>預防方式：recording rules 形成 DAG（有向無環圖）。Prometheus 文件建議把 rule 分成 aggregation 層級 — 底層 group 算 raw metric 的 aggregation、上層 group 算 recording rule 的 aggregation。同一個 group 內的 rule 按宣告順序同步 evaluate。</p>
<h3 id="大-range-query-oom">大 range query OOM</h3>
<p>Dashboard panel 用 <code>rate(metric[30d])</code> 查詢 30 天 range — Prometheus 要載入 30 天的 samples 到記憶體做計算。100 萬 series × 30 天 × 15 秒 interval ≈ 1.7 億 samples per series 是不可能完成的查詢。</p>
<p>修法：長時間 range 必須用 recording rules 做 step-down aggregation。先用 <code>rate(...[5m])</code> recording rule 每 30 秒算一次、再用 <code>avg_over_time(recording_rule[30d])</code> 查詢。Recording rule 的 series 數量通常比原始 metric 少一到兩個數量級。</p>
<p>Prometheus 2.x 支援 <code>--query.max-samples</code> flag 限制單一 query 能處理的 sample 數量（預設 5000 萬），超過就回傳 error。這是 OOM 的最後防線、不是常態。</p>
<h3 id="counter-reset-導致-rate-異常">Counter reset 導致 rate 異常</h3>
<p>Process 重啟時 counter 歸零。<code>rate()</code> 和 <code>increase()</code> 自動偵測 counter reset 並補償，但有邊界條件：如果 scrape interval 內發生多次 restart（例如 crash loop），<code>rate()</code> 可能低估真實值（只能偵測到一次 reset）。</p>
<p>這種情境下的判讀：如果 <code>rate()</code> 的結果明顯低於預期、且同時段有 pod restart 紀錄，rate 低估是正常的。修法是解決 crash loop 本身、而非調整 PromQL。</p>
<h2 id="容量與-cost">容量與 Cost</h2>
<p>Recording rules 的 CPU 成本 = rule 數量 × 每條 rule 的 evaluation 時間 × (1 / evaluation interval)。</p>
<table>
  <thead>
      <tr>
          <th>Rule 數量</th>
          <th>平均 evaluation 時間</th>
          <th>Interval</th>
          <th>每秒 evaluation 消耗</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>50</td>
          <td>10ms</td>
          <td>30s</td>
          <td>50 × 0.01 / 30 = 0.017 core</td>
      </tr>
      <tr>
          <td>200</td>
          <td>50ms</td>
          <td>30s</td>
          <td>200 × 0.05 / 30 = 0.33 core</td>
      </tr>
      <tr>
          <td>500</td>
          <td>100ms</td>
          <td>15s</td>
          <td>500 × 0.1 / 15 = 3.33 core</td>
      </tr>
  </tbody>
</table>
<p>表中的 evaluation 時間是 10 萬到 50 萬 active series 規模下的經驗值。Series 數量影響 evaluation 時間 — 100 萬 series 的 complex aggregation 可能 500ms+，跟表中假設偏差很大。用 <code>prometheus_rule_group_last_duration_seconds</code> 量測自己環境的實際值。</p>
<p>500 條 complex rule 搭配 15 秒 interval 會消耗超過 3 個 CPU core 在 rule evaluation 上。這時候的修法方向有三：</p>
<ul>
<li>把 evaluation interval 放寬到 30s 或 60s（犧牲即時性）</li>
<li>把 rule 表達式最佳化（減少 aggregation 層數）</li>
<li>把 rule evaluation 卸載到 Mimir ruler（水平擴展）</li>
</ul>
<p>Recording rules 產生的新 series 也會增加 cardinality。200 條 recording rule × 平均 5 個 label 組合 = 1000 個新 series，通常可接受。但如果 recording rule 沒做 aggregation 而是直接 alias（<code>record: new_name expr: old_metric</code>），cardinality 不會減少，只增加了寫入成本。</p>
<p>判讀指標：<code>prometheus_rule_group_last_duration_seconds</code> 跟 <code>prometheus_rule_group_interval_seconds</code> 的比值。前者超過後者時，evaluation 跑不完、dashboard 跟 alert 都會延遲。見 <a href="../capacity-failure-modes/">容量規劃與故障模式</a> 的 Recording rule evaluation lag 段。</p>
<h3 id="recording-rules-作為成本控制工具">Recording rules 作為成本控制工具</h3>
<p><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">觀測成本治理案例</a>提出一個被低估的用法：recording rules 不只是加速查詢、也是控制 remote write 成本的手段。</p>
<p>模式是這樣的：application 暴露 200 個 label 組合的原始 metric（per-endpoint × per-status × per-region），recording rule 聚合成 5 個 label 組合（per-service × per-region）。如果 remote write 設定了 <code>write_relabel_configs</code> drop 掉原始 series、只 forward recording rule 產生的 aggregated series，remote write bandwidth 跟長期儲存的 cardinality 都大幅降低。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Step 1: recording rule 做 aggregation</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">groups</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cost_optimized</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">service_region:http_requests:rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">sum by (service, region) (rate(http_requests_total[5m]))</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c"># Step 2: remote write 只送 aggregated series</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="nt">remote_write</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;http://mimir:9009/api/v1/push&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">__name__]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;service_region:.*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">keep</span></span></span></code></pre></div><p>這個模式的取捨：長期儲存只有 aggregated 資料、無法回溯到原始 per-endpoint 維度。如果事故時需要 per-endpoint 的歷史資料，要麼保留原始 series 在本地 Prometheus（短期 retention）、要麼接受長期儲存只有 aggregated 粒度。</p>
<p>適用場景判斷：如果 dashboard 跟 alert 都只看 service-level 聚合、per-endpoint 維度只在即時除錯時才需要（Prometheus 本地 15 天 retention 夠用），這個模式的成本節省值得。如果有合規需求要 per-endpoint 歷史資料（例如 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">FinTech 案例</a> 的 evidence chain），就不能 drop 原始 series。</p>
<h3 id="evaluation-interval-對-cpu-的影響">Evaluation interval 對 CPU 的影響</h3>
<p>Rule group 的 <code>interval</code> 決定 evaluation 頻率。同一組 rules 從 30s interval 改成 15s interval，CPU 消耗翻倍。從 30s 改成 60s，CPU 減半但 alert 跟 dashboard 的即時性下降。</p>
<p>經驗值：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議 interval</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLI / SLO recording rules</td>
          <td>30s</td>
          <td>平衡即時性跟成本、多數 burn rate alert 的最小 window 是 5 分鐘</td>
      </tr>
      <tr>
          <td>Capacity trending rules</td>
          <td>60s-120s</td>
          <td>趨勢不需要秒級即時性</td>
      </tr>
      <tr>
          <td>High-frequency operational rules</td>
          <td>15s</td>
          <td>需要跟 scrape interval 對齊的場景（例如 real-time anomaly detection）</td>
      </tr>
  </tbody>
</table>
<p>15 秒 interval 的 rule group 要特別注意 evaluation 時間 — 如果 evaluation 本身花 12 秒，只剩 3 秒 buffer。<code>prometheus_rule_group_last_duration_seconds</code> 持續接近 <code>prometheus_rule_group_interval_seconds</code> 時，要麼拆 rule group 到不同 Prometheus instance、要麼放寬 interval。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="alertmanager">Alertmanager</h3>
<p>Alert rule 寫在 Prometheus 的 <code>rule_files</code> 內、觸發後送到 Alertmanager。Alertmanager 負責去重、分組、抑制與路由（route to PagerDuty / Slack / email）。Alert rule 的表達式跟 recording rule 共用同一組語意 — 讀 recording rule 而非原始 metric。</p>
<h3 id="grafana-dashboard">Grafana dashboard</h3>
<p>Grafana 的 Prometheus datasource 直接查 PromQL。Dashboard panel 推薦讀 recording rule series 而非寫 raw PromQL — 減少 dashboard 載入時間、確保 dashboard 跟 alert 看到的數字一致。</p>
<h3 id="對齊-slislo">對齊 SLI/SLO</h3>
<p>Recording rules 產生的 SLI metrics 是 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a> 的資料來源。SLO burn rate alert 也讀同一組 recording rule。確保 SLI recording rule 的 time window 跟 SLO window 對齊（例如 SLO 用 30 天 rolling window，recording rule 至少提供 5m 和 1h 兩個 aggregation 粒度給 burn rate 計算）。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：overview 跟日常操作入口</li>
<li><a href="../capacity-failure-modes/">容量規劃與故障模式</a>：recording rules 成長後的資源衝擊</li>
<li><a href="../remote-write-long-term-storage/">Remote Write 與長期儲存整合</a>：recording rule 在 remote write 架構下的部署選擇</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>：recording rules 如何餵給 SLO burn rate</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理</a>：recording rules 作為 cardinality 減量手段</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：recording rules 在 pre-aggregation 與 query tiering 中的定位</li>
</ul>
]]></content:encoded></item><item><title>Remote Write 與長期儲存整合</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「Remote write / read」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Remote write 把 Prometheus 的 metrics 即時推送到外部長期儲存，解決單機 retention 上限與跨實例統一查詢的限制。三個觸發點會讓團隊需要 remote write 與長期儲存：&lt;/p>
&lt;p>Prometheus 預設 retention 是 15 天。業務需要回顧 90 天的趨勢（容量規劃、季度 SLO 報告、成本歸因），本地 disk 不夠放。加大 disk 可以延長 retention，但 Prometheus 的查詢效能會隨資料量下降 — 本地 TSDB 不做 downsampling，查 90 天 range 的 query 要掃描全量 sample。&lt;/p>
&lt;p>多個 Prometheus 實例分散在不同叢集（prod-us、prod-eu、staging），團隊需要一個統一查詢入口看跨叢集 metrics。每個 Prometheus 各自保存自己的資料，沒有跨實例查詢能力。手動切換 Grafana datasource 容易遺漏某個叢集的異常。&lt;/p>
&lt;p>單機 Prometheus 是 SPOF — process crash 或 VM 故障時 metrics 完全不可用。跑兩個 Prometheus 各自 scrape 同一組 target 可以達到 HA，但兩份資料有微小差異（scrape 時間偏移），下游查詢需要 dedup。&lt;/p>
&lt;p>Remote write 解決這三個問題：Prometheus 保持短期本地儲存（scrape + 即時查詢），同時把 metrics 串流到長期儲存後端。長期後端負責壓縮、downsampling、跨實例查詢與 HA dedup。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="remote-write-protocol">Remote write protocol&lt;/h3>
&lt;p>Prometheus 透過 HTTP POST 把 time series 送到 remote write endpoint。每次 POST 包含一批 samples（protobuf 編碼、snappy 壓縮），由 Prometheus 的 WAL（write-ahead log）驅動 — WAL 記錄所有 scrape 到的 samples，remote write 從 WAL 讀取並串流到遠端。&lt;/p>
&lt;p>這個設計意味著 remote write 是 best-effort 但有 buffer：如果遠端暫時不可達，samples 會堆在 WAL 裡等重試。WAL 的大小有上限（&lt;code>--storage.tsdb.wal-segment-size&lt;/code>，預設 128 MB per segment），堆積太多會導致 WAL 佔用大量 disk。&lt;/p>
&lt;h3 id="exemplar-forwarding">Exemplar forwarding&lt;/h3>
&lt;p>Prometheus 2.26 開始支援 exemplar — 在 histogram 或 counter sample 上附加 trace_id / span_id。Remote write 也能把 exemplar 送到支援的後端（Mimir、Grafana Cloud、Tempo）。Exemplar 讓讀者從 metric anomaly 一鍵跳到對應的 trace，是 metrics-to-traces 橋接的關鍵能力。&lt;/p>
&lt;p>啟用方式：scrape config 加 &lt;code>enable_features: [exemplar-storage]&lt;/code>，remote write endpoint 支援 exemplar 即可自動 forward。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「Remote write / read」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Remote write 把 Prometheus 的 metrics 即時推送到外部長期儲存，解決單機 retention 上限與跨實例統一查詢的限制。三個觸發點會讓團隊需要 remote write 與長期儲存：</p>
<p>Prometheus 預設 retention 是 15 天。業務需要回顧 90 天的趨勢（容量規劃、季度 SLO 報告、成本歸因），本地 disk 不夠放。加大 disk 可以延長 retention，但 Prometheus 的查詢效能會隨資料量下降 — 本地 TSDB 不做 downsampling，查 90 天 range 的 query 要掃描全量 sample。</p>
<p>多個 Prometheus 實例分散在不同叢集（prod-us、prod-eu、staging），團隊需要一個統一查詢入口看跨叢集 metrics。每個 Prometheus 各自保存自己的資料，沒有跨實例查詢能力。手動切換 Grafana datasource 容易遺漏某個叢集的異常。</p>
<p>單機 Prometheus 是 SPOF — process crash 或 VM 故障時 metrics 完全不可用。跑兩個 Prometheus 各自 scrape 同一組 target 可以達到 HA，但兩份資料有微小差異（scrape 時間偏移），下游查詢需要 dedup。</p>
<p>Remote write 解決這三個問題：Prometheus 保持短期本地儲存（scrape + 即時查詢），同時把 metrics 串流到長期儲存後端。長期後端負責壓縮、downsampling、跨實例查詢與 HA dedup。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="remote-write-protocol">Remote write protocol</h3>
<p>Prometheus 透過 HTTP POST 把 time series 送到 remote write endpoint。每次 POST 包含一批 samples（protobuf 編碼、snappy 壓縮），由 Prometheus 的 WAL（write-ahead log）驅動 — WAL 記錄所有 scrape 到的 samples，remote write 從 WAL 讀取並串流到遠端。</p>
<p>這個設計意味著 remote write 是 best-effort 但有 buffer：如果遠端暫時不可達，samples 會堆在 WAL 裡等重試。WAL 的大小有上限（<code>--storage.tsdb.wal-segment-size</code>，預設 128 MB per segment），堆積太多會導致 WAL 佔用大量 disk。</p>
<h3 id="exemplar-forwarding">Exemplar forwarding</h3>
<p>Prometheus 2.26 開始支援 exemplar — 在 histogram 或 counter sample 上附加 trace_id / span_id。Remote write 也能把 exemplar 送到支援的後端（Mimir、Grafana Cloud、Tempo）。Exemplar 讓讀者從 metric anomaly 一鍵跳到對應的 trace，是 metrics-to-traces 橋接的關鍵能力。</p>
<p>啟用方式：scrape config 加 <code>enable_features: [exemplar-storage]</code>，remote write endpoint 支援 exemplar 即可自動 forward。</p>
<h3 id="dedup-策略">Dedup 策略</h3>
<p>跑兩個 Prometheus HA pair 時，兩個實例都 scrape 同一組 target、都 remote write 到同一個後端。後端會收到兩份幾乎相同但不完全一致的 samples（scrape 時間差 ±1-2 秒）。</p>
<p>Thanos 和 Mimir 都有 dedup 機制：Thanos 在 query 層根據 <code>external_labels</code>（replica label）做 dedup，每個 time window 只取一個 replica 的值。Mimir 在 ingester 層做 dedup，同一個 series 的重複 sample 在寫入時合併。</p>
<p>Dedup 的前提是兩個 Prometheus 實例設定不同的 <code>external_labels</code>（例如 <code>replica: a</code> / <code>replica: b</code>），讓後端能辨別哪些 series 是同一組的不同副本。</p>
<h2 id="配置">配置</h2>
<h3 id="remote-write-基本設定">Remote write 基本設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">remote_write</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;http://mimir-distributor:9009/api/v1/push&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">queue_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">capacity</span><span class="p">:</span><span class="w"> </span><span class="m">10000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">max_shards</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">max_samples_per_send</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">batch_send_deadline</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">__name__]</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;go_.*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">drop</span></span></span></code></pre></div><p><code>queue_config</code> 控制 remote write 的並行度與批次大小：</p>
<ul>
<li><code>capacity</code>：內存中暫存的 sample 數量。太小會頻繁 flush、太大會佔記憶體</li>
<li><code>max_shards</code>：並行的 write goroutine 數量。Shard 太少會造成 backlog、太多會壓垮遠端</li>
<li><code>max_samples_per_send</code>：每次 POST 的 sample 數量。5000 是常用值</li>
<li><code>batch_send_deadline</code>：即使 batch 沒滿也在這個時間內 flush，避免低流量時 sample 延遲太久</li>
</ul>
<p><code>write_relabel_configs</code> 在 remote write 前過濾 series — 不需要長期保存的 internal metrics（go runtime、scrape metadata）可以在這裡 drop，減少長期儲存的 cardinality 與成本。</p>
<h3 id="external-labelsha-與多叢集">External labels（HA 與多叢集）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">external_labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">cluster</span><span class="p">:</span><span class="w"> </span><span class="l">prod-us</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">replica</span><span class="p">:</span><span class="w"> </span><span class="l">a</span></span></span></code></pre></div><p><code>cluster</code> label 區分來源叢集，<code>replica</code> label 讓長期儲存做 dedup。每個 Prometheus 實例的 external_labels 必須唯一。</p>
<h3 id="三家長期儲存比較">三家長期儲存比較</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Mimir</th>
          <th>Thanos</th>
          <th>Cortex</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>架構模式</td>
          <td>Microservice（distributor / ingester / compactor / querier）</td>
          <td>Sidecar + Store Gateway + Compactor + Query</td>
          <td>Microservice（跟 Mimir 同源、Mimir 是 Cortex fork）</td>
      </tr>
      <tr>
          <td>部署複雜度</td>
          <td>中（Helm chart，最少 4 個元件）</td>
          <td>中高（sidecar 綁 Prometheus pod，元件分散）</td>
          <td>高（元件多、已進入維護模式）</td>
      </tr>
      <tr>
          <td>Query layer</td>
          <td>原生 PromQL + split/merge</td>
          <td>Thanos Query 做 fan-out + dedup</td>
          <td>原生 PromQL（跟 Mimir 共用）</td>
      </tr>
      <tr>
          <td>多租戶</td>
          <td>原生（X-Scope-OrgID header）</td>
          <td>有限（靠 label 或獨立部署）</td>
          <td>原生（Mimir 繼承）</td>
      </tr>
      <tr>
          <td>Downsampling</td>
          <td>支援（compactor 做 1h/5m 降取樣）</td>
          <td>支援（compactor）</td>
          <td>支援</td>
      </tr>
      <tr>
          <td>開發狀態</td>
          <td>活躍（Grafana Labs 主推）</td>
          <td>活躍（CNCF incubating）</td>
          <td>維護模式（Grafana Labs 把精力轉到 Mimir）</td>
      </tr>
      <tr>
          <td>對象儲存</td>
          <td>S3 / GCS / Azure Blob</td>
          <td>S3 / GCS / Azure Blob / 本地</td>
          <td>S3 / GCS</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>自管 compute + storage；Grafana Cloud 按 active series 計費</td>
          <td>自管 compute + storage</td>
          <td>自管（不推薦新部署）</td>
      </tr>
  </tbody>
</table>
<p>選擇判準依三個維度排序：</p>
<p><strong>已經在用 Grafana 生態</strong>（Grafana dashboard、Loki、Tempo）：Mimir 是最自然的選擇，跟 Grafana Stack 的整合最深，Grafana Cloud 可以免管 Mimir。</p>
<p><strong>需要最小化對 Prometheus 的改動</strong>：Thanos sidecar 模式不改 Prometheus 配置（sidecar 讀本地 TSDB block），適合「先加長期儲存、Prometheus 維持現狀」的漸進路徑。但 sidecar 綁 Prometheus pod，K8s 環境外的部署更複雜。</p>
<p><strong>多租戶需求</strong>：Mimir 原生支援多租戶隔離（每個 tenant 獨立 TSDB、query isolation），Thanos 的多租戶靠 label 或獨立部署。</p>
<p>Cortex 是 Mimir 的前身，新部署不推薦。既有 Cortex 部署可參考 Grafana Labs 的 Mimir migration guide。</p>
<h3 id="uber-m3-的第四條路">Uber M3 的第四條路</h3>
<p><a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">Uber M3 案例</a>選擇了自建 M3DB 而非 Mimir / Thanos / Cortex — 原因是 M3DB 在 2018 年啟動時、Mimir 尚未存在、Cortex 還在早期階段、Thanos 也剛開源。M3DB 的設計核心是 namespace-level retention（不同 namespace 不同 retention 跟 resolution）、跟 Uber 的 etcd service discovery 深度整合。</p>
<p>M3 的經驗對後來的三家有直接影響：Mimir 的 per-tenant retention、Thanos 的 downsampling compactor、都能追溯到 M3 先踩過的問題。今天做新部署不需要重走 M3 的路 — Mimir 跟 Thanos 已經成熟。但 M3 案例揭露的設計判準仍然有效：</p>
<ul>
<li><strong>跨 cluster 查詢需要 fan-out + dedup</strong>：三家都實作了這個能力，但部署配置跟 dedup 策略各有差異</li>
<li><strong>Downsampling 是長期成本控制的必要手段</strong>：不做 downsampling、90 天 range query 的效能跟成本都不可接受</li>
<li><strong>多租戶隔離不只是 query 層面</strong>：ingestion rate limit 跟 storage quota per tenant 才能防止「一個團隊的 cardinality 爆炸拖垮整個平台」</li>
</ul>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="remote-write-backlog-佔滿-wal">Remote write backlog 佔滿 WAL</h3>
<p><strong>觸發條件</strong>：遠端不可達（network 問題、後端過載）持續超過數分鐘，WAL segment 堆積。</p>
<p><strong>表現</strong>：<code>prometheus_remote_storage_bytes_total</code> 停止增長（寫不出去）、<code>prometheus_wal_storage_size_bytes</code> 持續增長、disk 使用率上升。嚴重時 WAL 佔滿 disk，Prometheus 無法寫入新 sample、連 local scrape 也受影響。</p>
<p><strong>修復</strong>：先恢復遠端連線。WAL backlog 會在連線恢復後自動 catch up — Prometheus 按 WAL 順序重送積壓的 samples。如果 catch up 時間太長（例如堆了數小時），remote write 的 max_shards 可以暫時調高加速回補，但要注意不要壓垮剛恢復的遠端。</p>
<p><strong>預防</strong>：監控 <code>prometheus_remote_storage_queue_highest_sent_timestamp_seconds</code> 跟 current time 的差距 — 差距代表 remote write 延遲。差距超過 5 分鐘時告警。設定 WAL 的 disk 空間上限（<code>--storage.tsdb.max-block-duration</code> 搭配 retention 控制 total disk）。</p>
<h3 id="target-不可達時的-retry-storm">Target 不可達時的 retry storm</h3>
<p><strong>觸發條件</strong>：remote write endpoint 回傳 5xx 或 429（rate limit），Prometheus 進入指數退避重試。大量 shard 同時 retry，CPU 跟 network 消耗上升。</p>
<p><strong>表現</strong>：<code>prometheus_remote_storage_retried_samples_total</code> 增長、CPU 使用上升、remote write 延遲拉大。如果後端本來就過載，retry storm 會讓情況惡化。</p>
<p><strong>修復</strong>：remote write 配置中的 <code>min_backoff</code> / <code>max_backoff</code> 控制 retry 間隔（預設 30ms / 5s）。可以調高 <code>min_backoff</code> 減緩 retry 頻率。長期修法是讓後端回傳 429 搭配 <code>Retry-After</code> header，Prometheus 會遵守。</p>
<h3 id="metrics-語意-drift">Metrics 語意 drift</h3>
<p><strong>觸發條件</strong>：多個 Prometheus 實例的 <code>write_relabel_configs</code> 不一致、或 external_labels 設定有誤。</p>
<p><strong>表現</strong>：同一個 metric 在長期儲存中出現語意不同的 series — 有些 instance 保留了某個 label、有些 drop 掉了。Dashboard 查詢結果不一致（取決於查到哪個實例的 series）。</p>
<p><strong>修復</strong>：remote write 的 <code>write_relabel_configs</code> 集中管理（配置模板或 Prometheus Operator 的 PrometheusSpec.remoteWrite）。每次修改 relabel 規則後，驗證所有實例的 series label set 一致。Mimir 的 <code>active_series</code> API 可以列出目前所有 active series 的 label set。</p>
<h3 id="remote-write-protocol-版本不匹配">Remote write protocol 版本不匹配</h3>
<p><strong>觸發條件</strong>：Prometheus 版本跟長期儲存後端期望的 remote write protocol 版本不一致。Prometheus 2.x 使用 remote write v1（protobuf + snappy），部分較新後端開始支援 v2（native histogram 支援、metadata 改進）。</p>
<p><strong>表現</strong>：後端回傳 400 Bad Request。Prometheus 對 4xx 的預設行為是不 retry（視為 client error、retry 無意義），samples 被 drop。<code>prometheus_remote_storage_samples_failed_total</code> 增長但不像 5xx 那樣有明顯的 retry storm — 靜默丟失更難察覺。</p>
<p><strong>修復</strong>：確認 Prometheus 版本跟後端的 protocol 相容性。Mimir / Thanos 的文件通常標明支援的 remote write protocol 版本。版本不匹配時升級 Prometheus 或降級後端配置。</p>
<h3 id="何時單機-prometheus-不夠">何時單機 Prometheus 不夠</h3>
<p>三個訊號同時出現時，remote write + 長期儲存從「可選」變成「必要」：</p>
<p><strong>Active series 超過 500 萬</strong>。單機 Prometheus 在 500 萬 series 左右開始出現記憶體壓力（head block ~20 GB）、WAL replay 時間拉長（重啟要數分鐘）、compaction 佔用 CPU。<a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">Uber 在 M3 專案</a>遇到的正是這個天花板 — 數十個叢集各自 scrape 的 metrics 匯總後 series 數遠超單機能力，但「用更大的 VM 跑 Prometheus」不是解法，因為 Prometheus 的 TSDB 是單線程 compaction、垂直擴展的效益有上限。</p>
<p><strong>Retention 需求超過 30 天</strong>。本地 TSDB 的 retention 拉長時，range query 的效能線性退化 — 查 90 天 range 要掃描的 block 數量是 15 天的 6 倍。Downsampling 是長期儲存後端的標準能力（Mimir / Thanos compactor 把 5 分鐘 resolution 降到 1 小時），但 Prometheus 本地 TSDB 不做 downsampling。Uber 的 M3DB 設計了 namespace-level retention（short-term 48h full resolution、long-term 1y downsampled），讓查詢成本不隨 retention 線性成長。</p>
<p><strong>跨叢集統一查詢</strong>。多個 Prometheus 各自 scrape 不同 cluster 時，工程師需要一個入口看「所有 cluster 的 checkout error rate」。手動切 Grafana datasource 容易遺漏。Remote write 把所有 Prometheus 的 metrics 匯入同一個長期儲存、用單一查詢入口（Mimir querier / Thanos Query）做 fan-out。</p>
<p>這三個需求在中型公司（50-200 服務、3+ K8s cluster）通常在 1-2 年內同時浮現。規劃 remote write 時不用等三個都出現 — 任一個出現就是啟動的合理時機。</p>
<h2 id="容量與-cost">容量與 Cost</h2>
<h3 id="remote-write-bandwidth">Remote write bandwidth</h3>
<p>Remote write 的 bandwidth ≈ ingestion rate × 每 sample 壓縮後大小（約 1-2 bytes with snappy）。</p>
<table>
  <thead>
      <tr>
          <th>Ingestion rate</th>
          <th>估算 bandwidth</th>
          <th>對應規模參考</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>~100-200 KB/s</td>
          <td>小型：5-10 服務、1 cluster</td>
      </tr>
      <tr>
          <td>50 萬 samples/sec</td>
          <td>~500 KB/s-1 MB/s</td>
          <td>中型：50 服務、2-3 cluster</td>
      </tr>
      <tr>
          <td>200 萬 samples/sec</td>
          <td>~2-4 MB/s</td>
          <td>大型：200 服務、5+ cluster</td>
      </tr>
      <tr>
          <td>1000 萬 samples/sec</td>
          <td>~10-20 MB/s</td>
          <td>平台級：Uber M3 等級</td>
      </tr>
  </tbody>
</table>
<p>每個 active series 在 15 秒 scrape interval 下每秒產生 ~0.067 個 sample。100 萬 active series 的 ingestion rate ≈ 6.7 萬 samples/sec，對應 ~70-140 KB/s remote write bandwidth。這個數字在內網環境下通常不是瓶頸。</p>
<p>真正的瓶頸在兩個地方：<strong>roundtrip latency</strong> 決定單 shard 吞吐上限（每次 POST 等回應才發下一批）、<strong>後端 ingestion capacity</strong> 決定能消化多少 samples/sec。Mimir 的 distributor 跟 ingester 可以水平擴展，但每加一個 ingester 增加 compute 成本。bandwidth 只是 capacity planning 的第一步，實際規模要用 Mimir 的 <code>cortex_distributor_received_samples_total</code> 跟 <code>cortex_ingester_memory_series</code> 做持續觀測。</p>
<h3 id="長期儲存的-compaction-與-downsampling-cost">長期儲存的 compaction 與 downsampling cost</h3>
<p>Mimir 和 Thanos 的 compactor 定期合併 block 並做 downsampling（5m → 1h 粒度）。Compaction 消耗 CPU 和 disk I/O，但跑在長期儲存自己的 compute 上，不影響 Prometheus。</p>
<p>成本結構：</p>
<ul>
<li><strong>Compute</strong>：distributor + ingester + querier + compactor 的 CPU / memory。Mimir 官方建議 ingester 是最吃資源的元件（記憶體中保存 active series）</li>
<li><strong>Object storage</strong>：S3 / GCS 的儲存量 ≈ ingestion rate × retention × 壓縮率。Compaction 跟 downsampling 會降低儲存量（通常 2-5x 壓縮）</li>
<li><strong>Query cost</strong>：長 range query 需要讀大量 block — 在 cloud object storage 上是 GET request 成本。Mimir 用 index cache（memcached）降低重複查詢的 GET request</li>
</ul>
<p>跟 Prometheus 本地 TSDB 比，長期儲存把 disk cost 換成 object storage cost（通常更便宜），但增加了 compute cost（長期儲存的 ingester / querier / compactor）。判斷轉折點的方式是比較本地 SSD cost × retention 跟 object storage cost + compute cost。retention 超過 30 天時，object storage 的成本優勢通常明顯。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="接-grafana-stack-lgtm">接 Grafana Stack LGTM</h3>
<p>Mimir 是 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> LGTM（Loki + Grafana + Tempo + Mimir）的 metrics 後端。Prometheus remote write 到 Mimir 後，Grafana 用 Mimir 作為 Prometheus-compatible datasource，查詢語言仍是 PromQL。Exemplar forwarding 讓 Mimir metrics 可以連結到 Tempo traces。</p>
<h3 id="接-telemetry-pipeline">接 Telemetry Pipeline</h3>
<p>Remote write 在 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 中扮演 metrics ingestion 段。如果同時使用 OpenTelemetry Collector，Collector 可以作為 remote write 的中繼（接收 Prometheus scrape → OTLP export → Mimir OTLP endpoint），但多一層中繼增加了 failure point。直接 Prometheus → Mimir remote write 是最簡路徑。</p>
<h3 id="接-cost-attribution">接 Cost Attribution</h3>
<p>長期儲存的多租戶能力讓 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a> 可以按 tenant / team / service 拆分 metrics 成本。Mimir 的 per-tenant active series quota 同時控制 cardinality 與成本。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：overview 跟日常操作入口</li>
<li><a href="../promql-recording-rules/">PromQL 與 Recording Rules 實務</a>：remote write 架構下 recording rules 的部署位置選擇</li>
<li><a href="../capacity-failure-modes/">容量規劃與故障模式</a>：remote write 作為容量超限時的卸載路徑</li>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>：Mimir 作為長期儲存的完整操作指南</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a>：remote write 在 pipeline 架構中的定位</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a>：多租戶 metrics 的成本拆分</li>
</ul>
]]></content:encoded></item></channel></rss>