Hình dung hai nhà ngoại giao nói chuyện qua một cặp phiên dịch có thẻ căn cước: mọi câu được dịch sang mật mã và mỗi bên biết chắc người kia là ai — tự động, không ai phải ký giấy tờ gì. Nhưng có một điều kiện thầm lặng: cả hai phải mang phiên dịch tới. Nếu một bên đến tay không, cuộc nói chuyện vẫn diễn ra — chỉ là bằng tiếng bản xứ, công khai, ai đứng gần cũng nghe. Đây chính xác là mTLS của service mesh: nó bảo vệ khi hai bên đều có proxy, và im lặng để lọt khi một bên không có. Bài này cài Linkerd lên cụm thật để đo cả cái nó cho lẫn cái nó lặng lẽ không cho.

Service mesh hứa mTLS, quan sát và điều khiển lưu lượng mà không sửa mã ứng dụng. Ta đo.

Độ trễ: cùng lời gọi, trước và sau

200 yêu cầu mỗi lần, qua ClusterIP, ba lần mỗi cấu hình.

p50 (ms) p95 (ms)
Trước 0,14 / 0,13 / 0,13 0,22 / 0,21 / 0,17
Sau 0,31 / 0,31 / 0,36 0,48 / 0,44 / 0,66

Chậm hơn khoảng 0,19 ms — gần 2,4 lần. Con số tương đối nghe đáng sợ, con số tuyệt đối thì không. Đó là hai chặng proxy: một ở pod gọi, một ở pod nhận. Với một dịch vụ HTTP thật, thời gian xử lý thường tính bằng mili giây, nên 0,19 ms là dưới 5%.

Nhưng với một chuỗi mười dịch vụ gọi nhau, nó thành gần 2 ms cộng dồn — và với dịch vụ nội bộ chỉ tra một khoá trong bộ nhớ đệm, nó có thể nhân đôi thời gian phản hồi. Ranh giới nằm ở chỗ dịch vụ của bạn nhanh cỡ nào, không nằm ở mesh.

Tài nguyên: nhẹ hơn tôi tưởng

linkerd-proxy trong pod   3-5 Mi RAM, 1m CPU mỗi cái
control plane (3 pod)     83 Mi tổng

Tôi vào bài này với ấn tượng "sidecar tốn hàng trăm MB". Với Linkerd thì không: proxy viết bằng Rust và đo được 3–5 MB. Nhưng đừng suy ra cho mọi mesh. Các mesh dựa trên Envoy nặng hơn đáng kể — đó là điều tôi không đo trong bài này, và đo trên một cụm rỗng thì cũng không nói được gì về lúc tải cao. Con số 5 MB đúng cho Linkerd, ở trạng thái nhàn rỗi, trên cụm này.

Chi phí thật hơn không phải RAM mà là số lượng: mỗi pod thành hai container. 500 pod là 500 proxy phải nâng cấp, và nâng cấp mesh nghĩa là khởi động lại toàn bộ pod trong cụm.

Hai chi tiết gặp lại từ các phần trước

Proxy chạy như sidecar native — initContainers với restartPolicy: Always, đúng cơ chế đo ở phần 36. Nhờ vậy nó khởi động trước ứng dụng và dừng sau, nên không còn cảnh Job có mesh thì không bao giờ kết thúc. Và ảnh proxy là distroless:

kubectl exec ... -c linkerd-proxy -- /bin/sh
-> stat /bin/sh: no such file or directory

Đúng tình huống ở phần 42. Muốn nhìn vào trong nó thì dùng kubectl debug --target.

mTLS bật sẵn, không khai gì cả

Đọc thẳng metric của proxy trong pod gọi:

tcp_open_total{direction="outbound", tls="true",
               server_id="default.mesh.serviceaccount.identity..."}

tls="true" và có danh tính của phía bên kia. Không có tệp cấu hình nào, không có chứng chỉ nào tôi phải tạo. Đây đúng là thứ mesh bán, và nó giao đúng hàng. Danh tính đó dựa trên ServiceAccount — chính thứ đo ở phần 33. Mesh dùng lại token đó để mỗi bên chứng minh mình là ai.

Nhưng cài mesh không tự nó chặn gì

Tôi tạo một pod ở namespace không có mesh rồi gọi vào Service trong mesh:

p50 0,24 ms, không lỗi

Chạy bình thường. Không mã hoá, không danh tính, không ai hỏi gì — đúng người đến tay không, cuộc nói chuyện vẫn diễn ra bằng tiếng bản xứ. Đây là điểm quan trọng nhất của bài. mTLS của mesh là cơ hội: hai bên đều có proxy thì mã hoá, một bên không có thì đi thẳng bằng văn bản rõ. Cài mesh xong rồi báo cáo "lưu lượng nội bộ đã được mã hoá" là sai, trừ khi bạn thêm chính sách bắt buộc.

Với Linkerd đó là Server + AuthorizationPolicy; với Istio là PeerAuthentication ở chế độ STRICT. Cả hai đều là bước riêng, không tự bật. Và điều đó hợp lý: bật cưỡng chế ngay lúc cài là cắt đứt mọi thứ chưa được tiêm — bộ giám sát, job cũ, dịch vụ ngoài cụm. Nhưng "hợp lý về vận hành" và "an toàn ngay sau khi cài" là hai chuyện khác nhau, và người ta hay nhầm.

