通过 Ingress 改善 gRPC 长连接负载分配:七猫的历史实践与使用边界

原文作者:秦付灵,七猫技术团队,发表于 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。

七猫原文 SLB 请求统计:四个 Pod 的请求数为 571、1656、15、15,总计 2257。
原文 SLB 请求统计。图片归秦付灵/七猫技术团队;保留原图,未经独立复测。

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

七猫原文 Ingress 请求统计:四个 Pod 的请求数为 472、478、498、486,总计 1934。
原文 Ingress 请求统计。图片归秦付灵/七猫技术团队,原图未改动。
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年原案例;界面、标识和实现细节应按其历史条件理解。

入口与控制器的整体拓扑
入口与控制器的整体拓扑 原图:秦付灵/七猫技术团队;来源,未改动。
2022年ACK组件管理界面
2022年ACK组件管理界面。截图显示阿里发行标识 v0.44.0.3-8e83e7dc6-aliyun;不能把它直接等同为上游社区版本号。 原图:秦付灵/七猫技术团队;来源,未改动。
2022年ACK路由创建界面
2022年ACK路由创建界面 原图:秦付灵/七猫技术团队;来源,未改动。
公网SLB访问拓扑
公网SLB访问拓扑 原图:秦付灵/七猫技术团队;来源,未改动。
同VPC私网SLB访问拓扑
同VPC私网SLB访问拓扑 原图:秦付灵/七猫技术团队;来源,未改动。
公网与私网双入口拓扑
公网与私网双入口拓扑 原图:秦付灵/七猫技术团队;来源,未改动。
BloomRPC历史TLS界面
BloomRPC历史TLS界面。保留TLS开关示意,截图中的业务标识仅属于历史案例,不能当成测试凭据或当前连接目标。 原图:秦付灵/七猫技术团队;来源,未改动。
原文另一轮多Pod负载观察的监控截图
原文补充的另一轮多 Pod 负载观察。该图显示的 Pod 集合与四 Pod 请求计数表不同,不能合并为同一次约70秒实验或据此计算吞吐。秦付灵/七猫技术团队原图,未改动;来源。

原始 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...)
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容