文章目录

  • 一、绪论
  • 二、内核如何接收网络包
    • 1、收包流程的一些核心概念
    • 2、网络接收过程总览
  • 三、内核如何与用户进程协作
    • 1、进程的一些基础概念
    • 3、同步阻塞IO工作流程
    • 4、epoll工作流程
  • 四、内核如何发送网络包
    • 1、发包流程的一些基础概念
    • 2、网络发送过程总览
    • 3、发包流程的内存拷贝操作
    • 4、数据从用户进程到网卡的详细过程
  • 五、深入理解本机网络IO
    • 1、跨机网络通信
    • 2、本机网络通信
  • 六、深入理解TCP连接
    • 1、TCP连接建立过程
    • 2、Linux内存管理
    • 3、TCP连接占多大内存,机器最多能支持多少TCP连接
    • 4、日常工作常见报错【实操[重要]】
      • a、端口不足
        • 【Cannot assign request address】
      • b、半/全连接队列满
        • 方法1、打开syncookie
        • 方法2、尽快调用accept
        • 方法3、加大连接队列长度
        • 方法4、尽早拒绝【connection reset by peer】
        • 方法5、尽量减少TCP连接的次数
      • c、【TIME_WAIT】连接过多怎么办?
      • d、【Too many open files】
  • 七、网络性能优化建议【重要】
    • 1、网络请求优化
    • 2、接收过程优化
    • 3、发送过程优化
    • 4、内核与进程协作优化
    • 5、握手挥手过程优化

为什么学习本文?

作为一个程序猿,写出来的代码不是能跑起来就行了,是必须能够稳定运行。应用程序都是跑在硬件、操作系统之上的,因此线上的很多问题都和底层相关。如果对这类底层知识【Linux内核、操作系统、网络、硬件设备等知识】了解的不够,则根本无法应对。内功的深浅,决定了是否具备基本的问题排查以及性能调优能力。

许多工作三年左右的后端开发人员会陷入一个成长瓶颈[个人深有体会],认为编程语言、框架、MySQL、Redis、Nginx、MQ等技术都用的很溜,总感觉没啥可以学的了。反观一些工作经验丰富的高级技术人员,他们对底层的理解都相当深刻。深厚的内功知识又使得他们学习起来新技术非常快。举个例子,大佬们一眼就能看出Java的NIO和Golang的net包原理是一样的,都是对epoll的不同封装。

因此我们需要修炼底层内功,掌握高性能原理。

PS:本篇文章的每一章节以若干思考题开始,答案都在本章中。 [ps:第一章的答案在全文]。

一、绪论

先从一个典型的服务端网络处理程序代码说起:

边看程序代码边思考如下问题:

1、调用read()后内核是如何完成网络包的接收的?RingBuffer了解吗?硬中断和软中断?网卡多队列?
2、内核如何与用户进程协作?了解阻塞吗?epoll原理?epoll是阻塞的吗?Redis为啥性能高?
3、调用send()后内核是如何完成网络包的发送的?会触发哪些内存拷贝?零拷贝是啥?Kafka为啥性能高?
4、TCP连接流程到底是怎么样的?为什么要先listen()?半连接和全连接队列?socket内核对象的创建时机?内核是如何管理内存的?服务端能支持多少连接?客户端能发起多少连接?
5、本机网络IO与跨机网络IO的有区别吗?
6、如何进行网络性能优化?容器网络虚拟化是如何实现的?

int main(){// 创建一个socket对象【六、深入理解TCP连接】fd = socket(AF_INET,SOCK_STREAM,0) // 创建了一系列的socket相关内核对象。bind(fd, ...)// 监听客户请求listen(fd, ...)cfd = accept(fd, ...)// 接收用户请求【二、内核如何接收网络包】read(cfd, ...)// 服务端处理【三、内核如何与用户进程协作】dosomething()// 给用户返回结果【四、内核如何发送网络包】send(cfd,buf,sizeof(buf),0)
}

二、内核如何接收网络包

引入:我们在应用层【客户端或服务端都可以】执行read调用后就能很方便的接收来自网络的另一端发送过来的数据,那么这些数据是如何从网卡跑到协议栈的?这里面隐藏着很多的内核组件细节工作。

思考:
1、RingBuffer到底是什么?它为什么会丢包?
2、网络相关的硬中断、软中断都是什么?
3、Linux中的ksoftirqd内核线程是干什么的?
4、为什么开启网卡多队列可以提升网络性能?
5、tcpdump工作原理?它能抓到被iptables封禁的包吗?
6、网络接收过程的CPU开销如何查看?

1、收包流程的一些核心概念

也是本篇文章的一些核心概念!

  • RingBuffer

    RingBuffer是内存中的一个特殊区域,它的内部是由【两个】环形队列组成。

    网卡在收到数据的时候以DMA的方式将包写到RingBuffer中。RingBuffer的大小和长度可以通过ethool工具查看。

    [root@k8s201 ~]# ethtool -g ens32
    Ring parameters for ens32:
    Pre-set maximums:          # 【最大值】
    RX:             4096
    RX Mini:        0
    RX Jumbo:       0
    TX:             4096
    Current hardware settings: # 【当前值】
    RX:             256
    RX Mini:        0
    RX Jumbo:       0
    TX:             256# 查看是否有RingBuffer的溢出【如果内核处理不及时会导致RingBuffer满】
    [root@k8s201 ~]# ethtool -S ens32
    NIC statistics:rx_packets: 1261tx_packets: 2051rx_bytes: 124822tx_bytes: 1369180... ...rx_fifo_errors: 189 #【不为0,说明有溢出,表明有包因RingBuffer装不下被丢弃】 tx_fifo_errors: 135... ...rx_long_byte_count: 124822rx_csum_offload_good: 1258# 可以通过ethool加大RingBuffer队列数
    [root@k8s201 ~]# ethtool -G ens32 rx 4096 tx 4096
    [root@k8s201 ~]# ethtool -g ens32
    Ring parameters for ens32:
    Pre-set maximums:
    RX:             4096
    RX Mini:        0
    RX Jumbo:       0
    TX:             4096
    Current hardware settings:
    RX:             4096
    RX Mini:        0
    RX Jumbo:       0
    TX:             4096
    
  • skb

    skb是struct_sk_buff对象的简称。它是Linux网络模块中的核心结构体,各个层用到的数据包都是存在这个结构体里的。

  • 硬中断与软中断

    在网卡将数据通过DMA放到内存中后,接着就发起硬中断,通知CPU进行处理。不过在硬中断的上下文里做的工作很少——将传过来的poll_list添加到了Per-CPU变量softnet _datapoll_list里(softnet_data中的poll_list是一个双向列表,其中的设备都带有输入帧等着被处理),接着触发软中断NET_RX_SOFTIRQ

    在软中断中对softnet_data的设备列表poll_list行遍历,执行网卡驱动提供的poll来收取网络包。处理完后会送到对应协议栈的ip_rcvudp_rcvtcp_rcv_v4等函数中。

    通过执行top命令可以查看与CPU内核相关开销信息, 其中 hi 就是CPU处理硬中断的开销, si 就是CPU处理软中断的开销。

    top - 15:25:15 up 0 min, 2 users, load average: 1.60, 0.41, 0.14
    Tasks: 168 total, 2 running, 166 sleeping, 0 stopped, 0 zombie
    %Cpu(s): 20.5 us, 7.1 sy, 0.0 ni, 72.3 id, 0.1 wa, 0.0 hi, 0.1 si, 0.0 st
    KiB Mem : 7990280 total, 6704700 free, 411120 used, 874460 buff/cache
    KiB Swap: 0 total, 0 free, 0 used. 7258852 avail Mem

    … …

    20.5% us 用户空间占用CPU百分比
    7.1% sy 内核空间占用CPU百分比
    0.0% ni 用户进程空间内改变过优先级的进程占用CPU百分比
    72.3% id 空闲CPU百分比
    0.1% wa 等待输入输出的CPU时间百分比
    0.0% hi 硬中断
    0.1% si 软中断

  • ksoftirqd

    ksoftirqd是内核线程,它包含了所有软中断处理逻辑,软中断就是在ksoftirqd内核线程中执行的。

    机器上有几个CPU核,就会创建几个ksoftirqd内核线程。

    [root@k8s201 ~]# ps -ef|grep ksoft
    root          3      2  0 15:24 ?        00:00:00 [ksoftirqd/0]
    root         14      2  0 15:24 ?        00:00:00 [ksoftirqd/1]
    root         19      2  0 15:24 ?        00:00:00 [ksoftirqd/2]
    root         24      2  0 15:24 ?        00:00:00 [ksoftirqd/3]# 查看软中断信息
    [root@k8s201 ~]# cat /proc/softirqs CPU0       CPU1       CPU2       CPU3   HI:          0          0          0          0TIMER:      30643      31141      34979      33856NET_TX:         31          1          2          1NET_RX:        843        413       5309        325BLOCK:       4573        562       2791       3707
    BLOCK_IOPOLL:          0          0          0          0TASKLET:         25          0        221          0 SCHED:      12094      12158      14858      12862 HRTIMER:          0          0          0          0 RCU:      28985      34202      33142      31919
    
  • DMA

    直接内存访问(DMA,Direct Memory Access)是一些计算机总线架构提供的功能,它能使数据从附加设备(如磁盘驱动器)直接发送到计算机主板的内存上。

  • 网卡多队列

    现在主流网卡都是支持多队列的【可以通过ethool命令查看、加大队列数】。

    每个队列都有独立的、不同的中断号【通过设置中断号对应的smp_affinity来亲和某一个CPU】。所以不同的队列再将数据接收到自己的RingBuffer后,可以分别向不同的CPU发送硬中断通知。

    ## 查看硬件中断号【一共有58个硬件中断】
    [root@k8s201 irq]# cat /proc/interrupts CPU0       CPU1       CPU2       CPU3       0:         89          0          0          0   IO-APIC-edge      timer1:         10          0          0          0   IO-APIC-edge      i80428:          1          0          0          0   IO-APIC-edge      rtc09:          0          0          0          0   IO-APIC-fasteoi   acpi12:         16          0          0          0   IO-APIC-edge      i804214:          0          0          0          0   IO-APIC-edge      ata_piix15:        108          0        427          0   IO-APIC-edge      ata_piix16:          1          0          0          0   IO-APIC-fasteoi   vmwgfx17:       9124       2679          0          0   IO-APIC-fasteoi   ioc018:        371          0      19152          0   IO-APIC-fasteoi   ens32... ...57:          0          0          0          0   PCI-MSI-edge      vmw_vmci
    ## 查看18号中断的亲和性
    [root@k8s201 irq]# cat /proc/irq/18/smp_affinity
    00000000,00000000,00000000,00000004
    # 4即100,第三位为1,即18号中断亲和第三个CPU核心——CPU2。
    

    一般来说,哪个CPU核响应的硬中断,那么该硬中断发起的软中断任务必然由这个CPU核来处理。

    如果通过执行top命令发现某个CPU核的si过高,那么很有可能你的业务上当前数据包的接收已经非常频繁了,需要通过开启网卡多队列的配置,并设置每个队列中断号上的smp_affinity,将各个队列的硬中断打散到不同的CPU上,来让其它CPU参与进来,分担这个CPU核接收包的内核工作量。

