4、gRPC高级实践:拦截器、负载均衡、健康检查、TLS安全通信、与网关集成

好,咱们接着聊。前面几章我们把gRPC的基础通信模式讲透了,包括Unary、Server Streaming、Client Streaming、Bidirectional Streaming。但说实话,那些只是「能用」的层面。真正到了生产环境,你会发现光会调接口远远不够。这一章,我带你看看gRPC在实际项目中那些「不得不做」的高级实践。

我个人习惯把这一章叫做「gRPC的护城河工程」。为什么?因为拦截器、负载均衡、健康检查、TLS、网关集成,这些东西单独拎出来每一个都不难,但组合在一起,就是一套完整的微服务通信保障体系。你想想看,没有这些,你的服务就像没穿盔甲上战场——能跑,但随时可能倒下。

4.1 拦截器:给通信加点「中间件」

拦截器,说白了就是gRPC的中间件。它可以在请求发出前或响应返回后,插入一段自定义逻辑。我在项目中遇到过最典型的场景:统一打印请求日志、注入Trace ID、做权限校验、统计接口耗时。

gRPC的拦截器分两种:客户端拦截器和服务端拦截器。每种又分一元拦截器(Unary Interceptor)和流式拦截器(Stream Interceptor)。

先看一个最简单的服务端一元拦截器:

// 服务端一元拦截器
func LoggingInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    log.Printf("收到请求: %s", info.FullMethod)
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("请求完成: %s, 耗时: %v", info.FullMethod, time.Since(start))
    return resp, err
}

// 注册拦截器
s := grpc.NewServer(grpc.UnaryInterceptor(LoggingInterceptor))

嗯,这里要注意:如果你有多个拦截器,gRPC原生只支持一个UnaryInterceptor。怎么办?用grpc_middleware库,或者自己写一个链式调用。我个人习惯用go-grpc-middleware,它提供了现成的链式支持。

我的经验:拦截器里尽量不要做耗时操作,比如查数据库。因为它是同步阻塞的,会拖慢整个请求链路。我曾经见过一个团队在拦截器里调了外部API,结果服务雪崩了。如果你非要这么做,记得加超时和熔断。

客户端拦截器写法类似,只是注册方式不同:

conn, err := grpc.Dial(address,
    grpc.WithUnaryInterceptor(ClientLoggingInterceptor))

4.2 负载均衡:别让一台机器累死

gRPC的负载均衡和HTTP不太一样。HTTP通常是客户端通过DNS轮询或反向代理(如Nginx)来做。但gRPC是长连接,如果还用Nginx做代理,所有请求都会打到同一台后端,负载均衡就失效了。

gRPC官方推荐的做法是「客户端负载均衡」。什么意思?客户端自己维护一个后端地址列表,然后根据策略选一个来建连接。

gRPC内置了几种负载均衡策略:

策略名称 说明 适用场景
pick_first 默认策略,只连第一个地址 开发调试
round_robin 轮询,依次连接 通用场景
grpclb 外部负载均衡器 大规模集群

使用轮询策略很简单:

conn, err := grpc.Dial(
    "dns:///service-name:50051",
    grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
    grpc.WithInsecure(),
)

我曾经踩过一个坑:客户端缓存了DNS解析结果,服务扩容后新节点一直没流量。后来发现是gRPC默认的DNS解析刷新时间太长。解决方案是手动设置grpc.WithBlockgrpc.WithTimeout,或者用Consul、etcd做服务发现。

避坑指南:如果你用Kubernetes,千万别直接用Service的ClusterIP做gRPC负载均衡。因为kube-proxy的负载均衡是基于TCP连接的,而gRPC是长连接,所有请求都会走同一个Pod。正确做法是用Headless Service + 客户端负载均衡,或者直接用Istio。

4.3 健康检查:服务活着吗?

微服务架构里,服务随时可能挂掉。如果客户端还在往挂掉的服务发请求,那体验就很糟糕了。gRPC提供了标准的健康检查协议(Health Checking Protocol),定义在grpc.health.v1中。

服务端实现健康检查:

