Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang xây một hệ thống thương mại điện tử. Service checkout cần gọi service inventory để kiểm tra tồn kho trước khi cho khách đặt hàng. Ở thời điểm ban đầu, khi hệ thống còn nhỏ, bạn hard-code địa chỉ: inventory đang chạy ở 10.0.1.5:8080. Mọi thứ hoạt động trơn tru — cho đến một ngày.
Ngày đó, đội DevOps deploy phiên bản mới của inventory. Container cũ bị kill, container mới khởi động và được cấp một IP hoàn toàn khác: 10.0.3.12. Ngay lập tức, checkout bắt đầu ném lỗi connection refused hàng loạt. Khách hàng không thể thanh toán. Bạn phải vội vàng sửa config, redeploy checkout, và cầu nguyện rằng IP không đổi lần nữa.
Đây chính là bài toán trung tâm mà Service Discovery giải quyết: trong một hệ thống phân tán hiện đại, nơi các service được đóng gói trong container, tự động scale lên xuống theo tải, và bị thay thế liên tục bởi các cơ chế như rolling update hay auto-healing, thì địa chỉ mạng của một service không còn là hằng số. Nó là một biến số thay đổi liên tục.
Service Discovery là cơ chế cho phép service A tìm ra "hiện tại service B đang ở những địa chỉ nào" một cách tự động, mà không cần con người can thiệp. Đây là một trong những viên gạch nền tảng phân biệt kiến trúc microservices "chạy được trên slide" với kiến trúc microservices "chạy được thật ở production". Nếu bạn quyết định tách hệ thống thành nhiều service (Bài 17), bạn buộc phải trả lời câu hỏi: các service đó tìm nhau bằng cách nào? Bài học này sẽ trang bị cho bạn khung tư duy và các pattern để đưa ra quyết định đó.
Khái niệm cốt lõi
Service Registry — cuốn danh bạ trung tâm
Trái tim của gần như mọi giải pháp service discovery là một thành phần gọi là Service Registry (sổ đăng ký service). Hãy hình dung nó như một cuốn danh bạ điện thoại luôn được cập nhật: mỗi khi một instance của service khởi động, nó "gọi điện" báo với registry rằng "tôi là inventory, tôi đang ở 10.0.3.12:8080, tôi khỏe mạnh". Khi instance đó chết hoặc bị scale down, entry của nó bị xóa khỏi danh bạ.
Registry lưu trữ ba loại thông tin quan trọng: tên logic của service (ví dụ inventory), danh sách các instance đang sống (IP + port của từng bản sao), và trạng thái sức khỏe của từng instance. Bất kỳ service nào muốn gọi inventory chỉ cần hỏi registry: "cho tôi danh sách instance khỏe mạnh của inventory" thay vì phải biết IP cụ thể.
Ba câu hỏi cần trả lời
Mọi thiết kế service discovery phải trả lời ba câu hỏi:
- Registration — Đăng ký thế nào? Instance làm sao báo cho registry biết sự tồn tại của mình?
- Discovery — Tra cứu thế nào? Client làm sao lấy được địa chỉ hiện tại của service đích?
- Health checking — Kiểm tra sức khỏe thế nào? Registry làm sao biết một instance đã chết để loại nó ra?
Self-registration vs Third-party registration
Về cách đăng ký, có hai trường phái.
Self-registration (tự đăng ký): bản thân instance chịu trách nhiệm gọi API của registry để đăng ký khi khởi động và hủy đăng ký khi tắt. Nó cũng tự gửi tín hiệu "heartbeat" (nhịp tim) định kỳ để chứng minh mình còn sống. Ưu điểm là đơn giản, service biết rõ trạng thái của chính nó. Nhược điểm là code đăng ký bị trộn lẫn vào business logic của service, và mỗi ngôn ngữ lập trình phải tự viết lại logic này. Netflix Eureka là ví dụ điển hình của trường phái này.
Third-party registration (đăng ký qua bên thứ ba): có một thành phần độc lập gọi là "registrar" quan sát vòng đời của các instance (ví dụ lắng nghe sự kiện từ Docker hoặc orchestrator) và tự động đăng ký/hủy đăng ký thay cho chúng. Ưu điểm là service không cần biết gì về registry, giữ được sự tách biệt (decoupling). Đây là mô hình mà Kubernetes sử dụng: bạn không viết code đăng ký, Kubernetes tự làm việc đó dựa trên định nghĩa Pod và Service.
Client-side discovery vs Server-side discovery
Đây là quyết định kiến trúc quan trọng nhất trong bài, nên tôi sẽ giải thích kỹ.
Client-side discovery: client (service đang gọi) tự hỏi registry để lấy danh sách instance, rồi tự mình chọn một instance để gọi (thường bằng thuật toán load balancing như round-robin). Toàn bộ logic "chọn ai để gọi" nằm ở phía client.
checkout ──(1) hỏi: instance của inventory?──▶ Registry
checkout ◀──(2) trả về: [10.0.3.12, 10.0.3.18]── Registry
checkout ──(3) tự chọn 10.0.3.12 rồi gọi thẳng──▶ inventory instance
Ưu điểm: ít "chặng nhảy" mạng (không qua trung gian), client có toàn quyền kiểm soát chiến lược load balancing (ví dụ ưu tiên instance cùng availability zone để giảm latency). Nhược điểm: mỗi client phải nhúng thư viện discovery, và logic này phải viết lại cho từng ngôn ngữ. Netflix Eureka + Ribbon là bộ đôi kinh điển của mô hình này.
Server-side discovery: client chỉ cần gọi tới một địa chỉ ổn định (thường là một load balancer hoặc một virtual IP). Thành phần trung gian này mới là bên hỏi registry và định tuyến request tới instance phù hợp. Client hoàn toàn không biết về sự tồn tại của registry.
checkout ──(1) gọi tới địa chỉ ổn định: inventory.internal──▶ Load Balancer
Load Balancer ──(2) hỏi registry + chọn instance──▶ inventory instance
Ưu điểm: client cực kỳ đơn giản, không cần thư viện gì đặc biệt, độc lập ngôn ngữ. Đây là lý do Kubernetes chọn mô hình này — bạn chỉ cần gọi http://inventory và Kubernetes lo phần còn lại qua kube-proxy và cluster DNS. Nhược điểm: thêm một chặng mạng, và load balancer trở thành thành phần cần được quản lý và làm cho highly-available.
Health checking — trái tim của tính đúng đắn
Một registry chỉ hữu ích khi thông tin trong đó chính xác. Nếu registry vẫn trả về một instance đã chết, request sẽ được gửi vào hư không. Vì vậy health check là bắt buộc.
Có hai kiểu chính. Push model (heartbeat): instance chủ động gửi tín hiệu sống tới registry mỗi vài giây; nếu registry không nhận được heartbeat sau một khoảng thời gian, nó coi instance đã chết. Pull model (active probe): registry hoặc load balancer chủ động gọi tới một endpoint health check (ví dụ GET /healthz) của instance để kiểm tra. Kubernetes dùng pull model với livenessProbe và readinessProbe — trong đó readinessProbe đặc biệt quan trọng: một Pod chưa "ready" sẽ không được đưa vào danh sách endpoint nhận traffic, dù nó đang chạy.
Tình huống thực tế
Tình huống 1 — Sàn TMĐT Việt Nam chuyển từ hard-code sang Consul
Một sàn thương mại điện tử tại TP.HCM (gọi là "ShopViet") bắt đầu với kiến trúc chạy trên khoảng 40 máy ảo, mỗi service được ghi địa chỉ cứng trong file config.yaml. Vào các đợt sale lớn như 11/11, họ scale service pricing từ 6 lên 30 instance để chịu tải. Vấn đề: mỗi lần thêm instance, đội vận hành phải sửa tay file config của 8 service khác gọi tới pricing, rồi rolling restart. Một lần, một kỹ sư gõ nhầm IP, khiến 4% request pricing đổ vào một máy đã tắt — hệ thống giảm giá hiển thị sai trong 20 phút cao điểm.
Họ chuyển sang HashiCorp Consul với mô hình third-party registration + client-side discovery. Mỗi service đăng ký với Consul agent chạy cùng máy, khai báo một health check HTTP tới /health. Client dùng Consul DNS (pricing.service.consul) để resolve ra danh sách instance khỏe mạnh. Kết quả: việc scale từ 6 lên 30 instance giờ hoàn toàn tự động, không ai phải sửa config. Số sự cố do sai địa chỉ trong mùa sale giảm về 0.
Bài học rút ra: hard-code địa chỉ không phải là "sai về mặt kỹ thuật" khi hệ thống còn nhỏ và tĩnh — nó chỉ trở thành quả bom hẹn giờ khi tần suất thay đổi instance tăng lên. Ngưỡng để đầu tư service discovery chính là lúc bạn bắt đầu scale động hoặc deploy thường xuyên.
Tình huống 2 — Startup fintech dùng Kubernetes và "không viết gì cả"
Một startup fintech ở Singapore xây hệ thống thanh toán trên Kubernetes ngay từ đầu. Đội kỹ sư khá bất ngờ khi nhận ra họ gần như không phải làm gì cho service discovery: chỉ cần định nghĩa một Kubernetes Service tên payment-gateway, thì bất kỳ Pod nào trong cluster gọi http://payment-gateway đều được định tuyến đúng.
Cơ chế bên dưới: Kubernetes tự đăng ký các Pod healthy vào đối tượng Endpoints (third-party registration), cấp cho Service một virtual IP ổn định (ClusterIP), và kube-proxy định tuyến traffic tới các Pod (server-side discovery). CoreDNS lo phần phân giải tên. Toàn bộ đúng-sai của discovery phụ thuộc vào readinessProbe: có lần một Pod payment-gateway khởi động chậm vì phải warm-up cache 15 giây, nhưng vì thiếu readinessProbe, Kubernetes đưa nó vào nhận traffic ngay khi container start, khiến ~30 request đầu tiên bị timeout. Sau khi thêm readinessProbe kiểm tra endpoint /ready (chỉ trả 200 khi cache đã warm), vấn đề biến mất.
Bài học rút ra: nếu bạn đã dùng Kubernetes, đừng đi cài thêm một service registry riêng như Eureka — bạn sẽ có hai nguồn sự thật mâu thuẫn nhau. Hãy tận dụng discovery built-in, và dành sự chú ý cho readinessProbe vì đó mới là nơi bug thực sự phát sinh.
Tình huống 3 — Ngân hàng số và bài toán multi-datacenter
Một ngân hàng số ở Việt Nam vận hành hệ thống trên hai datacenter (một ở Hà Nội, một ở TP.HCM) để đảm bảo tính sẵn sàng khi một DC gặp sự cố. Họ dùng client-side discovery với Eureka, nhưng gặp vấn đề: khi client ở Hà Nội cần gọi service account, đôi khi registry trả về instance ở TP.HCM, khiến mỗi request phải đi qua đường truyền liên tỉnh, cộng thêm ~25ms latency — con số đáng kể cho một API được gọi hàng chục lần trong mỗi giao dịch.
Giải pháp là tận dụng lợi thế của client-side discovery: họ gắn metadata zone cho mỗi instance khi đăng ký, và cấu hình client ưu tiên chọn instance cùng zone (zone-aware routing), chỉ fallback sang zone khác khi zone cục bộ không còn instance khỏe mạnh. Latency P99 của các call nội bộ giảm khoảng 40%.
Bài học rút ra: đây chính là điểm mạnh mà server-side discovery đơn giản khó cho bạn — client-side discovery cho phép áp dụng chiến lược chọn instance thông minh dựa trên bối cảnh (zone, độ trễ, tải). Khi latency nội bộ là yếu tố sống còn, khả năng kiểm soát ở phía client trở thành lợi thế quyết định.
Hướng dẫn từng bước
Khi cần thiết kế service discovery cho hệ thống của bạn, hãy đi theo trình tự sau:
Bước 1 — Xác định bạn có thực sự cần service discovery động không. Nếu hệ thống chỉ có vài service chạy trên địa chỉ tĩnh, ít thay đổi, thì một load balancer đơn giản với DNS thủ công có thể đủ. Đừng thêm phức tạp khi chưa cần. Ngưỡng cần cân nhắc: bạn có auto-scaling không? Bạn deploy bao nhiêu lần mỗi tuần? Instance có bị thay IP thường xuyên không?
Bước 2 — Kiểm tra nền tảng bạn đang chạy. Nếu đã dùng Kubernetes, câu trả lời gần như luôn là "dùng discovery built-in" (Service + DNS). Nếu chạy trên VM thuần hoặc hạ tầng lai, bạn cần một registry độc lập như Consul, etcd, hoặc Eureka.
Bước 3 — Chọn giữa client-side và server-side discovery. Chọn server-side nếu bạn muốn client đơn giản, đa ngôn ngữ, ít phụ thuộc thư viện (phù hợp đa số trường hợp). Chọn client-side nếu bạn cần kiểm soát chi tiết chiến lược load balancing như zone-aware routing hay latency-based routing.
Bước 4 — Quyết định cơ chế registration. Ưu tiên third-party/tự động (như cách orchestrator làm) để tránh nhét code discovery vào business logic. Chỉ chọn self-registration khi bạn không có nền tảng hỗ trợ tự động.
Bước 5 — Thiết kế health check nghiêm túc. Định nghĩa rõ endpoint health check phản ánh đúng khả năng phục vụ thực tế (kiểm tra cả kết nối tới database, cache mà service phụ thuộc). Phân biệt "liveness" (còn sống) và "readiness" (sẵn sàng nhận traffic). Đặt timeout và ngưỡng fail hợp lý — quá nhạy sẽ gây "flapping" (instance liên tục vào/ra danh sách), quá chậm sẽ để traffic đổ vào instance đã chết.
Bước 6 — Xử lý trường hợp registry gặp sự cố. Registry là thành phần tối quan trọng. Client nên cache danh sách instance gần nhất để nếu registry tạm thời không phản hồi, hệ thống vẫn hoạt động với dữ liệu cũ thay vì sập hoàn toàn.
Lỗi thường gặp & mẹo
Lỗi 1 — Ghi địa chỉ cứng "tạm thời" rồi quên mất. Rất nhiều đội bắt đầu với IP hard-code "để chạy trước đã", rồi con số đó nằm im cho tới khi gây sự cố ở production. Mẹo: ngay cả khi chưa dùng registry, hãy luôn gọi qua tên logic (DNS) chứ đừng gọi IP trần.
Lỗi 2 — Health check quá hời hợt. Một endpoint /health chỉ trả về 200 OK mà không kiểm tra gì thực sự sẽ báo "khỏe" ngay cả khi service đã mất kết nối tới database. Mẹo: health check nên phản ánh khả năng phục vụ thật, nhưng cũng đừng biến nó thành một chuỗi kiểm tra nặng nề khiến bản thân health check gây quá tải.
Lỗi 3 — Nhầm lẫn liveness và readiness. Nếu bạn dùng cùng một tiêu chí cho cả hai, một service đang bận warm-up có thể bị kill nhầm (vì trượt liveness) thay vì chỉ tạm thời không nhận traffic (readiness). Mẹo: liveness trả lời "có nên restart tôi không?", readiness trả lời "có nên gửi traffic cho tôi không?".
Lỗi 4 — Không tính đến độ trễ cập nhật của registry. Khi một instance chết, có một khoảng thời gian ngắn (vài giây tới vài chục giây) trước khi registry nhận ra và loại nó ra. Trong khoảng đó, một số request vẫn bị gửi tới instance chết. Mẹo: kết hợp service discovery với retry và circuit breaker (Bài 41) ở phía client để chịu được lỗi thoáng qua này.
Lỗi 5 — Dựng hai hệ thống discovery song song. Chạy Kubernetes nhưng lại cài thêm Eureka bên trong, tạo ra hai nguồn sự thật. Mẹo: mỗi hệ thống chỉ nên có một nguồn sự thật duy nhất về việc "service đang ở đâu".
Mẹo về DNS caching: khi dùng DNS-based discovery, hãy cẩn thận với TTL và DNS caching ở tầng client (JVM chẳng hạn cache DNS khá lâu theo mặc định), vì nó có thể khiến client vẫn dùng địa chỉ cũ dù registry đã cập nhật.
Bài tập thực hành
- Phân tích hệ thống của bạn: Chọn một hệ thống bạn đang làm (hoặc một hệ thống giả định 5 service). Với từng cặp service gọi nhau, hãy trả lời: hiện chúng tìm nhau bằng cách nào? Có địa chỉ hard-code nào không? Điều gì xảy ra nếu instance đích đổi IP?
- Thiết kế trên giấy: Cho một hệ thống chạy trên VM (không phải Kubernetes) với auto-scaling. Hãy chọn: registry nào, client-side hay server-side, registration kiểu gì, health check ra sao. Viết một đoạn giải thích 200 từ bảo vệ lựa chọn của bạn — đây chính là phôi thai của một ADR (Bài 4).
- So sánh: Lập bảng 2 cột so sánh client-side và server-side discovery theo các tiêu chí: độ phức tạp ở client, số chặng mạng, khả năng kiểm soát load balancing, độ phụ thuộc ngôn ngữ. Sau đó viết một câu kết luận: "Tôi sẽ chọn ... khi ..." cho mỗi loại.
- Thực hành readiness: Nếu có môi trường Kubernetes (dù là minikube trên máy cá nhân), hãy tạo một Deployment với một container cố tình "ngủ" 10 giây trước khi sẵn sàng. Thử nghiệm với và không có
readinessProbe, quan sát sự khác biệt về việc traffic có bị gửi vào Pod chưa sẵn sàng hay không.
Tóm tắt
Service Discovery giải quyết một vấn đề tất yếu của hệ thống phân tán hiện đại: địa chỉ mạng của service là biến số, không phải hằng số. Trái tim của nó là Service Registry — cuốn danh bạ luôn cập nhật danh sách instance khỏe mạnh của từng service.
Ba câu hỏi cốt lõi cần trả lời là: đăng ký thế nào (self-registration vs third-party), tra cứu thế nào (client-side vs server-side discovery), và kiểm tra sức khỏe thế nào (heartbeat vs active probe). Client-side discovery cho bạn quyền kiểm soát tối đa chiến lược chọn instance (như zone-aware routing), đổi lại client phức tạp hơn; server-side discovery giữ client đơn giản và đa ngôn ngữ, đổi lại thêm một chặng trung gian.
Quyết định thực tế thường rẽ theo nền tảng: nếu bạn dùng Kubernetes, hãy tận dụng discovery built-in và tập trung vào readinessProbe; nếu chạy trên VM hoặc hạ tầng lai, hãy chọn một registry như Consul hoặc Eureka. Dù chọn gì, hãy nhớ ba điều: health check phải phản ánh khả năng phục vụ thật, luôn có một nguồn sự thật duy nhất, và kết hợp discovery với retry cùng circuit breaker để chịu được độ trễ cập nhật không thể tránh khỏi. Đó là ranh giới giữa một microservices "chạy trên slide" và một microservices "sống được ở production".