一、生产目标与调用链
本文从零构建一个 Go gRPC 订单查询服务,覆盖 Protobuf 契约、代码生成、TLS、Deadline、重试、健康检查、优雅退出、负载均衡、OpenTelemetry 和故障演练。重点不是让 RPC “能通”,而是让失败有边界、请求可追踪、发布可回滚。
Client -> resolver/load balancer -> gRPC server replicas
| | |-- TLS/mTLS
| | |-- deadline propagation
| | |-- health service
| `-- retry policy `-- traces + metrics + logs
`-- request id / idempotency key
二、初始化与工具版本
固定 Go module 和 Protobuf 工具版本。生成器应作为构建依赖记录,CI 重新生成后必须无 diff。
mkdir order-rpc && cd order-rpc
go mod init example.com/order-rpc
go get google.golang.org/grpc
go get google.golang.org/protobuf
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
protoc --version
go version
生产仓库建议用 tools.go 或构建镜像固定生成器版本,而不是每次安装 latest。
三、设计可演进的 Protobuf 契约
字段编号发布后不能复用;删除字段要 reserved。错误使用标准 gRPC status,业务响应不要再嵌套一套模糊 code/message。
syntax = "proto3";
package orders.v1;
option go_package = "example.com/order-rpc/gen/orders/v1;ordersv1";
service OrderService {
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);
}
message GetOrderRequest { string order_id = 1; }
message GetOrderResponse { Order order = 1; }
message Order {
string id = 1;
string status = 2;
int64 total_cents = 3;
reserved 4;
reserved "legacy_note";
}
message WatchOrderRequest { string order_id = 1; }
message OrderEvent { string order_id = 1; string status = 2; }
mkdir -p gen
protoc -I proto \
--go_out=gen --go_opt=paths=source_relative \
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
proto/orders/v1/order.proto
git diff --exit-code -- gen
四、实现服务与规范错误
校验输入,映射 NotFound、InvalidArgument、Unavailable 等状态。内部错误日志保留 cause,但不要把数据库错误文本返回客户端。
func (s *Server) GetOrder(ctx context.Context, req *ordersv1.GetOrderRequest) (*ordersv1.GetOrderResponse, error) {
if req.GetOrderId() == "" {
return nil, status.Error(codes.InvalidArgument, "order_id is required")
}
order, err := s.repo.Find(ctx, req.GetOrderId())
if errors.Is(err, sql.ErrNoRows) {
return nil, status.Error(codes.NotFound, "order not found")
}
if err != nil {
s.logger.Error("find order", "order_id", req.GetOrderId(), "error", err)
return nil, status.Error(codes.Internal, "internal error")
}
return &ordersv1.GetOrderResponse{Order: mapOrder(order)}, nil
}
Repository 必须接受 context,让客户端取消或 deadline 能传入数据库驱动。
五、客户端必须设置 Deadline
时间预算必须端到端传播
gRPC 默认不设置 deadline,客户端可能无限等待。deadline 要覆盖 DNS、连接、排队、服务处理和响应传输,并由上游 SLO 推导。
ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
defer cancel()
resp, err := client.GetOrder(ctx, &ordersv1.GetOrderRequest{OrderId: id})
if status.Code(err) == codes.DeadlineExceeded {
metrics.DeadlineExceeded.Add(ctx, 1)
}
服务端在昂贵步骤前检查剩余时间:
if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < 50*time.Millisecond {
return nil, status.Error(codes.DeadlineExceeded, "insufficient time budget")
}
Go gRPC 默认传播 deadline,但跨 HTTP、消息队列或自定义客户端时要显式传递时间预算。
六、重试只用于幂等且可恢复的调用
限制放大倍数
重试会放大流量。只对明确幂等的读请求和 Unavailable 等瞬时错误启用,设置最大尝试、退避、抖动和总 deadline。写请求必须使用幂等键或不自动重试。
{
"methodConfig": [{
"name": [{"service": "orders.v1.OrderService", "method": "GetOrder"}],
"timeout": "0.8s",
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.05s",
"maxBackoff": "0.2s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE"]
}
}]
}
conn, err := grpc.NewClient(target,
grpc.WithTransportCredentials(creds),
grpc.WithDefaultServiceConfig(serviceConfig),
)
监控 original attempts 与 transparent retries,防止下游故障时重试风暴。
七、TLS 与 mTLS
服务端使用 SAN 正确的证书;客户端验证 CA 和 server name。内部零信任场景可要求客户端证书。
cert, err := tls.LoadX509KeyPair("/run/tls/server.crt", "/run/tls/server.key")
if err != nil { log.Fatal(err) }
caPEM, _ := os.ReadFile("/run/tls/ca.crt")
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caPEM)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientCAs: pool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS13,
}
server := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
openssl s_client -connect orders.example.com:443 \
-servername orders.example.com -showcerts </dev/null
grpcurl -cacert ca.crt -cert client.crt -key client.key \
orders.example.com:443 grpc.health.v1.Health/Check
证书轮换要演练双 CA 信任窗口,避免同时替换服务端证书和信任根造成全断。
八、健康检查与就绪状态
实现标准 gRPC Health Checking Protocol,负载均衡器和客户端可以按服务名查询。启动时先 NOT_SERVING,依赖完成后再 SERVING;退出前先切回 NOT_SERVING。
healthServer := health.NewServer()
healthpb.RegisterHealthServer(server, healthServer)
healthServer.SetServingStatus("orders.v1.OrderService", healthpb.HealthCheckResponse_NOT_SERVING)
if err := repo.Ping(ctx); err != nil { log.Fatal(err) }
healthServer.SetServingStatus("orders.v1.OrderService", healthpb.HealthCheckResponse_SERVING)
健康检查不能执行重查询;数据库可用性用带短 deadline 的 ping,并避免因为非关键依赖短抖动让全部实例同时摘除。
九、拦截器:认证、恢复、指标和日志
拦截器顺序要固定:请求 ID/trace -> 认证 -> 限流 -> 业务指标 -> panic recovery。日志字段包含 method、status、duration、trace_id,不记录消息中的敏感字段。
server := grpc.NewServer(
grpc.Creds(credentials.NewTLS(tlsConfig)),
grpc.ChainUnaryInterceptor(
otelgrpc.UnaryServerInterceptor(),
requestIDInterceptor,
authInterceptor,
metricsInterceptor,
recoveryInterceptor,
),
grpc.MaxRecvMsgSize(4<<20),
grpc.MaxSendMsgSize(4<<20),
)
限制消息大小,防止单次请求耗尽内存。流式接口还要限制并发流、发送频率和每个订阅的缓冲。
十、OpenTelemetry 链路
服务启动时配置 OTLP exporter 和 resource,客户端/服务端都安装 gRPC instrumentation。采样率、敏感属性和 exporter 队列需要明确。
exp, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("order-rpc"),
attribute.String("service.version", release),
)),
)
otel.SetTracerProvider(tp)
curl -s http://otel-collector:13133/
grpcurl -plaintext orders:9090 list
指标至少覆盖每 method 的请求数、状态码、延迟直方图、活跃 RPC、消息大小、deadline exceeded、重试、连接状态和健康状态。
十一、优雅退出与发布
先摘流量再停止进程
收到 SIGTERM 后先将健康状态设为 NOT_SERVING,等待负载均衡停止新流量,再 GracefulStop;超时后 Stop。
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
<-sig
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
done := make(chan struct{})
go func() { server.GracefulStop(); close(done) }()
select {
case <-done:
case <-time.After(25 * time.Second):
server.Stop()
}
Kubernetes 探针可通过原生 gRPC:
ports:
- {name: grpc, containerPort: 9090}
readinessProbe:
grpc: {port: 9090, service: orders.v1.OrderService}
periodSeconds: 5
livenessProbe:
grpc: {port: 9090}
periodSeconds: 10
terminationGracePeriodSeconds: 35
十二、测试与故障演练
使用 buf/protoc lint 保证契约,单元测试用 bufconn,集成测试启动 TLS 服务,负载测试覆盖 unary 和 streaming。故障注入包括延迟、Unavailable、连接断开、证书过期和优雅退出。
go test -race ./...
go vet ./...
grpcurl -cacert ca.crt orders.example.com:443 list
ghz --insecure --proto proto/orders/v1/order.proto \
--call orders.v1.OrderService.GetOrder \
-d '{"order_id":"demo-1"}' -c 50 -z 5m orders:9090
tc qdisc add dev eth0 root netem delay 300ms 50ms loss 1%
# run deadline/retry assertions, then restore
tc qdisc del dev eth0 root
故障演练必须验证客户端不会无限等待、重试量受控、trace 能定位慢段、发布时长流不中断或按契约重连。
十三、排障顺序
- DNS 与 TCP:
getent hosts、nc -vz。 - TLS:证书链、SAN、客户端证书和时间。
- HTTP/2:代理是否保留 gRPC,是否误降级。
- gRPC status:区分 DeadlineExceeded、Unavailable、PermissionDenied。
- 服务依赖:数据库、缓存和 worker saturation。
- trace:定位客户端、代理、服务还是下游耗时。
grpcurl -vv -cacert ca.crt orders.example.com:443 \
grpc.health.v1.Health/Check
GODEBUG=http2debug=2 ./order-client --id demo-1
十四、官方资料与验收清单
- gRPC Go Quickstart 与 Basics:生成代码、服务实现、客户端和 streaming。
- gRPC Deadlines:客户端 deadline 与传播。
- gRPC Retry、Health Checking、Authentication 与 Status Codes 指南。
- OpenTelemetry Go 与 gRPC instrumentation 官方文档。
验收:契约兼容检查通过;所有客户端有 deadline;只对幂等方法重试;TLS/mTLS 验证通过;标准健康服务正确切换;SIGTERM 可优雅结束;指标按 method/status 聚合;trace 跨客户端与服务连续;故障演练没有无限等待或重试风暴。