gRPC 生产落地:从 ClusterIP 长连接倾斜到 Headless DNS、Envoy 与全链路治理
gRPC 在 Kubernetes 中“能调用成功”并不等于“已经具备生产级负载均衡”。当客户端只解析到一个 ClusterIP,再通过一条长期存在的 HTTP/2 连接发送请求时,多个 Pod 很可能长期冷热不均。
本文以 service-a 这一通用 Java 服务为例,复盘一条典型的生产治理路径:保留现有 ClusterIP Service 兼容原有调用,再增加只供内部 gRPC 发现的 Headless Service,让 Java gRPC 客户端和 Envoy 看见每一个 Pod,并把服务发现、健康检查、摘流、连接排空、超时重试、可观测性和发布验收串成闭环。
如果需要先补齐协议和 Java 基础,可以依次阅读 HTTP/2 协议层、Java 与 Netty 实现 和 gRPC 生产治理;本文专注 Kubernetes 与 Envoy 的端点级落地。
这不是一份只解决“DNS 返回几个 IP”的配置说明。生产落地需要同时回答六个问题:
- 客户端究竟发现了 VIP,还是每个 Pod?
- 负载均衡发生在 TCP 连接、HTTP/2 Stream,还是 RPC 级别?
- 不可用 Pod 会不会继续出现在发现结果里?
- Pod 下线时,存量长连接和在途 RPC 如何处理?
- Java 客户端与 Envoy 谁负责重试、健康检查和摘除?
- 如何证明流量真的分散,而不是只证明 DNS 能解析?
一、事故起点:ClusterIP 没坏,但层级不对
原来的调用链如下:
Java / Envoy
│
│ dns:///service-a.application.svc.cluster.local:8080
▼
ClusterIP 10.96.120.20:8080
│
│ kube-proxy / dataplane:按连接选择后端
▼
Pod A / Pod B / Pod C
普通 Kubernetes Service 的 DNS 返回 Service 虚拟 IP。客户端看到的后端集合只有一个地址:10.96.120.20:8080。kube-proxy 或集群数据面会把新连接转发到某个 Pod,但它并不理解每个 gRPC RPC 的业务边界。
问题来自 gRPC 的传输模型:
- gRPC 通常在 HTTP/2 长连接上复用多个并发 Stream;
- kube-proxy 的负载均衡通常发生在新建连接时,而不是每个 RPC 开始时;
- 一条已经建立的连接会继续流向当初选中的 Pod;
- Java 客户端即使配置了
round_robin,如果名称解析器只交给它一个 ClusterIP,它也只有一个地址可选。
所以,round_robin + ClusterIP 并不会自动变成 Pod 级轮询。它最多在“一个虚拟 IP”这一层轮询,真正的 Pod 选择仍留给连接级转发。
ClusterIP 非常适合普通 HTTP、短连接、由 Sidecar 负责端点发现,或不要求客户端直接感知 Pod 的场景。这里的问题是:我们希望 gRPC 客户端或 Envoy 自己执行端点级负载均衡,却只给了它一个 VIP。
二、目标架构:保留 ClusterIP,增加 gRPC 专用 Headless Service
不要为了 gRPC 直接把原 Service 改成 Headless。更稳妥的方式是保留兼容入口,再新增一个内部发现入口:
Headless Service 没有 ClusterIP,也不依赖 kube-proxy 提供虚拟 IP 转发。带 selector 的 Headless Service 会生成 EndpointSlice,集群 DNS 会把 Service 名称解析为后端 Pod IP。客户端因此能把每个 Pod 视为独立地址。
这个架构有三个好处:
- 原有 HTTP、运维脚本和未迁移调用方继续使用
service-a; - Java gRPC 客户端可以对多个地址执行
round_robin; - Envoy
STRICT_DNS可以把每个 DNS 地址建模为独立上游并管理连接池。
2.1 Headless Service 清单
apiVersion: v1
kind: Service
metadata:
name: service-a-grpc-headless
namespace: application
labels:
app.kubernetes.io/managed-by: Helm
spec:
clusterIP: None
publishNotReadyAddresses: false
selector:
app: service-a
ports:
- name: grpc
port: 8080
targetPort: 8080
protocol: TCP
这里最重要的不是 clusterIP: None,而是以下三个条件必须同时成立:
selector必须与 Deployment 的 Pod label 完全一致;targetPort必须命中真实监听 gRPC 的8080;publishNotReadyAddresses: false必须与正确的 gRPC readinessProbe 配合。
如果 selector 漂移,Service 会存在、DNS 名也可能存在,但 EndpointSlice 没有可用地址。如果 readinessProbe 仍检查 HTTP 8200,则“HTTP 已就绪、gRPC 端口不可服务”的 Pod 仍可能进入 Headless DNS。
2.2 必须进入 Helm,而不是只在控制台创建
假设当前资源由 Helm release application-stack 管理,Headless Service 也应成为 Chart 的声明式资源。控制台手工创建的问题不只是“下次可能被覆盖”,还包括:
- 测试、预发和生产环境无法复现;
helm template、代码审查和 Git 历史看不到它;- selector、端口或标签变化时不会同步演进;
- 回滚 release 时资源状态与应用版本不一致。
可以把它做成可开关的 Chart 模板:
grpcHeadlessService:
enabled: true
name: service-a-grpc-headless
port: 8080
publishNotReadyAddresses: false
{{- if .Values.grpcHeadlessService.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.grpcHeadlessService.name }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
clusterIP: None
publishNotReadyAddresses: {{ .Values.grpcHeadlessService.publishNotReadyAddresses }}
selector:
app: service-a
ports:
- name: grpc
port: {{ .Values.grpcHeadlessService.port }}
targetPort: 8080
protocol: TCP
{{- end }}
实际 Chart 若已有 _helpers.tpl 和统一 selector helper,应复用同一 helper,不要复制一套可能漂移的标签。
三、Java 客户端:发现多个地址之后还要真正启用负载均衡
调用方配置改为:
application:
grpc:
enabled: true
governance:
enabled: true
load-balancing-policy: round_robin
grpc:
client:
service-a:
address: dns:///service-a-grpc-headless.application.svc.cluster.local:8080
negotiation-type: plaintext
dns:/// 的三个 / 不是笔误。它表达的是使用 DNS NameResolver,并把后面的 FQDN 作为 target path。解析结果应是一组 Pod IP,而不是一个 ClusterIP。
仅修改 YAML 仍不足以证明生效,还要核实以下调用链:
application.yml
→ Spring ConfigurationProperties / starter 配置绑定
→ ManagedChannelBuilder.forTarget(...)
→ defaultLoadBalancingPolicy("round_robin") 或 Service Config
→ 长生命周期 ManagedChannel
→ 复用 Stub 发起 RPC
生产中最常见的两个“配置看起来正确但没有效果”问题是:
- 自定义治理属性没有真正传给
ManagedChannelBuilder,客户端仍使用默认策略; - 每次请求都创建并关闭 Channel,既失去连接复用,也制造握手、DNS 和线程资源开销。
Channel 应按目标服务长生命周期复用。Stub 通常是轻量、线程安全的调用入口,可以在调用时派生 deadline 等选项,不需要每个请求重建 Channel。
3.1 round_robin 不等于每个业务请求绝对平均
round_robin 以可用 Subchannel 为基础选择后端,但以下因素会让短窗口内请求数不完全相等:
- Pod 加入和退出的时间不同;
- RPC 时长、流式调用和并发度不同;
- 某些地址尚未进入 READY;
- 客户端实例数量和各自连接状态不同;
- 重试可能把一次业务请求变成多次 attempt。
验收目标应该是“无单 Pod 长期独占且负载与容量合理”,而不是强求每个 Pod 的计数精确相等。
四、Envoy:必须同时配置 STRICT_DNS、HTTP/2 与端点级健康治理
Helm values 可以这样表达:
grpcBackend:
host: service-a-grpc-headless.application.svc.cluster.local
port: 8080
dnsRefreshRate: 5s
lbPolicy: LEAST_REQUEST
但这些只是业务 Chart 的字段名。最终渲染出的 Envoy cluster 必须真正包含对应语义:
clusters:
- name: service_a_grpc
type: STRICT_DNS
connect_timeout: 1s
dns_refresh_rate: 5s
lb_policy: LEAST_REQUEST
load_assignment:
cluster_name: service_a_grpc
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: service-a-grpc-headless.application.svc.cluster.local
port_value: 8080
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
health_checks:
- timeout: 1s
interval: 5s
unhealthy_threshold: 2
healthy_threshold: 2
grpc_health_check:
service_name: ""
outlier_detection:
consecutive_5xx: 5
interval: 10s
base_ejection_time: 30s
max_ejection_percent: 50
三个容易遗漏的点:
- 只有 host 改成 Headless DNS,但 cluster 仍是
STATIC或LOGICAL_DNS,不会得到期望的多端点模型; - 上游没有显式启用 HTTP/2,gRPC 转发会失败或协议不匹配;
- 只依赖 DNS 移除而没有主动健康检查和异常实例摘除,故障收敛速度完全受 readiness、EndpointSlice 和 DNS 刷新链路影响。
STRICT_DNS 每次解析都会把返回的每个 IP 作为独立上游。IP 从解析结果消失后,Envoy 会认为该 host 已被移除,并开始排空关联连接池。dns_refresh_rate: 5s 表示最终一致性窗口,不代表故障发生后 5 秒内所有存量 Stream 都必然消失。
示例里的 consecutive_5xx 主要覆盖 HTTP/传输层 5xx,不能把所有非 OK 的 gRPC status 都等同为 HTTP 5xx。业务级 gRPC 失败仍要通过 gRPC status 指标、主动 Health Check 和必要的重试/异常摘除策略单独治理。
LEAST_REQUEST 适合 RPC 耗时差异较大的服务,因为它倾向选择当前活动请求更少的实例。对执行时间高度一致的一元 RPC,ROUND_ROBIN 也可能足够。策略选择要结合压测,而不是看到“最少请求”就默认更优。
五、健康检查:检查真实 gRPC 能力,而不是“另一个端口还活着”
示例服务的 gRPC 端口是 8080,HTTP 端口是 8200。如果 Pod readinessProbe 只检查 HTTP 8200,会出现这种错误状态:
HTTP 8200 Ready
gRPC 8080 未监听 / Health NOT_SERVING
Pod Ready = True
EndpointSlice ready = True
Headless DNS 返回这个 Pod IP
Java / Envoy 把 RPC 发给坏端点
服务端应实现标准 grpc.health.v1.Health 协议,并让 Kubernetes 直接探测 gRPC 8080。Kubernetes 原生 gRPC Probe 从 v1.27 起进入稳定状态:
containers:
- name: service-a
ports:
- name: grpc
containerPort: 8080
- name: http
containerPort: 8200
startupProbe:
grpc:
port: 8080
service: ""
periodSeconds: 2
failureThreshold: 60
timeoutSeconds: 1
readinessProbe:
grpc:
port: 8080
service: ""
periodSeconds: 5
failureThreshold: 2
timeoutSeconds: 1
livenessProbe:
grpc:
port: 8080
service: ""
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 1
探针语义应有所区分:
| 探针 | 回答的问题 | 失败结果 | 设计原则 |
|---|---|---|---|
| startup | gRPC Server 是否已完成启动 | 启动期继续等待,超限后重启 | 覆盖最慢启动和预热时间 |
| readiness | 现在是否应接收新 RPC | 从 Service Endpoint 摘除 | 可以包含关键依赖与业务预热状态 |
| liveness | 进程是否已不可恢复 | 重启容器 | 不要因短暂下游故障制造重启风暴 |
原生 gRPC Probe 使用数值端口,不能引用命名端口。旧版本集群可以使用 grpc-health-probe exec 探针或至少使用 tcpSocket: 8080 过渡,但 TCP 成功只能证明端口可建立连接,不能证明 gRPC 服务处于 SERVING。
对于只提供 TLS 的 gRPC 服务,还要先确认集群版本和 gRPC Probe TLS 能力;不能默认明文原生探针一定适用。本文示例中的集群内调用明确配置为 plaintext,因此探针与服务协议一致。
六、发布与下线:从“摘出 DNS”到“排空在途 RPC”
readiness 失败只会阻止新流量继续进入,并不会瞬间终止已经建立的 HTTP/2 Stream。正确的优雅下线顺序是:
Java 服务端应在关闭钩子中执行有上限的 graceful shutdown:
healthStatusManager.enterTerminalState();
server.shutdown();
if (!server.awaitTermination(25, TimeUnit.SECONDS)) {
server.shutdownNow();
}
Deployment 同时要提供足够的终止窗口:
spec:
template:
spec:
terminationGracePeriodSeconds: 40
如果应用无法在收到 SIGTERM 时先切换 readiness,可使用短 preStop 作为传播缓冲,但固定 sleep 只是兜底,不是完整的连接排空机制。真正的完成条件应是:停止接收新请求、等待在途 RPC,并在预算耗尽后强制关闭。
滚动发布还要配置合理的 maxUnavailable、maxSurge 和 PodDisruptionBudget,避免 DNS 中同时失去过多 Ready 地址。
七、Deadline、重试与 Wait-for-Ready:必须共同受预算约束
gRPC 默认不会替你设置业务 deadline。没有 deadline 的调用可能长期占用 Stream、线程、内存和下游连接。
ServiceReply reply = serviceAStub
.withDeadlineAfter(2, TimeUnit.SECONDS)
.execute(request);
deadline 应来自业务 SLO 和压测结果,并沿调用链传播。服务端检测到取消后,还要主动停止自己启动的数据库查询、外部请求或计算任务;仅仅让客户端超时,不会自动回收所有业务工作。
重试必须同时满足:
- 方法具备幂等性,或使用幂等键、任务 ID、唯一约束去重;
- 仅重试明确的瞬时状态,例如部分场景下的
UNAVAILABLE; - 单次 attempt、退避和总 deadline 有清晰预算;
- Java 客户端、Envoy 和业务代码只保留一个主要重试层,避免倍增;
- 监控区分 logical RPC 与 attempt 数量。
如果 Java 最多 3 次、Envoy 再最多 3 次,一次业务调用在最坏情况下可能变成 9 次上游 attempt。对创建任务、扣费、文件写入等非幂等操作,这既是容量风险,也是数据一致性风险。
wait-for-ready 适合可以等待短暂恢复的批处理或后台任务,但必须配合 deadline。在线请求通常更需要快速失败,把时间留给降级或上游返回。
八、Keepalive、连接与背压:不要用 Ping“修复”负载均衡
Keepalive 的作用是发现失效连接、穿越会回收空闲连接的中间网络设备,不是让请求更平均。过于激进的 HTTP/2 PING 会浪费 CPU 和网络资源,还可能被服务端判定为滥用并发送 GOAWAY。
上线前应统一客户端、Envoy、服务端和云负载设备的连接策略:
- 空闲连接多久会被关闭;
- 客户端多久发送 Keepalive;
- 服务端允许的最小 Ping 间隔;
- 最大连接年龄和优雅关闭时间;
- HTTP/2 每连接最大并发 Stream;
- 最大入站消息大小和元数据大小。
对于可能携带较大文件或执行长耗时任务的服务,更推荐 gRPC 只传任务元数据、对象存储地址和状态,不要把无限大小文件直接塞进单个 RPC。入口必须限制消息大小、并发数和业务队列长度。
Java 服务端还要把阻塞 I/O 与网络处理解耦,并使用有界业务线程池或并发许可。无限线程、无限队列和无限 deadline 叠加,是比负载不均更危险的事故组合。
九、安全:plaintext 是部署选择,不是默认安全结论
示例使用:
negotiation-type: plaintext
它只适用于经过威胁建模后确认可信的集群内网络。生产环境至少要明确:
- Namespace 与 NetworkPolicy 是否限制非预期调用方;
- 服务端是否校验 JWT、租户、用户和调用方身份;
- Envoy 是否完整转发
authorization、x-tenant-id、trace context 等元数据; - 日志和指标是否避免记录敏感文档、Token 和原始 PII;
- 跨集群、跨 VPC 或高敏数据链路是否应启用 TLS/mTLS;
- 证书轮换是否会触发连接重建,并经过故障演练。
Headless Service 让调用方直接连接 Pod IP,也意味着安全策略不能只保护 ClusterIP 入口。NetworkPolicy、服务端鉴权和 mTLS 应覆盖 Pod 级直连路径。
十、可观测性:不要只看成功率,要看每个端点的行为
生产指标至少覆盖四层:
| 层级 | 关键观测项 | 要回答的问题 |
|---|---|---|
| Kubernetes | Ready Pod 数、EndpointSlice 地址数、Pod 重启、终止状态 | 发现集合是否正确 |
| DNS / Resolver | 解析成功率、返回地址数、刷新失败、Channel connectivity state | 客户端是否持续看到端点变化 |
| gRPC | logical RPC、attempt、status code、deadline exceeded、active streams、消息大小 | 调用是否健康、重试是否放大 |
| Envoy / Pod | healthy host、upstream connect failure、active requests、P50/P95/P99、按 Pod QPS/CPU | 流量是否合理分布 |
建议日志字段:
trace_id
rpc.service
rpc.method
grpc.status_code
deadline_ms
attempt
peer.address
upstream_host
duration_ms
request_size
response_size
不要把 user_id、文件名或任务 ID 直接作为 Prometheus label,否则会制造高基数。它们可以进入受控日志或 Trace 属性。
10.1 推荐告警
- Ready Endpoint 数低于期望副本数;
- 5 分钟窗口内
UNAVAILABLE、DEADLINE_EXCEEDED明显上升; - attempt / logical RPC 比例持续升高;
- 单 Pod QPS、active streams 或 CPU 长期显著偏离同组实例;
- Envoy DNS 解析失败或 healthy host 归零;
- graceful shutdown 超时、强制关闭次数增加;
- gRPC 消息大小接近限制或服务端拒绝率上升。
十一、生产发布顺序与验收命令
建议按以下顺序发布,任何一步异常都可以切回原 ClusterIP 地址:
- 服务端先实现标准 gRPC Health 和优雅停机;
- 将正确的 gRPC startup/readiness/liveness Probe 发布到所有 Pod;
- 在
application-stackChart 中加入 Headless Service; - 用 Helm 渲染和 lint 验证资源,再升级测试环境;
- 验证 EndpointSlice 与 DNS 确实返回所有 Ready Pod IP;
- 先灰度 Envoy 或少量 Java 调用方;
- 验证 RPC 分布、错误率、P99 和重试放大;
- 执行 Pod 删除、滚动发布、单 Pod gRPC 故障和 DNS 短暂失败演练;
- 扩大到全部调用方,并保留原 ClusterIP 作为回滚路径。
11.1 Helm 静态验证
helm lint ./application-stack
helm template application-stack ./application-stack \
--namespace application \
-f values-production.yaml \
| grep -A30 'name: service-a-grpc-headless'
确认渲染结果而不是 values 文件本身。对 Envoy 同样要检查最终 ConfigMap 或启动配置中确实出现 STRICT_DNS、Headless FQDN、HTTP/2 和预期的 LB policy。
11.2 EndpointSlice
kubectl -n application get endpointslice \
-l kubernetes.io/service-name=service-a-grpc-headless \
-o wide
kubectl -n application get endpointslice \
-l kubernetes.io/service-name=service-a-grpc-headless \
-o yaml
应看到多个地址,且 conditions.ready: true。地址数量应与 Ready 且被 selector 命中的 Pod 数一致。
11.3 集群内 DNS
kubectl -n application exec <caller-or-envoy-pod> -- \
nslookup service-a-grpc-headless.application.svc.cluster.local
DNS 应返回多个 Pod IP,而不是 10.96.120.20。如果镜像带 dig,可进一步查看:
kubectl -n application exec <caller-or-envoy-pod> -- \
dig +short service-a-grpc-headless.application.svc.cluster.local A
11.4 gRPC Health
grpcurl -plaintext \
-d '{}' \
service-a-grpc-headless.application.svc.cluster.local:8080 \
grpc.health.v1.Health/Check
再逐个 Pod IP 验证,确保不是只有其中一个 Pod 能返回 SERVING。
11.5 真实负载分布
持续发送足够长时间、足够并发的 RPC,然后按 peer.address、Envoy upstream_host 或服务端 Pod label 聚合请求数、active streams、耗时和错误率。
nslookup 只能证明发现结果,不能证明 Java 的 round_robin 已生效;EndpointSlice 正确也不能证明 Envoy 最终配置已加载。必须用真实 RPC 分布完成最后一跳验收。
11.6 故障演练
至少完成以下测试:
- 删除一个正在接收请求的 Pod,观察 EndpointSlice、DNS、Envoy 和 Java Channel 的收敛;
- 让某个 Pod 的 gRPC Health 返回
NOT_SERVING,但保持进程和 HTTP8200存活; - 连续滚动发布,确认在途 RPC 不出现集中失败;
- 暂时阻断一个 Pod 的
8080,验证主动健康检查和异常摘除; - 注入慢响应,验证 deadline、并发限制和重试预算;
- 恢复 Pod,确认它能重新加入发现集合并平稳承接流量。
十二、这次 gRPC 生产落地踩过的坑
| 坑 | 表象 | 根因 | 正确处理 |
|---|---|---|---|
| 把 ClusterIP 当成 Pod 发现 | DNS 正常、调用也成功,但单 Pod 很热 | DNS 只返回 VIP | gRPC 专用 Headless Service |
配了 round_robin 仍不均 | 客户端日志显示策略已配置 | Resolver 只有一个 ClusterIP 地址 | dns:///headless-fqdn:8080 |
| 只手工创建 Service | 当天有效,后续环境不一致 | 资源不在 Helm/Git 声明中 | 加入 application-stack Chart |
| Headless 有名无端点 | Service 存在但解析不到 IP | selector 与 Pod label 不匹配 | 对照 Deployment label 与 EndpointSlice |
| 错误发布 NotReady 地址 | 启动中或故障 Pod 仍收到 RPC | publishNotReadyAddresses: true | 保持 false,并修正 readiness |
| readiness 检查错端口 | HTTP 正常但 gRPC 大量失败 | 只探测 8200 | 标准 gRPC Health 探测 8080 |
| 只改 Envoy host | DNS 有多个 IP,Envoy 仍只用一个 | cluster 类型不是 STRICT_DNS | 检查最终渲染配置 |
| Envoy 上游不是 HTTP/2 | 连接或协议错误 | 未声明上游 HTTP/2 | 配置 HTTP/2 protocol options |
| 每次请求新建 Channel | CPU、线程和连接数上升 | 把 Channel 当成短生命周期客户端 | 复用长生命周期 Channel |
| 用 Keepalive 修复倾斜 | Ping 增多,分布仍不稳定 | Keepalive 不负责 RPC 负载均衡 | 修复服务发现与 LB policy |
| 没有 deadline | 故障时请求和线程长期堆积 | gRPC 默认不提供业务超时 | 按方法设置和传播 deadline |
| 多层同时重试 | 下游故障时流量反而暴涨 | Java、Envoy、业务代码叠加重试 | 选定主重试层并设总预算 |
| 非幂等写被自动重试 | 重复任务、重复扣费或重复写入 | 未区分方法语义 | 幂等键、唯一约束,默认不重试写 |
| readiness 一摘就退出 | 滚动发布仍丢在途 RPC | Endpoint 移除不等于 Stream 已完成 | gRPC graceful shutdown + 终止预算 |
| liveness 检查所有依赖 | 下游故障引发本服务重启风暴 | 把“暂不可接流量”当成“进程已死” | 依赖故障进 readiness,liveness 保守 |
plaintext 被当成理所当然 | Pod 直连绕过边界保护 | 没有威胁建模 | NetworkPolicy、鉴权,必要时 mTLS |
只用 nslookup 验收 | DNS 多 IP,但流量仍倾斜 | 配置绑定或 LB policy 未生效 | 观察真实 RPC 的 upstream_host 分布 |
| 追求绝对平均 | 短窗口内计数不同就判定失败 | 请求时长、健康和连接状态不同 | 观察长期容量、延迟与热点,而非机械均分 |
十三、最终生产基线
一套可交付的 gRPC 生产基线应至少包含:
- 服务发现:Headless Service、正确 selector、Ready EndpointSlice;
- 客户端治理:DNS target、Pod 级 LB、长生命周期 Channel;
- 代理治理:Envoy
STRICT_DNS、HTTP/2、LB、健康检查、异常摘除; - 健康模型:标准 gRPC Health、startup/readiness/liveness 分工;
- 生命周期:先摘流、再停止新请求、最后排空在途 RPC;
- 韧性:deadline、单层受预算重试、幂等、背压和有界资源;
- 安全:身份与租户元数据、NetworkPolicy、TLS/mTLS 决策;
- 可观测性:状态码、attempt、端点分布、活动 Stream、P99 与 DNS/连接状态;
- 交付方式:Helm/Git 管理、分阶段灰度、可回滚、故障演练;
- 验收证据:EndpointSlice、DNS、Health、真实 RPC 分布和 Pod 下线收敛。
真正的生产落地不是“把 Service 改成 Headless”这一行 YAML,而是让 发现、选择、健康、摘流、排空、重试和观测使用同一套端点事实。只要其中一个环节仍停留在 ClusterIP、错误端口或未生效的配置上,流量倾斜和发布抖动就可能再次出现。