Năm phần trước đều giả định TLS kết thúc ngay trong tiến trình Go. Thực tế, câu hỏi đầu tiên khi dựng hệ thống thường là ngược lại: để app tự nói TLS, hay đặt một reverse proxy phía trước lo TLS rồi nói HTTP trần vào trong, hay dùng sidecar kết thúc TLS rồi mã hoá lại vào app như service mesh làm. Mỗi lựa chọn đổi chỗ chi phí mà phần 2 và phần 4 đã đo: bắt tay, mã hoá theo byte, và RAM mỗi kết nối, sang một tiến trình khác, có khi một máy khác, và thêm vào đó chi phí của chính hop mới. Bài này dựng cả bốn topology với cùng một app Go và một proxy Go, đo CPU của từng tiến trình trên mỗi request, rồi đặt netem lên hop nội bộ để thấy proxy ở máy khác sẽ tốn thêm gì.
Bốn topology và cách tách CPU từng tầng

Hình 1: Bốn topology T0 đến T3, cách đo CPU riêng từng tiến trình, năm phép đo A đến E, và lý do hop loopback vẫn đáng đo.
Bốn topology, cùng một handler trả 256 byte hoặc 64 KB. T0 client nói HTTP trần thẳng vào app, mốc so sánh. T1 app tự nói TLS, không có hop thêm. T2 một httputil.ReverseProxy kết thúc TLS rồi chuyển tiếp HTTP trần vào app qua loopback, mô hình nginx hay load balancer trước app. T3 proxy kết thúc TLS rồi mở TLS mới vào app, mô hình sidecar của service mesh: hai bắt tay, hai lần mã hoá cho mỗi byte. Proxy giữ pool 1.000 kết nối idle tới app và bật ForceAttemptHTTP2, nên trong T3 nó nói HTTP/2 với app qua ALPN dù client bên ngoài dùng HTTP/1.1.
Điểm mấu chốt của phép đo là tách CPU theo tiến trình. App và proxy mỗi cái mở một cổng điều khiển trả về utime cộng stime của chính nó và số request đã xử lý; client đọc trước và sau mỗi lượt tải, chia hiệu cho số request, ra µs CPU mỗi request của từng tầng. Nhờ vậy nhìn được không chỉ "tổng tốn bao nhiêu" mà "ai tốn". Năm phép đo: A keep-alive 64 kết nối trong 6 giây với body 256 byte, chạy cả HTTP/2 lẫn HTTP/1.1 để thấy ALPN chọn gì và chọn vậy tốn gì; B như A với body 64 KB, để chi phí mã hoá theo byte lộ ra; C mỗi request một kết nối mới với 16 worker, để thấy bắt tay rơi vào tầng nào; D một kết nối tuần tự, để đo độ trễ thuần của từng hop không bị hàng đợi che; E tc netem 0,5 ms mỗi chiều trên lo của dist-lab, biến hop proxy sang app thành RTT 1 ms như hai máy trong cùng trung tâm dữ liệu, trong khi đường client sang app qua eth0 không đổi, làm đối chứng.
Đo thật: keep-alive gần 0, hop ×3,7, bắt tay dồn về proxy

