Java

Spring Boot 启动慢怎么排查?把启动、外部依赖和 readiness 拆开看

Spring Boot 一慢启动,最怕把所有现象都叫成“框架太重”。问题常常分成几段:端口没起来、端口起来了但服务还没 ready,或者本地和线上压根不是同一种慢法;把这几段拉开后,才更容易判断该查哪一层。

  • Spring Boot
  • 启动优化
  • Java
  • 性能排查
16 分钟阅读

Spring Boot 启动慢,在本地也许只是开发体验问题;放到扩容、滚动发布和故障恢复里,它影响的就是可用性窗口。

难点不在“慢”这个词,而在于三种情况经常被混着说:端口没监听、端口起来但 readiness 不过、服务看似启动完成却还接不住流量。先把慢点钉在具体阶段,后面的日志和指标才有解释力。

如果日志已经说明问题不在启动阶段,而是服务起来后接口慢、CPU 高或 GC 抖动,就直接转去运行时排查 接口响应慢怎么排查?后端性能问题定位步骤,或者接 JVM 分支 Java 服务 CPU 高怎么排查:线上先看哪些证据更有用

先确认慢在启动的哪一段

先拿下面这组现象对一下,分清卡的是启动链本身,还是 ready 前后的可用性窗口。

你现在看到的现象更像什么下一步
从启动命令到端口监听明显很慢类加载、自动配置、Bean 初始化偏重按本文把启动链拆开
端口已经起来,但 readiness 长时间不过预热逻辑、外部依赖、健康检查边界问题按本文把 ready 前后的时间线拆开
本地慢得明显,线上还好IDE、类路径、调试参数、本地依赖环境问题把本地和线上拆开看
线上扩容慢、滚动发布拖很久数据源、外部依赖、预热和探针策略问题把扩容链和探针链拆开看
启动阶段 CPU 或 GC 也明显抬高启动链路里存在较重初始化或对象分配压力先拆启动链,再补看 JVM 指标
应用已经 ready,但只是运行时接口慢问题已不在启动阶段转去看运行时慢调用那条线

一、把“启动慢”具体到哪一段

处理启动慢时,先抄出 4 个时间点最有用:进程启动、端口监听、readiness 首次成功、实例真正开始稳定接流量。很多团队口中的“启动慢”其实不是同一种事:有的是端口迟迟起不来,有的是端口起来了但 readiness 卡着不过,也有的是服务 ready 了却因为预热没做完仍然接不住流量。时间线不拆开,后面看日志、看探针、看 Bean 初始化都会混。

Spring Boot 启动慢最怕一上来只报总耗时。至少把下面几个边界分清楚。

1. 端口未起来

更像下面这些方向:

  • 类加载和组件扫描偏重
  • 自动配置太多
  • Bean 创建和初始化逻辑太重
  • 数据源、ORM、配置解析在启动早期就卡住

这类问题的典型特点是:应用还没有真正进入“对外可服务”阶段,瓶颈主要在启动过程本身。

2. 端口已起来,但服务还不可用

更像下面这些方向:

  • 启动后预热逻辑还没跑完
  • 缓存、规则、字典、索引在后台加载
  • 外部依赖初始化很慢
  • readiness / health check 配置让实例长时间进不了可用状态

这类问题比“端口没起来”更容易误导,因为表面上看应用像是已经成功启动了,但业务角度它还不能稳定接流量。

3. 本地慢和线上慢,不要走同一套排查顺序

如果主要是本地启动慢,更该优先怀疑:

  • IDE 启动方式
  • 开发环境类路径太重
  • 调试参数和热部署插件
  • 本地数据库、Nacos、Redis 等依赖连接

如果主要是线上扩容慢,更该优先怀疑:

  • 数据源建连和探活
  • 配置中心、注册中心、消息中间件连接
  • 启动预热逻辑
  • readiness 与健康检查边界

所以更重要的问题不是“Spring Boot 为什么慢”,而是:到底慢在启动前半段、启动后半段,还是卡在可用性判定阶段。

二、第一轮重点看哪些现象

