0

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:

  1. 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-service và nhét vào bộ nhớ đệm của client.

  2. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí