<?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>Cqrs on Tarragon</title><link>https://tarrragon.github.io/blog/tags/cqrs/</link><description>Recent content in Cqrs on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 22 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/cqrs/index.xml" rel="self" type="application/rss+xml"/><item><title>讀寫分離與查詢擴展</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/read-write-separation/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/read-write-separation/</guid><description>&lt;p>Monitor 的寫入路徑（SDK flush → HTTP endpoint → Storage）和讀取路徑（Dashboard 刷新、Debug 即席查詢、聚合趨勢、Rule engine 評估）在 SQLite 階段不太會互相干擾 — 事件量小、查詢簡單、WAL mode 讓讀寫各自進行。進入 PostgreSQL 層之後，兩條路徑的負載都會成長，而且成長方向不同。本章處理的是讀寫開始互相干擾時的辨識訊號和應對策略。&lt;/p>
&lt;h2 id="讀寫競爭的具體場景">讀寫競爭的具體場景&lt;/h2>
&lt;p>Monitor 的 PostgreSQL 層同時承擔三種負載，各自的資源消耗特性不同。&lt;/p>
&lt;h3 id="寫入負載">寫入負載&lt;/h3>
&lt;p>SDK flush 是 Monitor 的主要寫入來源。多個 SDK 同時 flush 時，collector 透過連線池並行寫入 PostgreSQL。每筆 INSERT 涉及主表寫入 + 索引更新（&lt;code>idx_type_ts&lt;/code>、&lt;code>idx_session&lt;/code>、&lt;code>idx_name&lt;/code>）。寫入量隨 SDK 數量和 flush 頻率線性成長。&lt;/p>
&lt;p>Downsample job 是另一種寫入：定期把原始事件聚合到 &lt;code>hourly_summary&lt;/code> / &lt;code>daily_summary&lt;/code>。Downsample 執行時同時做大量 SELECT（讀原始事件）和 INSERT（寫摘要），佔用連線和 I/O。&lt;/p>
&lt;h3 id="dashboard-讀取負載">Dashboard 讀取負載&lt;/h3>
&lt;p>Dashboard 是穩定的高頻背景負載。總覽頁每 30 秒刷新、Error 列表每分鐘刷新、趨勢圖每分鐘重算。每次刷新執行一到多個聚合查詢（&lt;code>GROUP BY name&lt;/code>、&lt;code>COUNT(*)&lt;/code>、時間分桶）。&lt;/p>
&lt;p>Dashboard 查詢的掃描量隨資料累積成長。「過去 7 天每小時的 error 數量」在第一週掃描幾千筆，三個月後掃描幾十萬筆。如果沒有用 &lt;code>hourly_summary&lt;/code> 摘要表、而是直接查原始 events 表，查詢時間會隨資料量線性增加。&lt;/p>
&lt;h3 id="debug-即席讀取負載">Debug 即席讀取負載&lt;/h3>
&lt;p>Debug 查詢是偶發的突增負載。開發者在排查問題時，可能用 session_id 拉出整條事件鏈、用 error name 掃描最近 N 筆 stack trace、或用 &lt;code>data-&amp;gt;&amp;gt;'duration_ms'&lt;/code> 做 ad-hoc 效能分析。這些查詢的特徵是不可預測、偶發但延遲敏感 — 開發者在等結果。&lt;/p>
&lt;h3 id="競爭發生在哪">競爭發生在哪&lt;/h3>
&lt;p>三種負載打同一個 PostgreSQL 時，競爭集中在兩個資源：&lt;/p>
&lt;p>&lt;strong>連線池&lt;/strong>：collector 的 &lt;code>SetMaxOpenConns&lt;/code> 是固定值（例如 20）。如果 ingestion 佔用 15 條連線做批次 INSERT、dashboard 需要 3 條做聚合查詢、debug 需要 2 條做 ad-hoc 查詢 — 剛好佔滿。這時 downsample job 啟動需要連線，會排隊等待。&lt;/p>
&lt;p>&lt;strong>I/O 頻寬&lt;/strong>：聚合查詢需要掃描大量資料（sequential scan 或 index scan + heap access），跟 INSERT 的隨機寫入搶磁碟 I/O。在 HDD 或低階 SSD 上，一個 heavy 聚合查詢可以讓同時進行的 INSERT latency 從毫秒跳到十毫秒。&lt;/p>
&lt;p>&lt;strong>鎖競爭&lt;/strong>：PostgreSQL 的 MVCC 讓 SELECT 跟 INSERT 不互相阻塞（reader 不等 writer），但 Downsample 的 INSERT OR REPLACE 跟 ingestion 的 INSERT 可能在同一張表上競爭 row-level lock。長時間的 aggregation query 也可能觸發 &lt;code>idle in transaction&lt;/code> 問題，佔住連線不釋放。&lt;/p>
&lt;h2 id="辨識訊號">辨識訊號&lt;/h2>
&lt;p>讀寫競爭的辨識訊號是「寫入跟讀取的效能同時退化，而且退化是交互的」：&lt;/p>
&lt;ul>
&lt;li>Ingestion 的 INSERT latency 在 dashboard 刷新時段（每 30 秒）出現週期性尖峰&lt;/li>
&lt;li>Dashboard 的聚合查詢在 SDK 高峰 flush 時段（例：每整點、app 啟動潮）變慢&lt;/li>
&lt;li>Debug 即席查詢在 downsample job 執行期間 timeout&lt;/li>
&lt;li>PostgreSQL 的 &lt;code>pg_stat_activity&lt;/code> 顯示多個 &lt;code>idle in transaction&lt;/code> 或 &lt;code>waiting&lt;/code> 狀態&lt;/li>
&lt;li>連線池使用率持續高於 80%，偶發 &lt;code>too many connections&lt;/code> 或連線等待&lt;/li>
&lt;/ul>
&lt;p>單純的寫入慢（沒有讀取影響）或單純的查詢慢（沒有寫入影響）不是讀寫競爭，可能是索引缺失或查詢效率問題。讀寫競爭的特徵是「兩邊同時退化、一邊忙的時候另一邊也變慢」。&lt;/p></description><content:encoded><![CDATA[<p>Monitor 的寫入路徑（SDK flush → HTTP endpoint → Storage）和讀取路徑（Dashboard 刷新、Debug 即席查詢、聚合趨勢、Rule engine 評估）在 SQLite 階段不太會互相干擾 — 事件量小、查詢簡單、WAL mode 讓讀寫各自進行。進入 PostgreSQL 層之後，兩條路徑的負載都會成長，而且成長方向不同。本章處理的是讀寫開始互相干擾時的辨識訊號和應對策略。</p>
<h2 id="讀寫競爭的具體場景">讀寫競爭的具體場景</h2>
<p>Monitor 的 PostgreSQL 層同時承擔三種負載，各自的資源消耗特性不同。</p>
<h3 id="寫入負載">寫入負載</h3>
<p>SDK flush 是 Monitor 的主要寫入來源。多個 SDK 同時 flush 時，collector 透過連線池並行寫入 PostgreSQL。每筆 INSERT 涉及主表寫入 + 索引更新（<code>idx_type_ts</code>、<code>idx_session</code>、<code>idx_name</code>）。寫入量隨 SDK 數量和 flush 頻率線性成長。</p>
<p>Downsample job 是另一種寫入：定期把原始事件聚合到 <code>hourly_summary</code> / <code>daily_summary</code>。Downsample 執行時同時做大量 SELECT（讀原始事件）和 INSERT（寫摘要），佔用連線和 I/O。</p>
<h3 id="dashboard-讀取負載">Dashboard 讀取負載</h3>
<p>Dashboard 是穩定的高頻背景負載。總覽頁每 30 秒刷新、Error 列表每分鐘刷新、趨勢圖每分鐘重算。每次刷新執行一到多個聚合查詢（<code>GROUP BY name</code>、<code>COUNT(*)</code>、時間分桶）。</p>
<p>Dashboard 查詢的掃描量隨資料累積成長。「過去 7 天每小時的 error 數量」在第一週掃描幾千筆，三個月後掃描幾十萬筆。如果沒有用 <code>hourly_summary</code> 摘要表、而是直接查原始 events 表，查詢時間會隨資料量線性增加。</p>
<h3 id="debug-即席讀取負載">Debug 即席讀取負載</h3>
<p>Debug 查詢是偶發的突增負載。開發者在排查問題時，可能用 session_id 拉出整條事件鏈、用 error name 掃描最近 N 筆 stack trace、或用 <code>data-&gt;&gt;'duration_ms'</code> 做 ad-hoc 效能分析。這些查詢的特徵是不可預測、偶發但延遲敏感 — 開發者在等結果。</p>
<h3 id="競爭發生在哪">競爭發生在哪</h3>
<p>三種負載打同一個 PostgreSQL 時，競爭集中在兩個資源：</p>
<p><strong>連線池</strong>：collector 的 <code>SetMaxOpenConns</code> 是固定值（例如 20）。如果 ingestion 佔用 15 條連線做批次 INSERT、dashboard 需要 3 條做聚合查詢、debug 需要 2 條做 ad-hoc 查詢 — 剛好佔滿。這時 downsample job 啟動需要連線，會排隊等待。</p>
<p><strong>I/O 頻寬</strong>：聚合查詢需要掃描大量資料（sequential scan 或 index scan + heap access），跟 INSERT 的隨機寫入搶磁碟 I/O。在 HDD 或低階 SSD 上，一個 heavy 聚合查詢可以讓同時進行的 INSERT latency 從毫秒跳到十毫秒。</p>
<p><strong>鎖競爭</strong>：PostgreSQL 的 MVCC 讓 SELECT 跟 INSERT 不互相阻塞（reader 不等 writer），但 Downsample 的 INSERT OR REPLACE 跟 ingestion 的 INSERT 可能在同一張表上競爭 row-level lock。長時間的 aggregation query 也可能觸發 <code>idle in transaction</code> 問題，佔住連線不釋放。</p>
<h2 id="辨識訊號">辨識訊號</h2>
<p>讀寫競爭的辨識訊號是「寫入跟讀取的效能同時退化，而且退化是交互的」：</p>
<ul>
<li>Ingestion 的 INSERT latency 在 dashboard 刷新時段（每 30 秒）出現週期性尖峰</li>
<li>Dashboard 的聚合查詢在 SDK 高峰 flush 時段（例：每整點、app 啟動潮）變慢</li>
<li>Debug 即席查詢在 downsample job 執行期間 timeout</li>
<li>PostgreSQL 的 <code>pg_stat_activity</code> 顯示多個 <code>idle in transaction</code> 或 <code>waiting</code> 狀態</li>
<li>連線池使用率持續高於 80%，偶發 <code>too many connections</code> 或連線等待</li>
</ul>
<p>單純的寫入慢（沒有讀取影響）或單純的查詢慢（沒有寫入影響）不是讀寫競爭，可能是索引缺失或查詢效率問題。讀寫競爭的特徵是「兩邊同時退化、一邊忙的時候另一邊也變慢」。</p>
<h2 id="read-replica-分離">Read Replica 分離</h2>
<p>Read replica 是 Monitor 在 PostgreSQL 層後的第一步讀寫分離。概念簡單：寫入走 primary、讀取走 replica，兩者物理隔離。</p>
<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">SDK ──→ Collector
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">             │
</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">        ▼                ▼
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   Primary (write)   Replica (read)
</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">        │  replication →  │
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        │                │
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        ▼                ▼
</span></span><span class="line"><span class="ln">10</span><span class="cl">   Ingestion        Dashboard + Debug
</span></span><span class="line"><span class="ln">11</span><span class="cl">   Downsample       聚合查詢</span></span></code></pre></div><p>Collector 持有兩個連線池 — 一個連 primary（用於 <code>Store()</code>、<code>Downsample()</code>、<code>Purge()</code>），一個連 replica（用於 <code>Query()</code>、<code>Aggregate()</code>、Dashboard 的所有讀取）。</p>
<h3 id="storage-interface-的調整">Storage interface 的調整</h3>
<p>現有的 <code>BasicStorage</code> interface 不需要改動。實作層在初始化時接收兩個 DSN（primary + replica），內部根據操作類型選擇連線池：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">type</span> <span class="nx">PostgresStorage</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">primary</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span>  <span class="c1">// write operations</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">replica</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span>  <span class="c1">// read operations (nil = use primary)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>當 replica DSN 未設定時，所有操作走 primary — 行為跟目前一樣，不破壞 single-instance 部署。</p>
<h3 id="replica-lag-對各查詢場景的影響">Replica lag 對各查詢場景的影響</h3>
<p>PostgreSQL streaming replication 的 lag 在同 AZ 通常 &lt; 100ms，跨 AZ 可能到秒級。各查詢場景對 lag 的容忍度不同：</p>
<table>
  <thead>
      <tr>
          <th>查詢場景</th>
          <th>Lag 容忍度</th>
          <th>走哪裡</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dashboard 總覽</td>
          <td>秒級可接受</td>
          <td>Replica</td>
          <td>30 秒刷新一次，lag &lt; 1 秒不影響判讀</td>
      </tr>
      <tr>
          <td>Error 列表</td>
          <td>秒級可接受</td>
          <td>Replica</td>
          <td>新 error 晚一秒出現在列表上不影響 debug</td>
      </tr>
      <tr>
          <td>聚合趨勢圖</td>
          <td>分鐘級可接受</td>
          <td>Replica</td>
          <td>趨勢圖本身就是歷史資料的聚合</td>
      </tr>
      <tr>
          <td>Funnel / Cohort</td>
          <td>分鐘級可接受</td>
          <td>Replica</td>
          <td>分析查詢看的是天級或週級的資料</td>
      </tr>
      <tr>
          <td>Debug 即席查詢</td>
          <td>數秒可能不接受</td>
          <td>Primary</td>
          <td>開發者剛送一筆 test event 想立刻查到</td>
      </tr>
      <tr>
          <td>Rule engine 查歷史</td>
          <td>秒級可接受</td>
          <td>Replica</td>
          <td>Rule 的閾值判斷容忍短暫延遲</td>
      </tr>
  </tbody>
</table>
<p>Debug 即席查詢的 lag 問題是 read-after-write 一致性 — 開發者從 SDK 送出 test event 後立刻查詢，如果查 replica 可能還沒同步到。解法是讓 debug query API 提供 <code>consistency=strong</code> 參數，強制走 primary。預設走 replica（大部分 debug 查的是歷史資料），只有需要 read-after-write 時切 primary。</p>
<h3 id="引入時機">引入時機</h3>
<p>Read replica 的引入時機是「辨識訊號」段列出的讀寫競爭訊號持續出現，而且已經做過基本最佳化（索引補齊、dashboard 改讀 summary 表、downsample job 調整執行時段避開高峰）仍然不夠。</p>
<p>引入 read replica 的成本是多一台 PostgreSQL 實例（或 managed service 的 read replica 選項）和 replication 設定。Monitor 的 PostgreSQL 層已經承擔外部 DB 的運維成本，加 replica 是增量而非從零開始。</p>
<h2 id="預聚合作為讀取面的第一道防線">預聚合作為讀取面的第一道防線</h2>
<p>在引入 read replica 之前，預聚合是降低讀取負載最有效的方式 — 不改架構、不加機器、只改查詢的資料來源。</p>
<p>Monitor 已經有 <code>hourly_summary</code> 跟 <code>daily_summary</code> 兩張摘要表（見 <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a>）。Dashboard 的趨勢圖跟 Error 計數應該讀摘要表而非原始 events 表。</p>
<p>預聚合沒處理到的讀取負載是「需要原始事件的查詢」— Debug 即席查詢（看 stack trace）、Session 回放（看事件序列）、Funnel 分析（跨 session JOIN）。這些查詢必須掃描原始資料，預聚合無法取代。當這類查詢的負載開始擠壓寫入時，才是引入 read replica 的時機。</p>
<p>概念上，預聚合就是 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 在關聯式資料庫的實作。Downsample job 定期執行 aggregation query、把結果寫入 summary 表，dashboard 讀 summary 表而非重算 raw data。Monitor 的 <code>hourly_summary</code> 等同於 Prometheus 的 recording rule output、PostgreSQL 的 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a> 等同於 TSDB 的 continuous aggregate。</p>
<h2 id="cqrs-的判讀訊號">CQRS 的判讀訊號</h2>
<p>Read replica 解決的是「讀寫搶同一台機器的 I/O 跟連線」。當問題不只是資源競爭、而是讀寫的資料形狀根本不同時，read replica 不夠 — 需要獨立的 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a>。</p>
<p><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 的完整概念見知識卡。以下是 Monitor 情境下，什麼訊號出現時該考慮從 read replica 往 CQRS 方向演進。</p>
<h3 id="訊號一讀取需要的資料形狀跟-events-表差異太大">訊號一：讀取需要的資料形狀跟 events 表差異太大</h3>
<p>Monitor 的 events 表是 append-only 的正規化結構（一筆事件一個 row）。如果讀取面需要的是：</p>
<ul>
<li>每個 user 的行為摘要（最近登入、最常用功能、累計 error 數）— 需要跨所有事件聚合成 per-user profile</li>
<li>即時的 error fingerprint 索引（相同 stack trace 的 error 自動分群、計數、追蹤首次出現時間）— 需要維護一張反正規化的 error group 表</li>
<li>跨 session 的 funnel conversion 快照 — 需要維護一張 pre-computed funnel 表</li>
</ul>
<p>這些讀取形狀無法用 <code>SELECT FROM events</code> + 索引高效產生，需要獨立的 read model 持續從 events 推算。</p>
<h3 id="訊號二預聚合的種類和刷新頻率失控">訊號二：預聚合的種類和刷新頻率失控</h3>
<p>Summary 表從 2 張（hourly + daily）增長到 5 張、10 張，每張的刷新頻率從每小時變成每分鐘。Downsample job 的執行時間從秒級增長到分鐘級，開始擠壓 ingestion。</p>
<p>這時候 summary 表已經不只是「摘要」，而是事實上的 read model — 專門為讀取需求設計的獨立資料結構。承認這個事實、把 summary 表的維護從 Downsample job 拆出來成為獨立的 projection consumer，就是進入 <a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 的起點。</p>
<h3 id="訊號三讀取跟寫入需要獨立擴展">訊號三：讀取跟寫入需要獨立擴展</h3>
<p>寫入量穩定（SDK 數量不變），但讀取面因為新增 dashboard、新增分析維度、新增使用者而持續成長。Read replica 可以加多台分攤讀取，但每台 replica 仍然存的是跟 primary 一樣的 events 表結構 — 讀取查詢的複雜度不變，只是分攤到更多機器。</p>
<p>獨立的 read model 可以用完全不同的 schema（反正規化、pre-joined、pre-aggregated），讓讀取查詢從 O(N) 的聚合變成 O(1) 的 lookup。這是 CQRS 的核心價值 — 讀取面的效能不再受限於寫入面的資料結構。</p>
<h3 id="monitor-目前的位置">Monitor 目前的位置</h3>
<p>Monitor 目前在「SQLite → PostgreSQL → Read Replica」這條路徑的前半段。MVP 用 SQLite、功能需求觸發 PostgreSQL、讀寫競爭觸發 Read Replica。CQRS 是更遠的演進方向，只有上述三個訊號明確出現時才值得引入。</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">SQLite（零依賴）
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → PostgreSQL（聚合分析觸發）
</span></span><span class="line"><span class="ln">3</span><span class="cl">    → 預聚合 summary 表（讀取負載觸發）
</span></span><span class="line"><span class="ln">4</span><span class="cl">      → Read Replica（讀寫競爭觸發）
</span></span><span class="line"><span class="ln">5</span><span class="cl">        → 獨立 read model / CQRS（資料形狀不對稱觸發）</span></span></code></pre></div><p>每一步都是被具體的效能訊號或功能需求推動的，跟 Monitor 整體的「按觀察到的瓶頸切換」原則一致。教學的價值在於讓讀者在每一步都知道「下一步是什麼、什麼訊號出現時該走」— 而不是在 SQLite 階段就預先設計 CQRS。</p>
<h2 id="跟-backend-的概念對照">跟 Backend 的概念對照</h2>
<p>Monitor 的讀寫分離路徑跟 backend 教材的概念有直接對應：</p>
<table>
  <thead>
      <tr>
          <th>Monitor 演進階段</th>
          <th>Backend 對應概念</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQLite WAL（讀寫各自進行）</td>
          <td><a href="/blog/backend/knowledge-cards/write-ahead-log/" data-link-title="Write-Ahead Log" data-link-desc="說明資料庫如何先寫入 log 再合併回主資料，以提供持久性與崩潰復原">WAL mode</a> 的 reader-writer 並行</td>
      </tr>
      <tr>
          <td>PostgreSQL summary 表</td>
          <td><a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">Materialized view</a> 的最簡實作</td>
      </tr>
      <tr>
          <td>Read replica</td>
          <td><a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 Query Boundary</a> 的讀寫分流</td>
      </tr>
      <tr>
          <td>獨立 read model</td>
          <td><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> + <a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">Projection</a></td>
      </tr>
      <tr>
          <td>Downsample job → 獨立 worker</td>
          <td><a href="/blog/backend/knowledge-cards/event-sourcing/" data-link-title="Event Sourcing" data-link-desc="說明用 append-only 事件流取代 mutable state 作為正式紀錄的設計模式、需求判準與代價">Event sourcing</a> 架構中 projection consumer 的起點</td>
      </tr>
  </tbody>
</table>
<p>Monitor 的規模演進路徑是 backend 概念的具體實例 — 從自用工具到小型服務、從單機到讀寫分離、從 summary 表到可能的 CQRS，每一步都能回到 backend 教材找到概念基礎。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Storage backend 的可插拔架構 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>功能分層的定義 → <a href="/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇</a></li>
<li>Ingestion 端的流量防線 → <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a></li>
<li>讀寫分離的通用概念 → <a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS 知識卡</a></li>
<li>資料庫層的讀寫分離設計 → <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership 與 Query Boundary</a></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></li>
</ul>
]]></content:encoded></item></channel></rss>