网络高并发的笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lengye7/article/details/86017208

嗯,这个数字以你的机器配置而言差不多到了ASIO的尽头了。

话说回来其实也不用特别纠结锁这事,在Linux下用epoll+多线程,想要没锁几乎是不可能的事,除非就像我之前说的那样,每个线程一个epollfd,不过这样又会有load balance的问题。
一般在大型应用中常见的做法是划分几个io_service/epollfd,每个io_service对应一组线程,这样既不会让负载特别不平衡,也能充分利用多个CPU core,尤其是服务器上有几十上百个core的时候,放在一个thread pool里调度简直是丧心病狂。

对于高负载应用,想要进一步提高性能还有几个思路,一个是interrupt coalescing以减少内核中断的次数,不过这个需要硬件核驱动的支持;一个是CPU/NIC/thread绑定,这样可以减少cache miss,另一个就是user-mode networking stack(当然这就是一个大坑),或者就像OSv那样干脆把你的应用放到核心态运行,这样Ring 0/3切来切去的开销就没有了。

至于TOE之类死贵的解决方案就不提了,反正不是壕公司不会用这玩意儿,再说这玩意儿其实也不太好用。

首先赞一个,可见楼主的网络编程功底炉火纯青 ^_^

然后谈谈这些年来我对服务端设计的一些看法。

可能是我身处于金融行业,技术老大说稳定比性能更重要,于是我们的设计方案相对保守,简单=稳定,举个例子,互联网行业的日志库都是打开日志文件,写写写...并做大量优化,我们的日志库是打开日志文件,写,关闭文件,再打开日志文件...技术老大说宁可速度慢,也要保证一旦core发生时,应用已经写出的日志必须能查到。我们的后台是自己研发的,多进程Leader-Follow模型,多进程而不是多线程是防止我们的交易开发人员写出的程序core时,不会影响其它正在处理的交易,Leader-Follow模型是因为其能简单的实现并发,仅此而已,没有很高大上很炫很酷的功能和机制,也没有很高的QPS。

这几年随着业务量大幅提升,单台服务器吃不消了,怎么办?技术老大说做横向扩展负载均衡,我们在不动原有服务端设计的前提下,又一次简单的大幅提高了整个后台的处理性能。

这么多年培养了我质朴的系统设计观,但有时候在网上看到伙伴们为了追求性能极致而提出不断优化的优秀方案时还保有一些激动。

最后谈一下我个人对服务端设计的一个看法,用户态的性能优化差不多已经到达极限了,如果还想有更高的提升,不妨考虑内核态,就好像现在的很多负载均衡软件性能其实都差不多,但始终都对LVS望尘莫及。

一个包都不丢,是否包含了断电的情况?如果战时状态,电源别切断备用电源也用光或者挂了,在有包要发但尚未发完的时候断电了,这个包是不是就丢了

应该也存在网络不好有尚未处理完的包时另一方断掉的情况

对方挂了,这个包怎么处理,持久化的方式,下次对方再连上来再发?

不知道这种和断电的情况算不算丢包的范围

如果不算,那就是网络畅通为前提,那样的话,估计tcp不胜任的情况不多见吧

其实说白了, 就是程序员要尽力做到逻辑上不丢包.  

为了说清楚这个问题, 还是讲个虚拟的故事吧:  

BOSS让网络专家开发了一个状态采集服务器A,  BOSS安排三个程序员:菜鸟程序员, 资深程序员, 大神程序员 分别完成三个子项目B,C,D子项目向状态采集服务器报告状态的客户端代码, 要求: 先完成一个简单的联调程序, 包括连接, 发送一个测试数据包, 拆除连接, 退出.  代码要高效, 出错可以重发, 不出错不重发, 不浪费网络资源, 但是要保证采集服务器收到数据. 因为服务器端压力很大, 为了节约服务器资源, 服务器只负责收, 对客户端无回馈信息.  这是个典型的单向服务器-客户端模型, 也是最简单的通信模型.

三个程序员拿到项目,  开始动手写起了代码.

菜鸟程序员很快完成子项目B的代码, 代码中, 他设置了错误检测语句, 确认发包的代码返回出错则重发, 不出错不重发, 发完之后, 为了提高效率, 他使用了硬关闭hard_close, 然后程序退出.  但只过了一会,菜鸟程序员埋的地雷爆炸了,他的数据包没有被发送到采集服务器A, 采集服务器上什么信息都没有.  

