30、实战项目(下):Web服务器性能测试、压力测试、问题排查、优化迭代、项目总结与经验分享

上一章我们把Web服务器跑起来了,能响应请求了。但说实话,那只是万里长征第一步。一个能跑的服务器和一个能扛住高并发的服务器,中间差着十万八千里。这一章,我们就来聊聊怎么把服务器从「能用」变成「好用」。

性能测试:先摸清家底

做优化之前,你得先知道服务器现在什么水平。我个人习惯用 wrkab 这两个工具。它们简单粗暴,能快速给出吞吐量和延迟数据。

先看一个典型的测试命令:

wrk -t12 -c400 -d30s http://localhost:8080/

这个命令的意思是:用12个线程,保持400个并发连接,持续压30秒。跑完之后,你会看到类似这样的输出:

Running 30s test @ http://localhost:8080/
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    45.67ms   12.34ms 198.76ms   75.23%
    Req/Sec     7.23k     1.02k    9.87k    68.45%
  2598764 requests in 30.00s, 3.47GB read
Requests/sec:  86625.47
Transfer/sec:  118.43MB

这里有几个关键指标:Latency(延迟)、Req/Sec(每秒请求数)、Requests/sec(总吞吐量)。我一般先看延迟的均值和中位数,如果中位数超过100ms,那就要警惕了。

我的经验:第一次压测时,别一上来就上400并发。从50并发开始,逐步往上加。这样你能看到服务器在哪个点开始「撑不住」。我曾经有个项目,200并发时表现完美,加到250直接崩溃——原来是连接池配小了。

压力测试:找到系统的天花板

性能测试是看「好不好」,压力测试是看「会不会挂」。说白了,就是要把服务器往死里压,找到它的极限在哪里。

我常用的压测策略是阶梯式加压:

  1. 先以100并发跑5分钟,观察CPU和内存
  2. 加到200并发,再跑5分钟
  3. 继续加到400、800、1600...
  4. 直到出现大量超时或错误

记录下每个阶段的指标,你会得到一条曲线。这条曲线能告诉你很多事情:

并发数 吞吐量 (req/s) 平均延迟 (ms) 错误率 CPU使用率
100 25000 4.2 0% 35%
200 48000 4.5 0% 60%
400 86000 5.1 0.1% 85%
800 92000 12.8 2.3% 95%
1600 78000 45.6 15.7% 98%

看到没?到800并发时,吞吐量几乎不再增长,延迟却翻倍了。这就是典型的「拐点」。我建议你把服务器的运行水位控制在拐点以下,留出30%的余量。

注意:压测时一定要监控系统资源。我曾经遇到过CPU没跑满,但吞吐量上不去的情况——后来发现是网卡中断分配不均,所有流量都打到了一个CPU核心上。

问题排查:从现象到根因

压测发现问题后,怎么排查?我有一套固定的排查流程:

  1. 看CPU:用 tophtop,看每个核心的使用率。如果某个核心跑满,其他核心空闲,那就是惊群效应锁竞争
  2. 看内存:用 free -mvmstat,看有没有内存泄漏。我习惯每5秒采样一次,连续看几分钟。
  3. 看网络:用 netstat -s 看丢包和重传。重传率超过0.1%就要注意了。
  4. 看IO:用 iostat -x 1 看磁盘IO。如果 await 超过10ms,磁盘可能是瓶颈。

举个例子。有一次我压测时发现吞吐量上不去,CPU使用率只有40%。用 perf top 一看,发现 epoll_wait 占了60%的CPU时间。这说明什么?说明事件循环在空转,没有事件可处理。后来发现是连接数太少,请求处理太快,导致CPU大部分时间在等新连接。

核心思路:排查问题不是瞎猜,而是用数据说话。每个指标都指向一个方向,组合起来就能定位根因。

优化迭代:小步快跑

找到问题之后,就是优化了。我一般遵循「一次只改一个变量」的原则。改完就跑一次压测,看效果。这样你能清楚知道每个改动带来了多少收益。

常见的优化方向:

  • 线程模型:从单线程到多线程,再到Reactor模型。我建议用线程数等于CPU核心数,避免上下文切换。
  • 内存管理:用对象池代替频繁的new/delete。我曾在项目中用了一个简单的内存池,吞吐量提升了30%。
  • 锁优化:用读写锁代替互斥锁,或者用无锁队列。记住,锁的粒度越小越好。
  • IO模型:从阻塞IO到非阻塞IO,再到异步IO。epoll是Linux下的首选。

看一个具体的优化案例。这是优化前的代码:

// 优化前:每次请求都new一个对象
void handle_request(int fd) {
    Request* req = new Request();
    req->parse(fd);
    // ... 处理逻辑
    delete req;
}

优化后:

// 优化后:使用对象池
ObjectPool<Request> pool(1024);

void handle_request(int fd) {
    Request* req = pool.acquire();
    req->parse(fd);
    // ... 处理逻辑
    pool.release(req);
}

改动很小,但效果很明显。因为 newdelete 涉及系统调用和内存分配,在高并发下会成为瓶颈。

避坑指南:我曾经在优化时犯过一个错误——同时改了线程模型和内存管理,结果性能提升了50%,但完全不知道是哪个改动起的作用。后来花了半天时间回退验证。所以,一次只改一个变量,这是铁律。

项目总结:那些踩过的坑

做完这个项目,我总结了几条经验,分享给你:

  • 不要过早优化:先把功能跑通,再考虑性能。我见过太多人一开始就纠结于用epoll还是select,结果代码写了一周还没跑起来。
  • 压测要模拟真实场景:不要只压一个接口,要混合压。因为不同接口的处理时间不同,会影响整体表现。
  • 监控要到位:没有监控的优化就是盲人摸象。我建议至少监控CPU、内存、网络、磁盘四个维度。
  • 文档要跟上:每次优化都要记录做了什么、为什么做、效果如何。不然三个月后你自己都看不懂当时的代码。

下面这张图是我对这个项目的整体总结,你可以对照着回顾一下:

Web服务器性能优化全流程 1. 性能测试 wrk / ab 摸清家底 2. 压力测试 阶梯加压找拐点 3. 问题排查 CPU/内存/网络/IO 4. 优化迭代 一次只改一个变量 5. 验证效果 对比优化前后数据 6. 项目总结 经验沉淀与文档 持续迭代,直到满足目标 关键指标 吞吐量 (req/s) 延迟 (ms) 错误率 (%) CPU使用率 (%)

经验分享:给后来者的建议

最后,我想分享几点个人体会:

  • 动手比看书重要:网络编程的书我看了不下十本,但真正让我理解透彻的,是亲手写一个服务器并把它压垮的过程。
  • 学会看源码:遇到问题别急着搜博客,先去翻翻nginx、redis的源码。这些顶级项目的代码就是最好的教科书。
  • 重视基础:TCP/IP协议栈、操作系统原理、数据结构——这些基础打牢了,遇到任何问题你都能从底层去分析。
  • 保持好奇心:为什么这个参数设成64性能最好?为什么用这个锁比那个锁快?多问几个为什么,你会学到更多。

嗯,这个项目到这里就结束了。从设计到实现,从测试到优化,我们走完了一个完整的闭环。希望你能从中收获的,不只是代码,更是一种解决问题的思路和方法。


公众号:蓝海资料掘金营,微信deep3321