接口服务公网超时排查记录:SecureRandom 阻塞问题分析与解决

接口服务公网超时排查记录:SecureRandom 阻塞问题分析与解决

📝 前言

在开发时,遇到一个特别奇怪的问题:同一套服务,在局域网环境调用正常,部署到公网云服务器后出现请求超时。具体表现为客户端报 Read timed out,Postman 等待数分钟后返回 500,但服务端业务日志显示处理成功且数据库正常入库,应用层无异常日志。

🔍 一、 问题现象

  1. 局域网环境:接口调用正常,响应时间在毫秒级,返回加密后的响应体。
  2. 公网环境 :调用同一接口,传入相同参数,出现异常:
    • Java 客户端调用:抛出 java.net.SocketTimeoutException: Read timed out 异常。
    • Postman 调用:请求长时间无响应。
  3. 服务端日志:业务日志显示"业务处理成功",数据库记录正常插入,无任何 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 以上。

🧪 五、 验证修复

  1. 修改代码或添加 JVM 参数后,重启服务。
  2. 在客户端使用 Postman 重新发送带完整参数的 POST 请求。
  3. 响应时间从 3.225 分钟降至毫秒级,客户端成功接收到加密响应,问题解决。

📌 六、 总结与建议

  1. 明确日志边界:业务代码执行完毕不等同于 HTTP 请求处理完毕。Filter、Interceptor、序列化、响应加密等环节都可能发生阻塞。
  2. 慎用 getInstanceStrong() :除非在生成根证书或极高安全级别的长期密钥,否则建议使用 new SecureRandom()。
  3. 关注云服务器熵源 :现代 Linux 内核(5.6+)虽然对熵源做了优化,但老版本内核或虚拟机依然可能遇到熵池枯竭。部署到公网 Linux 时,添加 -Djava.security.egd=file:/dev/./urandom 是一个有效的运维习惯。
相关推荐
AIgorithmGEEK1 小时前
[Linux]从手写报头到内核套路:序列化、反序列化与自定义协议全链路
linux·运维·服务器·网络·序列化·反序列化
Nil2081 小时前
leetcode 139单词拆分
linux·运维·服务器
Starry-sky(jing)1 小时前
BUG: unable to handle kernel paging request 完整排查:dmesg 四要素与三路定罪
linux·运维·服务器·内核·排障
yuniko-n1 小时前
【JUC】Lock 和 synchronzied 锁
java
mounter6251 小时前
从硬件互连到操作系统变革:CXL 技术演进与 Linux 内核工程挑战
linux·运维·服务器
西柚小萌新1 小时前
【LLM&&AI应用开发 八股文】--4.3.Agent智能体(下)
java·开发语言·数据库
北极有牛1 小时前
cpp学习笔记--常量指针
java·开发语言·算法
java资料站1 小时前
二、Spring AI Alibaba · ChatModel
java·windows·spring
十年Java程序媛2 小时前
Java 接口和抽象类对比|Java8 新特性,抛弃老旧八股,正确选型
java·spring boot·后端