Bài 3: Xây dựng Service Registry với Consul và Golang SDK
Trong các bài trước, chúng ta đã nắm bắt lý thuyết về Service Discovery và cơ chế Health Check. Giờ là lúc hiện thực hóa kiến trúc này bằng một công cụ công nghiệp thực tế: HashiCorp Consul.
Consul là một giải pháp trọn gói cung cấp cả Service Discovery, Health Checking và Key-Value Store. Trong bài học này, chúng ta sẽ viết một ứng dụng Golang hoàn chỉnh thực hiện 3 nhiệm vụ cốt lõi:
-
Register: Tự động đăng ký một Service Instance lên Consul khi khởi động.
-
Health Check (HTTP): Cung cấp một endpoint
/healthđể Consul định kỳ "bác sĩ thăm khám". -
Discovery: Truy vấn ngược lại Consul để tìm địa chỉ của các service khác.
1. Chuẩn bị môi trường
Trước tiên, bạn cần cài đặt và chạy Consul ở chế độ phát triển (Dev mode) trên máy cá nhân (thường chạy qua Docker):
Bash
docker run -d --name consul -p 8500:8500 hashicorp/consul:latest consul agent -dev -client=0.0.0.0
Sau khi chạy xong, bạn có thể truy cập giao diện Web UI tại http://localhost:8500 để quan sát danh sách các service.
Trong project Go của bạn, hãy cài đặt official Go SDK của Consul:
Bash
go get github.com/hashicorp/consul/api
2. Viết mã nguồn Service Instance (Đăng ký & Health Check)
Dưới đây là mã nguồn chuẩn của một Microservice (ví dụ: Payment-Service) tự động đăng ký với Consul ngay khi khởi động.
Go
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/hashicorp/consul/api"
)
const (
serviceID = "payment-service-01"
serviceName = "payment-service"
servicePort = 8080
serviceHost = "127.0.0.1"
)
func main() {
// 1. Kết nối đến Consul Agent (Mặc định chạy ở port 8500)
config := api.DefaultConfig()
config.Address = "127.0.0.1:8500"
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Không thể kết nối Consul: %v", err)
}
// 2. Định nghĩa cấu hình đăng ký Service & Health Check
registration := &api.AgentServiceRegistration{
ID: serviceID,
Name: serviceName,
Port: servicePort,
Address: serviceHost,
// Cấu hình HTTP Health Check (Consul sẽ chủ động gọi endpoint này định kỳ)
Check: &api.AgentServiceCheck{
HTTP: fmt.Sprintf("http://%s:%d/health", serviceHost, servicePort),
Interval: "5s", // Kiểm tra mỗi 5 giây
Timeout: "3s", // Timeout sau 3 giây
DeregisterCriticalServiceAfter: "10s", // Tự động xóa khỏi danh bạ nếu lỗi liên tục trong 10s
},
}
// 3. Thực hiện đăng ký lên Consul Registry
err = client.Agent().ServiceRegister(registration)
if err != nil {
log.Fatalf("Đăng ký service thất bại: %v", err)
}
log.Printf("[%s] Đã đăng ký thành công với Consul!", serviceName)
// Đảm bảo khi service tắt thì phải tự động Deregister
defer func() {
client.Agent().ServiceDeregister(serviceID)
log.Printf("[%s] Đã hủy đăng ký (Deregister) khỏi Consul.", serviceName)
}()
// 4. Khởi ạo HTTP Server cho Service
mux := http.NewServeMux()
// Endpoint Health Check để Consul "thăm khám"
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
// Endpoint nghiệp vụ chính
mux.HandleFunc("/pay", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Thanh toán thành công qua instance: %s", serviceID)
})
// Lắng nghe port ngầm định
serverAddr := fmt.Sprintf(":%d", servicePort)
go func() {
log.Printf("Server đang chạy tại %s", serverAddr)
if err := http.ListenAndServe(serverAddr, mux); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
// 5. Bắt tín hiệu ngắt (Ctrl+C hoặc Docker Stop) để thực hiện Graceful Shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Đang tắt service...")
}
Khi bạn chạy chương trình này (go run main.go), truy cập vào http://localhost:8500, bạn sẽ thấy payment-service xuất hiện với một dấu chấm màu xanh lá (Healthy) do Consul định kỳ gọi vào /health thành công.
3. Viết mã nguồn Service Consumer (Khám phá dịch vụ)
Bây giờ, giả sử một service khác (như Order-Service) muốn gọi tới Payment-Service. Thay vì gọi cứng địa chỉ IP, nó sẽ hỏi Consul để lấy danh sách các instance đang sống.
Go
package main
import (
"fmt"
"log"
"github.com/hashicorp/consul/api"
)
func main() {
// 1. Kết nối Consul client
config := api.DefaultConfig()
config.Address = "127.0.0.1:8500"
client, err := api.NewClient(config)
if err != nil {
log.Fatalf("Không thể kết nối Consul: %v", err)
}
// 2. Truy vấn danh sách các instance KHỎE MẠNH của "payment-service"
// Tham số thứ 2 (passingOnly = true) cực kỳ quan trọng:
// Nó tự động lọc bỏ các service đang bị chết hoặc timeout.
services, _, err := client.Health().Service("payment-service", "", true, nil)
if err != nil {
log.Fatalf("Lỗi truy vấn service discovery: %v", err)
}
if len(services) == 0 {
log.Println("Không tìm thấy instance nào khả dụng!")
return
}
// 3. Chọn một instance (Ví dụ: Lấy instance đầu tiên - Thuật toán Round Robin đơn giản)
target := services[0].Service
targetURL := fmt.Sprintf("http://%s:%d/pay", target.Address, target.Port)
log.Printf("Đã tìm thấy service tại địa chỉ: %s", targetURL)
// Từ đây, bạn có thể dùng http.Get(targetURL) để gọi request bình thường...
}
Tổng kết
Thông qua Consul SDK và Golang, chúng ta đã xây dựng được một hệ thống Service Discovery hoàn chỉnh: Service tự đăng ký khi lên sóng, tự dọn dẹp khi tắt, cung cấp cơ chế Health Check tự động, và cho phép các service khác tra cứu linh hoạt.
Tuy nhiên, Consul là một giải pháp độc lập (External Registry) đòi hỏi phải dựng thêm một cluster chuyên biệt. Trong bài tiếp theo, chúng ta sẽ khám phá một lựa chọn khác vô cùng mạnh mẽ trong thế giới Cloud-Native: Etcd, trái tim đằng sau Kubernetes.
All rights reserved