LinuxTCP栈有无数个可以改变其行为的sysctl旋钮。这包括可用于接收或发送操作的内存量、最大套接字数、可选功能和协议扩展。有许多文章建议出于各种“性能调整”或“安全”原因禁用TCP扩展,例如时间戳或选择性确认(SACK)。本文提供了这些扩展的背景、默认情况下启用它们的原因、它们之间的关系以及关闭它们通常是个坏主意的原因。TCP窗口缩放TCP可以维持的数据传输速率受到几个因素的限制。其中包括:往返时间(RTT)。这是数据包到达目的地并返回回复所需的时间。越低越好。涉及的网络路径的最低链接速度。丢包频率。新数据可用于传输的速度。例如,CPU需要能够足够快地向网络适配器传递数据。如果CPU需要先加密数据,则适配器可能必须等待新数据。同样,如果磁盘存储读取数据的速度不够快,它也会成为瓶颈。TCP接收窗口的最大可能大小。接收窗口确定TCP在必须等待接收方报告收到该数据之前可以传输多少数据(以字节为单位)。这是由接收方宣布的。接收方将在读取并确认收到传入数据时不断更新此值。接收窗口的当前值包含在TCP标头中,它是TCP发送的每个数据段的一部分。因此,一旦发送方收到来自对等方的确认,它就知道当前的接收窗口。这意味着往返时间(RTT)越长,发送方获得接收窗口更新所需的时间就越长。TCP的未确认(传输中)数据限制为最大64KB。在大多数网络场景中,这甚至不足以维持良好的数据速率。让我们看一些例子。理论数据速率在100毫秒的往返时间(RTT)下,TCP每秒可以传输高达640KB的数据。延迟为1秒时,最大理论数据速率降至64KB/s。这是因为接收窗口。一旦发送了64KB的数据,接收窗口就满了。发送方必须等到对等方通知它应用程序已读取至少部分数据。发送的第一个段将TCP窗口减少段的大小。在接收窗口值的更新信息可用之前需要往返一次。当更新延迟1秒到达时,即使链接有足够的可用带宽,也会导致64KB的限制。为了充分利用具有几毫秒延迟的快速网络,有必要拥有比传统TCP支持的窗口更大的窗口。“64KB限制”是协议规范的产物:TCP标头仅保留16位用于接收窗口大小。这允许接收窗口最大为64KB。这个大小在最初设计TCP协议的时候并没有被认为是一个限制。不幸的是,仅通过更改TCP标头是无法支持更大的最大窗口值的。这样做意味着TCP的所有实现都必须同时更新,否则它们将无法相互理解。为了解决这个问题,我们更改了接收窗口值的解释。“窗口缩放选项”允许您更改此解释,同时保持与现有实现的兼容性。TCP选项:向后兼容协议扩展TCP支持可选扩展。这允许使用新功能增强协议,而无需立即更新所有实现。当TCP发起方连接到对等方时,它还会发送支持的扩展列表。所有扩展都遵循相同的格式:唯一的选项编号,后跟选项的长度,以及选项数据本身。TCP响应者检查连接请求中包含的任何选项号。如果它遇到一个它不理解的选项号,它会跳过伴随该选项号的数据的“长度”字节并检查下一个选项号。回复者省略了回复中不理解的内容。这使发送方和接收方都能理解所支持的通用选项集。使用窗口缩放时,选项数据始终由单个数字组成。窗口比例选项窗口比例选项(WSopt):种类:3,长度:3+--------+--------+--------+|种类=3|长度=3|shift.cnt|+--------+--------+--------+111窗口缩放选项告诉右边等,接收窗口值在TCP标头应按给定的数字缩放以获得实际大小。例如,一个宣布窗口比例因子为7的TCP发起者试图向响应者表明任何未来携带接收窗口值为512的数据包实际上将宣布一个65536字节的窗口。这是128(2^7)的因数。这将允许TCP窗口达到8MB。不理解此选项的TCP响应者将忽略它,并且为响应连接请求而发送的TCP数据包(SYN-ACK)将不包含此窗口缩放选项。在这种情况下,双方只能使用64k的窗口大小。幸运的是,几乎每个TCP堆栈都默认支持并启用此选项,包括Linux。响应者包括它自己想要的比例因子。两个同行可以使用不同的因素。声明缩放因子为0也是合法的。这意味着对等方应该如实告知其接收到的接收窗口值,但它允许在确认方向缩放该值,然后接收方可以使用更大的接收窗口.与SACK或TCP时间戳不同,窗口缩放选项仅出现在TCP连接的前两个数据包中,之后无法更改。也不可能通过查看不包含初始连接三向握手的连接的数据包捕获来确定比例因子。支持的最大缩放系数为14。这将允许TCP窗口大小达到1GB。窗口缩放的缺点是在非常特殊的情况下它会导致数据损坏。但在禁用此选项之前,请知道损坏通常是不可能的。还有一种解决方案可以防止这种情况发生。不幸的是,有些人在没有意识到它与窗口缩放的关系的情况下禁用了这个解决方案。首先,让我们看看需要解决的实际问题。设想以下事件序列:发送方发送段:s_1、s_2、s_3、...s_n。接收方看到:s_1、s_3、...s_n,并发送对s_1的确认。发送方认为s_2丢失了,重新发送。它还发送包含在段s_n+1中的新数据。然后接收方看到:s_2、s_n+1、s_2:数据包s_2被接收了两次。当发送方过早触发重传时,就会发生这种情况。在正常情况下,即使使用窗口缩放,这种错误的重传也不应该成为问题。接收方只会丢弃重复项。从旧数据到新数据的TCP序列号最大可达4GB。如果它变得大于此值,则序列环绕到0并再次增加。这本身不是问题,但如果发生得足够快,上述情况可能会引起歧义。如果环绕发生在正确的时刻,序列号s_2(重新发送的数据包)可能已经大于s_n+1。因此,在最后的步骤(4)中,接收方可以将其解释为:s_2、s_n+1、s_n+m,即它可以将“旧”数据包s_2视为包含新数据。通常情况下,这不会发生,因为即使在高带宽链路上,“环绕”也只会每隔几秒或几分钟发生一次。原始数据包和不需要的重传数据包之间的差距会小得多。例如,对于50MB/s的传输速度,重复不会成为问题,直到它们延迟一分钟以上。序列号环绕的速度不够快,小的延迟会导致这个问题。一旦TCP达到“GB/s”吞吐量,序列号就会如此快速地回绕,以至于即使是几毫秒的延迟也会导致TCP无法检测到的重复。通过解决接收窗口过小的问题,TCP现在可以用于以前无法达到的网络速度,这产生了一个新的,尽管很少见的问题。为了在非常低的RTT环境中安全地使用GB/s,接收方必须能够检测到这些旧的副本,而不仅仅是依赖序列号。TCPTimestampingBestDeadline用最简单的术语来说,TCPTimestamping只是给数据包加上时间戳,以解决由非常快的序列号回绕引起的歧义。如果一个段似乎包含新数据,但时间戳比接收窗口内的前一个数据包早,则序列号已被重新包装,“新”数据包实际上是一个较旧的副本。即使在极端情况下,这也解决了重传歧义。但是,该扩展不仅仅检测旧数据包。TCP时间戳的另一个主要功能是更精确的往返时间测量(RTTm)。需要准确的RTT估计当双方都支持时间戳时,每个TCP段携带两个额外的数字:时间戳值和回显时间戳。TCP时间戳选项(TSopt):种类:8,长度:10+--------+----+----------------+---------------+|种类=8|10|TS值(TSval)|EchoReply(TSecr)|+--------+----+---------------+----------------+1144准确的RTT估计对TCP性能至关重要。TCP自动重新发送未确认的数据。重传由定时器触发:如果超时,TCP认为一个或多个未收到确认的数据包丢失。然后再发送。然而,“尚未确认”并不意味着段丢失。也有可能接收方到目前为止还没有发送确认,或者确认仍在传输中。这就造成了一个两难境地:TCP必须等待足够长的时间才能使这种轻微的延迟变得微不足道,但它也不能等待太久。低网络延迟vs高网络延迟在高延迟网络上,如果计时器触发得太快,TCP通常会在不必要的重传上浪费时间和带宽。但是,在低延迟网络中,等待时间过长可能会在确实发生数据包丢失时导致吞吐量降低。因此,在低延迟网络中,定时器应该比在高延迟网络中更早到期。因此,TCP重传超时不能使用一个固定的常数值作为超时时间。它需要根据它在网络中经历的延迟来调整这个值。往返时间的测量TCP根据预期的往返时间(RTT)选择重传超时。事先不知道RTT。它是通过测量发送的数据段与TCP收到该数据段所载数据的确认之间的差值来估计的。它因几个因素而变得复杂。出于性能原因,TCP不会为收到的每个数据包生成新的确认。它等待的时间很短:如果有更多的段到达,它们的接收可以用一个ACK??数据包来确认。这称为“累积ACK”。往返时间不是恒定的。这是由于多种因素造成的。例如,客户端可能是移动电话,移动时会切换到不同的基站。也可能是当链路或CPU使用率增加时,数据包交换需要更长的时间。在计算期间必须忽略必须重新发送的数据包。这是因为发送端分不清对重传数据段的ACK是对原传输数据(毕竟已经到达)的确认还是对重传数据的确认。最后一点很重要:当TCP忙于从丢失中恢复时,它可能只接收重传段的ACK。因此,它无法在此恢复阶段测量(更新)RTT。所以,它无法调整重传超时,然后超时会呈指数增长。这是一个非常具体的案例(并且它假设其他机制如快速重传或SACK不起作用)。但是,使用TCP时间戳,即使在这种情况下也会进行RTT评估。如果使用扩展,对等方将从TCP段的扩展空间中读取时间戳值并将其存储在本地。然后它将该值作为“回声时间戳”放入它发回的所有数据段中。因此,该选项带有两个时间戳:它的发送者自己的时间戳和它从对等方收到的最新时间戳。原始发件人使用“回声时间戳”来计算RTT。它是当前时间戳时钟与“回显时间戳”中反映的值之间的增量。时间戳的其他用途TCP时间戳除了PAWS(ProtectionAgainstWrappedSequences)和RTT测量之外还有其他用途。例如,可以检测是否不需要重传。如果确认携带较旧的回显时间戳,则确认是针对原始数据包的,而不是重新发送的数据包。TCP时间戳的另一个更模糊的用例与TCPsyncookie功能有关。在服务器端建立TCP连接当连接请求到达的速度快于服务器应用程序接受新传入连接的速度时,连接积压最终会达到其极限。这可能是由于系统配置错误或应用程序中的错误。当一个或多个客户端发送连接请求而不对“SYNACK”响应做出反应时,也会发生这种情况。这将用不完整的连接填充连接队列。这些条目需要几秒钟才能超时。这称为同步洪水攻击。TCP时间戳和TCPSynCookie一些TCP堆栈允许在队列已满时继续接受新连接。发生这种情况时,Linux内核将在系统日志中打印一条显眼的消息:端口P上可能发生SYN泛洪。正在发送Cookie。检查SNMP计数器。这种机制将完全绕过连接队列。通常存储在连接队列中的信息被编码到SYN/ACK响应TCP序列号中。当ACK返回时,可以根据序列号重建队列条目。序列号存储信息的空间有限。因此,使用“TCPSynCookie”机制建立的连接不能支持TCP选项。但是,两个对等方共有的TCP选项可以存储在时间戳中。ACK数据包在回声时间戳字段中反映此值,这也允许恢复商定的TCP选项。否则,cookie连接将受到标准64KB接收窗口的限制。常见误解——时间戳不利于性能不幸的是,一些指南建议禁用TCP时间戳以减少内核需要访问时间戳时钟以获取当前时间的次数。这是不正确的。如前所述,RTT估计是TCP的必要组成部分。因此,内核在接收/发送数据包时始终使用微秒时间戳。在其余的数据包处理步骤中,Linux重新使用RTT估计所需的时钟时间戳。这也避免了额外的时钟命中来为传出的TCP数据包添加时间戳。整个时间戳选项只需要每个数据包10字节的TCP选项空间,这不会显着减少数据包有效负载的可用空间。常见误区-时间戳是一个安全问题一些安全审计工具和(较旧的)博客文章建议禁用TCP时间戳,因为据说它们会泄漏系统正常运行时间:这样可以估计系统/内核补丁的级别。这在过去是正确的:时间戳时钟基于一个不断增加的值,每次系统启动时该值都以固定值开始。时间戳值给出了机器运行时间(正常运行时间)的估计值。从Linux4.12开始,TCP时间戳不再显示正常运行时间。发送的所有时间戳值都使用特定于对等方的偏移量。时间戳值也每49天循环一次。换句话说,从地址“A”发起或结束的连接将看到与到远程地址“B”的连接不同的时间戳。运行sysctlnet.ipv4.tcp_timeamp=2以禁用随机偏移。这使得分析由wireshark或tcpdump等工具记录的数据包跟踪变得更加容易-从主机发送的数据包在其TCP选项时间戳中都具有相同的时钟基础。因此,对于正常操作,默认设置应保持不变。选择性确认如果同一数据窗口中的多个数据包丢失,TCP将受到影响。这是因为TCP确认是累积的,但仅适用于按顺序到达的数据包。例如:发送方发送段s_1,s_2,s_3,...s_n发送方收到s_2的ACK这意味着s_1和s_2都已收到,发送方不再需要保留这些段。应该重新发送s_3吗?s_4呢?s_n?发送方等待s_2到达的“重传超时”或“重复ACK”。如果发生重传超时或多个重复的ACK到达s_2,则发送方再次发送s_3。如果发送方收到对s_n的确认,则s_3是唯一丢失的数据包。这是理想的情况。仅发送一个丢失的数据包。如果发送方收到一个小于s_n的确认段,比如s_4,这意味着多个数据包丢失了。发送方还需要重传下一个数据段。重传策略可能简单地重复相同的序列:重新发送下一个数据包,直到接收方指示它已经处理了直到s_n的所有数据包。这种方法的问题在于它需要一个RTT,直到发送方知道接下来必须重新发送哪些数据包。虽然这种策略避免了不必要的重传,但TCP可能需要几秒或更长时间才能重新发送整个数据窗口。另一种方法是一次重新发送几个数据包。当丢失多个数据包时,此方法允许TCP更快地恢复。在上面的例子中,TCP重新发送了s_3,s_4,s_5,...,但只确保s_3丢失。从延迟的角度来看,这两种策略都不是最优的。如果只需要重发一个数据包,第一种策略是很快的,但是当有多个数据包丢失时,时间就太长了。即使要重发多个数据包,第二个也很快,但代价是浪费带宽。此外,这样的TCP发送方可能在进行不必要的重传时发送了新数据。有了可用的信息,TCP无法知道丢失了哪些数据包。这就是TCP选择性确认(SACK)的用武之地。与窗口缩放和时间戳一样,它是另一个可选但非常有用的TCP功能。SACK选项TCPSack-Permitted选项:种类:4,长度2+--------+--------+|种类=4|长度=2|+---------+--------+支持此扩展的发件人在连接请求中包含“允许SACK”选项。如果两个端点都支持扩展,则检测到数据流中数据包丢失的对等方可以将此信息通知发送方。TCPSACK选项:种类:5,长度:可变+--------+------+|种类=5|长度|+--------+--------+--------+--------+|第一个块的左边缘|+--------+--------+------+--------+|第一个块的右边缘|+--------+--------+-------+--------+||/。../||+--------+--------+--------+-------+|第n个块的左边缘|+--------+--------+--------+---------+|第n个块的右边缘|+--------+--------+--------+--------+接收方遇到s_2后跟s_5...s_n,a当发送对s_2的确认时,将包含SACK块:+--------+------+|种类=5|10|+--------+------+--------+------+|左边缘:s_5|+--------+--------+--------+------+|右边缘:s_n|+--------+--------+-------+--------+这告诉发送方通过s_2的段全部到达顺序,但也让发送者知道段s_5到s_n也已收到。然后发送方可以重新发送这两个数据包(s_3,s_4)并继续发送新数据。神话般的无损网络理论上,如果连接不丢包,那么SACK就没有优势。或者连接具有如此低的延迟,以至于等待完整的RTT甚至都没有关系。实际上,几乎不可能保证无损行为。即使网络及其所有交换机和路由器都有足够的带宽和缓冲空间,数据包仍然可能丢失:主机操作系统可能会遇到内存压力并丢弃数据包。请记住,一台主机可能同时处理数以万计的数据包流。CPU可能无法足够快地处理来自网络接口的传入数据包。这会导致网络适配器本身丢失数据包。如果TCP时间戳不可用,即使RTT非常小的连接也可能在丢失恢复期间暂时停止。使用SACK不会增加TCP数据包的大小,除非连接遇到数据包丢失。因此,几乎没有理由禁用此功能。几乎所有TCP堆栈都支持SACK——它通常只在不进行TCP批量数据传输的低功耗类物联网设备上不存在。当Linux系统接受来自此类设备的连接时,TCP会自动为受影响的连接禁用SACK。总结本文研究的三个TCP扩展都与TCP性能有关,最好保留默认设置:启用。TCP握手确保只使用双方都能理解的扩展,因此永远不需要全局禁用扩展,因为对等方可能不支持它。关闭这些扩展可能会导致严重的性能损失,尤其是TCP窗口缩放和SACK。可以禁用TCP时间戳而不会立即产生不利影响,但现在没有令人信服的理由这样做。启用它们还支持TCP选项,即使SYNcookie生效。
