高性能HTTP服务压测工具:设计思路与实现原理(C/C++代码实现)

做后端开发或运维的同学都绕不开一个问题:上线的HTTP服务到底能扛住多少请求?每秒能处理多少请求(RPS)?高并发下会不会报错?传统的单线程压测工具往往自己先"卡壳",测不出服务的真实性能------今天我们就聊聊一款能充分利用服务器资源、精准测试高负载HTTP服务的压测工具,拆解它的设计思路和实现原理。

一、为什么需要"够快"的HTTP压测工具?

先说说痛点:比如Apache Bench(ab)这类常用压测工具是单线程的------不管你的服务器有8核、16核CPU,这个工具最多只能占满一个核心。如果被测服务是Nginx、Redis这类基于事件驱动的高并发服务,单线程压测工具自己就会成为瓶颈:工具的CPU跑满了,服务还没到极限,测出来的RPS数值根本不是服务的真实能力,相当于"用小水管去测大水库的出水速度"。

所以我们需要一款"自身性能足够强"的压测工具:它能充分利用服务器的多核CPU,高效管理上千个并发连接,让被测服务真正跑满,才能测出真实的性能上限。

二、先搞懂:HTTP压测到底测什么?

大白话讲,HTTP压测就是模拟大量用户同时给服务发HTTP请求,核心就看三个指标:

  1. 总请求数:一共发了多少个请求;
  2. 成功/失败请求数:成功是服务返回1xx/2xx/3xx状态码,失败是4xx(客户端错误)/5xx(服务端错误);
  3. 每秒请求数(RPS):最核心的指标,反映服务的处理能力,计算方式=总请求数÷总耗时。

压测的最终目标,就是找到服务在"不报错、响应时间可接受"前提下的最大RPS。

三、高性能压测工具的核心设计思路

要做出"不拖后腿"的压测工具,核心就解决两个问题:把CPU核心用满高效管理大量并发连接,对应的设计思路就两点:

1. 多线程:榨干多核CPU的性能

既然单线程只能用一个核心,那就创建和CPU核心数一致的线程(比如8核CPU开8个线程)。每个线程独立处理一部分并发连接,让所有CPU核心都跑起来,工具自身不会因为CPU不够用而成为瓶颈。

2. IO多路复用(epoll):高效管上千个连接

如果每个连接都配一个线程(线程池模式),并发数到几千、几万时,线程切换的开销会把程序拖垮。这时候就需要IO多路复用------用一个线程通过epoll管理成百上千个连接,只处理"有事件发生"的连接(比如连接能发数据、有响应可读),不用挨个轮询,效率直接拉满。

3. 非阻塞IO:避免单个连接卡住整体

所有网络连接都设置为非阻塞模式:比如发起连接时不用等连接建立完成,发数据时能发多少发多少,不会因为一个连接的网络延迟,导致整个程序卡住。

四、核心实现原理(从代码逻辑拆解)

我们以这款工具的核心代码为例,用大白话讲清楚它是怎么跑起来的,还附了流程示意图,一看就懂。

1. 整体执行流程

整个工具的运行过程就6步,流程图如下:
解析命令行参数
解析目标URL:拆分主机/端口/请求路径
初始化目标服务器地址(IP+端口)
创建指定数量的工作线程
每个线程:初始化并发连接+epoll事件循环
事件循环:处理连接写/读事件→统计请求结果→循环压测
接收到终止信号(Ctrl+C)或达到总请求数→停止压测
计算耗时、RPS、成功率→输出测试结果