2、网络接收过程总览

收包流程:
当网卡收到数据以后,以DMA的方式把网卡收到的数据帧写到内存里,再向CPU发起一个硬中断,以通知CPU有数据到达。当CPU收到硬中断请求后,会去调用网络设备驱动注册的中断处理函数。网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU资源。ksoftirad内核线程检测到有软中断请求到达,调用执行网卡驱动提供的poll来收取网络包,收到后交由各级协议栈处理【对应协议栈包括ip_rcvudp_rcvtcp_rcv_v4等】。对于TCP包来说,会被放到用户socket的接收队列中。

  • 中断处理流程【硬中断、软中断】

  • 收包发包流程中 tcpdump能否抓到被iptables封禁的包?

    tcpdump工作在设备层,是通过虚拟协议的方式工作的。它通过调用packet_create将抓包函数以协议的形式挂到ptype_all上。

    当收包的时候,驱动中实现的igb_poll函数最终会调用到_netif_receive_skb_core这个函数会在将包送到协议栈函数(ip_rcvudp_rcvtcp_rcv_v4等)之前,将包先送到ptype_all抓包点。我们平时工作中经常会用到的tcpdump就是基于这些抓包点来工作的。

如图:tcpdump工作在网络设备层,netfilter工作在IP/ARP协议栈层【如果配置过于复杂的规则,则会消耗过多的CPU,加大网络延迟】。在收包过程中,netfilter工作在tcpdump之后,所有iptables封禁规则不会影响到tcpdump的抓包。在发包过程中,则相反,iptables封禁的包不会被tcpdump抓包。

三、内核如何与用户进程协作

引入:在协议栈接收处理完输入的网络包以后,要能通知到用户进程,让用户进程能够收到并处理这些数据。这就需要进程与内核之间的配合。主要有两类方案——同步阻塞IO、多路IO复用。

思考:
1、阻塞到底是怎么一回事?
2、同步阻塞IO都需要哪些开销?
3、多路复用epoll为什么能提高网络性能?
4、epoll也是阻塞的吗?
5、为什么redis的网络性能很突出?

1、进程的一些基础概念

  • 阻塞

    阻塞其实是进程因为等待某个事件而主动让出CPU并挂起的操作。

    分析某个技术是否阻塞时,关键是要看进程有没有放弃CPU。如果放弃了就是阻塞。如果没有放弃就是非阻塞。

  • 同步阻塞IO

    即传统的BIO,直观简单易懂,但是性能不高。

    是内核与用户进程协作的一种方式——阻塞:每个进程专门为了等待socket上数据的到来,会主动让出CPU,进入睡眠状态,然后由调度器调度下一个就绪状态的进程来执行。等到数据准备好之后,睡眠进程被唤醒,总共需要两次进程上下文切换的开销【上下文切换其实没有在做有意义的工作】。另外,一个进程同时只能等待一条socket连接,如果有很多并发,则需要很多进程,每个进程都需要大约几MB的内存开销。

    同步阻塞网络IO就是高性能网络开发路上的绊脚石!

  • IO多路复用

    是内核与用户进程协作的另外一种方式——IO多路复用:常见的方案有select、poll、epoll等。

    针对epoll,在用户进程中,通过调用epoll_wait来查看就绪链表中是否有事件到达,如果有,直接取走进行处理。处理完毕再次调用epoll_wait。 在高并发的实践中,只要活儿足够多,epoll_wait根本不会让进程阻塞。用户进程会一直干活儿,一直干活儿,直到epoll_wait里实在没活儿可干的时候才主动让出CPU【可见epoll也是阻塞:阻塞不会导致低性能,过多频繁的阻塞才会】。这就是epoll高效的核心 原因所在!

    epoll高性能最根本的原因是极大程度的减少了无用的进程上下文开销,让进程更加专注于处理网络请求。

    至于红黑树,仅仅是提高了epoll查找、添加、删除socket时的效率而已,不算epoll在高并发场景高性能的根本原因。

3、同步阻塞IO工作流程

同步阻塞方式接收网络包的整个过程分为两部分:

第一部分是我们自己的代码所在的进程,我们调用的socket()函数会进入内核态创建必要内核对象。recv()函数在进入内核态以后负责查看接收队列,以及在没有数据可处理的时候把当前进程阻塞掉,让出CPU。

第二部分是硬中断、软中断上下文(系统内核线程ksoftirqd)。在这些组件中,将包处理完后会放到socket的接收队列中。然后根据socket内核对象找到其等待队列中正在因为等待而被阻塞掉的进程,把它唤醒。

同步阻塞总体流程如下图所示。每次一个进程专门为了等一个socket上的数据就被从CPU上拿下来,然后换上另一个进程。等到数据准备好,睡眠的进程又会被唤醒,总共产生两次进程上下文切换开销。根据业界的测试,每一次切换大约花费3-5微秒,在不同的服务器上会有一点儿出入,但上下浮动不会太大。

