服务响应时间慢: Java SecureRandom和/dev/random

6

我正在尝试调试在Tomcat上部署的应用程序提供的几个慢响应。现在我正在关注SecureRandom/dev/random(其中一些其他可能的原因已经被调查并排除)。

  • 第一个调用在Tomcat重新启动后恰好需要30.0xy秒(即使请求是在启动后的4分钟之后到达的)
  • 稍后,一些调用需要确切的15.0pq秒(我无法建立任何特定模式,pq是TP99所花费的大约时间)

服务调用涉及加密和解密(AES/ECB/PKCS5Padding)。

SecureRandom初始化/重新填充是否导致了这种情况?

(虽然在catalina.log中写有日志,说"Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [28,760] milliseconds."

此外,为了检查是否使用了/dev/random/dev/urandom,我使用这个问题的测试。令我惊讶的是,我没有看到从它们中的任何一个读取,不像链接问题中发生的那样。这是strace日志的最后几行:

3561  lstat("/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0.x86_64/jre/lib/jsse.jar", {st_mode=S_IFREG|0644, st_size=258525, ...}) = 0
3561  open("/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0.x86_64/jre/lib/jsse.jar", O_RDONLY) = 6
3561  stat("/dev/random", {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 8), ...}) = 0
3561  stat("/dev/urandom", {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 9), ...}) = 0
3561  open("/dev/random", O_RDONLY)     = 7
3561  open("/dev/urandom", O_RDONLY)    = 8
3561  unlink("/tmp/hsperfdata_xxxx/3560") = 0

SecureRandom用于种子生成,那么用于种子生成的是什么呢?

顺便提一下,java -version

java version "1.6.0_32"
OpenJDK Runtime Environment (IcedTea6 1.13.4) (rhel-7.1.13.4.el6_5-x86_64)
OpenJDK 64-Bit Server VM (build 23.25-b01, mixed mode)

你可以尝试创建一个小应用程序,使用 new SecureRandom 创建一个新的随机生成器,然后从中读取一些字节,看看是否从 urandom 中读取。确保你针对相同的 Java 运行时,并检查 java.security.egd 属性是否未使用 java -D 设置为 Tomcat。 - Maarten Bodewes
我运行了一个示例程序(代码取自链接中的问题)。根据附加到问题的strace日志,它既没有从/dev/random读取,也没有从/dev/urandom读取。 - TJ-
1
你能不能也检查一下 jre/lib/security/java.security 这个文件,看一下 securerandom.source 是怎么定义的? - Maarten Bodewes
谢谢@owlstead。它是/dev/urandom。 - TJ-
你能在那里尝试一下/dev/./urandom,看看会发生什么吗? - Maarten Bodewes
2个回答

5
我无法检查您的OpenJDK具体版本,但我可以检查jdk6-b33SecureRandom使用SeedGenerator获取种子字节。
public byte[] engineGenerateSeed(int numBytes) {
    byte[] b = new byte[numBytes];
    SeedGenerator.generateSeed(b);
    return b;
}

SeedGenerator从SunEntries获取seedSource(字符串)。
String egdSource = SunEntries.getSeedSource();

SunEntries 首先尝试从系统属性 java.security.egd 中获取源,如果找不到,则尝试从 java.security 属性文件中获取属性 securerandom.source ,如果未找到该属性,则返回空字符串。

// name of the *System* property, takes precedence over PROP_RNDSOURCE
private final static String PROP_EGD = "java.security.egd";
// name of the *Security* property
private final static String PROP_RNDSOURCE = "securerandom.source";

final static String URL_DEV_RANDOM = "file:/dev/random";
final static String URL_DEV_URANDOM = "file:/dev/urandom";

private static final String seedSource;

static {
    seedSource = AccessController.doPrivileged(
            new PrivilegedAction<String>() {

        public String run() {
            String egdSource = System.getProperty(PROP_EGD, "");
            if (egdSource.length() != 0) {
                return egdSource;
            }
            egdSource = Security.getProperty(PROP_RNDSOURCE);
            if (egdSource == null) {
                return "";
            }
            return egdSource;
        }
    });
}

SeedGenerator 会检查这个值以初始化实例

// Static instance is created at link time
private static SeedGenerator instance;

