测试20万qps的web接口(四)
本篇文章主要描述:探讨两种新的的方式 -- 修改tcp连接的源ip。
在上篇文章中,想借助lvs来线性扩大client端的tcp最大连接数,发现这种方式不行。
client端只有1个ip,server端也只有1个ip,client端与server端之间建立连接的四元组是:client_ip + port_range + server_ip + server_port。
这里只有port_range是可变的,因此client建立的最大连接数是固定的。
在使用lvs之前,原本以为client会与lvs的rip建立连接,没想到client是与lvs的vip建立连接。
在本篇文章中,换了一种思维方式来突破最大连接数问题。
现在设想是client拥有多个ip,server只有1个ip,在client端向server端建立连接时,动态轮询使用客户端的多个ip,以此来线性扩大最大连接数。
无论是上篇文章中的方案,还是本篇文章中的方案,都期望不去修改client端的压测程序的使用方式。
比如curl和jmeter可以指定连接使用的源ip,而wrk没有这样的功能。如果让压测程序适配源ip修改(通过参数或是修改源码),会带来极大的复杂度。
错误方式(使用netfilter)
和AI拉扯了好多天,AI给出了一个方案:使用netfilter的mangle表的Output链来对进出的数据包做修改,然后通过路由策略来动态设置不同的源ip。
测试时发现这个方案不行,AI又给出了一个方案:使用netfilter的raw表的Output链来对进出的数据包做修改。这种方式也不行。
用了这么久的AI,经常发现:在一些比较少见和复杂的问题上,AI知道一些大致的解决方案,但是它不能保证是否正确可行。
看来还是要懂一些原理,要不然碰到一些复杂问题,死磕也没用。

