ClickHouse OpenTelemetry Rust 觀測性 Rotel

Rotel 用 Rust 把 OpenTelemetry Collector 記憶體壓掉 75%:單核吞吐量從 137K 拉到 462K spans/sec,otelcol 的 GC 與 allocator lock 是這樣繞過的

Rotel 是 Streamfold 用 Rust 從零實作的 OpenTelemetry collector,同樣講 OTLP、同樣寫 ClickHouse,記憶體用量少 75%、單機吞吐量拉到 3.7M spans/sec。文章拆解三個關鍵優化、實際部署方式,以及不該切過去的場景。

Rotel 是 2026 年 3 月由 Streamfold 釋出的 Rust 版 OpenTelemetry Collector,同一個 OTLP 協議、同一個 ClickHouse exporter,記憶體吃 75% 少、CPU 少一半。在 ClickHouse 官方 blog 貼出的 benchmark 裡,單機吞吐量從官方 otelcol 的 1.1M spans/sec 拉到 3.7M spans/sec,單核從 137K 拉到 462K。跑 OpenTelemetry Collector 在生產環境收 traces 的團隊,多半經歷過同一個成長痛:一開始 spans 每秒幾千條,otelcol 吃 100MB 記憶體、CPU 佔一成,一切安詳;等到接上 30 個服務、spans 衝到每秒十萬條,同一個 binary 佔到 800MB、CPU 上七成,Go runtime 的 GC 尾巴讓 p99 latency 翻倍。Rotel 做對了什麼、沒做什麼、該不該切過去,這裡整理過一輪。

otelcol 卡住的三個地方

翻 otelcol-contrib 的 profile,會發現高流量時 hot path 幾乎都指向三件事:Go runtime 的 GC pressure、tokio 或 goroutine scheduler 的 context switch、以及 glibc allocator 的 arena lock。

Go 的 GC 是低延遲導向的並行三色標記,但收 telemetry 這種「每條 span 產生一個 protobuf message、幾百個小物件」的 workload,allocation rate 極高。GC 一秒觸發二三十次,heap growth 又追不上 batch flush,尾巴一定會拖 p99。這不是 otelcol 寫壞,是 Go 語言在這種 workload 的天花板。

第二層是 unmarshaling。收 OTLP 訊息時,otelcol 把整個 protobuf decode、轉成 pdata 內部資料結構、經過 processors、再序列化成下游要的格式。這條路徑上有大量的 heap allocation 與 slice 複製,跑在 goroutine 上讓 scheduler 不斷做 preemption。ClickHouse 工程團隊在寫他們的採用文章時提到,光把 CPU-intensive 的 unmarshal 從 async 任務裡搬到專屬 thread pool,就能把 throughput 從 1.45M 拉到 3.6M spans/sec。這是同一個問題在 Rust 那邊呈現的樣子,Go 的 runtime 讓等值優化更難做。

第三層更陰險:allocator lock contention。glibc 的 malloc 用 per-thread arena,但當 A thread allocation、B thread deallocation 時,需要跨 arena 拿 lock。otelcol 的 pipeline 讓 allocation 發生在 receiver 執行緒、deallocation 發生在後端 exporter 執行緒,這種 cross-arena 模式在流量拉高後會用光 arena lock 的 CPU 時間。多數人不會發現這是問題,直到 perf top 顯示 40% 時間卡在 __lll_lock_wait

Rotel 拆掉這三層的方法

Rotel 完全用 Rust 從零寫,不 fork otelcol,也不共用 pdata 資料結構。它只講 OTLP,加上一組窄的 exporters(ClickHouse、Datadog、AWS X-Ray、AWS EMF、Kafka、file),先把 90% 使用者的路徑做到底。

Rust 沒有 GC 是最直接的收益。所有物件的生命週期在編譯期決定,heap allocation 的量比等值 Go 程式少一個數量級,GC pause 直接歸零。但這只是起點,Streamfold 團隊真正的優化在後面。

RowBinary 編碼取代 JSON。往 ClickHouse 寫資料時,otelcol contrib 的 ClickHouse exporter 會把 span 轉成 JSON、走 HTTP 打進去,ClickHouse 那頭再 parse JSON 塞 columns。這條路徑兩端各多一次 serialization。Rotel 直接走 ClickHouse 的 RowBinary 二進位格式,等於 span 從 memory struct 直接吐到 wire、伺服器端 zero-copy 吃進 column engine,兩邊 CPU 都省。

