以 TrendAI 為例 · 如何實現低成本高效率的
一年前,我們在這個舞台分享了一件事
那就是
以使用者為軸心的 AI Observability
每個推論、每個 token、每條 trace 都是使用者訊號。
1.0 的取樣與分庫架構,已經跟不上 AI First 時代的觀測需求。
怎麼做、踩了哪些坑、省了多少錢。
~14× 便宜 · 同時完整保留、不採樣 · 90 天保留 · 跨 trace 分析
全球 10 region prod 實際成本:S3 $11K + MSK $9K + 運算 $9K = $29K/月
去年講「為什麼要去 2.0」· 今年講「PB 級怎麼做」
raw OTLP 展開 ~37 TB/day → Parquet+ZSTD 存 ~2 TB/day · 壓縮 19× · 90 天滾動 ~0.6 PB 落地(全球實測)
Observability 2.0 = metrics + logs + traces · metrics 有 Thanos、logs 有 Loki 已夠好 · trace 最難、資料最大 → 我們先啃。今天只講 trace。
接下來每一頁,都在證明這三件事其中一件 —— 全用 TrendAI 的真實數字
# prod_values.yaml · Dynamic Allocation minExecutors: 3 maxExecutors: 8 executor: 8 cores · 16GB # 背壓三道閘 maxOffsetsPerTrigger: 180,000 minPartitions: 256 maxRecordsPerPartition: 2,000 processingTime: 180s
教訓:不追求秒級 · 換來成本可控 + Iceberg 寫入健康。
Kafka (MSK):64 partition · RF2 · 保留 ~1h · 每則 ~400 spans · offset 存 S3 checkpoint 不在 Kafka → Kafka UI 看不到 consumer lag
為什麼鎖單 AZ?跨 AZ 流量 $0.01/GB。
EMR on EKS 帶來什麼
⚠ 代價:單 AZ failure = streaming 停 · 恢復靠 checkpoint @ S3(多 AZ)
CREATE TABLE spans (...) USING iceberg PARTITIONED BY ( hours(start_time), is_valid_company, bucket(8, device_id), bucket(8, trace_id) ) TBLPROPERTIES ( 'target-file-size-bytes'='536870912' -- 512MB )
hours(start_time) — 時間範圍查詢 99% 跳過is_valid_company — 過濾 invalid / null tenantbucket(8, device_id) — CUJ 的關鍵,同 device 落同 bucketbucket(8, trace_id) — 單 trace 單點查詢直達,不掃整小時做這些 → us1 沒爆 · 維運用獨立 Helm chart 部署在不同 AZ(跟 streaming 分散風險)
資料一份不動 · 引擎隨時換 —— 這才是 Lakehouse 的真正威力
Grafana 已經會跟 Tempo 講話。不要重寫 Grafana —— 寫一個假 Tempo,背後接 Trino + Iceberg。
GET /api/traces/{traceID}GET /api/search · /api/search/tags⚠ 只 mimic API · 不 mimic 架構:Tempo Ingester 用 in-memory queue → RAM-bound;我們用 Kafka 當 buffer → disk 便宜、省一個元件。
# Trino row → OTLP protobuf async def get_trace(trace_id): rows = await trino.query(""" SELECT * FROM spans WHERE trace_id = ? AND start_time BETWEEN ? AND ? """, trace_id, t0, t1) return rows_to_otlp(rows) # Grafana 不知道、也不在乎背後是什麼
✅ Grafana 設定零修改 · ✅ 換掉儲存層上層無感 · ✅ 不買 Tempo SaaS
❌ 沒時間範圍
SELECT * FROM spans WHERE trace_id = 'abc...'; -- us1 planner 估: -- 掃 82.73 TB / 176 億 spans -- → coordinator OOM
✅ 加時間範圍
SELECT * FROM spans WHERE start_time BETWEEN ? AND ? AND trace_id = 'abc...'; -- partition + bucket prune -- 實讀 100 MB · CPU 1.1s
黃金法則:外層 query 也要 start_time,否則 prune 跑不掉
Columnar:SELECT 8 欄只抓那 8 根 column chunk,其餘連下載都不下載。
resource_attrs/span_attrs 是巢狀 rich payload(http 細節、自訂欄位)—— 全留的價值所在。不需要時不付錢、需要時只付那一欄。
| 查法 | Physical | CPU |
|---|---|---|
| 不走分區 | 82.73 TB | 會死 |
SELECT * 寬窗 | 935 MB | 52 s |
| 只選 8 欄 窄窗 | 100 MB | 1.1 s |
省的大頭是反序列化、不是 I/O(不碰巢狀 attr 就全免)· Parquet 不是 index:粗篩靠 partition、省讀靠 columnar,中間沒索引(刻意的)
跨 trace 看見 APM 看不到的 · 同時全球 $29K/月、便宜 ~14×
一個 Auth 服務 被 23 個服務依賴、8,600 萬次呼叫 · 這圖是一句 GROUP BY SQL 跑出來的,APM 採樣 1% 根本畫不出來
trace_id 生命週期。單次 API 成功 ≠ 使用者成功。device_id 串成 journey。看使用者真的成功了沒。一次 agent 安裝橫跨 5 條產品線 · 平均 12 條 trace · Tempo/Jaeger 做不到 · bucket(8, device_id) 把同 device 物理聚一起才划算
root cause 不是系統卡關,是 agent 生命週期 · APM 只看到「99.6%」一個數字 · 我們看到每一個沒走完的 agent 發生什麼事
Phase 1 · 每條 trace_id 一行
WITH session_matrix AS ( SELECT trace_id, ANY_VALUE(device_id) device_id, BOOL_OR(span_name LIKE '%onboarding%') step1, BOOL_OR(span_name = 'Completed') step6, ... FROM spans WHERE start_time BETWEEN ? AND ? GROUP BY trace_id )
Phase 2 · 每個 device_id 一行
SELECT device_id, array_join(array_agg(status ORDER BY time), ' → ') AS journey FROM session_matrix GROUP BY device_id -- dev-002 | A(OK) ← 漏斗破洞
bucket(8, device_id) 把同 device 物理聚一起 · za 實測:一條 journey 跨 12 條 trace · 延遲 p50 48s · p95 86s · p99 114s
對手:同規格 3 TB/day 估算(≈ 單一大區)· 我們:全球 10 region prod 實際帳單
| 指標 | Jaeger + ES | Tempo | Big Tracing on Lakehouse |
|---|---|---|---|
| 月成本 | ~$300–500K | ~$60–100K | ~$29K |
| 年成本 | ~$5M | ~$700K–1M | ~$348K |
| 採樣率 | 實務 1–10% | 100% | 100% |
| 保留期 | 7–14 天 | 90 天 | 90 天 |
| 跨 trace (CUJ) | ❌ | ❌ TraceQL 不行 | ✅ SQL 直接 |
| 引擎可替換 | 綁 ES | 綁 Tempo | Trino → Athena/StarRocks |
便宜 ~14× —— 而且這是我們全球 vs 對手單區,正規化後差距更大 · vs Tempo 便宜 2–3× 還多 5 倍能力
maxExecutors=8 是 hard cap。重啟追資料 lag 噴 → 背壓三道閘要全設。start_time 的 SQL = coordinator 卡死、其他查詢被拖。PB 級沒有捷徑 —— 這三個坑,你遲早會踩。
Q & A · Thank you