Bài 5: Service Discovery với gRPC & Client-side Load Balancing
Trong các bài trước, chúng ta đã xây dựng cơ chế Service Discovery lưu trữ trên Consul và Etcd. Tuy nhiên, các ví dụ đó đều dừng lại ở việc gọi HTTP REST thuần túy.
Khi hệ thống microservices phát sinh nhu cầu giao tiếp nội bộ tốc độ cao với độ trễ thấp nhất, gRPC (sử dụng giao thức HTTP/2 và Protobuf) trở thành tiêu chuẩn vàng. Bài học này sẽ giải quyết một bài toán cấp cao: Làm thế nào để gRPC Client tự động phát hiện instance và cân bằng tải (Client-side Load Balancing) mà không cần đi qua một Proxy trung gian?
1. Cơ chế hoạt động của gRPC Resolver & Balancer
Mặc định, khi bạn khởi tạo một gRPC connection trong Go: conn, err := grpc.Dial("localhost:8080", ...) gRPC sẽ coi chuỗi "localhost:8080" là một địa chỉ tĩnh cố định.
Để tích hợp Service Discovery, chúng ta phải "dạy" cho gRPC hiểu một scheme (lược đồ) tùy chỉnh, ví dụ: my-service://payment-service. Khi đó, gRPC sẽ thực hiện hai bước thông minh:
-
Resolver: Dùng scheme này để truy vấn ngược lại Service Registry (Consul/Etcd), lấy toàn bộ danh sách IP hiện tại của
payment-servicevà nhét vào bộ nhớ đệm của client. -
Balancer: Nhận danh sách IP từ Resolver, áp dụng thuật toán cân bằng tải (như
roundrobin) để tự động phân phối các lệnh gọi RPC đến các instance khác nhau theo từng request.
2. Thiết kế Custom gRPC Resolver trong Golang
Để viết một Resolver trong Go, chúng ta cần cài đặt package google.golang.org/grpc/resolver. Cấu trúc cốt lõi yêu cầu tạo ra hai thành phần: resolver.Builder (để khởi tạo) và resolver.Resolver (để theo dõi và cập nhật địa chỉ).
Dưới đây là một mô hình chuẩn lập trình Custom Resolver lấy cảm hứng từ Etcd/Consul:
Go
package main
import (
"context"
"fmt"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
)
// 1. ĐỊNH NGHĨA BUILDER
type RegistryResolverBuilder struct {
// Giả lập danh sách lấy từ Consul/Etcd
instances []string
}
func NewRegistryResolverBuilder(instances []string) *RegistryResolverBuilder {
return &RegistryResolverBuilder{instances: instances}
}
// Scheme định danh cho resolver của chúng ta (vd: consul://...)
func (*RegistryResolverBuilder) Scheme() string {
return "custom-registry"
}
func (b *RegistryResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &RegistryResolver{
target: target,
cc: cc,
instances: b.instances,
}
r.start()
return r, nil
}
// 2. ĐỊNH NGHĨA RESOLVER
type RegistryResolver struct {
target resolver.Target
cc resolver.ClientConn
instances []string
}
func (r *RegistryResolver) start() {
// Cập nhật danh sách địa chỉ lần đầu tiên cho gRPC ClientConn
r.updateState()
}
func (r *RegistryResolver) updateState() {
var addrs []resolver.Address
for _, inst := range r.instances {
addrs = append(addrs, resolver.Address{Addr: inst})
}
// Bắn danh sách IP mới vào hệ thống cân bằng tải nội bộ của gRPC
r.cc.UpdateState(resolver.State{Addresses: addrs})
}
func (*RegistryResolver) ResolveNow(resolver.ResolveNowOptions) {}
func (*RegistryResolver) Close() {}
func main() {
// Đăng ký Builder vào hệ thống toàn cục của gRPC
// Giả lập payment-service đang có 2 instance chạy ở port 8081 và 8082
mockInstances := []string{"127.0.0.1:8081", "127.0.0.1:8082"}
resolver.Register(NewRegistryResolverBuilder(mockInstances))
// 3. KẾT NỐI GRPC DÙNG SCHEME TÙY CHỈNH KÈM CÂN BẰNG TẢI ROUND-ROBIN
// Cú pháp target: custom-registry://anything
conn, err := grpc.Dial(
"custom-registry://payment-service",
grpc.WithTransportCredentials(insecure.NewCredentials()),
// Ép gRPC sử dụng thuật toán cân bằng tải Round-Robin giữa các instance
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithBlock(),
grpc.WithTimeout(2*time.Second),
)
if err != nil {
log.Fatalf("Không thể kết nối gRPC: %v", err)
}
defer conn.Close()
fmt.Println("gRPC Client đã kết nối thành công qua Custom Registry Resolver!")
// Từ đây, mỗi khi bạn gọi Stub gRPC, nó sẽ tự động luân phiên
// giữa 127.0.0.1:8081 và 127.0.0.1:8082.
}
3. Ưu điểm của Client-side Load Balancing với gRPC
-
Độ trễ bằng 0 (Zero-hop proxy): Client giao tiếp thẳng với instance đích mà không cần đi qua một Nginx hay API Gateway trung gian nào, giảm thiểu tối đa thời gian chuyển tiếp gói tin (latency).
-
Hiệu năng cao: gRPC tự động quản lý kết nối TCP (Connection Pooling). Nếu một instance chết, Resolver sẽ cập nhật lại State, gRPC client tự động ngắt kết nối cũ và mở kết nối mới tới instance còn sống mà không làm sập tiến trình ứng dụng.
Tổng kết
Đến đây, bạn đã làm chủ toàn bộ bức tranh về Service Discovery trong Golang: từ việc hiểu rõ lý thuyết kiến trúc, xây dựng cơ chế Health Check, tích hợp với các công cụ phân tán công nghiệp (Consul, Etcd), cho đến việc tự thiết kế một gRPC Resolver hiệu năng cao.
Trong bài học cuối cùng của series, chúng ta sẽ tự tay code một "Mini-Registry" từ con số không (không dùng thư viện ngoài) để thấu hiểu tường tận mọi cơ chế tầng thấp nhất của hệ thống phân tán.
All rights reserved