本文从网络请求、接受过程、发送过程、内核协作、TCP过程给出了实际有效的优化建议并解析原理。

部分内容来源于 《深入理解Linux网络》、《Linux内核源码分析TCP实现》

网络请求优化

减少不必要的网络IO

在系统设计与开发过程中,应尽量避免不必要的网络I/O操作,尤其是在可以通过本地进程或内存内完成的场景下,避免使用网络通信来实现。网络虽然是现代分布式系统中的核心组件,能够连接不同模块、简化开发流程,并支持大规模系统的构建,但滥用网络会引发显著的性能开销和复杂性。

网络I/O,特别是本地网络I/O,存在较大的系统开销。每次网络请求都涉及从用户态切换到内核态,进而触发网络协议栈的处理、软中断的执行,以及回环设备的处理,最终唤醒或通知用户进程。这一系列操作消耗了大量的CPU资源和系统周期。更为复杂的是,网络I/O通常会带来进程上下文的频繁切换,而这些额外的切换实际上并没有为系统的业务逻辑带来任何增值,只是增加了系统的计算负担。

跨机器的网络通信带来的开销更为显著,除了上述本地通信中的系统调用、协议栈处理等开销外,还涉及网卡的DMA拷贝、网络往返延迟(RTT),这会进一步拉长请求的响应时间,影响系统的整体性能。

尽量合并网络请求

在尽可能的情况下,应该将多个网络请求合并为一次,以减少多次请求带来的CPU开销和往返时延(RTT)。

假设有一个Redis服务器,存储了每个应用程序相关的各种信息,如应用名称、版本等。现在需要根据已安装的应用列表查询Redis,来查看哪些应用有新版本可更新。假如系统中有60个应用,那么代码如下所示:

#include <stdio.h>
#include <hiredis/hiredis.h>  // Redis的C客户端库

void checkUpdate(const char *app, const char *version) {
    // 模拟检查更新的函数
    printf("Checking update for %s, current version: %s\n", app, version);
}

int main() {
    // 已安装应用的列表
    const char *installed_apps[] = {"app1", "app2", "app3", /*...*/ "app60"};
    int app_count = 60;

    // 连接到Redis
    redisContext *c = redisConnect("127.0.0.1", 6379);
    if (c == NULL || c->err) {
        printf("Error: %s\n", c->errstr);
        return 1;
    }

    // 每次独立查询Redis
    for (int i = 0; i < app_count; ++i) {
        redisReply *reply = (redisReply *)redisCommand(c, "GET %s", installed_apps[i]);
        if (reply->type == REDIS_REPLY_STRING) {
            checkUpdate(installed_apps[i], reply->str);
        }
        freeReplyObject(reply);  // 释放Redis的响应对象
    }

    // 关闭Redis连接
    redisFree(c);
    return 0;
}

每次循环中都会通过redis->get(包名)发送一个请求。也就是说,60个应用会导致60次网络请求,每次都带来一个网络往返时间(RTT),这会导致性能下降。

一个更好的做法是通过Redis的批量获取命令,比如hmget,或者使用Redis的pipeline机制。这样,通过一次网络I/O操作就可以获取到所有需要的数据,避免了多次网络往返,从而极大地提升了性能。通过合并请求,减少了60次网络RTT。

#include <stdio.h>
#include <hiredis/hiredis.h>  // Redis的C客户端库

void checkUpdate(const char *app, const char *version) {
    // 模拟检查更新的函数
    printf("Checking update for %s, current version: %s\n", app, version);
}

int main() {
    // 已安装应用的列表
    const char *installed_apps[] = {"app1", "app2", "app3", /*...*/ "app60"};
    int app_count = 60;

    // 连接到Redis
    redisContext *c = redisConnect("127.0.0.1", 6379);
    if (c == NULL || c->err) {
        printf("Error: %s\n", c->errstr);
        return 1;
    }

    // 构建HMGET命令,用于批量获取所有应用的版本信息
    redisReply *reply = (redisReply *)redisCommand(c, "HMGET app_versions %s %s %s ... %s", 
                                                   installed_apps[0], installed_apps[1], installed_apps[2], installed_apps[app_count-1]);

    // 检查Redis响应并处理
    if (reply->type == REDIS_REPLY_ARRAY) {
        for (int i = 0; i < reply->elements; ++i) {
            if (reply->element[i]->type == REDIS_REPLY_STRING) {
                checkUpdate(installed_apps[i], reply->element[i]->str);
            }
        }
    }

    // 释放Redis响应对象
    freeReplyObject(reply);

    // 关闭Redis连接
    redisFree(c);
    return 0;
}

