JCLOUDS-847: Poor upload performance for putBlob

This change improves the performance of writing to sockets with the
default Java URL connection HTTP client, by enlarging the buffer used
for socket writes from an implicit hard-coded 4KB / 8KB buffer to a
configurable 32KB buffer.

The buffer size is now controlled by the following property with the
following default value:

jclouds.output-socket-buffer-size: 32768

The implementation is based on a variant of ByteStreams.copy (written as
ByteStreams2.copy) which accepts the buffer size as an argument, unlike
the original Guava code that uses a hard-coded size.

The change was done directly within the loop that copies the input
stream to the output stream, and not by wrapping a BufferedOutputStream
around the existing output stream, in order to avoid copying the payload
twice.

On some platforms this change can improve both the putBlob throughput
and the total CPU consumption.
This commit is contained in:
Dori Polotsky 2019-04-22 22:55:09 +03:00 committed by Andrew Gaul
parent d51d6e44bc
commit 1c57d07f70
7 changed files with 49 additions and 6 deletions

View File

@ -325,7 +325,15 @@ public final class Constants {
* </code>
*/
public static final String PROPERTY_TIMEOUTS_PREFIX = "jclouds.timeouts.";
/**
* Integer property. Default (32768).
* <p/>
* Buffer size for socket write (currently honored only by the default
* Java URL HTTP client, JavaUrlHttpCommandExcecutorService).
*/
public static final String PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE = "jclouds.output-socket-buffer-size";
/**
* Boolean property. Default (true).
* <p/>

View File

@ -27,6 +27,7 @@ import static org.jclouds.Constants.PROPERTY_MAX_CONNECTIONS_PER_HOST;
import static org.jclouds.Constants.PROPERTY_MAX_CONNECTION_REUSE;
import static org.jclouds.Constants.PROPERTY_MAX_PARALLEL_DELETES;
import static org.jclouds.Constants.PROPERTY_MAX_SESSION_FAILURES;
import static org.jclouds.Constants.PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE;
import static org.jclouds.Constants.PROPERTY_PRETTY_PRINT_PAYLOADS;
import static org.jclouds.Constants.PROPERTY_SCHEDULER_THREADS;
import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
@ -89,6 +90,7 @@ public abstract class BaseApiMetadata implements ApiMetadata {
props.setProperty(PROPERTY_MAX_PARALLEL_DELETES, numUserThreads + "");
props.setProperty(PROPERTY_IDEMPOTENT_METHODS, "DELETE,GET,HEAD,OPTIONS,PUT");
props.setProperty(PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE, 32768 + "");
return props;
}

View File

@ -22,6 +22,7 @@ import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.net.HttpHeaders.HOST;
import static com.google.common.net.HttpHeaders.USER_AGENT;
import static org.jclouds.Constants.PROPERTY_IDEMPOTENT_METHODS;
import static org.jclouds.Constants.PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE;
import static org.jclouds.Constants.PROPERTY_USER_AGENT;
import static org.jclouds.http.HttpUtils.filterOutContentHeaders;
import static org.jclouds.io.Payloads.newInputStreamPayload;
@ -50,6 +51,7 @@ import org.jclouds.http.HttpUtils;
import org.jclouds.http.IOExceptionRetryHandler;
import org.jclouds.http.handlers.DelegatingErrorHandler;
import org.jclouds.http.handlers.DelegatingRetryHandler;
import org.jclouds.io.ByteStreams2;
import org.jclouds.io.ContentMetadataCodec;
import org.jclouds.io.MutableContentMetadata;
import org.jclouds.io.Payload;
@ -58,7 +60,6 @@ import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableMultimap.Builder;
import com.google.common.io.ByteStreams;
import com.google.common.io.CountingOutputStream;
import com.google.inject.Inject;
@ -69,6 +70,7 @@ public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorSe
protected final HostnameVerifier verifier;
@Inject(optional = true)
protected Supplier<SSLContext> sslContextSupplier;
protected final int outputSocketBufferSize;
protected final String userAgent;
@Inject
@ -77,6 +79,7 @@ public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorSe
DelegatingErrorHandler errorHandler, HttpWire wire, @Named("untrusted") HostnameVerifier verifier,
@Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider, Function<URI, Proxy> proxyForURI,
@Named(PROPERTY_IDEMPOTENT_METHODS) String idempotentMethods,
@Named(PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE) int outputSocketBufferSize,
@Named(PROPERTY_USER_AGENT) String userAgent) {
super(utils, contentMetadataCodec, retryHandler, ioRetryHandler, errorHandler, wire, idempotentMethods);
if (utils.getMaxConnections() > 0) {
@ -86,6 +89,7 @@ public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorSe
this.verifier = checkNotNull(verifier, "verifier");
this.proxyForURI = checkNotNull(proxyForURI, "proxyForURI");
this.userAgent = userAgent;
this.outputSocketBufferSize = outputSocketBufferSize;
}
@Override
@ -295,7 +299,7 @@ public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorSe
CountingOutputStream out = new CountingOutputStream(connection.getOutputStream());
InputStream is = payload.openStream();
try {
ByteStreams.copy(is, out);
ByteStreams2.copy(is, out, outputSocketBufferSize);
} catch (IOException e) {
logger.error(e, "error after writing %d/%s bytes to %s", out.getCount(), lengthDesc, connection.getURL());
throw e;

View File

@ -17,11 +17,13 @@
package org.jclouds.io;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.jclouds.util.Closeables2.closeQuietly;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.google.common.annotations.Beta;
import com.google.common.hash.HashCode;
@ -31,6 +33,8 @@ import com.google.common.io.ByteStreams;
@Beta
public class ByteStreams2 {
private static final int INPUT_STREAM_READ_END_OF_STREAM_INDICATOR = -1;
public static HashCode hashAndClose(InputStream input, HashFunction hashFunction) throws IOException {
checkNotNull(input, "input");
checkNotNull(hashFunction, "hashFunction");
@ -51,4 +55,23 @@ public class ByteStreams2 {
closeQuietly(input);
}
}
public static long copy(InputStream from, OutputStream to, int bufferSize) throws IOException {
checkNotNull(from, "from");
checkNotNull(to, "to");
checkArgument(bufferSize >= 1, "bufferSize must be >= 1");
byte[] buf = new byte[bufferSize];
long total = 0L;
while (true) {
int len = from.read(buf);
if (len == INPUT_STREAM_READ_END_OF_STREAM_INDICATOR) {
return total;
}
to.write(buf, 0, len);
total += (long)len;
}
}
}

View File

@ -17,6 +17,7 @@
package org.jclouds.http.internal;
import static org.jclouds.Constants.PROPERTY_IDEMPOTENT_METHODS;
import static org.jclouds.Constants.PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE;
import static org.jclouds.Constants.PROPERTY_USER_AGENT;
import java.net.Proxy;
@ -92,10 +93,11 @@ public class TrackingJavaUrlHttpCommandExecutorService extends JavaUrlHttpComman
@Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider, Function<URI, Proxy> proxyForURI,
List<HttpCommand> commandsInvoked,
@Named(PROPERTY_IDEMPOTENT_METHODS) String idempotentMethods,
@Named(PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE) int outputSocketBufferSize,
@Named(PROPERTY_USER_AGENT) String userAgent)
throws SecurityException, NoSuchFieldException {
super(utils, contentMetadataCodec, retryHandler, ioRetryHandler, errorHandler, wire, verifier,
untrustedSSLContextProvider, proxyForURI, idempotentMethods, userAgent);
untrustedSSLContextProvider, proxyForURI, idempotentMethods, outputSocketBufferSize, userAgent);
this.commandsInvoked = commandsInvoked;
}

View File

@ -17,6 +17,7 @@
package org.jclouds.dynect.v3.config;
import static org.jclouds.Constants.PROPERTY_IDEMPOTENT_METHODS;
import static org.jclouds.Constants.PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE;
import static org.jclouds.Constants.PROPERTY_USER_AGENT;
import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
import static org.jclouds.rest.config.BinderUtils.bindHttpApi;
@ -106,10 +107,11 @@ public class DynECTHttpApiModule extends HttpApiModule<DynECTApi> {
DelegatingErrorHandler errorHandler, HttpWire wire, @Named("untrusted") HostnameVerifier verifier,
@Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider, Function<URI, Proxy> proxyForURI,
@Named(PROPERTY_IDEMPOTENT_METHODS) String idempotentMethods,
@Named(PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE) int outputSocketBufferSize,
@Named(PROPERTY_USER_AGENT) String userAgent)
throws SecurityException, NoSuchFieldException {
super(utils, contentMetadataCodec, retryHandler, ioRetryHandler, errorHandler, wire, verifier,
untrustedSSLContextProvider, proxyForURI, idempotentMethods, userAgent);
untrustedSSLContextProvider, proxyForURI, idempotentMethods, outputSocketBufferSize, userAgent);
}
/**

View File

@ -17,6 +17,7 @@
package org.jclouds.profitbricks.http;
import static org.jclouds.Constants.PROPERTY_IDEMPOTENT_METHODS;
import static org.jclouds.Constants.PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE;
import static org.jclouds.Constants.PROPERTY_USER_AGENT;
import static org.jclouds.util.Closeables2.closeQuietly;
@ -71,9 +72,10 @@ public class ResponseStatusFromPayloadHttpCommandExecutorService extends JavaUrl
@Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider, Function<URI, Proxy> proxyForURI,
ParseSax<ServiceFault> faultHandler,
@Named(PROPERTY_IDEMPOTENT_METHODS) String idempotentMethods,
@Named(PROPERTY_OUTPUT_SOCKET_BUFFER_SIZE) int outputSocketBufferSize,
@Named(PROPERTY_USER_AGENT) String userAgent) {
super(utils, contentMetadataCodec, retryHandler, ioRetryHandler, errorHandler, wire, verifier, untrustedSSLContextProvider, proxyForURI,
idempotentMethods, userAgent);
idempotentMethods, outputSocketBufferSize, userAgent);
this.faultHandler = faultHandler;
}