背景
一个看似简单的小说推荐功能,在本地开发和云服务器直接部署时都运行良好,但一旦迁移到 Docker 容器环境,接口响应时间从毫秒级飙升到几十秒甚至超时,最终导致后端服务假死。只有重启容器才能恢复。
经过系列排查,找到罪魁祸首如下:
Random rand = SecureRandom.getInstanceStrong();
问题现象
本地 Windows/Mac 开发环境:推荐接口响应 < 10ms
云服务器直接部署(CentOS 8):推荐接口响应 < 20ms
Docker 容器部署(同一台 CentOS 8):推荐接口响应 5s~60s+,高并发时线程池耗尽
错误日志
2026-06-30 18:35:18.123 ERROR [http-nio-9091-exec-42]
org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/api]
Servlet.service() for servlet [dispatcherServlet] threw exceptionjava.lang.IllegalStateException: Thread blocked waiting for entropy
at java.base/java.security.SecureRandom.nextBytes(SecureRandom.java)
at org.example.chyznovel.service.impl.BookServiceImpl.listRecBooks(BookServiceImpl.java:202)
浏览器端表现
- OPTIONS 预检请求返回 504 Gateway Time-out
- 实际 POST 请求偶尔成功,但大部分超时
- Nginx 日志显示大量 upstream timeout
原因分析
1. SecureRandom.getInstanceStrong() 的工作原理
SecureRandom.getInstanceStrong() 是 Java 提供的密码学安全随机数生成器,它会:
- Linux 系统:读取 /dev/random 设备文件
- Windows 系统:调用 CryptoAPI
- macOS 系统:读取 /dev/urandom
关键问题在于:/dev/random 是阻塞式的真随机数源。
2. 熵池(Entropy Pool)机制
Linux 内核维护一个"熵池",收集系统中的各种"噪音"作为随机数种子:
- 键盘敲击时间间隔
- 鼠标移动轨迹
- 硬盘读写延迟
- 网络包到达时间
- 中断事件
当应用程序从 /dev/random 读取数据时:
- 熵池充足:立即返回随机数
- 熵池不足:阻塞等待,直到收集到足够的熵
查看当前熵池大小:
cat /proc/sys/kernel/random/entropy_avail
# 正常值:> 1000
# 危险值:< 100(此时读取 /dev/random 会阻塞)
3. 为什么容器环境特别容易触发?
| 环境 | 熵源丰富度 | 是否容易阻塞 |
|---|---|---|
| 本地开发机 | 丰富(有鼠标、键盘、GUI) | 否 |
| 物理服务器 | 较丰富(有硬盘、网络、中断) | 偶尔 |
| Docker 容器 | 贫乏(隔离环境,无外设) | 极易 |
容器的熵池问题:
- 隔离性导致熵源减少:容器内没有鼠标、键盘等交互设备
- 共享宿主机熵池:多个容器竞争同一个宿主机的熵资源
- 启动初期熵池为空:容器刚启动时熵池几乎为零,需要时间积累
- 高并发放大问题:每个线程调用 getInstanceStrong() 都会消耗熵,快速耗尽池子
4. 为什么本地和直接部署没问题?
- 本地开发:你的电脑有鼠标、键盘、浏览器等大量源,熵池始终充足
- 云服务器直接部署:虽然没有 GUI,但有网络流量、磁盘 I/O、定时中断等熵源,基本够用
- 容器部署:熵源被大幅削减,加上多容器竞争,极易触发阻塞
解决方案
方案一:改用 ThreadLocalRandom(推荐)
适用场景:推荐算法、游戏逻辑、A/B 测试等不需要密码学安全的场景
方案二:使用非阻塞的 SecureRandom
适用场景:必须用强随机数,但不能接受阻塞
方案三:增加容器熵源(治标不治本)
如果确实需要用 SecureRandom.getInstanceStrong(),可以优化容器熵池:
1. 安装 haveged(熵守护进程)
# Dockerfile
FROM openjdk:21-slimRUN apt-get update && \
apt-get install -y haveged && \
apt-get cleanCMD ["haveged", "-w", "1024", "-v", "1", "--Foreground"]
# docker-compose.yml
services:
app:
image: myapp
cap_add:
- SYS_ADMIN # haveged 需要特权
2. 挂载宿主机的 /dev/random
# docker-compose.yml
services:
app:
volumes:
- /dev/random:/dev/random
- /dev/urandom:/dev/urandom
什么时候必须用 SecureRandom?
必须用的场景(密码学安全要求):
- 生成加密密钥(AES、RSA)
- 生成 JWT Token 签名密钥
- 生成密码盐值(salt)
- 生成 CSRF Token
- 生成一次性验证码(防止暴力破解)
- 彩票/赌博系统开奖(法律要求不可预测)
不需要用的场景(伪随机即可):
- 推荐算法随机排序
- 游戏中的随机掉落
- A/B 测试分组
- 负载均衡随机选择
- 模拟数据生成
- 任何"即使被猜到也没关系"的场景
随机数生成器选型指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 密钥 / Token 生成 | new SecureRandom() | 需要强随机性,但频率低,阻塞可接受 |
| 验证码生成 | new SecureRandom() | 防暴力破解,需要不可预测性 |
| 推荐 / 游戏 / A-B 测试 | ThreadLocalRandom.current() | 高频调用,性能优先,伪随机够用 |
| SSL / TLS 握手 | new SecureRandom() | JDK 内部已优化,无需手动干预 |
| ⚠️ 绝对不能用(除非有特殊强随机需求且能接受阻塞) | SecureRandom.getInstanceStrong() | 可能严重阻塞,导致应用超时或线程池耗尽 |
在 Docker 容器等低熵环境中,即使你用了
new SecureRandom(),也建议配置-Djava.security.egd=file:/dev/urandom,避免因熵池不足导致阻塞。如果你用的是物理服务器且业务对性能极度敏感(如高并发网关),对于非安全类随机(如路由分发、采样),优先用
ThreadLocalRandom。SecureRandom.getInstanceStrong()在容器环境几乎等于自杀式阻塞,除非你明确知道自己在做什么,否则坚决避开。(以后再也不乱抄网上的了嘤嘤嘤!)
为什么 Java 不默认用非阻塞的?
这是一个历史遗留问题:
- 安全性优先:Java 设计者认为"宁可慢,不能不安全"
- 向后兼容:改变默认行为可能影响现有应用
- 开发者责任:框架提供工具,具体选型由开发者决定
其他语言的类似陷阱
- Python:os.urandom() vs random.SystemRandom()
- Node.js:crypto.randomBytes()(异步非阻塞)vs crypto.pseudoRandomBytes()
- Go:crypto/rand(阻塞)vs math/rand(非阻塞)
通用原则:区分"密码学安全随机数"和"普通伪随机数"的使用场景。