diff --git a/.gitignore b/.gitignore index a3da24348..8331409e1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Servers/ .settings/ RemoteSystemsTempFiles/ .classpath +.factorypath .project # Puthon related files .pydevproject diff --git a/assembly/src/main/descriptors/windows-src.xml b/assembly/src/main/descriptors/windows-src.xml index 56be220be..0c15e3a52 100644 --- a/assembly/src/main/descriptors/windows-src.xml +++ b/assembly/src/main/descriptors/windows-src.xml @@ -45,6 +45,7 @@ sshd-openpgp/src/test/resources/**/* sshd-core/src/test/resources/org/apache/sshd/client/opensshcerts/**/* + sshd-benchmarks/src/main/resources/org/apache/sshd/**/* **/eclipse-classes/** @@ -95,6 +96,7 @@ sshd-core/src/docs/*.txt sshd-core/src/test/resources/org/apache/sshd/client/opensshcerts/**/* + sshd-benchmarks/src/main/resources/org/apache/sshd/**/* sshd-openpgp/src/test/resources/**/* dos diff --git a/pom.xml b/pom.xml index 195587cc9..e1d4a062c 100644 --- a/pom.xml +++ b/pom.xml @@ -143,7 +143,7 @@ validate - + toolchains @@ -683,7 +683,7 @@ - + org.slf4j jul-to-slf4j test @@ -730,6 +730,8 @@ docs/** sshd-sources/** src/test/resources/** + src/main/resources/org/apache/sshd/benchmarks/** + src/main/java/org/apache/sshd/benchmarks/**/*.md **/stty-output-*.txt **/target/** @@ -937,6 +939,7 @@ false true + target/generated-sources/annotations target/generated-sources/java @@ -1379,6 +1382,7 @@ + sshd-benchmarks sshd-common sshd-putty sshd-openpgp diff --git a/sshd-benchmarks/README.md b/sshd-benchmarks/README.md new file mode 100644 index 000000000..ac3df2555 --- /dev/null +++ b/sshd-benchmarks/README.md @@ -0,0 +1,20 @@ +# JMH Benchmarks + +This project provides a few JMH benchmarks. It is not part of the Apache MINA sshd binary distribution. + +For details about the benchmarks, see the individual READMEs. + +Note that benchmarking or timing individual SSH or SFTP operations is difficult because there are always +at least two servers and a network connection in between involved. Some tests run an SSH peer in a local +docker container. The network connection is fast and has a very low latency, but the machine executing +the benchmarks also executes the docker container, which may skew timings. If tests are run against an +external SSH peer on some other machine, the network may be slower and/or have a higher latency, which +again may give unusable timings. Moreover, if the network is a general-purpose network, other traffic may +influence timings, and if there are firewalls and/or proxies in between, getting meaningful timings may +be even harder. + +Other processes running on the machine executing the benchmarks may also influence timings. In +particular, suspend automatic backups during benchmarking, and try to avoid interference from virus +scanners. Shut down messaging programs (Teams and the like, but also E-Mail programs), maybe even Web +Browsers or programs that may suddenly check for available updates). Don't do other things on the +machine while the benchmark runs; even scrolling in some other window will skew timings. diff --git a/sshd-benchmarks/pom.xml b/sshd-benchmarks/pom.xml new file mode 100644 index 000000000..2c3cf6ef5 --- /dev/null +++ b/sshd-benchmarks/pom.xml @@ -0,0 +1,188 @@ + + + 4.0.0 + + + org.apache.sshd + sshd + 2.14.0-SNAPSHOT + .. + + + sshd-benchmarks + Apache Mina SSHD :: Benchmarks + jar + 2024 + + + UTF-8 + 1.37 + benchmarks + ${project.version} + + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + args4j + args4j + 2.33 + + + + org.apache.sshd + sshd-common + ${sshd-version} + + + org.apache.sshd + sshd-core + ${sshd-version} + + + org.apache.sshd + sshd-sftp + ${sshd-version} + + + + net.i2p.crypto + eddsa + + + + org.bouncycastle + bcpg-jdk18on + + + org.bouncycastle + bcpkix-jdk18on + + + org.bouncycastle + bcprov-jdk18on + + + org.slf4j + slf4j-api + + + org.slf4j + jcl-over-slf4j + + + org.slf4j + jul-to-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + org.testcontainers + testcontainers + ${testcontainers.version} + + + com.github.mwiede + jsch + + + + junit + junit + provided + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + ${uberjar.name} + false + + + org.openjdk.jmh.Main + + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + org.apache.maven.plugins + maven-install-plugin + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + diff --git a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/README.md b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/README.md new file mode 100644 index 000000000..bb592e954 --- /dev/null +++ b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/README.md @@ -0,0 +1,10 @@ +# Benchmarks + +This is a generalized benchmark runner for Apache MINA sshd JMH benchmarks. It takes a number of command line options to control which benchmarks are run and how. + +Without arguments, it runs the full "SFTP upload" benchmark suite against an OpenSSH instance running in a local docker container. The docker engine must be running for this to work. There are command-line arguments to run the benchmarks against any external server; the benchmarks all assume that there is an `upload` directory into which they can write. Command-line argument `--help` lists the available options. + +Via the `--run` option, one can control which benchmarks are run. + +Benchmarks are run with a warm-up of 4 iterations, and then 10 timed iterations. + diff --git a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/RunBenchmarks.java b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/RunBenchmarks.java new file mode 100644 index 000000000..dba3261f1 --- /dev/null +++ b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/RunBenchmarks.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.benchmarks; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.Option; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +public final class RunBenchmarks { + + @Option(name = "--run", aliases = "{ -r }", metaVar = "REGEX", + usage = "Tests to run. If not given, runs all SftpUploadBenchmarks.") + private String include = "SftpUploadBenchmark"; + + @Option(name = "--server", aliases = "{ -s }", metaVar = "USER@HOSTNAME:PORT", + usage = "Specifies the hostname to connect to. At least USER and HOSTNAME must be given. If absent, a local container will be used (and the docker engine must be running).") + private String server; + + @Option(name = "--identity", aliases = "{ -i }", metaVar = "PRIVATE_KEY_FILE", + usage = "The SSH private key to connect to the host. Mandatory if --server is given; otherwise ignored. If it starts with ~/ or ~\\, the ~ is replaced by the user's home directory.") + private String userKey; + + @Option(name = "--help", aliases = { "-h" }, usage = "Displays this help text and exits.") + private boolean help; + + private String user; + private String hostname; + private Path key; + private int port; + + private RunBenchmarks() { + super(); + } + + private void run(String... args) throws Exception { + if (!parseArguments(args)) { + return; + } + TestServer.INSTANCE.start(user, hostname, port, key); + try { + Options opt = new OptionsBuilder() // + .include(include) // + .param("sftpHost", TestServer.INSTANCE.getHost()) // + .param("sftpPort", Integer.toString(TestServer.INSTANCE.getPort())) // + .param("sftpUser", TestServer.INSTANCE.getUser()) // + .param("sftpKey", TestServer.INSTANCE.getPrivateKey().toString()) // + .param("initialFile", TestServer.INSTANCE.getFile().toString()) // + .mode(Mode.AverageTime) // + .warmupIterations(4) // + .measurementIterations(10) // + .shouldFailOnError(true) // + .timeUnit(TimeUnit.MILLISECONDS) // + .forks(1) // + .threads(1) // + .build(); + new Runner(opt).run(); + } finally { + TestServer.INSTANCE.stop(); + } + } + + private boolean parseArguments(String... args) { + CmdLineParser parser = new CmdLineParser(this); + try { + parser.parseArgument(args); + if (help) { + printUsage(parser); + return false; + } + return splitServer(); + } catch (CmdLineException err) { + System.err.println("Invalid arguments, try --help: " + err.getLocalizedMessage()); + return false; + } + } + + private boolean splitServer() { + if (server == null) { + return true; + } + try { + server = server.trim(); + int i = server.indexOf('@'); + if (i < 0) { + throw new IllegalArgumentException("no user name"); + } + user = server.substring(0, i).trim(); + if (user.isEmpty()) { + throw new IllegalArgumentException("no user name"); + } + int j = server.indexOf(':', i); + if (j > i) { + hostname = server.substring(i + 1, j).trim(); + port = Integer.parseUnsignedInt(server.substring(j + 1)); + } else { + hostname = server.substring(i + 1).trim(); + port = 22; + } + if (port < 1024 && port != 22 || port > 65535) { + throw new IllegalArgumentException("invalid port " + port); + } + if (hostname.isEmpty()) { + throw new IllegalArgumentException("no host name"); + } + if (userKey == null || userKey.isEmpty()) { + throw new IllegalArgumentException("need a private key when a host is specified"); + } + if (userKey.startsWith("~/") || userKey.startsWith('~' + File.separator)) { + String homeDir = System.getProperty("user.home"); + key = Paths.get(homeDir, userKey.substring(2)); + } else { + key = Paths.get(userKey); + } + if (!Files.isRegularFile(key)) { + throw new IllegalArgumentException("private key " + userKey + " not found or not a file"); + } + return true; + } catch (IllegalArgumentException e) { + System.err.println( + "Server must have the format USER@HOSTNAME or USER@HOSTNAME:PORT, and there must be a private key. Try --help. Error: " + + e.getLocalizedMessage()); + return false; + } + } + + private void printUsage(CmdLineParser parser) { + System.err.print("SftpBenchmarks "); + parser.printSingleLineUsage(System.err); + System.err.println(); + + System.err.println(); + parser.printUsage(System.err); + System.err.println(); + + System.err.flush(); + } + + public static void main(String... args) throws Exception { + RunBenchmarks exec = new RunBenchmarks(); + exec.run(args); + } + +} diff --git a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/TestServer.java b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/TestServer.java new file mode 100644 index 000000000..ce47ebec3 --- /dev/null +++ b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/TestServer.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.benchmarks; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.MountableFile; + +public enum TestServer { + + INSTANCE; + + private static final String RESOURCES = "/" + TestServer.class.getPackage().getName().replace('.', '/'); + + private static final long FILE_SIZE = 20L * 1024 * 1024; + + private GenericContainer sftpHost; + private String user; + private String hostname; + private int port; + private Path initialFile; + private Path keyFile; + + private static void generateFile(Path p, long size) throws IOException { + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(p))) { + for (long i = 0; i < size; i++) { + out.write((int) (i & 0xFFL)); + } + } + } + + public void start(String user, String hostname, int port, Path userKey) throws IOException { + boolean container = (user == null || user.isEmpty()) // + || (hostname == null || hostname.isEmpty()) // + || port < 1024 && port != 22 // + || port > 65535 // + || userKey == null // + || !Files.isRegularFile(userKey); + if (container) { + @SuppressWarnings("resource") + GenericContainer sftp = new GenericContainer<>("atmoz/sftp:alpine") // + .withEnv("SFTP_USERS", "foo::::upload") + // Set it up for pubkey auth + .withCopyFileToContainer(MountableFile.forClasspathResource(RESOURCES + "/rsa_key.pub"), + "/home/foo/.ssh/keys/id_rsa.pub") + // Give it static known host keys! + .withCopyFileToContainer(MountableFile.forClasspathResource(RESOURCES + "/ed25519_key", 0x180), + "/etc/ssh/ssh_host_ed25519_key") + .withCopyFileToContainer(MountableFile.forClasspathResource(RESOURCES + "/rsa_key", 0x180), + "/etc/ssh/ssh_host_rsa_key") + .withCopyFileToContainer( + MountableFile.forClasspathResource(RESOURCES + "/disable_force_command.sh", 0x1ff), + "/etc/sftp.d/disable_force_command.sh") + .withExposedPorts(22); + sftpHost = sftp; + sftpHost.start(); + this.user = "foo"; + this.hostname = "localhost"; + this.port = sftpHost.getMappedPort(22); + keyFile = Files.createTempFile("sftpperf", ".key"); + try (InputStream in = getClass().getResourceAsStream(RESOURCES + "/rsa_key")) { + Files.copy(in, keyFile, StandardCopyOption.REPLACE_EXISTING); + } + } else { + this.user = user; + this.hostname = hostname; + this.port = port; + keyFile = userKey; + } + initialFile = Files.createTempFile("sftpperf", ".bin"); + generateFile(initialFile, FILE_SIZE); + initialFile = initialFile.toAbsolutePath(); + keyFile = keyFile.toAbsolutePath(); + } + + public String getHost() { + return hostname; + } + + public String getUser() { + return user; + } + + public int getPort() { + return port; + } + + public Path getFile() { + return initialFile; + } + + public Path getPrivateKey() { + return keyFile; + } + + public void stop() { + try { + File f = initialFile.toFile(); + if (!f.delete() && f.exists()) { + f.deleteOnExit(); + } + } finally { + if (sftpHost != null) { + sftpHost.close(); // Synonym with stop() + } + } + } +} diff --git a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/CatBenchmark.java b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/CatBenchmark.java new file mode 100644 index 000000000..32ba6ae3a --- /dev/null +++ b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/CatBenchmark.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.benchmarks.sftp.upload; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.KeyPair; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ChannelExec; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.channel.StreamingChannel.Streaming; +import org.apache.sshd.common.io.IoOutputStream; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.client.SftpClientFactory; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.TearDown; + +public final class CatBenchmark { + + private CatBenchmark() { + super(); + } + + public static class SftpUploadBenchmark extends CommonState { + + private SshClient sshClient; + private ClientSession sshSession; + private ChannelExec exec; + private SftpClient sftpClient; + + public SftpUploadBenchmark() { + super(); + } + + @Override + protected void prepare() throws Exception { + // Create a client, session and SftpClient + sshClient = createClient(); + } + + private SshClient createClient() throws Exception { + SshClient client = SshClient.setUpDefaultClient(); + if ("jsch".equals(settings)) { + // Same as JSch default + client.setCipherFactoriesNames("aes128-ctr"); + client.setMacFactoriesNames("hmac-sha2-256-etm@openssh.com"); + } + client.setServerKeyVerifier((s, a, k) -> true); + // Load the user key + try (InputStream in = Files.newInputStream(Paths.get(sftpKey), StandardOpenOption.READ)) { + Iterable clientKeys = SecurityUtils.loadKeyPairIdentities(null, null, in, null); + client.setKeyIdentityProvider(s -> clientKeys); + } + client.start(); + return client; + } + + @Override + protected void endTests() throws Exception { + if (sshClient != null) { + sshClient.close(false); + } + } + + @Override + protected void setupSession() throws Exception { + sshSession = sshClient.connect(sftpUser, sftpHost, Integer.parseInt(sftpPort)).verify().getClientSession(); + sshSession.auth().verify(2000); + sftpClient = SftpClientFactory.instance().createSftpClient(sshSession); + Thread.sleep(1000); + } + + @Override + protected void downloadTo(Path file) throws IOException { + try (InputStream in = sftpClient.read("/home/" + sftpUser + "/upload/testfile.bin")) { + Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING); + } + } + + @Override + protected void closeSession() throws IOException { + sftpClient.remove("/home/" + sftpUser + "/upload/testfile.bin"); + if (sshSession != null) { + sshSession.close(false); + sshSession = null; + } + } + + @Setup(Level.Invocation) + public void createChannel() throws Exception { + exec = sshSession.createExecChannel("cat > upload/testfile.bin"); + exec.setStreaming(Streaming.Async); + exec.open().verify(); + } + + @TearDown(Level.Invocation) + public void closeChannel() throws IOException { + exec.close(false); + } + + @Benchmark + public void catUpload() throws Exception { + try (InputStream localInputStream = Files.newInputStream(testData)) { + IoOutputStream out = exec.getAsyncIn(); + byte[] buffer = new byte[32 * 1024]; + int offset = 0; + int length = buffer.length; + for (;;) { + int n = localInputStream.read(buffer, offset, length); + if (n < 0) { + break; + } + offset += n; + length -= n; + if (length == 0) { + out.writeBuffer(new ByteArrayBuffer(buffer)).verify(1000); + offset = 0; + length = buffer.length; + } + } + if (offset > 0) { + out.writeBuffer(new ByteArrayBuffer(buffer, 0, offset)).verify(1000); + } + out.close(false); + } + } + } + +} diff --git a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/CommonState.java b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/CommonState.java new file mode 100644 index 000000000..6b321f22a --- /dev/null +++ b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/CommonState.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.benchmarks.sftp.upload; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@State(Scope.Benchmark) +public abstract class CommonState { + + private static final Logger LOG = LoggerFactory.getLogger(CommonState.class); + + @Param({ "jsch", "sshd" }) + protected String settings = ""; + + @Param("") + protected String sftpHost; + @Param("") + protected String sftpPort; + @Param("") + protected String sftpUser; + @Param("") + protected String sftpKey; + @Param("") + protected String initialFile; + + protected Path testData; + + protected CommonState() { + super(); + } + + protected abstract void downloadTo(Path localPath) throws IOException; + + protected void downloadAndVerify(Path original) throws IOException { + // Download what got uploaded and verify the two files are identical. + Path downloaded = Files.createTempFile("dwnld", "bin"); + try { + downloadTo(downloaded); + if (!equalFiles(original, downloaded)) { + LOG.error("File got corrupted in upload/download"); + throw new IOException("Files differ"); + } + } finally { + File f = downloaded.toFile(); + if (!f.delete() && f.isFile()) { + f.deleteOnExit(); + } + } + } + + private boolean equalFiles(Path a, Path b) throws IOException { + try (InputStream inA = new BufferedInputStream(Files.newInputStream(a)); + InputStream inB = new BufferedInputStream(Files.newInputStream(b))) { + int byteA = inA.read(); + int byteB = inB.read(); + while (byteA >= 0 || byteB >= 0) { + if (byteA != byteB) { + return false; + } + if (byteA < 0) { + return true; + } + byteA = inA.read(); + byteB = inB.read(); + } + } + return true; + } + + protected abstract void prepare() throws Exception; + + @Setup(Level.Trial) + public void setup() throws Exception { + testData = Paths.get(initialFile); + prepare(); + } + + protected void endTests() throws Exception { + // By default nothing. + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + endTests(); + } + + protected abstract void setupSession() throws Exception; + + @Setup(Level.Iteration) + public void startSsh() throws Exception { + setupSession(); + } + + protected abstract void closeSession() throws Exception; + + @TearDown(Level.Iteration) + public void endSsh() throws Exception { + downloadAndVerify(Paths.get(initialFile)); + closeSession(); + } +} diff --git a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/JSchBenchmark.java b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/JSchBenchmark.java new file mode 100644 index 000000000..e6ba9a40a --- /dev/null +++ b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/JSchBenchmark.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.benchmarks.sftp.upload; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSch; +// import com.jcraft.jsch.Logger; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.SftpException; +import org.openjdk.jmh.annotations.Benchmark; + +public final class JSchBenchmark { + + private JSchBenchmark() { + super(); + } + + public static class SftpUploadBenchmark extends CommonState { + + private JSch sshClient; + private Session sshSession; + private ChannelSftp sftpClient; + + public SftpUploadBenchmark() { + super(); + } + + @Override + protected void prepare() throws Exception { + sshClient = createClient(); + } + + private JSch createClient() throws Exception { + if ("jsch".equals(settings)) { + JSch.setConfig("cipher.c2s", "aes128-ctr"); + } else { + JSch.setConfig("cipher.c2s", "chacha20-poly1305@openssh.com"); // Needs BC + } + JSch client = new JSch(); + client.addIdentity(sftpKey); + return client; + } + + @Override + public void setupSession() throws Exception { + sshSession = sshClient.getSession(sftpUser, sftpHost, Integer.parseInt(sftpPort)); + sshSession.setConfig("StrictHostKeyChecking", "no"); + sshSession.connect(); + sftpClient = (ChannelSftp) sshSession.openChannel("sftp"); + sftpClient.connect(); + Thread.sleep(1000); + } + + @Override + protected void downloadTo(Path localPath) throws IOException { + try (OutputStream out = Files.newOutputStream(localPath)) { + sftpClient.get("/home/" + sftpUser + "/upload/testfile.bin", out); + } catch (SftpException e) { + throw new IOException("Download failed", e); + } + } + + @Override + protected void closeSession() throws Exception { + if (sftpClient != null) { + sftpClient.disconnect(); + sftpClient = null; + } + if (sshSession != null) { + sshSession.disconnect(); + sshSession = null; + } + } + + @Benchmark + public void sftpClientWrite() throws Exception { + sftpClient.put(initialFile, "/home/" + sftpUser + "/upload/testfile.bin"); + } + + } + +} diff --git a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/README.md b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/README.md new file mode 100644 index 000000000..f2ccfce08 --- /dev/null +++ b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/README.md @@ -0,0 +1,18 @@ +# SFTP benchmarks + +This is a suite of benchmarks for file uploading via SFTP. The benchmarks time the upload of a 20MB file to an SFTP server. + +The benchmarks can be run using the `RunBenchmarks` runner with option `--run SftpUploadBenchmark`. + +The benchmark suite has three parts: + +* `CatUpload` uploads the file not via SFTP but using the equivalent of `ssh user@host 'cat > upload/testfile.bin' < localfile.bin`. This gives a crude baseline that should always be faster than SFTP. First, there is no overhead for SFTP message headers, so it should need a little less SSH packets, and second, there are no SFTP ACKs at all. + +* `JschBenchmark` uploads the file via Jsch. + +* `SshBenchmark` uploads the file in various ways using Apache MINA sshd. + +All benchmarks time the raw time for the file transfer. SSH session setup and SFTP session setup are not measured. All benchmarks also download the uploaded file and compare it to the original, and fail on differences. This sanity check is also not part of the timing. + +Benchmarks are run twice, once with the default cipher settings of JSch (`aes128-ctr` cipher and `hmac-sha2-256-etm@openssh.com` MAC), and once with the default cipher of Apache MINA sshd (`chacha20-poly1305@openssh.com`). + diff --git a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/SshBenchmark.java b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/SshBenchmark.java new file mode 100644 index 000000000..f8fadaac0 --- /dev/null +++ b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/sftp/upload/SshBenchmark.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.benchmarks.sftp.upload; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.KeyPair; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.sftp.SftpModuleProperties; +import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.client.SftpClientFactory; +import org.apache.sshd.sftp.client.fs.SftpFileSystem; +import org.apache.sshd.sftp.client.impl.SftpOutputStreamAsync; +import org.openjdk.jmh.annotations.Benchmark; + +public final class SshBenchmark { + + private SshBenchmark() { + super(); + } + + public static class SftpUploadBenchmark extends CommonState { + + private SshClient sshClient; + private ClientSession sshSession; + private SftpClient sftpClient; + private SftpFileSystem sftpFs; + + public SftpUploadBenchmark() { + super(); + } + + @Override + protected void prepare() throws Exception { + // Create a client, session and SftpClient + sshClient = createClient(); + } + + private SshClient createClient() throws Exception { + SshClient client = SshClient.setUpDefaultClient(); + if ("jsch".equals(settings)) { + // Same as JSch default + client.setCipherFactoriesNames("aes128-ctr"); + client.setMacFactoriesNames("hmac-sha2-256-etm@openssh.com"); + } + client.setServerKeyVerifier((s, a, k) -> true); + // Load the user key + try (InputStream in = Files.newInputStream(Paths.get(sftpKey), StandardOpenOption.READ)) { + Iterable clientKeys = SecurityUtils.loadKeyPairIdentities(null, null, in, null); + client.setKeyIdentityProvider(s -> clientKeys); + } + client.start(); + return client; + } + + @Override + protected void endTests() throws Exception { + if (sshClient != null) { + sshClient.close(false); + } + } + + @Override + protected void setupSession() throws Exception { + sshSession = sshClient.connect(sftpUser, sftpHost, Integer.parseInt(sftpPort)).verify().getClientSession(); + sshSession.auth().verify(2000); + sftpClient = SftpClientFactory.instance().createSftpClient(sshSession); + sftpFs = SftpClientFactory.instance().createSftpFileSystem(sshSession); + Thread.sleep(1000); + } + + @Override + protected void downloadTo(Path file) throws IOException { + try (InputStream in = sftpClient.read("/home/" + sftpUser + "/upload/testfile.bin")) { + Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING); + } + } + + @Override + protected void closeSession() throws IOException { + sftpClient.remove("/home/" + sftpUser + "/upload/testfile.bin"); + if (sshSession != null) { + sshSession.close(false); + sshSession = null; + } + } + + @Benchmark + public void sftpClientPut() throws IOException { + sftpClient.put(testData, "/home/" + sftpUser + "/upload/testfile.bin", SftpClient.OpenMode.Create, + SftpClient.OpenMode.Truncate, SftpClient.OpenMode.Write); + } + + @Benchmark + public void sftpClientWrite() throws IOException { + try (OutputStream out = sftpClient.write("/home/" + sftpUser + "/upload/testfile.bin", SftpClient.OpenMode.Create, + SftpClient.OpenMode.Truncate, SftpClient.OpenMode.Write)) { + Files.copy(testData, out); + } + } + + @Benchmark + public void sftpClientTransfer() throws IOException { + try (SftpOutputStreamAsync out = (SftpOutputStreamAsync) sftpClient.write( + "/home/" + sftpUser + "/upload/testfile.bin", SftpClient.OpenMode.Create, SftpClient.OpenMode.Truncate, + SftpClient.OpenMode.Write)) { + try (InputStream localInputStream = Files.newInputStream(testData)) { + out.transferFrom(localInputStream); + } + } + } + + @Benchmark + public void sftpFileSystemWrite() throws IOException { + Path remoteFile = sftpFs.getPath("/home/" + sftpUser + "/upload", "testfile.bin"); + Files.copy(testData, remoteFile, StandardCopyOption.REPLACE_EXISTING); + } + + @Benchmark + public void sftpStream32k() throws IOException { + try (InputStream localInputStream = Files.newInputStream(testData); + OutputStream remoteOutputStream = sftpClient.write("/home/" + sftpUser + "/upload/testfile.bin")) { + IoUtils.copy(localInputStream, remoteOutputStream, 32 * 1024); + } + } + + @Benchmark + public void sftpTransferFrom() throws IOException { + SftpModuleProperties.COPY_BUF_SIZE.set(sshSession, 32 * 1024); + try (FileChannel readableChannel = FileChannel.open(testData, StandardOpenOption.READ); + FileChannel writeableChannel = sftpClient.openRemoteFileChannel( + "/home/" + sftpUser + "/upload/testfile.bin", SftpClient.OpenMode.Create, + SftpClient.OpenMode.Truncate, SftpClient.OpenMode.Write)) { + long position = 0; + long toWrite = readableChannel.size(); + do { + long transferred = writeableChannel.transferFrom(readableChannel, position, toWrite); + position += transferred; + toWrite -= transferred; + } while (toWrite > 0); + } + } + } + +} diff --git a/sshd-benchmarks/src/main/resources/logback.xml b/sshd-benchmarks/src/main/resources/logback.xml new file mode 100644 index 000000000..960baf26a --- /dev/null +++ b/sshd-benchmarks/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + + + \ No newline at end of file diff --git a/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/disable_force_command.sh b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/disable_force_command.sh new file mode 100644 index 000000000..befe70f39 --- /dev/null +++ b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/disable_force_command.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Remove the ForceCommand and the chroot jail to enable the cat upload benchmark +sed -i 's/ForceCommand internal-sftp//g' /etc/ssh/sshd_config +sed -i 's/ChrootDirectory %h//g' /etc/ssh/sshd_config diff --git a/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/ed25519_key b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/ed25519_key new file mode 100644 index 000000000..c599ecb6f --- /dev/null +++ b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/ed25519_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBbwMG5AH0I3R787lv7XKYJkA5p5PoRSCC3YNlHheI+iwAAAIhHK6krRyup +KwAAAAtzc2gtZWQyNTUxOQAAACBbwMG5AH0I3R787lv7XKYJkA5p5PoRSCC3YNlHheI+iw +AAAECegC4DHWX/fh2doVuwKRnSsBRWLsgptHoiJvir77yQGVvAwbkAfQjdHvzuW/tcpgmQ +Dmnk+hFIILdg2UeF4j6LAAAAAAECAwQF +-----END OPENSSH PRIVATE KEY----- diff --git a/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/ed25519_key.pub b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/ed25519_key.pub new file mode 100644 index 000000000..ced937903 --- /dev/null +++ b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/ed25519_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFvAwbkAfQjdHvzuW/tcpgmQDmnk+hFIILdg2UeF4j6L diff --git a/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/rsa_key b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/rsa_key new file mode 100644 index 000000000..b5b70aeaa --- /dev/null +++ b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/rsa_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAxY3Hr1SqpJIQ9SbFfGMGweVy8jg2TEH3GC1K0LudQHJwogRi ++debdCqUtuSITbpPhjkeZSk9rq198d6RhT6TQmY9J8wLL2/+VXZk/rMVEEjeXQS3 +ImRnL2vVmkAunv6LwfDGHIovkhwj3/lqGWphDAKnHyXusPDwQ3N4LFGgxwXvRGqc +lzmP8H+KDWaaPapk1AZCBIoD4JbL8faBtLNU01r+pB3sIKvfsPJ5DxPErThfrPuD +qIbA3axEqFlgX4aVl3yMnSWjfhLhO7xD3YwrtUhannHt8pZQo5FkwCGWDpkG3xs+ +qK3ZACrhMFMTvPuDS83jDtEzNd5KYb4KnkOPMQIDAQABAoIBAQCE5GktgrD/39pU +b25tzFehW25FjpbIGZ/UvbMUUwDnd5RZCMZj9yv1qyc7GOSwFOKmEgpmVqXNuZt9 +dxFBJuT8x7Xf7Zygnp/icbBivakvuTUMMb3X/t6CwfGAwCgcgHMXVZaPYE275f4k +Dq3Wxv7di3NMusGkeY/GcAipF4gmGKKe7Ck1ifRypF2cDJsgTtsoFUHNNKfnT3gf +OcJsVLRl0osbsxdqU+Tep46+jHrNt8J9n2VeRNRIqGHj0CkNdpLQOs+MjvIO3Hgq +9NUxwIExwaPnBpTLlWwfemCz3JQnlAineMbYBGa1tpAA3Iw56NWcNbiOPyUyffbI +wBC4r1uZAoGBAPESsergFD+ontChEI+h38oM/D9DKCObZR2kz6WArZ54i1dJWOgh +HCsuxgPjxmaddPKghfNhUORdZBynuS5G7n6BfItNilDiFm2KBk12d38OVovUFo1Q +r5akclKf0kFxHt5TzHIrNAv7B4OF0Uk3kuDHM7ITX3qDpTSBLlzPAUUHAoGBANHJ +QIPmuF2q+PXnnSgdEyiETfl/IqUTXQyxda8kRIPJKKHZKPHZePhgJKUq9VP32PrP +AxIBNrS3Netsp+EAApj09hmWUcgJRIU1/wjpVGqUmguYgh8nVFOPDudOJD5ltQ/A +enzQ19IkGroaQB8CBGZsPaBAvqRZ5PLbm+BZEPQHAoGAblaMMGCXY/udlQfjOJpy +f1wqKBpoyMNbKJJCqBGZZaruu+jKVJSy++DQqP8b0+PFnzdxl8+24o8MP0FVNKUq +i6RgiLHY2ORiN4ixEctjLjg1zJIqMEv50g06di7IYUORSVk5fhfgHourCLu66rQQ ++eiy9JKBZOXUO4/U1I26mwkCgYAhfuCuLsiBLCtUGAcfwISuk3FfxMzjTpQs0qjX +rhLCd/vk26eN9gs6nR88v/8ryQb8BNGYrljtwdL6I/8qDbZcdcBVlYq5RcGLA3QV +GCxCWDfAYjlkgAMW1GCsze07iUG/ohvskevjwaAC1u4mBUxujhnI3I2T8EZ+AFKD +H7V1QQKBgQDNt+zjSdLtA9AczxDwWmi5SbS+k+nGbi6AQO9i73wky/wxx7FonfWS +2skkOUIst3HBc0Oz+CJTfNFQK6GVqtzTdlZFhMYS0ua1Djd6q6S648+K0cieY4r5 +5irivHYVN8t7lBcvbA7E7yD6dHXSHsn6yOLTrV382qRfJTbxG7ZVWA== +-----END RSA PRIVATE KEY----- diff --git a/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/rsa_key.pub b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/rsa_key.pub new file mode 100644 index 000000000..efecd1b08 --- /dev/null +++ b/sshd-benchmarks/src/main/resources/org/apache/sshd/benchmarks/rsa_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFjcevVKqkkhD1JsV8YwbB5XLyODZMQfcYLUrQu51AcnCiBGL515t0KpS25IhNuk+GOR5lKT2urX3x3pGFPpNCZj0nzAsvb/5VdmT+sxUQSN5dBLciZGcva9WaQC6e/ovB8MYcii+SHCPf+WoZamEMAqcfJe6w8PBDc3gsUaDHBe9EapyXOY/wf4oNZpo9qmTUBkIEigPglsvx9oG0s1TTWv6kHewgq9+w8nkPE8StOF+s+4OohsDdrESoWWBfhpWXfIydJaN+EuE7vEPdjCu1SFqece3yllCjkWTAIZYOmQbfGz6ordkAKuEwUxO8+4NLzeMO0TM13kphvgqeQ48x user01