乐趣区

关于网络编程:不为人知的网络编程十一从底层入手深度分析TCP连接耗时的秘密

本文作者张彦飞,原题“聊聊 TCP 连贯耗时的那些事儿”,有少许改变。

1、引言

对于基于互联网的通信利用(比方 IM 聊天、推送零碎),数据传递时应用 TCP 协定绝对较多。这是因为在 TCP/IP 协定簇的传输层协定中,TCP 协定具备牢靠的连贯、谬误重传、拥塞管制等长处,所以目前在利用场景上比 UDP 更宽泛一些。

置信你也肯定听闻过 TCP 也存在一些毛病,能常都是陈词滥调的开销要略大。然而各路技术博客里都在单单说开销大、或者开销小,而少见不给出具体的量化剖析。不客气的讲,相似阐述都是没什么养分的废话。

通过日常工作的思考之后,我更想弄明确的是,TCP 的开销到底有多大,是否进行量化。一条 TCP 连贯的建设须要耗时提早多少,是多少毫秒,还是多少微秒?能不能有一个哪怕是粗略的量化预计?当然影响 TCP 耗时的因素有很多,比方网络丢包等等。我明天只分享我在工作实际中遇到的比拟高发的各种状况。

写在后面:得益于 Linux 内核的开源,本文中所提及的底层以及具体的内核级代码例子,都是以 Linux 零碎为例。

学习交换:

  • 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》
  • 开源 IM 框架源码:https://github.com/JackJiang2…(备用地址点此)

(本文已同步公布于:http://www.52im.net/thread-32…)

2、系列文章

本文是系列文章中的第 11 篇,本系列文章的纲要如下:

《鲜为人知的网络编程(一):浅析 TCP 协定中的疑难杂症(上篇)》
《鲜为人知的网络编程(二):浅析 TCP 协定中的疑难杂症(下篇)》
《鲜为人知的网络编程(三):敞开 TCP 连贯时为什么会 TIME_WAIT、CLOSE_WAIT》
《鲜为人知的网络编程(四):深入研究剖析 TCP 的异样敞开》
《鲜为人知的网络编程(五):UDP 的连接性和负载平衡》
《鲜为人知的网络编程(六):深刻地了解 UDP 协定并用好它》
《鲜为人知的网络编程(七):如何让不牢靠的 UDP 变的牢靠?》
《鲜为人知的网络编程(八):从数据传输层深度解密 HTTP》
《鲜为人知的网络编程(九):实践联系实际,全方位深刻了解 DNS》
《鲜为人知的网络编程(十):深刻操作系统,从内核了解网络包的接管过程(Linux 篇)》
《鲜为人知的网络编程(十一):从底层动手,深度剖析 TCP 连贯耗时的机密》(本文)
《鲜为人知的网络编程(十二):彻底搞懂 TCP 协定层的 KeepAlive 保活机制》
《鲜为人知的网络编程(十三):深刻操作系统,彻底搞懂 127.0.0.1 本机网络通信》
《鲜为人知的网络编程(十四):拔掉网线再插上,TCP 连贯还在吗?一文即懂!》

3、现实状况下的 TCP 连贯耗时剖析

要想搞清楚 TCP 连贯的耗时,咱们须要具体理解连贯的建设过程。

在前文《深刻操作系统,从内核了解网络包的接管过程(Linux 篇)》中咱们介绍了数据包在接收端是怎么被接管的:数据包从发送方进去,通过网络达到接管方的网卡;在接管方网卡将数据包 DMA 到 RingBuffer 后,内核通过硬中断、软中断等机制来解决(如果发送的是用户数据的话,最初会发送到 socket 的接管队列中,并唤醒用户过程)。

在软中断中,当一个包被内核从 RingBuffer 中摘下来的时候,在内核中是用 struct sk_buff 构造体来示意的(参见内核代码 include/linux/skbuff.h)。其中的 data 成员是接管到的数据,在协定栈逐层被解决的时候,通过批改指针指向 data 的不同地位,来找到每一层协定关怀的数据。

对于 TCP 协定包来说,它的 Header 中有一个重要的字段 -flags。

如下图:

通过设置不同的标记位,将 TCP 包分成 SYNC、FIN、ACK、RST 等类型:

1)客户端通过 connect 零碎调用命令内核收回 SYNC、ACK 等包来实现和服务器 TCP 连贯的建设;
2)在服务器端,可能会接管许许多多的连贯申请,内核还须要借助一些辅助数据结构 - 半连贯队列和全连贯队列。
咱们来看一下整个连贯过程:

