《Linux 性能优化实战》第六周--网络性能实战篇

个人感悟

本周主要从案例分析来学习Linux网络问题如何分析与解决,这也是这个专栏四个基础模块的最后一小部分。对于网络性能评估,一般情况需要从上到下对每个协议层进行性能测试,然后根据性能测试结果结合Linux网络协议栈原理,找出导致性能瓶颈的根源。

在优化网络的性能时,可以结合Linux系统的网络协议栈和网络收发流程,从应用程序、套接字、传输层、网络层再到链路层等,对每个层进行逐层优化。

  • 应用程序中,主要优化I/O模型、工作模型以及应用层的网络协议
  • 套接字层,主要优化套接字的缓冲区大小
  • 传输层,主要优化TCP和UDP协议
  • 网络层,主要优化路由、转发、分片以及ICMP协议
  • 链路层,主要优化网络包的收发、网络功能卸载以及网卡选项

对于DDoS攻击,由于其分布式、大流量、难追踪等特点,目前还无法完全御防,只能设法缓解DDoS带来的影响。在实际应用中通常让Linux服务器配合专业的流量清洗以及网络防火墙设备一起来缓解该问题。

新工具GET

网络流量分析:
确认网络包的收发是否正常
tcpdump Wireshark
确认单次请求和并发请求时的网络延迟是否正常
hping3,wrk
确认路由是否正确,并查看路由中每一跳网关的延迟
traceroute
观察应用程序对网络套接字的调用情况是否正常
strace
Linux动态追踪框架
SystemTap

接下来是本周读书笔记


Lesson 37 案例篇:DNS解析时快时慢应该怎么办?

域名与DNS解析

cat /etc/resolv.conf
nameserver 8.8.8.8

除了nslookup之外,另一个常用的DNS解析工具dig,还提供了trace功能,可以展示递归查询的整个过程。

dig +trace +ndnssec xiaozhazi.github.io #nodnssec表示禁止DNS安全扩展

实验

DNS解析失败

sudo docker run -it --rm -v $(mktemp):/etc/resolv.conf feisky/dnsutils bash 
nslookup xiaozhazi.github.io  
#connection timeout, no servers could be reached
ping -c3 8.8.8.8 # works normally

nslookup -debug xiaozhazi.github.io 

发现并没有连接DNS服务器而是连接环回地址,此时猜测可能容器内部没有配置DNS服务器。 在resolv.conf文件中添加即可。

DNS解析不稳定

sudo docker run -it --rm --cap-add=NET_ADMIN --dns 8.8.8.8 feisky/dnsutils bash 
time nslookup time.geekbang.org  # real time=10s
ping -c3 8.8.8.8  #latency=140ms
ping -c3 114.114.114.114 #latency=31ms, change dnsserver
#rerun nslookup now timecost=64ms
#此时重复执行仍会出现1s延时的情况,说明容器内没有使用DNS缓存

/etc/init.d/dnsmasq start 
然后修改resolv.conf文件,将DNS服务器改为dnsmasq的监听地址
此时再重复执行nslookup除第一次运行外,都只需10ms左右

DNS解析结果不稳定,可能存在以下几种情况:

  • DNS服务器本身有问题,响应慢且不稳定
  • 客户端到DNS服务器的网络延迟较大
  • DNS请求或响应包,在某些情况下被链路中的网络设备弄丢了

几种常见的DNS优化方法:

  • 对DNS解析的结果进行缓存
  • 对DNS解析的结果进行预取
  • 使用HTTPDNS取代常规的DNS解析,使用HTTP协议栈绕过链路中的DNS服务器,可以避免域名被劫持的问题
  • 基于DNS的全局负载均衡GSLB,根据用户的位置返回距离最近的IP地址

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

我们通常使用ping来测试服务延迟,不过有时候ping本身也会出现意想不到的问题,此时就需要我们抓取ping命令执行时收发的网络包,然后分析这些网络包,进而找出问题根源。

  • tcmdump仅支持命令行格式使用,常用在服务器中抓取和分析网络包
  • Wireshark还提供了图形界面和汇总分析工具,在分析复杂的网络场景是比较实用

实验分析

#禁止接收从DNS服务器中发送过来并包含googleusercontent的包
iptables -I INPUT -p udp --sport 53 -m string --string googleusercontent --algo bm -j DROP
ping -c3 geektime.org

#此时三次请求都得到了响应,每次延迟30ms左右,没有丢包。但是总时间却超过了11s
#是不是DNS解析慢的原因呢?发现ping的输出中三次都是用的IP地址,说明ping只需要在最开始运行时解析一次得到IP
#用nslookup验证了下不存在域名解析慢的问题

