最大化第三方 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 来模拟各种故障场景:服务器断开连接、请求到达服务器但没有响应、请求从未到达服务器、部分请求传输失败等等。在构建健壮的微服务基础设施时,这个工具包非常有价值!
这种方法的优点是你完全控制测试环境,同时仍保持真实条件。你可以将代码推向极限而不担心外部因素,然后自信地部署,确切知道你的系统在压力下的行为。



