跳过正文
最大化第三方 API 请求吞吐量:实用测试方法
  1. 文章/

最大化第三方 API 请求吞吐量:实用测试方法

·1408 字·3 分钟
NeatGuyCoding
作者
NeatGuyCoding

最大化第三方 API 请求吞吐量:实用测试方法
#

一位社区成员最近提出了一个有趣的问题:“我如何尽可能快地向第三方 API 发送请求,而不担心压垮他们的服务器?开发和验证这种方法的最佳方式是什么?”

以下是我在实践中发现非常有效的综合策略:

游戏计划
#

1. 异步或回家 你肯定想使用 WebClient 的异步、非阻塞 I/O 功能。或者,像 Vert.x 这样的框架是绝佳选择。我现在会暂缓使用虚拟线程 - 虽然它们令人兴奋,但它们还没有完全准备好用于生产。

2. 隔离你的测试环境 关键洞察:如果你只关心代码的性能并想最大化对目标 API 的压力,在开发期间永远不要直接测试第三方接口。你需要先隔离你的代码,确保它按预期执行。

为什么?响应时间有太多变量和不稳定性。想想看 - 你和 API 之间的网络带宽、你的网卡性能、他们端的潜在速率限制,如果你不限制连接数,CDN 可能完全阻止你。此外,即使是非阻塞请求,如果你同时发出 10,000 个请求,许多请求无论如何都会在网络层排队。

3. 使用真实模拟进行本地测试 为了测试你自己的代码同时模拟真实延迟,我通常在本地运行测试。我的首选方法使用带有 httpbin 镜像的 TestContainers(kennethreitz/httpbin:latest)。对于你的特定场景,你可以为每个请求添加时间并调用 /anything 端点来收集响应 - 此端点只是回显你发送的所有参数。

想模拟带宽约束?添加一个 toxicproxy 镜像并通过它路由你的 httpbin 调用以进行带宽限制。需要模拟 API 延迟?使用 /delay/0.1 端点(用于 100 毫秒延迟)。

代码示例
#

这是一个实用示例(快速测试设置,未微调,仅用于演示测试方法)。首先,让我们创建一个可重用的 TestContainer 基类:

import eu.rekawek.toxiproxy.Proxy;
import eu.rekawek.toxiproxy.ToxiproxyClient;
import eu.rekawek.toxiproxy.model.ToxicDirection;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.ToxiproxyContainer;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.io.IOException;

@Testcontainers
public class CommonMicroServiceTest {

    private static final Network network = Network.newNetwork();

    private static final String HTTPBIN = "httpbin";
    public static final int HTTPBIN_PORT = 80;
    public static final GenericContainer<?> HTTPBIN_CONTAINER
            = new GenericContainer<>("kennethreitz/httpbin:latest")
            .withExposedPorts(HTTPBIN_PORT)
            .withNetwork(network)
            .withNetworkAliases(HTTPBIN);
    /**
     * <a href="https://java.testcontainers.org/modules/toxiproxy/">toxiproxy</a>
     * 使用 toxiproxy 包装 httpbin
     * 可以使用 toxiproxy 模拟网络故障和其他条件
     * 可用端口范围:8666~8697
     */
    private static final ToxiproxyContainer TOXIPROXY_CONTAINER = new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.5.0")
            .withNetwork(network);

    private static final int GOOD_HTTPBIN_PROXY_PORT = 8666;
    private static final int READ_TIMEOUT_HTTPBIN_PROXY_PORT = 8667;
    private static final int RESET_PEER_HTTPBIN_PROXY_PORT = 8668;

    public static final String GOOD_HOST;
    public static final int GOOD_PORT;
    /**
     * 表示请求到达服务器但超时或无法响应的情况(例如,服务器重启)
     */
    public static final String READ_TIMEOUT_HOST;
    public static final int READ_TIMEOUT_PORT;
    public static final String RESET_PEER_HOST;
    public static final int RESET_PEER_PORT;

    /**
     * 表示请求从未发出,TCP 连接无法建立的情况
     */
    public static final String CONNECT_TIMEOUT_HOST = "localhost";
    /**
     * 端口 1 保证无法访问
     */
    public static final int CONNECT_TIMEOUT_PORT = 1;


