<?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/tags/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, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/prometheus/index.xml" rel="self" type="application/rss+xml"/><item><title>斷網環境的監控與可觀測性</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-monitoring/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-monitoring/</guid><description>&lt;p>斷網環境不能用 Datadog、New Relic、Sentry Cloud、PagerDuty Cloud 這些 SaaS 監控服務——它們全部需要往外發送資料。監控的三個核心能力（metric 收集、log 彙整、告警通知）全部要用 self-hosted 的開源工具在隔離網路內搭建。原則跟連網環境相同（metric 跟資源同生命週期、alarm 要連到動作），差別在工具的部署和儲存規劃要自己管。&lt;/p>
&lt;h2 id="metric-收集prometheus--grafana">Metric 收集：Prometheus + Grafana&lt;/h2>
&lt;p>Prometheus 是 pull-based 的 metric 收集系統——它主動去 scrape 各服務的 metric endpoint，不需要服務往外推資料。這個架構天然適合斷網：所有流量都在內網、不需要出站連線。&lt;/p>
&lt;h3 id="離線安裝">離線安裝&lt;/h3>
&lt;p>Prometheus 和 Grafana 都是單一二進位或容器映像，離線安裝跟&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">映像搬運&lt;/a>相同的流程：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：下載 release binary&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">wget https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">wget https://dl.grafana.com/oss/release/grafana-11.1.0.linux-amd64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後解壓、設定 systemd service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">tar xzf prometheus-2.53.0.linux-amd64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">sudo mv prometheus-2.53.0.linux-amd64 /opt/prometheus&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果用容器部署，先把映像搬進內部 registry 再 pull：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 內部：從內部 registry 啟動&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker run -d -p 9090:9090 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v /etc/prometheus:/etc/prometheus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v /data/prometheus:/prometheus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> registry.internal:5000/prometheus:v2.53.0&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="scrape-設定">Scrape 設定&lt;/h3>
&lt;p>Prometheus 的 &lt;code>prometheus.yml&lt;/code> 定義要 scrape 的目標。斷網環境通常用 static config（手動列出目標）而非 service discovery（需要雲端 API）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">scrape_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">job_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;node-exporter&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;server-01:9100&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;server-02:9100&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;db-01:9100&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">job_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;app&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;app-01:8080&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;app-02:8080&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metrics_path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;/metrics&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>新增機器時手動把它加進 targets 清單。如果用 Consul（內網 service discovery），Prometheus 支援 Consul SD、可以自動發現新服務。&lt;/p>
&lt;h3 id="node-exporter">Node Exporter&lt;/h3>
&lt;p>每台需要監控的 Linux 機器裝一個 node_exporter（單一二進位、無依賴），暴露 CPU、記憶體、磁碟、網路等系統 metric。離線安裝同理——下載 binary、搬運、解壓、設成 service。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">tar xzf node_exporter-1.8.1.linux-amd64.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sudo cp node_exporter-1.8.1.linux-amd64/node_exporter /usr/local/bin/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">sudo useradd --no-create-home --shell /bin/false node_exporter
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 建立 systemd service（略）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="log-收集loki-或-elk">Log 收集：Loki 或 ELK&lt;/h2>
&lt;h3 id="grafana-loki輕量">Grafana Loki（輕量）&lt;/h3>
&lt;p>Loki 是 Grafana 生態的 log 彙整系統，架構類似 Prometheus（pull/push 都支援），但儲存的是 log stream 而非 metric。它不索引 log 內容（只索引 label），所以儲存成本遠低於 Elasticsearch。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c"># loki-config.yaml 基本設定&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">auth_enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http_listen_port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">storage_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filesystem&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/data/loki/chunks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">schema_config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="ld">2024-01-01&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">store&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tsdb&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">object_store&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">filesystem&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schema&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v13&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">index&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">index_&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">period&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">24h&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>搭配 Promtail（log 收集 agent）在每台機器上收集 log 並推送到 Loki：&lt;/p></description><content:encoded><![CDATA[<p>斷網環境不能用 Datadog、New Relic、Sentry Cloud、PagerDuty Cloud 這些 SaaS 監控服務——它們全部需要往外發送資料。監控的三個核心能力（metric 收集、log 彙整、告警通知）全部要用 self-hosted 的開源工具在隔離網路內搭建。原則跟連網環境相同（metric 跟資源同生命週期、alarm 要連到動作），差別在工具的部署和儲存規劃要自己管。</p>
<h2 id="metric-收集prometheus--grafana">Metric 收集：Prometheus + Grafana</h2>
<p>Prometheus 是 pull-based 的 metric 收集系統——它主動去 scrape 各服務的 metric endpoint，不需要服務往外推資料。這個架構天然適合斷網：所有流量都在內網、不需要出站連線。</p>
<h3 id="離線安裝">離線安裝</h3>
<p>Prometheus 和 Grafana 都是單一二進位或容器映像，離線安裝跟<a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">映像搬運</a>相同的流程：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：下載 release binary</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">wget https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz
</span></span><span class="line"><span class="ln">3</span><span class="cl">wget https://dl.grafana.com/oss/release/grafana-11.1.0.linux-amd64.tar.gz
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 搬運後解壓、設定 systemd service</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">tar xzf prometheus-2.53.0.linux-amd64.tar.gz
</span></span><span class="line"><span class="ln">7</span><span class="cl">sudo mv prometheus-2.53.0.linux-amd64 /opt/prometheus</span></span></code></pre></div><p>如果用容器部署，先把映像搬進內部 registry 再 pull：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 內部：從內部 registry 啟動</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d -p 9090:9090 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -v /etc/prometheus:/etc/prometheus <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  -v /data/prometheus:/prometheus <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  registry.internal:5000/prometheus:v2.53.0</span></span></code></pre></div><h3 id="scrape-設定">Scrape 設定</h3>
<p>Prometheus 的 <code>prometheus.yml</code> 定義要 scrape 的目標。斷網環境通常用 static config（手動列出目標）而非 service discovery（需要雲端 API）：</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="nt">scrape_configs</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">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;node-exporter&#39;</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">static_configs</span><span class="p">:</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">targets</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="s1">&#39;server-01:9100&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">          </span>- <span class="s1">&#39;server-02:9100&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">          </span>- <span class="s1">&#39;db-01:9100&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;app&#39;</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">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="nt">targets</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="s1">&#39;app-01:8080&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">          </span>- <span class="s1">&#39;app-02:8080&#39;</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">metrics_path</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;/metrics&#39;</span></span></span></code></pre></div><p>新增機器時手動把它加進 targets 清單。如果用 Consul（內網 service discovery），Prometheus 支援 Consul SD、可以自動發現新服務。</p>
<h3 id="node-exporter">Node Exporter</h3>
<p>每台需要監控的 Linux 機器裝一個 node_exporter（單一二進位、無依賴），暴露 CPU、記憶體、磁碟、網路等系統 metric。離線安裝同理——下載 binary、搬運、解壓、設成 service。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 搬運後安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">tar xzf node_exporter-1.8.1.linux-amd64.tar.gz
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo cp node_exporter-1.8.1.linux-amd64/node_exporter /usr/local/bin/
</span></span><span class="line"><span class="ln">4</span><span class="cl">sudo useradd --no-create-home --shell /bin/false node_exporter
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 建立 systemd service（略）</span></span></span></code></pre></div><h2 id="log-收集loki-或-elk">Log 收集：Loki 或 ELK</h2>
<h3 id="grafana-loki輕量">Grafana Loki（輕量）</h3>
<p>Loki 是 Grafana 生態的 log 彙整系統，架構類似 Prometheus（pull/push 都支援），但儲存的是 log stream 而非 metric。它不索引 log 內容（只索引 label），所以儲存成本遠低於 Elasticsearch。</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"># loki-config.yaml 基本設定</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">auth_enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</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">server</span><span class="p">:</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">http_listen_port</span><span class="p">:</span><span class="w"> </span><span class="m">3100</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">storage_config</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">filesystem</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">directory</span><span class="p">:</span><span class="w"> </span><span class="l">/data/loki/chunks</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">schema_config</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">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">from</span><span class="p">:</span><span class="w"> </span><span class="ld">2024-01-01</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">store</span><span class="p">:</span><span class="w"> </span><span class="l">tsdb</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">object_store</span><span class="p">:</span><span class="w"> </span><span class="l">filesystem</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">schema</span><span class="p">:</span><span class="w"> </span><span class="l">v13</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">index</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="l">index_</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="nt">period</span><span class="p">:</span><span class="w"> </span><span class="l">24h</span></span></span></code></pre></div><p>搭配 Promtail（log 收集 agent）在每台機器上收集 log 並推送到 Loki：</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"># promtail-config.yaml</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">clients</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="l">http://loki.internal:3100/loki/api/v1/push</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">scrape_configs</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">job_name</span><span class="p">:</span><span class="w"> </span><span class="l">system</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">static_configs</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">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">localhost]</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">labels</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">job</span><span class="p">:</span><span class="w"> </span><span class="l">syslog</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">__path__</span><span class="p">:</span><span class="w"> </span><span class="l">/var/log/*.log</span></span></span></code></pre></div><h3 id="elk-stack功能豐富">ELK Stack（功能豐富）</h3>
<p>Elasticsearch + Logstash + Kibana 是功能最完整的 log 平台，但資源消耗大（Elasticsearch 建議至少 4GB RAM 起跳）。適合需要全文搜索 log 內容的場景。</p>
<p>離線安裝：Elastic 提供離線安裝包（<code>.deb</code> / <code>.rpm</code>），或用 Docker 映像。三個組件都要搬運。</p>
<p>選型判準：5 台以下的小環境用 Loki（輕量、跟 Prometheus + Grafana 同一套 dashboard）。需要全文搜索、已有 ELK 經驗的團隊用 ELK。</p>
<h2 id="告警沒有外部-webhook-怎麼通知">告警：沒有外部 webhook 怎麼通知</h2>
<p>連網環境的告警通常發到 Slack webhook、PagerDuty API、或 email relay service。斷網環境這些路徑都不通。</p>
<h3 id="內部-smtp">內部 SMTP</h3>
<p>如果隔離網路內有 email server（很多企業內網有 Exchange 或 Postfix），Prometheus Alertmanager 可以發 email 告警：</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"># alertmanager.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">route</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">receiver</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;email-team&#39;</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">receivers</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">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;email-team&#39;</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">email_configs</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">to</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;oncall@internal.corp&#39;</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">from</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;alertmanager@internal.corp&#39;</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">smarthost</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;smtp.internal.corp:25&#39;</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">require_tls</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span></span></span></code></pre></div><h3 id="內部即時通訊">內部即時通訊</h3>
<p>如果內網有 Mattermost（Slack 的 self-hosted 替代）或 Rocket.Chat，Alertmanager 可以用 webhook 發送到這些工具的 incoming webhook endpoint。</p>
<h3 id="實體告警">實體告警</h3>
<p>極端情境（沒有 email、沒有 chat）：Alertmanager 把告警寫到檔案或資料庫、搭配值班制度定期查看。或用 Grafana 的 dashboard + 控制室大螢幕，值班人員直接看板。</p>
<p>告警的設計原則跟連網環境相同——symptom-based（錯誤率、延遲）優先於 cause-based（CPU、記憶體），閾值設計避免告警疲勞。差別在通知的到達速度可能慢一些（email 比 Slack push 慢），所以閾值要稍微保守（提早告警）。</p>
<h2 id="metric-與-log-的儲存規劃">Metric 與 Log 的儲存規劃</h2>
<p>SaaS 監控的儲存是雲端自動擴展的。Self-hosted 的儲存要自己規劃——磁碟滿了 Prometheus 就停止收集、Loki 就停止寫入。</p>
<h3 id="容量估算">容量估算</h3>
<p>Prometheus 的儲存量取決於 series 數量 × scrape 間隔 × 保留天數。粗估公式：</p>





