原文作者:秦付灵,七猫技术团队,发表于 2022 年 9 月 5 日。本文根据原文核查整理,保留案例、关键配置和原始统计图,并补充截至 2026 年 10 月的版本与安全说明。
历史版本说明:原案例使用当时的阿里云 ACK、SLB 与社区 Ingress-NGINX,原文正文未给出上游社区版本号;其ACK截图显示阿里发行标识 v0.44.0.3-8e83e7dc6-aliyun。Kubernetes 官方已公告社区 Ingress-NGINX 于 2026 年 3 月退役,此后不再提供版本、缺陷或安全更新。现有部署仍可能继续运行,但本文不作为新集群选型和安装推荐。社区 Ingress-NGINX 与 F5 提供的另一款 NGINX Ingress Controller 也不是同一项目。详见官方退役公告和后续声明。
问题出在连接与请求的分配粒度
七猫推荐引擎原来通过阿里云 SLB 向集群外提供入口和负载均衡。服务使用 gRPC 长连接,客户端数量又不多,即使设置了定时重连,多个 Pod 之间仍存在明显的请求量差异。新建的 Kubernetes 模型服务集群也出现类似现象,因此团队尝试接入当时已支持 gRPC 转发的 Ingress-NGINX。
这个问题不能简单归结为“SLB 不支持 gRPC”。四层入口按连接选择后端,而 HTTP/2 可以在一条连接上承载许多 RPC。少量连接长时间停留在少数 Pod 上,就可能使其他 Pod 长期空闲。七层代理在能够识别并转发独立 RPC 的情况下,可以用不同粒度做上游分配。
编辑补充:这里讨论的是独立 RPC 的分配,不是把一条正在进行的双向流或服务端流拆成若干片段,随意迁到其他 Pod。长流的整个生命周期通常仍落在所选后端,扩容也不会自动把已有流平均切开。
Ingress、Service 与 Controller 各做什么
Ingress 是 Kubernetes 中描述入口转发规则的资源;Service 提供对后端工作负载的服务抽象;Ingress Controller 则是读取规则并实际处理请求的程序。只有创建 Ingress 对象,没有与其匹配的控制器,规则不会自动生效。
控制器通过 API Server 观察资源及后端变化,生成或更新代理配置,并依据 Host、路径等条件转发请求。原文用“重新生成 nginx.conf 并加载”的方式解释这一过程;实际更新机制依控制器版本和变更类型而定。原文中的 nginx -s load 不是标准 NGINX 重载命令,标准命令为 nginx -s reload;在受控制器管理的部署中,也不应把手工重载当作常规配置入口。
在案例拓扑中,Controller 自身又通过一个 type: LoadBalancer 的 Service 暴露,对外入口仍可由 SLB 提供。引入 Ingress 不必意味着完全移除云负载均衡器,而是改变其后的流量处理层。
原文列举的能力包括 HTTP/HTTPS、WebSocket/WSS 与 gRPC 转发、金丝雀与蓝绿发布、请求头改写、重定向、路径重写、限流、会话保持、访问日志、Prometheus 指标,以及通过 HPA 等方式扩缩容。具体功能和注解依赖控制器实现;尤其不能把允许任意配置片段的灵活性理解成无条件安全。
保留案例中的独立 gRPC Ingress 清单
原文在当时 ACK 控制台的“运维管理—组件管理—核心组件”中安装控制器,生成 kube-system 下的 Deployment 和 LoadBalancer Service。这是 2022 年的产品界面和部署经验,名称及当前支持状况应以云平台现行文档为准。
业务方可以通过控制台或 YAML 创建 Ingress。下面保留原案例的主要独立清单,同时作两项编辑修正:删除无有效值的 service-weight 注解;把复制来的、与实际域名不一致的证书注释改成要求证书覆盖本例域名。该清单用于理解历史配置,不应直接部署到新集群。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rec-gateway-ingress
namespace: rec-test1
labels:
name: rec-gateway-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
# 原文的历史实验值,不是通用超时建议
nginx.ingress.kubernetes.io/proxy-read-timeout: "1"
nginx.ingress.kubernetes.io/proxy-send-timeout: "1"
spec:
ingressClassName: nginx
rules:
- host: rec-test1.demo.com
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: rec-gateway-headless
port:
number: 9090
tls:
# 此 Secret 须事先存在于 rec-test1 命名空间;
# 证书须覆盖 rec-test1.demo.com。
- secretName: demo.com
hosts:
- rec-test1.demo.com
rules.host 是请求使用的域名,path 是路径匹配规则,backend.service 指向已有 Service 的名称与服务端口。这份 Ingress 不会创建后端应用、Service、DNS 记录或 TLS Secret。
backend-protocol: "GRPC" 告诉该控制器使用 gRPC 向后端转发。该配置在入口终止客户端 TLS,后端一段使用明文 gRPC;如果要求入口到后端也加密,需要按所用控制器配置 GRPCS、后端证书与验证策略。不能把入口有 TLS 写成端到端加密,也不能把 HTTP/2 等同于必须在所有链路使用 TLS。
两个 "1" 是案例采用的短代理读写超时。对于长 RPC、流式响应和长时间无数据的连接,这可能过于激进。应结合代理对超时的具体定义、业务 deadline、客户端 keepalive 和服务端策略配置,而不是照搬一秒。
路径规则与 Helm 片段需要分开看
Ingress 的 pathType 包括 Exact、Prefix 与 ImplementationSpecific。前两者表达精确匹配和按路径元素进行的前缀匹配;后一种把规则交给控制器实现。现代 networking.k8s.io/v1 清单要求提供该字段,不能把原文“Prefix 是默认类型”理解成可以省略。
原文还展示了与 gRPC 主线不同的 HTTP 路径改写:开启 use-regex,用 /api(/|$)(.*) 捕获后面的路径,再用 rewrite-target: "/$2" 去掉前缀。但示例又设置 pathType: Prefix,不能视为现代控制器通用有效的正则配置。正则语法、路径验证和重写行为需要按实际版本核验。
其 Helm 组合示意同时放入 Deployment、Headless Service 和 Ingress,用 --- 分隔 YAML 文档;Headless Service 的关键字段是 clusterIP: None,并通过标签选择器匹配 Pod。原文 Deployment 含省略号,最后一个 if 又缺少闭合 end,所以它并不是可直接运行的完整 Chart。本稿保留设计解释,不把不完整支线伪装成可交付部署模板。
作者在当时环境中选择 Headless Service。是否直接使用端点、是否存在额外 Service 转发层,以及最终均衡方式,取决于控制器实现;不能把 Headless 本身视为请求级均衡的充分条件。
入口地址与客户端 TLS
原文分别讨论公网 SLB、同 VPC 私网 SLB,以及同时提供两种入口。历史 ACK 配置通过 service.beta.kubernetes.io/alicloud-loadbalancer-address-type: intranet 表达私网入口;同时需要两种入口时,可为控制器配置额外的 LoadBalancer Service。这类操作涉及云资源、费用、路由和访问边界,没有连接云账户、读取 Secret 或修改集群。
gRPC 客户端访问案例中的 443 端口,应启用 TLS 并正确验证服务端身份。原文用 BloomRPC 和 Postman 举例,并给出 Go SDK 配置。原始代码先创建 insecure.NewCredentials(),只有安全开关打开时才改用 TLS,这使默认行为取决于未展示的配置。
编辑修正版(仅替换原 SDK 片段中传输凭证的选择):下面直接要求 TLS,显式指定服务端名并保留证书验证。它不是完整 Go 程序,也没有执行连接。
// 需要 crypto/tls、google.golang.org/grpc/credentials。
// 相比原文:不再以 insecure.NewCredentials() 作为默认值。
credential := credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: "rec-test1.demo.com",
})
// 将 credential 传给原有的 grpc.WithTransportCredentials。
如果使用私有 CA,还需配置可信根,而不是关闭验证。客户端没有提供自己的证书,通常表示未配置双向 TLS 的客户端身份;这与服务端证书是否需要验证是两件事。原文示例不构成 mTLS 实现。
原 SDK 片段还设置读写缓冲、收发消息大小、round_robin、Unary interceptor,以及每十秒发送、超时一秒并允许无活动流时发送的 keepalive。它们都依赖上下文中未展示的变量和服务端限制。HTTP/2 PING 只能探测连接活性,不能替代业务健康检查;过于频繁的 PING 也可能被服务端拒绝。客户端对入口连接做 round-robin,与入口向 Pod 分配 RPC,属于不同层次。
原始对照:四个 Pod、一个客户端连接、约七十秒
作者用一个客户端连接访问同一接口,比较 SLB 与 Ingress 两种入口下四个 Pod 的请求量。原文称客户端长连接活跃周期为六十秒,因而最初一分钟大部分请求落到一个 Pod,重连后又主要落到另一个 Pod。

改走 Ingress 后,原图中四个 Pod 的计数更接近:

| Pod 后缀 | SLB 请求数 | Ingress 请求数 |
|---|---|---|
| fvn2m | 571 | 472 |
| c4vfg | 1656 | 498 |
| r8g2k | 15 | 486 |
| xbxft | 15 | 478 |
| 合计 | 2257 | 1934 |
这两张图支持的结论是:在作者所展示的场景中,Ingress 入口后的请求分布更均匀。两组总请求数分别为 2257 和 1934,不能将它们当成相同样本下的吞吐或延迟基准,也不能据此推出对所有 gRPC 工作负载都有性能提升。
作者还报告新增或删除 Pod 时过渡较平滑,并把 SLB 侧偶发不可用归因于端点更新或与 ipvs 不同步。这是作者当时的观察和解释;原文没有提供足以独立验证所有故障原因的追踪数据。未独立复现实验,不能扩展为普遍零中断承诺。
案例留下的可复用经验
当少量长连接导致后端忙闲不均时,先判断负载均衡发生在哪一层、按什么对象选择后端,再考虑客户端策略、请求级代理或其他架构。引入代理会增加计算资源、证书和配置维护成本,也需要对高负载进行调优;云 SLB 的实现、资源消耗和计费模式不能用原文中“只是一套规则,不占资源”的简化说法概括。
这个案例展示了连接粒度与 RPC 粒度的差异,以及用每个 Pod 的请求计数检验分布的方法。用于今天的系统时,应先选择仍受维护、符合当前平台支持范围的入口实现,再验证 TLS、超时、长流、扩缩容与失败恢复行为。
正文与原始配图归秦付灵/七猫技术团队。本文为中文核查整理版本;编辑补充、删去无效注解和 TLS 凭证修正均已说明。完整源文中的 Helm 支线没有被宣称为可运行 Chart。代码仅做静态审查;未发现硬编码秘密不代表没有其他漏洞。
原文界面与拓扑补图
下图均属于2022年原案例;界面、标识和实现细节应按其历史条件理解。








原始 Go SDK 片段及上下文
以下保留原 SDK 片段,含原有默认明文凭证路径以及未展示定义的变量,不能作为独立可运行程序。连接案例中的443端口时,应使用前文的TLS凭证修订并验证服务端;不要因该历史片段的安全开关未开启而直接采用明文。缓冲、消息大小、拦截器与keepalive需要完整程序和服务端策略配合。
var target = "rec-test1.demo.com:443"
var credential = insecure.NewCredentials()
if o.enabledSecurity {
credential = credentials.NewTLS(nil)
}
dialOptions := []grpc.DialOption{
grpc.WithTransportCredentials(credential),
grpc.WithWriteBufferSize(writeBufSize),
grpc.WithReadBufferSize(readBufSize),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(maxRecvBytes),
grpc.MaxCallSendMsgSize(maxSendBytes),
),
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
grpc.WithChainUnaryInterceptor(chain...),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,
Timeout: time.Second,
PermitWithoutStream: true,
}),
}
conn, err := grpc.DialContext(ctx, target, dialOptions...)












暂无评论内容