Posts Tagged ‘TCPIP’

17
Feb

Win2003Server的SYN Attack Protect陷阱

by huubby in Network, Windows

发现这个陷阱源于一个自己写的HTTP服务端。因为项目里要用到支持GET请求就能满足需要的HTTP服务端,图方便就用内部封的异步Socket类实现了这么个简单的服务端。没想到,就是这个简单的HTTP服务,还是出现了几次莫名其妙的问题。第一次出现在刚完成代码功能测试时,对HTTP的一知半解导致查了几天之后才发现原来是没处理HTTP头里的Keep-Alive。

最近又碰见另一个百思不得其解铃还需系铃人人得而诛之的问题,测试服务端性能时,用Apache的AB压服务器,设置的并发数稍大就只能完成一部分请求,AB的提示是“远程主机主动关闭连接”。具体来说就是,当AB参数为请求数1000,并发数200的时候,服务端能够正常完成测试;增加并发数到300,则服务端只能完成1000个请求中的300多个,客户端抓包观察到大量请求在TCP三次握手完成后立刻被RST。

用自己封装的异步Socket类写个测试客户端,循环连接服务端,产生大量连接,同样连接数也只能到300多个,其余的大量连接得到类型为WSAENETRESET的socket错误。

用来实现HTTP服务端的异步Socket类没有什么特别操作。逻辑是这样的:

  • 服务端首先产生一个Socket句柄(废话…),转为异步模式后监听端口,等待客户端连接。
  • 一旦有客户端连接,服务端调用accept得到新的Socket,加入到一个Socket集合进行统一处理。
  • 每次统一处理时,查询所有Socket的事件,对不同的事件进行不同的处理。
  • 服务端对所有的Socket句柄不主动执行关闭操作,除非调用recv时得到的长度为0,即认为是客户端主动关闭,随后服务端进入被动关闭。

应用开发出现问题的第一反应是检查自己的程序。首先怀疑是逻辑不严密,在某特殊情况下Reset客户端连接。在服务端每个可能关闭socket的地方增加日志,再用AB压一次观察输出。但服务端日志没有记录到任何异常的关闭操作,问题似乎不是服务端关闭连接导致的。

既然没有异常关闭socket的情况,那应该是其他地方出问题。考虑listen函数的backlog参数,会不会是这里的原因呢?

listen函数原型:

int listen( __in  SOCKET s,  __in  int backlog );

来看backlog参数在MSDN的说明

The maximum length of the queue of pending connections

再往下看,MSDN中对listen函数有一段这样的描述

If a connection request arrives and the queue is full, the client will receive an error with an indication of WSAECONNREFUSED.

就是说,如果客户端连接数过大,超过调用listen函数设置的backlog队列大小,新的连接会收到WSAECONNREFUSED的socket错误。但观察到的现象是客户端socket错误为WSAENETRESET,不符合文档的描述。

过了listen函数再往下看,代码进入日志能够记录的范围。上面说过,服务端没有异常关闭的操作。到这里仍然没找到原因。目前为止,最有可能的情况就是瞬间的连接数过多导致backlog满。就算是这个原因,客户端收到的也应该是WSAECONNREFUSED错误而不是观察到的WSAENETRESET错误,绕进死胡同了。就这个问题在stackoverflow发的询问帖,回复里也有人说可能是backlog满导致客户端无法连接,但却无法解释为什么收到的socket错误不是MSDN描述的WSAECONNREFUSED。

问题就这样被搁置下来,直到三天后我忙完手头的事情,用部分关键词google,得到了某个错误现象匹配度很高的搜索结果。这里是原帖,截取一段简单描述

We started getting problems when customers implemented SP1 on their Windows Server 2003
boxes. We’d see clients who thought they had successfully connected, but who would get a
RESET back from the server immediately after the 3-way handshake (SYN, SYN-ACK, ACK). This
seems *completely broken* to me.