真正能帮你缩小范围的,不是总启动时间这一行,而是下面这几组现象。

1. 启动总耗时和时间拆段

先把这几件事记下来:

  • 总启动时间是多少
  • 是最近才开始慢,还是一直慢
  • 每次都慢,还是偶发慢
  • 哪个环境最明显

然后尽量把启动过程拆段看:

  • 容器初始化前后
  • 数据源和 ORM 初始化
  • 外部依赖连接
  • 业务预热与 ready 前等待

2. 启动日志的时间点

把日志时间点抄出来最有价值。

你真正想从日志里确认的是:

  • 卡在 Spring 容器初始化前后
  • 卡在数据源或 ORM 初始化
  • 卡在 Redis、配置中心、注册中心、MQ 等外部依赖
  • 卡在某个自定义 Bean 或启动任务

这一层的目标不是先下结论,而是把“总共 20 秒”拆成“哪 8 秒最值得先查”。

3. readiness 和 health check 行为

这一层很容易被漏掉,但线上很关键。

要看:

  • 端口监听和 readiness 成功之间隔了多久
  • 探针失败是因为应用真的还没准备好,还是检查路径太重
  • 是启动慢,还是启动后可用判定太严格
  • 应用 ready 之前是否跑了阻塞式预热逻辑

如果你不先看这层,很容易把“服务已启动但尚不可用”误判成“Spring Boot 启动本身很慢”。

4. 本地与线上差异

很多启动慢问题只有在某一个环境里明显。

更要先对比:

  • 本地和线上的类路径、依赖数量是否一致
  • 本地是否开了调试参数、热部署、IDE agent
  • 线上是否多了配置中心、注册中心、探针、Sidecar
  • 启动参数、资源限制、容器规格是否一致

三、推荐排查顺序

如果你已经遇到 Spring Boot 启动慢,可以按下面这个顺序收窄范围,而不是一上来就乱翻配置。

先判断是“端口未起来”还是“端口已起来但不可用”

这是整条排查顺序里最重要的一刀。

  • 端口未起来:优先查框架初始化、自动配置、Bean 初始化、数据源建连
  • 端口已起来但不可用:优先查预热逻辑、外部依赖、健康检查和 readiness 边界

如果这一步不先切开,后面很多证据都会混在一起。

再看启动日志,把时间花在哪一段拆出来

重点不是只看这行:

Started Application in 18.532 seconds

更重要的是启动过程中的各段日志时间点。只要日志打得足够清楚,很多问题根本不用先上 Profiler,就已经能缩到具体阶段了。

第三步:如果日志还不够细,再打开启动分析能力

Spring Boot 自带的这些能力都很值得用:

  • --debug
  • ApplicationStartup
  • Actuator 启动指标

它们最有价值的不是“输出更多信息”,而是帮你确认:

  • 哪些自动配置生效了
  • 哪些 Bean 初始化特别重
  • 哪些生命周期阶段耗时明显偏高

第四步:按最大耗时段继续下钻

如果最大耗时落在下面这些方向,后续思路会不同:

  • 自动配置 / 组件扫描偏重
  • Bean 初始化逻辑过重
  • 数据源、连接池、ORM 初始化慢
  • 外部依赖连接慢
  • 业务预热和 readiness 阶段卡住

第五步:最后再决定优化动作

只有在前面证据足够明确之后,才去决定:

  • 缩扫描范围
  • 清理 starter
  • 延迟初始化
  • 异步预热
  • 调整建连和探活策略
  • 改健康检查与 readiness 边界

四、这类问题背后的常见根因

根因 1:自动配置和组件扫描太重

Spring Boot 的便利来自默认自动配置,但项目一大,这也会变成启动成本。常见场景是:

  • 引入了很多用不到的 starter
  • 扫描范围过大
  • Mapper、Entity、配置类扫得太宽
  • 条件装配判断过多

如果日志显示还没进入业务初始化,就已经花掉了大量时间,这条线很值得优先查。

根因 2:Bean 初始化逻辑太重

这类问题在真实项目里非常高频,而且通常比“框架本身重”更真实。

