ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java应用零停机发布实战:Azure部署槽与蓝绿切换原理详解

Java应用零停机发布实战:Azure部署槽与蓝绿切换原理详解 你有没有经历过这种时刻周五晚上八点业务群里突然冒出一条告警说下单接口超时率在涨你第一反应不是看日志而是看手里那个用了半年的发布脚本——停止服务、备份、拉新包、再启动整套流程下来三十分钟起步。我过去就是这种状态每次升级Java服务都像做一次小手术。后来我把发布流程搬到Azure上用部署槽Deployment Slot做蓝绿切换配合健康检查和JVM预热一次升级的停摆时间从三十多分钟被压到了三秒左右流量切过去了用户几乎无感。这套玩法不是什么只有大厂才能用的“黑科技”Azure App Service本身自带这套机制只要你的Java应用肯配合绝大多数Spring Boot、Tomcat、Java SE项目都能改过来。这篇内容不是给架构师看的高深理论而是给每天和Java、Tomcat、JVM内存较劲的工程师准备的实操记录。不管是做后端开发、负责应用上云还是单纯想在简历上写一笔“零停机发布”都可以照着这个思路走。我会把整个改造背后的原理、具体操作步骤、Java侧需要做的配置以及我在实践里踩过的坑全部拆开讲一遍。先算清楚账再来谈怎么把那三十分钟变成三秒。1. 先算一笔账30分钟的停机到底贵在哪1.1 手动重启一次Java应用时间都花在哪了很多人觉得“重启一次三十分钟太夸张了实际也就两三分钟”。如果你只是用一个单机小服务确实可能只需要两分钟。但如果你面对的是一个正式的Java Web应用有多个实例、数据库连接池、Redis缓存、消息队列消费端情况完全不同。手动升级的真实流程往往是这样的先拉取新版本代码在本地或者构建机上打包这步快则几分钟慢则十几分钟然后连上服务器把旧进程停掉这一步一般几秒钟接着替换文件、启动新进程JVM要完成类加载、Spring上下文初始化、数据源和Redis连接池建立、各种Filter和AOP Bean的装配这个阶段通常要耗费几十秒到几分钟你以为进程起来就完事了不对JIT编译器还没有把热点代码编译成机器码这时候突然灌进来的请求全都在解释执行模式下运行接口耗时可能比正常高好几倍。再加上连接池初始化时对数据库的连接压力整个系统需要一段时间才能恢复到正常水位。所以三十分钟其实算是保守估计。如果中间遇到配置不对、包损坏、启动时端口被占用再排查一遍一小时就没了。这段时间里用户访问全部失败前端要么转圈要么直接报错。对于电商、金融、办公类系统来说每一次这样的操作都是在透支用户信任。1.2 为什么说“3秒”才是值得追求的指标三秒切完本质上并不是所有实例必须在三秒内完成启动而是流量从旧版本迁移到新版本的这个动作只需要三秒。新版本早在用户看得到之前就已经在另一个环境里启动好了跑过健康检查了连热门接口都预热过了。切流的时候基础设施层只是把路由指针从旧版本指到新版本用户不会感知到进程重启也不会感受到连接中断。理想情况下的指标就是发布期间请求成功率不掉、P99延迟不涨、业务日志不出现断档。如果只看停机时长从三十分钟变成三秒已经是两个物种的区别。更重要的是这种模式把“发布”从一个高风险动作变成了一个可逆动作。发现问题后你可以立刻把流量再切回去整个过程同样只需三秒。手动重启能给你这种安全感吗很难。2. 无感升级的底层机制部署槽与蓝绿切换2.1 部署槽的本质同一套代码两个独立的生命周期Azure App Service的部署槽从概念上讲就是同一个App Service下面的另一个独立运行环境。每一个槽位都有自己的URL、自己的应用代码、自己的Redis或者数据库连接配置也可以有自己的实例配置。生产环境和staging环境节点上跑的代码可以不一样但是对外暴露的服务名是同一个正式域名始终指向生产槽位。这个设计为什么适合做无感升级因为它提供的是一个“先准备好再切流量”的蓝绿部署模型。传统发布是“停机换版本”新老版本必须前后接力蓝绿发布是同时运行两套版本流量按需切换。新版本在staging槽里跑了一段时间验证没问题再把路由切到新槽。整个过程不碰旧实例旧代码一直在原地待命。一旦新版本有问题交换的动作倒过来再做一次旧代码就回滚回去了。创建槽位的成本很低它和主应用共享同一个App Service Plan的计算资源但拥有独立的主机名和应用设置。我喜欢拿“备用引擎”来类比飞机不止一个引擎不是为了好看而是为了其中一个引擎出问题时整个系统还能继续飞。部署槽就是生产应用的那个备用引擎平时你可以在备用引擎上折腾正式引擎不受影响。2.2 流量交换的瞬间不是重启而是切换路由部署槽的交换操作是把源槽和目标槽的应用内容、配置项、连接字符串等做一次互换。这一步不是停进程也不是重新启动生产环境而是由平台层完成配置和路由的交接所以速度快用户端不会有明显的连接重置。但这里有个关键前提目标槽必须已经准备好。如果你直接用一个刚部署完、JVM还没热起来、Spring Boot还没完全启动好的staging槽去交换即使流量切换只用了三秒新版本自己也没能力接住流量。所以Azure在交换之前会向目标槽发预热请求等待目标槽返回健康状态后才会真正执行交换。这个过程你可以理解为一次“考勤检查”——你先把新员工带进来体检体检合格后再让他正式上岗体检不过就留在外面不影响原有员工干活。这也是为什么Java应用需要额外配合的原因。JVM的冷启动问题不会因为槽位机制而自动消失它只会从你的发布时间段被转移到交换之前的预热期。把启动慢、GC暂停、首次请求超时这些问题在预热期消化掉才能真正做到切换之后的“无感”。2.3 什么时候适合用部署槽什么时候别硬上部署槽适合的场景是应用可以同时存在两个版本且两个版本都能兼容当前数据结构和第三方依赖。比如普通业务接口升级、前端资源更新、依赖库升级这种场景新老版本可以共存部署槽非常好用。不适合的场景是数据库结构不向后兼容的发布。比如新版要清理一张大表里的某个字段旧代码如果继续运行就会报错。这种情况下即使流量切了上一秒还在跑的旧代码也会因为Schema不匹配出现大量异常。遇到这种改动需要把数据库迁移设计成“先加字段、后删字段、双写兼容”的渐进模式或者干脆把发布窗口还给业务先停机做数据变更再切流。部署槽解决的是流量切换问题不是所有发布问题。3. Java应用侧要做的四件事向JVM要“随时可切换”3.1 Java的冷启动陷阱为什么“新版本健康一接流量就抖动”有过经历的人都知道Java应用启动完成不代表它已经准备好接受所有流量。Spring Boot启动日志打印出“Started Application in 12.5 seconds”这只说明ApplicationContext装配好了Tomcat端口已经监听但类加载器里还有很多Class没有被加载JIT编译器还没有把热点方法编译成机器码MyBatis的Mapper代理类可能还在懒加载状态HikariCP连接池可能只建立了最小连接数。这时候如果一大波真实流量涌进来会发生什么每个请求都会触发新的类加载甚至触发同步锁竞争解释执行的速度比编译执行慢一个量级数据库连接池被瞬间打满线程池开始排队。用户看到的接口响应时间会比正常情况高出几倍严重的直接超时。所以无感升级不只是“切得快”更是“切之前已经热好身了”。3.2 健康检查别只查“进程活着”Azure App Service自带的健康检查功能会每隔三十秒向指定路径发起请求。如果连续多次失败它会把对应实例从负载均衡轮转里摘除。在交换操作之前Azure也会用这个健康检查判断目标槽是否就绪。很多Java项目做的所谓健康检查就是Spring Boot默认的/actuator/health而这个端点经常只返回内存和磁盘状态根本不检查数据库连接是否可用、Redis是否通、消息队列是否还连着。结果就是健康检查显示UP实际接流量时数据库连不上一交换就事故。我建议把健康检查路径设置成一个独立的内部接口比如/internal/health并且在这个接口里做三类验证一是数据库连接是否真实可用执行一条SELECT 1二是Redis连接是否可响应PING三是关键的外部依赖是否就绪比如配置中心、认证中心。注意检查操作不要做成高频率的真实查询避免把数据库打得太重可以加一个短超时比如每三十秒检查一次单次不超过两秒。换句话讲健康检查的语义应该是“这个实例现在能不能安全接流量”而不是“进程有没有活着”。3.3 预热手段让新槽在交换前已经“热”我见过很多团队用部署槽以后只等健康检查通过就交换结果接口还是狼藉一片。问题出在健康检查只验证“服务能响应请求”没有让JVM把真正的热点事务跑一遍。预热需要针对你自己的业务接口来做。最简单的办法是在启动完成后用一段内部预热逻辑调用自己几个核心接口比如高频的订单查询接口、商品列表接口、登录校验接口。这样Spring MVC的路由映射、拦截器、AOP代理、MyBatis的SQL映射、JIT编译都会在用户流量到来之前完成初始化。在Azure侧还有一个非常实用的配置预热路径。Azure交换操作会向目标槽的根路径发一个预热请求默认使用的是根路径/你可以通过应用设置把它指到自己的一个轻量接口。我习惯于在staging槽里设置一条WEBSITE_SWAP_WARMUP_PING_PATH/internal/warmup这个接口返回HTTP 200就行真正的预热逻辑放在应用启动阶段做。这样交换前Azure会帮我们“敲一敲门”确保应用已经可以对外响应。3.4 JVM与连接池参数给切换加最后一道保险Java应用在部署槽模式下最怕的是启动阶段内存不够或者连接池初始化超时。我通常会在Java应用里配置这么几个参数堆内存大小建议用-XX:MaxRAMPercentage70.0让JVM根据容器内存自动限制堆大小而不是写死一个-Xmx。App Service的套餐内存可能调整写死容易出岔子。GC用-XX:UseG1GC大多数Java服务用这个都比CMS和Parallel靠谱停顿时间更平滑。连接池把minimumIdle和maximumPoolSize设置成合理值并在应用启动时建立好核心连接。HikariCP默认的initializationFailTimeout可能让启动时数据库抖动直接失败你可以根据场景调整超时和重试策略。如果追求启动速度可以临时用-XX:TieredStopAtLevel1只做C1编译启动会更快但长期峰值性能会下降。我不建议生产环境长期使用更适合在预热阶段配合验证。连接池参数方面HikariCP的maximumPoolSize不要盲目的设成几十上百很多时候20左右足够minimumIdle设成与maximumPoolSize一致可以让启动时把连接全部建好避免请求到来时再一个接一个地建连接。一句话让新实例在切换前把最贵的工作干完。4. 完整实操从手动重启到无感升级的改造步骤4.1 环境与清单先确认几件事动手之前先确认自己的App Service套餐等级。部署槽功能在Basic、Standard、Premium和Isolated计划中支持但Free和Shared是不支持的。如果你的应用还跑在低档套餐上第一步不是配槽位而是先把套餐升到Standard及以上否则后面全是白忙活。然后梳理应用的外部依赖数据库连接串、Redis连接串、消息队列地址、第三方服务的密钥。这些配置在实现自动切换之前必须先想清楚哪些是“跟随代码槽位走的”哪些是“永远指向固定环境”的。尤其是数据库和Redis这类敏感配置我建议全部标记为槽位设置不参与交换。也就是说无论生产槽还是staging槽它们各自保留自己的连接串不会因为交换而互相带跑。这一步没做好后面交换一次staging可能会把生产库连接串带到自己那边造成数据错乱。最后检查一下Java应用的健康检查接口是否已经准备好了。没有的话赶紧补一个轻量级的健康检查Endpoint这是后面所有自动化的基础。4.2 创建部署槽并部署新版本假设你的应用叫java-demo-app资源组叫rg-java-demo先在Shell里用Azure CLI创建一个叫staging的部署槽并指定从生产环境复制配置az webapp deployment slot create \ --resource-group rg-java-demo \ --name java-demo-app \ --slot staging \ --configuration-source java-demo-app创建完成后在Azure门户里找到这个App Service进入“部署槽”会看到staging这个槽位它有独立的URL类似java-demo-app-staging.azurewebsites.net。这个阶段可以放心地在上面部署新版本代码。部署JAR包时可以用下面的命令直接把本地构建好的JAR推上去az webapp deploy \ --resource-group rg-java-demo \ --name java-demo-app \ --slot staging \ --type jar \ --src-path target/demo-1.0.0.jar首次部署完成后访问staging的URL做一次冒烟测试确认新版本能正常启动、能连上它自己对应的数据库、页面能打开。注意此时生产环境完全没有动旧版本还在正常对外服务。4.3 锁定配置槽位设置与环境漂移控制这是整个部署槽改造中最容易翻车的一步。生产环境的数据库连接字符串和staging环境通常指向不同的数据库。如果你不做任何设置交换操作会把两个槽位的所有配置互换新代码可能带着staging的连接串跑到生产直接连错库。所以要让关键配置变成“槽位设置”意思就是无论怎么交换这套配置始终留在它所属的槽位里。在Azure门户中进入App Service的“配置”页面找到对应的应用设置或连接字符串勾选“部署槽设置”选项。保存之后这条配置就不会在交换时被带走了。这个操作解决了环境隔离问题。生产槽的数据库连接串永远指向生产库staging槽的数据路连接串永远指向测试库。两者各自独立交换的时候代码可以互换但配置按槽位固定。4.4 配置健康检查与自动交换在Azure门户的App Service“健康检查”页面打开健康检查开关路径填/internal/health失败重试次数建议设置成3-5次。这个健康检查的作用有两个平时保证实例不健康的节点不接收流量交换时保证新版本就绪后再切流。接下来设置自动交换。进入staging部署槽找到“配置”页面里的“自动交换”选项把自动交换目标设为生产。这样以后我们往staging部署完成且健康检查通过后Azure会自动完成交换。这个设置特别适合CD流水线但我个人建议第一次改造时不要直接开自动交换而是手动执行几次交换确认整个流程稳定后再交给自动化。4.5 正式切换流量交换与验收不带自动交换的初始阶段手动交换也很简单。在部署槽页面点“交换”或者用CLIaz webapp deployment slot swap \ --resource-group rg-java-demo \ --name java-demo-app \ --slot staging \ --target-slot production执行之后你会发现本质上是两个槽位互换身份。staging里现在跑的是旧的生产代码生产环境则变成了新版本。因为新版本已经提前启动并预热过交换过程一般不会造成请求断流。交换完成后立刻去看三样东西生产环境的请求成功率、P99延迟曲线、应用日志。如果三个指标都平稳发布就算成功。此时staging里保留的还是旧代码万一新版本有隐藏问题立刻再执行一次上面的命令旧版本就能在两三秒内回到生产。4.6 回滚演练给“手滑”留后路很多团队做完部署槽切换从没真正演练过回滚。我在实践里吃过亏新版本切换后突然出现一个线上问题需要马上回滚结果因为我们改了某个数据库字段旧代码根本跑不动旧库最后硬着头皮原地修了三小时。所以回滚演练这件事必须做。在非高峰期故意部署一个有问题的版本到staging执行交换然后立刻再交换回来全程观察生产可用性。回滚操作本身很快但前提是数据库Schema和外部依赖对两个版本的代码都兼容。这也是为什么我在第一节就强调部署槽解决流量切换数据库设计必须配合。建议每周做一次回滚演练让团队形成肌肉记忆真出事的时候不会慌。5. 实践半年后我最想提醒的四个坑5.1 健康检查接口写得太“敷衍”会误伤发布我见过一个团队健康检查接口只返回ok字符串什么依赖都不查。结果发布时新版本启动成功健康检查通过自动交换发生生产流量打进来才发现Redis没连上接口大面积超时。这个问题的根因就是健康检查没有和真实的依赖链绑定。建议健康检查必须覆盖核心依赖但同时也要控制检查频率和超时。如果每三秒查一次数据库发布时数据库本身就有压力健康检查又成了数据库的压力源。把超时控制在两秒内检查频率至少控制在十五秒以上并且把结果做缓存比如三十秒内只执行一次真实检查后续直接返回缓存状态。5.2 数据库迁移与新旧代码的兼容期这是部署槽模式最大的隐藏风险点。很多Java后端升级时喜欢顺带改数据库表结构比如新增字段、修改索引、删除列。如果你的新代码需要新字段才能跑通旧代码在交换后还保留在staging里并且随时可能被回滚那么数据库的改动就要兼顾两个版本。最稳妥的做法是采用增量兼容模式发布新代码之前先把数据库升级到新旧代码都能工作的状态新版本运行稳定后再执行数据清理和旧字段下线。整个过程拆成两个发布窗口而不是把代码和表结构绑在一起一次完成。这会让发布周期变长但能避免大量线上故障。5.3 用户会话和长连接3秒切换后的那点“粘滞”部署槽交换本身很快但如果你依赖本地会话保存登录状态比如Tomcat的HttpSession存在内存里切换之后用户会重新登录体感上就不是“无感升级”了。要真正做到零感知最简单的方案是把会话存储外置到Redis统一管理登录态和临时状态。WebSocket和长连接是另一个坑交换操作会让旧连接断开新版后端需要能接受客户端重连。如果业务对实时性要求高最好引入消息推送网关或至少做好客户端的自动重连机制。不要天真地以为所有场景都可以做到真正的无缝。5.4 小规格套餐的隐藏成本部署槽不是免费送的部署槽和应用共享同一个App Service Plan的计算资源。如果你的套餐只有一个实例生产环境和staging环境会挤在同一台VM上内存和CPU相互争抢。原本生产还能勉强跑加了staging之后反而更卡了。建议改造之前先把App Service Plan至少提升到Standard S2及以上或者把实例数至少开到2。这一点往往会被忽略因为创建槽位本身不额外收费但运行槽位是会消耗计算资源的。如果想省钱可以只在发布窗口期启动staging发布完成后把staging停掉成本会下降很多但这意味着自动交换和管理便利性会受到限制需要你自己权衡。5.5 常见问题排查速查表现象可能原因处理方案交换时提示目标槽不健康健康检查接口路径配置错误或接口确实检测到依赖不可用先查看staging应用日志用curl访问/internal/health确认返回200后再重试交换后生产连错数据库连接字符串没有标记为槽位设置进入配置页面将关键的连接字符串勾选为“部署槽设置”交换后CPU瞬间飙升新实例没有充分预热JIT和连接池在硬扛流量增加启动预热逻辑设置预热路径让staging在交换前接收一轮模拟请求回滚后旧版本报错数据库Schema已经变更旧代码无法适配数据库迁移采用兼容模式确保新老版本在切换窗口内共存自动交换没有触发目标槽健康检查失败或自动交换配置指向错误查看部署槽配置确认自动交换目标为production检查健康检查日志实例内存不足App Service Plan规格偏小两个槽位互相挤压临时升级套餐规格或发布后再创建/停止staging槽这半年实践下来我的体会是Azure把无感升级这套基础设施做得已经很成熟了真正决定成败的反而是Java应用自己的准备度。部署槽给了我们一个“可以随时反悔”的安全岛但能不能做到三秒切换后用户毫无感觉靠的还是健康检查是否真实、预热是否到位、数据库是否兼容、配置是否锁定。别急着把所有服务都一次性切换过来先拿一个非核心的Java服务练手把staging槽、健康检查、预热、回滚这几条链路彻底跑通再逐步推广。这套流程跑顺了以后你会明显感觉到发布这件事从“高危手术”变成了“日常操作”心态完全不一样。
返回列表