Hình 2: CPU từng tầng và độ trễ cho bốn topology với keep-alive, body 64 KB, kết nối mới mỗi request, một kết nối tuần tự, và hop nội bộ có RTT 1 ms.
| phép đo | T0 plaintext | T1 TLS ở app | T2 proxy → plaintext | T3 proxy → TLS |
|---|---|---|---|---|
| A keep-alive 256 B, CPU app + proxy | 31 µs | 29 µs | 30 + 110 µs | 35 + 112 µs |
| A p50, HTTP/1.1 | 0,33 ms | 0,35 ms | 1,83 ms | 2,31 ms |
| B 64 KB, CPU app + proxy | 43 µs | 65 µs | 42 + 198 µs | 143 + 300 µs |
| C kết nối mới, CPU app + proxy | 66 µs | 353 µs | 52 + 564 µs | 61 + 456 µs |
| D một kết nối, p50 | 0,09 ms | 0,09 ms | 0,21 ms | 0,24 ms |
| E hop RTT 1 ms, p50 một kết nối | — | 0,09 ms | 1,65 ms | 1,86 ms |
Với keep-alive và request nhỏ, TLS trong app không tốn gì đo được. App plaintext 31 µs CPU mỗi request, app TLS 29 µs; chênh lệch nằm trong nhiễu của bộ đếm 10 ms. Mã hoá 256 byte bằng AES-GCM có lệnh phần cứng là vài chục nanô giây theo phần 4, chìm hẳn trong 30 µs mà Go dành cho parse HTTP, goroutine, và hai syscall. Req/s của T0 và T1 cần đọc cẩn thận: 92.643 so với 103.717 nghe như TLS nhanh hơn plaintext, nhưng cả hai đều bị chặn ở client, nơi tốn 46 và 37 µs mỗi request, nên hai con số đó đo client chứ không đo server; con số mang đi được là CPU mỗi request phía server. Với body 64 KB, chi phí mới hiện: 65 µs so với 43, tức 22 µs cho 64 KB, tương đương 0,34 ns mỗi byte, khớp với 0,42 ns mỗi byte cho cả hai chiều ở phần 4.
Hop qua proxy tốn 110 µs mỗi request, gấp 3,7 lần chính app, và cộng 0,12 ms độ trễ. T2: app vẫn 30 µs, proxy thêm 110 µs, tổng 140 so với 29 của T1; p50 từ 0,35 lên 1,83 ms ở 64 kết nối. Mục D với một kết nối tuần tự cho độ trễ thuần: 0,09 ms cho T0 và T1, 0,21 cho T2, 0,24 cho T3, tức một hop loopback thêm 0,12 đến 0,15 ms dù không có mạng nào ở giữa. Phần lớn 110 µs là việc ReverseProxy làm: parse request lần hai, dựng request mới, copy body qua hai socket, cộng với chi phí lập lịch khi request đi qua hai tiến trình. Proxy viết bằng C như nginx hay HAProxy sẽ rẻ hơn con số này đáng kể, nhưng không về 0; tỉ lệ "hop proxy đắt hơn một app Go trả 256 byte" là thứ bền.
Mã hoá lại biến 64 KB thành việc đắt nhất bảng. T3 với body 64 KB: app 143 µs, proxy 300 µs, tổng 443 so với 65 của T1, p50 7,79 ms so với 0,80, req/s 8.431 so với 54.869. Mỗi byte được mã hoá ở app, giải mã ở proxy, mã hoá lại ở proxy, giải mã ở client: bốn lần thay vì hai, và app nhận thêm phần nói HTTP/2 với proxy. Với request nhỏ, T3 chỉ đắt hơn T2 vài µs; với payload lớn, sidecar mã hoá lại là nơi CPU biến mất, và đây là chi phí mà các service mesh trả cho "mTLS mọi nơi" mà không phải lúc nào cũng được nêu rõ.
Khi mỗi request là một kết nối mới, proxy gom hết bắt tay và app rẻ như plaintext. Mục C: T1 app phải bắt tay mỗi request, 353 µs, đúng cỡ 230 đến 300 µs bắt tay cộng 30 µs xử lý mà phần 4 và 2 đo. T2: app chỉ 52 µs, gần bằng 66 µs của T0 kết nối mới, vì proxy giữ pool kết nối tới app và bắt tay TLS với client rơi hết vào proxy, 564 µs. T3 tương tự, 61 và 456. Đây là lợi ích thật của termination ở proxy: nó cô lập bão bắt tay, lúc 10.000 client cùng nối lại sau deploy, khỏi tiến trình chạy logic nghiệp vụ, và cho phép scale tầng TLS độc lập với tầng app. Mục E xác nhận pool hoạt động: với hop 1 ms RTT, T3 kết nối mới chỉ chậm thêm 0,35 ms chứ không thêm hai RTT của một bắt tay mới, vì proxy tái dùng kết nối đã có tới app.
Proxy ở máy khác cộng ít nhất một RTT vào mỗi request. Mục E đặt 0,5 ms mỗi chiều trên lo, hop nội bộ thành RTT 1 ms, đường client đi eth0 không đổi; T1 vẫn 0,09 ms làm đối chứng. T2 một kết nối: 0,21 lên 1,65 ms; T3: 0,24 lên 1,86. Cả hai tăng hơn 1 ms, tức nhiều hơn một RTT lý thuyết; phần dư không được truy tiếp, nhưng điểm chính không đổi: hop xa cộng ít nhất một RTT mạng vào mọi request, keep-alive hay không, và ở 64 kết nối nó cắt req/s của T2 từ 31.543 xuống 23.290 vì mỗi kết nối chờ lâu hơn. Proxy cùng máy với app, như sidecar, tránh được khoản này và chỉ còn chi phí phần mềm 0,12 ms.
HTTP/2 không tự động nhanh hơn, và ALPN quyết định ai nói gì với ai. Client Go mặc định thương lượng h2 qua ALPN; với 64 worker, nó dùng một kết nối TCP và ghép 64 stream, nên T1 h2 chỉ đạt 56.146 req/s với p50 1,10 ms so với 103.717 và 0,35 của HTTP/1.1 trên 64 kết nối: một kết nối là một hàng đợi, frame h2 có chi phí, và cả hai phía chỉ dùng được một luồng cho kết nối đó. Qua proxy, h2 còn tệ hơn: T2 h2 19.327 req/s so với 31.543 h1, app tốn 51 µs thay vì 30 vì proxy mở nhiều kết nối HTTP/1.1 vào app để phục vụ các stream song song. Trong T3, app thấy HTTP/2.0 dù client nói h1, vì proxy và app tự thương lượng h2 với nhau. HTTP/2 thắng ở mạng thật với RTT lớn và nhiều tài nguyên nhỏ; trong trung tâm dữ liệu với RTT dưới 1 ms, HTTP/1.1 keep-alive trên nhiều kết nối thường rẻ hơn.
Đánh đổi cần cân nhắc
TLS trong app là mặc định đúng cho dịch vụ nói với ít client bền. Chi phí keep-alive bằng 0 đo được, không có hop, không có tiến trình thứ hai để vận hành. Khi bắt tay là vấn đề, tức nhiều client ngắn hạn hoặc bão nối lại, proxy termination trả lại cho app 300 µs mỗi kết nối, đổi bằng 110 µs mỗi request và 0,12 ms, hoặc 1 RTT nếu khác máy. Tính bằng số kết nối mới trên số request để biết phía nào nặng hơn.
Sidecar mã hoá lại là chi phí theo byte, không theo request. Với API trả vài trăm byte, T3 chỉ đắt hơn T2 vài µs. Với dịch vụ trả file, ảnh, hay response vài chục KB, mỗi byte đi qua bốn lần AEAD và hai tiến trình; con số 443 so với 65 µs cho 64 KB là cái giá của "mTLS mọi nơi". Nếu RAM cũng là ràng buộc, nhớ phần 2: mỗi hop TLS giữ thêm 25 KB cho mỗi kết nối, và T3 có hai hop.
Phạm vi đo. Proxy là httputil.ReverseProxy thuần Go, không tối ưu; nginx, HAProxy, Envoy sẽ cho hop rẻ hơn, có thể một nửa. Tất cả chạy trên một máy 10 lõi ảo hoá, client và server chia sẻ CPU, nên req/s tuyệt đối không mang đi được; CPU mỗi request của từng tiến trình và tỉ lệ giữa các topology thì có. Chưa đo: HTTP/2 với nhiều kết nối phía client, TLS offload bằng kTLS hay thẻ mạng, và proxy giữ kết nối client dài trong khi app restart, lợi ích vận hành lớn nhất của termination mà bài này không chạm tới.
Ba ý mang về
- Với keep-alive, TLS trong app không tốn gì đo được: 29 so với 31 µs. Đừng thêm proxy vì sợ TLS; hãy thêm vì cần cô lập bắt tay, vì app không giữ nổi kết nối khi restart, hoặc vì cần routing.
- Mỗi hop proxy là 110 µs CPU và 0,12 ms trên cùng máy, cộng một RTT nếu khác máy; mã hoá lại nhân chi phí theo byte lên hai lần. Request 256 byte không cảm thấy; response 64 KB qua sidecar chậm 6,5 lần so với TLS trong app.
- Khi kết nối ngắn, proxy gom bắt tay và app về 52 µs thay vì 353. Nhìn tỉ lệ kết nối mới trên request của hệ thống bạn trước khi chọn; đó là con số quyết định termination nằm ở đâu.
Nguồn
- Go
net/http/httputil—ReverseProxy,Transport, cách tái dùng kết nối upstream: https://pkg.go.dev/net/http/httputil#ReverseProxy - Go
net/http—Transport.ForceAttemptHTTP2,MaxIdleConnsPerHost,TLSNextProtođể tắt HTTP/2: https://pkg.go.dev/net/http#Transport - Go
golang.org/x/net/http2/h2c— HTTP/2 không TLS cho hop nội bộ: https://pkg.go.dev/golang.org/x/net/http2/h2c - RFC 7301 — TLS Application-Layer Protocol Negotiation Extension (ALPN): https://www.rfc-editor.org/rfc/rfc7301
- RFC 9113 — HTTP/2, mục 5 Streams and Multiplexing: https://www.rfc-editor.org/rfc/rfc9113#section-5
- Istio — Performance and Scalability, chi phí sidecar theo request: https://istio.io/latest/docs/ops/deployment/performance-and-scalability/
- Linux
tc-netem(8)— mô phỏng độ trễ trên một interface: https://man7.org/linux/man-pages/man8/tc-netem.8.html
Phần sau quay về sự cố phổ biến nhất của TLS trong vận hành: chứng chỉ hết hạn. Mô phỏng vòng đời ngắn với CA nội bộ, viết bộ kiểm tra hạn còn lại cho mọi endpoint, xoay tự động trước hạn bằng chính cơ chế GetCertificate của phần 5, và đo khoảng thời gian không có chứng chỉ hợp lệ khi quy trình xoay chạy trễ.