在这里插入图片描述

调用者与被调用机器尽可能部署得近

为了降低网络通信中的RTT(Round Trip Time,往返时延),应尽量将调用方(客户端)与被调用方(服务端)部署在地理上靠近的机房,避免跨区域、长距离的网络传输。

  • 同一机房内的服务器之间,RTT通常只有零点几毫秒,这是因为数据在本地网络设备间传输,路径短且网络设备的负载小。

  • 同地区不同机房之间,RTT一般在1毫秒左右,主要由于机房之间可能存在较短的跨机房网络传输(例如城域网或专线)。

  • 跨地区、长距离传输时,例如从北京机房到广东机房,RTT会显著增加至30-40毫秒。这是由于信号必须经过长距离的光纤线路,且可能要通过多个路由器和交换机,每一跳都会引入一定的延迟。

内网不使用外网域名

当服务通过外网域名(例如 www.sogou.com)进行调用时,流量会经过公共互联网,可能涉及多个路由节点、跨国/地区的数据中心以及复杂的网络拓扑。相比之下,内网域名的调用通常仅需通过公司内部的局域网或专线,可能只需经过少数交换机或路由器,延迟显著降低。

同时,使用外网域名通常需要依赖公网DNS解析,增加了额外的解析时间。而内网调用可以直接通过公司内部的DNS服务器解析,加快了域名解析的速度。

接受过程优化

调整网卡RingBuffer大小

RingBuffer在Linux网络接收过程中充当了一个中转站的角色。网络接口卡(NIC)负责将收到的数据包写入RingBuffer中,而ksoftirqd内核线程则负责从RingBuffer中读取数据并处理。

正常情况下,ksoftirqd处理网络包的速度足够快时,RingBuffer不会出现问题,能够及时将数据包从网络卡转移到处理器。但如果瞬时有大量的数据包涌入,而ksoftirqd处理速度不够快,就会出现RingBuffer溢出的情况。后续进入的网络包将无法写入,导致丢包。

在这里插入图片描述
为了缓解瞬时高峰导致的丢包,可以通过增加RingBuffer的大小来应对。这可以通过ethtool命令来调整:

ethtool -G eth1 rx 4096 tx 4096

这会将接收(rx)和发送(tx)缓冲区的大小调整为4096个包,如图9.3所示。这样可以在网络负载高峰时,给数据包更多的缓冲空间,减少由于RingBuffer过小导致的丢包。

尽管增加RingBuffer的大小可以减少丢包,但它也会引入潜在的副作用。缓冲区过大意味着数据包在被处理之前需要等待更长时间,这会增加网络包的处理延迟。因此,这种方法仅适用于缓解短期高峰,不能作为长期的性能提升手段。

多队列网卡RSS调优

硬件中断是CPU处理外部设备请求的机制,网卡作为网络设备,会在接收到网络包时触发中断通知CPU进行处理。在Linux系统中,/proc/interrupts 文件详细记录了每个中断号在不同CPU核上的处理情况,包括每个中断号累计的中断次数以及与设备的对应关系。

在这里插入图片描述

从示例可以看到,网卡(如virtio1-input.0)的中断编号为27,所有的中断都由CPU3处理,累计的中断次数已达到11亿次。这表明系统默认的配置将该网卡的所有中断绑定到了单个CPU核心上。

中断亲和性(IRQ affinity) 是指将特定的硬件中断绑定到某个或某组CPU核上进行处理。亲和性设置可以通过查看/proc/irq/{中断号}/smp_affinity文件来确认。文件内容以十六进制数表示绑定的CPU核,例如8代表绑定到CPU3(因为二进制的1000表示第四个CPU核)。

默认情况下,某些中断可能会集中在单一CPU核上处理,导致该核负载过高,其他核闲置。因此,通过调整中断亲和性,可以更均匀地分布中断请求,优化系统的性能和响应时间。

这个特性叫做接收端扩展(Receive Side Scaling,RSS)。旨在通过将网络流量分发到多个接收队列,并由不同的CPU并行处理,来提升系统的网络性能。在RSS启用的情况下,网卡会根据数据包中的特定信息(如源地址、目的地址等)通过哈希算法,将数据包分散到不同的接收队列中