要知道从开发者角度来看,进程上下文切换其实没有做有意义的工作。如果是网络IO密集型的应用,CPU就会被迫不停地做进程切换这种无用功。

4、epoll工作流程

与epoll相关的函数有如下三个:

  • epoll_create:创建一个epoll对象。

    用户进程调用epoll_create函数,内核会创建一个eventpoll内核对象,并把它关联到当前进程的已打开文件列表中。

    eventpoll内核对象包含三个成员:

    wq等待队列链表:软中断数据就绪时就会通过wq来找到阻塞在epoll对象的用户进程。

    rbr红黑树:管理用户进程添加进来的所有socket连接。

    rdllist就绪描述符链表:用于进程判断找出就绪连接,而不用遍历rbr红黑树。

  • epoll_ctl:向epoll对象添加要管理的连接。

    使用epoll_ctl注册每一个socket时,内核会做如下三件事:
    1、分配一个红黑树节点对象epitem
    2、将等待事件添加到socket的等待队列中,其回调函数是ep_poll_callback
    3、将epitem插入epoll对象的红黑树。

    这里为什么要用红黑树?→为了让epoll在查找效率、插入效率、内存开销等多个方面比较平衡。

  • epoll_wait: 等待其管理的连接上的IO事件。

    检查就绪队列链表有没有数据,有数据就返回,没有数据就创建一个等待队列项插入到等待队列,然后把自己阻塞,让出CPU,完事。

    复用进程,一个进程可以等待多个连接。当socket有数据到来时就直接处理,极大减少阻塞开销。

学习目的:要熟悉epoll的内部实现方式,理解它的红黑树和就绪队列,知道它高性能的根本原因是让进程大部分时间都在处理用户工作,而不是频繁地切换上下文。

总结一下,epoll相关的函数里内核运行环境也分两部分:

第一部分:用户进程内核态。调用epoll_wait等函数时会将进程陷入内核态来执行。这部分代码负责查看接收队列,以及负责把当前进程阻塞掉,让出CPU。

第二部分:硬、软中断上下文。在这些组件中,将包从网卡接收过来进行处理,然后放到socket的接收队列。对于epoll来说,再找到socket关联的epitem,并把它添加到epoll对象的就绪链表中。这个时候再捎带检查一下epoll上是否有被阻塞的进程,如果有唤醒它。

其实在实践中,只要活儿足够多,epoll_wait根本不会让进程阻塞。用户进程会一直干活儿,一直干活儿,直到epoll_wait里实在没活儿可干的时候才主动让出CPU。这就是epoll高效的核心原因所在!

Redis 在网络IO性能上表现非常突出,单进程的服务器在极限情况下可以达到10万的QPS。

Redis 的主要业务逻辑就是在本机内存上的数据结构的读写,几乎没有网络IO和磁盘IO,单个请求处理起来很快。所以它把主服务端程序干脆就做成了单进程的,这样省去了多进程之间协作的负担,也更大程度减少了进程切换。进程主要的工作过程就是调用epoll_wait等待事件,有了事件以后处理,处理完之后再调用epoll_wait。一直工作,一直工作,直到实在没有请求需要处理,或者进程时间片到的时候才让出CPU。将工作效率发挥到了极致!

四、内核如何发送网络包

引入:调用send()之后,内核是怎样把网络数据包发送出去的?

思考:
1、查看/proc/softirqs为什么NET_RX要比NET_TX大得多?
2、发送网络数据的时候都涉及哪些内存拷贝操作?
3、零拷贝是什么?
4、为什么kafka的网络性能很突出?

1、发包流程的一些基础概念

  • sy和si

    sy表示用户进程的系统态时间,si表示CPU开销软中断消耗的部分。查看CPU开销情况这两个指标都要考虑。
    hi表示CPU开销硬中断消耗的部分。

  • NET_RS和NET_TX

    网络发送/接收数据时,通过硬中断[定义好的]来通知CPU。NET_RX_SOFTIRQ和NET_TX_SOFTIRQ,R表示receive,T表示transmit。

  • 邻居子系统

    邻居子系统是位于网络层和数据链路层中间的一个系统,其作用是为网络层提供一个下层的封装,让网络层不必关心下层的地址信息,让下层来决定最终发送到哪个MAC地址。

  • 零拷贝、sendfile系统调用

    如果想把本机的一个文件通过网络发送出去,我们的做法之一就是先用read系统调用把文件读取到内存,然后再调用send把文件发送出去。

    假设数据之前从来没有读取过,那么read 硬盘上的数据需要经过两次拷贝才能到用户进程的内存。第一次是从硬盘DMA到Page Cache,第二次是从Page Cache拷贝到用户内存,这是read过程。结合send系统调用,那么read + send系统调用发送一个文件出去数据需要经过的拷贝过程如下图所示。

    如界要发送的数据量比较大,那需要花费不少的时间在大量的数据拷贝上。sendfile就是内核提供的一个可用来减少发送文件时拷贝开销的一个技术方案——零拷贝。在sendfile系统调用里,数据不需要拷贝的用户空间,在内核态就能完成发送处理,这就显著减少了需要拷贝的次数。

图中包括两种减少内存拷贝的方法:mmap和sendfile。其中mmap见文章第七章。

kafka出类拔萃的性能的重要原因之一就是采用了sendfile系统调用来发送网络数据包,减少了内核态和用户态之间的的频繁数据拷贝次数。

2、网络发送过程总览

发送网络数据概览:

从图中可以看到,数据被拷贝到内核态,然后经过协议栈处理后进入RingBuffer。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知CPU【虽然是发送数据,但最终触发的软中断是NET_RX而不是NET_TX;另外,对于接收【读】来说,都是经过NET_RX软中断,对于发送来说,绝大部分工作都是在用户进程内核态处理了,只有系统态配额用尽才让NET_TX软中断上】,最后清理RingBuffer

3、发包流程的内存拷贝操作

这里的内存拷贝,只特指待发送数据的内存拷贝。

第一次拷贝操作是在内核申请完skb之后,这时候会将用户传递进来的buffer里的数据内容都拷贝到skb。如果要发送的数据量比较大,这个拷贝操作开销还是不小的。

第二次拷贝操作是从传输层进入网络层的时候,每一个skb都会被克隆出来一个新的副本。
目的是保存原始的skb,当网络对方没有返回ACK的时候,还可以重新发送,以实现TCP中要求的可靠传输。不过这次只是浅拷贝,只拷贝skb描述符本身,所指向的数据还是复用的。

第三次拷贝操作不是必需的,只有当IP层发现skb大于MTU时才需要进行。此时会再申请额外的skb,并将原来的skb拷贝为多个小的skb。

这里插个题外话,大家在谈论网络性能优化中经常听到“零拷贝”,我觉得这个词有一点点夸张的成分。TCP为了保证可靠性,第二次的拷贝根本就没法省。如果包大于MTU,分片时的拷贝同样避免不了。

4、数据从用户进程到网卡的详细过程

五、深入理解本机网络IO

引入:前面网络的接收和发送流程已经闭环。但是还有一种非常常见的特殊情况:本机网络的接收和发送是怎么样的?与跨机网络通信过程有什么区别?

思考:
1、127.0.0.1本机网络IO需要经过网卡吗?
2、访问本机服务使用127.0.0.1能比使用本地IP更快吗?
3、数据包在内核是什么走向?与外网发送相比流程上有什么差别吗?

1、跨机网络通信

跨机发送过程和接收过程分别详细见第四章【发送网络数据】和第二章【接收网络数据】。

2、本机网络通信

本机网络通信和跨机网络通信流程大致相同,不同点有两处:路由和驱动程序。

路由:路由表分为两类,local和main两个路由表,先查询local后查询main。local表记录的是所有本机路由,也包括本机ip。

驱动程序:执行的是纯软件性质的虚拟’‘驱动’‘设备 loopback,并非真正的物理设备驱动。