Unmarshal 搬離 async runtime。Rust 的 tokio 執行緒模型跟 Go 的 goroutine 面對同一個問題:async task 卡在 CPU-bound 運算會讓其他 IO task 餓死。Rotel 明確把 protobuf 解碼工作 dispatch 到 dedicated thread pool,async runtime 只處理 IO 事件,兩者用 channel 溝通。這個改動把 tokio scheduler 的 context switch 從每秒幾十萬次砍到幾千次,thread pool 內的 CPU 時間也不再跟 IO 搶。這是 ClickHouse blog 描述的、把 throughput 從 1.45M 拉到 3.6M spans/sec 的關鍵一步。

Allocation 與 deallocation 綁在同一組執行緒。Rotel 團隊實測後把 allocator arena 對齊到同一 thread pool 內,讓 malloc/free 都落在同一個 arena,跨 arena lock 直接消失。這種優化在 Go 幾乎不可能做——runtime 完全接管 goroutine 的 scheduling,開發者管不到執行緒親和性。

部署 Rotel 只需要一個 binary 或一個 container

Rotel 沒有 YAML config file,所有選項都是 CLI flag 或 ROTEL_ 前綴的環境變數。這個設計在小型部署很爽——不用維護 config、不用學 otelcol 的 receivers / processors / pipelines / exporters 四層階層。跑起來大概長這樣:

1
2
3
4
5
6
7
8
9
docker run -d --name rotel \
-p 4317:4317 -p 4318:4318 \
-e ROTEL_EXPORTER=clickhouse \
-e ROTEL_CLICKHOUSE_EXPORTER_ENDPOINT=http://clickhouse:8123 \
-e ROTEL_CLICKHOUSE_EXPORTER_DATABASE=otel \
-e ROTEL_BATCH_MAX_SIZE=8192 \
-e ROTEL_BATCH_TIMEOUT=200ms \
-e ROTEL_OTEL_RESOURCE_ATTRIBUTES="service.name=edge,region=tw" \
public.ecr.aws/rotel-dev/rotel

這樣就開好一個收 OTLP gRPC (4317) 與 HTTP (4318)、批次送進 ClickHouse 的 collector。對比 otelcol 要寫一份含 receivers、processors、pipelines、exporters 的 YAML 然後 mount 進 container,Rotel 直接省掉 config drift 的維護成本。

VPS 上跑 edge collector、Kubernetes 拿來當 DaemonSet、AWS Lambda 掛 Extension,都是同一顆 binary。Lambda Extension 那個特別誇張,bundle 8MB、cold start 加不到 70ms,這是官方 otelcol Lambda layer 做不到的量級(後者大約 40MB、cold start 200ms 起跳)。

多路徑的 pipeline 也支援。同時開 OTLP 與 Kafka receiver、把 traces 拆到 ClickHouse、metrics 拆到 Datadog,flag 一路串下來就能設完:

1
2
3
4
5
6
7
rotel start \
--receivers otlp,kafka \
--kafka-receiver-brokers kafka:9092 \
--kafka-receiver-traces \
--exporter clickhouse,datadog \
--clickhouse-exporter-endpoint http://clickhouse:8123 \
--datadog-exporter-api-key ${DD_API_KEY}

file receiver 也值得一提。它能 tail 一個 glob pattern 下的 log 檔案、內建 nginx access log / syslog 等常見 parser,直接把純文字 log 轉成 OTLP logs 送到後端。這對「還沒導入 log SDK、但先想把 nginx access log 打進 ClickHouse」的過渡場景很實用,比起另外裝 Vector 或 Fluent Bit 少一個 daemon。Linux-only 的 kmsg receiver 則會拉 /dev/kmsg 的 kernel log,做 host-level 觀測時省掉一次 systemd-journal-remote 的中繼。

什麼場景還是回頭用 otelcol

誠實講:Rotel 不是 otelcol 的直接替代品,它是「窄場景做深」的取捨。以下幾種需求,切過去會踩坑。

需要大量 contrib processors 的場景。otelcol-contrib 生態有幾百個 processor:tail_samplingprobabilistic_samplerresource detectionk8sattributesspanmetrics connectortransform processor 等等。Rotel 現在只內建 attributes 與 redaction 兩個 prebuilt processor,其餘要用 Python SDK 或 Rust SDK 自己寫。前者用 PyO3 綁定,寫起來像 lambda function 但會付 Python 執行的效能稅;後者最快但要編譯成 dynamic library、寫 Rust。