以下是具体的配置命令,实验没有成功(如果有大佬看出了问题,可以指点一下哈)。
bash
# 修改rt_tables,定义路由表id和别名之间的映射
[root@client ~]# vi /etc/iproute2/rt_tables
201 src_1
202 src_2
203 src_3
# 添加多个ip
[root@client ~]# ip addr add 10.0.0.201/24 dev ens33
[root@client ~]# ip addr add 10.0.0.202/24 dev ens33
[root@client ~]# ip addr add 10.0.0.203/24 dev ens33
# 添加路由规则
# 10.0.0.132是目的地址
ip route add 10.0.0.132 dev ens33 src 10.0.0.201 table src_1
ip route add 10.0.0.132 dev ens33 src 10.0.0.202 table src_2
ip route add 10.0.0.132 dev ens33 src 10.0.0.203 table src_3
# 添加路由策略
[root@client ~]# ip rule add pref 201 fwmark 1 lookup src_1
[root@client ~]# ip rule add pref 202 fwmark 2 lookup src_2
[root@client ~]# ip rule add pref 203 fwmark 3 lookup src_3
# 设置netfilter
iptables -t mangle -F
iptables -t mangle -A OUTPUT -d 10.0.0.132 -j CONNMARK --restore-mark
iptables -t mangle -A OUTPUT -d 10.0.0.132 -m mark --mark 0 -m state --state NEW -m statistic --mode nth --every 3 --packet 0 -j MARK --set-mark 1
iptables -t mangle -A OUTPUT -d 10.0.0.132 -m mark --mark 0 -m state --state NEW -m statistic --mode nth --every 3 --packet 1 -j MARK --set-mark 2
iptables -t mangle -A OUTPUT -d 10.0.0.132 -m mark --mark 0 -m state --state NEW -m statistic --mode nth --every 3 --packet 2 -j MARK --set-mark 3
iptables -t mangle -A OUTPUT -d 10.0.0.132 -m state --state NEW -j CONNMARK --save-mark
# 查看连接情况
[root@client ~]# for i in $(seq 1 6); do curl --no-keepalive -s http://10.0.0.132 -o /dev/null; done
[root@client ~]# conntrack -L | grep 10.0.0.132
conntrack v1.4.4 (conntrack-tools): 8 flow entries have been shown.
tcp 6 9 TIME_WAIT src=10.0.0.134 dst=10.0.0.132 sport=51252 dport=80 src=10.0.0.132 dst=10.0.0.134 sport=80 dport=51252 [ASSURED] mark=2 secctx=system_u:object_r:unlabeled_t:s0 use=1
tcp 6 12 TIME_WAIT src=10.0.0.134 dst=10.0.0.132 sport=51258 dport=80 src=10.0.0.132 dst=10.0.0.134 sport=80 dport=51258 [ASSURED] mark=3 secctx=system_u:object_r:unlabeled_t:s0 use=1
tcp 6 11 TIME_WAIT src=10.0.0.134 dst=10.0.0.132 sport=51256 dport=80 src=10.0.0.132 dst=10.0.0.134 sport=80 dport=51256 [ASSURED] mark=1 secctx=system_u:object_r:unlabeled_t:s0 use=1
tcp 6 4 TIME_WAIT src=10.0.0.134 dst=10.0.0.132 sport=51250 dport=80 src=10.0.0.132 dst=10.0.0.134 sport=80 dport=51250 [ASSURED] mark=1 secctx=system_u:object_r:unlabeled_t:s0 use=1
tcp 6 10 TIME_WAIT src=10.0.0.134 dst=10.0.0.132 sport=51254 dport=80 src=10.0.0.132 dst=10.0.0.134 sport=80 dport=51254 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1
正确方式(使用LD_PRELOAD)
失败次数过多时,会时不时怀疑自我。
有几次都想放弃这种方式,想着要不要去修改压测程序的源码。
看到AI说"src_ip在connect时已经确定了,没法做修改",突然想到了一个测试方案。
还好自己会点C语言,也懂点Socket C API的基本使用。看看能不能在connect函数中做拦截处理。
通过搜索,发现Linux上有LD_PRELOAD这个环境变量,可以有选择性的载入函数(覆盖libc中的函数?)
通过简单的测试,发现这个方案的确可行。
以下是我参考的几篇文章。
https://www.cnblogs.com/sandeepin/p/ld-preload-inject.html
https://blog.csdn.net/chen_jianjian/article/details/80627693
https://github.com/yongboy/bindp
具体的一些源码、配置。
bash
# 添加源ip
[root@client ~]# ip addr add 10.0.0.201/32 dev ens33 label ens33:201
[root@client ~]# ip addr add 10.0.0.202/32 dev ens33 label ens33:202
[root@client ~]# ip addr add 10.0.0.203/32 dev ens33 label ens33:203
# 查看源ip
[root@client ~]# ip addr show ens33
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:50:56:20:79:ae brd ff:ff:ff:ff:ff:ff
inet 10.0.0.100/24 brd 10.0.0.255 scope global noprefixroute ens33
valid_lft forever preferred_lft forever
inet 10.0.0.201/32 scope global ens33:201
valid_lft forever preferred_lft forever
inet 10.0.0.202/32 scope global ens33:202
valid_lft forever preferred_lft forever
inet 10.0.0.203/32 scope global ens33:203
valid_lft forever preferred_lft forever
inet6 fe80::250:56ff:fe20:79ae/64 scope link
valid_lft forever preferred_lft forever
# mybind.c关键源码片段,参考自github上的bindp项目
# 覆盖connect函数
int connect (int fd, const struct sockaddr *sk, socklen_t sl) {
unsigned short _pf = get_address_family(sk);
if (_pf == AF_INET) {
char *src_ip;
int rn = rand() % 3;
if (rn == 0) {
src_ip = "10.0.0.201";
} else if (rn == 1) {
src_ip = "10.0.0.202";
} else {
src_ip = "10.0.0.203";
}
// 手动 bind
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(0); // 0 表示随机端口
if (inet_pton(AF_INET, src_ip, &local_addr.sin_addr) <= 0) {
perror("inet_pton");
return 1;
}
if (bind(fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {
perror("bind");
return 1;
}
return real_connect (fd, sk, sl);
} else {
return real_connect (fd, sk, sl);
}
}
# 编译成共享库
gcc -shared -fPIC mybind.c -o mybind.so -ldl
# 设置环境变量
export LD_PRELOAD=$PWD/mybind.so
# 删除环境变量
unset LD_PRELOAD
测试curl,可以看到nginx日志中的源ip是自动变化的。
bash
# 执行10次curl
root@client:~# for in in {1..10}; do LD_PRELOAD=/root/mybind.so curl 10.0.0.101; sleep 1; done
# nginx输出
10.0.0.202 - - [27/Aug/2026:10:57:21 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.203 - - [27/Aug/2026:10:57:22 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.202 - - [27/Aug/2026:10:57:23 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.202 - - [27/Aug/2026:10:57:24 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.203 - - [27/Aug/2026:10:57:25 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.201 - - [27/Aug/2026:10:57:26 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.201 - - [27/Aug/2026:10:57:27 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.202 - - [27/Aug/2026:10:57:28 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.201 - - [27/Aug/2026:10:57:29 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
10.0.0.201 - - [27/Aug/2026:10:57:30 +0000] "GET / HTTP/1.1" 200 13 "-" "curl/8.5.0"
测试wrk,可以看到nginx日志中的源ip是自动变化的。
bash
root@client:~# LD_PRELOAD=$PWD/mybind.so wrk -t1 -c1 -d10s -T5s -H "Connection: Close" --latency http://10.0.0.101:80/
===_init invoked.===
Running 10s test @ http://10.0.0.101:80/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 415.41us 189.67us 6.60ms 95.09%
Req/Sec 573.81 92.75 820.00 68.00%
Latency Distribution
50% 382.00us
75% 427.00us
90% 510.00us
99% 1.06ms
5722 requests in 10.02s, 1.33MB read
Requests/sec: 571.25
Transfer/sec: 135.56KB
# 查看nginx服务器的连接状态
root@server:~# ss -ant | grep 80 | wc -l
5358
root@server:~# ss -ant | grep 80 | more
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.201:37715
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.201:48883
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.202:47177
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.203:35371
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.202:54339
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.203:59321
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.203:52073
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.202:49359
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.203:33715
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.202:39237
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.202:47303
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.202:58353
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.203:43839
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.202:51511
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.202:34169
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.203:41137
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.203:39441
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.201:57915
TIME-WAIT 0 0 10.0.0.101:80 10.0.0.201:53397
结论
通过LD_PRLOAD覆盖libc中的connect函数,可以自定义修改源ip,不需要修改压测程序的参数或源码。
无论是自己写的简单测试程序,还是curl和wrk,它们在运行时都没有指定源ip,而是由connect函数自动设置的源ip。
这样的话,只要源ip足够多,可以创建的最大连接数就会足够大。
后续规划
方案没问题,后续就要做细化和具体测试。
connect函数中的ip是写死的,轮询源ip时使用了简单的随机,这些可以优化一下。