You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
android-notes/blogs/computer_network/优化 TCP 三次握手性能.md

5.5 KiB

如何提升 TCP 三次握手的性能?

参考:如何提升 TCP 三次握手的性能

TCP 是一个可以双向传输的全双工协议,所以需要经过三次握手才能建立连接。三次握手在一个 HTTP 请求中的平均时间占比在 10% 以上,在网络状况不佳、高并发或者遭遇 SYN 泛洪攻击等场景中,如果不能正确的调整三次握手中的参数,就会对性能有很大的影响。

首先看客户端的优化,客户端在发出 SYN 报文后但没有收到对端 ACK 时,客户端会重发 SYN,重试的次数由 tcp_syn_retries 参数控制,默认是 6 次:

net.ipv4.tcp_syn_retries = 6

第 1 次重试发生在 1 秒钟后,接着会以翻倍的方式在第 2、4、8、16、32 秒共做 6 次重试,最后一次重试会等待 64 秒,如果仍然没有返回 ACK,才会终止三次握手。所以说总耗时是 127 秒,超过 2 分钟。

如果这是一台有明确任务的服务器,就可以根据网络的稳定性和目标服务器的繁忙程度修改重试次数,调整客户端的三次握手时间上限。比如内网中通讯时,就可以适当调低重试次数,尽快的把错误暴露给应用程序。

再看服务端的优化,服务端在收到 SYN 报文并回复 SYN+ACK 时,此时服务端会把该连接信息放入一个 SYN 版连接队列里面,当这个队列溢出时,服务端将无法在建立新的连接。所以此时可以修改 SYN 半连接队列的大小的,即:

net.ipv4.tcp_max_syn_backlog = 1024

如果 SYN 半连接队列已满,只能丢弃连接吗?并不是这样,开启 syncookies 功能就可以在不使用 SYN 队列的情况下成功建立连接。syncoookie 是这么做的:服务端根据当前状态计算出一个值,放在已方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功。

Linux 下怎样开启 syncookies 功能呢?修改 top_syncookies 参数即可,其中值为 0 时表示关闭该功能,2 表示无条件开启功能,而 1 则表示仅当 SYN 半连接队列放不下时再启用它。由于 syncookie 仅用于应对 SYN 泛洪攻击,这种方式建立的连接,许多 TCP 特性都无法使用,所以,应当把 tcp_syncookies 设置为 1,仅在队列满时再启用:

net.ipv4.tcp_syncookies = 1

当服务端发送完 SYN+ACK 报文后,但是却为收到对端 ACK,这时候服务端就会重发 SYN+ACK。当网络繁忙、不稳定时,报文丢失就会变严重,此时应该调大重发次数,反之则可以调小重发次数。对应的参数如下:

net.ipv4.tcp_synack_retries = 5

当服务端收到 ACK 后,内核就会把连接从 SYN 半连接队列中移除,再移入 accept 队列,等待进程调用 accept 函数时再把连接取出来。如果进程不能及时的调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃。

实际上,丢弃连接只是 Linux 的默认行为,我们还可以选择向客户端发送 RST 复位报文,告诉客户端连接已经建立失败。打开这一功能需要将 tcp_abort_on_overflow 参数设置为 1。

不过通常情况下,应该把该参数设置为 0,因为这样更有利于应对突发流量。

net.ipv4.tcp_abort_on_overflow = 0

举个例子,当 accept 队列满导致服务端丢掉了 ACK,与此同时,客户端的连接状态却是 ESTABLISHED,进程就会在建立好的连接上发送请求。只要服务端没有为请求回复 ACK,请求就会被多次重发。如果服务端上的进程只有短暂的繁忙导致 accept 队列满,那么当 accept 队列有空位时,再次接收到的请求由于含有 ACK,仍然会触发服务端成功建立连接。所以 tcp_abort_on_overflow 设为 0 可以提高连接建立的成功率,只有你非常肯定 accept 队列会长期溢出,才能设置为 1 以尽快通知客户端。

TFO 技术如何绕过三次握手?

TFO 即 TCP Fast Open,客户端可以在首个 SYN 报文中就携带请求,这节省了 1 个 RTT 的时间。TFO 具体是如何实现的呢?

为了让客户端在 SYN 报文中携带请求数据,必须解决服务端的信任问题。因为此时服务端的 SYN 报文还没有发给客户端,客户端是能够正常建立连接还未可知,但此时服务端需要假定连接已经建立成功,并把请求交付给进程去处理,所以服务端必须能够信任这个客户端。

TFO 到底怎样达成这一目的呢?它把通讯分为两个阶段,第一阶段为首次建立连接,这时走正常的三次握手,但在客户端的 SYN 报文会明确的告诉服务端它想使用 TFO 功能,这样服务端会把客户端 IP 地址用只有自己知道的密钥加密(比如 AES),作为 Cookie 携带在返回的 SYN+ACK 报文中,客户端收到后会将 Cookie 换存在本地。

之后,如果客户端再次向服务端建立连接,就可以在第一个 SYN 报文中携带请求数据,同时还要附带缓存的 Cookie。服务端收到后,会用自己的密钥验证返回 SYN+ACK。虽然客户端收到后还会返回 ACK,但服务器不等收到 ACK 就可以发送 HTTP 相应了,这就减少了握手带来的 1 个 RTT 的时间消耗。

小结一下,如何优化 TCP 的三次握手?我们可以从控制重发次数、调整连接队列的大小、开启 TFO 等思路去着手优化。