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

Ảnh chụp đoạn mã nền tối TLS kết thúc ở đâu app proxy hay proxy rồi mã hoá lại với Go 1.23 net http và httputil ReverseProxy dist-lab chạy app và proxy client go-lab hop proxy app qua lo, bốn topology cùng một ứng dụng trả 256 byte hoặc 64 KB gồm T0 client plaintext tới app 9512 mốc so sánh không TLS ở đâu cả, T1 client TLS tới app 9510 TLS trong ứng dụng không hop thêm, T2 client TLS tới proxy 9511 plaintext tới app 9512 TLS termination ở proxy bên trong HTTP trần, T3 client TLS tới proxy 9513 TLS tới app 9510 mã hoá lại sidecar hai bắt tay hai lần mã hoá, proxy bằng httputil ReverseProxy pool 1000 kết nối idle tới app ForceAttemptHTTP2 vào app TLS bằng h2, đo gì ở đâu mỗi tiến trình app proxy mở cổng điều khiển trả CPU utime cộng stime và số request ra micro giây CPU mỗi request từng tầng client req trên giây p50 p99 phía gọi giao thức ALPN thương lượng h2 hay http 1.1 app thấy giao thức nào, A keep-alive 64 kết nối 6 giây 256 byte h2 và h1, B như A body 64 KB chi phí mã hoá theo byte, C mỗi request một kết nối mới 16 worker bắt tay ở tầng nào, D một kết nối tuần tự độ trễ mỗi hop, E tc netem 0,5 ms trên lo của dist-lab hop proxy app RTT 1 ms như proxy ở máy khác cùng DC, vì sao hop qua lo mà vẫn phải đo proxy và app cùng máy hop đi qua loopback không có độ trễ mạng nhưng có đủ chi phí phần mềm thêm một tiến trình parse HTTP copy body hai lần syscall gấp đôi T3 thêm một tầng TLS nữa netem trên lo bù phần độ trễ mạng không đo nginx Envoy chúng rẻ hơn ReverseProxy của Go nhưng tỉ lệ giữa các topology là thứ mang đi được

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

Ảnh chụp output thật nền tối TLS keep-alive gần 0 hop proxy gấp 3,7 CPU với go-lab tới dist-lab micro giây CPU mỗi request đo riêng từng tiến trình app proxy Go 1.23 aarch64, A keep-alive 64 kết nối 256 byte gồm T0 plaintext tới app h1 92643 req trên giây p50 0,33 ms p99 2,57 CPU app 31 micro giây tổng 31, T1 TLS ở app h1 103717 0,35 2,60 29 micro giây tổng 29 TLS keep-alive xấp xỉ 0, T1 TLS ở app h2 56146 1,10 2,33 31 micro giây h2 một kết nối, T2 proxy tới plaintext app h1 31543 1,83 6,39 app 30 proxy 110 tổng 140 hop thêm 110 micro giây gấp 4,5, T2 h2 19327 3,14 6,37 51 153 204, T3 proxy tới TLS app mã hoá lại h1 26001 2,31 5,77 35 112 147 app nhận h2, T3 h2 23573 2,70 5,07 39 115 154, T0 T1 bị chặn ở client CPU client 46 và 37 micro giây mỗi request tin CPU mỗi request của server đừng tin req trên giây, B body 64 KB T0 78201 req trên giây app 43 micro giây T1 54869 app 65 micro giây thêm 22 micro giây cho 64 KB bằng 0,34 ns mỗi byte khớp phần 4 T2 18442 app 42 cộng proxy 198 T3 8431 app 143 cộng proxy 300 mã hoá hai lần p50 7,79 ms, C mới T0 29722 req trên giây app 66 micro giây T1 6868 app 353 micro giây bắt tay nằm ở app T2 4719 app 52 cộng proxy 564 T3 5306 app 61 cộng proxy 456 bắt tay dồn về proxy app như T0, D c bằng 1 T0 0,09 ms T1 0,09 ms T2 0,21 ms T3 0,24 ms một hop loopback thêm 0,12 tới 0,15 ms, E hop proxy app RTT 1 ms netem 0,5 ms mỗi chiều trên lo gồm T2 c bằng 1 không netem 0,21 ms RTT 1 ms 1,65 ms T3 c bằng 1 0,24 lên 1,86 ms, T2 c bằng 64 31543 req trên giây 1,83 còn 23290 req trên giây 2,41 T3 c bằng 64 26001 xuống 22205 p50 2,31 lên 2,68, T3 kết nối mới c bằng 16 5306 req trên giây 2,81 còn 4838 req trên giây 3,16 pool tới app có sẵn hop không bắt tay lại, T1 không qua lo 0,09 ms cả hai đối chứng netem chỉ chạm hop nội bộ, nói thẳng với keep-alive TLS trong app gần như miễn phí cái đắt là hop proxy gấp 3,7 CPU thêm 0,12 ms và thêm 1 RTT nếu proxy ở máy khác proxy chỉ thắng khi có bão bắt tay nó gom hết chi phí handshake về một chỗ và app chỉ còn 52 tới 61 micro giây mỗi request

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ề

  1. 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.
  2. 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.
  3. 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

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ễ.