<?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>Storage on Tarragon</title><link>https://tarrragon.github.io/blog/tags/storage/</link><description>Recent content in Storage on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 01 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/storage/index.xml" rel="self" type="application/rss+xml"/><item><title>模組一：資料庫與持久化</title><link>https://tarrragon.github.io/blog/backend/01-database/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/</guid><description>&lt;p>資料庫模組的核心目標是說明 application 狀態進入持久化層後，如何維持一致性、可演進性與可測性。語言教材會先定義 repository port、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol&lt;/a> 或 interface；本模組負責說明具體資料庫 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter&lt;/a> 如何實作這些邊界。閱讀本模組前，可先建立 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 的共同語意。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendors&lt;/a> — T1 收錄 PostgreSQL / MySQL / SQLite / MongoDB / DynamoDB / CockroachDB / Aurora，每個服務頁提供定位、適用場景、取捨、容量判準、案例對照與下一步路由。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分類&lt;/th>
 &lt;th>內容方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SQLite&lt;/td>
 &lt;td>embedded &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a>、單機服務、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a>、測試資料庫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PostgreSQL&lt;/td>
 &lt;td>schema design、index、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration&lt;/td>
 &lt;td>versioned schema、rollback、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract&lt;/a> migration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>unit of work、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary&lt;/a>、deadlock、retry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Repository &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">adapter&lt;/a>&lt;/td>
 &lt;td>SQL row mapping、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract&lt;/a> test、錯誤轉換&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="選型入口">選型入口&lt;/h2>
&lt;p>資料庫選型的核心判斷是資料是否承擔正式狀態與一致性。當資料需要長期保存、支援查詢、被多個流程共同讀寫，並且需要交易保護時，應先評估 relational database 或 document database。&lt;/p>
&lt;p>SQLite 適合單機服務、embedded app、測試資料庫與低操作成本場景；PostgreSQL 適合多使用者後端、複雜查詢、transaction、index 與長期 schema evolution。Migration 工具解決 schema 隨版本演進的問題；transaction boundary 解決多筆資料一起成功或失敗的問題；repository adapter 解決 application port 到具體 SQL 實作的轉換。&lt;/p>
&lt;p>接近真實網路服務的例子包括訂單系統、會員系統、訂閱方案、付款紀錄與權限資料。這些資料都需要明確 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，因此本模組會從資料模型、一致性、migration 與 repository adapter 邊界開始說明。&lt;/p>
&lt;h2 id="與語言教材的分工">與語言教材的分工&lt;/h2>
&lt;p>語言教材處理 repository interface / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol&lt;/a>、取消與逾時、error wrapping、memory fake 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract&lt;/a> test。Backend database 模組處理 SQL schema、migration tool、transaction isolation、connection pool 與資料庫錯誤語意。&lt;/p></description><content:encoded><![CDATA[<p>資料庫模組的核心目標是說明 application 狀態進入持久化層後，如何維持一致性、可演進性與可測性。語言教材會先定義 repository port、<a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a> 或 interface；本模組負責說明具體資料庫 <a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter</a> 如何實作這些邊界。閱讀本模組前，可先建立 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、<a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a>、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a> 與 <a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 的共同語意。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendors</a> — T1 收錄 PostgreSQL / MySQL / SQLite / MongoDB / DynamoDB / CockroachDB / Aurora，每個服務頁提供定位、適用場景、取捨、容量判準、案例對照與下一步路由。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQLite</td>
          <td>embedded <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a>、單機服務、<a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a>、測試資料庫</td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td>schema design、index、<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a>、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a>、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a></td>
      </tr>
      <tr>
          <td>Migration</td>
          <td>versioned schema、rollback、<a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a> migration</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>unit of work、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、deadlock、retry</td>
      </tr>
      <tr>
          <td>Repository <a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">adapter</a></td>
          <td>SQL row mapping、<a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test、錯誤轉換</td>
      </tr>
  </tbody>
</table>
<h2 id="選型入口">選型入口</h2>
<p>資料庫選型的核心判斷是資料是否承擔正式狀態與一致性。當資料需要長期保存、支援查詢、被多個流程共同讀寫，並且需要交易保護時，應先評估 relational database 或 document database。</p>
<p>SQLite 適合單機服務、embedded app、測試資料庫與低操作成本場景；PostgreSQL 適合多使用者後端、複雜查詢、transaction、index 與長期 schema evolution。Migration 工具解決 schema 隨版本演進的問題；transaction boundary 解決多筆資料一起成功或失敗的問題；repository adapter 解決 application port 到具體 SQL 實作的轉換。</p>
<p>接近真實網路服務的例子包括訂單系統、會員系統、訂閱方案、付款紀錄與權限資料。這些資料都需要明確 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，因此本模組會從資料模型、一致性、migration 與 repository adapter 邊界開始說明。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理 repository interface / <a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a>、取消與逾時、error wrapping、memory fake 與 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test。Backend database 模組處理 SQL schema、migration tool、transaction isolation、connection pool 與資料庫錯誤語意。</p>
<p>跨模組端到端串聯（DB → cache → event → observability）見 <a href="/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15 跨模組 Checkout Episode</a>。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a></td>
          <td>高併發下的 SQL 讀寫邊界</td>
          <td>共用 <code>sql.DB</code>、控制連線池、縮小 transaction 範圍</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2</a></td>
          <td>schema design 與資料建模</td>
          <td>規劃 table、index、key 與命名規則</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3</a></td>
          <td>transaction 與一致性邊界</td>
          <td>判斷何時使用 transaction、retry 與 isolation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">1.4</a></td>
          <td>repository adapter 實作</td>
          <td>把 SQL row mapping 與錯誤轉換封裝成 adapter</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5</a></td>
          <td>攻擊者視角（紅隊）：資料層弱點判讀</td>
          <td>用越權查詢、資料外洩路徑與恢復成本檢查資料層設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6</a></td>
          <td>資料庫轉換實作</td>
          <td>把雙寫、回填、切流與回滾做成可分段驗證流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7</a></td>
          <td>Schema Migration Rollout 證據實作示範</td>
          <td>以訂單付款狀態欄位演進示範 evidence、gate 與 decision log</td>
      </tr>
      <tr>
          <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</a></td>
          <td>State Ownership 與 Query Boundary</td>
          <td>分辨正式狀態、派生狀態與不同查詢責任</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9</a></td>
          <td>Reconciliation 與 Data Repair</td>
          <td>把資料錯誤轉成可驗證、可修復、可稽核流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10</a></td>
          <td>KV / Document DB 容量規劃</td>
          <td>partition key 設計、capacity mode、multi-model 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11</a></td>
          <td>全球分散式 OLTP</td>
          <td>Spanner / Aurora DSQL / Cosmos DB multi-region 跟 <a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP</a> 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12</a></td>
          <td>大規模 DB 遷移實戰</td>
          <td>dual-write / <a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a> / cutover / <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13</a></td>
          <td>應用層查詢反模式與 Query 預算</td>
          <td>N+1、select *、缺索引、ORM lazy load、long transaction 與每請求 query 預算</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/production-slow-log-loop/" data-link-title="1.14 Production Slow Log Closed Loop" data-link-desc="把 production slow log 從『偶爾看一下』變成『定期審視 &#43; PR review 整合 &#43; regression 偵測』的閉環、補 1.13 反模式清單後的操作層">1.14</a></td>
          <td>Production Slow Log Closed Loop</td>
          <td>採集 / Normalize / PR review 整合 / Regression 偵測 — 把 slow log 從事故工具變成定期審視訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendor-article-spec/" data-link-title="資料庫 Vendor 文章撰寫規格" data-link-desc="把 PostgreSQL 與 MySQL batch 的正文經驗整理成資料庫 vendor overview、deep article 與 migration playbook 的撰寫規格">Vendor 文章撰寫規格</a></td>
          <td>Vendor overview / deep article / migration playbook 分工</td>
          <td>把 PostgreSQL / MySQL batch 經驗整理成後續資料庫服務頁的撰寫規格</td>
      </tr>
  </tbody>
</table>
<h2 id="觀念網路補完方向">觀念網路補完方向</h2>
<p>資料庫章節下一輪的核心責任是把正式狀態的演進路徑講完整。現有章節已經涵蓋 schema、transaction、repository adapter 與 migration playbook，但還需要補上 state ownership、query boundary、migration safety 與 reconciliation 之間的引用關係，讓讀者知道資料庫變更如何從設計、發布、觀測一路接到事故決策。</p>
<table>
  <thead>
      <tr>
          <th>補完方向</th>
          <th>需要回答的問題</th>
          <th>主要路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>State ownership</td>
          <td>哪些資料是正式狀態，哪些只是 cache、index 或事件副本</td>
          <td><a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、<a href="/blog/backend/00-service-selection/state-storage-selection/" data-link-title="0.2 狀態與資料儲存選型" data-link-desc="區分 source of truth、快取、搜尋索引、event log 與 object storage 的選型邊界">0.2</a></td>
      </tr>
      <tr>
          <td>Query boundary</td>
          <td>交易查詢、列表查詢、報表查詢與對帳查詢是否混在一起</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a></td>
      </tr>
      <tr>
          <td>Migration safety</td>
          <td>schema 變更是否能分批、驗證、暫停與回退</td>
          <td><a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>Reconciliation</td>
          <td>資料錯誤發生後如何驗證、修復、對帳與留下證據</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a>、<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
      </tr>
      <tr>
          <td>Data protection</td>
          <td>正式資料在查詢、匯出、修復與刪除時如何保留責任邊界</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4</a>、<a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7</a></td>
      </tr>
  </tbody>
</table>
<p>這些方向要寫成資料庫自己的敘事，避免把 04/06/08 的欄位直接搬進來。資料庫關心的是狀態能否正確演進；觀測、驗證與事故流程接收這個演進結果作為下游證據。</p>
<h2 id="知識卡補強方向">知識卡補強方向</h2>
<p>資料庫模組的 knowledge card 缺口集中在「變更如何被驗證」與「資料如何被修復」。已有 <a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">schema migration</a>、<a href="/blog/backend/knowledge-cards/expand-contract/" data-link-title="Expand / Contract" data-link-desc="說明先擴充相容面、再收斂舊路徑的遷移做法">Expand / Contract</a>、<a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">backfill</a> 與 <a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">dual write</a> 可作為第一批錨點。</p>
<p>下一批候選卡片包括 migration validation、read compatibility、<a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover window</a>、reconciliation、data repair runbook 與 fail-forward migration。這些卡片要先定義服務責任與使用時機，再讓 1.6 migration playbook 與後續實作文章引用。</p>
<h2 id="vendor-文章規格入口">Vendor 文章規格入口</h2>
<p>資料庫 vendor 文章的下一輪重點是把 PostgreSQL / MySQL batch 經驗變成可重複使用的撰寫規格。後續寫 SQLite、MongoDB、DynamoDB、Aurora、Spanner、Cosmos DB 與 CockroachDB 前，先讀 <a href="/blog/backend/01-database/vendor-article-spec/" data-link-title="資料庫 Vendor 文章撰寫規格" data-link-desc="把 PostgreSQL 與 MySQL batch 的正文經驗整理成資料庫 vendor overview、deep article 與 migration playbook 的撰寫規格">資料庫 Vendor 文章撰寫規格</a>；該文分清 vendor overview、deep article 與 migration playbook 的責任，並列出 PG / MySQL 回收出的橫向調整項。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>資料庫的第一條實作路徑已完成： <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據實作示範</a>。這篇以訂單資料表付款狀態欄位演進為例，說明 migration plan、<a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、rollback condition 與 incident decision route 如何一起成立。</p>
<p>這條路徑的前置引用是 1.2 schema design、1.3 transaction boundary、1.6 migration playbook、<a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a> 與 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。完成後可依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入 02 cache migration。</p>
<p>資料庫路徑的 artifact 對齊重點是「先證明資料演進正確，再討論是否放行」。對 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 要交 <code>Source/Time range/Query link/Owner/Data quality</code>，並在 query 內容覆蓋 validation query、row count 差異與 replication lag；對 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11</a> / <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 要交 <code>Gate decision/Checks/Stop condition/Rollback window/Owner</code>，呈現 expand/contract 分段結果；對 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 要交 <code>Timestamp/Decision/Context/Evidence/Owner/Expected effect/Rollback condition</code>，記錄 pause / rollback / fail-forward 的判斷與依據。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/05-core-services/deployment-order-database/" data-link-title="部署順序與資料庫上 IaC" data-link-desc="核心服務的依賴圖決定部署順序，資料庫作為第一批上層服務需要最謹慎的 IaC 描述 — 涵蓋 RDS 接線、連線管理、read replica 與端點暴露">infra 模組五：資料庫上 IaC</a>：RDS 的 IaC 描述（subnet group、parameter group、連線管理、read replica）與部署順序</li>
<li>→ <a href="/blog/infra/05-core-services/stateful-protection-dependency/" data-link-title="Stateful 資源保護與跨服務依賴表達" data-link-desc="stateful 資源的保護策略（multi-AZ、備份、刪除保護）、stateful 與 stateless 的操作差異，以及用 output 與 data source 表達服務間依賴">infra 模組五：Stateful 資源保護</a>：multi-AZ、backup retention、deletion protection、PITR 的 IaC 設定</li>
</ul>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>資料庫使用方式會受語言的 connection pool、transaction scope、ORM 行為、錯誤處理與 migration 生態影響。同步 thread-based runtime 要控制 blocking query 與 pool 大小；async runtime 要確認 database client 是否真正非阻塞；輕量並發 runtime 要限制同時查詢數量，避免把大量 task 轉成資料庫連線壓力。強型別語言適合把 row mapping、schema 與錯誤分類型別化；動態語言則需要靠 migration、runtime validation、fixture 與 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a> test 保護資料邊界。</p>
]]></content:encoded></item><item><title>Case Study：Blog 語意搜尋從 pickle 到 production</title><link>https://tarrragon.github.io/blog/llm/04-applications/hands-on/blog-vector-search/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/hands-on/blog-vector-search/</guid><description>&lt;p>本案例記錄一個技術 blog（2,738 篇 markdown、24,216 chunks）的語意搜尋工具從 demo 到 production 的完整過程。每段標出對應 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 RAG storage 工程&lt;/a> 的哪個判讀步驟，讓讀者看到原理章的框架怎麼落到具體決策。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>實測日期&lt;/strong>：2026-07-01
&lt;strong>環境&lt;/strong>：macOS Apple Silicon、Ollama 0.7.x、&lt;code>nomic-embed-text&lt;/code>（768 維）
&lt;strong>Corpus&lt;/strong>：&lt;code>content/&lt;/code> 全量 2,738 個 markdown 檔、24,216 chunks
&lt;strong>前置 demo&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &amp;#43; cosine retrieval &amp;#43; Ollama chat、validating 4.0 RAG 原理">rag-demo&lt;/a>（pickle、463 chunks）&lt;/p>&lt;/blockquote>
&lt;h3 id="讀法建議">讀法建議&lt;/h3>
&lt;p>本案例用 Go 重寫了 RAG storage 層，Go 實作細節佔不少篇幅。依你的背景選讀法：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Python 開發者、想選自己專案的 storage 方案&lt;/strong>：先跳到「通用可複製流程」（語言無關的五步驟）→「四方案 benchmark」→「二次選型評估」（結論/理由/前提三層框架），這三段跨語言可遷移。Go 實作段（架構、效能優化）可 skim。&lt;/li>
&lt;li>&lt;strong>Go 開發者、想做類似工具&lt;/strong>：從頭讀，每段都跟你相關。&lt;/li>
&lt;li>&lt;strong>只想看選型框架、不管實作&lt;/strong>：直接跳「二次選型評估」。&lt;/li>
&lt;/ul>
&lt;h2 id="從-demo-到-production-的重寫動機">從 demo 到 production 的重寫動機&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &amp;#43; cosine retrieval &amp;#43; Ollama chat、validating 4.0 RAG 原理">rag-demo&lt;/a> 用 Python pickle 跑通了 RAG 概念驗證：71 篇 → 463 chunks → pickle 儲存 → cosine retrieval → Ollama 生成。概念層完全正確（4.1 的 retrieval + augmentation 骨架），但作為這個 blog 的日常工具有三個&lt;strong>專案特有的&lt;/strong>限制：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>工具鏈語言不同&lt;/strong>：blog 的核心工具是 Go（lint / fmt / cards），加 Python dependency 讓其他維護者 clone 後多一步環境設定。Python 專案不會有這個問題 — pickle 綁 Python 對 Python 專案是優點而非缺點。&lt;/li>
&lt;li>&lt;strong>只索引部分 corpus&lt;/strong>：rag-demo 只跑 &lt;code>content/llm/&lt;/code>（71 篇），blog 全量有 2,738 篇、24 個 section。&lt;/li>
&lt;li>&lt;strong>Demo 定位&lt;/strong>：ingest.py / query.py 是教學程式碼，不是維護工具（沒有 status、沒有 section filter）。&lt;/li>
&lt;/ol>
&lt;p>這是一次&lt;strong>完整重寫&lt;/strong>、不是漸進升級 — rag-demo 的 Python 程式碼不會被修改或遷移，而是用 Go 重新實作相同的 RAG pipeline（chunk → embed → store → search）、保留相同的概念架構。rag-demo 作為教學 demo 繼續存在。&lt;/p>
&lt;p>升級目標：一個跟 &lt;code>mdtools&lt;/code> 同級的 Go CLI 工具，能對全量 content 做語意搜尋，其他維護者 clone 後 &lt;code>go build&lt;/code> 即可用。完整原始碼在 &lt;code>scripts/blogsearch/&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<p>本案例記錄一個技術 blog（2,738 篇 markdown、24,216 chunks）的語意搜尋工具從 demo 到 production 的完整過程。每段標出對應 <a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 RAG storage 工程</a> 的哪個判讀步驟，讓讀者看到原理章的框架怎麼落到具體決策。</p>
<blockquote>
<p><strong>實測日期</strong>：2026-07-01
<strong>環境</strong>：macOS Apple Silicon、Ollama 0.7.x、<code>nomic-embed-text</code>（768 維）
<strong>Corpus</strong>：<code>content/</code> 全量 2,738 個 markdown 檔、24,216 chunks
<strong>前置 demo</strong>：<a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">rag-demo</a>（pickle、463 chunks）</p></blockquote>
<h3 id="讀法建議">讀法建議</h3>
<p>本案例用 Go 重寫了 RAG storage 層，Go 實作細節佔不少篇幅。依你的背景選讀法：</p>
<ul>
<li><strong>Python 開發者、想選自己專案的 storage 方案</strong>：先跳到「通用可複製流程」（語言無關的五步驟）→「四方案 benchmark」→「二次選型評估」（結論/理由/前提三層框架），這三段跨語言可遷移。Go 實作段（架構、效能優化）可 skim。</li>
<li><strong>Go 開發者、想做類似工具</strong>：從頭讀，每段都跟你相關。</li>
<li><strong>只想看選型框架、不管實作</strong>：直接跳「二次選型評估」。</li>
</ul>
<h2 id="從-demo-到-production-的重寫動機">從 demo 到 production 的重寫動機</h2>
<p><a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">rag-demo</a> 用 Python pickle 跑通了 RAG 概念驗證：71 篇 → 463 chunks → pickle 儲存 → cosine retrieval → Ollama 生成。概念層完全正確（4.1 的 retrieval + augmentation 骨架），但作為這個 blog 的日常工具有三個<strong>專案特有的</strong>限制：</p>
<ol>
<li><strong>工具鏈語言不同</strong>：blog 的核心工具是 Go（lint / fmt / cards），加 Python dependency 讓其他維護者 clone 後多一步環境設定。Python 專案不會有這個問題 — pickle 綁 Python 對 Python 專案是優點而非缺點。</li>
<li><strong>只索引部分 corpus</strong>：rag-demo 只跑 <code>content/llm/</code>（71 篇），blog 全量有 2,738 篇、24 個 section。</li>
<li><strong>Demo 定位</strong>：ingest.py / query.py 是教學程式碼，不是維護工具（沒有 status、沒有 section filter）。</li>
</ol>
<p>這是一次<strong>完整重寫</strong>、不是漸進升級 — rag-demo 的 Python 程式碼不會被修改或遷移，而是用 Go 重新實作相同的 RAG pipeline（chunk → embed → store → search）、保留相同的概念架構。rag-demo 作為教學 demo 繼續存在。</p>
<p>升級目標：一個跟 <code>mdtools</code> 同級的 Go CLI 工具，能對全量 content 做語意搜尋，其他維護者 clone 後 <code>go build</code> 即可用。完整原始碼在 <code>scripts/blogsearch/</code>。</p>
<h2 id="選型過程對應-422-演化階梯--工程約束">選型過程（對應 4.22 演化階梯 + 工程約束）</h2>
<h3 id="第一軸規模判讀">第一軸：規模判讀</h3>
<p>全量 content 產生 24,216 chunks（原本估計 ~1,500）。按 4.22 判讀樹，24K 落在「10K-100K → HNSW 或 brute-force」區間。預估 vs 實際的 16 倍落差揭露一個教訓：<strong>估計 chunk 數不能用篇數乘以常數</strong>，要看每篇的實際長度跟 chunking 策略。</p>
<h3 id="第二軸工程約束本專案特有">第二軸：工程約束（本專案特有）</h3>
<p>以下四個 constraint 反映<strong>這個 blog 專案的偏好</strong>、不是通用判準。換一組 constraint 會篩出完全不同的方案 — Python 專案不會有「Go 單 binary」constraint、已有 Docker 的團隊不會排斥外部 server。讀者套用時應先列出自己專案的 constraint、不是照搬這張表。</p>
<table>
  <thead>
      <tr>
          <th>Constraint</th>
          <th>砍掉什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go 單 binary</td>
          <td>Python-only 方案（pickle / FAISS）</td>
      </tr>
      <tr>
          <td>不要 CGo</td>
          <td>sqlite-vec（需要 <code>mattn/go-sqlite3</code>）</td>
      </tr>
      <tr>
          <td>不要外部 server</td>
          <td>Qdrant / Weaviate / Pinecone</td>
      </tr>
      <tr>
          <td>Ollama 原生</td>
          <td>OpenAI / Cohere embedding（多一個 API key）</td>
      </tr>
  </tbody>