[root@k8s201 ~]# ip route list table local
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
broadcast 172.17.0.0 dev docker0 proto kernel scope link src 172.17.0.1
local 172.17.0.1 dev docker0 proto kernel scope host src 172.17.0.1
broadcast 172.17.255.255 dev docker0 proto kernel scope link src 172.17.0.1
broadcast 192.168.168.0 dev ens32 proto kernel scope link src 192.168.168.201
local 192.168.168.201 dev ens32 proto kernel scope host src 192.168.168.201
broadcast 192.168.168.255 dev ens32 proto kernel scope link src 192.168.168.201 [root@k8s201 ~]# ip route list table main
default via 192.168.168.2 dev ens32 proto static metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.168.0/24 dev ens32 proto kernel scope link src 192.168.168.201 metric 100

总的来说,本机网络IO和跨机网络IO比较起来,确实是节约了驱动上的一些开销。发送数据不需要进 RingBuffer的驱动队列,直接把 skb 传给接收协议栈(经过软中断)。但是在内核的其他组件上,可是一点儿都没少,系统调用、协议栈(传输层、网络层等)、设备子系统整个走了一遍,连“驱动”程序都走了(里然对于回环设备来说只是一个纯软件的建拔出来的东西)。所以即使是本机网络IO,切忌误以为没啥开销就滥用。
【扩充知识点:如果想要绕开协议找的开销,需要动用eBPF技术。使用eBPF的sockmap和sk redirect可以达到真正不走协议栈的目的】

即使用本机IP也不会走网卡,它和127.0.0.1没有差别,都是走虚拟的环回设备IO。这是因为内核在设置IP的时候,把所有的本机IP都初始化到local路由由表里了,类型固定是RTN_LOCAL。在后面的路由项选择的时候发现类型是RTN_LOCAL就会选择环回设备IO。

六、深入理解TCP连接

引入:目前的互联网应用绝大部分都是运行在TCP之上的,它是当今互联网的基石。我们需要深度理解TCP的建立流程。socket系列对象的创建?内核的内存管理?

思考:
1、为什么服务端程序需要先listen一下?
2、半连接队列和全连接队列的长度如何确定?这些队列满了怎么办?
3、一个客户端端口可以同时用在两条连接上吗?
4、新连接的socket内核对象是什么时候建立的?
5、建立一条TCP连接需要消耗多长时间?一条空的TCP连接消耗多少内存?
6、内核是如何管理内存的?
7、客户端最多能支持多少网络连接?服务端最多能支持多少网络连接?

1、TCP连接建立过程

TCP的三次握手在内核的实现中,并不只是简单的状态流转,还包括端口选择、半连接队列、syncookie、全连接队列、重传计时等关键操作。

建立连接前,需要先进行step1的准备工作。

  • Step1、服务端调用listen()函数进入监听状态。listen最主要的工作就是申请和初始化接收队列,包括全连接队列(链表)和半连接队列(哈希表)。它们是三次握手中非常重要的两个数据结构。
  • Step2、客户端调用connect()函数,把本地socket状态设置为TCP_SYN_SENT,选择一个可用端口,接着发出SYN握手请求并启动重传定时器。【客户端在connect时创建本地socket对象】
  • Step3、第一次握手:服务端接收到SYN请求后先判断半连接队列再判断全连接队列是否满了,如果满了则可能直接丢弃请求;如果没满则发出同步确认SYN_ACK,并申请request_sockt添加到半连接队列,同时也启动重传定时器。
  • Step4、第二次握手:客户端接收到syn_ack时清除调用connect时设置的重传定时器,把当前socket状态设置为ESTABLISHED,开启保活计时器后发出第三次握手的ack确认。
  • Step5、第三次握手:服务端响应第三次握手所做的工作是把当前半连接对象删除,创建新的sock后加入全连接队列,最后设置socket状态ESTABLISHED。【服务端在第三次握手结束时创建新sock对象】
  • Step6、accept()的重点工作就是从已经建立好的全连接队列中取出一个socket返回给用户进程。

socket系统调用完毕之后,在内核中就申请了配套的一组数据结构内核对象。这些内核对象互相保留着和其它内核对象的关联关系。所有的网络相关操作,包括数据发送和接收等,都是以这些数据结构为基础来进行的。

TCP连接本质上是客户端和服务端分别在内存上维护的一对socket内核对象。它们都在各自内核对象上记录了双方的IP对、端口对【四元组】,通过这个在通信时找到对方。

建立通信链路:

客户端:当客户端要与服务端通信,客户端自己首先要创建一个 Socket 实例,操作系统将为这个 Socket 实例分配一个没有被使用的本地端口号,并创建一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭。在创建 Socket 实例的构造函数正确返回之前,将要进行 TCP 的三次握手协议,TCP 握手协议完成后,Socket 实例对象将创建完成,否则将抛出 IOException 错误。

服务端:与之对应的服务端将创建一个 ServerSocket 实例,ServerSocket 创建比较简单只要指定的端口号没有被占用,一般实例创建都会成功,同时操作系统也会为 ServerSocket 实例创建一个底层数据结构,这个数据结构中包含指定监听的端口号和包含监听地址的通配符,通常情况下都是“*”即监听所有地址。之后当调用 accept() 方法时,将进入阻塞状态,等待客户端的请求。当一个新的请求到来时,将为这个连接创建一个新的套接字数据结构,该套接字数据的信息包含的地址和端口信息正是请求源地址和端口。这个新创建的数据结构将会关联到 ServerSocket 实例的一个未完成的连接数据结构列表中,注意这时服务端与之对应的 Socket 实例并没有完成创建,而要等到与客户端的三次握手完成后,这个服务端的 Socket 实例才会返回,并将这个 Socket 实例对应的数据结构从未完成列表中移到已完成列表中。所以 ServerSocket 所关联的列表中每个数据结构,都代表与一个客户端的建立的 TCP 连接。

2、Linux内存管理

内核是整台Linux服务器的基石,它的内存管理方案必须足够优秀,否则将直接影响整台服务器的稳定性。

内核采用SLAB的方式来管理内存,如下图所示,总共分成四步:

  • 1、把所有内存条和CPU进行分组,组成node

    每一个CPU以及和它直连的内存条组成一个node节点。CPU和内存构成NUMA架构。

    一般不建议开启numa架构,倡导关闭numa,并设置swapness=0,以降低使用swap的可能性。

    尽量避免出现swap现象:每颗cpu对应的内存条使用不均匀的情况下可能出现swap,这会降低性能。

  • 2、把每一个node划分成多个zone

    每个node会划分为若干zone。其中node0必然包含:
    ZONE_DMA:地址段最低,供IO设备DMA访问使用。
    ZONE_DMA32:64位系统下有效,用于支持32位地址总线的DMA设备。
    ZONE_NORMAL:除了前两个,其它都属于ZONE_NORMAL区域。

  • 3、每个zone下都用伙伴系统来管理空闲页面。

    每个zone下都包含了许许多多个Page【每个Page页面大小是4KB】。这些Page页面由伙伴系统来管理。

    伙伴系统将Page空闲页面按照尺寸4KB、8KB、16KB、…、4MB分为11个数组。

    伙伴系统中的伙伴是指两个内存块,大小相同、地址连续,同属于一个大块区域。

    基于伙伴系统的内存分配【_alloc_pages】中,有可能需要将大块内存拆分成两个小伙伴。在释放中,可能会将两个小伙伴合并再次组成更大块的连续内存

  • 4、提供slab分配器来管理各种内核对象。

    在内核运行中实际使用的对象中,多大尺寸的对象都有。有的对象1KB左右,有的只有几百、几十字节。如果直接分配一个4KB的页,就太浪费了。于是在伙伴系统之上,内核又给自己搞了一个专用slab内存分配器

前三步是基础模块,为应用程序分配内存时的请求调页组件也能够用到。但第四步是内核专用的。每个slab缓存都是用来存储固定大小,甚至是特定的一种内核对象。这样当一个对象释放内存后,另一个同类对象可以直接使用这块内存,几乎没有任何碎片。极大地提高了分配效率,同时也降低了碎片率。

通过查看/proc/slabinfo可以看到所有的kmem_cache【它们是Linux初始化或者运行过程中分配出来的,它们有的是专用的,有的是通用的】。通过slabtop命令从大到小按占用内存进行排列,这用来分析内核内存开销非常方便。