资深程序员仔细的编写了子项目C的联调代码, 设置了错误检测语句, 对各种情况都充分的考虑, 确认出错则重发, 不出错不重发, 发完之后, 为了安全可靠, 他使用了保守的软关闭soft_close, 然后程序退出. 采集服务器A收到了测试数据包, 一切似乎都很好很正常,  但是资深程序员为自己的未来之路埋设了地雷, 这个地雷什么时候爆炸不知道, 运气好的话, 也许永远都不会有人踩上去.  

大神程序员同样认真的编写了子项目D的代码, 同样设置了错误检测, 发送代码也是返回出错则重发, 不出错不重发, 发完之后, 他使用shutdown send, (TCP/IP将EOF标志附加在发送缓存队列的最后), 然后, 进入接收状态, 期望能够在收端收到些什么东西, 1毫秒之后, 收端没有收到任何有效字节的东西, 只返回了错误检测EOF, 大神确认此期间没有错误, close,   然后退出了程序.  否则,启动重连重发.  大神程序员不仅仅依赖错误检测, 他更依赖正确信号, 他没有给自己埋地雷. 

hard_close清除了TCP/IP发送缓冲区的数据, 缓冲区中未发出的数据包丢失在黑洞之中, 了无踪迹. 代码B的数据根本就没有发出去.
soft_close之后数据还在发送, 如果网络中断或者拥塞, 很偶然的情况下, 数据包同样丢失了, 但是代码C却对此一无所知, 未能弥补网络错误.
代码D收到了服务器端发过来的EOF标志, 就说明数据已经被完整的接收了, 这个是正确标志, 说明任务真正完成了. 
代码D完整的流程如下:
客户端发完数据, shutdown send,  (TCP/IP将EOF标志附加在发送缓存队列的最后), 服务器端从缓冲区中收数据, 收到最后一个字节以后, 给出错误, 检测到EOF标志(文件结束标志), 服务器端shutdown send/both或者soft close/hard close,  TCP/IP都将EOF标志附加在发送缓存队列的最后, 然后该标志被传送到客户端, 客户端收到此标志, 就确认客户端发送的每一个字节都已经被服务器端接收,于是close,  数据已经被完整发送到服务器A了.

对于非民用级别, 这些我前面说过了, 要在TCP/IP和应用层中间, 加1-2层, 这1-2层代码, 

国内各大电信级别设备制造商, 中国银监会等等, 各系统都有自己的设计构造, 与各外界计算机接口, 都有类似的东西.  

过去, 一直没有比较统一的思路.各系统数据交换格式,以及校验比较混乱.  Google等公司开源了部分代码, 提供了一些解决的思路.

一个功能点是数据切分,  Google先开源了Google protbuff, 后开源了Google flatbuff,  其中, 国内人多数只用protbuff, 但是, 实际上flatbuff性能更高, 高几倍的性能, Google flatbuff后开源, 所以, 使用的人不多,  Google一般都是把淘汰的东西, 拿出来开源, 内部应该有更好的东西了.

另外一个功能点就是  校验和高效加解密.  这个一般是关键性应用才使用的, 算非民用级别的东西, 这些是关键性应用等需要的. 例如: 航天的TCP/IP中间层,  这一层对应用是透明的, 或者是半透明的, 应用层不需要特殊处理, 就可以得到, 更可靠的数据. 

我准备后续做的就是这部分, 去掉了加解密部分不做, 做数据切分 +数据校验,  尽力跟Google的设计比拼一下, 还要一段时间努力, Google里面做protobuff 和flatbuff的都是牛人,

我过去没有做过类似的项目, 怕考虑不周, 慢慢做. 

好吧, 先说一个真实的事情,  时间久了,  原文可能有出入, 靠记忆转写. 

"TCP/IP校验不足"造成的"黑客入侵"事件

美国某著名的金融交易公司发生的虚假的 "非法入侵案", 某交易网络中断三天, 交易缓慢, 损失巨大.

某公司的网络, 号称是全世界最安全的网络, 5级防火墙, 清一色的全世界最好的设备, IBM, CISCO....., 其中IBM是那种号称双计算双校验, 永不出错系列的超级计算机, 

不说内存硬盘之类的, 即使是CPU核心, 也是每一步计算至少2个CPU同步计算, 计算结果同步硬件对比, 出错自动同步重做, 永不出错, 全部是硬件级的校验.

机房是那种安全级别最高的, 层层有授权, 平常基本没有人能够进入核心, 号称永不停机.

某天, 突然一级告警, 某个方向的交易数据提示, 检测到"非法的篡改的交易数据", 不是一条两条, 是铺天盖地的信息.  "黑客攻击???"  这是首先的反应......

