0%

Java 安全随机数在阿里云ECS卡住

场景

业务有一个安全随机数方法,需要调用生成数据掉落区间进行结果判定。
最开始业务是在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
2
$ cat /proc/sys/kernel/random/entropy_avail
25

解决方案

  • 修改安全随机数获取方法
    业务需要,安全随机数必须保留,因此不能使用普通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
    31
    import 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
    7
    $ javac 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
    33
    import 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
    17
    nohup /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