场景
业务有一个安全随机数方法,需要调用生成数据掉落区间进行结果判定。
最开始业务是在AWS的EC2部署, 后面业务需要迁移,选择了阿里云的ECS,系统是CentOS 8.5
遇到的问题
- 业务有一个逻辑需要生成安全随机数,但是迁移后业务会在安全随机数卡住没有后续,DEBUG级别日志也没有任何日志报错
- 数据、代码全部相同,本地IDEA运行,在开发机本机正常,数据库和缓存连接阿里云服务的外网地址,一切正常。
- 尝试使用Docker image, 现象相同。
- 查询部分说明是发行版系统在Kernel 4之后的版本关闭了安全随机数,需要重新编译内核才能开启,因此尝试将ECS退回到CentOS 7.9版本,仍未恢复。
定位问题
- 尝试本机连接阿里云RDS测试业务是否正常
本机运行正常,代码相同,服务器业务不可用 - 尝试统一环境,使用Docker进行
相同的Docker-Compose文件,相同代码,本机正常,服务器业务不可用 - 测试IDEA远程Debug
断点进入后,到安全随机数获取方法直接没有后续 - 尝试抽取方法,直接javac运行
没有任何后续输出,命令行卡住
由于数据库、缓存使用本地开发机连接正常,代码没有任何变化,即使使用了Docker、尝试变更系统也未解决,因此判定是ECS硬件问题。
由于安全随机数需要足够的熵,即足够的设备才能生成,运行如下命令查询阿里云ECS随机数值,只有25,而安全随机数需要100以上才能成功运行
1 | cat /proc/sys/kernel/random/entropy_avail |
解决方案
- 修改安全随机数获取方法
业务需要,安全随机数必须保留,因此不能使用普通Random,这里使用了SecureRandom的另一个方法,保证获取可用的工具。
SecureRandom.getInstance(“SHA1PRNG”) 在部分 Linux 发行版上仍然可能阻塞,因为它可能仍然尝试访问 /dev/random 作为熵源(虽然概率低)。
SecureRandom 需要熵(entropy),如果系统熵池不足(比如云服务器没有鼠标、键盘等设备),它可能会等到足够的熵才会返回。 - 修改启动命令,使用 /dev/urandom 作为熵源
新增启动参数-Djava.security.egd=file:/dev/urandom
, 保证业务运行
示例
原代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class RandomTest {
private static final SecureRandom sr;
static {
SecureRandom temp;
try {
// 获取高强度安全随机数生成器
temp = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
// 获取普通的安全随机数生成器
temp = new SecureRandom();
}
sr = temp;
}
public static void main(String[] args) {
byte[] buffer = new byte[8];
// 用安全随机数填充buffer
sr.nextBytes(buffer);
long randomFactor = 0;
for (byte b : buffer) {
randomFactor = (randomFactor << 8) | (b & 0xFF);
}
double randomValue = sr.nextDouble() + (randomFactor / (double) Long.MAX_VALUE);
// 设置上限为max
System.out.println(Math.abs(randomValue % 1.0));
}
}原代码现象
1
2
3
4
5
6
7javac RandomTest.java
java RandomTest
# 没有输出手动中断代码修改部分
SecureRandom.getInstanceStrong()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class RandomTest {
private static final SecureRandom sr;
static {
SecureRandom temp;
try {
// 获取高强度安全随机数生成器
//temp = SecureRandom.getInstanceStrong();
temp = SecureRandom.getInstance("SHA1PRNG");
// temp = SecureRandom.getInstance("NativePRNGNonBlocking"); // 或者使用这个, 不会阻塞
} catch (NoSuchAlgorithmException e) {
// 获取普通的安全随机数生成器
temp = new SecureRandom();
}
sr = temp;
}
public static void main(String[] args) {
byte[] buffer = new byte[8];
// 用安全随机数填充buffer
sr.nextBytes(buffer);
long randomFactor = 0;
for (byte b : buffer) {
randomFactor = (randomFactor << 8) | (b & 0xFF);
}
double randomValue = sr.nextDouble() + (randomFactor / (double) Long.MAX_VALUE);
// 设置上限为max
System.out.println(Math.abs(randomValue % 1.0));
}
}启动命令添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17nohup /usr/local/java/jdk-21/bin/java \
-Djava.awt.headless=true \
-Djava.net.preferIPv4Stack=true \
-Djava.security.egd=file:/dev/urandom \ # 这里添加 JVM 选项,防止 SecureRandom 阻塞
-server \
-Xms1024m -Xmx2048m \
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m \
-XX:SurvivorRatio=6 \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintFlagsFinal \
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 \ # 这里是外部Debug接口, 用来IDEA远程DEBUG,需ECS开放端口
-jar /wls/esxd/esxd-api.jar \
--spring.profiles.active=sit \
> /wls/esxd/stdout.log 2>&1 &
代码记录
Code: https://github.com/swzxsyh/Case
Rivision Num: 6c09f88ea4f2f41a1fd93cfb838e34ae7158a8c0