    static {
        //不使用 @Container 注解进行生命周期管理,因为我们需要在静态块中生成代理
        //不用担心容器清理 - testcontainers 启动一个 ryuk 容器来监控和关闭所有容器
        HTTPBIN_CONTAINER.start();
        TOXIPROXY_CONTAINER.start();
        final ToxiproxyClient toxiproxyClient = new ToxiproxyClient(TOXIPROXY_CONTAINER.getHost(), TOXIPROXY_CONTAINER.getControlPort());
        try {
            Proxy proxy = toxiproxyClient.createProxy("good", "0.0.0.0:" + GOOD_HTTPBIN_PROXY_PORT, HTTPBIN + ":" + HTTPBIN_PORT);
            //禁用流量,将导致 READ TIMEOUT
            proxy = toxiproxyClient.createProxy("read_timeout", "0.0.0.0:" + READ_TIMEOUT_HTTPBIN_PROXY_PORT, HTTPBIN + ":" + HTTPBIN_PORT);
            proxy.toxics().bandwidth("UP_DISABLE", ToxicDirection.UPSTREAM, 0);
            proxy.toxics().bandwidth("DOWN_DISABLE", ToxicDirection.DOWNSTREAM, 0);
            proxy = toxiproxyClient.createProxy("connect_timeout", "0.0.0.0:" + RESET_PEER_HTTPBIN_PROXY_PORT, HTTPBIN + ":" + HTTPBIN_PORT);
            proxy.toxics().resetPeer("UP_SLOW_CLOSE", ToxicDirection.UPSTREAM, 1);
            proxy.toxics().resetPeer("DOWN_SLOW_CLOSE", ToxicDirection.DOWNSTREAM, 1);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        GOOD_HOST = TOXIPROXY_CONTAINER.getHost();
        GOOD_PORT = TOXIPROXY_CONTAINER.getMappedPort(GOOD_HTTPBIN_PROXY_PORT);
        READ_TIMEOUT_HOST = TOXIPROXY_CONTAINER.getHost();
        READ_TIMEOUT_PORT = TOXIPROXY_CONTAINER.getMappedPort(READ_TIMEOUT_HTTPBIN_PROXY_PORT);
        RESET_PEER_HOST = TOXIPROXY_CONTAINER.getHost();
        RESET_PEER_PORT = TOXIPROXY_CONTAINER.getMappedPort(RESET_PEER_HTTPBIN_PROXY_PORT);
    }
}

这是实际的测试代码:

@Test
public void test() {
    // 创建自定义连接提供者
    ConnectionProvider provider = ConnectionProvider.builder("customConnectionProvider")
            .maxConnections(100) // 增加最大连接数,但不要太高以避免 CDN DDoS 检测
            .pendingAcquireMaxCount(10000) // 增加等待队列大小
            .build();

    HttpClient httpClient = HttpClient.create(provider)
            .responseTimeout(Duration.ofMillis(100000)); // 响应超时
    WebClient build = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();
    List<Mono<String>> monos = Lists.newArrayList();
    for (int i = 0; i < 10000; i++) {
        //使用 TestContainer 端口模拟 0.1s 延迟
        Mono<String> stringMono = build.get().uri("http://localhost:" +
                CommonMicroServiceTest.HTTPBIN_CONTAINER.getMappedPort(HTTPBIN_PORT) + "/delay/0.1")
                .retrieve().bodyToMono(String.class);
        monos.add(stringMono);
    }
    long start = System.currentTimeMillis();
    String block = Mono.zip(monos, objects -> {
        log.info("{}", objects);
        return "ok";
    }).block();
    log.info("block: {} in {}ms", block, System.currentTimeMillis() - start);
}

测试结果: block: ok in 10362ms

这与我们的预期完全一致:

  • 10,000 个请求,每个延迟 0.1 秒,连接池为 100
  • 总时间 ≈ 0.1 × 10000/100 = 10 秒

专业提示
#

我经常使用 toxicproxy 来模拟各种故障场景:服务器断开连接、请求到达服务器但没有响应、请求从未到达服务器、部分请求传输失败等等。在构建健壮的微服务基础设施时,这个工具包非常有价值!

这种方法的优点是你完全控制测试环境,同时仍保持真实条件。你可以将代码推向极限而不担心外部因素,然后自信地部署,确切知道你的系统在压力下的行为。

相关文章

使用 JFR 排查 SSL 性能瓶颈

·868 字·2 分钟
深入分析微服务性能问题,包括 CPU 峰值和数据库连接异常。通过 JFR 分析,我们发现根本原因是 Java SecureRandom 在 /dev/random 上阻塞,并提供使用 /dev/urandom 的解决方案。