没配 startupProbe,为什么慢启动会被当成启动失败?
慢启动最像假故障的时候,不在于报错文案多像失败,而在于日志还在往前走,探针已经开始连续判死。把启动日志、Pod 事件和 probe 介入时间对齐,才知道这是启动链真断了,还是平台在应用快起来前先动了手。
最像“应用自己起不来”的慢启动现场,往往长这样:
- Pod 一直在重启。
- 应用日志每次都停在差不多的位置。
- 你看着它就差一点要起来,平台却总在前面把它重拉一次。
有一次新实例发布,最容易误导人的不是错误日志,而是这组时间:
| 时间 | Pod / Probe 事件 | 应用日志 | 当时该怎么理解 |
|---|---|---|---|
| 10:14:02 | 容器启动 | Spring Boot banner 输出 | 进程正常拉起 |
| 10:14:18 | readiness 开始失败 | Bean 初始化进行中 | 还没 ready,不奇怪 |
| 10:14:41 | liveness 第一次失败 | 远程配置拉取仍在重试 | 平台开始把启动期当运行期判断 |
| 10:14:56 | liveness 连续失败达阈值 | Hibernate 初始化刚结束 | 应用还在推进,不像彻底卡死 |
| 10:14:58 | Pod 被重启 | 没来得及打印 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 真正保护的是哪一段时间
它保护的不是“所有启动慢都合理”,而是给下面这段时间一个单独语义:
应用还在合法启动中,此时不能拿运行期的存活标准去处置它。
所以我更关心的是这条时间链:
- 进程启动。
- 应用日志持续推进。
- 关键 Bean、外部依赖、预热动作陆续完成。
- 应用第一次真正 ready。
- 这之后才轮到 liveness / readiness 按运行期标准接管。
如果 2 到 4 之间经常超过当前 probe 窗口,而应用又能最终起来,那 startupProbe 就不是“可有可无的优化项”,而是启动期和运行期之间的隔离带。
这时候先补 startupProbe,还是先砍启动链
两件事通常都要做,但优先级不一样。
| 现场长相 | 更该先做什么 | 为什么 |
|---|---|---|
| 日志一直推进,放宽窗口就能起来 | 先补 startupProbe / 启动保护窗口 | 先避免平台误杀,保住发布和扩容 |
| 日志长期卡在某个初始化点 | 先拆启动链根因 | 只是放宽窗口,问题还在 |
| 首次 ready 需要 2 到 3 分钟,但业务确实允许慢热 | 先给启动期独立语义,再考虑减负 | 启动慢未必等于失败 |
| 连老实例重启都越来越慢 | 优先治理启动负担 | 这已经不是 probe 单点问题 |
也就是说,startupProbe 解决的是“别把慢启动误判成失败”,不是“让启动自动变快”。
现场里最有效的一次验证,不是调一串数字
我更愿意做一次对照实验:
- 保持镜像和配置不变。
- 单独给一台实例补上合理的 startupProbe。
- 把 Pod 事件、probe 日志、应用启动日志一起记录下来。
如果结果是:
- 不再重启。
- 日志顺利推进到
Started Application。 - readiness 在稍后正常转绿。
那就能很清楚地证明:之前的“启动失败”,本质上是启动保护窗口缺失。
再往下,才轮到你继续优化启动链本身,比如减初始化负担、去掉启动期远程调用、把预热从阻塞启动改成后台渐进完成。
哪些信号说明你不该只留在这篇里
- 应用已经起来了,但流量切换、readiness、优雅下线总是错位,去看Spring Boot 健康检查、就绪探针和优雅下线为什么经常让服务状态看起来不对?。
- 你已经明确卡在 Bean 初始化链,去看 Bean 初始化专题会更快。
- 只有部分新实例起不来,老实例一直正常,更像发布后实例差异问题,适合切去实例对比那条线。
慢启动最容易被误判,不是因为告警写得太吓人,而是因为平台和应用对“现在到底算启动中,还是已经失败”这件事根本没说同一种语言。
把日志推进、probe 失败时刻和 Pod 重启时刻对齐之后,很多看起来像启动失败的现场,其实只是缺了一段本该存在的启动保护窗口。