要重点看:

  • @PostConstruct
  • InitializingBean
  • CommandLineRunner
  • ApplicationRunner
  • 自定义 BeanPostProcessor / BeanFactoryPostProcessor

如果这些位置做了查库、调 HTTP、加载大配置、预热缓存、构建大对象图,Spring 容器就会被整体拖住。

根因 3:数据源、连接池和 ORM 初始化慢

线上扩容慢、启动窗口长,数据库相关初始化经常是高频根因。

更常见的场景有:

  • 数据库建连慢、DNS 解析慢、连接重试多
  • 连接池启动参数让应用在启动时预创建过多连接
  • JPA 自动建表 / 校验结构较重
  • MyBatis mapper 扫描和插件初始化偏重

根因 4:外部依赖把启动阶段整体拖长

现代 Spring Boot 项目很少只连数据库,通常还会连:

  • Redis
  • Kafka / RocketMQ
  • Nacos / Apollo / 配置中心
  • 注册中心
  • 对象存储
  • 远程服务或探活接口

这类问题很容易被误解成“应用自己慢”,但真实情况往往是:应用在等别的系统。

根因 5:预热逻辑和 readiness 设计不合理

这是线上最常见、也最容易和“启动慢”混淆的一类问题。

比如:

  • 加载大字典表
  • 全量缓存预热
  • 规则引擎初始化
  • 大量远程配置拉取
  • 启动后必须跑完一批任务才算 ready

这类动作本身未必错,关键要问:它是否必须阻塞应用进入可用状态。

五、最容易误判的地方

  • 端口没起来和端口起来但 readiness 不过,不是同一种问题,排查顺序也不一样
  • 本地慢不等于线上也会慢,很多本地慢来自 IDE、类路径和开发依赖环境
  • 线上扩容慢不一定是 Spring Boot 本身重,很多时候是数据源、配置中心、MQ、探针或预热链在拖
  • 启动慢不一定都该靠调 JVM 参数解决,很多时候根因在 Bean 初始化和业务准备动作
  • readiness 失败不一定代表应用没启动成功,也可能只是健康检查路径设计得太重或太早

六、FAQ:启动慢里最容易混掉的几件事

1. Spring Boot 启动慢时,应该先看日志还是先看代码?

默认先看日志。因为日志能最快把问题拆到具体阶段:是卡在容器初始化、数据源建连、外部依赖,还是卡在某个启动任务。先把阶段拆出来,再去看代码,效率会高很多。

2. 端口已经起来了,但实例还是接不住流量,这算启动慢吗?

更准确地说,这是“端口已起来,但服务尚未 ready”。这种场景更该优先查预热逻辑、外部依赖和健康检查边界,而不是只盯 Spring Boot 框架初始化。

3. 本地启动慢和线上启动慢,为什么排查方式不同?

因为两边约束不同。本地更容易受 IDE、类路径、热部署、调试参数和本地依赖环境影响;线上更容易受配置中心、注册中心、数据库建连、探针和容器资源限制影响。

4. readiness 失败和启动慢,应该怎么区分?

如果端口监听已经完成,但实例迟迟不被流量接入,更多是 readiness 边界问题;如果应用连端口都没监听起来,更像启动过程本身慢。前者更像“可用性判定慢”,后者才更像“启动链本身慢”。

5. 启动慢时,一上来就开 --debug 有必要吗?

如果普通日志已经能把阶段拆清楚,未必要一开始就开;但当你怀疑自动配置过重、条件装配过多,或者想更细地看启动步骤时,--debugApplicationStartup 和 Actuator 启动指标都很有价值。

七、已经确认不在启动链,就直接切出去

如果日志和时间线已经说明问题不在启动链本身,下面这些文章可以直接接着查:

八、关键还是把慢点钉在时间线上

Spring Boot 启动慢,最怕的是把这些不同阶段的卡点都算进同一个“启动慢”。

先把端口监听、readiness 首次成功和稳定接流量这几个时间点摊开,再回头看自动配置、Bean 初始化、外部依赖或预热逻辑,后面的优化动作才不会变成盲调。