gRPC 内部 RPC 切换实录:P99 80ms→18ms 带宽 -71%

内部 RPC 从 REST+JSON 切到 gRPC 全实录:IDL 设计 + protobuf 字段规范 + Go 服务端 keepalive + Java 客户端连接池 + 错误码 status + 拦截器(recovery/tracing/metrics) + K8s Headless Service 客户端负载均衡 + 流式 RPC。P99 从 80ms 降到 18ms,带宽节约 71%。

2023 年我们一个内部服务调用从 REST + JSON 迁到 gRPC,初期遇到一堆坑:连接管理、超时、负载均衡、流式调用、错误码、超长 protobuf。投了一个月把这些都吃透,内部 RPC 调用 P99 从 80ms 降到 18ms,带宽节约 70%,服务依赖一目了然。本文复盘 gRPC 在 Go + Java 混合生产环境的完整实战。

为什么从 REST 切到 gRPC

原架构:Spring Boot + REST + JSON
痛点:
- JSON 体积大(平均 8KB,gRPC 后 1.5KB)
- 客户端代码全靠手写
- 没有正式接口定义,文档对不上
- 流式不友好(SSE 难用)
- 序列化慢(Jackson 反射)

切 gRPC 收益:
- HTTP/2 多路复用,单连接并发请求
- Protobuf 体积小、序列化快
- IDL 强类型,跨语言生成 SDK
- 流式天然支持(server / client / bidi stream)
- 内置健康检查、负载均衡、错误码
- Envoy / Istio 等服务网格完美支持

IDL 设计

// order.proto
syntax = "proto3";
package order.v1;

option go_package = "github.com/biz/order/v1;orderv1";
option java_package = "com.biz.order.v1";
option java_multiple_files = true;

import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";

service OrderService {
    rpc GetOrder(GetOrderRequest) returns (Order);
    rpc CreateOrder(CreateOrderRequest) returns (Order);
    rpc UpdateOrder(UpdateOrderRequest) returns (Order);
    rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);

    // 流式:服务端推送订单状态变化
    rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);

    // 客户端流:批量上传订单
    rpc BatchCreate(stream CreateOrderRequest) returns (BatchCreateResponse);

    // 双向流:实时聊天
    rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message Order {
    string id = 1;
    int64 user_id = 2;
    repeated OrderItem items = 3;
    OrderStatus status = 4;
    google.protobuf.Timestamp created_at = 5;
    google.protobuf.Timestamp updated_at = 6;

    // 业务字段
    string remark = 100;        // 业务字段从 100 开始,系统字段保留 1-99

    reserved 7, 8;              // 已废弃的字段,不要重用
    reserved "old_field";
}

enum OrderStatus {
    ORDER_STATUS_UNSPECIFIED = 0;   // proto3 enum 0 必须是默认值
    ORDER_STATUS_PENDING = 1;
    ORDER_STATUS_PAID = 2;
    ORDER_STATUS_SHIPPED = 3;
    ORDER_STATUS_COMPLETED = 4;
    ORDER_STATUS_CANCELLED = 5;
}

message GetOrderRequest {
    string id = 1;
    google.protobuf.FieldMask mask = 2;     // 只返回需要的字段
}

message UpdateOrderRequest {
    Order order = 1;
    google.protobuf.FieldMask update_mask = 2;
}

// 设计原则:
// 1. 字段号一旦发布永久保留,删字段用 reserved
// 2. enum 0 必须是 UNSPECIFIED
// 3. message 名字 + 字段名 snake_case(自动转 camelCase)
// 4. Request / Response 显式定义,不要直接用业务 message
// 5. 业务字段从 100 开始,系统字段 1-99

Go 服务端实现

// 生成代码
// protoc --go_out=. --go-grpc_out=. order.proto

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/grpc/keepalive"
    "google.golang.org/grpc/health"
    "google.golang.org/grpc/health/grpc_health_v1"

    pb "github.com/biz/order/v1"
)

type OrderServer struct {
    pb.UnimplementedOrderServiceServer
    repo Repository
}

func (s *OrderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
    if req.Id == "" {
        return nil, status.Error(codes.InvalidArgument, "id is required")
    }

    order, err := s.repo.GetByID(ctx, req.Id)
    if err == ErrNotFound {
        return nil, status.Errorf(codes.NotFound, "order %s not found", req.Id)
    }
    if err != nil {
        return nil, status.Errorf(codes.Internal, "db error: %v", err)
    }

    return order.ToProto(), nil
}