在这个连贯过程中,咱们来简略剖析一下每一步的耗时:

1)客户端收回 SYNC 包:客户端个别是通过 connect 零碎调用来收回 SYN 的,这里牵涉到本机的零碎调用和软中断的 CPU 耗时开销;
2)SYN 传到服务器:SYN 从客户端网卡被收回,开始“跨过山和大海,也穿过三三两两 ……”,这是一次短途远距离的网络传输;
3)服务器解决 SYN 包:内核通过软中断来收包,而后放到半连贯队列中,而后再收回 SYN/ACK 响应。又是 CPU 耗时开销;
4)SYC/ACK 传到客户端:SYC/ACK 从服务器端被收回后,同样跨过很多山、可能很多大海来到客户端。又一次短途网络跋涉;
5)客户端解决 SYN/ACK:客户端内核收包并解决 SYN 后,通过几 us 的 CPU 解决,接着收回 ACK。同样是软中断解决开销;
6)ACK 传到服务器:和 SYN 包,一样,再通过简直同样远的路,传输一遍。又一次短途网络跋涉;
7)服务端收到 ACK:服务器端内核收到并解决 ACK,而后把对应的连贯从半连贯队列中取出来,而后放到全连贯队列中。一次软中断 CPU 开销;
8)服务器端用户过程唤醒:正在被 accpet 零碎调用阻塞的用户过程被唤醒,而后从全连贯队列中取出来曾经建设好的连贯。一次上下文切换的 CPU 开销。

以上几步操作,能够简略划分为两类:

第一类:是内核耗费 CPU 进行接管、发送或者是解决,包含零碎调用、软中断和上下文切换。它们的耗时根本都是几个 us 左右;
第二类:是网络传输,当包被从一台机器上收回当前,两头要通过各式各样的网线、各种交换机路由器。所以网络传输的耗时相比本机的 CPU 解决,就要高的多了。依据网络远近个别在几 ms~ 到几百 ms 不等。

1ms 就等于 1000us,因而网络传输耗时比双端的 CPU 开销要高 1000 倍左右,甚至更高可能还到 100000 倍。

所以:在失常的 TCP 连贯的建设过程中,个别思考网络延时即可。

PS:一个 RTT 指的是包从一台服务器到另外一台服务器的一个来回的延迟时间。

所以从全局来看:TCP 连贯建设的网络耗时大概须要三次传输,再加上少许的单方 CPU 开销,总共大概比 1.5 倍 RTT 大一点点。

不过,从客户端视角来看:只有 ACK 包收回了,内核就认为连贯是建设胜利了。所以如果在客户端打点统计 TCP 连贯建设耗时的话,只须要两次传输耗时 - 既 1 个 RTT 多一点的工夫。(对于服务器端视角来看同理,从 SYN 包收到开始算,到收到 ACK,两头也是一次 RTT 耗时)。

4、极其状况下的 TCP 连贯耗时剖析

上一节能够看到:在客户端视角,失常状况下一次 TCP 连贯总的耗时也就就大概是一次网络 RTT 的耗时。如果所有的事件都这么简略,我想我的这次分享也就没有必要了。事件不肯定总是这么美妙,意外的产生在劫难逃。

在某些状况下,可能会导致 TCP 连贯时的网络传输耗时上涨、CPU 解决开销减少、甚至是连贯失败。本节将就我在线上遇到过的各种切身体会的沟沟坎坎,来剖析一下极其状况下的 TCP 连贯耗时状况。

4.1 客户端 connect 调用耗时失控案例
失常一个零碎调用的耗时也就是几个 us(微秒)左右。然而在我的《追踪将服务器 CPU 耗光的凶手!》一文中,笔者的一台服务器过后遇到一个情况:某次运维同学转达过去说该服务 CPU 不够用了,须要扩容。

过后的服务器监控如下图:

该服务之前始终每秒抗 2000 左右的 qps,CPU 的 idel 始终有 70%+,怎么忽然就 CPU 一下就不够用了呢。

而且更奇怪的是 CPU 被打到谷底的那一段时间,负载却并不高(服务器为 4 核机器,负载 3 - 4 是比拟失常的)。

起初通过排查当前发现当 TCP 客户端 TIME_WAIT 有 30000 左右,导致可用端口不是特地短缺的时候,connect 零碎调用的 CPU 开销间接上涨了 100 多倍,每次耗时达到了 2500us(微秒),达到了毫秒级别。

当遇到这种问题的时候,尽管 TCP 连贯建设耗时只减少了 2ms 左右,整体 TCP 连贯耗时看起来还可承受。但这里的问题在于这 2ms 多都是在耗费 CPU 的周期,所以问题不小。

解决起来也非常简单,方法很多:批改内核参数 net.ipv4.ip_local_port_range 多预留一些端口号、改用长连贯都能够。

4.2 TCP 半 / 全连贯队列满的案例
如果连贯建设的过程中,任意一个队列满了,那么客户端发送过去的 syn 或者 ack 就会被抛弃。客户端期待很长一段时间无果后,而后会收回 TCP Retransmission 重传。

拿半连贯队列举例:

要晓得的是下面 TCP 握手超时重传的工夫是秒级别的。也就是说一旦 server 端的连贯队列导致连贯建设不胜利,那么光建设连贯就至多须要秒级以上。而失常的在同机房的状况下只是不到 1 毫秒的事件,整整高了 1000 倍左右。

尤其是对于给用户提供实时服务的程序来说,用户体验将会受到较大影响。如果连重传也没有握手胜利的话,很可能等不及二次重试,这个用户拜访间接就超时了。

还有另外一个更坏的状况是:它还有可能会影响其它的用户。

如果你应用的是过程 / 线程池这种模型提供服务,比方:php-fpm。咱们晓得 fpm 过程是阻塞的,当它响应一个用户申请的时候,该过程是没有方法再响应其它申请的。如果你开了 100 个过程 / 线程,而某一段时间内有 50 个过程 / 线程卡在和 redis 或者 mysql 服务器的握手连贯上了(留神:这个时候你的服务器是 TCP 连贯的客户端一方)。这一段时间内相当于你能够用的失常工作的过程 / 线程只有 50 个了。而这个 50 个 worker 可能基本解决不过去,这时候你的服务可能就会产生拥挤。再继续略微工夫长一点的话,可能就产生雪崩了,整个服务都有可能会受影响。

既然结果有可能这么重大,那么咱们如何查看咱们手头的服务是否有因为半 / 全连贯队列满的状况产生呢?

在客户端:能够抓包查看是否有 SYN 的 TCP Retransmission。如果有偶发的 TCP Retransmission,那就阐明对应的服务端连贯队列可能有问题了。

在服务端的话:查看起来就更不便一些了。netstat -s 可查看到以后零碎半连贯队列满导致的丢包统计,但该数字记录的是总丢包数。你须要再借助 watch 命令动静监控。如果上面的数字在你监控的过程中变了,那阐明以后服务器有因为半连贯队列满而产生的丢包。你可能须要加大你的半连贯队列的长度了。

$ watch’netstat -s | grep LISTEN’

8 SYNs to LISTEN sockets ignored

对于全连贯队列来说呢,查看办法也相似:

$ watch’netstat -s | grep overflowed’

160 timesthe listen queue of a socket overflowed

如果你的服务因为队列满产生丢包,其中一个做法就是加大半 / 全连贯队列的长度。半连贯队列长度 Linux 内核中,次要受 tcp_max_syn_backlog 影响 加大它到一个适合的值就能够。

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

1024

echo “2048” > /proc/sys/net/ipv4/tcp_max_syn_backlog

全连贯队列长度是应用程序调用 listen 时传入的 backlog 以及内核参数 net.core.somaxconn 二者之中较小的那个。你可能须要同时调整你的应用程序和该内核参数。

cat /proc/sys/net/core/somaxconn

128

echo “256” > /proc/sys/net/core/somaxconn

改完之后咱们能够通过 ss 命令输入的 Send- Q 确认最终失效长度:

$ ss -nlt