private static final Debug debug = Debug.getInstance("provider");

final static String URL_DEV_RANDOM = SunEntries.URL_DEV_RANDOM;
final static String URL_DEV_URANDOM = SunEntries.URL_DEV_URANDOM;

// Static initializer to hook in selected or best performing generator
static {
    String egdSource = SunEntries.getSeedSource();

    // Try the URL specifying the source
    // e.g. file:/dev/random
    //
    // The URL file:/dev/random or file:/dev/urandom is used to indicate
    // the SeedGenerator using OS support, if available.
    // On Windows, the causes MS CryptoAPI to be used.
    // On Solaris and Linux, this is the identical to using
    // URLSeedGenerator to read from /dev/random

    if (egdSource.equals(URL_DEV_RANDOM) || egdSource.equals(URL_DEV_URANDOM)) {
        try {
            instance = new NativeSeedGenerator();
            if (debug != null) {
                debug.println("Using operating system seed generator");
            }
        } catch (IOException e) {
            if (debug != null) {
                debug.println("Failed to use operating system seed "
                              + "generator: " + e.toString());
            }
        }
    } else if (egdSource.length() != 0) {
        try {
            instance = new URLSeedGenerator(egdSource);
            if (debug != null) {
                debug.println("Using URL seed generator reading from "
                              + egdSource);
            }
        } catch (IOException e) {
            if (debug != null)
                debug.println("Failed to create seed generator with "
                              + egdSource + ": " + e.toString());
        }
    }

    // Fall back to ThreadedSeedGenerator
    if (instance == null) {
        if (debug != null) {
            debug.println("Using default threaded seed generator");
        }
        instance = new ThreadedSeedGenerator();
    }
}

如果源代码是
final static String URL_DEV_RANDOM = "file:/dev/random";

或者
final static String URL_DEV_URANDOM = "file:/dev/urandom"

使用 NativeSeedGenerator,在 Windows 上尝试使用本机的 CryptoAPI,在 Linux 上该类只是扩展了 SeedGenerator.URLSeedGenerator

package sun.security.provider;

import java.io.IOException;

/**
 * Native seed generator for Unix systems. Inherit everything from
 * URLSeedGenerator.
 *
 */
class NativeSeedGenerator extends SeedGenerator.URLSeedGenerator {

    NativeSeedGenerator() throws IOException {
        super();
    }

}

并调用超类构造函数,默认加载/dev/random

URLSeedGenerator() throws IOException {
    this(SeedGenerator.URL_DEV_RANDOM);
}

因此,默认情况下,OpenJDK使用/dev/random,直到您在系统属性java.security.egd或安全属性文件的securerandom.source属性中设置其他值。

如果您想使用strace查看读取结果,可以更改命令行并添加trace=open,read表达式。

sudo strace -o a.strace -f -e trace=open,read java class

你可以看到类似这样的东西(我使用Oracle JDK 6进行测试)
13225 open("/dev/random", O_RDONLY)     = 8
13225 read(8, "@", 1)                   = 1
13225 read(3, "PK\3\4\n\0\0\0\0\0RyzB\36\320\267\325u\4\0\0u\4\0\0 \0\0\0", 30) = 30
....
....

Tomcat Wiki中关于更快启动的部分建议在启动过程中遇到延迟时使用非阻塞熵源,如/dev/urandom。
更多信息请参见: https://wiki.apache.org/tomcat/HowTo/FasterStartUp#Entropy_Source
希望这可以帮助你。

1
感谢提供“read”跟踪信息,我确认在此JVM中使用了\dev\urandom。没有egd覆盖,并且securerandom.source也是\dev\urandom。因此,这排除了SecureRandom作为原因的可能性,我将不得不查看其他可能的原因。 - TJ-

2
问题并不在于SecureRandom本身,而是如果/dev/random没有足够的数据则会阻塞。您可以使用urandom,但如果需要加密强度的随机种子,则可能不是一个好主意。 在无头Linux系统上,您可以安装haveged守护程序。这将保持/dev/random具有足够的数据,以便调用不必等待生成所需的熵。 我已经在Debian Aws实例上执行了此操作,并观察到SecureRandom generateBytes调用从25秒降至亚毫秒(Openjdk 1.7某个版本,具体版本记不清了)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接