相关文章推荐
内向的花卷  ·  列出邮件 - Microsoft ...·  2 年前    · 
冷冷的小笼包  ·  对视图 进行增删改 ...·  2 年前    · 
鬼畜的铁链  ·  c# - Converting ...·  2 年前    · 
一直单身的机器猫  ·  Android ...·  2 年前    · 
英俊的书签  ·  org.hibernate.exceptio ...·  2 年前    · 

Java HTTP/2 客户端库的性能比较

本机 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 的性能之坑早有人分析,其实现比较惨:

软件包版本:

测试代码如下,供参考下各个库的 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 = "Basic " + Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8));
    @Override
    public CompletionStage<byte[]> httpPost(URI uri, byte[] body) {
        return httpClient.preparePost(uri.toString())
                .setHeader("Authorization", basicAuth)
                .setHeader("Content-Type", "application/json")
                .setBody(body)
                .execute()
                .toCompletableFuture()
                .thenApply(r -> r.getResponseBodyAsBytes());
    @Override
    public void close() throws IOException {
        httpClient.close();
}

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 = "Basic " + Base64.getEncoder().encodeToString((user + ":" + 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("Authorization", basicAuth);
                .body(new BytesRequestContent("application/json", 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);
}

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("JdkHttpClient-" + 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 = "Basic " + Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8));
    @Override
    public CompletionStage<byte[]> httpPost(URI uri, byte[] body) {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(uri)
                .header("Authorization", basicAuth)
                .header("Content-Type", "application/json")
                .POST(BodyPublishers.ofByteArray(body))
                .timeout(requestTimeout)
                .build();
        return httpClient.sendAsync(request, BodyHandlers.ofByteArray())
                .thenApply(HttpResponse::body);
    @Override
    public void close() throws IOException {
        executor.shutdown();

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 = "Basic " + Base64.getEncoder().encodeToString((user + ":" + 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("Authorization", 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("cancelled"));
        return future;
    @Override
    public void close() throws IOException {
        httpClient.close(CloseMode.GRACEFUL);
}

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 = "Basic " + Base64.getEncoder().encodeToString((user + ":" + 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("Authorization", basicAuth)
                .post(RequestBody.create(body, MediaType.get("application/json")))
                .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();
}

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 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$ echo "127.0.0.1 localhost.local" | sudo tee -a /etc/hosts
$ brew install caddy
$ caddy run
$ keytool -importcert -v -trustcacerts -alias caddy -file "$HOME/Application Support/Caddy/pki/authorities/local/root.crt" \
          -storepass changeit -keystore `/usr/libexec/java_home`/lib/security/cacerts
public class HttpClientBenchmark {
    private final static URI uri = URI.create("https://localhost.local:2015/");
    private final static String USER = "hello";
    private final static String PASSWORD = "world";
    private final static byte[] BODY = "a".repeat(100).getBytes();
    public static void main(String[] args) throws InterruptedException, IOException {
        //System.setProperty("javax.net.debug", "all");
        run("AHC", new AHCHttpClient(USER, PASSWORD, Duration.ofSeconds(5), Duration.ofSeconds(5)));
        run("Jetty", new JettyHttpClient(USER, PASSWORD, Duration.ofSeconds(5), Duration.ofSeconds(5)));
        run("JDK", new JdkHttpClient(USER, PASSWORD, Duration.ofSeconds(5), Duration.ofSeconds(5)));
        run("Apache", new ApacheHttpClient(USER, PASSWORD, 5000, 5000));
        run("Ok", 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("## begin " + name + ": " + 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("## end " + name + ": " + 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[i];
            bad += badRequests[i];
            if (min > minLatency[i]) {
                min = minLatency[i];
            if (max < maxLatency[i]) {
                max = maxLatency[i];
            total += totalLatency[i];
        System.out.println(String.format(
                "%s ok=%s bad=%s minLatency=%s maxLatency=%s avgLatency=%s qps=%s",