Bài 4: Khám phá Etcd làm Distributed Registry trong Golang
Ở Bài 3, chúng ta đã làm quen với Consul — một giải pháp Service Discovery chuyên dụng và đầy đủ tính năng. Tuy nhiên, trong thế giới Cloud-Native hiện đại (đặc biệt là các hệ thống vận hành xung quanh Kubernetes), có một "ông vua" khác đảm nhận vai trò lưu trữ cấu hình phân tán và dịch vụ: Etcd.
Etcd là cơ sở dữ liệu Key-Value phân tán, đồng bộ cao, sử dụng thuật toán đồng thuận Raft Consensus Algorithm. Nếu Consul giống như một hệ thống quản lý chuyên biệt, thì Etcd giống như một "bộ não" cực kỳ nhỏ gọn, mạnh mẽ và tin cậy tuyệt đối.
Trong bài học này, chúng ta sẽ tìm hiểu cách tận dụng Etcd làm Service Registry bằng Golang thông qua package chính thức clientv3.
1. Cơ chế cốt lõi: Lease (Thuê bao) và TTL trong Etcd
Khác với Consul (có sẵn tính năng Health Check HTTP riêng), Etcd tiếp cận bài toán quản lý trạng thái sống/chết của service thông qua một khái niệm toán học đơn giản nhưng cực kỳ hiệu quả gọi là Lease (Hợp đồng thuê bao) kèm theo TTL (Time-To-Live).
-
Tạo Lease: Một service instance khi khởi động sẽ xin Etcd cấp một "gói thuê" có thời hạn (ví dụ: TTL = 10 giây).
-
Gắn Key với Lease: Khi đăng ký service, nó lưu một Key (ví dụ:
/services/payment-service/127.0.0.1:8080) và trói buộc Key này vào chiếc Lease vừa tạo. -
Giữ nhịp (KeepAlive): Service chạy một vòng lặp ngầm liên tục gửi tín hiệu "gia hạn" (KeepAlive) lên Etcd trước khi hết 10 giây.
-
Kịch bản khi Service chết: Nếu service bị sập nguồn hoặc mất mạng, nó không thể gửi tín hiệu gia hạn. Ngay khi đồng hồ TTL về 0, Etcd tự động xóa sạch Key đó khỏi hệ thống. Các service khác truy vấn sẽ ngay lập tức không còn nhìn thấy instance đã chết nữa.
2. Chuẩn bị môi trường
Chạy một node Etcd cục bộ thông qua Docker:
Bash
docker run -d --name etcd -p 2379:2379 -p 2380:2380 \
-e ALLOW_NONE_AUTHENTICATION=yes \
bitnami/etcd:latest
Cài đặt Etcd client SDK cho Golang trong project của bạn:
Bash
go get go.etcd.io/etcd/client/v3
3. Viết mã nguồn Service Registration với Etcd (Provider)
Dưới đây là đoạn code hoàn chỉnh thể hiện cách một Microservice kết nối, xin Lease, đăng ký địa chỉ và duy trì nhịp sống với Etcd.
Go
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
func main() {
// 1. Kết nối đến Etcd Cluster (Mặc định port 2379)
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatalf("Không thể kết nối Etcd: %v", err)
}
defer cli.Close()
ctx := context.Background()
// Thông tin định danh của instance này
serviceKey := "/services/payment-service/instance-01"
serviceVal := "127.0.0.1:8080"
// 2. Cấp một Lease có thời hạn TTL = 10 giây
leaseResp, err := cli.Grant(ctx, 10)
if err != nil {
log.Fatalf("Không thể tạo Lease: %v", err)
}
leaseID := leaseResp.ID
// 3. Đăng ký Key-Value và trói buộc vào Lease
_, err = cli.Put(ctx, serviceKey, serviceVal, clientv3.WithLease(leaseID))
if err != nil {
log.Fatalf("Không thể đăng ký service lên Etcd: %v", err)
}
log.Printf("[Provider] Đã đăng ký thành công: %s -> %s", serviceKey, serviceVal)
// 4. Thiết lập cơ chế KeepAlive tự động gia hạn Lease ở background goroutine
ch, err := cli.KeepAlive(ctx, leaseID)
if err != nil {
log.Fatalf("Lỗi thiết lập KeepAlive: %v", err)
}
go func() {
for {
ka, ok := <-ch
if !ok {
log.Println("[Provider] Kênh KeepAlive đã đóng!")
return
}
log.Printf("[Provider] Đã gia hạn Lease thành công (TTL: %d)", ka.TTL)
}
}()
// Lắng nghe tín hiệu tắt ứng dụng để dọn dẹp sạch sẽ
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("[Provider] Đang hủy đăng ký khỏi Etcd...")
// Khi tắt ứng dụng, chủ động thu hồi lease để xóa key ngay lập tức thay vì đợi timeout 10s
cli.Revoke(ctx, leaseID)
}
4. Viết mã nguồn Service Discovery (Consumer)
Khi một service khác cần gọi payment-service, nó không cần gọi từng key cố định mà sẽ sử dụng Prefix Query (Truy vấn theo tiền tố) để lấy toàn bộ danh sách các instance đang hoạt động.
Go
package main
import (
"context"
"fmt"
"log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatalf("Không thể kết nối Etcd: %v", err)
}
defer cli.Close()
ctx := context.Background()
// Truy vấn tất cả các key bắt đầu bằng tiền tố của payment-service
prefixKey := "/services/payment-service/"
resp, err := cli.Get(ctx, prefixKey, clientv3.WithPrefix())
if err != nil {
log.Fatalf("Lỗi truy vấn Service Discovery: %v", err)
}
if len(resp.Kvs) == 0 {
log.Println("Không tìm thấy instance nào khả dụng!")
return
}
fmt.Println("Danh sách các instance đang hoạt động:")
for _, ev := range resp.Kvs {
fmt.Printf("- Key: %s | Địa chỉ: %s\n", ev.Key, ev.Value)
}
}
Tổng kết
Sử dụng Etcd làm Distributed Registry mang lại sự đồng bộ cực kỳ cao nhờ nền tảng thuật toán Raft. Cơ chế Lease + KeepAlive giúp tự động thanh lọc các instance chết mà không cần phải xây dựng một hệ thống HTTP Health Check phức tạp bên ngoài.
Tuy nhiên, trong mô hình Microservices, việc gọi HTTP thuần túy qua lại giữa các instance đôi khi chưa tối ưu bằng giao thức hiệu năng cao gRPC. Bài tiếp theo sẽ nâng tầm hệ thống của bạn bằng cách kết hợp Service Discovery với gRPC Client-side Load Balancing.
All rights reserved