1. 蓝绿发布到底解决了什么问题搞运维和开发的兄弟应该都有过这种经历:一个版本上线,把全站用户给搭进去了。我印象很深的一次,某电商项目改了订单状态的流转逻辑,测试环境怎么验都没问题,结果一上线,线上订单全部卡在“支付成功待发货”的状态。当时还不是蓝绿发布,用的滚动更新,新版本的Pod一批一批地替换旧Pod,等我们反应过来有问题的时候,新代码已经扩散到一半的节点上了,回滚都要等滚动完了才能继续操作。那半小时里,用户每下一单就失败一单,客服电话直接被打爆。事后复盘,大家聊到一个问题:如果当时有一整套老版本环境可以随时切回去,结果会不会不一样?答案其实是肯定的。这就是蓝绿发布最朴素的出发点——手里留一套能随时顶上来的老环境。蓝绿发布这个词,最早是从什么场景火起来的我不确定,但在Kubernetes这套体系里,它已经成了很多团队上线必配的一套流程。网上讲蓝绿发布的文章不少,但大部分只给你贴两个Deployment的YAML,然后说“改一下Service的selector就切流量了”。听起来确实简单,但真正在生产和预发环境里跑过一轮之后,你会发现里面有大量坑:连接池怎么平滑迁移、旧环境什么时候才能撤、验证流量怎么打、回滚的时候日志会不会乱掉、数据库兼容性怎么处理。这篇文章我不打算给你讲纯理论,我按自己实操过的路径,把蓝绿发布从原理到落地再到避坑,完整拆一遍,内容包括最关键的切换机制、资源编排、应用配置、流量验证和应急回滚。适合谁来读?如果你是刚接触Kubernetes的初级运维,看了能直接对着文中的配置在集群里搭出一套能用的蓝绿发布流程;如果你已经在用滚动更新或者正在被灰度发布的复杂度搞得头疼,这篇文章能帮你理清楚蓝绿这条路线为什么在某些场景下更合适。老实说,蓝绿不是银弹,但它确实是“快速回滚”这个目标下实现成本最低的一种方案。1.1 蓝绿发布的原理:一个入口、两套环境蓝绿发布的核心逻辑用一句话讲就是:同一时间存在两套完全独立的应用环境,分别叫蓝色环境(当前生产版本)和绿色环境(即将上线的新版本),两套环境共享同一个服务入口,通过入口的调度规则把真实流量全部切到蓝色或者绿色。这套逻辑放到Kubernetes里,基础设施层面的表达非常干净——两套Deployment分别跑蓝色版本和绿色版本,它们各自有独立的Pod和Label,外面套一个Service,利用Service的selector字段指向不同标签,就能实现流量在蓝绿之间的切换。很多人第一次接触这个概念的时候会有一个疑惑:为什么不用Ingress去切?Ingress切的是域名路由,可以很精细地做到按路径、按请求头分流,但它更倾向于灰度发布和A/B测试。蓝绿发布要的是“整体切换、一步到位”,切换的粒度应该是服务级而不是路由级。所以在Kubernetes里,最契合蓝绿发布思路的流量控制点就是Service。Service这一层对Ingress来说是不透明的一个抽象,它内部的selector无论指向哪一批Pod,Ingress都不需要做任何变更,域名和路由规则始终保持原来的样子。这种“后端悄悄换、前端毫无感知”的特性,正是蓝绿发布最舒服的地方。再细分一点,流量入口其实分两层。最外层是Ingress,负责域名、TLS证书、路径转发规则,这一层在整个发布期间基本不动;内层是Service,它通过selector选择后端Pod。蓝绿切换的本质动作就是修改Service的selector,让它的流量从指向蓝色Deployment的Label变成指向绿色Deployment的Label。整个过程不涉及Pod重建,连接断开后立即建立新的连接就能访问新版本。还有个很容易被忽略的点:Service只做转发,不做会话保持。如果应用有登录态或者会话数据,切换后用户请求落在新版本Pod上,Session如果存在本地内存里就直接丢了。这个问题不是蓝绿发布带来的,是任何发布方式都绕不开的,但蓝绿发布一瞬间切换的特性会让这个问题集中爆发。所以做蓝绿发布之前,先想清楚应用状态是不是无状态的、会话是不是已经外置到Redis或者别的存储里了。如果这些没准备好,蓝绿发布带来的不是平滑,是灾难。1.2 为什么我最终选了蓝绿而不是滚动或灰度先说说滚动更新。Kubernetes默认的发布方式就是RollingUpdate,新老Pod同时存在,逐个替换。滚动更新的优点是资源占用少,不需要额外准备一整套环境,缺点是发布过程中新老版本会同时对外提供服务,如果新旧版本之间不兼容(比如数据库字段变更、API接口语义变化),滚动期间就会出现一部分请求走新逻辑、一部分请求走老逻辑的割裂状态。更让人头疼的是回滚,一旦发现问题,你得等滚动更新把新版本全部替换完或者手动介入暂停,然后反向再滚一轮。这个过程耗时很长,期间服务一直处于异常状态。灰度发布则更加精细。它按流量比例分流,比如先切5%的流量到新版本,验证没问题再逐步扩大到10%、30%、50%、100%。灰度发布适合那种需要长时间观察、逐步放量的场景,尤其适合用户量很大、一旦出事影响面就非常广的业务。但灰度发布的基础设施要求也高:要么用Ingress的Annotation做流量权重,要么引入Service Mesh(比如服务网格),要么在网关层做复杂的规则配置。自己做灰度发布,规则写起来不复杂,但监控、追踪、日志都需要按版本维度去拆,对中小团队来说成本不低。蓝绿发布夹在中间。它比滚动更新“安全”,因为线上始终有一套完整的、经过验证的版本在兜底;它比灰度发布“简单”,因为不需要按比例切流量、不需要维护复杂的规则,所有的流量就是“整切”或者“不切”两个状态。蓝绿唯一的缺点是需要占用双倍的资源,在流量高峰期间相当于你养了两套生产环境。但对大多数业务体量来说,这个资源成本相比故障造成的损失,完全值得。我的建议是:交易链路、账务系统这类“绝对不能出大问题”的服务优先考虑蓝绿;对外的新功能尝试、可容忍小范围异常的服务,再考虑灰度。2. 动手前,环境准备和资源规划怎么搞2.1 集群规模与组件选型先把环境说清楚。我这次实操用的集群是三台2核4G的工作节点,Kubernetes版本1.24,容器运行时是containerd,Ingress控制器用的是Ingress-Nginx,版本1.8左右。这套配置没什么特别的门槛,基本是中小团队的标配。如果你用的是托管的Kubernetes服务,核心操作都是一样的,只是Ingress的创建入口不一样。镜像仓库我用的私有Registry,应用镜像打好tag之后直接push上去。这里有一个细节值得注意:mirror tag区分度要高,不要一直用latest,因为蓝绿切换的时候你要从Service的selector上明确看出来现在指向的是哪个版本的Pod,如果镜像tag都是latest,光了看Pod的列表,你根本分不清哪个是蓝哪个是绿。版本号强制规范一下,比如v1.8.0和v1.9.0这种语义化版本号,配置和Git提交记录也对得上,排查问题的时候省很多事。还有一点,蓝绿发布虽然是应用层的东西,但它和集群的调度策略是联动的。如果你的节点只有一个,两套环境会跑在同一台机器上,发布期间两个版本的Pod同时存在,CPU、内存都会有明显波动,压力测试不充分的情况下,可能出现想切流量但新版本还没Ready的情况。生产环境最好保证至少三个工作节点,并且两套Deployment的Pod反亲和性策略避免调度到同一个节点上。下面是一段Pod反亲和性的配置示例:affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: my-app topologyKey: kubernetes.io/hostname这个配置的意思是:尽量让同属一个应用的新旧版本Pod不落在同一台宿主机上。好处是,如果新版本部署后出现了内存泄漏之类的问题,不至于把集群里所有节点一次性拖垮。2.2 命名空间和资源隔离怎么设计蓝绿发布如果只在一个命名空间里搞,省事,但风险控制上不如单独建一个发布专用命名空间来得干净。我习惯的做法是:给这个应用单独建一个命名空间,比如叫app-production,里面放这个应用所有相关的Deployment、Service、ConfigMap和Ingress。这样做的好处是,你执行kubectl get all的时候,看到的就是这个应用的全貌;某个环境出了问题,也不会误操作到别的应用。命名空间内部再按发布单元划分就更清晰了。比如这个订单服务,它依赖数据库、Redis、消息队列,那数据库和Redis这些中间件可以用独立的命名空间管理,应用部署在这个命名空间里只关注自身的Deployment和Service。蓝绿发布真正要切换的只是应用这层,中间件是不参与切换的,如果中间件也参与蓝绿切换,那就是另一个复杂度的故事了,一般不建议在初期就这么设计。整体的拓扑关系是这样的:一套命名空间,跑两个Deployment(蓝色和绿色),各自带一组Pod,然后一个共享的Service。这个Service的名字和应用名保持一致,方便Ingress绑定。切换动作就是改Service的selector,Ingress、域名、证书全都不用动。希望你把这个结构记清楚,后面的实操字符串都是围绕这个拓扑展开的。3. 蓝绿发布的完整实操流程3.1 第一步:部署蓝色环境(当前生产版本)模拟一个场景:订单服务当前线上版本是v1.8.0,跑在一个名叫my-app-blue的Deployment里。蓝色环境的标签配置我会非常刻意地设计,因为它决定了之后切换和排查的准确性。先看YAML:apiVersion: apps/v1 kind: Deployment metadata: name: my-app-blue namespace: app-production spec: replicas: 3 selector: matchLabels: app: my-app track: blue template: metadata: labels: app: my-app track: blue version: v1.8.0 spec: containers: - name: app-container image: registry.internal/order-service:v1.8.0 ports: - containerPort: 8080 env: - name: APP_VERSION value: "v1.8.0" - name: DB_DSN valueFrom: secretKeyRef: name: db-secret key: dsn readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 10两个标签很关键:track: blue和version: v1.8.0。track是发布环境的角色标识,version是版本标识。在排查问题的时候,我们通过track就能快速判断当前的流量指向;在查看日志和监控的时候,version能让你一眼看到当前Pod跑的是哪一版代码。这两个标签在后续的Service selector里都会用到。readinessProbe(就绪探针)不是可有可无的,它是蓝绿切换节奏的纪律器。注意我配置的是/health接口,这个接口最好不只是简单的“程序在运行”,而应该包含关键依赖的健康状态,比如数据库连接池是否可用、Redis是否连接成功。如果数据库挂了但探针依然返回200,那Kubernetes会认为Pod是健康的,流量切过去之后用户就直接面对错误页面。这个接口的意义在蓝绿发布里格外重要,因为你是整体切换,没有小流量验证的机会。应用本身的安全基线也别落下,比如Pod里跑的用户不要用root,加resources的requests和limits,不然流量高峰的时候宿主机资源被撑爆了谁都不好受。这是我在生产环境踩过的坑,新版本上线后内存占用比预估的多了不少,要不是提前配置了limits,整个节点都能被拖死。3.2 第二步:部署绿色环境(新版本)现在把v1.9.0版本部署成绿色环境。YAML结构和蓝色基本一致,差异只有三处:Deployment的名字变成my-app-green,track标签变成green,镜像tag变成v1.9.0。别小看这三处差异,Deployment名字如果搞成和蓝色一样,Kubernetes会把旧的直接替换掉,绿环境就起不来了。apiVersion: apps/v1 kind: Deployment metadata: name: my-app-green namespace: app-production spec: replicas: 3 selector: matchLabels: app: my-app track: green template: metadata: labels: app: my-app track: green version: v1.9.0 spec: containers: - name: app-container image: registry.internal/order-service:v1.9.0 ports: - containerPort: 8080 env: - name: APP_VERSION value: "v1.9.0" - name: DB_DSN valueFrom: secretKeyRef: name: db-secret key: dsn readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 10为什么是新环境先部署、用户无感知地跑起来,而不是直接改流量?这是因为部署本身是有风险的:镜像可能拉取失败、配置可能不匹配、新版本可能启动就崩溃。如果直接改Service的selector,等于把用户直接丢进一个还没有经过任何验证的环境里。正确的节奏是:绿环境先独立启动,Pod进入Running且Ready状态,确认新版本能正常对外提供服务之后,才进行流量切换。这个“先部署、后切换”的节奏是蓝绿发布的纪律核心。部署完之后,别忘了看一眼Pod的运行状态。kubectl get pods -n app-production -l track=green,如果看到Pod一直Pending或者CrashLoopBackOff,先别急着切流量。可以进入到Pod里看启动日志,或者直接kubectl logs 看有没有报错。这阶段排查问题的成本是最低的,不需要回滚,不需要恢复现场,改完配置重新部署就行。3.3 第三步:创建共享Service和Ingress两套Deployment都就绪之后,创建一个共享的Service。这个Service的名字叫my-app-svc,selector先指向蓝色环境,因为当前生产流量还在蓝色那边。apiVersion: v1 kind: Service metadata: name: my-app-svc namespace: app-production spec: selector: app: my-app track: blue ports: - port: 80 targetPort: 8080先指向蓝色,让业务先保持在线状态的稳定。接着创建Ingress:apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-app-ingress namespace: app-production spec: ingressClassName: nginx rules: - host: order.example.com http: paths: - path: / pathType: Prefix backend: service: name: my-app-svc port: number: 80Ingress配置里的backend是my-app-svc,这个一直不用变。域名、TLS、路径规则都绑定在Service这一层上,而Service的selector才是切换流量的开关。这里有个容易踩的坑,如果创建Ingress的时候把Service名字误写成了my-app-blue,那后续不管你怎么改Service的selector,流量都不会切到绿色环境,因为Ingress压根是直连蓝色Service。正确的做法是,让Ingress永远指向那个共享的Service。这个阶段先验证一下:访问order.example.com,流量应该正常落在蓝色环境的Pod上。可以在蓝色环境某个Pod的日志里确认访问记录是存在的,这能帮你确认整条链路通了。3.4 第四步:一键切换流量万事俱备,切流量。这个动作本质上就是修改Service的selector,让track从blue变成green。执行命令:kubectl patch svc my-app-svc -n app-production -p '{"spec":{"selector":{"app":"my-app","track":"green"}}}'或者用update方式:kubectl edit svc my-app-svc -n app-production把selector里的track改成green保存退出即可。我一般推荐用patch命令,因为可重复、可脚本化,而edit方式要人工去改,容易误操作。执行完之后,看一眼Service关联的Endpoints:kubectl get endpoints my-app-svc -n app-production正常情况下,ENDINGS列显示的Pod IP会从蓝环境3个Pod的IP变成绿环境3个Pod的IP。如果显示的还是蓝色环境的IP,说明selector改错了,或者绿色环境的Pod没有通过就绪探针。这一步检查一定要做,它能第一时间发现问题。再看一下Pod的状态和日志:kubectl get pods -n app-production -o wide kubectl logs -n app-production -l track=green --tail=50如果绿色环境的Pod日志里出现了正常的访问记录,说明流量已经成功切过来了。此时在用户侧访问order.example.com,看到的就是新版本的页面和功能。注意,这个切换是瞬间完成的,Service的转发规则会立即更新到数据链路。但已存在的长连接不会立刻断掉,已经建立好的TCP连接会继续走完生命周期。这个细节后面在问题排查部分我会详细展开,这里你只要知道蓝绿切换不等于无缝切换就行了。3.5 第五步:验证与稳定期管理流量切过去之后,不是切换动作做完就完事了,还有一段验证观察期。观察期的长短取决于你的业务性质。订单服务这种核心交易链路,我一般至少观察15到30分钟,期间要盯几个指标,而不是单纯看页面能不能打开:接口错误率是否在正常基线范围P99延迟是否有明显增加应用日志里有没有大量异常堆栈基础设施监控里看一眼CPU、内存、磁盘IO、网络带宽的变化趋势数据库慢查询是否增多依赖的下游服务有没有出现超时报错我习惯把所有这些集中到一个观察面板上。如果团队已经有了监控系统,直接贴上应用的维度就行;如果没有,临时用命令行盯日志也要盯。发布期间,人不离开终端,这是最朴素但也最有效的办法。在观察期内,如果应用出现任何不符合预期的情况,直接执行回滚。回滚操作和切换一样简单,把selector改回blue:kubectl patch svc my-app-svc -n app-production -p '{"spec":{"selector":{"app":"my-app","track":"blue"}}}'这个操作能在几秒钟之内让流量全部回到原来的蓝色环境。蓝绿发布最大的价值就是这个:回滚不是重新发布一版代码,而是“切回去”,成本极低。观察期确认没问题之后,再把蓝色环境的副本数缩容到0,或者直接删除蓝色Deployment,释放资源。这一步一般放在第二天业务低峰期做,因为万一切换后一个晚上出现了延迟性问题,蓝色环境还在,还能兜底。3.6 第六步:回滚的完整路线回滚分几种情况,处理方式不一样。第一种情况,切换后几分钟内发现了明显问题,比如接口大面积报错、用户反馈页面打不开。这种就果断切回去,不用思考。执行的命令就是把selector的track改成blue,然后观察蓝色环境的日志和延迟,确认业务恢复了。此时绿色环境不要删除,因为报错信息还要从绿色环境的日志里捞出来分析,如果删了连现场都没了。第二种情况,切换后过了几个小时甚至一天才发现问题,比如某个批处理任务到了晚上才跑,跑挂了。这时候流量已经全部打在绿色环境上,你需要评估:是修复新版本的问题然后再切回来,还是直接回滚到蓝色环境。如果回滚,需要把Service切回蓝色,同时绿色环境暂时保留。等定位问题、修复完成后,重新把绿色环境更新成修复后的版本再切。这种场景下,蓝色环境实际上成了长期存在的“保护网”。第三种情况,新版本和数据库之间有兼容性问题。比如新版本代码需要读取数据库里新增的某个字段,但回滚到旧版本之后,旧版本代码不认这个字段,导致回滚后依然报错。这种问题在蓝绿发布里很经典,它说明一个原则:数据库变更和应用变更是不同步的。如果数据库结构的变更是一次性的、不可逆的,那蓝绿发布的回滚能力就打折了。这个问题没有银弹,只能尽量让数据库变更与应用代码向下兼容,或者把数据库变更放在应用发布更早的时间点去执行,给回滚留下容错空间。这也是很多团队在蓝绿发布流程规范里会专门写明的一条纪律:数据库变更必须与应用发布解耦,且变更要支持双向兼容。4. 我踩过的坑:常见问题与排查实录4.1 切换瞬间大量报错,长连接全断了第一次在业务高峰期做蓝绿切换的时候,切完流量我这边监控面板直接飘红,大量请求超时、连接重置、用户端不断重试。排查下来发现,问题不在Service切换本身,而在于应用没有处理好“优雅退出”。先说原理。滚动更新时新老Pod交替,老Pod收到SIGTERM信号后会有一段graceful shutdown窗口,默认是30秒,让程序把存量请求处理完再退出,期间新Pod已经Ready了,流量会慢慢往新Pod上引导。但蓝绿切换时,蓝色环境的Pod并没有收到任何终止信号,它们是“被抛弃”的——Service的selector变了,新的连接不再转发到蓝色Pod,但已经建立的HTTP长连接、gRPC连接、数据库连接池连接还挂在蓝色Pod上。如果客户端是长连接型应用且没有重连机制,这些连接会一直挂到超时,用户侧表现就是请求一直转圈。解决这个问题有两个层面。第一,应用自身要设置合理的HTTP keep-alive超时时间,不要让连接无限期挂在一个Pod上;第二,网关层要有连接失效后的重试机制,比如Nginx超时后重新发起请求到新的后端。如果是对外提供API的服务,客户端SDK最好设置连接池探活和自动重建机制。这个方法得提前和运维、客户端开发的同事都对好,否则切换的时候很容易翻车。另外一个隐含问题是,假如蓝色环境的Pod上还挂着正在处理的数据库事务,切流量后事务还没提交完,这批请求会完成,但下一次查询可能已经转到绿色环境,而绿色环境和蓝色环境共享同一个数据库,理论上数据是一致的,但如果事务中途失败,会产生死锁或数据不一致。这种问题排查起来非常痛苦,建议逻辑复杂的服务增加一个“排空窗口”,切换前先等待存量请求耗尽(比如通过Ingress层面先停止转发、等30秒再切Service),这一步能做,但需要业务方配合评估。4.2 新旧版本Pod同时收到流量这个坑我遇到过两次,排查过程比较曲折。现象是:切换完成后,蓝色和绿色环境的Pod日志里都有请求,流量成了“一半一半”或者“随机分布”。第一次排查,怀疑是Service的selector没有完全覆盖,导致两个环境都被选中了。翻看Service的YAML,selector是app: my-app和track: green,按理说只应该选中绿色Pod。后来发现蓝色Deployment的template里track标签虽然是blue,但Pod上还有一个手工加的标签track: blue,而且这个标签和Service里某个历史配置匹配上了。Kubernetes的Service选择器匹配逻辑是“所有标签都必须匹配”,但历史遗留的Service定义有可能出现过没写track的情况,导致蓝色Pod的app标签匹配就进了Endpoints。解决办法是严格梳理所有资源的标签一致性,旧的、无用的标签一律删掉,Service定义和Deployment模板里的标签保持完全一致。第二次是因为多Service共存。当时我的Ingress同时绑定了两个Service,一个指向蓝色、一个指向绿色,想实现灰度验证的效果,但忘记在切换时把蓝色Service的权重改成0了。结果Ingress层面按轮询把流量分发到两个Service,蓝绿两边都收到了请求。这个错误本质上是对蓝绿发布模式理解不透彻——蓝绿发布不是让你同时开两个入口,而是只有一个入口、一个Service,靠selector切。如果确实需要临时验证新版本功能,更安全的做法是:在绿色环境Deployment旁边临时起一个仅供测试的Service,访问路径上加一个二级域名或者Header路由,和正式流量完全隔离。验证完了再销毁这个临时Service。4.3 Pod已经Ready,但流量还是不走它这个坑很有代表性。绿色环境Deployment部署完成,kubectl get pods看到所有Pod状态都是Running,readiness探针也过了,selectors也没写错,但切换之后流量还是全部打到了蓝色环境的Pod上,绿色一个请求都没有。最终定位到问题出在Service的Endpoints上。Kubernetes在更新Service的selector后,Endpoints的刷新不是“改一下就行”那种瞬间生效,而是有一小段异步同步周期,正常情况下是秒级。但如果绿色环境Pod数量多、Endpoint Controller处理不过来,或者网络插件(CNI)没有及时同步节点上的转发规则,就会出现几秒钟甚至几十秒的“切换前状态”。这个可以通过强制更新Endpoints来看:kubectl get endpoints my-app-svc -n app-production kubectl describe endpoints my-app-svc -n app-production如果Endpoints已经更新成绿色IP,但流量依然不走绿色,那问题就出在kube-proxy和CNI的规则同步上。最简单的办法是重启一下相关节点的kube-proxy,或者等它自动重新同步(一般30秒内会恢复)。遇到这种问题别慌,蓝绿切换不是瞬时无感的,给它一个合理的“排空窗口期”就好,窗口期内可能有一小部分请求仍落到旧环境,这属于正常现象,最好是选择业务低峰期做切换。还有一个比较隐蔽的情况:如果集群启用了会话亲和(sessionAffinity),切换后客户端IP会被哈希到固定的后端IP上,一旦哈希到已不存在的旧Pod IP上,请求就断了。蓝绿发布要默认关闭sessionAffinity,除非你的应用已经做了外部会话存储,否则不要开。4.4 ConfigMap与Secret跟着版本漂移应用跑起来了,但配置还是旧的,这个问题对蓝绿发布来说是致命伤。很多应用通过ConfigMap注入配置,如果你只有一个ConfigMap,名字叫my-app-config,蓝绿两个Deployment都引用了同一个ConfigMap对象。这种情况下,如果修改了ConfigMap的内容且更新了新版本代码来适配新配置,但蓝色环境仍在跑旧代码,它读取到新配置就会直接挂掉。Kubernetes的ConfigMap更新后,Pod里挂载的文件不会立即生效,默认有同步延迟(kubelet默认每分钟同步一次),而且有的应用只会在启动时读取一次配置。所以在蓝绿发布的场景里,配置和代码应该同步版本化:每个版本有自己独立的ConfigMap,在Deployment的envFrom里引用版本对应的ConfigMap。比如v1.8.0引用my-app-config-v1-8-0,v1.9.0引用my-app-config-v1-9-0。这样切换的时候,代码和配置天然是一套的。另外一个容易被忽略的是Secret。数据库账号密码、第三方API密钥这些敏感信息,如果只存在一个Secret,蓝绿环境共同引用,切换本身没问题,但一旦新版本需要换一套密钥,旧版本还在用新密钥,可能直接导致旧的可用环境变成不可用,回滚的兜底也就失效了。个人建议,Secret也按环境隔离,一个环境一套,别混杂。4.5 双环境长期运行,资源成本怎么控蓝绿发布的资源双倍问题,是很多团队最终放弃它的主要原因,尤其是没有预算买更多机器的中小企业。我的做法是,给蓝绿两套环境设置不同的资源规格。蓝色环境因为是稳定兜底,保持正常副本数和资源请求;绿色环境部署的时候可以先把副本数压低到最小,比如2个,验证阶段通过后,在切换流量之前再扩容到和蓝色环境相同的副本数。这样部署阶段资源占用小,切换之后才真正承担全部流量。再者,观察期结束、确认新版本稳定后,蓝色环境可以直接缩容到1个副本甚至0个副本。如果业务有周期性的低峰期,那么低峰期缩容蓝环境,高峰期前重新拉起。这些操作可以通过Kubernetes的HPA(水平自动扩缩容)或者CronJob写脚本实现。不过不建议把这个自动化搞得太复杂,蓝绿发布的核心价值是“可快速回滚”,如果蓝环境长期缩容到0,回滚就需要重新拉Pod、等Ready,最快的效果也会大打折扣。一般建议至少保留1个副本随时待命。还有一个省钱思路:非关键服务的蓝绿发布可以复用同一批节点的空闲资源,只要节点的Requests总和没有超过节点容量就不会产生额外费用。Kubernetes调度器按Requests来分配资源,你的Blue+Green同时跑在一台4核的虚拟机上,只要总Requests低于4核,就都能跑。当然Pod实际使用的CPU可能超过Requests,所以生产环境还是要有监控兜底,别超卖得太狠。5. 把蓝绿发布自动化,形成可复用的发布管线5.1 一键发布脚本设计手动操作做一两次可以,但每次发布都要敲一堆kubectl命令,存在两个问题:一是容易出错,二是没法留痕。所以我把整个流程封装成了一个发布脚本,核心逻辑就是几个阶段函数。脚本的大致结构是这样的:#!/bin/bash set -euo pipefail NAMESPACE="app-production" APP_NAME="my-app" NEW_VERSION="${1:-v1.9.0}" OLD_VERSION="${2:-v1.8.0}" pre_deploy() { echo ">>> 部署新版本(绿色环境)" kubectl apply -f "green-deployment-${NEW_VERSION}.yaml" kubectl rollout status deployment/my-app-green -n ${NAMESPACE} --timeout=180s } switch_traffic() { echo ">>> 切换流量到绿色环境" kubectl patch svc ${APP_NAME}-svc -n ${NAMESPACE} \ -p "{\"spec\":{\"selector\":{\"app\":\"${APP_NAME}\",\"track\":\"green\"}}}" echo ">>> 等待 Endpoints 更新" sleep 5 kubectl get endpoints ${APP_NAME}-svc -n ${NAMESPACE} } rollback() { echo ">>> 回滚到蓝色环境" kubectl patch svc ${APP_NAME}-svc -n ${NAMESPACE} \ -p "{\"spec\":{\"selector\":{\"app\":\"${APP_NAME}\",\"track\":\"blue\"}}}" } case "${1:-}" in pre) pre_deploy ;; switch) switch_traffic ;; rollback) rollback ;; *) echo "用法: $0 {pre|switch|rollback}" ;; esac这套脚本的指导思想是“一个动作、一条命令、一秒钟切回”。pre阶段只部署新环境,不影响线上流量;switch阶段才真正把用户流量切到新环境;rollback阶段一键回到旧环境。每个阶段都有明确的输出,日志里会记录发布操作的时间戳和执行的patch命令,发布完还能把输出归档成文件,作为审计凭据。脚本里有一个细节值得注意:switch之后sleep 5秒,让Endpoint Controller有时间更新Pod列表。这个等待时间不是随便定的,它取决于你的集群规模,小集群几秒即可,大集群可能得10秒以上。等Endpoints确认更新完毕,再继续后续操作。如果你在切换后立刻做压力测试,可能测到的是还没切换完成的中间状态,容易误判。5.2 与CI/CD流水线集成发布脚本只是最底层的手动操作,真正顺畅的用法是接进CI/CD流水线。以常见的GitLab CI或者GitHub Actions为例,阶段划分大概是:构建阶段:代码合并到主干后自动构建镜像,打上版本号tag并推送到镜像仓库预发布阶段:调用pre_deploy,部署绿色环境,但此时流量还在蓝色验证阶段:跑自动化冒烟测试,执行关键业务链路的用例(登录、下单、支付、查询),还可以用爬虫打一个小流量到绿色环境的临时Service上切换阶段:人工确认或自动执行switch_traffic稳定观察阶段:持续15-30分钟后,监控指标没有异常再往下走清理阶段:缩容蓝色环境每一步的准入条件一定要卡死。比如切换到绿色环境之前,自动化测试必须全部通过,冒烟测试覆盖率不能低于某个阈值,不然任何人都能跳过验证直接发布。这套流程跑顺之后,发布这个动作就从“高危操作”变成了“例行操作”,开发测试同学都能执行,运维只需要在处理异常情况的时候介入。5.3 蓝绿发布的进阶玩法:与灰度结合讲到这里,有人会问:蓝绿和灰度只能二选一吗?其实可以结合。比如电商大促前,核心交易链路适合蓝绿发布,确保整体稳定;但预言一个新功能的时候,更适合灰度发布,看用户反馈再放量。可以在同一套基础设施里,给核心服务配置蓝绿发布,给非核心服务配置Ingress的canary规则做灰度,这样既保证了核心链路的稳定性,又保留了产品验证的灵活性。还有一种组合是“金丝雀蓝绿”,做法是:绿色环境部署好但先不切全量,在Ingress层加一条临时规则,把内部测试人员的IP段或者某个Header(比如X-Canary: true)转发到绿色环境,其他流量继续走蓝色。这种方式本质上是用灰度发布的方式验证蓝绿环境,验证通过后再整切。很多团队愿意用这个方式,因为它既保留了蓝绿的大规模回滚能力,又解决了蓝绿完全按全量发布缺少“最后一公里”验证的问题。实现方式很简单:在Ingress上多加一条path规则,优先级高于现有规则,指定特定Header的请求转发到绿色环境对应的一个临时Service。这条规则用完之后删除即可。这套玩法基础设施成本不增加多少,但运维操作的复杂度确实上来了,所以建议团队磨合稳定了再上。6. 写在最后:我的几点体会蓝绿发布做久了,你会发现它本质上不是技术问题,而是管理和决策问题。技术层面的切换和回滚都很快,难的是你团队有没有底气在发现问题的那一刻果断按回滚键。很多人切换完成后看着监控曲线不对劲,第一反应是再等等看会不会恢复——这个“等等看”的窗口期,往往就是故障扩大化的根源。我的做法是事先写清楚切换后哪些指标属于“立即回滚红线”(比如错误率超过1%、P99延迟翻倍、数据库慢查询明显上升),只要触发红线,不做任何犹豫,直接回滚。这样整个团队有章法可循,发布负责人不需要临场做情绪化决策。另外,蓝绿发布并不适合所有服务。如果公司初创期,服务才刚刚上线两周,用户量极少,直接改代码重启就行,蓝绿这套流程反而是负担。它的价值在服务发展稳定、上线风险成本大于资源成本之后才凸显出来。这个判断标准建议作为决策的起点,而不是别人用你也用。最后分享一个小技巧:发布过程中把操作命令的输出去重定向到文件里保存,甚至可以每天发一次定时任务,把当前Service的selector状态输出成报告存档。这样一旦出问题,回溯是谁在什么时间点改了哪些配置,清清楚楚,扯皮的时候也有了事实依据。希望这篇实战经验能帮到那些正准备引入蓝绿发布,或者已经在蓝绿发布里折腾得焦头烂额的同行。
1. 蓝绿发布到底解决了什么问题搞运维和开发的兄弟应该都有过这种经历:一个版本上线,把全站用户给搭进去了。我印象很深的一次,某电商项目改了订单状态的流转逻辑,测试环境怎么验都没问题,结果一上线,线上订单全部卡在“支付成功待发货”的状态。当时还不是蓝绿发布,用的滚动更新,新版本的Pod一批一批地替换旧Pod,等我们反应过来有问题的时候,新代码已经扩散到一半的节点上了,回滚都要等滚动完了才能继续操作。那半小时里,用户每下一单就失败一单,客服电话直接被打爆。事后复盘,大家聊到一个问题:如果当时有一整套老版本环境可以随时切回去,结果会不会不一样?答案其实是肯定的。这就是蓝绿发布最朴素的出发点——手里留一套能随时顶上来的老环境。蓝绿发布这个词,最早是从什么场景火起来的我不确定,但在Kubernetes这套体系里,它已经成了很多团队上线必配的一套流程。网上讲蓝绿发布的文章不少,但大部分只给你贴两个Deployment的YAML,然后说“改一下Service的selector就切流量了”。听起来确实简单,但真正在生产和预发环境里跑过一轮之后,你会发现里面有大量坑:连接池怎么平滑迁移、旧环境什么时候才能撤、验证流量怎么打、回滚的时候日志会不会乱掉、数据库兼容性怎么处理。这篇文章我不打算给你讲纯理论,我按自己实操过的路径,把蓝绿发布从原理到落地再到避坑,完整拆一遍,内容包括最关键的切换机制、资源编排、应用配置、流量验证和应急回滚。适合谁来读?如果你是刚接触Kubernetes的初级运维,看了能直接对着文中的配置在集群里搭出一套能用的蓝绿发布流程;如果你已经在用滚动更新或者正在被灰度发布的复杂度搞得头疼,这篇文章能帮你理清楚蓝绿这条路线为什么在某些场景下更合适。老实说,蓝绿不是银弹,但它确实是“快速回滚”这个目标下实现成本最低的一种方案。1.1 蓝绿发布的原理:一个入口、两套环境蓝绿发布的核心逻辑用一句话讲就是:同一时间存在两套完全独立的应用环境,分别叫蓝色环境(当前生产版本)和绿色环境(即将上线的新版本),两套环境共享同一个服务入口,通过入口的调度规则把真实流量全部切到蓝色或者绿色。这套逻辑放到Kubernetes里,基础设施层面的表达非常干净——两套Deployment分别跑蓝色版本和绿色版本,它们各自有独立的Pod和Label,外面套一个Service,利用Service的selector字段指向不同标签,就能实现流量在蓝绿之间的切换。很多人第一次接触这个概念的时候会有一个疑惑:为什么不用Ingress去切?Ingress切的是域名路由,可以很精细地做到按路径、按请求头分流,但它更倾向于灰度发布和A/B测试。蓝绿发布要的是“整体切换、一步到位”,切换的粒度应该是服务级而不是路由级。所以在Kubernetes里,最契合蓝绿发布思路的流量控制点就是Service。Service这一层对Ingress来说是不透明的一个抽象,它内部的selector无论指向哪一批Pod,Ingress都不需要做任何变更,域名和路由规则始终保持原来的样子。这种“后端悄悄换、前端毫无感知”的特性,正是蓝绿发布最舒服的地方。再细分一点,流量入口其实分两层。最外层是Ingress,负责域名、TLS证书、路径转发规则,这一层在整个发布期间基本不动;内层是Service,它通过selector选择后端Pod。蓝绿切换的本质动作就是修改Service的selector,让它的流量从指向蓝色Deployment的Label变成指向绿色Deployment的Label。整个过程不涉及Pod重建,连接断开后立即建立新的连接就能访问新版本。还有个很容易被忽略的点:Service只做转发,不做会话保持。如果应用有登录态或者会话数据,切换后用户请求落在新版本Pod上,Session如果存在本地内存里就直接丢了。这个问题不是蓝绿发布带来的,是任何发布方式都绕不开的,但蓝绿发布一瞬间切换的特性会让这个问题集中爆发。所以做蓝绿发布之前,先想清楚应用状态是不是无状态的、会话是不是已经外置到Redis或者别的存储里了。如果这些没准备好,蓝绿发布带来的不是平滑,是灾难。1.2 为什么我最终选了蓝绿而不是滚动或灰度先说说滚动更新。Kubernetes默认的发布方式就是RollingUpdate,新老Pod同时存在,逐个替换。滚动更新的优点是资源占用少,不需要额外准备一整套环境,缺点是发布过程中新老版本会同时对外提供服务,如果新旧版本之间不兼容(比如数据库字段变更、API接口语义变化),滚动期间就会出现一部分请求走新逻辑、一部分请求走老逻辑的割裂状态。更让人头疼的是回滚,一旦发现问题,你得等滚动更新把新版本全部替换完或者手动介入暂停,然后反向再滚一轮。这个过程耗时很长,期间服务一直处于异常状态。灰度发布则更加精细。它按流量比例分流,比如先切5%的流量到新版本,验证没问题再逐步扩大到10%、30%、50%、100%。灰度发布适合那种需要长时间观察、逐步放量的场景,尤其适合用户量很大、一旦出事影响面就非常广的业务。但灰度发布的基础设施要求也高:要么用Ingress的Annotation做流量权重,要么引入Service Mesh(比如服务网格),要么在网关层做复杂的规则配置。自己做灰度发布,规则写起来不复杂,但监控、追踪、日志都需要按版本维度去拆,对中小团队来说成本不低。蓝绿发布夹在中间。它比滚动更新“安全”,因为线上始终有一套完整的、经过验证的版本在兜底;它比灰度发布“简单”,因为不需要按比例切流量、不需要维护复杂的规则,所有的流量就是“整切”或者“不切”两个状态。蓝绿唯一的缺点是需要占用双倍的资源,在流量高峰期间相当于你养了两套生产环境。但对大多数业务体量来说,这个资源成本相比故障造成的损失,完全值得。我的建议是:交易链路、账务系统这类“绝对不能出大问题”的服务优先考虑蓝绿;对外的新功能尝试、可容忍小范围异常的服务,再考虑灰度。2. 动手前,环境准备和资源规划怎么搞2.1 集群规模与组件选型先把环境说清楚。我这次实操用的集群是三台2核4G的工作节点,Kubernetes版本1.24,容器运行时是containerd,Ingress控制器用的是Ingress-Nginx,版本1.8左右。这套配置没什么特别的门槛,基本是中小团队的标配。如果你用的是托管的Kubernetes服务,核心操作都是一样的,只是Ingress的创建入口不一样。镜像仓库我用的私有Registry,应用镜像打好tag之后直接push上去。这里有一个细节值得注意:mirror tag区分度要高,不要一直用latest,因为蓝绿切换的时候你要从Service的selector上明确看出来现在指向的是哪个版本的Pod,如果镜像tag都是latest,光了看Pod的列表,你根本分不清哪个是蓝哪个是绿。版本号强制规范一下,比如v1.8.0和v1.9.0这种语义化版本号,配置和Git提交记录也对得上,排查问题的时候省很多事。还有一点,蓝绿发布虽然是应用层的东西,但它和集群的调度策略是联动的。如果你的节点只有一个,两套环境会跑在同一台机器上,发布期间两个版本的Pod同时存在,CPU、内存都会有明显波动,压力测试不充分的情况下,可能出现想切流量但新版本还没Ready的情况。生产环境最好保证至少三个工作节点,并且两套Deployment的Pod反亲和性策略避免调度到同一个节点上。下面是一段Pod反亲和性的配置示例:affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: my-app topologyKey: kubernetes.io/hostname这个配置的意思是:尽量让同属一个应用的新旧版本Pod不落在同一台宿主机上。好处是,如果新版本部署后出现了内存泄漏之类的问题,不至于把集群里所有节点一次性拖垮。2.2 命名空间和资源隔离怎么设计蓝绿发布如果只在一个命名空间里搞,省事,但风险控制上不如单独建一个发布专用命名空间来得干净。我习惯的做法是:给这个应用单独建一个命名空间,比如叫app-production,里面放这个应用所有相关的Deployment、Service、ConfigMap和Ingress。这样做的好处是,你执行kubectl get all的时候,看到的就是这个应用的全貌;某个环境出了问题,也不会误操作到别的应用。命名空间内部再按发布单元划分就更清晰了。比如这个订单服务,它依赖数据库、Redis、消息队列,那数据库和Redis这些中间件可以用独立的命名空间管理,应用部署在这个命名空间里只关注自身的Deployment和Service。蓝绿发布真正要切换的只是应用这层,中间件是不参与切换的,如果中间件也参与蓝绿切换,那就是另一个复杂度的故事了,一般不建议在初期就这么设计。整体的拓扑关系是这样的:一套命名空间,跑两个Deployment(蓝色和绿色),各自带一组Pod,然后一个共享的Service。这个Service的名字和应用名保持一致,方便Ingress绑定。切换动作就是改Service的selector,Ingress、域名、证书全都不用动。希望你把这个结构记清楚,后面的实操字符串都是围绕这个拓扑展开的。3. 蓝绿发布的完整实操流程3.1 第一步:部署蓝色环境(当前生产版本)模拟一个场景:订单服务当前线上版本是v1.8.0,跑在一个名叫my-app-blue的Deployment里。蓝色环境的标签配置我会非常刻意地设计,因为它决定了之后切换和排查的准确性。先看YAML:apiVersion: apps/v1 kind: Deployment metadata: name: my-app-blue namespace: app-production spec: replicas: 3 selector: matchLabels: app: my-app track: blue template: metadata: labels: app: my-app track: blue version: v1.8.0 spec: containers: - name: app-container image: registry.internal/order-service:v1.8.0 ports: - containerPort: 8080 env: - name: APP_VERSION value: "v1.8.0" - name: DB_DSN valueFrom: secretKeyRef: name: db-secret key: dsn readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 10两个标签很关键:track: blue和version: v1.8.0。track是发布环境的角色标识,version是版本标识。在排查问题的时候,我们通过track就能快速判断当前的流量指向;在查看日志和监控的时候,version能让你一眼看到当前Pod跑的是哪一版代码。这两个标签在后续的Service selector里都会用到。readinessProbe(就绪探针)不是可有可无的,它是蓝绿切换节奏的纪律器。注意我配置的是/health接口,这个接口最好不只是简单的“程序在运行”,而应该包含关键依赖的健康状态,比如数据库连接池是否可用、Redis是否连接成功。如果数据库挂了但探针依然返回200,那Kubernetes会认为Pod是健康的,流量切过去之后用户就直接面对错误页面。这个接口的意义在蓝绿发布里格外重要,因为你是整体切换,没有小流量验证的机会。应用本身的安全基线也别落下,比如Pod里跑的用户不要用root,加resources的requests和limits,不然流量高峰的时候宿主机资源被撑爆了谁都不好受。这是我在生产环境踩过的坑,新版本上线后内存占用比预估的多了不少,要不是提前配置了limits,整个节点都能被拖死。3.2 第二步:部署绿色环境(新版本)现在把v1.9.0版本部署成绿色环境。YAML结构和蓝色基本一致,差异只有三处:Deployment的名字变成my-app-green,track标签变成green,镜像tag变成v1.9.0。别小看这三处差异,Deployment名字如果搞成和蓝色一样,Kubernetes会把旧的直接替换掉,绿环境就起不来了。apiVersion: apps/v1 kind: Deployment metadata: name: my-app-green namespace: app-production spec: replicas: 3 selector: matchLabels: app: my-app track: green template: metadata: labels: app: my-app track: green version: v1.9.0 spec: containers: - name: app-container image: registry.internal/order-service:v1.9.0 ports: - containerPort: 8080 env: - name: APP_VERSION value: "v1.9.0" - name: DB_DSN valueFrom: secretKeyRef: name: db-secret key: dsn readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 10为什么是新环境先部署、用户无感知地跑起来,而不是直接改流量?这是因为部署本身是有风险的:镜像可能拉取失败、配置可能不匹配、新版本可能启动就崩溃。如果直接改Service的selector,等于把用户直接丢进一个还没有经过任何验证的环境里。正确的节奏是:绿环境先独立启动,Pod进入Running且Ready状态,确认新版本能正常对外提供服务之后,才进行流量切换。这个“先部署、后切换”的节奏是蓝绿发布的纪律核心。部署完之后,别忘了看一眼Pod的运行状态。kubectl get pods -n app-production -l track=green,如果看到Pod一直Pending或者CrashLoopBackOff,先别急着切流量。可以进入到Pod里看启动日志,或者直接kubectl logs 看有没有报错。这阶段排查问题的成本是最低的,不需要回滚,不需要恢复现场,改完配置重新部署就行。3.3 第三步:创建共享Service和Ingress两套Deployment都就绪之后,创建一个共享的Service。这个Service的名字叫my-app-svc,selector先指向蓝色环境,因为当前生产流量还在蓝色那边。apiVersion: v1 kind: Service metadata: name: my-app-svc namespace: app-production spec: selector: app: my-app track: blue ports: - port: 80 targetPort: 8080先指向蓝色,让业务先保持在线状态的稳定。接着创建Ingress:apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-app-ingress namespace: app-production spec: ingressClassName: nginx rules: - host: order.example.com http: paths: - path: / pathType: Prefix backend: service: name: my-app-svc port: number: 80Ingress配置里的backend是my-app-svc,这个一直不用变。域名、TLS、路径规则都绑定在Service这一层上,而Service的selector才是切换流量的开关。这里有个容易踩的坑,如果创建Ingress的时候把Service名字误写成了my-app-blue,那后续不管你怎么改Service的selector,流量都不会切到绿色环境,因为Ingress压根是直连蓝色Service。正确的做法是,让Ingress永远指向那个共享的Service。这个阶段先验证一下:访问order.example.com,流量应该正常落在蓝色环境的Pod上。可以在蓝色环境某个Pod的日志里确认访问记录是存在的,这能帮你确认整条链路通了。3.4 第四步:一键切换流量万事俱备,切流量。这个动作本质上就是修改Service的selector,让track从blue变成green。执行命令:kubectl patch svc my-app-svc -n app-production -p '{"spec":{"selector":{"app":"my-app","track":"green"}}}'或者用update方式:kubectl edit svc my-app-svc -n app-production把selector里的track改成green保存退出即可。我一般推荐用patch命令,因为可重复、可脚本化,而edit方式要人工去改,容易误操作。执行完之后,看一眼Service关联的Endpoints:kubectl get endpoints my-app-svc -n app-production正常情况下,ENDINGS列显示的Pod IP会从蓝环境3个Pod的IP变成绿环境3个Pod的IP。如果显示的还是蓝色环境的IP,说明selector改错了,或者绿色环境的Pod没有通过就绪探针。这一步检查一定要做,它能第一时间发现问题。再看一下Pod的状态和日志:kubectl get pods -n app-production -o wide kubectl logs -n app-production -l track=green --tail=50如果绿色环境的Pod日志里出现了正常的访问记录,说明流量已经成功切过来了。此时在用户侧访问order.example.com,看到的就是新版本的页面和功能。注意,这个切换是瞬间完成的,Service的转发规则会立即更新到数据链路。但已存在的长连接不会立刻断掉,已经建立好的TCP连接会继续走完生命周期。这个细节后面在问题排查部分我会详细展开,这里你只要知道蓝绿切换不等于无缝切换就行了。3.5 第五步:验证与稳定期管理流量切过去之后,不是切换动作做完就完事了,还有一段验证观察期。观察期的长短取决于你的业务性质。订单服务这种核心交易链路,我一般至少观察15到30分钟,期间要盯几个指标,而不是单纯看页面能不能打开:接口错误率是否在正常基线范围P99延迟是否有明显增加应用日志里有没有大量异常堆栈基础设施监控里看一眼CPU、内存、磁盘IO、网络带宽的变化趋势数据库慢查询是否增多依赖的下游服务有没有出现超时报错我习惯把所有这些集中到一个观察面板上。如果团队已经有了监控系统,直接贴上应用的维度就行;如果没有,临时用命令行盯日志也要盯。发布期间,人不离开终端,这是最朴素但也最有效的办法。在观察期内,如果应用出现任何不符合预期的情况,直接执行回滚。回滚操作和切换一样简单,把selector改回blue:kubectl patch svc my-app-svc -n app-production -p '{"spec":{"selector":{"app":"my-app","track":"blue"}}}'这个操作能在几秒钟之内让流量全部回到原来的蓝色环境。蓝绿发布最大的价值就是这个:回滚不是重新发布一版代码,而是“切回去”,成本极低。观察期确认没问题之后,再把蓝色环境的副本数缩容到0,或者直接删除蓝色Deployment,释放资源。这一步一般放在第二天业务低峰期做,因为万一切换后一个晚上出现了延迟性问题,蓝色环境还在,还能兜底。3.6 第六步:回滚的完整路线回滚分几种情况,处理方式不一样。第一种情况,切换后几分钟内发现了明显问题,比如接口大面积报错、用户反馈页面打不开。这种就果断切回去,不用思考。执行的命令就是把selector的track改成blue,然后观察蓝色环境的日志和延迟,确认业务恢复了。此时绿色环境不要删除,因为报错信息还要从绿色环境的日志里捞出来分析,如果删了连现场都没了。第二种情况,切换后过了几个小时甚至一天才发现问题,比如某个批处理任务到了晚上才跑,跑挂了。这时候流量已经全部打在绿色环境上,你需要评估:是修复新版本的问题然后再切回来,还是直接回滚到蓝色环境。如果回滚,需要把Service切回蓝色,同时绿色环境暂时保留。等定位问题、修复完成后,重新把绿色环境更新成修复后的版本再切。这种场景下,蓝色环境实际上成了长期存在的“保护网”。第三种情况,新版本和数据库之间有兼容性问题。比如新版本代码需要读取数据库里新增的某个字段,但回滚到旧版本之后,旧版本代码不认这个字段,导致回滚后依然报错。这种问题在蓝绿发布里很经典,它说明一个原则:数据库变更和应用变更是不同步的。如果数据库结构的变更是一次性的、不可逆的,那蓝绿发布的回滚能力就打折了。这个问题没有银弹,只能尽量让数据库变更与应用代码向下兼容,或者把数据库变更放在应用发布更早的时间点去执行,给回滚留下容错空间。这也是很多团队在蓝绿发布流程规范里会专门写明的一条纪律:数据库变更必须与应用发布解耦,且变更要支持双向兼容。4. 我踩过的坑:常见问题与排查实录4.1 切换瞬间大量报错,长连接全断了第一次在业务高峰期做蓝绿切换的时候,切完流量我这边监控面板直接飘红,大量请求超时、连接重置、用户端不断重试。排查下来发现,问题不在Service切换本身,而在于应用没有处理好“优雅退出”。先说原理。滚动更新时新老Pod交替,老Pod收到SIGTERM信号后会有一段graceful shutdown窗口,默认是30秒,让程序把存量请求处理完再退出,期间新Pod已经Ready了,流量会慢慢往新Pod上引导。但蓝绿切换时,蓝色环境的Pod并没有收到任何终止信号,它们是“被抛弃”的——Service的selector变了,新的连接不再转发到蓝色Pod,但已经建立的HTTP长连接、gRPC连接、数据库连接池连接还挂在蓝色Pod上。如果客户端是长连接型应用且没有重连机制,这些连接会一直挂到超时,用户侧表现就是请求一直转圈。解决这个问题有两个层面。第一,应用自身要设置合理的HTTP keep-alive超时时间,不要让连接无限期挂在一个Pod上;第二,网关层要有连接失效后的重试机制,比如Nginx超时后重新发起请求到新的后端。如果是对外提供API的服务,客户端SDK最好设置连接池探活和自动重建机制。这个方法得提前和运维、客户端开发的同事都对好,否则切换的时候很容易翻车。另外一个隐含问题是,假如蓝色环境的Pod上还挂着正在处理的数据库事务,切流量后事务还没提交完,这批请求会完成,但下一次查询可能已经转到绿色环境,而绿色环境和蓝色环境共享同一个数据库,理论上数据是一致的,但如果事务中途失败,会产生死锁或数据不一致。这种问题排查起来非常痛苦,建议逻辑复杂的服务增加一个“排空窗口”,切换前先等待存量请求耗尽(比如通过Ingress层面先停止转发、等30秒再切Service),这一步能做,但需要业务方配合评估。4.2 新旧版本Pod同时收到流量这个坑我遇到过两次,排查过程比较曲折。现象是:切换完成后,蓝色和绿色环境的Pod日志里都有请求,流量成了“一半一半”或者“随机分布”。第一次排查,怀疑是Service的selector没有完全覆盖,导致两个环境都被选中了。翻看Service的YAML,selector是app: my-app和track: green,按理说只应该选中绿色Pod。后来发现蓝色Deployment的template里track标签虽然是blue,但Pod上还有一个手工加的标签track: blue,而且这个标签和Service里某个历史配置匹配上了。Kubernetes的Service选择器匹配逻辑是“所有标签都必须匹配”,但历史遗留的Service定义有可能出现过没写track的情况,导致蓝色Pod的app标签匹配就进了Endpoints。解决办法是严格梳理所有资源的标签一致性,旧的、无用的标签一律删掉,Service定义和Deployment模板里的标签保持完全一致。第二次是因为多Service共存。当时我的Ingress同时绑定了两个Service,一个指向蓝色、一个指向绿色,想实现灰度验证的效果,但忘记在切换时把蓝色Service的权重改成0了。结果Ingress层面按轮询把流量分发到两个Service,蓝绿两边都收到了请求。这个错误本质上是对蓝绿发布模式理解不透彻——蓝绿发布不是让你同时开两个入口,而是只有一个入口、一个Service,靠selector切。如果确实需要临时验证新版本功能,更安全的做法是:在绿色环境Deployment旁边临时起一个仅供测试的Service,访问路径上加一个二级域名或者Header路由,和正式流量完全隔离。验证完了再销毁这个临时Service。4.3 Pod已经Ready,但流量还是不走它这个坑很有代表性。绿色环境Deployment部署完成,kubectl get pods看到所有Pod状态都是Running,readiness探针也过了,selectors也没写错,但切换之后流量还是全部打到了蓝色环境的Pod上,绿色一个请求都没有。最终定位到问题出在Service的Endpoints上。Kubernetes在更新Service的selector后,Endpoints的刷新不是“改一下就行”那种瞬间生效,而是有一小段异步同步周期,正常情况下是秒级。但如果绿色环境Pod数量多、Endpoint Controller处理不过来,或者网络插件(CNI)没有及时同步节点上的转发规则,就会出现几秒钟甚至几十秒的“切换前状态”。这个可以通过强制更新Endpoints来看:kubectl get endpoints my-app-svc -n app-production kubectl describe endpoints my-app-svc -n app-production如果Endpoints已经更新成绿色IP,但流量依然不走绿色,那问题就出在kube-proxy和CNI的规则同步上。最简单的办法是重启一下相关节点的kube-proxy,或者等它自动重新同步(一般30秒内会恢复)。遇到这种问题别慌,蓝绿切换不是瞬时无感的,给它一个合理的“排空窗口期”就好,窗口期内可能有一小部分请求仍落到旧环境,这属于正常现象,最好是选择业务低峰期做切换。还有一个比较隐蔽的情况:如果集群启用了会话亲和(sessionAffinity),切换后客户端IP会被哈希到固定的后端IP上,一旦哈希到已不存在的旧Pod IP上,请求就断了。蓝绿发布要默认关闭sessionAffinity,除非你的应用已经做了外部会话存储,否则不要开。4.4 ConfigMap与Secret跟着版本漂移应用跑起来了,但配置还是旧的,这个问题对蓝绿发布来说是致命伤。很多应用通过ConfigMap注入配置,如果你只有一个ConfigMap,名字叫my-app-config,蓝绿两个Deployment都引用了同一个ConfigMap对象。这种情况下,如果修改了ConfigMap的内容且更新了新版本代码来适配新配置,但蓝色环境仍在跑旧代码,它读取到新配置就会直接挂掉。Kubernetes的ConfigMap更新后,Pod里挂载的文件不会立即生效,默认有同步延迟(kubelet默认每分钟同步一次),而且有的应用只会在启动时读取一次配置。所以在蓝绿发布的场景里,配置和代码应该同步版本化:每个版本有自己独立的ConfigMap,在Deployment的envFrom里引用版本对应的ConfigMap。比如v1.8.0引用my-app-config-v1-8-0,v1.9.0引用my-app-config-v1-9-0。这样切换的时候,代码和配置天然是一套的。另外一个容易被忽略的是Secret。数据库账号密码、第三方API密钥这些敏感信息,如果只存在一个Secret,蓝绿环境共同引用,切换本身没问题,但一旦新版本需要换一套密钥,旧版本还在用新密钥,可能直接导致旧的可用环境变成不可用,回滚的兜底也就失效了。个人建议,Secret也按环境隔离,一个环境一套,别混杂。4.5 双环境长期运行,资源成本怎么控蓝绿发布的资源双倍问题,是很多团队最终放弃它的主要原因,尤其是没有预算买更多机器的中小企业。我的做法是,给蓝绿两套环境设置不同的资源规格。蓝色环境因为是稳定兜底,保持正常副本数和资源请求;绿色环境部署的时候可以先把副本数压低到最小,比如2个,验证阶段通过后,在切换流量之前再扩容到和蓝色环境相同的副本数。这样部署阶段资源占用小,切换之后才真正承担全部流量。再者,观察期结束、确认新版本稳定后,蓝色环境可以直接缩容到1个副本甚至0个副本。如果业务有周期性的低峰期,那么低峰期缩容蓝环境,高峰期前重新拉起。这些操作可以通过Kubernetes的HPA(水平自动扩缩容)或者CronJob写脚本实现。不过不建议把这个自动化搞得太复杂,蓝绿发布的核心价值是“可快速回滚”,如果蓝环境长期缩容到0,回滚就需要重新拉Pod、等Ready,最快的效果也会大打折扣。一般建议至少保留1个副本随时待命。还有一个省钱思路:非关键服务的蓝绿发布可以复用同一批节点的空闲资源,只要节点的Requests总和没有超过节点容量就不会产生额外费用。Kubernetes调度器按Requests来分配资源,你的Blue+Green同时跑在一台4核的虚拟机上,只要总Requests低于4核,就都能跑。当然Pod实际使用的CPU可能超过Requests,所以生产环境还是要有监控兜底,别超卖得太狠。5. 把蓝绿发布自动化,形成可复用的发布管线5.1 一键发布脚本设计手动操作做一两次可以,但每次发布都要敲一堆kubectl命令,存在两个问题:一是容易出错,二是没法留痕。所以我把整个流程封装成了一个发布脚本,核心逻辑就是几个阶段函数。脚本的大致结构是这样的:#!/bin/bash set -euo pipefail NAMESPACE="app-production" APP_NAME="my-app" NEW_VERSION="${1:-v1.9.0}" OLD_VERSION="${2:-v1.8.0}" pre_deploy() { echo ">>> 部署新版本(绿色环境)" kubectl apply -f "green-deployment-${NEW_VERSION}.yaml" kubectl rollout status deployment/my-app-green -n ${NAMESPACE} --timeout=180s } switch_traffic() { echo ">>> 切换流量到绿色环境" kubectl patch svc ${APP_NAME}-svc -n ${NAMESPACE} \ -p "{\"spec\":{\"selector\":{\"app\":\"${APP_NAME}\",\"track\":\"green\"}}}" echo ">>> 等待 Endpoints 更新" sleep 5 kubectl get endpoints ${APP_NAME}-svc -n ${NAMESPACE} } rollback() { echo ">>> 回滚到蓝色环境" kubectl patch svc ${APP_NAME}-svc -n ${NAMESPACE} \ -p "{\"spec\":{\"selector\":{\"app\":\"${APP_NAME}\",\"track\":\"blue\"}}}" } case "${1:-}" in pre) pre_deploy ;; switch) switch_traffic ;; rollback) rollback ;; *) echo "用法: $0 {pre|switch|rollback}" ;; esac这套脚本的指导思想是“一个动作、一条命令、一秒钟切回”。pre阶段只部署新环境,不影响线上流量;switch阶段才真正把用户流量切到新环境;rollback阶段一键回到旧环境。每个阶段都有明确的输出,日志里会记录发布操作的时间戳和执行的patch命令,发布完还能把输出归档成文件,作为审计凭据。脚本里有一个细节值得注意:switch之后sleep 5秒,让Endpoint Controller有时间更新Pod列表。这个等待时间不是随便定的,它取决于你的集群规模,小集群几秒即可,大集群可能得10秒以上。等Endpoints确认更新完毕,再继续后续操作。如果你在切换后立刻做压力测试,可能测到的是还没切换完成的中间状态,容易误判。5.2 与CI/CD流水线集成发布脚本只是最底层的手动操作,真正顺畅的用法是接进CI/CD流水线。以常见的GitLab CI或者GitHub Actions为例,阶段划分大概是:构建阶段:代码合并到主干后自动构建镜像,打上版本号tag并推送到镜像仓库预发布阶段:调用pre_deploy,部署绿色环境,但此时流量还在蓝色验证阶段:跑自动化冒烟测试,执行关键业务链路的用例(登录、下单、支付、查询),还可以用爬虫打一个小流量到绿色环境的临时Service上切换阶段:人工确认或自动执行switch_traffic稳定观察阶段:持续15-30分钟后,监控指标没有异常再往下走清理阶段:缩容蓝色环境每一步的准入条件一定要卡死。比如切换到绿色环境之前,自动化测试必须全部通过,冒烟测试覆盖率不能低于某个阈值,不然任何人都能跳过验证直接发布。这套流程跑顺之后,发布这个动作就从“高危操作”变成了“例行操作”,开发测试同学都能执行,运维只需要在处理异常情况的时候介入。5.3 蓝绿发布的进阶玩法:与灰度结合讲到这里,有人会问:蓝绿和灰度只能二选一吗?其实可以结合。比如电商大促前,核心交易链路适合蓝绿发布,确保整体稳定;但预言一个新功能的时候,更适合灰度发布,看用户反馈再放量。可以在同一套基础设施里,给核心服务配置蓝绿发布,给非核心服务配置Ingress的canary规则做灰度,这样既保证了核心链路的稳定性,又保留了产品验证的灵活性。还有一种组合是“金丝雀蓝绿”,做法是:绿色环境部署好但先不切全量,在Ingress层加一条临时规则,把内部测试人员的IP段或者某个Header(比如X-Canary: true)转发到绿色环境,其他流量继续走蓝色。这种方式本质上是用灰度发布的方式验证蓝绿环境,验证通过后再整切。很多团队愿意用这个方式,因为它既保留了蓝绿的大规模回滚能力,又解决了蓝绿完全按全量发布缺少“最后一公里”验证的问题。实现方式很简单:在Ingress上多加一条path规则,优先级高于现有规则,指定特定Header的请求转发到绿色环境对应的一个临时Service。这条规则用完之后删除即可。这套玩法基础设施成本不增加多少,但运维操作的复杂度确实上来了,所以建议团队磨合稳定了再上。6. 写在最后:我的几点体会蓝绿发布做久了,你会发现它本质上不是技术问题,而是管理和决策问题。技术层面的切换和回滚都很快,难的是你团队有没有底气在发现问题的那一刻果断按回滚键。很多人切换完成后看着监控曲线不对劲,第一反应是再等等看会不会恢复——这个“等等看”的窗口期,往往就是故障扩大化的根源。我的做法是事先写清楚切换后哪些指标属于“立即回滚红线”(比如错误率超过1%、P99延迟翻倍、数据库慢查询明显上升),只要触发红线,不做任何犹豫,直接回滚。这样整个团队有章法可循,发布负责人不需要临场做情绪化决策。另外,蓝绿发布并不适合所有服务。如果公司初创期,服务才刚刚上线两周,用户量极少,直接改代码重启就行,蓝绿这套流程反而是负担。它的价值在服务发展稳定、上线风险成本大于资源成本之后才凸显出来。这个判断标准建议作为决策的起点,而不是别人用你也用。最后分享一个小技巧:发布过程中把操作命令的输出去重定向到文件里保存,甚至可以每天发一次定时任务,把当前Service的selector状态输出成报告存档。这样一旦出问题,回溯是谁在什么时间点改了哪些配置,清清楚楚,扯皮的时候也有了事实依据。希望这篇实战经验能帮到那些正准备引入蓝绿发布,或者已经在蓝绿发布里折腾得焦头烂额的同行。