I have an application that spawns multiple short-lived java (IBM Java 8) programs in which they need to make an SSL request to a server. I am running into an issue where if I run several instances of these in parallel they all take an equal amount, and much longer time to run. In some cases it saves almost no time as if I had run them serially. For example, if 1 instance takes 3 seconds to run, if I run 5 in parallel they might all take 15 seconds to run.
I have noticed that this does not seem to be an issue on Windows systems. I'm not very familiar with the Java security libraries, and I did find this post Slow SecureRandom initialization which seems like it might be the root cause, but I was unable to get the code snippets to work for the Cipher.getInstance()
call.
To demonstrate the problem, I have distilled it down to this code snippet:
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();
}
}
If you don't want to install IBM Java, I have tested this in the ibmjava docker container using the command:
λ 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}
Vs on windows:
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}
I have also noticed a similar issue with calls to SSLSocketFactory.createSocket()
but am hopeful that the solution to this Cipher problem would also solve that.
Thank you very much in advance.
Edit 1 05/04/2020 @rustyx
root@9a230345867e:/# cat $JAVA_HOME/lib/security/java.security | grep securerandom.source=
securerandom.source=file:/dev/urandom
I also killed one of the child java processes (after waiting about 7 seconds) using the -3
signal, which included the output:
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 "".
The output from that javacore can be found here.
Edit 2 05/04/2020 I should note, the code used when creating the javacore had the following lines commented out. Did this during my testing the code on open java and forgot to undo it. Behavior is still the same, but figured should mention.
Provider provider = new com.ibm.crypto.plus.provider.IBMJCEPlusFIPS();
Security.insertProviderAt( provider, 1 );