Spring Boot 启动卡住 3 分钟才起来:不是慢查询,是 SecureRandom 在等熵

举报
yd_273983069 发表于 2026/10/31 10:18:42 2026/10/31
【摘要】 一个 Spring Boot 服务在测试环境启动要三分多钟,日志停在 Tomcat 初始化之后没有任何输出。查到最后是 JVM 的 SecureRandom 在 /dev/random 上阻塞等熵。本文讲清这个现象的判据、成因,以及三种从轻到重的修法。

先说现象。一个 Spring Boot 服务,本地起得很快,二十秒左右;部署到一台新装的测试机上,启动要三分多钟,而且不是均匀地慢——是卡在某一个点完全不动,然后突然继续,后面就正常了。


日志的最后一行通常长这样:


  o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)


之后两三分钟没有任何输出,CPU 占用接近 0,内存也没有异常增长。第一次遇到会怀疑是不是数据库连接池在等超时、是不是配置中心连不上、是不是某个 @PostConstruct 里在 sleep。逐个排除都不对。


真正在等的东西


JVM 启动时,Tomcat 要给 session 生成一个 sessionId 的生成器(SessionIdGenerator),默认实现会去拿一个 SecureRandom 实例。SecureRandom 在 Linux 上默认走 NativePRNG,也就是读 /dev/random 或 /dev/urandom。


问题就出在这儿:/dev/random 和 /dev/urandom 不是一回事。


- /dev/urandom 是非阻塞的,熵池没初始化好也会给你数,只是质量打点折扣;

- /dev/random 是阻塞的,熵池里的熵不够时,read() 会一直挂住,直到系统收集到足够的中断噪声。


而熵从哪来?键盘、鼠标、磁盘 IO、网卡中断。一台刚装好的虚拟机、一个没有物理外设的云主机、一个容器,这几样都很少,开机后熵池几乎是空的。所以第一次有人要「高质量随机数」的时候,就得等。等多久完全看这台机器什么时候攒够噪声——三十秒、三分钟、十分钟都有可能。


这就解释了三个反常点:本地开发机为什么快(天天在用,熵池满的)、为什么卡的位置固定(Tomcat 初始化 SessionIdGenerator 那一行)、为什么 CPU 是 0(在等内核,不在算)。


怎么确认


最直接的证据在这两个地方。


第一个,看 JVM 的线程栈。用 jstack 抓一下,或者直接 kill -3 <pid> 让它在日志里打线程转储,然后搜 SecureRandom:


  "main" #1 prio=5 os_prio=0 ... java.lang.Thread.State: RUNNABLE

      at java.io.FileInputStream.readBytes(Native Method)

      at java.io.FileInputStream.read(FileInputStream.java:255)

      at sun.security.provider.NativePRNG$RandomIO.readBytes(NativePRNG.java:315)

      at sun.security.provider.SecureRandom.engineNextBytes(SecureRandom.java:233)


栈里出现 NativePRNG 加 readBytes,基本就定了。注意线程状态会显示 RUNNABLE 而不是 BLOCKED 或 WAITING——这是一个很容易误导人的地方,它其实是在等 IO,但 JVM 认为线程在跑。


第二个,看系统熵池。


  cat /proc/sys/kernel/random/entropy_avail


正常应该在 1000 以上(上限 4096)。如果这个数字只有几十到一两百,而且你 cat 它的时候数字几乎不动,那就没跑了。还可以看一眼池子大小:


  cat /proc/sys/kernel/random/poolsize


另外 dmesg 里有时会有 random: crng init done 的时间戳,能对上启动卡住的时间窗口。


三种修法,从轻到重


1)指定 JVM 用非阻塞的熵源


最省事的一招,加启动参数:


  -Djava.security.egd=file:/dev/urandom


或者用等价的 /dev/./urandom(某些老版本 JDK 会缓存这个属性值,加点号能绕过缓存)。


这个参数会让 SecureRandom 走 urandom,不再阻塞。代价是:urandom 在极早期(crng 还没初始化时)返回的随机数质量不如 random。对于「生成 sessionId」这种场景完全够用——sessionId 不需要密码学强度的真随机。


2)装熵源守护进程


如果确实需要高质量随机数(比如服务里要做密钥生成),那就补熵,而不是绕过。


  # Debian/Ubuntu

  apt install haveged

  systemctl enable --now haveged


haveged 用 CPU 的时序抖动来喂熵池,装上之后 entropy_avail 会稳定在 3000 以上。rng-tools 是另一条路,用硬件 RNG 或者 jitterentropy。


云主机和容器里更推荐这个方案,因为 /dev/random 的阻塞行为在这些环境里最容易踩。


3)容器环境里的正确姿势


如果是跑在容器里,加 -Djava.security.egd 只是绕过了 JVM 层的阻塞,内核的 crng 初始化还是要发生的。容器共享宿主内核,所以真正要做的是保证宿主机在容器起来之前 crng 已经初始化完成。


具体表现是:容器刚起来的那几十秒里,getrandom() 系统调用会阻塞。Java 层的 /dev/urandom 读取在 Java 8 早期版本也是走 getrandom,会跟着卡。较新的 JDK(8u162 之后、11 之后)在这方面改过行为,会先用 urandom 兜底,卡顿明显减少。


如果你用 Kubernetes,还有个容易忽略的点:Pod 的 securityContext 里如果没挂 /dev/random,某些镜像启动脚本里的熵相关命令会直接失败。不过这属于另一类问题了。


两个容易混的邻近问题


一是 JVM 的种子生成不只在启动时。有些库(比如某些 JWT 实现、某些加密工具类)会在运行中 new SecureRandom(),如果每次都新建实例,在高并发下也会短暂卡顿。正确做法是全局一个静态实例——SecureRandom 是线程安全的,不需要每次 new。


二是「启动慢」还有另一个经典原因:JAR 包太大导致类加载慢、或者 Spring 的 component scan 扫了太多包。这两个的判据不同——类加载慢会有明显的 CPU 占用,component scan 慢会在日志里看到 scanning 的进度。只有 SecureRandom 阻塞是 CPU 为 0、日志完全静止。三者用「CPU 占用 + 日志是否静止」两个维度就能区分开。


小结


启动卡住几分钟、CPU 为 0、日志停在 Tomcat 初始化之后,先别怀疑数据库和配置中心。抓个 jstack 看有没有 NativePRNG,再 cat 一下 entropy_avail。确认之后,-Djava.security.egd=file:/dev/urandom 是最快的止血,haveged 是更彻底的修法。尤其是新建的云主机和容器镜像,这个问题出现的概率比想象中高。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。