网络是一种把不同计算机或网络设备连接到一起的技术,它本质上是一种进程间通信方式,特别是跨系统的进程间通信,必须要通过网络才能进行。随着高并发、分布式、云计算、微服务等技术的普及,网络的性能也变得越来越重要。
1 测试工具
hping3
hping3 在 系统的软中断 CPU 使用率升高案例 中曾经介绍过,它可以构造 TCP/IP 协议数据包,对系统进行安全审计、防火墙测试、DoS 攻击测试等。
运行 hping3 命令,来模拟 DoS 攻击
# -S参数表示设置TCP协议的SYN(同步序列号),-p表示目的端口为80
# -i u10表示每隔10微秒发送一个网络帧
$ hping3 -S -p 80 -i u10 192.168.0.30
在实践时要注意:如果你的现象不那么明显,那么请尝试把参数里面的 u10 调小(比如调成 u1),或者加上–flood 选项;如果你的终端一完全没有响应了,那么请适当调大 u10(比如调成 u30),否则后面就不能通过 SSH 操作 VM1。
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
http性能
测试 HTTP 的性能,也有大量的工具可以使用,比如 ab、webbench 等,都是常用的 HTTP 压力测试工具。其中,ab 是 Apache 自带的 HTTP 压测工具,主要测试 HTTP 服务的每秒请求数、请求延迟、吞吐量以及请求延迟的分布情况等。
应用负载性能
当你用 iperf 或者 ab 等测试工具,得到 TCP、HTTP 等的性能数据后,这些数据是否就能表示应用程序的实际性能呢?我想,你的答案应该是否定的。比如,你的应用程序基于 HTTP 协议,为最终用户提供一个 Web 服务。这时,使用 ab 工具,可以得到某个页面的访问性能,但这个结果跟用户的实际请求,很可能不一致。因为用户请求往往会附带着各种各种的负载(payload),而这些负载会影响 Web 应用程序内部的处理逻辑,从而影响最终性能。那么,为了得到应用程序的实际性能,就要求性能工具本身可以模拟用户的请求负载,而 iperf、ab 这类工具就无能为力了。幸运的是,我们还可以用 wrk、TCPCopy、Jmeter 或者 LoadRunner 等实现这个目标。
以 wrk 为例,它是一个 HTTP 性能测试工具,内置了 LuaJIT,方便你根据实际需求,生成所需的请求负载,或者自定义响应的处理方法。
2 指标
带宽、吞吐量、延时、PPS(Packet Per Second)等指标衡量网络的性能。
带宽,表示链路的最大传输速率,单位通常为 b/s (比特 / 秒)。
吞吐量,表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽限制,而吞吐量 / 带宽,也就是该网络的使用率。
延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。
PPS,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。
除了这些指标,网络的可用性(网络能否正常通信)、并发连接数(TCP 连接数量)、丢包率(丢包百分比)、重传率(重新传输的网络包比例)等也是常用的性能指标。
3 工具
ifconfig 或者 ip
ifconfig 和 ip 分别属于软件包 net-tools 和 iproute2,iproute2 是 net-tools 的下一代。通常情况下它们会在发行版中默认安装。但如果你找不到 ifconfig 或者 ip 命令,可以安装这两个软件包
这些具体指标的含义,在文档中都有详细的说明,不过,这里有几个跟网络性能密切相关的指标,需要你特别关注一下。
第一,网络接口的状态标志。ifconfig 输出中的 RUNNING ,或 ip 输出中的 LOWER_UP ,都表示物理网络是连通的,即网卡已经连接到了交换机或者路由器中。如果你看不到它们,通常表示网线被拔掉了。
第二,MTU 的大小。MTU 默认大小是 1500,根据网络架构的不同(比如是否使用了 VXLAN 等叠加网络),你可能需要调大或者调小 MTU 的数值。
第三,网络接口的 IP 地址、子网以及 MAC 地址。这些都是保障网络功能正常工作所必需的,你需要确保配置正确。
第四,网络收发的字节数、包数、错误数以及丢包情况,特别是 TX 和 RX 部分的 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,通常表示出现了网络 I/O 问题。
其中:errors 表示发生错误的数据包数,比如校验错误、帧同步错误等;
dropped 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer,但因为内存不足等原因丢包;
overruns 表示超限数据包数,即网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;
carrier 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等;
collisions 表示碰撞数据包数。
netstat 或者 ss ,来查看套接字、网络栈、网络接口以及路由表的信息
sar 增加 -n 参数就可以查看网络的统计信息,比如网络接口(DEV)、网络接口错误(EDEV)、TCP、UDP、ICMP 等等
hping3 (CPU案例中作为一个 SYN 攻击的工具来使用。)实际上, hping3 更多的用途,是作为一个测试网络包处理能力的性能工具
pktgen 支持丰富的自定义选项,方便你根据实际需要构造所需网络包,从而更准确地测试出目标服务器的性能。
iperf 或者 netperf 测试 TCP 和 UDP 的吞吐量。它们都以客户端和服务器通信的方式,测试一段时间内的平均吞吐量
nslookup和dig——dns
tcpdump 和 Wireshark 就是最常用的网络抓包和分析工具,更是分析网络性能必不可少的利器
tcpdump 仅支持命令行格式使用,常用在服务器中抓取和分析网络包。Wireshark 除了可以抓包外,还提供了强大的图形界面和汇总分析工具,在分析复杂的网络情景时,尤为简单和实用。
yum install -y tcpdump wireshark
SystemTap 是 Linux 的一种动态追踪框架,它把用户提供的脚本,转换为内核模块来执行,用来监测和跟踪内核的行为
4 命令
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
你可以看到,往返延迟 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),就会输出一个星号
ss
# -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:*
接收队列(Recv-Q)和发送队列(Send-Q)需要你特别关注,它们通常应该是 0。当你发现它们不是 0 时,说明有网络包的堆积发生。当然还要注意,在不同套接字状态下,它们的含义不同。
当套接字处于连接状态(Established)时,Recv-Q 表示套接字缓冲还没有被应用程序取走的字节数(即接收队列长度)。而 Send-Q 表示还没有被远端主机确认的字节数(即发送队列长度)。
当套接字处于监听状态(Listening)时,Recv-Q 表示全连接队列的长度。而 Send-Q 表示全连接队列的最大长度。
所谓全连接,是指服务器收到了客户端的 ACK,完成了 TCP 三次握手,然后就会把这个连接挪到全连接队列中。这些全连接中的套接字,还需要被 accept() 系统调用取走,服务器才可以开始真正处理客户端的请求。
与全连接队列相对应的,还有一个半连接队列。所谓半连接是指还没有完成 TCP 三次握手的连接,连接只进行了一半。服务器收到了客户端的 SYN 包后,就会把这个连接放到半连接队列中,然后再向客户端发送 SYN+ACK 包
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
iperf
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 相比,还是有些差距的。
ab
yum install -y httpd-tools
# -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 的测试结果分为三个部分,分别是请求汇总、连接时间汇总还有请求延迟汇总。以上面的结果为例,我们具体来看。在请求汇总部分,你可以看到:Requests per second 为 1074;每个请求的延迟(Time per request)分为两行,第一行的 927 ms 表示平均延迟,包括了线程运行的调度时间和网络请求响应时间,而下一行的 0.927ms ,则表示实际请求的响应时间;Transfer rate 表示吞吐量(BPS)为 890 KB/s。连接时间汇总部分,则是分别展示了建立连接、请求、等待以及汇总等的各类时间,包括最小、最大、平均以及中值处理时间。最后的请求延迟汇总部分,则给出了不同时间段内处理请求的百分比,比如, 90% 的请求,都可以在 274ms 内完成
wrk
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,如下图所示:
你可以在 setup 阶段,为请求设置认证参数
-- example script that demonstrates response handling and
-- retrieving an authentication token to set on all future
-- requests
token = nil
path = "/authenticate"
request = function()
return wrk.format("GET", path)
end
response = function(status, headers, body)
if not token and status == 200 then
token = headers["X-Token"]
path = "/resource"
wrk.headers["X-Token"] = token
end
end
而在执行测试时,通过 -s 选项,执行脚本的路径:
$ wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/
wrk 需要你用 Lua 脚本,来构造请求负载。这对于大部分场景来说,可能已经足够了 。不过,它的缺点也正是,所有东西都需要代码来构造,并且工具本身不提供 GUI 环境。像 Jmeter 或者 LoadRunner(商业产品),则针对复杂场景提供了脚本录制、回放、GUI 等更丰富的功能,使用起来也更加方便。
像 Jmeter 或者 LoadRunner(商业产品),则针对复杂场景提供了脚本录制、回放、GUI 等更丰富的功能,使用起来也更加方便
nslookup
$ 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
dig
# +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 的输出,主要包括四部分。
第一部分,是从 114.114.114.114 查到的一些根域名服务器(.)的 NS 记录。
第二部分,是从 NS 记录结果中选一个(h.root-servers.net),并查询顶级域名 org. 的 NS 记录。
第三部分,是从 org. 的 NS 记录中选择一个(b0.org.afilias-nst.org),并查询二级域名 geekbang.org. 的 NS 服务器。
最后一部分,就是从 geekbang.org. 的 NS 服务器(dns10.hichina.com)查询最终主机 time.geekbang.org. 的 A 记录。这个输出里展示的各级域名的 NS 记录,其实就是各级域名服务器的地址,可以让你更清楚 DNS 解析的过程。 为了帮你更直观理解递归查询,我把这个过程整理成了一张流程图,你可以保存下来理解。
不仅仅是发布到互联网的服务需要域名,很多时候,我们也希望能对局域网内部的主机进行域名解析(即内网域名,大多数情况下为主机名)。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 服务器,用来解析外网的域名
tcpdump
$ tcpdump -nn udp port 53 or host 35.190.27.188
时间戳 协议 源地址.源端口 > 目的地址.目的端口 网络包详细信息
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 打开它。打开后,你就可以看到下面这个界面:
从 Wireshark 的界面里,你可以发现,它不仅以更规整的格式,展示了各个网络包的头部信息;
还用了不同颜色,展示 DNS 和 ICMP 这两种不同的协议。你也可以一眼看出,中间的两条 PTR 查询并没有响应包。接着,在网络包列表中选择某一个网络包后,在其下方的网络包详情中,你还可以看到,这个包在协议栈各层的详细信息。比如,以编号为 5 的 PTR 包为例:
你可以看到,IP 层(Internet Protocol)的源地址和目的地址、传输层的 UDP 协议(Uder Datagram Protocol)、应用层的 DNS 协议(Domain Name System)的概要信息。
继续点击每层左边的箭头,就可以看到该层协议头的所有信息。
比如点击 DNS 后,就可以看到 Transaction ID、Flags、Queries 等 DNS 协议各个字段的数值以及含义。
5 场景
协议栈统计信息
$ 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
# 数字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
这儿输出的指标比较多,我来简单解释下它们的含义。
rxpck/s 和 txpck/s 分别是接收和发送的 PPS,单位为包 / 秒。
rxkB/s 和 txkB/s 分别是接收和发送的吞吐量,单位是 KB/ 秒。
rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包 / 秒。
%ifutil 是网络接口的使用率,即半双工模式下为 (rxkB/s+txkB/s)/Bandwidth,而全双工模式下为 max(rxkB/s, txkB/s)/Bandwidth。
其中,Bandwidth 可以用 ethtool 来查询,它的单位通常是 Gb/s 或者 Mb/s,不过注意这里小写字母 b ,表示比特而不是字节。我们通常提到的千兆网卡、万兆网卡等,单位也都是比特。如下你可以看到,我的 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 的输出,可以分为两部分。第一部分,是每个 ICMP 请求的信息,包括 ICMP 序列号(icmp_seq)、TTL(生存时间,或者跳数)以及往返延时。第二部分,则是三次 ICMP 请求的汇总。比如上面的示例显示,发送了 3 个网络包,并且接收到 3 个响应,没有丢包发生,这说明测试主机到 114.114.114.114 是连通的;平均往返延时(RTT)是 244ms,也就是从发送 ICMP 开始,到接收到 114.114.114.114 回复的确认,总共经历 244ms
C10K,C10M
C10K 问题的根源,一方面在于系统有限的资源;另一方面,也是更重要的因素,是同步阻塞的 I/O 模型以及轮询的套接字接口,限制了网络事件的处理效率。Linux 2.6 中引入的 epoll ,完美解决了 C10K 的问题,现在的高性能网络方案都基于 epoll。
从 C10K 到 C100K ,可能只需要增加系统的物理资源就可以满足;
但从 C100K 到 C1000K ,就不仅仅是增加物理资源就能解决的问题了。这时,就需要多方面的优化工作了,从硬件的中断处理和网络功能卸载、到网络协议栈的文件描述符数量、连接状态跟踪、缓存队列等内核的优化,再到应用程序的工作模型优化,都是考虑的重点。
再进一步,要实现 C10M ,就不只是增加物理资源,或者优化内核和应用程序可以解决的问题了。这时候,就需要用 XDP 的方式,在内核协议栈之前处理网络包;或者用 DPDK 直接跳过网络协议栈,在用户空间通过轮询的方式直接处理网络包。
当然了,实际上,在大多数场景中,我们并不需要单机并发 1000 万的请求。通过调整系统架构,把这些请求分发到多台服务器中来处理,通常是更简单和更容易扩展的方案。思考
DNS时快时慢
DNS(Domain Name System),即域名系统,是互联网中最基础的一项服务,主要提供域名和 IP 地址之间映射关系的查询服务
DNS 不仅方便了人们访问不同的互联网服务,更为很多应用提供了,动态服务发现和全局负载均衡(Global Server Load Balance,GSLB)的机制。这样,DNS 就可以选择离用户最近的 IP 来提供服务。即使后端服务的 IP 地址发生变化,用户依然可以用相同域名来访问。
开启 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.
这次要加一个 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 服务器本身有问题,响应慢并且不稳定;或者是,客户端到 DNS 服务器的网络延迟比较大;再或者,DNS 请求或者响应包,在某些情况下被链路中的网络设备弄丢了
根据上面 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
不过,如果你多次运行 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 记录不过期,使用缓存中的记录就可以了
所以,想要为系统开启 DNS 缓存,就需要你做额外的配置。比如,最简单的方法,就是使用 dnsmasq。dnsmasq 是最常用的 DNS 缓存服务之一,还经常作为 DHCP 服务来使用。它的安装和配置都比较简单,性能也可以满足绝大多数应用程序对 DNS 缓存的需求。
# /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 解析需要的时间也都很稳定。
网络性能问题
ping 获取ip地址
# 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
再打开另一个终端(终端二),SSH 登录案例机器后,执行下面的命令
$ tcpdump -nn udp port 53 or host 35.190.27.188
当然,你可以直接用 tcpdump 不加任何参数来抓包,但那样的话,就可能抓取到很多不相干的包。由于我们已经执行过 ping 命令,知道了 geekbang.org 的 IP 地址是 35.190.27.188,也知道 ping 命令会执行 DNS 查询。所以,上面这条命令,就是基于这个规则进行过滤。
我来具体解释一下这条命令。
-nn ,表示不解析抓包中的域名(即不反向解析)、协议以及端口号。
udp port 53 ,表示只显示 UDP 协议的端口号(包括源端口和目的端口)为 53 的包。
host 35.190.27.188 ,表示只显示 IP 地址(包括源地址和目的地址)为 35.190.27.188 的包。
这两个过滤条件中间的“ or ”,表示或的关系,也就是说,只要满足上面两个条件中的任一个,就可以展示出来。
接下来,回到终端一,执行相同的 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 的选项以及接口的基本信息;从第三行开始,就是抓取到的网络包的输出。这些输出的格式,都是 时间戳 协议 源地址.源端口 > 目的地址.目的端口 网络包详细信息(这是最基本的格式,可以通过选项增加其他字段)。前面的字段,都比较好理解。但网络包的详细信息,本身根据协议的不同而不同。所以,要理解这些网络包的详细含义,就要对常用网络协议的基本格式以及交互原理,有基本的了解。
比如,第一条就表示,从本地 IP 发送到 114.114.114.114 的 A 记录查询请求,它的报文格式记录在 RFC1035 中,你可以点击这里查看。在这个 tcpdump 的输出中,36909+ 表示查询标识值,它也会出现在响应中,加号表示启用递归查询。A? 表示查询 A 记录。geektime.org. 表示待查询的域名。30 表示报文长度。
接下来的一条,则是从 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 工作缓慢的问题。
Wireshark ——HTTP 的例子,并理解 TCP 三次握手和四次挥的工作原理
进入终端一,执行下面的命令,首先查出 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:
$ curl http://example.com
最后,再回到终端一,按下 Ctrl+C 停止 tcpdump,并把得到的 web.pcap 拷贝出来。使用 Wireshark 打开 web.pcap 后,你就可以在 Wireshark 中,看到如下的界面
由于 HTTP 基于 TCP ,所以你最先看到的三个包,分别是 TCP 三次握手的包。接下来,中间的才是 HTTP 请求和响应包,而最后的三个包,则是 TCP 连接断开时的三次挥手包。从菜单栏中,点击 Statistics -> Flow Graph,然后,在弹出的界面中的 Flow type 选择 TCP Flows,你可以更清晰的看到,整个过程中 TCP 流的执行过程:
这其实跟各种教程上讲到的,TCP 三次握手和四次挥手很类似,作为对比, 你通常看到的 TCP 三次握手和四次挥手的流程,基本是这样的:
不过,对比这两张图,你会发现,这里抓到的包跟上面的四次挥手,并不完全一样,实际挥手过程只有三个包,而不是四个。其实,之所以有三个包,是因为服务器端收到客户端的 FIN 后,服务器端同时也要关闭连接,这样就可以把 ACK 和 FIN 合并到一起发送,节省了一个包,变成了“三次挥手”。而通常情况下,服务器端收到客户端的 FIN 后,很可能还没发送完数据,所以就会先回复客户端一个 ACK 包。稍等一会儿,完成所有数据包的发送后,才会发送 FIN 包。这也就是四次挥手了。抓包后, Wireshark 中就会显示下面这个界面(原始网络包来自 Wireshark TCP 4-times close 示例,你可以点击 这里 下载):
当然,Wireshark 的使用方法绝不只有这些,更多的使用方法,同样可以参考 官方文档 以及 WIKI。
缓解Doss攻击下的性能下降问题
DDoS 简介
DDoS 的前身是 DoS(Denail of Service),即拒绝服务攻击,指利用大量的合理请求,来占用过多的目标资源,从而使目标服务无法响应正常请求。DDoS(Distributed Denial of Service) 则是在 DoS 的基础上,采用了分布式架构,利用多台主机同时攻击目标主机。这样,即使目标服务部署了网络防御设备,面对大量网络请求时,还是无力应对。
从攻击的原理上来看,DDoS 可以分为下面几种类型。
第一种,耗尽带宽。无论是服务器还是路由器、交换机等网络设备,带宽都有固定的上限。带宽耗尽后,就会发生网络拥堵,从而无法传输其他正常的网络报文。
第二种,耗尽操作系统的资源。网络服务的正常运行,都需要一定的系统资源,像是 CPU、内存等物理资源,以及连接表等软件资源。一旦资源耗尽,系统就不能处理其他正常的网络连接。
第三种,消耗应用程序的运行资源。应用程序的运行,通常还需要跟其他的资源或系统交互。如果应用程序一直忙于处理无效请求,也会导致正常请求的处理变慢,甚至得不到响应。
比如,构造大量不同的域名来攻击 DNS 服务器,就会导致 DNS 服务器不停执行迭代查询,并更新缓存。这会极大地消耗 DNS 服务器的资源,使 DNS 的响应变慢
检查网络状况。你应该还记得我们多次用过的 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 的输出中,你可以看到,网络接收的 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 的过程:
实际上,SYN Flood 正是互联网中最经典的 DDoS 攻击方式。从上面这个图,
你也可以看到它的原理:即客户端构造大量的 SYN 包,请求建立 TCP 连接;
而服务器收到包后,会向源 IP 发送 SYN+ACK 报文,并等待三次握手的最后一次 ACK 报文,直到超时
这种等待状态的 TCP 连接,通常也称为半开连接。由于连接表的大小有限,大量的半开连接就会导致连接表迅速占满,从而无法建立新的 TCP 连接。参考下面这张 TCP 状态图,你能看到,此时,服务器端的 TCP 连接,会处于 SYN_RECEIVED 状态:
这其实提示了我们,查看 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 命令后,才会动态生效。
6 小结
疑问:
Q1:一台机器不是只有65536个端口吗,每个网络请求都需要消耗一个端口,这样大于65536个请求会不会导致端口不够用呢?
A1:如果是只有一个IP地址的客户端,那的确是受限于端口数量。
不过,如果是服务器端,那可以接受的连接就多得多了(组合客户端IP+端口)
Q2:
select/poll是LT模式,epoll缺省使用的也是水平触发模式(LT)。
目前业界对于ET的最佳实践大概就是Nginx了,单线程redis也是使用的LT
说下我对水平触发(LT)和边缘触发(ET)我的理解。
LT:文件描述符准备就绪时(FD关联的读缓冲区不为空,可读。写缓冲区还没满,可写),触发通知。
也就是你文中表述的"只要文件描述符可以非阻塞地执行 I/O ,就会触发通知..."
ET:当FD关联的缓冲区发生变化时(例如:读缓冲区由空变为非空,有新数据达到,可读。写缓冲区满变有空间了,有数据被发送走,可写),触发通知,仅此一次
也就是你文中表述的"只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时"
网络出现性能问题排查:
性能评估是优化网络性能的前提,只有在你发现网络性能瓶颈时,才需要进行网络性能优化。根据 TCP/IP 协议栈的原理,不同协议层关注的性能重点不完全一样,也就对应不同的性能测试方法。
比如,在应用层,你可以使用 wrk、Jmeter 等模拟用户的负载,测试应用程序的每秒请求数、处理延迟、错误数等;
而在传输层,则可以使用 iperf 等工具,测试 TCP 的吞吐情况;
再向下,你还可以用 Linux 内核自带的 pktgen ,测试服务器的 PPS。由于低层协议是高层协议的基础。
所以,一般情况下,我们需要从上到下,对每个协议层进行性能测试,然后根据性能测试的结果,结合 Linux 网络协议栈的原理,找出导致性能瓶颈的根源,进而优化网络性能。
DNS解析性能问题排查:
在应用程序的开发过程中,我们必须考虑到 DNS 解析可能带来的性能问题,掌握常见的优化方法。这里,我总结了几种常见的 DNS 优化方法。对 DNS 解析的结果进行缓存。缓存是最有效的方法,但要注意,一旦缓存过期,还是要去 DNS 服务器重新获取新记录。不过,这对大部分应用程序来说都是可接受的。对 DNS 解析的结果进行预取。这是浏览器等 Web 应用中最常用的方法,也就是说,不等用户点击页面上的超链接,浏览器就会在后台自动解析域名,并把结果缓存起来。使用 HTTPDNS 取代常规的 DNS 解析。这是很多移动应用会选择的方法,特别是如今域名劫持普遍存在,使用 HTTP 协议绕过链路中的 DNS 服务器,就可以避免域名劫持的问题。基于 DNS 的全局负载均衡(GSLB)。这不仅为服务提供了负载均衡和高可用的功能,还可以根据用户的位置,返回距离最近的 IP 地址。
碰到dns问题最多的就是劫持,现在公网都是强制https,内部用powerdns
网络性能排查
当你发现针对相同的网络服务,使用 IP 地址快而换成域名却慢很多时,就要想到,有可能是 DNS 在捣鬼。DNS 的解析,不仅包括从域名解析出 IP 地址的 A 记录请求,还包括性能工具帮你,“聪明”地从 IP 地址反查域名的 PTR 请求。实际上,根据 IP 地址反查域名、根据端口号反查协议名称,是很多网络工具默认的行为,而这往往会导致性能工具的工作缓慢。所以,通常,网络性能工具都会提供一个选项(比如 -n 或者 -nn),来禁止名称解析。在工作中,当你碰到网络性能问题时,不要忘记 tcpdump 和 Wireshark 这两个大杀器。你可以用它们抓取实际传输的网络包,再排查是否有潜在的性能问题。
网络延迟排查
网络延迟,是最核心的网络性能指标。由于网络传输、网络包处理等各种因素的影响,网络延迟不可避免。但过大的网络延迟,会直接影响用户的体验。所以,在发现网络延迟增大后,你可以用 traceroute、hping3、tcpdump、Wireshark、strace 等多种工具,来定位网络中的潜在问题。比如,使用 hping3 以及 wrk 等工具,确认单次请求和并发请求情况的网络延迟是否正常。使用 traceroute,确认路由是否正确,并查看路由中每一跳网关的延迟。使用 tcpdump 和 Wireshark,确认网络包的收发是否正常。使用 strace 等,观察应用程序对网络套接字的调用情况是否正常。这样,你就可以依次从路由、网络包的收发、再到应用程序等,逐层排查,直到定位问题根源。
NAT
NAT原理
NAT 技术能够重写 IP 数据包的源 IP 或目的 IP,所以普遍用来解决公网 IP 地址短缺的问题。它可以让网络中的多台主机,通过共享同一个公网 IP 地址,来访问外网资源。同时,由于 NAT 屏蔽了内网网络,也为局域网中机器起到安全隔离的作用
它的主要原理就是,网络中的多台主机,通过共享同一个公网 IP 地址,来访问外网资源。同时,由于 NAT 屏蔽了内网网络,自然也就为局域网中的机器提供了安全隔离。
你既可以在支持网络地址转换的路由器(称为 NAT 网关)中配置 NAT,也可以在 Linux 服务器中配置 NAT。如果采用第二种方式,Linux 服务器实际上充当的是“软”路由器的角色。
NAT 的主要目的,是实现地址转换。
根据实现方式的不同,NAT 可以分为三类:
静态 NAT,即内网 IP 与公网 IP 是一对一的永久映射关系;
动态 NAT,即内网 IP 从公网 IP 池中,动态选择一个进行映射;
网络地址端口转换 NAPT(Network Address and Port Translation),即把内网 IP 映射到公网 IP 的不同端口上,让多个内网 IP 可以共享同一个公网 IP 地址
NAPT 是目前最流行的 NAT 类型,我们在 Linux 中配置的 NAT 也是这种类型。而根据转换方式的不同,我们又可以把 NAPT 分为三类。
第一类是源地址转换 SNAT,即目的地址不变,只替换源 IP 或源端口。SNAT 主要用于,多个内网 IP 共享同一个公网 IP ,来访问外网资源的场景。
第二类是目的地址转换 DNAT,即源 IP 保持不变,只替换目的 IP 或者目的端口。DNAT 主要通过公网 IP 的不同端口号,来访问内网的多种服务,同时会隐藏后端服务器的真实 IP 地址。
第三类是双向地址转换,即同时使用 SNAT 和 DNAT。当接收到网络包时,执行 DNAT,把目的 IP 转换为内网 IP;而在发送网络包时,执行 SNAT,把源 IP 替换为外部 IP。
第三类是双向地址转换,即同时使用 SNAT 和 DNAT。当接收到网络包时,执行 DNAT,把目的 IP 转换为内网 IP;而在发送网络包时,执行 SNAT,把源 IP 替换为外部 IP。双向地址转换,其实就是外网 IP 与内网 IP 的一对一映射关系,所以常用在虚拟化环境中,为虚拟机分配浮动的公网 IP 地址。
传输层优化
1 请求数较多时,大量TIME_WAIT处理
1 增大处于 TIME_WAIT 状态的连接数量 net.ipv4.tcp_max_tw_buckets ,并增大连接跟踪表的大小 net.netfilter.nf_conntrack_max。
2 减小 net.ipv4.tcp_fin_timeout 和 net.netfilter.nf_conntrack_tcp_timeout_time_wait ,让系统尽快释放它们所占用的资源。
3 开启端口复用 net.ipv4.tcp_tw_reuse。这样,被 TIME_WAIT 状态占用的端口,还能用到新建的连接中。
4 增大本地端口的范围 net.ipv4.ip_local_port_range 。这样就可以支持更多连接,提高整体的并发能力。
5 增加最大文件描述符的数量。你可以使用 fs.nr_open 和 fs.file-max ,分别增大进程和系统的最大文件描述符数;或在应用程序的 systemd 配置文件中,配置 LimitNOFILE ,设置应用程序的最大文件描述符
2 为了缓解 SYN FLOOD 等,利用 TCP 协议特点进行攻击而引发的性能问题,你可以考虑优化与 SYN 状态相关的内核选项
1 增大 TCP 半连接的最大数量 net.ipv4.tcp_max_syn_backlog ,或者开启 TCP SYN Cookies net.ipv4.tcp_syncookies ,来绕开半连接数量限制的问题(注意,这两个选项不可同时使用)。
2 减少 SYN_RECV 状态的连接重传 SYN+ACK 包的次数 net.ipv4.tcp_synack_retries。
3 在长连接的场景中,通常使用 Keepalive 来检测 TCP 连接的状态,以便对端连接断开后,可以自动回收。但是,系统默认的 Keepalive 探测间隔和重试次数,一般都无法满足应用程序的性能要求。所以,这时候你需要优化与 Keepalive 相关的内核选项
1 缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time;
2 缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl;
3 减少 Keepalive 探测失败后,一直到通知应用程序前的重试次数 net.ipv4.tcp_keepalive_probes。
网络层优化
从路由和转发的角度出发,你可以调整下面的内核选项
在需要转发的服务器中,比如用作 NAT 网关的服务器或者使用 Docker 容器时,开启 IP 转发,即设置 net.ipv4.ip_forward = 1。
调整数据包的生存周期 TTL,比如设置 net.ipv4.ip_default_ttl = 64。注意,增大该值会降低系统性能。
开启数据包的反向地址校验,比如设置 net.ipv4.conf.eth0.rp_filter = 1。这样可以防止 IP 欺骗,并减少伪造 IP 带来的 DDoS 问题。
Q1 最大连接数是不是受限于 65535 个端口
对客户端来说,每次发起 TCP 连接请求时,都需要分配一个空闲的本地端口,去连接远端的服务器。由于这个本地端口是独占的,所以客户端最多只能发起 65535 个连接。
对服务器端来说,其通常监听在固定端口上(比如 80 端口),等待客户端的连接。根据五元组结构,我们知道,客户端的 IP 和端口都是可变的。如果不考虑 IP 地址分类以及资源限制,服务器端的理论最大连接数,可以达到 2 的 48 次方(IP 为 32 位,端口号为 16 位),远大于 65535。
所以,综合来看,客户端最大支持 65535 个连接,而服务器端可支持的连接数是海量的。当然,由于 Linux 协议栈本身的性能,以及各种物理和软件的资源限制等,这么大的连接数,还是远远达不到的(实际上,C10M 就已经很难了)