# 查看每个node的情况:CPU和内存
[root@k8s201 ~]# dmidecode
[root@k8s201 ~]# numactl --hardware
# 查看zone信息
[root@k8s201 ~]# cat /proc/zoneinfo
[root@k8s201 ~]# cat /proc/zoneinfo
Node 0, zone      DMApages free     3968... ...pagesetscpu: 0count: 0high:  0batch: 1vm stats threshold: 6cpu: 1count: 0high:  0batch: 1vm stats threshold: 6cpu: 2count: 0high:  0batch: 1vm stats threshold: 6cpu: 3count: 0high:  0batch: 1vm stats threshold: 6all_unreclaimable: 0start_pfn:         1inactive_ratio:    1
Node 0, zone    DMA32pages free     610393... ...pagesetscpu: 0count: 107high:  186batch: 31vm stats threshold: 36cpu: 1count: 181high:  186batch: 31vm stats threshold: 36cpu: 2count: 129high:  186batch: 31vm stats threshold: 36cpu: 3count: 142high:  186batch: 31vm stats threshold: 36all_unreclaimable: 0start_pfn:         4096inactive_ratio:    4
# 查看各个尺寸的可用连续内存块数量
[root@k8s201 ~]# cat /proc/pagetypeinfo
Page block order: 9
Pages per block:  512Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10
Node    0, zone      DMA, type    Unmovable      0      0      0      0      2      1      1      0      1      0      0
Node    0, zone      DMA, type  Reclaimable      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone      DMA, type      Movable      0      0      0      0      0      0      0      0      0      1      3
Node    0, zone      DMA, type      Reserve      0      0      0      0      0      0      0      0      0      0      0
... ...Number of blocks type     Unmovable  Reclaimable      Movable      Reserve          CMA      Isolate
Node 0, zone      DMA            1            0            7            0            0            0
Node 0, zone    DMA32           24            8         1496            0            0            0
Node 0, zone   Normal          846           14         1700            0            0            0
# 查看slab信息
[root@k8s201 ~]# cat /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
ovl_inode            192    192    680   48    8 : tunables    0    0    0 : slabdata      4      4      0
nf_conntrack_ffffffffa5111640    204    204    320   51    4 : tunables    0    0    0 : slabdata      4      4      0
xfs_dqtrx              0      0    528   62    8 : tunables    0    0    0 : slabdata      0      0      0
xfs_dquot              0      0    488   67    8 : tunables    0    0    0 : slabdata      0      0      0
xfs_ili             5100   5520    168   48    2 : tunables    0    0    0 : slabdata    115    115      0
xfs_inode           7248   7718    960   34    8 : tunables    0    0    0 : slabdata    227    227      0
... ...
# 通过slabtop命令从大到小按占用内存进行排列
[root@k8s201 ~]# slabtopActive / Total Objects (% used)    : 339829 / 347134 (97.9%)Active / Total Slabs (% used)      : 6649 / 6649 (100.0%)Active / Total Caches (% used)     : 70 / 97 (72.2%)Active / Total Size (% used)       : 99997.71K / 102412.47K (97.6%)Minimum / Average / Maximum Object : 0.01K / 0.29K / 8.00KOBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME                   46988  46988 100%    0.12K    691       68      5528K kernfs_node_cache42756  42077  98%    0.19K   1018       42      8144K dentry29056  27749  95%    0.06K    454       64      1816K kmalloc-6426826  26826 100%    0.04K    263      102      1052K selinux_inode_security26290  26122  99%    0.58K    478       55     15296K inode_cache17808  17292  97%    0.19K    424       42      3392K kmalloc-19216640  15884  95%    0.25K    260       64      4160K kmalloc-25614976  14333  95%    0.50K    234       64      7488K kmalloc-51213272  13272 100%    0.09K    316       42      1264K kmalloc-9612032  12032 100%    0.02K     47      256       188K kmalloc-1610920  10920 100%    0.07K    195       56       780K avc_node10752  10752 100%    0.01K     21      512        84K kmalloc-8... ...

3、TCP连接占多大内存,机器最多能支持多少TCP连接

举例:一个Redis实例上就出现了6000条长连接。假设连接上绝大部分时间都是空用的,也就是说可以假设没有发送缓存区、接收缓存区的开销,那么二个socket大约需要如下几个内核对象:

  • struct socket_alloc,大小约为0.62KB,slab缓存名是sock_inode_cache
  • stuct tcp_sock,大小约为1.94KB,slab缓存名是tcp
  • struct dentry,大小约为0.19KB,slab缓存名是dentry
  • struct file,大小约为0.25KB,slab缓存名是flip

加上slab上多少会存在一点儿碎片无法使用,这组内核对象的大小大约总共是3.3KB左右。粗算6000条ESTABLISH状态的空长连接在内存上的开销也就是6000×3.3KB≈20MB而已。这些连接对于服务器来说是小case。对于CPU的开销:只要没有数据包的发送和接收处理,是不需要消耗CPU的。

长连接上在没有数据传输的情况下,只有极少量的保活包传输,CPU开销可以忽略不计。

所有大多场景下,建议用长连接代替短连接。

TPC连接四元组中任意一个元素发生改变,就代表是一条完全不同的连接。所以一个服务器理论上可以建立2^32(IP数)X 2^16(端口数)= 两百多万亿。现代一台服务器都有上百G内存,如果是针对空闲TCP连接【仅仅占用内存3KB多点】,单机千万(C10000K)都不是问题。然而服务器开销的大头往往不是连接本身,而是在每条连接上的数据收发以及业务逻辑处理

如果项目确实需要百万条TCP连接,那么一般来说还需要高效的IO事件管理——epoll技术【第三章节】。

  • 开放场景问题:做一个支持1亿用户的长连接推送产品需要多少台机器?

对于长连接的推送模块这种服务来说,给客户端发送数据只是偶尔的,一般一天也就顶多发送一次两次。绝大部分情况下TCP连接都会空闲,CPU开销可以忽路。

再来考虑内存,假设服务器内存是128GB的。那么一台服务器可以考虑支持500万条的并发。这样会消耗大约不到20GB的内存用来保存这500万条连接对应的socket。 还剩下100GB以上的内存,用来应对接收、发送缓存区等其它的开销足够了。所以,1亿用户,仅仅需要20台机器就差不多够用了!

4、日常工作常见报错【实操[重要]】

a、端口不足

如果端口不足,会导致connect系统调用的时候过多的执行自旋等待与哈希查找,会引起CPU开销上涨,严重时会耗尽CPU,影响用户业务逻辑的执行。这种问题一般从以下三个方面入手:
1、通过调整ip_local_port_range来尽量加大端口范围。
2、尽量复用连接,使用长连接来削减频繁的握手处理。
3、开启tcp_tw_reusetcp_tw_recycle,有用但是不太推荐。

【Cannot assign request address】

一条TCP连接由一个四元组构成:Server IP、Server Port、Client IP、Client Port,只要一个元素不同,就是一个不同的连接。在连接建立前,前三个元素是已经确定的,第四个元素需要动态的选择出来。

客户端调用connect发起连接选择可用端口时,会在net.ipv4.ip_local_port_range设置的可用端口范围把整个可用端口范围遍历一遍,直到找到可用端口位置。如果遍历完都没有找到合适可用的端口,就返回"Cannot assign request address"。一般来说,很快就可以找到可用端口,但假如端口被消耗掉很多已经不充足了,那么就会循环遍历执行很多遍,会非常消耗CPU。

这时查看net.ipv4.ip_local_port_range中设置的可用端口范围是不是太小了,如果太小则调大些。

vim /etc/sysctl.conf
# 修改端口范围
net.ipv4.ip_local_port_range = 5000 65000
# 设置最大TIME_WAIT数量
net.ipv4.tcp_max_tw_buckets = 10000
sysctl -p

b、半/全连接队列满

服务端在第一次握手时,以下两种情况可能会丢包:
1、半连接队列满,且tcp_syncookies为0。[所以建议开启参数tcp_syncookies]
2、全连接队列满,且有未完成的半连接请求。

这时客户端会发起重传,重传的时间间隔是2s、4s、8s、… 重传的次数受内核参数net.ipv4.tcp_syn_retries影响。

服务端在第三次握手时,如果全连接队列满,仍将发送丢包。

