Có hai kiểu hoá đơn rất khác nhau mà người ta hay gộp làm một. Kiểu thứ nhất là tiền thuê nhà: cố định hằng tháng, bạn có ở nhà nhiều hay ít cũng thế. Kiểu thứ hai là tiền điện: tỷ lệ với việc bạn dùng bao nhiêu. Nhầm hai kiểu này khi lập ngân sách là sai lầm kinh điển — và ba lớp quan sát (log, chỉ số, trace) chia đúng theo lằn ranh đó, một lớp là tiền thuê nhà, hai lớp là tiền điện. Ba mươi phần của sê-ri đã đo từng lớp riêng lẻ; phần này tôi cộng chúng lại trên cùng một dịch vụ, bật dần từng lớp, và đo tổng.
(Các con số là thông lượng, thời gian CPU và dung lượng đo trong container, không phải perf counter phần cứng.)
Bảng số liệu
Một dịch vụ HTTP làm việc tính toán thật, 20 000 yêu cầu mỗi lần đo, bật dần từng lớp bằng biến môi trường.
| Cấu hình | Thông lượng | CPU | Bộ nhớ | Dữ liệu sinh ra |
|---|---|---|---|---|
| Trần trụi | 4 125 req/s | 3,939 s | 19,2 MB | 0 |
| + log | 3 949 (−4,3%) | 4,163 s (+5,7%) | 19,4 MB | 1,28 MB |
| + log + chỉ số | 3 913 (−5,1%) | 4,193 s (+6,4%) | 23,9 MB (+24,5%) | +1 312 B |
| + log + chỉ số + trace | 3 591 (−12,9%) | 4,698 s (+19,3%) | 28,0 MB (+45,8%) | +2,57 MB |
Tách riêng từng lớp:
| Thông lượng | CPU | Bộ nhớ | Dữ liệu | |
|---|---|---|---|---|
| Log | −4,3% | +5,7% | +0,2 MB | 1,28 MB |
| Chỉ số | −0,9% | +0,7% | +4,5 MB | 1 312 B |
| Trace | −8,2% | +12,0% | +4,1 MB | 2,57 MB |
Và quy ra ở 1 000 yêu cầu mỗi giây:
| Mỗi ngày | |
|---|---|
| Log — 67 byte mỗi yêu cầu | 5,4 GB |
| Trace — 135 byte mỗi yêu cầu | 10,8 GB |
| Chỉ số — 1 312 byte mỗi lần thu thập, chu kỳ 15 giây | 7,2 MB |
Bốn điều bảng này nói
Bật cả ba lớp lấy đi 12,9% thông lượng, 19,3% CPU và 45,8% bộ nhớ. Đó là một khoản thật — gần một phần tám công suất — nhưng nó không phải thảm hoạ, và nó mua về toàn bộ khả năng biết chuyện gì đang xảy ra. Trace là lớp đắt nhất ở cả ba cột: −8,2% thông lượng, +12,0% CPU, 2,57 MB dữ liệu; nó cũng là lớp duy nhất trả lời được câu "chậm ở đâu" — như phần 20 đã đo, khi một dịch vụ giữa chuỗi chậm thì không chỉ số nào ở đầu chuỗi chỉ ra được thủ phạm. Chỉ số gần như miễn phí về CPU và băng thông nhưng tốn bộ nhớ: +0,7% CPU và 1 312 byte dữ liệu (trong khi trace sinh 2,57 MB cho cùng lượng việc), nhưng nó chiếm thêm 4,5 MB bộ nhớ — nhiều hơn cả trace.
Điều thứ tư mới là cái đáng khắc vào đầu, vì nó là khác biệt bản chất chứ không phải mức độ: chỉ số không tỷ lệ với lưu lượng. 1 312 byte đó là toàn bộ trạng thái của mọi bộ đếm và histogram; nó vẫn là 1 312 byte nếu có 20 triệu yêu cầu thay vì 20 nghìn. Log và trace thì ngược lại — mỗi yêu cầu thêm một dòng và bốn span. Ở 1 000 yêu cầu mỗi giây, khác biệt ấy thành 10,8 GB mỗi ngày cho trace so với 7,2 MB cho chỉ số — gấp 1 540 lần. Đây đúng là tiền điện so với tiền thuê nhà.
Vì sao mô hình dữ liệu quyết định cách chi phí tăng
Ba lớp có ba mô hình dữ liệu khác nhau, và mô hình quyết định cách chi phí tăng. Chỉ số giữ trạng thái gộp: một Counter là một số, nó tăng lên chứ không dài ra, nên chi phí bộ nhớ tỷ lệ với số chuỗi thời gian (phần 11 đã đo) chứ không tỷ lệ với số sự kiện — đó là lý do nó chiếm 4,5 MB ngay từ đầu (các đối tượng histogram và bộ đăng ký) rồi gần như đứng yên. Log và trace ghi sự kiện: mỗi yêu cầu để lại dấu vết riêng nên dung lượng tỷ lệ tuyến tính với lưu lượng, và chi phí CPU cũng vậy — định dạng một chuỗi, dựng một span, đẩy vào hàng đợi, làm một lần cho mỗi yêu cầu. Trace đắt hơn log vì mỗi yêu cầu tạo bốn span chứ không phải một dòng, và mỗi span cần dựng đối tượng, quản lý ngữ cảnh, đặt thuộc tính rồi tuần tự hoá; phần 21 đo riêng phần đó là 48,6 µs mỗi span khi có xuất OTLP.
Điều đáng nói là thứ tự chi phí không giống thứ tự giá trị: chỉ số rẻ nhất nhưng chỉ nói "có vấn đề", trace đắt nhất nhưng là thứ duy nhất nói "vấn đề ở chặng nào" — cắt theo chi phí là cắt ngược với giá trị.
Ra thực tế thì ngân sách 10–15% công suất cho quan sát là hợp lý, và hãy coi nó là chi phí vận hành chứ không phải thứ để tiết kiệm — một sự cố kéo dài thêm một giờ vì thiếu dữ liệu đắt hơn nhiều so với 12,9% thông lượng. Nếu buộc phải cắt thì cắt theo thứ tự: lấy mẫu trace trước (phần 23 đã đo — lấy mẫu đuôi giữ 100% trace lỗi với 5,93% chi phí lưu), rồi giảm số dòng log, và đừng cắt chỉ số vì nó rẻ nhất và là thứ duy nhất cho bạn cảnh báo. Tính ngân sách lưu trữ theo từng lớp chứ đừng gộp — 10,8 GB trace và 7,2 MB chỉ số là hai bài toán khác nhau tới mức gộp vào một dòng là vô nghĩa. Đo cả bộ nhớ chứ đừng chỉ đo CPU: chỉ số chỉ tốn 0,7% CPU nhưng 24,5% bộ nhớ, nên nếu container của bạn có giới hạn bộ nhớ chặt thì đó mới là cột đáng lo. Và đừng suy tỷ lệ phần trăm này cho dịch vụ của bạn — dịch vụ của tôi xử lý một yêu cầu trong khoảng 240 µs, một dịch vụ tốn 20 ms mỗi yêu cầu sẽ thấy cùng chi phí tuyệt đối đó thành dưới 1%.
Mẫu số chung
Cái lằn ranh "chi phí cố định theo trạng thái gộp so với chi phí tuyến tính theo từng sự kiện" không riêng gì quan sát — nó là câu hỏi trung tâm khi thiết kế bất kỳ hệ nào phải ghi lại điều đã xảy ra.
Trong thế giới Java, đây đúng là khác biệt giữa một Counter/Timer của Micrometer (một ô nhớ cộng dồn, kích thước cố định dù triệu request) và một dòng Logback ghi mỗi request (phình theo lưu lượng). Ai từng thấy đĩa đầy vì DEBUG bật nhầm ở production đều hiểu: log là chi phí tuyến tính, và một if (log.isDebugEnabled()) đặt sai chỗ biến mỗi request thành một lần ghi — trong khi thêm một metric gauge gần như không thêm gì.
Trong Go, cùng lằn ranh: expvar/prometheus client giữ trạng thái gộp cố định, còn zap/slog ghi từng sự kiện tuyến tính. Và đây là lý do "cardinality bùng nổ" là cơn ác mộng của metric — nếu bạn nhét một trường vô hạn giá trị (user_id, request_id) làm nhãn, metric mất tính cố định và phình như log, đúng cái bẫy mà mô hình gộp sinh ra để tránh.
Còn ở cơ sở dữ liệu, đây là lựa chọn kinh điển giữa một bảng tổng hợp / materialized view (một hàng cập-nhật-tại-chỗ, kích thước cố định theo số nhóm) và một bảng append-only ghi từng sự kiện (phình theo số giao dịch). Cùng câu hỏi: bạn cần trạng thái hiện tại (rẻ, gộp được) hay cần lịch sử từng sự kiện (đắt, tuyến tính, nhưng chứa chi tiết không tái dựng được) — và câu trả lời quyết định hoá đơn lưu trữ nhiều hơn bất cứ tối ưu nào.
Mẫu số chung: chi phí của một hệ ghi-lại-dữ-liệu không nằm ở con số tuyệt đối lúc này mà ở mô hình phình của nó — gộp-trạng-thái thì cố định và bạn ngủ yên, ghi-từng-sự-kiện thì tuyến tính với lưu lượng và sẽ ăn hết đĩa đúng lúc bạn bận nhất. Vì thế câu hỏi đúng khi thêm bất kỳ thứ gì "để sau này biết chuyện gì xảy ra" không phải "nó tốn bao nhiêu hôm nay" mà là "chi phí của nó tỷ lệ với cái gì — với số nhóm cố định, hay với lưu lượng" — vì chỉ vế sau mới cho bạn một hoá đơn không bao giờ ngừng lớn.
Muốn tự đo hoá đơn quan sát của chính dịch vụ mình trong ba mươi giây, không cần công cụ gì — chạy hai lần, một lần bình thường, một lần tắt hết:
# Do chi phi quan sat cua chinh dich vu ban, khong can cong cu gi
# Chay hai lan: mot lan nhu binh thuong, mot lan tat het quan sat
# 1. Lay so nen (tat log/metric/trace bang bien moi truong cua ung dung ban)
# 2. Bat lai va do lai
# 3. So ba cot nay:
ps -o pid,%cpu,rss,comm -p <PID_UNG_DUNG>
curl -s localhost:<PORT>/metrics | wc -c # so byte metric moi lan thu thap
ls -l /var/log/<ung-dung>.log # so byte log tich luy
Ba con số đó — CPU, bộ nhớ, byte mỗi lớp — là toàn bộ hoá đơn. Cột cuối là cột hay bị bỏ quên nhất, và ở 1 000 yêu cầu mỗi giây nó là hàng chục gigabyte mỗi ngày.