import "google.golang.org/grpc/health/grpc_health_v1"

type HealthImpl struct{}

func (h *HealthImpl) Check(ctx context.Context,
    req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
    return &grpc_health_v1.HealthCheckResponse{
        Status: grpc_health_v1.HealthCheckResponse_SERVING,
    }, nil
}

func (h *HealthImpl) Watch(req *grpc_health_v1.HealthCheckRequest,
    stream grpc_health_v1.Health_WatchServer) error {
    // 流式健康检查,可以实时推送状态变化
    for {
        stream.Send(&grpc_health_v1.HealthCheckResponse{
            Status: grpc_health_v1.HealthCheckResponse_SERVING,
        })
        time.Sleep(time.Second)
    }
}

客户端可以主动调用健康检查接口,或者让gRPC内置的健康检查器自动处理。我个人建议用后者,省心:

import "google.golang.org/grpc/health"

// 客户端自动健康检查
conn, err := grpc.Dial(address,
    grpc.WithHealthCheck(),
    grpc.WithDefaultServiceConfig(`{"healthCheckConfig": {"serviceName": ""}}`),
)

嗯,这里有个细节:健康检查的serviceName可以留空,表示检查整个服务。如果你有多个子服务,可以分别注册不同的健康检查状态。

4.4 TLS安全通信:别裸奔

说实话,我见过不少团队在开发环境用grpc.WithInsecure(),然后直接搬到生产环境。这是非常危险的。gRPC默认不加密,所有数据都是明文传输。你想想看,如果有人在中间抓包,用户的密码、订单信息全暴露了。

配置TLS其实不复杂,分三步:生成证书、服务端加载、客户端加载。

服务端:

// 加载服务端证书和私钥
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
    log.Fatalf("加载证书失败: %v", err)
}
s := grpc.NewServer(grpc.Creds(creds))

客户端:

// 加载CA证书,验证服务端身份
creds, err := credentials.NewClientTLSFromFile("ca.crt", "server.example.com")
if err != nil {
    log.Fatalf("加载CA证书失败: %v", err)
}
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(creds))
我的建议:生产环境一定要用双向TLS(mTLS),即客户端和服务端互相验证证书。这样能防止中间人攻击。Kubernetes里可以用cert-manager自动管理证书,省去手动续期的麻烦。

我曾经在项目中遇到过证书过期导致服务中断的事故。那天凌晨三点,告警电话响了,所有服务间通信全部失败。排查了半天,发现是证书过期了。从那以后,我养成了一个习惯:证书有效期设置成10年,并且在监控里加上证书过期告警。

4.5 与网关集成:统一入口

微服务多了,客户端不可能直接调用每个服务。这时候就需要一个API网关,统一对外暴露HTTP接口,内部转成gRPC调用。

常见的方案有两种:

  • gRPC-Gateway:Google官方方案,根据proto文件自动生成HTTP转gRPC的代码。
  • Envoy:高性能代理,原生支持gRPC,可以做负载均衡、限流、熔断。

我个人更倾向于Envoy,因为它功能更全,而且和Istio天然集成。但如果你团队小、不想引入额外组件,gRPC-Gateway是个轻量级选择。

gRPC-Gateway的使用方式:

// 在proto文件中添加HTTP绑定
service UserService {
    rpc GetUser (GetUserRequest) returns (User) {
        option (google.api.http) = {
            get: "/v1/users/{user_id}"
        };
    }
}

// 生成代码后,启动HTTP服务
mux := runtime.NewServeMux()
err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, "localhost:50051", opts)

这样,客户端就可以通过GET /v1/users/123来调用gRPC服务了。

避坑指南:gRPC-Gateway生成的HTTP接口默认不支持WebSocket和Server-Sent Events。如果你需要流式通信,还是得用gRPC原生协议,或者用Envoy做协议转换。

好了,这一章的内容就到这里。拦截器、负载均衡、健康检查、TLS、网关集成,这五个点单独看都不难,但组合起来就是一套完整的微服务通信基础设施。下一章,我们会聊聊事件驱动架构,那是另一个有意思的话题。