为什么你得学些 TCP 的知识?

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

这不是指要明白 TCP 的所有东西,也不是说要通读 《TCP/IP 详解》。不过懂一点 TCP 知识是很有必要的。理由如下:

当我还在 Recurse Center 的时候,我用 Python 写过 TCP 协议栈(还写过一篇文章:如果你用 Python 写 TCP 协议栈会遇到什么?)。这是一次有趣的学习经历,但是也仅此而已。

一年以后,工作中有人在 Slack 上提到:“嘿,我在向 NSQ 发布消息时,每次要耗费 40 毫秒”。我已经断断续续思考了一个星期,但是没有任何结果。

一点背景知识:NSQ 是一个消息队列,你通过本地的一个 HTTP 请求向其发布消息。发送本地的一个 HTTP 请求确实不应该花费 40 毫秒,有时候会更差。NSQ 守护进程的负载不高,也没有使用过多的内存,也看不到 GC 停顿。这究竟是为什么呢?神呐,救救我吧!

突然我记起我一周以前看过的一篇叫做“性能研究(In search of performance)”的文章——我们如何为每个 POST 请求节省 200ms。在这篇文章中,他们说到为什么每个 POST 请求会花费额外的 200 毫秒。就是这个原因。这是该文章中的关键段落:

延迟确认(ACK) 与 TCP_NODELAY

Ruby 的 Net::HTTP 会将 POST 请求切分为两个 TCP 包,一个消息头,一个消息体。相反,curl 会将这两者合并为一个包。更糟糕的是,Net::HTTP 在打开 TCP 套接字时不会设置 TCP_NODELAY,这将导致第二个包需要等到第一个包的接收确认通知之后才能发送。这是 Nagle 算法导致的。

 

转换到连接的另一端,HAProxy 需要决定如何确认这两个包。在 1.4.18 版本中(我们正在用的版本),它是通过 TCP 延迟确认通知来实现的。延迟确认对 Nagle 算法有非常糟糕的影响,会导致请求暂停直到服务器延迟确认超时。

现在我们解释这个段落说的内容。

  • TCP 是一个通过数据包传输数据的算法

  • 他们的 HTTP 库将 POST 请求分割成两个小的数据包发送

接下来,TCP 采用类似如下的步骤进行交互:

application:Hi!这里有一个数据包。
HAProxy:(沉默),等待第二个包发送
HAProxy:对了,我需要返回一个确认,不过没关系,等会吧
application: (沉默)
application:好吧,我正在等待确认,可能现在网络延迟比较大
HAProxy:好吧,太烦人了,这是一个确认。
application:好极了,这是第二个数据包!!!
HAProxy:亲,我们已经搞定了。

这个过程是不是应用程序和 HAProxy 都在消极等待另一方发送信息?这就是那额外的 200ms。应用程序这么做的是因为 Nagle 算法,而 HAProxy 消息等待的原因是延迟确认。

据我所知,延迟确认是所有 Linux 系统的默认行为。所以这不是一个偶然或者异常情况,如果发送 TCP 数据包多一个 1 个,你就会遇到这种情况。

现在,我们成为专家了

读过这篇文章之后我很快就忘了。不过当我被额外的 40 毫秒难住的时候,我又记起来了。

所以我认为——这不可能是我的问题,可能吗?可能吗??然后我发了一封邮件给我团队说:“我想我快要疯了,但是这可能是 TCP 的问题”。

所以我提交了一次修订,将我的应该调整为 TCP_NODELAY,然后问题就“嘣”的一声解决了。

40 毫秒的延迟立马就消失了。所有的事情都解决了,我就是个天才。

我们是否应该完全停止使用延迟确认?

我刚好在 Hacker News 看到 John Nagle (Nagle 算法的创始人)对 @alicemazzy 提到这个问题的评论。

本质问题是延迟确认。200 毫秒的“延迟确认”是一个非常不好的主意,1985 年中,在伯利克(Berkeley)研究 BSD 的人实际上没有真正明白这个问题。延迟确认是应用层对 200 毫秒内是否响应的一场赌博,但是即便每次它都赌输了,TCP 仍在使用延迟确认。

他继续说到,确认本身是很小并且消耗很低的,延迟确认引起的问题可能比它解决的问题还要多。

不懂得 TCP 你就无法解决 TCP 问题

我曾经也认为,TCP 是一个相当底层的问题,我不需要明白。大多数时候你的确不需要明白。但是有的时候,当你在实践中遇到由于 TCP 算法引起的 bug 时,懂点 TCP 知识就变得非常重要了。(正如我们经常在博客中讨论的,许多事情都是这样,比如系统调用和操作系统:) )

延迟确认及 TCP_NODELAY 的交互非常不好——这对任何语言实现的 HTTP 请求都有影响。你不需要很深入的去了解,成为系统程序专家。但是了解一点 TCP 是如何运作的,对我的工作的确大有裨益。通过对 TCP 的学习,我才意识到这篇博客所描述的问题也许正好是我所熟悉的领域。我也一直在使用 strace,并且会一直使用下去。

 

 

