网络性能篇

倪朋飞 《Linux 性能优化实战》

33 | 关于 Linux 网络,你必须知道这些(上)

  网络模型:7层网络模型(OSI 网络模型)与4层网络模型(TCP/IP 网络模型)

Linux 网络收发流程;环形缓冲区、sk_buff 缓冲区、套接字缓冲区;网卡接收数据后,经过几次拷贝才能到用户进程
===================================================================================================
网络包的接收流程


1.当一个网络帧到达网卡后,网卡会通过 DMA 方式,把这个网络包放到收包队列中;然后通过硬中断,告诉中断处理程序已经收到了网络包。

2.接着,网卡中断处理程序会为网络帧分配内核数据结构(sk_buff),并将其拷贝到 sk_buff 缓冲区中;然后再通过软中断,通知内核收到了新的网络帧。

3.接下来,内核协议栈从缓冲区中取出网络帧,并通过网络协议栈,从下到上逐层处理这个网络帧。比如,
    a.在链路层检查报文的合法性,找出上层协议的类型(比如 IPv4 还是 IPv6),再去掉帧头、帧尾,然后交给网络层。
    b.网络层取出 IP 头,判断网络包下一步的走向,比如是交给上层处理还是转发。当网络层确认这个包是要发送到本机后,就会取出上层协议的类型(比如 TCP 还是 UDP),去掉 IP 头,再交给传输层处理。
    c.传输层取出 TCP 头或者 UDP 头后,根据 < 源 IP、源端口、目的 IP、目的端口 > 四元组作为标识,找出对应的 Socket,并把数据拷贝到 Socket 的接收缓存中。

4.最后,应用程序就可以使用 Socket 接口,读取到新接收到的数据了。

--------------------------------------------------------------------------------------------
https://segmentfault.com/a/1190000008836467
    1: 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,且该网卡没有开启混杂模式,该包会被网卡丢弃。
    2: 网卡将数据包通过DMA的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。注: 老的网卡可能不支持DMA,不过新的网卡一般都支持。
    3: 网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了
    4: CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数
    5: 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。
    6: 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。

软中断会触发内核网络模块中的软中断处理函数,后续流程如下
    7: 内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第6步中是网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数
    8: net_rx_action调用网卡驱动里的poll函数来一个一个的处理数据包
    9: 在pool函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
    10: 驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数
    11: napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS,如果开启了,将会调用enqueue_to_backlog
    12: 在enqueue_to_backlog函数中,会将数据包放入CPU的softnet_data结构体的input_pkt_queue中,然后返回,如果input_pkt_queue满了的话,该数据包将会被丢弃,queue的大小可以通过net.core.netdev_max_backlog来配置
    13: CPU会接着在自己的软中断上下文中处理自己input_pkt_queue里的网络数据(调用__netif_receive_skb_core)
    14: 如果没开启RPS,napi_gro_receive会直接调用__netif_receive_skb_core
    15: 看是不是有AF_PACKET类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。
    16: 调用协议栈相应的函数,将数据包交给协议栈处理。
    17: 待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU

由于是UDP包,所以第一步会进入IP层,然后一级一级的函数往下调:
    ip_rcv: ip_rcv函数是IP模块的入口函数,在该函数里面,第一件事就是将垃圾数据包(目的mac地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来)直接丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数
    NF_INET_PRE_ROUTING: netfilter放在协议栈中的钩子,可以通过iptables来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走
    routing: 进行路由,如果是目的IP不是本地IP,且没有开启ip forward功能,那么数据包将被丢弃,如果开启了ip forward功能,那将进入ip_forward函数
    ip_forward: ip_forward会先调用netfilter注册的NF_INET_FORWARD相关函数,如果数据包没有被丢弃,那么将继续往后调用dst_output_sk函数
    dst_output_sk: 该函数会调用IP层的相应函数将该数据包发送出去,同下一篇要介绍的数据包发送流程的后半部分一样。
    ip_local_deliver:如果上面routing的时候发现目的IP是本地IP,那么将会调用该函数,在该函数中,会先调用NF_INET_LOCAL_IN相关的钩子程序,如果通过,数据包将会向下发送到UDP层

    udp_rcv: udp_rcv函数是UDP模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续
    sock_queue_rcv_skb: 主要干了两件事,一是检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包,然后就是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃(在Linux里面,每个socket上都可以像tcpdump里面一样定义filter,不满足条件的数据包将会被丢弃)
    __skb_queue_tail: 将数据包放入socket接收队列的末尾
    sk_data_ready: 通知socket数据包已经准备好

socket
    应用层一般有两种方式接收数据,一种是recvfrom函数阻塞在那里等着数据来,这种情况下当socket收到通知后,recvfrom就会被唤醒,然后读取接收队列的数据;
    另一种是通过epoll或者select监听相应的socket,当收到通知后,再调用recvfrom函数去读取接收队列的数据。两种情况都能正常的接收到相应的数据包。
--------------------------------------------------------------------------------------------
45 | 答疑(五):网络收发过程中,缓冲区位置在哪里?................
Linux 网络的收发流程。这个流程涉及到了多个队列和缓冲区,包括:
    网卡收发网络包时,通过 DMA 方式交互的环形缓冲区;
    网卡中断处理程序为网络帧分配的,内核数据结构 sk_buff 缓冲区;
    应用程序通过套接字接口,与网络协议栈交互时的套接字缓冲区。

1.这些缓冲区都处于内核管理的内存中。
    环形缓冲区,由于需要 DMA 与网卡交互,理应属于网卡设备驱动的范围。
    sk_buff 缓冲区,是一个维护网络帧结构的双向链表,链表中的每一个元素都是一个网络帧(Packet)。
        虽然 TCP/IP 协议栈分了好几层,但上下不同层之间的传递,实际上只需要操作这个数据结构中的指针,而无需进行数据复制。
    套接字缓冲区,则允许应用程序,给每个套接字配置不同大小的接收或发送缓冲区。
        应用程序发送数据,实际上就是将数据写入缓冲区;
        应用程序接收数据,其实就是从缓冲区中读取。至于缓冲区中数据的进一步处理,则由传输层的 TCP 或 UDP 协议来完成。

2.这些缓冲区,跟前面内存部分讲到的 Buffer 和 Cache 有什么关联吗?
    在内存模块曾提到过,内存中提到的 Buffer ,都跟块设备直接相关;而其他的都是 Cache。
    实际上,sk_buff、套接字缓冲、连接跟踪等,都通过 slab 分配器来管理。你可以直接通过 /proc/slabinfo,来查看它们占用的内存大小。
    
--------------------------------------------------------------------------------------------
Linux经典面试题:网卡接收数据后,经过几次拷贝才能到用户进程
https://www.isolves.com/it/rj/czxt/linux/2021-07-04/41098.html

    1、当数据包到达网卡依据配置会将网络数据拷贝到DMA中,并触发硬件中断
    2、驱动程序将从ring buffer中读取,填充内核skbuff结构。执行上层协议栈操作
    3、socket read操作将数据从内核拷贝到用户态
Linux 网络收发流程;环形缓冲区、sk_buff 缓冲区、套接字缓冲区;网卡接收数据后,经过几次拷贝才能到用户进程
网络包的发送流程
https://segmentfault.com/a/1190000008926093

了解网络包的接收流程后,就很容易理解网络包的发送流程。网络包的发送流程就是上图的右半部分,很容易发现,网络包的发送方向,正好跟接收方向相反。

1.首先,应用程序调用 Socket API(比如 sendmsg)发送网络包。

2.由于这是一个系统调用,所以会陷入到内核态的套接字层中。套接字层会把数据包放到 Socket 发送缓冲区中。

3.接下来,网络协议栈从 Socket 发送缓冲区中,取出数据包;再按照 TCP/IP 栈,从上到下逐层处理。比如,传输层和网络层,分别为其增加 TCP 头和 IP 头,执行路由查找确认下一跳的 IP,并按照 MTU 大小进行分片。

4.分片后的网络包,再送到网络接口层,进行物理地址寻址,以找到下一跳的 MAC 地址。然后添加帧头和帧尾,放到发包队列中。这一切完成后,会有软中断通知驱动程序:发包队列中有新的网络帧需要发送。

5.最后,驱动程序通过 DMA ,从发包队列中读出网络帧,并通过物理网卡把它发送出去。
网络包的发送流程

 

 34 | 关于 Linux 网络,你必须知道这些(下)

性能指标:带宽、吞吐量、延时、PPS、网络的可用性(网络能否正常通信)、并发连接数(TCP 连接数量)、丢包率(丢包百分比)、重传率(重新传输的网络包比例)
=========================================================================================================================
带宽,表示链路的最大传输速率,单位通常为 b/s (比特 / 秒)。
吞吐量,表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽限制,而吞吐量 / 带宽,也就是该网络的使用率。
延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。
PPS,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。

除了这些指标,网络的可用性(网络能否正常通信)、并发连接数(TCP 连接数量)、丢包率(丢包百分比)、重传率(重新传输的网络包比例)等也是常用的性能指标。
性能指标:带宽、吞吐量、延时、PPS、网络的可用性(网络能否正常通信)、并发连接数(TCP 连接数量)、丢包率(丢包百分比)、重传率(重新传输的网络包比例)
ifconfig、ip -s addr 输出解析
================================================================================================
$ ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
      inet 10.240.0.30 netmask 255.240.0.0 broadcast 10.255.255.255
      inet6 fe80::20d:3aff:fe07:cf2a prefixlen 64 scopeid 0x20<link>
      ether 78:0d:3a:07:cf:3a txqueuelen 1000 (Ethernet)
      RX packets 40809142 bytes 9542369803 (9.5 GB)
      RX errors 0 dropped 0 overruns 0 frame 0
      TX packets 32637401 bytes 4815573306 (4.8 GB)
      TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
​
$ ip -s addr show dev eth0
    2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
      link/ether 78:0d:3a:07:cf:3a brd ff:ff:ff:ff:ff:ff
      inet 10.240.0.30/12 brd 10.255.255.255 scope global eth0
          valid_lft forever preferred_lft forever
      inet6 fe80::20d:3aff:fe07:cf2a/64 scope link
          valid_lft forever preferred_lft forever
      RX: bytes packets errors dropped overrun mcast
       9542432350 40809397 0       0       0       193
      TX: bytes packets errors dropped carrier collsns
       4815625265 32637658 0       0       0       0

第一,网络接口的状态标志。ifconfig 输出中的 RUNNING ,或 ip 输出中的 LOWER_UP ,都表示物理网络是连通的,即网卡已经连接到了交换机或者路由器中。如果你看不到它们,通常表示网线被拔掉了。
第二,MTU 的大小。MTU 默认大小是 1500,根据网络架构的不同(比如是否使用了 VXLAN 等叠加网络),你可能需要调大或者调小 MTU 的数值。
第三,网络接口的 IP 地址、子网以及 MAC 地址。这些都是保障网络功能正常工作所必需的,你需要确保配置正确。
第四,网络收发的字节数、包数、错误数以及丢包情况,特别是 TX 和 RX 部分的 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,通常表示出现了网络 I/O 问题。其中:
    errors 表示发生错误的数据包数,比如校验错误、帧同步错误等;
    dropped 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer,但因为内存不足等原因丢包;
    overruns 表示超限数据包数,即网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;
    carrier 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等;
    collisions 表示碰撞数据包数。
ifconfig、ip -s addr 输出解析
套接字信息:netstat -nlp、ss -ltnp;协议栈统计信息:netstat -s、ss -s
================================================================================================
$ netstat -nlp | head -n 3      # -p 表示显示进程信息
    Active Internet connections (only servers)
    Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
    tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      840/systemd-resolve


$ ss -ltnp | head -n 3          # -p 表示显示进程信息
    State    Recv-Q    Send-Q        Local Address:Port        Peer Address:Port
    LISTEN   0         128           127.0.0.53%lo:53               0.0.0.0:*        users:(("systemd-resolve",pid=840,fd=13))
    LISTEN   0         128                 0.0.0.0:22               0.0.0.0:*        users:(("sshd",pid=1459,fd=3))


接收队列(Recv-Q)和发送队列(Send-Q)需要你特别关注,它们通常应该是 0。当你发现它们不是 0 时,说明有网络包的堆积发生。

在不同套接字状态下,它们的含义不同:
    当套接字处于连接状态(Established)时,
        Recv-Q 表示套接字缓冲还没有被应用程序取走的字节数(即接收队列长度)。
        而 Send-Q 表示还没有被远端主机确认的字节数(即发送队列长度)。

    当套接字处于监听状态(Listening)时,
        Recv-Q 表示全连接队列的长度。
        而 Send-Q 表示全连接队列的最大长度。

所谓全连接,是指服务器收到了客户端的 ACK,完成了 TCP 三次握手,然后就会把这个连接挪到全连接队列中。这些全连接中的套接字,还需要被 accept() 系统调用取走,服务器才可以开始真正处理客户端的请求。
与全连接队列相对应的,还有一个半连接队列。所谓半连接是指还没有完成 TCP 三次握手的连接,连接只进行了一半。服务器收到了客户端的 SYN 包后,就会把这个连接放到半连接队列中,然后再向客户端发送 SYN+ACK 包。

---------------------------------------------------------------------------------------------------
协议栈统计信息

$ netstat -s
    ...
    Tcp:
        3244906 active connection openings
        23143 passive connection openings
        115732 failed connection attempts
        2964 connection resets received
        1 connections established
        13025010 segments received
        17606946 segments sent out
        44438 segments retransmitted
        42 bad segments received
        5315 resets sent
        InCsumErrors: 42
    ...
$ ss -s
    Total: 186 (kernel 1446)
    TCP:   4 (estab 1, closed 0, orphaned 0, synrecv 0, timewait 0/0), ports 0
    Transport Total     IP        IPv6
    *    1446      -         -
    RAW    2         1         1
    UDP    2         2         0
    TCP    4         3         1
    ...

ss 只显示已经连接、关闭、孤儿套接字等简要统计,而 netstat 则提供的是更详细的网络协议栈信息。
套接字信息:netstat -nlp、ss -ltnp;协议栈统计信息:netstat -s、ss -s
网络吞吐和 PPS :sar -n DEV;带宽用 ethtool 来查询;连通性和延时:ping测试
==================================================================================================================
sar 增加 -n 参数就可以查看网络的统计信息,比如网络接口(DEV)、网络接口错误(EDEV)、TCP、UDP、ICMP 等等

