Ba mươi sáu phần trước đều giả định một điều: bạn sửa được mã nguồn. Thêm thư viện chỉ số, gắn SDK trace, đổi định dạng log — tất cả đều bắt đầu bằng việc mở tệp nguồn ra. Phần này bỏ giả định đó. Dịch vụ cần quan sát là một binary bạn không có mã, không build lại được, và không được phép khởi động lại theo ý mình.

Còn đúng hai đường: đặt một proxy chắn trước nó, hoặc gắn eBPF vào nhân hệ điều hành. Cả hai đều "không đụng vào ứng dụng", nhưng giá thì khác nhau một bậc.

So sanh proxy sidecar va eBPF khi quan sat mot dich vu khong sua duoc ma

Bảng số liệu

Hệ đo: một nginx phát chuỗi tĩnh, một nginx thứ hai làm proxy đứng trước, một client đo độ trễ từng request, tuần tự, trên kết nối giữ sẵn, đơn vị micro giây. Mỗi thành phần ghim vào một lõi CPU riêng. 8 000 request mỗi lượt, ba lần.

Cách quan sát p50 So với nền Sự kiện eBPF ghi nhận
Không quan sát gì 49,3 µs
eBPF đo thời gian (2 probe + map + histogram) 50,0 µs +0,7 µs (+1,4%) 8 201
eBPF đếm (3 probe) 50,5 µs +1,4 µs (+2,9%) 8 201 / 8 200 / 9
Proxy sidecar 82,2 µs +33,1 µs (+67%)
Proxy eBPF cùng lúc 81,9 µs +32,6 µs 24 601

Proxy đắt hơn eBPF khoảng 24 lần. Hai biến thể eBPF chênh nhau 0,7 µs — nằm trong nhiễu, nên nên đọc là "cả hai đều tốn cỡ một micro giây".

Cột cuối là cột quan trọng nhất trong bảng, và tôi sẽ giải thích ở cuối bài tại sao.

Điều đáng nhớ

Proxy không đắt vì nó chậm, mà vì nó là một chặng đầy đủ. 33,1 µs là thời gian để gói tin đi qua một tiến trình nữa: nhận kết nối, đọc, phân tích HTTP, mở kết nối sau, ghi, rồi làm ngược lại. Con số ấy cộng vào mọi request, kể cả những request bạn không quan tâm.

eBPF gần như miễn phí vì nó không đứng trên đường đi. Probe chạy trong nhân, ngay tại chỗ syscall vốn đã xảy ra. Biểu đồ phân bố mà chính probe ấy thu được cho thấy recvfrom mất 256–1024 ns — tức là bản thân syscall được đo chỉ chiếm khoảng 1,6% thời gian một request 49,3 µs. Gắn thêm vài chục nano giây vào một chỗ vốn chỉ tốn vài trăm nano giây thì không đủ làm lệch con số tổng.

Nhưng eBPF thấy ít hơn hẳn. Ba probe của tôi đếm được 8 201 lần recvfrom và 8 200 lần writev. Chúng không nói được request đó là GET hay POST, đường dẫn nào, mã trả về bao nhiêu. Muốn có những thứ đó phải tự phân tích byte trong bộ đệm — và nếu ứng dụng dùng TLS thì byte ấy đã mã hoá, phải chuyển sang gắn uprobe vào SSL_read/SSL_write của thư viện, tức là quay lại phụ thuộc vào chi tiết bên trong ứng dụng. Proxy thì biết ngay mã trả về, vì nó một máy chủ HTTP.

Đó mới là đánh đổi thật: 33,1 µs là giá của ngữ nghĩa HTTP, không phải giá của việc quan sát.

Dòng cuối bảng: đặt cả hai không tốn thêm gì. 81,9 µs so với 82,2 µs của proxy đơn thuần — chênh lệch nằm trong nhiễu. Đã trả tiền cho proxy rồi thì thêm eBPF gần như không mất gì.

Vì sao con số eBPF qua proxy là 24 601

Với proxy, cùng một bộ 8 200 request lại cho 24 601 lần recvfrom — đúng gấp 3,00 lần. Không phải sai số: bộ lọc của tôi bắt theo tên tiến trình nginx, mà trong cấu hình có hai nginx, và mỗi request đi qua ba lượt đọc:

  1. proxy đọc request từ client,
  2. app đọc request từ proxy,
  3. proxy đọc phản hồi từ app.

