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缺失或非法时默认0,canary-weight-total默认100;- 若
canary未启用(Enabled=false),但同时配置了canary-weight > 0、canary-by-header、canary-by-header-value、canary-by-header-pattern或canary-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-phqzc10 次请求中canary出现 4 次、production出现 6 次,接近 50/50 的权重比例,符合canary-weight: "50"的预期(权重分流是随机的,短样本存在波动属正常现象)。验证通过后,即可逐步调高权重完成灰度放量,或移除金丝雀 Ingress 完成回滚。
金丝雀注解全解析
除了示例中用到的canary与canary-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-pattern | PCRE 正则 | 与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: 10且canary-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-balancenginx.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)要求同时满足:
- 备选后端存在,且主后端与备选后端名称不同(防止金丝雀合并进自身);
- 主后端不是默认 upstream(
defUpstreamName); - 主后端必须存在对应的 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),仅供参考