很快各地纷纷告警, 交易被冻住了, 失去响应.  毕竟是超级大公司, 各类专家云集, 连续三天, 动用了所有可能动用的手段,  没有找到原因, 暗藏的黑手, 无影无踪........

老板已经通知了有关政府部门, 并且得到了强力支持, 全国专家云集, 一定要找到黑手, 不达目的, 绝不罢休, 三天没有抓住这个黑客,

后来其实很快已经有专家意识到, 问题可能出在内部, 但是,在没有证据的情况下, 谁都不敢乱说.

BOSS也坚持即使是内部原因, 也一定要找到, 绝不能轻易重启动服务器, 否则, 内部的炸弹不直到什么时候还会爆发.

第三天, 救火英雄出现了, 后来成为公司技术主管,  他找到了原因: TCP/IP校验不足的问题.

1) 为了保证可靠和高性能, 数据格式是定长的, 代码非常简洁易懂, 所有的数据全部有多组校验, 发现多组校验码不正确, 即可判断"交易数据"被篡改, 多个交易记录不一致, 触发一级告警.

2) 某台服务器是最后一道防火墙, 数据在这里做最后检查,确认后, 进入交易核心,  信息在进入核心前还有多次校验多次加密,  都没有用. 因为最后一道防火墙把这些包装在外面的校验层都去掉了.

3) 由于某种原因, 交易流的近一半是经过2)某个socket连接进核心的, 某socket单条连接上的流量惊人. 后来, 这条连接上的交易都失败了.该socket的流量将经过CISCO对称网络, 抵达核心层. 单CISCO路由器下线不影响服务.

4) 某台CISCO路由器(机房内)在某种情况下, 一个bit出现错误, 该bit位刚好是要转发的IP包的长度字节中的一位, 并且刚好是最低位. 路由器的内存无校验, 无法纠正此错误. 
    某数据包进入路由器, 和出路由器的一个数据包, 少了一个字节.  一个数据包的一个字节丢失的错误是造成整个事件的根本原因.

5) TCP/IP的校验码是16位的, 有6.5万分之一, 不能检测错误状态. 对于此单字节丢失, 恰好, 未能给予检测出.

6) 数据格式定长, 因此, 从这个数据包以后, 来自于某链路的全部交易数据都被系统认为是"非法的篡改的交易数据".

后来,系统的交易日志, 确认, 将所有日志数据向后移动一个字节, 并从上个日志补上最后一个字节, 就都正确了.

总之, 这个人也是牛人, 发现了所有的问题, 并且解释了所有的前因后果, 所以, 后来被提升为主管.


讲这个故事, 就是要说明一下, TCP/IP层与应用层之间, 为什么要加一层, 校验层, 这个校验层很有讲究的.
这个故事里, 该公司也有校验的, 还有几组校验, 算法各不相同, 还有私有算法的. 

另外, 也有人说, 我们公司每天交易上亿,从未出过错误, 我相信TCP/IP. 
其实吧, 在同一个HUB上, 不经过路由器的话,  不加校验也没有问题, 因为, TCP/IP的底层, 以太网接口层, 校验码的长度远超TCP/IP层, 也够用了.
 

这个算是大数据的底层架构之一:  网络错误自修复统一协议栈, 这一层是Google一直没有开源的一层架构. 

正在  自修复协议栈的开发, 正在做代码中. 这一层提供给应用层一个高可靠全透明的网络环境, 数据的读写结构是struct, 最终目标是访问远端数据跟访问本地共享内存中的一样简单可靠.

自修复协议栈的设计看上去很简单, 似乎怎么定义都可以, 

协议栈的核心就是定义TLVV: type, length, value, verify. 最后还有一个可选项control.  其中, 做代码的时候, 最难的部分是control, 这个是底部错误流程的体现, 

其实, 这个层多数游戏公司也都有做类似的东西. 各类网络接口, 都有类似的东西, 

就是只有大公司有统一架构, 其他部门直接调用库就可以了, 不需要考虑底层的东西. 过滤掉全部错误处理.

里面的门道很多的. 由于是底层库, IO层, 效率要求高, 可靠性要求高, 对应用层来说, 没有什么错误可见,  这层是一个高可靠的透明的东西, 自动重发,自动重连,自动数据校正, 跟访问本地内存的可靠性一样高.

所有的数据都跟UDP一样, 有完整性, 应用层收发的结构不是数据流, 是struct.  

这一层协议栈控制字节如果有流控设计, 下一层使用UDP也是可以的.

设计上要注意的地方也多,  先说4个点:

1)校验码的位置有讲究: 校验码要放在数据的结尾, 中间任何一Byte字节丢失后, 校验码由于被移位的原因, 一定会被检出.  放在前面, 对于丢字节的情况, 存在一定概率未被检出.

2)同步码很重要; 很多人不设计同步码的. 其实, 同步码长度要求不高, 允许与数据重复, 只有同步出错才用到. 找同步的时候, 结合校验码, 就跳过了疑似同步码, 对于单个数据包被破坏, 只要有同步头在, 最多丢弃这个包, 后续的数据包都是完整的. 再同步是相当的容易. 同步头一般放到最后或者最前, 个人倾向放到最后.

3)控制字节的位置重要: 一般要放到最前方的字节,出错概率最小.单字节无大小序, 对于后面的处理方便. 
控制字节最终决定控制流程, 统一架构的设计主要在这里体现, 大公司里一般非做此层库的, 一般不接触此控制字节的内容, 因为这些对上层是透明的, 看不到.

4)校验字节的校验方法很重要: 校验方法有很多, 为了可靠, 每个字节的32位校验+不支持情况下Boost crcr32c软件校验, 至少要有2次非相关的校验, 带来64位校验的效果, 只有2倍的处理复杂度, 提高到64位校验的精确度. 校验方法尽量采用硬件校验, 目前采用的是intel CPU自带的SSE4.2指令校验, 校验方法是crc32c, 校验速度基本是内存瓶颈.   标准的64位校验性能比较低.

最终修改代码, 想办法 把BOOST ASIO 的IO_SERVICE队列中的锁也去掉了,  呵呵呵呵呵

现在代码设置了一堆的IO_SERVICE,  放到vector中, 依次给 socket 使用.

参数multi,  处理收发IO_SERVICE的数量=逻辑CPU数量 乘以 multi, 

如果muti=1, 则  每个work下的IO_SERVICE由一个线程驱动的测试情况:  大约32.5万QPS ECHO.

如果muti=2, 则  每个work下的IO_SERVICE由2个线程驱动的测试情况:  大约31.4万QPS ECHO.

现在socket连接的收发处理中, 代码本身无锁, 但是底层对象唯一有锁的地方, 就是一个std::shared_ptr, 内部引用计数有锁, 个人也测试过, 更改为裸指针, 性能几乎无变化, 
大约每次ECHO, 对应2次shared_ptr的使用, 单独对比测试std::shared_ptr和裸指针, 大约1000万ECHO, 可以节约0.2秒. 不超过1%的变化
个人认为宁可不改, 因为有裸指针的代码, 可信度降低不少.

多线程代码中减少锁的使用, 降低锁冲突, 可以提高性能.

每一/二个线程一个io_service没别的问题,就是容易出现负载不平衡,有的连接忙死,有的连接闲死,你还没法挪……不过大多数情况下这不是个大问题。

另外shared_ptr的引用计数没有锁,只是个atomic<size_t>


接下来你可以做一下线程/CPU绑定,把malloc/new换成内存池,还能再榨出点性能来。

两种方案:
方案一, 按照Boost的介绍, 是可以保证只有一个操作能够并发, 也就保证了强一致性.
这个方案最终读写在核心态都是单线程, 靠外围lock-free协调解决冲突, 核心读写均是单并发. 这个比拼的是lock-free的协调能力.
现在, 只有这个方案是内存数据库的可选方案之一.

方案二, 有人曾经做过, 目前了解到的情况是:
代码写操作单线程, 可以提供写一致性, 同时并发读, 读并发没有数量限制, SSE提供校验方法, 读要校验, 校验失败要重新读, 来解决读写数据冲突, 特点是高并发读, 单并发写. 
主要是在一个小代码中做cache服务的, 一个是当时只选择hash表, 而且是一次性分配内存, 这样地址都固定, 读写都不涉及hash表结构的变化, hash表只做了一层, 无链表结构.
有重复直接覆盖, 所以,只保留最后的数据.
这个方案, 现在看来只是cache版, 无法担任数据库的技术解决方案.

回复 83# wlmqgzm 

lock free只能保证数据的“完整性”,但没法保证数据的“一致性”,尤其是当你需要transaction这种东西的时候,最好情况下你能得到MVCC这个级别的保障,但很多场合下这是远远不够的,而且即便是这个级别的保障也复杂的能干掉一票脑细胞。

你可以试试用lock free技术实现一个支持transaction的主从表数据结构,然后就会发现做出来的东西未必比直接用lock的版本快…………

