香雨站

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 81|回复: 3

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

[复制链接]

3

主题

6

帖子

12

积分

新手上路

Rank: 1

积分
12
发表于 2023-1-13 20:24:54 | 显示全部楼层 |阅读模式
本机 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 = "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();
    }
}<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 = "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);
        }
    }
}<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("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();
    }
}
<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 = "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);
    }
}<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 = "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();
    }
}<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 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
EOF

$ 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;
            bad += badRequests;
            if (min > minLatency) {
                min = minLatency;
            }
            if (max < maxLatency) {
                max = maxLatency;
            }
            total += totalLatency;
        }

        System.out.println(String.format(
                "%s ok=%s bad=%s minLatency=%s maxLatency=%s avgLatency=%s qps=%s",
                name, ok, bad, min / 1000_000L, max / 1000_000L, total / (ok + bad) / 1000_000L, (ok + bad) / (duration / 1000 / 1000 / 1000)
        ));

        httpClient.close();
    }
}
回复

使用道具 举报

1

主题

4

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2023-1-13 20:24:59 | 显示全部楼层
压测用本机?
回复

使用道具 举报

2

主题

6

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2023-1-13 20:25:16 | 显示全部楼层
代码已经给出来了,你可以搞两台机器试试呗。
回复

使用道具 举报

2

主题

3

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2025-2-24 17:15:42 | 显示全部楼层
小白一个 顶一下
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|香雨站

GMT+8, 2025-3-15 05:09 , Processed in 1.492063 second(s), 24 queries .

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.. 技术支持 by 巅峰设计

快速回复 返回顶部 返回列表