【问题标题】:Slow SecureRandom initialization缓慢的 SecureRandom 初始化
【发布时间】:2018-08-25 15:37:23
【问题描述】:

假设你做简单的事情:

public class Main {
    public static void main(String[] args) {
        long started = System.currentTimeMillis();
        try {
            new URL(args[0]).openConnection();
        } catch (Exception ignore) {
        }
        System.out.println(System.currentTimeMillis() - started);
    }
}

现在将http://localhost 运行为args[0]

需要~100 msec 才能完成。

现在试试https://localhost

需要5000+ msec

现在在 linux 或 docker 上运行同样的东西:

  • http:~100 msec
  • https:~350 msec

这是为什么? 为什么平台之间的差异如此之大? 你能做些什么呢?

对于长时间运行的应用程序服务器和具有自己漫长而繁重的初始化序列的应用程序,这 5 秒可能无关紧要。

但是,在许多应用程序中,最初的 5 秒“挂起”很重要,并且可能会变得令人沮丧...

【问题讨论】:

  • 谢谢,@尤金。可能是重复的答案,是的。问题似乎完全不同:-)
  • 不是真的,连问题都是伪装的“一样”;最终归结为为什么它如此缓慢,但情况不同,同意。
  • 这里不想太迂腐,但也不是全部“为什么这么慢?”问题是一样的。 (1)“为什么我的 https:// 这么慢?”以及(2)“为什么我插入无关的USB时我的文件IO这么慢?” -- 这些是完全不同的问题,他们可能有完全不同的答案。
  • @Eugene patrikbeno 的论点是可以理解的,我不介意他是否接受自己的回答。所以让我重新提出这个问题。反正相关主题已经链接了。

标签: java windows performance random


【解决方案1】:

(注意:另请参阅此答案末尾的最新更新)

说明

原因是默认 SecureRandom 提供者。

在 Windows 上,有 2 个SecureRandom 提供程序可用:

- provider=SUN, type=SecureRandom, algorithm=SHA1PRNG
- provider=SunMSCAPI, type=SecureRandom, algorithm=Windows-PRNG

在 Linux 上(使用 Oracle JDK 8u162 在 Alpine docker 中测试):

- provider=SUN, type=SecureRandom, algorithm=NativePRNG
- provider=SUN, type=SecureRandom, algorithm=SHA1PRNG
- provider=SUN, type=SecureRandom, algorithm=NativePRNGBlocking
- provider=SUN, type=SecureRandom, algorithm=NativePRNGNonBlocking

这些在jre/lib/security/java.security 文件中指定。

security.provider.1=sun.security.provider.Sun
...
security.provider.10=sun.security.mscapi.SunMSCAPI

默认情况下,使用第一个 SecureRandom 提供程序。在 Windows 上,默认是 sun.security.provider.Sun,当 JVM 使用 -Djava.security.debug="provider,engine=SecureRandom" 运行时,此实现报告如下:

Provider: SecureRandom.SHA1PRNG algorithm from: SUN
provider: Failed to use operating system seed generator: java.io.IOException: Required native CryptoAPI features not  available on this machine
provider: Using default threaded seed generator

而且默认的线程种子生成器非常慢。

你需要使用SunMSCAPI提供者。

解决方案 1:配置

在配置中重新排序提供程序:

编辑jre/lib/security/java.security:

security.provider.1=sun.security.mscapi.SunMSCAPI
...
security.provider.10=sun.security.provider.Sun

我不知道这可以通过系统属性来完成。

或者可能是的,使用-Djava.security.properties(未经测试,see this

解决方案 2:程序化

以编程方式重新排序提供程序:

Optional.ofNullable(Security.getProvider("SunMSCAPI")).ifPresent(p->{
    Security.removeProvider(p.getName());
    Security.insertProviderAt(p, 1);
});

JVM 现在报告以下 (-Djava.security.debug="provider,engine=SecureRandom"):

Provider: SecureRandom.Windows-PRNG algorithm from: SunMSCAPI

解决方案 3:程序化 v2

this idea 的启发,下面的代码只插入一个SecureRandom 服务,从现有的SunMSCAPI 提供程序动态配置,而不显式依赖sun.* 类。这也避免了与SunMSCAPI 提供者的所有服务不分青红皂白地优先排序相关的潜在风险。

public interface WindowsPRNG {

    static void init() {
        String provider = "SunMSCAPI"; // original provider
        String type = "SecureRandom"; // service type
        String alg = "Windows-PRNG"; // algorithm
        String name = String.format("%s.%s", provider, type); // our provider name
        if (Security.getProvider(name) != null) return; // already registered
        Optional.ofNullable(Security.getProvider(provider)) // only on Windows
                .ifPresent(p-> Optional.ofNullable(p.getService(type, alg)) // should exist but who knows?
                        .ifPresent(svc-> Security.insertProviderAt( // insert our provider with single SecureRandom service
                                new Provider(name, p.getVersion(), null) {{
                                    setProperty(String.format("%s.%s", type, alg), svc.getClassName());
                                }}, 1)));
    }

}

性能

<140 msec(而不是5000+ msec

详情

当您使用 URL.openConnection("https://...") 时,调用堆栈下方的某处有对 new SecureRandom() 的调用

它调用getPrngAlgorithm()(见SecureRandom:880

这会返回它找到的第一个 SecureRandom 提供者。

出于测试目的,对URL.openConnection() 的调用可以替换为:

new SecureRandom().generateSeed(20);

免责声明

我不知道供应商重新订购会造成任何负面影响。但是,可能会有一些,特别是考虑到默认的提供者选择算法。

无论如何,至少在理论上,从功能的角度来看,这对应用程序应该是透明的。

2019-01-08 更新

Windows 10(版本 1803):无法再在任何最新 JDK 上重现此问题(从旧的 oracle 1.7.0_72 到 openjdk "12-ea" 2019-03-19 均已测试)。

看起来是 Windows 问题,已在最新的操作系统更新中修复。最近的 JRE 版本中也可能发生或未发生相关更新。 但是,即使我最旧的 JDK 7 update 72 安装肯定会受到影响,并且肯定不会以任何方式修补,我也无法重现原始问题。

使用此解决方案时,性能仍有小幅提升(平均 cca 350 毫秒),但默认行为不再遭受无法忍受的 5+ 秒惩罚。

【讨论】:

  • 好帖子。如果不是因为这个,我可能永远不会知道操作系统上的不同提供商之间存在如此大的性能差异。您应该向 Oracle 提出一张票,因为该修复似乎更多地成为 JDK 的一部分。
  • 我想知道为什么原生 CryptoAPI 功能不可用。这就是我想要发现的。
  • @JamesKPolk,我同意。我能找到的最深层原因是 NativeSeedGenerator.nativeGenerateSeed() 返回 false。见:NativeSeedGenerator.javaWinCAPISeedGenerator.c
  • 也相关SO question
  • @MaartenBodewes,谢谢。在 Windows 操作系统中已修复。我更新了答案。
猜你喜欢
  • 1970-01-01
  • 2014-11-18
  • 1970-01-01
  • 2012-10-31
  • 1970-01-01
  • 2023-01-19
  • 2015-03-27
  • 2014-08-04
  • 1970-01-01
相关资源
最近更新 更多