Vậy có nên dùng không

Mesh đáng giá khi bạn cần ít nhất hai trong ba thứ sau, trên nhiều dịch vụ:

  • mTLS giữa các dịch vụ, có danh tính, không sửa mã.
  • Quan sát đồng nhất: tỷ lệ thành công, độ trễ, đồ thị phụ thuộc — không cần mỗi đội tự gắn thư viện.
  • Điều khiển lưu lượng: chia phần trăm, thử lại, ngắt mạch, phát hành từng phần.

Chỉ cần một trong ba thì thường có cách rẻ hơn: NetworkPolicy cho phân tách (phần 19), thư viện đo sẵn cho quan sát, Ingress cho chia lưu lượng ở biên.

Và cái giá ít ai tính trước: mesh là một tầng nữa phải gỡ rối. Khi có sự cố, câu hỏi "lỗi ở ứng dụng hay ở proxy" sẽ xuất hiện mỗi lần. Với ba dịch vụ thì cái giá đó lớn hơn lợi ích; với ba trăm thì ngược lại.

Nếu quyết định dùng

Ba điều đo được trong bài gợi ra thứ tự triển khai:

  1. Tiêm dần theo namespace, đừng tiêm cả cụm một lượt. Nhãn linkerd.io/inject=enabled đặt ở namespace và chỉ có tác dụng khi pod được tạo lại — nên bạn kiểm soát được nhịp độ.
  2. Đo độ trễ trước và sau cho đúng dịch vụ của bạn. 0,19 ms ở đây không phải con số của bạn.
  3. Bật cưỡng chế mTLS sau cùng, khi đã chắc mọi thứ cần nói chuyện đều đã có proxy. Trước đó, đừng nói với ai rằng lưu lượng đã được mã hoá.

Mẫu số chung: mặc định "mở" chỉ bảo vệ khi bạn thêm cưỡng chế

Cái bẫy "cài mesh không tự chặn gì" là một dạng của phân biệt nền tảng trong bảo mật: mặc định mở (permissive / fail-open / opportunistic) so với mặc định đóng (fail-closed / enforced). mTLS cơ hội của mesh mở theo thiết kế — nó nâng cấp lên mã hoá khi được, nhưng lùi về văn bản rõ khi không, và không bao giờ từ chối. Nên "đã cài" khác hẳn "đang cưỡng chế", và báo cáo trạng thái bảo mật theo cái được cài thay vì cái được enforce là sai lầm lặp đi lặp lại.

Trong thế giới Java và web, đây đúng là khác biệt giữa STARTTLS cơ hội (dùng TLS nếu máy chủ hỗ trợ, im lặng lùi về plaintext nếu không) và TLS bắt buộc cộng HSTS (từ chối kết nối không mã hoá) — một cái fail-open, một cái fail-closed. Cũng là khác biệt giữa một filter Spring Security ghi log nhưng không chặn và cấu hình denyAll() mặc định từ chối rồi mới mở đúng chỗ cần. Và ở tầng mạng, đây là firewall default-allow so với default-deny: cái đầu chỉ an toàn nếu bạn nhớ chặn hết mọi lỗ, cái sau an toàn ngay cả với cái bạn quên.

Mẫu số chung: một cơ chế bảo vệ mặc-định-mở chỉ thật sự bảo vệ sau khi bạn thêm chính sách cưỡng chế — nên "đã cài/đã bật" không đồng nghĩa "đang được thực thi", và trạng thái bảo mật phải đo bằng cái bị từ chối khi vi phạm, không bằng cái đã cài đặt. Câu hỏi đúng không phải "tôi đã bật mTLS/TLS/auth chưa" mà là "một kết nối không tuân thủ có bị chặn không, hay nó vẫn lọt qua trong im lặng" — thử đúng cái trường hợp vi phạm mới biết. Với mesh, hãy tạo một pod ngoài mesh và xem nó có gọi vào được không; nếu được, bạn chưa cưỡng chế gì cả.

Nếu đã có mesh, muốn tự kiểm lưu lượng thật sự được mã hoá bao nhiêu phần:

kubectl exec <pod-co-mesh> -c <container-ung-dung> -- \
  python3 -c "
import urllib.request
d = urllib.request.urlopen('http://127.0.0.1:4191/metrics', timeout=5).read().decode()
co = khong = 0
for l in d.split('\n'):
    if 'tcp_open_total' not in l or 'outbound' not in l: continue
    n = float(l.rsplit(' ', 1)[-1] or 0)
    if 'tls=\"true\"' in l: co += n
    elif 'loopback' not in l: khong += n
print(f'ma hoa {co:.0f} / khong ma hoa {khong:.0f}')
"

Cột thứ hai lớn hơn 0 nghĩa là có luồng đang đi bằng văn bản rõ — thường là tới một dịch vụ chưa được tiêm mà không ai để ý.

Phần sau: chạy cơ sở dữ liệu trong Kubernetes — đo, và nói rõ khi nào không nên.