服务发现与 gRPC 服务间通信治理
30 秒版(开场)
交易所内网大量 gRPC 短链路(BFF→order、order→risk、wallet→ledger 查询)。服务发现常见:K8s DNS + Headless Service、Consul/etcd、或 Mesh(xDS)。Go 侧必配:超时、重试(仅幂等)、keepalive、客户端 LB、OTel trace 透传。关键词:不用裸 IP、metadata 传 trace_id/user_id。
3 分钟版(精讲深度)
- 是什么:微服务实例动态上下线时,调用方如何找到健康实例并完成可靠 RPC。
- 为什么:交易链路 P99 敏感;滚动发布频繁;实例数随行情波动。
- 怎么做:K8s 内
order-svc.namespace.svc:50051;客户端 resolver 订阅 endpoints;每 RPC context.WithTimeout。
10 分钟版
通信拓扑(交易所)
flowchart LR
BFF[BFF Pod] -->|gRPC| OMS[order-svc]
OMS -->|gRPC| Risk[risk-svc]
OMS -->|gRPC stream| ME[matching-svc]
Wallet[wallet-svc] -->|gRPC| Ledger[ledger-svc]
subgraph Discovery[K8s / Consul]
SVC[Service Registry]
end
OMS -.-> SVC
Risk -.-> SVC
服务发现方案对比
| 方案 |
机制 |
交易所适用 |
| K8s ClusterIP |
kube-dns 解析 Service |
默认首选 |
| Headless + 客户端 LB |
拿全部 Pod IP 轮询/加权 |
撮合、行情低延迟 |
| Consul/etcd |
独立注册中心 |
多集群、混合云 |
| Istio xDS |
Mesh 控制面下发 |
统一 mTLS/重试策略 |
gRPC Go 治理清单
| 项 |
配置要点 |
| 超时 |
每笔 RPC ctx 超时;BFF 聚合取 最短子超时之和上限 |
| 重试 |
仅幂等读 或带 idempotency-key 的写;下单 禁止盲重试 |
| Keepalive |
防 NAT/LB 静默断连;EnforcementPolicy.MinTime |
| 负载均衡 |
grpc-go round_robin + K8s resolver |
| 熔断 |
client 侧 gobreaker 或 Mesh outlier detection |
| 序列化 |
Protobuf;decimal 用 string 传输 |
| 元数据 |
traceparent、x-user-id、x-tenant-id |
交易所 RPC 分类
| 调用 |
超时建议 |
重试 |
| risk-svc 预检 |
50~100ms |
否 |
| ledger 余额查询 |
100ms |
是(读) |
| order 提交 |
200ms |
否(靠 clientOrderId 幂等) |
| wallet 冻结 |
300ms |
谨慎(查状态再重试) |
K8s Headless 示例(讲解)
matching-svc Headless:dns:///matching-svc.trading.svc.cluster.local
- Go
grpc.NewClient + round_robin 负载到各撮合 Pod(每 Pod 负责 symbol 子集时改 自定义 resolver/自定义 LB policy)
生产场景
- 滚动发布:readiness 通过后再进 endpoints;BFF 短暂 503 可接受
- 跨 AZ:优先同 AZ 调用;ledger 写走主 AZ
- DEX wallet→RPC:非 gRPC,走 HTTP provider 池 + 熔断
深挖问答
- gRPC 和 REST 怎么选? → 见 S-NET-01;内网 gRPC,公网 REST。
- 服务发现挂了怎么办? → 客户端缓存 endpoints + 指数退避;K8s 本地 watch。
- 大 payload(深度快照)? → 压缩 gzip;或走 MQ 推增量。
反模式
- 硬编码 Pod IP
- 下单 RPC 默认重试 3 次 → 重复单
- 全链路无 deadline → 级联阻塞
代码示例
conn, err := grpc.NewClient(
"dns:///order-svc.trading.svc.cluster.local:50051",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
)
if err != nil {
return err
}
defer conn.Close()
client := pb.NewRiskClient(conn)
callCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
_, err = client.PreCheckRisk(callCtx, req)
延伸阅读