tcpdump -nn udp port 53 or host XXX(geektime.org ip)
#另一个终端执行ping指令 

逐条分析tcpdump输出,发现有两条反向地址解析PTR请求,只看到了请求包没有应答包。而且每条记录都执行了5s才出现下一个网络包。

因此这里的ping缓慢是因为两次PTR请求超时导致的, 在ping执行时禁掉PTR即可

ping -n -c3 geektime.org

tcpdump

tcpdump 基于libpcap,利用内核中的AF_PACKET套接字,抓取网络接口中传输的网络包,并提供了请打的过滤规则,从大量的网络包中挑出最想关注的信息。

-i   #tcpdump -i eth0  指定网络接口
-nn  #tcpdump -nn   不解析IP地址和端口号的名称 
-c   #tcpdump -c5   限制要抓取网络包的个数
-A   #tcpdump -A    以ASSCII格式显示网络包内容
-w   #tcpdump -w file.pcap  保存到文件中,通常以pcap作为后缀
-e   #tcpdump -e    输出链路层的头部信息

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

DDoS简介

DDoS, Distributed Denial of Service。 前身是DoS,即拒绝服务攻击,指利用大量的合理请求,来占用过多的目标资源,从而使得目标服务无法响应正常请求。

DDoS则是采用的分布式架构,利用多台主机同时攻击目标主机。这样,即使目标服务部署了网络防御设备,面对大量网络请求时还是无力应对。目前已知的最大流量攻击正是Github遭受的DDoS攻击,峰值流量达到了1.35Tbps,PPS更是超过了1.2亿。

从攻击原理来看,DDoS分为:

  • 耗尽带宽。无论是服务器还是路由器、交换机等网络设备,带宽都有固定的上限。带宽耗尽后就会发生网络拥堵,无法传输其他正常的网络报文。
  • 耗尽操作系统资源。例如CPU、内存等物理资源,以及连接表等软件资源。
  • 消耗应用程序的运行资源。应用程序的运行,通常要和其他的资源或系统交互,如果程序一直忙于处理无效请求,也会导致正常请求的处理变慢甚至无法响应

实验

通过hping3命令模拟DoS攻击:

hping3 -S -p 80 -i u10 XXX.XXX.XXX.XXX
#-S表示设置TCP协议的SYN

用Sar来观察,可以看到网络接收的PPS(每秒收发的报文数)已经达到了2w多,但是BPS(每秒收发的字节数)只有1174KB,即每个包只有54B。全是小包

sar -n DEV 1

继续通过tcpdump抓取eth0网卡的包

tcpdump -i eth0 -n tcp port 80

Flag[S]表示是SYN包,大量的SYN包表明,这是一个SYN Flood攻击。通过Wireshark可以更直观的输出SYN Flood的过程。

其原理是:

  • 客户端构造大量的SYN包,请求建立TCP连接
  • 服务器收到包后,向源IP发送SYN+ACK报文,并等待三次握手的最后一次ACK报文,直到超时

这种等待状态的TCP连接,通常称为半开连接。由于连接表的大小有限,大量的半开连接就会导致连接表迅速占满,从而无法建立新的TCP连接。

netstat -n -p | grep SYN_REC #定位半开连接的IP
iptables -I INPUT -s XXX.XXX.XXX.XXX -p tcp -j REJECT

如果遇到多台服务器同时发送SYN Flood攻击,这种方法可能就无效了。因为很可能无法SSH到机器上,因此提前要对系统做一些TCP,限制半开连接的数量/减少连接失败内核重启次数。

# /etc/sysctl.conf
sysctl -w net.ipv4.tcp_max_syn_backlog=1024
sysctl -w net.ipv4.tcp_synack_retries=1

还可以启用TCP SYN Cookies来防御SYN Flood攻击。

sysctl -w net.ipv4.tcp_syncookies=1

DDoS到底该如何防御

  • 可以用XDP或者DPDK构建DDoS方案,在内核网络协议栈前,或者跳过内核协议栈来识别并丢弃DDoS报文
  • 对于流量型的DDoS,当服务器的带宽被耗尽时,服务器内部处理就无能为力了。此时只能在服务器外部的网络设备中增加专业的入侵检测和防御设备,配置流量清洗设备阻断恶意流量等。
  • 对于慢速请求,响应流量很大时使得应用程序会耗费大量的资源处理,此时需要应用程序考虑识别,并尽早拒绝掉这些恶意流量。比如合理利用缓存,增加WAF(Web Application Firewall),使用CDN等。

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

