2014年09月12日

网络学习点滴(一)


最近


最近在工作之余,主要做了三件事:

最核心的是学习网络编程,一开始就以muduo作为切入点,结合陈硕大神的muduo源码和《Muduo网络编程手册》,一直陆陆续续的学,随后遇到公司办的中间件性能挑战赛,恰好用上了从muduo中学来的网络知识。在大神遍地的公司里,最终还有幸从两百多支队伍中跑入了前十,和晖哥的合作真是无比愉快,学到颇多。最后一个微博数据抓取的事,源于头脑里突然闪现的想法,就学习动手实现了简单的爬虫以及页面解析,其间感受到作为个码农,拥有台主机是多么必要的事, 比赛得到的阿里云代金券可以派上用场了。

把学习到的这些知识整理出来也是一件愉快的事,若在文字上的水平能靠近池建强大神,那则是大幸之事。喜欢强哥的文字风格,强烈推荐。

Markdown


先来个小插曲,关于Markdown语法的。自从将博客搬到github上之后,写博客都是使用Markdown语法完成的,其中的感受是,Markdown显著的提升了写技术类博客的效率和快感,排版问题如此便捷。Markdown完全是程序员用自己的技术带着情怀改善自己的工作环境。

John Gruber创建Markdown至今很多年了,由于推崇开放原则,衍生了很多版本,不同版本之间的语法规范也各异,可以从下面的两篇博客中了解到差异的存在以及带来的思考。

差异的存在,就导致了对标准化强烈的需求,因此存在另一拨大神努力的推进Markdown标准化的发展,就在9月6号,John MacFarlane和Jeff Atwood大神努力了近两年,推出了Standard Markdown,顾名思义,这是个标准版本,至少希望是,还推出了域名www.standardmarkdown.com。然而,戏剧化的事来了,John Gruber大神邮件过来了,说不能这么搞,结果就是Standard Markdown变成了CommonMark,同时standardmarkdown的域名也关闭了。似乎,标准不是这么容易。

知乎上有简单的帖子在讨论Markdown标准的阻碍在哪 。

之所以Standard Markdown在推出后又易名的原因,可以看Jeff Atwood贴出的博客,详细解释了。

对于个普通程序猿如我来说,Markdown目前只用在了Github写博客, StackOverflow提问作答上。尽管使用的场景不多,用到的语法规范少,但是还是希望Markdown能够存在核心的规范, 减少大家适应不同平台的成本。Github自家的规范可以参考下面的文档。

Muduo


这一节,主要总结学习muduo的心得,收获的知识点等零碎的东西。还是按照自己的风格,总结前,把相关的连接贴出来,方便看到来源,以便理解。

好,总结开始。

协议层面

1.连接建立与断开

学习muduo,当然离不开网络的基础知识TCP协议。众所周知,TCP是面向连接的,可靠的传输层协议。连接与可靠就导致TCP相比于UDP复杂不少。最基本的就是TCP建立连接时的三次握手和TCP断开连接时的四次握手。

上面的博客给出了使用TCP协议的通信两端在协议层面的状态转换图。通过三次握手之后,通信两端建立连接,那么此时两端在协议层面可以是平等的了(参考:《Muduo网络编程手册》P14 三个半事件之一)。在muduo的实现中,TcpClient建立对TcpServer的连接之后,Client和Server的关系就是对等的了,二者都使用同样的TcpConnection类。双方均可选择关闭连接,发送接收数据。

在关闭连接过程中,经历了四次挥手,我们需要知道TCP协议是全双工的,即通信两端均可读写连接。因此关闭连接,需要两端都参与关闭,才能关闭完成,因此在关闭连接的时候,通信两端将处于多个状态。谁先关闭连接,谁会处于TIME_WAIT的状态。TIME_WAIT状态的存在是用来保证TCP连接的可靠性的。因为网络是不可靠的,最后主动关闭端发送的ACK可能丢失,导致被关闭端会再次发送一个FIN包,如果没有TIME_WAIT状态,那么socket立马关闭,可能又被新连接利用起来,结果新连接收到一个FIN包,则状态混乱了。主动关闭端经历一个TIME_WAIT的状态,该过程是2MSL(Max Segment lifetime)的时间,这样可以保证重复发送的FIN包在网络上消失。

因此我们可以解释两个常见的问题:

另一个问题,我们的系统大多模块都是只在内网通信的,这个环境,就说明了网络的通信质量会较好。换句话说,对于内部网络通信,包的重传率是很低的,那么TIME_WAIT阶段考虑的ACK包丢失或延迟,FIN重发的问题就没那么必要,所以,对于内网的TCP连接,可以放心的减少TIME_WAIT需要等待的2MSL时间。

2.TCP_NODELAY

在参与公司中间件性能挑战赛的过程,遇到一个和TCP_NODELAY相关的问题。具体的场景如下:

client与server之间的通信过程是:client发送一个请求,server就应答一行数据,如此往复直到server端无数据则结束。client发送的数据是一个字节,即意味着client需要发送大量的小包过去。在初期,忘记设置client 端 socket的TCP_NODELAY flag了,然后再补充上,结果测试发现性能并没有因此改善,直觉是设置TCP_NODELAY 没有加速小包的发送。

这个问题是由于没有对Nagle算法了解过而只存在印象中的以为,以为Nagle算法会将小包直接存下来,等待发送的数据量到达MSS(Max Segment Size)之后或者达到了200ms的时间,才发送数据包。其实,仔细想想,应该不是这样的,在我们实现的程序里,client需要发送将近上千万个小包,如果每个小包都要等待200ms,那么完成此所有数据传输的时间将大的吓人,而实际上却还是很快的。

google之后,明白了其中的原因。参考下面的资料。

看完上面的两篇文章之后,估计就明白了为什么在我们的程序里,设置了TCP_NODELAY也没改善性能。因为我们是基于write+read+write+read这样的模式操作的,第一次write之后,因为no unconfirmed data in pipe, 所以包直接发送出去了,随后server返回response,client读取之后第二次write,同样,包正常出去。因此,设置TCP_NODELAY和没设置,在我们的模式里是没有任何影响的。

在google nagle算法时,发现两端在交互时除了nagle,还有所谓的Delayed ACK一起在发挥作用。首先推荐一篇清晰,很重要的paper,源自Apple的工程师。

这篇文章发表在2005年,源自于作者在Mac OS X上做测试时发现了一个网络性能的问题。

测试程序通过TCP协议重复的发送100,000bytes的数据,每发送100,000bytes的数据则等待另一端程序在应用层的ACK。作者发现windows系统可以达到3.5Mb/s的速度,而Mac OS X只能有2.7Mb/s的速度,不是因为windows 就强于 Mac OS X的缘故,随后作者发现当一次发送数据减少到99,912bytes时,Mac OS X的测试结果达到了5.2Mb/s,而改成99,913bytes的时候, 又变成了2.7Mb/s的速度。

从文章中贴出的图可以清晰的看到当一次发送100,000bytes时,会出现明显的200ms的空闲期,这个时期没有数据传送。这一问题都源于Delayed ACK和Nagle算法。

Delayed ACK意味着接收端不会立刻对每个接收的包发送ack。Delayed ACK算法是:当接收当一个tcp segment时,会按照下面的逻辑来返回ACK(为了方便理解,统一将接收端称为server,发送端称为client):

  1. server端会对第二个packet,立马返回ACK。(补充:第二个packet,指连续收到两个数据包中的第二个)
  2. server端接收到packet之后,如果应用程序会产生response data,就直接将ack和这个response data整合成一个包发送出去。
  3. 如果应用程序不产生response data,Delayed ACK算法就会延迟ACK发送,延迟时间默认是200ms。

对于第一点,文中原话是TCP will ACK every second packet immediately. 所以可以明白StackOverflow上的问题Winsock 200ms delay issue 中的第一个回答的讨论。偶数包的话可以迅速ACK,奇数会block 200ms。

继续回到刚才的文章中,作者分析了一次发送数据大小的特征:

上面的计算中,强调了一个packet的大小是1448,因为在不同的平台,这个packet的大小还有差异,在windows上一个tcp segment是1460,而Mac OS X和其他的系统给TCP包添加了一个TCP time stamp 选项,所以TCP segment的大小要少12bytes,是1448bytes。

那么以每次99,900bytes的数据量发送,那么可以直接发送68个packets。

上面的过程意味在每次发送99,900数据时,一切都很顺很快很喜人。同上的分析过程,当改为100,000bytes之后,就变为如下了:

显然,上面的过程就陷入短暂的死锁了,直到华丽丽的200ms时间流逝。如果真的就这样消逝200ms而什么也没做,那么性能会大打折扣。原本100,000bytes的数据在local gigabit Ethernet link上发送只需1ms,现在由于nagle和Delayed ACK,而变为了201ms。

作者最后补充到,在Mac OS X v10.5和iOS上,实现的是改良版的Nagle算法。

对于Nagle算法带来的问题,可以简单的在socket上设置TCP_NODELAY,所有因此带来的问题都没了。作者还提到了一个解决方法,就是Double Buffer,基于Double Buffer保证在等待一个response时,总有两个request outstanding。具体的思路请参考原文,

对于Nagle算法的理解,可以引用作者的一句话来概括:

Nagle' s algorithm says that if you have any unacknowledged data outstanding, then you can't send a runt packet.

总结

Muduo网络库中有太多的东西可以学习了,不仅是网络层面的知识,还有语言,工程,系统等等方面的,仔细的一层层看下去,收获会让你惊喜。

后面还会有一系列的收获总结。

前一篇: Python C++绑定 后一篇: Openstack Heat Resource 调度创建