Observability lý tưởng là ghi mọi thứ: mọi request một trace đầy đủ, mọi sự kiện một dòng log. Nhưng lý tưởng đó va vào thực tế phũ phàng — một dịch vụ lớn xử lý hàng triệu, hàng tỷ request mỗi ngày, và lưu trọn trace/log cho từng cái là khối dữ liệu khổng lồ: tốn tiền lưu trữ khủng khiếp, tốn băng thông gửi đi, và làm chậm chính ứng dụng. Ở quy mô đó, sampling — chỉ giữ một phần dữ liệu — không phải lựa chọn mà là bắt buộc.

Câu hỏi khó không phải "có sample không" mà là "sample thế nào". Cách ngây thơ nhất — giữ ngẫu nhiên p% — nghe hợp lý nhưng có một khiếm khuyết chí mạng: nó bỏ sót đúng thứ bạn cần nhất. Vì trong observability, cái đáng xem nhất là các trace lỗi và chậm — mà chúng lại hiếm. Giữ ngẫu nhiên 1% nghĩa là bạn chỉ thấy 1% số lỗi. Bài này (phần 11 loạt Observability) đo thật khiếm khuyết đó và một chiến lược sampling thông minh hơn hẳn.

Cơ chế: head sampling vs error-biased (tail)

Ảnh chụp đoạn mã nền tối minh hoạ sampling head vs tail, khối head sampling giữ p phần trăm ngẫu nhiên không nhìn nội dung if rand.Float64 nhỏ hơn p ví dụ p bằng 0.01 1 phần trăm thì keep trace quyết định ngay khi trace bắt đầu rẻ đơn giản quyết được ngay ở đầu request nhưng lỗi hiếm 1 phần trăm chỉ được giữ với xác suất p nên bỏ sót, khối error-biased kiểu tail luôn giữ lỗi chậm cộng sample phần OK if trace.isError hoặc trace.slow thì keep trace luôn giữ cái đáng chú ý else if rand.Float64 nhỏ hơn baseP thì keep trace chỉ sample phần bình thường bắt khoảng 100 phần trăm lỗi mà tổng lượng giữ vẫn thấp cái giá phải chờ trace xong mới biết nó lỗi nên phải buffer

Hình 1: Head sampling quyết định giữ/bỏ ngay đầu request theo xác suất p — rẻ, đơn giản, nhưng lỗi hiếm chỉ được giữ với xác suất p nên bị bỏ sót. Error-biased (kiểu tail) luôn giữ trace lỗi/chậm và chỉ sample phần bình thường — bắt gần 100% lỗi mà tổng lượng vẫn thấp, đổi lại phải chờ trace xong mới biết nó lỗi (cần buffer).

Khác biệt cốt lõi nằm ở khi nào và dựa vào gì để quyết định:

  • Head sampling: quyết định ngay khi request bắt đầu, dựa vào một đồng xu ngẫu nhiên. Không biết trace sẽ lỗi hay không — vì nó chưa xảy ra. Rẻ và đơn giản.
  • Tail / error-biased: quyết định sau khi trace kết thúc, dựa vào nội dung — nó có lỗi không, có chậm không. Giữ hết cái đáng chú ý, sample thưa phần bình thường.

Đo thật trong go-lab

Mình tạo trong go-lab (golang 1.23) 100.000 trace giả với phân phối thực tế: 1% lỗi, 2% chậm. Rồi áp ba chiến lược và đếm: giữ bao nhiêu trace, bắt được bao nhiêu lỗi.

Ảnh chụp bảng kết quả chạy thật trong go-lab output thật golang 1.23 100.000 trace 1 phần trăm lỗi 2 phần trăm chậm, tổng 100.000 trace 1029 lỗi 2005 chậm 3017 đáng chú ý, bảng chiến lược Head 1 phần trăm trace giữ 976 1.0 phần trăm lỗi bắt được 14 trên 1029 tỉ lệ 1 phần trăm bỏ sót 99 phần trăm, Head 10 phần trăm trace giữ 9.924 9.9 phần trăm lỗi bắt được 108 trên 1029 tỉ lệ 10 phần trăm bỏ sót 90 phần trăm, Error-biased trace giữ 3.963 4.0 phần trăm lỗi bắt được 1029 trên 1029 tỉ lệ 100 phần trăm bắt hết, chú thích head sampling giữ ngẫu nhiên nên lỗi hiếm bị bỏ theo tỉ lệ giữ 1 phần trăm thì chỉ thấy 1 phần trăm lỗi error-biased giữ tổng cộng chỉ 4 phần trăm trace mà bắt 100 phần trăm lỗi vì luôn giữ cái đáng chú ý chỉ sample phần bình thường rẻ hơn head 10 phần trăm mà không mù một sự cố nào

Hình 2: Kết quả thật — Head 1%: giữ 976 trace nhưng chỉ bắt 14/1029 lỗi (bỏ sót 99%); Head 10%: bắt 108/1029 (bỏ sót 90%); Error-biased: giữ tổng cộng chỉ 3.963 (4%) mà bắt 1029/1029 lỗi = 100%.