$ sar -n DEV 1      # 数字1表示每隔1秒输出一组数据
Linux 4.15.0-1035 (ubuntu)   01/06/19   _x86_64_  (2 CPU)
13:21:40        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
13:21:41         eth0     18.00     20.00      5.79      4.25      0.00      0.00      0.00      0.00
13:21:41      docker0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
13:21:41           lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00

    rxpck/s 和 txpck/s 分别是接收和发送的 PPS,单位为包 / 秒。
    rxkB/s 和 txkB/s 分别是接收和发送的吞吐量,单位是 KB/ 秒。
    rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包 / 秒。
    %ifutil 是网络接口的使用率,即半双工模式下为 (rxkB/s+txkB/s)/Bandwidth,而全双工模式下为 max(rxkB/s, txkB/s)/Bandwidth。

--------------------------------------------------------------------------------------------------------------
Bandwidth 可以用 ethtool 来查询,它的单位通常是 Gb/s 或者 Mb/s,不过注意这里小写字母 b ,表示比特而不是字节。我们通常提到的千兆网卡、万兆网卡等,单位也都是比特。
$ ethtool eth0 | grep Speed
  Speed: 1000Mb/s

--------------------------------------------------------------------------------------------------------------
# -c3表示发送三次ICMP包后停止
$ ping -c3 114.114.114.114
PING 114.114.114.114 (114.114.114.114) 56(84) bytes of data.
64 bytes from 114.114.114.114: icmp_seq=1 ttl=54 time=244 ms
64 bytes from 114.114.114.114: icmp_seq=2 ttl=47 time=244 ms
64 bytes from 114.114.114.114: icmp_seq=3 ttl=67 time=244 ms
--- 114.114.114.114 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 244.023/244.070/244.105/0.034 ms
网络吞吐和 PPS :sar -n DEV;带宽用 ethtool 来查询;连通性和延时:ping测试

35 | 基础篇:C10K 和 C1000K 回顾

C100的网络处理模型
在 C10K 以前,Linux 中网络处理都用同步阻塞的方式,也就是每个请求都分配一个进程或者线程。
    问题:请求数只有 100 个时,这种方式自然没问题,但增加到 10000 个请求时,10000 个进程或线程的调度、上下文切换乃至它们占用的内存,都会成为瓶颈。
C100的网络处理模型
C100升级至C10K的优化:I/O 模型优化:I/O 多路复用(select/poll、epoll、异步 I/O)+工作模型优化(主进程+多个 worker 子进程;监听到相同端口的多进程模型)
=========================================================================================================================
I/O 模型优化

 I/O 事件通知的方式:水平触发和边缘触发,它们常用在套接字接口的文件描述符中。
    水平触发:只要文件描述符可以非阻塞地执行 I/O ,就会触发通知。
        也就是说,应用程序可以随时检查文件描述符的状态,然后再根据状态,进行 I/O 操作。
    边缘触发:只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时,才发送一次通知。
        这时候,应用程序需要尽可能多地执行 I/O,直到无法继续读写,才可以停止。如果 I/O 没执行完,或者因为某种原因没来得及处理,那么这次通知也就丢失了。


 I/O 多路复用:
    第一种,使用非阻塞 I/O 和水平触发通知,比如使用 select 或者 poll。
        根据刚才水平触发的原理,select 和 poll 需要从文件描述符列表中,找出哪些可以执行 I/O ,然后进行真正的网络 I/O 读写。
        由于 I/O 是非阻塞的,一个线程中就可以同时监控一批套接字的文件描述符,这样就达到了单线程处理多请求的目的。
        问题:应用软件使用 select 和 poll 时,需要对这些文件描述符列表进行轮询,这样,请求数多的时候就会比较耗时。并且,select 和 poll 还有一些其他的限制。
            select 使用固定长度的位相量,表示文件描述符的集合,因此会有最大描述符数量的限制。
            比如,在 32 位系统中,默认限制是 1024。并且,在 select 内部,检查套接字状态是用轮询的方法,处理耗时跟描述符数量是 O(N) 的关系。
            poll 改进了 select 的表示方法,换成了一个没有固定长度的数组,这样就没有了最大描述符数量的限制(当然还会受到系统文件描述符限制)。
            但应用程序在使用 poll 时,同样需要对文件描述符列表进行轮询,这样,处理耗时跟描述符数量就是 O(N) 的关系。
            除此之外,应用程序每次调用 select 和 poll 时,还需要把文件描述符的集合,从用户空间传入内核空间,由内核修改后,再传出到用户空间中。这一来一回的内核空间与用户空间切换,也增加了处理成本。
    第二种,使用非阻塞 I/O 和边缘触发通知,比如 epoll。
        epoll 使用红黑树,在内核中管理文件描述符的集合,这样,就不需要应用程序在每次操作时都传入、传出这个集合。
        epoll 使用事件驱动的机制,只关注有 I/O 事件发生的文件描述符,不需要轮询扫描整个集合。
    第三种,使用异步 I/O(Asynchronous I/O,简称为 AIO)。
        异步 I/O 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。
        而在 I/O 完成后,系统会用事件通知(比如信号或者回调函数)的方式,告诉应用程序。这时,应用程序才会去查询 I/O 操作的结果。
            由于异步 I/O 跟我们的直观逻辑不太一样,想要使用的话,一定要小心设计,其使用难度比较高。


工作模型优化
    第一种,主进程 + 多个 worker 子进程,这也是最常用的一种模型。这种方法的一个通用工作模式就是:
        主进程执行 bind() + listen() 后,创建多个子进程;
        然后,在每个子进程中,都通过 accept() 或 epoll_wait() ,来处理相同的套接字。
        例如nginx:是由主进程和多个 worker 进程组成。
            主进程主要用来初始化套接字,并管理子进程的生命周期;
            而 worker 进程,则负责实际的请求处理。
        惊群:
            当网络 I/O 事件发生时,多个进程被同时唤醒,但实际上只有一个进程来响应这个事件,其他被唤醒的进程都会重新休眠。
        为了避免惊群问题, Nginx 在每个 worker 进程中,都增加一个了全局锁(accept_mutex)。这些 worker 进程需要首先竞争到锁,只有竞争到锁的进程,才会加入到 epoll 中,这样就确保只有一个 worker 子进程被唤醒。
        进程的管理、调度、上下文切换的成本非常高。那为什么使用多进程模式的 Nginx ,却具有非常好的性能呢?
            最主要的一个原因就是,这些 worker 进程,实际上并不需要经常创建和销毁,而是在没任务时休眠,有任务时唤醒。只有在 worker 由于某些异常退出时,主进程才需要创建新的进程来代替它。
    
    第二种,监听到相同端口的多进程模型。
        在这种方式下,所有的进程都监听相同的接口,并且开启 SO_REUSEPORT 选项,由内核负责将请求负载均衡到这些监听进程中去。
            由于内核确保了只有一个进程被唤醒,就不会出现惊群问题了。
            不过要注意,想要使用 SO_REUSEPORT 选项,需要用 Linux 3.9 以上的版本才可以。
----------------------------------------------------------------------------------------
C10K 问题的根源,一方面在于系统有限的资源;另一方面,也是更重要的因素,是同步阻塞的 I/O 模型以及轮询的套接字接口,限制了网络事件的处理效率。
    Linux 2.6 中引入的 epoll ,完美解决了 C10K 的问题,现在的高性能网络方案都基于 epoll。
C100升级至C10K的优化:I/O 模型优化:I/O 多路复用(select/poll、epoll、异步 I/O)+工作模型优化(主进程+多个 worker 子进程;监听到相同端口的多进程模型)
C10K升级至C100K的优化
============================================================================================================
C100K ,可能只需要增加系统的物理资源就可以满足;
C10K升级至C100K的优化
C100K升级至C1000K的优化:从应用程序到 Linux 内核、再到 CPU、内存和网络等各个层次的深度优化;以及借助硬件进行优化
=====================================================================================================================
C1000K
    使用"I/O 模型优化"解决了C10K-C100K的问题,但是C1000K场景却有新的问题:
        1.从物理资源使用上来说,100 万个请求需要大量的系统资源。比如,
            假设每个请求需要 16KB 内存的话,那么总共就需要大约 15 GB 内存。
            而从带宽上来说,假设只有 20% 活跃连接,即使每个连接只需要 1KB/s 的吞吐量,总共也需要 1.6 Gb/s 的吞吐量。千兆网卡显然满足不了这么大的吞吐量,所以还需要配置万兆网卡,或者基于多网卡 Bonding 承载更大的吞吐量。
        2.从软件资源上来说,大量的连接也会占用大量的软件资源,比如文件描述符的数量、连接状态的跟踪(CONNTRACK)、网络协议栈的缓存大小(比如套接字读写缓存、TCP 读写缓存)等等。
        3.大量请求带来的中断处理,也会带来非常高的处理成本。
            这样,就需要多队列网卡、中断负载均衡、CPU 绑定、RPS/RFS(软中断负载均衡到多个 CPU 核上),以及将网络包的处理卸载(Offload)到网络设备(如 TSO/GSO、LRO/GRO、VXLAN OFFLOAD)等各种硬件和软件的优化。

    C1000K 的解决方法,本质上还是构建在 epoll 的非阻塞 I/O 模型上。
    只不过,除了 I/O 模型之外,还需要从应用程序到 Linux 内核、再到 CPU、内存和网络等各个层次的深度优化,特别是需要借助硬件,来卸载那些原来通过软件处理的大量功能。
-------------------------------------------------------------------------------
C1000K ,不仅仅是增加物理资源就能解决的问题了。
    这时,就需要多方面的优化工作了,从硬件的中断处理和网络功能卸载、到网络协议栈的文件描述符数量、连接状态跟踪、缓存队列等内核的优化,再到应用程序的工作模型优化,都是考虑的重点。
C100K升级至C1000K的优化:从应用程序到 Linux 内核、再到 CPU、内存和网络等各个层次的深度优化;以及借助硬件进行优化
C1000K升级至C10M的优化:DPDK、XDP
=====================================================================================================================
C10M
    问题:
        实际上,在 C1000K 问题中,各种软件、硬件的优化很可能都已经做到头了。
        特别是当升级完硬件(比如足够多的内存、带宽足够大的网卡、更多的网络功能卸载等)后,
        你可能会发现,无论你怎么优化应用程序和内核中的各种网络参数,想实现 1000 万请求的并发,都是极其困难的。
        根本原因在于:Linux 内核协议栈做了太多太繁重的工作。从网卡中断带来的硬中断处理程序开始,到软中断中的各层网络协议处理,最后再到应用程序,这个路径实在是太长了,就会导致网络包的处理优化,到了一定程度后,就无法更进一步了。

    解决问题:
        最重要就是跳过内核协议栈的冗长路径,把网络包直接送到要处理的应用程序那里去。这里有两种常见的机制,DPDK 和 XDP。
            第一种机制,DPDK,是用户态网络的标准。它跳过内核协议栈,直接由用户态进程通过轮询的方式,来处理网络接收。
                关于轮询,你肯定会下意识认为它是低效的象征,但是进一步反问下自己,它的低效主要体现在哪里呢?是查询时间明显多于实际工作时间的情况下吧!
                那么,换个角度来想,如果每时每刻都有新的网络包需要处理,轮询的优势就很明显了
                此外,DPDK 还通过大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
                
            第二种机制,XDP(eXpress Data Path),则是 Linux 内核提供的一种高性能网络数据路径。
                它允许网络包,在进入内核协议栈之前,就进行处理,也可以带来更高的性能。XDP 底层跟我们之前用到的 bcc-tools 一样,都是基于 Linux 内核的 eBPF 机制实现的。
                XDP 对内核的要求比较高,需要的是 Linux 4.8 以上版本,并且它也不提供缓存队列。基于 XDP 的应用程序通常是专用的网络应用,常见的有 IDS(入侵检测系统)、DDoS 防御、 cilium 容器网络插件等。
-----------------------------------------------------------------------------------
C10M ,不只是增加物理资源,或者优化内核和应用程序可以解决的问题了。
    这时候,就需要用 XDP 的方式,在内核协议栈之前处理网络包;
    或者用 DPDK 直接跳过网络协议栈,在用户空间通过轮询的方式直接处理网络包。
C1000K升级至C10M的优化:DPDK、XDP

36 | 套路篇:怎么评估系统的网络性能?

网络接口层和网络层的性能测试:hping3、pktgen工具
==========================================================================================================
网络接口层和网络层,它们主要负责网络包的封装、寻址、路由以及发送和接收。
    在这两个网络协议层中,最重要的性能指标:每秒可处理的网络包数 PPS

hping3 作为一个测试网络包处理能力的性能工具。

Linux 内核自带的高性能网络测试工具 pktgen
pktgen 作为一个内核线程来运行,需要你加载 pktgen 内核模块后,再通过 /proc 文件系统来交互。下面就是 pktgen 启动的两个内核线程和 /proc 文件系统的交互文件:
    $ modprobe pktgen       #如果 modprobe 命令执行失败,说明你的内核没有配置 CONFIG_NET_PKTGEN 选项。这就需要你配置 pktgen 内核模块(即 CONFIG_NET_PKTGEN=m)后,重新编译内核,才可以使用。
    $ ps -ef | grep pktgen | grep -v grep
    root     26384     2  0 06:17 ?        00:00:00 [kpktgend_0]
    root     26385     2  0 06:17 ?        00:00:00 [kpktgend_1]
    $ ls /proc/net/pktgen/
    kpktgend_0  kpktgend_1  pgctrl