一般来说,建立一条连接大约耗时1s以内[几十毫秒],但是如果丢包,丢包导致的重试都是秒级以上的响应耗时,如果重试多次,Nginx就可能直接包访问超时了。

全连接队列满判断:通过执行 netstat -s输出 xx times the listen queue of a socket overflowed 如果查看到数字有变化,那么服务器就发送全连接队列溢出了。
半连接队列满判断:如果确保tcp_syncookies为1,那么就不会出现半连接队列溢出的情况。通过查看当前SYN_RECV状态的连接数量来判断是否存在半连接队列溢出。netstat -antp | grep SYN_RECV | wc -l

# 查看全连接队列是否溢出
netstat -s | grep overflowed
141160 times the listen queue of a socket overflowed
# 查看半连接队列是否溢出
netstat -antp | grep SYN_RECV | wc -l

针对丢包可以从以下五个方面入手

方法1、打开syncookie

现代的Linux,可以通过打开syncookie来防止过多的请求打满半连接队列,包括SYN Flood攻击,来解决服务端因为半连接队列满而发生的丢包问题。

方法2、尽快调用accept

第三次握手完毕时,socket内核对象就创建好了。应用程序应该尽快在握手成功后通过accept把新连接对象取走。不要因忙于处理其它业务逻辑而导致全连接队列满。不过这条基本上都能遵守。

方法3、加大连接队列长度

半连接队列长度:min(backlog,somaxconn,tcp_max_syn_backlog)+1再向上取整到2的N次幂,最小不小于16
全连接队列长度:min(backlog,net.core.somaxconn)

所以针对半/全连接队列满的情况,需要调整以上一到多个参数来达到加大队列长度的目的。以减少TCP握手异常概率。

方法4、尽早拒绝【connection reset by peer】

加大队列长度后可以容忍偶发的队列溢出的情况。但是如果仍有长时间处理不过来的情况,干脆直接报错,不要让客户端超时等待。例如将MySQL、Redis等服务器的内核参数tcp_abort_on_overflow设置为1。如果队列满了,直接发reset指令给客户端,客户端收到错误"connection reset by peer"的提示,就知道服务端现在队列满了。牺牲掉一个客户端的访问请求,总比把整个网站搞崩了要强的多。

方法5、尽量减少TCP连接的次数

如果以上法子都没能解决问题,那么应该思考是否可以用长连接代替短连接,减少过于频繁的三次握手。这个方法不但能降低握手出现问题的概率,还能减少三次握手的各种内存、CPU、时间上的开销,对提升性能大有帮助。

c、【TIME_WAIT】连接过多怎么办?

从内存角度来看,一条TIME_WAIT状态的TCP连接仅仅占用0.4KB左右的内存而已。端口占用问题也只会在连接同一个server的时候才需要考虑【网络连接四元组】。所以不必担心。如果想解决,有三种法子:
1、可以考虑使用tcp_max_tw_buckets来限制TIME_WAIT连接总数。
2、打开tcp_tw_recycletcp_tw_reuse来快速回收端口。
3、干脆直接用长连接代替频繁的短连接。

# 如下,有31948条状态为TIME_WAIT的TCP连接
sudo netstat -antp | grep TIME_WAIT | wc -l
31948vim /etc/sysctl.conf
# 设置tcp_max_tw_buckets最大TIME_WAIT数量
net.ipv4.tcp_max_tw_buckets = 10000
# 快速回收端口
net.ipv4.tcp_timestamps = 1  # 需要保证开启
tcp_tw_recycle = 1
tcp_tw_reuse = 1
sysctl -p

d、【Too many open files】

Linux每打开一个文件(包括socket)都需要消耗一定的内存资源。为了避免个别进程不受控制的打开过多文件而让整个服务器崩溃,需要限制能打开的文件数。如果超过这个内核限制,则产生“Too many open files”报错。

Linux上能打开多少文件,有两种限制:

  • 进程级别的,限制的是单个进程上可以打开的文件数。
  • 系统级别的,整个系统上可以打开的文件数。
# 文件打开数量设置举例
vim /etc/sysctl.conf
# 系统级别的设置110万
fs.file-max=1100000
# 进程级别也设置110万,这个值要保证比hard nofile。否则后果严重:用户无法登录。
fs.nr_open=1100000vim /etc/security/limits.conf
# 用户进程级别都设置为100万【必须都设置,按照两者最小生效】
* soft nofile 1000000
* hard nofile 1000000

七、网络性能优化建议【重要】

引入:按网络建立的流程给出网络性能优化的一些建议。
请注意:每一种优化手段都有它适用或者不适用的场景。

1、网络请求优化

  • 建议1、尽量减少不必要的网络IO

    描述:不要为了简便,疯狂的通过本机网络IO来调用各种SDK,即使是本机网络IO也要尽可能减少。

  • 建议2、尽量合并网络请求

    描述:尽可能把多次的网络请求合并到一次,这样既节约了双端的CPU开销,也能降低多次RTT导致的耗时。
    即能批量处理就批量处理。

  • 建议3、调用者与被调用机器尽可能部署的近一些。

    描述:尽量把每个机房内部的数据请求都在本地机房解决,减少跨地网络传输。比如跨AZ高可用的网络延迟问题。

  • 建议4、内网调用不要用外网域名,尽量使用内网域名。

    描述:一般来说,每个外网服务都会配置对应的内网域名。不用外网域名访问的原因有3:
    1、外网接口慢。很明显,内网访问快于外网访问。
    2、带宽成本高。外网是需要带宽成本的,而内网不需要。
    3、NAT单点瓶颈。请求外网一般都需要经过NAT服务器。一个公司的NAT服务器就那么几台,它很容易成为瓶颈。NAT机器挂了,就无法通过外网域名访问,导致故障率增加。

2、接收过程优化

  • 建议1、调整网络RingBuffer大小。

    描述:网线中的数据包到达网卡后,第一站就是RingBuffer。网卡在RingBuffer中寻找可用内存位置后,DMA引擎就会把数据直接DMA到RingBuffer内存中去。

    在Linux的整个网络栈中,RingBuffer扮演一个任务的收发中转站的角色

    对于接收过程来说,网卡负责把数据写入RingBuffer,ksoftirqd内核线程负责从中取出数据。只要ksoftirqd工作足够快,就不会出现问题。但是如果RingBuffer突然被写入了很多数据包,ksoftirqd处理不过来了,RingBuffer可能瞬间被填满,后面再进来包就会被丢掉,产生丢包现象。这时需要调整RingBuffer的大小,分配一个更大一点的“中转站”。

    优点:可以解决偶发的瞬时丢包。
    缺点:排队的包过多,网络包的处理延时会加大。

    # 查看是否有丢包
    ethtool -S eth0
    ... ...
    fx_fifo_errors:0
    tx_fifo_errors:0
    ... ...
    # 查看RingBuffer长度大小
    ethtool -g eth0
    # 设置RingBuffer长度为4096
    ethtool -G eth0 rx 4096 tx 4096
    
  • 建议2、多队列网卡RSS调优。【核心

    每个队列都有自己的中断号,有独立的中断号就可以独立向某个CPU核心发起硬中断请求。让对应CPU来poll包。

    描述:通过将不同队列的CPU的亲和性打散到多个CPU核上。就可以让多核同时并行处理接收到的包。这个特性叫RSS【Receive Side Scaling,接收端扩展】。这是加快Linux内核处理网络包的速度的一个非常有用的办法。直接简单加大队列数比加大RingBuffer更加有用。因为加大RingBuffer只是提供更大的空间让网络包继续排队,而加大队列数则能让包更早的被内核处理

    # 查看中断信息 如下:27号中断发生1189562次中断都在CPU3上,这是因为亲和性。
    cat /proc/interruptsCPU0     CPU1     CPU2      CPU3
    ... ...
    27:  351       0         0      1189562
    28:  0         1296842   0      0
    ... ...
    # 查看队列的亲和关系  8的二进制1000  第四位是1,表示CPU3
    cat /proc/irq/27/sam_affinity
    8
    # 查看队列数
    ethool -l eth0
    # 设置队列数
    ethool -L eth0 combined 32
    # 队列中断号和CPU的亲和性由irqbalance服务自动维护。
    ps -ef|grep irqbalance
    
  • 建议3、硬中断合并。

    描述:CPU在做一件新的事之前,要加载该进程的地址空间,装入进程代码,读取进程数据,各级cache要慢慢预热。因此如果能适当降低中断的频率,多攒几个包再一起发出中断,对提升CPU的整体工作效率是有帮助的。所以网卡允许我们对硬中断进行合并。
    【攒一堆数据包后再通知CPU来处理】

    优点:减少中断数量使得Linux整体网络包吞吐更高。
    缺点:一些网络包的处理延时可能会加大。

    # 查看硬中断合并的设置
    ethtool -c eht0
    ... ...
    Adaptive RX: off TX: off   # 自适应中断合并
    ... ...
    rx-usecs:1        # 经过rx-usecs时间后,会发生一次RX中断
    rx-frames:0       # 累积rx-frames个帧后,会发生一次RX中断
    rx-usecs-irq:0
    rx-frames-irq:0
    ... ...
    # 开启硬中断合并的设置
    ethtool -C eht0 adaptive-rx on
    
  • 建议4、软中断budget调整。

    描述:类似硬中断合并。软中断也可以设置为多处理一些包之后,再让出CPU。
    【处理完一堆数据包后再让出CPU】

    # 查看。如下:内核ksoftirqd在一次处理完300个包后,处理够后就会让出CPU。
    sysctl -a | grep budget
    net.core.netdev_budget = 300
    # 设置软中断budget为600个包
    vim /etc/sysctl.conf
    net.core.netdev_budget = 600
    
  • 建议5、接收处理合并。

    描述:硬中断合并是指攒一堆数据包后再通知CPU来处理,不过数据包仍然是分开的。
    LRO(Large Receive Offload)GRO(Generic Receive Offload)能把数据包合并后再往上层传递。
    其中LRO是在网卡上就把数据包合并的事做完了。GRo是在内核源码中用软件的方式实现数据包合并。

    # 查看是否开启LRO和GRO
    ethtool -k eth0
    ... ...
    generic-receive-offload:off
    large-receive-offload:on
    ... ...
    # 开启LRO和GRO
    ethtool -K eth0 gro on
    ethtool -K eth0 lro on
    

