Phần 9 kết luận rằng chọn kiểu chỉ số gần như không ảnh hưởng gì, chọn nhãn thì ảnh hưởng tất cả. Bài này đo cụ thể "tất cả" là bao nhiêu.

Nhãn nhân với nhau và cái giá ở phía Prometheus

Nhãn không cộng, chúng nhân

Một CounterVec với ba nhãn path, method, status:

path method status Số chuỗi Byte mỗi lần quét
1.000 1 1 1.000 72.953
1.000 4 1 4.000 291.623
1.000 4 5 20.000 1.457.863

Thêm nhãn method với bốn giá trị: gấp 4 lần. Thêm status với năm giá trị: gấp 20 lần so với ban đầu.

Đây là điều dễ nói mà khó cảm nhận cho tới khi nhìn số: mỗi nhãn mới không cộng vào, nó nhân lên. Ba nhãn nghe hoàn toàn vô hại — đường dẫn, phương thức, mã trạng thái — biến 1.000 chuỗi thành 20.000.

Và công thức không dừng ở đó. Thêm instance (20 bản sao) và env (3 môi trường):

1.000 × 4 × 5 × 20 × 3 = 1.200.000 chuỗi

instancejob được Prometheus tự thêm vào, nên chúng luôn có mặt kể cả khi bạn không khai.

Cái giá ở phía Prometheus

Chạy Prometheus thật, quét một mục tiêu có số giá trị nhãn tăng dần:

Giá trị nhãn Chuỗi thời gian RAM Prometheus Truy vấn sum(rate())
100 1.500 21,2 MiB 1 ms
1.000 15.000 48,24 MiB 1 ms
10.000 150.005 265,9 MiB 7 ms
50.000 750.000 1,307 GiB 47 / 56 / 53 ms

Khoảng 1,83 KiB bộ nhớ cho mỗi chuỗi thời gian, tăng tuyến tính.

Và độ trễ truy vấn tăng 52 lần — từ 1 ms lên khoảng 52 ms. Cũng tuyến tính, vì sum(rate()) phải duyệt qua mọi chuỗi khớp trước khi gộp.

Với một cụm thật có vài trăm mục tiêu, con số 1,83 KiB mỗi chuỗi là công thức để dự toán RAM:

RAM ≈ số chuỗi hoạt động × 2 KiB

Một triệu chuỗi là khoảng 2 GB — và đó chỉ là phần đang hoạt động trong bộ nhớ, chưa tính bộ đệm truy vấn.

Ba nhãn không bao giờ được đặt

user_id, request_id, trace_id. Số giá trị bằng số người dùng hoặc số yêu cầu — không có trần. Đây là dữ liệu thuộc về log, nơi mỗi bản ghi độc lập và không nhân với nhau.

path chưa chuẩn hoá. /api/v1/don-hang/12345 sinh một chuỗi cho mỗi đơn hàng. Phải rút về khuôn mẫu tuyến trước khi đưa vào nhãn:

// sai
c.WithLabelValues(r.URL.Path).Inc()

// dung — lay khuon mau tu bo dinh tuyen
c.WithLabelValues(mux.CurrentRoute(r).GetPathTemplate()).Inc()

error_message. Chuỗi tự do, và thông báo lỗi thường nhúng cả giá trị vào: "không tìm thấy người dùng 8471". Dùng error_code hoặc error_type — tập giá trị hữu hạn và biết trước.

Quy tắc chung: một nhãn chỉ hợp lệ khi bạn liệt kê được toàn bộ giá trị có thể của nó ra giấy. Không liệt kê được thì nó thuộc về log.

Bùng nổ chậm khó thấy hơn bùng nổ nhanh

Nhãn path thêm một tuyến mới mỗi tháng thì không ai để ý. Nhưng chuỗi cũ không biến mất ngay: Prometheus giữ chúng trong bộ nhớ cho tới hết cửa sổ khối hiện tại (mặc định 2 giờ), và trên đĩa cho tới hết thời gian giữ.

Nghĩa là một lần triển khai sai — đẩy request_id vào nhãn trong mười phút — để lại hậu quả suốt cả chu kỳ giữ dữ liệu.

Cách phát hiện sớm nhất là theo dõi chính số chuỗi:

prometheus_tsdb_head_series
rate(prometheus_tsdb_head_series_created_total[5m])

Dòng thứ hai quan trọng hơn: tốc độ tạo chuỗi mới. Một hệ thống lành mạnh có tốc độ này gần 0 khi không triển khai gì. Khác 0 liên tục nghĩa là có nhãn đang sinh giá trị mới không ngừng.

Ba cách chặn

metric_relabel_configs — vứt chuỗi ngay khi nhận, trước khi ghi vào TSDB:

metric_relabel_configs:
  - source_labels: [__name__]
    regex: 'chi_so_hong_.*'
    action: drop
  - regex: 'request_id|user_id'
    action: labeldrop

Đây là cách chữa nhanh nhất khi đã lỡ, và nó chạy được ngay mà không cần đợi triển khai lại ứng dụng.

sample_limit — từ chối cả lần quét nếu vượt ngưỡng:

scrape_configs:
  - job_name: 'dich-vu'
    sample_limit: 50000

Nghe thô bạo, nhưng nó biến một sự cố âm thầm (Prometheus chết vì hết RAM, mất toàn bộ giám sát) thành một sự cố ồn ào và cục bộ (một mục tiêu ngừng được quét, up vẫn báo).

Xem lại trước khi thêm nhãn. Câu hỏi duy nhất cần trả lời: nhãn này có bao nhiêu giá trị, và tôi có liệt kê được chúng không.

Chỗ phép đo này không nói tới

Tôi đo với chuỗi có tên nhãn ngắn (path, method, status) và giá trị ngắn. Tên dài hơn thì cả byte lẫn RAM đều tăng — bảng ký hiệu của Prometheus lưu mỗi chuỗi ký tự một lần, nhưng chỉ mục thì không.

Tôi chạy Prometheus mặc định, không bật --enable-feature=memory-snapshot-on-shutdown hay chỉnh --storage.tsdb.head-chunks-write-queue-size. Cấu hình đó ảnh hưởng tới con số RAM.

Và tôi đo chuỗi đang hoạt động. Chuỗi đã ngừng nhận mẫu vẫn chiếm chỗ trên đĩa và vẫn làm chậm truy vấn theo khoảng dài — phần 19 sẽ đo lưu trữ dài hạn.

Thử ba mươi giây

Tìm chỉ số nào đang sinh nhiều chuỗi nhất:

# 10 chi so nhieu chuoi nhat
topk(10, count by (__name__)({__name__=~".+"}))

# Nhan nao co nhieu gia tri nhat trong mot chi so
count(count by (path) (http_requests_total))

# Toc do sinh chuoi moi — phai gan 0 khi khong trien khai
rate(prometheus_tsdb_head_series_created_total[5m])

# Tong so chuoi, va uoc tinh RAM
prometheus_tsdb_head_series
prometheus_tsdb_head_series * 1.83 / 1024   # KiB -> MiB

Truy vấn đầu tiên nặng — chạy nó ngoài giờ cao điểm. Nếu một chỉ số chiếm hơn 20% tổng số chuỗi, gần như chắc chắn nó có một nhãn lẽ ra không nên tồn tại.

Phần sau: histogram và phân vị — đo cách chọn bucket.