Recv-Q Send-Q Local Address:Port Address:Port

0 128 :80 :*

Recv- Q 通知了咱们以后该过程的全连贯队列应用长度状况。如果 Recv- Q 曾经迫近了 Send-Q, 那么可能不须要等到丢包也应该筹备加大你的全连贯队列了。

如果加大队列后依然有十分偶发的队列溢出的话,咱们能够暂且容忍。

如果依然有较长时间解决不过去怎么办?

另外一个做法就是间接报错,不要让客户端超时期待。

例如将 Redis、Mysql 等后端接口的内核参数 tcp_abort_on_overflow 为 1。如果队列满了,间接发 reset 给 client。通知后端过程 / 线程不要薄情地傻等。这时候 client 会收到谬误“connection reset by peer”。就义一个用户的拜访申请,要比把整个站都搞崩了还是要强的。

5、TCP 连贯耗时实测剖析

5.1 测试前的筹备
我写了一段非常简单的代码,用来在客户端统计每创立一个 TCP 连贯须要耗费多长时间。

<?php

$ip= {服务器 ip};

$port= {服务器端口};

$count= 50000;

function buildConnect($ip,$port,$num){

for($i=0;$i<$num;$i++){$socket= socket_create(AF_INET,SOCK_STREAM,SOL_TCP);

    if($socket==false) {echo"$ip $port socket_create() 失败的起因是:".socket_strerror(socket_last_error($socket))."\n";

        sleep(5);

        continue;

    }



    if(false == socket_connect($socket, $ip, $port)){echo"$ip $port socket_connect() 失败的起因是:".socket_strerror(socket_last_error($socket))."\n";

        sleep(5);

        continue;

    }

    socket_close($socket);

}

}

$t1= microtime(true);

buildConnect($ip, $port, $count);

echo(($t2-$t1)*1000).’ms’;

在测试之前,咱们须要本机 linux 可用的端口数短缺,如果不够 50000 个,最好调整短缺。

echo “5000 65000” /proc/sys/net/ipv4/ip_local_port_range

5.2 失常状况下的测试
留神:无论是客户端还是服务器端都不要抉择有线上服务在跑的机器,否则你的测试可能会影响失常用户拜访

首先:我的客户端位于河北怀来的 IDC 机房内,服务器抉择的是公司广东机房的某台机器。执行 ping 命令失去的提早大概是 37ms,应用上述脚本建设 50000 次连贯后,失去的连贯均匀耗时也是 37ms。

这是因为后面咱们说过的,对于客户端来看,第三次的握手只有包发送进来,就认为是握手胜利了,所以只须要一次 RTT、两次传输耗时。尽管这两头还会有客户端和服务端的零碎调用开销、软中断开销,但因为它们的开销失常状况下只有几个 us(微秒),所以对总的连贯建设延时影响不大。

接下来:我换了一台指标服务器,该服务器所在机房位于北京。离怀来有一些间隔,然而和广东比起来可要近多了。这一次 ping 进去的 RTT 是 1.6~1.7ms 左右,在客户端统计建设 50000 次连贯后算出每条连贯耗时是 1.64ms。

再做一次试验:这次选中试验的服务器和客户端间接位于同一个机房内,ping 提早在 0.2ms~0.3ms 左右。跑了以上脚本当前,试验后果是 50000 TCP 连贯总共耗费了 11605ms,均匀每次须要 0.23ms。

线上架构提醒:这里看到同机房提早只有零点几 ms,然而跨个间隔不远的机房,光 TCP 握手耗时就涨了 4 倍。如果再要是跨地区到广东,那就是百倍的耗时差距了。线上部署时,现实的计划是将本人服务依赖的各种 mysql、redis 等服务和本人部署在同一个地区、同一个机房(再变态一点,甚至能够是甚至是同一个机架)。因为这样包含 TCP 链接建设啥的各种网络包传输都要快很多。要尽可能防止短途跨地区机房的调用状况呈现。

5.3 TCP 连贯队列溢出状况下的测试
测试完了跨地区、跨机房和跨机器。这次为了快,间接和本机建设连贯后果会咋样呢?

Ping 本机 ip 或 127.0.0.1 的提早大略是 0.02ms,本机 ip 比其它机器 RTT 必定要短。我感觉必定连贯会十分快,嗯试验一下。