3、发送过程优化

  • 建议1、控制数据包大小。

    描述:如果可能,在应用中可以尝试将数据的大小控制在一个MTU中 [实际的MTU大小通过MTU发现机制来确定,在以太网中为1500字节]。通过这种方式可以优化网络性能。【早期QQ就应用了这个技巧】

    如果数据包的尺寸大于MTU,就会执行分片。分片会带来两个问题:
    1、需要进行额外的切分处理,分片过程多了一次内存拷贝,有额外性能开销。
    2、只要有一个分片丢失,整个包都要重传。
    所以避免分片既杜绝了分片开销,也大大降低了重传率

  • 建议2、推迟分片。

    描述:如果开启TSO(TCP Segementation Offload)GSO(Generic Segementation Offload)。就可以把分片的过程推迟到更下面的设备层去做。

    # 查看是否开启TSO和GSO
    ethtool -k eth0
    ... ...
    tcp-segementation-offload:off
    generic-segementatio-offload:on
    ... ...
    # 开启TSO和GSO
    ethtool -K eth0 tso on
    ethtool -K eth0 gso on
    
  • 建议3、尽可能减少内存拷贝。【核心

    描述:将文件从一个机器发送到另外一个机器,传统的做法是先调用read把文件读出来,再调用write吧数据写出去。这样需要频繁的在内核态内存和用户态内存之间拷贝。

    如下图,有两种减少内存拷贝的方法:
    方法一:mmap技术:使用mmap系统调用,映射进来的这段地址空间内存在用户态和内核态都是可以使用的,这样就可以节约一次从内核态到用户态的拷贝过程。不过mmap系统调用的开销并没有减少,还是发生两次内核态和用户态的上下文切换
    方法二:sendfile技术。如果只是想把文件发送出去,并不关心它的内容则可以使用sendfile系统调用。它将读取文件和发送文件合并起来了,将系统调用的开销也节省了一次。再配合网卡支持的DMA功能,可以直接从PageCache缓存区中DMA拷贝到网卡中,这样就将绝大部分的CPU拷贝开销都省去了。如下图

  • 建议4、多队列网卡调优

    描述:可以通过设置,为CPU指定要发送的队列。这样做的好处:
    1、更少的CPU争用同一个队列,大大减少设备队列上锁的冲突。如果能为每个CPU都配置独立的队列,则完全可以消除队列锁的开销
    2、CPU和发送队列一对一绑定以后,能提高传输结构的局部性,从而进一步提高效率。——局部性原理。

  • 建议5、使用eBPF绕开协议栈的本机网络IO

    描述:如果业务中充斥着大量的本机网络IO,则可以使用eBOF的sockmap和sk redirect绕过TCP/IP协议栈,而直接发送给接收端的socket。

4、内核与进程协作优化

  • 建议1尽量少用recvform等进程阻塞的方式【核心】

    描述:使用了recvfrom阻塞方式来接收socket上的数据时,每次一个进程专门为了等一个socket上的数据就被从CPU上拿下来,然后再换上另一个进程。等到数据准备好了,睡眠的进程又会被唤醒。总共两次进程上下文切换开销。如果服务器上有大量的用户请求需要处理,那就需要有很多的进程存在,而且不停地切换来切换去。这样做的缺点有如下这些:

    • 因为每个进程只能同时等待一条连接,所以需要大量的进程。
    • 进程之间互相切换的时候需要消耗很多CPU周期,一次切换大约是3-5微秒左右。
    • 频繁的切换导致L1、L2、L3等高速缓存的效果大打折扣。

    我们可能以为这种网络IO模型很少见了,但其实在很多传统的客户端SDK中,此如MySQL、Redis和Kafka仍然沿用了这种方式。

  • 建议2、使用成熟的网络库。

    描述:epoll可以高效的管理海量的socket连接。而在服务端,有各种成熟的网络库可以使用,比如:Java中的Netty、Golang中的net包、PHP中的Swoodle、C++中的Sogou Workflow都对epoll进行了不同程度的封装。

  • 建议3、使用Kernel-ByPass新技术。

    描述:如果各种优化措施都做了还是达不到网络性能要求,那么终极优化大招——Kernel-ByPass技术可以绕开内核协议栈,在用户态就能实现网络包的收发。这样不大绕开了繁杂的内核协议栈的处理,还避免了内核态/用户态之间的频繁拷贝和切换,将性能发挥到极致。

    还有写新技术:Solarflare软硬件方案、DPDK等等。

