ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java应用零停机迁移实战:优雅停机与流量切换

Java应用零停机迁移实战:优雅停机与流量切换

1. 从玄学到科学:解密Java应用零停机迁移的本质

很多人把"热迁移"视为某种黑魔法,认为这是只有云服务商才能实现的底层能力。但经过三次深夜实战和无数次回滚后,我要告诉你:应用层的零停机迁移完全是可以被掌控的工程实践。关键在于理解三个核心概念:

  1. 优雅停机(Graceful Shutdown):不是直接杀死进程,而是让应用完成现有请求、拒绝新请求、释放资源后再退出。就像餐厅打烊时,会让已入座的客人吃完,但不再接待新客人。

  2. 流量切换(Traffic Switching):负载均衡器能够精确控制流量分发,确保只有健康实例接收请求。类似于机场值机柜台,只开放准备就绪的窗口。

  3. 部署策略(Deployment Strategy):云平台提供的蓝绿部署、滚动更新等机制,本质是控制新旧版本交替的节奏。如同剧院换场,新布景准备就绪前旧布景保持演出。

2. Spring Boot优雅停机深度改造

2.1 配置层面的基础设置

在application-prod.yml中,这些配置项缺一不可:

server: shutdown: graceful # 启用优雅停机 spring: lifecycle: timeout-per-shutdown-phase: 30s # 最大等待时间 management: endpoint: health: show-details: always endpoints: web: exposure: include: health,info health: livenessstate: enabled: true readinessstate: enabled: true

警告:timeout-per-shutdown-phase必须根据业务峰值时的请求处理时间设定。我们电商系统设置为30秒,是因为历史数据显示99%的订单支付请求能在25秒内完成。

2.2 代码层面的连接管理

仅仅配置是不够的,还需要在代码中处理各种资源释放。这是我们的核心改造点:

@Slf4j @Component public class GracefulShutdown implements ApplicationListener<ContextClosedEvent> { @Autowired private DataSource dataSource; @Autowired private RedisConnectionFactory redisConnectionFactory; @Override public void onApplicationEvent(ContextClosedEvent event) { log.info("Starting graceful shutdown..."); // 1. 关闭数据库连接池 if (dataSource instanceof HikariDataSource) { ((HikariDataSource) dataSource).close(); log.info("Hikari connection pool closed"); } // 2. 释放Redis连接 if (redisConnectionFactory instanceof LettuceConnectionFactory) { ((LettuceConnectionFactory) redisConnectionFactory).destroy(); log.info("Lettuce connections released"); } // 3. 等待异步任务完成 awaitAsyncTasksCompletion(); } private void awaitAsyncTasksCompletion() { // 具体实现省略... } }

2.3 线程池的特殊处理

最容易忽视的是异步线程池的关闭。我们在生产环境踩过的坑:

@Bean(destroyMethod = "shutdown") public ExecutorService asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setWaitForTasksToCompleteOnShutdown(true); // 关键! executor.setAwaitTerminationSeconds(60); // 关键! executor.setThreadNamePrefix("Async-"); executor.initialize(); return executor.getThreadPoolExecutor(); }

血泪教训:如果没有设置waitForTasksToCompleteOnShutdown,正在处理的异步任务会直接被中断,我们因此丢失过一批订单数据。

3. Azure部署槽交换实战

3.1 部署槽配置要点

在Azure门户配置部署槽时,这些设置直接影响迁移成功率:

  1. 连接字符串:必须确保生产槽和暂存槽使用相同的连接字符串配置
  2. 应用设置:除环境特定变量外,其他设置应与生产环境一致
  3. 自动交换:首次部署建议禁用,手动验证后再启用

3.2 交换前检查清单

这是我们团队内部使用的检查脚本(简化版):

# 检查新版本健康状态 $health = Invoke-RestMethod -Uri "https://staging-slot.azurewebsites.net/actuator/health" if ($health.status -ne "UP") { throw "Staging slot is not healthy" } # 验证数据库连接 $dbCheck = Invoke-RestMethod -Uri "https://staging-slot.azurewebsites.net/actuator/health/db" if ($dbCheck.status -ne "UP") { throw "Database connection check failed" } # 检查待处理消息 $queueDepth = (Invoke-RestMethod -Uri "https://staging-slot.azurewebsites.net/metrics/queue").depth if ($queueDepth -gt 100) { Write-Warning "High queue depth detected: $queueDepth" }

3.3 交换后的回滚保护

在交换后30分钟内自动运行的回滚检查脚本:

$startTime = Get-Date $timeout = New-TimeSpan -Minutes 30 $success = $false while ((Get-Date) - $startTime -lt $timeout) { $errorRate = (Invoke-RestMethod -Uri "https://production-slot.azurewebsites.net/metrics/errors").rate if ($errorRate -gt 0.05) { # 错误率超过5% Write-Host "Error rate too high ($errorRate), initiating rollback..." az webapp deployment slot swap --name MyApp --resource-group MyRG --slot staging --action swap --target-slot production $success = $false break } Start-Sleep -Seconds 60 $success = $true } if ($success) { Write-Host "Swap validated successfully" }

4. AKS滚动更新参数详解

4.1 部署清单关键配置

这是经过生产验证的AKS部署配置片段:

apiVersion: apps/v1 kind: Deployment metadata: name: order-service spec: replicas: 5 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # 最多允许比期望副本数多1个 maxUnavailable: 0 # 保证始终有可用实例 template: spec: containers: - name: order-service livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 30 # 避免启动时误杀 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 failureThreshold: 3 lifecycle: preStop: exec: command: ["sh", "-c", "sleep 30"] # 给优雅停机留出时间

4.2 参数选择背后的思考

  1. maxSurge与maxUnavailable的权衡:

    • 保守策略 (maxUnavailable=0, maxSurge=1):保证100%可用性,但更新速度慢
    • 激进策略 (maxUnavailable=1, maxSurge=0):更新更快,但可能有短暂不可用
  2. 探针配置的艺术:

    • livenessProbe的initialDelaySeconds必须大于应用启动时间
    • readinessProbe的检查频率要高于平均请求处理时间
  3. preStop hook的必要性:

    • 给Kubernetes发送SIGTERM后,留出时间让应用完成优雅停机
    • sleep时间应略大于server.shutdown.grace-period

5. 零停机迁移Checklist

5.1 迁移前验证清单

  1. [ ] 优雅停机测试:发送SIGTERM后验证:

    • 新请求被立即拒绝(HTTP 503)
    • 已有请求完成处理
    • 所有连接池关闭
    • 异步任务完成或持久化
  2. [ ] 健康检查验证:

    • 人为制造故障,验证livenessProbe能正确检测
    • 模拟高负载,验证readinessProbe能正确响应
  3. [ ] 部署槽同步测试:

    • 配置文件一致性检查
    • 数据库连��测试
    • 外部服务连通性测试

5.2 迁移时监控指标

  1. 关键指标看板:

    • 请求错误率(<0.1%)
    • 平均响应时间(波动<20%)
    • 活跃连接数(平稳下降)
    • 线程池活跃线程数
  2. 警报阈值设置:

    • 5分钟内错误数>10 → 警告
    • 错误率>1%持续2分钟 → 自动回滚
    • 平均响应时间>3倍基线 → 调查

6. 真实案例:电商大促期间的迁移

去年双11前,我们需要将支付服务迁移到新版AKS集群。这是我们的操作时间线:

  1. 准备阶段(T-7天):

    • 在 staging 环境完成全流程演练3次
    • 根据演练结果调整优雅停机超时为45秒
    • 预生成所有K8s部署清单并提交代码库
  2. 执行窗口(T-1天 凌晨2点):

    • 00:00 开始降低流量(通过限流器控制)
    • 01:30 启动滚动更新,maxUnavailable=0
    • 02:15 验证所有新pod就绪
    • 02:30 逐步放开流量限制
  3. 观察期(T+3天):

    • 保持旧集群运行但不接收流量
    • 实时监控新集群各项指标
    • 准备随时回滚的自动化脚本

最终结果:峰值QPS 12,000的支付服务,迁移期间零失败交易,用户完全无感知。监控显示最大延迟波动仅8%。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进