// 服务端流
func (s *OrderServer) WatchOrder(req *pb.WatchOrderRequest, stream pb.OrderService_WatchOrderServer) error {
    ch, cancel := s.subscribe(req.OrderId)
    defer cancel()

    for {
        select {
        case <-stream.Context().Done():
            return nil
        case evt := <-ch:
            if err := stream.Send(evt); err != nil {
                return err
            }
        }
    }
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("listen: %v", err)
    }

    grpcServer := grpc.NewServer(
        grpc.MaxRecvMsgSize(16*1024*1024),
        grpc.MaxSendMsgSize(16*1024*1024),
        grpc.KeepaliveParams(keepalive.ServerParameters{
            MaxConnectionIdle:     5 * time.Minute,
            MaxConnectionAge:      30 * time.Minute,
            MaxConnectionAgeGrace: 5 * time.Minute,
            Time:                  20 * time.Second,
            Timeout:               5 * time.Second,
        }),
        grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
            MinTime:             15 * time.Second,
            PermitWithoutStream: true,
        }),
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            recoveryInterceptor,
            tracingInterceptor,
            metricsInterceptor,
            authInterceptor,
        )),
    )

    pb.RegisterOrderServiceServer(grpcServer, &OrderServer{})

    // 健康检查
    healthSrv := health.NewServer()
    grpc_health_v1.RegisterHealthServer(grpcServer, healthSrv)
    healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)

    // reflection(给 grpcurl / Postman 用)
    reflection.Register(grpcServer)

    grpcServer.Serve(lis)
}

Go 客户端

// 连接池 + 负载均衡
import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/balancer/roundrobin"
    "google.golang.org/grpc/credentials/insecure"
)

conn, err := grpc.NewClient(
    "dns:///order.svc.cluster.local:50051",
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
    grpc.WithKeepaliveParams(keepalive.ClientParameters{
        Time:                10 * time.Second,
        Timeout:             3 * time.Second,
        PermitWithoutStream: true,
    }),
    grpc.WithUnaryInterceptor(retryInterceptor),
    grpc.WithDefaultCallOptions(
        grpc.MaxCallRecvMsgSize(16*1024*1024),
        grpc.MaxCallSendMsgSize(16*1024*1024),
    ),
)

defer conn.Close()

client := pb.NewOrderServiceClient(conn)

// 超时 + cancel
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

order, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: "123"})
if err != nil {
    st, ok := status.FromError(err)
    if ok {
        switch st.Code() {
        case codes.NotFound:
            // 处理 not found
        case codes.DeadlineExceeded:
            // 处理超时
        case codes.Unavailable:
            // 服务不可用,可重试
        }
    }
}

错误码规范

// 标准 gRPC code 14 个,够用了
codes.OK                 // 0
codes.Canceled           // 1 - 客户端取消
codes.Unknown            // 2 - 未知
codes.InvalidArgument    // 3 - 参数错
codes.DeadlineExceeded   // 4 - 超时
codes.NotFound           // 5 - 资源不存在
codes.AlreadyExists      // 6 - 资源已存在
codes.PermissionDenied   // 7 - 没权限
codes.ResourceExhausted  // 8 - 资源不足/限流
codes.FailedPrecondition // 9 - 前置条件不满足
codes.Aborted            // 10 - 并发冲突
codes.OutOfRange         // 11 - 超出范围
codes.Unimplemented      // 12 - 未实现
codes.Internal           // 13 - 内部错误
codes.Unavailable        // 14 - 服务不可用
codes.DataLoss           // 15 - 数据丢失
codes.Unauthenticated    // 16 - 未认证

// 带 details 的错误(用 google.rpc.ErrorInfo)
import "google.golang.org/genproto/googleapis/rpc/errdetails"

st := status.New(codes.InvalidArgument, "invalid order")
det := &errdetails.BadRequest{
    FieldViolations: []*errdetails.BadRequest_FieldViolation{
        {Field: "user_id", Description: "must be > 0"},
    },
}
st, _ = st.WithDetails(det)
return nil, st.Err()

拦截器(中间件)

// 1. 恢复 panic
func recoveryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
    defer func() {
        if r := recover(); r != nil {
            log.Errorf("panic in %s: %v\n%s", info.FullMethod, r, debug.Stack())
            err = status.Errorf(codes.Internal, "internal error")
        }
    }()
    return handler(ctx, req)
}