Con số phơi bày khiếm khuyết và lời giải:

  • Head sampling bỏ sót lỗi đúng theo tỉ lệ giữ. Giữ 1% ngẫu nhiên → chỉ bắt 14/1029 lỗi (1%). Giữ 10% → bắt 108/1029 (10%). Đây là hệ quả toán học không thể tránh: nếu giữ ngẫu nhiên p%, bạn thấy p% mọi thứ, gồm cả p% số lỗi. Với head sampling 1% — cấu hình rất phổ biến để tiết kiệm — bạn mù 99% sự cố. Đúng lúc điều tra một lỗi cụ thể, khả năng cao trace của nó đã bị vứt.
  • Error-biased bắt trọn lỗi với chi phí thấp. Bằng cách luôn giữ trace lỗi/chậm và chỉ sample 1% phần bình thường, chiến lược này giữ tổng cộng chỉ 4% trace (3.963 cái) mà bắt 100% lỗi (1029/1029). So sánh trực tiếp: nó rẻ hơn head 10% (giữ 4% vs 9.9%) và bắt lỗi tốt hơn vô cùng (100% vs 10%). Không có đánh đổi ở đây — nó thắng cả hai chiều, vì nó nhìn vào cái đáng giá thay vì tung đồng xu mù.

Đánh đổi cần cân nhắc

Tail sampling bắt lỗi tốt nhưng phải buffer — đó là cái giá thật. Điểm yếu của error-biased/tail sampling: để quyết định "trace này có lỗi không", bạn phải chờ trace kết thúc. Nghĩa là phải giữ (buffer) toàn bộ span của trace trong bộ nhớ cho tới khi request xong, rồi mới quyết định giữ hay bỏ. Với trace ngắn thì ổn, nhưng với trace dài (hoặc phân tán qua nhiều service, mỗi service sinh span vào thời điểm khác nhau), việc buffer này tốn RAM và phức tạp — thường cần một tầng thu thập riêng (collector) gom span rồi mới lấy mẫu. Head sampling không có vấn đề này vì quyết ngay từ đầu. Đây là đánh đổi kinh điển: đơn giản+rẻ (head) vs chính xác+phức tạp (tail).

Consistent sampling để trace không đứt xuyên service. Trong hệ phân tán, một request đi qua nhiều service, mỗi service tự quyết sample. Nếu chúng quyết độc lập ngẫu nhiên, service A giữ span của nó còn service B vứt span của mình → bạn có trace đứt đoạn, vô dụng. Lời giải là consistent sampling: quyết định giữ/bỏ dựa trên chính trace_id (ví dụ hash trace_id rồi so ngưỡng) — mọi service thấy cùng trace_id sẽ ra cùng quyết định, nên hoặc giữ toàn bộ trace hoặc bỏ toàn bộ, không bao giờ đứt. Đây là lý do trace_id (phần 2) lại quan trọng: nó không chỉ để nối, mà còn để sample nhất quán.

Metric KHÔNG sample — chỉ log/trace sample. Một hiểu lầm nguy hiểm là đem sampling áp cho metric. Đừng. Metric (counter, histogram) đã là dữ liệu tổng hợp — request_total là tổng mọi request, nếu bạn chỉ đếm 1% rồi nhân 100 thì sai số lớn và mất chính xác, nhất là với sự kiện hiếm (lỗi). Metric rẻ sẵn (vài chuỗi số, như phần 3-5) nên không cần sample. Sampling chỉ dành cho log và trace — dữ liệu chi tiết per-event mới đắt. Nhớ ranh giới: đếm thì đếm hết (metric), chi tiết thì lấy mẫu (log/trace).

Ba ý mang về

  1. Head sampling ngẫu nhiên bỏ sót lỗi đúng theo tỉ lệ giữ: đo thật, giữ 1% thì chỉ bắt 14/1029 lỗi (bỏ sót 99%), giữ 10% bắt 108/1029 (bỏ sót 90%) — vì lỗi hiếm chỉ được giữ với xác suất p, đúng lúc điều tra sự cố thì trace đã bị vứt.
  2. Error-biased (tail) bắt trọn lỗi với chi phí thấp hơn: đo thật, luôn giữ trace lỗi/chậm + sample 1% phần bình thường → giữ tổng cộng chỉ 4% mà bắt 100% lỗi (1029/1029) — rẻ hơn head 10% và không mù một sự cố nào, vì nhìn vào nội dung thay vì tung đồng xu.
  3. Cái giá là buffer, và biết ranh giới: tail sampling phải giữ toàn trace tới khi xong mới quyết (tốn RAM, cần collector); dùng consistent sampling theo trace_id để trace không đứt xuyên service; và metric KHÔNG sample (đã tổng hợp, rẻ) — chỉ log/trace mới sample.

Nguồn

Phần sau là bài tổng kết cả loạt: ghép ba trụ cột log + metric + trace để chẩn đoán một sự cố từ đầu tới cuối, kèm một cây quyết định "gặp triệu chứng X thì nhìn trụ cột nào".