在这里插入图片描述

硬中断合并

在网络通信中,当网卡接收到数据包后,会通过硬中断的方式通知CPU进行处理。每次中断都意味着CPU需要暂时中断当前正在执行的任务,切换到内核态以处理网络包,完成处理后再返回到用户态继续执行原任务。

然而,频繁的中断会对系统性能造成较大开销,原因在于:

  • 上下文切换成本:每次中断意味着CPU需要进行上下文切换,保存当前任务的状态,然后处理中断。上下文切换不仅占用CPU时间,还可能导致缓存命中率下降,增加额外的内存访问延迟。
  • 缓存失效:当CPU处理硬中断时,当前任务的缓存可能会被替换,导致原任务恢复时需要重新加载数据,增加额外的处理时间

通过硬中断合并,网卡可以在收到多个网络包时,延迟中断的发起,将多个中断请求合并成一次中断。硬中断合并的实现通常通过配置网卡驱动程序的中断策略来完成,常见的参数包括:

  • 自适应中断合并(Adaptive RX):网卡驱动根据系统负载和网络流量自动调整中断合并策略。当流量较低时,驱动可以减少中断合并以保证及时性;当流量较高时,则可以增加合并的力度,降低中断频率,提升系统的整体吞吐量。
  • rx-usecs:指定一个时间窗口(以微秒为单位),当超过这个时间窗口后,网卡将触发一次中断。这意味着即使数据包数量较少,但只要达到指定的时间,网卡也会产生中断通知。
  • rx-frames:设置当网卡接收到指定数量的网络帧后,触发一次中断。这种方式在高网络流量场景中尤其有效,因为可以减少小数据包频繁中断的问题。

这些参数可以通过ethtool工具进行配置,例如:

ethtool -C eth0 adaptive-rx on   # 开启自适应中断合并
ethtool -C eth0 rx-usecs 100     # 设置100微秒后触发中断
ethtool -C eth0 rx-frames 64     # 每收到64个帧后触发中断

虽然硬中断合并可以减少中断频率,提升吞吐量,但也有一定的副作用。网卡会在收到一定数量的数据包或达到设定的时间后才触发中断,这可能会引入额外的处理延迟,尤其是在低流量场景中,延迟会更加明显。

软终端budget调整

在Linux内核中,软中断(softirq)是处理网络包的重要机制之一。硬中断是由硬件设备发起的,CPU必须立即响应。而软中断则是延迟处理的一种方式,用于完成更多耗时的任务,比如网络包的接收与处理。Linux通过ksoftirqd内核线程来处理软中断,确保网络包的高效处理。

ksoftirqd会集中处理一批网络包,完成一段网络处理任务后,主动让出CPU,使其他任务有机会执行。这种设计有助于防止网络处理占用过多的系统资源,同时确保其他任务的公平调度。

在Linux中,net.core.netdev_budget 是一个用于控制 ksoftirqd 每次被触发时最多处理多少个网络包的参数。这个参数直接影响了 ksoftirqd 的工作负载。默认值为300, ksoftirqd 每次最多处理300个包后会主动让出CPU,让其他系统任务得到调度。

如果系统的网络流量较大,或者对网络吞吐量有较高的要求,可以通过增加 net.core.netdev_budget 参数值,让 ksoftirqd 在一次调度中处理更多的网络包。例如,使用下面的命令将预算从300增加到600:

sysctl -w net.core.netdev_budget=600

接受处理合并

在高性能网络环境中,接收大量小数据包并逐一传递给上层协议栈处理,可能会造成不必要的开销,影响整体吞吐量和CPU利用率。LRO(Large Receive Offload)和GRO(Generic Receive Offload) 是两种优化技术,它们通过将多个接收的网络包合并为一个大包后,再传递给协议栈,来提高网络性能。

LRO 是一种依赖于网卡硬件的包合并技术,它能够在网卡层面将多个小的TCP段合并为一个大的TCP段,然后再交由上层协议栈处理。这种做法有效减少了数据包进入操作系统的次数,降低了协议栈处理的开销,提升了网络的吞吐量。由于LRO是在硬件层完成的,因此要求网卡硬件支持此功能。如果网卡不支持LRO,则无法使用这一优化技术。