pktgen 在每个 CPU 上启动一个内核线程,并可以通过 /proc/net/pktgen 下面的同名文件,跟这些线程交互;而 pgctrl 则主要用来控制这次测试的开启和停止。
在使用 pktgen 测试网络性能时,需要先给每个内核线程 kpktgend_X 以及测试网卡,配置 pktgen 选项,然后再通过 pgctrl 启动测试。
一个发包测试的示例
    # 定义一个工具函数,方便后面配置各种测试选项
    function pgset() {
        local result
        echo $1 > $PGDEV
        result=`cat $PGDEV | fgrep "Result: OK:"`
        if [ "$result" = "" ]; then
             cat $PGDEV | fgrep Result:
        fi
    }
    # 为0号线程绑定eth0网卡
    PGDEV=/proc/net/pktgen/kpktgend_0
    pgset "rem_device_all"   # 清空网卡绑定
    pgset "add_device eth0"  # 添加eth0网卡
    # 配置eth0网卡的测试选项
    PGDEV=/proc/net/pktgen/eth0
    pgset "count 1000000"    # 总发包数量
    pgset "delay 5000"       # 不同包之间的发送延迟(单位纳秒)
    pgset "clone_skb 0"      # SKB包复制
    pgset "pkt_size 64"      # 网络包大小
    pgset "dst 172.17.63.253" # 目的IP
    pgset "dst_mac ee:ff:ff:ff:ff:ff"  # 目的MAC
    # 启动测试
    PGDEV=/proc/net/pktgen/pgctrl
    pgset "start"

稍等一会儿,测试完成后,结果可以从 /proc 文件系统中获取。
    测试前:
    [root@centos7 ~]# cat /proc/net/pktgen/eth0
    Params: count 1000000  min_pkt_size: 64  max_pkt_size: 64
         frags: 0  delay: 5000  clone_skb: 0  ifname: eth0
         flows: 0 flowlen: 0
         queue_map_min: 0  queue_map_max: 0
         dst_min: 172.17.63.253  dst_max:
            src_min:   src_max:
         src_mac: 00:16:3e:1b:ae:12 dst_mac: ee:ff:ff:ff:ff:ff
         udp_src_min: 9  udp_src_max: 9  udp_dst_min: 9  udp_dst_max: 9
         src_mac_count: 0  dst_mac_count: 0
         Flags:
    Current:
         pkts-sofar: 0  errors: 0
         started: 0us  stopped: 0us idle: 0us
         seq_num: 0  cur_dst_mac_offset: 0  cur_src_mac_offset: 0
         cur_saddr: 0.0.0.0  cur_daddr: 172.17.63.253
         cur_udp_dst: 0  cur_udp_src: 0
         cur_queue_map: 0
         flows: 0
    Result: OK: dstmac ee:ff:ff:ff:ff:ff
    测试后:
    [root@centos7 ~]# cat /proc/net/pktgen/eth0
    Params: count 1000000  min_pkt_size: 64  max_pkt_size: 64       #测试选项 Params部分的数据无变化
         frags: 0  delay: 5000  clone_skb: 0  ifname: eth0
         flows: 0 flowlen: 0
         queue_map_min: 0  queue_map_max: 0
         dst_min: 172.17.63.253  dst_max:
            src_min:   src_max:
         src_mac: 00:16:3e:1b:ae:12 dst_mac: ee:ff:ff:ff:ff:ff
         udp_src_min: 9  udp_src_max: 9  udp_dst_min: 9  udp_dst_max: 9
         src_mac_count: 0  dst_mac_count: 0
         Flags:
    Current:                                                        #Current数据有变化
         pkts-sofar: 1000000  errors: 0             #packts so far(pkts-sofar)表示已经发送了 100 万个包,也就表明测试已完成。
         started: 7771818462020us  stopped: 7771835418066us idle: 654254us  #started/stopped时间
         seq_num: 1000001  cur_dst_mac_offset: 0  cur_src_mac_offset: 0
         cur_saddr: 172.17.22.136  cur_daddr: 172.17.63.253
         cur_udp_dst: 9  cur_udp_src: 9
         cur_queue_map: 0
         flows: 0
    Result: OK: 16956046(c16301791+d654254) usec, 1000000 (64byte,0frags)
      58976pps 30Mb/sec (30195712bps) errors: 0                     #测试结果:包含测试所用时间、网络包数量和分片、PPS、吞吐量以及错误数。 约58kpps

作为对比,你可以计算一下千兆交换机的 PPS。
交换机可以达到线速(满负载时,无差错转发),它的 PPS 就是 1000Mbit 除以以太网帧的大小,即 1000Mbps/((64+20)*8bit) = 1.5 Mpps(其中,20B 为以太网帧前导和帧间距的大小)。
网络接口层和网络层的性能测试:hping3、pktgen工具
传输层性能测试:TCP性能测试iperf3工具
============================================================================================================
TCP 和 UDP 的性能测试方法
iperf 和 netperf 都是最常用的网络性能测试工具,测试 TCP 和 UDP 的吞吐量。它们都以客户端和服务器通信的方式,测试一段时间内的平均吞吐量。
特别是现在的云计算时代,在你刚拿到一批虚拟机时,首先要做的,应该就是用 iperf ,测试一下网络性能是否符合预期。

启动 iperf 服务端
$ iperf3 -s -i 1 -p 10000   # -s表示启动服务端,-i表示汇报间隔,-p表示监听端口

iperf 客户端
$ iperf3 -c 192.168.1.150 -b 1G -t 15 -P 2 -p 10000
    # -c表示启动客户端,192.168.1.150为目标服务器的IP
    # -b表示目标带宽(单位是bits/s)
    # -t表示测试时间
    # -P表示并发数,-p表示目标服务器监听端口


[root@yefeng ~]# iperf3 -c 192.168.1.150 -b 1G -t 15 -P 2 -p 10000
    Connecting to host 192.168.1.150, port 10000
    [  4] local 192.168.1.88 port 33498 connected to 192.168.1.150 port 10000
    [  6] local 192.168.1.88 port 33504 connected to 192.168.1.150 port 10000
    [ ID] Interval           Transfer     Bandwidth       Retr  Cwnd
    [  4]   0.00-1.00   sec   111 MBytes   932 Mbits/sec   84    748 KBytes
    [  6]   0.00-1.00   sec   110 MBytes   925 Mbits/sec   54    830 KBytes
    [SUM]   0.00-1.00   sec   221 MBytes  1.86 Gbits/sec  138
    - - - - - - - - - - - - - - - - - - - - - - - - -
    ……………………………………
    - - - - - - - - - - - - - - - - - - - - - - - - -
    [  4]  14.00-15.00  sec   119 MBytes  1.00 Gbits/sec    0    998 KBytes
    [  6]  14.00-15.00  sec   121 MBytes  1.01 Gbits/sec   15    687 KBytes
    [SUM]  14.00-15.00  sec   240 MBytes  2.01 Gbits/sec   15
    - - - - - - - - - - - - - - - - - - - - - - - - -
    [ ID] Interval           Transfer     Bandwidth       Retr
    [  4]   0.00-15.00  sec  1.74 GBytes   995 Mbits/sec  220             sender
    [  4]   0.00-15.00  sec  1.74 GBytes   995 Mbits/sec                  receiver
    [  6]   0.00-15.00  sec  1.74 GBytes   996 Mbits/sec  114             sender
    [  6]   0.00-15.00  sec  1.74 GBytes   996 Mbits/sec                  receiver
    [SUM]   0.00-15.00  sec  3.48 GBytes  1.99 Gbits/sec  334             sender
    [SUM]   0.00-15.00  sec  3.48 GBytes  1.99 Gbits/sec                  receiver  #这台机器 TCP 接收的带宽(吞吐量)为 1.99 Gb/s;因为测试目标带宽1G,并发2;说明实际性能可以达到2Gb/s,甚至超过2Gb/s

    iperf Done.
[root@yefeng ~]# iperf3 -c 192.168.1.150 -b 10G -t 15 -P 2 -p 10000
    Connecting to host 192.168.1.150, port 10000
    [  4] local 192.168.1.88 port 44990 connected to 192.168.1.150 port 10000
    [  6] local 192.168.1.88 port 45000 connected to 192.168.1.150 port 10000
    [ ID] Interval           Transfer     Bandwidth       Retr  Cwnd
    [  4]   0.00-1.00   sec   295 MBytes  2.48 Gbits/sec   41    713 KBytes
    [  6]   0.00-1.00   sec   255 MBytes  2.14 Gbits/sec   42    891 KBytes
    [SUM]   0.00-1.00   sec   550 MBytes  4.62 Gbits/sec   83
    - - - - - - - - - - - - - - - - - - - - - - - - -
    ……………………………………
    - - - - - - - - - - - - - - - - - - - - - - - - -
    [  4]  14.00-15.00  sec   271 MBytes  2.27 Gbits/sec   31    807 KBytes
    [  6]  14.00-15.00  sec   272 MBytes  2.28 Gbits/sec   24    993 KBytes
    [SUM]  14.00-15.00  sec   543 MBytes  4.55 Gbits/sec   55
    - - - - - - - - - - - - - - - - - - - - - - - - -
    [ ID] Interval           Transfer     Bandwidth       Retr
    [  4]   0.00-15.00  sec  4.16 GBytes  2.38 Gbits/sec  180             sender
    [  4]   0.00-15.00  sec  4.15 GBytes  2.38 Gbits/sec                  receiver
    [  6]   0.00-15.00  sec  3.86 GBytes  2.21 Gbits/sec  180             sender
    [  6]   0.00-15.00  sec  3.85 GBytes  2.21 Gbits/sec                  receiver
    [SUM]   0.00-15.00  sec  8.01 GBytes  4.59 Gbits/sec  360             sender
    [SUM]   0.00-15.00  sec  8.01 GBytes  4.59 Gbits/sec                  receiver  #测试结果显示带宽为4.59 Gb/s,达不到测试目标20G

    iperf Done.
传输层性能测试:TCP性能测试iperf3工具
应用层性能测试:HTTP性能测试 ab工具;应用负载性能测试 wrk工具
=================================================================================================
HTTP 就是最常用的一个应用层协议。比如,常用的 Apache、Nginx 等各种 Web 服务,都是基于 HTTP。
要测试 HTTP 的性能,也有大量的工具可以使用,比如 ab、webbench 等,都是常用的 HTTP 压力测试工具。其中,ab 是 Apache 自带的 HTTP 压测工具,主要测试 HTTP 服务的每秒请求数、请求延迟、吞吐量以及请求延迟的分布情况等。

安装工具
    # Ubuntu
    $ apt-get install -y apache2-utils
    # CentOS
    $ yum install -y httpd-tools

服务端运行:
    $ docker run -p 80:80 -itd nginx