// 2. 链路追踪(OpenTelemetry)
import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"

grpc.NewServer(
    grpc.StatsHandler(otelgrpc.NewServerHandler()),
)

// 客户端
grpc.WithStatsHandler(otelgrpc.NewClientHandler())

// 3. Prometheus metrics
import grpc_prom "github.com/grpc-ecosystem/go-grpc-prometheus"

grpc_prom.EnableHandlingTimeHistogram()
grpcServer := grpc.NewServer(
    grpc.UnaryInterceptor(grpc_prom.UnaryServerInterceptor),
    grpc.StreamInterceptor(grpc_prom.StreamServerInterceptor),
)
grpc_prom.Register(grpcServer)

// 4. 重试(客户端)
import grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"

grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(
    grpc_retry.WithMax(3),
    grpc_retry.WithCodes(codes.Unavailable, codes.DeadlineExceeded),
    grpc_retry.WithBackoff(grpc_retry.BackoffExponential(100*time.Millisecond)),
))

负载均衡(K8s 场景)

# 问题:K8s Service 是 L4 LB,gRPC HTTP/2 长连接,
# 一个 client 只会落到一个 pod(负载不均衡)

# 方案 1:Headless Service + client-side LB
apiVersion: v1
kind: Service
metadata:
  name: order-grpc
spec:
  clusterIP: None     # headless
  selector:
    app: order
  ports:
    - port: 50051

# 客户端用 dns:/// 让 grpc 自己解析所有 pod IP
conn, _ := grpc.NewClient("dns:///order-grpc.default.svc:50051",
    grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`))

# 方案 2:Istio / Envoy
# Sidecar 代理 gRPC 流量,自动 L7 负载均衡

# 方案 3:proxyless gRPC + xDS(Istio 1.13+)
# gRPC 客户端直接从 Istio 获取负载均衡策略,不走 sidecar

性能对比

场景:订单查询服务,平均 8KB JSON 响应
压测:1000 并发,持续 5min

指标                  REST/JSON      gRPC          变化
==========================================================
QPS                  12000           45000         +275%
P50 延迟             20ms            5ms           -75%
P99 延迟             80ms            18ms          -78%
带宽                 95MB/s          28MB/s        -71%
CPU(server)         60%             35%           -42%
内存(server)        4GB             1.5GB         -63%
序列化耗时           5ms             0.3ms         -94%

业务影响:
- 内部服务调用全面切 gRPC,链路延迟降 60%
- 跨语言调用通过 IDL 强类型校验,联调时间缩短 70%
- 服务网格(Istio)无缝接入

避坑清单

  1. proto 字段号一旦发布永久保留,删字段用 reserved
  2. enum 0 必须是 UNSPECIFIED,避免默认值歧义
  3. K8s 必用 Headless Service + client-side LB
  4. 客户端必设 keepalive,防止 NAT 超时断开
  5. 超时必须设(context.WithTimeout),不要默认无超时
  6. 错误必须用 status.Error,带 codes
  7. 大消息(> 4MB)显式设 MaxRecvMsgSize
  8. 必装拦截器:recovery + tracing + metrics + auth
  9. 开 reflection,grpcurl 调试方便
  10. buf 替代 protoc(更现代,带 lint)

总结

gRPC 是内部服务通信的银弹:体积小、速度快、强类型、跨语言、流式天然。这次切换收益巨大,P99 延迟 80ms→18ms,带宽节省 70%。但也不是没有坑:K8s 负载均衡要特殊处理(Headless + client-side LB),keepalive 要配,大消息要调 size。最大的认知改变:gRPC 不是 REST 的替代品 — 对外 API 还是 REST + OpenAPI 更适合(浏览器友好、SEO 友好),gRPC 是内部服务调用的标配。如果你的微服务还在用 REST + JSON 互相调用,2024 年值得切 gRPC,收益看得见摸得着。配合 Istio / Envoy 服务网格,你能拿到熔断、限流、灰度、链路追踪一整套,这是 REST 给不了的。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Docker 镜像瘦身实录:80 服务从 800MB 平均降到 120MB

2026-5-19 12:55:28

技术教程

商品爆款 30w QPS 雪崩复盘:Redis 缓存三大问题工程修法

2026-5-19 13:00:57

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索