接口服务公网超时排查记录:SecureRandom 阻塞问题分析与解决
📝 前言
在开发时,遇到一个特别奇怪的问题:同一套服务,在局域网环境调用正常,部署到公网云服务器后出现请求超时。具体表现为客户端报 Read timed out,Postman 等待数分钟后返回 500,但服务端业务日志显示处理成功且数据库正常入库,应用层无异常日志。
🔍 一、 问题现象
- 局域网环境:接口调用正常,响应时间在毫秒级,返回加密后的响应体。
- 公网环境 :调用同一接口,传入相同参数,出现异常:
- Java 客户端调用:抛出
java.net.SocketTimeoutException: Read timed out异常。 - Postman 调用:请求长时间无响应。
- Java 客户端调用:抛出
- 服务端日志:业务日志显示"业务处理成功",数据库记录正常插入,无任何 ERROR 级别异常堆栈。
🕵️ 二、 排查过程
1. 排查公网网络与代理
初步怀疑公网链路中的 NAT、防火墙丢弃了 HTTP Chunked 结束块,或 Nginx 缓冲导致。
操作 :在客户端使用 curl -v 测试错误参数(不带请求体)。
结果 :瞬间返回 400,响应头为 Content-Length: 36,Connection: close。证明公网 TCP 链路和基础 HTTP 通信正常。
2. 定位系统熵源
联想到响应体使用了 AES-GCM 加密,需要生成随机 IV。Linux 系统存在阻塞式和非阻塞式随机源。
操作:在公网服务器执行:
bash
cat /proc/sys/kernel/random/entropy_avail
结果 :返回 520。(正常健康的 Linux 系统该值通常应在 3000 以上)。
5. 定位应用代码
查看响应体加密工具类 PayloadCrypto.java,定位到问题代码:
java
public String encrypt(String plain) {
try {
byte[] iv = new byte[IV_LEN];
SecureRandom.getInstanceStrong().nextBytes(iv); // 引发阻塞的代码
// ... 后续加密逻辑
}
}
💡 三、 原理分析
为什么 SecureRandom.getInstanceStrong() 会导致请求阻塞?
1. Linux 的随机数机制
/dev/random:阻塞式随机数生成器。依赖于系统收集的物理噪声(键盘、鼠标、磁盘 I/O、网卡中断)。如果系统熵池枯竭,读取它的线程会被挂起,直到收集到足够的随机事件。/dev/urandom:非阻塞式随机数生成器。即使熵池不足,也会利用密码学安全的伪随机数生成器(CSPRNG)立即计算出随机数,不会阻塞线程。
2. getInstanceStrong() 的执行机制
在 Linux 环境下,SecureRandom.getInstanceStrong() 会强制 JVM 使用阻塞式的 /dev/random。
- 云服务器通常没有物理键盘和鼠标,磁盘 I/O 和网络中断也相对较少。
- 当系统熵池只有 520 时,执行到此处会导致应用线程挂起,等待系统积累熵。
- 等待 3.2 分钟后,系统层面或 Tomcat 触发超时机制,返回 500。
- 局域网正常的原因:局域网服务器可能是 Windows 环境(底层走 CryptoAPI 不阻塞),或者是物理机且熵源充足。
3. 为什么业务日志正常且无异常?
因为数据库入库(业务逻辑)发生在响应体加密之前。线程卡在业务逻辑之后的加密环节,没有抛出 Exception,只是被系统挂起(Waiting 状态),因此应用层没有任何异常日志。
🛠️ 四、 解决方案
针对该问题,有以下几种解决方案(按推荐程度排序):
方案一:修改代码
对于 AES-GCM 的 IV(初始向量)来说,只需要保证唯一性和不可预测性,使用普通的 new SecureRandom() 即可,且默认使用非阻塞的 /dev/urandom。
java
// 修改前(阻塞,导致超时)
// SecureRandom.getInstanceStrong().nextBytes(iv);
// 修改后(非阻塞)
new SecureRandom().nextBytes(iv);
安全性说明 :
/dev/urandom提供的随机性在密码学上完全满足需求。getInstanceStrong()是为生成长期根证书私钥等极端场景设计的,不适用于每次 HTTP 请求的加密操作。
方案二:JVM 启动参数兜底
在服务端启动脚本中,强制 JVM 使用非阻塞的 urandom:
bash
java -Djava.security.egd=file:/dev/./urandom -jar your-app.jar
注意:file:/dev/./urandom 中的 ./ 是为了绕过某些 JDK 版本对路径的内部检查。
方案三:安装系统熵源补充服务
在公网 Linux 服务器上安装 haveged,让系统持续补充熵池:
bash
# CentOS/RHEL
yum install haveged -y
systemctl start haveged
systemctl enable haveged
# Ubuntu/Debian
apt-get install haveged -y
systemctl start haveged
systemctl enable haveged
安装后再次执行 cat /proc/sys/kernel/random/entropy_avail,数值会恢复到 3000 以上。
🧪 五、 验证修复
- 修改代码或添加 JVM 参数后,重启服务。
- 在客户端使用 Postman 重新发送带完整参数的 POST 请求。
- 响应时间从 3.225 分钟降至毫秒级,客户端成功接收到加密响应,问题解决。
📌 六、 总结与建议
- 明确日志边界:业务代码执行完毕不等同于 HTTP 请求处理完毕。Filter、Interceptor、序列化、响应加密等环节都可能发生阻塞。
- 慎用
getInstanceStrong():除非在生成根证书或极高安全级别的长期密钥,否则建议使用new SecureRandom()。 - 关注云服务器熵源 :现代 Linux 内核(5.6+)虽然对熵源做了优化,但老版本内核或虚拟机依然可能遇到熵池枯竭。部署到公网 Linux 时,添加
-Djava.security.egd=file:/dev/./urandom是一个有效的运维习惯。