“金丝雀发布”在Kubernetes圈子里的热度一直很高但很多人对它其实是一半懂一半不懂。看到Deployment分两批滚动就觉得是金丝雀看到Ingress配个weight也认为是金丝雀真到线上出问题才发现思路和工具根本是两回事。这篇文章我把自己实践Kubernetes金丝雀发布的完整过程整理出来从原生方案一步步讲到Argo Rollouts这种专门工具把原理、配置、参数计算和踩坑记录都放在一起适合正在做服务发布、看了不少文档但动手时依然心里没底的同学参考。1. 金丝雀发布到底是什么先看懂思路再动手1.1 从一个事故说起我先讲一个真实场景这比背概念直观得多。某公司的核心服务集群深夜发版开发小哥把新版本直接推到生产环境用了最常见的滚动发布策略。结果新版本有一个只在特定输入参数下触发的内存泄漏问题第一批Pod替换后刚开始看起来一切正常等流量慢慢上来内存涨得飞快。等监控告警响起来已经有30%的用户受到影响整个回滚过程又牵涉到旧镜像重新拉取、Pod重新调度前后折腾了近四十分钟。如果当时用了金丝雀发布情况会完全不同。新版本只接收比如5%到10%的流量哪怕真的出问题影响面也只是这一小撮流量。发现异常数据后直接把新版本缩到0旧版本全程稳定在线整个恢复过程用不了几分钟。这其实就是金丝雀发布的核心逻辑用一小部分真实流量验证新版本的稳定性验证通过后再逐步扩大范围直到全部切换。“金丝雀”这个名字来源于煤矿作业——矿工带金丝雀下井鸟对瓦斯敏感鸟出问题就提醒人赶紧撤离。放到服务发布里新版本就是那只金丝雀先用它试毒别拿所有用户当试验品。1.2 金丝雀发布和蓝绿、滚动、A/B测试的界线很多同学搞混这几个概念我拿表格把它们的差异说明白发布方式核心策略优点缺点适用场景滚动发布新老Pod逐批替换资源占用小操作简单无法精细化控制流量出了问题影响面逐渐扩大常规无状态服务蓝绿发布新旧两个环境整体切换回滚极快影响面可控需要双倍资源切换瞬间有连接断开风险对稳定性要求高、愿意多花资源的场景金丝雀发布新旧并存按比例切流量影响范围可控能验证真实流量需要观测和流量控制能力配置较复杂核心业务、高风险版本A/B测试新旧版本面向不同用户能验证业务效果不只是稳定性偏业务层面技术门槛同样不低推荐算法、文案样式等功能验证一个容易混淆的点是金丝雀发布和蓝绿发布。蓝绿是“两套环境整体切换”切过去之后旧环境处于待机状态随时可以整体拉回来金丝雀则是新旧版本长期共存流量按权重一点点转移更像是“局部试水”。还有一个常见的误用滚动发布里把Deployment的maxSurge和maxUnavailable调一调分批替换Pod很多团队管这叫“分批发布”然后对外也叫“金丝雀”。严格意义上这只能算“分批滚动”因为流量比例没有被精确控制新版本承接多少流量取决于调度瞬间Pod的分布在副本数较少的情况下第一批甚至可能承担50%以上的流量。真正的金丝雀发布核心指标是流量权重必须能明确回答“当前新版本接了多少比例的请求”这个问题。2. 原生Kubernetes里怎么做金丝雀两条路各有取舍2.1 分阶段扩缩容最朴素的“伪金丝雀”在不装任何外部组件的前提下最常见的做法是修改Deployment的管理方式走“先扩新、再缩旧”的流程。假设旧版本服务叫myapp当前副本数是10新版本用同一个Deployment滚动发布。常规做法是直接更新Deployment的镜像系统按策略逐步替换。分阶段扩缩容的做法不同我们分两步走把新版本的副本数从0扩到2比如通过把image切到新版本但replicas先设成2此时旧的10个Pod和新的2个Pod并存新版本大约承接1/6的流量观察一段时间后把新版本副本数扩到5旧版本缩到7让新版本承接约40%流量再观察直到新版本全部接管旧版本缩到0。实际操作上可以通过临时创建一个新版本Deployment来实现也可以在一个Deployment里通过修改replicas配合image字段调整。示例命令大致是这样# 先把新版本作为独立Deployment拉起来副本数为2 kubectl create deployment myapp-canary --imagemyapp:v2 --replicas2 # 观察没问题后扩容新版本缩容旧版本 kubectl scale deployment myapp-canary --replicas5 kubectl scale deployment myapp-stable --replicas7 # 最终全部切换 kubectl scale deployment myapp-canary --replicas10 kubectl scale deployment myapp-stable --replicas0前提是新旧两个Deployment都挂到同一个Service下且它们的Pod标签能被Service的selector同时选中。比如Service的selector是appmyapp新旧Deployment的Pod模板里都带这个标签就行。虽然操作简单但这条路的局限性非常明显流量比例和实例数比例近似相等没法精确做到“5%”实例数太少时完全控制不了必须手动执行多次命令靠人盯着监控判断是否继续如果服务有HPA水平自动伸缩参与管理HPA会根据负载自动调整副本数你手工缩旧版本的行为可能被HPA覆盖导致实际比例完全失控没有自动回滚机制发现异常要手动缩容人多手杂时很容易出现操作事故。我的判断是副本数少于5的小服务、或者内部工具类服务用这个办法凑合可以。核心生产服务千万别这么干。它最大的价值不是真的做金丝雀而是让新手通过这个过程理解“新旧并存”和“比例切换”的基本逻辑。2.2 流量权重切分Ingress层面的真金丝雀要想真正控制流量比例就要在流量入口做文章。Kubernetes集群里外部请求一般通过Ingress进入而Ingress Controller比如Ingress-Nginx有能力对同一个Host下的请求做权重分配。Ingress-Nginx提供了一个叫Canary的注解机制思路是这样先有一条常规的Ingress规则把域名指向稳定版本Service再创建一条标记为canary的Ingress规则指向金丝雀版本Service并通过权重注解告诉Nginx“把百分之多少的流量转发到金丝雀后端”。这是一个最简化的配置示例后面第三章会给出完整版本。这里先理解原理apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: myapp-canary annotations: nginx.ingress.kubernetes.io/canary: true nginx.ingress.kubernetes.io/canary-weight: 20 spec: ingressClassName: nginx rules: - host: myapp.example.com http: paths: - path: / pathType: Prefix backend: service: name: myapp-canary-svc port: number: 80这里最关键的是两条注解nginx.ingress.kubernetes.io/canary: true告诉Ingress Nginx这是一条金丝雀规则nginx.ingress.kubernetes.io/canary-weight: 20把20%的流量转发到金丝雀后端。Ingress Nginx对这类注解有特殊处理。它会在Nginx配置生成阶段把canary规则转成一段独立的location配置并通过内部变量控制转发比例。因此和常规Ingress不同canary规则的path必须和主Ingress的path保持一致否则不会被匹配。除了按权重Ingress Nginx还支持按Header、按Cookie、按IP来源来分流。比如想只让部分内部测试人员访问新版本可以加上下面的注解nginx.ingress.kubernetes.io/canary-by-header: canary nginx.ingress.kubernetes.io/canary-by-header-value: true这样一来只有请求头里带canary: true的请求才会被转发到金丝雀版本其他用户完全不受影响。按Header分流特别适合做内部验收不影响线上真实用户。这种方式的优点是比例精细到1%而且只要Ingress Controller不挂流量切换就是精准的。缺点是每次调整都要改注解并重新apply还是一个手动的过程。它比纯扩缩容好得多但依然不是终点。3. 实战用Ingress 多Service做一次完整的金丝雀发布3.1 准备环境和基础服务我建议你本地准备一套可重复实验的环境最简单的是用Kind或者Minikube启动一个单节点集群至少2核4G。公司环境里如果有现成测试集群也可以但要确认Ingress Controller用的是Ingress-Nginx。其他Ingress Controller比如某些云厂商自带的注解规则可能不一致后面操作会踩坑。环境就绪后先把Ingress-Nginx装好一般用Helm或官方yaml即可不展开细说。然后我们需要构建两个Deployment。为了便于观察流量走向应用本身最好在响应里带上版本信息最简单的办法是让应用读环境变量APP_VERSION在HTTP响应体或响应头里打印出来。这里用最简单的nginx镜像做演示通过环境变量注入一个小脚本让返回内容区分版本# 稳定版应用配置 kubectl create deployment myapp-stable --imagenginx:1.24 --replicas3 kubectl set env deployment/myapp-stable APP_VERSIONv1 POD_NAME$(hostname)严格来说真实应用不会用nginx做业务逻辑。我这里只是用nginx作为演示载体方便你看到流量去向。关键是理解整个流程而不是纠结容器里跑什么。两个Service的创建方式类似。稳定版Service定义如下apiVersion: v1 kind: Service metadata: name: myapp-stable-svc spec: selector: app: myapp-stable ports: - port: 80 targetPort: 80金丝雀版本Service用同样的方式创建selector换成app: myapp-canary名称用myapp-canary-svc。我的习惯是给每个Deployment配独立Service而不是共用同一个Service。原因后面讲。3.2 部署旧版本与新版本先部署稳定版本确认一切正常。三个Pod标签appmyapp-stable暴露80端口。然后创建金丝雀版本的Deployment。初期副本数不要太大我一般从1个Pod起步这样在10个稳定Pod的基础上新版本权重约为10%左右符合常规金丝雀先小范围验证的要求kubectl create deployment myapp-canary --imagenginx:1.25 --replicas1 kubectl set env deployment/myapp-canary APP_VERSIONv2 POD_NAME$(hostname) kubectl label deployment/myapp-canary appmyapp-canary --overwrite注意一个细节创建Deployment时指定--replicas1是为了让金丝雀Pod数量可控。很多人在这一步直接把副本数设成和稳定版一样多然后通过Ingress weight控制比例这也没问题但成本高资源紧张时不划算。我更倾向用“少量Pod 较低权重”的组合目的是让金丝雀版本在最开始只承担极小部分流量同时保留一定的计算裕度让新版本能跑起来。这里也有一个很容易犯的错金丝雀版本的Pod标签和Service selector必须严格一致。如果kubectl create deployment默认生成的标签是appmyapp-canary一般没问题但如果你之前手动指定过Deployment的--labels一定要确认Service selector匹配否则流量根本打不到金丝雀Pod上你会看到Ingress权重配了20%但后端一直504。3.3 配置Canary Ingress逐步切流量这一步是核心。首先需要有一条主Ingress指向稳定版本ServiceapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: myapp-main spec: ingressClassName: nginx rules: - host: myapp.example.com http: paths: - path: / pathType: Prefix backend: service: name: myapp-stable-svc port: number: 80再创建金丝雀Ingress。注意不要单独指定host要和主Ingress完全一致apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: myapp-canary annotations: nginx.ingress.kubernetes.io/canary: true nginx.ingress.kubernetes.io/canary-weight: 10 spec: ingressClassName: nginx rules: - host: myapp.example.com http: paths: - path: / pathType: Prefix backend: service: name: myapp-canary-svc port: number: 80应用它kubectl apply -f myapp-main.yaml kubectl apply -f myapp-canary.yaml验证配置是否生效有一个很实用的技巧进入Ingress Nginx的Pod直接查看生成的Nginx配置确认canary配置已经被加载。比如kubectl exec -it nginx-ingress-controller-pod -- grep -A 20 canary /etc/nginx/nginx.conf正常会看到类似set $canary 0;以及一张按权重分配的map配置比如set $canary_weight 10;。看不到说明注解没生效大概率是ingressClassName或注解拼写有问题。调整权重非常简单直接修改金丝雀Ingress的注解kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight30 --overwrite kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight50 --overwrite kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight100 --overwrite每一次修改后建议至少观察一个完整的统计周期确认错误率、延迟、CPU、内存等指标正常再继续放大比例。3.4 验证、观测与回滚验证流量比例最简单的方法是用压测工具发请求然后统计响应里带版本标识的数量比例for i in $(seq 1 100); do curl -s http://myapp.example.com/ | grep -o v[0-9] done | sort | uniq -c假设新版本权重配了10%压测100次请求统计结果大约会是v1出现90次左右v2出现10次左右。次数越多越接近设定权重。注意不要用浏览器直接反复刷新来做这个统计浏览器和服务器之间会复用长连接导致统计结果严重偏斜这个细节后面还会具体讲。全量切换时有两种做法取决于你的发布策略直接把金丝雀Ingress的weight改成100稳定版流量全部切到新版本再观察一段时间或者不做流量切分而是把稳定版Deployment的镜像直接更新为新版本等滚动发布完成后删除金丝雀Ingress和canary Service。我个人倾向第二种。金丝雀阶段本质上是一个“验证期”验证通过的最终结果应该是让稳定版Deployment进入新版本状态而不是长期维持两套Ingress并存。长期维护两条Ingress的额外成本不低而且容易清理不干净留下流量总是往旧名字上打的问题。回滚操作分两步。第一步把金丝雀权重改成0让新流量不再进入金丝雀版本kubectl annotate ingress myapp-canary nginx.ingress.kubernetes.io/canary-weight0 --overwrite第二步如果金丝雀Pod已经接入业务且发现有问题要立刻缩容金丝雀Deployment而不是只把权重改成0。因为已建立的TCP长连接和正在进行中的请求不会因为Ingress权重改变而中断它们还会继续打到金丝雀Pod上。你要把金丝雀副本缩到0才能彻底切断这些存量请求kubectl scale deployment myapp-canary --replicas0很多人栽在这上面权重归零后以为万事大吉结果老请求还在跑报错依然不断。4. 进阶Argo Rollouts自动金丝雀与Metrics分析4.1 为什么原生方案不够用需要独立工具用Ingress 多Service的方式做金丝雀已经比滚动发布强很多但它仍然是一个手工活。核心痛点有三个第一没有自动决策能力。是否继续放大流量、是否中止发布完全依赖人盯着监控。凌晨两点发版监控告警触发时你可能正在睡觉等发现异常已经是十分钟之后。第二没有自动回滚能力。即使你发现新版本有问题还要手动改注解、手动缩容。如果是人肉操作大脑不清醒的时候很容易把命令写错。第三权重切换和发布流程是割裂的。Ingress只负责转发流量Deployment只负责管理Pod两套系统互不相通。真正发布时你需要同时操作两个对象还要人工判断“什么时候把金丝雀流量放大”这个判断本身就容易出错。这就是Argo Rollouts这类渐进式交付工具存在的意义。它引入了一个叫Rollout的CRD把“部署新版本、调整流量权重、查看指标、决定是否继续”整个流程自动化。它能在发布过程中持续拉取监控系统的指标Prometheus、Datadog等一旦指标异常自动回滚到旧版本不需要人工干预。4.2 安装与核心配置解读Argo Rollouts的安装很简单一条命令具体命令可查阅官方文档最新版本kubectl create namespace argo-rollouts kubectl apply -n argo-rollouts -f 官方安装yaml再安装一个命令行插件kubectl argo rollouts方便观察发布状态。安装完成后它不会影响已有Deployment只有创建Rollout对象才会接管发布逻辑。下面是一个典型的Rollout配置我用注释解释核心字段apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: myapp-rollout spec: replicas: 10 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: myapp:v1 ports: - containerPort: 8080 strategy: canary: steps: - setWeight: 10 - pause: {duration: 30m} - setWeight: 50 - pause: {duration: 30m} - setWeight: 100 - pause: {} analysis: templates: - templateName: myapp-error-rate-check antiAffinity: requiredDuringSchedulingIgnoredDuringExecution: podAntiAffinity: requiredDuringScheduling: labelSelector: matchLabels: app: myapp topologyKey: kubernetes.io/hostname策略中的steps就是发布节奏setWeight: 10先把新版本流量调到10%pause: {duration: 30m}保持30分钟等待人工确认或等待指标分析完成setWeight: 50继续调到50%再等30分钟setWeight: 100全量切换最后一个pause: {}表示暂停等待人工最终确认没有duration字段意味着无限期暂停等操作者执行promote才放行。analysis字段是关键它指定了一个AnalysisTemplate用于定义“什么条件下允许继续发布”。下面的模板示例是监控错误率的常见写法apiVersion: argoproj.io/v1alpha1 kind: AnalysisTemplate metadata: name: myapp-error-rate-check spec: metrics: - name: error-rate interval: 30s count: 5 failureLimit: 3 provider: prometheus: query: | sum(rate(http_requests_total{code~5.., appmyapp}[1m])) / sum(rate(http_requests_total{appmyapp}[1m])) - name: latency-p99 interval: 30s count: 5 failureLimit: 3 provider: prometheus: query: | histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{appmyapp}[5m])) by (le))各参数含义interval: 30s每30秒查询一次指标count: 5最多查询5次每次结果均参与判定failureLimit: 3容忍3次失败超过即判定发布失败并回滚Prometheus query返回的结果如果超过预设阈值或者查询失败超过次数都会触发回滚。这里最重要的设计思路是把发布决策交给指标而不是交给人的感觉。人的感觉让你“再观察观察”但指标告诉你新版本P99延迟已经从500ms涨到3s这时候就该回滚。4.3 自动分析与回滚实战完整流程演示一下。先准备一套能产生指标的服务。可以用上一个示例里的myapp并在Prometheus里配置好采集。然后把上面的Rollout和AnalysisTemplate应用到集群kubectl apply -f analysis-template.yaml kubectl apply -f rollout.yaml查看发布进度kubectl argo rollouts get rollout myapp-rollout会看到类似这样的输出新版本ReplicaSet正在创建当前weight是10%等待pause时间完成Analysis状态是Running。手动推进发布执行以下命令也可以在pause时间结束后自动推进取决于你的配置kubectl argo rollouts promote myapp-rollout现在模拟一次故障。把Rollout里的镜像改成myapp:buggy-version这个版本故意对/health接口返回500。然后apply观察发布过程。在kubectl argo rollouts get rollout myapp-rollout里会看到Analysis的状态先是Running然后变成Failed紧接着Rollout自动把新版本缩到0旧版本保持完整可用整个过程无人干预。这个自动回滚动作的关键在于Argo Rollouts把Pod副本管理和Ingress权重管理统一在一个控制器里。它不只是像Deployment那样单纯地替换Pod而是会动态调整新旧ReplicaSet的副本数同时通过Service和Ingress的权重设置确保新版本只承担预设比例的流量。Ingress Nginx的配置也可以注入到Rollout中在Rollout的strategy.canary里指定trafficRouting字段spec: strategy: canary: trafficRouting: managedRoutes: - name: myapp-canary kind: Ingress这样Argo Rollouts会直接控制Ingress的canary注解不需要每步手工调整weight。实际项目中我通常还会配合以下实践在AnalysisTemplate里同时监控错误率、P99延迟、CPU使用率等多个指标而不是只看一个对核心接口增加SLO阈值错误率超过1%就直接回滚给关键发布配置一个松弛的failureLimit防止指标抖动导致误回滚比如错误率阈值设到2%failureLimit设到5次发布窗口尽量避开流量高峰即使金丝雀出问题影响面也更小。5. 常见问题与避坑实录5.1 高频问题速查问题现象可能原因解决办法金丝雀流量完全没有按权重走全打到了新版本canary Ingress的path与主Ingress不一致ingressClassName配置不对确认两条Ingress的host、path完全一致检查注解拼写查看Nginx配置里的canary变量权重配了10%但压测统计v2远高于10%客户端或压测工具复用了长连接Nginx对keep-alive连接中的请求按原连接分发压测时禁用连接复用如curl加-H Connection: close或用ab -c 1 -n 100逐个短连接金丝雀Pod起来了但Ingress转发报504Service的selector和Pod标签不匹配Pod的readinessProbe失败检查Service的Endpoint是否正常查看Pod状态和Ingress后端配置修改canary-weight后不生效注解值被覆盖或没使用--overwrite注解名拼写错误用kubectl annotate --overwrite重新设置确认注解名与Ingress-Nginx文档一致金丝雀发布完成后线上流量依然打到旧Service清理不彻底canary Ingress或canary Service未删除DNS或客户端缓存发布完成后统一清理canary相关的Ingress、Service、DeploymentRollout分析一直处于Running状态不继续也不回滚AnalysisTemplate无法连通Prometheusquery语法错误在Prometheus页面手动执行query验证检查分析模板的provider配置新旧Deployment共用Service时流量比例不受控每个Pod被Service随机分发实例数比例不等于流量比例尽量独立ServiceIngress方式控制权重别把希望寄托在Pod数量上5.2 我自己踩过的几个坑第一个坑是金丝雀Ingress没清理干净。有次我做完发布把canary的weight改成了0确认稳定版已经变更为新版本后就认为完事了。结果过了两周新的一次发布又把canary Ingress翻出来用里面的注解还是旧的weight值导致流量在新旧版本里乱串。从那以后我给自己定了条铁律金丝雀发布结束后canary Ingress和canary Service必须删掉不能留到下一次复用。每次发布都从干净状态开始。第二个坑是只看错误率没看延迟。有一版金丝雀发布错误率一直低于1%指标很漂亮但实际用户反馈页面变慢了。查了半天才发现新版本引入了串行调用P99延迟从400ms飙到了2s错误率不高但体验明显下降。后来我的AnalyisTemplate里强制同时包含错误率和P99延迟两个指标缺一不可。第三个坑是新旧版本兼容性没做够。金丝雀发布期间新旧版本必须同时对外提供服务这就要求数据表结构、缓存Key、消息队列的Topic都保持兼容。有次发布新版本改了Redis Key的结构旧版本还在按老Key读数据直接导致缓存击穿数据库被打爆。金丝雀发布虽然控制了流量比例但没控制数据层的兼容风险这一点比流量控制本身更容易被忽略。第四个坑是测试只用了单一请求路径。我用curl反复请求同一个URL验证金丝雀流量比例和功能都正常就放心把权重放大到50%。结果新版本在另一个接口上有参数校验的严重Bug等真实用户走到那个接口才暴露影响面已经扩大了。建议在金丝雀阶段就引入尽可能真实的业务流量或者至少覆盖核心接口和关键链路不要只验证一个健康检查路径。我个人在实际项目里最常用的组合是核心服务用Argo Rollouts Prometheus分析常规服务用Ingress-Nginx的canary注解手动控制临时实验直接用多Deployment分批扩缩。这三条路各有用武之地关键是搞清楚每条路的控制精度和自动化程度再匹配上服务的等级。先把手动流程跑通再逐步自动化这个顺序不会错。
阅读完成 · 觉得有帮助?