Golang gRPC客户端负载均衡实现示例
在分布式系统中,gRPC服务的客户端负载均衡是提升系统可用性和吞吐量的关键手段。gRPC本身就支持客户端侧的负载均衡能力,我们可以通过内置的负载均衡策略或者自定义策略来实现请求的分发。本文将通过一个完整的示例,演示如何在Golang环境下实现gRPC客户端的负载均衡。
前置准备
开始之前需要确保环境中已经安装以下依赖:
- Go 1.18及以上版本
- protoc 编译器,用于生成gRPC相关代码
- protoc-gen-go、protoc-gen-go-grpc 插件
首先我们定义一个简单的gRPC服务,用于演示负载均衡效果。创建 proto 文件 helloworld.proto,内容如下:
syntax = "proto3";
package helloworld;
option go_package = "github.com/example/helloworld";
// 定义问候服务
service Greeter {
// 发送问候请求
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 请求参数,包含客户端名称
message HelloRequest {
string name = 1;
}
// 响应参数,包含服务端返回的问候语
message HelloReply {
string message = 1;
}执行以下命令生成对应的Go代码:
protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ helloworld.proto
服务端实现
为了演示负载均衡,我们需要启动多个相同服务的不同实例,每个实例监听不同的端口,并且在返回的响应中标识自己的端口,方便观察请求的分发情况。
package main
import (
"context"
"fmt"
"log"
"net"
pb "github.com/example/helloworld"
"google.golang.org/grpc"
)
// 定义服务端结构体,实现Greeter服务接口
type server struct {
pb.UnimplementedGreeterServer
port string // 记录当前服务实例的端口
}
// 实现SayHello方法,返回包含端口信息的问候语
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("收到请求,来自: %s, 当前服务端口: %s", in.Name, s.port)
return &pb.HelloReply{Message: fmt.Sprintf("Hello %s, 当前服务端口: %s", in.Name, s.port)}, nil
}
// 启动单个gRPC服务实例
func startServer(port string) {
// 监听指定端口
lis, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatalf("监听端口 %s 失败: %v", port, err)
}
// 创建gRPC服务端
s := grpc.NewServer()
// 注册Greeter服务
pb.RegisterGreeterServer(s, &server{port: port})
log.Printf("服务启动成功,监听端口: %s", port)
// 启动服务端
if err := s.Serve(lis); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
func main() {
// 启动三个不同端口的服务实例,模拟集群
go startServer("50051")
go startServer("50052")
go startServer("50053")
// 阻塞主 goroutine,避免程序退出
select {}
}上述代码启动了三个服务实例,分别监听50051、50052、50053端口,每个实例在返回响应时会携带自己的端口信息,方便后续验证负载均衡效果。
客户端负载均衡实现
gRPC客户端可以通过配置 grpc.WithDefaultServiceConfig 来指定负载均衡策略,常用的内置策略有 round_robin(轮询)和 pick_first(默认,选择第一个可用连接)。我们这里使用轮询策略,并且通过服务发现的方式指向多个服务端地址。
package main
import (
"context"
"log"
"time"
pb "github.com/example/helloworld"
"google.golang.org/grpc"
)
func main() {
// 配置负载均衡策略为轮询,并且指定多个服务端地址
serviceConfig := `{
"loadBalancingPolicy": "round_robin",
"methodConfig": [
{
"name": [{"service": "helloworld.Greeter"}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2.0,
"retryableStatusCodes": ["UNAVAILABLE"]
}
}
]
}`
// 创建gRPC客户端连接,指定多个服务地址,使用自定义的服务配置
conn, err := grpc.Dial(
"passthrough:///127.0.0.1:50051,127.0.0.1:50052,127.0.0.1:50053",
grpc.WithDefaultServiceConfig(serviceConfig),
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("创建连接失败: %v", err)
}
defer conn.Close()
// 创建Greeter客户端
c := pb.NewGreeterClient(conn)
// 发送10次请求,观察请求分发到不同服务实例的情况
for i := 0; i < 10; i++ {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
// 发送SayHello请求
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: fmt.Sprintf("client-request-%d", i)})
if err != nil {
log.Fatalf("请求失败: %v", err)
}
log.Printf("收到响应: %s", r.Message)
// 间隔100毫秒发送下一次请求
time.Sleep(time.Millisecond * 100)
}
}这里需要注意连接的地址格式,我们使用了 passthrough:/// 前缀,这是gRPC内置的透传解析器,会直接把后面的地址列表传递给负载均衡器处理。如果是生产环境,通常会结合服务发现组件(如etcd、consul)来动态获取服务地址,只需要替换对应的解析器即可,负载均衡的配置方式保持一致。
运行与验证
首先启动服务端程序,等待三个服务实例都启动成功。然后运行客户端程序,观察客户端的输出日志,可以看到10次请求的响应中,服务端口会按照50051、50052、50053的顺序循环出现,符合轮询负载均衡的预期效果。
如果某个服务实例突然下线,gRPC客户端的负载均衡器会自动将请求转发到剩余可用的实例上,当下线实例恢复后,也会重新加入请求分发的候选列表,从而实现客户端侧的高可用。
自定义负载均衡策略(可选)
如果内置的负载均衡策略无法满足需求,gRPC也支持自定义负载均衡策略。需要实现 balancer.Builder 和 balancer.Picker 接口,然后通过 balancer.Register 方法注册自定义策略,最后在客户端配置中指定策略名称即可。不过大部分场景下,内置的轮询或者加权轮询策略已经足够使用。
通过上述示例可以看到,gRPC的客户端负载均衡实现并不复杂,只需要在客户端连接时配置对应的负载均衡策略和服务地址列表,就可以轻松实现请求的分发,提升系统的整体性能和可用性。
GolanggRPC客户端负载均衡round_robin服务发现 本作品最后修改时间:2026-05-23 11:35:09