测试20万qps的web接口(四)

测试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时使用了简单的随机,这些可以优化一下。

相关推荐
ranger211 天前
测试20万qps的web接口(三)
网络测试
Johnstons3 个月前
如何精确模拟网络丢包进行测试?实测指南
开发语言·网络·php·网络测试·网络损伤·弱网模拟
请为小H留灯6 个月前
网络连通性测试:3 种常用方法(Telnet、Ping、Curl)详解
网络·curl·telnet·网络测试
_OP_CHEN8 个月前
【测试理论与实践】(四)测试用例篇(上):从概念到万能思路,解锁测试设计核心密码
运维·测试开发·测试用例·bug·压力测试·测试·网络测试
阿巴~阿巴~9 个月前
简易回声服务器实现与网络测试指南
linux·服务器·网络·udp协议·网络测试·udp套接字编程
阿巴~阿巴~9 个月前
UDP服务端绑定INADDR_ANY后,客户端该用什么IP访问?
服务器·网络·udp协议·网络测试·udp套接字编程·inaddr_any
Code_Shawn2 年前
UDP_USER_INTERFACE_07:用户界面 - 源IP地址(待发送)
网络·网络协议·tcp/ip·udp·网络测试·tc8 测试
Code_Shawn2 年前
UDP_INTRODUCTION_03:介绍 - 挂起的监听调用
网络·网络协议·tcp/ip·udp·网络测试·tc8 测试
Code_Shawn2 年前
UDP_INVALID_ADDRESSES_01:无效地址 - 多播源地址
网络·网络协议·tcp/ip·udp·网络测试·tc8 测试