关于linux:聊聊TCP连接耗时的那些事儿

4次阅读

共计 7146 个字符,预计需要花费 18 分钟才能阅读完成。

在互联网后端日常开发接口的时候中,不论你应用的是 C、Java、PHP 还是 Golang,都防止不了须要调用 mysql、redis 等组件来获取数据,可能还须要执行一些 rpc 近程调用,或者再调用一些其它 restful api。在这些调用的底层,根本都是在应用 TCP 协定进行传输。这是因为在传输层协定中,TCP 协定具备牢靠的连贯,谬误重传,拥塞管制等长处,所以目前利用比 UDP 更宽泛一些。
置信你也肯定听闻过 TCP 也存在一些毛病,那就是陈词滥调的开销要略大。然而各路技术博客里都在单单说开销大、或者开销小,而少见不给出具体的量化剖析。不客气一点,这都是养分不大的废话。通过日常工作的思考之后,我更想弄明确的是,开销到底多大。一条 TCP 连贯的建设须要耗时提早多少,是多少毫秒,还是多少微秒?能不能有一个哪怕是粗略的量化预计?当然影响 TCP 耗时的因素有很多,比方网络丢包等等。我明天只分享我在工作实际中遇到的比拟高发的各种状况。

失常 TCP 连贯建设过程

要想搞清楚 TCP 连贯的建设耗时,咱们须要具体理解连贯的建设过程。在前文《图解 Linux 网络包接管过程》中咱们介绍了数据包在接收端是怎么被接管的。数据包从发送方进去,通过网络达到接管方的网卡。在接管方网卡将数据包 DMA 到 RingBuffer 后,内核通过硬中断、软中断等机制来解决(如果发送的是用户数据的话,最初会发送到 socket 的接管队列中,并唤醒用户过程)。

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

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

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

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

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

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

  • 第一类是内核耗费 CPU 进行接管、发送或者是解决,包含零碎调用、软中断和上下文切换。它们的耗时根本都是几个 us 左右。具体的剖析过程能够参见《一次零碎调用开销到底有多大?》、《软中断会吃掉你多少 CPU?》、《过程 / 线程上下文切换会用掉你多少 CPU?》这三篇文章。
  • 第二类是网络传输,当包被从一台机器上收回当前,两头要通过各式各样的网线、各种交换机路由器。所以网络传输的耗时相比本机的 CPU 解决,就要高的多了。依据网络远近个别在几 ms\~ 到几百 ms 不等。。

1ms 就等于 1000us,因而网络传输耗时比双端的 CPU 开销要高 1000 倍左右,甚至更高可能还到 100000 倍。所以,在失常的 TCP 连贯的建设过程中,个别能够思考网络延时即可。一个 RTT 指的是包从一台服务器到另外一台服务器的一个来回的延迟时间。所以从全局来看,TCP 连贯建设的网络耗时大概须要三次传输,再加上少许的单方 CPU 开销,总共大概比 1.5 倍 RTT 大一点点。不过从客户端视角来看,只有 ACK 包收回了,内核就认为连贯是建设胜利了。所以如果在客户端打点统计 TCP 连贯建设耗时的话,只须要两次传输耗时 - 既 1 个 RTT 多一点的工夫。(对于服务器端视角来看同理,从 SYN 包收到开始算,到收到 ACK,两头也是一次 RTT 耗时)

TCP 链接建设时的异常情况

上一节能够看到在客户端视角,,在失常状况下一次 TCP 连贯总的耗时也就就大概是一次网络 RTT 的耗时。如果所有的事件都这么简略,我想我的这次分享也就没有必要了。事件不肯定总是这么美妙,总会有意外产生。在某些状况下,可能会导致连贯时的网络传输耗时上涨、CPU 解决开销减少、甚至是连贯失败。当初咱们说一下我在线上遇到过的各种沟沟坎坎。

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 多预留一些端口号、改用长连贯都能够。

2)半 / 全连贯队列满

如果连贯建设的过程中,任意一个队列满了,那么客户端发送过去的 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 times the 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”。就义一个用户的拜访申请,要比把整个站都搞崩了还是要强的。

连贯耗时实测

我写了一段非常简单的代码,用来在客户端统计每创立一个 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

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

首先我的客户端位于河北怀来的 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 链接建设啥的各种网络包传输都要快很多。要尽可能防止短途跨地区机房的调用状况呈现。

2)连贯队列溢出

测试完了跨地区、跨机房和跨机器。这次为了快,间接和本机建设连贯后果会咋样呢?
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 左右比拟失常。

论断

TCP 连贯建设异常情况下,可能须要好几秒,一个害处就是会影响用户体验,甚至导致以后用户拜访超时都有可能。另外一个害处是可能会诱发雪崩。所以当你的服务器应用短连贯的形式拜访数据的时候,肯定要学会要监控你的服务器的连贯建设是否有异样状态产生。如果有,学会优化掉它。当然你也能够采纳本机内存缓存,或者应用连接池来放弃长连贯,通过这两种形式间接防止掉 TCP 握手挥手的各种开销也能够。

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

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



相干浏览:

  • 1. 图解 Linux 网络包接管过程
  • 2.Linux 网络包接管过程的监控与调优
  • 3. 过程 / 线程切换到底须要多少开销?
  • 4. 软中断会吃掉你多少 CPU?
  • 5. 一次零碎调用开销到底有多大?
  • 6. 追踪将服务器 CPU 耗光的凶手

我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术实践,也不只介绍实践经验。而是把实践与实际联合起来,用实际加深对实践的了解、用实践进步你的技术实际能力。欢送你来关注我的公众号,也请分享给你的好友~~~

正文完
 0