<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">每日儲存 ≈ active_series × sample_size(2B) × (86400 / scrape_interval) × compression_ratio(~0.1)</span></span></code></pre></div><p>1 萬個 active series、15 秒 scrape interval、保留 30 天 ≈ 約 5GB。保留 90 天 ≈ 約 15GB。</p>
<p>Loki 的儲存量取決於 log 流量。粗估：每天 10GB 的 raw log 在 Loki 壓縮後約 1-2GB，保留 30 天 ≈ 30-60GB。</p>
<h3 id="retention-設定">Retention 設定</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">global</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">scrape_interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</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">storage</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">tsdb</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">retention.time</span><span class="p">:</span><span class="w"> </span><span class="l">30d</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">retention.size</span><span class="p">:</span><span class="w"> </span><span class="l">10GB </span><span class="w"> </span><span class="c"># 以先到的為準</span></span></span></code></pre></div><p>超過容量時 Prometheus 自動刪除最舊的資料。設定 retention 前先確認磁碟空間足夠——斷網環境擴容磁碟的流程（採購 + 安裝）可能需要週到月級的時間。</p>
<h2 id="ntp-時間同步">NTP 時間同步</h2>
<p>斷網環境容易被忽略的一個問題是時間同步。沒有 NTP server（<code>pool.ntp.org</code>）可連的機器，時鐘會漂移——幾天後各台機器的時間差可能達到秒級。當 Prometheus 收到的 metric timestamp 跟 Loki 收到的 log timestamp 有幾秒落差，事故排查時 metric 跟 log 對不上。</p>
<p>解法是在隔離網路內架一台 NTP server，所有機器從它同步：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 內部 NTP server（chrony）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># /etc/chrony/chrony.conf</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">local</span> stratum <span class="m">10</span>         <span class="c1"># 沒有外部來源時、自己當 stratum 10</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">allow 10.0.0.0/16        <span class="c1"># 允許內部網段同步</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 其他機器指向內部 NTP</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">server ntp.internal iburst</span></span></code></pre></div><p>如果隔離網路的閘道可以開 NTP（UDP 123），讓閘道從外部 NTP 同步、內部機器從閘道同步，時間精度可以維持在毫秒級。</p>
<p>時程參考：Prometheus + Grafana + Alertmanager 的初次建置約需 1-2 天。Loki + Promtail 約需半天到一天。NTP server 約需 2 小時。後續維護主要是 Prometheus/Loki 版本更新的搬運（每次 1-2 小時）和儲存容量監控。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：監控工具的離線安裝走 content ferry 模式</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器管理</a>：Prometheus/Grafana/Loki 的容器映像搬運</li>
<li>→ <a href="/blog/infra/06-observability-logging/" data-link-title="模組六：可觀測性與 log 一併寫進 code" data-link-desc="log group、metric、alarm 跟基礎設施同生命週期管理，出事時追得到查得到">模組六：可觀測性與 log</a>：連網環境的可觀測性 IaC</li>
<li>→ <a href="/blog/infra/takeover/legacy-external-monitoring/" data-link-title="無 SSH 環境的監控與告警" data-link-desc="無 SSH 環境沒辦法裝 agent、沒辦法串 log pipeline，用外部 HTTP check、錯誤追蹤服務與效能基線建立最低成本的監控能力">無 SSH 環境的監控與告警</a>：另一個極端——完全外部監控</li>
<li>→ <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">Monitoring 04：Collector 架構與部署</a>：SDK 和 Collector 的應用層監控，斷網環境需要把 Collector endpoint 指向 self-hosted backend</li>
<li>→ <a href="/blog/monitoring/06-commercial-comparison/self-hosted-vs-commercial/" data-link-title="自架 vs 商業的判斷決策表" data-link-desc="使用者數、網路範圍、功能需求、合規要求四個維度判斷該自架還是用商業方案">Monitoring 06：Self-hosted vs Commercial</a>：斷網環境只能走 self-hosted 路線</li>
</ul>
]]></content:encoded></item><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>4.C11 Uber：M3 大規模 Metrics 平台</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/</guid><description>&lt;p>Uber 的 M3 案例揭露了 metrics 系統從「每個團隊各跑一套 Prometheus」到「全公司共用的 metrics 平台」的轉折點。轉折的核心判斷是：當 active series 總量超過單機 Prometheus 的記憶體上限、且多個團隊需要跨叢集查詢時，自建平台層的成本低於持續橫向複製 Prometheus 實例的成本。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Uber 的服務觀測涵蓋行程追蹤、即時定價、ETA 計算、司機定位、支付結算與推播通知。每個微服務都暴露 Prometheus-compatible metrics，隨著服務數量成長到數千個，寫入速率達到每秒數十億 data points。&lt;/p>
&lt;p>早期每個團隊各自部署 Prometheus，各管自己的 retention、scrape config 與 alerting rules。規模小時這個模式運作良好 — 每個 Prometheus 實例只需要處理自己團隊的幾萬到幾十萬 series。但當組織成長到數百個團隊、數千個服務時，散落的 Prometheus 實例帶來三個問題。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="單機記憶體天花板">單機記憶體天花板&lt;/h3>
&lt;p>Prometheus 的 TSDB 把 active series 放在記憶體的 head block，每個 series 消耗約 3-4 KB（詳見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/" data-link-title="Prometheus 容量規劃與故障模式" data-link-desc="說明 Prometheus 單機容量邊界、cardinality 與 retention 的資源模型、常見故障模式與判讀方式">Prometheus 容量規劃&lt;/a>）。當單一 Prometheus 實例需要 scrape 的 series 超過 1000 萬時，head block 就需要 40+ GB 記憶體。加上 query execution 跟 WAL replay 的暫時開銷，單機很容易 OOM。&lt;/p>
&lt;p>團隊的第一反應是按服務拆分多個 Prometheus 實例，但這讓跨服務查詢變得困難 — 要看一條 request 從 gateway 到 payment 的 latency 分布，需要分別查三個 Prometheus 再手動關聯。&lt;/p>
&lt;h3 id="retention-與長期趨勢">Retention 與長期趨勢&lt;/h3>
&lt;p>Prometheus 預設 retention 15 天。容量規劃與季度趨勢分析需要 90 天甚至 1 年的歷史資料。把 Prometheus retention 拉長到 90 天，disk 跟 memory 需求同步上升，而且 compaction 效率在資料量大時會下降。&lt;/p>
&lt;p>團隊需要的是分層 retention — 近期資料保留全精度、歷史資料做 downsampling 後保留更久。Prometheus 原生不支援 downsampling。&lt;/p>
&lt;h3 id="高可用與跨叢集查詢">高可用與跨叢集查詢&lt;/h3>
&lt;p>Prometheus 沒有原生 HA — 標準做法是跑兩個 instance scrape 同一批 target，靠下游去重。但兩個 instance 各自獨立儲存，查詢只打一個；instance 故障切換時會有短暫資料缺口。&lt;/p>
&lt;p>跨叢集查詢更困難。Prometheus federation 可以做簡單的 metric 聚合，但 federation 本身是 pull-based scrape — federation target 太多或 series 太大時，federation Prometheus 自己也會 OOM。&lt;/p>
&lt;h2 id="解法m3-平台">解法：M3 平台&lt;/h2>
&lt;p>Uber 開發了 M3 — 一個 Prometheus-compatible 的分散式 metrics 平台，由三個核心元件組成。&lt;/p>
&lt;h3 id="m3db分散式-time-series-storage">M3DB：分散式 time series storage&lt;/h3>
&lt;p>M3DB 是分散式 TSDB，資料按 namespace 和 shard 分布在多個節點。每個 namespace 可以有不同的 retention 和 resolution — 例如 &lt;code>realtime&lt;/code> namespace 保留 2 天全精度，&lt;code>aggregated_1m&lt;/code> namespace 保留 90 天 1 分鐘精度。這解決了 retention tiering 的問題。&lt;/p></description><content:encoded><![CDATA[<p>Uber 的 M3 案例揭露了 metrics 系統從「每個團隊各跑一套 Prometheus」到「全公司共用的 metrics 平台」的轉折點。轉折的核心判斷是：當 active series 總量超過單機 Prometheus 的記憶體上限、且多個團隊需要跨叢集查詢時，自建平台層的成本低於持續橫向複製 Prometheus 實例的成本。</p>
<h2 id="業務背景">業務背景</h2>
<p>Uber 的服務觀測涵蓋行程追蹤、即時定價、ETA 計算、司機定位、支付結算與推播通知。每個微服務都暴露 Prometheus-compatible metrics，隨著服務數量成長到數千個，寫入速率達到每秒數十億 data points。</p>
<p>早期每個團隊各自部署 Prometheus，各管自己的 retention、scrape config 與 alerting rules。規模小時這個模式運作良好 — 每個 Prometheus 實例只需要處理自己團隊的幾萬到幾十萬 series。但當組織成長到數百個團隊、數千個服務時，散落的 Prometheus 實例帶來三個問題。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="單機記憶體天花板">單機記憶體天花板</h3>
<p>Prometheus 的 TSDB 把 active series 放在記憶體的 head block，每個 series 消耗約 3-4 KB（詳見 <a href="/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/" data-link-title="Prometheus 容量規劃與故障模式" data-link-desc="說明 Prometheus 單機容量邊界、cardinality 與 retention 的資源模型、常見故障模式與判讀方式">Prometheus 容量規劃</a>）。當單一 Prometheus 實例需要 scrape 的 series 超過 1000 萬時，head block 就需要 40+ GB 記憶體。加上 query execution 跟 WAL replay 的暫時開銷，單機很容易 OOM。</p>
<p>團隊的第一反應是按服務拆分多個 Prometheus 實例，但這讓跨服務查詢變得困難 — 要看一條 request 從 gateway 到 payment 的 latency 分布，需要分別查三個 Prometheus 再手動關聯。</p>
<h3 id="retention-與長期趨勢">Retention 與長期趨勢</h3>
<p>Prometheus 預設 retention 15 天。容量規劃與季度趨勢分析需要 90 天甚至 1 年的歷史資料。把 Prometheus retention 拉長到 90 天，disk 跟 memory 需求同步上升，而且 compaction 效率在資料量大時會下降。</p>
<p>團隊需要的是分層 retention — 近期資料保留全精度、歷史資料做 downsampling 後保留更久。Prometheus 原生不支援 downsampling。</p>
<h3 id="高可用與跨叢集查詢">高可用與跨叢集查詢</h3>
<p>Prometheus 沒有原生 HA — 標準做法是跑兩個 instance scrape 同一批 target，靠下游去重。但兩個 instance 各自獨立儲存，查詢只打一個；instance 故障切換時會有短暫資料缺口。</p>
<p>跨叢集查詢更困難。Prometheus federation 可以做簡單的 metric 聚合，但 federation 本身是 pull-based scrape — federation target 太多或 series 太大時，federation Prometheus 自己也會 OOM。</p>
<h2 id="解法m3-平台">解法：M3 平台</h2>
<p>Uber 開發了 M3 — 一個 Prometheus-compatible 的分散式 metrics 平台，由三個核心元件組成。</p>
<h3 id="m3db分散式-time-series-storage">M3DB：分散式 time series storage</h3>
<p>M3DB 是分散式 TSDB，資料按 namespace 和 shard 分布在多個節點。每個 namespace 可以有不同的 retention 和 resolution — 例如 <code>realtime</code> namespace 保留 2 天全精度，<code>aggregated_1m</code> namespace 保留 90 天 1 分鐘精度。這解決了 retention tiering 的問題。</p>
<p>M3DB 的記憶體模型跟 Prometheus 不同 — 近期資料在記憶體，冷資料在 disk，不像 Prometheus 把所有 active series 都放 head block。這讓它能處理遠超單機 Prometheus 的 series 數量。</p>
<h3 id="m3-coordinator統一查詢入口">M3 Coordinator：統一查詢入口</h3>
<p>M3 Coordinator 接收 PromQL 查詢，轉譯後分發到 M3DB 節點，聚合結果後返回。對 Grafana 和 alerting rules 來說，M3 Coordinator 的 API 跟 Prometheus 完全相容 — 不需要改 dashboard 或 alert config。</p>
<h3 id="m3-aggregator寫入路徑聚合">M3 Aggregator：寫入路徑聚合</h3>
<p>高 cardinality 的原始 series 在寫入 M3DB 前先經過 M3 Aggregator 做 pre-aggregation — 例如把每秒的 request count 聚合成每分鐘，再寫入長期 namespace。這控制了長期儲存的資料量跟成本。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Prometheus standalone</th>
          <th>M3 平台</th>
          <th>Mimir / Thanos（替代）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署複雜度</td>
          <td>低（單一 binary）</td>
          <td>高（M3DB + Coordinator + Aggregator）</td>
          <td>中到高</td>
      </tr>
      <tr>
          <td>單機 series 上限</td>
          <td>~500 萬-1000 萬</td>
          <td>不適用（分散式）</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>Retention tiering</td>
          <td>無</td>
          <td>原生支援</td>
          <td>Thanos compactor / Mimir 支援</td>
      </tr>
      <tr>
          <td>PromQL 相容</td>
          <td>原生</td>
          <td>相容</td>
          <td>相容</td>
      </tr>
      <tr>
          <td>社群活躍度</td>
          <td>高（CNCF）</td>
          <td>低（Uber 主導、2023 後維護縮減）</td>
          <td>高（Grafana Labs / 社群）</td>
      </tr>
      <tr>
          <td>適用規模</td>
          <td>單團隊到中型組織</td>
          <td>大型組織（數十億 series）</td>
          <td>中型到大型</td>
      </tr>
  </tbody>
</table>
<p>M3 的最大風險是社群活躍度 — Uber 自 2023 年後縮減了 M3 的開發投入，Grafana Mimir 成為更活躍的替代。新專案選型時，Mimir 跟 Thanos 的社群支援度跟 Grafana 生態整合度都優於 M3。M3 的價值在於它驗證了「分散式 TSDB + 寫入路徑聚合 + retention tiering」這組設計模式，這組模式在 Mimir 跟 Thanos 裡以不同形式被採用。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<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>：active series、cardinality 與 recording rules 的基礎模型，M3 的 pre-aggregation 對應 recording rules 的平台化版本。</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>：M3 的 Aggregator 是 pipeline 中 processing 層的實例。</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/" data-link-title="Remote Write 與長期儲存整合" data-link-desc="說明 Prometheus remote write 的配置、三家長期儲存後端比較（Mimir / Thanos / Cortex）、故障模式與容量規劃">Prometheus Remote Write 與長期儲存</a>：M3 是 remote write 目標之一，跟 Mimir / Thanos / Cortex 的比較在該文。</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>：M3 的 per-namespace cardinality limit 是治理機制的生產實例。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>單一 Prometheus 實例 memory 接近機器上限，開始 OOM restart</li>
<li>多個 Prometheus 實例各自 scrape，跨服務查詢需要手動關聯</li>
<li>Retention 15 天不夠做季度趨勢分析，但拉長 retention 資源撐不住</li>
<li>團隊開始問「我們的 metrics 總共有多少 series、誰佔最多」但沒有統一的 cardinality 觀測</li>
<li>Grafana federation dashboard 查詢越來越慢或經常 timeout</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.uber.com/en-GB/blog/m3/">M3: Uber&rsquo;s Open Source, Large-scale Metrics Platform for Prometheus</a></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>Self-managed Prometheus → Grafana Cloud Metrics：feature × ops × cost 對照</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/migrate-prometheus-to-cloud-metrics/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/migrate-prometheus-to-cloud-metrics/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &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;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a>（Grafana Cloud Metrics、Mimir-backed）。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Operational = High → Type C operational redesign hybrid&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="feature--ops--cost-三維對照">Feature / ops / cost 三維對照&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Self-managed Prometheus&lt;/th>
 &lt;th>Grafana Cloud Metrics&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Storage backend&lt;/td>
 &lt;td>Local disk + remote_write (optional)&lt;/td>
 &lt;td>Mimir + S3 (auto cold tier)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention&lt;/td>
 &lt;td>TSDB local 15 天 default&lt;/td>
 &lt;td>13 個月 default、可延長&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HA&lt;/td>
 &lt;td>Two Prometheus + sidecar&lt;/td>
 &lt;td>Built-in multi-AZ&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cardinality limit&lt;/td>
 &lt;td>自管 limit + recording rule&lt;/td>
 &lt;td>1.5M active series / tier、scale-up 配額&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query API&lt;/td>
 &lt;td>PromQL + Prometheus HTTP API&lt;/td>
 &lt;td>完全相容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert&lt;/td>
 &lt;td>Alertmanager self-managed&lt;/td>
 &lt;td>Grafana Cloud Alerting&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dashboard&lt;/td>
 &lt;td>Grafana self-managed&lt;/td>
 &lt;td>Grafana Cloud (included)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Long-term storage&lt;/td>
 &lt;td>Thanos / Cortex / Mimir 自管&lt;/td>
 &lt;td>Mimir 內建&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost (mid-tier)&lt;/td>
 &lt;td>$500-2000 / mo + ops FTE&lt;/td>
 &lt;td>$300-1500 / mo (按 series)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational FTE&lt;/td>
 &lt;td>0.3-0.8&lt;/td>
 &lt;td>0.05-0.15&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>Low（PromQL + API 完全相容）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>（HA / retention / scaling 全託管）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>Low（同 Prometheus metric paradigm）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>Low（remote_write endpoint 改）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Operational = High → Type C standard。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 跟 <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>（Grafana Cloud Metrics、Mimir-backed）。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Operational = High → Type C operational redesign hybrid</em>。</p></blockquote>
<h2 id="feature--ops--cost-三維對照">Feature / ops / cost 三維對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed Prometheus</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Storage backend</td>
          <td>Local disk + remote_write (optional)</td>
          <td>Mimir + S3 (auto cold tier)</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>TSDB local 15 天 default</td>
          <td>13 個月 default、可延長</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Two Prometheus + sidecar</td>
          <td>Built-in multi-AZ</td>
      </tr>
      <tr>
          <td>Cardinality limit</td>
          <td>自管 limit + recording rule</td>
          <td>1.5M active series / tier、scale-up 配額</td>
      </tr>
      <tr>
          <td>Query API</td>
          <td>PromQL + Prometheus HTTP API</td>
          <td>完全相容</td>
      </tr>
      <tr>
          <td>Alert</td>
          <td>Alertmanager self-managed</td>
          <td>Grafana Cloud Alerting</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Grafana self-managed</td>
          <td>Grafana Cloud (included)</td>
      </tr>
      <tr>
          <td>Long-term storage</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Mimir 內建</td>
      </tr>
      <tr>
          <td>Cost (mid-tier)</td>
          <td>$500-2000 / mo + ops FTE</td>
          <td>$300-1500 / mo (按 series)</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8</td>
          <td>0.05-0.15</td>
      </tr>
  </tbody>
</table>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Low（PromQL + API 完全相容）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td><strong>High</strong>（HA / retention / scaling 全託管）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low（同 Prometheus metric paradigm）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Low（remote_write endpoint 改）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Operational = High → Type C standard。</p>
<h2 id="為什麼遷retention--ops--vendor-consolidation-三條-driver">為什麼遷：retention / ops / vendor consolidation 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Retention</td>
          <td>Prometheus TSDB local 預設 15 天、長期 retention 需要 Thanos / Cortex / Mimir 自管</td>
      </tr>
      <tr>
          <td>Ops FTE</td>
          <td>Self-managed Prometheus + Alertmanager + Grafana 自管全部加起來 0.5-1 FTE</td>
      </tr>
      <tr>
          <td>Vendor consolidation</td>
          <td>已用 Grafana Cloud（logs / traces）、metric 加進 stack 統一</td>
      </tr>
  </tbody>
</table>
<h2 id="operational-redesign">Operational redesign</h2>
<table>
  <thead>
      <tr>
          <th>Concept</th>
          <th>Self-managed</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>Helm chart + manual config</td>
          <td>UI 一鍵建</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Two Prometheus 配置</td>
          <td>內建 multi-AZ Mimir</td>
      </tr>
      <tr>
          <td>Long-term retention</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Built-in (S3-backed)</td>
      </tr>
      <tr>
          <td>Cardinality control</td>
          <td>Manual recording rule + relabel</td>
          <td>Adaptive sampling + cardinality limit</td>
      </tr>
      <tr>
          <td>Alerting</td>
          <td>Alertmanager 自管</td>
          <td>Grafana Cloud Alerting (integrated)</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Grafana self-host</td>
          <td>Grafana Cloud (free tier 包含)</td>
      </tr>
  </tbody>
</table>
<h2 id="migration-4-phase">Migration 4-phase</h2>
<h3 id="phase-0audit">Phase 0：Audit</h3>
<ul>
<li>列所有 Prometheus job / scrape config</li>
<li>統計 active series 數（Mimir tier 計費基準）</li>
<li>估 retention 需求</li>
</ul>
<h3 id="phase-1grafana-cloud-setup">Phase 1：Grafana Cloud setup</h3>
<ul>
<li>Account + organization 設定</li>
<li>API key for <code>remote_write</code></li>
<li>Grafana Cloud Mimir endpoint 啟用</li>
</ul>
<h3 id="phase-2dual-write">Phase 2：Dual-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="l">https://prometheus-prod-XX-prod-us-central-0.grafana.net/api/prom/push</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">basic_auth</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">username</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;INSTANCE_ID&gt;</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">password</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;API_KEY&gt;</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">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="c"># Optional: drop high-cardinality before sending</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">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">10</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;high_card_metric_.*&#39;</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">action</span><span class="p">:</span><span class="w"> </span><span class="l">drop</span></span></span></code></pre></div><p>跑 4-8 週、確認 query 結果一致 + cost 在預期。</p>
<h3 id="phase-3cutover">Phase 3：Cutover</h3>
<ul>
<li>Dashboard / alert 切到 Grafana Cloud endpoint</li>
<li>應用層 / Grafana 自管 instance 關閉 query 對 self-managed Prometheus</li>
</ul>
<h3 id="phase-4cleanup">Phase 4：Cleanup</h3>
<ul>
<li>Self-managed Prometheus stop scrape</li>
<li>留 1-2 月歷史查詢能力（用 archive snapshot）</li>
<li>Decommission</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1cardinality-爆cost-暴漲">Case 1：Cardinality 爆、cost 暴漲</h3>
<p><strong>徵兆</strong>：dual-write 第 2 週 Grafana Cloud series 從預估 100K 漲到 800K、cost 翻 8 倍。</p>
<p><strong>根因</strong>：application-level high-cardinality label（user_id / request_id）沒被 drop、scraped 進來。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>write_relabel_configs</code> drop unbounded label</li>
<li>Application metric 設計改 fixed-bucket histogram、不用 unbounded label</li>
<li>Mimir cardinality limit 設保護 + alert</li>
</ol>
<h3 id="case-2recording-rule-對應失效">Case 2：Recording rule 對應失效</h3>
<p><strong>徵兆</strong>：cutover 後 Grafana dashboard 某些 panel 顯示空；發現用了 Prometheus 端 recording rule (<code>job:request_count:rate5m</code>)、Grafana Cloud 端沒對應 rule。</p>
<p><strong>根因</strong>：Prometheus 端 recording rule 是 <em>server-side</em>、不會跟著 remote_write 帶過去；Grafana Cloud 需要自己 setup recording rule。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Export 所有 recording rule、import 到 Grafana Cloud Mimir</li>
<li>或改用 <em>raw query</em> + Grafana query template、不依賴 recording rule</li>
</ol>
<h3 id="case-3promql-微差行為">Case 3：PromQL 微差行為</h3>
<p><strong>徵兆</strong>：某些 query 在 self-managed Prometheus 跑得好好的、切 Grafana Cloud Mimir 後 returns slightly different results。</p>
<p><strong>根因</strong>：Mimir 對某些 edge case（empty result handling / staleness marker timing）行為跟 Prometheus 略不同；多數 query 一致、&lt; 1% query 受影響。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover dual-query 驗證、用 critical dashboard 比對</li>
<li>Affected query 重寫、用更 robust PromQL pattern</li>
<li>文件 known incompatibility list</li>
</ol>
<h3 id="case-4alert-routing-改變">Case 4：Alert routing 改變</h3>
<p><strong>徵兆</strong>：Cutover 後 PagerDuty / Slack 收不到 alert；發現 Alertmanager 端 webhook 沒切。</p>
<p><strong>根因</strong>：alert 邏輯從 self-managed Alertmanager 搬到 Grafana Cloud Alerting、routing / contact 配置完全重做。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 在 Grafana Cloud 端 rebuild alert + routing</li>
<li>雙 alert pipeline 跑 1-2 週、確認 Grafana Cloud 收到</li>
<li>Cutover 切 routing、SOC drill 一次</li>
</ol>
<h3 id="case-5歷史資料查不到">Case 5：歷史資料查不到</h3>
<p><strong>徵兆</strong>：Cutover 後 SOC 想 query 6 個月前事件、Grafana Cloud 只有 2 個月（dual-write 後的）資料。</p>
<p><strong>根因</strong>：Grafana Cloud 從 dual-write 開始才有資料、之前的 self-managed Prometheus historical data 沒 backfill。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Phase 2 期間用 <code>promtool tsdb dump</code> + <code>mimirtool</code> 把 self-managed historical 灌進 Mimir</li>
<li>或保留 self-managed Prometheus read-only 6 個月（給 historical query）</li>
<li>Long-term：retention 從 cutover 開始算、historical 是 <em>one-time backfill</em></li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute (100 host, 100K series)</td>
          <td>$500-1000 / mo + ops</td>
          <td>$300-800 / mo</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8 = $3K-8K</td>
          <td>0.05-0.15 = $500-1500</td>
      </tr>
      <tr>
          <td>Long-term retention</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Built-in 13 個月</td>
      </tr>
      <tr>
          <td>Total (mid-tier)</td>
          <td>$4K-9K / mo (含 FTE)</td>
          <td>$1K-2.5K / mo</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-2 FTE × 1-2 個月</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-datadog--grafana-stack-migration-對位">跟 <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack migration</a> 對位</h3>
<p>兩條 Grafana Stack 路線：</p>
<ul>
<li>Self-host (Mimir + Loki + Tempo) on K8s：開源、自管</li>
<li>Grafana Cloud：SaaS、operational simplification</li>
</ul>
<p>本篇是「self-managed Prometheus → Grafana Cloud」、互補；如果跑兩階段（self-host → Cloud）跟「Datadog → Grafana Cloud」差不多。</p>
<h3 id="跟-opentelemetry-整合">跟 OpenTelemetry 整合</h3>
<p>OTel Collector 可同時 ship 到 Mimir (metric) + Loki (log) + Tempo (trace)；Migration 順便升 OTel 化避免下次 vendor 切換重複。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></li>
<li>Target vendor：<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></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-to-msk/" data-link-title="Self-managed Kafka → AWS MSK：把 $15K/month operational cost 拆解到 managed" data-link-desc="Kafka self-managed → MSK 是 Type C operational redesign — protocol 完全相容、operational stack（ZooKeeper / brokers / monitoring / patching）全託管；本文用 cost 拆解開頭、5 個 production 踩雷（client connection pattern / version pinning / metric pipeline / IAM auth / cross-cluster mirror）">Kafka → MSK</a> / <a href="/blog/backend/04-observability/vendors/elastic-stack/migrate-to-elastic-cloud/" data-link-title="Self-managed ELK → Elastic Cloud：5 年 ELK 集群的 lifecycle 收尾" data-link-desc="Self-managed ELK Stack → Elastic Cloud 是 Type C operational redesign — protocol drop-in、operational stack（cluster sizing / shard 治理 / upgrade / backup）全託管；本文按 5 年 ELK lifecycle (build → scale → degrade → save → migrate) 組織、5 個 production 踩雷">ELK → Elastic Cloud</a></li>
<li>平行 D-type 對位：<a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></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>