java密码。从unix平台上的多个JVM实例调用getInstance()时速度较慢
我有一个生成多个短期java(IBM java 8)程序的应用程序,在这些程序中,它们需要向服务器发出SSL请求。我遇到了一个问题,如果我并行运行多个实例,它们都需要相同的数量和更长的运行时间。在某些情况下,它几乎不节省时间,就像我连续运行它们一样。例如,如果一个实例需要3秒才能运行,如果我并行运行5个实例,它们可能都需要15秒才能运行
我注意到这在Windows系统上似乎不是问题。我对Java安全库不是很熟悉,我确实找到了这篇文章Slow SecureRandom initialization,这似乎是根本原因,但我无法让代码片段用于Cipher.getInstance()
调用
为了演示这个问题,我将其归结为以下代码片段:
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.security.Provider;
import java.security.Security;
import java.util.ArrayList;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.crypto.Cipher;
public class CipherTesting {
private static final int DEFAULT = 20;
public static void main( String[] args ) throws Exception {
int num = parseArg( args );
if ( num > 1 ) {
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool( num );
List<Future<String>> results = new ArrayList<>();
for ( int i = 0 ; i < num ; i++ ) {
results.add( executor.submit( new Callable<String>() {
@Override
public String call() throws Exception {
String output = execute( "java", "-cp", System.getProperty( "java.class.path" ),
CipherTesting.class.getSimpleName(), "" + 1 );
return output;
}
} ) );
}
executor.shutdown();
executor.awaitTermination( 1, TimeUnit.MINUTES );
List<Integer> times = new ArrayList<>( num );
for ( Future<String> result : results ) {
System.err.println( result.get() );
times.add( Integer.parseInt( result.get().split( ":" )[1].trim() ) );
}
IntSummaryStatistics stats = times.stream().mapToInt( ( x ) -> x ).summaryStatistics();
System.out.println( stats.toString() );
} else {
test();
}
}
private static void test() {
try {
Provider provider = new com.ibm.crypto.plus.provider.IBMJCEPlusFIPS();
Security.insertProviderAt( provider, 1 );
long start = System.currentTimeMillis();
Cipher.getInstance( "AES/CBC/PKCS5Padding", provider.getClass().getSimpleName() );
long end = System.currentTimeMillis();
System.out.println( "JVM" + ManagementFactory.getRuntimeMXBean().getName().replaceAll( "@.+", "" )
+ " Time: " + ( end - start ) );
} catch ( Exception e ) {
e.printStackTrace();
}
}
private static int parseArg( String[] args ) {
if ( args.length == 0 ) {
return DEFAULT;
} else if ( args.length == 1 && args[0].trim().matches( "\\d+" ) ) {
return Integer.parseInt( args[0] );
} else {
System.out.println( "first argument must be a number" );
System.exit( 1 );
return 0;
}
}
private static String execute( String... commands ) throws IOException {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec( commands );
BufferedReader stdInput = new BufferedReader( new InputStreamReader( proc.getInputStream() ) );
// Read the output from the command
String s = null;
StringBuilder sb = new StringBuilder();
while ( ( s = stdInput.readLine() ) != null ) {
sb.append( s );
}
return sb.toString();
}
}
如果您不想安装IBM Java,我已经在ibmjava docker容器中使用以下命令进行了测试:
λ docker run -it --rm -v %CD%:/example ibmjava:8 java -cp /example/bin/ CipherTestingSimple 20
JVM76 Time: 15469
JVM77 Time: 17598
JVM80 Time: 15173
JVM55 Time: 14612
JVM78 Time: 16281
JVM58 Time: 17610
JVM49 Time: 17600
JVM48 Time: 16641
JVM75 Time: 16497
JVM54 Time: 19725
JVM59 Time: 16494
JVM61 Time: 17435
JVM56 Time: 18056
JVM73 Time: 16212
JVM81 Time: 18385
JVM74 Time: 17136
JVM60 Time: 17857
JVM57 Time: 17073
JVM72 Time: 16422
JVM79 Time: 15348
IntSummaryStatistics{count=20, sum=337624, min=14612, average=16881.200000, max=19725}
windows上的Vs:
JVM5476 Time: 906
JVM34144 Time: 3576
JVM28468 Time: 2751
JVM36084 Time: 2741
JVM16700 Time: 2560
JVM8640 Time: 2454
JVM34112 Time: 3140
JVM33364 Time: 3362
JVM17132 Time: 3999
JVM14160 Time: 3683
JVM11816 Time: 933
JVM33252 Time: 2878
JVM13660 Time: 2315
JVM12068 Time: 2416
JVM24240 Time: 3218
JVM30032 Time: 2965
JVM32316 Time: 3081
JVM14436 Time: 4532
JVM12764 Time: 2793
JVM14692 Time: 962
IntSummaryStatistics{count=20, sum=55265, min=906, average=2763.250000, max=4532}
我还注意到调用SSLSocketFactory.createSocket()
时出现了类似的问题,但我希望这个密码问题的解决方案也能解决这个问题
提前非常感谢
编辑2020年4月5日 @鲁斯泰克斯
root@9a230345867e:/# cat $JAVA_HOME/lib/security/java.security | grep securerandom.source=
securerandom.source=file:/dev/urandom
我还使用-3
信号杀死了一个子java进程(在等待了大约7秒后),该信号包括以下输出:
JVMDUMP039I Processing dump event "user", detail "" at 2020/05/04 16:23:13 - please wait.
JVMDUMP032I JVM requested Java dump using '//javacore.20200504.162313.2812.0001.txt' in response to an event
JVMDUMP010I Java dump written to //javacore.20200504.162313.2812.0001.txt
JVMDUMP013I Processed dump event "user", detail "".
该javacore的输出可以在here中找到
编辑2020年4月2日 我应该注意,创建javacore时使用的代码注释掉了以下几行。我在open java上测试代码时做了这件事,但忘了撤销它。行为仍然是一样的,但我应该提到
Provider provider = new com.ibm.crypto.plus.provider.IBMJCEPlusFIPS();
Security.insertProviderAt( provider, 1 );
# 1 楼答案
IBMJCEPlusFIPS是一家美国出口管制供应商。因此,IBM JCE会验证所有捆绑的安全JAR的签名,以实现这一点
如果您对主线程进行一些stacktrace示例,您将看到它实际上主要忙于验证JAR文件和自检(此处省略)
IBM JCE似乎是硬编码来执行这些检查的,而且类是模糊的,因此绕过它并不容易
如果文件系统I/O是瓶颈(Windows上肯定是这样,更不用说Windows内部运行的VM了),那么并行执行的性能会越来越差
您可以尝试使用OpenJDK和BouncyCastle这样的提供商作为可能的替代方案
但总的来说,Java虚拟机的启动速度很慢,这是另一个例子,说明了为什么您应该只启动一次JVM,并尽可能多地重复使用它
如果您想进一步研究这个问题,请使用探查器或这个可怜的人探查器来查看被测线程正在忙什么