网络延迟

网络延迟 网络数据传输所用的时间,这个时间可能单向也可以指双向的。 双向的往返通道延迟,RTT Round-Trip Time

应用程序延迟 从应用程序接收请求到发回响应全程所用的时间。

通常用ping来测试网络延迟,但是ping基于ICMP通过计算ICMP回显响应报文和回显请求报文的时间差,来获得延时。这个过程不需要特殊认证,通常会被很多网络攻击利用。为了避免被攻击,很多网络服务会把ICMP禁掉。此时可以借助traceroute和hping3工具。

hping3 -c 3 -S -p 80 baidu.com
# -c表示发送3次,-S设置TCPSYN,-p端口号
traceroute --tcp -p 80 -n baidu.com
# -n表示不对结果中的IP地址执行反向域名解析

实验分析

设计了对比实验,在80端口运行官方Nginx容器,在8080端口运行案例Nginx容器

sudo docker run --network=host --name=good -itd nginx
sudo docker run --network=host --name=nginx -itd feisky/nginx:latency

通过hping3命令分别测试其延迟时,发现差不多都是7ms。

再用wrk测试并发请求下的延迟,分别测试机器并发100时两个端口的性能

wrk --latency -c 100 -t 2 --timeout 2 http://IP:Port

此时官方Nginx的延迟在9ms左右,而案例应用则是44ms左右。 此时我们首先想到的是通过tcpdump抓取8080端口的网络包,并保存文件到nginx.pcap

tcpdump -nn tcp port 8080 -w nginx.pcap
wrk --latency -c 100 -t 2 --timeout 2 htto://IP:Port 

再把抓取到的nginx.pcap文件复制到装有Wireshark的机器中进行分析,此时只过滤除TCP Stream的。 通过输出界面可以看出三次握手和第一次请求和响应都挺快,但是第二次请求就比较慢,40ms之后才发送ACK响应。

而TCP延迟确认(Delayed ACK)的最小超时时间就是40ms。 延迟确认是针对TCP ACK机制的一种优化,不用每次请求都发送一个ACK,而是等一会看看有没有其他包需要发送,捎带着ACK一起发送过去。如果等不到就在超时后单独发送ACK。

man TCP,发现TCP可以设置TCP_QUICKACK开启快速确认模式,否则默认采用延迟确认机制。

为了验证猜想,用strace观察wrk为套接字设置了哪些TCP选项,证明确实没有TCP_QUICKACK

strace -f wrk --latency -c 100 -t 2 --timeout 2 http://IP:Port
'''
setsockopt(52,SOL_TCP,TCP_NODELAY,[1],4)=0
'''

但是这只是客户端的行为,按理说Nginx服务器不应该受此影响,再回过去分析网络包,重新观察Wireshark输出。发现第二个分组是等到客户端第一个分组的ACK后才发送的,有点类似延迟确认,不过此时不是ACK包,而是发送数据。

此时考虑Nagle算法,纳格算法,是TCP协议中用于减少小包发送数量的一种优化算法,目的是为了提高实际带宽利用率。算法规定一个TCP连接上,最多只能有一个未确认的未完成分组,在收到这个分组的ACK之前,不发送其他分组。这些小分组会被组合起来,并在收到ACK后,用同一个分组发送出去。

Nagle算法和Linux默认的延迟确认机制一起使用后,网络延迟会非常明显。 TCP可以设置TCP_NODELAY来禁用掉Nagle算法。

sudo docker exec nginx cat /etc/nginx/nginx.conf | grep tcp_nodelay
    tcp_nodelay off;

将其设置为on即可。

总结

遇到网络延迟增大问题时,可以通过以下工具来定位网络中的潜在问题:

  • hping3,wrk确认单次请求和并发请求时的网络延迟是否正常
  • traceroute 确认路由是否正确,并查看路由中每一跳网关的延迟
  • tcpdump和Wireshark 确认网络包的收发是否正常
  • strace观察应用程序对网络套接字的调用情况是否正常

Lesson 41/42 如何优化NAT性能?

NAT原理

NAT,Network Address Tranlation,可以重写IP数据包的源IP或者目的IP,被普遍用来解决公网IP地址短缺的问题。原理是,网络中的多台主机通过共享一个公网IP地址,来访问外网资源。

  • 静态NAT,内网IP和公网IP一对一永久映射关系
  • 动态NAT,内网IP从公网IP池中动态选择一个进行映射
  • 网络地址端口转换NAPT,Network Address and Port Translation,即把内网IP映射到公网IP的不同端口上,让多个内网IP可以共享同一个公网地址

