mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
del
This commit is contained in:
191
极客时间专栏/geek/Linux性能优化实战/网络性能篇/33 | 关于 Linux 网络,你必须知道这些(上).md
Normal file
191
极客时间专栏/geek/Linux性能优化实战/网络性能篇/33 | 关于 Linux 网络,你必须知道这些(上).md
Normal 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 头后,根据 <源 IP、源端口、目的 IP、目的端口> 四元组作为标识,找出对应的 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 网络。你碰到过哪些网络相关的性能瓶颈?你又是怎么样来分析它们的呢?你可以结合今天学到的网络知识,提出自己的观点。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
268
极客时间专栏/geek/Linux性能优化实战/网络性能篇/34 | 关于 Linux 网络,你必须知道这些(下).md
Normal file
268
极客时间专栏/geek/Linux性能优化实战/网络性能篇/34 | 关于 Linux 网络,你必须知道这些(下).md
Normal 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 的网络性能呢?
|
||||
|
||||
## 性能指标
|
||||
|
||||
实际上,我们通常用带宽、吞吐量、延时、PPS(Packet 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 和 iproute2,iproute2 是 net-tools 的下一代。通常情况下它们会在发行版中默认安装。但如果你找不到 ifconfig 或者 ip 命令,可以安装这两个软件包。
|
||||
|
||||
|
||||
以网络接口 eth0 为例,你可以运行下面的两个命令,查看它的配置和状态:
|
||||
|
||||
```
|
||||
$ ifconfig eth0
|
||||
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
|
||||
inet 10.240.0.30 netmask 255.240.0.0 broadcast 10.255.255.255
|
||||
inet6 fe80::20d:3aff:fe07:cf2a prefixlen 64 scopeid 0x20<link>
|
||||
ether 78:0d:3a:07:cf:3a txqueuelen 1000 (Ethernet)
|
||||
RX packets 40809142 bytes 9542369803 (9.5 GB)
|
||||
RX errors 0 dropped 0 overruns 0 frame 0
|
||||
TX packets 32637401 bytes 4815573306 (4.8 GB)
|
||||
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
|
||||
|
||||
$ ip -s addr show dev eth0
|
||||
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
|
||||
link/ether 78:0d:3a:07:cf:3a brd ff:ff:ff:ff:ff:ff
|
||||
inet 10.240.0.30/12 brd 10.255.255.255 scope global eth0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::20d:3aff:fe07:cf2a/64 scope link
|
||||
valid_lft forever preferred_lft forever
|
||||
RX: bytes packets errors dropped overrun mcast
|
||||
9542432350 40809397 0 0 0 193
|
||||
TX: bytes packets errors dropped carrier collsns
|
||||
4815625265 32637658 0 0 0 0
|
||||
|
||||
```
|
||||
|
||||
你可以看到,ifconfig 和 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:(("systemd-resolve",pid=840,fd=13))
|
||||
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",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 网络性能。你常用什么指标来衡量网络的性能?又用什么思路分析相应性能问题呢?你可以结合今天学到的知识,提出自己的观点。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
198
极客时间专栏/geek/Linux性能优化实战/网络性能篇/35 | 基础篇:C10K 和 C1000K 回顾.md
Normal file
198
极客时间专栏/geek/Linux性能优化实战/网络性能篇/35 | 基础篇:C10K 和 C1000K 回顾.md
Normal 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 个请求,只要每个请求处理占用不到 200KB(2GB/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/O(Asynchronous 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 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
|
||||
|
||||
第二种机制,XDP(eXpress 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 问题。你碰到过哪些网络并发相关的性能瓶颈?你又是怎么样来分析它们的呢?你可以结合今天学到的网络知识,提出自己的观点。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
423
极客时间专栏/geek/Linux性能优化实战/网络性能篇/36 | 套路篇:怎么评估系统的网络性能?.md
Normal file
423
极客时间专栏/geek/Linux性能优化实战/网络性能篇/36 | 套路篇:怎么评估系统的网络性能?.md
Normal 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 服务来说,更多会用并发连接数和每秒请求数(QPS,Query 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.30,MAC 地址为 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 > $PGDEV
|
||||
|
||||
result=`cat $PGDEV | fgrep "Result: OK:"`
|
||||
if [ "$result" = "" ]; then
|
||||
cat $PGDEV | fgrep Result:
|
||||
fi
|
||||
}
|
||||
|
||||
# 为0号线程绑定eth0网卡
|
||||
PGDEV=/proc/net/pktgen/kpktgend_0
|
||||
pgset "rem_device_all" # 清空网卡绑定
|
||||
pgset "add_device eth0" # 添加eth0网卡
|
||||
|
||||
# 配置eth0网卡的测试选项
|
||||
PGDEV=/proc/net/pktgen/eth0
|
||||
pgset "count 1000000" # 总发包数量
|
||||
pgset "delay 5000" # 不同包之间的发送延迟(单位纳秒)
|
||||
pgset "clone_skb 0" # SKB包复制
|
||||
pgset "pkt_size 64" # 网络包大小
|
||||
pgset "dst 192.168.0.30" # 目的IP
|
||||
pgset "dst_mac 11:11:11:11:11:11" # 目的MAC
|
||||
|
||||
# 启动测试
|
||||
PGDEV=/proc/net/pktgen/pgctrl
|
||||
pgset "start"
|
||||
|
||||
```
|
||||
|
||||
稍等一会儿,测试完成后,结果可以从 /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 far(pkts-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 = "/authenticate"
|
||||
|
||||
request = function()
|
||||
return wrk.format("GET", path)
|
||||
end
|
||||
|
||||
response = function(status, headers, body)
|
||||
if not token and status == 200 then
|
||||
token = headers["X-Token"]
|
||||
path = "/resource"
|
||||
wrk.headers["X-Token"] = token
|
||||
end
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
而在执行测试时,通过 -s 选项,执行脚本的路径:
|
||||
|
||||
```
|
||||
$ wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/
|
||||
|
||||
```
|
||||
|
||||
wrk 需要你用 Lua 脚本,来构造请求负载。这对于大部分场景来说,可能已经足够了 。不过,它的缺点也正是,所有东西都需要代码来构造,并且工具本身不提供 GUI 环境。
|
||||
|
||||
像 Jmeter 或者 LoadRunner(商业产品),则针对复杂场景提供了脚本录制、回放、GUI 等更丰富的功能,使用起来也更加方便。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我带你一起回顾了网络的性能指标,并学习了网络性能的评估方法。
|
||||
|
||||
性能评估是优化网络性能的前提,只有在你发现网络性能瓶颈时,才需要进行网络性能优化。根据 TCP/IP 协议栈的原理,不同协议层关注的性能重点不完全一样,也就对应不同的性能测试方法。比如,
|
||||
|
||||
<li>
|
||||
在应用层,你可以使用 wrk、Jmeter 等模拟用户的负载,测试应用程序的每秒请求数、处理延迟、错误数等;
|
||||
</li>
|
||||
<li>
|
||||
而在传输层,则可以使用 iperf 等工具,测试 TCP 的吞吐情况;
|
||||
</li>
|
||||
<li>
|
||||
再向下,你还可以用 Linux 内核自带的 pktgen ,测试服务器的 PPS。
|
||||
</li>
|
||||
|
||||
由于低层协议是高层协议的基础。所以,一般情况下,我们需要从上到下,对每个协议层进行性能测试,然后根据性能测试的结果,结合 Linux 网络协议栈的原理,找出导致性能瓶颈的根源,进而优化网络性能。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想请你来聊一聊。
|
||||
|
||||
<li>
|
||||
你是如何评估网络性能的?
|
||||
</li>
|
||||
<li>
|
||||
在评估网络性能时,你会从哪个协议层、选择哪些指标,作为性能测试最核心的目标?
|
||||
</li>
|
||||
<li>
|
||||
你又会用哪些工具,测试并分析网络的性能呢?
|
||||
</li>
|
||||
|
||||
你可以结合今天学到的网络知识,总结自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
502
极客时间专栏/geek/Linux性能优化实战/网络性能篇/37 | 案例篇:DNS 解析时快时慢,我该怎么办?.md
Normal file
502
极客时间专栏/geek/Linux性能优化实战/网络性能篇/37 | 案例篇:DNS 解析时快时慢,我该怎么办?.md
Normal 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 的由来。
|
||||
|
||||
DNS(Domain Name System),即域名系统,是互联网中最基础的一项服务,主要提供域名和 IP 地址之间映射关系的查询服务。
|
||||
|
||||
DNS 不仅方便了人们访问不同的互联网服务,更为很多应用提供了,动态服务发现和全局负载均衡(Global Server Load Balance,GSLB)的机制。这样,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
|
||||
|
||||
; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> +trace +nodnssec time.geekbang.org
|
||||
;; global options: +cmd
|
||||
. 322086 IN NS m.root-servers.net.
|
||||
. 322086 IN NS a.root-servers.net.
|
||||
. 322086 IN NS i.root-servers.net.
|
||||
. 322086 IN NS d.root-servers.net.
|
||||
. 322086 IN NS g.root-servers.net.
|
||||
. 322086 IN NS l.root-servers.net.
|
||||
. 322086 IN NS c.root-servers.net.
|
||||
. 322086 IN NS b.root-servers.net.
|
||||
. 322086 IN NS h.root-servers.net.
|
||||
. 322086 IN NS e.root-servers.net.
|
||||
. 322086 IN NS k.root-servers.net.
|
||||
. 322086 IN NS j.root-servers.net.
|
||||
. 322086 IN NS f.root-servers.net.
|
||||
;; Received 239 bytes from 114.114.114.114#53(114.114.114.114) in 1340 ms
|
||||
|
||||
org. 172800 IN NS a0.org.afilias-nst.info.
|
||||
org. 172800 IN NS a2.org.afilias-nst.info.
|
||||
org. 172800 IN NS b0.org.afilias-nst.org.
|
||||
org. 172800 IN NS b2.org.afilias-nst.org.
|
||||
org. 172800 IN NS c0.org.afilias-nst.info.
|
||||
org. 172800 IN NS d0.org.afilias-nst.org.
|
||||
;; Received 448 bytes from 198.97.190.53#53(h.root-servers.net) in 708 ms
|
||||
|
||||
geekbang.org. 86400 IN NS dns9.hichina.com.
|
||||
geekbang.org. 86400 IN NS dns10.hichina.com.
|
||||
;; Received 96 bytes from 199.19.54.1#53(b0.org.afilias-nst.org) in 1833 ms
|
||||
|
||||
time.geekbang.org. 600 IN A 39.106.233.176
|
||||
;; Received 62 bytes from 140.205.41.16#53(dns10.hichina.com) in 4 ms
|
||||
|
||||
```
|
||||
|
||||
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 CPU,8GB 内存。
|
||||
</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。
|
||||
|
||||
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
|
||||
|
||||
## 案例分析
|
||||
|
||||
### 案例1:DNS解析失败
|
||||
|
||||
首先,执行下面的命令,进入今天的第一个案例。如果一切正常,你将可以看到下面这个输出:
|
||||
|
||||
```
|
||||
# 进入案例环境的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 "nameserver 114.114.114.114" > /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 就会自动清理刚才运行的容器。
|
||||
|
||||
### 案例2:DNS解析不稳定
|
||||
|
||||
接下来,我们再来看第二个案例。执行下面的命令,启动一个新的容器,并进入它的终端中:
|
||||
|
||||
```
|
||||
$ 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 > /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 > /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 问题?你是通过哪些方法来排查的,又通过哪些方法解决的呢?你可以结合今天学到的知识,总结自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
@@ -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 CPU,8GB 内存。
|
||||
</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次响应,没有丢包,但三次发送和接受的总时间居然超过了 11s(11049ms),这就有些不可思议了吧。
|
||||
|
||||
会想起上一节的 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 > 114.114.114.114.53: 36909+ A? geektime.org. (30)
|
||||
14:02:31.507699 IP 114.114.114.114.53 > 172.16.3.4.56669: 36909 1/0/0 A 35.190.27.188 (46)
|
||||
14:02:31.508164 IP 172.16.3.4 > 35.190.27.188: ICMP echo request, id 4356, seq 1, length 64
|
||||
14:02:31.539667 IP 35.190.27.188 > 172.16.3.4: ICMP echo reply, id 4356, seq 1, length 64
|
||||
14:02:31.539995 IP 172.16.3.4.60254 > 114.114.114.114.53: 49932+ PTR? 188.27.190.35.in-addr.arpa. (44)
|
||||
14:02:36.545104 IP 172.16.3.4.60254 > 114.114.114.114.53: 49932+ PTR? 188.27.190.35.in-addr.arpa. (44)
|
||||
14:02:41.551284 IP 172.16.3.4 > 35.190.27.188: ICMP echo request, id 4356, seq 2, length 64
|
||||
14:02:41.582363 IP 35.190.27.188 > 172.16.3.4: ICMP echo reply, id 4356, seq 2, length 64
|
||||
14:02:42.552506 IP 172.16.3.4 > 35.190.27.188: ICMP echo request, id 4356, seq 3, length 64
|
||||
14:02:42.583646 IP 35.190.27.188 > 172.16.3.4: ICMP echo reply, id 4356, seq 3, length 64
|
||||
|
||||
```
|
||||
|
||||
这次输出中,前两行,表示 tcpdump 的选项以及接口的基本信息;从第三行开始,就是抓取到的网络包的输出。这些输出的格式,都是 `时间戳 协议 源地址.源端口 > 目的地址.目的端口 网络包详细信息`(这是最基本的格式,可以通过选项增加其他字段)。
|
||||
|
||||
前面的字段,都比较好理解。但网络包的详细信息,本身根据协议的不同而不同。所以,要理解这些网络包的详细含义,就要对常用网络协议的基本格式以及交互原理,有基本的了解。
|
||||
|
||||
当然,实际上,这些内容都会记录在 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 的输出格式,我在前面已经介绍了它的基本格式:
|
||||
|
||||
```
|
||||
时间戳 协议 源地址.源端口 > 目的地址.目的端口 网络包详细信息
|
||||
|
||||
```
|
||||
|
||||
其中,网络包的详细信息取决于协议,不同协议展示的格式也不同。所以,更详细的使用方法,还是需要你去查询 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 -> 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 解决过哪些网络问题呢?你又是如何排查、分析并解决的呢?你可以结合今天学到的网络知识,总结自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
333
极客时间专栏/geek/Linux性能优化实战/网络性能篇/39 | 案例篇:怎么缓解 DDoS 攻击带来的性能下降问题?.md
Normal file
333
极客时间专栏/geek/Linux性能优化实战/网络性能篇/39 | 案例篇:怎么缓解 DDoS 攻击带来的性能下降问题?.md
Normal 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 这两个大杀器,抓取实际传输的网络包,排查潜在的性能问题。
|
||||
|
||||
今天,我们一起来看另外一个问题,怎么缓解 DDoS(Distributed Denial of Service)带来的性能下降问题。
|
||||
|
||||
## DDoS 简介
|
||||
|
||||
DDoS 的前身是 DoS(Denail of Service),即拒绝服务攻击,指利用大量的合理请求,来占用过多的目标资源,从而使目标服务无法响应正常请求。
|
||||
|
||||
DDoS(Distributed Denial of Service) 则是在 DoS 的基础上,采用了分布式架构,利用多台主机同时攻击目标主机。这样,即使目标服务部署了网络防御设备,面对大量网络请求时,还是无力应对。
|
||||
|
||||
比如,目前已知的最大流量攻击,正是去年 Github 遭受的 [DDoS 攻击](https://githubengineering.com/ddos-incident-report/),其峰值流量已经达到了 1.35Tbps,PPS 更是超过了 1.2 亿(126.9 million)。
|
||||
|
||||
从攻击的原理上来看,DDoS 可以分为下面几种类型。
|
||||
|
||||
第一种,耗尽带宽。无论是服务器还是路由器、交换机等网络设备,带宽都有固定的上限。带宽耗尽后,就会发生网络拥堵,从而无法传输其他正常的网络报文。
|
||||
|
||||
第二种,耗尽操作系统的资源。网络服务的正常运行,都需要一定的系统资源,像是CPU、内存等物理资源,以及连接表等软件资源。一旦资源耗尽,系统就不能处理其他正常的网络连接。
|
||||
|
||||
第三种,消耗应用程序的运行资源。应用程序的运行,通常还需要跟其他的资源或系统交互。如果应用程序一直忙于处理无效请求,也会导致正常请求的处理变慢,甚至得不到响应。
|
||||
|
||||
比如,构造大量不同的域名来攻击 DNS 服务器,就会导致 DNS 服务器不停执行迭代查询,并更新缓存。这会极大地消耗 DNS 服务器的资源,使 DNS 的响应变慢。
|
||||
|
||||
无论是哪一种类型的 DDoS,危害都是巨大的。那么,如何可以发现系统遭受了 DDoS 攻击,又该如何应对这种攻击呢?接下来,我们就通过一个案例,一起来看看这些问题。
|
||||
|
||||
## 案例准备
|
||||
|
||||
下面的案例仍然基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境是这样的:
|
||||
|
||||
<li>
|
||||
机器配置:2 CPU,8GB 内存。
|
||||
</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 只需要 2ms(0.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,这样每个包的大小就只有 54B(1174*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 > 192.168.0.30: Flags [S], seq 1288268370, win 512, length 0
|
||||
09:15:48.287050 IP 192.168.0.2.27131 > 192.168.0.30: Flags [S], seq 2084255254, win 512, length 0
|
||||
09:15:48.287052 IP 192.168.0.2.27116 > 192.168.0.30: Flags [S], seq 677393791, win 512, length 0
|
||||
09:15:48.287055 IP 192.168.0.2.27141 > 192.168.0.30: Flags [S], seq 1276451587, win 512, length 0
|
||||
09:15:48.287068 IP 192.168.0.2.27154 > 192.168.0.30: Flags [S], seq 1851495339, win 512, length 0
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
这个输出中,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)。
|
||||
|
||||
比如,很多针对应用程序的攻击,都会伪装成正常用户来请求资源。这种情况下,请求流量可能本身并不大,但响应流量却可能很大,并且应用程序内部也很可能要耗费大量资源处理。
|
||||
|
||||
这时,就需要应用程序考虑识别,并尽早拒绝掉这些恶意流量,比如合理利用缓存、增加 WAF(Web 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,还有哪些方法查找这些攻击的源地址?
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
434
极客时间专栏/geek/Linux性能优化实战/网络性能篇/40 | 案例篇:网络请求延迟变大了,我该怎么办?.md
Normal file
434
极客时间专栏/geek/Linux性能优化实战/网络性能篇/40 | 案例篇:网络请求延迟变大了,我该怎么办?.md
Normal 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 测试的结果,就是往返延时 RTT(Round-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 CPU,8GB 内存。
|
||||
</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
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
...
|
||||
<p><em>Thank you for using nginx.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
# 8080端口正常
|
||||
$ curl http://192.168.0.30:8080
|
||||
...
|
||||
<p><em>Thank you for using nginx.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
接着,我们再用上面提到的 hping3 ,来测试它们的延迟,看看有什么区别。还是在终端二,执行下面的命令,分别测试案例机器 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” -> “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 -> 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,而这时还没收到第一个分组的 ACK,Server 也会在这里一直等着。
|
||||
</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 不用再等 ACK,536 和 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>
|
||||
|
||||
这样,你就可以依次从路由、网络包的收发、再到应用程序等,逐层排查,直到定位问题根源。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想邀请你一起来聊聊,你所理解的网络延迟,以及在发现网络延迟增大时,你又是怎么分析的呢?你可以结合今天的内容,和你自己的操作记录,来总结思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
198
极客时间专栏/geek/Linux性能优化实战/网络性能篇/41 | 案例篇:如何优化 NAT 性能?(上).md
Normal file
198
极客时间专栏/geek/Linux性能优化实战/网络性能篇/41 | 案例篇:如何优化 NAT 性能?(上).md
Normal 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>
|
||||
网络地址端口转换 NAPT(Network 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>
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
531
极客时间专栏/geek/Linux性能优化实战/网络性能篇/42 | 案例篇:如何优化 NAT 性能?(下).md
Normal file
531
极客时间专栏/geek/Linux性能优化实战/网络性能篇/42 | 案例篇:如何优化 NAT 性能?(下).md
Normal 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 CPU,8GB 内存。
|
||||
</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 "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse
|
||||
deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse
|
||||
deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | \
|
||||
sudo tee -a /etc/apt/sources.list.d/ddebs.list
|
||||
# Install dbgsym
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F2EDC64DC5AEE1F6B9C621F0C8CAB6595FDFF622
|
||||
apt-get update
|
||||
apt install ubuntu-dbgsym-keyring
|
||||
stap-prep
|
||||
apt-get install linux-image-`uname -r`-dbgsym
|
||||
|
||||
# CentOS
|
||||
yum install systemtap kernel-devel yum-utils kernel
|
||||
stab-prep
|
||||
|
||||
```
|
||||
|
||||
本次案例还是我们最常见的 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/
|
||||
...
|
||||
<p><em>Thank you for using nginx.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
继续在终端二中,执行 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/
|
||||
...
|
||||
<p><em>Thank you for using nginx.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
然后,再次执行上述的 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 <nhorman@redhat.com>
|
||||
# An example script to mimic the behavior of the dropwatch utility
|
||||
# http://fedorahosted.org/dropwatch
|
||||
############################################################
|
||||
|
||||
# Array to hold the list of drop points we find
|
||||
global locations
|
||||
|
||||
# Note when we turn the monitor on and off
|
||||
probe begin { printf("Monitoring for dropped packets\n") }
|
||||
probe end { printf("Stopping dropped packet monitor\n") }
|
||||
|
||||
# increment a drop counter for every location we drop at
|
||||
probe kernel.trace("kfree_skb") { locations[$location] <<< 1 }
|
||||
|
||||
# Every 5 seconds report our drop locations
|
||||
probe timer.sec(5)
|
||||
{
|
||||
printf("\n")
|
||||
foreach (l in locations-) {
|
||||
printf("%d packets dropped at %s\n",
|
||||
@count(locations[l]), symname(l))
|
||||
}
|
||||
delete locations
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这个脚本,跟踪内核函数 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 "=" -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 带来的性能问题?你是怎么定位和分析它的根源的?最后,又是通过什么方法来优化解决的?你可以结合今天的案例,总结自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
183
极客时间专栏/geek/Linux性能优化实战/网络性能篇/43 | 套路篇:网络性能优化的几个思路(上).md
Normal file
183
极客时间专栏/geek/Linux性能优化实战/网络性能篇/43 | 套路篇:网络性能优化的几个思路(上).md
Normal 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/O(Asynchronous I/O,AIO)。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 的三个数值分别是 min,default,max,系统会根据这些设置,自动调整TCP接收/发送缓冲区的大小。
|
||||
</li>
|
||||
<li>
|
||||
udp_mem 的三个数值分别是 min,pressure,max,系统会根据这些设置,自动调整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>
|
||||
|
||||
而其他各个网络层的优化方法,建议你先自己想一想,下一节,我们再来一起总结。
|
||||
|
||||
## 思考
|
||||
|
||||
最后,我想邀请你一起来聊聊,你在碰到网络的性能问题时,是怎么解决的?你可以结合今天的内容,从应用程序、套接字等方面,来总结自己的思路。
|
||||
|
||||
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
228
极客时间专栏/geek/Linux性能优化实战/网络性能篇/44 | 套路篇:网络性能优化的几个思路(下).md
Normal file
228
极客时间专栏/geek/Linux性能优化实战/网络性能篇/44 | 套路篇:网络性能优化的几个思路(下).md
Normal 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>
|
||||
|
||||
第二种,从分片的角度出发,最主要的是调整 MTU(Maximum Transmission Unit)的大小。
|
||||
|
||||
通常,MTU 的大小应该根据以太网的标准来设置。以太网标准规定,一个网络帧最大为 1518B,那么去掉以太网头部的 18B 后,剩余的 1500 就是以太网 MTU 的大小。
|
||||
|
||||
在使用 VXLAN、GRE 等叠加网络技术时,要注意,网络叠加会使原来的网络包变大,导致 MTU 也需要调整。
|
||||
|
||||
比如,就以 VXLAN 为例,它在原来报文的基础上,增加了 14B 的以太网头部、 8B 的 VXLAN 头部、8B 的 UDP 头部以及 20B 的 IP 头部。换句话说,每个包比原来增大了 50B。
|
||||
|
||||
所以,我们就需要把交换机、路由器等的 MTU,增大到 1550, 或者把 VXLAN 封包前(比如虚拟化环境中的虚拟网卡)的 MTU 减小为 1450。
|
||||
|
||||
另外,现在很多网络设备都支持巨帧,如果是这种环境,你还可以把 MTU 调大为 9000,以提高网络吞吐量。
|
||||
|
||||
第三种,从 ICMP 的角度出发,为了避免 ICMP 主机探测、ICMP Flood 等各种网络问题,你可以通过内核选项,来限制 ICMP 的行为。
|
||||
|
||||
<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 Offload):GRO 修复了 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,在网络包进入内核协议栈前进行处理。
|
||||
|
||||
## 思考
|
||||
|
||||
在整个板块的学习中,我只列举了最常见的几个网络性能优化思路。除此之外,一定还有很多其他从应用程序、系统再到网络设备的优化方法。我想请你一起来聊聊,你还知道哪些优化方法吗?
|
||||
|
||||
欢迎在留言区跟我讨论,也欢迎你把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
|
||||
|
||||
|
||||
104
极客时间专栏/geek/Linux性能优化实战/网络性能篇/45 | 答疑(五):网络收发过程中,缓冲区位置在哪里?.md
Normal file
104
极客时间专栏/geek/Linux性能优化实战/网络性能篇/45 | 答疑(五):网络收发过程中,缓冲区位置在哪里?.md
Normal 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机器的网络带宽等,都是潜在的瓶颈,并且还存在单点的问题。这些情况,在我们实际使用中都需要特别注意。
|
||||
|
||||
今天主要回答这些问题,同时也欢迎你继续在留言区写下疑问和感想,我会持续不断地解答。希望借助每一次的答疑,可以和你一起,把文章知识内化为你的能力,我们不仅在实战中演练,也要在交流中进步。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user