Ingress NGINX Controller 金丝雀发布(Canary)完整指南:基于注解的灰度路由实现与实战部署
2026/9/14 1:36:39 网站建设 项目流程

Ingress NGINX Controller 金丝雀发布(Canary)完整指南:基于注解的灰度路由实现与实战部署

【免费下载链接】ingress-nginxIngress NGINX Controller for Kubernetes项目地址: https://gitcode.com/GitHub_Trending/in/ingress-nginx

导读

本文围绕 Ingress NGINX Controller 的 Canary(金丝雀)路由能力,讲解如何通过 Ingress 注解将一部分请求按权重、请求头或 Cookie 规则引流到新版本服务,实现生产环境下的灰度发布。读完本文,你将掌握从主/金丝雀 Deployment 与 Ingress 搭建、权重与条件式分流配置,到请求验证与源码级分流原理的完整实战方案。

什么是 Canary 路由

在生产环境中发布新版本时,直接全量切换往往伴随着不可控风险。金丝雀发布(Canary Release)的核心思路是:让一小部分真实流量先访问新版本,在确认新版本稳定后再逐步扩大流量比例,最终完成全量切换。

Ingress NGINX Controller 通过在Ingress 资源上设置特定注解来支持金丝雀路由,无需改动 Deployment 或 Service 本身,只需为同一个 Host 额外创建一个标记为 canary 的 Ingress,控制器便会将主 Ingress 与新版本 Ingress 合并,并按规则对流量进行分流。官方示例位于仓库 docs/examples/canary/README.md,本文即以此示例为主线展开。