</table>
<p>剩餘選項：<strong>Go + flat file + brute-force</strong>。</p>
<h3 id="第三軸延遲容忍">第三軸：延遲容忍</h3>
<p>CLI 工具、每天用幾次、不是 API server。&lt; 500ms 可接受。</p>
<p>結論：選階段二（flat file），brute-force cosine。</p>
<h2 id="實作架構">實作架構</h2>





<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">scripts/blogsearch/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── main.go                     # CLI: ingest / query / status
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">├── cmd/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│   ├── ingest.go               # walk content/ → chunk → embed → store
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│   ├── query.go                # load → embed query → cosine top-K → lazy load text
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│   └── status.go               # index stats
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">└── internal/
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    ├── chunk/chunk.go           # paragraph-aware markdown chunking
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    ├── embed/embed.go           # Ollama HTTP API wrapper
</span></span><span class="line"><span class="ln">10</span><span class="cl">    ├── search/search.go         # brute-force cosine similarity
</span></span><span class="line"><span class="ln">11</span><span class="cl">    └── store/store.go           # 三檔案 binary store</span></span></code></pre></div><h3 id="日常使用">日常使用</h3>





<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">./bin/blogsearch query <span class="s2">&#34;retry 策略&#34;</span>
</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 class="c1"># 只搜特定 section</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">./bin/blogsearch query -section backend <span class="s2">&#34;connection pool 設定&#34;</span>
</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"><span class="c1"># 查 index 狀態</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">./bin/blogsearch status</span></span></code></pre></div><h3 id="storage-格式三檔案分離">Storage 格式（三檔案分離）</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">.blogsearch/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── vectors.bin    # float32 binary（70.9 MB）— bulk read + unsafe.Slice 零拷貝
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── meta.json      # compact metadata 不含 text（7.3 MB）
</span></span><span class="line"><span class="ln">4</span><span class="cl">└── texts.bin      # length-prefixed chunk text（19.2 MB）— top-K 才 lazy load</span></span></code></pre></div><p>分離 text 的設計理由：query 時只需要 vectors + metadata 做 cosine search（78 MB），top-K 結果才從 texts.bin 按 offset 讀取 5 筆 text。省掉 19 MB 的 JSON 解析。</p>
<h2 id="效能優化歷程">效能優化歷程</h2>
<h3 id="初版95-秒">初版：9.5 秒</h3>
<p>初版用逐 4-byte Read 載入 vectors.bin（17.5M 次 <code>f.Read(buf)</code>），加上 27MB 的 index.json（含所有 chunk text）一次 JSON 解析。</p>
<h3 id="優化版034-秒28x">優化版：0.34 秒（28x）</h3>
<p>三項改動：</p>
<table>
  <thead>
      <tr>
          <th>改動</th>
          <th>從</th>
          <th>到</th>
          <th>效果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>vectors.bin 讀法</td>
          <td>逐 4-byte Read</td>
          <td><code>os.ReadFile</code> + <code>unsafe.Slice</code></td>
          <td>I/O call 17.5M → 1</td>
      </tr>
      <tr>
          <td>metadata 格式</td>
          <td>含 text（27 MB）</td>
          <td>不含 text（7.3 MB）</td>
          <td>JSON parse 快 4x</td>
      </tr>
      <tr>
          <td>text 載入</td>
          <td>全量</td>
          <td>top-K lazy load（只讀 5 筆）</td>
          <td>省 19 MB 讀取</td>
      </tr>
  </tbody>
</table>
<p>瓶頸分析：0.34 秒裡、embedding API call（Ollama）約 77ms、file I/O + JSON parse 約 200ms、cosine 計算約 50ms。cosine 計算只佔 15%。</p>
<h2 id="通用可複製流程抽掉-goblog">通用可複製流程（抽掉 Go/blog）</h2>
<p>本案例的 Go 實作細節（<code>unsafe.Slice</code>、<code>os.ReadFile</code>）是語言特定的、但背後的流程步驟跨語言通用：</p>
<ol>
<li><strong>Walk corpus</strong>：遞迴掃描目標目錄的所有文件（markdown / code / 任意文字）</li>
<li><strong>Chunk</strong>：段落感知分割、soft token cap、保留語意邊界（原理見 <a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 Chunking</a>）</li>
<li><strong>Embed</strong>：對每個 chunk 呼叫 embedding API（本地 Ollama 或 cloud API），得到固定維度向量</li>
<li><strong>Store</strong>：向量 + metadata + text 分離存檔（binary vectors / compact JSON / lazy-load text）</li>
<li><strong>Search</strong>：embed query → brute-force cosine → top-K → lazy load text for display</li>
</ol>
<p>Python 實作同流程只是把第 4 步的 binary 檔換成 pickle / FAISS index / SQLite DB、第 5 步的 cosine 換成 numpy / FAISS / sqlite-vec query。Node.js / Rust 同理。</p>
<p>關鍵優化原則也跨語言：「分離向量與文字、query 時只載入向量、top-K 才載入文字」讓 I/O 量從 ~98MB 降到 ~78MB、JSON parse 從 27MB 降到 7MB。這個原則用什麼語言實作都有效。</p>
<h2 id="四方案同-corpus-benchmark">四方案同 corpus Benchmark</h2>
<p>用同一個 corpus（24,216 chunks、768 維、nomic-embed-text）比較四種 storage 方案。Benchmark 腳本在 <code>scripts/blogsearch-bench/bench.py</code>。</p>
<h3 id="前置依賴">前置依賴</h3>
<p>Benchmark 腳本讀 Go 工具產生的 index（<code>.blogsearch/</code> 下的 <code>vectors.bin</code> + <code>meta.json</code>）。完整指令鏈：</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="nb">cd</span> scripts/blogsearch <span class="o">&amp;&amp;</span> go build -o ../../bin/blogsearch .   <span class="c1"># build Go 工具</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ollama serve <span class="p">&amp;</span>                                                  <span class="c1"># 啟動 Ollama</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">ollama pull nomic-embed-text                                    <span class="c1"># pull embedding model</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">./bin/blogsearch ingest -content content -out .blogsearch       <span class="c1"># 建 index（~4 分鐘）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">uv run --with sqlite-vec --with faiss-cpu --with numpy <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  scripts/blogsearch-bench/bench.py --index .blogsearch         <span class="c1"># 跑 benchmark</span></span></span></code></pre></div><p>若無 Go 環境，可用自己的 Python embedding 腳本產生相同格式的 <code>vectors.bin</code>（little-endian float32、n × dim 連續排列）+ <code>meta.json</code>（<code>{&quot;dim&quot;: 768, &quot;count&quot;: n, &quot;metas&quot;: [...]}</code>），benchmark 腳本只讀這兩個檔案、不依賴 Go binary 本身。Corpus 格式無硬性要求，任何目錄下的 <code>.md</code> 檔案都可索引。</p>
<h3 id="方法論">方法論</h3>
<ul>
<li><strong>Embedding</strong>：四方案共用同一組 embedding（從 Go index 載入），排除 embedding model 差異</li>
<li><strong>Query</strong>：同一句 query（&ldquo;RAG storage 選型&rdquo;），跑 5 次取 median</li>
<li><strong>Ingest 時間</strong>：只計 storage 操作（不含 embedding），Go 方案含 embedding 不可分離故標 —</li>
<li><strong>環境</strong>：macOS Apple Silicon、Python 3.12、Go 1.25</li>
</ul>
<h3 id="結果">結果</h3>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>Ingest（純 storage）</th>
          <th>Query（median）</th>
          <th>Index 大小</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go + flat file</td>
          <td>—</td>
          <td>151ms</td>
          <td>97.4 MB</td>
      </tr>
      <tr>
          <td>Python sqlite-vec</td>
          <td>2.9s</td>
          <td>19ms</td>
          <td>75.3 MB</td>
      </tr>
      <tr>
          <td>Python FAISS flat</td>
          <td>40ms</td>
          <td>1.8ms</td>
          <td>in-memory</td>
      </tr>
      <tr>
          <td>Python FAISS HNSW</td>
          <td>23.3s</td>
          <td>0.5ms</td>
          <td>in-memory</td>
      </tr>
  </tbody>
</table>
<h3 id="三個關鍵發現">三個關鍵發現</h3>
<p><strong>延遲瓶頸在 I/O 和實作、不在演算法</strong>。Go flat file 的 151ms 裡、cosine 計算約 50ms、file I/O 約 100ms。FAISS flat 用 numpy BLAS 做同樣的 brute-force cosine、純計算 1.8ms — 計算層差約 28 倍（Go pure loop vs BLAS 向量化指令），加上 I/O 後端到端差 84 倍。</p>
<p><strong>HNSW 的 query 加速在此規模 ROI 低</strong>。FAISS HNSW query 0.5ms vs flat 1.8ms、每次省 1.3ms。但 HNSW build 要 23.3s。每天查 100 次、要 179 天才回本 build 成本（23.3s ÷ 0.13s/天）。4.22 的判讀結論（「此規模 brute-force 夠用」）被數據驗證。</p>
<p><strong>sqlite-vec 的 19ms 是「DB overhead 換功能」</strong>。比 FAISS flat 慢 10 倍、但多了 SQL metadata filter、transaction 保護、disk persistence。對「需要 filter 但不想維運 server」的場景有意義。</p>
<h3 id="讀數據的注意事項">讀數據的注意事項</h3>
<ul>
<li>Go 151ms 含 file I/O（每次 query 重載 78MB）；如果做 daemon mode（常駐、載入一次），query 會降到 ~50ms（純 cosine + overhead）</li>
<li>FAISS 數字是 in-memory baseline（index 已載入），不含 index 檔案的載入時間</li>
<li>sqlite-vec 數字含 disk I/O（每次 query 從 SQLite 讀取），是 persistent storage 的真實代價</li>
<li>四方案都不含 Ollama embedding call 時間（~77ms），實際端到端延遲要加上</li>
</ul>
<h2 id="二次選型評估同結論理由鏈翻轉">二次選型評估：同結論、理由鏈翻轉</h2>
<p>Benchmark 數據出來後，80 倍效能差距讓原始選型（Go + flat file）受到質疑：「是否該換 Python + FAISS 或 sqlite-vec？」重新用 WRAP 框架評估，結論相同（維持 Go），但理由鏈完全不同。</p>
<h3 id="第一次選型的理由事前">第一次選型的理由（事前）</h3>
<p>「Go 工具鏈統一（mdtools 是 Go）+ 單 binary 分發（clone 後 <code>go build</code> 即可）。」</p>
<h3 id="實測推翻的前提">實測推翻的前提</h3>
<table>
  <thead>
      <tr>
          <th>原始假設</th>
          <th>實測</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Corpus ~1,500 chunks</td>
          <td>24,216 chunks（16 倍）</td>
      </tr>
      <tr>
          <td>Brute-force &lt; 10ms</td>
          <td>Go 151ms（I/O 瓶頸、不是計算）</td>
      </tr>
      <tr>
          <td>語言效能差異不大</td>
          <td>Go pure cosine vs numpy BLAS 差 80 倍</td>
      </tr>
      <tr>
          <td>「工具鏈統一」很重要</td>
          <td>mdtools（pre-commit、延遲敏感）跟 blogsearch（手動 CLI、每天幾次）使用模式不同，強制統一語言是用「同一棟建築」邏輯要求「不同用途房間用同一種建材」</td>
      </tr>
  </tbody>
</table>
<p>第一次的理由鏈幾乎全數被推翻。如果只看理由，應該換方案。</p>
<h3 id="第二次選型的理由事後">第二次選型的理由（事後）</h3>
<p>重新評估時加入三個第一次沒有的變數：</p>
<p><strong>端到端延遲 vs in-memory benchmark</strong>。84 倍是端到端的數字（Go 151ms 含 I/O vs FAISS 1.8ms in-memory）。但 FAISS 從 disk 載入 index 也要 ~100-200ms，端到端差距縮小到 2 倍。sqlite-vec 是唯一不需要全量載入的方案（disk-based HNSW、端到端 19ms），差距從「84 倍」變成「8 倍」。</p>
<p><strong>使用頻率決定 ROI</strong>。CLI 工具、每天 ~10 次手動 query。每次省 130ms（151 vs 19），一天省 1.3 秒。重寫投入 2-3 小時，回本時間 ≈ 19 年。注意這個計算對頻率極敏感：每天 100 次（如被整合進 MCP server 當 agent 工具）回本縮短到 1.9 年、每天 1000 次則 69 天。上方 HNSW ROI 也用每天 100 次計算 — 兩處頻率假設不同是因為比較對象不同（HNSW build 成本 vs 語言重寫成本），但讀者套到自己場景時應先確定自己的查詢頻率。</p>
<p><strong>Ingest 瓶頸在 Ollama API、跟語言無關</strong>。~4 分鐘的 ingest 裡、embedding API call 佔 95% 以上。換 Python 不會改善 ingest 速度。</p>
<h3 id="維持的理由是痛點不存在">維持的理由是「痛點不存在」</h3>
<p>維持 Go 的理由是<strong>改善的絕對收益太小、投入回不了本</strong> — 151ms 對 CLI 使用模式不構成痛點，與「Go 好」或「工具鏈統一」無關。</p>
<h3 id="這個翻轉的教學意義">這個翻轉的教學意義</h3>
<p>正確的結論配錯誤的理由是脆弱的。第一次 WRAP 的結論（選 Go）在當時是對的，但理由鏈（工具鏈統一、&lt; 10ms）被實測推翻後，如果不重新建立正確的理由鏈，下次環境變動（比如 blogsearch 從 CLI 變成 API server）就會用已失效的理由做出錯誤判斷。</p>
<p>判讀工具選型時，要區分三層：</p>
<ol>
<li><strong>結論</strong>：選什麼方案</li>
<li><strong>理由</strong>：為什麼選（可能被推翻）</li>
<li><strong>前提</strong>：理由依賴的假設（規模、使用模式、效能數字）</li>
</ol>
<p>前提變了、理由就要重建，即使結論沒變。寫進決策紀錄時，三層都要記 — 只記結論的話，下次重新評估時沒有判讀基礎。</p>
<p>區分「正當理由重建」跟「動機性推理」（先有結論再找理由）的判準：新理由是否在看到數據之前也能成立？本例的「130ms 對 CLI 不痛」在實測前也成立（CLI 使用模式本來就低頻），所以是正當重建。如果新理由只能在看到特定數字之後才講得通（如「151ms 剛好在 200ms 閾值內」——但閾值是事後設的），就是 post-hoc rationalization。</p>
<h3 id="觸發換方案的訊號">觸發換方案的訊號</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>門檻</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 延遲不可接受</td>
          <td>&gt; 500ms</td>
          <td>先加 mmap（最小改動）</td>
      </tr>
      <tr>
          <td>使用模式改變</td>
          <td>從 CLI 變 API server</td>
          <td>換 Python sqlite-vec</td>
      </tr>
      <tr>
          <td>查詢頻率跳增</td>
          <td>被整合進 MCP server / agent 工具</td>
          <td>評估 daemon mode 或換 sqlite-vec</td>
      </tr>
      <tr>
          <td>Corpus 規模跳增</td>
          <td>&gt; 50K chunks</td>
          <td>重跑 benchmark</td>
      </tr>
      <tr>
          <td>需要原生 metadata filter</td>
          <td>code filter 維護成本過高</td>
          <td>換 Python sqlite-vec</td>
      </tr>
  </tbody>
</table>
<h2 id="embedding-model-選型對應-412-constraint-優先序">Embedding model 選型（對應 4.12 constraint 優先序）</h2>
<p>選 <code>nomic-embed-text</code> 的理由鏈：</p>
<ol>
<li><strong>Ollama 原生支援</strong>：<code>ollama pull</code> 一行、不需要額外 Python library 或 API key</li>
<li><strong>體積小</strong>：274 MB、跟 chat model 共用記憶體不打架</li>
<li><strong>已有驗證基線</strong>：rag-demo 用同一個模型跑過 463 chunks、retrieval 命中率確認可用</li>
<li><strong>768 維 sweet spot</strong>：24K chunks × 768 dim × 4 bytes = 70.9 MB，brute-force 可行</li>
</ol>
<p>未來如果 CJK retrieval 品質不夠（目前可用但未做系統性評估），<code>multilingual-e5-large</code> 或 <code>bge-m3</code> 是備選。換模型只需改 <code>embed.go</code> 的 Model 變數 + 重新 <code>blogsearch ingest</code>（4.22 的「四層可替換」設計）。</p>
<h2 id="cjk-混合-chunking-觀察">CJK 混合 Chunking 觀察</h2>
<p>Blog 內容是繁體中文 + 英文術語混合。Chunking 策略沿用 rag-demo 的 paragraph-aware split（空白行切段、soft token cap 400）。</p>
<p>Token 估算用 <code>len(s) / 2</code> 的 heuristic（CJK 字元多算一次）。不精確但 chunking 只需要粗略估算。跟 tokenizer 精確計算的差異在 ±20%、對 chunking 品質影響小於 chunk 邊界選擇的影響。</p>
<p>實際觀察：24,216 chunks 的 retrieval 品質在語意搜尋場景（「哪些文章跟 retry 有關」「RAG storage 選型」）表現良好。keyword 精確搜尋場景（「找 RFC 7807」）表現較弱 — 這是 embedding-only retrieval 的已知限制（見 4.1 的語意 vs 字面相似度對比），未來可加 BM25 做 hybrid search。</p>
<h2 id="跟其他章節的對應">跟其他章節的對應</h2>
<table>
  <thead>
      <tr>
          <th>本案例的段落</th>
          <th>對應原理章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>選型過程</td>
          <td><a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 演化階梯 + 工程約束</a></td>
      </tr>
      <tr>
          <td>二次選型評估</td>
          <td><a href="/blog/llm/04-applications/vector-storage-engineering/" data-link-title="4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀" data-link-desc="RAG storage backend 選型：規模到哪個階段該從 in-memory 升級到 vector DB、dependency chain 如何收窄選項">4.22 同 corpus 實測比較</a></td>
      </tr>
      <tr>
          <td>Embedding 選型</td>
          <td><a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12 實務選型 constraint 優先序</a></td>
      </tr>
      <tr>
          <td>Chunking</td>
          <td><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 Chunking 策略對比</a></td>
      </tr>
      <tr>
          <td>Benchmark 方法論</td>
          <td><a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 Benchmarking 方法論</a></td>
      </tr>
      <tr>
          <td>Storage 格式設計</td>
          <td><a href="/blog/llm/04-applications/artifact-management/" data-link-title="4.10 衍生產物管理原理：什麼進 git、什麼不該" data-link-desc="LLM 應用的 source / derived / external 三類產物對應 git / build cache / registry、與 production 部署的 reproducibility / cost / share 取捨">4.10 衍生產物管理</a></td>
      </tr>
      <tr>
          <td>Retrieval 品質</td>
          <td><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 Retrieval 失敗根因</a></td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>JSONL 匯出與備份格式</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/jsonl-storage/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/jsonl-storage/</guid><description>&lt;p>Collector 的 day-one 主要儲存是 SQLite（見 &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進&lt;/a>）。JSONL（JSON Lines）保留作為匯出和備份格式 — 人類可讀、grep 友好、SQLite 資料庫損壞時可以從 JSONL 重建。Collector 提供 &lt;code>monitor export --format=jsonl&lt;/code> 指令匯出事件，也可以設定同步寫入 JSONL 作為即時備份。&lt;/p>