5、握手挥手过程优化

  • 建议1、配置充足的端口范围。

    描述:不要等到端口用尽报错了才开始加大端口范围,一开始就应该设置一个比较充足的范围。

    vim /etc/sysctl.conf
    # 修改端口范围
    net.ipv4.ip_local_port_range = 1000 65000
    # 使得配置生效
    sysctl -p
    

    如果端口仍然不够用,可以考虑开启端口reuse和recycle。这样端口在连接、断开的时候就不需要等待2MSL的时间,可以快速回收。

    vim /etc/sysctl.conf
    # 修改端口范围
    net.ipv4.tcp_timestamps = 1  # 需要保证开启
    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.tw_recycle = 1
    # 使得配置生效
    sysctl -p
    
  • 建议2、客户端最好不要使用bind。

    描述:如果不是业务上有要求,建议客户端尽量不要使用bind。如果一旦使用bind绑定的端口,则这个端口只能用来发起一条连接。

  • 建议3、小心连接队列溢出

    描述:服务端使用了半连接队列/全连接队列来响应客户端的握手请求。如果发生溢出,就很可能会丢包。一旦出现丢包导致的握手问题,那么TCP连接耗时就是秒级别了,非常影响用户体验。

    我们一定要会观察和设置参数尽量避免队列溢出。

    #一、对于半连接队列
    #溢出观察
    netstat -antp | grep SYN_RECV | wc -l
    #避免手段:保证开启tcp_syncookies这个内核参数。#二、对于全连接队列
    #溢出观察
    netstat -s | grep overflowed
    141160 times the listen queue of a socket overflowed
    #避免手段:调整backlog和net.core.somaxconn内核参数
    
  • 建议4、减少握手重试。

    描述:一旦握手出现异常,客户端/服务端就会启动超时重传机制。重传时间间隔:1s、3s、7s、…

    实际上这么多重传次数是没有意义的,客户不会等那么久让你一直在那重连,不如将重试次数设置小一些,尽早放弃重试。客户端设置tcp_syn_retries参数来配置重传次数,服务器端设置tcp_synack_retries来控制半连接队列的超时次数。

  • 建议5、打开TFO设置。

    描述:打开TFO(TCP Fast Open)参数,就可以在客户端的第三次握手ack包中携带要发送给服务端的数据,相当于节约了一次RTT的时间开销。

    vim /etc/sysctl.conf
    # 打开TFO设置
    net.ipv4.tcp_fastopne = 3  # 服务端和客户端都可以启用
    # 使得配置生效
    sysctl -p
    

    建议6、保持充足的文件描述符上限。

    描述:在Linux下一切皆文件,包括网络连接socket。如果需要支持海量的并发连接,那么需要调整和加大文件描述符的上限。避免出现“Too many open files”报错。

    # 文件打开数量设置举例
    vim /etc/sysctl.conf
    # 系统级别的设置110万
    fs.file-max=1100000
    # 进程级别也设置110万,这个值要保证比hard nofile。否则后果严重:用户无法登录。
    fs.nr_open=1100000vim /etc/security/limits.conf
    # 用户进程级别都设置为100万【必须都设置,按照两者最小生效】
    * soft nofile 1000000
    * hard nofile 1000000
    
  • 建议7、如果请求频繁,请用长连接代替短连接。

    描述:如果程序需要频繁的请求某个服务端,一个更好的方法是使用长连接代替短连接。这样做的好处是:
    1、节约了握手开销。
    2、端口数量不容易出现问题。
    3、降低了队列满的可能性。

  • 建议8、TIME_WAIT的优化。

    从内存角度来看,一条TIME_WAIT状态的TCP连接仅仅占用0.4KB左右的内存而已。端口占用问题也只会在连接同一个server的时候才需要考虑【网络连接四元组】。所以不必担心。如果实在想优化,有三种方法:
    1、可以考虑使用tcp_max_tw_buckets来限制TIME_WAIT连接总数。
    2、打开tcp_tw_recycletcp_tw_reuse来快速回收端口。
    3、干脆直接用长连接代替频繁的短连接。

    # 如下,有31948条状态为TIME_WAIT的TCP连接
    sudo netstat -antp | grep TIME_WAIT | wc -l
    31948vim /etc/sysctl.conf
    # 设置tcp_max_tw_buckets最大TIME_WAIT数量
    net.ipv4.tcp_max_tw_buckets = 30000
    # 快速回收端口
    net.ipv4.tcp_timestamps = 1  # 需要保证开启
    tcp_tw_recycle = 1
    tcp_tw_reuse = 1
    sysctl -p
    

深入理解Linux网络学习总结相关推荐

  1. linux 虚拟机大量udp请求失败_理解 Linux 网络栈:Linux 网络协议栈简单总结分析...

    1. Linux 网络路径 1.1 发送端 1.1.1 应用层 (1) Socket 应用层的各种网络应用程序基本上都是通过 Linux Socket 编程接口来和内核空间的网络协议栈通信的.Linu ...

  2. 深入理解Linux内核 学习Linux内核的一些建议及书记推荐

    深入理解Linux内核 学习Linux内核的一些建议_华清远见教育集团 经典书籍 待到山花烂漫时,还是那些经典在微笑. 有关内核的书籍可以用汗牛充栋来形容,不过只有一些经典的神作经住了考验.首先是5本 ...

  3. 为大家献上《深入理解Linux网络》全书知识图谱!

    大家好,我是飞哥! 咱们的<深入理解Linux网络>上市短短一个月,就收获了非常多的好数据.为了能让这么多读者更好地吸收和掌握书中的知识,我做了个全书的知识图谱,今天送给大家! 我把书中的 ...

  4. 理解 Linux 网络栈(2):非虚拟化Linux 环境中的 Segmentation Offloading 技术

    本系列文章总结 Linux 网络栈,包括: (1)Linux 网络协议栈总结 (2)非虚拟化Linux环境中的网络分段卸载技术 GSO/TSO/UFO/LRO/GRO (3)QEMU/KVM + Vx ...

  5. 理解 Linux 网络栈:Linux 网络协议栈简单总结

    1. Linux 网络路径 1.1 发送端 1.1.1 应用层 (1) Socket 应用层的各种网络应用程序基本上都是通过 Linux Socket 编程接口来和内核空间的网络协议栈通信的.Linu ...

  6. 深入理解Linux网络技术内幕(十)——帧的接收

    文章目录 前言 与其他功能交互 设备的开启和关闭 队列 通知内核帧已接收:NAPI和netif_rx NAPI简介 NAPI所用之net_device字段 net_rx_action和NAPI 新旧驱 ...

  7. 深入理解Linux网络技术内幕学习笔记第十九章:因特网协议第四版(IPv4):Linux的原理和功能

    本章主要介绍Linux支持IP的数据结构和基本活动,如入口IP包如何传递至IP接收函数,校验和如何验证,以及IP选项如何处理. 主要的IPv4数据结构: struct iphdr{ };//ip报头 ...

  8. Linux网络学习第七部分:ip协议+数据链路层详解

    IP协议与TCP协议的关系 上一部分我们详细的讲了TCP的发送与接收,但事实上数据并不是从传输层TCP直接发送到对端主机的传输层TCP缓冲区,而是需要经过网络层ip协议的加工,在经过数据链路层加工(后 ...

  9. 理解 Linux 网络命名空间

    如果你使用过 Docker 和 Kubernetes,那么可能应该听说过 network namespace(网络命名空间),最近在我们的 <Kubernetes 网络训练营>课程中学习到 ...

最新文章

  1. 当一个项目中同时存在webroot和webcontext时
  2. SQL Server 中的事务和锁(三)-Range S-U,X-X 以及死锁
  3. java shp求相交面积_shp文件自相交处理的方法
  4. python参数化_Python 中如何实现参数化测试的方法示例
  5. 计算机二级offic考试题库2017,2017年9月计算机二级MSOffice考试题「带答案」
  6. 从N个元素中选择第i小的元素
  7. linux 7 定时任务设置,centos7 配置crontab定时任务
  8. mysql run sql files_HeidiSQL_5.1_Sourcecode
  9. 谷歌称远程办公影响工作效率,2021年回归线下办公
  10. 单机按钮来图片轮播_原生js如何实现轮播图效果?
  11. SylixOS学习三—— SylixOS的引导与安装1
  12. VMware10安装CentOS6.7
  13. 关于Hibernate ResultTransformer
  14. V4 乱码问题总结 v5 也可以参考
  15. 已解决-怎么关CSDN的一周小结
  16. linux限制用户只能访问网站,Linux中限制用户访问权限的3种方法
  17. 窗口最大化最大最小化关闭按钮超出屏幕 WM_GETMINMAXINFO
  18. 一篇彻底搞懂-->shell脚本
  19. php获取今天星期几,PHP获取星期几的常用方法小结
  20. 使用rsync实现文件同步

热门文章

  1. 嵌入式linux热启动和冷启动,使用keil判断ARM的冷启动和热启动的方法
  2. 傅园慧的“洪荒大法”为何让你魂不守舍?
  3. ThinkPHP--基础
  4. ubantu系统命令
  5. 你知道的互联网商业模式有哪些呢?来结合具体实例说明一下
  6. Cache 'redisCache' does not allow 'null' values;设置值为空时不存入Redis;设置unless无效;
  7. 32位系统最大支持4GB内存原因
  8. Part I 空气曲棍球 Chapter5 (5.1 We Have an Aspect Ratio Problem)
  9. Snacktools:一套基于Web应用的富媒体编辑器
  10. springboot 集成百度编辑器ueditor