间断建设 5W TCP 连贯:总工夫耗费 27154ms,均匀每次须要 0.54ms 左右。

嗯!?怎么比跨机器还长很多?

有了后面的实践根底,咱们应该想到了:因为本机 RTT 太短,所以霎时连贯建设申请量很大,就会导致全连贯队列或者半连贯队列被打满的状况。一旦产生队列满,过后撞上的那个连贯申请就得须要 3 秒 + 的连贯建设延时。所以下面的试验后果中,均匀耗时看起来比 RTT 高很多。

在试验的过程中,我应用 tcpdump 抓包看到了上面的一幕。原来有少部分握手耗时 3s+,起因是半连贯队列满了导致客户端期待超时后进行了 SYN 的重传。

咱们又从新改成每 500 个连贯,sleep 1 秒。嗯好,终于没有卡的了(或者也能够加大连贯队列长度)。

论断是:本机 50000 次 TCP 连贯在客户端统计总耗时 102399 ms,减去 sleep 的 100 秒后,均匀每个 TCP 连贯耗费 0.048ms。比 ping 提早略高一些。

这是因为当 RTT 变的足够小的时候,内核 CPU 耗时开销就会显现出来了,另外 TCP 连贯要比 ping 的 icmp 协定更简单一些,所以比 ping 提早略高 0.02ms 左右比拟失常。

6、本文小结

TCP 连贯在建设异样的状况下,可能须要好几秒,一个害处就是会影响用户体验,甚至导致以后用户拜访超时都有可能。另外一个害处是可能会诱发雪崩。

所以当你的服务器应用短连贯的形式拜访数据的时候:肯定要学会要监控你的服务器的连贯建设是否有异样状态产生。如果有,学会优化掉它。当然你也能够采纳本机内存缓存,或者应用连接池来放弃长连贯,通过这两种形式间接防止掉 TCP 握手挥手的各种开销也能够。

再说失常状况下:TCP 建设的延时大概就是两台机器之间的一个 RTT 耗时,这是防止不了的。然而你能够管制两台机器之间的物理间隔来升高这个 RTT,比方把你要拜访的 redis 尽可能地部署的离后端接口机器近一点,这样 RTT 也能从几十 ms 削减到最低可能零点几 ms。

最初咱们再思考一下:如果咱们把服务器部署在北京,给纽约的用户拜访可行吗?

后面的咱们同机房也好,跨机房也好,电信号传输的耗时根本能够疏忽(因为物理间隔很近),网络提早基本上是转发设施占用的耗时。然而如果是逾越了半个地球的话,电信号的传输耗时咱们可得算一算了。北京到纽约的球面间隔大略是 15000 公里,那么抛开设施转发提早,仅仅光速流传一个来回(RTT 是 Rround trip time,要跑两次),须要工夫 = 15,000,000 *2 / 光速 = 100ms。理论的提早可能比这个还要大一些,个别都得 200ms 以上。建设在这个提早上,要想提供用户能拜访的秒级服务就很艰难了。所以对于海内用户,最好都要在当地建机房或者购买海内的服务器。

附录:更多网络编程精髓材料

[1] 网络编程 (根底) 材料:

《TCP/IP 详解 – 第 17 章·TCP:传输控制协议》
《技术往事:扭转世界的 TCP/IP 协定(宝贵多图、手机慎点)》
《通俗易懂 - 深刻了解 TCP 协定(上):实践根底》
《实践经典:TCP 协定的 3 次握手与 4 次挥手过程详解》
《P2P 技术详解(一):NAT 详解——具体原理、P2P 简介》
《网络编程懒人入门(一):疾速了解网络通信协定(上篇)》
《网络编程懒人入门(二):疾速了解网络通信协定(下篇)》
《网络编程懒人入门(三):疾速了解 TCP 协定一篇就够》
《网络编程懒人入门(四):疾速了解 TCP 和 UDP 的差别》
《网络编程懒人入门(五):疾速了解为什么说 UDP 有时比 TCP 更有劣势》
《网络编程懒人入门(六):史上最艰深的集线器、交换机、路由器性能原理入门》
《网络编程懒人入门(七):深入浅出,全面了解 HTTP 协定》
《网络编程懒人入门(八):手把手教你写基于 TCP 的 Socket 长连贯》
《网络编程懒人入门(九):艰深解说,有了 IP 地址,为何还要用 MAC 地址?》
《网络编程懒人入门(十):一泡尿的工夫,疾速读懂 QUIC 协定》
《网络编程懒人入门(十一):一文读懂什么是 IPv6》
《网络编程懒人入门(十二):疾速读懂 Http/ 3 协定,一篇就够!》
《网络编程懒人入门(十三):一泡尿的工夫,疾速搞懂 TCP 和 UDP 的区别》
《网络编程懒人入门(十四):到底什么是 Socket?一文即懂!》
《技术扫盲:新一代基于 UDP 的低延时网络传输层协定——QUIC 详解》
《让互联网更快:新一代 QUIC 协定在腾讯的技术实际分享》
《聊聊 iOS 中网络编程长连贯的那些事》
《IPv6 技术详解:基本概念、利用现状、技术实际(上篇)》
《IPv6 技术详解:基本概念、利用现状、技术实际(下篇)》
《Java 对 IPv6 的反对详解:反对状况、相干 API、演示代码》
《从 HTTP/0.9 到 HTTP/2:一文读懂 HTTP 协定的历史演变和设计思路》
《脑残式网络编程入门(一):跟着动画来学 TCP 三次握手和四次挥手》
《脑残式网络编程入门(二):咱们在读写 Socket 时,到底在读写什么?》
《脑残式网络编程入门(三):HTTP 协定必知必会的一些常识》
《脑残式网络编程入门(四):疾速了解 HTTP/ 2 的服务器推送(Server Push)》
《脑残式网络编程入门(五):每天都在用的 Ping 命令,它到底是什么?》
《脑残式网络编程入门(六):什么是公网 IP 和内网 IP?NAT 转换又是什么鬼?》
《脑残式网络编程入门(七):面视必备,史上最艰深计算机网络分层详解》
《脑残式网络编程入门(八):你真的理解 127.0.0.1 和 0.0.0.0 的区别?》
《脑残式网络编程入门(九):面试必考,史上最艰深大小端字节序详解》
《迈向高阶:优良 Android 程序员必知必会的网络根底》
《Android 程序员必知必会的网络通信传输层协定——UDP 和 TCP》
《技术大牛陈硕的分享:由浅入深,网络编程学习教训干货总结》
《可能会搞砸你的面试:你晓得一个 TCP 连贯上能发动多少个 HTTP 申请吗?》
《5G 时代曾经到来,TCP/IP 老矣,尚能饭否?》

更多同类文章 ……
[2] 网络编程 (高阶) 材料:

《高性能网络编程(一):单台服务器并发 TCP 连接数到底能够有多少》
《高性能网络编程(二):上一个 10 年,驰名的 C10K 并发连贯问题》
《高性能网络编程(三):下一个 10 年,是时候思考 C10M 并发问题了》
《高性能网络编程(四):从 C10K 到 C10M 高性能网络应用的实践摸索》
《高性能网络编程(五):一文读懂高性能网络编程中的 I / O 模型》
《高性能网络编程(六):一文读懂高性能网络编程中的线程模型》
《高性能网络编程(七):到底什么是高并发?一文即懂!》
《IM 开发者的零根底通信技术入门(十):零根底,史上最强 5G 技术扫盲》
《IM 开发者的零根底通信技术入门(十一):为什么 WiFi 信号差?一文即懂!》
《IM 开发者的零根底通信技术入门(十二):上网卡顿?网络掉线?一文即懂!》
《IM 开发者的零根底通信技术入门(十三):为什么手机信号差?一文即懂!》
《IM 开发者的零根底通信技术入门(十四):高铁上无线上网有多难?一文即懂!》
《IM 开发者的零根底通信技术入门(十五):了解定位技术,一篇就够》
《以网游服务端的网络接入层设计为例,了解实时通信的技术挑战》
《知乎技术分享:知乎千万级并发的高性能长连贯网关技术实际》
《淘宝技术分享:手淘亿级挪动端接入层网关的技术演进之路》

更多同类文章 ……
(本文已同步公布于:http://www.52im.net/thread-32…)

退出移动版