cpp 复制代码
...
int main(int argc, char* argv[]) 
{
...

	if (argc == 1)
		print_usage();

	do {
		next_option = getopt_long(argc, argv, short_options, long_options, NULL);

		switch (next_option) {

			case 'n':
				max_requests = strtoull(optarg, 0, 10);
				break;

			case 'c':
				concurrency = atoi(optarg);
				break;

			case 't':
				num_threads = atoi(optarg);
				break;

			case 'd':
				debug = 0x03;
				break;

			case '%':
				print_usage();

			case -1:
				break;

			default:
				printf("Unexpected argument: '%c'\n", next_option);
				return 1;
		}
	} while (next_option != -1);

	if (optind >= argc) {
		printf("Missing URL\n");
		return 1;
	}

	/* parse URL */
	s = argv[optind];

	if (!strncmp(s, HTTP_REQUEST_PREFIX, sizeof(HTTP_REQUEST_PREFIX) - 1))
		s += (sizeof(HTTP_REQUEST_PREFIX) - 1);

	host = s;

	rq = strpbrk(s, ":/");

	if (rq == NULL)
		rq = "/";

	else if (*rq == '/') {
		host = malloc(rq - s);
		memcpy(host, rq, rq - s);

	} else if (*rq == ':') {
		*rq++ = 0;
		port = atoi(rq);
		rq = strchr(rq, '/');
		if (rq == NULL)
			rq = "/";
	}

	h = gethostbyname(host);
	if (!h || !h->h_length) {
		printf("gethostbyname failed\n");
		return 1;
	}

	ssin.sin_addr.s_addr = *(u_int32_t*)h->h_addr;
	ssin.sin_family = PF_INET;
	ssin.sin_port = htons(port);

	outbuf = malloc(strlen(rq) + sizeof(HTTP_REQUEST_FMT) + strlen(host));
	outbufsize = sprintf(outbuf, HTTP_REQUEST_FMT, rq, host);

	ticks = max_requests / 10;

	signal(SIGINT, &sigint_handler);

	if (!max_requests) {
		ticks = 1000;
		printf("[Press Ctrl-C to finish]\n");
	}

	start_time();

	for(n = 0; n < num_threads - 1; ++n)
		pthread_create(&useless_thread, 0, &worker, 0);

	worker(0);

	delta = tve.tv_sec - tv.tv_sec + ((double)(tve.tv_usec - tv.tv_usec)) / 1e6;

	printf("\n"
			"requests:      %"PRIu64"\n"
			"good requests: %"PRIu64" [%d%%]\n"
			"bad requests:  %"PRIu64" [%d%%]\n"
			"seconds:       %.3f\n"
			"requests/sec:  %.3f\n"
			"\n",
			num_requests,
			good_requests, (int)(num_requests ? good_requests * 100 / num_requests : 0),
			bad_requests, (int)(num_requests ? bad_requests * 100 / num_requests: 0),
			delta,
			delta > 0
				? max_requests / delta
				: 0
		  );

	return 0;
}

If you need the complete source code, please add the WeChat number (c17865354792)

2. 关键步骤拆解

(1)参数解析:确定压测规则

运行工具时我们会传几个关键参数(和ab类似):

  • -n:总请求数(不传就是无限压测,按Ctrl+C停止);
  • -c:并发连接数(同时发多少个请求);
  • -t:线程数(建议等于CPU核心数)。

代码会先解析这些参数,比如确定"要发10000个请求,同时保持100个并发,用8个线程处理"。

bash 复制代码
# 核心测试命令:10000个总请求、100个并发、8个线程,测试本地8080端口
./http_bench -n 10000 -c 100 -t 8 localhost:8080
# 无限发请求,100个并发、8个线程,按Ctrl+C停止并输出结果
./http_bench -c 100 -t 8 localhost:8080
(2)URL解析:找到要压测的服务

比如输入localhost:8087/test,代码会拆分出:

  • 主机:localhost(通过域名解析转成IP地址);
  • 端口:8087(默认80);
  • 请求路径:/test。

然后拼接成标准的HTTP GET请求(比如GET /test HTTP/1.0\r\nHost: localhost\r\n\r\n),存在内存里备用------不用每次发请求都拼接,减少开销。

(3)工作线程:核心的"压测工人"

每个工作线程是真正干活的,核心流程就三件事:

① 创建epoll实例:相当于给这个线程配一个"连接管家",专门管所有并发连接的事件;

② 初始化并发连接:按指定的并发数(比如100)创建非阻塞的网络连接,发起连接请求后,把这些连接注册到epoll里,关注"可写事件(EPOLLOUT)";

③ 进入epoll事件循环(最核心):

  • 当连接触发"可写事件":说明连接建好了,把提前准备的HTTP请求发出去;发完后,把这个连接的关注事件改成"可读事件(EPOLLIN)",等着收响应;
  • 当连接触发"可读事件":说明服务返回响应了,读取响应数据后,重点检查状态码(比如响应里第10个字符是4/5,就是4xx/5xx错误);统计成功/失败后,关闭这个连接,立刻新建一个连接------保证并发数不变,持续压测。