从源码实现看,金丝雀的核心机制是"备选后端(Alternative Backends)合并":控制器在处理 Ingress 时,会将标记为 canary 的 Ingress 先搁置,再通过mergeAlternativeBackends将金丝雀后端合并进主后端(见 internal/ingress/controller/controller.go#L917-L927),最终由 NGINX 依据流量整形策略(TrafficShapingPolicy)决定请求去向。

第一步:创建主版本 Deployment 与 Service

先创建主版本应用及其 Service,它是正式流量的默认目的地。示例中使用官方测试镜像registry.k8s.io/ingress-nginx/e2e-test-echo:v1.2.9,该镜像会回显请求来源 Pod 的主机名,便于后续验证流量分流效果:

echo " --- # Deployment apiVersion: apps/v1 kind: Deployment metadata: name: production labels: app: production spec: replicas: 1 selector: matchLabels: app: production template: metadata: labels: app: production spec: containers: - name: production image: registry.k8s.io/ingress-nginx/e2e-test-echo:v1.2.9@sha256:9920d084b452b38ee663005a455aa7ed12c15afa512741ea9596e206a189bdf0 ports: - containerPort: 80 env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP --- # Service apiVersion: v1 kind: Service metadata: name: production labels: app: production spec: ports: - port: 80 targetPort: 80 protocol: TCP name: http selector: app: production " | kubectl apply -f -

注意 Service 通过selector: app: production关联主版本 Pod,端口 80 对容器端口 80。

第二步:创建金丝雀版本 Deployment 与 Service

金丝雀版本(新版本)的 Deployment 与 Service 结构与主版本完全一致,仅资源名称与标签不同(本例使用canary)。它可以承载新版本代码,承接被分流过来的部分请求:

echo " --- # Deployment apiVersion: apps/v1 kind: Deployment metadata: name: canary labels: app: canary spec: replicas: 1 selector: matchLabels: app: canary template: metadata: labels: app: canary spec: containers: - name: canary image: registry.k8s.io/ingress-nginx/e2e-test-echo:v1.2.9@sha256:9920d084b452b38ee663005a455aa7ed12c15afa512741ea9596e206a189bdf0 ports: - containerPort: 80 env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP --- # Service apiVersion: v1 kind: Service metadata: name: canary labels: app: canary spec: ports: - port: 80 targetPort: 80 protocol: TCP name: http selector: app: canary " | kubectl apply -f -

第三步:创建指向主版本的 Ingress

接下来通过 Ingress 暴露主版本应用。这一步创建的 Ingress不携带任何 canary 相关注解,它是金丝雀规则合并的目标(主后端):

echo " --- # Ingress apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: production annotations: spec: ingressClassName: nginx rules: - host: echo.prod.mydomain.com http: paths: - pathType: Prefix path: / backend: service: name: production port: number: 80 " | kubectl apply -f -

这里通过ingressClassName: nginx关联 Ingress NGINX Controller,Host 为echo.prod.mydomain.com,路径前缀/全部路由到production服务。

第四步:创建指向金丝雀版本的 Ingress

这是整个金丝雀发布的核心配置。为同一 Host 再创建一个携带 canary 注解的 Ingress,控制器会将其作为主 Ingress 的"替代后端"合并。创建时务必注意以下几点:

  • Host 必须与主 Ingress 完全一致(都是echo.prod.mydomain.com),否则无法匹配合并;
  • nginx.ingress.kubernetes.io/canary: "true"注解是必需的,它将该 Ingress 标记为金丝雀。若缺失此注解,两个同 Host 的 Ingress 会发生冲突(Ingress 冲突检测会将其视为规则重叠);
  • nginx.ingress.kubernetes.io/canary-weight: "50"控制分流权重,此处表示请求有 50% 的概率命中金丝雀版本,其余 50% 仍流向主版本。
echo " --- # Ingress apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: canary annotations: nginx.ingress.kubernetes.io/canary: \"true\" nginx.ingress.kubernetes.io/canary-weight: \"50\" spec: ingressClassName: nginx rules: - host: echo.prod.mydomain.com http: paths: - pathType: Prefix path: / backend: service: name: canary port: number: 80 " | kubectl apply -f -

注解校验:未启用却配置权重会怎样

从源码 internal/ingress/annotations/canary/main.go 可以看到,canary 注解的解析逻辑非常明确:

  • canary注解缺失或非法时,Enabled默认置为false
  • canary-weight缺失或非法时默认0canary-weight-total默认100
  • canary未启用(Enabled=false),但同时配置了canary-weight > 0canary-by-headercanary-by-header-valuecanary-by-header-patterncanary-by-cookie中的任意一项,解析会返回NewInvalidAnnotationConfiguration错误("configured but not enabled")。

单元测试 internal/ingress/annotations/canary/main_test.go 中的TestAnnotations用例逐一验证了这些行为,例如{"canary disabled and weight", false, 20, "", "", true}即断言"未启用 canary 却配置权重"时解析必须报错。因此,先设置canary: "true",再配置分流规则是正确姿势。

第五步:验证分流效果

应用以上资源后,使用curl --resolve将域名解析到 Ingress Controller 的 IP(将INGRESS_CONTROLLER_IP替换为你的控制器实际地址)发起连续请求:

for i in $(seq 1 10); do curl -s --resolve echo.prod.mydomain.com:80:$INGRESS_CONTROLLER_IP echo.prod.mydomain.com | grep "Hostname"; done

由于e2e-test-echo镜像会回显处理请求的 Pod 主机名,你会看到类似下面的输出,说明主版本与金丝雀版本各承接了约一半请求:

Hostname: production-5c5f65d859-phqzc Hostname: canary-6697778457-zkfjf Hostname: canary-6697778457-zkfjf Hostname: production-5c5f65d859-phqzc Hostname: canary-6697778457-zkfjf Hostname: production-5c5f65d859-phqzc Hostname: production-5c5f65d859-phqzc Hostname: production-5c5f65d859-phqzc Hostname: canary-6697778457-zkfjf Hostname: production-5c5f65d859-phqzc

10 次请求中canary出现 4 次、production出现 6 次,接近 50/50 的权重比例,符合canary-weight: "50"的预期(权重分流是随机的,短样本存在波动属正常现象)。验证通过后,即可逐步调高权重完成灰度放量,或移除金丝雀 Ingress 完成回滚。

金丝雀注解全解析

除了示例中用到的canarycanary-weight,控制器还支持基于请求头、Cookie 的条件式分流。所有 canary 注解的官方说明见 docs/user-guide/nginx-configuration/annotations.md#canary,注解常量定义于 internal/ingress/annotations/canary/main.go#L28-L36。

注解取值作用说明
nginx.ingress.kubernetes.io/canary"true"/"false"将该 Ingress 标记为金丝雀规则,是其他 canary 注解生效的前提
nginx.ingress.kubernetes.io/canary-by-header字符串指定用于触发分流到金丝雀的请求头名称。请求头值为always时强制路由到金丝雀;为never时绝不路由到金丝雀;其他值则忽略该头,按优先级继续与其余规则比较
nginx.ingress.kubernetes.io/canary-by-header-value字符串canary-by-header配合使用,自定义命中金丝雀的请求头值(替代硬编码的always)。未定义canary-by-header时不生效
nginx.ingress.kubernetes.io/canary-by-header-patternPCRE 正则canary-by-header-value行为类似,但使用正则匹配。设置了canary-by-header-value时本注解被忽略;正则处理出错时视为不匹配
nginx.ingress.kubernetes.io/canary-by-cookie字符串指定用于触发分流到金丝雀的 Cookie 名称。Cookie 值为always时路由到金丝雀,never时绝不路由,其他值忽略并按优先级继续比较
nginx.ingress.kubernetes.io/canary-weight整数(0 ~ weight-total)随机请求被路由到金丝雀服务的百分比权重。0表示无请求进入金丝雀;等于weight-total表示全部请求进入金丝雀
nginx.ingress.kubernetes.io/canary-weight-total整数流量总权重,默认100。例如设置canary-weight: 10canary-weight-total: 1000时,金丝雀承接 1% 流量

优先级与匹配顺序

金丝雀规则按如下优先级依次评估,先命中的规则生效

canary-by-header -> canary-by-cookie -> canary-weight

即:先检查请求头规则(含 header-value / header-pattern),再检查 Cookie 规则,最后才回退到权重随机分流。这一设计允许你做"指定测试用户(通过 Header/Cookie)优先走新版本,其余流量按权重灰度"的精细化发布。

与 session affinity 的协同

当金丝雀 Ingress 同时启用了会话粘性(session affinity)时,注解nginx.ingress.kubernetes.io/affinity-canary-behavior决定其行为:

  • sticky(默认):被分流到金丝雀的用户后续请求继续命中金丝雀,保证会话一致性;
  • legacy:恢复历史行为,忽略会话粘性对金丝雀的影响。

从源码 internal/ingress/controller/controller.go#L1597-L1599 可以看到,在合并备选后端时,若CanaryBehavior不是legacy,主后端的SessionAffinity配置会被深拷贝(DeepCopyInto)到金丝雀后端上,从而让两种实现路径行为一致。

其他注解的继承规则

当 Ingress 被标记为 canary 后,其上的其他非 canary 注解会被忽略(自动继承主 Ingress 的对应配置),仅有以下例外:

  • nginx.ingress.kubernetes.io/load-balance
  • nginx.ingress.kubernetes.io/upstream-hash-by
  • 与 session affinity 相关的注解

这意味着金丝雀 Ingress 只需关心"如何分流",而重写规则、超时、限流等行为均与主 Ingress 保持一致,避免配置漂移。

源码视角:金丝雀流量整形与后端合并原理

TrafficShapingPolicy:流量的抽象层

控制器解析完 canary 注解后,会将结果封装为ingress.TrafficShapingPolicy(见 internal/ingress/controller/controller.go#L1911-L1921),包含权重、总权重、请求头、请求头值、请求头正则与 Cookie 六个字段,随后挂载到对应 upstream 上:

// newTrafficShapingPolicy creates new ingress.TrafficShapingPolicy instance using canary configuration func newTrafficShapingPolicy(cfg *canary.Config) ingress.TrafficShapingPolicy { return ingress.TrafficShapingPolicy{ Weight: cfg.Weight, WeightTotal: cfg.WeightTotal, Header: cfg.Header, HeaderValue: cfg.HeaderValue, HeaderPattern: cfg.HeaderPattern, Cookie: cfg.Cookie, } }

该策略最终在 internal/ingress/controller/nginx.go#L970 被写入 NGINX 配置模板(TrafficShapingPolicy: backend.TrafficShapingPolicy),由 NGINX 在请求处理时依据此策略完成实际分流。可以推断,Weight/WeightTotal驱动随机按比例分流,而Header/Cookie字段驱动条件匹配分流,与注解文档的描述一一对应。

后端合并的三个前置条件

金丝雀后端不会盲目合并进主后端,canMergeBackend(internal/ingress/controller/controller.go#L1578-L1580)要求同时满足:

  1. 备选后端存在,且主后端与备选后端名称不同(防止金丝雀合并进自身);
  2. 主后端不是默认 upstream(defUpstreamName);
  3. 主后端必须存在对应的 server(NoServer为假)。

合并时,备选后端名称会被追加到主后端的AlternativeBackends列表;若目标已存在则跳过(幂等)。此外,只有存在至少一个非 canary Ingress 时才执行合并(nonCanaryIngressExists,见 controller.go#L1570-L1572),这保证了"金丝雀不会落单"。

冲突检测与已知限制

控制器在路径冲突检测时同样感知 canary 状态(controller.go#L1850-L1860):只有两个 Ingress标记为 canary 时才会判定为重叠冲突,而 canary Ingress 与主 Ingress 的同 Host 同路径则被允许共存——这正是金丝雀机制能够成立的前提。

同时需要了解官方文档明确指出的已知限制:

每条 Ingress 规则(每个 Host + path 组合)最多只能应用一个 canary Ingress

如果需要同时灰度多个版本,应当串行调整权重而非叠加多个金丝雀 Ingress。

实战建议

  • 从低权重起步:建议先设置canary-weight: "5"(配合canary-weight-total: "100"),观察日志、指标与错误率稳定后再逐步上调;
  • 善用请求头灰度canary-by-header适合内部测试——QA 团队携带特定请求头(如canary: always)即可稳定命中新版本,不受权重随机性影响;
  • 验证会话一致性:若应用依赖会话,注意affinity-canary-behavior默认sticky会保持金丝雀用户会话连续,升级前需评估两种行为对业务的影响;
  • 回滚即删除:移除金丝雀 Ingress(kubectl delete ingress canary)即可瞬间恢复 100% 主版本流量,无需重建资源。

延伸阅读

  • 金丝雀注解完整文档:docs/user-guide/nginx-configuration/annotations.md(另见注解速查表 annotations.md#L43-L49)
  • 注解解析器实现:internal/ingress/annotations/canary/main.go 与单元测试 internal/ingress/annotations/canary/main_test.go
  • 后端合并与流量整形:internal/ingress/controller/controller.go(合并逻辑见 L917-L927、L1569-L1619、L1911-L1921)
  • 会话亲和示例:docs/examples/affinity/cookie/README.md

【免费下载链接】ingress-nginxIngress NGINX Controller for Kubernetes项目地址: https://gitcode.com/GitHub_Trending/in/ingress-nginx

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询