This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
<audio id="audio" title="33 | 关于 Linux 网络,你必须知道这些(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/37/39/377ecc334777ee0d28cb8ca30de4b039.mp3"></audio>
你好,我是倪朋飞。
前几节,我们一起学习了文件系统和磁盘 I/O 的工作原理,以及相应的性能分析和优化方法。接下来,我们将进入下一个重要模块—— Linux 的网络子系统。
由于网络处理的流程最复杂,跟我们前面讲到的进程调度、中断处理、内存管理以及 I/O 等都密不可分,所以,我把网络模块作为最后一个资源模块来讲解。
同 CPU、内存以及 I/O 一样,网络也是 Linux 系统最核心的功能。网络是一种把不同计算机或网络设备连接到一起的技术,它本质上是一种进程间通信方式,特别是跨系统的进程间通信,必须要通过网络才能进行。随着高并发、分布式、云计算、微服务等技术的普及,网络的性能也变得越来越重要。
那么Linux 网络又是怎么工作的呢?又有哪些指标衡量网络的性能呢?接下来的两篇文章,我将带你一起学习 Linux 网络的工作原理和性能指标。
## 网络模型
说到网络,我想你肯定经常提起七层负载均衡、四层负载均衡,或者三层设备、二层设备等等。那么,这里说的二层、三层、四层、七层又都是什么意思呢?
实际上,这些层都来自国际标准化组织制定的**开放式系统互联通信参考模型**Open System Interconnection Reference Model简称为 OSI 网络模型。
为了解决网络互联中异构设备的兼容性问题并解耦复杂的网络包处理流程OSI 模型把网络互联的框架分为应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层等七层,每个层负责不同的功能。其中,
<li>
应用层,负责为应用程序提供统一的接口。
</li>
<li>
表示层,负责把数据转换成兼容接收系统的格式。
</li>
<li>
会话层,负责维护计算机之间的通信连接。
</li>
<li>
传输层,负责为数据加上传输表头,形成数据包。
</li>
<li>
网络层,负责数据的路由和转发。
</li>
<li>
数据链路层负责MAC寻址、错误侦测和改错。
</li>
<li>
物理层,负责在物理网络中传输数据帧。
</li>
但是 OSI 模型还是太复杂了,也没能提供一个可实现的方法。所以,在 Linux 中,我们实际上使用的是另一个更实用的四层模型,即 TCP/IP 网络模型。
TCP/IP 模型,把网络互联的框架分为应用层、传输层、网络层、网络接口层等四层,其中,
<li>
应用层,负责向用户提供一组应用程序,比如 HTTP、FTP、DNS 等。
</li>
<li>
传输层,负责端到端的通信,比如 TCP、UDP 等。
</li>
<li>
网络层,负责网络包的封装、寻址和路由,比如 IP、ICMP 等。
</li>
<li>
网络接口层,负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。
</li>
为了帮你更形象理解TCP/IP 与 OSI 模型的关系,我画了一张图,如下所示:
<img src="https://static001.geekbang.org/resource/image/f2/bd/f2dbfb5500c2aa7c47de6216ee7098bd.png" alt="">
当然了,虽说 Linux 实际按照 TCP/IP 模型,实现了网络协议栈,但在平时的学习交流中,我们习惯上还是用 OSI 七层模型来描述。比如,说到七层和四层负载均衡,对应的分别是 OSI 模型中的应用层和传输层(而它们对应到 TCP/IP 模型中,实际上是四层和三层)。
TCP/IP 模型包括了大量的网络协议这些协议的原理也是我们每个人必须掌握的核心基础知识。如果你不太熟练推荐你去学《TCP/IP 详解》的卷一和卷二,或者学习极客时间出品的《[趣谈网络协议](https://time.geekbang.org/course/intro/85)》专栏。
## Linux网络栈
有了 TCP/IP 模型后,在进行网络传输时,数据包就会按照协议栈,对上一层发来的数据进行逐层处理;然后封装上该层的协议头,再发送给下一层。
当然,网络包在每一层的处理逻辑,都取决于各层采用的网络协议。比如在应用层,一个提供 REST API 的应用,可以使用 HTTP 协议,把它需要传输的 JSON 数据封装到 HTTP 协议中,然后向下传递给 TCP 层。
而封装做的事情就很简单了,只是在原来的负载前后,增加固定格式的元数据,原始的负载数据并不会被修改。
比如,以通过 TCP 协议通信的网络包为例,通过下面这张图,我们可以看到,应用程序数据在每个层的封装格式。
<img src="https://static001.geekbang.org/resource/image/c8/79/c8dfe80acc44ba1aa9df327c54349e79.png" alt="">
其中:
<li>
传输层在应用程序数据前面增加了 TCP 头;
</li>
<li>
网络层在 TCP 数据包前增加了 IP 头;
</li>
<li>
而网络接口层,又在 IP 数据包前后分别增加了帧头和帧尾。
</li>
这些新增的头部和尾部,都按照特定的协议格式填充,想了解具体格式,你可以查看协议的文档。 比如,你可以查看[这里](https://zh.wikipedia.org/wiki/%E4%BC%A0%E8%BE%93%E6%8E%A7%E5%88%B6%E5%8D%8F%E8%AE%AE#%E5%B0%81%E5%8C%85%E7%B5%90%E6%A7%8B),了解 TCP 头的格式。
这些新增的头部和尾部增加了网络包的大小但我们都知道物理链路中并不能传输任意大小的数据包。网络接口配置的最大传输单元MTU就规定了最大的 IP 包大小。在我们最常用的以太网中MTU 默认值是 1500这也是 Linux 的默认值)。
一旦网络包超过 MTU 的大小,就会在网络层分片,以保证分片后的 IP 包不大于MTU 值。显然MTU 越大,需要的分包也就越少,自然,网络吞吐能力就越好。
理解了 TCP/IP 网络模型和网络包的封装原理后你很容易能想到Linux 内核中的网络栈,其实也类似于 TCP/IP 的四层结构。如下图所示,就是 Linux 通用 IP 网络栈的示意图:
<img src="https://static001.geekbang.org/resource/image/c7/ac/c7b5b16539f90caabb537362ee7c27ac.png" alt="">
(图片参考《性能之巅》图 10.7 通用 IP 网络栈绘制)
我们从上到下来看这个网络栈,你可以发现,
<li>
最上层的应用程序,需要通过系统调用,来跟套接字接口进行交互;
</li>
<li>
套接字的下面,就是我们前面提到的传输层、网络层和网络接口层;
</li>
<li>
最底层,则是网卡驱动程序以及物理网卡设备。
</li>
这里我简单说一下网卡。网卡是发送和接收网络包的基本设备。在系统启动过程中,网卡通过内核中的网卡驱动程序注册到系统中。而在网络收发过程中,内核通过中断跟网卡进行交互。
再结合前面提到的 Linux 网络栈,可以看出,网络包的处理非常复杂。所以,网卡硬中断只处理最核心的网卡数据读取或发送,而协议栈中的大部分逻辑,都会放到软中断中处理。
## Linux网络收发流程
了解了 Linux 网络栈后,我们再来看看, Linux 到底是怎么收发网络包的。
>
注意以下内容都以物理网卡为例。事实上Linux 还支持众多的虚拟网络设备,而它们的网络收发流程会有一些差别。
### 网络包的接收流程
我们先来看网络包的接收流程。
当一个网络帧到达网卡后,网卡会通过 DMA 方式,把这个网络包放到收包队列中;然后通过硬中断,告诉中断处理程序已经收到了网络包。
接着网卡中断处理程序会为网络帧分配内核数据结构sk_buff并将其拷贝到 sk_buff 缓冲区中;然后再通过软中断,通知内核收到了新的网络帧。
接下来,内核协议栈从缓冲区中取出网络帧,并通过网络协议栈,从下到上逐层处理这个网络帧。比如,
<li>
在链路层检查报文的合法性,找出上层协议的类型(比如 IPv4 还是 IPv6再去掉帧头、帧尾然后交给网络层。
</li>
<li>
网络层取出 IP 头,判断网络包下一步的走向,比如是交给上层处理还是转发。当网络层确认这个包是要发送到本机后,就会取出上层协议的类型(比如 TCP 还是 UDP去掉 IP 头,再交给传输层处理。
</li>
<li>
传输层取出 TCP 头或者 UDP 头后,根据 &lt;源 IP、源端口、目的 IP、目的端口&gt; 四元组作为标识,找出对应的 Socket并把数据拷贝到 Socket 的接收缓存中。
</li>
最后,应用程序就可以使用 Socket 接口,读取到新接收到的数据了。
为了更清晰表示这个流程,我画了一张图,这张图的左半部分表示接收流程,而图中的粉色箭头则表示网络包的处理路径。
<img src="https://static001.geekbang.org/resource/image/3a/65/3af644b6d463869ece19786a4634f765.png" alt="">
### 网络包的发送流程
了解网络包的接收流程后,就很容易理解网络包的发送流程。网络包的发送流程就是上图的右半部分,很容易发现,网络包的发送方向,正好跟接收方向相反。
首先,应用程序调用 Socket API比如 sendmsg发送网络包。
由于这是一个系统调用,所以会陷入到内核态的套接字层中。套接字层会把数据包放到 Socket 发送缓冲区中。
接下来,网络协议栈从 Socket 发送缓冲区中,取出数据包;再按照 TCP/IP 栈,从上到下逐层处理。比如,传输层和网络层,分别为其增加 TCP 头和 IP 头,执行路由查找确认下一跳的 IP并按照 MTU 大小进行分片。
分片后的网络包,再送到网络接口层,进行物理地址寻址,以找到下一跳的 MAC 地址。然后添加帧头和帧尾,放到发包队列中。这一切完成后,会有软中断通知驱动程序:发包队列中有新的网络帧需要发送。
最后,驱动程序通过 DMA ,从发包队列中读出网络帧,并通过物理网卡把它发送出去。
## **小结**
在今天的文章中,我带你一起梳理了 Linux 网络的工作原理。
多台服务器通过网卡、交换机、路由器等网络设备连接到一起,构成了相互连接的网络。由于网络设备的异构性和网络协议的复杂性,国际标准化组织定义了一个七层的 OSI 网络模型,但是这个模型过于复杂,实际工作中的事实标准,是更为实用的 TCP/IP 模型。
TCP/IP 模型,把网络互联的框架,分为应用层、传输层、网络层、网络接口层等四层,这也是 Linux 网络栈最核心的构成部分。
<li>
应用程序通过套接字接口发送数据包,先要在网络协议栈中从上到下进行逐层处理,最终再送到网卡发送出去。
</li>
<li>
而接收时,同样先经过网络栈从下到上的逐层处理,最终才会送到应用程序。
</li>
了解了Linux 网络的基本原理和收发流程后,你肯定迫不及待想知道,如何去观察网络的性能情况。那么,具体来说,哪些指标可以衡量 Linux 的网络性能呢?别急,我将在下一节中为你详细讲解。
## 思考
最后,我想请你来聊聊你所理解的 Linux 网络。你碰到过哪些网络相关的性能瓶颈?你又是怎么样来分析它们的呢?你可以结合今天学到的网络知识,提出自己的观点。
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,268 @@
<audio id="audio" title="34 | 关于 Linux 网络,你必须知道这些(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d1/da/d15ac64e53110f242c6fc7c1b8f121da.mp3"></audio>
你好,我是倪朋飞。
上一节,我带你学习了 Linux 网络的基础原理。简单回顾一下Linux 网络根据 TCP/IP 模型构建其网络协议栈。TCP/IP 模型由应用层、传输层、网络层、网络接口层等四层组成,这也是 Linux 网络栈最核心的构成部分。
应用程序通过套接字接口发送数据包时,先要在网络协议栈中从上到下逐层处理,然后才最终送到网卡发送出去;而接收数据包时,也要先经过网络栈从下到上的逐层处理,最后送到应用程序。
了解Linux 网络的基本原理和收发流程后,你肯定迫不及待想知道,如何去观察网络的性能情况。具体而言,哪些指标可以用来衡量 Linux 的网络性能呢?
## 性能指标
实际上我们通常用带宽、吞吐量、延时、PPSPacket Per Second等指标衡量网络的性能。
<li>
**带宽**,表示链路的最大传输速率,单位通常为 b/s (比特/秒)。
</li>
<li>
**吞吐量**,表示单位时间内成功传输的数据量,单位通常为 b/s比特/秒)或者 B/s字节/秒)。吞吐量受带宽限制,而吞吐量/带宽,也就是该网络的使用率。
</li>
<li>
**延时**,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT
</li>
<li>
**PPS**,是 Packet Per Second包/秒的缩写表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。
</li>
除了这些指标,**网络的可用性**(网络能否正常通信)、**并发连接数**TCP连接数量、**丢包率**(丢包百分比)、**重传率**(重新传输的网络包比例)等也是常用的性能指标。
接下来请你打开一个终端SSH登录到服务器上然后跟我一起来探索、观测这些性能指标。
## **网络配置**
分析网络问题的第一步,通常是查看网络接口的配置和状态。你可以使用 ifconfig 或者 ip 命令,来查看网络的配置。我个人更推荐使用 ip 工具,因为它提供了更丰富的功能和更易用的接口。
>
ifconfig 和 ip 分别属于软件包 net-tools 和 iproute2iproute2 是 net-tools 的下一代。通常情况下它们会在发行版中默认安装。但如果你找不到 ifconfig 或者 ip 命令,可以安装这两个软件包。
以网络接口 eth0 为例,你可以运行下面的两个命令,查看它的配置和状态:
```
$ ifconfig eth0
eth0: flags=4163&lt;UP,BROADCAST,RUNNING,MULTICAST&gt; 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&lt;link&gt;
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: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; 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 和 ip 命令输出的指标基本相同只是显示格式略微不同。比如它们都包括了网络接口的状态标志、MTU 大小、IP、子网、MAC 地址以及网络包收发的统计信息。
这些具体指标的含义,在文档中都有详细的说明,不过,这里有几个跟网络性能密切相关的指标,需要你特别关注一下。
第一网络接口的状态标志。ifconfig 输出中的 RUNNING ,或 ip 输出中的 LOWER_UP ,都表示物理网络是连通的,即网卡已经连接到了交换机或者路由器中。如果你看不到它们,通常表示网线被拔掉了。
第二MTU 的大小。MTU 默认大小是 1500根据网络架构的不同比如是否使用了 VXLAN 等叠加网络),你可能需要调大或者调小 MTU 的数值。
第三,网络接口的 IP 地址、子网以及 MAC 地址。这些都是保障网络功能正常工作所必需的,你需要确保配置正确。
第四,网络收发的字节数、包数、错误数以及丢包情况,特别是 TX 和 RX 部分的 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,通常表示出现了网络 I/O 问题。其中:
<li>
errors 表示发生错误的数据包数,比如校验错误、帧同步错误等;
</li>
<li>
dropped 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer但因为内存不足等原因丢包
</li>
<li>
overruns 表示超限数据包数,即网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;
</li>
<li>
carrier 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等;
</li>
<li>
collisions 表示碰撞数据包数。
</li>
## **套接字信息**
ifconfig 和 ip 只显示了网络接口收发数据包的统计信息,但在实际的性能问题中,网络协议栈中的统计信息,我们也必须关注。你可以用 netstat 或者 ss ,来查看套接字、网络栈、网络接口以及路由表的信息。
我个人更推荐,使用 ss 来查询网络的连接信息,因为它比 netstat 提供了更好的性能(速度更快)。
比如,你可以执行下面的命令,查询套接字信息:
```
# head -n 3 表示只显示前面3行
# -l 表示只显示监听套接字
# -n 表示显示数字地址和端口(而不是名字)
# -p 表示显示进程信息
$ netstat -nlp | head -n 3
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
# -l 表示只显示监听套接字
# -t 表示只显示 TCP 套接字
# -n 表示显示数字地址和端口(而不是名字)
# -p 表示显示进程信息
$ ss -ltnp | head -n 3
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:((&quot;systemd-resolve&quot;,pid=840,fd=13))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:((&quot;sshd&quot;,pid=1459,fd=3))
```
netstat 和 ss 的输出也是类似的,都展示了套接字的状态、接收队列、发送队列、本地地址、远端地址、进程 PID 和进程名称等。
其中接收队列Recv-Q和发送队列Send-Q需要你特别关注它们通常应该是 0。当你发现它们不是 0 时,说明有网络包的堆积发生。当然还要注意,在不同套接字状态下,它们的含义不同。
当套接字处于连接状态Established
<li>
Recv-Q 表示套接字缓冲还没有被应用程序取走的字节数(即接收队列长度)。
</li>
<li>
而 Send-Q 表示还没有被远端主机确认的字节数(即发送队列长度)。
</li>
当套接字处于监听状态Listening
<li>
Recv-Q 表示全连接队列的长度。
</li>
<li>
而 Send-Q 表示全连接队列的最大长度。
</li>
所谓全连接,是指服务器收到了客户端的 ACK完成了 TCP 三次握手,然后就会把这个连接挪到全连接队列中。这些全连接中的套接字,还需要被 accept() 系统调用取走,服务器才可以开始真正处理客户端的请求。
与全连接队列相对应的,还有一个半连接队列。所谓半连接是指还没有完成 TCP 三次握手的连接,连接只进行了一半。服务器收到了客户端的 SYN 包后,就会把这个连接放到半连接队列中,然后再向客户端发送 SYN+ACK 包。
## **协议栈统计信息**
类似的,使用 netstat 或 ss ,也可以查看协议栈的信息:
```
$ 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 的输出示例,就展示了 TCP 协议的主动连接、被动连接、失败重试、发送和接收的分段数量等各种信息。
## **网络吞吐和 PPS**
接下来,我们再来看看,如何查看系统当前的网络吞吐量和 PPS。在这里我推荐使用我们的老朋友 sar在前面的 CPU、内存和 I/O 模块中,我们已经多次用到它。
给 sar 增加 -n 参数就可以查看网络的统计信息比如网络接口DEV、网络接口错误EDEV、TCP、UDP、ICMP 等等。执行下面的命令,你就可以得到网络接口统计信息:
```
# 数字1表示每隔1秒输出一组数据
$ sar -n DEV 1
Linux 4.15.0-1035-azure (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
```
这儿输出的指标比较多,我来简单解释下它们的含义。
<li>
rxpck/s 和 txpck/s 分别是接收和发送的 PPS单位为包/秒。
</li>
<li>
rxkB/s 和 txkB/s 分别是接收和发送的吞吐量单位是KB/秒。
</li>
<li>
rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包/秒。
</li>
<li>
%ifutil 是网络接口的使用率,即半双工模式下为 (rxkB/s+txkB/s)/Bandwidth而全双工模式下为 max(rxkB/s, txkB/s)/Bandwidth。
</li>
其中Bandwidth 可以用 ethtool 来查询,它的单位通常是 Gb/s 或者 Mb/s不过注意这里小写字母 b ,表示比特而不是字节。我们通常提到的千兆网卡、万兆网卡等,单位也都是比特。如下你可以看到,我的 eth0 网卡就是一个千兆网卡:
```
$ ethtool eth0 | grep Speed
Speed: 1000Mb/s
```
## **连通性和延时**
最后,我们通常使用 ping ,来测试远程主机的连通性和延时,而这基于 ICMP 协议。比如,执行下面的命令,你就可以测试本机到 114.114.114.114 这个 IP 地址的连通性和延时:
```
# -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
```
ping 的输出,可以分为两部分。
<li>
第一部分,是每个 ICMP 请求的信息,包括 ICMP 序列号icmp_seq、TTL生存时间或者跳数以及往返延时。
</li>
<li>
第二部分,则是三次 ICMP 请求的汇总。
</li>
比如上面的示例显示,发送了 3 个网络包,并且接收到 3 个响应,没有丢包发生,这说明测试主机到 114.114.114.114 是连通的平均往返延时RTT是 244ms也就是从发送 ICMP 开始,到接收到 114.114.114.114 回复的确认,总共经历 244ms。
## 小结
我们通常使用带宽、吞吐量、延时等指标,来衡量网络的性能;相应的,你可以用 ifconfig、netstat、ss、sar、ping 等工具,来查看这些网络的性能指标。
在下一节中,我将以经典的 C10K 和 C100K 问题,带你进一步深入 Linux 网络的工作原理。
## 思考
最后,我想请你来聊聊,你理解的 Linux 网络性能。你常用什么指标来衡量网络的性能?又用什么思路分析相应性能问题呢?你可以结合今天学到的知识,提出自己的观点。
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,198 @@
<audio id="audio" title="35 | 基础篇C10K 和 C1000K 回顾" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ff/db/ffca647e964a56f8357e1dc95d2f75db.mp3"></audio>
你好,我是倪朋飞。
前面内容,我们学习了 Linux 网络的基础原理以及性能观测方法。简单回顾一下Linux 网络基于 TCP/IP 模型,构建了其网络协议栈,把繁杂的网络功能划分为应用层、传输层、网络层、网络接口层等四个不同的层次,既解决了网络环境中设备异构的问题,也解耦了网络协议的复杂性。
基于 TCP/IP 模型,我们还梳理了 Linux 网络收发流程和相应的性能指标。在应用程序通过套接字接口发送或者接收网络包时这些网络包都要经过协议栈的逐层处理。我们通常用带宽、吞吐、延迟、PPS 等来衡量网络性能。
今天,我们主要来回顾下经典的 C10K 和 C1000K 问题,以更好理解 Linux 网络的工作原理,并进一步分析,如何做到单机支持 C10M。
注意C10K 和 C1000K 的首字母 C 是 Client 的缩写。C10K 就是单机同时处理 1 万个请求并发连接1万的问题而 C1000K 也就是单机支持处理 100 万个请求并发连接100万的问题。
## C10K
[C10K 问题](http://www.kegel.com/c10k.html)最早由 Dan Kegel 在 1999年提出。那时的服务器还只是 32 位系统,运行着 Linux 2.2 版本(后来又升级到了 2.4 和 2.6,而 2.6 才支持 x86_64只配置了很少的内存2GB和千兆网卡。
怎么在这样的系统中支持并发 1 万的请求呢?
从资源上来说对2GB 内存和千兆网卡的服务器来说,同时处理 10000 个请求,只要每个请求处理占用不到 200KB2GB/10000的内存和 100Kbit 1000Mbit/10000的网络带宽就可以。所以物理资源是足够的接下来自然是软件的问题特别是网络的 I/O 模型问题。
说到 I/O 的模型,我在文件系统的原理中,曾经介绍过文件 I/O其实网络 I/O 模型也类似。在 C10K 以前Linux 中网络处理都用同步阻塞的方式,也就是每个请求都分配一个进程或者线程。请求数只有 100 个时,这种方式自然没问题,但增加到 10000 个请求时10000 个进程或线程的调度、上下文切换乃至它们占用的内存,都会成为瓶颈。
既然每个请求分配一个线程的方式不合适,那么,为了支持 10000 个并发请求,这里就有两个问题需要我们解决。
第一,怎样在一个线程内处理多个请求,也就是要在一个线程内响应多个网络 I/O。以前的同步阻塞方式下一个线程只能处理一个请求到这里不再适用是不是可以用非阻塞 I/O 或者异步 I/O 来处理多个网络请求呢?
第二,怎么更节省资源地处理客户请求,也就是要用更少的线程来服务这些请求。是不是可以继续用原来的 100 个或者更少的线程,来服务现在的 10000 个请求呢?
当然,事实上,现在 C10K 的问题早就解决了,在继续学习下面的内容前,你可以先自己思考一下这两个问题。结合前面学过的内容,你是不是已经有了解决思路呢?
### I/O 模型优化
异步、非阻塞 I/O 的解决思路,你应该听说过,其实就是我们在网络编程中经常用到的 I/O 多路复用I/O Multiplexing。I/O 多路复用是什么意思呢?
别急,详细了解前,我先来讲两种 I/O 事件通知的方式:水平触发和边缘触发,它们常用在套接字接口的文件描述符中。
<li>
水平触发:只要文件描述符可以非阻塞地执行 I/O ,就会触发通知。也就是说,应用程序可以随时检查文件描述符的状态,然后再根据状态,进行 I/O 操作。
</li>
<li>
边缘触发:只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时,才发送一次通知。这时候,应用程序需要尽可能多地执行 I/O直到无法继续读写才可以停止。如果 I/O 没执行完,或者因为某种原因没来得及处理,那么这次通知也就丢失了。
</li>
接下来,我们再回过头来看 I/O 多路复用的方法。这里其实有很多实现方法,我带你来逐个分析一下。
**第一种,使用非阻塞 I/O 和水平触发通知,比如使用 select 或者 poll。**
根据刚才水平触发的原理select 和 poll 需要从文件描述符列表中,找出哪些可以执行 I/O ,然后进行真正的网络 I/O 读写。由于 I/O 是非阻塞的,一个线程中就可以同时监控一批套接字的文件描述符,这样就达到了单线程处理多请求的目的。
所以,这种方式的最大优点,是对应用程序比较友好,它的 API 非常简单。
但是,应用软件使用 select 和 poll 时需要对这些文件描述符列表进行轮询这样请求数多的时候就会比较耗时。并且select 和 poll 还有一些其他的限制。
select 使用固定长度的位相量,表示文件描述符的集合,因此会有最大描述符数量的限制。比如,在 32 位系统中,默认限制是 1024。并且在 select 内部,检查套接字状态是用轮询的方法,处理耗时跟描述符数量是 O(N) 的关系。
而 poll 改进了 select 的表示方法,换成了一个没有固定长度的数组,这样就没有了最大描述符数量的限制(当然还会受到系统文件描述符限制)。但应用程序在使用 poll 时,同样需要对文件描述符列表进行轮询,这样,处理耗时跟描述符数量就是 O(N) 的关系。
除此之外,应用程序每次调用 select 和 poll 时,还需要把文件描述符的集合,从用户空间传入内核空间,由内核修改后,再传出到用户空间中。这一来一回的内核空间与用户空间切换,也增加了处理成本。
有没有什么更好的方式来处理呢?答案自然是肯定的。
**第二种,使用非阻塞 I/O 和边缘触发通知,比如 epoll**
既然 select 和 poll 有那么多的问题,就需要继续对其进行优化,而 epoll 就很好地解决了这些问题。
<li>
epoll 使用红黑树,在内核中管理文件描述符的集合,这样,就不需要应用程序在每次操作时都传入、传出这个集合。
</li>
<li>
epoll 使用事件驱动的机制,只关注有 I/O 事件发生的文件描述符,不需要轮询扫描整个集合。
</li>
不过要注意epoll 是在 Linux 2.6 中才新增的功能2.4 虽然也有,但功能不完善)。由于边缘触发只在文件描述符可读或可写事件发生时才通知,那么应用程序就需要尽可能多地执行 I/O并要处理更多的异常事件。
**第三种,使用异步 I/OAsynchronous I/O简称为 AIO**。在前面文件系统原理的内容中我曾介绍过异步I/O 与同步 I/O 的区别。异步I/O 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。而在 I/O完成后系统会用事件通知比如信号或者回调函数的方式告诉应用程序。这时应用程序才会去查询 I/O 操作的结果。
异步 I/O 也是到了 Linux 2.6 才支持的功能,并且在很长时间里都处于不完善的状态,比如 glibc 提供的异步 I/O 库,就一直被社区诟病。同时,由于异步 I/O 跟我们的直观逻辑不太一样,想要使用的话,一定要小心设计,其使用难度比较高。
### 工作模型优化
了解了 I/O 模型后,请求处理的优化就比较直观了。使用 I/O 多路复用后,就可以在一个进程或线程中处理多个请求,其中,又有下面两种不同的工作模型。
**第一种,主进程+多个 worker 子进程,这也是最常用的一种模型**。这种方法的一个通用工作模式就是:
<li>
主进程执行 bind() + listen() 后,创建多个子进程;
</li>
<li>
然后,在每个子进程中,都通过 accept() 或 epoll_wait() ,来处理相同的套接字。
</li>
比如,最常用的反向代理服务器 Nginx 就是这么工作的。它也是由主进程和多个 worker 进程组成。主进程主要用来初始化套接字,并管理子进程的生命周期;而 worker 进程,则负责实际的请求处理。我画了一张图来表示这个关系。
<img src="https://static001.geekbang.org/resource/image/45/7e/451a24fb8f096729ed6822b1615b097e.png" alt="">
这里要注意accept() 和 epoll_wait() 调用,还存在一个惊群的问题。换句话说,当网络 I/O 事件发生时,多个进程被同时唤醒,但实际上只有一个进程来响应这个事件,其他被唤醒的进程都会重新休眠。
<li>
其中accept() 的惊群问题,已经在 Linux 2.6 中解决了;
</li>
<li>
而 epoll 的问题,到了 Linux 4.5 ,才通过 EPOLLEXCLUSIVE 解决。
</li>
为了避免惊群问题, Nginx 在每个 worker 进程中都增加一个了全局锁accept_mutex。这些 worker 进程需要首先竞争到锁,只有竞争到锁的进程,才会加入到 epoll 中,这样就确保只有一个 worker 子进程被唤醒。
不过,根据前面 CPU 模块的学习,你应该还记得,进程的管理、调度、上下文切换的成本非常高。那为什么使用多进程模式的 Nginx ,却具有非常好的性能呢?
这里最主要的一个原因就是,这些 worker 进程,实际上并不需要经常创建和销毁,而是在没任务时休眠,有任务时唤醒。只有在 worker 由于某些异常退出时,主进程才需要创建新的进程来代替它。
当然,你也可以用线程代替进程:主线程负责套接字初始化和子线程状态的管理,而子线程则负责实际的请求处理。由于线程的调度和切换成本比较低,实际上你可以进一步把 epoll_wait() 都放到主线程中,保证每次事件都只唤醒主线程,而子线程只需要负责后续的请求处理。
**第二种,监听到相同端口的多进程模型**。在这种方式下,所有的进程都监听相同的接口,并且开启 SO_REUSEPORT 选项,由内核负责将请求负载均衡到这些监听进程中去。这一过程如下图所示。
<img src="https://static001.geekbang.org/resource/image/90/bd/90df0945f6ce5c910ae361bf2b135bbd.png" alt="">
由于内核确保了只有一个进程被唤醒就不会出现惊群问题了。比如Nginx 在 1.9.1 中就已经支持了这种模式。
<img src="https://static001.geekbang.org/resource/image/af/38/af2e6c3a19a6e90098772b5df0605b38.png" alt=""><br>
(图片来自 [Nginx 官网博客](https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/)
不过要注意想要使用SO_REUSEPORT选项需要用 Linux 3.9 以上的版本才可以。
## C1000K
基于 I/O 多路复用和请求处理的优化C10K 问题很容易就可以解决。不过,随着摩尔定律带来的服务器性能提升,以及互联网的普及,你并不难想到,新兴服务会对性能提出更高的要求。
很快,原来的 C10K 已经不能满足需求,所以又有了 C100K 和 C1000K也就是并发从原来的 1 万增加到10 万、乃至 100 万。从 1 万到 10 万,其实还是基于 C10K 的这些理论epoll 配合线程池,再加上 CPU、内存和网络接口的性能和容量提升。大部分情况下C100K 很自然就可以达到。
那么再进一步C1000K 是不是也可以很容易就实现呢?这其实没有那么简单了。
首先从物理资源使用上来说100 万个请求需要大量的系统资源。比如,
<li>
假设每个请求需要 16KB 内存的话,那么总共就需要大约 15 GB 内存。
</li>
<li>
而从带宽上来说,假设只有 20% 活跃连接,即使每个连接只需要 1KB/s 的吞吐量,总共也需要 1.6 Gb/s 的吞吐量。千兆网卡显然满足不了这么大的吞吐量,所以还需要配置万兆网卡,或者基于多网卡 Bonding 承载更大的吞吐量。
</li>
其次从软件资源上来说大量的连接也会占用大量的软件资源比如文件描述符的数量、连接状态的跟踪CONNTRACK、网络协议栈的缓存大小比如套接字读写缓存、TCP 读写缓存)等等。
最后大量请求带来的中断处理也会带来非常高的处理成本。这样就需要多队列网卡、中断负载均衡、CPU 绑定、RPS/RFS软中断负载均衡到多个 CPU 核上以及将网络包的处理卸载Offload到网络设备如 TSO/GSO、LRO/GRO、VXLAN OFFLOAD等各种硬件和软件的优化。
C1000K 的解决方法,本质上还是构建在 epoll 的非阻塞 I/O 模型上。只不过,除了 I/O 模型之外,还需要从应用程序到 Linux 内核、再到 CPU、内存和网络等各个层次的深度优化特别是需要借助硬件来卸载那些原来通过软件处理的大量功能。
## C10M
显然,人们对于性能的要求是无止境的。再进一步,有没有可能在单机中,同时处理 1000 万的请求呢?这也就是 [C10M](http://c10m.robertgraham.com/p/blog-page.html) 问题。
实际上,在 C1000K 问题中,各种软件、硬件的优化很可能都已经做到头了。特别是当升级完硬件(比如足够多的内存、带宽足够大的网卡、更多的网络功能卸载等)后,你可能会发现,无论你怎么优化应用程序和内核中的各种网络参数,想实现 1000 万请求的并发,都是极其困难的。
究其根本,还是 Linux 内核协议栈做了太多太繁重的工作。从网卡中断带来的硬中断处理程序开始,到软中断中的各层网络协议处理,最后再到应用程序,这个路径实在是太长了,就会导致网络包的处理优化,到了一定程度后,就无法更进一步了。
要解决这个问题最重要就是跳过内核协议栈的冗长路径把网络包直接送到要处理的应用程序那里去。这里有两种常见的机制DPDK 和 XDP。
第一种机制DPDK是用户态网络的标准。它跳过内核协议栈直接由用户态进程通过轮询的方式来处理网络接收。
<img src="https://static001.geekbang.org/resource/image/99/3a/998fd2f52f0a48a910517ada9f2bb23a.png" alt=""><br>
(图片来自 [https://blog.selectel.com/introduction-dpdk-architecture-principles/](https://blog.selectel.com/introduction-dpdk-architecture-principles/)
说起轮询,你肯定会下意识认为它是低效的象征,但是进一步反问下自己,它的低效主要体现在哪里呢?是查询时间明显多于实际工作时间的情况下吧!那么,换个角度来想,如果每时每刻都有新的网络包需要处理,轮询的优势就很明显了。比如:
<li>
在 PPS 非常高的场景中,查询时间比实际工作时间少了很多,绝大部分时间都在处理网络包;
</li>
<li>
而跳过内核协议栈后,就省去了繁杂的硬中断、软中断再到 Linux 网络协议栈逐层处理的过程,应用程序可以针对应用的实际场景,有针对性地优化网络包的处理逻辑,而不需要关注所有的细节。
</li>
此外DPDK 还通过大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
第二种机制XDPeXpress Data Path则是 Linux 内核提供的一种高性能网络数据路径。它允许网络包在进入内核协议栈之前就进行处理也可以带来更高的性能。XDP 底层跟我们之前用到的 bcc-tools 一样,都是基于 Linux 内核的 eBPF 机制实现的。
XDP 的原理如下图所示:
<img src="https://static001.geekbang.org/resource/image/06/be/067ef9df4212cd4ede3cffcdac7001be.png" alt=""><br>
(图片来自 [https://www.iovisor.org/technology/xdp](https://www.iovisor.org/technology/xdp)
你可以看到XDP 对内核的要求比较高,需要的是 Linux [4.8 以上版本](https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#xdp),并且它也不提供缓存队列。基于 XDP 的应用程序通常是专用的网络应用,常见的有 IDS入侵检测系统、DDoS 防御、 [cilium](https://github.com/cilium/cilium) 容器网络插件等。
## 小结
今天我带你回顾了经典的 C10K 问题并进一步延伸到了C1000K 和 C10M 问题。
C10K 问题的根源,一方面在于系统有限的资源;另一方面,也是更重要的因素,是同步阻塞的 I/O 模型以及轮询的套接字接口限制了网络事件的处理效率。Linux 2.6 中引入的 epoll ,完美解决了 C10K 的问题,现在的高性能网络方案都基于 epoll。
从 C10K 到 C100K ,可能只需要增加系统的物理资源就可以满足;但从 C100K 到 C1000K ,就不仅仅是增加物理资源就能解决的问题了。这时,就需要多方面的优化工作了,从硬件的中断处理和网络功能卸载、到网络协议栈的文件描述符数量、连接状态跟踪、缓存队列等内核的优化,再到应用程序的工作模型优化,都是考虑的重点。
再进一步,要实现 C10M ,就不只是增加物理资源,或者优化内核和应用程序可以解决的问题了。这时候,就需要用 XDP 的方式,在内核协议栈之前处理网络包;或者用 DPDK 直接跳过网络协议栈,在用户空间通过轮询的方式直接处理网络包。
当然了,实际上,在大多数场景中,我们并不需要单机并发 1000 万的请求。通过调整系统架构,把这些请求分发到多台服务器中来处理,通常是更简单和更容易扩展的方案。
## 思考
最后,我想请你来聊聊,你所理解的 C10K 和 C1000K 问题。你碰到过哪些网络并发相关的性能瓶颈?你又是怎么样来分析它们的呢?你可以结合今天学到的网络知识,提出自己的观点。
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,423 @@
<audio id="audio" title="36 | 套路篇:怎么评估系统的网络性能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3a/a1/3a78c888173618b0756720676bbe4fa1.mp3"></audio>
你好,我是倪朋飞。
上一节,我们回顾了经典的 C10K 和 C1000K 问题。简单回顾一下C10K 是指如何单机同时处理 1 万个请求并发连接1万的问题而 C1000K 则是单机支持处理 100 万个请求并发连接100万的问题。
I/O 模型的优化,是解决 C10K 问题的最佳良方。Linux 2.6 中引入的 epoll完美解决了 C10K 的问题,并一直沿用至今。今天的很多高性能网络方案,仍都基于 epoll。
自然,随着互联网技术的普及,催生出更高的性能需求。从 C10K 到 C100K我们只需要增加系统的物理资源就可以满足要求但从 C100K 到 C1000K ,光增加物理资源就不够了。
这时,就要对系统的软硬件进行统一优化,从硬件的中断处理,到网络协议栈的文件描述符数量、连接状态跟踪、缓存队列,再到应用程序的工作模型等的整个网络链路,都需要深入优化。
再进一步,要实现 C10M就不是增加物理资源、调优内核和应用程序可以解决的问题了。这时内核中冗长的网络协议栈就成了最大的负担。
<li>
需要用 XDP 方式,在内核协议栈之前,先处理网络包。
</li>
<li>
或基于 DPDK ,直接跳过网络协议栈,在用户空间通过轮询的方式处理。
</li>
其中DPDK 是目前最主流的高性能网络方案,不过,这需要能支持 DPDK 的网卡配合使用。
当然,实际上,在大多数场景中,我们并不需要单机并发 1000 万请求。通过调整系统架构,把请求分发到多台服务器中并行处理,才是更简单、扩展性更好的方案。
不过,这种情况下,就需要我们评估系统的网络性能,以便考察系统的处理能力,并为容量规划提供基准数据。
那么,到底该怎么评估网络的性能呢?今天,我就带你一起来看看这个问题。
## 性能指标回顾
在评估网络性能前,我们先来回顾一下,衡量网络性能的指标。在 Linux 网络基础篇中我们曾经说到带宽、吞吐量、延时、PPS 等,都是最常用的网络性能指标。还记得它们的具体含义吗?你可以先思考一下,再继续下面的内容。
首先,**带宽**,表示链路的最大传输速率,单位是 b/s比特/秒)。在你为服务器选购网卡时,带宽就是最核心的参考指标。常用的带宽有 1000M、10G、40G、100G 等。
第二,**吞吐量**,表示没有丢包时的最大数据传输速率,单位通常为 b/s (比特/秒)或者 B/s字节/秒)。吞吐量受带宽的限制,吞吐量/带宽也就是该网络链路的使用率。
第三,**延时**,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。这个指标在不同场景中可能会有不同的含义。它可以表示建立连接需要的时间(比如 TCP 握手延时),或者一个数据包往返所需时间(比如 RTT
最后,**PPS**,是 Packet Per Second包/秒的缩写表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,而基于 Linux 服务器的转发,很容易受到网络包大小的影响(交换机通常不会受到太大影响,即交换机可以线性转发)。
这四个指标中,带宽跟物理网卡配置是直接关联的。一般来说,网卡确定后,带宽也就确定了(当然,实际带宽会受限于整个网络链路中最小的那个模块)。
另外你可能在很多地方听说过“网络带宽测试”这里测试的实际上不是带宽而是网络吞吐量。Linux 服务器的网络吞吐量一般会比带宽小,而对交换机等专门的网络设备来说,吞吐量一般会接近带宽。
最后的 PPS则是以网络包为单位的网络传输速率通常用在需要大量转发的场景中。而对 TCP 或者 Web 服务来说更多会用并发连接数和每秒请求数QPSQuery per Second等指标它们更能反应实际应用程序的性能。
## 网络基准测试
熟悉了网络的性能指标后,接下来,我们再来看看,如何通过性能测试来确定这些指标的基准值。
你可以先思考一个问题。我们已经知道Linux 网络基于 TCP/IP 协议栈,而不同协议层的行为显然不同。那么,测试之前,你应该弄清楚,你要评估的网络性能,究竟属于协议栈的哪一层?换句话说,你的应用程序基于协议栈的哪一层呢?
根据前面学过的 TCP/IP 协议栈的原理,这个问题应该不难回答。比如:
<li>
基于 HTTP 或者 HTTPS 的 Web 应用程序,显然属于应用层,需要我们测试 HTTP/HTTPS 的性能;
</li>
<li>
而对大多数游戏服务器来说,为了支持更大的同时在线人数,通常会基于 TCP 或 UDP ,与客户端进行交互,这时就需要我们测试 TCP/UDP 的性能;
</li>
<li>
当然,还有一些场景,是把 Linux 作为一个软交换机或者路由器来用的。这种情况下,你更关注网络包的处理能力(即 PPS重点关注网络层的转发性能。
</li>
接下来,我就带你从下往上,了解不同协议层的网络性能测试方法。不过要注意,低层协议是其上的各层网络协议的基础。自然,低层协议的性能,也就决定了高层的网络性能。
注意,以下所有的测试方法,都需要两台 Linux 虚拟机。其中一台,可以当作待测试的目标机器;而另一台,则可以当作正在运行网络服务的客户端,用来运行测试工具。
## 各协议层的性能测试
### 转发性能
我们首先来看,网络接口层和网络层,它们主要负责网络包的封装、寻址、路由以及发送和接收。在这两个网络协议层中,每秒可处理的网络包数 PPS就是最重要的性能指标。特别是 64B 小包的处理能力,值得我们特别关注。那么,如何来测试网络包的处理能力呢?
说到网络包相关的测试,你可能会觉得陌生。不过,其实在专栏开头的 CPU 性能篇中,我们就接触过一个相关工具,也就是软中断案例中的 hping3。
在那个案例中hping3 作为一个 SYN 攻击的工具来使用。实际上, hping3 更多的用途,是作为一个测试网络包处理能力的性能工具。
今天我再来介绍另一个更常用的工具Linux 内核自带的高性能网络测试工具 [pktgen](https://wiki.linuxfoundation.org/networking/pktgen)。pktgen 支持丰富的自定义选项,方便你根据实际需要构造所需网络包,从而更准确地测试出目标服务器的性能。
不过,在 Linux 系统中,你并不能直接找到 pktgen 命令。因为 pktgen 作为一个内核线程来运行,需要你加载 pktgen 内核模块后,再通过 /proc 文件系统来交互。下面就是 pktgen 启动的两个内核线程和 /proc 文件系统的交互文件:
```
$ modprobe pktgen
$ 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 则主要用来控制这次测试的开启和停止。
>
如果 modprobe 命令执行失败,说明你的内核没有配置 CONFIG_NET_PKTGEN 选项。这就需要你配置 pktgen 内核模块(即 CONFIG_NET_PKTGEN=m重新编译内核才可以使用。
在使用 pktgen 测试网络性能时,需要先给每个内核线程 kpktgend_X 以及测试网卡,配置 pktgen 选项,然后再通过 pgctrl 启动测试。
以发包测试为例,假设发包机器使用的网卡是 eth0而目标机器的 IP 地址为 192.168.0.30MAC 地址为 11:11:11:11:11:11。
<img src="https://static001.geekbang.org/resource/image/f0/09/f01dc79465e7f1d03b6fbdabbe4ad109.png" alt="">
接下来,就是一个发包测试的示例。
```
# 定义一个工具函数,方便后面配置各种测试选项
function pgset() {
local result
echo $1 &gt; $PGDEV
result=`cat $PGDEV | fgrep &quot;Result: OK:&quot;`
if [ &quot;$result&quot; = &quot;&quot; ]; then
cat $PGDEV | fgrep Result:
fi
}
# 为0号线程绑定eth0网卡
PGDEV=/proc/net/pktgen/kpktgend_0
pgset &quot;rem_device_all&quot; # 清空网卡绑定
pgset &quot;add_device eth0&quot; # 添加eth0网卡
# 配置eth0网卡的测试选项
PGDEV=/proc/net/pktgen/eth0
pgset &quot;count 1000000&quot; # 总发包数量
pgset &quot;delay 5000&quot; # 不同包之间的发送延迟(单位纳秒)
pgset &quot;clone_skb 0&quot; # SKB包复制
pgset &quot;pkt_size 64&quot; # 网络包大小
pgset &quot;dst 192.168.0.30&quot; # 目的IP
pgset &quot;dst_mac 11:11:11:11:11:11&quot; # 目的MAC
# 启动测试
PGDEV=/proc/net/pktgen/pgctrl
pgset &quot;start&quot;
```
稍等一会儿,测试完成后,结果可以从 /proc 文件系统中获取。通过下面代码段中的内容,我们可以查看刚才的测试报告:
```
$ cat /proc/net/pktgen/eth0
Params: count 1000000 min_pkt_size: 64 max_pkt_size: 64
frags: 0 delay: 0 clone_skb: 0 ifname: eth0
flows: 0 flowlen: 0
...
Current:
pkts-sofar: 1000000 errors: 0
started: 1534853256071us stopped: 1534861576098us idle: 70673us
...
Result: OK: 8320027(c8249354+d70673) usec, 1000000 (64byte,0frags)
120191pps 61Mb/sec (61537792bps) errors: 0
```
你可以看到,测试报告主要分为三个部分:
<li>
第一部分的 Params 是测试选项;
</li>
<li>
第二部分的 Current 是测试进度,其中, packts so farpkts-sofar表示已经发送了 100 万个包,也就表明测试已完成。
</li>
<li>
第三部分的 Result 是测试结果包含测试所用时间、网络包数量和分片、PPS、吞吐量以及错误数。
</li>
根据上面的结果我们发现PPS 为 12 万,吞吐量为 61 Mb/s没有发生错误。那么12 万的 PPS 好不好呢?
作为对比,你可以计算一下千兆交换机的 PPS。交换机可以达到线速满负载时无差错转发它的 PPS 就是 1000Mbit 除以以太网帧的大小,即 1000Mbps/((64+20)*8bit) = 1.5 Mpps其中20B 为以太网帧前导和帧间距的大小)。
你看,即使是千兆交换机的 PPS也可以达到 150 万 PPS比我们测试得到的 12 万大多了。所以,看到这个数值你并不用担心,现在的多核服务器和万兆网卡已经很普遍了,稍做优化就可以达到数百万的 PPS。而且如果你用了上节课讲到的 DPDK 或 XDP ,还能达到千万数量级。
### TCP/UDP 性能
掌握了 PPS 的测试方法接下来我们再来看TCP 和 UDP 的性能测试方法。说到 TCP 和 UDP 的测试,我想你已经很熟悉了,甚至可能一下子就能想到相应的测试工具,比如 iperf 或者 netperf。
特别是现在的云计算时代,在你刚拿到一批虚拟机时,首先要做的,应该就是用 iperf ,测试一下网络性能是否符合预期。
iperf 和 netperf 都是最常用的网络性能测试工具,测试 TCP 和 UDP 的吞吐量。它们都以客户端和服务器通信的方式,测试一段时间内的平均吞吐量。
接下来,我们就以 iperf 为例,看一下 TCP 性能的测试方法。目前iperf 的最新版本为 iperf3你可以运行下面的命令来安装
```
# Ubuntu
apt-get install iperf3
# CentOS
yum install iperf3
```
然后,在目标机器上启动 iperf 服务端:
```
# -s表示启动服务端-i表示汇报间隔-p表示监听端口
$ iperf3 -s -i 1 -p 10000
```
接着,在另一台机器上运行 iperf 客户端,运行测试:
```
# -c表示启动客户端192.168.0.30为目标服务器的IP
# -b表示目标带宽(单位是bits/s)
# -t表示测试时间
# -P表示并发数-p表示目标服务器监听端口
$ iperf3 -c 192.168.0.30 -b 1G -t 15 -P 2 -p 10000
```
稍等一会儿15秒测试结束后回到目标服务器查看 iperf 的报告:
```
[ ID] Interval Transfer Bandwidth
...
[SUM] 0.00-15.04 sec 0.00 Bytes 0.00 bits/sec sender
[SUM] 0.00-15.04 sec 1.51 GBytes 860 Mbits/sec receiver
```
最后的 SUM 行就是测试的汇总结果,包括测试时间、数据传输量以及带宽等。按照发送和接收,这一部分又分为了 sender 和 receiver 两行。
从测试结果你可以看到,这台机器 TCP 接收的带宽(吞吐量)为 860 Mb/s 跟目标的 1Gb/s 相比,还是有些差距的。
### HTTP 性能
从传输层再往上,到了应用层。有的应用程序,会直接基于 TCP 或 UDP 构建服务。当然也有大量的应用基于应用层的协议来构建服务HTTP 就是最常用的一个应用层协议。比如,常用的 Apache、Nginx 等各种 Web 服务,都是基于 HTTP。
要测试 HTTP 的性能,也有大量的工具可以使用,比如 ab、webbench 等,都是常用的 HTTP 压力测试工具。其中ab 是 Apache 自带的 HTTP 压测工具,主要测试 HTTP 服务的每秒请求数、请求延迟、吞吐量以及请求延迟的分布情况等。
运行下面的命令,你就可以安装 ab 工具:
```
# Ubuntu
$ apt-get install -y apache2-utils
# CentOS
$ yum install -y httpd-tools
```
接下来,在目标机器上,使用 Docker 启动一个 Nginx 服务,然后用 ab 来测试它的性能。首先,在目标机器上运行下面的命令:
```
$ docker run -p 80:80 -itd nginx
```
而在另一台机器上,运行 ab 命令,测试 Nginx 的性能:
```
# -c表示并发请求数为1000-n表示总的请求数为10000
$ ab -c 1000 -n 10000 http://192.168.0.30/
...
Server Software: nginx/1.15.8
Server Hostname: 192.168.0.30
Server Port: 80
...
Requests per second: 1078.54 [#/sec] (mean)
Time per request: 927.183 [ms] (mean)
Time per request: 0.927 [ms] (mean, across all concurrent requests)
Transfer rate: 890.00 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 27 152.1 1 1038
Processing: 9 207 843.0 22 9242
Waiting: 8 207 843.0 22 9242
Total: 15 233 857.7 23 9268
Percentage of the requests served within a certain time (ms)
50% 23
66% 24
75% 24
80% 26
90% 274
95% 1195
98% 2335
99% 4663
100% 9268 (longest request)
```
可以看到ab 的测试结果分为三个部分,分别是请求汇总、连接时间汇总还有请求延迟汇总。以上面的结果为例,我们具体来看。
在请求汇总部分,你可以看到:
<li>
Requests per second 为 1074
</li>
<li>
每个请求的延迟Time per request分为两行第一行的 927 ms 表示平均延迟,包括了线程运行的调度时间和网络请求响应时间,而下一行的 0.927ms ,则表示实际请求的响应时间;
</li>
<li>
Transfer rate 表示吞吐量BPS为 890 KB/s。
</li>
连接时间汇总部分,则是分别展示了建立连接、请求、等待以及汇总等的各类时间,包括最小、最大、平均以及中值处理时间。
最后的请求延迟汇总部分,则给出了不同时间段内处理请求的百分比,比如, 90% 的请求,都可以在 274ms 内完成。
### 应用负载性能
当你用 iperf 或者 ab 等测试工具,得到 TCP、HTTP 等的性能数据后,这些数据是否就能表示应用程序的实际性能呢?我想,你的答案应该是否定的。
比如,你的应用程序基于 HTTP 协议,为最终用户提供一个 Web 服务。这时,使用 ab 工具可以得到某个页面的访问性能但这个结果跟用户的实际请求很可能不一致。因为用户请求往往会附带着各种各种的负载payload而这些负载会影响 Web 应用程序内部的处理逻辑,从而影响最终性能。
那么为了得到应用程序的实际性能就要求性能工具本身可以模拟用户的请求负载而iperf、ab 这类工具就无能为力了。幸运的是,我们还可以用 wrk、TCPCopy、Jmeter 或者 LoadRunner 等实现这个目标。
以 [wrk](https://github.com/wg/wrk) 为例,它是一个 HTTP 性能测试工具,内置了 LuaJIT方便你根据实际需求生成所需的请求负载或者自定义响应的处理方法。
wrk 工具本身不提供 yum 或 apt 的安装方法,需要通过源码编译来安装。比如,你可以运行下面的命令,来编译和安装 wrk
```
$ https://github.com/wg/wrk
$ cd wrk
$ apt-get install build-essential -y
$ make
$ sudo cp wrk /usr/local/bin/
```
wrk 的命令行参数比较简单。比如,我们可以用 wrk ,来重新测一下前面已经启动的 Nginx 的性能。
```
# -c表示并发连接数1000-t表示线程数为2
$ wrk -c 1000 -t 2 http://192.168.0.30/
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如下图所示
<img src="https://static001.geekbang.org/resource/image/d0/82/d02b845aa308b7a38a5735f3db8d9682.png" alt="">
(图片来自[网易云博客](https://sq.163yun.com/blog/article/200008406328934400)
比如,你可以在 setup 阶段,为请求设置认证参数(来自于 wrk 官方[示例](https://github.com/wg/wrk/blob/master/scripts/auth.lua)
```
-- example script that demonstrates response handling and
-- retrieving an authentication token to set on all future
-- requests
token = nil
path = &quot;/authenticate&quot;
request = function()
return wrk.format(&quot;GET&quot;, path)
end
response = function(status, headers, body)
if not token and status == 200 then
token = headers[&quot;X-Token&quot;]
path = &quot;/resource&quot;
wrk.headers[&quot;X-Token&quot;] = token
end
end
```
而在执行测试时,通过 -s 选项,执行脚本的路径:
```
$ wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/
```
wrk 需要你用 Lua 脚本,来构造请求负载。这对于大部分场景来说,可能已经足够了 。不过,它的缺点也正是,所有东西都需要代码来构造,并且工具本身不提供 GUI 环境。
像 Jmeter 或者 LoadRunner商业产品则针对复杂场景提供了脚本录制、回放、GUI 等更丰富的功能,使用起来也更加方便。
## 小结
今天,我带你一起回顾了网络的性能指标,并学习了网络性能的评估方法。
性能评估是优化网络性能的前提,只有在你发现网络性能瓶颈时,才需要进行网络性能优化。根据 TCP/IP 协议栈的原理,不同协议层关注的性能重点不完全一样,也就对应不同的性能测试方法。比如,
<li>
在应用层,你可以使用 wrk、Jmeter 等模拟用户的负载,测试应用程序的每秒请求数、处理延迟、错误数等;
</li>
<li>
而在传输层,则可以使用 iperf 等工具,测试 TCP 的吞吐情况;
</li>
<li>
再向下,你还可以用 Linux 内核自带的 pktgen ,测试服务器的 PPS。
</li>
由于低层协议是高层协议的基础。所以,一般情况下,我们需要从上到下,对每个协议层进行性能测试,然后根据性能测试的结果,结合 Linux 网络协议栈的原理,找出导致性能瓶颈的根源,进而优化网络性能。
## 思考
最后,我想请你来聊一聊。
<li>
你是如何评估网络性能的?
</li>
<li>
在评估网络性能时,你会从哪个协议层、选择哪些指标,作为性能测试最核心的目标?
</li>
<li>
你又会用哪些工具,测试并分析网络的性能呢?
</li>
你可以结合今天学到的网络知识,总结自己的思路。
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,502 @@
<audio id="audio" title="37 | 案例篇DNS 解析时快时慢,我该怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3a/65/3a6966d0c9e7c25f87b5f518bdff1f65.mp3"></audio>
你好,我是倪朋飞。
上一节我带你一起学习了网络性能的评估方法。简单回顾一下Linux 网络基于 TCP/IP 协议栈构建,而在协议栈的不同层,我们所关注的网络性能也不尽相同。
在应用层,我们关注的是应用程序的并发连接数、每秒请求数、处理延迟、错误数等,可以使用 wrk、JMeter 等工具,模拟用户的负载,得到想要的测试结果。
而在传输层,我们关注的是 TCP、UDP 等传输层协议的工作状况,比如 TCP 连接数、 TCP 重传、TCP 错误数等。此时,你可以使用 iperf、netperf 等,来测试 TCP 或 UDP 的性能。
再向下到网络层,我们关注的则是网络包的处理能力,即 PPS。Linux 内核自带的 pktgen就可以帮你测试这个指标。
由于低层协议是高层协议的基础,所以一般情况下,我们所说的网络优化,实际上包含了整个网络协议栈的所有层的优化。当然,性能要求不同,具体需要优化的位置和目标并不完全相同。
前面在评估网络性能(比如 HTTP 性能)时,我们在测试工具中指定了网络服务的 IP 地址。IP 地址是 TCP/IP 协议中,用来确定通信双方的一个重要标识。每个 IP 地址又包括了主机号和网络号两部分。相同网络号的主机组成一个子网;不同子网再通过路由器连接,组成一个庞大的网络。
然而IP 地址虽然方便了机器的通信,却给访问这些服务的人们,带来了很重的记忆负担。我相信,没几个人能记得住 GitHub 所在的 IP 地址,因为这串字符,对人脑来说并没有什么含义,不符合我们的记忆逻辑。
不过,这并不妨碍我们经常使用这个服务。为什么呢?当然是因为还有更简单、方便的方式。我们可以通过域名 github.com 访问,而不是必须依靠具体的 IP 地址,这其实正是域名系统 DNS 的由来。
DNSDomain Name System即域名系统是互联网中最基础的一项服务主要提供域名和 IP 地址之间映射关系的查询服务。
DNS 不仅方便了人们访问不同的互联网服务更为很多应用提供了动态服务发现和全局负载均衡Global Server Load BalanceGSLB的机制。这样DNS 就可以选择离用户最近的 IP 来提供服务。即使后端服务的 IP 地址发生变化,用户依然可以用相同域名来访问。
DNS显然是我们工作中基础而重要的一个环节。那么DNS 出现问题时,又该如何分析和排查呢?今天,我就带你一起来看看这个问题。
## 域名与 DNS 解析
域名我们本身都比较熟悉,由一串用点分割开的字符组成,被用作互联网中的某一台或某一组计算机的名称,目的就是为了方便识别,互联网中提供各种服务的主机位置。
要注意,域名是全球唯一的,需要通过专门的域名注册商才可以申请注册。为了组织全球互联网中的众多计算机,域名同样用点来分开,形成一个分层的结构。而每个被点分割开的字符串,就构成了域名中的一个层级,并且位置越靠后,层级越高。
我们以极客时间的网站 time.geekbang.org 为例,来理解域名的含义。这个字符串中,最后面的 org 是顶级域名,中间的 geekbang 是二级域名,而最左边的 time 则是三级域名。
如下图所示,注意点(.)是所有域名的根,也就是说所有域名都以点作为后缀,也可以理解为,在域名解析的过程中,所有域名都以点结束。
<img src="https://static001.geekbang.org/resource/image/1b/82/1b509317968f3f73810ac1d313ced982.png" alt="">
通过理解这几个概念你可以看出域名主要是为了方便让人记住而IP 地址是机器间的通信的真正机制。把域名转换为 IP 地址的服务也就是我们开头提到的域名解析服务DNS而对应的服务器就是域名服务器网络协议则是 DNS 协议。
这里注意DNS 协议在 TCP/IP 栈中属于应用层,不过实际传输还是基于 UDP 或者 TCP 协议UDP 居多) ,并且域名服务器一般监听在端口 53 上。
既然域名以分层的结构进行管理,相对应的,域名解析其实也是用递归的方式(从顶级开始,以此类推),发送给每个层级的域名服务器,直到得到解析结果。
不过不要担心递归查询的过程并不需要你亲自操作DNS 服务器会替你完成,你要做的,只是预先配置一个可用的 DNS 服务器就可以了。
当然我们知道通常来说每级DNS 服务器,都会有最近解析记录的缓存。当缓存命中时,直接用缓存中的记录应答就可以了。如果缓存过期或者不存在,才需要用刚刚提到的递归方式查询。
所以,系统管理员在配置 Linux 系统的网络时,除了需要配置 IP 地址,还需要给它配置 DNS 服务器,这样它才可以通过域名来访问外部服务。
比如,我的系统配置的就是 114.114.114.114 这个域名服务器。你可以执行下面的命令,来查询你的系统配置:
```
$ cat /etc/resolv.conf
nameserver 114.114.114.114
```
另外DNS 服务通过资源记录的方式,来管理所有数据,它支持 A、CNAME、MX、NS、PTR 等多种类型的记录。比如:
<li>
A 记录,用来把域名转换成 IP 地址;
</li>
<li>
CNAME 记录,用来创建别名;
</li>
<li>
而 NS 记录,则表示该域名对应的域名服务器地址。
</li>
简单来说,当我们访问某个网址时,就需要通过 DNS 的 A 记录,查询该域名对应的 IP 地址,然后再通过该 IP 来访问 Web 服务。
比如,还是以极客时间的网站 time.geekbang.org 为例,执行下面的 nslookup 命令,就可以查询到这个域名的 A 记录,可以看到,它的 IP 地址是 39.106.233.176
```
$ 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
```
这里要注意,由于 114.114.114.114 并不是直接管理 time.geekbang.org 的域名服务器,所以查询结果是非权威的。使用上面的命令,你只能得到 114.114.114.114 查询的结果。
前面还提到了如果没有命中缓存DNS 查询实际上是一个递归过程,那有没有方法可以知道整个递归查询的执行呢?
其实除了 nslookup另外一个常用的 DNS 解析工具 dig ,就提供了 trace 功能,可以展示递归查询的整个过程。比如你可以执行下面的命令,得到查询结果:
```
# +trace表示开启跟踪查询
# +nodnssec表示禁止DNS安全扩展
$ dig +trace +nodnssec time.geekbang.org
; &lt;&lt;&gt;&gt; DiG 9.11.3-1ubuntu1.3-Ubuntu &lt;&lt;&gt;&gt; +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
```
dig trace 的输出,主要包括四部分。
<li>
第一部分,是从 114.114.114.114 查到的一些根域名服务器(.)的 NS 记录。
</li>
<li>
第二部分,是从 NS 记录结果中选一个h.root-servers.net并查询顶级域名 org. 的 NS 记录。
</li>
<li>
第三部分,是从 org. 的 NS 记录中选择一个b0.org.afilias-nst.org并查询二级域名 geekbang.org. 的 NS 服务器。
</li>
<li>
最后一部分,就是从 geekbang.org. 的 NS 服务器dns10.hichina.com查询最终主机 time.geekbang.org. 的 A 记录。
</li>
这个输出里展示的各级域名的 NS 记录,其实就是各级域名服务器的地址,可以让你更清楚 DNS 解析的过程。 为了帮你更直观理解递归查询,我把这个过程整理成了一张流程图,你可以保存下来理解。
<img src="https://static001.geekbang.org/resource/image/5f/d3/5ffda41ec62fc3c9e0de3fa3443c9cd3.png" alt="">
当然不仅仅是发布到互联网的服务需要域名很多时候我们也希望能对局域网内部的主机进行域名解析即内网域名大多数情况下为主机名。Linux 也支持这种行为。
所以,你可以把主机名和 IP 地址的映射关系,写入本机的 /etc/hosts 文件中。这样,指定的主机名就可以在本地直接找到目标 IP。比如你可以执行下面的命令来操作
```
$ cat /etc/hosts
127.0.0.1 localhost localhost.localdomain
::1 localhost6 localhost6.localdomain6
192.168.0.100 domain.com
```
或者,你还可以在内网中,搭建自定义的 DNS 服务器,专门用来解析内网中的域名。而内网 DNS 服务器,一般还会设置一个或多个上游 DNS 服务器,用来解析外网的域名。
清楚域名与 DNS 解析的基本原理后,接下来,我就带你一起来看几个案例,实战分析 DNS 解析出现问题时,该如何定位。
## 案例准备
本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 docker 等工具,如 apt install docker.io。
</li>
你可以先打开一个终端SSH 登录到 Ubuntu 机器中,然后执行下面的命令,拉取案例中使用的 Docker 镜像:
```
$ docker pull feisky/dnsutils
Using default tag: latest
...
Status: Downloaded newer image for feisky/dnsutils:latest
```
然后,运行下面的命令,查看主机当前配置的 DNS 服务器:
```
$ cat /etc/resolv.conf
nameserver 114.114.114.114
```
可以看到,我这台主机配置的 DNS 服务器是 114.114.114.114。
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
## 案例分析
### 案例1DNS解析失败
首先,执行下面的命令,进入今天的第一个案例。如果一切正常,你将可以看到下面这个输出:
```
# 进入案例环境的SHELL终端中
$ docker run -it --rm -v $(mktemp):/etc/resolv.conf feisky/dnsutils bash
root@7e9ed6ed4974:/#
```
注意这儿root后面的 7e9ed6ed4974是 Docker 生成容器的 ID前缀你的环境中很可能是不同的 ID所以直接忽略这一项就可以了。
>
注意:下面的代码段中, /# 开头的命令都表示在容器内部运行的命令。
接着,继续在容器终端中,执行 DNS 查询命令,我们还是查询 time.geekbang.org 的 IP 地址:
```
/# nslookup time.geekbang.org
;; connection timed out; no servers could be reached
```
你可以发现,这个命令阻塞很久后,还是失败了,报了 connection timed out 和 no servers could be reached 错误。
看到这里,估计你的第一反应就是网络不通了,到底是不是这样呢?我们用 ping 工具检查试试。执行下面的命令,就可以测试本地到 114.114.114.114 的连通性:
```
/# 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 命令失败的原因呢?这里其实有很多方法,最简单的一种,就是开启 nslookup 的调试输出,查看查询过程中的详细步骤,排查其中是否有异常。
比如,我们可以继续在容器终端中,执行下面的命令:
```
/# 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 呢?
你可能已经想到了症结所在——有可能是因为容器中没有配置 DNS 服务器。那我们就执行下面的命令确认一下:
```
/# cat /etc/resolv.conf
```
果然,这个命令没有任何输出,说明容器里的确没有配置 DNS 服务器。到这一步,很自然的,我们就知道了解决方法。在 /etc/resolv.conf 文件中,配置上 DNS 服务器就可以了。
你可以执行下面的命令,在配置好 DNS 服务器后,重新执行 nslookup 命令。自然,我们现在发现,这次可以正常解析了:
```
/# echo &quot;nameserver 114.114.114.114&quot; &gt; /etc/resolv.conf
/# 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.176
```
到这里,第一个案例就轻松解决了。最后,在终端中执行 exit 命令退出容器Docker 就会自动清理刚才运行的容器。
### 案例2DNS解析不稳定
接下来,我们再来看第二个案例。执行下面的命令,启动一个新的容器,并进入它的终端中:
```
$ docker run -it --rm --cap-add=NET_ADMIN --dns 8.8.8.8 feisky/dnsutils bash
root@0cd3ee0c8ecb:/#
```
然后,跟上一个案例一样,还是运行 nslookup 命令,解析 time.geekbang.org 的 IP 地址。不过,这次要加一个 time 命令,输出解析所用时间。如果一切正常,你可能会看到如下输出:
```
/# 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
user 0m0.004s
sys 0m0.0
```
可以看到,这次解析非常慢,居然用了 10 秒。如果你多次运行上面的 nslookup 命令,可能偶尔还会碰到下面这种错误:
```
/# time nslookup time.geekbang.org
;; connection timed out; no servers could be reached
real 0m15.011s
user 0m0.006s
sys 0m0.006s
```
换句话说,跟上一个案例类似,也会出现解析失败的情况。综合来看,现在 DNS 解析的结果不但比较慢,而且还会发生超时失败的情况。
这是为什么呢?碰到这种问题该怎么处理呢?
其实根据前面的讲解我们知道DNS 解析,说白了就是客户端与服务器交互的过程,并且这个过程还使用了 UDP 协议。
那么,对于整个流程来说,解析结果不稳定,就有很多种可能的情况了。比方说:
<li>
DNS 服务器本身有问题,响应慢并且不稳定;
</li>
<li>
或者是,客户端到 DNS 服务器的网络延迟比较大;
</li>
<li>
再或者DNS 请求或者响应包,在某些情况下被链路中的网络设备弄丢了。
</li>
根据上面 nslookup 的输出你可以看到现在客户端连接的DNS 是 8.8.8.8,这是 Google 提供的 DNS 服务。对 Google 我们还是比较放心的DNS 服务器出问题的概率应该比较小。基本排除了DNS服务器的问题那是不是第二种可能本机到 DNS 服务器的延迟比较大呢?
前面讲过ping 可以用来测试服务器的延迟。比如,你可以运行下面的命令:
```
/# 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
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 的输出可以看到,这里的延迟已经达到了 140ms这也就可以解释为什么解析这么慢了。实际上如果你多次运行上面的 ping 测试,还会看到偶尔出现的丢包现象。
```
$ 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
```
这也进一步解释了,为什么 nslookup 偶尔会失败,正是网络链路中的丢包导致的。
碰到这种问题该怎么办呢?显然,既然延迟太大,那就换一个延迟更小的 DNS 服务器,比如电信提供的 114.114.114.114。
配置之前,我们可以先用 ping 测试看看,它的延迟是不是真的比 8.8.8.8 好。执行下面的命令,你就可以看到,它的延迟只有 31ms
```
/# 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=67 time=31.130 ms
64 bytes from 114.114.114.114: icmp_seq=1 ttl=56 time=31.302 ms
64 bytes from 114.114.114.114: icmp_seq=2 ttl=56 time=31.250 ms
--- 114.114.114.114 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 31.130/31.227/31.302/0.072 ms
```
这个结果表明,延迟的确小了很多。我们继续执行下面的命令,更换 DNS 服务器,然后,再次执行 nslookup 解析命令:
```
/# echo nameserver 114.114.114.114 &gt; /etc/resolv.conf
/# time 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.176
real 0m0.064s
user 0m0.007s
sys 0m0.006s
```
你可以发现,现在只需要 64ms 就可以完成解析,比刚才的 10s 要好很多。
到这里,问题看似就解决了。不过,如果你多次运行 nslookup 命令,估计就不是每次都有好结果了。比如,在我的机器中,就经常需要 1s 甚至更多的时间。
```
/# time 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.176
real 0m1.045s
user 0m0.007s
sys 0m0.004s
```
1s 的 DNS 解析时间还是太长了,对很多应用来说也是不可接受的。那么,该怎么解决这个问题呢?我想你一定已经想到了,那就是使用 DNS 缓存。这样,只有第一次查询时需要去 DNS 服务器请求,以后的查询,只要 DNS 记录不过期,使用缓存中的记录就可以了。
不过要注意,我们使用的主流 Linux 发行版,除了最新版本的 Ubuntu (如 18.04 或者更新版本)外,其他版本并没有自动配置 DNS 缓存。
所以,想要为系统开启 DNS 缓存,就需要你做额外的配置。比如,最简单的方法,就是使用 dnsmasq。
dnsmasq 是最常用的 DNS 缓存服务之一,还经常作为 DHCP 服务来使用。它的安装和配置都比较简单,性能也可以满足绝大多数应用程序对 DNS 缓存的需求。
我们继续在刚才的容器终端中,执行下面的命令,就可以启动 dnsmasq
```
/# /etc/init.d/dnsmasq start
* Starting DNS forwarder and DHCP server dnsmasq [ OK ]
```
然后,修改 /etc/resolv.conf将 DNS 服务器改为 dnsmasq 的监听地址,这儿是 127.0.0.1。接着,重新执行多次 nslookup 命令:
```
/# echo nameserver 127.0.0.1 &gt; /etc/resolv.conf
/# 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
```
现在我们可以看到,只有第一次的解析很慢,需要 0.5s,以后的每次解析都很快,只需要 11ms。并且后面每次 DNS 解析需要的时间也都很稳定。
案例的最后,还是别忘了执行 exit退出容器终端Docker 会自动清理案例容器。
## 小结
今天,我带你一起学习了 DNS 的基本原理,并通过几个案例,带你一起掌握了,发现 DNS 解析问题时的分析和解决思路。
DNS 是互联网中最基础的一项服务,提供了域名和 IP 地址间映射关系的查询服务。很多应用程序在最初开发时,并没考虑 DNS 解析的问题,后续出现问题后,排查好几天才能发现,其实是 DNS 解析慢导致的。
试想,假如一个 Web 服务的接口,每次都需要 1s 时间来等待 DNS 解析,那么,无论你怎么优化应用程序的内在逻辑,对用户来说,这个接口的响应都太慢,因为响应时间总是会大于 1 秒的。
所以,在应用程序的开发过程中,我们必须考虑到 DNS 解析可能带来的性能问题,掌握常见的优化方法。这里,我总结了几种常见的 DNS 优化方法。
<li>
对 DNS 解析的结果进行缓存。缓存是最有效的方法,但要注意,一旦缓存过期,还是要去 DNS 服务器重新获取新记录。不过,这对大部分应用程序来说都是可接受的。
</li>
<li>
对 DNS 解析的结果进行预取。这是浏览器等 Web 应用中最常用的方法,也就是说,不等用户点击页面上的超链接,浏览器就会在后台自动解析域名,并把结果缓存起来。
</li>
<li>
使用 HTTPDNS 取代常规的 DNS 解析。这是很多移动应用会选择的方法,特别是如今域名劫持普遍存在,使用 HTTP 协议绕过链路中的 DNS 服务器,就可以避免域名劫持的问题。
</li>
<li>
基于 DNS 的全局负载均衡GSLB。这不仅为服务提供了负载均衡和高可用的功能还可以根据用户的位置返回距离最近的 IP 地址。
</li>
## 思考
最后,我想请你来聊一聊,你所碰到的 DNS 问题。你都碰到过哪些类型的 DNS 问题?你是通过哪些方法来排查的,又通过哪些方法解决的呢?你可以结合今天学到的知识,总结自己的思路。
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,388 @@
<audio id="audio" title="38 | 案例篇:怎么使用 tcpdump 和 Wireshark 分析网络流量?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/93/a6/935e147b172f260ea55bede88c7ae8a6.mp3"></audio>
你好,我是倪朋飞。
上一节,我们学习了 DNS 性能问题的分析和优化方法。简单回顾一下DNS 可以提供域名和 IP 地址的映射关系也是一种常用的全局负载均衡GSLB实现方法。
通常,需要暴露到公网的服务,都会绑定一个域名,既方便了人们记忆,也避免了后台服务 IP 地址的变更影响到用户。
不过要注意DNS 解析受到各种网络状况的影响,性能可能不稳定。比如公网延迟增大,缓存过期导致要重新去上游服务器请求,或者流量高峰时 DNS 服务器性能不足等,都会导致 DNS 响应的延迟增大。
此时,可以借助 nslookup 或者 dig 的调试功能,分析 DNS 的解析过程,再配合 ping 等工具调试 DNS 服务器的延迟从而定位出性能瓶颈。通常你可以用缓存、预取、HTTPDNS 等方法,优化 DNS 的性能。
上一节我们用到的ping是一个最常用的测试服务延迟的工具。很多情况下ping 可以帮我们定位出延迟问题,不过有时候, ping 本身也会出现意想不到的问题。这时就需要我们抓取ping 命令执行时收发的网络包,然后分析这些网络包,进而找出问题根源。
tcpdump 和 Wireshark 就是最常用的网络抓包和分析工具,更是分析网络性能必不可少的利器。
<li>
tcpdump 仅支持命令行格式使用,常用在服务器中抓取和分析网络包。
</li>
<li>
Wireshark 除了可以抓包外,还提供了强大的图形界面和汇总分析工具,在分析复杂的网络情景时,尤为简单和实用。
</li>
因而,在实际分析网络性能时,先用 tcpdump 抓包,后用 Wireshark 分析,也是一种常用的方法。
今天,我就带你一起看看,怎么使用 tcpdump 和 Wireshark ,来分析网络的性能问题。
## 案例准备
本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境是这样的:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 tcpdump、Wireshark 等工具,如:
</li>
```
# Ubuntu
apt-get install tcpdump wireshark
# CentOS
yum install -y tcpdump wireshark
```
由于 Wireshark 的图形界面,并不能通过 SSH 使用,所以我推荐你在本地机器(比如 Windows中安装。你可以到 [https://www.wireshark.org/](https://www.wireshark.org/) 下载并安装 Wireshark。
>
跟以前一样,案例中所有命令,都默认以 root 用户在Windows中运行Wireshark时除外运行。如果你是用普通用户身份登陆系统请运行 sudo su root 命令切换到 root 用户。
## 再探 ping
前面讲过ping 是一种最常用的网络工具,常用来探测网络主机之间的连通性以及延迟。关于 ping 的原理和使用方法,我在前面的 [Linux网络基础篇](https://time.geekbang.org/column/article/81057) 已经简单介绍过,而 DNS 缓慢的案例中,也多次用到了 ping 测试 DNS 服务器的延迟RTT
不过,虽然 ping 比较简单但有时候你会发现ping 工具本身也可能出现异常,比如运行缓慢,但实际网络延迟却并不大的情况。
接下来我们打开一个终端SSH 登录到案例机器中,执行下面的命令,来测试案例机器与极客邦科技官网的连通性和延迟。如果一切正常,你会看到下面这个输出:
```
# ping 3 次默认每次发送间隔1秒
# 假设DNS服务器还是上一期配置的114.114.114.114
$ 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
rtt min/avg/max/mdev = 31.146/33.074/36.809/2.649 ms
```
ping 的输出界面, [Linux网络基础篇](https://time.geekbang.org/column/article/81057) 中我们已经学过,你可以先复习一下,自己解读并且分析这次的输出。
不过要注意,假如你运行时发现 ping 很快就结束了,那就执行下面的命令,再重试一下。至于这条命令的含义,稍后我们再做解释。
```
# 禁止接收从DNS服务器发送过来并包含googleusercontent的包
$ iptables -I INPUT -p udp --sport 53 -m string --string googleusercontent --algo bm -j DROP
```
根据 ping 的输出你可以发现geektime.org 解析后的 IP 地址是 35.190.27.188,而后三次 ping 请求都得到了响应延迟RTT都是 30ms 多一点。
但汇总的地方就有点儿意思了。3次发送收到3次响应没有丢包但三次发送和接受的总时间居然超过了 11s11049ms这就有些不可思议了吧。
会想起上一节的 DNS 解析问题,你可能会怀疑,这可能是 DNS 解析缓慢的问题。但到底是不是呢?
再回去看 ping 的输出,三次 ping 请求中,用的都是 IP 地址,说明 ping 只需要在最开始运行时,解析一次得到 IP后面就可以只用 IP了。
我们再用 nslookup 试试。在终端中执行下面的 nslookup 命令,注意,这次我们同样加了 time 命令,输出 nslookup 的执行时间:
```
$ 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
user 0m0.006s
sys 0m0.003s
```
可以看到,域名解析还是很快的,只需要 44ms显然比 11s 短了很多。
到这里,再往后该怎么分析呢?其实,这时候就可以用 tcpdump 抓包,查看 ping 在收发哪些网络包。
我们再打开另一个终端终端二SSH 登录案例机器后,执行下面的命令:
```
$ tcpdump -nn udp port 53 or host 35.190.27.188
```
当然,你可以直接用 tcpdump 不加任何参数来抓包,但那样的话,就可能抓取到很多不相干的包。由于我们已经执行过 ping 命令,知道了 geekbang.org 的 IP 地址是35.190.27.188,也知道 ping 命令会执行 DNS 查询。所以,上面这条命令,就是基于这个规则进行过滤。
我来具体解释一下这条命令。
<li>
-nn ,表示不解析抓包中的域名(即不反向解析)、协议以及端口号。
</li>
<li>
udp port 53 ,表示只显示 UDP协议的端口号包括源端口和目的端口为53的包。
</li>
<li>
host 35.190.27.188 ,表示只显示 IP 地址包括源地址和目的地址为35.190.27.188的包。
</li>
<li>
这两个过滤条件中间的“ or ”,表示或的关系,也就是说,只要满足上面两个条件中的任一个,就可以展示出来。
</li>
接下来,回到终端一,执行相同的 ping 命令:
```
$ ping -c3 geektime.org
...
--- geektime.org ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 11095ms
rtt min/avg/max/mdev = 81.473/81.572/81.757/0.130 ms
```
命令结束后,再回到终端二中,查看 tcpdump 的输出:
```
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 &gt; 114.114.114.114.53: 36909+ A? geektime.org. (30)
14:02:31.507699 IP 114.114.114.114.53 &gt; 172.16.3.4.56669: 36909 1/0/0 A 35.190.27.188 (46)
14:02:31.508164 IP 172.16.3.4 &gt; 35.190.27.188: ICMP echo request, id 4356, seq 1, length 64
14:02:31.539667 IP 35.190.27.188 &gt; 172.16.3.4: ICMP echo reply, id 4356, seq 1, length 64
14:02:31.539995 IP 172.16.3.4.60254 &gt; 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 &gt; 114.114.114.114.53: 49932+ PTR? 188.27.190.35.in-addr.arpa. (44)
14:02:41.551284 IP 172.16.3.4 &gt; 35.190.27.188: ICMP echo request, id 4356, seq 2, length 64
14:02:41.582363 IP 35.190.27.188 &gt; 172.16.3.4: ICMP echo reply, id 4356, seq 2, length 64
14:02:42.552506 IP 172.16.3.4 &gt; 35.190.27.188: ICMP echo request, id 4356, seq 3, length 64
14:02:42.583646 IP 35.190.27.188 &gt; 172.16.3.4: ICMP echo reply, id 4356, seq 3, length 64
```
这次输出中,前两行,表示 tcpdump 的选项以及接口的基本信息;从第三行开始,就是抓取到的网络包的输出。这些输出的格式,都是 `时间戳 协议 源地址.源端口 &gt; 目的地址.目的端口 网络包详细信息`(这是最基本的格式,可以通过选项增加其他字段)。
前面的字段,都比较好理解。但网络包的详细信息,本身根据协议的不同而不同。所以,要理解这些网络包的详细含义,就要对常用网络协议的基本格式以及交互原理,有基本的了解。
当然,实际上,这些内容都会记录在 IETF 互联网工程任务组)发布的 [RFC](https://tools.ietf.org/rfc/index)(请求意见稿)中。
比如,第一条就表示,从本地 IP 发送到 114.114.114.114 的 A 记录查询请求,它的报文格式记录在 RFC1035 中,你可以点击[这里](https://www.ietf.org/rfc/rfc1035.txt)查看。在这个 tcpdump 的输出中,
<li>
36909+ 表示查询标识值,它也会出现在响应中,加号表示启用递归查询。
</li>
<li>
A? 表示查询 A 记录。
</li>
<li>
geektime.org. 表示待查询的域名。
</li>
<li>
30 表示报文长度。
</li>
接下来的一条,则是从 114.114.114.114 发送回来的 DNS 响应——域名 geektime.org. 的 A 记录值为 35.190.27.188。
第三条和第四条,是 ICMP echo request 和 ICMP echo reply响应包的时间戳 14:02:31.539667,减去请求包的时间戳 14:02:31.508164 ,就可以得到,这次 ICMP 所用时间为 30ms。这看起来并没有问题。
但随后的两条反向地址解析 PTR 请求,就比较可疑了。因为我们只看到了请求包,却没有应答包。仔细观察它们的时间,你会发现,这两条记录都是发出后 5s 才出现下一个网络包,两条 PTR 记录就消耗了 10s。
再往下看,最后的四个包,则是两次正常的 ICMP 请求和响应,根据时间戳计算其延迟,也是 30ms。
到这里,其实我们也就找到了 ping 缓慢的根源,正是两次 PTR 请求没有得到响应而超时导致的。PTR 反向地址解析的目的,是从 IP 地址反查出域名但事实上并非所有IP 地址都会定义 PTR 记录,所以 PTR 查询很可能会失败。
所以,在你使用 ping 时,如果发现结果中的延迟并不大,而 ping 命令本身却很慢,不要慌,有可能是背后的 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
```
你可以发现,现在只需要 2s 就可以结束,比刚才的 11s 可是快多了。
到这里, 我就带你一起使用 tcpdump ,解决了一个最常见的 ping 工作缓慢的问题。
案例最后,如果你在开始时,执行了 iptables 命令,那也不要忘了删掉它:
```
$ iptables -D INPUT -p udp --sport 53 -m string --string googleusercontent --algo bm -j DROP
```
不过,删除后你肯定还有疑问,明明我们的案例跟 Google 没啥关系,为什么要根据 googleusercontent ,这个毫不相关的字符串来过滤包呢?
实际上,如果换一个 DNS 服务器,就可以用 PTR 反查到 35.190.27.188 所对应的域名:
```
$ nslookup -type=PTR 35.190.27.188 8.8.8.8
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
188.27.190.35.in-addr.arpa name = 188.27.190.35.bc.googleusercontent.com.
Authoritative answers can be found from:
```
你看,虽然查到了 PTR 记录,但结果并非 geekbang.org而是 188.27.190.35.bc.googleusercontent.com。其实这也是为什么案例开始时将包含 googleusercontent 的丢弃后ping 就慢了。因为 iptables ,实际上是把 PTR 响应给丢了,所以会导致 PTR 请求超时。
tcpdump 可以说是网络性能分析最有效的利器。接下来,我再带你一起看看 tcpdump 的更多使用方法。
## tcpdump
我们知道tcpdump 也是最常用的一个网络分析工具。它基于 [libpcap](https://www.tcpdump.org/) ,利用内核中的 AF_PACKET 套接字,抓取网络接口中传输的网络包;并提供了强大的过滤规则,帮你从大量的网络包中,挑出最想关注的信息。
tcpdump 为你展示了每个网络包的详细细节,这就要求,在使用前,你必须要对网络协议有基本了解。而要了解网络协议的详细设计和实现细节, [RFC](https://www.rfc-editor.org/rfc-index.html) 当然是最权威的资料。
不过RFC 的内容对初学者来说可能并不友好。如果你对网络协议还不太了解推荐你去学《TCP/IP详解》特别是第一卷的 TCP/IP 协议族。这是每个程序员都要掌握的核心基础知识。
再回到 tcpdump工具本身它的基本使用方法还是比较简单的也就是 **tcpdump [选项] [过滤表达式]**。当然,选项和表达式的外面都加了中括号,表明它们都是可选的。
>
提示:在 Linux 工具中,如果你在文档中看到,选项放在中括号里,就说明这是一个可选选项。这时候就要留意一下,这些选项是不是有默认值。
查看 tcpdump 的 [手册](https://www.tcpdump.org/manpages/tcpdump.1.html) ,以及 pcap-filter 的[手册](https://www.tcpdump.org/manpages/pcap-filter.7.html)你会发现tcpdump 提供了大量的选项以及各式各样的过滤表达式。不过不要担心,只需要掌握一些常用选项和过滤表达式,就可以满足大部分场景的需要了。
为了帮你更快上手 tcpdump 的使用,我在这里也帮你整理了一些最常见的用法,并且绘制成了表格,你可以参考使用。
首先来看一下常用的几个选项。在上面的ping 案例中,我们用过 **-nn** 选项,表示不用对 IP 地址和端口号进行名称解析。其他常用选项,我用下面这张表格来解释。
<img src="https://static001.geekbang.org/resource/image/85/ff/859d3b5c0071335429620a3fcdde4fff.png" alt="">
接下来,我们再来看常用的过滤表达式。刚刚用过的是 udp port 53 or host 35.190.27.188 ,表示抓取 DNS 协议的请求和响应包,以及源地址或目的地址为 35.190.27.188 的包。
其他常用的过滤选项,我也整理成了下面这个表格。
<img src="https://static001.geekbang.org/resource/image/48/b3/4870a28c032bdd2a26561604ae2f7cb3.png" alt="">
最后,再次强调 tcpdump 的输出格式,我在前面已经介绍了它的基本格式:
```
时间戳 协议 源地址.源端口 &gt; 目的地址.目的端口 网络包详细信息
```
其中,网络包的详细信息取决于协议,不同协议展示的格式也不同。所以,更详细的使用方法,还是需要你去查询 tcpdump 的 [man](https://www.tcpdump.org/manpages/tcpdump.1.html) 手册(执行 man tcpdump 也可以得到)。
不过讲了这么多你应该也发现了。tcpdump 虽然功能强大可是输出格式却并不直观。特别是当系统中网络包数比较多比如PPS 超过几千)的时候,你想从 tcpdump 抓取的网络包中分析问题,实在不容易。
对比之下Wireshark 则通过图形界面,以及一系列的汇总分析工具,提供了更友好的使用界面,让你可以用更快的速度,摆平网络性能问题。接下来,我们就详细来看看它。
## Wireshark
Wireshark 也是最流行的一个网络分析工具,它最大的好处就是提供了跨平台的图形界面。跟 tcpdump 类似Wireshark 也提供了强大的过滤规则表达式,同时,还内置了一系列的汇总分析工具。
比如,拿刚刚的 ping 案例来说,你可以执行下面的命令,把抓取的网络包保存到 ping.pcap 文件中:
```
$ tcpdump -nn udp port 53 or host 35.190.27.188 -w ping.pcap
```
接着,把它拷贝到你安装有 Wireshark 的机器中,比如你可以用 scp 把它拷贝到本地来:
```
$ scp host-ip/path/ping.pcap .
```
然后,再用 Wireshark 打开它。打开后,你就可以看到下面这个界面:
<img src="https://static001.geekbang.org/resource/image/6b/2c/6b854703dcfcccf64c0a69adecf2f42c.png" alt="">
从 Wireshark 的界面里,你可以发现,它不仅以更规整的格式,展示了各个网络包的头部信息;还用了不同颜色,展示 DNS 和 ICMP 这两种不同的协议。你也可以一眼看出,中间的两条 PTR 查询并没有响应包。
接着,在网络包列表中选择某一个网络包后,在其下方的网络包详情中,你还可以看到,这个包在协议栈各层的详细信息。比如,以编号为 5 的 PTR 包为例:
<img src="https://static001.geekbang.org/resource/image/59/25/59781a5dc7b1b9234643991365bfc925.png" alt="">
你可以看到IP 层Internet Protocol的源地址和目的地址、传输层的 UDP 协议Uder Datagram Protocol、应用层的 DNS 协议Domain Name System的概要信息。
继续点击每层左边的箭头,就可以看到该层协议头的所有信息。比如点击 DNS 后,就可以看到 Transaction ID、Flags、Queries 等 DNS 协议各个字段的数值以及含义。
当然Wireshark 的功能远不止如此。接下来我再带你一起,看一个 HTTP 的例子,并理解 TCP 三次握手和四次挥手的工作原理。
这个案例我们将要访问的是 [http://example.com/](http://example.com/) 。进入终端一,执行下面的命令,首先查出 example.com 的 IP。然后执行 tcpdump 命令,过滤得到的 IP 地址,并将结果保存到 web.pcap 中。
```
$ dig +short example.com
93.184.216.34
$ tcpdump -nn host 93.184.216.34 -w web.pcap
```
>
实际上,你可以在 host 表达式中,直接使用域名,即 **tcpdump -nn host example.com -w web.pcap**
接下来,切换到终端二,执行下面的 curl 命令,访问 [http://example.com](http://example.com)
```
$ curl http://example.com
```
最后,再回到终端一,按下 Ctrl+C 停止 tcpdump并把得到的 web.pcap 拷贝出来。
使用 Wireshark 打开 web.pcap 后,你就可以在 Wireshark 中,看到如下的界面:
<img src="https://static001.geekbang.org/resource/image/07/9d/07bcdba5b563ebae36f5b5b453aacd9d.png" alt="">
由于 HTTP 基于 TCP ,所以你最先看到的三个包,分别是 TCP 三次握手的包。接下来,中间的才是 HTTP 请求和响应包,而最后的三个包,则是 TCP 连接断开时的三次挥手包。
从菜单栏中,点击 Statistics -&gt; Flow Graph然后在弹出的界面中的 Flow type 选择 TCP Flows你可以更清晰的看到整个过程中 TCP 流的执行过程:
<img src="https://static001.geekbang.org/resource/image/4e/bb/4ec784752fdbc0cc5ead036a6419cbbb.png" alt="">
这其实跟各种教程上讲到的TCP 三次握手和四次挥手很类似,作为对比, 你通常看到的 TCP 三次握手和四次挥手的流程,基本是这样的:
<img src="https://static001.geekbang.org/resource/image/52/e8/5230fb678fcd3ca6b55d4644881811e8.png" alt="">
(图片来自[酷壳](https://coolshell.cn/articles/11564.html))
不过,对比这两张图,你会发现,这里抓到的包跟上面的四次挥手,并不完全一样,实际挥手过程只有三个包,而不是四个。
其实,之所以有三个包,是因为服务器端收到客户端的 FIN 后,服务器端同时也要关闭连接,这样就可以把 ACK 和 FIN 合并到一起发送,节省了一个包,变成了“三次挥手”。
而通常情况下,服务器端收到客户端的 FIN 后,很可能还没发送完数据,所以就会先回复客户端一个 ACK 包。稍等一会儿,完成所有数据包的发送后,才会发送 FIN 包。这也就是四次挥手了。
抓包后, Wireshark 中就会显示下面这个界面(原始网络包来自 Wireshark TCP 4-times close 示例,你可以点击 [这里](https://wiki.wireshark.org/TCP%204-times%20close) 下载):
<img src="https://static001.geekbang.org/resource/image/0e/99/0ecb6d11e5e7725107c0291c45aa7e99.png" alt="">
当然Wireshark 的使用方法绝不只有这些,更多的使用方法,同样可以参考 [官方文档](https://www.wireshark.org/docs/) 以及 [WIKI](https://wiki.wireshark.org/)。
## 小结
今天,我们一起学了 tcpdump 和 Wireshark 的使用方法,并通过几个案例,学会了如何运用这两个工具来分析网络的收发过程,并找出潜在的性能问题。
当你发现针对相同的网络服务,使用 IP 地址快而换成域名却慢很多时,就要想到,有可能是 DNS 在捣鬼。DNS 的解析,不仅包括从域名解析出 IP 地址的 A 记录请求,还包括性能工具帮你,“聪明”地从 IP 地址反查域名的 PTR 请求。
实际上,**根据 IP 地址反查域名、根据端口号反查协议名称,是很多网络工具默认的行为,而这往往会导致性能工具的工作缓慢**。所以,通常,网络性能工具都会提供一个选项(比如 -n 或者 -nn来禁止名称解析。
在工作中当你碰到网络性能问题时不要忘记tcpdump 和 Wireshark 这两个大杀器。你可以用它们抓取实际传输的网络包,再排查是否有潜在的性能问题。
## 思考
最后,我想请你来聊一聊,你是如何使用 tcpdump 和 Wireshark 的。你用 tcpdump 或者 Wireshark 解决过哪些网络问题呢?你又是如何排查、分析并解决的呢?你可以结合今天学到的网络知识,总结自己的思路。
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,333 @@
<audio id="audio" title="39 | 案例篇:怎么缓解 DDoS 攻击带来的性能下降问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/15/c6/15eb08485d456e1660aa901e5fb046c6.mp3"></audio>
你好,我是倪朋飞。
上一节我带你学习了tcpdump 和 Wireshark 的使用方法,并通过几个案例,带你用这两个工具实际分析了网络的收发过程。碰到网络性能问题,不要忘记可以用 tcpdump 和 Wireshark 这两个大杀器,抓取实际传输的网络包,排查潜在的性能问题。
今天,我们一起来看另外一个问题,怎么缓解 DDoSDistributed Denial of Service带来的性能下降问题。
## DDoS 简介
DDoS 的前身是 DoSDenail of Service即拒绝服务攻击指利用大量的合理请求来占用过多的目标资源从而使目标服务无法响应正常请求。
DDoSDistributed Denial of Service 则是在 DoS 的基础上,采用了分布式架构,利用多台主机同时攻击目标主机。这样,即使目标服务部署了网络防御设备,面对大量网络请求时,还是无力应对。
比如,目前已知的最大流量攻击,正是去年 Github 遭受的 [DDoS 攻击](https://githubengineering.com/ddos-incident-report/),其峰值流量已经达到了 1.35TbpsPPS 更是超过了 1.2 亿126.9 million
从攻击的原理上来看DDoS 可以分为下面几种类型。
第一种,耗尽带宽。无论是服务器还是路由器、交换机等网络设备,带宽都有固定的上限。带宽耗尽后,就会发生网络拥堵,从而无法传输其他正常的网络报文。
第二种耗尽操作系统的资源。网络服务的正常运行都需要一定的系统资源像是CPU、内存等物理资源以及连接表等软件资源。一旦资源耗尽系统就不能处理其他正常的网络连接。
第三种,消耗应用程序的运行资源。应用程序的运行,通常还需要跟其他的资源或系统交互。如果应用程序一直忙于处理无效请求,也会导致正常请求的处理变慢,甚至得不到响应。
比如,构造大量不同的域名来攻击 DNS 服务器,就会导致 DNS 服务器不停执行迭代查询,并更新缓存。这会极大地消耗 DNS 服务器的资源,使 DNS 的响应变慢。
无论是哪一种类型的 DDoS危害都是巨大的。那么如何可以发现系统遭受了 DDoS 攻击,又该如何应对这种攻击呢?接下来,我们就通过一个案例,一起来看看这些问题。
## 案例准备
下面的案例仍然基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境是这样的:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 docker、sar 、hping3、tcpdump、curl 等工具,比如 apt-get install docker.io hping3 tcpdump curl。
</li>
这些工具你应该都比较熟悉了。其中hping3 在 [系统的软中断CPU使用率升高案例](https://time.geekbang.org/column/article/72147) 中曾经介绍过,它可以构造 TCP/IP 协议数据包对系统进行安全审计、防火墙测试、DoS 攻击测试等。
本次案例用到三台虚拟机,我画了一张图来表示它们之间的关系。
<img src="https://static001.geekbang.org/resource/image/d6/12/d64dd4603a4bd90d110f382d313d8c12.png" alt="">
你可以看到,其中一台虚拟机运行 Nginx ,用来模拟待分析的 Web 服务器;而另外两台作为 Web 服务器的客户端,其中一台用作 DoS 攻击,而另一台则是正常的客户端。使用多台虚拟机的目的,自然还是为了相互隔离,避免“交叉感染”。
>
由于案例只使用了一台机器作为攻击源,所以这里的攻击,实际上还是传统的 DoS ,而非 DDoS。
接下来,我们打开三个终端,分别 SSH 登录到三台机器上下面的步骤都假设终端编号与图示VM 编号一致),并安装上面提到的这些工具。
同以前的案例一样,下面的所有命令,都默认以 root 用户运行。如果你是用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
接下来,我们就进入到案例操作环节。
## 案例分析
首先,在终端一中,执行下面的命令运行案例,也就是启动一个最基本的 Nginx 应用:
```
# 运行Nginx服务并对外开放80端口
# --network=host表示使用主机网络这是为了方便后面排查问题
$ docker run -itd --name=nginx --network=host nginx
```
然后,在终端二和终端三中,使用 curl 访问 Nginx 监听的端口,确认 Nginx 正常启动。假设 192.168.0.30 是 Nginx 所在虚拟机的 IP 地址,那么运行 curl 命令后,你应该会看到下面这个输出界面:
```
# -w表示只输出HTTP状态码及总时间-o表示将响应重定向到/dev/null
$ curl -s -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null http://192.168.0.30/
...
Http code: 200
Total time:0.002s
```
从这里可以看到,正常情况下,我们访问 Nginx 只需要 2ms0.002s)。
接着,在终端二中,运行 hping3 命令,来模拟 DoS 攻击:
```
# -S参数表示设置TCP协议的SYN同步序列号-p表示目的端口为80
# -i u10表示每隔10微秒发送一个网络帧
$ hping3 -S -p 80 -i u10 192.168.0.30
```
现在,再回到终端一,你就会发现,现在不管执行什么命令,都慢了很多。不过,在实践时要注意:
<li>
如果你的现象不那么明显,那么请尝试把参数里面的 u10 调小(比如调成 u1或者加上flood选项
</li>
<li>
如果你的终端一完全没有响应了,那么请适当调大 u10比如调成 u30否则后面就不能通过 SSH 操作 VM1。
</li>
然后,到终端三中,执行下面的命令,模拟正常客户端的连接:
```
# --connect-timeout表示连接超时时间
$ 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
```
你可以发现,在终端三中,正常客户端的连接超时了,并没有收到 Nginx 服务的响应。
这是发生了什么问题呢?我们再回到终端一中,检查网络状况。你应该还记得我们多次用过的 sar它既可以观察 PPS每秒收发的报文数还可以观察 BPS每秒收发的字节数
我们可以回到终端一中,执行下面的命令:
```
$ 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
```
关于 sar 输出中的各列含义,我在前面的 Linux 网络基础中已经介绍过,你可以点击 [这里](https://time.geekbang.org/column/article/81057) 查看,或者执行 man sar 查询手册。
从这次 sar 的输出中,你可以看到,网络接收的 PPS 已经达到了 20000 多,但是 BPS 却只有 1174 kB这样每个包的大小就只有 54B1174*1024/22274=54
这明显就是个小包了,不过具体是个什么样的包呢?那我们就用 tcpdump 抓包看看吧。
在终端一中,执行下面的 tcpdump 命令:
```
# -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 &gt; 192.168.0.30: Flags [S], seq 1288268370, win 512, length 0
09:15:48.287050 IP 192.168.0.2.27131 &gt; 192.168.0.30: Flags [S], seq 2084255254, win 512, length 0
09:15:48.287052 IP 192.168.0.2.27116 &gt; 192.168.0.30: Flags [S], seq 677393791, win 512, length 0
09:15:48.287055 IP 192.168.0.2.27141 &gt; 192.168.0.30: Flags [S], seq 1276451587, win 512, length 0
09:15:48.287068 IP 192.168.0.2.27154 &gt; 192.168.0.30: Flags [S], seq 1851495339, win 512, length 0
...
```
这个输出中Flags [S] 表示这是一个 SYN 包。大量的 SYN 包表明,这是一个 SYN Flood 攻击。如果你用上一节讲过的 Wireshark 来观察,则可以更直观地看到 SYN Flood 的过程:
<img src="https://static001.geekbang.org/resource/image/f3/13/f397305c87be6ae43e065d3262ec9113.png" alt="">
实际上SYN Flood 正是互联网中最经典的 DDoS 攻击方式。从上面这个图,你也可以看到它的原理:
<li>
即客户端构造大量的 SYN 包,请求建立 TCP 连接;
</li>
<li>
而服务器收到包后,会向源 IP 发送 SYN+ACK 报文并等待三次握手的最后一次ACK报文直到超时。
</li>
这种等待状态的 TCP 连接,通常也称为半开连接。由于连接表的大小有限,大量的半开连接就会导致连接表迅速占满,从而无法建立新的 TCP 连接。
参考下面这张 TCP 状态图,你能看到,此时,服务器端的 TCP 连接,会处于 SYN_RECEIVED 状态:
<img src="https://static001.geekbang.org/resource/image/86/a2/86dabf9cc66b29133fa6a239cfee38a2.png" alt="">
(图片来自 [Wikipedia](https://en.wikipedia.org/wiki/File:Tcp_state_diagram.png)
这其实提示了我们,查看 TCP 半开连接的方法,关键在于 SYN_RECEIVED 状态的连接。我们可以使用 netstat 来查看所有连接的状态不过要注意SYN_REVEIVED 的状态,通常被缩写为 SYN_RECV。
我们继续在终端一中,执行下面的 netstat 命令:
```
# -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。
进一步,我们还可以通过 wc 工具,来统计所有 SYN_RECV 状态的连接数:
```
$ netstat -n -p | grep SYN_REC | wc -l
193
```
找出源 IP 后,要解决 SYN 攻击的问题只要丢掉相关的包就可以。这时iptables 可以帮你完成这个任务。你可以在终端一中,执行下面的 iptables 命令:
```
$ iptables -I INPUT -s 192.168.0.2 -p tcp -j REJECT
```
然后回到终端三中,再次执行 curl 命令,查看正常用户访问 Nginx 的情况:
```
$ 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: 200
Total time:1.572171s
```
现在,你可以发现,正常用户也可以访问 Nginx 了,只是响应比较慢,从原来的 2ms 变成了现在的 1.5s。
不过一般来说SYN Flood 攻击中的源 IP 并不是固定的。比如,你可以在 hping3 命令中,加入 --rand-source 选项,来随机化源 IP。不过这时刚才的方法就不适用了。
幸好,我们还有很多其他方法,实现类似的目标。比如,你可以用以下两种方法,来限制 syn 包的速率:
```
# 限制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
```
到这里,我们已经初步限制了 SYN Flood 攻击。不过这还不够,因为我们的案例还只是单个的攻击源。
如果是多台机器同时发送 SYN Flood这种方法可能就直接无效了。因为你很可能无法 SSH 登录SSH 也是基于 TCP 的)到机器上去,更别提执行上述所有的排查命令。
所以,这还需要你事先对系统做一些 TCP 优化。
比如SYN Flood 会导致 SYN_RECV 状态的连接急剧增大。在上面的 netstat 命令中,你也可以看到 190 多个处于半开状态的连接。
不过,半开状态的连接数是有限制的,执行下面的命令,你就可以看到,默认的半连接容量只有 256
```
$ sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 256
```
换句话说, SYN 包数再稍微增大一些,就不能 SSH 登录机器了。 所以,你还应该增大半连接的容量,比如,你可以用下面的命令,将其增大为 1024
```
$ sysctl -w net.ipv4.tcp_max_syn_backlog=1024
net.ipv4.tcp_max_syn_backlog = 1024
```
另外,连接每个 SYN_RECV 时如果失败的话内核还会自动重试并且默认的重试次数是5次。你可以执行下面的命令将其减小为 1 次:
```
$ sysctl -w net.ipv4.tcp_synack_retries=1
net.ipv4.tcp_synack_retries = 1
```
除此之外TCP SYN Cookies 也是一种专门防御 SYN Flood 攻击的方法。SYN Cookies 基于连接信息包括源地址、源端口、目的地址、目的端口等以及一个加密种子如系统启动时间计算出一个哈希值SHA1这个哈希值称为 cookie。
然后,这个 cookie 就被用作序列号,来应答 SYN+ACK 包,并释放连接状态。当客户端发送完三次握手的最后一次 ACK 后,服务器就会再次计算这个哈希值,确认是上次返回的 SYN+ACK 的返回包,才会进入 TCP 的连接状态。
因而,开启 SYN Cookies 后,就不需要维护半开连接状态了,进而也就没有了半连接数的限制。
>
注意,开启 TCP syncookies 后,内核选项 net.ipv4.tcp_max_syn_backlog 也就无效了。
你可以通过下面的命令,开启 TCP SYN Cookies
```
$ sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1
```
注意,上述 sysctl 命令修改的配置都是临时的,重启后这些配置就会丢失。所以,为了保证配置持久化,你还应该把这些配置,写入 /etc/sysctl.conf 文件中。比如:
```
$ cat /etc/sysctl.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_max_syn_backlog = 1024
```
不过要记得,写入 /etc/sysctl.conf 的配置,需要执行 sysctl -p 命令后,才会动态生效。
当然案例结束后,别忘了执行 docker rm -f nginx 命令,清理案例开始时启动的 Nginx 应用。
## DDoS到底该怎么防御
到这里,今天的案例就结束了。不过,你肯定还有疑问。你应该注意到了,今天的主题是“缓解”,而不是“解决” DDoS 问题。
为什么不是解决 DDoS ,而只是缓解呢?而且今天案例中的方法,也只是让 Nginx 服务访问不再超时,但访问延迟还是比一开始时的 2ms 大得多。
实际上,当 DDoS 报文到达服务器后Linux 提供的机制只能缓解,而无法彻底解决。即使像是 SYN Flood 这样的小包攻击,其巨大的 PPS ,也会导致 Linux 内核消耗大量资源,进而导致其他网络报文的处理缓慢。
虽然你可以调整内核参数,缓解 DDoS 带来的性能问题,却也会像案例这样,无法彻底解决它。
在之前的 [C10K、C100K](https://time.geekbang.org/column/article/81268) [文章](https://time.geekbang.org/column/article/81268) 中我也提到过Linux 内核中冗长的协议栈,在 PPS 很大时,就是一个巨大的负担。对 DDoS 攻击来说,也是一样的道理。
所以,当时提到的 C10M 的方法,用到这里同样适合。比如,你可以基于 XDP 或者 DPDK构建 DDoS 方案,在内核网络协议栈前,或者跳过内核协议栈,来识别并丢弃 DDoS 报文避免DDoS 对系统其他资源的消耗。
不过,对于流量型的 DDoS 来说,当服务器的带宽被耗尽后,在服务器内部处理就无能为力了。这时,只能在服务器外部的网络设备中,设法识别并阻断流量(当然前提是网络设备要能扛住流量攻击)。比如,购置专业的入侵检测和防御设备,配置流量清洗设备阻断恶意流量等。
既然 DDoS 这么难防御,这是不是说明, Linux 服务器内部压根儿就不关注这一点,而是全部交给专业的网络设备来处理呢?
当然不是,因为 DDoS 并不一定是因为大流量或者大 PPS有时候慢速的请求也会带来巨大的性能下降这种情况称为慢速 DDoS
比如,很多针对应用程序的攻击,都会伪装成正常用户来请求资源。这种情况下,请求流量可能本身并不大,但响应流量却可能很大,并且应用程序内部也很可能要耗费大量资源处理。
这时,就需要应用程序考虑识别,并尽早拒绝掉这些恶意流量,比如合理利用缓存、增加 WAFWeb Application Firewall、使用 CDN 等等。
## 小结
今天我们学习了分布式拒绝服务DDoS时的缓解方法。DDoS 利用大量的伪造请求,使目标服务耗费大量资源,来处理这些无效请求,进而无法正常响应正常的用户请求。
由于 DDoS 的分布式、大流量、难追踪等特点,目前还没有方法可以完全防御 DDoS 带来的问题,只能设法缓解这个影响。
比如,你可以购买专业的流量清洗设备和网络防火墙,在网络入口处阻断恶意流量,只保留正常流量进入数据中心的服务器中。
在 Linux 服务器中你可以通过内核调优、DPDK、XDP 等多种方法,来增大服务器的抗攻击能力,降低 DDoS 对正常服务的影响。而在应用程序中,你可以利用各级缓存、 WAF、CDN 等方式,缓解 DDoS 对应用程序的影响。
## 思考
最后给你留一个思考题。
看到今天的案例,你可能会觉得眼熟。实际上,它正是在 [系统的软中断CPU使用率升高案例](https://time.geekbang.org/column/article/72147) 基础上扩展而来的。当时,我们是从软中断 CPU 使用率的角度来分析的也就是说DDoS 会导致软中断 CPU 使用率softirq升高。
回想一下当时的案例和分析思路,再结合今天的案例,你觉得还有没有更好的方法,来检测 DDoS 攻击呢?除了 tcpdump还有哪些方法查找这些攻击的源地址
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,434 @@
<audio id="audio" title="40 | 案例篇:网络请求延迟变大了,我该怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c2/cb/c23f2e96f03174654ce3378d68b1e7cb.mp3"></audio>
你好,我是倪朋飞。
上一节我们学习了碰到分布式拒绝服务DDoS的缓解方法。简单回顾一下DDoS 利用大量的伪造请求,导致目标服务要耗费大量资源,来处理这些无效请求,进而无法正常响应正常用户的请求。
由于 DDoS 的分布式、大流量、难追踪等特点,目前确实还没有方法,能够完全防御 DDoS 带来的问题,我们只能设法缓解 DDoS 带来的影响。
比如,你可以购买专业的流量清洗设备和网络防火墙,在网络入口处阻断恶意流量,只保留正常流量进入数据中心的服务器。
在 Linux 服务器中你可以通过内核调优、DPDK、XDP 等多种方法,增大服务器的抗攻击能力,降低 DDoS 对正常服务的影响。而在应用程序中,你可以利用各级缓存、 WAF、CDN 等方式,缓解 DDoS 对应用程序的影响。
不过要注意,如果 DDoS 的流量,已经到了 Linux 服务器中,那么,即使应用层做了各种优化,网络服务的延迟一般还是会比正常情况大很多。
所以,在实际应用中,我们通常要让 Linux 服务器,配合专业的流量清洗以及网络防火墙设备,一起来缓解这一问题。
除了 DDoS 会带来网络延迟增大外,我想,你肯定见到过不少其他原因导致的网络延迟,比如
<li>
网络传输慢,导致延迟;
</li>
<li>
Linux 内核协议栈报文处理慢,导致延迟;
</li>
<li>
应用程序数据处理慢,导致延迟等等。
</li>
那么,当碰到这些原因的延迟时,我们该怎么办呢?又该如何定位网络延迟的根源呢?今天,我就通过一个案例,带你一起看看这些问题。
## 网络延迟
我相信,提到**网络延迟**时,你可能轻松想起它的含义——网络数据传输所用的时间。不过要注意,这个时间可能是单向的,指从源地址发送到目的地址的单程时间;也可能是双向的,即从源地址发送到目的地址,然后又从目的地址发回响应,这个往返全程所用的时间。
通常,我们更常用的是双向的往返通信延迟,比如 ping 测试的结果,就是往返延时 RTTRound-Trip Time
除了网络延迟外,另一个常用的指标是**应用程序延迟**,它是指,从应用程序接收到请求,再到发回响应,全程所用的时间。通常,应用程序延迟也指的是往返延迟,是网络数据传输时间加上数据处理时间的和。
在 [Linux 网络基础篇](https://time.geekbang.org/column/article/81057) 中,我曾经介绍到,你可以用 ping 来测试网络延迟。ping 基于 ICMP 协议,它通过计算 ICMP 回显响应报文与 ICMP 回显请求报文的时间差,来获得往返延时。这个过程并不需要特殊认证,常被很多网络攻击利用,比如端口扫描工具 nmap、组包工具 hping3 等等。
所以,为了避免这些问题,很多网络服务会把 ICMP 禁止掉,这也就导致我们无法用 ping ,来测试网络服务的可用性和往返延时。这时,你可以用 traceroute 或 hping3 的 TCP 和 UDP 模式,来获取网络延迟。
比如,以 baidu.com 为例,你可以执行下面的 hping3 命令,测试你的机器到百度搜索服务器的网络延迟:
```
# -c表示发送3次请求-S表示设置TCP SYN-p表示端口号为80
$ hping3 -c 3 -S -p 80 baidu.com
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表示使用TCP协议-p表示端口号-n表示不对结果中的IP地址执行反向域名解析
$ traceroute --tcp -p 80 -n baidu.com
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就会输出一个星号。
知道了基于 TCP 测试网络服务延迟的方法后,接下来,我们就通过一个案例,来学习网络延迟升高时的分析思路。
## 案例准备
下面的案例仍然基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境是这样的:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 docker、hping3、tcpdump、curl、wrk、Wireshark 等工具,比如 apt-get install docker.io hping3 tcpdump curl。
</li>
这里的工具你应该都比较熟悉了,其中 wrk 的安装和使用方法在 [怎么评估系统的网络性能](https://time.geekbang.org/column/article/81497) 中曾经介绍过。如果你还没有安装,请执行下面的命令来安装它:
```
$ https://github.com/wg/wrk
$ cd wrk
$ apt-get install build-essential -y
$ make
$ sudo cp wrk /usr/local/bin/
```
由于Wireshark 需要图形界面,如果你的虚拟机没有图形界面,就可以把 Wireshark 安装到其他的机器中(比如 Windows 笔记本)。
本次案例用到两台虚拟机,我画了一张图来表示它们的关系。
<img src="https://static001.geekbang.org/resource/image/55/63/55347dc1ec78688da5673f29b60aa863.png" alt="">
接下来,我们打开两个终端,分别 SSH 登录到两台机器上以下步骤假设终端编号与图示VM 编号一致),并安装上面提到的这些工具。注意, curl 和 wrk 只需要安装在客户端 VM即 VM2中。
同以前的案例一样,下面的所有命令都默认以 root 用户运行,如果你是用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
>
如果安装过程中有什么问题,同样鼓励你先自己搜索解决,解决不了的,可以在留言区向我提问。如果你以前已经安装过了,就可以忽略这一点了。
接下来,我们就进入到案例操作的环节。
## 案例分析
为了对比得出延迟增大的影响,首先,我们来运行一个最简单的 Nginx也就是用官方的 Nginx 镜像启动一个容器。在终端一中,执行下面的命令,运行官方 Nginx它会在 80 端口监听:
```
$ docker run --network=host --name=good -itd nginx
fb4ed7cb9177d10e270f8320a7fb64717eac3451114c9fab3c50e02be2e88ba2
```
继续在终端一中,执行下面的命令,运行案例应用,它会监听 8080 端口:
```
$ docker run --name nginx --network=host -itd feisky/nginx:latency
b99bd136dcfd907747d9c803fdc0255e578bad6d66f4e9c32b826d75b6812724
```
然后,在终端二中执行 curl 命令,验证两个容器已经正常启动。如果一切正常,你将看到如下的输出:
```
# 80端口正常
$ curl http://192.168.0.30
&lt;!DOCTYPE html&gt;
&lt;html&gt;
...
&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
# 8080端口正常
$ curl http://192.168.0.30:8080
...
&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
```
接着,我们再用上面提到的 hping3 ,来测试它们的延迟,看看有什么区别。还是在终端二,执行下面的命令,分别测试案例机器 80 端口和 8080 端口的延迟:
```
# 测试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
```
```
# 测试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 端口和 8080 端口的性能:
```
# 测试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%
Req/Sec 6.20k 426.80 8.25k 85.50%
Latency Distribution
50% 7.78ms
75% 8.22ms
90% 9.14ms
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%
Req/Sec 1.15k 120.29 1.92k 88.50%
Latency Distribution
50% 44.02ms
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
```
从上面两个输出可以看到官方Nginx监听在80端口的平均延迟是 9.19ms,而案例 Nginx 的平均延迟(监听在 8080 端口)则是 43.6ms。从延迟的分布上来看,官方 Nginx 90% 的请求,都可以在 9ms以内完成而案例 Nginx 50% 的请求,就已经达到了 44 ms。
再结合上面 hping3 的输出,我们很容易发现,案例 Nginx 在并发请求下的延迟增大了很多,这是怎么回事呢?
分析方法我想你已经想到了,上节课学过的,使用 tcpdump 抓取收发的网络包,分析网络的收发过程有没有问题。
接下来,我们在终端一中,执行下面的 tcpdump 命令,抓取 8080 端口上收发的网络包,并保存到 nginx.pcap 文件:
```
$ tcpdump -nn tcp port 8080 -w nginx.pcap
```
然后切换到终端二中,重新执行 wrk 命令:
```
# 测试8080端口性能
$ wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/
```
当 wrk 命令结束后,再次切换回终端一,并按下 Ctrl+C 结束 tcpdump 命令。然后,再把抓取到的 nginx.pcap ,复制到装有 Wireshark 的机器中(如果 VM1 已经带有图形界面,那么可以跳过复制步骤),并用 Wireshark 打开它。
由于网络包的数量比较多,我们可以先过滤一下。比如,在选择一个包后,你可以单击右键并选择 “Follow” -&gt; “TCP Stream”如下图所示
<img src="https://static001.geekbang.org/resource/image/45/98/4590d2477d54bf9aa3d2881ff3296498.png" alt="">
然后,关闭弹出来的对话框,回到 Wireshark 主窗口。这时候,你会发现 Wireshark 已经自动帮你设置了一个过滤表达式 tcp.stream eq 24。如下图所示图中省去了源和目的IP地址
<img src="https://static001.geekbang.org/resource/image/f9/6e/f9fa457f95276ae4904a91619501376e.png" alt="">
从这里,你可以看到这个 TCP 连接从三次握手开始的每个请求和响应情况。当然,这可能还不够直观,你可以继续点击菜单栏里的 Statics -&gt; Flow Graph选中 “Limit to display filter” 并设置 Flow type 为 “TCP Flows”
<img src="https://static001.geekbang.org/resource/image/ff/cc/ff498170eb58abcdd841709fb4c036cc.png" alt="">
注意,这个图的左边是客户端,而右边是 Nginx 服务器。通过这个图就可以看出,前面三次握手,以及第一次 HTTP 请求和响应还是挺快的,但第二次 HTTP 请求就比较慢了特别是客户端在收到服务器第一个分组后40ms 后才发出了 ACK 响应(图中蓝色行)。
看到 40ms 这个值,你有没有想起什么东西呢?实际上,这是 TCP 延迟确认Delayed ACK的最小超时时间。
这里我解释一下延迟确认。这是针对 TCP ACK 的一种优化机制,也就是说,不用每次请求都发送一个 ACK而是先等一会儿比如 40ms看看有没有“顺风车”。如果这段时间内正好有其他包需要发送那就捎带着 ACK 一起发送过去。当然,如果一直等不到其他包,那就超时后单独发送 ACK。
因为案例中 40ms 发生在客户端上,我们有理由怀疑,是客户端开启了延迟确认机制。而这儿的客户端,实际上就是前面运行的 wrk。
查询 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 的问题。
不过别忘了这只是客户端的行为按理来说Nginx 服务器不应该受到这个行为的影响。那是不是我们分析网络包时,漏掉了什么线索呢?让我们回到 Wireshark 重新观察一下。
<img src="https://static001.geekbang.org/resource/image/72/8a/72eb14e8996147a458aa6523110c938a.png" alt="">
仔细观察 Wireshark 的界面,其中, 1173 号包,就是刚才说到的延迟 ACK 包;下一行的 1175 ,则是 Nginx 发送的第二个分组包,它跟 697 号包组合起来,构成一个完整的 HTTP 响应ACK 号都是 85
第二个分组没跟前一个分组697 号)一起发送,而是等到客户端对第一个分组的 ACK 后1173 号)才发送,这看起来跟延迟确认有点像,只不过,这儿不再是 ACK而是发送数据。
看到这里,我估计你想起了一个东西—— Nagle 算法(纳格算法)。进一步分析案例前,我先简单介绍一下这个算法。
Nagle 算法,是 TCP 协议中用于减少小包发送数量的一种优化算法,目的是为了提高实际带宽的利用率。
举个例子,当有效负载只有 1 字节时,再加上 TCP 头部和 IP 头部分别占用的 20 字节,整个网络包就是 41 字节,这样实际带宽的利用率只有 2.4%1/41。往大了说如果整个网络带宽都被这种小包占满那整个网络的有效利用率就太低了。
Nagle 算法正是为了解决这个问题。它通过合并 TCP 小包提高网络带宽的利用率。Nagle 算法规定,一个 TCP 连接上,最多只能有一个未被确认的未完成分组;在收到这个分组的 ACK 前,不发送其他分组。这些小分组会被组合起来,并在收到 ACK 后,用同一个分组发送出去。
显然Nagle 算法本身的想法还是挺好的,但是知道 Linux 默认的延迟确认机制后,你应该就不这么想了。因为它们一起使用时,网络延迟会明显。如下图所示:
<img src="https://static001.geekbang.org/resource/image/c5/c6/c51439692921cbf67b746a45fded2ec6.png" alt="">
<li>
当 Sever 发送了第一个分组后,由于 Client 开启了延迟确认,就需要等待 40ms 后才会回复 ACK。
</li>
<li>
同时,由于 Server 端开启了 Nagle而这时还没收到第一个分组的 ACKServer 也会在这里一直等着。
</li>
<li>
直到 40ms 超时后Client 才会回复ACK然后Server 才会继续发送第二个分组。
</li>
既然可能是 Nagle 的问题,那该怎么知道,案例 Nginx 有没有开启 Nagle 呢?
查询 tcp 的文档,你就会知道,只有设置了 TCP_NODELAY 后Nagle 算法才会禁用。所以,我们只需要查看 Nginx 的 tcp_nodelay 选项就可以了。
```
TCP_NODELAY
If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even
if there is only a small amount of data. When not set, data is buffered until there is a sufficient
amount to send out, thereby avoiding the frequent sending of small packets, which results in poor uti
lization of the network. This option is overridden by TCP_CORK; however, setting this option forces an
explicit flush of pending output, even if TCP_CORK is currently set.
```
我们回到终端一中,执行下面的命令,查看案例 Nginx 的配置:
```
$ docker exec nginx cat /etc/nginx/nginx.conf | grep tcp_nodelay
tcp_nodelay off;
```
果然,你可以看到,案例 Nginx 的 tcp_nodelay 是关闭的,将其设置为 on ,应该就可以解决了。
改完后问题是否就解决了呢自然需要验证我们一下。修改后的应用我已经打包到了Docker 镜像中,在终端一中执行下面的命令,你就可以启动它:
```
# 删除案例应用
$ docker rm -f nginx
# 启动优化后的应用
$ docker run --name nginx --network=host -itd feisky/nginx:nodelay
```
接着,切换到终端二,重新执行 wrk 测试延迟:
```
$ 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%
Req/Sec 6.22k 282.13 6.93k 68.50%
Latency Distribution
50% 7.78ms
75% 8.20ms
90% 9.02ms
99% 73.14ms
123990 requests in 10.01s, 100.50MB read
Requests/sec: 12384.04
Transfer/sec: 10.04MB
```
果然,现在延迟已经缩短成了 9ms跟我们测试的官方 Nginx 镜像是一样的Nginx 默认就是开启 tcp_nodelay 的) 。
作为对比,我们用 tcpdump ,抓取优化后的网络包(这儿实际上抓取的是官方 Nginx 监听的 80 端口)。你可以得到下面的结果:
<img src="https://static001.geekbang.org/resource/image/b5/ba/b5f1643cdca33f29408881542fca4eba.png" alt="">
从图中你可以发现,由于 Nginx 不用再等 ACK536 和 540 两个分组是连续发送的;而客户端呢,虽然仍开启了延迟确认,但这时收到了两个需要回复 ACK 的包,所以也不用等 40ms可以直接合并回复 ACK。
案例最后,不要忘记停止这两个容器应用。在终端一中,执行下面的命令,就可以删除案例应用:
```
$ docker rm -f nginx good
```
## 小结
今天,我们学习了网络延迟增大后的分析方法。网络延迟,是最核心的网络性能指标。由于网络传输、网络包处理等各种因素的影响,网络延迟不可避免。但过大的网络延迟,会直接影响用户的体验。
所以,在发现网络延迟增大后,你可以用 traceroute、hping3、tcpdump、Wireshark、strace 等多种工具,来定位网络中的潜在问题。比如,
<li>
使用 hping3 以及 wrk 等工具,确认单次请求和并发请求情况的网络延迟是否正常。
</li>
<li>
使用 traceroute确认路由是否正确并查看路由中每一跳网关的延迟。
</li>
<li>
使用 tcpdump 和 Wireshark确认网络包的收发是否正常。
</li>
<li>
使用 strace 等,观察应用程序对网络套接字的调用情况是否正常。
</li>
这样,你就可以依次从路由、网络包的收发、再到应用程序等,逐层排查,直到定位问题根源。
## 思考
最后,我想邀请你一起来聊聊,你所理解的网络延迟,以及在发现网络延迟增大时,你又是怎么分析的呢?你可以结合今天的内容,和你自己的操作记录,来总结思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,198 @@
<audio id="audio" title="41 | 案例篇:如何优化 NAT 性能?(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/59/cb/59e35c709ed73a88e940b6ceec1a01cb.mp3"></audio>
你好,我是倪朋飞。
上一节,我们探究了网络延迟增大问题的分析方法,并通过一个案例,掌握了如何用 hping3、tcpdump、Wireshark、strace 等工具,来排查和定位问题的根源。
简单回顾一下,网络延迟是最核心的网络性能指标。由于网络传输、网络包处理等各种因素的影响,网络延迟不可避免。但过大的网络延迟,会直接影响用户的体验。
所以,在发现网络延迟增大的情况后,你可以先从路由、网络包的收发、网络包的处理,再到应用程序等,从各个层级分析网络延迟,等到找出网络延迟的来源层级后,再深入定位瓶颈所在。
今天我再带你来看看另一个可能导致网络延迟的因素即网络地址转换Network Address Translation缩写为 NAT。
接下来,我们先来学习 NAT 的工作原理,并弄清楚如何优化 NAT 带来的潜在性能问题。
## NAT原理
NAT 技术可以重写 IP 数据包的源 IP 或者目的 IP被普遍地用来解决公网 IP 地址短缺的问题。它的主要原理就是,网络中的多台主机,通过共享同一个公网 IP 地址,来访问外网资源。同时,由于 NAT 屏蔽了内网网络,自然也就为局域网中的机器提供了安全隔离。
你既可以在支持网络地址转换的路由器(称为 NAT 网关)中配置 NAT也可以在 Linux 服务器中配置 NAT。如果采用第二种方式Linux 服务器实际上充当的是“软”路由器的角色。
NAT 的主要目的是实现地址转换。根据实现方式的不同NAT 可以分为三类:
<li>
静态 NAT即内网 IP 与公网 IP 是一对一的永久映射关系;
</li>
<li>
动态 NAT即内网 IP 从公网 IP 池中,动态选择一个进行映射;
</li>
<li>
网络地址端口转换 NAPTNetwork Address and Port Translation即把内网 IP 映射到公网 IP 的不同端口上,让多个内网 IP 可以共享同一个公网 IP 地址。
</li>
NAPT 是目前最流行的 NAT 类型,我们在 Linux 中配置的 NAT 也是这种类型。而根据转换方式的不同,我们又可以把 NAPT 分为三类。
第一类是源地址转换SNAT即目的地址不变只替换源 IP 或源端口。SNAT 主要用于,多个内网 IP 共享同一个公网 IP ,来访问外网资源的场景。
第二类是目的地址转换DNAT即源 IP 保持不变,只替换目的 IP 或者目的端口。DNAT 主要通过公网 IP 的不同端口号,来访问内网的多种服务,同时会隐藏后端服务器的真实 IP 地址。
第三类是双向地址转换,即同时使用 SNAT 和 DNAT。当接收到网络包时执行 DNAT把目的 IP 转换为内网 IP而在发送网络包时执行 SNAT把源 IP 替换为外部 IP。
双向地址转换,其实就是外网 IP 与内网 IP 的一对一映射关系,所以常用在虚拟化环境中,为虚拟机分配浮动的公网 IP 地址。
为了帮你理解 NAPT我画了一张图。我们假设
<li>
本地服务器的内网 IP 地址为 192.168.0.2
</li>
<li>
NAT 网关中的公网 IP 地址为 100.100.100.100
</li>
<li>
要访问的目的服务器 baidu.com 的地址为 123.125.115.110。
</li>
那么 SNAT 和 DNAT 的过程,就如下图所示:
<img src="https://static001.geekbang.org/resource/image/c7/e4/c743105dc7bd955a4a300d6b55b7a0e4.png" alt="">
从图中,你可以发现:
<li>
当服务器访问 baidu.com 时NAT 网关会把源地址,从服务器的内网 IP 192.168.0.2 替换成公网 IP 地址 100.100.100.100,然后才发送给 baidu.com
</li>
<li>
当 baidu.com 发回响应包时NAT 网关又会把目的地址,从公网 IP 地址 100.100.100.100 替换成服务器内网 IP 192.168.0.2,然后再发送给内网中的服务器。
</li>
了解了 NAT 的原理后,我们再来看看,如何在 Linux 中实现 NAT 的功能。
## iptables与NAT
Linux 内核提供的 Netfilter 框架,允许对网络数据包进行修改(比如 NAT和过滤比如防火墙。在这个基础上iptables、ip6tables、ebtables 等工具,又提供了更易用的命令行接口,以便系统管理员配置和管理 NAT、防火墙的规则。
其中iptables 就是最常用的一种配置工具。要掌握 iptables 的原理和使用方法,最核心的就是弄清楚,网络数据包通过 Netfilter 时的工作流向,下面这张图就展示了这一过程。
<img src="https://static001.geekbang.org/resource/image/c6/56/c6de40c5bd304132a1b508ba669e7b56.png" alt=""><br>
(图片来自 [Wikipedia](https://en.wikipedia.org/wiki/Iptables)
在这张图中绿色背景的方框表示表table用来管理链。Linux 支持 4 种表,包括 filter用于过滤、nat用于NAT、mangle用于修改分组数据 和 raw用于原始数据包等。
跟 table 一起的白色背景方框则表示链chain用来管理具体的 iptables 规则。每个表中可以包含多条链,比如:
<li>
filter 表中,内置 INPUT、OUTPUT 和 FORWARD 链;
</li>
<li>
nat 表中内置PREROUTING、POSTROUTING、OUTPUT 等。
</li>
当然,你也可以根据需要,创建你自己的链。
灰色的 conntrack表示连接跟踪模块。它通过内核中的连接跟踪表也就是哈希表记录网络连接的状态是 iptables 状态过滤(-m state和 NAT 的实现基础。
iptables 的所有规则,就会放到这些表和链中,并按照图中顺序和规则的优先级顺序来执行。
针对今天的主题,要实现 NAT 功能,主要是在 nat 表进行操作。而 nat 表内置了三个链:
<li>
PREROUTING用于路由判断前所执行的规则比如对接收到的数据包进行 DNAT。
</li>
<li>
POSTROUTING用于路由判断后所执行的规则比如对发送或转发的数据包进行 SNAT 或 MASQUERADE。
</li>
<li>
OUTPUT类似于 PREROUTING但只处理从本机发送出去的包。
</li>
熟悉 iptables 中的表和链后,相应的 NAT 规则就比较简单了。我们还以 NAPT 的三个分类为例,来具体解读一下。
### **SNAT**
根据刚才内容我们知道SNAT 需要在 nat 表的 POSTROUTING 链中配置。我们常用两种方式来配置它。
第一种方法,是为一个子网统一配置 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显然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 转发功能。
你可以执行下面的命令,查看这一功能是否开启。如果输出的结果是 1就表示已经开启了 IP 转发:
```
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
```
如果还没开启,你可以执行下面的命令,手动开启:
```
$ sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
```
当然,为了避免重启后配置丢失,不要忘记将配置写入 /etc/sysctl.conf 文件中:
```
$ cat /etc/sysctl.conf | grep ip_forward
net.ipv4.ip_forward=1
```
讲了这么多的原理,那当碰到 NAT 的性能问题时,又该怎么办呢?结合我们今天学过的 NAT 原理,你先自己想想,动手试试,下节课我们继续“分解”。
## 小结
今天,我们一起学习了 Linux 网络地址转换 NAT 的原理。
NAT 技术能够重写 IP 数据包的源 IP 或目的 IP所以普遍用来解决公网 IP 地址短缺的问题。它可以让网络中的多台主机,通过共享同一个公网 IP 地址,来访问外网资源。同时,由于 NAT 屏蔽了内网网络,也为局域网中机器起到安全隔离的作用。
Linux 中的NAT ,基于内核的连接跟踪模块实现。所以,它维护每个连接状态的同时,也会带来很高的性能成本。具体 NAT 性能问题的分析方法,我们将在下节课继续学习。
## 思考
最后给你留一个思考题。MASQUERADE 是最常用的一种 SNAT 规则,常用来为多个内网 IP 地址提供共享的出口 IP。
假设现在有一台 Linux 服务器,使用了 MASQUERADE 的方式,为内网的所有 IP 提供出口访问功能。那么,
<li>
当多个内网 IP 地址的端口号相同时MASQUERADE 还可以正常工作吗?
</li>
<li>
如果内网 IP 地址数量或请求数比较多,这种方式有没有什么隐患呢?
</li>
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,531 @@
<audio id="audio" title="42 | 案例篇:如何优化 NAT 性能?(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/30/bd/30b5d89dee30ee92a0400cba23d78bbd.mp3"></audio>
你好,我是倪朋飞。
上一节,我们学习了 NAT 的原理,明白了如何在 Linux 中管理 NAT 规则。先来简单复习一下。
NAT 技术能够重写 IP 数据包的源 IP 或目的 IP所以普遍用来解决公网 IP 地址短缺的问题。它可以让网络中的多台主机,通过共享同一个公网 IP 地址,来访问外网资源。同时,由于 NAT 屏蔽了内网网络,也为局域网中机器起到安全隔离的作用。
Linux 中的NAT ,基于内核的连接跟踪模块实现。所以,它维护每个连接状态的同时,也对网络性能有一定影响。那么,碰到 NAT 性能问题时,我们又该怎么办呢?
接下来,我就通过一个案例,带你学习 NAT 性能问题的分析思路。
## 案例准备
下面的案例仍然基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境是这样的:
<li>
机器配置2 CPU8GB 内存。
</li>
<li>
预先安装 docker、tcpdump、curl、ab、SystemTap 等工具,比如
</li>
```
# Ubuntu
$ apt-get install -y docker.io tcpdump curl apache2-utils
# CentOS
$ curl -fsSL https://get.docker.com | sh
$ yum install -y tcpdump curl httpd-tools
```
大部分工具,你应该都比较熟悉,这里我简单介绍一下 SystemTap 。
[SystemTap](https://sourceware.org/systemtap/) 是 Linux 的一种动态追踪框架,它把用户提供的脚本,转换为内核模块来执行,用来监测和跟踪内核的行为。关于它的原理,你暂时不用深究,后面的内容还会介绍到。这里你只要知道怎么安装就可以了:
```
# Ubuntu
apt-get install -y systemtap-runtime systemtap
# Configure ddebs source
echo &quot;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&quot; | \
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
```
本次案例还是我们最常见的 Nginx并且会用 ab 作为它的客户端,进行压力测试。案例中总共用到两台虚拟机,我画了一张图来表示它们的关系。
<img src="https://static001.geekbang.org/resource/image/70/c6/7081ad1b72535107e94f852ac41e0dc6.png" alt="">
接下来,我们打开两个终端,分别 SSH 登录到两台机器上以下步骤假设终端编号与图示VM 编号一致并安装上面提到的这些工具。注意curl 和 ab 只需要在客户端 VM即 VM2中安装。
同以前的案例一样,下面的所有命令都默认以 root 用户运行。如果你是用普通用户身份登陆系统,请运行 sudo su root 命令,切换到 root 用户。
>
如果安装过程中有什么问题,同样鼓励你先自己搜索解决,解决不了的,可以在留言区向我提问。如果你以前已经安装过了,就可以忽略这一点了。
接下来,我们就进入到案例环节。
## 案例分析
为了对比 NAT 带来的性能问题,我们首先运行一个不用 NAT 的 Nginx 服务,并用 ab 测试它的性能。
在终端一中,执行下面的命令,启动 Nginx注意选项 --network=host ,表示容器使用 Host 网络模式,即不使用 NAT
```
$ docker run --name nginx-hostnet --privileged --network=host -itd feisky/nginx:80
```
然后到终端二中,执行 curl 命令,确认 Nginx 正常启动:
```
$ curl http://192.168.0.30/
...
&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
```
继续在终端二中,执行 ab 命令,对 Nginx 进行压力测试。不过在测试前要注意Linux 默认允许打开的文件描述数比较小,比如在我的机器中,这个值只有 1024
```
# open files
$ ulimit -n
1024
```
所以,执行 ab 前,先要把这个选项调大,比如调成 65536:
```
# 临时增大当前会话的最大文件描述符数
$ ulimit -n 65536
```
接下来,再去执行 ab 命令,进行压力测试:
```
# -c表示并发请求数为5000-n表示总的请求数为10万
# -r表示套接字接收错误时仍然继续执行-s表示设置每个请求的超时时间为2s
$ ab -c 5000 -n 100000 -r -s 2 http://192.168.0.30/
...
Requests per second: 6576.21 [#/sec] (mean)
Time per request: 760.317 [ms] (mean)
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
Processing: 0 27 39.8 19 961
Waiting: 0 23 39.5 16 951
Total: 1 204 716.3 28 7349
...
```
关于 ab 输出界面的含义,我已经在 [怎么评估系统的网络性能](https://time.geekbang.org/column/article/81497) 文章中介绍过,忘了的话自己先去复习。从这次的界面,你可以看出:
<li>
每秒请求数Requests per second为 6576
</li>
<li>
每个请求的平均延迟Time per request为 760ms
</li>
<li>
建立连接的平均延迟Connect为 177ms。
</li>
记住这几个数值,这将是接下来案例的基准指标。
>
注意,你的机器中,运行结果跟我的可能不一样,不过没关系,并不影响接下来的案例分析思路。
接着回到终端一停止这个未使用NAT的Nginx应用
```
$ docker rm -f nginx-hostnet
```
再执行下面的命令,启动今天的案例应用。案例应用监听在 8080 端口,并且使用了 DNAT ,来实现 Host 的 8080 端口,到容器的 8080 端口的映射关系:
```
$ docker run --name nginx --privileged -p 8080:8080 -itd feisky/nginx:nat
```
Nginx 启动后,你可以执行 iptables 命令,确认 DNAT 规则已经创建:
```
$ iptables -nL -t nat
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 地址。
接下来,我们切换到终端二中,执行 curl 命令,确认 Nginx 已经正常启动:
```
$ curl http://192.168.0.30:8080/
...
&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
```
然后,再次执行上述的 ab 命令,不过这次注意,要把请求的端口号换成 8080
```
# -c表示并发请求数为5000-n表示总的请求数为10万
# -r表示套接字接收错误时仍然继续执行-s表示设置每个请求的超时时间为2s
$ 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)
Time per request: 65380.868 [ms] (mean)
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
Processing: 0 37916 59283.2 1 130682
Waiting: 0 2 8.7 1 414
Total: 1 39216 58711.6 1021 130682
...
```
再重新看看 ab 的输出,这次的结果显示:
<li>
每秒请求数Requests per second为 76
</li>
<li>
每个请求的延迟Time per request为 65s
</li>
<li>
建立连接的延迟Connect为 1300ms。
</li>
显然,每个指标都比前面差了很多。
那么,碰到这种问题时,你会怎么办呢?你可以根据前面的讲解,先自己分析一下,再继续学习下面的内容。
在上一节,我们使用 tcpdump 抓包的方法,找出了延迟增大的根源。那么今天的案例,我们仍然可以用类似的方法寻找线索。不过,现在换个思路,因为今天我们已经事先知道了问题的根源——那就是 NAT。
回忆一下Netfilter 中,网络包的流向以及 NAT 的原理,你会发现,要保证 NAT 正常工作,就至少需要两个步骤:
<li>
第一,利用 Netfilter 中的钩子函数Hook修改源地址或者目的地址。
</li>
<li>
第二,利用连接跟踪模块 conntrack ,关联同一个连接的请求和响应。
</li>
是不是这两个地方出现了问题呢?我们用前面提到的动态追踪工具 SystemTap 来试试。
由于今天案例是在压测场景下,并发请求数大大降低,并且我们清楚知道 NAT 是罪魁祸首。所以,我们有理由怀疑,内核中发生了丢包现象。
我们可以回到终端一中,创建一个 dropwatch.stp 的脚本文件,并写入下面的内容:
```
#! /usr/bin/env stap
############################################################
# Dropwatch.stp
# Author: Neil Horman &lt;nhorman@redhat.com&gt;
# 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(&quot;Monitoring for dropped packets\n&quot;) }
probe end { printf(&quot;Stopping dropped packet monitor\n&quot;) }
# increment a drop counter for every location we drop at
probe kernel.trace(&quot;kfree_skb&quot;) { locations[$location] &lt;&lt;&lt; 1 }
# Every 5 seconds report our drop locations
probe timer.sec(5)
{
printf(&quot;\n&quot;)
foreach (l in locations-) {
printf(&quot;%d packets dropped at %s\n&quot;,
@count(locations[l]), symname(l))
}
delete locations
}
```
这个脚本,跟踪内核函数 kfree_skb() 的调用,并统计丢包的位置。文件保存好后,执行下面的 stap 命令就可以运行丢包跟踪脚本。这里的stap是 SystemTap 的命令行工具:
```
$ stap --all-modules dropwatch.stp
Monitoring for dropped packets
```
当你看到 probe begin 输出的 “Monitoring for dropped packets” 时,表明 SystemTap 已经将脚本编译为内核模块,并启动运行了。
接着,我们切换到终端二中,再次执行 ab 命令:
```
$ ab -c 5000 -n 10000 -r -s 30 http://192.168.0.30:8080/
```
然后,再次回到终端一中,观察 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 来完成。
我们切换到终端二中,再次执行 ab 命令:
```
$ ab -c 5000 -n 10000 -r -s 30 http://192.168.0.30:8080/
```
然后,再次切换回终端一,执行 perf record 和 perf report 命令
```
# 记录一会比如30s后按Ctrl+C结束
$ perf record -a -g -- sleep 30
# 输出报告
$ perf report -g graph,0
```
在 perf report 界面中,输入查找命令 / 然后,在弹出的对话框中,输入 nf_hook_slow最后再展开调用栈就可以得到下面这个调用图
<img src="https://static001.geekbang.org/resource/image/0e/3c/0e844a471ff1062a1db70a303add943c.png" alt="">
从这个图我们可以看到nf_hook_slow 调用最多的有三个地方,分别是 ipv4_conntrack_in、br_nf_pre_routing 以及 iptable_nat_ipv4_in。换言之nf_hook_slow 主要在执行三个动作。
<li>
第一接收网络包时在连接跟踪表中查找连接并为新的连接分配跟踪对象Bucket
</li>
<li>
第二,在 Linux 网桥中转发包。这是因为案例 Nginx 是一个 Docker 容器,而容器的网络通过网桥来实现;
</li>
<li>
第三,接收网络包时,执行 DNAT即把 8080 端口收到的包转发给容器。
</li>
到这里,我们其实就找到了性能下降的三个来源。这三个来源,都是 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
...
```
你可以看到,这里最重要的三个指标:
<li>
net.netfilter.nf_conntrack_count表示当前连接跟踪数
</li>
<li>
net.netfilter.nf_conntrack_max表示最大连接跟踪数
</li>
<li>
net.netfilter.nf_conntrack_buckets表示连接跟踪表的大小。
</li>
所以,这个输出告诉我们,当前连接跟踪数是 180最大连接跟踪数是 1000连接跟踪表的大小则是 65536。
回想一下前面的 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
[104243.800401] nf_conntrack: nf_conntrack: table full, dropping packet
[104262.962157] nf_conntrack: nf_conntrack: table full, dropping packet
```
其中net_ratelimit 表示有大量的日志被压缩掉了,这是内核预防日志攻击的一种措施。而当你看到 “nf_conntrack: table full” 的错误时,就表明 nf_conntrack_max 太小了。
那是不是,直接把连接跟踪表调大就可以了呢?调节前,你先得明白,连接跟踪表,实际上是内存中的一个哈希表。如果连接跟踪数过大,也会耗费大量内存。
其实,我们上面看到的 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
```
然后再切换到终端二中,重新执行 ab 命令。注意,这次我们把超时时间也改回原来的 2s
```
$ 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
```
果然,现在你可以看到:
<li>
每秒请求数Requests per second为 6315不用NAT时为6576
</li>
<li>
每个请求的延迟Time per request为 791ms不用NAT时为760ms
</li>
<li>
建立连接的延迟Connect为 355ms不用NAT时为177ms
</li>
这个结果,已经比刚才的测试好了很多,也很接近最初不用 NAT 时的基准结果了。
不过,你可能还是很好奇,连接跟踪表里,到底都包含了哪些东西?这里的东西,又是怎么刷新的呢?
实际上,你可以用 conntrack 命令行工具,来查看连接跟踪表的内容。比如:
```
# -L表示列表-o表示以扩展格式显示
$ conntrack -L -o extended | head
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 等工具,对其进行统计分析。
比如,我们还是以 ab 为例。在终端二启动 ab 命令后,再回到终端一中,执行下面的命令:
```
# 统计总的连接跟踪数
$ 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 &quot;=&quot; -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
```
所以,如果你的连接数非常大,确实也应该考虑,适当减小超时时间。
除了上面这些常见配置conntrack 还包含了其他很多配置选项,你可以根据实际需要,参考 nf_conntrack 的[文档](https://www.kernel.org/doc/Documentation/networking/nf_conntrack-sysctl.txt)来配置。
## 小结
今天,我带你一起学习了,如何排查和优化 NAT 带来的性能问题。
由于 NAT 基于 Linux 内核的连接跟踪机制来实现。所以,在分析 NAT 性能问题时,我们可以先从 conntrack 角度来分析,比如用 systemtap、perf 等,分析内核中 conntrack 的行文;然后,通过调整 netfilter 内核选项的参数,来进行优化。
其实Linux 这种通过连接跟踪机制实现的 NAT也常被称为有状态的 NAT而维护状态也带来了很高的性能成本。
所以除了调整内核行为外在不需要状态跟踪的场景下比如只需要按预定的IP和端口进行映射而不需要动态映射我们也可以使用无状态的 NAT (比如用 tc 或基于 DPDK 开发),来进一步提升性能。
## 思考
最后,给你留一个思考题。你有没有碰到过 NAT 带来的性能问题?你是怎么定位和分析它的根源的?最后,又是通过什么方法来优化解决的?你可以结合今天的案例,总结自己的思路。
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="43 | 套路篇:网络性能优化的几个思路(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/06/cd/0619e7441f3343612b311d5d6a0fe3cd.mp3"></audio>
你好,我是倪朋飞。
上一节我们了解了NAT网络地址转换的原理学会了如何排查 NAT 带来的性能问题,最后还总结了 NAT 性能优化的基本思路。我先带你简单回顾一下。
NAT 基于 Linux 内核的连接跟踪机制,实现了 IP 地址及端口号重写的功能,主要被用来解决公网 IP 地址短缺的问题。
在分析 NAT 性能问题时,可以先从内核连接跟踪模块 conntrack 角度来分析,比如用 systemtap、perf、netstat 等工具,以及 proc 文件系统中的内核选项,来分析网络协议栈的行为;然后,通过内核选项调优、切换到无状态 NAT、使用 DPDK 等方式,进行实际优化。
通过前面的学习,你应该已经体会到,网络问题比我们前面学过的 CPU、内存或磁盘 I/O 都要复杂。无论是应用层的各种 I/O 模型,冗长的网络协议栈和众多的内核选项,抑或是各种复杂的网络环境,都提高了网络的复杂性。
不过,也不要过分担心,只要你掌握了 Linux 网络的基本原理和常见网络协议的工作流程,再结合各个网络层的性能指标来分析,你会发现,定位网络瓶颈并不难。
找到网络性能瓶颈后,下一步要做的就是优化了,也就是如何降低网络延迟,并提高网络的吞吐量。学完相关原理和案例后,我就来讲讲,优化网络性能问题的思路和一些注意事项。
由于网络优化思路的内容比较多,我们分两节来学习,今天我们先来看上篇。
## 确定优化目标
跟 CPU 和 I/O 方面的性能优化一样,优化前,我会先问问自己,网络性能优化的目标是什么?换句话说,我们观察到的网络性能指标,要达到多少才合适呢?
实际上,虽然网络性能优化的整体目标,是降低网络延迟(如 RTT和提高吞吐量如 BPS 和 PPS但具体到不同应用中每个指标的优化标准可能会不同优先级顺序也大相径庭。
就拿上一节提到的 NAT 网关来说,由于其直接影响整个数据中心的网络出入性能,所以 NAT 网关通常需要达到或接近线性转发,也就是说, PPS 是最主要的性能目标。
再如,对于数据库、缓存等系统,快速完成网络收发,即低延迟,是主要的性能目标。
而对于我们经常访问的 Web 服务来说,则需要同时兼顾吞吐量和延迟。
所以,为了更客观合理地评估优化效果,我们首先应该明确优化的标准,即要对系统和应用程序进行基准测试,得到网络协议栈各层的基准性能。
在 [怎么评估系统的网络性能](https://time.geekbang.org/column/article/81497) 中我已经介绍过网络性能测试的方法。简单回顾一下Linux 网络协议栈,是我们需要掌握的核心原理。它是基于 TCP/IP 协议族的分层结构,我用一张图来表示这个结构。
<img src="https://static001.geekbang.org/resource/image/c7/ac/c7b5b16539f90caabb537362ee7c27ac.png" alt="">
明白了这一点,在进行基准测试时,我们就可以按照协议栈的每一层来测试。由于底层是其上方各层的基础,底层性能也就决定了高层性能。所以我们要清楚,底层性能指标,其实就是对应高层的极限性能。我们从下到上来理解这一点。
首先是网络接口层和网络层,它们主要负责网络包的封装、寻址、路由,以及发送和接收。每秒可处理的网络包数 PPS就是它们最重要的性能指标特别是在小包的情况下。你可以用内核自带的发包工具 pktgen ,来测试 PPS 的性能。
再向上到传输层的 TCP 和 UDP它们主要负责网络传输。对它们而言吞吐量BPS、连接数以及延迟就是最重要的性能指标。你可以用 iperf 或 netperf ,来测试传输层的性能。
不过要注意,网络包的大小,会直接影响这些指标的值。所以,通常,你需要测试一系列不同大小网络包的性能。
最后再往上到了应用层最需要关注的是吞吐量BPS、每秒请求数以及延迟等指标。你可以用 wrk、ab 等工具,来测试应用程序的性能。
不过,这里要注意的是,测试场景要尽量模拟生产环境,这样的测试才更有价值。比如,你可以到生产环境中,录制实际的请求情况,再到测试中回放。
总之,根据这些基准指标,再结合已经观察到的性能瓶颈,我们就可以明确性能优化的目标。
## 网络性能工具
同前面学习一样,我建议从指标和工具两个不同维度出发,整理记忆网络相关的性能工具。
第一个维度,从网络性能指标出发,你更容易把性能工具同系统工作原理关联起来,对性能问题有宏观的认识和把握。这样,当你想查看某个性能指标时,就能清楚知道,可以用哪些工具。
这里,我把提供网络性能指标的工具,做成了一个表格,方便你梳理关系和理解记忆。你可以把它保存并打印出来,随时查看。当然,你也可以把它当成一个“指标工具”指南来使用。
<img src="https://static001.geekbang.org/resource/image/a1/3b/a1eb07e281e5795be83c11d7255c543b.png" alt="">
再来看第二个维度,从性能工具出发。这可以让你更快上手使用工具,迅速找出想要观察的性能指标。特别是在工具有限的情况下,我们更要充分利用好手头的每一个工具,用少量工具也要尽力挖掘出大量信息。
同样的,我也将这些常用工具,汇总成了一个表格,方便你区分和理解。自然,你也可以当成一个“工具指标”指南使用,需要时查表即可。
<img src="https://static001.geekbang.org/resource/image/0d/a0/0d87b39b89a1b7f325fc5477c0182ea0.png" alt="">
## 网络性能优化
总的来说,先要获得网络基准测试报告,然后通过相关性能工具,定位出网络性能瓶颈。再接下来的优化工作,就是水到渠成的事情了。
当然,还是那句话,要优化网络性能,肯定离不开 Linux 系统的网络协议栈和网络收发流程的辅助。你可以结合下面这张图再回忆一下这部分的知识。
<img src="https://static001.geekbang.org/resource/image/a1/3f/a118911721f9b67ce9c83de15666753f.png" alt="">
接下来,我们就可以从应用程序、套接字、传输层、网络层以及链路层等几个角度,分别来看网络性能优化的基本思路。
### 应用程序
应用程序,通常通过套接字接口进行网络操作。由于网络收发通常比较耗时,所以应用程序的优化,主要就是对网络 I/O 和进程自身的工作模型的优化。
相关内容,其实我们在 [C10K 和 C1000K 回顾](https://time.geekbang.org/column/article/81268) 的文章中已经学过了。这里我们简单回顾一下。
从网络 I/O 的角度来说,主要有下面两种优化思路。
第一种是最常用的 I/O 多路复用技术 epoll主要用来取代 select 和 poll。这其实是解决 C10K 问题的关键,也是目前很多网络应用默认使用的机制。
第二种是使用异步 I/OAsynchronous I/OAIO。AIO 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。等到 I/O完成后系统会用事件通知的方式告诉应用程序结果。不过AIO 的使用比较复杂,你需要小心处理很多边缘情况。
而从进程的工作模型来说,也有两种不同的模型用来优化。
第一种,主进程+多个 worker 子进程。其中,主进程负责管理网络连接,而子进程负责实际的业务处理。这也是最常用的一种模型。
第二种,监听到相同端口的多进程模型。在这种模型下,所有进程都会监听相同接口,并且开启 SO_REUSEPORT 选项,由内核负责,把请求负载均衡到这些监听进程中去。
除了网络 I/O 和进程的工作模型外,应用层的网络协议优化,也是至关重要的一点。我总结了常见的几种优化方法。
<li>
使用长连接取代短连接,可以显著降低 TCP 建立连接的成本。在每秒请求次数较多时,这样做的效果非常明显。
</li>
<li>
使用内存等方式,来缓存不常变化的数据,可以降低网络 I/O 次数,同时加快应用程序的响应速度。
</li>
<li>
使用 Protocol Buffer 等序列化的方式,压缩网络 I/O 的数据量,可以提高应用程序的吞吐。
</li>
<li>
使用 DNS 缓存、预取、HTTPDNS 等方式,减少 DNS 解析的延迟,也可以提升网络 I/O 的整体速度。
</li>
### 套接字
套接字可以屏蔽掉 Linux 内核中不同协议的差异,为应用程序提供统一的访问接口。每个套接字,都有一个读写缓冲区。
<li>
读缓冲区,缓存了远端发过来的数据。如果读缓冲区已满,就不能再接收新的数据。
</li>
<li>
写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会被阻塞。
</li>
所以,为了提高网络的吞吐量,你通常需要调整这些缓冲区的大小。比如:
<li>
增大每个套接字的缓冲区大小 net.core.optmem_max
</li>
<li>
增大套接字接收缓冲区大小 net.core.rmem_max 和发送缓冲区大小 net.core.wmem_max
</li>
<li>
增大 TCP 接收缓冲区大小 net.ipv4.tcp_rmem 和发送缓冲区大小 net.ipv4.tcp_wmem。
</li>
至于套接字的内核选项,我把它们整理成了一个表格,方便你在需要时参考:
<img src="https://static001.geekbang.org/resource/image/5f/f0/5f2d4957663dd8bf3410da8180ab18f0.png" alt="">
不过有几点需要你注意。
<li>
tcp_rmem 和 tcp_wmem 的三个数值分别是 mindefaultmax系统会根据这些设置自动调整TCP接收/发送缓冲区的大小。
</li>
<li>
udp_mem 的三个数值分别是 minpressuremax系统会根据这些设置自动调整UDP发送缓冲区的大小。
</li>
当然,表格中的数值只提供参考价值,具体应该设置多少,还需要你根据实际的网络状况来确定。比如,发送缓冲区大小,理想数值是吞吐量*延迟,这样才可以达到最大网络利用率。
除此之外,套接字接口还提供了一些配置选项,用来修改网络连接的行为:
<li>
为 TCP 连接设置 TCP_NODELAY 后,就可以禁用 Nagle 算法;
</li>
<li>
为 TCP 连接开启 TCP_CORK 后,可以让小包聚合成大包后再发送(注意会阻塞小包的发送);
</li>
<li>
使用 SO_SNDBUF 和 SO_RCVBUF ,可以分别调整套接字发送缓冲区和接收缓冲区的大小。
</li>
## 小结
今天,我们一起梳理了常见的 Linux 网络性能优化方法。
在优化网络性能时,你可以结合 Linux 系统的网络协议栈和网络收发流程,然后从应用程序、套接字、传输层、网络层再到链路层等,进行逐层优化。
当然,其实我们分析、定位网络瓶颈,也是基于这些进行的。定位出性能瓶颈后,就可以根据瓶颈所在的协议层进行优化。比如,今天我们学了应用程序和套接字的优化思路:
<li>
在应用程序中,主要优化 I/O 模型、工作模型以及应用层的网络协议;
</li>
<li>
在套接字层中,主要优化套接字的缓冲区大小。
</li>
而其他各个网络层的优化方法,建议你先自己想一想,下一节,我们再来一起总结。
## 思考
最后,我想邀请你一起来聊聊,你在碰到网络的性能问题时,是怎么解决的?你可以结合今天的内容,从应用程序、套接字等方面,来总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,228 @@
<audio id="audio" title="44 | 套路篇:网络性能优化的几个思路(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/52/8c/52200a44d73bf5a6d14c30d8035a6a8c.mp3"></audio>
你好,我是倪朋飞。
上一节,我们学了网络性能优化的几个思路,我先带你简单复习一下。
在优化网络的性能时,你可以结合 Linux 系统的网络协议栈和网络收发流程,然后从应用程序、套接字、传输层、网络层再到链路层等每个层次,进行逐层优化。上一期我们主要学习了应用程序和套接字的优化思路,比如:
<li>
在应用程序中,主要优化 I/O 模型、工作模型以及应用层的网络协议;
</li>
<li>
在套接字层中,主要优化套接字的缓冲区大小。
</li>
今天,我们顺着 TCP/IP 网络模型,继续向下,看看如何从传输层、网络层以及链路层中,优化 Linux 网络性能。
## 网络性能优化
### 传输层
传输层最重要的是 TCP 和 UDP 协议,所以这儿的优化,其实主要就是对这两种协议的优化。
我们首先来看TCP协议的优化。
TCP 提供了面向连接的可靠传输服务。要优化 TCP我们首先要掌握 TCP 协议的基本原理,比如流量控制、慢启动、拥塞避免、延迟确认以及状态流图(如下图所示)等。
<img src="https://static001.geekbang.org/resource/image/c0/d1/c072bb9c9dfd727ed187bc24beb3e3d1.png" alt="">
关于这些原理的细节,我就不再展开讲解了。如果你还没有完全掌握,建议你先学完这些基本原理后再来优化,而不是囫囵吞枣地乱抄乱试。
掌握这些原理后,你就可以在不破坏 TCP 正常工作的基础上,对它进行优化。下面,我分几类情况详细说明。
第一类,在请求数比较大的场景下,你可能会看到大量处于 TIME_WAIT 状态的连接,它们会占用大量内存和端口资源。这时,我们可以优化与 TIME_WAIT 状态相关的内核选项,比如采取下面几种措施。
<li>
增大处于 TIME_WAIT 状态的连接数量 net.ipv4.tcp_max_tw_buckets ,并增大连接跟踪表的大小 net.netfilter.nf_conntrack_max。
</li>
<li>
减小 net.ipv4.tcp_fin_timeout 和 net.netfilter.nf_conntrack_tcp_timeout_time_wait ,让系统尽快释放它们所占用的资源。
</li>
<li>
开启端口复用 net.ipv4.tcp_tw_reuse。这样被 TIME_WAIT 状态占用的端口,还能用到新建的连接中。
</li>
<li>
增大本地端口的范围 net.ipv4.ip_local_port_range 。这样就可以支持更多连接,提高整体的并发能力。
</li>
<li>
增加最大文件描述符的数量。你可以使用 fs.nr_open 和 fs.file-max ,分别增大进程和系统的最大文件描述符数;或在应用程序的 systemd 配置文件中,配置 LimitNOFILE ,设置应用程序的最大文件描述符数。
</li>
第二类,为了缓解 SYN FLOOD 等,利用 TCP 协议特点进行攻击而引发的性能问题,你可以考虑优化与 SYN 状态相关的内核选项,比如采取下面几种措施。
<li>
增大 TCP 半连接的最大数量 net.ipv4.tcp_max_syn_backlog ,或者开启 TCP SYN Cookies net.ipv4.tcp_syncookies ,来绕开半连接数量限制的问题(注意,这两个选项不可同时使用)。
</li>
<li>
减少 SYN_RECV 状态的连接重传 SYN+ACK 包的次数 net.ipv4.tcp_synack_retries。
</li>
第三类,在长连接的场景中,通常使用 Keepalive 来检测 TCP 连接的状态,以便对端连接断开后,可以自动回收。但是,系统默认的 Keepalive 探测间隔和重试次数,一般都无法满足应用程序的性能要求。所以,这时候你需要优化与 Keepalive 相关的内核选项,比如:
<li>
缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time
</li>
<li>
缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl
</li>
<li>
减少Keepalive 探测失败后,一直到通知应用程序前的重试次数 net.ipv4.tcp_keepalive_probes。
</li>
讲了这么多TCP 优化方法,我也把它们整理成了一个表格,方便你在需要时参考(数值仅供参考,具体配置还要结合你的实际场景来调整):
<img src="https://static001.geekbang.org/resource/image/b0/e0/b07ea76a8737ed93395736795ede44e0.png" alt="">
优化 TCP 性能时,你还要注意,如果同时使用不同优化方法,可能会产生冲突。
比如,就像网络请求延迟的案例中我们曾经分析过的,服务器端开启 Nagle 算法,而客户端开启延迟确认机制,就很容易导致网络延迟增大。
另外,在使用 NAT 的服务器上,如果开启 net.ipv4.tcp_tw_recycle ,就很容易导致各种连接失败。实际上,由于坑太多,这个选项在内核的 4.1 版本中已经删除了。
说完TCP我们再来看 UDP 的优化。
UDP 提供了面向数据报的网络协议它不需要网络连接也不提供可靠性保障。所以UDP 优化,相对于 TCP 来说,要简单得多。这里我也总结了常见的几种优化方案。
<li>
跟上篇套接字部分提到的一样,增大套接字缓冲区大小以及 UDP 缓冲区范围;
</li>
<li>
跟前面 TCP 部分提到的一样,增大本地端口号的范围;
</li>
<li>
根据 MTU 大小,调整 UDP 数据包的大小,减少或者避免分片的发生。
</li>
### 网络层
接下来,我们再来看网络层的优化。
网络层,负责网络包的封装、寻址和路由,包括 IP、ICMP 等常见协议。在网络层,最主要的优化,其实就是对路由、 IP 分片以及 ICMP 等进行调优。
第一种,从路由和转发的角度出发,你可以调整下面的内核选项。
<li>
在需要转发的服务器中,比如用作 NAT 网关的服务器或者使用 Docker 容器时,开启 IP 转发,即设置 net.ipv4.ip_forward = 1。
</li>
<li>
调整数据包的生存周期 TTL比如设置 net.ipv4.ip_default_ttl = 64。注意增大该值会降低系统性能。
</li>
<li>
开启数据包的反向地址校验,比如设置 net.ipv4.conf.eth0.rp_filter = 1。这样可以防止 IP 欺骗,并减少伪造 IP 带来的 DDoS 问题。
</li>
第二种,从分片的角度出发,最主要的是调整 MTUMaximum 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 的行为。
<li>
比如,你可以禁止 ICMP 协议,即设置 net.ipv4.icmp_echo_ignore_all = 1。这样外部主机就无法通过 ICMP 来探测主机。
</li>
<li>
或者,你还可以禁止广播 ICMP即设置 net.ipv4.icmp_echo_ignore_broadcasts = 1。
</li>
### 链路层
网络层的下面是链路层,所以最后,我们再来看链路层的优化方法。
链路层负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。自然,链路层的优化,也是围绕这些基本功能进行的。接下来,我们从不同的几个方面分别来看。
由于网卡收包后调用的中断处理程序(特别是软中断),需要消耗大量的 CPU。所以将这些中断处理程序调度到不同的 CPU 上执行,就可以显著提高网络吞吐量。这通常可以采用下面两种方法。
<li>
比如,你可以为网卡硬中断配置 CPU 亲和性smp_affinity或者开启 irqbalance 服务。
</li>
<li>
再如,你可以开启 **RPS**Receive Packet Steering**RFS**Receive Flow Steering将应用程序和软中断的处理调度到相同CPU 上,这样就可以增加 CPU 缓存命中率,减少网络延迟。
</li>
另外,现在的网卡都有很丰富的功能,原来在内核中通过软件处理的功能,可以卸载到网卡中,通过硬件来执行。
<li>
**TSO**TCP Segmentation Offload**UFO**UDP Fragmentation Offload在 TCP/UDP 协议中直接发送大包而TCP 包的分段(按照 MSS 分段)和 UDP 的分片(按照 MTU 分片)功能,由网卡来完成 。
</li>
<li>
**GSO**Generic Segmentation Offload在网卡不支持 TSO/UFO 时,将 TCP/UDP 包的分段,延迟到进入网卡前再执行。这样,不仅可以减少 CPU 的消耗,还可以在发生丢包时只重传分段后的包。
</li>
<li>
**LRO**Large Receive Offload在接收 TCP 分段包时,由网卡将其组装合并后,再交给上层网络处理。不过要注意,在需要 IP 转发的情况下,不能开启 LRO因为如果多个包的头部信息不一致LRO 合并会导致网络包的校验错误。
</li>
<li>
**GRO**Generic Receive OffloadGRO 修复了 LRO 的缺陷,并且更为通用,同时支持 TCP 和 UDP。
</li>
<li>
**RSS**Receive Side Scaling也称为多队列接收它基于硬件的多个接收队列来分配网络接收进程这样可以让多个 CPU 来处理接收到的网络包。
</li>
<li>
**VXLAN** 卸载:也就是让网卡来完成 VXLAN 的组包功能。
</li>
最后,对于网络接口本身,也有很多方法,可以优化网络的吞吐量。
<li>
比如,你可以开启网络接口的多队列功能。这样,每个队列就可以用不同的中断号,调度到不同 CPU 上执行,从而提升网络的吞吐量。
</li>
<li>
再如,你可以增大网络接口的缓冲区大小,以及队列长度等,提升网络传输的吞吐量(注意,这可能导致延迟增大)。
</li>
<li>
你还可以使用 Traffic Control 工具,为不同网络流量配置 QoS。
</li>
到这里,我就从应用程序、套接字、传输层、网络层,再到链路层,分别介绍了相应的网络性能优化方法。通过这些方法的优化后,网络性能就可以满足绝大部分场景了。
最后,别忘了一种极限场景。还记得我们学过的的 C10M 问题吗?
在单机并发 1000 万的场景中对Linux 网络协议栈进行的各种优化策略,基本都没有太大效果。因为这种情况下,网络协议栈的冗长流程,其实才是最主要的性能负担。
这时,我们可以用两种方式来优化。
第一种,使用 DPDK 技术跳过内核协议栈直接由用户态进程用轮询的方式来处理网络请求。同时再结合大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
第二种,使用内核自带的 XDP 技术,在网络包进入内核协议栈前,就对其进行处理,这样也可以实现很好的性能。
## 小结
这两节课,我们一起梳理了常见的 Linux 网络性能优化方法。
在优化网络的性能时,我们可以结合 Linux 系统的网络协议栈和网络收发流程,从应用程序、套接字、传输层、网络层再到链路层等,对每个层次进行逐层优化。
实际上,我们分析和定位网络瓶颈,也是基于这些网络层进行的。而定位出网络性能瓶颈后,我们就可以根据瓶颈所在的协议层,进行优化。具体而言:
<li>
在应用程序中,主要是优化 I/O 模型、工作模型以及应用层的网络协议;
</li>
<li>
在套接字层中,主要是优化套接字的缓冲区大小;
</li>
<li>
在传输层中,主要是优化 TCP 和 UDP 协议;
</li>
<li>
在网络层中,主要是优化路由、转发、分片以及 ICMP 协议;
</li>
<li>
最后,在链路层中,主要是优化网络包的收发、网络功能卸载以及网卡选项。
</li>
如果这些方法依然不能满足你的要求,那就可以考虑,使用 DPDK 等用户态方式,绕过内核协议栈;或者,使用 XDP在网络包进入内核协议栈前进行处理。
## 思考
在整个板块的学习中,我只列举了最常见的几个网络性能优化思路。除此之外,一定还有很多其他从应用程序、系统再到网络设备的优化方法。我想请你一起来聊聊,你还知道哪些优化方法吗?
欢迎在留言区跟我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

View File

@@ -0,0 +1,104 @@
<audio id="audio" title="45 | 答疑(五):网络收发过程中,缓冲区位置在哪里?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/53/13/53790b168243d36d147e35bbcd369713.mp3"></audio>
你好,我是倪朋飞。
专栏更新至今,四大基础模块的最后一个模块——网络篇,我们就已经学完了。很开心你还没有掉队,仍然在积极学习思考和实践操作,热情地留言和互动。还有不少同学分享了在实际生产环境中,碰到各种性能问题的分析思路和优化方法,这里也谢谢你们。
今天是性能优化答疑的第五期。照例,我从网络模块的留言中,摘出了一些典型问题,作为今天的答疑内容,集中回复。同样的,为了便于你学习理解,它们并不是严格按照文章顺序排列的。
每个问题,我都附上了留言区提问的截屏。如果你需要回顾内容原文,可以扫描每个问题右下方的二维码查看。
## 问题 1网络收发过程中缓冲区的位置
<img src="https://static001.geekbang.org/resource/image/49/28/49649598767b9ef537169558ee6be128.png" alt="">
第一点,是网络收发过程中,收发队列和缓冲区位置的疑问。
在 [关于 Linux 网络,你必须要知道这些](https://time.geekbang.org/column/article/80898) 中,我曾介绍过 Linux 网络的收发流程。这个流程涉及到了多个队列和缓冲区,包括:
<li>
网卡收发网络包时,通过 DMA 方式交互的**环形缓冲区**
</li>
<li>
网卡中断处理程序为网络帧分配的,内核数据结构 **sk_buff 缓冲区**
</li>
<li>
应用程序通过套接字接口,与网络协议栈交互时的**套接字缓冲区。**
</li>
不过相应的,就会有两个问题。
首先,这些缓冲区的位置在哪儿?是在网卡硬件中,还是在内存中?这个问题其实仔细想一下,就很容易明白——这些缓冲区都处于内核管理的内存中。
其中,**环形缓冲区**,由于需要 DMA 与网卡交互,理应属于网卡设备驱动的范围。
**sk_buff 缓冲区**是一个维护网络帧结构的双向链表链表中的每一个元素都是一个网络帧Packet。虽然 TCP/IP 协议栈分了好几层,但上下不同层之间的传递,实际上只需要操作这个数据结构中的指针,而无需进行数据复制。
**套接字缓冲区**,则允许应用程序,给每个套接字配置不同大小的接收或发送缓冲区。应用程序发送数据,实际上就是将数据写入缓冲区;而接收数据,其实就是从缓冲区中读取。至于缓冲区中数据的进一步处理,则由传输层的 TCP 或 UDP 协议来完成。
其次,这些缓冲区,跟前面内存部分讲到的 Buffer 和 Cache 有什么关联吗?
这个问题其实也不难回答。我在内存模块曾提到过,内存中提到的 Buffer ,都跟块设备直接相关;而其他的都是 Cache。
实际上sk_buff、套接字缓冲、连接跟踪等都通过 slab 分配器来管理。你可以直接通过 /proc/slabinfo来查看它们占用的内存大小。
## 问题 2内核协议栈是通过一个内核线程的方式来运行的吗
第二个问题,内核协议栈的运行,是按照一个内核线程的方式吗?在内核中,又是如何执行网络协议栈的呢?
<img src="https://static001.geekbang.org/resource/image/9b/1c/9bea298bcc349e80f46c1a406472381c.png" alt="">
说到网络收发,在中断处理文章中我曾讲过,其中的软中断处理,就有专门的内核线程 ksoftirqd。每个 CPU 都会绑定一个 ksoftirqd 内核线程,比如, 2 个CPU 时,就会有 ksoftirqd/0 和 ksoftirqd/1 这两个内核线程。
不过要注意并非所有网络功能都在软中断内核线程中处理。内核中还有很多其他机制比如硬中断、kworker、slab 等),这些机制一起协同工作,才能保证整个网络协议栈的正常运行。
关于内核中网络协议栈的工作原理,以及如何动态跟踪内核的执行流程,专栏后续还有专门的文章来讲。如果对这部分感兴趣,你可以先用我们提到过的 perf、systemtap、bcc-tools 等,试着来分析一下。
## 问题 3最大连接数是不是受限于 65535 个端口
<img src="https://static001.geekbang.org/resource/image/50/8c/504ddb710169cb247b349d0d8a32818c.png" alt=""><img src="https://static001.geekbang.org/resource/image/0a/70/0a5cb5d25a4b09c5f46ca8941e9fca70.png" alt="">
我们知道,无论 TCP 还是 UDP端口号都只占 16 位,也就说其最大值也只有 65535。那是不是说如果使用 TCP 协议,在单台机器、单个 IP 地址时,并发连接数最大也只有 65535 呢?
对于这个问题首先你要知道Linux 协议栈通过五元组来标志一个连接即协议源IP、源端口、目的IP、目的端口)。
明白了这一点,这个问题其实就有了思路。我们应该分客户端和服务器端,这两种场景来分析。
对客户端来说,每次发起 TCP 连接请求时,都需要分配一个空闲的本地端口,去连接远端的服务器。由于这个本地端口是独占的,所以客户端最多只能发起 65535 个连接。
对服务器端来说,其通常监听在固定端口上(比如 80 端口等待客户端的连接。根据五元组结构我们知道客户端的IP和端口都是可变的。如果不考虑 IP 地址分类以及资源限制,服务器端的理论最大连接数,可以达到 2 的 48 次方IP 为 32 位,端口号为 16 位远大于65535。
所以综合来看客户端最大支持65535个连接而服务器端可支持的连接数是海量的。当然由于 Linux 协议栈本身的性能以及各种物理和软件的资源限制等这么大的连接数还是远远达不到的实际上C10M 就已经很难了)。
## 问题 4 “如何优化 NAT 性能”课后思考
<img src="https://static001.geekbang.org/resource/image/c6/a0/c623453e2e054d2f4407ab1e4a87f5a0.png" alt="">
在 [如何优化 NAT 性能](https://time.geekbang.org/column/article/83189) 的最后, 我给你留了两个思考题。
MASQUERADE 是最常用的 SNAT 规则之一,通常用来为多个内网 IP 地址,提供共享的出口 IP。假设现在有一台 Linux 服务器,用了 MASQUERADE 方式,为内网所有 IP 提供出口访问功能。那么,
<li>
当多个内网 IP 地址的端口号相同时MASQUERADE 还能正常工作吗?
</li>
<li>
内网 IP 地址数量或者请求数比较多的时候,这种使用方式有没有什么潜在问题呢?
</li>
对于这两个思考题我来也、ninuxer 等同学,都给出了不错的答案:
<img src="https://static001.geekbang.org/resource/image/24/52/245ba322ff2975e56db18206f0797d52.png" alt=""><img src="https://static001.geekbang.org/resource/image/f4/55/f41d8ad99120f22a7967e3afffe97555.png" alt="">
先看第一点,当多个内网 IP 地址的端口号相同时MASQUERADE 当然仍可以正常工作。不过,你肯定也听说过,配置 MASQUERADE 后,需要各个应用程序去手动配置修改端口号。
实际上MASQUERADE 通过 conntrack 机制,记录了每个连接的信息。而在刚才第三个问题 中,我提到过,标志一个连接需要五元组,只要这五元组不是同时相同,网络连接就可以正常进行。
再看第二点,在内网 IP 地址和连接数比较小时,这种方式的问题不大。但在 IP 地址或并发连接数特别大的情况下,就可能碰到各种各样的资源限制。
比如MASQUERADE 既然把内部多个 IP ,转换成了相同的外网 IP即 SNAT那么为了确保发送出去的源端口不重复原来网络包的源端口也可能会被重新分配。这样的话转换后的外网 IP 的端口号,就成了限制连接数的一个重要因素。
除此之外连接跟踪、MASQUERADE机器的网络带宽等都是潜在的瓶颈并且还存在单点的问题。这些情况在我们实际使用中都需要特别注意。
今天主要回答这些问题,同时也欢迎你继续在留言区写下疑问和感想,我会持续不断地解答。希望借助每一次的答疑,可以和你一起,把文章知识内化为你的能力,我们不仅在实战中演练,也要在交流中进步。