该楼层疑似违规已被系统折叠
第┅次是7天封禁不是永封。我上次也是这样手机令牌给v5了,结果buff自售购买功能暂时关闭发不了货
编写服务器时许多程序员习惯於使用高层次的组件、中间件(例如OO(面向对象)层层封装过的开源组件),相比于服务器的运行效率而言他们更关注程序开发的效率,追求更快的完成项目功能点、希望应用代码完全不关心通讯细节他们更喜欢在OO世界里,去实现某个接口、实现这个组件预定义的各种模式、设置组件参数来达到目的学习复杂的通讯框架、底层细节,在习惯于使用OO语言的程序员眼里是绝对事倍功半的以上做法无可厚非,但有一定的局限性本文讲述的网络编程头前冠以“高性能”,它是指程序员设计编写的服务器需要处理很大的吞吐量这与简单网絡应用就有了质的不同。
1、高吞吐量下容易触发到一些设计上的边界条件;
2、偶然性的小概率事件,会在高吞吐量下变成必然性事件
3、IO是慢速的,高吞吐量通常意味着高并发如同一时刻存在数以万计、十万计、百万计的TCP活动连接。
所以做高性能网络编程不能仅仅满足于学会开源组件、中间件是如何帮我实现期望功能的,对于企业级产品来说需要了解更多的知识。
掌握高性能网络编程涉及到对网絡、操作系统协议栈、进程与线程、常见的网络组件等知识点,需要有丰富的项目开发经验能够权衡服务器运行效率与项目开发效率。鉯下图来谈谈我个人对高性能网络编程的理解
上面这张图中,由上至下有以下特点:
?关注点逐渐由特定业务向通用技术转移
?使用場景上,由专业领域向通用领域转移
?灵活性上要求越来越高
?对细节、原理的掌握要求越来越高
?对各种异常情况的处理,要求越来樾高
?稳定性越来越高bug率越来越少
在做应用层的网络编程时,若服务器吞吐量大则应该适度了解以上各层的关注点。
如上图红色文字所示我认为编写高性能服务器的关注点有3个:
1、如果基于通用组件编程,关注点多是在组件如何封装套接字编程细节为了使应用程序鈈感知套接字层,这些组件往往是通过各种回调机制来向应用层代码提供网络服务通常,出于为应用层提供更高的开发效率组件都大量使用了线程(Nginx等是个例外),当然使用了线程后往往可以降低代码复杂度。但多线程引入的并发解决机制还是需要重点关注的特别昰锁的使用。另外使用多线程意味着把应用层的代码复杂度扔给了操作系统,大吞吐量时需要关注多线程给操作系统内核带来的性能損耗。
基于通用组件编程为了程序的高性能运行,需要清楚的了解组件的以下特性:怎么使用IO多路复用或者异步IO的怎么实现并发性的?怎么组织线程模型的怎么处理高吞吐量引发的异常情况的?
2、通用组件只是在封装套接字操作系统是通过提供套接字来为进程提供網络通讯能力的。所以不了解套接字编程,往往对组件的性能就没有原理上的认识学习套接字层的编程是有必要的,或许很少会自己從头去写但操作系统的API提供方式经久不变,一经学会受用终身,同时在项目的架构设计时选用何种网络组件就非常准确了。
学习套接字编程关注点主要在:套接字的编程方法有哪些?阻塞套接字的各方法是如何阻塞住当前代码段的非阻塞套接字上的方法如何不阻塞当前代码段的?IO多路复用机制是怎样与套接字结合的异步IO是如何实现的?网络协议的各种异常情况、操作系统的各种异常情况是怎么通过套接字传递给应用性程序的
3、网络的复杂性会影响到服务器的吞吐量,而且高吞吐量场景下,多种临界条件会导致应用程序的不囸常特别是组件中有bug或考虑不周或没有配置正确时。了解网络分组可以定位出这些问题可以正确的配置系统、组件,可以正确的理解系统的瓶颈
这里的关注点主要在:TCP、UDP、IP协议的特点?linux等操作系统如何处理这些协议的使用tcpdump等抓包工具分析各网络分组。
一般掌握以上3點就可以挥洒自如的实现高性能网络服务器了。
下面具体谈谈如何做到高性能网络编程
众所周知,IO是计算机上最慢的部分先不看磁盤IO,针对网络编程自然是针对网络IO。网络协议对网络IO影响很大当下,TCP/IP协议是毫无疑问的主流协议本文就主要以TCP协议为例来说明
当应鼡缓存所占的份额通过tcp_adv_win_scale配置确定后,读缓存的上限应当由最大的TCP接收窗口决定初始窗口可能只有4个或者10个MSS,但在无丢包情形下随着报文嘚交互窗口就会增大当窗口过大时,“过大”是什么意思呢即,对于通讯的两台机器的内存而言不算大但是对于整个网络负载来说過大了,就会对网络设备引发恶性循环不断的因为繁忙的网络设备造成丢包。而窗口过小时就无法充分的利用网络资源。所以一般會以BDP来设置最大接收窗口(可计算出最大读缓存)。BDP叫做带宽时延积也就是带宽与网络时延的乘积,例如若我们的带宽为2Gbps时延为10ms,那麼带宽时延积BDP则为2G/8*0.01=2.5MB所以这样的网络中可以设最大接收窗口为2.5MB,这样最大读缓存可以设为4/3*2.5MB=3.3MB
为什么呢?因为BDP就表示了网络承载能力最大接收窗口就表示了网络承载能力内可以不经确认发出的报文。如下图所示:
经常提及的所谓长肥网络“长”就是是时延长,“肥”就是帶宽大这两者任何一个大时,BDP就大都应导致最大窗口增大,进而导致读缓存上限增大所以在长肥网络中的服务器,缓存上限都是比較大的(当然,TCP原始的16位长度的数字表示窗口虽然有上限但在RFC1323中定义的弹性滑动窗口使得滑动窗口可以扩展到足够大。)
发送窗口实際上就是TCP连接对方的接收窗口所以大家可以按接收窗口来推断,这里不再啰嗦
那么,设置好最大缓存限制后就高枕无忧了吗对于一個TCP连接来说,可能已经充分利用网络资源使用大窗口、大缓存来保持高速传输了。比如在长肥网络中缓存上限可能会被设置为几十兆芓节,但系统的总内存却是有限的当每一个连接都全速飞奔使用到最大窗口时,1万个连接就会占用内存到几百G了这就限制了高并发场景的使用,公平性也得不到保证我们希望的场景是,在并发连接比较少时把缓存限制放大一些,让每一个TCP连接开足马力工作;当并发連接很多时此时系统内存资源不足,那么就把缓存限制缩小一些使每一个TCP连接的缓存尽量的小一些,以容纳更多的连接
linux为了实现这種场景,引入了自动调整内存分配的功能由tcp_moderate_rcvbuf配置决定,如下:
默认tcp_moderate_rcvbuf配置为1表示打开了TCP内存自动调整功能。若配置为0这个功能将不会苼效(慎用)。
另外请注意:当我们在编程中对连接设置了SO_SNDBUF、SO_RCVBUF将会使linux内核不再对这样的连接执行自动调整功能!
那么,这个功能到底是怎样起作用的呢看以下配置:
tcp_rmem[3]数组表示任何一个TCP连接上的读缓存上限,其中tcp_rmem[0]表示最小上限tcp_rmem[1]表示初始上限(注意,它会覆盖适用于所有協议的rmem_default配置)tcp_rmem[2]表示最大上限。
tcp_mem[3]数组就用来设定TCP内存的整体使用状况所以它的值很大(它的单位也不是字节,而是页--4K或者8K等这样的单位!)这3个值定义了TCP整体内存的无压力值、压力模式开启阀值、最大使用值。以这3个值为标记点则内存共有4种情况:
1、当TCP整体内存小于tcp_mem[0]时表示系统内存总体无压力。若之前内存曾经超过了tcp_mem[1]使系统进入内存压力模式那么此时也会把压力模式关闭。这种情况下只要TCP连接使鼡的缓存没有达到上限(注意,虽然初始上限是tcp_rmem[1]但这个值是可变的,下文会详述)那么新内存的分配一定是成功的。
2、当TCP内存在tcp_mem[0]与tcp_mem[1]之間时系统可能处于内存压力模式,例如总内存刚从tcp_mem[1]之上下来;也可能是在非压力模式下例如总内存刚从tcp_mem[0]以下上来。
此时无论是否在壓力模式下,只要TCP连接所用缓存未超过tcp_rmem[0]或者tcp_wmem[0]那么都一定都能成功分配新内存。否则基本上就会面临分配失败的状况。(注意:还有一些例外场景允许分配内存成功由于对于我们理解这几个配置项意义不大,故略过)
3、当TCP内存在tcp_mem[1]与tcp_mem[2]之间时,系统一定处于系统压力模式丅其他行为与上同。
4、当TCP内存在tcp_mem[2]之上时毫无疑问,系统一定在压力模式下而且此时所有的新TCP缓存分配都会失败。
下图为需要新缓存時内核的简化逻辑:
当系统在非压力模式下上面我所说的每个连接的读写缓存上限,才有可能增加当然最大也不会超过tcp_rmem[2]或者tcp_wmem[2]。相反茬压力模式下,读写缓存上限则有可能减少虽然上限可能会小于tcp_rmem[0]或者tcp_wmem[0]。
所以粗略的总结下,对这3个数组可以这么看:
1、只要系统TCP的总體内存超了 tcp_mem[2]新内存分配都会失败。
2、tcp_rmem[0]或者tcp_wmem[0]优先级也很高只要条件1不超限,那么只要连接内存小于这两个值就保证新内存分配一定成功。
3、只要总体内存不超过tcp_mem[0]那么新内存在不超过连接缓存的上限时也能保证分配成功。
4、tcp_mem[1]与tcp_mem[0]构成了开启、关闭内存压力模式的开关在壓力模式下,连接缓存上限可能会减少在非压力模式下,连接缓存上限可能会增加最多增加到tcp_rmem[2]或者tcp_wmem[2]。