(4)数据统计:保证准确不混乱

多线程会同时修改"总请求数""成功请求数"这些数据,代码里用了原子操作(比如__sync_fetch_and_add),避免多个线程"抢着改数"导致统计错误。比如线程A和B同时给"总请求数"加1,原子操作能保证最终加的是2,而不是1。

(5)结果计算:输出直观的压测报告

当达到总请求数,或按Ctrl+C触发终止信号,工具会停止压测,计算从开始到结束的耗时,算出RPS(总请求数÷耗时)、成功/失败请求的百分比,最后输出像这样的报告:

复制代码
requests:      10000
good requests: 10000 [100%]
bad requests:  0 [0%]
seconds:       0.742
requests/sec:  13469.301

五、背后的核心知识点:搞懂这些,你也能写这类工具

这款工具的设计,用到了高性能网络编程的核心知识点,也是做高并发程序的通用思路:

1. IO模型的选择:epoll是高并发的"最优解"

常见的IO模型有3种,对比一下就知道epoll的优势:

  • 阻塞IO:一个连接卡壳,整个程序都等,并发数超不过几百;
  • select/poll:能管多连接,但要轮询所有连接,连接多了(上千)效率暴跌;
  • epoll:只关注"有事件的连接",连接数越多优势越明显,轻松管上万并发。

这也是Nginx、Redis这类高并发服务都用epoll的原因------压测工具要测这类服务,自己也得用epoll,不然根本跟不上。

2. 多线程的核心:避免"抢数据"

多线程共享数据时,必须用原子操作或锁,不然会出现"数据不一致"。比如两个线程同时给"总请求数"加1,不用原子操作的话,可能最后只加了1,统计结果就错了。

3. 非阻塞IO:不让单个连接拖慢整体

所有网络连接设为非阻塞(fcntl设置O_NONBLOCK),比如发起连接时不用等结果,epoll会在连接建好后通知;发/收数据时能处理多少就处理多少,不会因为一个连接的网络慢,导致整个事件循环卡住。

4. HTTP协议极简应用

压测用的是最简单的HTTP 1.0请求(没有长连接、没有复杂请求头),只保证"能发请求、能判响应对错"------核心是测性能,不是测HTTP协议的兼容性,减少工具自身的开销。

六、设计思路的核心价值:测准服务的真实性能

这款工具的设计本质,就是"让压测工具自身的性能适配硬件和业务场景":

  1. 多线程利用多核CPU:避免工具自身CPU瓶颈,能"喂饱"被测服务;
  2. epoll+非阻塞IO:高效管理大量并发连接,模拟真实的高并发场景;
  3. 极简的请求处理:减少工具自身的开销,把资源都用在"发请求、收响应"上。

举个实际例子:用单线程工具测8核的Nginx服务,工具占满1个核,测出来RPS只有8000;用8线程的工具测,能占满8个核,测出来RPS能到13000------这才是Nginx的真实性能。

总结

高性能HTTP压测工具的设计,核心就三个关键点:

  1. 多线程充分利用多核CPU,避免工具自身成为性能瓶颈;
  2. epoll+非阻塞IO高效管理高并发连接,适配事件驱动型服务的特性;
  3. 原子操作保证多线程数据统计的准确性,极简HTTP处理减少工具开销。

Welcome to follow WeChat official account【程序猿编码

相关推荐
2301_803554522 小时前
c++hpc岗位
c++
迎仔2 小时前
网络硬件设备通俗指南:从“大喇叭”到“算力工厂”
网络·智能路由器
坐怀不乱杯魂2 小时前
Linux - 线程
linux·c++
LaoZhangGong1232 小时前
学习TCP/IP的第4步:重点掌握TCP序列号和确认号
网络·学习·tcp/ip·以太网
diediedei2 小时前
C++中的适配器模式变体
开发语言·c++·算法
天赐学c语言2 小时前
1.25 - 零钱兑换 && 理解右值以及move的作用
c++·算法·leecode
北冥湖畔的燕雀2 小时前
C++智能指针:告别内存泄漏的利器
c++·算法
CSDN_RTKLIB2 小时前
【编码实战】源字符集设置
c++
傻乐u兔2 小时前
C语言进阶————数据在内存中的存储1
c语言·数据结构·算法