啊哈,跟我们的问题看起来一样!他认为是Windows 2003 Server默认开启的SYN攻击保护开关在作怪,猜想可能是SYN攻击保护机制的实现影响到Winsock的accept函数行为。正常的行为是,当backlog队列满时,三次握手中第一个SYN包就会导致服务端响应RST+ACK拒绝客户端的连接。但SYN保护机制起作用后,这个行为变成直接接受新的连接再去检查backlog队列,当发现backlog队列满时丢弃此连接。服务端此时要干掉连接就只能在已完成三次握手的连接上发送一个RST,于是客户端出现莫名其妙的WSAENETRESET错误。

这是他的问题以及猜测,能不能解决我们的问题,还要实验一下。打开注册表才发现,我们的服务器上没有他描述的HKLM\System\CurrentControlSet\Services\Tcpip\Parameters\SynAttackProtect这一项,没关系,没有我们可以创造一个,设置键值为0,重启机器(万恶的重启!)。此时再用AB去压服务端,在客户端果然抓到的RST+ACK包。用自己写的客户端测试,socket错误也由之前百思不得其解的WSAENETRESET变成WSAECONNREFUSED。

问题原因到这里基本确认,由于我们的服务端实现在接受客户端连接时速度不够快,用AB压服务端时的大量连接填满backlog队列,这些连接同时也触发服务器默认开启的SYN攻击保护机制,操作系统认为受到SYN攻击,后续的其他连接于是被重置。关于这个问题,这里也有提及。这种跟文档描述不一致的行为害苦了广大程序员,在别人2005年就受过苦之后,2010年我这个倒霉蛋又再次遇上。不过奇怪的是,网上搜索不到多少关于这个问题的中文资料,英文倒是又不少人提过。

之前在跟项目组同事的讨论原因时,也考虑过系统或者路由在应用程序之下断开连接的可能性,但是由于对操作系统环境不熟悉,不知道存在这么一个SynAttackProtect机制,影响到问题原因的确认,算是一个教训吧。虽然存在文档描述与实际行为不一致的原因,但对程序运行平台的熟悉程度还是直接影响了定位问题的速度。

顺便说一句,测试中发现,Windows 2003 Server SP2里的backlog最大值似乎是200,不过没有查过官方文档确认,纯属推测。

PS:就这个问题跟带我出道的师傅讨论,又有另一个疑问。当SYN保护机制生效时,backlog队列满的情况下,新的连接应该不会再被加入到队列中,为什么服务端调用accept仍然得到了这个连接?有人了解SynAttackProtect机制实现的话,请不吝赐教!

17
Oct

Nagle算法和魔兽世界的延迟

by huubby in Network