分享案例之一:TCP

斜体部分位读者的提问

你好,请教你一个问题,在用tcp通讯时,应用层每次发送包的大小是尽量一个mtu以内呢还是无所谓?在移动网络下跟pc下分别如何?我的理解是tcp既然是流式的,并且自己会切包,超时重传也是基于切要后的包(不像udp按照切完前),所以无所谓,不知道这样理解是不是正确的?

 

TCP/IP协议模块,在应用层看来,就是自己的本地代理,代理自己与别的计算机程序通信。

代理算好听的,说难听一点就是一群打杂的。TCP/IP将底层通信有关的所有细节都大包独揽了,留下有限几个接口函数,让应用层给自己发号施令(^_^)。

接口函数,经常被写作API,API是Application Programming Interface的缩写,如果没有这些API接口,应用层如何对那些底层干活的发号施令?

先假设应用层需要传输一个10M的文件,应用层通过send()接口函数发送数据,问题来了,send()最大可以接受多少字节的数据?

这个是send()函数的实现限制,太大了超过TCP的缓冲区肯定不行,最好每次发送的数据不超过TCP缓冲区的一个单元的大小。

所以应用层会遵照send()的大小限制,每次发送的数据都在最大限度以内,这个算分片吗?

算第一次分片,但是这个是无法避免的!但是这个过程耗费的资源比较少,只是顺序读取10M字节过程,每次读取的字节长度有限制。

 

TCP接到send()发送的字节,会立马保持原封不动发送吗?

不确定!

TCP会先将这些数据放在自己的仓库(发送缓冲区),至于什么时候发,每次发多少,已经不是应用层所能左右的了。

TCP是基于字节流发送,可能将用户的一次数据发送砍成多个segment发送,也可能将多次应用层的发送合并在一个segment发送。而决定TCP一次最大能发segment的大小则和MSS有关,而MSS最终和MTU有关。

终于说到了MTU,大家可能知道MTU是数据链路层对网络层的限制,以最常见的Ethernet链路为例,Ethernet MTU = 1500,这个意思是Ethernet Payload最大的尺寸不得超过1500字节。这1500字节包含Ethernet头部、尾部吗?

当然不包含!

那问题又来了,Ethernet帧如果加上4个字节的802.1Q的头部,MTU =
1500 里包含4字节的802.1Q吗?

包含!

这样操作的话,意味着用户的IP报文如果是1500字节,添加一个802.1Q会变成1504,意味着需要分片处理!

分片是非常不利的选择,当前网络所做的很多努力都是极力避免分片!

如何避免分片呢?

其实很简单,凡是可能添加802.1Q接口将MTU = 1504,甚至1508,这样即使添加1个、2个802.1Q头,用户1500字节的IP报文也不会被分片。

Okay,再回到正题,大家有没有思考过为何Ethernet要有MTU的限制?

Ethernet最初对IP报文也没有什么限制,IP报文最大可以有65535字节长,但是发现Ethernet对于长报文无法可靠地传输,而将报文限制在一定的尺寸,Ethernet可以将报文大概率传输到目的地,于是就有了MTU这一限制措施,MTU是为了更可靠地传输数据。

既然物理硬件有了MTU的限制,那么希望TCP/IP将这种硬性的限制措施,层层向上传达,于是TCP选择了MSS来限制发送segment的大小。

MSS = MTU – IP Header – TCP Header = 1500 -20
-20 = 1460

TCP MSS真的可以避免分片吗?

TCP连接的双方依据本地物理链路的MTU,按照以上的公式计算出本地的MSS,然后双方交换各自的MSS,双方会选择两者中小的MSS来继续通信。但有没有想过,如果路径中的MTU比连接双方的MTU都小,是不是分片就无法避免了?答案是肯定的!

这个时候必须分片,不分片就会丢,这是DF = 1的状况,需要给源主机发送ICMP消息,问题是ICMP消息能到达源主机吗?如果不能到达,通信就会断,即使TCP有重传机制。

能到达源主机,源的TCP意识到这一点,会将重传的报文重新切片,重新发送,这没有什么问题,只是耽误一点时间而已。

DF = 0时,可以直接分片,尽管耗费很多分片的资源,到达目的地再重组,也要耗费一点资源。

问题是,没有端口号的分片,经过安全设备时,可能会遇到障碍,这同样会造成通信的障碍。

看,一旦分片造成多大的麻烦,麻烦意味着CPU资源的耗费,为了避免这些不必要的动作,只要不分片,一切都会变的简单。

UDP

UDP是块式消息,UDP本身没有任何分片的能力,也没有任何重传的能力,这些能力需要依赖应用层、IP层。

计算机网络发展到今天,大家已经形成了一个共识,如果用户的数据确实需要分片传输,务必保证分片的动作由应用层来完成,到达目的地由应用层将字节流,再整理成有意义的消息块。

转载于:https://my.oschina.net/mickelfeng/blog/1931515

你可能感兴趣的:(python,操作系统,网络)