NAPT是目前最流行的NAT类型,根据转换方式又分为三类:

  • 源地址转换SNAT,目的地址不变只替换源IP或者源端口
  • 目的地址转换DNAT,源IP保持不变只替换目的IP或目的端口
  • 双向地址转换,当接收网络包时执行DNAT,将目的地址转换为内网IP,发送网络包时执行SNAT,把源IP替换为外部IP

比如,本地服务器IP为192.168.0.2,NAT网关IP为100.100.100,目的服务器baidu.com地址为123.125.115.110

  • 服务器访问baidu.com时,NAT地址会把源地址从本地服务器IP替换为网关IP,然后才发送给baidu.com
  • baidu.com发回响应包时,NAT网关又把目的地址替换为本地服务器IP,然后发送给目的服务器

iptables与NAT

Linux内核提供的Netfiler框架,允许对网络数据包进行修改和过滤。 以及iptables、ip6tables、ebtables等工具。

NAT表中内置了三个链:

  • PREROUTING,路由判断前所执行的规则,比如,对接收到的数据包进行DNAT
  • POSTROUTING,路由判断后所执行的规则,比如,对发送或转发的数据包进行SNAT或MASQUERADE
  • OUTPUT,类似于PREROUTING,但只处理从本机发送出去的包

SNAT配置需要在NAT表中的POSTROUTING链中配置:

    1. 为一个子网统一配置SNAT,并由Linux选择默认的出口IP,即MASQUERAGE iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -j MASQUERADE
    1. 为具体的IP地址配置SNAT,并制定转换后的源地址
      iptables -t nat -A POSTROUTING -s 192.168.0.2 -j SNAT –to-source 100.100.100.100

DNAT配置需要在NAT表中的PREROUTING或OUTPUT链中配置,其中前者更常用
iptables -t nat -A PREROUTING -d 100.100.100.100 -j DNAT –to-destination 192.168.0.2

在使用iptables配置NAT规则后,Linux需要转发来自其他IP的网络包,要确保开启Linux的IP转发功能

sysctl -w net.ipv4.ip_forward=1

实验

主要使用了SystemTap工具,Linux的一种动态追踪框架,把用户提供的脚本转换为内核模块来执行,用来监测和跟踪内核的行为。

先运行一个不用NAT的Nginx服务,用ab测试其性能作为基准性能。然后运行使用DNAT的Nginx容器

sudo docker run --name nginx --priviledged -p 8080:8080 -itd feisky/nginx:nat
iptables -nL -t nat  #ensure DNAT rules are created

再用ab测试时发现连接超时错误,将超时时间延长后减少总测试次数发现延迟比基准值相差太多。

因为我们已经知道根源时NAT,因此不需要tcpdump再抓包分析来源。此时用SystemTap工具来测试,先写一个dropwatch.stp脚本

#! /usr/bin/env stop
global locations

probe begin {printf "Monitoring for dropped packets\n"}
probe end {printf "Stopping dropped packet monitor \n"}

probe kernel.trace("kfree_skb") { locations[$location] <<< 1 }

probe timer.sec(5)
{
  printf("\n")
  foreach ( l in locations-) {
    printf("%d packets dropped at %s\n",@count(locations[l]),sysname(l))
  }
  delete locations
}
---------
stap --all-modules dropwatch.stp

当probebegin输出后执行ab测试,观察stap命令输出,发现大量丢包发生在nf_hook_slow位置。再用perf report来查看nf_hook_slow的调用位置,主要来自于三个地方。分别是ipv4——conntrack_in,br_nf_pre_routing以及iptable_nat_ipv4_in。即nf_hook_slow主要在执行三个动作:

  • 接收网络包时,在连接跟踪表中查找连接,并为新连接分配跟踪对象
  • Linux网桥中转发包,因为实验中容器网络通过网桥实现
  • 接收网络包时,执行DNAT将8080端口的包转发给容器

此时要优化只有从内核着手,DNAT的基础时conntrack,因此主要针对其参数进行优化

sysctl -a | grep conntrack

net.netfilter.nf_conntrack_count
net.netfilter.nf_conntrack_max 
net.netfilter.nf_conntrack_buckets
net.netfilter.nf_conntrack_tcp_timout_syn_recv
net.netfilter.nf_conntrack_tcp_timeout_syn_sent
net.netfilter.nf_conntrack_tcp_timeout_time_wait

Lesson 44/44 套路篇:网络性能优化的几个思路

