Java

没配 startupProbe,为什么慢启动会被当成启动失败?

慢启动最像假故障的时候,不在于报错文案多像失败,而在于日志还在往前走,探针已经开始连续判死。把启动日志、Pod 事件和 probe 介入时间对齐,才知道这是启动链真断了,还是平台在应用快起来前先动了手。

  • Spring Boot
  • startupProbe
  • Readiness
  • 启动慢
  • 故障排查
18 分钟阅读

最像“应用自己起不来”的慢启动现场,往往长这样:

  • Pod 一直在重启。
  • 应用日志每次都停在差不多的位置。
  • 你看着它就差一点要起来,平台却总在前面把它重拉一次。

有一次新实例发布,最容易误导人的不是错误日志,而是这组时间:

时间Pod / Probe 事件应用日志当时该怎么理解
10:14:02容器启动Spring Boot banner 输出进程正常拉起
10:14:18readiness 开始失败Bean 初始化进行中还没 ready,不奇怪
10:14:41liveness 第一次失败远程配置拉取仍在重试平台开始把启动期当运行期判断
10:14:56liveness 连续失败达阈值Hibernate 初始化刚结束应用还在推进,不像彻底卡死
10:14:58Pod 被重启没来得及打印 Started Application典型“快起来前先被判死”
10:15:03下一轮启动开始日志又从头来现场看上去像反复启动失败

如果只看“Pod 被重启”这件事,很容易下结论说应用启动失败了。可把日志推进和 probe 介入窗口摆在一起看,结论常常会反过来:应用不是没在启动,而是还没拿到启动期的独立保护窗口。

先判断是不是“慢,但一直在往前走”

我一般不会先改配置,而是先回答一个更基础的问题:

这次到底是启动链真的断了,还是应用虽然慢,但一直在往前推进?

更像“慢,但还在推进”的信号通常有这些:

  • 每次重启前,日志位置都比上一秒更靠后,而不是卡在完全同一行。
  • 没有明确的 fatal error、OutOfMemoryError、端口绑定失败、配置缺失这类硬错误。
  • 同一镜像在更宽松的 probe 窗口下能够正常起来。
  • 应用距离 Started Application 只差几十秒,却总在这个窗口前被重启。

相反,如果你看到的是:

  • 日志每次都卡死在同一个 Bean 创建点。
  • 某个外部依赖调用永远超时,没有继续推进。
  • 不管探针怎么放宽,应用都起不来。

那就别把问题全推给 startupProbe 了,启动链本身确实已经断了。

启动慢,最常慢在哪几段

没有 startupProbe 的危险,在于平台会拿运行期标准去解释启动期。但真正耗时的段落,仍然值得拆清。

Bean 初始化链太重

这类服务会把大量工作压在启动路径里:

  • 扫描和装配过多。
  • 大量 Bean 在 @PostConstruct 或初始化回调里做重活。
  • 启动时就做全量预热、全量字典加载、规则编译。

它的特征是日志一直在推进,但推进得慢,而且常常集中在某几个初始化阶段。

外部依赖等待太长

另一类慢启动,时间不是花在本地,而是花在启动时就去碰外部资源:

  • 配置中心。
  • 数据库迁移或连接验证。
  • Redis、MQ、对象存储、第三方鉴权。
  • 启动时就发起远程探测或缓存预热。

这类问题最容易在没有 startupProbe 时被误杀,因为平台并不知道你现在只是“还在启动”,它只看见一个迟迟没通过探针的进程。

端口可探测,业务还没 ready

有些服务更隐蔽:Tomcat 端口已经起来,HTTP 也能回,但真正的业务前置资源还没准备好。

这时如果 liveness / readiness 语义没分清,就会出现:

  • 端口通了,于是平台以为应用已经活得很好。
  • 业务探针又还没准备好,于是开始反复失败。
  • 启动阶段被运行期判断搅乱,看起来像“偶发性启动失败”。

startupProbe 真正保护的是哪一段时间

它保护的不是“所有启动慢都合理”,而是给下面这段时间一个单独语义:

应用还在合法启动中,此时不能拿运行期的存活标准去处置它。

所以我更关心的是这条时间链:

  1. 进程启动。
  2. 应用日志持续推进。
  3. 关键 Bean、外部依赖、预热动作陆续完成。
  4. 应用第一次真正 ready。
  5. 这之后才轮到 liveness / readiness 按运行期标准接管。

如果 2 到 4 之间经常超过当前 probe 窗口,而应用又能最终起来,那 startupProbe 就不是“可有可无的优化项”,而是启动期和运行期之间的隔离带。

这时候先补 startupProbe,还是先砍启动链

两件事通常都要做,但优先级不一样。

现场长相更该先做什么为什么
日志一直推进,放宽窗口就能起来先补 startupProbe / 启动保护窗口先避免平台误杀,保住发布和扩容
日志长期卡在某个初始化点先拆启动链根因只是放宽窗口,问题还在
首次 ready 需要 2 到 3 分钟,但业务确实允许慢热先给启动期独立语义,再考虑减负启动慢未必等于失败
连老实例重启都越来越慢优先治理启动负担这已经不是 probe 单点问题

也就是说,startupProbe 解决的是“别把慢启动误判成失败”,不是“让启动自动变快”。

现场里最有效的一次验证,不是调一串数字

我更愿意做一次对照实验:

  • 保持镜像和配置不变。
  • 单独给一台实例补上合理的 startupProbe。
  • 把 Pod 事件、probe 日志、应用启动日志一起记录下来。

如果结果是:

  • 不再重启。
  • 日志顺利推进到 Started Application
  • readiness 在稍后正常转绿。

那就能很清楚地证明:之前的“启动失败”,本质上是启动保护窗口缺失。

再往下,才轮到你继续优化启动链本身,比如减初始化负担、去掉启动期远程调用、把预热从阻塞启动改成后台渐进完成。

哪些信号说明你不该只留在这篇里

慢启动最容易被误判,不是因为告警写得太吓人,而是因为平台和应用对“现在到底算启动中,还是已经失败”这件事根本没说同一种语言。

把日志推进、probe 失败时刻和 Pod 重启时刻对齐之后,很多看起来像启动失败的现场,其实只是缺了一段本该存在的启动保护窗口。