Đây là điều đáng nhớ với ai đang chạy service mesh: thêm một sidecar là nhân ba số sự kiện mà mọi công cụ eBPF trên máy đó phải xử lý. Chi phí eBPF vốn nhỏ, nhưng nó nhân lên theo số chặng, không theo số request.

Ba lần đo cho eBPF tốn đúng 0%

Đây là phần đáng giá nhất của bài này, và nó suýt không được viết ra.

Lần một: proxy nhanh hơn đường thẳng. Bộ đo đầu tiên dùng ab đo thông lượng, và cho proxy 49 349 req/s so với 48 452 req/s khi đi thẳng. Thêm một chặng mà nhanh hơn là chuyện không thể. CPU tố cáo lý do: app tốn 0,13 giây qua proxy nhưng 0,23 giây khi đi thẳng — vì ab mở kết nối mới cho mỗi request, còn proxy giữ sẵn 64 kết nối tới app. Tôi đang đo lợi ích của việc tái dùng kết nối chứ không phải chi phí của proxy. Cùng lúc đó, thông lượng nền dao động 42 544–49 349 req/s giữa các lần chạy (±13%), và ab chỉ báo độ trễ tròn tới mili giây — quá thô để thấy một chặng cỡ vài chục micro giây.

Chữa bằng bốn thay đổi: đo độ trễ tuần tự thay vì thông lượng, kết nối giữ sẵn cho cả hai đường, ghim mỗi thành phần vào một lõi CPU riêng, và đồng hồ micro giây.

Lần hai: probe gắn vào syscall mà ứng dụng không bao giờ gọi. Tôi đặt probe lên read. Kết quả: @doc: 0, và độ trễ trùng khít với nền. Nginx đọc socket bằng recvfrom, không bằng read. Đếm toàn bộ syscall cho thấy read, readv, sendto đều bằng 0, còn recvfrom 2 201 và writev 2 200 cho 2 200 request.

Lần ba: chương trình bpftrace chết ngay lúc khởi động. Cú pháp /A/ /B/ — hai khối điều kiện rời — không hợp lệ ở bpftrace 0.17; phải viết /A && B/. Tiến trình thoát tức khắc, tệp kết quả 0 byte, thông báo lỗi đi ra stderr mà tôi không đọc. Độ trễ đo được: 50,2 µs, so với nền 49,3 µs.

Cả ba lần đều cho ra một câu chuyện hoàn toàn thuyết phục: "eBPF gần như miễn phí". Kết luận ấy tình cờ đúng, và đó chính là chỗ nguy hiểm — tôi suýt đăng một con số đúng được rút ra từ một phép đo không hề chạy.

Thứ duy nhất bắt được cả ba là cột "sự kiện eBPF ghi nhận": một con số biết trước để đối chiếu. 8 200 request thì probe phải thấy 8 200 sự kiện. Thấy 0, hoặc thấy trống, là hỏng — bất kể độ trễ trông hợp lý đến đâu.

Chỗ tôi không kết luận được

Dịch vụ đo là nginx trả chuỗi tĩnh trong 49 µs. Một ứng dụng thật mất 5–50 ms mỗi request thì 33,1 µs của proxy chỉ còn là 0,07–0,7%, gần như không đáng kể. Tỷ lệ 24 lần là thật, nhưng ý nghĩa của nó phụ thuộc hoàn toàn vào việc ứng dụng của bạn nhanh cỡ nào. Proxy đắt khi dịch vụ nhanh, và rẻ khi dịch vụ chậm.

Đo trên một máy, mạng nội bộ Docker, không TLS. Bật TLS thì proxy phải giải mã và mã hoá lại — chi phí sẽ khác hẳn, và eBPF sẽ mất luôn khả năng đọc nội dung.

Thử ba mươi giây

Nếu bạn đang chạy một công cụ quan sát dựa trên eBPF, hãy hỏi nó một câu duy nhất: trong một phút vừa rồi nó ghi nhận bao nhiêu sự kiện? Rồi so với số request mà bạn biết chắc hệ thống đã phục vụ trong phút ấy.

Hai con số phải khớp. Nếu công cụ không trả lời được câu hỏi đó, bạn không có cách nào phân biệt "chi phí gần bằng không" với "không chạy".