&lt;p>JSONL 的格式是每行一個 JSON 物件。作為匯出格式，核心優勢是工具鏈成熟 — &lt;code>grep&lt;/code> 過濾、&lt;code>jq&lt;/code> 結構化查詢、&lt;code>tail -f&lt;/code> 即時監控，不需要 database client。&lt;/p>
&lt;h2 id="一天一檔">一天一檔&lt;/h2>
&lt;p>事件按日期分檔：&lt;code>events-2026-06-19.jsonl&lt;/code>、&lt;code>events-2026-06-20.jsonl&lt;/code>。每天零點（或 UTC 日期變更時）切換到新檔案。&lt;/p>
&lt;p>一天一檔的好處：&lt;/p>
&lt;p>&lt;strong>時間範圍查詢直接對應到檔案&lt;/strong>。查「昨天的 error」只需要讀一個檔案，不需要掃描整個資料集。&lt;/p>
&lt;p>&lt;strong>保留策略按檔案操作&lt;/strong>。保留 30 天的資料 = 刪除 30 天前的檔案。不需要 database 的 TTL 機制或 partition pruning。&lt;/p>
&lt;p>&lt;strong>備份和搬移按檔案操作&lt;/strong>。rsync 一個目錄就完成備份；搬移特定日期的資料 = 搬移對應檔案。&lt;/p>
&lt;p>一天一檔的風險是單日資料量過大時，單一檔案的 grep 查詢會變慢。自用工具場景下，單日事件量通常在數千到數萬筆，檔案大小在 MB 級，grep 查詢在秒級完成。當單日事件量超過百萬筆時，需要考慮演進到更適合的儲存方案（見 &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進&lt;/a>）。&lt;/p>
&lt;h2 id="append-only-寫入">Append-only 寫入&lt;/h2>
&lt;p>JSONL 的寫入模式是 append-only — 新事件追加到檔案尾端，已寫入的事件不修改。&lt;/p>
&lt;p>Append-only 的操作特性：&lt;/p>
&lt;p>&lt;strong>寫入不需要鎖&lt;/strong>。&lt;code>os.OpenFile&lt;/code> 用 &lt;code>O_APPEND&lt;/code> flag 開啟，OS 保證每次 write 是 atomic 的（在 write size 不超過 &lt;code>PIPE_BUF&lt;/code> 的前提下，Linux 上是 4096 bytes）。單一事件的 JSON 通常在這個限制內。&lt;/p>
&lt;p>&lt;strong>不會損壞既有資料&lt;/strong>。寫入失敗（磁碟滿、程序崩潰）最多造成最後一行不完整，不影響前面的行。恢復時刪除最後一行的不完整片段即可。&lt;/p>
&lt;p>&lt;strong>支援 tail -f 即時監控&lt;/strong>。&lt;code>tail -f events-2026-06-19.jsonl | jq .&lt;/code> 即時顯示新寫入的事件，不需要額外的 streaming 機制。&lt;/p>
&lt;h2 id="gzip-壓縮">Gzip 壓縮&lt;/h2>
&lt;p>歷史檔案（非當天的）用 gzip 壓縮。JSON 文字的壓縮率通常在 80-90%（10MB 壓縮到 1-2MB）。&lt;/p>
&lt;p>壓縮策略：&lt;/p>
&lt;p>&lt;strong>當天的檔案不壓縮&lt;/strong>。保持 append-only 和 tail -f 的能力。&lt;/p>
&lt;p>&lt;strong>日期切換時壓縮前一天的檔案&lt;/strong>。用 cron job 或 collector 啟動時檢查，把 &lt;code>events-2026-06-18.jsonl&lt;/code> 壓縮為 &lt;code>events-2026-06-18.jsonl.gz&lt;/code>。&lt;/p>
&lt;p>&lt;strong>查詢壓縮檔用 zgrep / zcat&lt;/strong>。&lt;code>zgrep &amp;quot;error&amp;quot; events-2026-06-18.jsonl.gz&lt;/code> 不需要先解壓。&lt;/p>
&lt;h2 id="jsonl-備份的保留">JSONL 備份的保留&lt;/h2>
&lt;p>JSONL 備份檔的保留策略和 SQLite 主要儲存的分層保留獨立 — JSONL 是最後的重建來源，保留期限可以比 SQLite 中的原始事件更長。&lt;/p>
&lt;p>典型配置：JSONL 備份保留 30 天（即使 SQLite 中的原始事件只保留 7 天），提供 SQLite 損壞時的 30 天重建窗口。超過 30 天的 JSONL 壓縮檔用 cron job 清理：&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">find /var/lib/collector/events/ -name &lt;span class="s2">&amp;#34;events-*.jsonl.gz&amp;#34;&lt;/span> -mtime +30 -delete&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>主要儲存的查詢驅動分層保留策略見 &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Collector 的 day-one 主要儲存是 SQLite（見 <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a>）。JSONL（JSON Lines）保留作為匯出和備份格式 — 人類可讀、grep 友好、SQLite 資料庫損壞時可以從 JSONL 重建。Collector 提供 <code>monitor export --format=jsonl</code> 指令匯出事件，也可以設定同步寫入 JSONL 作為即時備份。</p>
<p>JSONL 的格式是每行一個 JSON 物件。作為匯出格式，核心優勢是工具鏈成熟 — <code>grep</code> 過濾、<code>jq</code> 結構化查詢、<code>tail -f</code> 即時監控，不需要 database client。</p>
<h2 id="一天一檔">一天一檔</h2>
<p>事件按日期分檔：<code>events-2026-06-19.jsonl</code>、<code>events-2026-06-20.jsonl</code>。每天零點（或 UTC 日期變更時）切換到新檔案。</p>
<p>一天一檔的好處：</p>
<p><strong>時間範圍查詢直接對應到檔案</strong>。查「昨天的 error」只需要讀一個檔案，不需要掃描整個資料集。</p>
<p><strong>保留策略按檔案操作</strong>。保留 30 天的資料 = 刪除 30 天前的檔案。不需要 database 的 TTL 機制或 partition pruning。</p>
<p><strong>備份和搬移按檔案操作</strong>。rsync 一個目錄就完成備份；搬移特定日期的資料 = 搬移對應檔案。</p>
<p>一天一檔的風險是單日資料量過大時，單一檔案的 grep 查詢會變慢。自用工具場景下，單日事件量通常在數千到數萬筆，檔案大小在 MB 級，grep 查詢在秒級完成。當單日事件量超過百萬筆時，需要考慮演進到更適合的儲存方案（見 <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a>）。</p>
<h2 id="append-only-寫入">Append-only 寫入</h2>
<p>JSONL 的寫入模式是 append-only — 新事件追加到檔案尾端，已寫入的事件不修改。</p>
<p>Append-only 的操作特性：</p>
<p><strong>寫入不需要鎖</strong>。<code>os.OpenFile</code> 用 <code>O_APPEND</code> flag 開啟，OS 保證每次 write 是 atomic 的（在 write size 不超過 <code>PIPE_BUF</code> 的前提下，Linux 上是 4096 bytes）。單一事件的 JSON 通常在這個限制內。</p>
<p><strong>不會損壞既有資料</strong>。寫入失敗（磁碟滿、程序崩潰）最多造成最後一行不完整，不影響前面的行。恢復時刪除最後一行的不完整片段即可。</p>
<p><strong>支援 tail -f 即時監控</strong>。<code>tail -f events-2026-06-19.jsonl | jq .</code> 即時顯示新寫入的事件，不需要額外的 streaming 機制。</p>
<h2 id="gzip-壓縮">Gzip 壓縮</h2>
<p>歷史檔案（非當天的）用 gzip 壓縮。JSON 文字的壓縮率通常在 80-90%（10MB 壓縮到 1-2MB）。</p>
<p>壓縮策略：</p>
<p><strong>當天的檔案不壓縮</strong>。保持 append-only 和 tail -f 的能力。</p>
<p><strong>日期切換時壓縮前一天的檔案</strong>。用 cron job 或 collector 啟動時檢查，把 <code>events-2026-06-18.jsonl</code> 壓縮為 <code>events-2026-06-18.jsonl.gz</code>。</p>
<p><strong>查詢壓縮檔用 zgrep / zcat</strong>。<code>zgrep &quot;error&quot; events-2026-06-18.jsonl.gz</code> 不需要先解壓。</p>
<h2 id="jsonl-備份的保留">JSONL 備份的保留</h2>
<p>JSONL 備份檔的保留策略和 SQLite 主要儲存的分層保留獨立 — JSONL 是最後的重建來源，保留期限可以比 SQLite 中的原始事件更長。</p>
<p>典型配置：JSONL 備份保留 30 天（即使 SQLite 中的原始事件只保留 7 天），提供 SQLite 損壞時的 30 天重建窗口。超過 30 天的 JSONL 壓縮檔用 cron job 清理：</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">find /var/lib/collector/events/ -name <span class="s2">&#34;events-*.jsonl.gz&#34;</span> -mtime +30 -delete</span></span></code></pre></div><p>主要儲存的查詢驅動分層保留策略見 <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a>。</p>
<h2 id="匯出的實作注意">匯出的實作注意</h2>
<p>匯出使用 streaming — 從 storage 逐筆讀取、逐行寫出，記憶體使用和事件總量無關。300 萬筆事件（約 900MB JSONL）不需要整批載入記憶體。</p>
<p>匯出的 JSONL 檔案包含事件明文（已 redaction 的欄位除外）。匯出後的檔案不受 collector 的存取控制保護，注意存放位置和存取權限。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Collector 的完整架構 → <a href="/blog/monitoring/04-collector/architecture/" data-link-title="Collector 架構" data-link-desc="HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine 的五段式處理鏈路">Collector 架構</a></li>
<li>查詢設計 → <a href="/blog/monitoring/04-collector/query-api/" data-link-title="查詢 API 設計" data-link-desc="CLI grep 友好的 JSONL 結構 &#43; HTTP 查詢 endpoint — 兩種查詢介面各自的適用場景和設計要點">查詢 API 設計</a></li>
<li>儲存撐不住時的演進 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
</ul>
]]></content:encoded></item><item><title>儲存上 IaC — S3 bucket 的安全與生命週期</title><link>https://tarrragon.github.io/blog/infra/05-core-services/storage-s3/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/05-core-services/storage-s3/</guid><description>&lt;p>S3 bucket 描述的是物件儲存的存在、命名、加密設定、版本控制與存取政策。bucket 本身沒有重建代價意義上的狀態問題 — 困難在它「裝的東西」。空 bucket 可隨時重建，裝了正式資料的 bucket 與 RDS 一樣不可隨意 destroy。把安全設定與生命週期規則寫進 IaC，讓這些防線成為可版本控制、可審查的程式碼，而非散落在 Console 的隱性設定。&lt;/p>
&lt;h2 id="bucket-的四道安全防線">bucket 的四道安全防線&lt;/h2>
&lt;p>一個 S3 bucket 在 IaC 裡至少要描述四個獨立資源，各自對應一道防線。Terraform 把它們拆成獨立資源是設計選擇 — 每道防線可以單獨 review、單獨調整、單獨追蹤變更歷史。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket&amp;#34; &amp;#34;assets&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;acme-${var.env}-assets&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> tags&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> { service&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> &amp;#34;cdn-origin&amp;#34;, env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">env&lt;/span> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_versioning&amp;#34; &amp;#34;assets&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">assets&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> versioning_configuration { status&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Enabled&amp;#34;&lt;/span> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_server_side_encryption_configuration&amp;#34; &amp;#34;assets&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">assets&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="k">rule&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="k">apply_server_side_encryption_by_default&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="n"> sse_algorithm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;aws:kms&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_public_access_block&amp;#34; &amp;#34;assets&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">assets&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="n"> block_public_acls&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="n"> block_public_policy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="n"> ignore_public_acls&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="n"> restrict_public_buckets&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="versioning">versioning&lt;/h3>
&lt;p>&lt;code>versioning&lt;/code> 讓物件的每次覆寫都保留前一版。誤覆寫時可以從版本歷史回退到前一個正確版本，誤刪時物件只是被標記為 delete marker、前一版仍然存在。這道防線對承載正式資料的 bucket 是必要的 — 沒有 versioning 的 bucket，一次誤操作就是資料永久遺失。&lt;/p>
&lt;p>versioning 開啟後會累積歷史版本的儲存量。搭配生命週期規則設定 &lt;code>noncurrent_version_expiration&lt;/code> 可以控制保留多少天的舊版本，避免儲存成本無限成長。這個天數是「保留能力」跟「儲存成本」的取捨 — 保留 30 天通常足以涵蓋發現問題到回退的時間差，受合規要求的資料則依規定延長。&lt;/p>
&lt;h3 id="server-side-encryption">server-side encryption&lt;/h3>
&lt;p>&lt;code>server_side_encryption&lt;/code> 確保物件在 S3 落地時加密。&lt;code>aws:kms&lt;/code> 使用 KMS 管理的金鑰，加密操作對應用程式透明 — 寫入時自動加密、讀取時自動解密，不需要改應用程式碼。選 &lt;code>aws:kms&lt;/code> 而非 &lt;code>AES256&lt;/code>（SSE-S3）的判斷依據是存取控制粒度：KMS 金鑰可以獨立設定 key policy，讓「誰能解密」這件事跟「誰能讀 bucket」分開管理，適合跨帳號或跨團隊的場景。&lt;/p>
&lt;p>使用 KMS 加密的 bucket 在跨帳號存取時，目標帳號除了要有 bucket 的讀取權限，還需要 KMS key 的 &lt;code>kms:Decrypt&lt;/code> 權限 — 少了這一步會拿到 &lt;code>AccessDenied&lt;/code>，錯誤訊息通常指向 S3 權限而非 KMS，排查時容易走錯方向。&lt;/p>
&lt;h3 id="public-access-block">public access block&lt;/h3>
&lt;p>&lt;code>public_access_block&lt;/code> 的四個布林全設 true，等於從 bucket 層級封死對外公開的可能。即使有人之後誤加了一條公開的 bucket policy 或 ACL，這個 block 也會擋住。它是一道兜底機制 — 擋的是設定錯誤，不是正常操作。&lt;/p>
&lt;p>靜態掃描工具（checkov / tfsec）會標記缺少 public access block 的 bucket。這正是&lt;a href="https://tarrragon.github.io/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程&lt;/a>裡自動化護欄的典型攔截對象 — 漏設的 bucket 會在 PR 階段被擋下，而非部署到線上才發現。&lt;/p></description><content:encoded><![CDATA[<p>S3 bucket 描述的是物件儲存的存在、命名、加密設定、版本控制與存取政策。bucket 本身沒有重建代價意義上的狀態問題 — 困難在它「裝的東西」。空 bucket 可隨時重建，裝了正式資料的 bucket 與 RDS 一樣不可隨意 destroy。把安全設定與生命週期規則寫進 IaC，讓這些防線成為可版本控制、可審查的程式碼，而非散落在 Console 的隱性設定。</p>
<h2 id="bucket-的四道安全防線">bucket 的四道安全防線</h2>
<p>一個 S3 bucket 在 IaC 裡至少要描述四個獨立資源，各自對應一道防線。Terraform 把它們拆成獨立資源是設計選擇 — 每道防線可以單獨 review、單獨調整、單獨追蹤變更歷史。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket&#34; &#34;assets&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="s2">&#34;acme-${var.env}-assets&#34;</span>
</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 class="n">  tags</span> <span class="o">=</span><span class="n"> { service</span> <span class="o">=</span><span class="n"> &#34;cdn-origin&#34;, env</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">env</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></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket_versioning&#34; &#34;assets&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">assets</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  versioning_configuration { status</span> <span class="o">=</span> <span class="s2">&#34;Enabled&#34;</span> }
</span></span><span class="line"><span class="ln">10</span><span class="cl">}
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket_server_side_encryption_configuration&#34; &#34;assets&#34;</span> {
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">assets</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="k">rule</span> {
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">apply_server_side_encryption_by_default</span> {
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">      sse_algorithm</span> <span class="o">=</span> <span class="s2">&#34;aws:kms&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    }
</span></span><span class="line"><span class="ln">18</span><span class="cl">  }
</span></span><span class="line"><span class="ln">19</span><span class="cl">}
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket_public_access_block&#34; &#34;assets&#34;</span> {
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="n">  bucket</span>                  <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">assets</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">  block_public_acls</span>       <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="n">  block_public_policy</span>     <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="n">  ignore_public_acls</span>      <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="n">  restrict_public_buckets</span> <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">}</span></span></code></pre></div><h3 id="versioning">versioning</h3>
<p><code>versioning</code> 讓物件的每次覆寫都保留前一版。誤覆寫時可以從版本歷史回退到前一個正確版本，誤刪時物件只是被標記為 delete marker、前一版仍然存在。這道防線對承載正式資料的 bucket 是必要的 — 沒有 versioning 的 bucket，一次誤操作就是資料永久遺失。</p>
<p>versioning 開啟後會累積歷史版本的儲存量。搭配生命週期規則設定 <code>noncurrent_version_expiration</code> 可以控制保留多少天的舊版本，避免儲存成本無限成長。這個天數是「保留能力」跟「儲存成本」的取捨 — 保留 30 天通常足以涵蓋發現問題到回退的時間差，受合規要求的資料則依規定延長。</p>
<h3 id="server-side-encryption">server-side encryption</h3>
<p><code>server_side_encryption</code> 確保物件在 S3 落地時加密。<code>aws:kms</code> 使用 KMS 管理的金鑰，加密操作對應用程式透明 — 寫入時自動加密、讀取時自動解密，不需要改應用程式碼。選 <code>aws:kms</code> 而非 <code>AES256</code>（SSE-S3）的判斷依據是存取控制粒度：KMS 金鑰可以獨立設定 key policy，讓「誰能解密」這件事跟「誰能讀 bucket」分開管理，適合跨帳號或跨團隊的場景。</p>
<p>使用 KMS 加密的 bucket 在跨帳號存取時，目標帳號除了要有 bucket 的讀取權限，還需要 KMS key 的 <code>kms:Decrypt</code> 權限 — 少了這一步會拿到 <code>AccessDenied</code>，錯誤訊息通常指向 S3 權限而非 KMS，排查時容易走錯方向。</p>
<h3 id="public-access-block">public access block</h3>
<p><code>public_access_block</code> 的四個布林全設 true，等於從 bucket 層級封死對外公開的可能。即使有人之後誤加了一條公開的 bucket policy 或 ACL，這個 block 也會擋住。它是一道兜底機制 — 擋的是設定錯誤，不是正常操作。</p>
<p>靜態掃描工具（checkov / tfsec）會標記缺少 public access block 的 bucket。這正是<a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>裡自動化護欄的典型攔截對象 — 漏設的 bucket 會在 PR 階段被擋下，而非部署到線上才發現。</p>
<p>定期用 CLI 掃一遍帳號內所有 bucket 的公開狀態，命中的每個 bucket 都要能回答「這個公開是故意的、理由是什麼」：</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">aws s3api list-buckets --query <span class="s1">&#39;Buckets[].Name&#39;</span> --output text <span class="p">|</span> tr <span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  <span class="k">while</span> <span class="nb">read</span> b<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nv">status</span><span class="o">=</span><span class="k">$(</span>aws s3api get-public-access-block --bucket <span class="s2">&#34;</span><span class="nv">$b</span><span class="s2">&#34;</span> 2&gt;/dev/null <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>      jq -r <span class="s1">&#39;.PublicAccessBlockConfiguration | to_entries[] | select(.value==false) | .key&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$status</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$b</span><span class="s2">: </span><span class="nv">$status</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="k">done</span></span></span></code></pre></div><h2 id="生命週期規則">生命週期規則</h2>
<p>儲存成本隨物件數量與保留時間線性成長。生命週期規則讓 IaC 描述「某類物件多久後搬到更便宜的儲存層、再多久後刪掉」，把成本控制變成可版本控制的設定。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket_lifecycle_configuration&#34; &#34;assets&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">assets</span><span class="p">.</span><span class="k">id</span>
</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 class="k">rule</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    id</span>     <span class="o">=</span> <span class="s2">&#34;archive-old-logs&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">    status</span> <span class="o">=</span> <span class="s2">&#34;Enabled&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    filter { prefix</span> <span class="o">=</span> <span class="s2">&#34;logs/&#34;</span> }
</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 class="k">transition</span> {
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">      days</span>          <span class="o">=</span> <span class="m">30</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">      storage_class</span> <span class="o">=</span> <span class="s2">&#34;GLACIER_IR&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    }
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">    expiration { days</span> <span class="o">=</span> <span class="m">365</span> }
</span></span><span class="line"><span class="ln">14</span><span class="cl">  }
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="k">rule</span> {
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">    id</span>     <span class="o">=</span> <span class="s2">&#34;cleanup-old-versions&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">    status</span> <span class="o">=</span> <span class="s2">&#34;Enabled&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">filter</span> {}
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">noncurrent_version_expiration</span> {
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="n">      noncurrent_days</span> <span class="o">=</span> <span class="m">30</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    }
</span></span><span class="line"><span class="ln">24</span><span class="cl">  }
</span></span><span class="line"><span class="ln">25</span><span class="cl">}</span></span></code></pre></div><h3 id="儲存層的取捨">儲存層的取捨</h3>
<p>S3 提供多個儲存層，各自在存取延遲與儲存單價之間取捨：</p>
<table>
  <thead>
      <tr>
          <th>儲存層</th>
          <th>存取延遲</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Standard</td>
          <td>毫秒級</td>
          <td>頻繁讀取的熱資料</td>
      </tr>
      <tr>
          <td>Standard-IA</td>
          <td>毫秒級</td>
          <td>不常存取但需要時立即讀到</td>
      </tr>
      <tr>
          <td>Glacier Instant</td>
          <td>毫秒級</td>
          <td>每季存取一次的歸檔</td>
      </tr>
      <tr>
          <td>Glacier Flexible</td>
          <td>分鐘到小時級</td>
          <td>稽核留存、年度查閱</td>
      </tr>
      <tr>
          <td>Glacier Deep Archive</td>
          <td>12 小時級</td>
          <td>法規留存、極少存取</td>
      </tr>
  </tbody>