网络性能优化首先要获得网络基准测试报告,然后通过相关性能工具,定位出网络性能瓶颈,再进行优化。可以从应用程序、套接字、传输层、网络层以及链路层分别来看

应用程序

应用程序通过套接字接口进行网络操作,主要对网络I/O和进程自身的工作模型进行优化。
除了之前C10K的多路复用技术之外,应用层也有一些网络协议优化可以考虑:

  • 长连接代替短连接,显著降低TCP建立连接的成本
  • 使用内存方式来缓存不常变化的数据,降低网络I/O次数,同时加快应用程序的响应速度
  • 使用Protocol Buffer等序列化的方式,压缩网络I/O的数据了,可以提高应用程序的吞吐
  • 使用DNS缓存、预取、HTTPDNS等方式,减少DNS解析的延迟,也可以提升网络IO的整体速度

套接字

每个套接字都有一个读写缓冲区,为了提高网络吞吐量,通常需要调整这些缓冲区的大小

  • 读缓冲区,缓存了远端发来的数据。
  • 写缓冲区,缓存了要发出去的数据。

    net.core.optmem_max
    net.core.rmem_max net.core.wmem_max
    net.ipv4.tcp_rmem net.ipv4.tcp_wmem
    

传输层

TCP

  • 请求数大的场景下,大量处于TIME_WAIT状态的连接,会占用大量内存和端口资源。这种场景下可以优化与TIME_WAIRT相关的内核选项。
    • 增加处于TIME_WAIT状态的连接数量net.ipv4.tcp_max_tw_buckets,并增大连接跟踪表的大小net,netfilter.nf_conntrack_max;
    • 减少net.ipv4.tcp_fin_timeout,net.netfilter.nf_conntrack_tcp_timeout_time_wait,让系统尽快释放它们占用的资源
    • 开启端口复用net.ipv4.tcp_tw_reuse,这样被TIME_WAIT状态占用的端口还能用于新建的连接中
    • 增大本地端口的范围net.ipv4.ip_local_port_range,支持更多的连接,提高整体的并发能力
    • 增加最大文件描述符的数量
  • 缓解SYN FLOOD等攻击,可以优化与SYN状态相关的内核选项
    • 增大TCP半连接的最大数量,或者开启TCP SYN Cookies来绕开半开连接数量限制
    • 减少SYN_RECV的重传SYN+ACK次数
  • 在长连接场景中,通常使用Keepalive来检测TCP连接的状态,以便对端连接断开后可以自动回收。系统默认的Keepalive探测间隔和重试次数一般都无法满足应用程序的性能要求。考虑优化与Keepalive相关的内核选项。
    • 缩短最后一次数据包到Keepalive的探测包间隔时间
    • 缩短发送Keepalive探测包的间隔时间
    • 减少探测失败后一直到通知应用程序前的重试次数

UDP

  • 增大套接字缓冲区大小以及UDP缓冲区范围
  • 增大本地端口号的范围
  • 根据MTU大小,调整UDP数据包的大小,减少或避免分片的发生

###网络层
网络层主要对路由、IP分片以及ICMP等进行优化。

  • 从路由和转发的角度出发
    • 在需要转发的服务器中,开启IP转发。 net.ipv4.ip_forward=1
    • 调整数据包的生存周期TTL net.ipv4.ip_default_ttl
    • 开启数据包的反向地址校验,防止IP欺骗,减少伪造IP带来的DDoS问题, net.ipv4.conf.eth0.rp_filter=1
  • 从分片的角度出发,调整MTU的大小
  • 从ICMP的角度出发,为了避免ICMP主机探测,ICMP Flood等问题,限制ICMP的行为
    • 禁用ICMP
    • 禁止广播ICMP

###链路层

网卡收包后调用的中断处理程序,需要消耗大量的CPU,可以将这些中断处理程序调度到不同的CPU上执行,提高网络吞吐量。

  • 为网卡硬中断配置CPU亲和性,或者开启irqbalance服务
  • 开启RPS(Receive Packet Steering)和RFS(Receive Flow Steering),将应用程序和软中断的处理调度到相同的CPU上。增加CPU缓存命中率,减少网络延迟
  • 将原来在内核中通过软件处理的功能,卸载到网卡中通过硬件执行
    • TSO (TCP Segmentation Offload), UFO (UDP Fragmentation Offload)
    • GSO (Generic Segmentation Offload)
    • LRO (Large Receive Offload)
    • GRO (Generic Receive Offload)
    • RSS (Receive Side Scaling)
    • VXLAN卸载