GRO 是软件实现的包合并技术,它在Linux内核中实现,并不依赖网卡硬件。GRO和LRO的目的一样,都是将多个接收到的小包合并为一个大包再传递给上层协议栈,减少协议栈处理的开销。

LRO/GRO非常适用于网络流量密集型的服务器场景,如Web服务器、数据库服务器、大规模文件传输服务器等。在这些场景中,LRO/GRO能够通过减少小包处理的频率,提升系统的整体性能。

发送过程优化

控制数据包大小

MTU的大小通常为1500字节,这是网络层中IP协议可以处理的最大数据包尺寸。如果数据包超过了MTU,IP层会将其分片,即把大数据包拆分成若干小片,以符合MTU要求。这个过程有几方面的影响:

  • 内存拷贝的增加:分片过程会增加系统的内存拷贝操作,因为每个分片都需要重新分配内存来存储。这种额外的内存操作会影响处理速度,增加系统开销,并可能导致性能下降。

  • 丢包风险增大:在网络传输过程中,每个分片都可能面临丢失的风险。由于分片后的数据包是相互依赖的,如果任意一个分片丢失,接收端将无法重组完整的数据包。这意味着重传操作会被触发,大大增加了数据包成功传输的延迟和资源占用。

  • 重传延迟:一旦发生丢包,重传机制将启动。这种延迟对于网络性能的影响是显著的,尤其是在实时性要求较高的应用场景下。

如果应用场景允许,可以尝试将每个数据包的大小控制在MTU范围内。这样做的好处包括减少分片需求,从而降低内存拷贝的次数,减小丢包风险以及避免重传延迟。

减少内存拷贝

通过 read() 和 write() 发送文件时,数据从硬盘读取到内核态的 PageCache(缓存区),然后通过 read() 系统调用将数据复制到用户态进程的内存中。再通过 write() 系统调用将数据从用户态传送到 Socket 的发送缓冲区。最后网络驱动使用 DMA(直接内存访问)从缓冲区传输数据到网卡进行发送。

在这里插入图片描述

数据在用户态和内核态之间来回拷贝了两次(硬盘到内核,内核到用户,再返回内核)。这增加了 CPU 的负载和延迟。

目前减少内存拷贝主要有两种方法,分别是使用mmap和sendfile两个系统调用。使
用mmap系统调用的话,映射进来的这段地址空间的内存在用户态和内核态都是可以使用的。如果所发送数据是mmap映射进来的数据,则内核直接就可以从地址空间中读取,这样就节约了一次从内核态到用户态的拷贝过程。

在这里插入图片描述
不过系统调用的开销没有减少,还是发生了两次用户态与内核态的转换。如果只是想把一个文件发送出去,而不关心它的内容。可以使用sendfile() 。

sendfile() 是一种优化的系统调用,它直接将内核态中的 PageCache 数据发送到Socket,而不经过用户态。这种方式利用了硬件的 SG-DMA(散集 DMA)功能,可以直接从 PageCache 通过 DMA 将数据发送到网卡,而跳过了用户态的数据拷贝。

在这里插入图片描述

推迟分片

这一优化的关键在于使用 TSO(TCP Segmentation Offload)或 GSO(Generic Segmentation Offload)等硬件加速技术。

在网络传输中,IP 层会根据 MTU(最大传输单元)来决定是否需要对大数据包进行分片。传统做法是在 IP 层检测到数据包大于 MTU 时进行分片。然而,分片的过程会涉及到协议头的重新计算、数据的重新组织,这会对 CPU 造成负担,尤其是在高流量场景下。

这里有一个例外情况,即如果数据包是 GSO 数据包(通过 skb_is_gso() 检查),则不会在此处进行分片,而是推迟到设备层处理。这意味着 GSO 数据包不会在 IP 层分片,而是先保持大包形式。

ip_finish_output() 函数部分逻辑:

if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
    return ip_fragment(skb, ip_finish_output2);
else
    return ip_finish_output2(skb);

dev_hard_start_xmit() 函数部分逻辑:

if (netif_needs_gso(skb, features)) {
    if (unlikely(dev_gso_segment(skb, features)))
        goto out_kfree_skb;
    if (skb->next)
        goto gso;
}

如果网卡支持 TSO,那么系统不会切分数据包,而是将整个大包直接交给网卡驱动,由网卡硬件处理数据包的分片。这意味着分片的工作被推迟到硬件层处理,从而大幅减轻了 CPU 的负担。