</table>
<p><code>transition</code> 規則的日數設定要回推自業務需求：log 在除錯期間需要即時讀取（Standard），超過 30 天後幾乎只在事故回顧時才翻（Glacier Instant Retrieval 或 Standard-IA），超過一年可以淘汰或移到更深的歸檔層。把這些規則寫進 IaC，「為什麼 logs 只留一年」就是一個能在 PR 上被討論的決定，而非某人在 Console 點了不知道大家知不知道的設定。</p>
<h2 id="bucket-policy-與跨帳號存取">bucket policy 與跨帳號存取</h2>
<p>bucket policy 描述誰能對這個 bucket 做什麼操作，是 bucket 層級的存取控制。它跟 IAM policy 的差別在施力點：IAM policy 貼在身分上、定義「這個身分能做什麼」；bucket policy 貼在資源上、定義「這個 bucket 允許誰來」。兩者同時生效 — 一個請求要同時被身分端和資源端允許才會放行（除非有顯式 deny）。</p>
<p>跨帳號存取是 bucket policy 最常見的使用場景。一個帳號的 S3 bucket 要讓另一個帳號的 IAM role 讀取，需要兩端同時授權：bucket policy 允許那個 role 的 ARN，對方帳號的 IAM policy 也允許對這個 bucket 操作。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket_policy&#34; &#34;cross_account_read&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">assets</span><span class="p">.</span><span class="k">id</span>
</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 class="n">  policy</span> <span class="o">=</span> <span class="k">jsonencode</span><span class="p">(</span>{
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    Version</span> <span class="o">=</span> <span class="s2">&#34;2012-10-17&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">    Statement</span> <span class="o">=</span> <span class="p">[</span>{
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">      Sid</span>       <span class="o">=</span> <span class="s2">&#34;AllowCrossAccountRead&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">      Effect</span>    <span class="o">=</span> <span class="s2">&#34;Allow&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">      Principal</span> <span class="o">=</span><span class="n"> { AWS</span> <span class="o">=</span> <span class="s2">&#34;arn:aws:iam::111222333444:role/data-reader&#34;</span> }
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">      Action</span>    <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;s3:GetObject&#34;, &#34;s3:ListBucket&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">      Resource</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">assets</span><span class="p">.</span><span class="k">arn</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="s2">&#34;${aws_s3_bucket.assets.arn}/*&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    }<span class="p">]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  }<span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">}</span></span></code></pre></div><p>bucket policy 的常見陷阱是 <code>Principal: &quot;*&quot;</code> — 允許任何人存取。這跟 security group 的 <code>0.0.0.0/0</code> 是同一類風險。除了做為 CloudFront Origin Access Control（OAC）的配合設定，幾乎沒有合理場景需要把 Principal 設成 wildcard。checkov 的 <code>CKV_AWS_70</code> 規則專門攔這個。</p>
<p>把 bucket policy 寫進 IaC 的好處是每一條授權都有 PR 紀錄 — 誰在什麼時候加了一條跨帳號存取、為什麼加、reviewer 同意了沒有。散落在 Console 的 bucket policy 沒有這些追蹤，某天發現一條不認得的授權時，只能去翻 CloudTrail 猜它是什麼時候加的。</p>
<h2 id="事件通知">事件通知</h2>
<p>S3 事件通知讓 bucket 在物件被建立、刪除或還原時，自動觸發下游處理 — 寫入後自動縮圖、上傳後自動掃毒、刪除後自動通知。這些觸發關係寫進 IaC，讓「這個 bucket 會觸發什麼」成為可查詢的事實，而非散落在 Console 的隱性接線。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_s3_bucket_notification&#34; &#34;assets&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  bucket</span> <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">assets</span><span class="p">.</span><span class="k">id</span>
</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 class="k">lambda_function</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    lambda_function_arn</span> <span class="o">=</span> <span class="k">aws_lambda_function</span><span class="p">.</span><span class="k">thumbnail</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">    events</span>              <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;s3:ObjectCreated:*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    filter_prefix</span>       <span class="o">=</span> <span class="s2">&#34;uploads/&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    filter_suffix</span>       <span class="o">=</span> <span class="s2">&#34;.jpg&#34;</span>
</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">}
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_lambda_permission&#34; &#34;allow_s3&#34;</span> {
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">  statement_id</span>  <span class="o">=</span> <span class="s2">&#34;AllowS3Invoke&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">  action</span>        <span class="o">=</span> <span class="s2">&#34;lambda:InvokeFunction&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">  function_name</span> <span class="o">=</span> <span class="k">aws_lambda_function</span><span class="p">.</span><span class="k">thumbnail</span><span class="p">.</span><span class="k">function_name</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">  principal</span>     <span class="o">=</span> <span class="s2">&#34;s3.amazonaws.com&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">  source_arn</span>    <span class="o">=</span> <span class="k">aws_s3_bucket</span><span class="p">.</span><span class="k">assets</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">}</span></span></code></pre></div><p>事件通知的兩個配置常被忽略。第一是權限：S3 要觸發 Lambda，Lambda 的 resource-based policy 必須允許 S3 呼叫它（上面的 <code>aws_lambda_permission</code>），少了這段 apply 會成功但事件不會觸發，除錯時不容易發現。第二是 filter：不設 prefix / suffix 的通知會對 bucket 裡每一個物件操作都觸發，包括生命週期搬遷產生的物件變動 — 流量遠超預期。用 filter 把觸發範圍收斂到需要處理的路徑與檔案類型。</p>
<p>事件通知也可以導向 SQS 或 SNS，適合需要非同步佇列處理或 fan-out 到多個消費者的場景。選擇依據是下游的消費模式：Lambda 適合輕量即時處理（毫秒級回應），SQS 適合需要 backpressure 和重試的批次處理，SNS 適合同一事件需要同時通知多個服務。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：checkov / tfsec 攔截缺少 public access block 或加密的 bucket</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：bucket 的 tagging 與成本歸因</li>
<li>→ <a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二：身分與憑證地基</a>：bucket policy 與 IAM policy 的權限模型交集</li>
</ul>
]]></content:encoded></item><item><title>模組四：Collector 設計</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/</guid><description>&lt;p>回答「收到的事件怎麼處理」。挑戰在 collector 端，不在 SDK 端。&lt;/p>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Collector 架構（HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> JSONL 匯出與備份格式（匯出格式、gzip 壓縮、備份保留）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 查詢 API 設計（CLI grep 友好 vs HTTP 查詢 endpoint）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Rule engine 設計（條件 → 動作 → 模板）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 規模演進：可插拔 Storage Backend（SQLite 預設 / PostgreSQL 觸發）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 功能分層與 Backend 選擇（SQLite 層 vs PostgreSQL 層的功能邊界）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> SQLite Backend 效能基準（寫入吞吐 / 查詢延遲 / 資源消耗的量化預期）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Ingestion Scaling（四層防線 — SDK 取樣 → Collector 背壓 → 水平擴展 → Queue 解耦）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 查詢消費模式（Debug / Alerting / 產品決策 / 安全審計 / 效能監控）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> DevOps Dashboard 設計&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Developer Dashboard 設計&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 中台 Dashboard 設計&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Container 部署設計（SQLite 在 container 中的 I/O 考量、volume mount、graceful shutdown）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 讀寫分離與查詢擴展（讀寫競爭辨識、Read Replica、預聚合、CQRS 判讀訊號）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 端到端資料完整性（資料損失地圖、完整性指標、被自己 SDK DDoS 的防護）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Error Fingerprint 與去重分群（fingerprint 演算法、message normalization、error_groups 表）&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 01 資料庫&lt;/a>：PostgreSQL backend 的資料庫設計、&lt;a href="https://tarrragon.github.io/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 邊界">State Ownership 與 Query Boundary&lt;/a>&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">backend 04 觀測查詢設計&lt;/a>：觀測領域的讀取路徑設計、CQRS 特化應用&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">backend 09 效能容量&lt;/a>：高併發寫入 / 大資料查詢的效能挑戰&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/" data-link-title="模組三：流量管控" data-link-desc="收到的流量超過處理能力時怎麼辦 — 背壓、rate limit、熔斷、bulkhead 四種防護機制">DevOps 流量管控&lt;/a>：背壓、rate limit、熔斷的基礎概念&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/devops/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">DevOps 突發流量&lt;/a>：突發流量分類、降級策略、queue 緩衝&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-monitoring/" data-link-title="斷網環境的監控與可觀測性" data-link-desc="Self-hosted 監控（Prometheus &amp;#43; Grafana）、離線 log 收集（Loki / ELK）、不能 phone home 的告警、NTP 時間同步">斷網環境的監控&lt;/a>：Collector 在斷網環境的部署方式——endpoint 改指 self-hosted backend、SDK 的 offline buffer 更重要&lt;/li>
&lt;li>實作 repo：tarrragon/monitor 的 collector/ + docs/challenges/（撞牆記錄）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「收到的事件怎麼處理」。挑戰在 collector 端，不在 SDK 端。</p>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> Collector 架構（HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine）</li>
<li><input checked="" disabled="" type="checkbox"> JSONL 匯出與備份格式（匯出格式、gzip 壓縮、備份保留）</li>
<li><input checked="" disabled="" type="checkbox"> 查詢 API 設計（CLI grep 友好 vs HTTP 查詢 endpoint）</li>
<li><input checked="" disabled="" type="checkbox"> Rule engine 設計（條件 → 動作 → 模板）</li>
<li><input checked="" disabled="" type="checkbox"> 規模演進：可插拔 Storage Backend（SQLite 預設 / PostgreSQL 觸發）</li>
<li><input checked="" disabled="" type="checkbox"> 功能分層與 Backend 選擇（SQLite 層 vs PostgreSQL 層的功能邊界）</li>
<li><input checked="" disabled="" type="checkbox"> SQLite Backend 效能基準（寫入吞吐 / 查詢延遲 / 資源消耗的量化預期）</li>
<li><input checked="" disabled="" type="checkbox"> Ingestion Scaling（四層防線 — SDK 取樣 → Collector 背壓 → 水平擴展 → Queue 解耦）</li>
<li><input checked="" disabled="" type="checkbox"> 查詢消費模式（Debug / Alerting / 產品決策 / 安全審計 / 效能監控）</li>
<li><input checked="" disabled="" type="checkbox"> DevOps Dashboard 設計</li>
<li><input checked="" disabled="" type="checkbox"> Developer Dashboard 設計</li>
<li><input checked="" disabled="" type="checkbox"> 中台 Dashboard 設計</li>
<li><input checked="" disabled="" type="checkbox"> Container 部署設計（SQLite 在 container 中的 I/O 考量、volume mount、graceful shutdown）</li>
<li><input checked="" disabled="" type="checkbox"> 讀寫分離與查詢擴展（讀寫競爭辨識、Read Replica、預聚合、CQRS 判讀訊號）</li>
<li><input checked="" disabled="" type="checkbox"> 端到端資料完整性（資料損失地圖、完整性指標、被自己 SDK DDoS 的防護）</li>
<li><input checked="" disabled="" type="checkbox"> Error Fingerprint 與去重分群（fingerprint 演算法、message normalization、error_groups 表）</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 01 資料庫</a>：PostgreSQL backend 的資料庫設計、<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 邊界">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 與資源治理">backend 04 觀測查詢設計</a>：觀測領域的讀取路徑設計、CQRS 特化應用</li>
<li>→ <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">backend 09 效能容量</a>：高併發寫入 / 大資料查詢的效能挑戰</li>
<li>→ <a href="/blog/devops/03-traffic-management/" data-link-title="模組三：流量管控" data-link-desc="收到的流量超過處理能力時怎麼辦 — 背壓、rate limit、熔斷、bulkhead 四種防護機制">DevOps 流量管控</a>：背壓、rate limit、熔斷的基礎概念</li>
<li>→ <a href="/blog/devops/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">DevOps 突發流量</a>：突發流量分類、降級策略、queue 緩衝</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-monitoring/" data-link-title="斷網環境的監控與可觀測性" data-link-desc="Self-hosted 監控（Prometheus &#43; Grafana）、離線 log 收集（Loki / ELK）、不能 phone home 的告警、NTP 時間同步">斷網環境的監控</a>：Collector 在斷網環境的部署方式——endpoint 改指 self-hosted backend、SDK 的 offline buffer 更重要</li>
<li>實作 repo：tarrragon/monitor 的 collector/ + docs/challenges/（撞牆記錄）</li>
</ul>
]]></content:encoded></item><item><title>模組五：核心服務上 IaC</title><link>https://tarrragon.github.io/blog/infra/05-core-services/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/05-core-services/</guid><description>&lt;p>地基就緒後，依「地基 → 上層」的順序把實際承載業務的服務寫進 IaC。前四個模組建立的身分、網路與環境分離是底層平面，這一層在它們之上描述資料庫、運算、儲存與入口 — 業務流量真正落地的地方。順序與依賴的表達方式決定了這層能不能被乾淨地重建、拆除與演進。&lt;/p>
&lt;h2 id="上核心服務的順序">上核心服務的順序&lt;/h2>
&lt;p>核心服務的部署順序由依賴方向決定：被依賴的先建，依賴別人的後建。網路與身分是幾乎所有上層服務的共同前置 — 資料庫要放進私有 subnet、運算要套用 IAM role 才能讀 S3、load balancer 要掛在公開 subnet 並引用 security group。這些底層平面若還沒成形，上層資源會在 apply 時因為找不到 subnet ID 或 role ARN 而失敗，或更糟，建在預設 VPC 裡繞過了所有隔離設計。&lt;/p>
&lt;p>把順序交給 IaC 工具的依賴圖自動推導，比人工排序可靠。當運算資源的定義引用了 subnet 與 security group 的資源屬性，Terraform 會解析出「subnet 先於運算」的邊，apply 時自動排程。人工維護一份「先做 A 再做 B」的清單會隨資源增加而失準，依賴圖則隨程式碼本身演進。&lt;/p>
&lt;p>順序失控的早期徵兆是：某個上層資源的定義裡寫了一串 hardcode 的 subnet ID 或 VPC ID，代表它沒有透過依賴圖連到底層平面。底層一旦重建、ID 改變，上層不會自動跟上，state 與雲端現實之間的不一致（即 drift）就此產生。把硬編碼的 ID 換成對底層資源屬性或 data source 的引用，順序才會回到工具掌控之內。&lt;/p>
&lt;h2 id="各類服務怎麼描述">各類服務怎麼描述&lt;/h2>
&lt;p>四類核心服務承擔不同責任，IaC 描述它們時關注的屬性也不同。共通原則是：描述服務的「身分與接線」，而非把每個執行期參數都塞進程式碼。&lt;/p>
&lt;p>&lt;strong>資料庫（RDS）&lt;/strong> 是這層裡最需要謹慎描述的資源，因為它持有無法重建的狀態。IaC 定義它的 instance class、引擎版本、所在的 subnet group（決定它落在哪些私有 subnet）、套用的 parameter group 與 security group。連線端點不要硬編碼，改用資源 output 暴露給上層運算引用。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_db_instance&amp;#34; &amp;#34;primary&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n"> identifier&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;app-prod-primary&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n"> engine&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;postgres&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> engine_version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;16.3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="n"> instance_class&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;db.r6g.large&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="n"> db_subnet_group_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_db_subnet_group&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">private&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="n"> vpc_security_group_ids&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">aws_security_group&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">db&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>運算（ECS / EKS）&lt;/strong> 描述的是業務程式碼的執行載體。重點屬性是它跑在哪些 subnet、套用哪個 task / pod 的 IAM role、掛到哪個 load balancer 的 target group，以及與容器映像版本解耦 — 映像 tag 通常由 CI/CD 在部署期注入，不寫死在 infra 程式碼裡。這層只描述「運算容量與接線」，實際跑什麼版本由部署流程決定，這個邊界讓 infra 變更與應用發布各走各的節奏。&lt;/p>
&lt;p>ECS 與 EKS 在這裡被併寫，但兩者的維運模型不同、存在實際選型：ECS 是受管的容器編排，控制平面由雲商代管、心智負擔低，接線概念貼近 AWS 原生資源；EKS 是受管的 Kubernetes，換來跨雲可攜的生態與更細的編排控制，代價是要承擔 Kubernetes 自身的運維面（升級、附加元件、RBAC）。團隊已有 Kubernetes 能力或需要其生態時 EKS 的成本才划算，否則 ECS 的低負擔通常是預設起點。IaC 描述的接線骨架相近，差異主要落在編排層的資源類型。&lt;/p>
&lt;p>運算到資料庫之間還有一段常被略過的接線：連線管理。無狀態運算水平擴張時，每個實例各自開連線，容易把資料庫的連線數打滿 — 出現「擴運算反而拖垮 DB」的訊號時，要引入連線池或受管的連線代理（如 RDS Proxy），把連線收斂後再進資料庫，這層也可寫進 IaC 並輸出端點給運算引用。當讀流量遠大於寫、且能容忍副本的複寫延遲時，read replica 是把讀請求導離主庫的下一步，運算端依讀寫分流引用不同端點。&lt;/p>
&lt;p>&lt;strong>儲存（S3）&lt;/strong> 描述的是 bucket 的存在、命名、加密設定、版本控制與存取政策。bucket 本身幾乎沒有重建代價意義上的狀態問題 — 困難在它「裝的東西」。空 bucket 可隨時重建，裝了正式資料的 bucket 與 RDS 一樣不可隨意 destroy。描述時把加密、public access block、生命週期規則寫進去，這些是安全與成本的預設防線。&lt;/p>
&lt;p>&lt;strong>入口（ALB）&lt;/strong> 描述流量進入系統的第一站。它定義 listener（監聽哪些 port 與協定）、target group（流量導向哪些運算後端）、health check 條件與 TLS 憑證。ALB 本身是 stateless 的 — 重建一個 load balancer 不會遺失資料，但會換掉它的 DNS 名稱，所以對外服務通常在它前面再掛一層穩定的 DNS 記錄。健康檢查的路徑與閾值是這裡最常被忽略的判讀點：閾值太寬鬆會把壞掉的後端留在輪替裡，太嚴格會在部署瞬間誤判健康的新實例。HTTPS listener 引用的 TLS 憑證也屬於這層的接線 — 憑證由 ACM 簽發與自動續期，IaC 用憑證資源描述它（涵蓋網域與驗證方式），再把憑證 ARN 接到 listener 上，讓「憑證存在、續期、掛載」整條鏈都進版本控制，而非在 Console 手動上傳一份會過期沒人盯的憑證。&lt;/p></description><content:encoded><![CDATA[<p>地基就緒後，依「地基 → 上層」的順序把實際承載業務的服務寫進 IaC。前四個模組建立的身分、網路與環境分離是底層平面，這一層在它們之上描述資料庫、運算、儲存與入口 — 業務流量真正落地的地方。順序與依賴的表達方式決定了這層能不能被乾淨地重建、拆除與演進。</p>
<h2 id="上核心服務的順序">上核心服務的順序</h2>
<p>核心服務的部署順序由依賴方向決定：被依賴的先建，依賴別人的後建。網路與身分是幾乎所有上層服務的共同前置 — 資料庫要放進私有 subnet、運算要套用 IAM role 才能讀 S3、load balancer 要掛在公開 subnet 並引用 security group。這些底層平面若還沒成形，上層資源會在 apply 時因為找不到 subnet ID 或 role ARN 而失敗，或更糟，建在預設 VPC 裡繞過了所有隔離設計。</p>
<p>把順序交給 IaC 工具的依賴圖自動推導，比人工排序可靠。當運算資源的定義引用了 subnet 與 security group 的資源屬性，Terraform 會解析出「subnet 先於運算」的邊，apply 時自動排程。人工維護一份「先做 A 再做 B」的清單會隨資源增加而失準，依賴圖則隨程式碼本身演進。</p>
<p>順序失控的早期徵兆是：某個上層資源的定義裡寫了一串 hardcode 的 subnet ID 或 VPC ID，代表它沒有透過依賴圖連到底層平面。底層一旦重建、ID 改變，上層不會自動跟上，state 與雲端現實之間的不一致（即 drift）就此產生。把硬編碼的 ID 換成對底層資源屬性或 data source 的引用，順序才會回到工具掌控之內。</p>
<h2 id="各類服務怎麼描述">各類服務怎麼描述</h2>
<p>四類核心服務承擔不同責任，IaC 描述它們時關注的屬性也不同。共通原則是：描述服務的「身分與接線」，而非把每個執行期參數都塞進程式碼。</p>
<p><strong>資料庫（RDS）</strong> 是這層裡最需要謹慎描述的資源，因為它持有無法重建的狀態。IaC 定義它的 instance class、引擎版本、所在的 subnet group（決定它落在哪些私有 subnet）、套用的 parameter group 與 security group。連線端點不要硬編碼，改用資源 output 暴露給上層運算引用。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_db_instance&#34; &#34;primary&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  identifier</span>             <span class="o">=</span> <span class="s2">&#34;app-prod-primary&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  engine</span>                 <span class="o">=</span> <span class="s2">&#34;postgres&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  engine_version</span>         <span class="o">=</span> <span class="s2">&#34;16.3&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">  instance_class</span>         <span class="o">=</span> <span class="s2">&#34;db.r6g.large&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">  db_subnet_group_name</span>   <span class="o">=</span> <span class="k">aws_db_subnet_group</span><span class="p">.</span><span class="k">private</span><span class="p">.</span><span class="k">name</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">  vpc_security_group_ids</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_security_group</span><span class="p">.</span><span class="k">db</span><span class="p">.</span><span class="k">id</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">}</span></span></code></pre></div><p><strong>運算（ECS / EKS）</strong> 描述的是業務程式碼的執行載體。重點屬性是它跑在哪些 subnet、套用哪個 task / pod 的 IAM role、掛到哪個 load balancer 的 target group，以及與容器映像版本解耦 — 映像 tag 通常由 CI/CD 在部署期注入，不寫死在 infra 程式碼裡。這層只描述「運算容量與接線」，實際跑什麼版本由部署流程決定，這個邊界讓 infra 變更與應用發布各走各的節奏。</p>
<p>ECS 與 EKS 在這裡被併寫，但兩者的維運模型不同、存在實際選型：ECS 是受管的容器編排，控制平面由雲商代管、心智負擔低，接線概念貼近 AWS 原生資源；EKS 是受管的 Kubernetes，換來跨雲可攜的生態與更細的編排控制，代價是要承擔 Kubernetes 自身的運維面（升級、附加元件、RBAC）。團隊已有 Kubernetes 能力或需要其生態時 EKS 的成本才划算，否則 ECS 的低負擔通常是預設起點。IaC 描述的接線骨架相近，差異主要落在編排層的資源類型。</p>
<p>運算到資料庫之間還有一段常被略過的接線：連線管理。無狀態運算水平擴張時，每個實例各自開連線，容易把資料庫的連線數打滿 — 出現「擴運算反而拖垮 DB」的訊號時，要引入連線池或受管的連線代理（如 RDS Proxy），把連線收斂後再進資料庫，這層也可寫進 IaC 並輸出端點給運算引用。當讀流量遠大於寫、且能容忍副本的複寫延遲時，read replica 是把讀請求導離主庫的下一步，運算端依讀寫分流引用不同端點。</p>
<p><strong>儲存（S3）</strong> 描述的是 bucket 的存在、命名、加密設定、版本控制與存取政策。bucket 本身幾乎沒有重建代價意義上的狀態問題 — 困難在它「裝的東西」。空 bucket 可隨時重建，裝了正式資料的 bucket 與 RDS 一樣不可隨意 destroy。描述時把加密、public access block、生命週期規則寫進去，這些是安全與成本的預設防線。</p>
<p><strong>入口（ALB）</strong> 描述流量進入系統的第一站。它定義 listener（監聽哪些 port 與協定）、target group（流量導向哪些運算後端）、health check 條件與 TLS 憑證。ALB 本身是 stateless 的 — 重建一個 load balancer 不會遺失資料，但會換掉它的 DNS 名稱，所以對外服務通常在它前面再掛一層穩定的 DNS 記錄。健康檢查的路徑與閾值是這裡最常被忽略的判讀點：閾值太寬鬆會把壞掉的後端留在輪替裡，太嚴格會在部署瞬間誤判健康的新實例。HTTPS listener 引用的 TLS 憑證也屬於這層的接線 — 憑證由 ACM 簽發與自動續期，IaC 用憑證資源描述它（涵蓋網域與驗證方式），再把憑證 ARN 接到 listener 上，讓「憑證存在、續期、掛載」整條鏈都進版本控制，而非在 Console 手動上傳一份會過期沒人盯的憑證。</p>
<h2 id="stateful-資源的特殊處理">stateful 資源的特殊處理</h2>
<p>stateful 資源的 IaC 描述要把「保護狀態」當成第一類需求，而非事後補上的選項。RDS 是典型 — 它的高可用、備份與還原能力全都能、也應該用程式碼描述，這樣保護策略本身就進入版本控制與審查流程，而非散落在某人手動點過的 Console 設定裡。</p>
<p>multi-AZ 用一個布林屬性開啟，背後是 RDS 在另一個可用區維護同步副本。它解的是可用性：主庫故障時 failover 到 standby，但這個切換有秒級到一兩分鐘的窗口而非零停機，期間連線會中斷重連。要先界定它的邊界，才不會把它當成超出職責的工具。standby 副本是熱備不可讀，所以 multi-AZ 不提供讀取擴展 — 要分攤讀流量得另開 read replica 或改用 multi-AZ cluster 形態。它也不防邏輯損壞：誤刪一張表或一筆錯誤的批次更新會同步複製到 standby，這類風險由 backup 與時間點還原（PITR）負責，與 multi-AZ 的可用性職責正交，兩者要分別配置。</p>
<p>backup 用保留天數與備份視窗描述，RDS 依此每日自動快照並保留交易日誌以支援還原到任意時間點。自動備份的保留上限是 35 天，更長的留存要靠手動快照或匯出到 S3 自行管理。下方 <code>backup_retention_period</code> 取 14 是以 RPO 與合規要求反推的結果 — 一般營運場景 14 天足以涵蓋「發現問題到決定還原」的時間差，受監理或需要更長追溯窗口的服務則往 30 天甚至接上手動快照保險。手動快照用獨立資源描述，常見於重大變更前的保險點。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_db_instance&#34; &#34;primary&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  multi_az</span>                   <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  backup_retention_period</span>    <span class="o">=</span> <span class="m">14</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  backup_window</span>              <span class="o">=</span> <span class="s2">&#34;03:00-04:00&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">  deletion_protection</span>        <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">  skip_final_snapshot</span>        <span class="o">=</span> <span class="kt">false</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">  final_snapshot_identifier</span>  <span class="o">=</span> <span class="s2">&#34;app-prod-final&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">}</span></span></code></pre></div><p>該在 review 攔下的訊號是：正式環境的 stateful 資源若 <code>backup_retention_period</code> 為 0 或 <code>deletion_protection</code> 為 false，代表狀態保護沒有寫進程式碼。把這些屬性視為正式資料庫的硬性下限，而非可調的偏好。</p>
<h2 id="stateful-與-stateless-的差異怎麼影響操作">stateful 與 stateless 的差異怎麼影響操作</h2>
<p>stateful 與 stateless 資源的根本差別在重建代價，這個差別會傳導到刪除保護與 drift 風險的處理方式。stateless 資源（ECS service、ALB、無狀態運算）重建只是換一組新實例，幾分鐘內恢復、沒有資料損失，所以它們可以被頻繁地 destroy 與 recreate，是 IaC 最擅長的對象。</p>
<p>stateful 資源（RDS、裝了資料的 S3、持久化 volume）重建意味著資料遺失或漫長的還原，代價可能是數小時的停機與不可逆的損失。這個差別帶來三個操作後果。第一，刪除保護是必要的：stateful 資源開啟 deletion protection，讓「不小心 destroy」需要先顯式關閉保護這一步，多一道人為確認。第二，state drift 的容忍度不同：stateless 資源的 drift 可以靠重建抹平，stateful 資源的 drift（例如有人手動改了 parameter group）要謹慎處理，因為 IaC 的「修正回程式碼狀態」動作可能觸發重啟或重建。第三，變更的審查強度不同：改動 stateful 資源的 plan 輸出要逐行看，特別警惕任何顯示為 <code>replace</code>（先刪後建）而非 <code>update in-place</code> 的項目 — 對資料庫而言這通常代表資料會被丟棄。</p>
<p>實務上把這個差別寫進流程：stateful 資源的變更走更嚴格的 PR review 與分階段套用，這部分的自動化護欄在「模組七：infra 走 PR 流程與自動化護欄」展開。</p>
<h2 id="服務之間的依賴怎麼表達">服務之間的依賴怎麼表達</h2>
<p>服務間依賴用 output 與 data source 表達，讓引用關係成為程式碼裡可追蹤的邊，而非靠人記憶的隱性約定。同一個 state 內，直接引用資源屬性即可建立依賴 — 運算資源引用資料庫的端點 output，IaC 自動推導出「資料庫先於運算」，也讓端點變更時上層自動取得新值。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">output</span> <span class="s2">&#34;db_endpoint&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  value</span> <span class="o">=</span> <span class="k">aws_db_instance</span><span class="p">.</span><span class="k">primary</span><span class="p">.</span><span class="k">endpoint</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">}</span></span></code></pre></div><p>跨 state（例如網路地基與核心服務分屬不同 Terraform state，呼應「模組四：環境分離與模組化」的拆分）時，下游用 data source 唯讀地讀取上游已建立的資源。下游查詢上游的 VPC 與 subnet，取得 ID 來放置自己的資源，而不複製貼上硬編碼的值。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">data</span> <span class="s2">&#34;aws_vpc&#34; &#34;main&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  tags</span> <span class="o">=</span><span class="n"> { Name</span> <span class="o">=</span> <span class="s2">&#34;app-prod&#34;</span> }
</span></span><span class="line"><span class="ln">3</span><span class="cl">}</span></span></code></pre></div><p>兩種方式的取捨在耦合與隔離之間。同 state 引用最直接、依賴圖最完整，但 state 越大、單次 apply 的爆炸半徑越大。跨 state 的 data source 把爆炸半徑切小、讓網路地基能獨立演進，代價是依賴關係跨越了 state 邊界、需要約定上游一定先 apply。判讀訊號是：若一份核心服務程式碼裡出現大量寫死的 ID，通常代表該用 data source 而沒用 — 這是日後上游重建時 drift 與 broken reference 的來源。把硬編碼的引用換成 data source，依賴關係才會在程式碼裡顯性化、可被工具與 review 看見。</p>
<p>服務都接上後，下一個關注點是讓它們可被觀測 — log 與 metric 與服務同生命週期建立，這部分在「模組六：可觀測性與 log 同生命週期」展開。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/05-core-services/deployment-order-database/" data-link-title="部署順序與資料庫上 IaC" data-link-desc="核心服務的依賴圖決定部署順序，資料庫作為第一批上層服務需要最謹慎的 IaC 描述 — 涵蓋 RDS 接線、連線管理、read replica 與端點暴露">部署順序與資料庫上 IaC</a></td>
          <td>依賴圖決定部署順序，RDS 接線、連線管理、read replica 與端點暴露</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/05-core-services/compute-ecs-eks/" data-link-title="運算平台上 IaC — ECS 與 EKS" data-link-desc="容器運算平台的 IaC 描述：ECS 與 EKS 選型、task definition 與映像版本解耦、IAM task role 分離、auto-scaling 策略">運算平台上 IaC — ECS 與 EKS</a></td>
          <td>ECS 與 EKS 選型、task definition 與映像版本解耦、IAM task role、auto-scaling</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/05-core-services/storage-s3/" data-link-title="儲存上 IaC — S3 bucket 的安全與生命週期" data-link-desc="S3 bucket 的加密、版本控制、公開存取封鎖、生命週期規則、bucket policy 與事件通知怎麼寫進 IaC，讓儲存的安全與成本防線可審查可追蹤">儲存上 IaC — S3 bucket 的安全與生命週期</a></td>
          <td>加密、版本控制、公開存取封鎖、生命週期規則、bucket policy 與事件通知</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/05-core-services/loadbalancer-alb/" data-link-title="入口上 IaC — ALB、TLS 與健康檢查" data-link-desc="Application Load Balancer 的 listener、target group、健康檢查閾值設計，以及用 ACM 把 TLS 憑證的簽發、驗證與掛載整條鏈寫進版本控制">入口上 IaC — ALB、TLS 與健康檢查</a></td>
          <td>listener、target group、健康檢查閾值設計、ACM 憑證與 DNS 別名</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/05-core-services/stateful-protection-dependency/" data-link-title="Stateful 資源保護與跨服務依賴表達" data-link-desc="stateful 資源的保護策略（multi-AZ、備份、刪除保護）、stateful 與 stateless 的操作差異，以及用 output 與 data source 表達服務間依賴">Stateful 資源保護與跨服務依賴表達</a></td>
          <td>multi-AZ 邊界、備份保留、刪除保護、stateful vs stateless 操作差異、output 與 data source</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/05-core-services/acm-tls-dns-setup/" data-link-title="ACM 憑證、DNS 與 HTTPS 設定" data-link-desc="從 Route 53 hosted zone 到 ACM 憑證申請、DNS 驗證、ALB HTTPS listener 與 HTTP 重導的完整設定流程">ACM 憑證、DNS 與 HTTPS 設定</a></td>
          <td>hosted zone、DNS 驗證、TLS listener、HTTP redirect、SAN 憑證、續期監控</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/05-core-services/ecs-fargate-cost-optimization/" data-link-title="ECS Fargate 成本分析與優化" data-link-desc="Fargate 的計價模型、與 EC2 launch type 的成本交叉點、Spot 與 Savings Plans 的折扣機制、task 規格的 rightsizing 方法，以及何時該切回 EC2">ECS Fargate 成本分析與優化</a></td>
          <td>Fargate vs EC2 成本比較、Fargate Spot、Savings Plans、task rightsizing</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend 模組五：部署平台</a>：PaaS / container 平台跑在這層之上</li>