其实, 我的lock-free方案已经初步成型了, 技术可以做的, 因为所有的操作核心是单线程的, 就是读队列,执行队列中的命令, 外围队列上使用了lock-free, 就是一个lock-free队列,没有其他代码,

现在就看明天测试了, 看核心代码效率.      上条回复,有修改.否决了方案二.

方案1这种所有load都在一个thread里,和加锁版的性能未必会有很大区别,而且由于request queue是一个lock free queue,这种queue不能block,所以当一个请求正在处理的时候其它所有I/O thread都在忙循环,相当的烧CPU。

方案2中你说的这种hash table,肯定是open addressing hash table对不,但这种hash table最大的问题就是尺寸上限不能变(不能rehash)而且key不能删,所以除非你你一下预留好大一块,否则跑着跑着就塞满了,这个时候除了报错退出没有任何办法可以挽救……

方案二, 这个hash不太大,  这个hash内容是可以覆盖的, hash也可以冲突, 老数据会被新数据覆盖. 

由于读struct中有校验码,并且校验码在最后, 读完struct就校验下,校验错就重读, 这样避免了struct写一半的情况

另外hash不用删, 办法是超时, 就是struct里面有时间变量, 超时无效, 就ok

这个是一个高性能无锁cache解决方案, 挺好的, 没有什么技术上的问题, 没有冲突, 该考虑的地方都有考虑.

其实, 仔细想来, 这个架构是目前的Memcache的理想替代品, 预计可以把Memcache的性能有一个大幅度的提高. 我再搜集点资料, 看先做哪个产品.... 

从代码数量和技术难度上看, 应该是cache好做一点, 好吧下周决定, 先从哪一个代码开始.

方案一, 所有load都在N个thread里, N=逻辑CPU的数量, 因此, 冲突概率要小N倍,  先把数据内存按CPU均分了, 每个CPU一个队列, 每个CPU只管自己一小部分,

lock free queue 的性能很好的, 比一般队列好, 设置一个大的队列,例如64k,  并且核心执行队列速度很快, 几乎就没有忙循环了,再有就usleep, 因为那么大的队列都满了,核心要多工作一会, 才能完成队列任务, 所以, 不存在烧CPU的问题, 因为usleep了.

覆盖是没问题的,hash冲突也是正常现象,但“不能删”这一点限制了它的用途,我之前写的Argos就因为这个必须要定期重建hash table,不大不小是个麻烦。

我个人觉得钻“无锁内存数据库”这个牛角尖价值不是太大,毕竟已经有一堆现成的性能不错的东西可以用,搞搞集群可能价值更高一些。
比如用gossip/raft/paxos做metadata同步的集群,可以随时扩充新节点,还可以在一定程度上保持数据一致性,毕竟这种东西现有的都不太好使。

Riak拿erlang写的,想改改都要头疼半天;Cassandra性能就是个渣;HBase读性能还说得过去但写性能比Cassandra还要渣;CouchDB的集群就是个玩笑;CouchBase的集群是一个比CouchDB还要搞笑的玩笑;ElasticSearch这东西集群也跟没集群差不多;MongoDB都集群了居然还有单点;MySQL……谁提MySQL来着…………

1. Lock free的各种实现都必须用到memory barrier,否则你根本做不出来
2. SSE指令不是原子操作,不能保证数的完整性和一致性
3. 在这种场合下checksum不能就地检测数据错误,除非你把整块数据copy出来自己再算一遍

所以你能做的就是:
要么把所有数据操作集中在1个thread里,就像redis那样,当然你可以用多个thread处理I/O
要么就使用race-free的数据结构保存数据,比如lock-free hash table或者干脆加把锁
这两个方案会导致所有的数据写操作都实际变成单线程,对你的系统吞吐量是有影响的。
想要降低这种影响,你只能切分数据,比如把一个hash table切成若干个,每个core/thread专门负责一个,这样可以降低冲突概率。
类似的手段就是集群,干脆把数据切分到多台机器上去,也一样可以降低冲突概率。

谢谢提醒, 多测试一下, 以前用过google-perftools里面带有tcmalloc, 好久不用都忘记了. 
只测试了最主流的三种, 默认:ptmalloc, 性能最快的tcmalloc, 号称多线程下有更好表现的jemalloc.  
ptmalloc是dlmalloc的分支, libumem没有找到新的版本, 只有2007年的版本, 就不测试了.
基本上还是tcmalloc性能最快.

另外, 更换内存引擎, Echo server收发包性能无变化, 因为accept时预先分配了12KByte, 单ab测试程序只有100个连接, 后来每个收发包都不涉及内存分配
应该对新建连接性能有影响. 