暴雪大神的《魔兽世界》从2005年4月26号公测到现在,风靡全球已有好几个春秋。还记得公测时小白一个的我打哀嚎被副本里的一个要一点点技术跳过去的平台折腾疯,无数次的跳到空中,眼看着那个台子在眼前越升越高,然后环顾四周,全是怪,挂,跑尸,复活,找路,再跳,挂…以至于我满级之后都不敢去哀嚎 : (

当然,玩了几年的wow,要说印象最深刻的嘛,当然要数游戏代理公司的小霸王服务器,以及小霸王那无敌的延迟(你肯定也这么想,哈)。曾经有一次,参加工会的BWL开荒,2号小红龙,只要开荒过这个怪的玩家,我相信你绝对不会忘记它的变态。那次开荒,变态的BOSS配合几百的延迟让全团一次次的扑地,直到身上的装备跟延迟变成一个颜色,还是没过,丧气解散,杯具!

不论是以前的九城时代,还是现在的网易时代,lag一直都是wow最大的问题,当然,大家普遍认为主要的原因在于服务器质量不靠谱,故有“小霸王”之名。但同时也要了解,暴雪一直在调整程序,以期粉丝们有更好的游戏体验。在TBC 2.3.2版本的patch说明中,就有如下与游戏体验相关的内容:

综合
* /timetest命令可显示游戏性能信息。输入/timetest 0可关闭该命令。开启此命令后,系统将在玩家下一次在飞行管理员处搭乘飞行坐骑时进行性能测试,并在着陆后显示结果数据。
* 微缩地图将不再显示带蓝色问号的任务NPC位置。
* 受到爆击后触发的效果:许多技能和天赋在2.3.0中作了改变,可以在玩家处于坐下状态时正常发挥因受到爆击而触发的效果。
* 当玩家完成了NPC给予的任务后,鼠标指向该NPC时将显示为问号,而不再是感叹号。
* 取消了公会银行的管理权限对公会会长的等级要求。公会会长将始终拥有对公会银行的完整管理权限,且此完整权限不可改变。
* 在公会银行的管理中新增了“提款:仅限于维修”功能。当公会将某层级的公会成员权限设定为此时,此阶层的成员将不能从公会银行直接提款,而只能用每天的提款额度进行直接修理。
* 玩家在受到攻击时将自动站起,即使该次攻击未能命中。
* 船只和飞艇上的乘务NPC又可以正常工作了。
* 通过禁用Nagle算法降低了网络延迟。

首先赞一下暴雪的产品态度,如果我们公司也一直致力于提高客户的使用体验,就算不能到暴雪这种程度,最起码也应该会有很好的口碑和用户忠诚度。好了,马屁拍完来研究一下这句“通过禁用Nagle算法降低了网络延迟 ”。
开始blabla之前,我先说说网络延迟的概念。不知道在其他领域是是不是有别的含义,但在我们游戏界(什么时候我算游戏界的了?-_-|||),特别是在魔兽世界,延迟指的就是痛苦,就是被别人打而无还手之力,就是剩下一滴血的时候用不了血瓶,用不了消失,用不了冰箱,It means hell!对不起,我又变态,哦,不,失态了。
正经的说,延迟在游戏中最典型最恶心的表现是,当你打算使用某个技能,游戏角色要在你按了键盘或点了鼠标后的一段时间内才能发出技能。如果你按了一系列技能的快捷键后游戏人物一点反应没有,而是要等到你倒了杯水回来的时候才突然像快进一样做出N多动作,那么恭喜你,网络延迟非常高,你的游戏人物应该大多数情况下会躺在地板上数星星。

那么这个Nagle算法是什么万恶的东东,会增加网络延迟?而且暴雪大神竟然一直到TBC才想起来关掉这玩意儿,不给力阿!话说,在很久很久以前…好吧好吧,别嘘。所谓Nagle算法,它是个根据一定的规则减少网络发送包数量的方法,进而改善了TCP/IP网络的效率。关于这个算法,《TCP/IP详解》里是这样说的

该算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他的小分组。相反,TCP收集这些少量的分组,并在确认到来时以一个分组的方式发出去。

在魔兽世界游戏里,我们的操作中有相当一部分的数据量是极小的,若没有Nagle算法,1个字节的数据可能也会被作为一个分组发送出去,考虑到TCP头有20字节,IP头也有20个字节,为了发送1个字节的数据产生的分组就达到41字节。这是一种极大的浪费,更为重要的是,这同时也增加了分组的数量,给客户端网络造成压力,我想这应该正是暴雪当初没关闭Nagle算法的原因。

2.3.2里的这条patch有什么神奇的作用到这已经水落石出,如果没有关闭Nagle算法,客户端的数据包可能由于数据量太小而被TCP层缓存下来,等到上一个分组的确认收到后才把所缓存的所有数据发送到服务端,从而导致客户端感觉延迟有所增加。关闭Nagle算法后,每个数据包在TCP层便不会被缓存,直接发送到服务端,改善客户端游戏体验的目的也就达到了。

其实在其他领域对Nagle算法“意见”也是很大的,比如实时性要求很高的应用,远程桌面之类的,像鼠标移动之类的小消息必须无延迟发送出去,否则客户用起来会抓狂,后果很严重!Host Requirements RFC针对TCP的Nagle算法实现这样说的

A TCP SHOULD implement the Nagle Algorithm [TCP:9] to coalesce short segments. However, there MUST be a way for
an application to disable the Nagle algorithm on an individual connection.

简言之,TCP必须实现Nagle算法,但同时也要提供方法能够在某个连接上关闭此算法。

Socket用户可以通过TCP_NODELAY选项来关闭Nagle算法,这个选项在netinet/tcp.h文件中定义。

over~