需要非主流 exporter 的場景。Prometheus remote_write、Elasticsearch、Splunk、Loki、Tempo、Grafana Cloud 這些 exporter 在 Rotel 都還沒有,只能繞一層 OTLP forwarder。下游是 SigNoz、Grafana Alloy、Jaeger 這種吃 OTLP 的服務就沒差;下游只吃自家協議就麻煩。

需要 tail-based sampling 的場景。tail sampling 需要 collector 把同一條 trace 的所有 spans 累積起來再決定要不要留,這是 stateful operation,Rotel 目前沒有。head-based sampling 在 SDK 那端做就行,但流量削減的靈活度差很多。

要跟現有 otelcol config 平滑 migration 的場景。如果團隊已經有一份維護三年、經過調校的 otelcol YAML,含 20 個 processor stage,切 Rotel 等於重寫。這個工程成本要跟省下的資源成本算清楚。

benchmark 數字背後的細節

3.7M spans/sec 這個數字容易被斷章取義,需要把 benchmark 條件講清楚才有意義。ClickHouse 官方跑的是 edge collector → Kafka → gateway collector → ClickHouse 的 streaming pipeline,gateway 端量 span 進出速率,測試機是 16 core 的 EC2 instance,ClickHouse table 用 Null engine 只驗吞吐不寫 disk。單看 gateway 這一段,Rotel 462K spans/sec/core、otelcol 137K spans/sec/core,差距接近 3.4 倍。

換算成一個典型的 VPS 場景比較貼近多數人的處境:4 vCPU 的機器,Rotel 大約能穩定收 1.5M spans/sec,otelcol 大概 500K spans/sec,兩者記憶體常駐值 Rotel 約 150MB、otelcol 約 550MB。這個 gap 在流量突刺時特別明顯——otelcol 在 GC 週期會出現 200ms 級的 pause,Rotel 因為沒有 GC,尾巴 latency 幾乎是平的一條線。

對 Kubernetes DaemonSet 場景更關鍵。每台 node 都要跑一顆 collector 收 local telemetry,otelcol 佔的 500MB 記憶體會直接吃掉 Pod resource quota;100 台 node 的 cluster 就是 50GB 記憶體憑空消失。換 Rotel 之後這個數字降到 15GB,多出來的 35GB 能拿去多跑一批 workload。

值不值得切過去

判斷標準其實很簡單:如果 collector 現在吃 300MB 以上記憶體、CPU 常態超過 50%、或者已經開始為 collector fleet 開額外 replica,Rotel 的收益能直接反映在帳單上。ClickHouse 團隊算過,2M logs/sec 的採集規模,otelcol 需要 800 顆 CPU core,換成 Rotel 大約 200 顆能撐——省下的成本遠比重寫 pipeline 的工程時間貴。

如果 collector 只是收幾千 spans/sec、跑一台 2 core 的 VPS,otelcol 的資源開銷根本不痛,切過去沒意義,反而失去 contrib processors 的生態。

臺灣本地想在單機 VPS 上跑一整套 OpenTelemetry stack(collector + ClickHouse + Grafana)的團隊,Rotel 值得評估。臺灣機房到自架 ClickHouse 的 RTT 通常在 5ms 以內,Rotel 的 batch 機制搭配 gRPC 壓縮基本上能把 network overhead 壓到可以忽略;剩下的就是 collector 本身的 CPU 開銷,這裡 Rust 的優勢會直接反映在 VPS 選型上——原本要 4 vCPU 才跑得動的 telemetry pipeline,2 vCPU 就綽綽有餘。

要跑 self-hosted 的 OpenTelemetry pipeline,主機資源永遠是第一個瓶頸。NCSE Network 提供臺灣是方電訊機房的 Intel Gold CPU、NVMe SSD VPS,適合放 collector、ClickHouse、Grafana 這類需要穩定 IO 與低延遲網路的觀測性 stack;有興趣可以到 ncse.tw 看方案細節。

需要穩定的雲端主機?

NCSE Network 提供企業級 VPS,7 天免費試用,臺灣是方電訊機房,99% SLA 保證。

查看 VPS 方案 →