|
本机 macOS 使用 Caddy 2.6.2 的 respond 功能跑一个 HTTP/2 服务,然后本机开 64 线程循环,每个线程发一万请求,Async-HTTP-Client、Jetty HTTPcClient、JDK HTTP client、Apache HTTP client、OkHttp3 的 HTTP/2 性能对比,全都使用异步风格的 API。

基于 Netty 的 AHC 性能最好,其次是自己撸网络编程的 Jetty HTTP client,JDK 的 HTTP client 虽然是标配但是性能有点惨,Apache HttpComponents 有廉颇老矣尚能饭否的感觉,使用 Kotlin 编写的 OkHttp3 居然性能垫底,让人意外,后来调查了下,发现是 OkHttp3 为 Android 应用开发设计,没打算用在服务端开发,其性能差的原因是因为其异步 API 默认限制了一共64并发,per-host 只有 5 并发。在放松这个限制后,OkHttp3 的异步风格 API 性能接近 JDK HTTP client,而 OkHttp3 的同步风格 API 性能略高于 JDK HTTP client。
总结下来,虽然 Async-HTTP-Client 作者刚刚宣布找下一任维护者,但依托于牛逼的 Netty 依然是 Java 的 HTTP client 之王啊!
另外, JDK HTTPClient 的性能之坑早有人分析,其实现比较惨:
软件包版本:
- org.asynchttpclient:async-http-client:2.12.3
- org.eclipse.jetty:jetty-client:11.0.12
- OpenJDK Termurin-17+35
- org.apache.httpcomponents.client5:httpclient5:5.2-beta1 (低版本有个 TLS 相关的问题)
- com.squareup.okhttp3:okhttp:5.0.0-alpha.10 (alpha.1 修正了一个 connection reset 的问题)
测试代码如下,供参考下各个库的 API 风格。
my.HttpClient 接口,方便统一调用做压测:
import java.io.Closeable;
import java.net.URI;
import java.util.concurrent.CompletionStage;
public interface HttpClient extends Closeable {
CompletionStage<byte[]> httpPost(URI uri, byte[] body);
}Async-HTTP-Client
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.concurrent.CompletionStage;
import org.asynchttpclient.AsyncHttpClient;
import static org.asynchttpclient.Dsl.*;
public class AHCHttpClient implements HttpClient {
private final AsyncHttpClient httpClient;
private final String basicAuth;
public AHCHttpClient(String user, String password, Duration connectTimeout, Duration requestTimeout) {
httpClient = asyncHttpClient(config()
.setConnectTimeout((int) connectTimeout.toMillis())
.setReadTimeout((int) requestTimeout.toMillis())
.setRequestTimeout((int) requestTimeout.toMillis()));
basicAuth = &#34;Basic &#34; + Base64.getEncoder().encodeToString((user + &#34;:&#34; + password).getBytes(StandardCharsets.UTF_8));
}
@Override
public CompletionStage<byte[]> httpPost(URI uri, byte[] body) {
return httpClient.preparePost(uri.toString())
.setHeader(&#34;Authorization&#34;, basicAuth)
.setHeader(&#34;Content-Type&#34;, &#34;application/json&#34;)
.setBody(body)
.execute()
.toCompletableFuture()
.thenApply(r -> r.getResponseBodyAsBytes());
}
@Override
public void close() throws IOException {
httpClient.close();
}
}<hr/>Jetty HTTP Client
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.BytesRequestContent;
public class JettyHttpClient implements HttpClient {
private final HttpClient httpClient;
private final String basicAuth;
private final long requestTimeout;
public JettyHttpClient(String user, String password, Duration connectTimeout, Duration requestTimeout) {
httpClient = new HttpClient();
httpClient.setConnectTimeout(connectTimeout.toMillis());
basicAuth = &#34;Basic &#34; + Base64.getEncoder().encodeToString((user + &#34;:&#34; + password).getBytes(StandardCharsets.UTF_8));
this.requestTimeout = requestTimeout.toMillis();
try {
httpClient.start();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
public CompletionStage<byte[]> httpPost(URI uri, byte[] body) {
CompletableFuture<byte[]> future = new CompletableFuture<>();
httpClient.POST(uri)
.timeout(requestTimeout, TimeUnit.MILLISECONDS)
.headers(m -> {
m.add(&#34;Authorization&#34;, basicAuth);
})
.body(new BytesRequestContent(&#34;application/json&#34;, body))
.send(new BufferingResponseListener(8 * 1024 * 1024 /* 8 MiB */) {
@Override
public void onComplete(Result result) {
if (result.isSucceeded()) {
try {
future.complete(getContent());
} catch (Exception ex) {
future.completeExceptionally(ex);
}
} else {
future.completeExceptionally(result.getFailure());
}
}
});
return future;
}
@Override
public void close() throws IOException {
try {
httpClient.stop();
} catch (Exception ex) {
throw new IOException(ex);
}
}
}<hr/>JDK HTTP Client
public class JdkHttpClient implements my.HttpClient {
private final HttpClient httpClient;
private final Duration requestTimeout;
private final String basicAuth;
private final ExecutorService executor = Executors.newFixedThreadPool(2, new ThreadFactory() {
ThreadFactory delegate = Executors.defaultThreadFactory();
@Override
public Thread newThread(Runnable r) {
Thread t = delegate.newThread(r);
t.setName(&#34;JdkHttpClient-&#34; + t.getName());
t.setDaemon(true);
return t;
}
});
public JdkHttpClient(String user, String password, Duration connectTimeout, Duration requestTimeout) {
httpClient = HttpClient.newBuilder()
.connectTimeout(connectTimeout)
.executor(executor) // https://sudonull.com/post/61032-The-Story-of-How-One-HTTP-2-Client-Engineer-Overclocked-JUG-Ru-Group-Blog
.build();
this.requestTimeout = requestTimeout;
basicAuth = &#34;Basic &#34; + Base64.getEncoder().encodeToString((user + &#34;:&#34; + password).getBytes(StandardCharsets.UTF_8));
}
@Override
public CompletionStage<byte[]> httpPost(URI uri, byte[] body) {
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.header(&#34;Authorization&#34;, basicAuth)
.header(&#34;Content-Type&#34;, &#34;application/json&#34;)
.POST(BodyPublishers.ofByteArray(body))
.timeout(requestTimeout)
.build();
return httpClient.sendAsync(request, BodyHandlers.ofByteArray())
.thenApply(HttpResponse::body);
}
@Override
public void close() throws IOException {
executor.shutdown();
}
}
<hr/>Apache HTTP Client
(经网友指点,调整了下设置,性能提高了一点,比 JDK HTTP client 好一点,还是比不上 AHC 和 Jetty HTTP client。)
@@ -36,6 +37,10 @@ public class ApacheHttpClient implements HttpClient {
httpClient = HttpAsyncClientBuilder.create()
.useSystemProperties()
.setDefaultRequestConfig(config)
+ .setConnectionManager(PoolingAsyncClientConnectionManagerBuilder.create()
+ .setMaxConnTotal(1000) // default 25
+ .setMaxConnPerRoute(1000) // default 5
+ .build())
.build();
httpClient.start();
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.io.CloseMode;
// Requires >= 5.2-alpha1 https://issues.apache.org/jira/browse/HTTPCLIENT-2182
@Deprecated
public class ApacheHttpClient implements HttpClient {
private final CloseableHttpAsyncClient httpClient;
private final String basicAuth;
public ApacheHttpClient(String user, String password, long connectTimeoutMs, long requestTimeoutMs) {
basicAuth = &#34;Basic &#34; + Base64.getEncoder().encodeToString((user + &#34;:&#34; + password).getBytes(StandardCharsets.UTF_8));
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS)
.setConnectionRequestTimeout(requestTimeoutMs, TimeUnit.MILLISECONDS)
.setResponseTimeout(requestTimeoutMs, TimeUnit.MILLISECONDS)
.build();
httpClient = HttpAsyncClientBuilder.create()
.useSystemProperties()
.setDefaultRequestConfig(config)
.build();
httpClient.start();
}
@Override
public CompletionStage<byte[]> httpPost(URI uri, byte[] body) {
SimpleHttpRequest request = SimpleRequestBuilder.post(uri)
.setHeader(&#34;Authorization&#34;, basicAuth)
.setBody(body, ContentType.APPLICATION_JSON)
.build();
CompletableFuture<byte[]> future = new CompletableFuture<>();
httpClient.execute(request, new FutureCallback<SimpleHttpResponse>() {
@Override
public void completed(SimpleHttpResponse response) {
try {
future.complete(response.getBodyBytes());
} catch (Exception ex) {
future.completeExceptionally(ex);
}
}
@Override
public void failed(Exception ex) {
future.completeExceptionally(ex);
}
@Override
public void cancelled() {
future.completeExceptionally(new CancellationException(&#34;cancelled&#34;));
}
});
return future;
}
@Override
public void close() throws IOException {
httpClient.close(CloseMode.GRACEFUL);
}
}<hr/>OkHttp3
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
@Deprecated
public class OKHttpClient implements HttpClient {
private final OkHttpClient httpClient;
private final String basicAuth;
public OKHttpClient(String user, String password, Duration connectTimeout, Duration requestTimeout) {
basicAuth = &#34;Basic &#34; + Base64.getEncoder().encodeToString((user + &#34;:&#34; + password).getBytes(StandardCharsets.UTF_8));
// https://square.github.io/okhttp/4.x/okhttp/okhttp3/-dispatcher/
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.getDispatcher$okhttp().setMaxRequests(1024); // default 64
builder.getDispatcher$okhttp().setMaxRequestsPerHost(1024); // default 5
httpClient = builder
.connectTimeout(connectTimeout)
.callTimeout(requestTimeout)
.readTimeout(requestTimeout)
.writeTimeout(requestTimeout)
.build();
}
@Override
public CompletionStage<byte[]> httpPost(URI uri, byte[] body) {
Request request = new Request.Builder().url(HttpUrl.get(uri))
.header(&#34;Authorization&#34;, basicAuth)
.post(RequestBody.create(body, MediaType.get(&#34;application/json&#34;)))
.build();
CompletableFuture<byte[]> future = new CompletableFuture<>();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException ex) {
future.completeExceptionally(ex);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
try {
if (response.isSuccessful()) {
future.complete(response.body().bytes());
} else {
future.completeExceptionally(new IOException(response.message()));
}
} catch (Exception ex) {
future.completeExceptionally(ex);
}
}
});
return future;
}
@Override
public void close() throws IOException {
httpClient.dispatcher().executorService().shutdown();
}
}<hr/>HttpClientBenchmark:
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
/*
$ cat >Caddyfile <<EOF
localhost.local:2015
respond &#34;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&#34;
EOF
$ echo &#34;127.0.0.1 localhost.local&#34; | sudo tee -a /etc/hosts
$ brew install caddy
$ caddy run
$ keytool -importcert -v -trustcacerts -alias caddy -file &#34;$HOME/Application Support/Caddy/pki/authorities/local/root.crt&#34; \
-storepass changeit -keystore `/usr/libexec/java_home`/lib/security/cacerts
*/
public class HttpClientBenchmark {
private final static URI uri = URI.create(&#34;https://localhost.local:2015/&#34;);
private final static String USER = &#34;hello&#34;;
private final static String PASSWORD = &#34;world&#34;;
private final static byte[] BODY = &#34;a&#34;.repeat(100).getBytes();
public static void main(String[] args) throws InterruptedException, IOException {
//System.setProperty(&#34;javax.net.debug&#34;, &#34;all&#34;);
run(&#34;AHC&#34;, new AHCHttpClient(USER, PASSWORD, Duration.ofSeconds(5), Duration.ofSeconds(5)));
run(&#34;Jetty&#34;, new JettyHttpClient(USER, PASSWORD, Duration.ofSeconds(5), Duration.ofSeconds(5)));
run(&#34;JDK&#34;, new JdkHttpClient(USER, PASSWORD, Duration.ofSeconds(5), Duration.ofSeconds(5)));
run(&#34;Apache&#34;, new ApacheHttpClient(USER, PASSWORD, 5000, 5000));
run(&#34;Ok&#34;, new OKHttpClient(USER, PASSWORD, Duration.ofSeconds(5), Duration.ofSeconds(5)));
}
public static void run(String name, my.HttpClient httpClient) throws InterruptedException, IOException {
System.out.println(&#34;## begin &#34; + name + &#34;: &#34; + new Date());
int n = 64;
int[] okRequests = new int[n];
int[] badRequests = new int[n];
long[] minLatency = new long[n];
long[] maxLatency = new long[n];
long[] totalLatency = new long[n];
Arrays.fill(minLatency, Long.MAX_VALUE);
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(n);
for (int i = 0; i < n; ++i) {
Thread t = new Thread(() -> {
try {
int j = Integer.valueOf(Thread.currentThread().getName());
startSignal.await();
for (int k = 0; k < 10000; ++k) {
long startTime = System.nanoTime();
try {
byte[] response = httpClient.httpPost(uri, BODY).toCompletableFuture().get();
if (response != null && response.length > 0) {
++okRequests[j];
} else {
++badRequests[j];
}
} catch (Exception ex) {
++badRequests[j];
ex.printStackTrace();
}
long endTime = System.nanoTime();
long latency = endTime - startTime;
if (minLatency[j] > latency) {
minLatency[j] = latency;
}
if (maxLatency[j] < latency) {
maxLatency[j] = latency;
}
totalLatency[j] += latency;
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
doneSignal.countDown();
}
});
t.setName(String.valueOf(i));
t.start();
}
Thread.sleep(1000);
long startTime = System.nanoTime();
startSignal.countDown();
doneSignal.await();
long duration = System.nanoTime() - startTime;
System.out.println(&#34;## end &#34; + name + &#34;: &#34; + new Date());
int ok = 0, bad = 0;
long min = Long.MAX_VALUE, max = Long.MIN_VALUE, total = 0;
for (int i = 0; i < n; ++i) {
ok += okRequests;
bad += badRequests;
if (min > minLatency) {
min = minLatency;
}
if (max < maxLatency) {
max = maxLatency;
}
total += totalLatency;
}
System.out.println(String.format(
&#34;%s ok=%s bad=%s minLatency=%s maxLatency=%s avgLatency=%s qps=%s&#34;,
name, ok, bad, min / 1000_000L, max / 1000_000L, total / (ok + bad) / 1000_000L, (ok + bad) / (duration / 1000 / 1000 / 1000)
));
httpClient.close();
}
} |
|