客户端进行测试:
    # -c表示并发请求数为1000,-n表示总的请求数为10000
    [root@yefeng ~]# ab -c 1000 -n 10000 http://192.168.1.150/
    This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/

    Benchmarking 192.168.1.150 (be patient)
    Completed 1000 requests
    ……………………
    Completed 10000 requests
    Finished 10000 requests


    Server Software:        nginx/1.23.0
    Server Hostname:        192.168.1.150
    Server Port:            80

    Document Path:          /
    Document Length:        615 bytes

    Concurrency Level:      1000
    Time taken for tests:   3.765 seconds
    Complete requests:      10000
    Failed requests:        0
    Write errors:           0
    Total transferred:      8480000 bytes
    HTML transferred:       6150000 bytes
    Requests per second:    2655.79 [#/sec] (mean)              #Requests per second
    Time per request:       376.536 [ms] (mean)                 #平均延迟,包括了线程运行的调度时间和网络请求响应时间
    Time per request:       0.377 [ms] (mean, across all concurrent requests)   #实际请求的响应时间
    Transfer rate:          2199.32 [Kbytes/sec] received       #吞吐量(BPS)   2199KB/s

    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0   29 160.9      0    1065
    Processing:    22  158 282.2    112    3578
    Waiting:        1  158 282.2    111    3578
    Total:         56  188 398.7    115    3582

    Percentage of the requests served within a certain time (ms)
      50%    115
      66%    138
      75%    140
      80%    142
      90%    155
      95%    196
      98%   1920
      99%   2704
     100%   3582 (longest request)

-------------------------------------------------------------------------------------------------------------
应用负载性能

当你用 iperf 或者 ab 等测试工具,得到 TCP、HTTP 等的性能数据;但这些数据不能表示应用程序的实际性能
    比如,你的应用程序基于 HTTP 协议,为最终用户提供一个 Web 服务。
    这时,使用 ab 工具,可以得到某个页面的访问性能,但这个结果跟用户的实际请求,很可能不一致。
    因为用户请求往往会附带着各种各种的负载(payload),而这些负载会影响 Web 应用程序内部的处理逻辑,从而影响最终性能。

为了得到应用程序的实际性能,就要求性能工具本身可以模拟用户的请求负载,可以用 wrk、TCPCopy、Jmeter 或者 LoadRunner 等实现这个目标。
wrk测试示例:
    $ wrk -c 1000 -t 2 http://192.168.0.30/    # -c表示并发连接数1000,-t表示线程数为2
    Running 10s test @ http://192.168.0.30/
      2 threads and 1000 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    65.83ms  174.06ms   1.99s    95.85%
        Req/Sec     4.87k   628.73     6.78k    69.00%
      96954 requests in 10.06s, 78.59MB read
      Socket errors: connect 0, read 0, write 0, timeout 179
    Requests/sec:   9641.31
    Transfer/sec:      7.82MB

    #这里使用 2 个线程、并发 1000 连接,重新测试了 Nginx 的性能。你可以看到,每秒请求数为 9641,吞吐量为 7.82MB,平均延迟为 65ms,比前面 ab 的测试结果要好很多。
    #这说明,性能工具本身的性能,对性能测试也是至关重要的。不合适的性能工具,并不能准确测出应用程序的最佳性能。

wrk 最大的优势,是其内置的 LuaJIT,可以用来实现复杂场景的性能测试。wrk 在调用 Lua 脚本时,可以将 HTTP 请求分为三个阶段,即 setup、running、done
在 setup 阶段,为请求设置认证参数
    -- example script that demonstrates response handling and
    -- retrieving an authentication token to set on all future
    -- requests
    token = nil
    path  = "/authenticate"
    request = function()
       return wrk.format("GET", path)
    end
    response = function(status, headers, body)
       if not token and status == 200 then
          token = headers["X-Token"]
          path  = "/resource"
          wrk.headers["X-Token"] = token
       end
    end

在执行测试时,通过 -s 选项,执行脚本的路径:
    $ wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/

wrk 需要你用 Lua 脚本,来构造请求负载。这对于大部分场景来说,可能已经足够了 。不过,它的缺点也正是,所有东西都需要代码来构造,并且工具本身不提供 GUI 环境。
像 Jmeter 或者 LoadRunner(商业产品),则针对复杂场景提供了脚本录制、回放、GUI 等更丰富的功能,使用起来也更加方便。
应用层性能测试:HTTP性能测试 ab工具;应用负载性能测试 wrk工具

37 | 案例篇:DNS 解析时快时慢,我该怎么办?

DNS概述;nslookup工具;dig工具;手工配置 DNS 缓存:dnsmasq工具
=======================================================================================================================
DNS 不仅方便了人们访问不同的互联网服务,更为很多应用提供了,动态服务发现和全局负载均衡(Global Server Load Balance,GSLB)的机制。
DNS 协议在 TCP/IP 栈中属于应用层,不过实际传输还是基于 UDP 或者 TCP 协议(UDP 居多) ,并且域名服务器一般监听在端口 53 上。


$ nslookup time.geekbang.org
    # 域名服务器及端口信息
    Server:    114.114.114.114
    Address:  114.114.114.114#53
    # 非权威查询结果
    Non-authoritative answer:
    Name:  time.geekbang.org
    Address: 39.106.233.17




# +trace表示开启跟踪查询
# +nodnssec表示禁止DNS安全扩展
$ dig +trace +nodnssec time.geekbang.org
    ; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> +trace +nodnssec time.geekbang.org
    ;; global options: +cmd
    .      322086  IN  NS  m.root-servers.net.
    .      322086  IN  NS  a.root-servers.net.
    .      322086  IN  NS  i.root-servers.net.
    .      322086  IN  NS  d.root-servers.net.
    .      322086  IN  NS  g.root-servers.net.
    .      322086  IN  NS  l.root-servers.net.
    .      322086  IN  NS  c.root-servers.net.
    .      322086  IN  NS  b.root-servers.net.
    .      322086  IN  NS  h.root-servers.net.
    .      322086  IN  NS  e.root-servers.net.
    .      322086  IN  NS  k.root-servers.net.
    .      322086  IN  NS  j.root-servers.net.
    .      322086  IN  NS  f.root-servers.net.
    ;; Received 239 bytes from 114.114.114.114#53(114.114.114.114) in 1340 ms
    org.      172800  IN  NS  a0.org.afilias-nst.info.
    org.      172800  IN  NS  a2.org.afilias-nst.info.
    org.      172800  IN  NS  b0.org.afilias-nst.org.
    org.      172800  IN  NS  b2.org.afilias-nst.org.
    org.      172800  IN  NS  c0.org.afilias-nst.info.
    org.      172800  IN  NS  d0.org.afilias-nst.org.
    ;; Received 448 bytes from 198.97.190.53#53(h.root-servers.net) in 708 ms
    geekbang.org.    86400  IN  NS  dns9.hichina.com.
    geekbang.org.    86400  IN  NS  dns10.hichina.com.
    ;; Received 96 bytes from 199.19.54.1#53(b0.org.afilias-nst.org) in 1833 ms
    time.geekbang.org.  600  IN  A  39.106.233.176
    ;; Received 62 bytes from 140.205.41.16#53(dns10.hichina.com) in 4 ms

-------------------------------------------------------------------------------------
手工配置 DNS 缓存:dnsmasq工具
我们使用的主流 Linux 发行版,除了最新版本的 Ubuntu (如 18.04 或者更新版本)外,其他版本并没有自动配置 DNS 缓存。
为系统开启 DNS 缓存,就需要你做额外的配置。比如,最简单的方法,就是使用 dnsmasq。
dnsmasq 是最常用的 DNS 缓存服务之一,还经常作为 DHCP 服务来使用。它的安装和配置都比较简单,性能也可以满足绝大多数应用程序对 DNS 缓存的需求。

/# /etc/init.d/dnsmasq start        #启动 dnsmasq
 * Starting DNS forwarder and DHCP server dnsmasq                    [ OK ]

dnsmasq迭代查询的dns IP应该还需要在相应的配置文件进行配置吧。。。

/# echo nameserver 127.0.0.1 > /etc/resolv.conf     #修改 /etc/resolv.conf,将 DNS 服务器改为 dnsmasq 的监听地址,这儿是 127.0.0.1
/# time nslookup time.geekbang.org
    Server:    127.0.0.1
    Address:  127.0.0.1#53
    Non-authoritative answer:
    Name:  time.geekbang.org
    Address: 39.106.233.176
    real  0m0.492s
    user  0m0.007s
    sys  0m0.006s
/# time nslookup time.geekbang.org
    Server:    127.0.0.1
    Address:  127.0.0.1#53
    Non-authoritative answer:
    Name:  time.geekbang.org
    Address: 39.106.233.176
    real  0m0.011s
    user  0m0.008s
    sys  0m0.003s
DNS概述;nslookup工具;dig工具;手工配置 DNS 缓存:dnsmasq工具
案例 1:DNS 解析失败;案例 2:DNS 解析不稳定
======================================================================================================
案例 1:DNS 解析失败
    $ docker run -it --rm -v $(mktemp):/etc/resolv.conf feisky/dnsutils bash


    /# nslookup time.geekbang.org   #容器内执行解析命令
    ;; connection timed out; no servers could be reached    #这个命令阻塞很久后,还是失败了,报了 connection timed out 和 no servers could be reached 错误。


    /# ping -c3 114.114.114.114     #容器网络没问题
    PING 114.114.114.114 (114.114.114.114): 56 data bytes
    64 bytes from 114.114.114.114: icmp_seq=0 ttl=56 time=31.116 ms
    64 bytes from 114.114.114.114: icmp_seq=1 ttl=60 time=31.245 ms
    64 bytes from 114.114.114.114: icmp_seq=2 ttl=68 time=31.128 ms
    --- 114.114.114.114 ping statistics ---
    3 packets transmitted, 3 packets received, 0% packet loss
    round-trip min/avg/max/stddev = 31.116/31.163/31.245/0.058 ms


    /# nslookup -debug time.geekbang.org    #使用其他工具进行调试
    ;; Connection to 127.0.0.1#53(127.0.0.1) for time.geekbang.org failed: connection refused.
    ;; Connection to ::1#53(::1) for time.geekbang.org failed: address not available.
        #nslookup 连接环回地址(127.0.0.1 和 ::1)的 53 端口失败。这里就有问题了,为什么会去连接环回地址,而不是我们的先前看到的 114.114.114.114


    /# cat /etc/resolv.conf     #确定原因:该容器未配置dns服务器地址


-------------------------------------------------------------------------------------------------------
案例 2:DNS 解析不稳定

$ docker run -it --rm --cap-add=NET_ADMIN --dns 8.8.8.8 feisky/dnsutils bash

/# time nslookup time.geekbang.org
    Server:    8.8.8.8
    Address:  8.8.8.8#53
    Non-authoritative answer:
    Name:  time.geekbang.org
    Address: 39.106.233.176
    real  0m10.349s         #解析非常慢,居然用了 10 秒
    user  0m0.004s
    sys  0m0.0

/# time nslookup time.geekbang.org  #多次运行上面的 nslookup 命令,可能偶尔还会碰到下面这种错误
    ;; connection timed out; no servers could be reached
    real  0m15.011s
    user  0m0.006s
    sys  0m0.006s


/# ping -c3 8.8.8.8
    PING 8.8.8.8 (8.8.8.8): 56 data bytes
    64 bytes from 8.8.8.8: icmp_seq=0 ttl=31 time=137.637 ms
    64 bytes from 8.8.8.8: icmp_seq=1 ttl=31 time=144.743 ms        #延迟已经达到了 140ms
    64 bytes from 8.8.8.8: icmp_seq=2 ttl=31 time=138.576 ms
    --- 8.8.8.8 ping statistics ---
    3 packets transmitted, 3 packets received, 0% packet loss
    round-trip min/avg/max/stddev = 137.637/140.319/144.743/3.152 ms

$ ping -c3 8.8.8.8
    PING 8.8.8.8 (8.8.8.8): 56 data bytes
    64 bytes from 8.8.8.8: icmp_seq=0 ttl=30 time=134.032 ms
    64 bytes from 8.8.8.8: icmp_seq=1 ttl=30 time=431.458 ms
    --- 8.8.8.8 ping statistics ---
    3 packets transmitted, 2 packets received, 33% packet loss      #出现了丢包
    round-trip min/avg/max/stddev = 134.032/282.745/431.458/148.713 ms


因为到8.8.8.8的网络质量差,所以更换dns服务器
/# echo nameserver 114.114.114.114 > /etc/resolv.conf
案例 1:DNS 解析失败;案例 2:DNS 解析不稳定

38 | 案例篇:怎么使用 tcpdump 和 Wireshark 分析网络流量?

tcpdump 和 Wireshark 工具介绍
========================================================
tcpdump 也是最常用的一个网络分析工具。它基于 libpcap  ,利用内核中的 AF_PACKET 套接字,抓取网络接口中传输的网络包;并提供了强大的过滤规则,帮你从大量的网络包中,挑出最想关注的信息。
查看 tcpdump 的 手册  ,以及 pcap-filter 的手册,你会发现,tcpdump 提供了大量的选项以及各式各样的过滤表达式。不过不要担心,只需要掌握一些常用选项和过滤表达式,就可以满足大部分场景的需要了。


Wireshark 也是最流行的一个网络分析工具,它最大的好处就是提供了跨平台的图形界面。跟 tcpdump 类似,Wireshark 也提供了强大的过滤规则表达式,同时,还内置了一系列的汇总分析工具。
tcpdump 和 Wireshark 工具介绍
案例:ping包慢
============================================================================
$ ping -c3 geektime.org
    PING geektime.org (35.190.27.188) 56(84) bytes of data.
    64 bytes from 35.190.27.188 (35.190.27.188): icmp_seq=1 ttl=43 time=36.8 ms
    64 bytes from 35.190.27.188 (35.190.27.188): icmp_seq=2 ttl=43 time=31.1 ms
    64 bytes from 35.190.27.188 (35.190.27.188): icmp_seq=3 ttl=43 time=31.2 ms
    --- geektime.org ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 11049ms #3 次发送,收到 3 次响应,没有丢包,但三次发送和接受的总时间居然超过了 11s(11049ms)
    rtt min/avg/max/mdev = 31.146/33.074/36.809/2.649 ms


# 禁止接收从DNS服务器发送过来并包含googleusercontent的包
$ iptables -I INPUT -p udp --sport 53 -m string --string googleusercontent --algo bm -j DROP
    #如果换一个 DNS 服务器,就可以用 PTR 反查到 35.190.27.188 所对应的域名;就不能复现ping包慢的现象了


$ time nslookup geektime.org
    Server:    114.114.114.114
    Address:  114.114.114.114#53
    Non-authoritative answer:
    Name:  geektime.org
    Address: 35.190.27.188
    real  0m0.044s      #域名解析还是很快的,只需要 44ms,显然比 11s 短了很多。
    user  0m0.006s
    sys  0m0.003s


那么为什么ping包的总市场耗费了11s?开始抓包分析
$ tcpdump -nn udp port 53 or host 35.190.27.188
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
    14:02:31.100564 IP 172.16.3.4.56669 > 114.114.114.114.53: 36909+ A? geektime.org. (30)
    14:02:31.507699 IP 114.114.114.114.53 > 172.16.3.4.56669: 36909 1/0/0 A 35.190.27.188 (46)
    14:02:31.508164 IP 172.16.3.4 > 35.190.27.188: ICMP echo request, id 4356, seq 1, length 64
    14:02:31.539667 IP 35.190.27.188 > 172.16.3.4: ICMP echo reply, id 4356, seq 1, length 64
    14:02:31.539995 IP 172.16.3.4.60254 > 114.114.114.114.53: 49932+ PTR? 188.27.190.35.in-addr.arpa. (44)  #自动进行反向解析?
    14:02:36.545104 IP 172.16.3.4.60254 > 114.114.114.114.53: 49932+ PTR? 188.27.190.35.in-addr.arpa. (44)
    14:02:41.551284 IP 172.16.3.4 > 35.190.27.188: ICMP echo request, id 4356, seq 2, length 64
    14:02:41.582363 IP 35.190.27.188 > 172.16.3.4: ICMP echo reply, id 4356, seq 2, length 64
    14:02:42.552506 IP 172.16.3.4 > 35.190.27.188: ICMP echo request, id 4356, seq 3, length 64
    14:02:42.583646 IP 35.190.27.188 > 172.16.3.4: ICMP echo reply, id 4356, seq 3, length 64


ping 缓慢的根源,正是两次 PTR 请求没有得到响应而超时导致的。
PTR 反向地址解析的目的,是从 IP 地址反查出域名,但事实上,并非所有 IP 地址都会定义 PTR 记录,所以 PTR 查询很可能会失败。

执行 man ping 命令,查询使用手册;加上 -n 选项禁止名称解析
$ ping -n -c3 geektime.org
    PING geektime.org (35.190.27.188) 56(84) bytes of data.
    64 bytes from 35.190.27.188: icmp_seq=1 ttl=43 time=33.5 ms
    64 bytes from 35.190.27.188: icmp_seq=2 ttl=43 time=39.0 ms
    64 bytes from 35.190.27.188: icmp_seq=3 ttl=43 time=32.8 ms
    --- geektime.org ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 2002ms
    rtt min/avg/max/mdev = 32.879/35.160/39.030/2.755 ms
案例:ping包慢

39 | 案例篇:怎么缓解 DDoS 攻击带来的性能下降问题?

DDoS说明;DoS攻击案例;针对DDOS攻击的优化
================================================================================================================================
DDoS 的前身是 DoS(Denail of Service),即拒绝服务攻击,指利用大量的合理请求,来占用过多的目标资源,从而使目标服务无法响应正常请求。
DDoS(Distributed Denial of Service) 则是在 DoS 的基础上,采用了分布式架构,利用多台主机同时攻击目标主机。这样,即使目标服务部署了网络防御设备,面对大量网络请求时,还是无力应对。
从攻击的原理上来看,DDoS 可以分为下面几种类型。
    第一种,耗尽带宽。无论是服务器还是路由器、交换机等网络设备,带宽都有固定的上限。带宽耗尽后,就会发生网络拥堵,从而无法传输其他正常的网络报文。
    第二种,耗尽操作系统的资源。网络服务的正常运行,都需要一定的系统资源,像是 CPU、内存等物理资源,以及连接表等软件资源。一旦资源耗尽,系统就不能处理其他正常的网络连接。
    第三种,消耗应用程序的运行资源。应用程序的运行,通常还需要跟其他的资源或系统交互。如果应用程序一直忙于处理无效请求,也会导致正常请求的处理变慢,甚至得不到响应。

------------------------------------------------------------------------------------------------------------------------------
案例:
本次案例用到三台虚拟机,1台服务器,1台client(攻击者),1台普通client

$ docker run -itd --name=nginx --network=host nginx
    # 运行Nginx服务并对外开放80端口
    # --network=host表示使用主机网络(这是为了方便后面排查问题)




$ curl -s -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null http://192.168.1.160/   ## -w表示只输出HTTP状态码及总时间,-o表示将响应重定向到/dev/null
    Http code: 200
    Total time:0.002s

运行 hping3 命令,来模拟 DoS 攻击:
$ hping3 -S -p 80 -i u10 192.168.1.160
    # -S参数表示设置TCP协议的SYN(同步序列号),-p表示目的端口为80
    # -i u10表示每隔10微秒发送一个网络帧

    如果你的现象不那么明显,那么请尝试把参数里面的 u10 调小(比如调成 u1),或者加上–flood 选项;
    如果你的终端一完全没有响应了,那么请适当调大 u10(比如调成 u30),否则后面就不能通过 SSH 操作 VM1。


普通client发起访问,发现超时问题
$ curl -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null --connect-timeout 10 http://192.168.0.30      #没有模拟出故障现象。。。
    ...
    Http code: 000
    Total time:10.001s
    curl: (28) Connection timed out after 10000 milliseconds


使用sar,经过计算,发现接收到大量小包;结果tcpdump抓包,发现了大量的SYS攻击,即TCP半开连接攻击
$ sar -n DEV 1
08:55:49        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
08:55:50      docker0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
08:55:50         eth0  22274.00    629.00   1174.64     37.78      0.00      0.00      0.00      0.02
08:55:50           lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
    #网络接收的 PPS 已经达到了 20000 多,但是 BPS 却只有 1174 kB,这样每个包的大小就只有 54B(1174*1024/22274=54)。


# -i eth0 只抓取eth0网卡,-n不解析协议名和主机名
# tcp port 80表示只抓取tcp协议并且端口号为80的网络帧
$ tcpdump -i eth0 -n tcp port 80
    09:15:48.287047 IP 192.168.0.2.27095 > 192.168.0.30: Flags [S], seq 1288268370, win 512, length 0
    09:15:48.287050 IP 192.168.0.2.27131 > 192.168.0.30: Flags [S], seq 2084255254, win 512, length 0
    09:15:48.287052 IP 192.168.0.2.27116 > 192.168.0.30: Flags [S], seq 677393791, win 512, length 0
    09:15:48.287055 IP 192.168.0.2.27141 > 192.168.0.30: Flags [S], seq 1276451587, win 512, length 0
    09:15:48.287068 IP 192.168.0.2.27154 > 192.168.0.30: Flags [S], seq 1851495339, win 512, length 0
    ...



# -n表示不解析名字,-p表示显示连接所属进程
$ netstat -n -p | grep SYN_REC
    tcp        0      0 192.168.0.30:80          192.168.0.2:12503      SYN_RECV    -
    tcp        0      0 192.168.0.30:80          192.168.0.2:13502      SYN_RECV    -
    tcp        0      0 192.168.0.30:80          192.168.0.2:15256      SYN_RECV    -
    tcp        0      0 192.168.0.30:80          192.168.0.2:18117      SYN_RECV    -
    ...
    从结果中,你可以发现大量 SYN_RECV 状态的连接,并且源 IP 地址为 192.168.0.2。

$ netstat -n -p | grep SYN_REC | wc -l
193 #通过 wc 工具,来统计所有 SYN_RECV 状态的连接数


问题解决:
$ iptables -I INPUT -s 192.168.0.2 -p tcp -j REJECT



----------------------------------------------------------------
你可以在 hping3 命令中,加入 --rand-source 选项,来随机化源 IP。
其他的优化方法:
# 限制syn并发数为每秒1次
$ iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT
# 限制单个IP在60秒新建立的连接数为10
$ iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT


----------------------------------------------------------------
针对DDOS攻击的优化:
$ sysctl net.ipv4.tcp_max_syn_backlog       #半开状态的连接数是有限制的,执行下面的命令,你就可以看到,默认的半连接容量只有 256
net.ipv4.tcp_max_syn_backlog = 256

$ sysctl -w net.ipv4.tcp_max_syn_backlog=1024
net.ipv4.tcp_max_syn_backlog = 1024         #增大半连接的容量



$ sysctl -w net.ipv4.tcp_synack_retries=1   #连接每个 SYN_RECV 时,如果失败的话,内核还会自动重试,并且默认的重试次数是 5 次
net.ipv4.tcp_synack_retries = 1         #将其减小为 1 次


$ sysctl -w net.ipv4.tcp_syncookies=1       #开启 SYN Cookies 后,就不需要维护半开连接状态了,进而也就没有了半连接数的限制。
net.ipv4.tcp_syncookies = 1     #注意,开启 TCP syncookies 后,内核选项 net.ipv4.tcp_max_syn_backlog 也就无效了。


上面的修改都是临时的。若要永久,则需要固化配置到文件
$ cat /etc/sysctl.conf          #写入 /etc/sysctl.conf 的配置,需要执行 sysctl -p 命令后,才会动态生效。
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_max_syn_backlog = 1024
DDoS说明;DoS攻击案例;针对DDOS攻击的优化

40 | 案例篇:网络请求延迟变大了,我该怎么办?

往返延时 RTT;应用程序延迟;延迟测试工具hping3、ping、traceroute
========================================================================================================================
往返延时 RTT(Round-Trip Time):
    双向的往返通信延迟,比如 ping 测试的结果

应用程序延迟
    从应用程序接收到请求,再到发回响应,全程所用的时间。
    通常,应用程序延迟也指的是往返延迟,是网络数据传输时间加上数据处理时间的和。


ping 基于 ICMP 协议,它通过计算 ICMP 回显响应报文与 ICMP 回显请求报文的时间差,来获得往返延时。
这个过程并不需要特殊认证,常被很多网络攻击利用,比如端口扫描工具 nmap、组包工具 hping3 等等。

为了避免这些问题,很多网络服务会把 ICMP 禁止掉,这也就导致我们无法用 ping ,来测试网络服务的可用性和往返延时。
这时,你可以用 traceroute 或 hping3 的 TCP 和 UDP 模式,来获取网络延迟。
$ hping3 -c 3 -S -p 80 baidu.com        # -c表示发送3次请求,-S表示设置TCP SYN,-p表示端口号为80
    HPING baidu.com (eth0 123.125.115.110): S set, 40 headers + 0 data bytes
    len=46 ip=123.125.115.110 ttl=51 id=47908 sport=80 flags=SA seq=0 win=8192 rtt=20.9 ms
    len=46 ip=123.125.115.110 ttl=51 id=6788  sport=80 flags=SA seq=1 win=8192 rtt=20.9 ms
    len=46 ip=123.125.115.110 ttl=51 id=37699 sport=80 flags=SA seq=2 win=8192 rtt=20.9 ms
    --- baidu.com hping statistic ---
    3 packets transmitted, 3 packets received, 0% packet loss
    round-trip min/avg/max = 20.9/20.9/20.9 ms      #从 hping3 的结果中,你可以看到,往返延迟 RTT 为 20.9ms。


$ traceroute --tcp -p 80 -n baidu.com       ## --tcp表示使用TCP协议,-p表示端口号,-n表示不对结果中的IP地址执行反向域名解析
    traceroute to baidu.com (123.125.115.110), 30 hops max, 60 byte packets
     1  * * *
     2  * * *
     3  * * *
     4  * * *
     5  * * *
     6  * * *
     7  * * *
     8  * * *
     9  * * *
    10  * * *
    11  * * *
    12  * * *
    13  * * *
    14  123.125.115.110  20.684 ms *  20.798 ms
        #traceroute 会在路由的每一跳发送三个包,并在收到响应后,输出往返延时。如果无响应或者响应超时(默认 5s),就会输出一个星号。
往返延时 RTT;应用程序延迟;延迟测试工具hping3、ping、traceroute
案例:延迟高(Nagle 算法+延时确认)
========================================================================================================================
server端运行2个容器
    $ docker run --network=host --name=good -itd nginx
    $ docker run --name nginx --network=host -itd feisky/nginx:latency

client端验证2个容器服务均正常
    # 80端口正常
    $ curl http://192.168.0.30
        <!DOCTYPE html>
        <html>
        ...
        <p><em>Thank you for using nginx.</em></p>
        </body>
        </html>
    # 8080端口正常
    $ curl http://192.168.0.30:8080
        ...
        <p><em>Thank you for using nginx.</em></p>
        </body>
        </html>


使用hping3测试2个容器服务
    # 测试80端口延迟
    $ hping3 -c 3 -S -p 80 192.168.0.30
        HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
        len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=80 flags=SA seq=0 win=29200 rtt=7.8 ms
        len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=80 flags=SA seq=1 win=29200 rtt=7.7 ms
        len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=80 flags=SA seq=2 win=29200 rtt=7.6 ms
        --- 192.168.0.30 hping statistic ---
        3 packets transmitted, 3 packets received, 0% packet loss
        round-trip min/avg/max = 7.6/7.7/7.8 ms     #从这个输出你可以看到,两个端口的延迟差不多,都是 7ms。不过,这只是单个请求的情况。换成并发请求的话,又会怎么样呢?

    # 测试8080端口延迟
    $ hping3 -c 3 -S -p 8080 192.168.0.30
        HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
        len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=8080 flags=SA seq=0 win=29200 rtt=7.7 ms
        len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=8080 flags=SA seq=1 win=29200 rtt=7.6 ms
        len=44 ip=192.168.0.30 ttl=64 DF id=0 sport=8080 flags=SA seq=2 win=29200 rtt=7.3 ms
        --- 192.168.0.30 hping statistic ---
        3 packets transmitted, 3 packets received, 0% packet loss
        round-trip min/avg/max = 7.3/7.6/7.7 ms     #从这个输出你可以看到,两个端口的延迟差不多,都是 7ms。不过,这只是单个请求的情况。换成并发请求的话,又会怎么样呢?


使用wrk命令进行100并发的测试
    # 测试80端口性能
    $ # wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30/
        Running 10s test @ http://192.168.0.30/
          2 threads and 100 connections
          Thread Stats   Avg      Stdev     Max   +/- Stdev
            Latency     9.19ms   12.32ms 319.61ms   97.80%      #官方 Nginx(监听在 80 端口)的平均延迟是 9.19ms,
            Req/Sec     6.20k   426.80     8.25k    85.50%
          Latency Distribution
             50%    7.78ms
             75%    8.22ms
             90%    9.14ms                                      #从延迟的分布上来看,官方 Nginx 90% 的请求,都可以在 9ms 以内完成
             99%   50.53ms
          123558 requests in 10.01s, 100.15MB read
        Requests/sec:  12340.91
        Transfer/sec:     10.00MB

    # 测试8080端口性能
    $ wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/
        Running 10s test @ http://192.168.0.30:8080/
          2 threads and 100 connections
          Thread Stats   Avg      Stdev     Max   +/- Stdev
            Latency    43.60ms    6.41ms  56.58ms   97.06%      #案例 Nginx 的平均延迟(监听在 8080 端口)则是 43.6ms
            Req/Sec     1.15k   120.29     1.92k    88.50%
          Latency Distribution
             50%   44.02ms                                      #从延迟的分布上来看,案例 Nginx 50% 的请求,就已经达到了 44 ms。
             75%   44.33ms
             90%   47.62ms
             99%   48.88ms
          22853 requests in 10.01s, 18.55MB read
        Requests/sec:   2283.31
        Transfer/sec:      1.85MB


再结合上面 hping3 的输出,我们很容易发现,案例 Nginx 在并发请求下的延迟增大了很多


server端进行抓包分析
$ tcpdump -nn tcp port 8080 -w nginx.pcap

一些wireshar图解的分析过程,未进行总结。直接说结论:
前面三次握手,以及第一次 HTTP 请求和响应还是挺快的,但第二次 HTTP 请求就比较慢了,特别是客户端在收到服务器第一个分组后,40ms 后才发出了 ACK 响应(图中蓝色行)。
   ***40ms 是 TCP 延迟确认(Delayed ACK)的最小超时时间。

查询 TCP 文档(执行 man tcp),你就会发现,只有 TCP 套接字专门设置了 TCP_QUICKACK ,才会开启快速确认模式;否则,默认情况下,采用的就是延迟确认机制:
    TCP_QUICKACK (since Linux 2.4.4)
                  Enable  quickack mode if set or disable quickack mode if cleared.  In quickack mode, acks are sent imme‐
                  diately, rather than delayed if needed in accordance to normal TCP operation.  This flag is  not  perma‐
                  nent,  it only enables a switch to or from quickack mode.  Subsequent operation of the TCP protocol will
                  once again enter/leave quickack mode depending on internal  protocol  processing  and  factors  such  as
                  delayed ack timeouts occurring and data transfer.  This option should not be used in code intended to be
                  portable.


为了验证我们的猜想,确认 wrk 的行为,我们可以用 strace ,来观察 wrk 为套接字设置了哪些 TCP 选项。
$ strace -f wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/
    ...
    setsockopt(52, SOL_TCP, TCP_NODELAY, [1], 4) = 0        #wrk 只设置了 TCP_NODELAY 选项,而没有设置 TCP_QUICKACK。这说明 wrk 采用的正是延迟确认,也就解释了上面这个 40ms 的问题。
    ...



这个案例还有Nagle 算法(纳格算法)的影响。
Nagle 算法,是 TCP 协议中用于减少小包发送数量的一种优化算法,目的是为了提高实际带宽的利用率。
    举个例子,当有效负载只有 1 字节时,再加上 TCP 头部和 IP 头部分别占用的 20 字节,整个网络包就是 41 字节,这样实际带宽的利用率只有 2.4%(1/41)。
    往大了说,如果整个网络带宽都被这种小包占满,那整个网络的有效利用率就太低了。
Nagle 算法规定,一个 TCP 连接上,最多只能有一个未被确认的未完成分组;在收到这个分组的 ACK 前,不发送其他分组。这些小分组会被组合起来,并在收到 ACK 后,用同一个分组发送出去。






所以这个案例的根因在于Nagle 算法(纳格算法)+延迟确认,其实就是tcp/ip的案例了。。

查询 tcp 的文档,你就会知道,只有设置了 TCP_NODELAY 后,Nagle 算法才会禁用。所以,我们只需要查看 Nginx 的 tcp_nodelay 选项就可以了。
$ docker exec nginx cat /etc/nginx/nginx.conf | grep tcp_nodelay
    tcp_nodelay    off;

优化tcp_nodelay并重启容器,再次测试
$ wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/
Running 10s test @ http://192.168.0.30:8080/
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     9.58ms   14.98ms 350.08ms   97.91%  #现在延迟已经缩短成了 9ms,跟我们测试的官方 Nginx 镜像是一样的(Nginx 默认就是开启 tcp_nodelay 的) 。
    Req/Sec     6.22k   282.13     6.93k    68.50%
  Latency Distribution
     50%    7.78ms
     75%    8.20ms
     90%    9.02ms      #现在延迟已经缩短成了 9ms,跟我们测试的官方 Nginx 镜像是一样的(Nginx 默认就是开启 tcp_nodelay 的) 。
     99%   73.14ms
  123990 requests in 10.01s, 100.50MB read
Requests/sec:  12384.04
Transfer/sec:     10.04MB





在发现网络延迟增大后,你可以用 traceroute、hping3、tcpdump、Wireshark、strace 等多种工具,来定位网络中的潜在问题。比如,
使用 hping3 以及 wrk 等工具,确认单次请求和并发请求情况的网络延迟是否正常。
使用 traceroute,确认路由是否正确,并查看路由中每一跳网关的延迟。
使用 tcpdump 和 Wireshark,确认网络包的收发是否正常。
使用 strace 等,观察应用程序对网络套接字的调用情况是否正常。
这样,你就可以依次从路由、网络包的收发、再到应用程序等,逐层排查,直到定位问题根源。
案例:延迟高(Nagle 算法+延时确认)

41 | 案例篇:如何优化 NAT 性能?(上)

NAT 原理;NAT分类;NAPT分类
====================================================================
NAT 技术可以重写 IP 数据包的源 IP 或者目的 IP,被普遍地用来解决公网 IP 地址短缺的问题。
它的主要原理就是,网络中的多台主机,通过共享同一个公网 IP 地址,来访问外网资源。同时,由于 NAT 屏蔽了内网网络,自然也就为局域网中的机器提供了安全隔离。

你既可以在支持网络地址转换的路由器(称为 NAT 网关)中配置 NAT,也可以在 Linux 服务器中配置 NAT。
如果采用第二种方式,Linux 服务器实际上充当的是“软”路由器的角色。


NAT 的主要目的,是实现地址转换。根据实现方式的不同,NAT 可以分为三类:
    静态 NAT,即内网 IP 与公网 IP 是一对一的永久映射关系;
    动态 NAT,即内网 IP 从公网 IP 池中,动态选择一个进行映射;
    网络地址端口转换 NAPT(Network Address and Port Translation),即把内网 IP 映射到公网 IP 的不同端口上,让多个内网 IP 可以共享同一个公网 IP 地址。

根据转换方式的不同,NAPT 可以分为三类。
    第一类是源地址转换 SNAT,即目的地址不变,只替换源 IP 或源端口。
        SNAT 主要用于,多个内网 IP 共享同一个公网 IP ,来访问外网资源的场景。
    第二类是目的地址转换 DNAT,即源 IP 保持不变,只替换目的 IP 或者目的端口。
        DNAT 主要通过公网 IP 的不同端口号,来访问内网的多种服务,同时会隐藏后端服务器的真实 IP 地址。
    第三类是双向地址转换,即同时使用 SNAT 和 DNAT。
        当接收到网络包时,执行 DNAT,把目的 IP 转换为内网 IP;而在发送网络包时,执行 SNAT,把源 IP 替换为外部 IP。
        双向地址转换,其实就是外网 IP 与内网 IP 的一对一映射关系,所以常用在虚拟化环境中,为虚拟机分配浮动的公网 IP 地址。
NAT 原理;NAT分类;NAPT分类
iptables 与 NAT;iptables配置SNAT、DNAT、FNAT
=======================================================================================================
iptables 与 NAT
    Linux 内核提供的 Netfilter 框架,允许对网络数据包进行修改(比如 NAT)和过滤(比如防火墙)。
    在这个基础上,iptables、ip6tables、ebtables 等工具,又提供了更易用的命令行接口,以便系统管理员配置和管理 NAT、防火墙的规则。
    其中,iptables 就是最常用的一种配置工具。要掌握 iptables 的原理和使用方法,最核心的就是弄清楚,网络数据包通过 Netfilter 时的工作流向

SNAT
    第一种方法,是为一个子网统一配置 SNAT,并由 Linux 选择默认的出口 IP。这实际上就是经常说的 MASQUERADE:
        $ iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -j MASQUERADE
    第二种方法,是为具体的 IP 地址配置 SNAT,并指定转换后的源地址:
        $ iptables -t nat -A POSTROUTING -s 192.168.0.2 -j SNAT --to-source 100.100.100.100

DNAT
    DNAT 需要在 nat 表的 PREROUTING 或者 OUTPUT 链中配置,其中, PREROUTING 链更常用一些(因为它还可以用于转发的包)。
        $ iptables -t nat -A PREROUTING -d 100.100.100.100 -j DNAT --to-destination 192.168.0.2

双向地址转换
    双向地址转换,就是同时添加 SNAT 和 DNAT 规则,为公网 IP 和内网 IP 实现一对一的映射关系,即:
        $ iptables -t nat -A POSTROUTING -s 192.168.0.2 -j SNAT --to-source 100.100.100.100
        $ iptables -t nat -A PREROUTING -d 100.100.100.100 -j DNAT --to-destination 192.168.0.2

在使用 iptables 配置 NAT 规则时,Linux 需要转发来自其他 IP 的网络包,所以你千万不要忘记开启 Linux 的 IP 转发功能。
    $ sysctl net.ipv4.ip_forward        #查看是否已开启IP转发
    net.ipv4.ip_forward = 1
    $ sysctl -w net.ipv4.ip_forward=1   #开启IP转发
    net.ipv4.ip_forward = 1
    $ cat /etc/sysctl.conf | grep ip_forward    #固化配置到 /etc/sysctl.conf 文件
    net.ipv4.ip_forward=1
iptables 与 NAT;iptables配置SNAT、DNAT、FNAT

 42 | 案例篇:如何优化 NAT 性能?(下)

SystemTap 和 stap工具
=================================================================================
SystemTap 是 Linux 的一种动态追踪框架,它把用户提供的脚本,转换为内核模块来执行,用来监测和跟踪内核的行为。
# Ubuntu
apt-get install -y systemtap-runtime systemtap
# Configure ddebs source
echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | \
sudo tee -a /etc/apt/sources.list.d/ddebs.list
# Install dbgsym
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F2EDC64DC5AEE1F6B9C621F0C8CAB6595FDFF622
apt-get update
apt install ubuntu-dbgsym-keyring
stap-prep
apt-get install linux-image-`uname -r`-dbgsym

# CentOS
yum install systemtap kernel-devel yum-utils kernel
stab-prep
SystemTap 和 stap工具
NAT案例:stap工具使用
====================================================================================================
server端:(nginx容器,正常数据,用于对比)
$ docker run --name nginx-hostnet --privileged --network=host -itd feisky/nginx:80


client端:
# open files
$ ulimit -n     #Linux 默认允许打开的文件描述数比较小,这个值只有 1024
1024
$ ulimit -n 65536   # 临时增大当前会话的最大文件描述符数

client端执行 ab 命令,进行压力测试
$ ab -c 5000 -n 100000 -r -s 2 http://192.168.0.30/ 
    # -c表示并发请求数为5000,-n表示总的请求数为10万
    # -r表示套接字接收错误时仍然继续执行,-s表示设置每个请求的超时时间为2s
    ...
    Requests per second:    6576.21 [#/sec] (mean)  #每秒请求数(Requests  per second)为 6576;
    Time per request:       760.317 [ms] (mean)     #每个请求的平均延迟(Time per request)为 760ms;
    Time per request:       0.152 [ms] (mean, across all concurrent requests)
    Transfer rate:          5390.19 [Kbytes/sec] received
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0  177 714.3      9    7338     #建立连接的平均延迟(Connect)为 177ms。
    Processing:     0   27  39.8     19     961
    Waiting:        0   23  39.5     16     951
    Total:          1  204 716.3     28    7349
    ...

-----------------------------------------------------------------
server端:(启动案例nginx容器)
$ docker rm -f nginx-hostnet
$ docker run --name nginx --privileged -p 8080:8080 -itd feisky/nginx:nat

$ iptables -nL -t nat   #执行 iptables 命令,确认 DNAT 规则已经创建
    Chain PREROUTING (policy ACCEPT)
    target     prot opt source               destination
    DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
    ...
    Chain DOCKER (2 references)
    target     prot opt source               destination
    RETURN     all  --  0.0.0.0/0            0.0.0.0/0
    DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:172.17.0.2:8080
    在 PREROUTING 链中,目的为本地的请求,会转到 DOCKER 链;而在 DOCKER 链中,目的端口为 8080 的 tcp 请求,会被 DNAT 到 172.17.0.2 的 8080 端口。其中,172.17.0.2 就是 Nginx 容器的 IP 地址。



client端再次执行 ab 命令,进行压力测试
$ ab -c 5000 -n 100000 -r -s 2 http://192.168.0.30:8080/
    ...
    apr_pollset_poll: The timeout specified has expired (70007)
    Total of 5602 requests completed
    #刚才正常运行的 ab ,现在失败了,还报了连接超时的错误。运行 ab 时的 -s 参数,设置了每个请求的超时时间为 2s,而从输出可以看到,这次只完成了 5602 个请求。

既然是为了得到 ab 的测试结果,我们不妨把超时时间延长一下试试,比如延长到 30s。延迟增大意味着要等更长时间,为了快点得到结果,我们可以同时把总测试次数,也减少到 10000:
$ ab -c 5000 -n 10000 -r -s 30 http://192.168.0.30:8080/
    ...
    Requests per second:    76.47 [#/sec] (mean)        #每秒请求数(Requests per second)为 76;
    Time per request:       65380.868 [ms] (mean)       #每个请求的延迟(Time per request)为 65s;
    Time per request:       13.076 [ms] (mean, across all concurrent requests)
    Transfer rate:          44.79 [Kbytes/sec] received
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0 1300 5578.0      1   65184        #建立连接的延迟(Connect)为 1300ms。
    Processing:     0 37916 59283.2      1  130682
    Waiting:        0    2   8.7      1     414
    Total:          1 39216 58711.6   1021  130682
    ...

每个指标都比前面差了很多!!!

-------------------------
排查分析:
可以使用 tcpdump 抓包的方法,找出了延迟增大的根源。那么今天的案例,我们仍然可以用类似的方法寻找线索。
不过,现在换个思路,因为今天我们已经事先知道了问题的根源——那就是 NAT

回忆一下 Netfilter 中,网络包的流向以及 NAT 的原理,你会发现,要保证 NAT 正常工作,就至少需要两个步骤:
    第一,利用 Netfilter 中的钩子函数(Hook),修改源地址或者目的地址。
    第二,利用连接跟踪模块 conntrack ,关联同一个连接的请求和响应。
是不是这两个地方出现了问题呢?我们用前面提到的动态追踪工具 SystemTap 来试试。
由于今天案例是在压测场景下,并发请求数大大降低,并且我们清楚知道 NAT 是罪魁祸首。所以,我们有理由怀疑,内核中发生了丢包现象。

在server端创建一个 dropwatch.stp 的脚本文件
    #! /usr/bin/env stap
    ############################################################
    # Dropwatch.stp
    # Author: Neil Horman <nhorman@redhat.com>
    # An example script to mimic the behavior of the dropwatch utility
    # http://fedorahosted.org/dropwatch
    ############################################################
    # Array to hold the list of drop points we find
    global locations
    # Note when we turn the monitor on and off
    probe begin { printf("Monitoring for dropped packets\n") }
    probe end { printf("Stopping dropped packet monitor\n") }
    # increment a drop counter for every location we drop at
    probe kernel.trace("kfree_skb") { locations[$location] <<< 1 }
    #这个脚本,跟踪内核函数 kfree_skb() 的调用,并统计丢包的位置。
    # Every 5 seconds report our drop locations
    probe timer.sec(5)
    {
      printf("\n")
      foreach (l in locations-) {
        printf("%d packets dropped at %s\n",
               @count(locations[l]), symname(l))
      }
      delete locations
    }
    
执行stap 命令,就可以运行丢包跟踪脚本
$ stap --all-modules dropwatch.stp      #stap,是 SystemTap 的命令行工具
Monitoring for dropped packets          #当你看到 probe begin 输出的 “Monitoring for dropped packets” 时,表明 SystemTap 已经将脚本编译为内核模块,并启动运行了。


client再次开始测试
$ ab -c 5000 -n 10000 -r -s 30 http://192.168.0.30:8080/



在server端观察 stap 命令的输出:
10031 packets dropped at nf_hook_slow       
676 packets dropped at tcp_v4_rcv
7284 packets dropped at nf_hook_slow
268 packets dropped at tcp_v4_rcv
    #大量丢包都发生在 nf_hook_slow 位置。这是在 Netfilter Hook 的钩子函数中,出现丢包问题了
    但是不是 NAT,还不能确定。接下来,我们还得再跟踪  nf_hook_slow 的执行过程,这一步可以通过 perf 来完成。

$ perf record -a -g -- sleep 30     # 记录一会(比如30s)后按Ctrl+C结束
$ perf report -g graph,0            # 输出报告
    
在 perf report 界面中,输入查找命令 / 然后,在弹出的对话框中,输入 nf_hook_slow;最后再展开调用栈,就可以得到下面这个调用图:

从这个图我们可以看到,nf_hook_slow 调用最多的有三个地方,分别是 ipv4_conntrack_in、br_nf_pre_routing 以及 iptable_nat_ipv4_in。
换言之,nf_hook_slow 主要在执行三个动作。
    第一,接收网络包时,在连接跟踪表中查找连接,并为新的连接分配跟踪对象(Bucket)。
    第二,在 Linux 网桥中转发包。这是因为案例 Nginx 是一个 Docker 容器,而容器的网络通过网桥来实现;
    第三,接收网络包时,执行 DNAT,即把 8080 端口收到的包转发给容器。


这三个来源,都是 Linux 的内核机制,所以接下来的优化,自然也是要从内核入手。
    Linux 内核为用户提供了大量的可配置选项,这些选项可以通过 proc 文件系统,或者 sys 文件系统,来查看和修改。
    除此之外,你还可以用 sysctl 这个命令行工具,来查看和修改内核配置。

我们今天的主题是 DNAT,而 DNAT 的基础是 conntrack,所以我们可以先看看,内核提供了哪些 conntrack 的配置选项。
    $ sysctl -a | grep conntrack
    net.netfilter.nf_conntrack_count = 180      #表示当前连接跟踪数;
    net.netfilter.nf_conntrack_max = 1000       #表示最大连接跟踪数;
    net.netfilter.nf_conntrack_buckets = 65536  #表示连接跟踪表的大小
    net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60
    net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120
    net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
    ...

回想一下前面的 ab 命令,并发请求数是 5000,而请求数是 100000。显然,跟踪表设置成,只记录 1000 个连接,是远远不够的。

实际上,内核在工作异常时,会把异常信息记录到日志中。比如前面的 ab 测试,内核已经在日志中报出了 “nf_conntrack: table full” 的错误。
执行 dmesg 命令,你就可以看到:
    $ dmesg | tail
    [104235.156774] nf_conntrack: nf_conntrack: table full, dropping packet
    [104243.800401] net_ratelimit: 3939 callbacks suppressed                    #net_ratelimit 表示有大量的日志被压缩掉了,这是内核预防日志攻击的一种措施。
    [104243.800401] nf_conntrack: nf_conntrack: table full, dropping packet     #看到 “nf_conntrack: table full” 的错误时,就表明 nf_conntrack_max 太小了。
    [104262.962157] nf_conntrack: nf_conntrack: table full, dropping packet


那是不是,直接把连接跟踪表调大就可以了呢?调节前,你先得明白,连接跟踪表,实际上是内存中的一个哈希表。如果连接跟踪数过大,也会耗费大量内存。
其实,我们上面看到的 nf_conntrack_buckets,就是哈希表的大小。
哈希表中的每一项,都是一个链表(称为 Bucket),而链表长度,就等于 nf_conntrack_max 除以 nf_conntrack_buckets。

    # 连接跟踪对象大小为376,链表项大小为16
    nf_conntrack_max*连接跟踪对象大小+nf_conntrack_buckets*链表项大小 
    = 1000*376+65536*16 B
    = 1.4 MB
接下来,我们将 nf_conntrack_max 改大一些,比如改成 131072(即 nf_conntrack_buckets 的 2 倍):
    $ sysctl -w net.netfilter.nf_conntrack_max=131072
    $ sysctl -w net.netfilter.nf_conntrack_buckets=65536


调整后,client重新进行测试
$ ab -c 5000 -n 100000 -r -s 2 http://192.168.0.30:8080/
    ...
    Requests per second:    6315.99 [#/sec] (mean)
    Time per request:       791.641 [ms] (mean)
    Time per request:       0.158 [ms] (mean, across all concurrent requests)
    Transfer rate:          4985.15 [Kbytes/sec] received
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0  355 793.7     29    7352
    Processing:     8  311 855.9     51   14481
    Waiting:        0  292 851.5     36   14481
    Total:         15  666 1216.3    148   14645
每秒请求数(Requests per second)为 6315(不用 NAT 时为 6576);
每个请求的延迟(Time per request)为 791ms(不用 NAT 时为 760ms);
建立连接的延迟(Connect)为 355ms(不用 NAT 时为 177ms)。
    确认NAT问题已经得到修复


----------------------------------------------------------------
连接跟踪表里,到底都包含了哪些东西?可以用 conntrack 命令行工具,来查看连接跟踪表的内容。
$ conntrack -L -o extended | head       # -L表示列表,-o表示以扩展格式显示
ipv4     2 tcp      6 7 TIME_WAIT src=192.168.0.2 dst=192.168.0.96 sport=51744 dport=8080 src=172.17.0.2 dst=192.168.0.2 sport=8080 dport=51744 [ASSURED] mark=0 use=1
ipv4     2 tcp      6 6 TIME_WAIT src=192.168.0.2 dst=192.168.0.96 sport=51524 dport=8080 src=172.17.0.2 dst=192.168.0.2 sport=8080 dport=51524 [ASSURED] mark=0 use=1
    连接跟踪表里的对象,包括了协议、连接状态、源 IP、源端口、目的 IP、目的端口、跟踪状态等。
    由于这个格式是固定的,所以我们可以用 awk、sort 等工具,对其进行统计分析。

    # 统计总的连接跟踪数
    $ conntrack -L -o extended | wc -l
    14289
    # 统计TCP协议各个状态的连接跟踪数
    $ conntrack -L -o extended | awk '/^.*tcp.*$/ {sum[$6]++} END {for(i in sum) print i, sum[i]}'
    SYN_RECV 4
    CLOSE_WAIT 9
    ESTABLISHED 2877
    FIN_WAIT 3
    SYN_SENT 2113
    TIME_WAIT 9283
    # 统计各个源IP的连接跟踪数
    $ conntrack -L -o extended | awk '{print $7}' | cut -d "=" -f 2 | sort | uniq -c | sort -nr | head -n 10
      14116 192.168.0.2
        172 192.168.0.96
    这里统计了总连接跟踪数,TCP 协议各个状态的连接跟踪数,以及各个源 IP 的连接跟踪数。
    你可以看到,大部分 TCP 的连接跟踪,都处于 TIME_WAIT 状态,并且它们大都来自于 192.168.0.2 这个 IP 地址(也就是运行 ab 命令的 VM2)。


这些处于 TIME_WAIT 的连接跟踪记录,会在超时后清理,而默认的超时时间是 120s,你可以执行下面的命令来查看:
$ sysctl net.netfilter.nf_conntrack_tcp_timeout_time_wait
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
    #如果你的连接数非常大,确实也应该考虑,适当减小超时时间。
    








[root@yefeng ~]# conntrack -L -o extended
ipv4     2 tcp      6 429696 ESTABLISHED src=192.168.1.55 dst=192.168.1.88 sport=51522 dport=22 src=192.168.1.88 dst=192.168.1.55 sport=22 dport=51522 [ASSURED] mark=0 use=1
ipv4     2 tcp      6 97 TIME_WAIT src=192.168.1.88 dst=39.155.141.16 sport=57874 dport=80 src=39.155.141.16 dst=192.168.1.88 sport=80 dport=57874 [ASSURED] mark=0 use=1
ipv4     2 udp      17 18 src=192.168.1.1 dst=255.255.255.255 sport=1024 dport=5001 [UNREPLIED] src=255.255.255.255 dst=192.168.1.1 sport=5001 dport=1024 mark=0 use=1
ipv4     2 tcp      6 431999 ESTABLISHED src=192.168.1.55 dst=192.168.1.88 sport=51521 dport=22 src=192.168.1.88 dst=192.168.1.55 sport=22 dport=51521 [ASSURED] mark=0 use=1
ipv4     2 udp      17 6 src=192.168.1.88 dst=192.168.1.1 sport=51563 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=51563 mark=0 use=1
ipv4     2 udp      17 4 src=192.168.1.88 dst=192.168.1.1 sport=40029 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=40029 mark=0 use=1
ipv4     2 tcp      6 114 TIME_WAIT src=192.168.1.88 dst=113.215.232.230 sport=58230 dport=80 src=113.215.232.230 dst=192.168.1.88 sport=80 dport=58230 [ASSURED] mark=0 use=1
ipv4     2 udp      17 6 src=192.168.1.88 dst=192.168.1.1 sport=50013 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=50013 mark=0 use=1
ipv4     2 udp      17 5 src=192.168.1.88 dst=192.168.1.1 sport=46460 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=46460 mark=0 use=1
ipv4     2 udp      17 1 src=192.168.1.150 dst=224.0.0.251 sport=5353 dport=5353 [UNREPLIED] src=224.0.0.251 dst=192.168.1.150 sport=5353 dport=5353 mark=0 use=1
ipv4     2 tcp      6 97 TIME_WAIT src=192.168.1.88 dst=101.6.15.130 sport=50882 dport=443 src=101.6.15.130 dst=192.168.1.88 sport=443 dport=50882 [ASSURED] mark=0 use=1
ipv4     2 udp      17 4 src=192.168.1.88 dst=192.168.1.1 sport=38380 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=38380 mark=0 use=1
ipv4     2 tcp      6 114 TIME_WAIT src=192.168.1.88 dst=113.215.232.230 sport=58244 dport=80 src=113.215.232.230 dst=192.168.1.88 sport=80 dport=58244 [ASSURED] mark=0 use=1
ipv4     2 tcp      6 114 TIME_WAIT src=192.168.1.88 dst=113.215.232.226 sport=58156 dport=80 src=113.215.232.226 dst=192.168.1.88 sport=80 dport=58156 [ASSURED] mark=0 use=1
ipv4     2 udp      17 5 src=192.168.1.88 dst=192.168.1.1 sport=38009 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=38009 mark=0 use=1
ipv4     2 tcp      6 27 LAST_ACK src=192.168.1.88 dst=69.195.83.87 sport=34414 dport=80 src=69.195.83.87 dst=192.168.1.88 sport=80 dport=34414 [ASSURED] mark=0 use=1
ipv4     2 udp      17 10 src=192.168.1.88 dst=192.168.1.1 sport=35936 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=35936 mark=0 use=1
ipv4     2 tcp      6 4 CLOSE src=192.168.1.88 dst=13.212.21.54 sport=56032 dport=443 src=13.212.21.54 dst=192.168.1.88 sport=443 dport=56032 [ASSURED] mark=0 use=1
ipv4     2 udp      17 11 src=192.168.1.88 dst=192.168.1.1 sport=41590 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=41590 mark=0 use=1
ipv4     2 tcp      6 29 LAST_ACK src=192.168.1.88 dst=101.6.15.130 sport=50880 dport=443 src=101.6.15.130 dst=192.168.1.88 sport=443 dport=50880 [ASSURED] mark=0 use=1
ipv4     2 udp      17 10 src=192.168.1.88 dst=192.168.1.1 sport=43122 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=43122 mark=0 use=2
ipv4     2 udp      17 1 src=10.0.1.150 dst=224.0.0.251 sport=5353 dport=5353 [UNREPLIED] src=224.0.0.251 dst=10.0.1.150 sport=5353 dport=5353 mark=0 use=1
ipv4     2 tcp      6 95 TIME_WAIT src=192.168.1.88 dst=111.186.58.212 sport=59260 dport=443 src=111.186.58.212 dst=192.168.1.88 sport=443 dport=59260 [ASSURED] mark=0 use=1
ipv4     2 udp      17 3 src=192.168.1.88 dst=192.168.1.1 sport=38654 dport=53 src=192.168.1.1 dst=192.168.1.88 sport=53 dport=38654 mark=0 use=1
ipv4     2 udp      17 27 src=0.0.0.0 dst=255.255.255.255 sport=68 dport=67 [UNREPLIED] src=255.255.255.255 dst=0.0.0.0 sport=67 dport=68 mark=0 use=1
ipv4     2 udp      17 29 src=192.168.1.107 dst=192.168.1.255 sport=5684 dport=5684 [UNREPLIED] src=192.168.1.255 dst=192.168.1.107 sport=5684 dport=5684 mark=0 use=1
conntrack v1.4.4 (conntrack-tools): 26 flow entries have been shown.
[root@yefeng ~]# netstat -natp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 192.168.122.1:53        0.0.0.0:*               LISTEN      1822/dnsmasq
tcp        0      0 127.0.0.1:6011          0.0.0.0:*               LISTEN      57023/sshd: root@pt
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      827/rpcbind
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      1498/master
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1387/sshd
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      1378/cupsd
tcp        0      0 192.168.1.88:22         192.168.1.55:51522      ESTABLISHED 57025/sshd: root@no
tcp        0      0 192.168.1.88:58244      113.215.232.230:80      TIME_WAIT   -
tcp        0      0 192.168.1.88:58230      113.215.232.230:80      TIME_WAIT   -
tcp        0      0 192.168.1.88:50882      101.6.15.130:443        TIME_WAIT   -
tcp        0      0 192.168.1.88:58156      113.215.232.226:80      TIME_WAIT   -
tcp        0     48 192.168.1.88:22         192.168.1.55:51521      ESTABLISHED 57023/sshd: root@pt
tcp        1      1 192.168.1.88:50880      101.6.15.130:443        LAST_ACK    -
tcp        0      0 192.168.1.88:57874      39.155.141.16:80        TIME_WAIT   -
tcp        1      1 192.168.1.88:34414      69.195.83.87:80         LAST_ACK    -
tcp        0      0 192.168.1.88:59260      111.186.58.212:443      TIME_WAIT   -
tcp6       0      0 ::1:6011                :::*                    LISTEN      57023/sshd: root@pt
tcp6       0      0 :::111                  :::*                    LISTEN      827/rpcbind
tcp6       0      0 :::22                   :::*                    LISTEN      1387/sshd
tcp6       0      0 ::1:631                 :::*                    LISTEN      1378/cupsd
tcp6       0      0 ::1:25                  :::*                    LISTEN      1498/master
NAT案例:stap工具使用

https://mp.weixin.qq.com/s/VYBs8iqf0HsNg9WAxktzYQ   记一次Docker/Kubernetes上无法解释的连接超时原因探寻之旅

 

其实遇到很多问题的时候多看看内核日志就知道了,linux很智能的,很多报错信息都在日志里面,越遇到系统优化层面,就多要看看内核日志,
我一般是使用journalctl -k -f来查看,有报错信息就Google,线上遇到nf_conntrack: table full,就是这样排查出来的,查看内核日志真的很重要,特别应用日志没看出什么来的时候

 

43 | 套路篇:网络性能优化的几个思路(上)

思路:确定优化目标
================================================================================================
网络性能优化的整体目标,是降低网络延迟(如 RTT)和提高吞吐量(如 BPS 和 PPS),但具体到不同应用中,每个指标的优化标准可能会不同,优先级顺序也大相径庭。
    NAT 网关案例,由于其直接影响整个数据中心的网络出入性能,所以 NAT 网关通常需要达到或接近线性转发,也就是说, PPS 是最主要的性能目标。
    再如,对于数据库、缓存等系统,快速完成网络收发,即低延迟,是主要的性能目标。
    对于我们经常访问的 Web 服务来说,则需要同时兼顾吞吐量和延迟。

所以,为了更客观合理地评估优化效果,我们首先应该明确优化的标准,即要对系统和应用程序进行基准测试,得到网络协议栈各层的基准性能。

------------------------------------------------------------------------
由于底层是其上方各层的基础,底层性能也就决定了高层性能。

首先是网络接口层和网络层,它们主要负责网络包的封装、寻址、路由,以及发送和接收。
    每秒可处理的网络包数 PPS,就是它们最重要的性能指标(特别是在小包的情况下)。你可以用内核自带的发包工具 pktgen ,来测试 PPS 的性能。
传输层的 TCP 和 UDP,它们主要负责网络传输。对它们而言,吞吐量(BPS)、连接数以及延迟,就是最重要的性能指标。你可以用 iperf 或 netperf ,来测试传输层的性能。
    要注意,网络包的大小,会直接影响这些指标的值。所以,通常,你需要测试一系列不同大小网络包的性能。
应用层,最需要关注的是吞吐量(BPS)、每秒请求数以及延迟等指标。你可以用 wrk、ab 等工具,来测试应用程序的性能。
    要注意的是,测试场景要尽量模拟生产环境,这样的测试才更有价值。比如,你可以到生产环境中,录制实际的请求情况,再到测试中回放。
思路:确定优化目标

 

网络性能工具

 

 

网络性能优化:要优化网络性能,肯定离不开 Linux 系统的网络协议栈和网络收发流程的辅助。从应用程序、套接字、传输层、网络层以及链路层等几个角度,分别来看网络性能优化的基本思路。

 

应用程序的优化
=========================================================================================================
应用程序,通常通过套接字接口进行网络操作。由于网络收发通常比较耗时,所以应用程序的优化,主要就是对网络 I/O 和进程自身的工作模型的优化。
从网络 I/O 的角度来说,主要有下面两种优化思路。
    第一种是最常用的 I/O 多路复用技术 epoll,主要用来取代 select 和 poll。这其实是解决 C10K 问题的关键,也是目前很多网络应用默认使用的机制。
    第二种是使用异步 I/O(Asynchronous I/O,AIO)。AIO 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。等到 I/O 完成后,系统会用事件通知的方式,告诉应用程序结果。不过,AIO 的使用比较复杂,你需要小心处理很多边缘情况。

从进程的工作模型来说,也有两种不同的模型用来优化。
    第一种,主进程 + 多个 worker 子进程。其中,主进程负责管理网络连接,而子进程负责实际的业务处理。这也是最常用的一种模型。
    第二种,监听到相同端口的多进程模型。在这种模型下,所有进程都会监听相同接口,并且开启 SO_REUSEPORT 选项,由内核负责,把请求负载均衡到这些监听进程中去。

除了网络 I/O 和进程的工作模型外,应用层的网络协议优化,也是至关重要的一点。
我总结了常见的几种优化方法:
    1.使用长连接取代短连接,可以显著降低 TCP 建立连接的成本。在每秒请求次数较多时,这样做的效果非常明显。
    2.使用内存等方式,来缓存不常变化的数据,可以降低网络 I/O 次数,同时加快应用程序的响应速度。
    3.使用 Protocol Buffer 等序列化的方式,压缩网络 I/O 的数据量,可以提高应用程序的吞吐。
    4.使用 DNS 缓存、预取、HTTPDNS 等方式,减少 DNS 解析的延迟,也可以提升网络 I/O 的整体速度。
应用程序的优化
套接字的优化:【调整系统参数】
=========================================================================================================
套接字可以屏蔽掉 Linux 内核中不同协议的差异,为应用程序提供统一的访问接口。每个套接字,都有一个读写缓冲区。
    读缓冲区,缓存了远端发过来的数据。如果读缓冲区已满,就不能再接收新的数据。
    写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会被阻塞。

为了提高网络的吞吐量,你通常需要调整这些缓冲区的大小。比如:
    增大每个套接字的缓冲区大小 net.core.optmem_max;
    增大套接字接收缓冲区大小 net.core.rmem_max 和发送缓冲区大小 net.core.wmem_max;
    增大 TCP 接收缓冲区大小 net.ipv4.tcp_rmem 和发送缓冲区大小 net.ipv4.tcp_wmem。


除此之外,套接字接口还提供了一些配置选项,用来修改网络连接的行为:
    为 TCP 连接设置 TCP_NODELAY 后,就可以禁用 Nagle 算法;
    为 TCP 连接开启 TCP_CORK 后,可以让小包聚合成大包后再发送(注意会阻塞小包的发送);
    使用 SO_SNDBUF 和 SO_RCVBUF ,可以分别调整套接字发送缓冲区和接收缓冲区的大小。
套接字的优化:【调整系统参数】

44 | 套路篇:网络性能优化的几个思路(下)

传输层的优化:TCP优化;UDP优化;:【调整系统参数】
================================================================================================================
TCP 协议的优化

TCP 提供了面向连接的可靠传输服务。要优化 TCP,我们首先要掌握 TCP 协议的基本原理,比如流量控制、慢启动、拥塞避免、延迟确认以及状态流图(如下图所示)等。

我分几类情况详细说明。
第一类,在请求数比较大的场景下,你可能会看到大量处于 TIME_WAIT 状态的连接,它们会占用大量内存和端口资源。这时,我们可以优化与 TIME_WAIT 状态相关的内核选项,比如采取下面几种措施。
    1.增大处于 TIME_WAIT 状态的连接数量 net.ipv4.tcp_max_tw_buckets ,并增大连接跟踪表的大小 net.netfilter.nf_conntrack_max。
    2.减小 net.ipv4.tcp_fin_timeout 和 net.netfilter.nf_conntrack_tcp_timeout_time_wait ,让系统尽快释放它们所占用的资源。
    3.开启端口复用 net.ipv4.tcp_tw_reuse。这样,被 TIME_WAIT 状态占用的端口,还能用到新建的连接中。
    4.增大本地端口的范围 net.ipv4.ip_local_port_range 。这样就可以支持更多连接,提高整体的并发能力。
    5.增加最大文件描述符的数量。你可以使用 fs.nr_open 和 fs.file-max ,分别增大进程和系统的最大文件描述符数;或在应用程序的 systemd 配置文件中,配置 LimitNOFILE ,设置应用程序的最大文件描述符数。

第二类,为了缓解 SYN FLOOD 等,利用 TCP 协议特点进行攻击而引发的性能问题,你可以考虑优化与 SYN 状态相关的内核选项,比如采取下面几种措施。
    1.增大 TCP 半连接的最大数量 net.ipv4.tcp_max_syn_backlog ,或者开启 TCP SYN Cookies net.ipv4.tcp_syncookies ,来绕开半连接数量限制的问题(注意,这两个选项不可同时使用)。
    2.减少 SYN_RECV 状态的连接重传 SYN+ACK 包的次数 net.ipv4.tcp_synack_retries。

第三类,在长连接的场景中,通常使用 Keepalive 来检测 TCP 连接的状态,以便对端连接断开后,可以自动回收。但是,系统默认的 Keepalive 探测间隔和重试次数,一般都无法满足应用程序的性能要求。所以,这时候你需要优化与 Keepalive 相关的内核选项,比如:
    1.缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time;
    2.缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl;
    3.减少 Keepalive 探测失败后,一直到通知应用程序前的重试次数 net.ipv4.tcp_keepalive_probes。


优化 TCP 性能时,你还要注意,如果同时使用不同优化方法,可能会产生冲突。
比如,就像网络请求延迟的案例中我们曾经分析过的,服务器端开启 Nagle 算法,而客户端开启延迟确认机制,就很容易导致网络延迟增大。

另外,在使用  NAT 的服务器上,如果开启 net.ipv4.tcp_tw_recycle ,就很容易导致各种连接失败。实际上,由于坑太多,这个选项在内核的 4.1 版本中已经删除了。

-----------------------------------------------------------------------------------------------------------------
UDP 提供了面向数据报的网络协议,它不需要网络连接,也不提供可靠性保障。所以,UDP 优化,相对于 TCP 来说,要简单得多。这里我也总结了常见的几种优化方案。
    跟上篇套接字部分提到的一样,增大套接字缓冲区大小以及 UDP 缓冲区范围;
    跟前面 TCP 部分提到的一样,增大本地端口号的范围;
    根据 MTU 大小,调整 UDP 数据包的大小,减少或者避免分片的发生。
传输层的优化:TCP优化;UDP优化;:【调整系统参数】

网络层的优化:【调整系统参数】
================================================================================================================
网络层,负责网络包的封装、寻址和路由,包括 IP、ICMP 等常见协议。在网络层,最主要的优化,其实就是对路由、 IP 分片以及 ICMP 等进行调优。
    第一种,从路由和转发的角度出发,你可以调整下面的内核选项。
        1.在需要转发的服务器中,比如用作 NAT 网关的服务器或者使用 Docker 容器时,开启 IP 转发,即设置 net.ipv4.ip_forward = 12.调整数据包的生存周期 TTL,比如设置 net.ipv4.ip_default_ttl = 64。注意,增大该值会降低系统性能。
        3.开启数据包的反向地址校验,比如设置 net.ipv4.conf.eth0.rp_filter = 1。这样可以防止 IP 欺骗,并减少伪造 IP 带来的 DDoS 问题。

    第二种,从分片的角度出发,最主要的是调整 MTU(Maximum Transmission Unit)的大小。
        通常,MTU 的大小应该根据以太网的标准来设置。以太网标准规定,一个网络帧最大为 1518B,那么去掉以太网头部的 18B 后,剩余的 1500 就是以太网 MTU 的大小。
        在使用 VXLAN、GRE 等叠加网络技术时,要注意,网络叠加会使原来的网络包变大,导致 MTU 也需要调整。
        比如,就以 VXLAN 为例,它在原来报文的基础上,增加了 14B 的以太网头部、 8B 的 VXLAN 头部、8B 的 UDP 头部以及 20B 的 IP 头部。换句话说,每个包比原来增大了 50B。
            所以,我们就需要把交换机、路由器等的 MTU,增大到 1550, 或者把 VXLAN 封包前(比如虚拟化环境中的虚拟网卡)的 MTU 减小为 1450。
            另外,现在很多网络设备都支持巨帧,如果是这种环境,你还可以把 MTU 调大为 9000,以提高网络吞吐量。

    第三种,从 ICMP 的角度出发,为了避免 ICMP 主机探测、ICMP Flood 等各种网络问题,你可以通过内核选项,来限制 ICMP 的行为。
        比如,你可以禁止 ICMP 协议,即设置 net.ipv4.icmp_echo_ignore_all = 1。这样,外部主机就无法通过 ICMP 来探测主机。
        或者,你还可以禁止广播 ICMP,即设置 net.ipv4.icmp_echo_ignore_broadcasts = 1。
网络层的优化:【调整系统参数】
链路层的优化:【调整or开启网卡特性】
================================================================================================================
链路层负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。自然,链路层的优化,也是围绕这些基本功能进行的。接下来,我们从不同的几个方面分别来看。
由于网卡收包后调用的中断处理程序(特别是软中断),需要消耗大量的 CPU。所以,将这些中断处理程序调度到不同的 CPU 上执行,就可以显著提高网络吞吐量。这通常可以采用下面两种方法。
    1.你可以为网卡硬中断配置 CPU 亲和性(smp_affinity),或者开启 irqbalance 服务。
    2.你可以开启 RPS(Receive Packet Steering)和 RFS(Receive Flow Steering),将应用程序和软中断的处理,调度到相同 CPU 上,这样就可以增加 CPU 缓存命中率,减少网络延迟。


另外,现在的网卡都有很丰富的功能,原来在内核中通过软件处理的功能,可以卸载到网卡中,通过硬件来执行。
    1.TSO(TCP Segmentation Offload)和 UFO(UDP Fragmentation Offload):在 TCP/UDP 协议中直接发送大包;而 TCP 包的分段(按照 MSS 分段)和 UDP 的分片(按照 MTU 分片)功能,由网卡来完成 。
    2.GSO(Generic Segmentation Offload):在网卡不支持 TSO/UFO 时,将 TCP/UDP 包的分段,延迟到进入网卡前再执行。这样,不仅可以减少 CPU 的消耗,还可以在发生丢包时只重传分段后的包。
    3.LRO(Large Receive Offload):在接收 TCP 分段包时,由网卡将其组装合并后,再交给上层网络处理。不过要注意,在需要 IP 转发的情况下,不能开启 LRO,因为如果多个包的头部信息不一致,LRO 合并会导致网络包的校验错误。
    4.GRO(Generic Receive Offload):GRO 修复了 LRO 的缺陷,并且更为通用,同时支持 TCP 和 UDP。
    5.RSS(Receive Side Scaling):也称为多队列接收,它基于硬件的多个接收队列,来分配网络接收进程,这样可以让多个 CPU 来处理接收到的网络包。
    6.VXLAN 卸载:也就是让网卡来完成 VXLAN 的组包功能。



最后,对于网络接口本身,也有很多方法,可以优化网络的吞吐量。
    1.你可以开启网络接口的多队列功能。这样,每个队列就可以用不同的中断号,调度到不同 CPU 上执行,从而提升网络的吞吐量。
    2.你可以增大网络接口的缓冲区大小,以及队列长度等,提升网络传输的吞吐量(注意,这可能导致延迟增大)。
    3.你还可以使用 Traffic Control 工具,为不同网络流量配置 QoS。



在单机并发 1000 万的场景中,对 Linux 网络协议栈进行的各种优化策略,基本都没有太大效果。因为这种情况下,网络协议栈的冗长流程,其实才是最主要的性能负担。
这时,我们可以用两种方式来优化。
    第一种,使用 DPDK 技术,跳过内核协议栈,直接由用户态进程用轮询的方式,来处理网络请求。同时,再结合大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
    第二种,使用内核自带的 XDP 技术,在网络包进入内核协议栈前,就对其进行处理,这样也可以实现很好的性能。
链路层的优化:【调整or开启网卡特性】

 

posted @ 2022-07-09 09:56  雲淡風輕333  阅读(389)  评论(0编辑  收藏  举报