对于框架,我还是觉得自制一个memory pool会比较好,因为框架里的内存分配使用情况基本是已知的,用memory pool可以(几乎)完全消除掉所有的内存分配和释放。

http://bbs.chinaunix.net/thread-4189684-17-1.html

摘要:C10K问题让我们意识到:当并发连接达到10K时,选择不同的解决方案,笔记本性能可能会超过16核服务器。对于C10K问题,我们或绕过,或克服;然而随着并发逐渐增多,在这个后10K的时代里,你是否有想过如何去克服C10M。

既然我们已经解决了 C10K并发连接问题,应该如何提高水平支持千万级并发连接?你可能会说不可能。不,现在系统已经在用你可能不熟悉甚至激进的方式支持千万级别的并发连接。

要知道它是如何做到的,我们首先要了解Errata Security的CEO Robert Graham,以及他在Shmoocon 2013大会上的“无稽之谈”—— C10M Defending The Internet At Scale。

Robert用一种我以前从未听说的方式来很巧妙地解释了这个问题。他首先介绍了一点有关Unix的历史,Unix的设计初衷并不是一般的服务器操作系统,而是电话网络的控制系统。由于是实际传送数据的电话网络,所以在控制层和数据层之间有明确的界限。问题是我们现在根本不应该使用Unix服务器作为数据层的一部分。正如设计只运行一个应用程序的服务器内核,肯定和设计多用户的服务器内核是不同的。
也就是他所说的——关键要理解内核不是解决办法,内核是问题所在。

这意味着: 

    不要让内核执行所有繁重的任务。将数据包处理,内存管理,处理器调度等任务从内核转移到应用程序高效地完成。让Linux只处理控制层,数据层完全交给应用程序来处理。

最终就是要设计这样一个系统,该系统可以处理千万级别的并发连接,它在200个时钟周期内处理数据包,在14万个时钟周期内处理应用程序逻辑。由于一次主存储器访问就要花费300个时钟周期,所以这是最大限度的减少代码和缓存丢失的关键。

面向数据层的系统可以每秒处理1千万个数据包,面向控制层的系统,每秒只能处理1百万个数据包。

这似乎很极端,请记住一句老话:可扩展性是专业化的。为了做好一些事情,你不能把性能问题外包给操作系统来解决,你必须自己做。
现在,让我们学习Robert如何创建一个能够处理千万级别并发连接的系统。 
C10K问题——最近十年 

十年前,工程师处理C10K可扩展性问题时,尽量避免服务器处理超过1万个的并发连接。通过改进操作系统内核以及用事件驱动服务器(如Nginx和Node)代替线程服务器(Apache),这个问题已经被解决。人们用十年的时间从Apache转移到可扩展服务器,在近几年,可扩展服务器的采用率增长得更快了。

Apache的问题 

    Apache的问题在于服务器的性能会随着连接数的增多而变差

    关键点:性能和可扩展性并不是一回事。当人们谈论规模时,他们往往是在谈论性能,但是规模和性能是不同的,比如Apache。

    持续几秒的短期连接,比如快速事务,如果每秒处理1000个事务,只有约1000个并发连接到服务器。

    事务延长到10秒,要维持每秒1000个事务,必须打开1万个并发连接。这种情况下:尽管你不顾DoS攻击,Apache也会性能陡降;同时大量的下载操作也会使Apache崩溃。

    如果每秒处理的连接从5千增加到1万,你会怎么做?比方说,你升级硬件并且提高处理器速度到原来的2倍。发生了什么?你得到两倍的性能,但你没有得到两倍的处理规模。每秒处理的连接可能只达到了6000。你继续提高速度,情况也没有改善。甚至16倍的性能时,仍然不能处理1万个并发连接。所以说性能和可扩展性是不一样的。

    问题在于Apache会创建一个CGI进程,然后关闭,这个步骤并没有扩展。

    为什么呢?内核使用的O(N^2)算法使服务器无法处理1万个并发连接。

    内核中的两个基本问题:

    连接数=线程数/进程数。当一个数据包进来,内核会遍历其所有进程以决定由哪个进程来处理这个数据包。

    连接数=选择数/轮询次数(单线程)。同样的可扩展性问题,每个包都要走一遭列表上所有的socket。

    解决方法:改进内核使其在常数时间内查找。

    使线程切换时间与线程数量无关。
    使用一个新的可扩展epoll()/IOCompletionPort常数时间去做socket查询。

    因为线程调度并没有得到扩展,所以服务器大规模对socket使用epoll方法,这样就导致需要使用异步编程模式,而这些编程模式正是Nginx和Node类型服务器具有的;所以当从Apache迁移到Nginx和Node类型服务器时,即使在一个配置较低的服务器上增加连接数,性能也不会突降;所以在10K连接时,一台笔记本电脑的速度甚至超过了16核的服务器。

