How we made ClickStack 5x faster for ClickHouse observability

Bài tóm tắt ClickHouse 2026 3. Quy mô lớn Độ khó: Khó

ClickHouse · 2026 · 3. Quy mô lớn · Độ khó: Khó — bài gốc

Bài của nhà cung cấp về sản phẩm của họ (ClickStack: log/trace trên ClickHouse); benchmark do họ tự dựng. Các con số là theo ClickHouse.

Bối cảnh

  • Schema log/trace mặc định của ClickStack thừa hưởng từ exporter ClickHouse của OpenTelemetry Collector, do cộng đồng đóng góp, chưa dùng tính năng mới của ClickHouse, và chưa từng được benchmark theo workload thật.
  • Ở quy mô petabyte, với khách lớn trong private preview và beta: query chậm, Top N trên khoảng thời gian ngắn chậm, search và aggregation bị timeout. Cách xử lý trước đó là sửa vá từng trường hợp.
  • Đầu 2026 lập nhóm chuyên: tìm mẫu truy vấn phổ biến trong sản phẩm (search kim-đáy-bể theo trace ID/user ID, search từ phổ biến trên thuộc tính, search trong body log, histogram trên trang search, tra một dòng log, liệt kê key/value cho autocomplete), rồi tối ưu theo chúng.

Cách đo

  • Dùng công cụ ClickCannon (dựng song song để trả lời "cần bao nhiêu tài nguyên cho ClickStack") và một framework dựng instance ClickHouse Cloud tạm trên Kubernetes. Cấu hình cố định, chỉ ba biến: schema, workload query, phiên bản ClickHouse. Ingest 640 MiB/s (khoảng 1,5 PB/tháng), 3 replica × 24 core, dữ liệu sắp theo timestamp như thực tế.
  • Ghi vào service cha, query trên service con (compute-compute separation) để đo riêng.
  • Mỗi thay đổi đánh giá theo thứ tự: ingest (throughput, CPU, RAM, số part tối đa mỗi partition, tức merge có theo kịp insert), rồi tài nguyên phía đọc, rồi từng query; mỗi lượt chạy cả workload rộng để bắt hồi quy ở chỗ khác.

Thay đổi chính

  • Primary key bắt đầu bằng bucket thời gian: khoá cũ (ServiceName, TimestampTime) không cho tối ưu read_in_order (dừng sớm Top N khi đủ LIMIT) vì không khớp thứ tự sắp theo Timestamp. Thử bucket 1 phút: kích hoạt được nhưng quá nhiều dải granule nhỏ, search và aggregation chậm đi khi khoảng thời gian dài. Chọn toStartOfFiveMinutes(Timestamp), rồi ServiceName: giữ được read_in_order mà dữ liệu vẫn liền khối, nén tốt.
  • Bỏ cột TimestampTime: khi đã có bucket 5 phút, cột thời gian độ chính xác giây không còn giúp gì (benchmark không thấy khác), bỏ cho đơn giản.
  • Text index (inverted, kết quả chính xác) thay bloom filter cho body log và TraceId: bloom filter có dương tính giả nên vẫn phải đọc và kiểm granule; text index loại chính xác và cho direct_read (trả lời một phần query ngay từ index, không đọc cột). TraceId nhanh hơn tới 22%; tốn thêm lúc insert và dung lượng index (nằm trên object storage nên rẻ). Tra một dòng log lại chậm đi vì chi phí đánh giá index, được bù bằng thay đổi sau.
  • _block_number + _block_offset: trước đây xem chi tiết một dòng log phải filter bằng mọi cột đã trả về (primary key không duy nhất). Bật lưu hai cột ảo này thì tra bằng primary key + vị trí chính xác.
  • Materialized view cho autocomplete: liệt kê key/value trước đây quét thẳng bảng log/trace (chậm, hoặc lấy mẫu thì thiếu giá trị). Nay ship sẵn rollup AggregatingMergeTree theo bucket 15 phút (key, value, tần suất) tính lúc insert; autocomplete không còn tranh tài nguyên với search, xếp gợi ý theo độ phổ biến.
  • Cột ALIAS key=value cho Map + text index: thuộc tính động lưu bằng Map nên tra một key phải đọc và parse cả map; bloom filter riêng cho key và value không biết hai thứ có cùng một cặp không. Ghép mỗi cặp thành chuỗi key=value trong một cột ALIAS (không lưu dữ liệu, chỉ để đánh index), viết lại Attr['foo']='bar' thành has(AttrItems, 'foo=bar'): dùng được text index và direct_read, có query nhanh tới 10 lần. Index sắp theo token nên tra tiền tố foo= cũng cho ra danh sách giá trị của một key. Đã cân nhắc kiểu JSON (tự tách đường dẫn hay dùng thành cột) nhưng theo họ chi phí insert hiếm khi đáng với thuộc tính thưa và luôn đổi.

Kết quả (theo ClickHouse)

  • Latency các query đại diện giảm hơn 5 lần, trong khi ingest, dung lượng và vận hành vẫn cân bằng. Schema cuối khác hẳn schema ban đầu.

Vì sao

  • Hiệu năng là tích của schema và query: UI sinh query khai thác bố cục lưu trữ, nên phải tối ưu cả hai cùng nhau (Query planner và hiệu năng truy vấn).
  • Không có benchmark lặp lại được thì mỗi tối ưu có thể làm chậm một query khác mà không ai thấy (ví dụ tra một dòng log chậm đi khi đổi sang text index).

Bài học cho SWE

  • Schema mặc định thừa hưởng từ cộng đồng là điểm khởi đầu, không phải điểm tối ưu; đo lại theo mẫu truy vấn thật của sản phẩm.
  • Độ mịn của khoá sắp xếp là đánh đổi: mịn quá thì phân mảnh, thô quá thì mất dừng sớm.
  • Cố định mọi biến trừ thứ đang thử, đo ingest trước rồi mới đo query, và luôn chạy cả workload để bắt hồi quy.
  • Chuyển việc từ lúc query sang lúc insert (rollup, index) cho những truy vấn lặp đi lặp lại như autocomplete (Observability và monitoring).

Liên quan