TSO/GSO 的作用: TSO(TCP Segmentation Offload)和 GSO(Generic Segmentation Offload)都是网络硬件的加速特性,它们允许系统将一个大数据包推迟到硬件层才进行分片。换句话说,数据包从 IP 层一直保持大包形式,直到物理网卡层才进行切分。这样做的目的是避免在 IP 层或 TCP 层频繁进行分片和协议头计算工作,这些操作会消耗大量的 CPU 资源。

在这里插入图片描述

多队列XPS调优

XPS(Transmit Packet Steering)是一种优化技术,用于提高多队列网卡的发送性能。在多核系统中,网络接口卡(NIC)通常配备多个发送队列,XPS的作用是将不同的CPU核与特定的网卡发送队列绑定起来,使得数据包的发送操作可以与特定的CPU核关联,从而减少CPU与队列之间的竞争,并提高缓存的局部性。

get_xps_queue() 是在网络栈中选择发送队列的关键函数。它的主要工作是根据当前的 CPU ID 查找对应的 XPS 配置,并决定数据包应该从哪个队列发送。

static inline int get_xps_queue(struct net_device *dev, struct sk_buff *skb) {
    // 获取XPS配置
    dev_maps = rcu_dereference(dev->xps_maps);
    if (dev_maps) {
        // 获取当前 CPU 对应的队列配置
        map = rcu_dereference(dev_maps->cpu_map[raw_smp_processor_id()]);
        if (map) {
            if (map->len == 1)
                queue_index = map->queues[0];
        }
    }
}

XPS 的配置文件位于 /sys/class/net/<interface>/queues/tx-<queue>/xps_cpus,这些文件中保存了发送队列和 CPU 的绑定信息。配置通过位掩码表示,每个位代表一个 CPU。举个例子:

# cat /sys/class/net/eth0/queues/tx-0/xps_cpus
00000001  # 表示 CPU0 绑定到 tx-0 队列
# cat /sys/class/net/eth0/queues/tx-1/xps_cpus
00000002  # 表示 CPU1 绑定到 tx-1 队列
# cat /sys/class/net/eth0/queues/tx-2/xps_cpus
00000004  # 表示 CPU2 绑定到 tx-2 队列
# cat /sys/class/net/eth0/queues/tx-3/xps_cpus
00000008  # 表示 CPU3 绑定到 tx-3 队列

这些设置确保每个发送队列与不同的 CPU 核绑定。例如,eth0 网卡的 tx-0 队列与 CPU0 绑定,而 tx-1 队列则与 CPU1 绑定。

使用eBPF绕开协议栈的本机IO

eBPF 是一种允许开发者动态加载和运行在内核中的小型程序的技术。这些程序可以在内核的各个钩子点上执行,以修改或优化数据的处理流程。

在传统的本机网络通信中,数据从一个 socket 发送到另一个 socket,仍然要经过传输层(TCP/UDP),网络层(IP),回环设备,最后再传递给接收端的 socket。这种方式虽然不经过物理网卡,但协议栈的处理过程是完整的,带来了不小的系统调用开销和协议处理开销。

使用 eBPF 时,数据可以直接通过 sockmap 和 sk_redirect 在发送端和接收端的 socket 之间进行传递。数据在进入协议栈之前,就已经被 eBPF 捕获并转发到目标 socket,而不需要经过传输层(TCP/UDP)和网络层(IP)的处理。这意味着,内核避免了协议栈的复杂处理流程,直接实现了数据的传输。适用于需要大量本机通信的应用场景,如微服务架构或容器内部通信。

eBPF sockmap:sockmap 是 eBPF 的一种数据结构,允许用户将多个 socket 存放在一个 map 中。使用 sockmap,内核可以快速地从一个 socket 中读取数据并发送到另一个 socket。

eBPF sk_redirect:sk_redirect 是 eBPF 的一个功能,允许将来自某个 socket 的数据直接重定向到另一个 socket。这样一来,数据不再需要经过复杂的协议栈,直接通过内核中的 eBPF 程序完成转发,极大地减少了 CPU 和内存的消耗。

内核与进程协作优化

少用进程阻塞的方式

