几个重要指标的关系
QPS= 并发数/平均响应时间
并发数 = QPS*平均响应时间
也就是说,并发连接数代表服务器抗压能力,接收连接的能力。qps代表在相同的并发数下,服务器处理的速度,响应时间越短,那么qps就越大。
不是说并发越多,qps越大,qps可能会在一个合适并发数,才会表现最大,就是每秒处理请求最多。
先学几个压测的工具。webbench,ab, wrk压测
webbench的标准测试可以向我们展示服务器的两项内容:每秒钟相应请求数(qps)和每秒钟传输数据量。webbench不但能具有标准静态页面的测试能力,还能对动态页面(ASP,PHP,JAVA,CGI)进 行测试的能力。
它的原理就是fork很多子进程,子进程随机选择ip+port,对你的网站发起连接,并发数,请求次数,持续时间自己设定。最后把结果汇总到父进程。
Webbench最多可以模拟3万个并发连接去测试网站的负载能力。
WebBench安装
首先下载 webbench脚本 http://home.tiscali.cz/cz210552/distfiles/webbench-1.5.tar.gz(或者去github下载)
解压webbench tar xvzf webbench-1.5.tar.gz
进入 webbench 目录
切换 root帐号:su root
安装 make && make install
测试命令
webbench -c 300 -t 60 http://test.domain.com/phpinfo.php
webbench -c 并发数 -t 运行测试时间 URL
就相当于向该网站发送100个连接即线程请求,持续60s。
会出现如下结果
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.Benchmarking: GET http://test.domain.com/phpinfo.php
300 clients, running 60 sec.Speed=24525 pages/min, 20794612 bytes/sec.
Requests: 24525 susceed, 0 failed.
每秒钟响应请求数(qps):24525 pages/min,每秒钟传输数据量20794612 bytes/sec.
返回数:24525次返回成功,0次返回失败
并发数量改成1000,
Speed=24920 pages/min, 21037312 bytes/sec.
Requests: 24833 susceed, 87 failed.
说明超负荷了,有失败的请求了。
压测遇到的问题
1、muduo并发数太少?
在muduo库测试的时候并发1000就出现failed问题,这不正常。按理说它10k都没问题的。
因为muduo库的logging做的确实很好 出问题了 马上就会以 LOG_ERRNO显示出来 我就发现是 Acceptor accept出现了问题 而显示的原因是OPEN FILE TOO MUCH …
之后通过top lsof看了看 发现cpu占比也是正常的 只跑到了45% 说明负荷也不大啊 lsof之后看了看文件描述符的占用情况 就发现 只开到了1024… 终于发现问题了
原来是linux默认只允许一个进程打开1024个文件描述符 我就说什么情况… 对于一个c1k1000并发连接的服务器而言 怎么可能是足够的…
下面是单次修改 file_open参数的方法 暂时我还不知道怎么永久修改
1、sudo vim /etc/security/limits.conf
这里的话 在最底部增加以下两行即可(进程可打开最大文件描述符数目65536)
* soft nofile 65536
* hard nofile 655362、ulimit -n 65536(设置目前用户的最大文件描述符数目 重启后重置为默认1024)
3、ulimit -n或者ulimit -a(查看当前目前最大可开启文件描述符数目)
muduo短连接应该31000qps左右
前面这个问题的解决要知道下面的知识
并发连接数受哪些因素影响?
首先肯定本质是受硬件影响。对于长连接来说,并发数跟承载数量有关,也就是线程数量有关,理论上,创建的线程越多,并发数越大。但是线程创建又受到内存影响,线程会占用内存空间,对于4个G的内存,几百个线程就满了。实际上,线程不可能创建这么多,对于计算密集型任务忽略IO,线程数和CPU核数一致就可以了,目的是充分利用CPU;对于IO密集型任务,线程数=核数(1+IO/CPU),线程太多反而容易引起线程频繁切换导致CPU利用率变低。除了CPU核数外,还有网路带宽的影响,这个一般成为瓶颈。
在排除硬件影响后,还有几个特殊的因素需要考虑。第一就是可以提供的文件描述符数量,一个文件描述符才能提供一个TCP连接,进程能打开的文件描述符默认为1024,这当然会影响并发连接数,最多只能连接1000左右,因为系统进程本身就要用掉一些文件描述符。所以要记得修改这个默认数值。
第三就是,有没有可能文件描述符以及其他资源等打开忘记关闭了造成内存泄漏,这会导致你的服务器在几十秒内内存爆掉,每次都用新的文件描述符,直接死机。内存占用率是越来越高 到后面直接被爆掉 进程被杀死。(解决办法,智能指针,怎么用智能指针解决内存泄露?)
2、内存占用越来越大,用智能指针解决内存泄露,回收文件描述符
可以用top查看。
3、主线程CPU使用率过高且客户端很多连接超时(没有log的情况下),而子线程很低
并发1000,按理说不应该有这么高的CPU使用率,正常应该,30左右。
问题出现在了listen的第二个参数 backlog改成几百就可以了。
试想下1W个并发,connect的时候,队列满了之后,服务器就不理会客户的connect,客户只能再次尝试,如果碰巧这时候队列还是满的。那么就再次尝试,如果命真的那么差。。估计挂掉之前都连不上,就超时了!!!!因为客户端不断尝试,那么就会不断和服务器发起三次握手的过程,服务器就会忙于和它建立握手,发送syn+ack等,所以cpu资源使用很多。
联系一些很多DDos攻击,就是一些半连接攻击或者全连接攻击,半连接攻击一些虚拟的ip来访问服务器,建立三次握手,但是最后一次不回复ack,那么就会存在半连接队列,让服务器瘫痪。
全连接就是建立三次握手,但不做事,一直在握手,也会导致服务器内存和CPU资源瘫痪。
我当时设置的backlog就是5。因为很多书上就是5,我认为这是一个比较好的值,就跟vector扩容为什么在1.5-2倍好。
listen(n)传入的值, n表示的是服务器拒绝(超过限制数量的)连接之前,操作系统可以挂起的最大连接数量。n也可以看作是"排队的数量"
TCP有如下两个队列:
SYN队列(半连接队列):当服务器端收到客户端的SYN报文时,会响应SYN/ACK报文,然后连接就会进入SYN RECEIVED状态,处于SYN RECEIVED状态的连接被添加到SYN队列,并且当它们的状态改变为ESTABLISHED时,即当接收到3次握手中的ACK分组时,将它们移动到accept队列。SYN队列的大小由内核参数/proc/sys/net/ipv4/tcp_max_syn_backlog设置。
accept队列(完全连接队列):accept队列存放的是已经完成TCP三次握手的连接,而accept系统调用只是简单地从accept队列中取出连接而已,并不是调用accept函数才会完成TCP三次握手,accept队列的大小可以通过listen函数的第二个参数控制。
4.优化工作线程,发现和muduo的qps差16%左右,工作线程cpu使用率高10%左右。
有没有可能是写工作业务代码解析http的时候(http echo),比较复杂,可以优化?协议解析?
5.开始超越?
分析瓶颈,然后自己的设计,线程池工作队列处理部分?协程部分? ET模式?
6 长连接短连接怎么测试?对压测结果影响的意义?
1000个并发数
服务器 | 短连接QPS | 长连接QPS |
---|---|---|
WebServer | 126798 | 335338 |
Muduo | 88430 | 358302 |
可以看出,长连接下的qps是短连接的3到四倍左右,因为没有了tcp连接建立和断开的开销,不需要频繁accept和shutdown\close等系统调用,也不需要频繁建立和销毁对应的结构体。
之所以可以超越muduo,是因为采用了ET模式。muduo是水平触发,et模式适用于高并发短连接的场景。
水平触发:socket接收缓冲区不为空,有数据可读,读事件一直触发。只要可读,就一直触发读事件,只要可写,就一直触发写事件。边缘触发:从不可读变为可读,从可读变为不可读,从不可写变为可写,从可写变为不可写,都只触发一次。当使用ET做触发模式时,我们通常使用while()循环来将sockfd的数据全部读出来
水平触发:
1.从accept成功之后,可写事件就会一直触发,直到发送缓冲区满了。对方给你发送了数据,只要你的接收缓冲区还有数据没取完就会一直触发可读事件。
2.当对方断开连接,不管是主动断开还是异常断开,都会触发可读可写事件。为什么?因为你recv和send的时候都会给你返回(read主动断开0,异常断开-1,write返回-1,触发SIGPIPE)。
int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );//通过epoll_wait监听到epoll事件响应,events中会保存响应的事件队列
sockfd = events[i].data.fd;//从事件队列中取出对应的文件描述符fd
n = recv(sockfd , buff, MAXLNE, 0);//通过recv将sockfd缓冲区的数据接收进buff中int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );//通过epoll_wait监听到epoll事件响应,events中会保存响应的事件队列
sockfd = events[i].data.fd;//从事件队列中取出对应的文件描述符fd
m_read_idx = 0;//记录一次读数据后的位置索引
while(true) {//// 从buff + m_read_idx索引出开始保存数据n = recv(sockfd , buff + m_read_idx, MAXLNE, 0 );//通过recv将sockfd缓冲区的数据接收进buff中if (n == -1) {if( errno == EAGAIN || errno == EWOULDBLOCK ) {// 没有数据break;} } else if (n == 0) { // 对方关闭连接//close(sockfd);break;}m_read_idx += n;
}
水平触发虽然编码简单,但是重复地事件触发会影响高并发服务器地性能,因为epoll_wait返回监控事件涉及到系统调用,需要用户态-内核态的转换。特别是对于高并发短连接,会频繁返回由于没有读完数据,并且短连接频繁断开链接也会触发可读可写事件,让epoll返回。
所以看到,短连接1000并发下,ET模式比LT模式性能高,长连接的话相差不大。
ET真的比LT快吗?
首先,如果并发量不大,或者说读取的数据都是小数据,按理说应该没有区别的;如果是大量数据,LT就会产生更多无畏的系统调用,慢一点;
从ET的处理过程中可以看到,ET的要求是需要一直读写,直到返回EAGAIN,否则就会遗漏事件。而LT的处理过程中,直到返回EAGAIN不是硬性要求,但通常的处理过程都会读写直到返回EAGAIN,但LT比ET多了一个开关EPOLLOUT事件的步骤
其次,如果server的响应通常较小,不会触发EPOLLOUT,那么适合使用LT,例如redis等。而nginx作为高性能的通用服务器,网络流量可以跑满达到1G,这种情况下很容易触发EPOLLOUT,则使用ET。
综上,对于高并发大数据肯定是ET好,小数据的话ET稍微快一点,原因是如果写出1m大小数据,一次写不完,LT会开启EPOLLOUT事件,如果写完关闭;ET是一直写,直到写完或者返回EGAIN,如果写完是不开启EPOLLOUT.
**如何通过压测等其他方法找到性能瓶颈?**要有自己的思考
文件描述符没有及时关闭?
处理逻辑问题?
总结:
1、遇到问题,1000就会有失败的连接了。第一,文件描述符限制问题,1024修改,发现还是不行,原来是ET模式导致accept只会发一次通知,由于并发量很大,所以没有及时加到epoll里面去,那么很多连接没办法及时加就会超时,导致连接失败;办法就是accept用while包一层,直到没有再回去。而且不仅仅是accept,对于read,write都要保证一次搞完不然数据不对的。所以我直接用LT,发现效率更高一点。
所以发现其实并不是ET比LT高效,得看什么场景。
首先对于读,其实没有什么影响,因为高并发下epollwait返回本来就频繁,不会因为你没读完就多返回很多次。实际上影响最大的是write.原因是,LT写缓冲区没满就会继续写。所以当你数据写完后返回eagin,这时候必须关闭一下epoll_ctl关闭一下写事件,不然写会一直触发,没有机会返回epollwait监听,回去后再加上。而ctl是系统调用,就会多了几次系统调用。而数据量越大,中间出错的概率高,所以这时候返回是要继续写的,LT就比较适合了。反而ET一次性读完会导致其他fd饥饿。
比如redis用的就是LT,nignx是ET.潜在的原因是redis数据量小,而nignx很有可能数据跑到1G这么多,数据越多,其实主要影响是在
- backlog问题。
主线程CPU使用率过高且客户端很多连接超时(没有log的情况下),而子线程很低**
并发1000,按理说不应该有这么高的CPU使用率,正常应该,30左右。子线程很低说明请求很少,因为客户端很多没有连接上。
这里我开始怀疑我的协程是不是有问题,但是按理说也只是多了一次子协程多主协程的切换,最多就寄存器换几个值,怎么可能出现CPU利用率这么高而且连接不上?
这里卡了很久。 后来发现一个点就是listen的第二个参数
问题出现在了listen的第二个参数 backlog改成几百582就可以了。
试想下1W个并发,connect的时候,队列满了之后,服务器就不理会客户的connect,客户只能再次尝试,如果碰巧这时候队列还是满的。那么就再次尝试,如果命真的那么差。。估计挂掉之前都连不上,就超时了!!!!因为客户端不断尝试,那么就会不断和服务器发起三次握手的过程,服务器就会忙于和它建立握手,发送syn+ack等,所以cpu资源使用很多。
联系一些很多DDos攻击,就是一些半连接攻击或者全连接攻击,半连接攻击一些虚拟的ip来访问服务器,建立三次握手,但是最后一次不回复ack,那么就会存在半连接队列,让服务器瘫痪。
全连接就是建立三次握手,但不做事,一直在握手,也会导致服务器内存和CPU资源瘫痪。
我当时设置的backlog就是5。因为很多书上就是5,我认为这是一个比较好的值,就跟vector扩容为什么在1.5-2倍好。
listen(n)传入的值, n表示的是服务器拒绝(超过限制数量的)连接之前,操作系统可以挂起的最大连接数量。n也可以看作是"排队的数量"
TCP有如下两个队列:
SYN队列(半连接队列):当服务器端收到客户端的SYN报文时,会响应SYN/ACK报文,然后连接就会进入SYN RECEIVED状态,处于SYN RECEIVED状态的连接被添加到SYN队列,并且当它们的状态改变为ESTABLISHED时,即当接收到3次握手中的ACK分组时,将它们移动到accept队列。SYN队列的大小由内核参数/proc/sys/net/ipv4/tcp_max_syn_backlog设置。
accept队列(完全连接队列):accept队列存放的是已经完成TCP三次握手的连接,而accept系统调用只是简单地从accept队列中取出连接而已,并不是调用accept函数才会完成TCP三次握手,accept队列的大小可以通过listen函数的第二个参数控制。
这里改完之后就正常了。
大概有八万左右的,和muduo差不多。或许不用协程会更高一点。
3、找瓶颈。
分析过程。我发现了一个问题,就是10000连接的时候子线程的CPU使用率,包括主线程的使用率都不是很高(70左右)?按理说,
我这种主从reactor模型在这么大的并发量,主线程,子线程CPU使用率应该跑满的,因为我没有连接数据库缓存rpc什么的,子线程获取任务直接发送数据了,不会有这么多的阻塞等待;
我发现了,问题就出在任务队列。需要加锁,而且是锁住整个队列,出队入队是串行化的。这样就会阻塞住。(出队入队是比较耗时的,要找到坐标。因为存在伪共享的问题,head,tail会互相影响,导致缓存行失效,每次都从内存取,需要几十到几百个时钟周期,并且pop要删除任务,删除操作需要空闲链表管理)。 所以我们采用的是一个环形的无锁消息队列。预先分配一个环形队列…
搞完后,发现cpu使用率增加了,大概92。 qps明显提高了40左右
top可以查看动态的CPU使用率,首先一个总体的。(进程数、CPU使用率、内存使用情况,SWAP分区情况) 然后下面是进程列表的具体情况;(很有可能进程超过100%,因为开启了多线程,NUMA架构,每个CPU加起来)
但是我们要查的是线程的使用情况。
top -H -p 15736 先看进程,然后-H参数可以看里面线程的CPU使用情况。
4 未来展望
C10K-C10M
首先硬件不是问题。核心是软件。(阻塞不仅仅在IO,还可能在数据拷贝,地址翻译,内存管理,数据处理)
问题核心:不要让OS内核执行所有繁重的任务:将数据包处理、数据拷贝,内存管理、处理器调度,地址翻译等任务从内核转移到应用程序高效地完成,让诸如Linux这样的OS只处理控制层,数据层完全交给应用程序来处理。
1、要知道linux内核一开始就是一个分时系统,最重要的是保证公平性,CFS算法。而我们的实际任务可能会有差别,所以最好把调度让应用程序完成,减轻内核调度的负担以及无意义的调度;
2、核绑定,任务可能会在不同的CPU核上运行,尤其现在是NUMA架构,会导致上下文的切换,以及cache命中率的问题;
3、数据包直接和应用层交互,Linux协议栈是复杂和繁琐的,数据包经过它无非会导致性能的巨大下降,并且会占用大量的内存资源。比如DPDK的网卡驱动就是在应用层,数据不经过内核。(当然发给自己的数据包肯定要经过内核)
4、大页机制的开启,减少地址转换开销。(4kb-2mb)
5、零拷贝技术(kafka),sendfile。直接在内核的page cache利用内存映射,然后记录偏移量和总量,读的时候直接读不要复制到socket内核缓存。(就比如我们线程池处理完发回客户端就要经过这些四次拷贝,可能零拷贝之后CPU利用率更高了)
2.1. HTTP echo 测试 QPS
测试机配置信息:Centos虚拟机,内存6G,CPU为4核
测试工具:wrk: https://github.com/wg/wrk.git
部署信息:wrk 与 TinyRPC 服务部署在同一台虚拟机上, 关闭 TinyRPC 日志
测试命令:
// -c 为并发连接数,按照表格数据依次修改
wrk -c 1000 -t 8 -d 30 --latency 'http://127.0.0.1:19999/qps?id=1'
测试结果:
QPS | WRK 并发连接 1000 | WRK 并发连接 2000 | WRK 并发连接 5000 | WRK 并发连接 10000 |
---|---|---|---|---|
IO线程数为 1 | 27000 QPS | 26000 QPS | 20000 QPS | 20000 QPS |
IO线程数为 4 | 140000 QPS | 130000 QPS | 123000 QPS | 118000 QPS |
IO线程数为 8 | 135000 QPS | 120000 QPS | 100000 QPS | 100000 QPS |
IO线程数为 16 | 125000 QPS | 127000 QPS | 123000 QPS | 118000 QPS |