Phần trước đo traceparent bị đánh rơi khi một chặng quên chuyển tiếp nó, và kết luận rằng cách chữa là dùng bộ công cụ tự động thay vì truyền tay. Bài này đo cái giá của lời khuyên đó: opentelemetry-instrumentation-requests đắt hơn việc tự viết span bao nhiêu, và đổi lại được gì.
Bảng số liệu
Một client Python gọi HTTP tới một dịch vụ trong cùng mạng Docker. Ba mươi vòng, mỗi vòng 400 lời gọi cho mỗi biến thể; trung vị của ba lần chạy toàn bài.
| Cách làm | ns mỗi lời gọi | Chi phí thêm | So với nền |
|---|---|---|---|
| Không trace | 221 993 | — | — |
| Gắn tay (tự tạo span, tự đặt 3 thuộc tính) | 241 797 | +19 804 ns | +8,9% |
Gắn tự động (RequestsInstrumentor) |
270 544 | +48 551 ns | +21,9% |
Gắn tự động đắt gấp 2,45 lần gắn tay.
Đổi lại được gì:
| Gắn tay | Gắn tự động | |
|---|---|---|
| Số thuộc tính | 3 | 4 |
| Thuộc tính | http.method, http.url, http.status_code |
thêm user_agent.original |
| Tên span | HTTP GET (tôi tự đặt) |
GET (theo chuẩn) |
| Số span thu được | 12 300 | 12 300 |
Điều đáng nhớ
Gắn tự động đắt gấp 2,45 lần, và thứ nó cho thêm chỉ là một thuộc tính. Nếu chỉ nhìn bảng này, kết luận có vẻ hiển nhiên: viết tay đi, rẻ hơn hai lần rưỡi.
Nhưng đó là so sánh sai. Chi phí không phải là thứ phân biệt hai cách làm — cái phân biệt chúng là cái giá của việc quên. Gắn tay đúng một chỗ thì mất một span ở đó, và không có cảnh báo nào. Phần trước của sê-ri đã đo hậu quả: một chặng đánh rơi ngữ cảnh làm tỷ lệ có trace đầy đủ rơi từ 9,93% xuống 1,01%.
Gắn tự động thì không thể quên. Một dòng instrument() áp dụng cho mọi lời gọi trong tiến trình, kể cả những lời gọi nằm trong thư viện bên thứ ba mà bạn không sửa được.
Và ở quy mô thật, cả hai đều nhỏ. Một lời gọi HTTP thật tốn 222 µs; 48,5 µs thêm vào là 21,9%. Với lời gọi qua mạng thật, độ trễ nền là hàng mili giây và cùng 48,5 µs đó thành dưới 5%. Chi phí này hiếm khi là lý do chính đáng để chọn.
Vì sao
Gắn tay chỉ làm đúng những gì tôi viết: mở span, đặt ba thuộc tính, đóng span. 19,8 µs cho toàn bộ chuyện đó — khớp với con số 8,4 µs đo được ở phần 21 cho việc tạo span thuần, cộng thêm phần đặt thuộc tính và quản lý ngữ cảnh.
Bộ công cụ tự động làm nhiều hơn hẳn, và phần lớn là những việc bạn sẽ quên nếu viết tay. Nó vá vào Session.send, kiểm tra xem có đang bị tắt tạm thời không, dựng tên span theo đúng quy ước ngữ nghĩa của OpenTelemetry, tiêm traceparent vào header của lời gọi đi ra, đọc lại mã trạng thái để đặt trạng thái span, và gọi các hook do người dùng cấu hình. Cộng lại thành 48,5 µs.
Đáng chú ý là bản gắn tay của tôi không tiêm traceparent. Tôi chỉ tạo span cho phía client; dịch vụ được gọi sẽ không biết gì về span đó và sẽ bắt đầu một trace mới. Nói cách khác, bản gắn tay rẻ hơn một phần vì nó làm ít hơn — và cái nó bỏ qua chính là thứ phần trước của sê-ri đã chứng minh là đắt nhất khi thiếu.
Nghĩa là gì trong thực tế
- Dùng gắn tự động làm nền, gắn tay để bổ sung. Bộ công cụ lo phần khung — tạo span, truyền ngữ cảnh, đặt thuộc tính chuẩn. Bạn thêm span thủ công cho những đoạn nghiệp vụ mà không thư viện nào biết là quan trọng.
- Đừng chọn gắn tay để tiết kiệm 28,7 µs mỗi lời gọi. Với lời gọi HTTP qua mạng thật, đó là phần nghìn của độ trễ. Chọn nó chỉ khi bạn có lý do khác — ví dụ thư viện không có bộ công cụ tự động.
- Nếu viết tay, đừng quên tiêm
traceparent. Đây là phần dễ bỏ sót nhất và là phần duy nhất thực sự quan trọng. Span phía client mà không có ngữ cảnh truyền đi thì chỉ là một cái đồng hồ bấm giờ đắt tiền. - Kiểm tra tên span và thuộc tính sau khi bật gắn tự động. Tên chuẩn là
GET, không phảiHTTP GET— nếu bảng điều khiển của bạn lọc theo tên span, việc chuyển sang gắn tự động sẽ làm chúng trống trơn.
Có một cách đọc bảng này khiến quyết định trở nên dễ hơn. Hãy hỏi: nếu một lập trình viên mới vào đội thêm một lời gọi HTTP ở đâu đó trong mã, span của lời gọi đó có xuất hiện không? Với gắn tự động, câu trả lời là có, không cần ai nhắc. Với gắn tay, câu trả lời phụ thuộc vào việc người đó có biết quy ước của đội hay không, có nhớ hay không, và có ai xem lại hay không.
Đó là khác biệt thật giữa hai cách, và nó không xuất hiện ở bất kỳ đâu trong bảng đo. Chi phí 28,7 µs mỗi lời gọi là thứ đo được và nhỏ; xác suất một chỗ bị bỏ sót là thứ không đo được và lớn dần theo thời gian, theo số người, theo số lần mã được sửa.
Chỗ tôi không kết luận được
Đây là Python. Chi phí 48,5 µs cho một lớp bọc là con số của ngôn ngữ động với việc vá thời gian chạy. Bộ công cụ tự động cho Java dùng biến đổi bytecode, cho Go thì thường phải sửa mã nguồn — cả hai đều có đặc tính chi phí hoàn toàn khác.
Tôi chỉ đo một bộ công cụ, cho một thư viện. opentelemetry-instrumentation-requests là một trong hàng chục bộ; bộ cho khung web hay cho driver cơ sở dữ liệu làm nhiều việc hơn và chắc chắn đắt hơn.
Độ biến thiên của nền là 19,6% tới 39,6% giữa vòng thấp nhất và cao nhất, vì nền là một lời gọi HTTP thật. Chi phí tôi đo — 19,8 và 48,5 µs trên nền 222 µs — nằm trong khoảng 9% tới 22%, tức là đủ lớn để tách khỏi nhiễu, nhưng không nhiều. Tôi tin thứ tự và tỷ lệ 2,45 lần; tôi không tin chữ số cuối của từng con số.
Một phép đo phải làm lại hai lần, và lần thứ hai vẫn sai. Bản đầu tiên đo ba biến thể tuần tự và cho kết quả vô lý: gắn tay ra −9% và −13% ở hai trong ba lần chạy — tức là thêm việc lại nhanh hơn. Tôi sửa bằng cách đo xen kẽ ba biến thể trong cùng mỗi vòng để nhiễu phân bố đều.
Kết quả vẫn sai, chỉ sai theo kiểu khác: lần này gắn tự động ra gần bằng 0 chi phí thêm. Lúc đó tôi mới thấy nguyên nhân thật — RequestsInstrumentor().instrument() vá toàn cục vào lớp Session, không vá vào một đối tượng. Tôi tạo ba Session riêng và tưởng chỉ một cái bị gắn công cụ; thực ra cả ba đều bị, kể cả cái tôi dùng làm nền. "Nền" và "tự động" là cùng một thứ, nên hiệu của chúng bằng 0, và "gắn tay" thực chất đang đo nền đã instrument cộng thêm span thủ công.
Cách chữa đúng là đo nền và gắn tay trước khi gọi instrument(), rồi mới bật lên và đo tự động. Bài học: với thư viện vá thời gian chạy, không có cách nào giữ một đối tượng "sạch" để làm đối chứng — ranh giới duy nhất là thời gian, trước và sau lời gọi vá.
Thử ba mươi giây
docker run --rm python:3.12-slim sh -c '
pip -q install requests opentelemetry-sdk opentelemetry-instrumentation-requests
python - <<EOF
import requests
from opentelemetry.instrumentation.requests import RequestsInstrumentor
s1 = requests.Session()
print("truoc instrument, Session.send la:", requests.Session.send.__qualname__)
RequestsInstrumentor().instrument()
s2 = requests.Session()
print("sau instrument, Session.send la:", requests.Session.send.__qualname__)
print()
print("s1 tao TRUOC khi instrument, nhung no dung ham nao?")
print(" s1.send is s2.send ve cung mot ham lop:", s1.send.__func__ is s2.send.__func__)
EOF'
Dòng cuối in True. Đó là lý do không thể giữ một Session làm đối chứng sạch, và là cái bẫy đã làm hỏng hai lần đo của tôi.