C10M问题——未来十年

不远的将来,服务器将要处理数百万的并发连接。IPv6协议下,每个服务器的潜在连接数都是数以百万级的,所以处理规模需要升级。

    如IDS / IPS这类应用程序需要支持这种规模,因为它们连接到一个服务器骨干网。其他例子:DNS根服务器,TOR节点,互联网Nmap,视频流,银行,Carrier NAT,VoIP PBX,负载均衡器,网页缓存,防火墙,电子邮件接收,垃圾邮件过滤。

    通常人们将互联网规模问题归根于应用程序而不是服务器,因为他们卖的是硬件+软件。你买设备,并将其应用到你的数据中心。这些设备可能包含一块Intel主板或网络处理器以及用来加密和检测数据包的专用芯片等。

    截至2013年2月,40gpbs, 32-cores, 256gigs RAM的X86服务器在Newegg网站上的报价是5000美元。该服务器可以处理1万个以上的并发连接,如果它们不能,那是因为你选择了错误的软件,而不是底层硬件的问题。这个硬件可以很容易地扩展到1千万个并发连接。

10M的并发连接挑战意味着什么:

    1千万的并发连接数 
    100万个连接/秒——每个连接以这个速率持续约10秒 
    10GB/秒的连接——快速连接到互联网。 
    1千万个数据包/秒——据估计目前的服务器每秒处理50K的数据包,以后会更多。过去服务器每秒可以处理100K的中断,并且每一个数据包都产生中断。 
    10微秒的延迟——可扩展服务器也许可以处理这个规模,但延迟可能会飙升。 
    10微秒的抖动——限制最大延迟 
    并发10核技术——软件应支持更多核的服务器。通常情况下,软件能轻松扩展到四核。服务器可以扩展到更多核,因此需要重写软件,以支持更多核的服务器。 

我们所学的是Unix而不是网络编程 

    很多程序员通过W. Richard Stevens所著的《Unix网络编程》学习网络编程技术。问题是,这本书是关于Unix的,而不只是网络编程。它告诉你,让Unix做所有繁重的工作,你只需要在Unix的上层写一个小服务器。但内核规模不够,解决的办法是尽可能将业务移动到内核之外,并且自己处理所有繁重的业务。

    这方面有影响的一个例子是Apache每个连接线程的模型。这意味着线程调度程序根据将要到来的数据确定接下来调用哪一个read()函数,也就是把线程调度系统当作数据包调度系统来用。(我真的很喜欢这一点,从来没有想过这样的说法)。

    Nginx宣称,它不把线程调度当作数据包调度程序,而是自己进行数据包调度。使用select找到socket,我们知道数据来了,就可以立即读取并处理数据,数据也不会堵塞。

    经验:让Unix处理网络堆栈,但之后的业务由你来处理。

怎样编写规模较大的软件?

如何改变你的软件,使其规模化?许多只提升硬件性能去支撑项目扩展的经验都是错误的,我们需要知道性能的实际情况。 

要达到到更高的水平,需要解决的问题如下: 

    数据包的可扩展性 
    多核的可扩展性 
    内存的可扩展性 

实现数据包可扩展——编写自己的个性化驱动来绕过堆栈 

    数据包的问题是它们需经Unix内核的处理。网络堆栈复杂缓慢,数据包最好直接到达应用程序,而非经过操作系统处理之后。

    做到这一点的方法是编写自己的驱动程序。所有驱动程序将数据包直接发送到应用程序,而不是通过堆栈。你可以找到这种驱动程序:PF_RING,NETMAP,Intel DPDK(数据层开发套件)。Intel不是开源的,但有很多相关的技术支持。

    速度有多快?Intel的基准是在一个相当轻量级的服务器上,每秒处理8000万个数据包(每个数据包200个时钟周期)。这也是通过用户模式。将数据包向上传递,使用用户模式,处理完毕后再返回。Linux每秒处理的数据包个数不超过百万个,将UDP数据包提高到用户模式,再次出去。客户驱动程序和Linux的性能比是80:1。

    对于每秒1000万个数据包的目标,如果200个时钟周期被用来获取数据包,将留下1400个时钟周期实现类似DNS / IDS的功能。

    通过PF_RING得到的是原始数据包,所以你必须做你的TCP堆栈。人们所做的是用户模式栈。Intel有现成的可扩展TCP堆栈