在使用传统的阻塞式 I/O 模型时,例如 recvfrom 函数,进程会在等待数据时被挂起,直到有数据到达为止。在这期间,操作系统会进行上下文切换,即将当前等待的进程从 CPU 上移除,换上其他进程继续执行。当数据准备好后,挂起的进程会被唤醒并重新放回 CPU。这种模型的主要问题在于频繁的上下文切换,尤其在高并发的服务器环境下,进程之间的切换会造成大量的 CPU 资源浪费。

尽量避免使用这种阻塞式 I/O 模型,尤其是在高并发环境中。可以考虑使用非阻塞 I/O 或事件驱动的模型来提升并发处理能力,减少上下文切换带来的开销。

使用成熟的网络库

在开发网络应用时,不需要自己从零构建网络模型,直接使用成熟的网络库可以大幅减少开发复杂性,同时获得良好的性能提升。这些库已经优化了 I/O 处理,可以根据应用需求选择合适的库和模式。

不同网络库在多线程模式下使用了不同的架构,比如说:

  • 单 Reactor:一个线程负责处理所有的 I/O 事件和业务逻辑。
  • 多 Reactor:多个线程分工合作,某些线程负责 I/O 事件的监听,其他线程负责业务逻辑处理。
  • Proactor 模式:I/O 操作由内核完成,用户进程只负责处理数据,避免了繁琐的 I/O 读写操作。

使用 Kernel-Bypass 新技术

在极端高性能场景下,传统的网络栈(如 TCP/IP 协议栈)会成为性能瓶颈。网络数据包在通过内核时,会经历复杂的协议栈处理(传输层、网络层、设备驱动等),并且涉及频繁的用户态与内核态的上下文切换和内存拷贝。尽管现代网络协议栈已经经过高度优化,但仍然会存在较高的延迟和 CPU 占用。

Kernel-Bypass 是一种绕过内核网络协议栈的技术,直接在用户态实现网络包的收发。这样可以避免繁杂的协议栈处理,同时减少内核态和用户态之间的拷贝和切换开销。

DPDK(Data Plane Development Kit):这是一个允许直接在用户态处理网络包的框架,广泛应用于高性能网络应用中。DPDK 通过将网卡绑定到用户态进程上,绕过内核网络栈来处理包转发。

TCP过程优化

配置充足的端口范围

在网络通信中,客户端需要选择一个本地端口来发起TCP连接,而默认情况下,系统分配的端口范围较小(可能只有1万个端口)。如果端口不够,内核会在可用端口池中查找可用端口,但这会导致端口重复选择,并增加查找和等待时间,甚至CPU资源消耗。因此,适当加大端口范围(例如5000至65000)可以显著降低查找和等待延迟,提高并发连接能力。

启用端口快速复用(twreuse和twrecycle): 在TCP连接断开后,端口会进入TIME-WAIT状态,需等待2MSL时间才能释放。为避免端口耗尽,启用tcp_tw_reuse和tcp_tw_recycle选项可以让端口在断开连接时快速释放,避免TIME-WAIT状态过多的端口积压。但需要注意的是,启用这些选项依赖于tcp_timestamps设置(确保其开启)。通过启用这三项配置,系统可以更快地回收端口,加快连接重建,提高连接处理速度:

# vi /etc/sysctl.conf
net.ipv4.tcp_timestamps=1
net.ipv4.tcp_twreuse=1
net.ipv4.tcp_twrecycle=1
# sysctl -p

客户端避免使用bind

在客户端程序中避免显式调用bind绑定端口可以有效提升并发连接的数量。TCP连接的四元组(源IP、源端口、目的IP、目的端口)只要不完全相同,就可以复用同一端口。若客户端显式绑定端口,内核只能为该端口提供一个连接,显著降低了连接复用能力和最大连接数。因此,建议让connect自动选择端口,从而充分利用端口复用的特性。

注意连接队列溢出

理解连接队列机制: 服务端在处理TCP连接时使用两个队列:半连接队列和全连接队列。

  • 半连接队列:存放已收到SYN但还未完成三次握手的连接请求。
  • 全连接队列:存放已完成三次握手、等待服务器accept的连接请求。

处理半连接队列溢出: 半连接队列的溢出可通过启用 tcp_syncookies 参数解决。Syncookies机制允许服务器在半连接队列已满时处理更多连接请求,避免丢包。可以通过如下配置启用:

# vi /etc/sysctl.conf
net.ipv4.tcp_syncookies=1
# sysctl -p

Syncookies可以在不增加服务器负载的情况下处理突发流量,减少握手超时和丢包。

