<?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>Promql on Tarragon</title><link>https://tarrragon.github.io/blog/tags/promql/</link><description>Recent content in Promql on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 22 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/promql/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>