<li>→ <a href="/blog/devops/" data-link-title="DevOps 實務指南" data-link-desc="負載平衡、水平擴展、流量管控、服務探活、容量規劃、高可用、突發流量、成本管理 — 服務營運的工程基礎">devops 實務指南</a>：這些服務上線後的運行期維運</li>
</ul>
]]></content:encoded></item><item><title>4.22 RAG storage 工程：從 pickle 到 vector database 的選型判讀</title><link>https://tarrragon.github.io/blog/llm/04-applications/vector-storage-engineering/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/vector-storage-engineering/</guid><description>&lt;p>做完 RAG proof-of-concept 後最常見的問題是「現在的 in-memory 方案什麼時候該換成 vector database」。RAG pipeline 的儲存方案是&lt;strong>工程選擇、不是概念要件&lt;/strong>。&lt;a href="https://tarrragon.github.io/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &amp;#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG 原理&lt;/a>定義的 retrieval + augmentation 二段式結構，跟 embedding 存在 pickle、flat file、SQLite、還是 Pinecone 無關 — 只要能「給一個 query vector，找到最相似的 chunk vectors」，retrieval 這一段就成立。&lt;/p>
&lt;p>本章整理 storage layer 的工程設計空間：什麼規模用什麼儲存、什麼訊號觸發升級、index 怎麼建怎麼更新、schema 怎麼設計、dependency chain 怎麼影響選型。全篇以一個約 2,700 篇 markdown（24K chunks）、Go 工具鏈的個人技術 blog 作為 running example（從 &lt;a href="https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &amp;#43; cosine retrieval &amp;#43; Ollama chat、validating 4.0 RAG 原理">pickle demo&lt;/a> 升級到 production 工具的過程）；Go-specific 的約束見「工程約束」段，Python 專案的路徑在各階段標示。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>本章涵蓋：&lt;/p>
&lt;ol>
&lt;li>RAG pipeline 的四個可替換層、判斷當前瓶頸落在哪一層。&lt;/li>
&lt;li>Corpus 規模跟使用模式對應的 storage backend 選擇。&lt;/li>
&lt;li>Index 的 build / update / rebuild 生命週期設計。&lt;/li>
&lt;li>ANN index 策略（HNSW / IVF / brute-force）的適用邊界。&lt;/li>
&lt;li>Storage 選型的 dependency 約束（語言生態、build chain、環境管理）。&lt;/li>
&lt;/ol>
&lt;h2 id="rag-pipeline-的四個可替換層">RAG pipeline 的四個可替換層&lt;/h2>
&lt;p>RAG 不是一個 monolithic 系統。從 query 進來到 augmented prompt 送進 LLM，經過四個獨立可替換的層：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>可替換選項範例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Chunking strategy&lt;/td>
 &lt;td>把 corpus 切成 retrieval 單位&lt;/td>
 &lt;td>fixed-size / recursive / heading-aware / AST-based&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Embedding model&lt;/td>
 &lt;td>把 chunk text 轉成向量&lt;/td>
 &lt;td>nomic-embed-text / bge-large / jina-v3&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Storage backend&lt;/strong>&lt;/td>
 &lt;td>存向量 + metadata、支援相似度查詢&lt;/td>
 &lt;td>pickle / flat file / FAISS / SQLite-vec / Pinecone&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retrieval algorithm&lt;/td>
 &lt;td>對 query vector 找 top-K 相似 chunk&lt;/td>
 &lt;td>brute-force cosine / HNSW / IVF / hybrid + rerank&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四層各自演化、各自有不同的升級時機。Chunking 跟 embedding model 影響 retrieval &lt;strong>品質&lt;/strong>（找到的東西對不對）；storage backend 跟 retrieval algorithm 影響 retrieval &lt;strong>效能&lt;/strong>（找的速度跟規模上限）。&lt;/p></description><content:encoded><![CDATA[<p>做完 RAG proof-of-concept 後最常見的問題是「現在的 in-memory 方案什麼時候該換成 vector database」。RAG pipeline 的儲存方案是<strong>工程選擇、不是概念要件</strong>。<a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG 原理</a>定義的 retrieval + augmentation 二段式結構，跟 embedding 存在 pickle、flat file、SQLite、還是 Pinecone 無關 — 只要能「給一個 query vector，找到最相似的 chunk vectors」，retrieval 這一段就成立。</p>
<p>本章整理 storage layer 的工程設計空間：什麼規模用什麼儲存、什麼訊號觸發升級、index 怎麼建怎麼更新、schema 怎麼設計、dependency chain 怎麼影響選型。全篇以一個約 2,700 篇 markdown（24K chunks）、Go 工具鏈的個人技術 blog 作為 running example（從 <a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">pickle demo</a> 升級到 production 工具的過程）；Go-specific 的約束見「工程約束」段，Python 專案的路徑在各階段標示。</p>
<h2 id="本章目標">本章目標</h2>
<p>本章涵蓋：</p>
<ol>
<li>RAG pipeline 的四個可替換層、判斷當前瓶頸落在哪一層。</li>
<li>Corpus 規模跟使用模式對應的 storage backend 選擇。</li>
<li>Index 的 build / update / rebuild 生命週期設計。</li>
<li>ANN index 策略（HNSW / IVF / brute-force）的適用邊界。</li>
<li>Storage 選型的 dependency 約束（語言生態、build chain、環境管理）。</li>
</ol>
<h2 id="rag-pipeline-的四個可替換層">RAG pipeline 的四個可替換層</h2>
<p>RAG 不是一個 monolithic 系統。從 query 進來到 augmented prompt 送進 LLM，經過四個獨立可替換的層：</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>責任</th>
          <th>可替換選項範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Chunking strategy</td>
          <td>把 corpus 切成 retrieval 單位</td>
          <td>fixed-size / recursive / heading-aware / AST-based</td>
      </tr>
      <tr>
          <td>Embedding model</td>
          <td>把 chunk text 轉成向量</td>
          <td>nomic-embed-text / bge-large / jina-v3</td>
      </tr>
      <tr>
          <td><strong>Storage backend</strong></td>
          <td>存向量 + metadata、支援相似度查詢</td>
          <td>pickle / flat file / FAISS / SQLite-vec / Pinecone</td>
      </tr>
      <tr>
          <td>Retrieval algorithm</td>
          <td>對 query vector 找 top-K 相似 chunk</td>
          <td>brute-force cosine / HNSW / IVF / hybrid + rerank</td>
      </tr>
  </tbody>
</table>
<p>四層各自演化、各自有不同的升級時機。Chunking 跟 embedding model 影響 retrieval <strong>品質</strong>（找到的東西對不對）；storage backend 跟 retrieval algorithm 影響 retrieval <strong>效能</strong>（找的速度跟規模上限）。</p>
<p>常見的認知混淆是把「RAG」跟「vector database」綁在一起。這個綁定在 production 規模可能合理（10M chunks 不用 vector DB 很難做），但在小規模場景會導致過度工程 — 1500 個 chunks 用 Pinecone 就像用 PostgreSQL 存 10 筆 config。</p>
<h2 id="storage-backend-的演化階梯">Storage backend 的演化階梯</h2>
<p>Storage backend 的選擇是<strong>規模驅動</strong>的工程決策。每個階段都能做 RAG，差別在效能、持久性、query 能力。以下規模閾值基於 768 維 embedding、單機常見配置的經驗判斷，切點依向量維度與硬體規格移動；實測數字（如 20 chunks/sec）另行標示：</p>
<h3 id="階段一in-memorypickle--python-list">階段一：In-memory（pickle / Python list）</h3>
<p>把所有 chunk embeddings 載入記憶體，brute-force 算 cosine similarity。</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">適用規模：&lt; 10K chunks
</span></span><span class="line"><span class="ln">2</span><span class="cl">延遲：cosine 計算 &lt; 2ms（numpy BLAS、in-memory）；file-based 實作加 I/O 載入時間
</span></span><span class="line"><span class="ln">3</span><span class="cl">持久性：pickle 檔、每次啟動重載
</span></span><span class="line"><span class="ln">4</span><span class="cl">優點：零 dependency、程式碼 &lt; 50 行、debug 容易
</span></span><span class="line"><span class="ln">5</span><span class="cl">限制：記憶體受限、無 metadata filter、無 incremental update</span></span></code></pre></div><p>本 blog 的 <a href="/blog/llm/01-local-llm-services/hands-on/rag-demo/" data-link-title="Hands-on：用 blog content 當 corpus 跑 RAG" data-link-desc="200 行 Python：embedding &#43; cosine retrieval &#43; Ollama chat、validating 4.0 RAG 原理">rag-demo</a> 就在這個階段：71 篇 markdown、463 chunks、pickle 儲存、22 秒索引、query &lt; 10ms。概念驗證完全夠用。</p>
<h3 id="階段二flat-filebinary-embedding-store">階段二：Flat file（binary embedding store）</h3>
<p>把 embeddings 存成 binary 格式（而非 Python pickle），配 JSON metadata index。跟階段一的差異是 <strong>language-agnostic persistence</strong> — 不綁定 Python 的 pickle 格式、Go / Rust / Node 都能讀。</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">適用規模：&lt; 10K chunks
</span></span><span class="line"><span class="ln">2</span><span class="cl">延遲：cosine 計算 &lt; 2ms；加 file I/O 載入（70MB vectors ≈ 150ms Go / &lt; 50ms mmap）
</span></span><span class="line"><span class="ln">3</span><span class="cl">持久性：binary file + metadata JSON、可 rebuild
</span></span><span class="line"><span class="ln">4</span><span class="cl">優點：跨語言、單檔案部署、不需要 DB server
</span></span><span class="line"><span class="ln">5</span><span class="cl">限制：brute-force O(n)、metadata filter 靠程式碼、schema 演化需 rebuild（換 embedding 模型要重建整個 index）、無 transaction 保護（binary 損毀靠 rebuild 復原）、每次 query 重載 file 是效能瓶頸</span></span></code></pre></div><p>Running example 的 blog 選了這個方案。驅動選擇的是<strong>工具鏈約束</strong>：該 blog 的核心工具是 Go（單 binary 分發的 lint / fmt 工具），用 pickle 就綁定 Python runtime、其他維護者 clone 後多一步環境設定（同規模下效能無差異）。Binary flat file 讓 Go 工具直接讀寫、維持單 binary 分發。Python 專案留在 pickle 完全合理，規模到 10K 再跳階段三 FAISS 更自然。</p>
<h3 id="階段三embedded-libraryfaiss--hnswlib--annoy">階段三：Embedded library（FAISS / HNSWLib / Annoy）</h3>
<p>引入 ANN（Approximate Nearest Neighbor）index，查詢從 O(n) 變成 O(log n)。</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">適用規模：10K - 100K chunks
</span></span><span class="line"><span class="ln">2</span><span class="cl">延遲：&lt; 5ms（HNSW sublinear）
</span></span><span class="line"><span class="ln">3</span><span class="cl">持久性：index 檔案、可 rebuild
</span></span><span class="line"><span class="ln">4</span><span class="cl">優點：不需要 server、嵌入應用 process
</span></span><span class="line"><span class="ln">5</span><span class="cl">限制：需要安裝 library（FAISS 有平台相依的 wheel）、index build 較慢</span></span></code></pre></div><p>升級訊號：brute-force latency 開始感覺到（&gt; 50ms）、或 corpus 大到記憶體載入太慢。1M chunks × 768 dim × 4 bytes = 3GB，載入開始有感。</p>
<h3 id="階段三piggyback-既有-dbpgvector--redis-vector">階段三½：Piggyback 既有 DB（pgvector / Redis vector）</h3>
<p>已有 PostgreSQL 或 Redis 的專案有一條跳板路徑：直接在既有 DB 加向量能力、不引入新 server。</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">適用規模：10K - 1M chunks（pgvector）、10K - 500K（Redis vector）
</span></span><span class="line"><span class="ln">2</span><span class="cl">延遲：&lt; 10ms（HNSW、同 DB process）
</span></span><span class="line"><span class="ln">3</span><span class="cl">持久性：DB 管理、有 transaction / WAL / backup
</span></span><span class="line"><span class="ln">4</span><span class="cl">優點：不增 server、SQL metadata filter 原生支援、既有維運流程直接沿用
</span></span><span class="line"><span class="ln">5</span><span class="cl">限制：DB 本身要夠大（向量索引佔額外記憶體）、效能跟 DB 負載共享</span></span></code></pre></div><p>升級訊號：已有 Postgres / Redis、需要 metadata filtering、但不想維運獨立 vector DB server。pgvector 讓「有 SQL 能力 + 有向量搜尋」在同一個 DB 完成；Redis vector（RediSearch）適合已有 Redis 且延遲敏感的場景。</p>
<p>這條路徑跟階段四的差異：階段四（Qdrant / Weaviate）是專用 vector DB、向量搜尋效能更高、但多一個 server 維運。Piggyback 路徑犧牲一些向量搜尋效能、換來零新增 server 的維運簡化。選擇取決於「向量搜尋是核心能力（階段四）、還是輔助功能（piggyback）」。</p>
<h3 id="階段四self-hosted-vector-databaseqdrant--weaviate--milvus">階段四：Self-hosted vector database（Qdrant / Weaviate / Milvus）</h3>
<p>獨立 server process，專精向量搜尋，支援 metadata filtering、incremental update、backup、replication。</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">適用規模：100K - 10M chunks
</span></span><span class="line"><span class="ln">2</span><span class="cl">延遲：&lt; 10ms（HNSW + 網路 overhead）
</span></span><span class="line"><span class="ln">3</span><span class="cl">持久性：server 管理、disk-based
</span></span><span class="line"><span class="ln">4</span><span class="cl">優點：metadata filter（SQL-like）、REST/gRPC API、可水平擴展
</span></span><span class="line"><span class="ln">5</span><span class="cl">限制：需要維運 server、佔用資源、增加系統複雜度</span></span></code></pre></div><p>升級訊號：需要 metadata filtering（「只搜 report/ 下的卡片」且頻率高）、需要多 process 並發 query、需要 incremental update 而非全量 rebuild。</p>
<p>典型場景是十人以上的團隊共用 RAG 知識庫：多人同時 query、文件隨 sprint 密集更新、需要按 project / team / access level 做 metadata filter。單人或小團隊的 side project 通常停在階段二或三就夠。回退路徑是「關掉 server、退回 embedded library」— 向量跟 metadata 仍在、只是失去 incremental update 跟 REST API。</p>
<h3 id="階段五hosted-saaspinecone--weaviate-cloud--qdrant-cloud">階段五：Hosted SaaS（Pinecone / Weaviate Cloud / Qdrant Cloud）</h3>
<p>由 vendor 管理的 vector database，免維運。</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">適用規模：&gt; 10M chunks、或不想維運
</span></span><span class="line"><span class="ln">2</span><span class="cl">延遲：10-50ms（加上網路 round trip）
</span></span><span class="line"><span class="ln">3</span><span class="cl">持久性：vendor 管理
</span></span><span class="line"><span class="ln">4</span><span class="cl">優點：免維運、自動擴展、SLA
</span></span><span class="line"><span class="ln">5</span><span class="cl">限制：cost、vendor lock-in、資料離開本地</span></span></code></pre></div><p>升級訊號：corpus 超過單機記憶體（10M+ chunks 的 HNSW index 含 graph overhead 可達數十 GB）、或團隊沒有 infra 維運能力。</p>
<p>典型場景是跨國 SaaS 產品的 knowledge base：文件數百萬、多語言、需要 geo-distributed 部署。此規模下 self-hosted 的維運成本（on-call、capacity planning、backup）可能高於 SaaS 訂閱。風險是 vendor lock-in — 切換 vendor 要 re-index 全量資料、migration 成本跟 corpus 大小成正比。回退計畫是保留 ingest pipeline 的 vendor-agnostic 部分（chunking + embedding），只替換 storage layer。</p>
<h3 id="階梯的核心判讀">階梯的核心判讀</h3>
<p>每階段的升級都帶來新的 dependency 跟維護成本。判讀「該不該升級」看三個訊號：</p>
<ol>
<li><strong>目前這個階段有具體痛點嗎？</strong> 沒有就不升級。</li>
<li><strong>升級解的是效能瓶頸還是功能缺口？</strong> 效能瓶頸先量測再決定；功能缺口（如 metadata filter）看使用頻率。</li>
<li><strong>升級引入的 dependency 成本能接受嗎？</strong> 單人 blog 加一個 server process 的維護成本跟十人團隊不同。</li>
</ol>
<p>常見路徑速查：Python 小型 side project 留在 pickle（階段一），規模到 10K 再上 FAISS（階段三）；Go 專案跳階段二（flat file）避免 Python dependency；已有 Postgres 的專案直接評估 pgvector（階段三½）；已有 Docker 的團隊直接評估階段四（vector DB container）。</p>
<p>常見誤解：「FAISS 跟 Pinecone 選哪個」— 兩者差在規模量級（FAISS 是嵌入式 library、適合 &lt; 100K；Pinecone 是 hosted SaaS、適合 &gt; 10M 或免維運），不是同層級的互斥選項。</p>
<h3 id="同-corpus-實測比較">同 corpus 實測比較</h3>
<p>以下是同一個 corpus（24,216 chunks、768 維、nomic-embed-text）在四種 storage 方案的實測結果（2026-07 macOS Apple Silicon）：</p>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>演化階段</th>
          <th>Ingest（純 storage）</th>
          <th>Query（median）</th>
          <th>Index 大小</th>
          <th>主要 dependency</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go + flat file</td>
          <td>階段二</td>
          <td>—</td>
          <td>151ms</td>
          <td>97.4 MB</td>
          <td>Go binary + Ollama</td>
      </tr>
      <tr>
          <td>Python sqlite-vec</td>
          <td>階段三½</td>
          <td>2.9s</td>
          <td>19ms</td>
          <td>75.3 MB</td>
          <td>Python + sqlite-vec</td>
      </tr>
      <tr>
          <td>Python FAISS flat</td>
          <td>階段三</td>
          <td>40ms</td>
          <td>1.8ms</td>
          <td>in-memory</td>
          <td>Python + faiss-cpu</td>
      </tr>
      <tr>
          <td>Python FAISS HNSW</td>
          <td>階段三</td>
          <td>23.3s</td>
          <td>0.5ms</td>
          <td>in-memory</td>
          <td>Python + faiss-cpu</td>
      </tr>
  </tbody>
</table>
<p>這張表揭露三個容易被理論估計遮蓋的事實：</p>
<p><strong>延遲的瓶頸在 I/O 和實作、不在演算法</strong>。Go flat file 的 151ms 裡，cosine 計算約 50ms、其餘約 100ms 是檔案載入（70MB vectors + 7MB metadata）。FAISS flat 用 numpy BLAS 做同樣的 brute-force cosine，純計算只要 1.8ms — 計算層差約 28 倍（Go pure loop vs BLAS 向量化指令），加上 I/O 差異後端到端差 84 倍。</p>
<p><strong>HNSW 的 query 加速在此規模 ROI 低，但原因要看對</strong>。FAISS HNSW query 0.5ms vs flat 1.8ms，每次查詢省 1.3ms；但 HNSW build 要 23.3s。如果每天查 100 次，要 179 天才回本 build 成本。在 10 萬+ chunks 規模這個比例會翻轉。</p>
<p><strong>sqlite-vec 的 19ms 是「DB overhead 換功能」的真實代價</strong>。比 FAISS flat 慢 10 倍，但多了 SQL metadata filter、transaction 保護、disk persistence — 不需要另起 server。這個 trade-off 在「需要 filter 但不想維運 server」的場景有意義。</p>
<h2 id="ann-index-策略">ANN index 策略</h2>
<p>Storage backend 到了階段三以上，需要選 ANN（Approximate Nearest Neighbor）index 策略。<a href="/blog/llm/knowledge-cards/vector-database/" data-link-title="Vector Database" data-link-desc="為高維向量 (embedding) 設計的儲存 &#43; 近似最近鄰 (ANN) 檢索系統：RAG 從 prototype 跨到 production 的關鍵元件">Vector database 卡</a>列了三種主流演算法，本段補充工程判讀。</p>
<h3 id="brute-forceexhaustive-search">Brute-force（exhaustive search）</h3>
<p>對 query vector 跟所有 stored vectors 算 cosine similarity，取 top-K。</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">時間複雜度：O(n × d)（n = chunk 數、d = 向量維度）
</span></span><span class="line"><span class="ln">2</span><span class="cl">精確度：100%（exact nearest neighbor）
</span></span><span class="line"><span class="ln">3</span><span class="cl">記憶體：n × d × 4 bytes（float32）
</span></span><span class="line"><span class="ln">4</span><span class="cl">適用：&lt; 10K chunks</span></span></code></pre></div><p>1500 chunks × 768 dim 的 brute-force，現代 CPU 做一次 cosine similarity sweep 大約 1-5ms。在這個規模，HNSW 的建 index 時間（秒級）反而比它省下的查詢時間（毫秒級）長。</p>
<h3 id="hnswhierarchical-navigable-small-world">HNSW（Hierarchical Navigable Small World）</h3>
<p>建多層隨機圖，查詢時從稀疏高層往密集低層跳，sublinear 找到近似最近鄰。</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">時間複雜度：O(log n × d)
</span></span><span class="line"><span class="ln">2</span><span class="cl">精確度：95-99%（approximate、可調 ef_search 參數換精度）
</span></span><span class="line"><span class="ln">3</span><span class="cl">記憶體：n × d × 4 bytes + graph overhead（通常 1.2-1.5x）
</span></span><span class="line"><span class="ln">4</span><span class="cl">Build 時間：O(n × log n)、比 brute-force 慢
</span></span><span class="line"><span class="ln">5</span><span class="cl">適用：10K - 10M chunks、記憶體充足</span></span></code></pre></div><p>HNSW 是目前 vector DB 的主流 index。工程取捨在兩個參數：<code>ef_construction</code>（build 精度、越高越慢但 graph 品質越好）跟 <code>ef_search</code>（query 精度、越高越慢但 recall 越高）。多數 vector DB 的預設值已經針對「recall &gt; 95%」調過。</p>
<h3 id="ivfinverted-file-index">IVF（Inverted File Index）</h3>
<p>先把向量 K-means 分群，query 時只搜最近的幾個群。</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">時間複雜度：O(n/k × d)（k = 群數、nprobe = 搜幾個群）
</span></span><span class="line"><span class="ln">2</span><span class="cl">精確度：依 nprobe、通常 90-98%
</span></span><span class="line"><span class="ln">3</span><span class="cl">記憶體：可以 disk-based（比 HNSW 省）
</span></span><span class="line"><span class="ln">4</span><span class="cl">Build 時間：K-means 收斂需要時間
</span></span><span class="line"><span class="ln">5</span><span class="cl">適用：&gt; 1M chunks、記憶體受限、可接受較低 recall</span></span></code></pre></div><p>IVF 在超大規模（10M+）的 disk-based 場景有優勢，實務常配 product quantization（PQ）壓縮向量換記憶體。PQ / scalar quantization 跟 index 演算法（HNSW / IVF）正交 — 是記憶體受限時的壓縮手段，可疊加在任一 index 上。消費級場景通常不需要 quantization。</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">Corpus 規模？
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── &lt; 10K chunks   → Brute-force（此規模無需再評估）
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── 10K - 100K     → HNSW（如果記憶體夠）或 brute-force（如果 latency 可接受）
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── 100K - 10M     → HNSW（主流）
</span></span><span class="line"><span class="ln">5</span><span class="cl">└── &gt; 10M          → IVF 或 HNSW + sharding</span></span></code></pre></div><p>規模是第一軸。兩個修正軸在同規模下改變選擇：</p>
<ul>
<li><strong>Dependency constraint</strong>（見「工程約束」段）：規模小但工具鏈排除某些 storage（如 Go 專案排除 CGo dependency）→ 從可行選項中選。</li>
<li><strong>Metadata filter 需求</strong>：規模小但高頻需要按 section / tag 過濾 → 跳過 embedded library、直接評估 vector DB 或 code filter。</li>
</ul>
<p>一個常見的過度工程信號：corpus 只有幾千筆但花時間調 HNSW 的 <code>ef_construction</code>。實測數據（24K chunks）：FAISS HNSW query 0.5ms vs flat 1.8ms、每次省 1.3ms，但 HNSW build 要 23.3s。每天查 100 次要 179 天回本 build 成本（23.3s ÷ 0.13s/天）。此規模的 brute-force 絕對延遲已在感知閾值下，HNSW 的優化收益趨近零。</p>
<p>判讀流程之外還有一個容易忽略的變數：<strong>實作語言的計算效能差異</strong>。同一個 brute-force cosine，numpy BLAS 做 24K × 768 只要 1.8ms，Go pure cosine 做同樣運算約需 50-80ms（不含 I/O）。選 storage 方案時如果估「brute-force &lt; 10ms」、前提是用了向量化計算的 library；pure Go / pure Python loop 會慢一到兩個數量級。</p>
<h2 id="index-生命週期">Index 生命週期</h2>
<p>Index 的 build / update / rebuild 流程影響日常維護成本。</p>
<h3 id="full-rebuild">Full rebuild</h3>
<p>每次從 corpus 全量重建 index：walk 所有檔案 → chunk → embed → store。</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">適用：corpus 小（&lt; 10K chunks）、更新頻率低（每週幾次）
</span></span><span class="line"><span class="ln">2</span><span class="cl">優點：邏輯最簡單、index 跟 corpus 保證一致
</span></span><span class="line"><span class="ln">3</span><span class="cl">成本：依 corpus 規模線性成長（本地 Ollama sequential embedding 約 100 chunks/sec、24K chunks ≈ 4 分鐘）</span></span></code></pre></div><p>Running example 的 blog 選 full rebuild：2,738 篇 markdown 產生 24K chunks，全量 ingest 在本地 Ollama 約 4 分鐘。每天變動 0-3 篇，rebuild 頻率跟 <code>git push</code> 對齊就夠。</p>
<h3 id="incremental-update">Incremental update</h3>
<p>只處理有變動的檔案：偵測 diff → 刪除舊 chunks → 重新 chunk + embed 變動檔 → 插入新 chunks。</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">適用：corpus 大（&gt; 10K chunks）、更新頻繁
</span></span><span class="line"><span class="ln">2</span><span class="cl">優點：只處理 delta、省 embedding API cost
</span></span><span class="line"><span class="ln">3</span><span class="cl">複雜度：需要 chunk ID 穩定（file path + chunk offset）、刪除 orphan</span></span></code></pre></div><p>Incremental update 的工程難點是 <strong>chunk ID 穩定性</strong>。如果 chunking 策略對同一個檔案的切法會因為上游內容變動而改變（例如段落感知 chunking，加一段就改變後續所有 chunk 邊界），「只更新變動的 chunk」就需要 diff 整個 chunk 序列，邏輯接近全量重建。</p>
<p>判讀「該不該做 incremental」：</p>
<ul>
<li>Embedding 是 cost 瓶頸嗎？本地 Ollama 的 embedding 幾乎免費（約 50ms/chunk、sequential）；cloud API（OpenAI text-embedding-3-small 約 $0.02/1M tokens、Cohere 類似）按 token 計費、corpus 大時差異顯著。</li>
<li>全量 rebuild 的時間能接受嗎？1500 chunks 在本地約 60-90 秒可以接受；15 萬 chunks 約 2 小時可能不行。</li>
<li>能容忍短暫不一致嗎？Full rebuild 期間 index 可能是舊版；incremental update 隨改隨更新。</li>
</ul>
<h3 id="rebuild-trigger">Rebuild trigger</h3>
<p>不管 full 或 incremental，都要決定「什麼觸發 rebuild」：</p>
<table>
  <thead>
      <tr>
          <th>Trigger 類型</th>
          <th>做法</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>手動</td>
          <td><code>blogsearch ingest</code> 手動跑</td>
          <td>個人工具</td>
      </tr>
      <tr>
          <td>Git hook</td>
          <td>pre-push 或 post-commit 自動 rebuild</td>
          <td>小團隊</td>
      </tr>
      <tr>
          <td>CI/CD</td>
          <td>push to main 後 CI job 跑 ingest</td>
          <td>多人協作</td>
      </tr>
      <tr>
          <td>File watcher</td>
          <td>inotify / fsevents 偵測 content/ 變動自動更新</td>
          <td>開發中即時回饋</td>
      </tr>
  </tbody>
</table>
<p>Trigger 跟團隊協作模式對齊：單人用手動；多人但 review cycle 長（每天幾次 push）用 Git hook 或 CI/CD；開發中密集寫作想即時看 retrieval 結果用 file watcher。Git hook 跟 CI/CD 的差異在 rebuild 跑在本地（hook）還是 server（CI）— 本地 rebuild 快（&lt; 2 分鐘）就用 hook、慢就推到 CI 避免 push 卡住。</p>
<p>本 blog 目前用手動 trigger — 維護者在寫新文章、需要查相關內容時跑 <code>blogsearch ingest</code>，日常使用頻率不高、不需要即時同步。</p>
<h2 id="schema-設計">Schema 設計</h2>
<p>每個 chunk 存的不只向量。至少有三類資料需要管理：</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">chunk = {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    vector:   float32[768],       // embedding
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    text:     string,             // 原始文字（generation 用）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    metadata: {                   // filtering + 溯源
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        source:    string,        // 來源檔案路徑
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        section:   string,        // 所屬 section（llm/ / backend/ / report/）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        title:     string,        // 文章標題
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        date:      string,        // 文章日期
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        tags:      []string,      // 文章 tags
</span></span><span class="line"><span class="ln">10</span><span class="cl">        chunk_idx: int,           // 該檔案內的第幾個 chunk
</span></span><span class="line"><span class="ln">11</span><span class="cl">    }
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><h3 id="metadata-filter-的設計取捨">Metadata filter 的設計取捨</h3>
<p>Metadata filter 是「在向量相似度之外加條件」：例如「只搜 report/ 下的卡片」「只搜 2026 年之後的文章」。</p>
<p>兩種實作路線：</p>
<p><strong>Code filter</strong>：先做 brute-force / ANN 取 top-N（N 大於最終需要的 K），再用程式碼 filter metadata，取 top-K。</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">優點：不需要 DB、flat file 就能做
</span></span><span class="line"><span class="ln">2</span><span class="cl">限制：filter 比例高時（如 90% 被 filter 掉）需要取很大的 N
</span></span><span class="line"><span class="ln">3</span><span class="cl">適用：filter 條件少、filter 比例低（&lt; 50%）</span></span></code></pre></div><p><strong>DB filter</strong>：在 vector DB 的 query 語法中直接加 metadata condition（如 Qdrant 的 <code>must</code> filter）。</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">優點：filter 在 index 層執行、效率高
</span></span><span class="line"><span class="ln">2</span><span class="cl">限制：需要 vector DB、schema 要先定好
</span></span><span class="line"><span class="ln">3</span><span class="cl">適用：filter 條件多、filter 比例高、query 頻繁</span></span></code></pre></div><p>本 blog 選 code filter：section 只有幾個值（llm / backend / report / work-log），filter 比例低，brute-force top-20 再 filter 到 top-5 就夠。</p>
<h3 id="hybrid-search-的-schema-考量">Hybrid search 的 schema 考量</h3>
<p><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG 原理</a>介紹了 <a href="/blog/llm/knowledge-cards/hybrid-search/" data-link-title="Hybrid Search" data-link-desc="把字面 retrieval（BM25）跟語意 retrieval（embedding）的結果用 RRF 等方法合併、補單一路線的盲點">hybrid search</a>（BM25 關鍵字精確匹配 + embedding 語意相似度的加權合併），在 storage 層的 schema 影響是：需要同時存<strong>原始文字</strong>（給 BM25）跟<strong>向量</strong>（給 embedding search）。</p>
<ul>
<li>In-memory / flat file：BM25 自己實作（或用 library），原始文字本來就存了。</li>
<li>Vector DB：多數支援 hybrid search（Qdrant 有 full-text index、Weaviate 有 BM25 + vector 合併查詢）。</li>
<li>SQLite-vec + FTS5：SQLite 原生支援 full-text search（FTS5），配 sqlite-vec 可以在同一個 DB 做 hybrid search。</li>
</ul>
<p>判讀「要不要 hybrid」：先只用 embedding search，retrieval 品質不夠再加 BM25。多數場景 embedding-only 已經夠用；keyword 精確匹配需求高的場景（如搜特定 error message、RFC 編號）才需要 BM25 補。</p>
<h2 id="工程約束dependency-chain-與-build-system">工程約束：dependency chain 與 build system</h2>
<p>Storage 選型不只看功能跟效能，還受<strong>工程約束</strong>影響 — 包括 dependency chain 跟實作語言的計算效能。以下用 Go 專案示範這兩類 constraint 的思考方式；Python / Docker / 前端專案的 constraint 不同、結論見「不同專案的 constraint 不同」段。</p>
<h3 id="case-studygo-專案為什麼不選-sqlite-vec">Case study：Go 專案為什麼不選 SQLite-vec</h3>
<p>SQLite-vec 是 SQLite 的 C extension，提供向量搜尋能力。功能上完全符合需求。但在 Go 生態裡，CGo（Go 呼叫 C 程式碼的橋接機制）引入額外代價：</p>
<table>
  <thead>
      <tr>
          <th>SQLite Go binding</th>
          <th>能用 sqlite-vec？</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>modernc.org/sqlite</code>（純 Go）</td>
          <td>不能</td>
          <td>純 Go 重寫的 SQLite 不支援載入 C extension</td>
      </tr>
      <tr>
          <td><code>mattn/go-sqlite3</code>（CGo binding）</td>
          <td>能</td>
          <td>需要 C compiler、交叉編譯困難、build 時間增加</td>
      </tr>
  </tbody>
</table>
<p>選 <code>mattn/go-sqlite3</code> 意味著：</p>
<ul>
<li>其他維護者 clone 後需要裝 C compiler（macOS 要 Xcode CLI tools、Linux 要 gcc）</li>
<li>CI/CD 需要配 CGo 環境</li>
<li>單 binary 分發的優勢消失（動態連結 libc）</li>
</ul>
<p>這些代價在大團隊可能值得，但對一個個人 blog 的工具來說，dependency chain 的複雜度超過功能收益。</p>
<h3 id="判讀-dependency-約束的反射">判讀 dependency 約束的反射</h3>
<p>每個 storage 選項都帶一條 dependency chain。評估時要問：</p>
<ol>
<li><strong>新維護者 clone 後要裝什麼？</strong> pip install / go build / docker pull / apt install？</li>
<li><strong>CI 要加什麼？</strong> C compiler / Python runtime / Docker image？</li>
<li><strong>哪些平台要支援？</strong> macOS / Linux / Windows？交叉編譯需求？</li>
<li><strong>runtime dependency 還是 build-time dependency？</strong> Runtime（要 server 跑著）的維護成本遠高於 build-time（build 完就不需要了）。</li>
</ol>
<p>本 blog 的 constraint 是：Go 單 binary、clone 後 <code>go build</code> 即可、不需要外部 server。這個 constraint 排除了 CGo dependency 跟任何 server-based 方案，把選項收窄到 flat file。代價是 Go pure cosine + file I/O 讓 query 延遲（151ms）比 Python FAISS（1.8ms）慢 80 倍 — 對 CLI 工具可接受，對高頻 API server 則是致命瓶頸。選型時把 dependency chain 跟計算效能一起評估，避免「dependency 輕但效能差」或「效能好但 dependency 重」的單軸判斷。</p>
<h3 id="不同專案的-constraint-不同">不同專案的 constraint 不同</h3>
<p>這個 constraint 是本 blog 的特定情境。其他專案的 constraint 可能完全不同：</p>
<ul>
<li>Python 生態的專案：pip install 是標準流程，但 FAISS 的 CPU/GPU wheel 有平台相依（M1 Mac 需要 <code>faiss-cpu</code> 特定版本、glibc 版本影響 Linux wheel），不是完全零 constraint。</li>
<li>已有 Docker 的專案：加一個 Qdrant container 看似 <code>docker-compose.yml</code> 多三行，但要考慮 image 體積（數百 MB）、記憶體分配、冷啟動時間、以及 CI 環境是否支援 Docker-in-Docker。</li>
<li>前端專案：WebAssembly 版 HNSW 可行但受 bundle size 跟瀏覽器記憶體上限約束，跟 backend storage 的 constraint 型態完全不同。</li>
</ul>
<p>Storage 選型沒有「最佳方案」— 只有在特定 constraint 下的最適方案。</p>
<h2 id="何時過時--何時不過時">何時過時 / 何時不過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>RAG pipeline 的四層可替換結構。</li>
<li>Storage 升級的判讀訊號（規模驅動、痛點驅動、不是技術驅動）。</li>
<li>Index 生命週期的 full rebuild vs incremental update 取捨。</li>
<li>Dependency chain 作為選型約束的思考框架。</li>
<li>ANN 策略的複雜度分析（brute-force O(n) vs HNSW O(log n) vs IVF O(n/k)）。</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 vector DB 的市場格局（Pinecone / Qdrant / Weaviate 的功能差異會持續變動）。</li>
<li>ANN library 的實作效能（新演算法可能比 HNSW 更好）。</li>
<li>語言生態的 binding 成熟度（Go 的 SQLite-vec 純 Go binding 可能出現）。</li>
<li>具體規模閾值（隨硬體進步、「brute-force 可行」的上限會提高）。</li>
</ul>
<h2 id="跟其他章節的關係">跟其他章節的關係</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>跟本章的分工</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/llm/04-applications/rag-principles/" data-link-title="4.1 RAG 原理：retrieval &#43; augmentation 模式" data-link-desc="為什麼模型需要外掛知識、語意相似 vs 字面相似、chunking 的本質取捨、retrieval 失敗的根本原因">4.1 RAG 原理</a></td>
          <td>定義 retrieval + augmentation 本質、本章處理 storage layer</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/rag-retrieval-enhancements/" data-link-title="4.2 RAG 檢索增強：query rewriting / HyDE / multi-step / context packing" data-link-desc="Query 端增強（rewriting / expansion / HyDE）、multi-step iterative retrieval、retrieve 後的 context packing（dedup / ordering / summarization）、adaptive retrieval：vanilla RAG 不夠時的下一層工具箱">4.2 RAG 檢索增強</a></td>
          <td>處理 retrieval algorithm 層的增強、本章處理 storage 層</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12 Embedding model</a></td>
          <td>處理向量怎麼生成（含實務選型 constraint 優先序）、本章處理向量怎麼存</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/artifact-management/" data-link-title="4.10 衍生產物管理原理：什麼進 git、什麼不該" data-link-desc="LLM 應用的 source / derived / external 三類產物對應 git / build cache / registry、與 production 部署的 reproducibility / cost / share 取捨">4.10 衍生產物管理</a></td>
          <td>Index 是 derived artifact、不進 git、用 manifest 描述</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/vector-database/" data-link-title="Vector Database" data-link-desc="為高維向量 (embedding) 設計的儲存 &#43; 近似最近鄰 (ANN) 檢索系統：RAG 從 prototype 跨到 production 的關鍵元件">Vector database 卡</a></td>
          <td>概念定義與 ANN 演算法摘要、本章補工程判讀</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步">下一步</h2>
<p>本章整理的是跨場景的 storage 工程原則。Running example 的 blog 基於這些原則選了「Go + flat file + brute-force」方案，完整實作過程（選型→重寫→效能優化→四方案 benchmark→二次選型評估）見 <a href="/blog/llm/04-applications/hands-on/blog-vector-search/" data-link-title="Case Study：Blog 語意搜尋從 pickle 到 production" data-link-desc="為 CLI 或個人工具選 RAG storage backend、或原始選型理由被 benchmark 推翻但結論不變時，如何區分結論、理由與前提">Case Study：Blog 語意搜尋從 pickle 到 production</a>。</p>
<p>想看 retrieval 品質不夠時的增強手段（query rewriting / HyDE / multi-step），回到 <a href="/blog/llm/04-applications/rag-retrieval-enhancements/" data-link-title="4.2 RAG 檢索增強：query rewriting / HyDE / multi-step / context packing" data-link-desc="Query 端增強（rewriting / expansion / HyDE）、multi-step iterative retrieval、retrieve 後的 context packing（dedup / ordering / summarization）、adaptive retrieval：vanilla RAG 不夠時的下一層工具箱">4.2 RAG 檢索增強</a>。想看 embedding 模型怎麼選（含工程 constraint 如何先砍選項再比品質）、怎麼判讀 MTEB 分數，回到 <a href="/blog/llm/04-applications/embedding-model-internals/" data-link-title="4.12 Embedding model 內部：訓練、選型、in-domain fine-tune" data-link-desc="Embedding model 怎麼訓練（contrastive learning &#43; hard negative mining）、怎麼挑（MTEB / 大小 / domain）、何時該自己 fine-tune">4.12 Embedding model 內部</a>。</p>
]]></content:encoded></item><item><title>Aurora Storage Architecture：quorum-based 分散式 log 與韌性即性能設計</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/storage-architecture/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/storage-architecture/</guid><description>&lt;p>Aurora 把 storage 從「block device + WAL on local disk」重寫成跨 AZ 分散式 log service、compute node 只負責 process query 跟 generate redo log records。這個設計直接決定 read replica、failover、backup 跟跨 AZ replication 的物理上限 — 不理解 storage layer 設計、就無法解釋為什麼 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &amp;#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix consolidation&lt;/a> 拿到 +75% 效能、為什麼 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &amp;#43;50% 不影響延遲">9.C4 DraftKings&lt;/a> replication lag 從 30 秒降到 10-30ms、為什麼 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered&lt;/a> 能同時把韌性跟性能當成單一目標。&lt;/p>
&lt;p>本文不是 Aurora overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&amp;#43;75% 效能改善的 production 證據">Aurora vendor 頁&lt;/a>）— 而是 storage-level 設計的實作層教學。覆蓋 quorum-based replication 的工程含義、「韌性即性能」frame 為什麼成立、OLTP workload 在 storage 設計下的讀寫雙峰錯位、跟容量規劃的判讀槓桿。&lt;/p>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>典型觸發場景：團隊從 RDS PostgreSQL / 自管 PostgreSQL 遷到 Aurora、看到「跨 AZ replication lag 從秒級降到毫秒級」、但讀文件「quorum」「4-of-6」「分散式 storage」訊息密集、不知道哪些設計決策要相信、哪些是 marketing 詞。&lt;/p>
&lt;p>讀者常見的具體疑問：&lt;/p>
&lt;ul>
&lt;li>「為什麼 Aurora 寫入比 RDS 還低、不是該因為跨 AZ network round-trip 而變慢？」&lt;/li>
&lt;li>「Storage layer 跟 compute layer 分離具體怎麼影響 backup、failover 跟 read replica？」&lt;/li>
&lt;li>「6 個 storage node 失去 2 個還能寫嗎？失去 3 個呢？」&lt;/li>
&lt;li>「Aurora 文件講『韌性』跟『性能』都用 storage 設計解釋、是同一件事還是兩件事？」&lt;/li>
&lt;/ul>
&lt;p>進一步問題：傳統工程文化把可靠性跟性能視為對立 — HA 投資（跨 AZ replication、failover 演練）通常被當成性能成本、不被視為性能來源。Aurora 設計反這個直覺、但讀者需要看到具體機制才能信。Standard Chartered case 揭露這個 frame 在受監管銀行業務（要求兩者同時達標）的價值；DraftKings 揭露具體數字（讀 &amp;lt; 1ms、寫 6ms）。&lt;/p>
&lt;h2 id="核心機制quorum-based-分散式-log">核心機制：quorum-based 分散式 log&lt;/h2>
&lt;p>Aurora storage 的 first-class concept 是 &lt;em>quorum 寫入 + 6-way 跨 AZ replication&lt;/em>。傳統 PostgreSQL primary 把 storage 跟 CPU / RAM 綁定、storage 擴容要換 instance、replication 在 compute 層做（streaming replication、logical replication）。Aurora 把 storage 拉到分散式 log service、6 個 storage node 各自獨立、application 看到的仍是 single primary SQL。&lt;/p></description><content:encoded><![CDATA[<p>Aurora 把 storage 從「block device + WAL on local disk」重寫成跨 AZ 分散式 log service、compute node 只負責 process query 跟 generate redo log records。這個設計直接決定 read replica、failover、backup 跟跨 AZ replication 的物理上限 — 不理解 storage layer 設計、就無法解釋為什麼 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix consolidation</a> 拿到 +75% 效能、為什麼 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> replication lag 從 30 秒降到 10-30ms、為什麼 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 能同時把韌性跟性能當成單一目標。</p>
<p>本文不是 Aurora overview（請看 <a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor 頁</a>）— 而是 storage-level 設計的實作層教學。覆蓋 quorum-based replication 的工程含義、「韌性即性能」frame 為什麼成立、OLTP workload 在 storage 設計下的讀寫雙峰錯位、跟容量規劃的判讀槓桿。</p>
<h2 id="問題情境">問題情境</h2>
<p>典型觸發場景：團隊從 RDS PostgreSQL / 自管 PostgreSQL 遷到 Aurora、看到「跨 AZ replication lag 從秒級降到毫秒級」、但讀文件「quorum」「4-of-6」「分散式 storage」訊息密集、不知道哪些設計決策要相信、哪些是 marketing 詞。</p>
<p>讀者常見的具體疑問：</p>
<ul>
<li>「為什麼 Aurora 寫入比 RDS 還低、不是該因為跨 AZ network round-trip 而變慢？」</li>
<li>「Storage layer 跟 compute layer 分離具體怎麼影響 backup、failover 跟 read replica？」</li>
<li>「6 個 storage node 失去 2 個還能寫嗎？失去 3 個呢？」</li>
<li>「Aurora 文件講『韌性』跟『性能』都用 storage 設計解釋、是同一件事還是兩件事？」</li>
</ul>
<p>進一步問題：傳統工程文化把可靠性跟性能視為對立 — HA 投資（跨 AZ replication、failover 演練）通常被當成性能成本、不被視為性能來源。Aurora 設計反這個直覺、但讀者需要看到具體機制才能信。Standard Chartered case 揭露這個 frame 在受監管銀行業務（要求兩者同時達標）的價值；DraftKings 揭露具體數字（讀 &lt; 1ms、寫 6ms）。</p>
<h2 id="核心機制quorum-based-分散式-log">核心機制：quorum-based 分散式 log</h2>
<p>Aurora storage 的 first-class concept 是 <em>quorum 寫入 + 6-way 跨 AZ replication</em>。傳統 PostgreSQL primary 把 storage 跟 CPU / RAM 綁定、storage 擴容要換 instance、replication 在 compute 層做（streaming replication、logical replication）。Aurora 把 storage 拉到分散式 log service、6 個 storage node 各自獨立、application 看到的仍是 single primary SQL。</p>
<p><strong>Storage layout</strong>：每個 storage segment 跨 3 AZ × 2 node、共 6 個 storage node。一個 cluster 的 storage 被切成多個 10GB segment、每個 segment 6-way 複製。</p>
<p><strong>Quorum 設定</strong>：</p>
<ul>
<li>Write quorum：4-of-6（4 個 storage node 確認寫入才算 commit）— 容忍 1 AZ 失效 + 1 node 失效仍能寫</li>
<li>Read quorum：3-of-6（讀 3 個 node 取最新版本）— 比 write 小、降低 read latency</li>
<li>算術不對稱：寫嚴讀鬆是設計選擇、不是 marketing — durability 由寫端保證、讀端可以放寬</li>
</ul>
<p><strong>Write path 跟傳統 PostgreSQL 的差異</strong>：</p>
<ul>
<li>PostgreSQL primary：寫 WAL 到 local disk + dirty page flush + 透過 streaming replication 推到 replica</li>
<li>Aurora compute node：只送 <em>redo log records</em> 到 storage、不送整個 page；storage node 自己 apply redo log 重建 page、自己 checkpoint、自己 backup</li>
<li>工程含義：compute node 寫量小、CPU 不被 dirty page flush 佔用、寫入路徑變短</li>
</ul>
<p><strong>「韌性即性能」frame</strong>（<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> 揭露）：</p>
<p>Aurora 把 HA 從 application-level（Patroni promotion + WAL catch-up）下推到 storage-level。設計含義是：storage 投資（6-way 跨 AZ replication）自動成為 read replica 的容量基底 — read replica 不需要 catch-up WAL、直接從共享 storage 讀、HA 預算同步轉成讀分流預算。</p>
<p>對 Standard Chartered 受監管銀行業務這代表：合規要求的 RPO / RTO 不能放棄、但業務也要求每秒 4000 TPS、兩者必須同時達成。傳統路徑要分別投資 HA（複雜的 streaming replication topology）跟性能（read replica catch-up tuning）、且兩個投資互相干擾。Aurora 讓 <em>同一份 storage 投資</em> 同時提供兩件事 — case「判讀」段第 2 點原話：「Aurora 的多 AZ storage + replica 同時提供性能（讀分流）跟韌性（故障切換）、達成 <em>韌性即性能</em> 的目標」。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>、<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication-lag</a>。</p>
<p><strong>跟通用 quorum 概念差在哪</strong>：Aurora quorum 是 <em>storage-level</em>（不是 application-level Cassandra 風格）、application 看到 single primary SQL、不用感知 quorum；vs Cassandra application 要選 consistency level（ONE / QUORUM / ALL）。</p>
<h2 id="oltp-workload-shape讀寫雙峰錯位">OLTP workload shape：讀寫雙峰錯位</h2>
<p>Aurora 設計的工程含義在 application 層落地時、要看 workload 形狀。<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 揭露一個 OLTP 容量規劃的典型 pattern。</p>
<p><strong>DraftKings 揭露的雙峰錯位</strong>（case「觀察」段最後一行原文）：「write workloads spike up significantly around payout events, but opening the app during the game also activates a lot of balance queries」— 比賽進行時是讀爆量（balance query）、payout event 時是寫爆量（ledger write）、兩個峰不在同一時刻。</p>
<p><strong>工程含義</strong>：</p>
<ul>
<li>讀寫資源規劃要分開、不能用「峰值總 TPS」單一數字規劃容量</li>
<li>讀峰拉 read replica 容量、寫峰靠 primary instance class 跟 commit batching、兩條路徑獨立預配</li>
<li>預估 headroom 也要分開：讀的 headroom 可以靠 auto-scale replica 接、寫的 headroom 要靠 primary 提前升 instance class（不能 auto-scale）</li>
</ul>
<p><strong>Application-level boundary</strong>：雙峰錯位是 <em>application 層</em> 拆讀寫 datasource 的決策訊號、storage layer 本身不解。Aurora 共享 storage 提供 lag 上限可預測（10-30ms）— 這是 read replica 變成「production-grade 可用」的前提、但讀寫分流要 application 端拆 read / write data source 才能落地。Storage 設計給的是「可預測的 lag 上限」、不是「自動讀寫分離」。</p>
<p><strong>跨 case 對照</strong>：</p>
<p><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> 揭露另一種雙峰 — 直播 + 投注 <em>兩種服務</em> 同時峰、不是同服務讀寫錯位。這兩種雙峰類型要分清楚：</p>
<ul>
<li>同服務讀寫錯位（DraftKings）：解法是 read / write data source 拆分、共享 Aurora cluster</li>
<li>跨服務雙峰（FanDuel）：解法是不同服務各自獨立擴容、betting 走 Aurora、streaming 走 CDN</li>
</ul>
<p>雙峰類型不同、容量規劃策略不同。</p>
<h2 id="step-by-step-配置--觀測">Step-by-step 配置 / 觀測</h2>
<p>Aurora storage 是 cluster-level、不暴露 segment-level config。讀者能影響的維度是 instance class、storage type、backup retention 跟 monitoring。</p>
<p><strong>Cluster 建立</strong>：</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">aws rds create-db-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --db-cluster-identifier my-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --engine aurora-postgresql <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --engine-version 15.5 <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --master-username admin <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --master-user-password <span class="s2">&#34;</span><span class="k">$(</span>aws secretsmanager get-secret-value --secret-id db-password --query SecretString --output text<span class="k">)</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --storage-type aurora-iopt1 <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --backup-retention-period <span class="m">7</span></span></span></code></pre></div><p>關鍵欄位：</p>
<ul>
<li><code>--storage-type aurora-iopt1</code>：Aurora I/O-Optimized、月費高 30% 但無 I/O 收費；write-heavy + scan-heavy workload 才划算</li>
<li><code>--storage-type aurora</code>（預設）：Standard storage、按 I/O 計費；read-light workload 划算</li>
<li><code>--backup-retention-period 7</code>：1-35 天、影響 PITR 範圍</li>
</ul>
<p><strong>觀測 storage 狀態</strong>：</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">aws rds describe-db-clusters <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --db-cluster-identifier my-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;DBClusters[0].{StorageType:StorageType,AllocatedStorage:AllocatedStorage,Status:Status}&#39;</span></span></span></code></pre></div><p><strong>CloudWatch metric</strong>（cluster-level）：</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">VolumeBytesUsed           # 當前 storage 用量、接近 128 TB 上限要警告
</span></span><span class="line"><span class="ln">2</span><span class="cl">VolumeReadIOPs            # storage 層讀 IOPS、判斷 I/O-Optimized ROI
</span></span><span class="line"><span class="ln">3</span><span class="cl">VolumeWriteIOPs           # storage 層寫 IOPS、跟 compute 層 WriteIOPS 對照
</span></span><span class="line"><span class="ln">4</span><span class="cl">AuroraVolumeBytesLeftTotal # 剩餘可用 storage</span></span></code></pre></div><p><strong>Performance Insights wait event</strong>：</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">db.IO.aurora_redo_log_flush   # quorum write 等待訊號、p99 &gt; 10ms 要看
</span></span><span class="line"><span class="ln">2</span><span class="cl">db.IO.aurora_storage_xx       # storage layer I/O 細節</span></span></code></pre></div><p><strong>驗證點</strong>：</p>
<ul>
<li>寫入 latency p99：PostgreSQL primary 1-3ms vs Aurora 3-6ms、跨 AZ network round-trip 是物理下界</li>
<li>Read latency p99：Aurora &lt; 1ms（從共享 storage 讀、不跨 AZ）</li>
<li>Storage autoscale event：128 TB 上限前自動 grow per 10GB</li>
</ul>
<p><strong>Rollback boundary</strong>：Aurora storage 是 cluster-level、無法回滾 storage 設計；唯一 rollback 是切回 RDS / 自管（走 migration playbook、不是配置層 rollback）。</p>
<h2 id="故障模式--邊界-case">故障模式 / 邊界 case</h2>
<h3 id="case-1誤以為-aurora-寫入一定比-postgresql-primary-快">Case 1：誤以為 Aurora 寫入一定比 PostgreSQL primary 快</h3>
<p>徵兆：團隊期待 Aurora 寫入比自管 PostgreSQL 快、實測 p99 寫入 latency 沒明顯改善、甚至小 row + 單筆 commit 場景 Aurora 反而慢。</p>
<p>原因：跨 AZ network round-trip 是 3-5ms 物理下界、4-of-6 quorum 至少要等 4 個 storage node ack、單筆小寫場景 local SSD primary 仍有 latency 優勢。Aurora 的寫入優勢在 <em>壓力下</em> 才顯現 — write throughput 高峰時 PostgreSQL primary 受限於 dirty page flush + WAL fsync + replica catch-up、Aurora 的 storage layer 各自獨立處理 redo log apply。</p>
<blockquote>
<p><strong>數字口徑</strong>：「跨 AZ round-trip 3-5ms」屬通用工程估算（光速下界 + AWS 區內 AZ 物理距離）、case 未直接量化、實際值依 region / AZ pair / instance 類型而異、要看 AWS 官方 latency table 或自家 benchmark 校正。下方 DraftKings 6ms 寫入是 case 揭露的 production reference、可作為對照基線。</p></blockquote>
<p>修：</p>
<ul>
<li>benchmark 要跑壓力測試、不能只測單筆 latency</li>
<li>寫入 latency 不是 Aurora 的核心賣點、是 <em>可預測的 read replica lag + 韌性</em> 才是</li>
<li>DraftKings 6ms 寫入是 production reference：跨 AZ quorum 的物理下界、不是 Aurora 慢</li>
</ul>
<h3 id="case-2az-level-outage-期間寫入-latency-spike">Case 2：AZ-level outage 期間寫入 latency spike</h3>
<p>徵兆：1 個 AZ 失效後、寫入 p99 從 6ms spike 到 30-50ms、application timeout 增加。</p>
<p>原因：失去 1 AZ 後 quorum 仍成立（4-of-6 → 用剩 4 個 node 寫）、但 storage node fault 期間需要等 timeout 才確認；單一 storage node 額外 fault 會把寫推到 timeout。Aurora 在 AZ outage 期間 <em>能寫</em>、但不是 <em>性能不變</em>。</p>
<p>修：</p>
<ul>
<li>監測 <code>AuroraVolumeBytesLeftTotal</code> 跟 storage IOPS 分布、AZ outage 期間自動切到剩餘 AZ</li>
<li>application 端做 retry + circuit breaker、不要假設寫入永遠 6ms</li>
<li>確認 cluster 至少跨 3 AZ deploy、單 AZ outage 才有 quorum 餘地</li>
</ul>
<h3 id="case-3io-optimized-費用誤判">Case 3：I/O-Optimized 費用誤判</h3>
<p>徵兆：team 看 Aurora I/O-Optimized「無 I/O 收費」直接切過去、月帳變高 25%、沒看到 ROI。</p>
<p>原因：Standard storage 按 I/O 收費、I/O-Optimized 月費比 Standard 高 30%。只有 <em>write-heavy + scan-heavy</em> workload（I/O 月費接近 instance 費用）才划算；read-light + write-light workload 反而吃虧。</p>
<p>修：</p>
<ul>
<li>先量測 baseline I/O：<code>VolumeReadIOPs + VolumeWriteIOPs × $0.20 per million I/O</code> vs Standard 月費</li>
<li>I/O 費用 &gt; instance 費用 30% 才切 I/O-Optimized</li>
<li>DraftKings 用 I/O-Optimized 是因為金融帳本 write-heavy + balance query scan-heavy、ROI 明顯</li>
</ul>
<h3 id="case-4storage-autoscale-假設">Case 4：Storage autoscale 假設</h3>
<p>徵兆：TRUNCATE / DROP 大表釋放 50% storage、但下月帳單沒回落。</p>
<p>原因：Aurora storage 自動 grow、但 <em>不自動 shrink</em>。已分配的 storage 持續計費、TRUNCATE / DROP 只釋放 logical space、physical storage 仍占用。要 shrink 必須走 logical migration（dump / restore 到新 cluster）。</p>
<p>修：</p>
<ul>
<li>大量 DROP 操作前先評估是否值得做 logical migration</li>
<li>用 partition + DETACH 而非 DROP TABLE、partition 可以單獨 archive</li>
<li>接受 storage 用量是 <em>peak watermark</em> 而非 <em>current usage</em></li>
</ul>
<h3 id="case-5replication-lag-誤解">Case 5：Replication lag 誤解</h3>
<p>徵兆：read replica lag 10-30ms 看起來夠快、application 假設 read-after-write consistency、用戶下注後立刻查 balance 偶發看到舊資料。</p>
<p>原因：10-30ms 是 <em>typical</em>、heavy write + slow query 期間可能秒級。Aurora 共享 storage 設計讓 lag <em>可預測</em>（不會像 PostgreSQL streaming replication unbounded）、但 <em>可預測</em> 不等於 <em>zero</em>。Read-after-write 場景仍需要 application 端處理。</p>
<p>修：</p>
<ul>
<li>用戶寫操作後 N 秒內走 primary（N 由 lag p99 決定、典型 100ms）</li>
<li>Aurora 提供 session pinning：寫完同 session 短期內走 primary</li>
<li>不能假設「Aurora replication lag 小到可以忽略」、要看 application 容忍度</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p><strong>核心 metric</strong>：</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">VolumeBytesUsed           # storage 用量、128 TB 上限預警
</span></span><span class="line"><span class="ln">2</span><span class="cl">AuroraReplicaLag          # replica lag、判斷讀寫分流可行性
</span></span><span class="line"><span class="ln">3</span><span class="cl">db.IO.aurora_redo_log_flush # quorum write 等待、storage 瓶頸訊號</span></span></code></pre></div><p><strong>Production reference number</strong>（<a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a> 揭露、case「觀察」段表格）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>DraftKings 在 Aurora MySQL 的數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>讀延遲</td>
          <td>&lt; 1 ms</td>
      </tr>
      <tr>
          <td>寫延遲</td>
          <td>6 ms</td>
      </tr>
      <tr>
          <td>Replication lag</td>
          <td>從 30 秒降到 10-30 ms</td>
      </tr>
  </tbody>
</table>
<p>這個 production reference 取代用「typical 3-5ms」籠統說法。讀寫 6x 差距是 OLTP 容量規劃槓桿 baseline — 寫延遲是 quorum 4-of-6 + 跨 AZ network round-trip 的物理下界、不是 storage 設計能再壓低。引用時要明示是 DraftKings production reference、不是 Aurora marketing。</p>
<p><strong>容量上限</strong>：</p>
<ul>
<li>128 TB / cluster（超過要拆 cluster、見 <a href="../read-replica-scaling/">Aurora read replica scaling</a> fleet 治理 SSoT）</li>
<li>15 read replica / region（<a href="../read-replica-scaling/">Aurora read replica scaling</a> 展開）</li>
<li>Storage 自動 grow per 10GB</li>
</ul>
<p><strong>跨 region replication</strong>：<a href="../global-database-multi-region/">Aurora Global Database</a> 用 <code>AuroraGlobalDBReplicationLag</code> 監測、&lt; 1 秒 typical。</p>
<p><strong>回路徑</strong>：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 抽 CloudWatch evidence、<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 判斷 storage-bound vs compute-bound。</p>
<h2 id="netflix-75-效能改善的根因">Netflix +75% 效能改善的根因</h2>
<p><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix consolidation</a> 案例揭露 storage 設計的具體效能含義。Netflix 把多套 RDBMS（PostgreSQL / MySQL / Oracle）統一到 Aurora、拿到 <em>up to 75%</em> 效能改善、-28% 成本。</p>
<p><strong>+75% 的根因</strong>：</p>
<ul>
<li>傳統 PostgreSQL primary 寫 WAL + dirty page flush + 透過 streaming replication 推到 replica</li>
<li>Compute 大量 CPU 用在 dirty page flush + replication encoding、不是用在 query processing</li>
<li>Aurora compute 只送 redo log records、storage 自己 apply page、自己 checkpoint</li>
<li>→ 同樣 instance class 下、Aurora compute 能處理更多 query</li>
</ul>
<p>這不是 marketing 的「分散式儲存讓效能提升」籠統說法、而是具體的 <em>compute 不再 flush dirty page</em>。</p>
<p><strong>scope warning（必明示、case 自帶警示原話）</strong>：</p>
<p>「effective 75% improvement 是跨多 workload 的最大改善幅度、不是『每個 workload 都 +75%』。實際每個 workload 改善幅度從 10% 到 75% 不等」（case「需要警惕」段第 1 點）。</p>
<p>引用 Netflix 時不能把 75% 套到單一 workload — 容量規劃要看自家 workload 形狀（write-heavy / read-heavy / scan-heavy）、預估改善幅度範圍而非單一數字。</p>
<h2 id="fleet-治理cross-link不展開">Fleet 治理（cross-link、不展開）</h2>
<p>Production scale 不是「單一巨型 Aurora cluster」而是 <em>fleet of clusters</em> — 5 case 揭露同一 frame：</p>
<ul>
<li>DraftKings 200 個獨立 cluster（按業務切分）</li>
<li>Netflix 多 cluster（微服務私有 store）</li>
<li>Standard Chartered 7 個 cluster（受監管市場 boundary）</li>
</ul>
<p>跨 case 合成的 fleet 拓樸 3 條 driver（business sharding / microservice ownership / 合規市場 boundary）跟「何時拆 cluster vs 加 replica」的判讀順序、SSoT 在 <a href="../read-replica-scaling/">Aurora read replica scaling</a> 邊界段。Storage 設計本身不解 fleet 邊界決策 — Aurora 解 single-cluster scaling（quorum / 共享 storage / 共享 backup）、但「拆幾個 cluster」是業務拓樸決策。</p>
<h2 id="邊界與整合--下一步">邊界與整合 / 下一步</h2>
<p><strong>Sibling deep articles</strong>：</p>
<ul>
<li><a href="../cross-az-failover-rto/">Aurora cross-AZ failover RTO</a> — storage 設計如何加速 failover（replica 不需要 catch-up）</li>
<li><a href="../read-replica-scaling/">Aurora read replica scaling</a> — 共享 storage 為什麼能養 15 replica + fleet 治理 SSoT</li>
<li><a href="../global-database-multi-region/">Aurora Global Database</a> — 跨 region storage replication 設計</li>
</ul>
<p><strong>Migration playbook</strong>：</p>
<ul>
<li><a href="../migrate-from-self-managed-pg-mysql/">PostgreSQL / MySQL → Aurora</a> — storage 設計差是 operational redesign 的核心 driver</li>
</ul>
<p><strong>1.x 章節互引</strong>：</p>
<ul>
<li><a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a> — quorum 寫入 vs single-primary transaction 邊界</li>
<li><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> — Aurora storage 是 single-region scaling、不是 distributed SQL</li>
</ul>
<p><strong>何時不用本文</strong>：single-region OLTP 用 RDS 仍足夠、storage architecture 細節不影響容量規劃時可跳過、看 <a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor overview</a> 即可。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor overview</a> — 服務定位、適用 / 不適用場景</li>
<li><a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">Quorum 卡片</a> — 概念基底</li>
<li><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag 卡片</a> — 對照通用 replication lag 模型</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a> — 本文遵循的 6 規格面寫作模板</li>
<li>官方：<a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.StorageReliability.html">Aurora storage architecture</a></li>
</ul>
]]></content:encoded></item></channel></rss>