监控全连接队列溢出: 可以使用 netstat -s 查看系统的全连接队列溢出情况。监控丢包次数是否增加可以反映队列的负荷状况。如果全连接队列发生溢出,通过 watch ‘netstat -s | grep overflowed’ 命令可以动态监控丢包状态。

160 times the listen queue of a socket overflowed

若数字变化,说明服务器的全连接队列已满。为解决该问题,可以调整 listen 调用中的 backlog 参数和内核参数 net.core.somaxconn,例如:

# vi /etc/sysctl.conf
net.core.somaxconn=1024  # 增加连接队列长度
# sysctl -p

减少握手重试

重试机制的原理: 在TCP握手过程中,如果某一方没有收到预期的回复,会启动超时重传机制,间隔时间呈指数级增长(例如1秒、3秒、7秒等)。在高负载或连接不稳定的环境中,过长的重试过程会增加响应时间甚至超出用户等待阈值。

针对用户直接访问的应用,缩短重试次数可以加快响应速度,提升用户体验。

  • 客户端 SYN 重传次数:由 tcp_syn_retries 控制。例如,将客户端的 tcp_syn_retries 配置为较小值(如2或3),减少握手等待时间。
  • 服务端 SYN-ACK 重试次数:服务端的 tcp_synack_retries 参数控制半连接队列中的SYN-ACK重传次数,可以适当调小以加快连接放弃速度。
# vi /etc/sysctl.conf
net.ipv4.tcp_syn_retries=2
net.ipv4.tcp_synack_retries=2
# sysctl -p

启用TCP Fast Open (TFO)

TFO的工作原理: TCP Fast Open (TFO)允许客户端在第三次握手的ACK包中携带数据,从而减少一个RTT(Round Trip Time)。这对需要快速建立连接并立即发送数据的应用(如HTTP请求)尤为有利。在客户端和服务端都支持TFO的情况下,通过跳过等待数据传输,可以降低延迟,提升传输效率。

启用TFO可以通过设置 net.ipv4.tcp_fastopen=3 来实现,配置文件为 /etc/sysctl.conf:

# vi /etc/sysctl.conf
net.ipv4.tcp_fastopen=3  # 双端启用TFO
# sysctl -p

调整文件描述符上限

在Linux中,所有资源(包括网络连接和文件)都通过文件描述符管理。高并发应用需要大量的文件描述符,因此增加文件描述符上限非常关键。否则,应用将因资源不足而报出“Too many open files”错误,导致服务中断。

为满足高并发需求,可设置系统级别和用户级别的文件描述符上限。假设需要支持100万并发连接,配置如下:

# vi /etc/sysctl.conf
fs.file-max=1100000      # 系统级别文件描述符上限
fs.nr_open=1100000       # 进程级别文件描述符上限
# sysctl -p

# vi /etc/security/limits.conf
* soft nofile 1000000     # 用户进程级别的软限制
* hard nofile 1000000     # 用户进程级别的硬限制

频繁请求时使用长连接

长连接的优势: 对于频繁访问同一服务(如缓存服务Redis)的应用,长连接能够减少连接开销和资源占用。

  • 节约握手开销:每次新建连接需要三次握手,而长连接则持续复用同一个连接,大幅降低握手延迟。
  • 避免连接队列溢出:长连接有效减少对连接队列的压力,避免因队列溢出而丢包和等待重试。
  • 端口资源节约:短连接在断开后需进入TIME-WAIT状态,频繁的短连接易导致端口资源耗尽。而长连接复用固定端口,降低端口占用风险。

TIME-WAIT状态优化

在短连接场景中,TIME-WAIT状态的连接较多,这是TCP设计中为确保数据可靠传输而设置的状态。在TIME-WAIT状态的连接会暂时占用端口,但一般不会造成严重资源消耗。

reuse和recycle参数: 启用 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_tw_recycle 可以让端口在TIME-WAIT状态下更快地复用。

限制TIME-WAIT连接数量: 可以通过设置 net.ipv4.tcp_max_tw_buckets 来限制TIME-WAIT状态连接的数量。例如,将其限制为32768,可以减少系统资源占用:

# vi /etc/sysctl.conf
net.ipv4.tcp_max_tw_buckets=32768
# sysctl -p

长连接替代短连接:如之前所述,使用长连接可以显著减少TIME-WAIT的产生,从根本上缓解TIME-WAIT过多的问题。

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