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)无缝接入
避坑清单
- proto 字段号一旦发布永久保留,删字段用 reserved
- enum 0 必须是 UNSPECIFIED,避免默认值歧义
- K8s 必用 Headless Service + client-side LB
- 客户端必设 keepalive,防止 NAT 超时断开
- 超时必须设(context.WithTimeout),不要默认无超时
- 错误必须用 status.Error,带 codes
- 大消息(> 4MB)显式设 MaxRecvMsgSize
- 必装拦截器:recovery + tracing + metrics + auth
- 开 reflection,grpcurl 调试方便
- 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