同样来自于很久之前的记录,边复习边添加了一些东西。很多细节忘了,现在再看这东西未免感觉十分…无语,但总归是感慨万分。
以下原博:
一直没有用 C++ 做过比较完整的“大”项目,所以趁着计网课设 + CSAPP 课后习题把这个经典(烂大街?)项目做一做。偷个懒,目标是 Reactor + 只有长连接 + (LT + ET)
Options
为了应对大并发,有必要对系统的一些变量进行修改。
-
/etc/sysctl.conf
1
2net.ipv4.tcp_max_syn_backlog = 40960
net.core.somaxconn = 40960知识点(来源 小林网络篇-TCP半连接队列和全连接队列):
-
(忽略
tcp_syncookies
)一个连接传入,依次判断半连接队列,全连接队列满否,最后如果max_syn_backlog
减去当前半连接队列长度小于(max_syn_backlog >> 2)
,则会丢弃。 -
半连接队列,即收到客户端第一次 SYN 包时的连接存放队列,并向客户端回 SYN+ACK 包,等待客户端确认连接(回传 ACK 包),随后内核将该连接取出,创建一个新的完全连接放置于全连接队列。
-
全连接队列,即经典三握后已建立连接的连接信息队列,等待服务端通过
accept
取出并创建socket
处理。 -
全连接队列长度:由
int listen(int sockfd, int backlog);
中的backlog
和net.core.somaxconn
共同控制,具体为backlog = min(backlog, somaxconn)
。 -
半连接队列长度比较复杂,其理论最大值由
max_qlen_log
控制,具体为qlen >> max_qlen_log
。1
2
3
4
5
6if max_syn_backlog > min(somaxconn, backlog) {
max_qlen_log = min(somaxconn, backlog) * 2;
}
else {
max_qlen_log = max_syn_backlog * 2;
}由1可知,分为两种情况:
1
2
3
4
5
6
7if (
当前半连接队列 < 半连接队列理论最大值 and
当前半连接队列 > 3/4 * max_syn_backlog
):
SYN_RECV最大值 = 3/4 * max_syn_backlog
else:
SYN_RECV最大值 = max_syn_backlog -
综上,
- 要想避免连接因为全连接队列满而被丢弃,则适当增大全连接队列长度,即同时增大
somaxconn
和backlog
。比如 worker 负载不过来,accept
函数调用不及时的情况 - 如果是半连接队列满的情况,常见SYN泛洪,即攻击者客户端只发送第一次握手的 SYN 包,不进行第三次握手的 ACK;或者突发高并发情况,半连接队列太小和网页应用处理效率不协调导致,方法当然也能增大半连接队列长度,即一同扩大
max_syn_backlog
和全连接队列长度;或者适当缩小重传未及时回包连接的次数;以及开启tcp_syncookies
,在半连接队列满时不放入队列,而是第二次握手计算一个cookie值与 SYN + ACK包一同发给客户端,客户端校验连接后直接建立连接,放入全连接队列。
- 要想避免连接因为全连接队列满而被丢弃,则适当增大全连接队列长度,即同时增大
-
-
文件描述符数量
1
ulimit -n 200000
每个连接都由一个 socket 管理,Linux 中表现为一个文件描述符。这东西是有上限的,爆了的话
accept
就会阻塞,或者非阻塞模式直接拒绝连接。 -
系统资源
其实还和系统资源有关,队列连接信息存储也是需要内存的。
Begin
先是跟着 CSAPP 写了个单进程单线程处理的(忘记过Web Bench了)
当然要跟上时代,随后就研究起多线程处理。
当然要先设定一个目标(某 Server BenchMark):
1 | Benchmarking: GET http://127.0.0.1:11451/8.svg (using HTTP/1.1) |
Profiler
我想要找到评估项目可以优化的点的方法,于是了解了 Profiler 这个工具。
项目中用了 Google 的 gperftool
插桩,利用信号启停以准确分析:
1 | void sigusrHandler(int signum) { |
生成火焰图:
1 | To cbt |
Process
最简单的想法就是把业务函数分离,丢到async
里达到多线程,当然结果惨不忍睹
(当时没控制变量,果咩)
1 | Runing info: 5000 clients, running 30 sec. |
该 Profiler 登场了:
火焰图原则(来源 如何读懂火焰图-阮一峰):
y 轴表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。
x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长。注意,x 轴不代表时间,而是所有的调用栈合并后,按字母顺序排列的。
火焰图就是看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。
于是一眼看过去,比如http
部分的的parse
,以及来自async
的线程调度开销。
于是写了个线程池,试了试只解析 headerLine:
一眼Parse
大锅,部分源码如下,也不知道是不是我 C++ 正则使用姿势不对(
1 | bool HttpContext::processRequestLine(const std::string& str) { |
手写解析状态机后的效果:
send
相对来说开销还是有一定比重。
接着写完parser
,Write
模块直接返回 404 page,压测一下
1 | Runing info: 1000 clients, running 10 sec. |
加大压力:
1 | Runing info: 10000 clients, running 60 sec. |
update:
改了一下文件传输思路。
根据文件传输大小分为两种传输方法:
- 小文件直接用
mmap
打到内存里做缓存 - 大文件用上
sendfile
函数
首先为了防止多个线程同时操作Buffer或者文件传输管理类导致读写指针的问题,所以 EPOLL 监听事件添加 ONESHOT 保证一个客户端读写只由一个线程负责;
由于是非阻塞读写,因此有必要有个记录指针一样的东西记录传输进度,防止线程调度导致的进度缺失。mmap
用offset
记录读写指针;sendfile
看起来倒还方便,利用文件描述符管理读写指针,发完后再reset
对应文件描述表的指针。
代码:
1 | // mmaper 是管理小文件映射的类,只要确保只有一个线程操作对象,就可以保证读写正确 |
1 | // 大文件也放这了,属实不应该( |
测试一下大文件传输速度,有点感人,和在 WSL 里面有关系?
update2:
处理了一个奇怪的闭包问题,同时考虑到全局变量带来的并发问题,于是将发送从Mmaper
解耦,将offset
等发送相关的东西放到另一个类Filer
中,以组合的方式存放于连接类对象中,也就是只要确保“一个线程一个连接”,那么就不会导致并发写的情况。Mmaper
仍在全局中,只需要在传输文件需要读写Mmaper
管理类对象时上锁。
再跑一次 WebBench
-
smallhyl_512K
1
2
3
4Runing info: 1000 clients, running 10 sec.
Speed=1514088 pages/min, 12926612 bytes/sec.
Requests: 252348 susceed, 0 failed. -
smallhyl_512K_10000_clients
1
2
3
4Runing info: 10000 clients, running 60 sec.
Speed=752712 pages/min, 1894325 bytes/sec.
Requests: 752712 susceed, 0 failed.dps有下降
-
bighyl_1G(太多连接内存会爆炸)
1
2
3
4Runing info: 100 clients, running 10 sec.
Speed=13769826 pages/min, -85439584 bytes/sec.
Requests: 2294971 susceed, 0 failed.
火焰图:
小文件:
大文件:
似乎sendfile
确实很可以
Final
其实这并不是第一版,第一版是课设的东糊西糊版本,自己写的Buffer
还有问题,不过好在错误处理得好,没在验收的时候爆炸。
后来不甘心,想着自己再磨一个。这一版中途因为各种原因,时间跨度比较长,中途疯狂补习大量知识,回头比较一些比较早写的代码,风格有很大的差异;中途还甚至想推翻了重新写,但写着写着感觉还不如缝缝补补,终于是坚持了下来;
感慨最深的是没有先写logger
,遇到问题啥头绪没有,只能单步调试(
参考了许多前辈的代码,比如最多 star 的那个(忘了也懒得找了),但是用的是裸指针管理对象,心里想着这不现代,又按照他的介绍看了看一个基于 C++11 的实现;最后根据 Profiler 自己研究一下send
模块。虽然很多功能没有完善,不过可以算基本完成了 Web Server 的学习(
Test Environment
Key | Value |
---|---|
操作系统 | Windows 10 |
运行环境 | Windows Subsystem for Ubuntu 22.04 |
CPU | i3-12100F |
内存 | 16 GB,3200 MHZ |