多核的可扩展性 

多核可扩展性不同于多线程可扩展性。我们都熟知这个理念:处理器的速度并没有变快,我们只是靠增加数量来达到目的。
大多数的代码都未实现4核以上的并行。当我们添加更多内核时,下降的不仅仅是性能等级,处理速度可能也会变得越来越慢,这是软件的问题。我们希望软件的提高速度同内核的增加接近线性正相关。
多线程编程不同于多核编程

    多线程

    每个CPU内核中不止一个线程
    用锁来协调线程(通过系统调用)
    每个线程有不同的任务

    多核

    每个CPU内核中只有一个线程
    当两个线程/内核访问同一个数据时,不能停下来互相等待
    同一个任务的不同线程

    要解决的问题是怎样将一个应用程序分布到多个内核中去

    Unix中的锁在内核实现。4内核使用锁的情况是大多数软件开始等待其他线程解锁。因此,增加内核所获得的收益远远低于等待中的性能损耗。

    我们需要这样一个架构,它更像高速公路而不是红绿灯控制的十字路口,无需等待,每个人都以自己的节奏行进,尽可能节省开销。

    解决方案:

    在每个核心中保存数据结构,然后聚合的对数据进行读取。

    原子性。CPU支持可以通过C语言调用的指令,保证原子性,避免冲突发生。开销很大,所以不要处处使用。

    无锁的数据结构。线程无需等待即可访问,在不同的架构下都是复杂的工作,请不要自己做。

    线程模型,即流水线与工作线程模型。这不只是同步的问题,而是你的线程如何架构。

    处理器关联。告诉操作系统优先使用前两个内核,然后设置线程运行在哪一个内核上,你也可以通过中断到达这个目的。所以,CPU由你来控制而不是Linux。

内存的可扩展性 

    如果你有20G的RAM,假设每次连接占用2K的内存,如果你还有20M的三级缓存,缓存中会没有数据。数据转移到主存中处理花费300个时钟周期,此时CPU没有做任何事情。

    每个数据包要有1400个时钟周期(DNS / IDS的功能)和200个时钟周期(获取数据包)的开销,每个数据包我们只有4个高速缓存缺失,这是一个问题。

    联合定位数据

    不要通过指针在满内存乱放数据。每次你跟踪一个指针,都会是一个高速缓存缺失:[hash pointer] -> [Task Control Block] -> [Socket] -> [App],这是四个高速缓存缺失。

    保持所有的数据在一个内存块:[TCB |socket| APP]。给所有块预分配内存,将高速缓存缺失从4减少到1。

    分页

    32GB的数据需占用64MB的分页表,不适合都存储在高速缓存。所以存在两个高速缓存缺失——分页表和它所指向的数据。这是开发可扩展的软件不能忽略的细节。

    解决方案:压缩数据,使用有很多内存访问的高速缓存架构,而不是二叉搜索树

    NUMA架构加倍了主存访问时间。内存可能不在本地socket,而是另一个socket上。

    内存池

    启动时立即预先分配所有的内存

    在对象,线程和socket的基础上进行分配。

    超线程

    每个网络处理器最多可以运行4个线程,英特尔只能运行2个。

    在适当的情况下,我们还需要掩盖延时,比如内存访问中一个线程在等待另一个全速的线程。

    大内存页

    减小页表规模。从一开始就预留内存,让你的应用程序管理内存。

总结

    网卡

    问题:通过内核工作效率不高
    解决方案:使用自己的驱动程序并管理它们,使适配器远离操作系统。

    CPU

    问题:使用传统的内核方法来协调你的应用程序是行不通的。
    解决方案:Linux管理前两个CPU,你的应用程序管理其余的CPU。中断只发生在你允许的CPU上。

    内存

    问题:内存需要特别关注,以求高效。

    解决方案:在系统启动时就分配大部分内存给你管理的大内存页

控制层交给Linux,应用程序管理数据。应用程序与内核之间没有交互,没有线程调度,没有系统调用,没有中断,什么都没有。
然而,你有的是在Linux上运行的代码,你可以正常调试,这不是某种怪异的硬件系统,需要特定的工程师。你需要定制的硬件在数据层提升性能,但是必须是在你熟悉的编程和开发环境上进行。 

原文连接:The Secret To 10 Million Concurrent Connections -The Kernel Is The Problem, Not The Solution (文/周小璐,审校/仲浩)

http://bbs.chinaunix.net/thread-4082130-1-2.html

猜你喜欢

转载自blog.csdn.net/lengye7/article/details/86017208