Frances Hu's Blog

Born to be wild!


  • Startseite

  • Archiv

  • Tags

《Linux 性能优化实战》第七周--综合实战篇

Veröffentlicht am 2020-07-10

个人感悟

本周主要通过一些案例,对之前所学的知识进行复习和深化。首先容器化的应用程序性能分析,依旧可以使用之前的方法来分析和定位,不过要结合命名空间、cgroups、iptables等来综合分析。比如:

  • cgroups影响容器应用的运行
  • iptables中的NAT会影响容器的网络性能
  • 叠加文件系统,会影响应用的I/O性能

对于网络丢包问题分析,要从Linux网络收发的流程入手,结合TCP/IP协议栈的原理来逐层分析。当碰到内核线程的资源使用异常时,很多常用的进程级性能工具并不能直接用到内核线程上。此时可以使用内核自带的perf来观察它们的行为,并找出热点函数,进一步定位性能瓶颈。不过perf汇总报告并不直观,可以通过火焰图来协助排查。

perf对系统内核线程进行分析时,内核线程依然还在正常运行,这种方法也被称为动态追踪技术。动态追踪技术,通过探针机制,来采集内核或者应用程序的运行信息,从而可以不用修改内核和应用程序的代码,就获得丰富的信息,帮助分析、定位想要排查的问题。在Linux系统中,常用的动态追踪方法包括ftrace、perf、eBPF/BCC以及SystemTap等。

  • 使用perf配合火焰图寻找热点函数,是一个比较通用的性能定位方法,在很多场景中都可以使用
  • 如果仍然满足不了需求的话,在新版的内核中eBPF和BCC是最灵活的动态追踪方法
  • 而在旧版本内核,特别是在RHEL系统中,由于eBPF支持受限,SystemTap和ftrace往往是更好的选择

接下来是本周读书笔记


Lesson 46 案例篇:为什么应用容器化后,启动慢了很多?

本课主要学习如何分析应用程序容器化后的性能问题。

实验

sudo docker run --name tomcat --cpus 0.1 -m 512M -p 8080:8080 -itd feisky/tomcat:8
curl localhost:8080

容器内核心应用逻辑比较简单,申请一个256M的内存然后输出“HelloWorld”。等待容器启动后运行curl命令给出了结果“HelloWorld”,但是随后出现Empty reply from server一直connection refused。

查看tomcat log并没有发现问题并且容器状态为Exited,此时利用docker inspect查看容器信息,发现State信息中OOMKilled为true,说明容器是被OOM杀死。但是我们已经指定了-m为512M正常不会遇到OOM问题。

此时利用dmesg命令查看系统日志,定位OOM相关日志,可以看到输出显示:

  • mem_cgroup_out_of_memory,超出了cgroup内存限制
  • java进程在容器内运行,容器内存的使用和限制都是512M,当前使用量已经超过该限制
  • 被杀死的进程,虚拟内存为4.3G,匿名内存页为505M,页内存为19M。

分析可知,Tomcat容器的内存主要用在了匿名内存,其实就是主动申请分配的堆内存。Tomcat是基于JAVA开发,自然想到JVM堆内存配置问题。

重新启动容器,执行下列命令查看JVM堆配置
sudo docker exec tomcat java -XX:+PrintFlagsFinal -version | grep HeapSize
sudo docker exec tomcat free -m #容器内部看到的仍然是主机内存

看到初始堆内存大小InitialHeapSize为126MB,最大堆内存大小为1.95GB,比容器限制要大。因为容器看不到该配置,虽然在启动容器时设置了内存限制,但是并不影响JVM使用。

解决方法:在运行容器时加上 -e JAVA_OPT=“-Xmx512m -Xms512m”限制JVM的初始内存和最大内存即可

重新启动容器后,查看tomcat log发现能正常启动,但是启动时间需要22s。

再次重启容器并使用top来观察输出,发现机器中CPU使用率并不高且内存也非常充足,再看进程上Java进程的CPU使用率为10%,内存使用率0.9%。 其他进程使用率几乎可以忽略。

继续重启容器,top拿到JAVA进程PID之后,再用pidstat分析该进程,发现虽然CPU使用率很低,只有10%,但是wait%却非常高达到了87%,说明线程大部分时间都在等待调度,没有真正运行。

再看我们运行容器时限制了–cpus 0.1,限制了CPU使用。将该值增加大1再重启此时只需要2s即可完成。

小结

在容器平台中最常见的一个问题就是刚开始图省事不进行资源限制,当容器数量增长之后就会经常出现各种异常问题,最终查下来可能就是某个应用资源使用过高,导致整台机器短时间无法响应。因此使用Docker运行Java应用时一定要确保设置容器资源限制的同时,配置好JVM选项。 也可以升级JAVA版本到JAVA10,即可自动解决类似问题。

Lesson 47/48 案例篇:服务器总是时不时丢包怎么办?

丢包率是网络性能中最核心的指标之一

实验

本次实验案例是一个Nginx应用,hping3和curl是Nginx的客户端。

sudo docker run --name nginx --hostname nginx --privileged -p 80:80 -itd feisky/nginx:drop
hping3 -c 10 -S -p 80 XXX.XXX.XXX.XXX

因为Nginx使用的是TCP协议,而ping是基于ICMP协议的,因此我们用hping3来测试。此时输出显示10个请求包值收到5个回复,每个请求的RTT波动较大,小的只有3ms大的则有3s左右。可以猜测3s左右的RTT是丢包重传导致。

从图中可以看出可能发生丢包的位置,实际上贯穿了整个网络协议栈:

  • 在两台VM连接之间,可能会发生传输失败的错误。如网络阻塞、线路错误等
  • 网卡收包后,环形缓冲区因溢出而丢包
  • 链路层,会因为网络帧校验失败、QoS等丢包
  • IP层,会因为路由失败、组包大小超过MTU等而丢包
  • 传输层,因为端口未监听、资源占用超过内核限制丢包
  • 套接字层,因为套接字缓冲区溢出而丢包
  • 应用层,应用程序异常而丢包
  • 此外如果配置了iptables规则,可能因为过滤规则而丢包

因为VM2只是一个hping3命令,为了简化排查同时假设VM1的网络和内核配置也没问题。因此可能发生问题的地方就是容器内部了。进入容器内部逐层排查丢包原因。

链路层分析

首先查看链路层,通过ethtool/netstat查看网卡的丢包记录,从输出中没有发现任何错误,说明容器的虚拟网卡没有丢包。(注意:如果tc等工具配置了QoS,tc规则导致额丢包不会包含在网卡的统计信息中)接下来检查eth0是否配置了tc规则,并查看有无丢包。

sudo docker exec -ti nginx /bin/bash
netstat -f

tc -s qdisc show dev eth0
qdisc netem 800d: root refcnt 2 limit 1000 loss 30%
 Sent 432 bytes 8 pkt (dropped 4, overlimits 0 requeues 0)

此时tc规则中看到,eth0上面配置了一个网络模拟排队规则qdisc netem,并且配置了丢包率为30%。后面统计信息显示发送了8个包,但是丢了4个。

发现这点问题之后,直接删掉netem模块即可。

tc qdisc del dev eth0 root netem loss 30%

此时再次执行hping3命令,发现还是50%的丢包,RTT的波动也很大,从3ms到1s。

网络层和传输层

在容器内部继续执行netstat -s命令,可以看到各协议的收发汇总,以及错误信息。 输出表明只有TCP协议发生了丢包和重传。TCP协议有多次超时和失败重试,并且主要错误是半连接充值。即主要失败都是三次握手失败。

  • 11 failed connection attempts
  • 4 sgements retransmitted
  • 11 resets received for embryonic SYN_RECV sockets
  • 4 TCPSynRetrans
  • 7 TCPTimeouts

iptables

因为iptables和内核的连接跟踪机制也可能会导致丢包,因此也需要进行排查。要确定是不是说连接跟踪导致的问题,只需要对比当前的连接跟踪数和最大连接跟踪数即可。由于连接跟踪在内核中是全局的,因此需要在主机中查看。

sysctl net.netfilter.nf_conntrack_max

此时当前连接跟踪数远小于最大连接跟踪数,因此丢包不可能是连接跟踪导致。

接下来回到容器内部查看iptables的过滤规则,发现有两条DROP规则的统计数值不是0,分别是在INPUT和OUTPUT链中。这两条规则是一样的,指的是使用statistic模块进行随机30%的丢包。删除这两条规则即可。

iptables -t filter -nvL
    Chain INPUT
    pkts  bytes  target  prot  opt  in  out  source      destination
    6     240    DROP    all   --   *    *   0.0.0.0/0   0.0.0.0/0   statistic mode random probability 0.299999999981

    Chain FORWARD
   pkts  bytes  target  prot  opt  in  out  source      destination

    Chain OUTPUT
    pkts  bytes  target  prot  opt  in  out  source      destination
    6     264    DROP    all   --    *   *   0.0.0.0/0   0.0.0.0/0   statistic mode random probability 0.299999999981

iptables -t filter -D INPUT -m statistic --mode random --probability 0.30 -j DROP
iptables -t filter -D OUTPUT -m statistic --mode random --probability 0.30 -j DROP  

再用hping3验证此时80端口接发包正常。下面用curl命令检查Nginx对HTTP请求的响应:

curl --max-time 3 http://XXX.XXX.XXX.XXX
    curl:(28) Operation timed out after 3000 milliseconds with 0 bytes received

这时候可以tcpdump转包来分析:

tcpdump -i eth0 -nn port 80

从结果中可以看出,前三个包是正常的TCP三次握手,但是第四个包确实在3s之后,并且还是客户端VM2发送来的FIN包,说明客户端的连接关闭了。因为curl命令设置了3s超时选项,所以这种情况是因为curl命令超时后退出。

重新执行netstat -i命令查看网卡有没有丢包问题,输出显示RX-DRP是344,即网卡接收时丢包了。但是之前hping3不丢包,现在换成curl GET却丢包,我们需要对比下这两个工具。

  • hping3只发送SYN包
  • curl在发送SYN包之后,还会发送HTTP GET 请求

HTTP GET本质上也是一个TCP包,但是和SYN包相比,它还携带了HTTP GET的数据。这时候就容易想到时MTU配置错误导致。查看eth0的MTU设置只有199,将其改为以太网默认值1500即可。

ifconfig eth0 mtu 1500

小结

遇到网络丢包问题,要从Linux网络收发的流程入手,结合TCP/IP协议栈的原理来逐层分析。

Lesson 49 案例篇:内核线程CPU利用率太高怎么办?

CPU使用率较高的内核线程,如果用之前的分析方法,一般需要借助于其他性能工具进行辅助分析。本节提供了一种直接观察内核线程的行为,更快定位瓶颈的方法。

内核线程

Linux中用户态进程的”祖先“都是PID为1的init进程,即systemd进程。但是systemd只管理用户态进程,那么内核态线程是有谁来管理呢?

  • 0号进程为idle进程,系统创建的第一个进程,它在初始化1号和2号进程后,演变为空闲任务。
  • 1号进程为init进程,即systemd进程,在用户态运行,用来管理其他用户态进程
  • 2号进程为kthreadd进程,在内核态运行用来管理内核线程。

所以要查找内核线程,只需要从2号进程开始,查找它的子孙进程即可

ps -f --ppid 2 -p 2 
#可以看出内核线程的名称都在中括号内,因此更简单的方法是直接查找名称中包含中括号的进程

ps -ef | grep '\[.*\]'
  • ksoftirqd 软中断
  • kswapd0, 用于内存回收
  • kworker,用于执行内核工作队列,分为绑定CPU和未绑定CPU两大类
  • migration,用于负载均衡中,把进程迁移到CPU上,每个CPU都有一个migration内核线程
  • jbd2/sda1-8, Journaling Block Device,用来为文件系统提供日志功能,以保证数据的完整性
  • pdflush,用于将内存中的脏页写入磁盘

实验

运行一个nginx容器,通过curl命令验证nginx服务正常开启。用hping3命令模拟Nginx客户端请求,此时回到第一个终端,发现系统响应变慢。

用top观察发现2个CPU上软中断使用率都超过了30%,正好是软中断内核线程ksoftirqd/0和ksoftirqd/1。对于内核线程我们用stace、pstack、lsof无法查看详细的调用栈情况,此时可以用内核提供的工具来分析。

perf record -a -g -p $pid --sleep 30
perf report

后续利用火焰图来协助排查分析定位热点函数,找出潜在的性能问题。

Lesson 50/51 案例篇:动态追踪怎么用?

动态追踪技术,通过探针机制,来采集内核或者应用程序的运行信息,从而可以不用修改内核和应用程序的代码,就获得丰富的信息,帮你分析、定位想要排查的问题。

Dtrace的工作原理:它的运行常驻在内核中,用户可以用dtrace命令,把D语言编写的追踪脚本,提交到内核中的运行时来执行。Dtrace可以跟踪用户态和内核态的所有事件,并通过一些列的优化措施,保证最小的性能开销。

Dtrace本身依然无法在Linux中运行,很多工程师都尝试过把Dtrace移植到Linux中,其中最著名的就是RedHat主推的SystemTap。

SystemTap也定义了一种类似的脚本语言,方便用户根据需要自由扩展。不过SystemTap没有常驻内核运行时,需要先把脚本编译为内核模块,然后再插入到内核中执行。因此systemTap启动比较慢,并且依赖于完整的调试符号表。

总的来说,为了追踪内核或用户空间的事件,Dtrace和SystemTap都会把用户传入的追踪处理函数,关联到被称为探针的检测点上。这些探针实际上也就是各种动态追踪技术所依赖的事件源。

根据事件类型不同,动态追踪所使用的事件源,可以分为静态探针、动态探针以及硬件事件三类。

  • 硬件事件通常由性能监控计数器PMC产生,包含了各种硬件的性能情况,比如CPU的缓存、指令周期、分支预测等;
  • 静态探针,是指实现在代码中定义好,并编译到应用程序或者内核中的探针。这些探针只有在开启探测功能时才会被执行到。
    • 跟踪点 tracepoints,实际上就是在源码中插入的一些带有控制条件的探测点,这些探测点允许时候再添加处理函数。如内核中的printk
    • USDT探针,全称时用户级静态定义跟踪,需要在源码中插入DTRACE_PROBE()代码,并编译到应用程序中。MYSQL/PostgreSQL也内置了USDT探针
  • 动态探针,指没有事先在代码中定义,但却可以在运行时动态添加的探针。常见的动态探针都两种:

    • kprobes用来跟踪内核态的函数,包括用于函数调用的kprobe和用于函数返回的kretprobe
    • uprobes用来跟踪用户态的函数,包括用于函数调用的uprobe和用于函数返回的uretprobe

      kprobes需要内核编译时开启CONFIG_KPROBE_EVENTS,uprobes需要内核编译中开启CONFIG_UPROBE_EVENTS

动态追踪机制

在探针基础上,Linux也提供了一系列的动态追踪机制,比如ftrace、perf、eBPF等

  • ftrace最早用于函数跟踪,后来又扩展支持了各种事件跟踪功能。
  • perf是一种最简单的静态跟踪机制,也可以通过perf来定义动态事件,只关注真正感兴趣的事件
  • eBPF是在BPF(Berkeley Packet Filter)的基础上扩展来的,不仅支持事件跟踪机制,还可以通过自定义的BPF代码

除此之外,还有很多内核外的工具,也提供了丰富的动态追踪功能,最常见的就是SystemTap和BCC,以及常用于容器性能分析的sysdig

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

Veröffentlicht am 2020-07-04

个人感悟

本周主要从案例分析来学习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卸载

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

Veröffentlicht am 2020-06-27

个人感悟

本周的学习首先了解了Linux网络的工作原理,OSI七层网络模型,TCP/IP模型以及网络包的收发流程。应用程序通过Socket接口发送数据包时先要在网络协议栈从上到下逐层处理最终到网卡上发送,接收也要经过网络协议栈从下到上逐层解析,最后送到应用程序。以及网络传输相关性能指标和响应的查看工具。

并且学习了经典的C10K问题,以及延伸的C1000K和C10M问题。这个印象中研究生毕业面试B家的时候被问到过。C10K问题的根源一方面在于系统有限的资源,另一方面,同步阻塞I/O模型以及轮询的套接字接口限制了网络事件的处理效率。目前高性能网络方法都基于epoll。从10K到100K,增加物理资源就能解决,但是到1000K时就需要多方面的优化工作,从硬件中断处理和网络功能卸载、到网络协议栈的文件描述符数量、连接状态跟踪、缓存队列等内核的优化,再到应用程序的工作模型优化,都需要考虑。

再进一步实现10M,就需要用XDP的方式,在内核协议栈之前处理网络包;或者用DPDK直接跳过网络协议栈在用户空间通过轮询的方式直接处理网络包。其中DPDK时目前最主流的高性能网络解决方案,但是需要能支持DPDK的网卡配合使用。

新工具GET

查看网络配置
ifconfig/ip
套接字信息/协议栈统计信息
netstat/ss
网络吞吐量和PPS
sar
带宽
ethtool
连通性和延时
ping
应用层性能
wrk/jmeter
传输层性能
iperf
转发性能
pktgen

捞评论GET

1、客户端的网络环境复杂,出现网络抖动如何分析解决?

在第一个网络出入口记录每次收发消息的内容和具体时间戳(精确到ms),遇到玩家反馈时根据id及发生的大致时间在日志中查找响应记录,看是服务器响应慢还是客户端到服务器的线路慢。可以考虑更多的接入点、专线、CDN等优化公网的链路延迟问题。


接下来是本周读书笔记


Lesson 33/34 关于网络,你必须知道这些

网络模型

开放式系统互联通信参考模型(Open System Interconnection Reference Model),简称OSI网络模型

为了解决网络互联中异构设备的兼容性,并解耦复杂的网络包处理流程,OSI模型把网络互联的框架分为应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层等七层。

  • 应用层,负责为应用程序提供统一的接口
  • 表示层,负责把数据转换成兼容接收系统的格式
  • 会话层,负责维护计算机之间的通信连接
  • 传输层,负责为数据加上传输表头,形成数据包
  • 网络层,负责数据的路由和转发
  • 数据链路层,负责MAC寻址,错误侦测和改错
  • 物理层,负责在物理网络中传输数据帧

在Linux中我们实际上使用的是一个更实用的四层模型,即TCP/IP网络模型。

TCP/IP模型把网络互联的框架分为应用层、传输层、网络层、网络接口层等四层。

  • 应用层,负责向用户提供一组应用程序,如HTTP,FTP,DNS等
  • 传输层,负责端到端的通信,如TCP,UDP
  • 网络层,负责网络包的封装、寻址和路由,如IP,ICMP
  • 网络接口层,负责网络包在物理网络中的传输,比如MAC寻址、错误侦测以及通过网卡传输网络帧等

Linux网络栈

  • 传输层在应用数据前面增加了TCP头
  • 网络层在TCP数据包前增加了IP头
  • 网络接口层在IP数据包前后分别增加了帧头和帧尾

网络接口配置的最大传输单元MTU规定了最大的IP包大小,以太网中MTU默认时1500

Linux网络收发流程

网络包的接收流程

  • 当一个网络帧到达网卡后,网卡通过DMA方式,把网络包放到收包队列中;然后通过硬中断告诉中断处理程序已经接收到了网络包。
  • 网卡中断处理程序为网络帧分配内核数据结构sk_buff,并将其拷贝到sk_buff缓冲区中;再通过软中断通知内核接收到了新的网络帧。
  • 内核协议栈从缓冲区中取出网络帧,并通过网络协议,从上到下处理这个网络帧
    • 链路层检查报文合法性,找出上层协议的类型(IPv4 or IPv6),再去掉帧头和帧尾,交给网络层;
    • 网络层取出IP头,判断网络包的下一步走向,比如是交给上层处理还是转发。当网络层确认这个包发送本机后,取出上层协议的类型(TCP or UDP),去掉IP头再交给传输层处理。
    • 传输层取出TCP/UDP头之后根据<源IP,源端口,目的IP,目的端口>四元组作为标识,找出对应的Socket,并把数据拷贝到Socket接收缓存中
  • 应用程序使用Socket接口读取到新收到的数据

网络包的发送流程

  • 应用程序通过调用Socket API发送网络包
  • 由于这是系统调用,会陷入内核态的套接字层中。套接字层把数据包放到Socket发送缓冲区中
  • 网络协议栈从Socket发送缓冲区中取出数据包,再按照TCP/IP栈,从上到下逐层处理
  • 分片后的网络包再发送到网络接口层,进行物理地址寻址,找到下一跳的MAC地址,然后添加帧头和帧尾,放到发包队列中。这一切完成后会有软中断通知驱动程序
  • 驱动程序通过DMA从发包队列中读出网络帧,并通过物理网卡发送出去

性能指标

  • 带宽, 表示链路的最大传输速率,单位b/s
  • 吞吐量,表示单位时间内成功传输的数据量,单位b/s或者B/s 吞吐量/带宽=网络使用率
  • 延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。
    • 建立连接需要的时间, TCP握手延时
    • 一个数据包往返所需的时间,RTT
  • PPS,Packet Per Second,表示以网络包为单位的传输速率。通常用来评估网络的转发能力

此外,网络的可用性、并发连接数、丢包率、重传率也是常用的性能指标。

网络配置

ifconfig eth0
ip -s addr show dev eth0
  • 网络接口的状态标志, ifconfig输出中的RUNNING,ip输出中的LOWER_UP,都表示物理网络是联通的
  • MTU大小
  • 网络接口的IP地址、子网、以及MAC地址
  • 网络收发的字节数、包数、错误数以及丢包情况,特别是TX和RX部分的errors、dropped、overruns、carrier以及collisions等指标不为0时,通常表示出现了网络I/O问题
    • errors表示发生错误的数据包数,比如校验错误、帧同步错误等
    • dropped表示丢弃的数据包数,即数据包已经收到了Ring Buffer,但是因为内存不足等原因丢包
    • overruns表示超限数据包数,即网络I/O速度过快,导致RingBuffer中的数据包来不及处理导致的丢包
    • carrier表示发生carrier错误的数据包数,比如双工模式不匹配、物理电缆出问题
    • collisions表示碰撞数据包数

套接字信息

netstat -nlp | head -n 3
ss -ltnp | head -n 3

netstat和ss用来查看套接字、网络栈、网络接口以及路由表的信息。其中Recv-Q和Send-Q信息需要特别关注,如果不是0的话说明有网络包的堆积发生。

当Socket处于Established时,Recv-Q表示套接字缓冲中还没有被应用取走的字节数,Send-Q表示还没有被远端主机确认的字节数。
当Socket处于Listening时,Recv-Q表示全连接队列的长度,Send-Q表示全连接队列的最大长度。

协议栈统计信息

netstat -s 
ss -s

吞吐量和PPS

sar -n DEV 1 

带宽可以用ethtool来查询

ethtool eth0 | grep Speed

连通性和延时

ping -c3 XXX.XXX.XXX.XXX

Lesson 35 基础篇:C10K和C1000K回顾

C10K问题最早由Dan Kegel在1999年提出,那是服务器还是32位系统,运行Linux2.2版本,只配置的很少的内存(2G)和千兆网卡。怎样在这样的系统中支持并发1万的请求?

从资源上说,2G内存和千兆网卡服务器,同时处理1w请求,只要每个请求处理占用不超200KB内存和100Kbit的网络带宽就可以。所以物理资源充足,接下来时软件的问题。

如果每个请求分配一个进程/线程,1w个请求会涉及1w个进程/线程的调度、上下文切换乃至它们占用的内存都会成为瓶颈。

  • 怎样在一个线程内处理多个请求?非阻塞I/O或者异步I/O?
  • 怎么更节省资源地处理用户请求?用最少的线程来服务这些请求?

I/O模型优化

I/O事件的通知方式:

  • 水平触发:只要文件描述符可以非阻塞的执行I/O,就会触发通知
  • 边缘出发:只有在文件描述符发生改变时(I/O请求到达时),才发送一次通知。

I/O多路复用的方法:

  • 使用非阻塞I/O和水平触发通知,比如使用select和poll
    • select和 poll从文件描述符列表中,找出哪些可以执行IO,然后进行真正的网络I/O读写。由于I/O是非阻塞的,一个线程中就可以同时监控一批套接字的文件描述符,达到了单线程处理多请求的目的。
    • 优点:对程序友好,API简单。
    • 缺点:需要对文件描述符列表轮询,请求多时较为耗时,且select和poll还有一些限制。以及应用程序每次调用select和poll时还需要把文件描述符的集合从用户空间传入内核空间,由内核修改后再传回用户空间。增加了处理成本。
  • 使用非阻塞I/O和边缘触发通知,如epoll
    • epoll使用红黑树在内核中管理文件描述符的集合,使用事件驱动的机制,只关注有I/O事件发生的文件描述符,不需要轮询整个集合
    • epoll在Linux2.6之后提供,由于边缘触发只在文件描述符可读或可写事件发生时才通知,应用程序需要尽可能多地执行I/O并要处理更多的异常事件
  • 使用异步I/O
    • 异步I/O也是在Linux2.6后提供,和直观逻辑不太一样,使用时要小心设计,难度较高

工作模型优化

  • 主进程+多个worker子进程
    • 主进程执行bind()+listen()后创建多个子进程
    • 每个子进程中都通过accept()和epoll_wait()来处理相同的套接字
    • Nginx就是采取这种模式,主进程用来初始化套接字并管理子进程的生命周期,worker进程用来负责实际的请求处理
    • accept和epoll_wait调用存在一个惊群问题,当网络I/O事件发生时多个进程被同时唤醒,但实际上只有一个进程来响应事件,其他被唤醒的进程都会重新休眠。
      • accept惊群问题在Linux2.6中解决了
      • epoll_wait到Linux4.5才通过EPOLLEXCLUSIVE解决
      • nginx通过在worker进程中增加一个全局锁来解决,worker进程首先要竞争到锁,然后才加入到epoll中,确保只有一个worker子进程被唤醒
  • 监听相同端口的多进程模型
    • 所有进程都监听相同的接口,并且开启SO_REUSEPORT选项,由内核将请求负载均衡到这些监听进程中
    • 不会存在惊群问题,Nginx1.9.1中支持该模式,SO_REUSEPORT选项在Linux3.9以上版本才有

C1000K

基于I/O多路复用和请求处理的优化,C10K问题很容易解决,那么C1000K呢?

100万个请求需要大量的系统资源

  • 假设一个请求16KB,需要15GB内存
  • 带宽上来看,假设只有20%的活跃连接,即使每个连接只需要1KB/s的吞吐量,总共也需要1.6Gb/s的吞吐量。因此还需要配置万兆网卡,或者基于多网卡bonding承载更大的吞吐量。

C1000K的解决方法,本质上还是构建在epoll的非阻塞I/O模型上,只不过除了I/O模型外还需要从应用程序到Linux内核,再到CPU、内存和网络各个层次的深度优化,特别是需要借助硬件来卸载哪些通过软件处理的大量功能。

C10M

同时处理1000w条请求呢?在C1000K时各种软件硬件的优化可能已经做到极致了,此时无论怎么优化应用程序和内核中各种网络参数,想实现1000万请求的并发都是及其困难的。

究其根本,还是Linux内核协议栈做了太多太多繁重的工作,从网卡中断带来的硬中断处理程序开始到软中断中的各层网络协议处理,最后再到应用程序,这个路径太长导致网络包的处理优化到一定程度后就无法再进一步。

要解决这个问题,就要跳过内核协议栈的冗长路径,把网络包直接发送到要处理的应用程序那里。

  • DPDK,用户态网络的标准,跳过内核协议栈直接由用户进程通过轮询方式处理网络接收。 还通过大页、CPU绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
  • XDP,Linux内核提供的一种高性能网络数据路径。它允许网络包在进入内核协议栈之前就进行处理,也可以带来更高的性能。XDP底层也是基于Linux内核的eBPF机制实现的。

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

各协议层的性能测试

转发性能

网络接口和网络层,主要负责网络包的封装、寻址、路由以及发送和接收。在这两个网络协议中,每秒可处理的网络包数PPS就是最重要的性能指标。特别是64B小包的处理能力,值得我们特别关注。如何来测试网络包的处理能力呢?

Linux内核自带的高性能网络测试工具pktgen,但是并不能直接找到pktgen命令,需要加载pktgen内核模块后,再通过/proc文件系统来交互

modprobe pktgen
ps -ef | grep pktgen | grep -v grep
ls /proc/net/pktgen/

TCP/UDP性能

iperf
netperf

HTTP性能

在应用层,有的应用程序会直接基于TCP或UDP构建服务,也有大量的应用基于应用层的协议来构建服务。HTTP就是一个最常用的应用层协议,要测试HTTP性能可以通过ab、webbench等。

ab
webbench

应用负载性能

wrk
TCPCopy
Jmeter

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

上节课学习了C10M的解决方案,不过在大多数场景下,我们并不需要单机并发1000万请求。通过调整系统架构,把请求分发到多台服务器中并行处理,才是更简单、扩展性更好的方案。

这就需要我们评估系统的网络性能,以便考察系统的处理能力,并为容量规划提供基准数据。

性能指标回顾

带宽、吞吐量、延时、PPS,这四个性能指标中带宽跟物理网卡配置直接关联;Linux服务器的网络吞吐量一般会比带宽小,交换机等专门的网络设备吞吐量一般接近带宽;PPS以网络包为单位的网络传输速率,通常用在需要大量转发的场景中;对于TCP或者Web服务来说通常会用并发连接数和每秒请求数QPS等指标。

网络基准测试

在测试之前需要弄清楚需要测试的应用程序基于协议栈的哪一层?

  • 基于HTTP和HTTPS的Web应用程序,属于应用层,需要测试HTTP/HTTPS的性能;
  • 大多数游戏服务器,为了支持更大的在线人数。通常会基于TCP/UDP与客户端交互,需要测试TCP/UDP性能
  • 还有一些场景将Linux作为一个软交换机或者路由器来使用,此时要更关注网络包的处理能力。即PPS,关注网络层的转发能力。

各协议层性能测试

转发性能

网络接口层和网络层,主要负责网络包的封装、寻址、路由以及发送和接收。这里最重要的性能指标就是PPS每秒可处理的网络包数。

可以用hping3或者pktgen来测试网络包处理能力。其中pktgen作为一个Linux内核自带的高性能网络测试工具,需要加载pktgen内核模块后再通过/proc文件系统交互。

modprobe pktgen
ls /proc/net/pktgen

在测试时,需要先给每个内核线程kpktgend_X以及测试网卡,配置pktgen选项。再通过pgctrl启动测试。

假设发包及其使用网卡eth0,目标机器的IP为192.168.0.30, MAC地址为11:11:11:11:11:11

#define function for test options
function pgset() {
    local result
    echo $1 > $PGDEV
    result=`cat $PGDEV | fgrep "Result: OK:"`

    if [ "$result" = "" ]; then 
        cat $PGDEV | fgrep Result:
    fi
} 

#bind eth0 for thread 0
PGDEV=/proc/net/pktgen/eht0
pgset "count 1000000" #total packages
pgset "delay 5000"
pgset "clone_skb 0"
pgset "pkt_size 64"
pgset "dst 192.168.0.30"
pgset "dst_mac 11.11.11.11.11.11"

#Start test
PGDEV=/proc/net/pktgen/pgctrl
pgset "start"

#Check result
cat /proc/net/pktgen/eth0

TCP/UDP性能

iperf
netperf

HTTP性能

ab -c 1000 -n 10000 http://www.baidu.com
webbench

应用负载性能

wrk TCPCopy Jmeter LoadRunner
工作中用过Jmeter进行测试

《Linux 性能优化实战》第四周--IO性能篇

Veröffentlicht am 2020-06-21

个人感悟

本周主要学习Linux I/O相关的基础知识以及遇到I/O异常问题如何分析解决。Linux一切皆文件。为了支持不同的文件系统,首先Linux在用户进程和文件系统之间实现了一层虚拟文件系统。用户进程和内核中的其他子系统只需要跟VFS提供的统一接口进行交互。其次,为了降低慢速磁盘对性能的影响,文件系统又通过页缓存、目录项缓存以及索引节点缓存来减少对应用程序性能的影响。

文件系统层、通用块层和块设备层组成了Linux存储系统I/O栈。其中通用块层是磁盘I/O的核心,向上为文件系统和应用程序提供访问块设备的标准接口,向下把各种异构磁盘抽象为统一的块设备,并对文件系统和应用程序发来的I/O请求进行重新排序、请求合并等。

通过实验学习了遇到IO瓶颈进一步导致CPU使用率高的问题如何分析和解决。一般通过iostat确认是否存在I/O性能瓶颈,再用strace和lsof定位应用程序以及它正在写入的日志文件路径。最后通过调整日志打印级别来解决。如果strace无法跟踪到write系统调用时,可以用filetop和opensnoop来定位具体的线程和读写文件目录;也可以加-p选项开启线程跟踪。

MYSQL的MyISAM引擎主要依赖系统缓存加速磁盘IO的访问。如果系统中还有其他应用同时运行,MyISAM引擎很难充分利用系统缓存。缓存可能会被其他应用程序占用,甚至被清理掉。因此最好不要将应用程序的性能优化完全建立在系统缓存上,最好能在应用程序内部分配内存,构建完全自主的缓存;或者利用第三方缓存应用,如Memcached,redis等。

对于磁盘IO瓶颈可以通过在内存充足时将数据放在更快的内存中来进行优化。也可以进一步利用Trie树等各种算法来进一步优化处理效率。

新工具GET

查看目录项和各种文件系统索引节点的缓存情况:

cat /proc/slabinfo | slabtop

磁盘IO观察

iostat -d -x 1      # -d -x 表示显示所有磁盘I/O的指标

进程IO观察

pidstat -d 1

iotop #可以按照I/O大小对进程排序找到I/O较大的进程

当strace无法跟踪到文件IO痕迹时

filetop 查看文件名以及使用情况
opensnoop 查看具体的文件目录

TCP网络连接可以用过nsenter工具来查看详细信息

思考

find / -name XXX 会不会导致系统的缓存升高?
会,导致inode_cache/dentry/proc_inode_cache/xfs_inode缓存升高

可以通过实验进行验证,先清除系统缓存,然后执行命令观察缓存使用情况。

iostat/pidstat已经证明了IO瓶颈是由哪个进程导致,为什么strace跟踪没有发现痕迹?

写文件是由子线程来进行处理的,默认strace是不开启线程跟踪的。在strace命令加上-fp选项既可以跟踪进程也可以跟踪线程。


接下来是本周读书笔记


Lesson 23 Linux文件系统是怎么工作的

  • 磁盘为文件系统提供了最基本的持久化存储
  • 文件系统在磁盘的基础上,提供了一个用来管理文件的树状结构

索引节点和目录项

Linux中一切皆文件。为了方便管理,Linux文件系统为每个文件都分配两个数据结构:

  • 索引节点: index node,记录文件的元数据(如inode编号,文件大小,访问权限,修改日期,数据位置等)。索引节点会持久化存储到磁盘中,同样占用磁盘空间。
  • 目录项: directory entry,记录文件的名字,索引节点指针以及与其他目录项的关联关系。目录项是由内核维护的一个内存数据结构,也叫目录项缓存。

索引节点是每个文件唯一标志,目录项维护文件系统的树状结构。目录项和索引节点关系是多对一。

磁盘在执行文件系统格式化时,会被分成三个存储区域:

  • 超级块,存储整个文件系统的状态
  • 索引节点区,用来存储索引节点
  • 数据块区,用来存储文件数据

虚拟文件系统

目录项、虚拟节点、逻辑块以及超级块构成了Linux文件系统的四大基本要素,为了支持各种不同的文件系统,内核在用户进程和文件系统之间引入了一个虚拟文件系统VFS抽象层。

VFS定义了一组所有文件系统都支持的数据结构和标准接口。这样用户进程和内核中的其他子系统只需要跟VFS提供的统一接口进行交互即可。

文件系统I/O

根据是否利用标准库缓存,可以分为:

  • 缓冲I/O,利用标准库缓存来加速文件的访问,标准库内部再通过系统调度访问文件
  • 非缓冲I/O,直接通过系统调用来访问文件,不再经过标准库缓存

根据是否利用系统的页缓存,分为:

  • 直接I/O,跳过操作系统的页缓存,直接跟文件系统交互来访问文件 (O_DIRECT)
  • 非直接I/O,文件读写时,先要经过系统的页缓存然后再由内核或额外的系统调用,真正写入磁盘

根据应用程序是否阻塞自身运行,分为:

  • 阻塞I/O,应用程序执行I/O操作后如果没有获得响应,就会阻塞当前线程
  • 非阻塞I/O,应用程序执行I/O操作后,不会阻塞当前的线程,可以继续执行其他的任务。然后再通过轮询或者事件通知的形式获取调用结果 (O_NONBLOCK)

根据是否响应结果,分为:

  • 所谓同步I/O,应用程序执行I/O操作后,要一直等到整个I/O完成后才能获得I/O响应 (O_SYNC/O_DSYNC)
  • 所谓异步I/O,应用程序执行I/O操作后,不用等待完成和完成后的响应,而是继续执行就可以。等这次I/O完成后,响应会用事件通知的方式告诉应用程序 (O_ASYNC)

性能观测

cat /proc/slabinfo | grep -E '^#|dentry|inode'

slabtop

Lesson 24/25 Linux磁盘I/O时怎么工作的?

###磁盘
磁盘是可以持久化的设备,根据存储介质不同,分为:

  • 机械磁盘(Hard Disk Driver),主要由盘片和读写磁头组成,数据存储在盘片的环状磁道中。读写数据时移动磁头,定位到数据所在的磁道中,然后才能访问。最小读写单位是扇区,一般512byte
  • 固态磁盘(Solid State Disk),由固态电子元器件组成,不需要磁道寻址。无论连续I/O还是随机I/O都比前者要好。最小读写单位是页,一般4KB,8KB等

两种磁盘随机I/O都要比连续I/O慢得多:

  • 机械磁盘随机I/O需要更多的磁头寻道和盘片旋转
  • 固态磁盘同样存在“先擦除再写入的限制”,随机读写会导致大量的垃圾回收
  • 连续I/O可以通过预读的方式来减少I/O请求的次数

###通用块层

通用块层,是处在文件系统和磁盘驱动中间的一个块设备抽象层。主要有以下功能:

  • 与VFS类似,向上为文件系统和应用程序提供块设备的标准接口;向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。
  • 给文件系统和应用程序发来的I/O请求排队,并通过重新排序、请求合并等来提升磁盘读写的能力。

I/O调度算法:

  • NONE,不使用任何I/O调度,常用在虚拟机中
  • NOOP,先入先出队列,只进行最基本的请求合并,常用与SSD磁盘
  • CFQ(Completely Fair Schedule),完全公平调度器,为每个进程维护一个I/O调度队列,并按照时间片来均匀分布每个进程的I/O请求。 类似进程CPU调度,CFQ还支持进程I/O的优先级调度,适用于大量进程的系统
  • Deadline,分别为读写请求创建不同的I/O队列,提高机械磁盘的吞吐量,并确保达到最终期限的请求被优先处理。多用于IO压力较重的场景,如数据库等

IO栈

Linux存储系统的I/O栈由上到下分为 文件系统层、通用块层、设备层。存储系统的IO通常是整个系统最慢的一环,所以Linux系统通过多种缓存机制来优化I/O效率。

  • 优化文件访问性能: 页缓存、索引节点缓存、目录项缓存等减少对下层设备的直接调用
  • 优化块设备访问性能:使用缓冲区来缓存块设备的数据

磁盘性能指标

  • 使用率, 磁盘处理I/O的时间百分比
  • 饱和度, 磁盘处理I/O的繁忙程度
  • IOPS, 每秒的I/O请求数
  • 吞吐量, 每秒I/O请求大小
  • 响应时间, I/O请求从发出到收到响应的间隔时间

磁盘I/O观测

iostat -d -x 1      # -d -x 表示显示所有磁盘I/O的指标

进程I/O观测

pidstat -d 1

iotop #可以按照I/O大小对进程排序找到I/O较大的进程

Lesson 26 案例篇:如何找出狂打日志的内鬼

实验

首先运行目标应用

sudo docker run -v /tmp:/tmp --name=app -itd feisky/logapp
ps -ef | grep /app.py #确保程序启动

我们先用top来观察CPU和内存的使用情况,然后再用iostat来观察磁盘使用情况

top 
#观察发现CPU0使用率高且iowait超过了90%,说明cpu0上正在运行IO密集型程序
#进程方面pythonCPU使用率较高,记录其pid号
#内存使用方面,总内存8G剩余700+M,Buffer/Cache占用较高

基本可以判断出CPU使用率中的iowait是一个潜在瓶颈,而内存中的缓存占比较大。
再用iostat查看I/O使用情况

iostat -x -d 1 
#发现sda的I/O使用率很高,很可能已经接近饱和
#查看前面指标,每秒写磁盘请求数是64,写大小是32MB,写请求响应时间7s,而请求队列长度则达到了1000+
#超慢的响应时间和请求队列过长,进一步验证了IO已经饱和

接下来分析I/O性能瓶颈的根源

pidstat -d 1 
#此时python进程的写比较大,且每秒数据超过了45M,说明python进程导致了IO瓶颈

strace -p XXXX 
#在write()系统调用上,可以看出进程向文件描述符编号为3的文件中写入了300M数据
#再观察后面的stat调用,可以看到它正在获取/tmp/logtest.txt.1的状态,这种格式的文件在日志回滚中常见

losf -p XXXX
#查看进程打开了哪些文件,/tmp/logtest.txt

综上说明进程以每次300MB的速度在疯狂的写日志,其中日志文件目录为/tmp/logtest.txt。此时查看案例源码发现其默认记录INFO级别以上的所有日志。此时将默认级别调高到WARNING级别,日志问题即可解决。


Lesson 27 案例篇:为什么我的磁盘IO延迟很高?

实验

本实验需要两台虚拟机,一台案例分析的目标机器运行Flask应用,另一台作为客户端请求单词的热度。

sudo docker run --name=app -p 10000:80 -itd feisky/word-app

然后在第二天机器 curl */popularity/word 发现一直没响应

回第一台机器来分析,首先执行df命令查看文件系统使用情况,发现也要等好久才输出。此时df显示系统还有足够多的磁盘空间。此时同样可以先用top来观察CPU和内存使用情况,再用iostat来观察磁盘的IO情况。

为了避免curl请求结束,在终端2循环执行curl,并用time观察每次执行时间。

while true; do 
    time curl */popularity/word
    sleep 1
done

top输出发现两个CPU的iowait都非常高,进程部分python进程的CPU使用率稍高,可能和iowait相关。 记录其pid

ps -aux | grep app.py #正好CPU使用率高的进程是我们的案例应用

iostat -x -d 1 #发现磁盘sda的I/O使用率已经达到98%,写响应时间18s,每秒32MB显然已经达到了IO瓶颈

pidstat -d 1 #再次看到了案例应用pid导致的io瓶颈

strace -p XXX

类似上节的套路,此时strace中可以看到大量的stat系统调用,却没有任何write调用。文件写明明应该有响应的write系统调用,现有工具却找不到痕迹。此时就该考虑换工具了,filetop基于eBPF机制,主要跟踪内核中文件的读写情况,并输出线程ID、读写大小、读写类型以及文件名称。

filetop -C #发现每隔一段时间线程号为XXX的python应用会写入大量的txt文件,再大量读。
ps -efT | grep XXX #该线程确实属于我们的应用进程

filetop只给出文件名,并没有给出文件路径。此时opensnoop工具登场

opensnoop #可以看到这些txt文件位于/tmp目录下,文件从0.txt到1000.txt

结合filetop和opensnoop我们可以猜测案例应用应该是写入1000个txt文件后,又将这些文件内容读取到内存中进行处理。在打断ls检查路径中文件时发现内容为空。此时再次运行opensnoop发现目录变化了,说明这些目录都是应用程序动态生成的,用后就删除了。

接下来查看程序源码发现该案例应用,在每个请求的处理过程中都会生成一批临时文件,然后读入内存处理,最后再删除整个目录。这是一种常见的利用磁盘空间处理大量数据的技巧,不过本次案例中的IO请求太重导致磁盘I/O利用率过高。

通过算法优化,在内存充足时将所有数据放到内存中处理,这样就能避免IO性能问题。


Lesson 28 案例篇:一个SQL查询要15s是怎么回事?

实验

案例由3个容器组成,一个mysql数据库应用,一个商品搜索应用,一个数据处理的应用。在执行搜索命令时遇到了返回数据为空且处理时间超过15s的问题。同样通过循环持续发送请求来进行问题问题,为了避免系统压力过大sleep 5s再开始新请求。

同样的套路,top iostat pidstat定位IO瓶颈问题以及mysqld进程。慢查询现象大多是CPU使用率高,但这里看到的却是IO问题,说明这并不是单纯的慢查询问题。

接下来通过strace发现线程XXX正在读取大量数据,且读取文件的描述符编号为38。再用lsof尝试查找对应的文件,此时发现lsof没有任何输出。

echo $? #查找上一条指令退出时返回值,结果为1说明lsof命令执行失败。

因为-p需要指定进程号,而我们传入线程号所以执行失败。
切换回进程号重新执行lsof命令,从输出可以看出确实mysqld进程打开了大量的文件,根据文件描述符找到对应的文件路径为/var/lib/mysql/test/products.MYD文件。

MYSQL中MYD文件时MyISAM引擎用来存储表数据的文件,文件名就是数据表的名字,父目录即为数据库的名字。 即改文件告诉我们mysqld正在读取test数据库中的products表。

如何确定这些文件是不是mysqld正在使用的数据库文件呢?有没有可能是不再使用的旧数据?我们通过查看mysqld配置的数据路径即可。

sudo docker exec -ti mysql mysql -e 'show global variable like "%datadir%";'

可以看到/var/lib/mysql确实是mysqld正在使用的数据存储目录。
即然找出了数据库和表,下一步就是弄清楚数据库中正在执行什么样的SQL。

在SQL命令界面执行

show full processlist #可以看到select * from products where productName=‘geektime’这条执行时间比较长

一般SQL慢查询问题,很可能是没有利用好索引导致,如何判断这条是不是这样?

explain  select * from products where productName-‘geektime’

其中pissible_keys和key都为NULL,type为ALL全表查询,这条查询语句根本没有使用索引,所以查询时会扫描整个表。

因此给productName建立索引即可, 优化后查询时间从15s缩短到了3ms。

该案例中测试时启动了一个DataService应用,其实停止该应用查询时间也能缩短到0.1s。这种情况下是否还存在IO瓶颈呢?通过vmstat来查看IO变化,发现磁盘读和iowait刚开始挺大,但是没过多久就变成了0,说明IO瓶颈消失。为什么呢?

通过查看DataService源码可以看到其读取文件前先将 /proc/sys/vm/drop_caches改为1。即释放文件缓存,而mysql读取的数据就是文件缓存,dataService不断释放缓存导致mysql直接访问磁盘。因此产生IO瓶颈。


Lesson 29 案例篇:Redis响应验证延迟,如何解决?

实验

本实验由python应用和redis两部分组成。Python应用是一个基于Flask的应用,会利用Redis来管理应用程序的缓存。

实验中在访问应用程序的缓存接口时,发现10s的长响应时间,接下来定位瓶颈。

同样为了避免分析过程中请求结束,通过loop循环来执行curl命令。

继续先通过top和iostat先分析是否存在IO瓶颈。 结果发现CPU的iowait比较高,但是磁盘每秒写数据为2.5M,IO使用率为0,没有IO瓶颈。

但是案例中测试时从Redis缓存中读取数据,对应应该时磁盘的读操作,iostat结果却显示时写操作。所以我们就要知道是什么进程在具体写磁盘。

运行pidstat -d查看发现是redis-server进程在写磁盘。接下来用strace+lsof查看到底在写什么。从系统调用看epoll_wait、read、write、fdatasync这些系统调用都比较频繁,刚才观察的写操作应该是write和fdatasync导致。lsof找出这些系统调用的操作对象,发现只有7号普通文件会产生磁盘写,其操作路径为/data/appendonly.aof。

在Redis中这对应着持久化配置中的appendonly和appendfsync选项,可能是由于它们配置不合理导致磁盘写较多。为了验证这个猜测,通过redis命令行查这两个选项的配置。

sudo docker exec -ti redis redis-cli config get 'append*'

发现appendfsync配置为always,appendonly配置为yes。

Redis提供了两种数据持久化方式:

  • 快照方式,按照指定的时间间隔生成数据的快照,并且保存在磁盘文件中。为避免阻塞主进程,Redis会fork一个子进程来进行快照的保存。 无论备份恢复都比追加文件性能好,缺点是在数据量大时fork子进程会用到比较大的内存,保存数据比较耗时。
  • 追加文件,在文件末尾追加记录的方式对redis写入数据进行持久化。提供appendfsync选项设置fsync策略:
    • always, 每个操作都会执行一次fsync,最安全
    • everysec,每秒钟调用一次fsync,即使最坏情况也只会丢失1s数据
    • no, 交给操作系统来处理

回头看上述测试,因为配置为always导致每次写数据都会调用一次fsync,从而造成比较大的磁盘IO压力。

但是为什么查询会有磁盘写呢,我们再次审视strace和lsof的输出,发现编号为8的TCP socket正好对应TCP读写,是一个标准的“请求-相应”格式。从socket中GET uuid:X后响应good,再从socket中读取SADD good X后响应1。对Redis来说SADD是一个写操作,所以Redis会将其持久化到appendonly.aof文件中。因此产生大量的磁盘读写。

接下来我们确认8号TCPsocket对应的Redis客户端是否为我们的案例应用。 通过lsof -i 找出TCP socket对应的TCP连接信息,进入容器网络命名空间内部看到完成的TCP连接。

PID=$(sudo docker inspect --format {{.State.Pid}} app)
nsenter --target $PID --net -- lsof -i

综合分析可知,首先redis配置always不太合理,本案例不需要这么高频的同步写,改为1s时间间隔足够;其次python应用在查询接口中会调用Redis的SADD命令,这很可能是不合理使用缓存导致。

修改配置后请求时间降低到0.9s,接着通过分析源码解决第二个问题。代码中Python应用将Redis当成临时空间,用来存储查询过程中找到的数据。优化将其放在内存中,再次查看响应时间已经降低到了0.2s。


Lesson 30 套路篇:如何迅速分析出系统IO瓶颈

性能指标

文件系统IO性能指标

  • 存储空间的使用情况,容量、使用量以及剩余空间等
    • 文件系统向外展示的空间使用,而非磁盘空间的真实用量
    • 索引节点的使用情况,包括容量、使用量以及剩余量 (如果文件系统中存储过多的小文件,就能碰到索引节点容量已满的问题)
  • 缓存使用情况,页缓存、目录项缓存、索引节点缓存以及各个具体文件系统的缓存
  • 文件系统IO, IOPS、响应延迟时间、以及吞吐量

Linux文件系统并没有提供直接查看这些指标的方法,只能通过系统调用、动态跟踪或者基准测试的方法来间接观察评估。

磁盘IO性能指标

  • 使用率
  • IOPS
  • 吞吐量
  • 响应时间
  • Buffer也常出现在内存和磁盘问题的分析中

性能工具

  • df,既可以查看文件系统数据的空间容量,也可以查看索引节点的容量
  • /proc/meminfo,/proc/slabinfo及slaptop,观察页缓存、目录项缓存、索引节点缓存以及具体的文件系统的缓存
  • iostat,pidstat观察磁盘和进程的IO情况
    • iostat查看磁盘的IO使用率、吞吐量、响应时间以及IOPS性能指标
    • pidstat查看进程的IO吞吐量以及块设备的IO延迟
  • strace+lsof定位问题进程正在读写的文件
  • filetop+opensnoop,从内核中跟踪系统调用,最终找出瓶颈来源

性能指标和工具的联系

根据指标找工具

根据工具查指标

如何迅速分析I/O的性能瓶颈

  • 先用iostat发现磁盘IO性能瓶颈
  • 再借助pidstat定位出导致瓶颈的进程
  • 随后分析进程的IO行为
  • 最后结合应用程序的原理,分析这些IO的来源

为了缩小排查范围,通常先运行几个支持指标较多的工具,如iostat、vmstat、pidstat等,然后再根据观察到的现象,结合系统和应用程序的原理,寻找下一步的分析方向。

例如MYSQL和Redis案例中,通过iostat确认磁盘出现IO性能瓶颈,然后用pidstat找出I/O最大的进程,接着借助strace找出该进程正在读写的文件,最后结合应用程序的原理找出大量IO的原因。

当用iostat发现磁盘IO性能瓶颈后,再用pidstat和vmstat检查,可能会发现IO来自内核线程。如Swap使用大量升高。这种情况下,就得进行内存分析,先找出占用大量内存的进程,再设法减少内存的使用。


Lesson 31 套路篇: 磁盘I/O性能优化的几个思路

IO基准测试

为了更客观的评估优化效果,首先应该对磁盘和文件系统进行基准测试,得到文件系统或磁盘IO的极限性能。
fio(Flexible I/O Tester)是最常用的基准测试工具

# 随机读
fio -name=randread -direct=1 -iodepth=64 -rw=randread -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
# 随机写
fio -name=randwrite -direct=1 -iodepth=64 -rw=randwrite -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
#顺序读
fio -name=read -direct=1 -iodepth=64 -rw=read -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
#顺序写
fio -name=write -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
  • direct表示是否跳过系统缓存,1表示跳过
  • iodepth表示使用异步I/O(asynchronous I/O)时同时发出的IO请求上限
  • rw表示I/O模式
  • ioengine表示I/O引擎,支持同步sync,异步libaio,内存映射mmap,网络net等
  • bs表示I/O的大小

结果报告中

  • slat表示从I/O提交到实际执行I/O的时长。 submission latency
  • clat表示从I/O提交到I/O完成的时长。 completion latency
  • lat表示从fio创建IO到IO完成的总时长

fio支持I/O的重放,先用blktrace记录磁盘设备的I/O访问情况,然后使用fio重放blktrace的记录。

blktrace /dev/sdb #跟踪磁盘IO
ls #查看blktrace记录的结果
blkparse sdb -d sdb.bin #将结果转化为二进制文件
fio --name=reply --filename=/dev/sdb --direct=1 --read_iolog=sdb.bin #使用fio重放日志

I/O性能优化

应用程序优化

  • 可以用追加写代替随机写,减少寻址开销,加快I/O写的速度
  • 可以借助缓存I/O,充分利用系统缓存,降低实际I/O的次数
  • 在应用程序内部构建自己的缓存,或者用Redis等外部缓存。一方面能在应用程序内部控制缓存的数据和生命周期,另一方面可以降低其他应用程序使用缓存对自身的影响
  • 需要频繁读写同一块磁盘空间时,可以用mmap代替read/write,减少内存的拷贝次数
  • 在需要写同步的场景中,尽量将写请求合并,即可以用fsync()取代O_SYNC
  • 在多个应用程序共享磁盘时,为了保证I/O不被某个应用完全占用,推荐使用cgroups的I/O子系统来限制进程/进程组的IOPS以及吞吐量
  • 在使用CFQ调度器时,可以用ionice来调整进程的调度优先级,提高核心应用的I/O优先级。

文件系统优化

  • 根据实际负载场景不同选择最适合的文件系统
  • 选好文件系统后进一步优化文件系统的配置选项,包括文件系统的特性、日志模式、挂载选项等
  • 优化文件系统的缓存
  • 不需要持久化时可以用内存文件系统tmpfs来获取更好的IO性能。

磁盘优化

  • 换用性能更好的磁盘, 如SSD替换HDD
  • 使用RAID将多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列。既可以提高数据的可靠性,又可以提升数据的访问性能
  • 针对磁盘和应用程序IO模式特征,选择最合适的IO调度算法
    • SSD和虚拟机中的磁盘,用noop调度算法
    • 数据库应用,用deadline算法
  • 对应用程序的数据进行磁盘级别的隔离。 为日志、数据库等I/O压力大的应用配置单独的磁盘
  • 顺序读多的场景增大磁盘的预读数据
    • 调整内核选项/sys/block/sdb/queue/read_ahead_kb, 默认为128KB
    • blockdev工具设置, blockdev –setra 8192 /dev/sdb, 这里单位为512B
  • 优化块设备的I/O选项
    • 调整磁盘队列的长度,/sys/block/sdb/queue/nr_requests

最后要注意磁盘本身是否存在硬件错误。 可以查看dmesg中是否有硬件I/O故障的日志。还可以用badblocks、smartctl等工具检测磁盘的硬件问题,或者用e2fsck来检测文件系统的错误。 如果发现问题可以用fsck来修复。


Lesson 32 答疑 (略)

捞评论学习:

  1. 数据写ES,运行一段时间后发现写入很慢,查IO时发现读IO很高写IO很少。用iotop定位es一些写的线程,将线程id转成16进制,用jstack打印出ES的堆栈信息,查处16进程的线程号的堆栈。发现原来时ES会根据doc id查数据,然后选择更新或新插入。ES数据量大时,会占用很多的读IO。 解决方法:写ES时不传入id,让es自动生成来解决。

《Linux 性能优化实战》第三周--内存性能篇

Veröffentlicht am 2020-06-14

个人感悟

本周主要学习内存性能方面的检测与优化。首先在概念上更加系统的了解了Linux内存的工作原理。进程看到的内存是内核提供的虚拟内存,通过页表映射到实际的物理内存。进程通过malloc申请内存根据页面大小有两种不同的方式,并且内存并不是立即分配而是在首次访问时通过缺页异常在内核态进行分配并更新页表。

通过阅读文档以及实验了解了Buffer和Cache的区别。前者是对磁盘数据的缓存,后者是对文件数据的缓存,且两者均作用于读写操作。并掌握cachestat/cachetop/pcstat等工具如何检测系统缓存命中指标,在实验中掌握如何处理缓存异常的场景。

在内存资源紧张时,Linux通过直接回收和定期扫描的方式来释放文件页和匿名页。其中资源是否紧张可以通过内存的三个阈值来判断。另外我们可以手动调整内存资源配置,例如修改 /proc/sys/vm/min_free_kbytes来调整内存阈值,/proc/sys/vm/swappiness来调整文件页和匿名页回收倾向。在NUMA架构下还可以设置/proc/sys/vm/zone_reclaim_node来调整本地内存的回收策略。

当Swap变高时,可以用sar,/proc/zoneinfo,/proc/pid/status等方法查看系统or进程的内存使用情况,进而找到Swap升高的根源和受影响的进程。不过通常我们禁止Swap的使用来提升系统的整体性能:

  • 内存足够大时,禁用Swap
  • 实在需要Swap时,可以尝试降低swapiness的值,减少回收时Swap的使用倾向
  • 响应延迟敏感的应用,可以用mlock/mlockall来锁定内存,禁止内存换出

之前在搭建组内K8S环境时按照教程都是先关闭Swap,不明所以。现在通过这周的学习才真正了解到缘由。

本周对内存使用情况监测所用的主要工具有:

  • 常用性能工具: free/top/ps,vmstat观察内存变化情况
  • 查看缓存命中情况: bcc包中的cachestat和cachetop,基于Linux内核的eBPF(extend Berkeley Packet Filters)来跟踪内核中管理的缓存
    • cachestat 查看整个操作系统缓存的读写命中情况
    • cachetop 提供了每个进程的缓存命中情况
  • 跟踪内存分配/释放: memleak
  • 查看内存各个指标变化: sar

对于系统内存问题的分析与定位,通常先运行几个覆盖面比较大的性能工具,如free,top,vmstat,pidstat等

  • 先用free和top查看系统整体内存使用情况
  • 再用vmstat和pidstat,查看一段时间的趋势,从而判断内存问题的类型
  • 最后进行详细分析,比如内存分配分析,缓存/缓冲区分析,具体进程的内存使用分析等

以及一些常见的优化思路:

  • 最好禁止Swap,若必须开启则尽量降低swappiness的值
  • 减少内存的动态分配,如可以用内存池,HugePage等
  • 尽量使用缓存和缓冲区来访问数据。如用堆栈明确声明内存空间来存储需要缓存的数据,或者用Redis外部缓存组件来优化数据的访问
  • cgroups等方式来限制进程的内存使用情况,确保系统内存不被异常进程耗尽
  • /proc/pid/oom_adj调整核心应用的oom_score,保证即使内存紧张核心应用也不会被OOM杀死

另外,在探索问题的过程中由于性能指标较多,我们不可能记住所有指标的详细含义,网上搜索有时并不能得到真正准确的答案,因此养成查文档的爱好非常重要。


接下来是本周读书笔记


Lesson 15 Linux内存是怎么工作的

内存映射

大多数计算机用的主存都是动态随机访问内存(DRAM),只有内核才可以直接访问物理内存。Linux内核给每个进程提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样进程就可以很方便的访问内存(虚拟内存)。

虚拟地址空间的内部分为内核空间和用户空间两部分,不同字长的处理器地址空间的范围不同。32位系统内核空间占用1G,用户空间占3G。 64位系统内核空间和用户空间都是128T,分别占内存空间的最高和最低处,中间部分为未定义。

并不是所有的虚拟内存都会分配物理内存,只有实际使用的才会。分配后的物理内存通过内存映射管理。为了完成内存映射,内核为每个进程都维护了一个页表,记录虚拟地址和物理地址的映射关系。页表实际存储在CPU的内存管理单元MMU中,处理器可以直接通过硬件找出要访问的内存。

当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存,更新进程页表,再返回用户空间恢复进程的运行。

MMU以页为单位管理内存,页大小4KB。为了解决页表项过多问题Linux提供了多级页表和HugePage的机制。

虚拟内存空间分布

从图中可以看出用户空间内存从低到高是五种不同的内存段:

  • 只读段 代码和常量等
  • 数据段 全局变量等
  • 堆 动态分配的内存,从低地址开始向上增长
  • 文件映射 动态库、共享内存等,从高地址开始向下增长
  • 栈 包括局部变量和函数调用的上下文等,栈的大小是固定的。一般8MB

内存分配与回收

分配

malloc对应到系统调用上有两种实现方式:

  • brk() 针对小块内存(<128K),通过移动堆顶位置来分配。内存释放后不立即归还内存,而是被缓存起来。
  • mmap()针对大块内存(>128K),直接用内存映射来分配,即在文件映射段找一块空闲内存分配。

前者的缓存可以减少缺页异常的发生,提高内存访问效率。但是由于内存没有归还系统,在内存工作繁忙时,频繁的内存分配/释放会造成内存碎片。

后者在释放时直接归还系统,所以每次mmap都会发生缺页异常。在内存工作繁忙时,频繁内存分配会导致大量缺页异常,使内核管理负担增加。

上述两种调用并没有真正分配内存,这些内存只有在首次访问时,才通过缺页异常进入内核中,由内核来分配。

回收

内存紧张时,系统通过以下方式来回收内存:

  • 回收缓存: LRU算法回收最近最少使用的内存页面;
  • 回收不常访问内存: 把不常用的内存通过交换分区写入磁盘
  • 杀死进程: OOM内核保护机制 (进程消耗内存越大oom_score越大,占用CPU越多oom_score越小,可以通过/proc手动调整oom_adj)

    echo -16 > /proc/$(pidof XXX)/oom_adj
    

如何查看内存使用情况

free来查看整个系统的内存使用情况

top/ps来查看某个进程的内存使用情况

  • VIRT 进程的虚拟内存大小
  • RES 常驻内存的大小,即进程实际使用的物理内存大小,不包括swap和共享内存
  • SHR 共享内存大小,与其他进程共享的内存,加载的动态链接库以及程序代码段
  • %MEM 进程使用物理内存占系统总内存的百分比

Lesson 16 怎样理解内存中的Buffer和Cache?

free数据来源

在free手册中可以看到buffer和cache的定义,但是并不能直观帮助我们理解

buffers: Memory used by kernel buffers (Buffers in /proc/meminfo)
cache: Memory used by the page cache and slabs (Cache and Sreclaimable in /proc/meminfo)

proc文件系统

接着看proc文件系统中的文档可以看到: Buffers是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据(通常不会特别大)。Cached是从磁盘读取文件的页缓存,用来缓存从文件中读取的数据。Slab包括可回收和不可回收两部分。

Buffers %lu: Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so)    

Cached %lu: In-memory cache for files read from the disk(the page cache). Doesn't include SwapCached.

SReclaimable %lu: Part of Slab, that might be reclaimed, such as ceches.

Sunreclaim %lu: Part of Slab, that cannot bt reclaimed on memory pressure.

案例

该实验对环境要求较高,需要系用配置多块磁盘,并且分区/dev/sdb1处于未使用状态。如果不满足千万不要尝试,否则会对磁盘分区造成损坏

首先安装sysstat包,然后清理系统缓存

echo 3 > /proc/sys/vm/drop_caches

场景1 磁盘和文件写案例

vmstat 1 #空闲情况下查看系统内存使用情况
dd if=/dev/urandom of=/tmp/file bs=1M count=500 #通过读取随机设备,生产一个500MB大小的文件
#此时观察vmstat,发现cache在不断增长,但是Buffer基本保持不变
#Cache开始增长时,块设备IO很少,dd命令结束后cache不再增长,但块设备写还会持续一段时间

echo 3 >/proc/sys/vm/drop_caches
dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048 #清理缓存后向磁盘分区写入2GB的随机数据
#此时观察vmstat输出,发现写磁盘会时buffer和cache都在增长,但是buffer增长快得多

通过该案例可以看出写文件时会用到cache缓存数据,写磁盘时会用到buffer来缓存数据。

场景2 磁盘和文件读案例

echo 3 > /proc/sys/vm/drop_caches
dd if=/tmp/file of=/dev/null
#观察vmstat输出,发现读取文件时buffer保持不变,cache不停增长

echo 3 >/proc/sys/vm/drop_caches
dd if=/dev/sda1 of=/dev/null bs=1M count=1024
#观察vmstat发现读磁盘时,buffer和cache都在增长,但是buffer增长快得多

通过上述实验可以看出buffer是对磁盘数据的缓存,cache是对文件数据的缓存,它们既会用在读请求也会用在写请求中。


Lesson 17 如何利用系统缓存优化程序的运行效率

缓存命中率

缓存命中率是指直接通过缓存获取数据的请求次数,占所有请求次数的百分比。命中率越高说明缓存带来的收益越高,应用程序的性能也就越好。

安装bcc包后可以通过cachestat和cachetop来监测缓存的读写命中情况。

安装pcstat后可以查看文件在内存中的缓存大小以及缓存比例。

#首先安装Go
export GOPATH=~/go
export PATH=~/go/bin:$PATH
go get golang.org/x/sys/unix
go ge github.com/tobert/pcstat/pcstat

实验案例一 dd缓存加速

dd if=/dev/sda1 of=file bs=1M count=512 #生产一个512MB的临时文件
echo 3 > /proc/sys/vm/drop_caches #清理缓存
pcstat file #确定刚才生成文件不在系统缓存中,此时cached和percent都是0
cachetop 5
dd if=file of=/dev/null bs=1M #测试文件读取速度
#此时文件读取性能为30+MB/s,查看cachetop结果发现并不是所有的读都落在磁盘上,读缓存命中率只有50%。
dd if=file of=/dev/null bs=1M #重复上述读文件测试
#此时文件读取性能为4+GB/s,读缓存命中率为100%
pcstat file #查看文件file的缓存情况,100%全部缓存

实验表明系统缓存对第二次dd命令有明显的加速效果,大大提高了文件读取的性能。同时要注意如果我们把dd作为性能测试工具时,由于缓存存在会导致测试结果严重失真。

实验案例二 O_DIRECT选项绕过系统缓存

cachetop 5
sudo docker run --privileged --name=app -itd feisky/app:io-direct
sudo docker logs app #确认案例启动成功
#实验结果表明每读32MB数据都要花0.9s,且cachetop输出中显示1024次缓存全部命中

但是凭感觉可知如果缓存命中读速度不应如此慢,读次数时1024,页大小为4K,五秒的时间内读取了1024*4KB数据,即每秒0.8MB,和结果中32MB相差较大。说明该案例没有充分利用缓存,怀疑系统调用设置了直接I/O标志绕过系统缓存。因此接下来观察系统调用

strace -p $(pgrep app)
#strace 结果可以看到openat打开磁盘分区/dev/sdb1,传入参数为O_RDONLY|O_DIRECT

这就解释了为什么读32MB数据那么慢,直接从磁盘读写肯定远远慢于缓存。找出问题后我们再看案例的源代码发现flags中指定了直接IO标志。删除该选项后重跑,验证性能变化。


Lesson 18 内存泄漏,如何定位和处理?

对应用程序来说,动态内存的分配和回收是核心又复杂的一个逻辑功能模块。管理内存的过程中会发生各种各样的“事故”:

  • 没正确回收分配的内存,导致了泄漏
  • 访问的是已分配内存边界外的地址,导致程序异常退出
  • …

内存的分配与回收

在Lesson15中我们了解到了虚拟内存分布从低到高分别是只读段,数据段,堆,内存映射段,栈五部分。其中会导致内存泄漏的是:

  • 堆: 由应用程序自己来分配和管理,除非程序退出这些堆内存不会被系统自动释放。
  • 内存映射段:包括动态链接库和共享内存,其中共享内存由程序自动分配和管理

内存泄漏的危害比较大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。 内存泄漏不断累积甚至会耗尽系统内存。

实验 如何检测内存泄漏

预先安装systat,docker,bcc

sudo docker run --name=app -itd feisky/app:mem-leak
sudo docker logs app
vmstat 3

可以看到free在不断下降,buffer和cache基本保持不变。说明系统的内存一致在升高。但并不能说明存在内存泄漏。此时可以通过memleak工具来跟踪系统或进程的内存分配/释放请求。

/usr/share/bcc/tools/memleak -a -p $(pidof app)

从memleak输出可以看到,应用在不停地分配内存,并且这些分配的地址并没有被回收。通过调用栈看到是fibonacci函数分配的内存没有释放。定位到源码后查看源码来修复增加内存释放函数即可。

另外,在该实验中也可以通过将动态分配的内存改为数组来避免内存泄漏的问题,数据放在栈中由系统自动分配与回收。


Lesson 19/20 为什么系统的Swap变高

系统内存资源紧张时通过内存回收和OOM杀死进程来解决。其中可回收内存包括:

  • 缓存/缓冲区,属于可回收资源,在文件管理中通常叫做文件页
    • 被应用程序修改过暂时没写入磁盘的数据(脏页),要先写入磁盘然后才能内存释放
      • 在应用程序中通过fsync将脏页同步到磁盘
      • 交给系统,内核线程pdflush负责这些脏页的刷新
  • 内存映射获取的文件映射页,也可以被释放掉,下次访问时从文件重新读取

对于程序自动分配的堆内存,也就是我们在内存管理中的匿名页,虽然这些内存不能直接释放,但是Linux提供了Swap机制将不常访问的内存写入到磁盘来释放内存,再次访问时从磁盘读取到内存即可。

Swap原理

Swap本质就是把一块磁盘空间或者一个本地文件当作内存来使用,包括换入和换出两个过程:

  • 换出: 将进程暂时不用的内存数据存储到磁盘中,并释放这些内存
  • 换入: 进程再次访问内存时,将它们从磁盘读到内存中

Linux如何衡量内存资源是否紧张?

  • 直接内存回收 新的大块内存分配请求,但剩余内存不足。此时系统会回收一部分内存;
  • kswapd0 内核线程定期回收内存。为了衡量内存使用情况,定义了pages_min,pages_low,pages_high三个阈值,并根据其来进行内存的回收操作。

    • 剩余内存 < pages_min,进程可用内存耗尽了,只有内核才可以分配内存
    • pages_min < 剩余内存 < pages_low,内存压力较大,kswapd0执行内存回收,直到剩余内存 > pages_high
    • pages_low < 剩余内存 < pages_high,内存有一定压力,但可以满足新内存请求
    • 剩余内存 > pages_high,说明剩余内存较多,无内存压力

      pages_low = pages_min 5 / 4
      pages_high = pages_min
      3 / 2

NUMA 与 SWAP

很多情况下系统剩余内存较多,但SWAP依旧升高,这是由于处理器的NUMA架构。

在NUMA架构下多个处理器划分到不同的Node,每个Node都拥有自己的本地内存空间。在分析内存的使用时应该针对每个Node单独分析。

numactl --hardware #查看处理器在Node的分布情况,以及每个Node的内存使用情况

内存三个阈值可以通过/proc/zoneinfo来查看,该文件中还包括活跃和非活跃的匿名页/文件页数。

当某个Node内存不足时,系统可以从其他Node寻找空闲资源,也可以从本地内存中回收内存。 通过/proc/sys/vm/zone_raclaim_mode来调整。

  • 0表示既可以从其他Node寻找空闲资源,也可以从本地回收内存
  • 1,2,4表示只回收本地内存,2表示可以会回脏数据回收内存,4表示可以用Swap方式回收内存。

swappiness

在实际回收过程中Linux根据/proc/sys/vm/swapiness选项来调整使用Swap的积极程度,从0-100,数值越大越积极使用Swap,即更倾向于回收匿名页;数值越小越消极使用Swap,即更倾向于回收文件页。

注意:这只是调整Swap积极程度的权重,即使设置为0,当剩余内存+文件页小于页高阈值时,还是会发生Swap。

实验 Swap升高时如何定位分析

free #首先通过free查看swap使用情况,若swap=0表示未配置Swap
#先创建并开启swap
fallocate -l 8G /mnt/swapfile
chmod 600 /mnt/swapfile
mkswap /mnt/swapfile
swapon /mnt/swapfile

free #再次执行free确保Swap配置成功

dd if=/dev/sda1 of=/dev/null bs=1G count=2048 #模拟大文件读取
sar -r -S 1  #查看内存各个指标变化 -r内存 -S swap
#根据结果可以看出,%memused在不断增长,剩余内存kbmemfress不断减少,缓冲区kbbuffers不断增大,由此可知剩余内存不断分配给了缓冲区
#一段时间之后,剩余内存很小,而缓冲区占用了大部分内存。此时Swap使用之间增大,缓冲区和剩余内存只在小范围波动

停下sar命令
cachetop5 #观察缓存
#可以看到dd进程读写只有50%的命中率,未命中数为4w+页,说明正式dd进程导致缓冲区使用升高
watch -d grep -A 15 ‘Normal’ /proc/zoneinfo #观察内存指标变化
#发现升级内存在一个小范围不停的波动,低于页低阈值时会突然增大到一个大于页高阈值的值

说明剩余内存和缓冲区的波动变化正是由于内存回收和缓存再次分配的循环往复。有时候Swap用的多,有时候缓冲区波动更多。此时查看swappiness值为60,是一个相对中和的配置,系统会根据实际运行情况来选去合适的回收类型。


Lesson 21 套路篇:如何“快准狠”找到系统内存存在的问题

内存性能指标

系统内存指标

  • 已用内存/剩余内存
  • 共享内存 (tmpfs实现)
  • 可用内存: 包括剩余内存和可回收内存
  • 缓存:磁盘读取文件的页缓存,slab分配器中的可回收部分
  • 缓冲区: 原始磁盘块的临时存储,缓存将要写入磁盘的数据

进程内存指标

  • 虚拟内存: 5大部分
  • 常驻内存: 进程实际使用的物理内存,不包括Swap和共享内存
  • 共享内存: 与其他进程共享的内存,以及动态链接库和程序的代码段
  • Swap内存: 通过Swap换出到磁盘的内存

缺页异常

  • 可以直接从物理内存中分配,次缺页异常
  • 需要磁盘IO介入(如Swap),主缺页异常。 此时内存访问会慢很多

内存性能工具

根据不同的性能指标来找合适的工具:

内存分析工具包含的性能指标:

如何迅速分析内存的性能瓶颈

通常先运行几个覆盖面比较大的性能工具,如free,top,vmstat,pidstat等

  • 先用free和top查看系统整体内存使用情况
  • 再用vmstat和pidstat,查看一段时间的趋势,从而判断内存问题的类型
  • 最后进行详细分析,比如内存分配分析,缓存/缓冲区分析,具体进程的内存使用分析等

常见的优化思路:

  • 最好禁止Swap,若必须开启则尽量降低swappiness的值
  • 减少内存的动态分配,如可以用内存池,HugePage等
  • 尽量使用缓存和缓冲区来访问数据。如用堆栈明确声明内存空间来存储需要缓存的数据,或者用Redis外部缓存组件来优化数据的访问
  • cgroups等方式来限制进程的内存使用情况,确保系统内存不被异常进程耗尽
  • /proc/pid/oom_adj调整核心应用的oom_score,保证即使内存紧张核心应用也不会被OOM杀死

Lesson 22 答疑 (略)

《Linux 性能优化实战》第二周--CPU性能篇(下)

Veröffentlicht am 2020-06-03

个人感悟

这两周主要围绕CPU性能优化相关基础知识进行学习。对于CPU性能问题的处理,首先我们要了解CPU相关性能指标。

CPU性能指标

平均负载,上下文切换,CPU使用率,CPU缓存命中率等.

平均负载是指系统处于可运行状态和不可中断状态的平均进程数(一般平均负载高于CPU数量70%的时候需要注意排查)。 平均负载升高可能是CPU密集进程导致,也可能是I/O或进程过多超负载导致. 可以用mpstat和pidstat辅助分析.

uptime #查看最近1/5/15分钟平均负载
watch -d uptime #实时查看变化情况
mpstat -P ALL 5 #查看CPU使用率,间隔5秒输出
pidstat -u 5 1  #查看进程CPU使用率/IOwait

上下文切换,主要分为进程上下文切换,线程上下文切换以及中断上下文切换(可以利用多线程替代多进程来提升性能)。可以通过vmstat查看系统总体上下文切换和中断次数. pidstat -w 查看每个进程上下文切换次数, -wt输出线程指标.

watch -d cat /proc/interrupts #查看中断变化具体来源
  • 系统调用涉及内核态和用户态的切换因此实际发生两次CPU上下文切换,不过不涉及虚拟内存等资源也不切换进程.
  • 进程的切换都在内核态,保存内核状态和CPU寄存器之前,先保存进程的虚拟内存,栈等
  • 同一进程内的线程切换只需要保存线程的私有数据,寄存器等.
  • 中断上下文切换并不涉及进程的用户态,比进程优先级高

CPU使用率过高时一般都是结合top/pidstat/perf来进行分析定位. 如果定位不到CPU使用率高的进程,可以跟踪top查看是否存在短时进程. 调用pstree分析父进程.

perf record -g
perf report #查看性能报告找到瓶颈

execsnoop #监控短时进程

iowait升高时,可以用dstat观察CPU和I/O的使用情况.

pidstat -d -p XXX n m  #-d输出I/O情况,指定线程号间隔n秒输出m组数据
strace -p XXX #跟踪系统调用
如果定位进程状态为Z,通过perf report查看调用栈

僵尸进程, 通过pstree查看父进程,在父进程中进行解决(子进程创建和清理的地方)

pstree -aps XXX

中断处理

/proc/softirqs #查看软中断
/proc/interrupts #查看硬中断

网络监测工具

sar -n DEV 1 #网络收发报告, 间隔1秒输出一组数据
tcpdump -i eth0 -n tcp port 80 #指定网卡eth0,tcp协议,端口号80

CPU性能分析套路

通过几组实验分析我们发现对于CPU性能,常用的瓶颈问题有一些套路可以定位问题所在。虽然CPU性能指标很多,但是都不是孤立存在的,很多指标间都存在一定的关联。 再遇到CPU性能问题时我们可以先运行几个支持指标较多的工具(top/vmstat/pidstat)来缩小排查范围,查看是否是因为某个进程导致,找出导致性能问题的进程之后,再用进程分析工具来分析进程的行为。比如strace分析系统调用情况,或者perf分析调用链中各级函数的执行情况。

CPU性能优化方案

定位到问题所在之后并不是要立即着手进行优化。在需要优化之前,要先考虑优化到底能提升多少性能(应用程序和系统资源多维度评估),选取哪些性能问题进行优化以及优化方案的取舍. 优化往往会带来复杂度的提升,所以要做好性能和复杂度的平衡.

CPU优化的一些常用方法

  • 应用程序优化
    • 编译器优化
    • 算法优化
    • 异步处理
    • 多线程代替多进程
    • 善用缓存
  • 系统优化
    • CPU绑定
    • CPU独占
    • 优先级调整
    • 为进程设置资源限制
    • NUMA优化
    • 中断负载均衡

接下来是本周读书笔记


Lesson 9 怎么理解Linux软中断

本周继续学习CPU性能相关知识点,Linux 性能优化实战第一周第8课提到不可中断状态一般都是短时进程,主要是系统的一种保护机制,保证硬件的交互过程不被意外打断. 但是如果进程长时间处于不可中断状态就需要注意是否存在磁盘I/O问题.

除了iowait,软中断softirq导致CPU使用率增加的场景也比较常见.

为了解决中断处理程序执行时间过长和中断丢失的问题,Linux将中断过程分为两阶段:

  • 快速处理中断 (硬中断, 会打断CPU正在执行的任务)
  • 延迟处理上半部未完成的工作, 通常以内核线程的方式运行 (软中断,内核线程执行)

以网卡接收数据包为例: 网卡接收到数据包之后,先通过硬中断通知内核新数据到达.此时内核调用中断处理程序来响应. 第一步快速处理中断,将网卡数据读到内存中,然后更新硬件寄存器的状态(表示数据已经读完); 第二步发送软中断信号,内核线程从内存中找到网络数据,按照网络协议栈对数据逐层解析和处理,直到将其发送给应用程序.

软中断不仅包括上述硬件设备中断处理程序的第二阶段,还包含一些内核自定义的事件. 如内核调度,RCU锁等.

~ cat /proc/softirqs   #查看软中断的运行情况 10种不同软中断类型
                CPU0       CPU1       CPU2       CPU3       
      HI:     940848    8695970     929219     958387
   TIMER:    6032528    8523080    6185804    7882723
  NET_TX:         13         18         10      21655
  NET_RX:      81303      81483      74123    5497701
   BLOCK:        112        109    1961852         73
IRQ_POLL:          0          0          0          0
 TASKLET:         40        152      69233      41556
   SCHED:    5262219    6279886    5054733    5849054
 HRTIMER:          0          0          0          0
     RCU:    4615615    5897520    4693046    5749548

➜  ~ ps -aux | grep softirq #一个CPU对应一个软中断内核线程
  root         7  0.0  0.0      0     0 ?        S    Jun01   0:00 [ksoftirqd/0]
  root        16  0.0  0.0      0     0 ?        S    Jun01   0:01 [ksoftirqd/1]
  root        22  0.0  0.0      0     0 ?        S    Jun01   0:01 [ksoftirqd/2]
  root        28  0.0  0.0      0     0 ?        S    Jun01   0:02 [ksoftirqd/3]

Lesson 10 系统的软中断CPU使用率高,怎么解决

所需工具

工具 用途
sar 系统报告工具,实时查看当前系统活动,配置保存和报告历史统计
hping3 构造TCP/IP包, 对系统进行安全审计,防火墙测试等
tcpdump 网络抓包工具, 用来分析各种网络问题

实验与分析

sudo docker run -itd --name=nginx -p 81:80 nginx  #启动nginx服务
curl http://localhost:81  #检查nginx服务是否正常运行

hping3 -S -p 81 -i u100 localhost    #每100微秒发送一个网络帧

本机测试中并没有监测到系统响应变慢,改为1微秒时数据包全丢, 10微秒时丢包率为80%.

top查看ksoftirqd软中断进程CPU使用率也不高.

继续执行后续指令

watch -d cat /proc/softirqs #观察高亮变化

此时发现TIMER/NET_RX/SCHED/RCU都在不停变化,且NET_RX变化速率较快,几K级别的在增加. 其它几种类型的软中断是保证linux调度,时钟,临界区保护等正常工作必须,有变化是正常的.

因此我们着手分析网络接收的软中断,选取sar工具查看网络收发情况

sar -n DEV 1  #每隔1秒输出网络收发报告

07:01:11 PM     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s rxcmp/s   txcmp/s  rxmcst/s   %ifutil
07:01:12 PM veth3a2b4de      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
07:01:12 PM        lo  20895.00  20895.00    843.42    843.42      0.00      0.00      0.00      0.00
07:01:12 PM   docker0      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
07:01:12 PM enp0s31f6      6.00      0.00      0.85      0.00      0.00      0.00      0.00      0.00
07:01:12 PM vethedb691a      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
07:01:12 PM br-1665f3682889      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  表示每秒接收/发送的千字节数 BPS 

看到网卡lo每秒接收的数据明显较多, 843*1024/20895 =41 说明每个网络帧只有41字节,小包问题.

进一步通过tcpdump分析这是什么样的网络帧

tcpdump -i lo -n tcp port 81

18:20:43.273056 IP 127.0.0.1.12537 > 127.0.0.1.81: Flags [R], seq 393134060, win 0, length 0
18:20:43.273184 IP 127.0.0.1.12538 > 127.0.0.1.81: Flags [S], seq 1240632262, win 512, length 0
18:20:43.273193 IP 127.0.0.1.81 > 127.0.0.1.12538: Flags [S.], seq 3698133257, ack 1240632263, win 65495, options [mss 65495], length 0
18:20:43.273198 IP 127.0.0.1.12538 > 127.0.0.1.81: Flags [R], seq 1240632263, win 0, length 0

可以看出网络帧是发送到nginx端口,Flags [S]表示这是一个SYN包, 可以锁定是SYN FLOOD问题,最简单的解决方法就是防火墙中封锁该来源IP.

总结

如果碰到软中断线程CPU使用率高的情况下, 可以借助sar和tcpdump等工具进一步分析来源.


Lesson 11 [套路] 如何迅速分析出系统CPU的瓶颈在哪里?

要想分析处理CPU性能问题, 首先我们需要了解CPU性能指标.

CPU性能指标

  • CPU使用率
    • 用户CPU使用率, 包括用户态(user)和低优先级用户态(nice). 该指标过高说明应用程序比较繁忙.
    • 系统CPU使用率, CPU在内核态运行的时间百分比(不含中断). 该指标高说明内核比较繁忙.
    • 等待I/O的CPU使用率, iowait, 该指标高说明系统与硬件设备I/O交互时间比较长.
    • 软/硬中断CPU使用率, 该指标高说明系统中发生大量中断.
    • steal CPU / guest CPU, 表示虚拟机占用的CPU百分比.
  • 平均负载

    理想情况下平均负载等于逻辑CPU个数,表示每个CPU都被充分利用. 若大于则说明系统负载较重.

  • 进程上下文切换

    包括无法获取资源的自愿切换和系统强制调度时的非自愿切换. 上下文切换本身是保证Linux正常运行的一项核心功能. 过多的切换则会将原本运行进程的CPU时间消耗在寄存器,内核占及虚拟内存等数据保存和恢复上.

  • CPU缓存命中率

    CPU缓存的复用情况,命中率越高性能越好. 其中L1/L2常用在单核,L3则用在多核中.

性能工具

回顾之前的几个CPU性能测试场景:

  • 平均负载案例
    • 先用uptime查看系统平均负载
    • 判断负载在升高后再用mpstat和pidstat分别查看每个CPU和每个进程CPU使用情况.找出导致平均负载较高的进程.
  • 上下文切换案例
    • 先用vmstat查看系统上下文切换和中断次数
    • 再用pidstat观察进程的自愿和非自愿上下文切换情况
    • 最后通过pidstat观察线程的上下文切换情况
  • 进程CPU使用率高案例
    • 先用top查看系统和进程的CPU使用情况,定位到进程
    • 再用perf top观察进程调用链,定位到具体函数
  • 系统CPU使用率高案例
    • 先用top查看系统和进程的CPU使用情况,top/pidstat都无法找到CPU使用率高的进程
    • 重新审视top输出
    • 从CPU使用率不高,但是处于Running状态的进程入手
    • perf record/report发现短时进程导致 (execsnoop工具)
  • 不可中断和僵尸进程案例
    • 先用top观察iowait升高,发现大量不可中断和僵尸进程
    • strace无法跟踪进程系统调用
    • perf分析调用链发现根源来自磁盘直接I/O
  • 软中断案例
    • top观察系统软中断CPU使用率高
    • 查看/proc/softirqs找到变化速率较快的几种软中断
    • sar命令发现是网络小包问题
    • tcpdump找出网络帧的类型和来源, 确定SYN FLOOD攻击导致

根据不同的性能指标来找合适的工具:

在生产环境中往往开发者没有权限安装新的工具包,只能最大化利用好系统中已经安装好的工具. 因此要了解一些主流工具能够提供哪些指标分析.

作者给了一个直观的套路, 先运行几个支持指标较多的工具, 如top/vmstat/pidstat,根据它们的输出可以得出是哪种类型的性能问题. 定位到进程后再用strace/perf分析调用情况进一步分析. 如果是软中断导致用/proc/softirqs


Lesson 12 CPU性能优化的几个思考

性能优化方法论

遇到性能问题,优化前首先思考三个问题:

  • 首先判断优化是否有效,能提升多少性能?
  • 多种性能问题同时存在情况下,先优化哪一个?
  • 提升性能的方法往往不唯一,如何选取? 是否总选最大程度提升的那一种?

怎样评估性能优化效果

  • 确定性能的量化指标
    • 不要局限在单一维度,至少从应用程序和系统资源两个维度,分别选择不同的指标
      • 应用程序: 吞吐量和请求延迟
      • 系统资源: CPU使用率
    • 好的应用程序是性能优化的最终目的和结果,系统资源的使用情况是影响应用程序性能的根源.
    • 例如, web程序可以用ab等工具测试并发请求数和响应延迟,同时可以用vmstat/pidstat观察系统和进程的CPU使用率
  • 测试优化前的性能指标
  • 测试优化后的性能指标
    • 要避免性能测试工具的干扰
    • 避免外部环境的变化

多个性能问题同时存在,如何选择?

并不是所有性能问题都值得优化,性能测试也存在二八法则

  • 如果是系统资源达到的瓶颈,首先优化系统资源的使用问题.
  • 针对不同类型的指标,首先优化那些由瓶颈导致,性能指标变化幅度较大的问题.

多个优化方法, 如何选择?

在提升性能的同时也要考虑优化成本.性能优化通常会带来复杂度的提升,降低程序的可维护性. (balance)

例如DPDK(Data Plane Development Kit)是一种优化网络处理速度的方法,通过绕开内核网络协议栈的方法,提升网络的处理能力. 但是它要求独占一个CPU以及一定数量的内存大页,并且以100%CPU使用率运行. 在CPU核数较少的情况下不适合.

CPU 优化

  • 应用程序优化
    • 编译器优化: 编译阶段开启优化选项, 如gcc -O2
    • 算法优化
    • 异步处理: 避免程序因为等待某个资源而一直阻塞,提升程序的并发处理能力. (将轮询替换为事件通知)
    • 多线程代替多进程: 减少上下文切换成本
    • 善用缓存: 加快程序处理速度
  • 系统优化
    • CPU绑定: 将进程绑定要1个/多个CPU上,提高CPU缓存命中率,减少CPU调度带来的上下文切换
    • CPU独占: CPU亲和性机制来分配进程
    • 优先级调整:使用nice适当降低非核心应用的优先级
    • 为进程设置资源显示: cgroups设置使用上限,防止由某个应用自身问题耗尽系统资源
    • NUMA优化: CPU尽可能访问本地内存
    • 中断负载均衡: irpbalance,将中断处理过程自动负载均衡到各个CPU上

避免过早优化

优化可能会带来复杂性的提升,降低可维护性.针对当前情况进行的优化可能不适应快递迭代的新需求.

因此性能优化最好逐步完善,根据性能评估的结果选择最重要的性能问题进行优化.

Lesson 13/14 答疑 (略)

读书群内分享

  • TPS QPS 系统吞吐量的区别和理解

    • QPS (Queries Per Second)每秒查询率,一台服务器每秒能够响应的查询次数.
    • TPS (Transactions Per Second)每秒事务数,软件测试的结果.

      • 用户请求服务器
      • 服务器内部处理
      • 服务器返回给客户

        QPS类似TPS,但是对于一个页面的访问形成一个TPS,但是一次页面请求可能包含多次对服务器的请求,可能计入多次QPS

    • 系统吞吐量, 包括几个重要参数:

      • QPS(TPS)
      • 并发数
      • 响应时间

        QPS(TPS)=并发数/平均相应时间

  • 深入理解Linux系统下proc文件系统内容

《Linux 性能优化实战》第一周--CPU性能篇(上)

Veröffentlicht am 2020-05-31

首先,非常感谢Go夜读杨文大佬组织了这样一个读书会, 可以让大家在繁忙的工作生活之余抽出时间来一起学习进步。希望能和群里小伙伴共同讨论学习,一起坚持下去。

第一本严格意义上并不是书籍,而是极客时间上倪老师的专栏《Linux性能优化实战》。无论是软件业务逻辑还是底层架构实现,我们在设计时都要尽可能的考虑性能开销,性能优化是软件系统中最有挑战的工作之一,比较考验程序员的综合能力。

在平时工作中我也会经常遇到一些性能相关的问题,不知如何排查和解决。希望读完这个专栏后能真正做到将性能问题和系统原理关联起来,特别是把系统从应用程序、库函数、系统调用再到内核和硬件等不同的层级贯穿起来。


Lesson 1 如何学习Linux性能优化

解决性能问题首先需要理解应用程序和系统的少数几个基本原理,再进行大量的实战练习,建立起整体性能的全局观。

性能指标

高并发和响应快对应着性能优化的两个核心指标:吞吐和延时。

  • 应用负载角度:直接影响了产品终端的用户体验
  • 系统资源角度:资源使用率、饱和度等

性能问题的本质就是系统资源已经到达瓶颈,但请求的处理还不够快,无法支撑更多的请求。 性能分析实际上就是找出应用或系统的瓶颈,设法去避免或缓解它们。

  • 选择指标评估应用程序和系统性能
  • 为应用程序和系统设置性能目标
  • 进行性能基准测试
  • 性能分析定位瓶颈
  • 性能监控和告警

对于不同的性能问题要选取不同的性能分析工具。 下面是常用的Linux Performance Tools以及对应分析的性能问题类型。

学习重点

建立整体系统性能的全局观:

  • 理解最基本的几个系统知识原理
  • 掌握必要的性能工具
  • 通过实际的场景演练,贯穿不同的组件。

Lesson 2 到底应该怎么理解“平均负载”

平均负载:单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数。它和我们传统意义上理解的CPU使用率并没有直接关系。

其中不可中断进程是正处于内核态关键流程中的进程(如常见的等待设备的I/O响应)。不可中断状态实际上是系统对进程和硬件设备的一种保护机制。

平均负载多少时合理

实际生产环境中将系统的平均负载监控起来,根据历史数据判断负载的变化趋势。当负载存在明显升高趋势时,及时进行分析和调查。 当然也可以当设置阈值(如当平均负载高于CPU数量的70%时)

现实工作中我们会经常混淆平均负载和CPU使用率的概念,其实两者并不完全对等:

  • CPU密集型进程,大量CPU使用会导致平均负载升高,此时两者一致
  • I/O密集型进程,等待I/O也会导致平均负载升高,此时CPU使用率并不一定高
  • 大量等待CPU的进程调度会导致平均负载升高,此时CPU使用率也会比较高

平均负载案例分析

分别对上述三种场景进行分析,使用的工具为iostat,mpstat,pidstat

其中还用到了Linux系统压力测试工具stress

CPU密集型进程

stress --cpu 1 --timeout 600 #模拟CPU使用率100%
watch -d uptime              #查看平均负载变化情况
mpstat -P ALL 5              #查看CPU使用率的变化情况

此实验中,CPU使用率为100%,但是iowait只有0,说明平均负载高是由CPU使用率导致

pidstat -u 5 1               #可以查看具体哪个进程导致了CPU使用率高

I/O密集型进程

stress -i 1 --timeout 600    #模拟I/O压力,不停地执行sync
watch -d uptime              #查看平均负载变化情况
mpstat -P ALL 5 1              #查看CPU使用率的变化情况

此实验中,CPU使用率为23%,而iowait高达67%,说明平均负载高是由iowait升高导致

pidstat -u 5 1               #可以查看具体哪个进程导致了iowait高

大量进程

stress -c 8 --timeout 600    #模拟8进程(高于CPU核数)
watch -d uptime              #查看平均负载变化情况
mpstat -P ALL S              #查看CPU使用率的变化情况

此实验中,CPU严重过载,8个进程在抢占CPU,导致平均负载升高

pidstat -u 5 1               #可以查看具体哪个进程导致了CPU使用率高

###总结
平均负载高时可能是CPU密集型进程导致,也可能是I/O繁忙导致。具体分析时可以结合mpstat/pidstat工具辅助分析负载来源。


Lesson 3 经常说的CPU上下文切换是什么意思?(上)

CPU上下文切换,就是把前一个任务的CPU上下文(CPU寄存器和PC)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的位置,运行新任务。其中,保存下来的上下文会存储在系统内核中,待任务重新调度执行时再加载,保证原来的任务状态不受影响。

按照任务类型,CPU上下文切换分为:

  • 进程上下文切换
  • 线程上下文切换
  • 中断上下文切换

进程上下文切换

Linux进程按照等级权限将进程的运行空间分为内核空间和用户空间。从用户态向内核态转变时需要通过系统调用来完成。

一次系统调用过程其实进行了两次CPU上下文切换:

  • CPU寄存器中用户态的指令位置先保存起来,CPU寄存器更新为内核态指令的位置,跳转到内核态运行内核任务;
  • 系统调用结束后,CPU寄存器恢复原来保存的用户态数据,再切换到用户空间继续运行。

系统调用过程中并不会涉及虚拟内存等进程用户态资源,也不会切换进程。和传统意义上的进程上下文切换不同。因此系统调用通常称为特权模式切换。

进程是由内核管理和调度的,进程上下文切换只能发生在内核态。 因此相比系统调用来说,在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存,栈保存下来。再加载新进程的内核态后,还要刷新进程的虚拟内存和用户栈。

进程只有在调度到CPU上运行时才需要切换上下文,有以下几种场景: CPU时间片轮流分配,系统资源不足导致进程挂起,进程通过sleep函数主动挂起,高优先级进程抢占时间片,硬件中断时CPU上的进程被挂起转而执行内核中的中断服务。

线程上下文切换

线程上下文切换分为两种:

  • 前后线程同属于一个进程,切换时虚拟内存资源不变,只需要切换线程的私有数据,寄存器等;
  • 前后线程属于不同进程,与进程上下文切换相同。

同进程的线程切换消耗资源较少,这也是多线程的优势。

中断上下文切换

中断上下文切换并不涉及到进程的用户态,因此中断上下文只包括内核态中断服务程序执行所必须的状态(CPU寄存器,内核堆栈,硬件中断参数等)。

中断处理优先级比进程高,所以中断上下文切换和进程上下文切换不会同时发生。

总结

尽量在程序中使用多线程,因为多线程上下文切换资源消耗较少。


Lesson 4 经常说的CPU上下文切换是什么意思?(下)

通过vmstat可以查看系统总体的上下文切换情况。

vmstat 5         #每隔5s输出一组数据
  • cs (context switch) 每秒上下文切换次数
  • in (interrupt) 每秒中断次数
  • r (runnning or runnable)就绪队列的长度,正在运行和等待CPU的进程数
  • b (Blocked) 处于不可中断睡眠状态的进程数

要查看每个进程的详细情况,需要使用pidstat来查看每个进程上下文切换情况

pidstat -w 5
  • cswch 每秒自愿上下文切换次数 (进程无法获取所需资源导致的上下文切换)
  • nvcswch 每秒非自愿上下文切换次数 (时间片轮流等系统强制调度)

实验分析

vmstat 1 1    #首先获取空闲系统的上下文切换次数
sysbench --threads=10 --max-time=300 threads run #模拟多线程切换问题

vmstat 1 1    #新终端观察上下文切换情况
此时发现cs数据明显升高,同时观察其他指标:
r列: 远超系统CPU个数,说明存在大量CPU竞争
us和sy列: sy列占比80%,说明CPU主要被内核占用
in列: 中断次数明显上升,说明中断处理也是潜在问题

综合以上分析,说明运行/等待CPU的进程过多,导致大量的上下文切换,上下文切换导致系统的CPU占用率高。

pidstat -w -u 1  #查看到底哪个进程导致的问题

从结果中看出是sysbench导致CPU使用率过高,但是pidstat输出的上下文次数加起来也并不多。分析sysbench模拟的是线程的切换,因此需要在pidstat后加-t参数查看线程指标。

另外对于中断次数过多,我们可以通过/proc/interrupts文件读取

watch -d cat /proc/interrupts

发现次数变化速度最快的是重调度中断(RES),该中断用来唤醒空闲状态的CPU来调度新的任务运行。分析还是因为过多任务的调度问题,和上下文切换分析一致。

总结

每秒上下文切换多少才正常呢?
经验来说,当每秒上下文切换超过1w次,或者切换次数出现数量级的增长时,系统可能出现了性能问题。此时可以根据上下文切换类型来具体分析是I/O问题还是CPU瓶颈,或者具体哪一类中断导致的异常。


Lesson 5 某个应用的CPU使用率达到100%,怎么办?

Linux作为多任务操作系统,将CPU时间划分为很短的时间片,通过调度器轮流分配给各个任务使用。为了维护CPU时间,Linux通过事先定义的节拍率,触发时间中断,并使用全局变了jiffies记录开机以来的节拍数。时间中断发生一次该值+1.

CPU使用率,除了空闲时间以外的其他时间占总CPU时间的百分比。可以通过/proc/stat中的数据来计算出CPU使用率。因为/proc/stat时开机以来的节拍数累加值,计算出来的是开机以来的平均CPU使用率,一般意义不大。可以间隔取一段时间的两次值作差来计算该段时间内的平均CPU使用率。 性能分析工具给出的都是间隔一段时间的平均CPU使用率,要注意间隔时间的设置。

CPU使用率可以通过top 或 ps来查看。分析进程的CPU问题可以通过perf,它以性能事件采样为基础,不仅可以分析系统的各种事件和内核性能,还可以用来分析指定应用程序的性能问题。

perf top / perf record / perf report (-g 开启调用关系的采样)

实验案例

sudo docker run --name nginx -p 10000:80 -itd feisky/nginx
sudo docker run --name phpfpm -itd --network container:nginx feisky/php-fpm

ab -c 10 -n 100 http://XXX.XXX.XXX.XXX:10000/ #测试Nginx服务性能

发现此时每秒可承受请求给长少,此时将测试的请求数从100增加到10000。 在另外一个终端运行top查看每个CPU的使用率。发现系统中几个php-fpm进程导致CPU使用率骤升。

接着用perf来分析具体是php-fpm中哪个函数导致该问题。

perf top -g -p XXXX #对某一个php-fpm进程进行分析

发现其中sqrt和add_function占用CPU过多, 此时查看源码找到原来是sqrt中在发布前没有删除测试代码段,存在一个百万次的循环导致。 将该无用代码删除后发现nginx负载能力明显提升。

总结

在碰到CPU使用率过高问题时,可以借助top pidstat确定引发CPU性能问题的来源,然后用perf来具体定位到引起性能问题的函数。可以用来代码性能进一步优化。


Lesson 6 系统的CPU使用率很高,为什么找不到高CPU的应用?

按照上篇的CPU性能问题分析套路,看起来CPU使用率问题很容易排查,真实场景中却不然。

实验案例分析

sudo docker run --name nginx -p 10000:80 -itd feisky/nginx:sp
sudo docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:sp
ab -c 100 -n 1000 http://XXX.XXX.XXX.XXX:10000/ #并发100个请求测试

实验结果中每秒请求数依旧不高,我们将并发请求数降为5后,nginx负载能力依旧很低。

此时用top和pidstat发现系统CPU使用率过高,但是并没有发现CPU使用率高的进程。

出现这种情况一般时我们分析时遗漏的什么信息,重新运行top命令并观察一会。发现就绪队列中处于Running状态的进行过多,超过了我们的并发请求次数5. 再仔细查看进程运行数据,发现nginx和php-fpm都处于sleep状态,真正处于运行的却是几个stress进程。

下一步就利用pidstat分析这几个stress进程,发现没有任何输出。用ps aux交叉验证发现依旧不存在该进程。说明不是工具的问题。再top查看发现stress进程的进程号变化了,此时有可能时以下两种原因导致:

  • 进程不停的崩溃重启(如段错误/配置错误等),此时进程退出后可能又被监控系统重启;
  • 短时进程导致,即其他应用内部通过exec调用的外面命令,这些命令一般只运行很短时间就结束,很难用top这种间隔较长的工具来发现

可以通过pstree来查找 stress的父进程,找出调用关系。

pstree | grep stress

发现是php-fpm调用的该子进程,此时去查看源码可以看出每个请求都会调用一个stress命令来模拟I/O压力。 之前top显示的结果是CPU使用率升高,是否真的是由该stress命令导致的,还需要继续分析。 代码中给每个请求加了verbose=1的参数后可以查看stress命令的输出,在中断测试该命令结果显示stress命令运行时存在因权限问题导致的文件创建失败的bug。

此时依旧只是猜测,下一步继续通过perf工具来分析。性能报告显示确实时stress占用了大量的CPU,通过修复权限问题来优化解决即可。

总结

对于常规问题无法解释的CPU使用率场景,首先要想到可能时短时应用导致的问题。对于短时进程,还可以用execsnoop工具来进行监控。


Lesson 7/8 系统中出现大量不可中断进程和僵尸进程怎么办?

进程状态

  • R Running/Runnable,表示进程在CPU的就绪队列中,正在运行或者等待运行;
  • D Disk Sleep,不可中断状态睡眠,一般表示进程正在跟硬件交互,并且交互过程中不允许被其他进程中断;
  • Z Zombie,僵尸进程,表示进程实际上已经结束,但是父进程还没有回收它的资源;
  • S Interruptible Sleep,可中断睡眠状态,表示进程因为等待某个事件而被系统挂起,当等待事件发生则会被唤醒并进入R状态;
  • I Idle,空闲状态,用在不可中断睡眠的内核线程上。 该状态不会导致平均负载升高;
  • T Stop/Traced,表示进程处于暂停或跟踪状态(SIGSTOP/SIGCONT, GDB调试);
  • X Dead,进程已经消亡,不会在top/ps中看到。

对于不可中断状态,一般都是在很短时间内结束,可忽略。但是如果系统或硬件发生故障,进程可能会保持不可中断状态很久,甚至系统中出现大量不可中断状态,此时需注意是否出现了I/O性能问题。

僵尸进程一般多进程应用容易遇到,父进程来不及处理子进程状态时子进程就提前退出,此时子进程就变成了僵尸进程。大量的僵尸进程会用尽PID进程号,导致新进程无法建立。

实验案例分析

磁盘O_DIRECT问题

sudo docker run --privileged --name=app -itd feisky/app:iowait
ps aux | grep '/app'

可以看到此时有多个app进程运行,状态分别时Ss+和D+。其中后面s表示进程是一个会话的领导进程,+号表示前台进程组。

其中进程组表示一组相互关联的进程,子进程是父进程所在组的组员。 会话指共享同一个控制终端的一个或多个进程组。

用top查看系统资源发现:1)平均负载在逐渐增加,且1分钟内平均负载达到了CPU个数,说明系统可能已经有了性能瓶颈;2)僵尸进程比较多且在不停增加;3)us和sys CPU使用率都不高,iowait却比较高;4)每个进程CPU使用率也不高,但有两个进程处于D状态,可能在等待IO。

分析目前数据可知:iowait过高导致系统平均负载升高,僵尸进程不断增长说明有程序没能正确清理子进程资源。

用dstat来分析,因为它可以同时查看CPU和I/O两种资源的使用情况,便于对比分析。

dstat 1 10    #间隔1秒输出10组数据

可以看到当wai(iowait)升高时磁盘请求read都会很大,说明iowait的升高和磁盘的读请求有关。接下来分析到底时哪个进程在读磁盘。

之前top查看的处于D状态的进程号,用pidstat -d -p XXX 展示进程的I/O统计数据。发现处于D状态的进程都没有任何读写操作。 在用pidstat -d 查看所有进程的I/O统计数据,看到app进程在进行磁盘读操作,每秒读取32MB的数据。进程访问磁盘必须使用系统调用处于内核态,接下来重点就是找到app进程的系统调用。

sudo strace -p XXX #对app进程调用进行跟踪

报错没有权限,因为已经时root权限了。所以遇到这种情况,首先要检查进程状态是否正常。 ps命令查找该进程已经处于Z状态,即僵尸进程。

这种情况下top pidstat之类的工具无法给出更多的信息,此时像第5篇一样,用perf record -d和perf report进行分析,查看app进程调用栈。

看到app确实在通过系统调用sys_read()读取数据,并且从new_sync_read和blkdev_direct_IO看出进程时进行直接读操作,请求直接从磁盘读,没有通过缓存导致iowait升高。

通过层层分析后,root cause是app内部进行了磁盘的直接I/O。然后定位到具体代码位置进行优化即可。

僵尸进程

上述优化后iowait显著下降,但是僵尸进程数量仍旧在增加。首先要定位僵尸进程的父进程,通过pstree -aps XXX,打印出该僵尸进程的调用树,发现父进程就是app进程。

查看app代码,看看子进程结束的处理是否正确(是否调用wait()/waitpid(),有没有注册SIGCHILD信号的处理函数等)。

总结

碰到iowait升高时,先用dstat pidstat等工具确认是否存在磁盘I/O问题,再找是哪些进程导致I/O,不能用strace直接分析进程调用时可以通过perf工具分析。

对于僵尸问题,用pstree找到父进程,然后看源码检查子进程结束的处理逻辑即可。

——————————————————————————————————————————————————————————————————
LastModify:2020-05-31

Kubernetes学习——调基础

Veröffentlicht am 2019-09-20

kube-proxy在大规模集群中的性能问题以及解决方法

The problem with kube-proxy: enabling IPVS on EKS
当集群规模增长到1000+个服务时,传统的iptables模式下的kube-proxy可能会遇到性能问题(calico team测试发现),解决方法是将模式切换为IPVS。在切换时需要跑一下kube-proxy cleanup或者重启worker node才能生效。

K8S集群中的所有worker node上都会运行一个kube-proxy的服务,用来负责将流量路由到后端pods。每次新服务创建时都会增加一个object存储到ETCD中,触发endpoint控制器,该控制器在ETCD中记录一组endpoints。这些断点传播到每个节点的kube-proxy中来更新本地iptable rules。在大规模集群中会遇到以下问题:

kube-proxy是用户空间程序,允许用户创建内核级的firewall规则。

  • 插入、删除entries随着集群服务规模增加而越来越低效;
  • 每一个收到的包都需要和iptables中的rule进行比对匹配,当iptable很大时会导致延迟增加,最终导致低吞吐量。
  • 表更新时需要全局锁,竞争会进一步导致延迟问题。如一5000个服务的集群可能需要10min来插入一条rule。

此时就需要IPVS来解决,它是一个内核模式下,传输层负载均衡直接将流量路由到实际的服务中。基于hash而非列表避免了上述问题。同时支持UDP、TCP和提供不同的负载均衡算法:round robin,least connected,destination hashing,source hashing,shortest expected delay,never queue等。

To enable IPVS, you need to add the –proxy-mode and –ipvs-scheduler flags to the kube-proxy command.

ECTD如何实现MVCC?

知道MVCC吗?在ETCD中如何实现的
数据库面对高并发下数据冲突的问题,常用的解决方案:

  • 避免冲突。使用悲观锁确保同一时刻只有一人能对数据进行更改,如读写锁,两阶段锁
  • 允许冲突,但发生时有能力检测到。乐观锁,先天乐观的认为冲突不会发生,除非被检测到。如逻辑时钟,MVCC

其中MVCC因其出色的性能优势,被越来越多的数据库采用。如Oracle,PostgreSQL,MySQL InnoDB,etcd等。它的基本思想是保存一个数据的多个历史版本,从而解决事务管理中数据隔离的问题。

版本一般选择使用时间戳或者事务ID来标识,在处理一个写请求时,MVCC并不是简单的覆盖旧值,而是为这一项添加一个新版本的数据。在读取数据项时,先确定一个要读取的版本,根据版本找到对应的数据。写操作创建新版本,读操作访问旧版本的方式使得读写隔离,不需要锁协调。MVCC读操作不会被阻塞,因此适合ETCD这种读操作比较多的场景。

type revision struct {
  main int64  //对应事务ID,全局递增不重复,在ETCD中被当做一个逻辑时钟来使用
  sub int64 //代表一次事务中不同的修改操作编号,从0开始递增
}

//keyIndex用来记录一个key声明周期中所涉及的版本,modified最近一次修改的版本号
type keyIndex struct {
  key []byte
  modified revision
  generations []generation
}
//一个generation代表一个key从创建到被删除的过程
type generation struct {
  ver int64
  created revision // first revision
  revs []revision
}

treeIndex是一个树状索引,它通过在内存中维护一个B树来达到加速查询key的功能。树的每一个节点都是keyIndex,它实现了Item接口,其中的比较函数主要是比较key的大小。这里只存储key的信息,value保存在磁盘。

backend封装了etcd中的存储,按照设计,backend可以对接多种存储,当前使用的是boltdb。纯GO实现的支持事务的KV存储,etcd就是基于boltdb事务实现的。 etcd在boltdb中存储的key是revision,value是etcd自己的KV组合,每次更新时,新的revision记在keyIndex中,同时revision对应的KV组合存储在boltdb中。

总之,内存btree维护的是key到keyIndex的映射,keyIndex内维护多版本的revision信息,revision可以映射到磁盘bolt中具体的value。

  1. K8S的服务发现是如何做的?
    浅谈K8S中的服务发现

每个Pod都会附着在一个大的扁平的IP网络中,通常是VXLAN叠加网络。每个Pod都有自己的IP,在pod网络中可路由,因此应用之间不需要NAT之类的网络过程,可直接通信。

Pod动态添加和删除时,都会伴随Pod IP列表的变化,如果每个应用都检测网络管理Pod列表会比较低效。K8S通过Service对象简化该过程。客户端和Service通信,Service负责将流量负载均衡给Pod。

Service可以分为前后端两部分:前端稳定可靠,名称、IP、端口在Service整个生命周期都不会改变,无需担心客户端DNS缓存超时等问题;后端是高度动态的,包括一组符合标签选择条件的Pod,通过负载均衡的方式访问。4层轮询,工作在连接层面,同一个连接的所有请求都会进入同一个pod。

服务注册: k8s通过DNS作为服务注册表,集群会在kube-system命名中间中运行一个DNS服务,以pod的形式,成为集群DNS。

  • 向API server以POST方式提交一个新的service定义;
  • 请求经过认证、鉴权以及准入策略检查;
  • service得到一个clusterIP,并保存到集群数据仓库中;
  • 在集群范围内广播Service配置;
  • 集群DNS服务得知该Service的创建,据此创建必要的DNSA记录 (集群DNS使用CoreDNS,实现了一个控制器对apiserver监听,一旦有新service对象就创建一个从service名到clusterIP的映射域名记录)

K8S自动为每个service创建endpoint对象,保存符合条件的pod列表。

服务发现: 本地DNS解析缓存没有记录时,向DNS服务器发起对域名your-app-svc的查询,得到服务的clusterIP。然后尝试向这个IP发送流量,service network没有路由所以容器把发现这种地址的流量都发送给缺省网关。这些流量会被转发到pod所在节点的网卡上。节点的网络栈也没有路由能到达service网络,只能发送给自己的缺省网关。每个k8s节点上都有kube-proxy系统服务,会监控apiserver上service的变化,并创建iptables或者IPVS规则。这些规则告知节点捕获service网络的报文,并转发给Pod IP。

创建新的 Service 对象时,会得到一个虚拟 IP,被称为 ClusterIP。服务名及其 ClusterIP 被自动注册到集群 DNS 中,并且会创建相关的 Endpoints 对象用于保存符合标签条件的健康 Pod 的列表,Service 对象会向列表中的 Pod 转发流量。与此同时集群中所有节点都会配置相应的 iptables/IPVS 规则,监听目标为 ClusterIP 的流量并转发给真实的 Pod IP。

一个 Pod 需要用 Service 连接其它 Pod。首先向集群 DNS 发出查询,把 Service 名称解析为 ClusterIP,然后把流量发送给位于 Service 网络的 ClusterIP 上。然而没有到 Service 网络的路由,所以 Pod 把流量发送给它的缺省网关。这一行为导致流量被转发给 Pod 所在节点的网卡,然后是节点的缺省网关。这个操作中,节点的内核修改了数据包 Header 中的目标 IP,使其转向健康的 Pod。

Kubernetes学习——调度策略

Veröffentlicht am 2019-07-10

In Kubernetes, scheduling refers to making sure that Pods are matched to Nodes so that Kubelet can run them.

首先学习Linux调度器的演化,然后Go语言调度器实现的发展,最后再看K8S调度器的演进以及如何自定义调度器。

1. Linux系统调度器演进

最初的进程调度

仅有sched.h sched.c两个文件构成,只有几十行代码就负责了操作系统进程的调度。所有的调度进程都存储在上限为64的数组中,调度器能处理的进程上限只有64.

先唤醒获得信号的可中断进程,从队列倒序查找计数器counter最大的可执行进程,counter是进程能够占用的时间切片数量:

  • 如果最大的counter > 0,调用汇编实现的switch_to切换进程
  • 如果最大的counter = 0,所有进程的可执行时间都为0,那么所有进程都会获得新的时间切片

Linux系统计时器每隔10ms触发一次do_timer将当前运行进程的counter-1,当前进程计数器归0时重新触发调度。

O(n)调度器

Linux v2.4-2.6版本使用的,在最坏情况下会遍历所有任务,所以调度时间复杂度为O(n)。调度器将CPU时间分割成不同的时期Epoch,即每个任务能使用的时间切片。

其实现比最初的复杂很多,在schedule函数中遍历运行队列的所有任务,并调用goodness函数计算它们的权重,获得下一个运行的进程。

默认情况下每个任务在一个周期内都会分配200ms左右的时间片,当系统包含100个进程时,同一个进程被运行两次的间隔是21s,严重影响了可用性。每轮调度完成后就会陷入没有任务需要调度的情况,对于需要实时交互的场景不适合。

O(1)调度器

Linux在v2.6.0-v2.6.22用了4年该调度器,支持在常数时间内完成调度。调度代码从2100行增加到了5000行。

  • 支持O(1)时间复杂度, 通过运行队列runqueue和优先数组prio_array,每个运行队列都持有两个优先数组,分别存储活跃和过期的进程数组。
  • 支持对称多处理器SMP扩展性,引入本地队列解决降低多处理器下锁的粒度和冲突的可能性。引入工作窃取保证多个运行队列任务的平衡
  • 优化了对称多处理器的亲和性

完全公平调度器

CFS是在v2.6.23被合并进内核的调度器,目的是最大化CPU利用率和交互性能。

  • 调度器查找运行队列中受到不公平待遇的进程,并为其分配计算资源,分配的计算资源是与其他资源运行时间的差值+最小能够运行的时间单位;
  • 进程运行结束后发现队列中又有其他进程受到不公平待遇,调度器又运行新的进程
  • …循环往复保证各个进程的运行时间差不会大于最小运行时间单位

运行队列通过红黑树来替代之前的链表,增删改查最坏时间复杂度为O(logN),树的最左侧节点运行时间最短,也就是下个待运行的进程。
调度过程和O(1)类似,只是增加了可选的工作窃取机制并改变了底层数据结构。通过调度类实现不同任务类型的不同调度策略。

2. Go语言调度器的演进

Go语言高并发支持依靠的就是运行时的调度器。

单线程调度器

0.x版本调度器只包含表示Goroutine的G和表示线程的M两种结构,全局只有一个线程。 此时调度器还是由C语言实现,调度函数只包含40行代码。

获取调度器的全局锁;调用gosave保存栈寄存器和pc;调用nextgandunlock获取下一个需要运行的goroutine并解锁调度器;修改全局线程m上要执行的G,调用gogo函数运行最新的G。

多线程调度器

在1.0版本正式支持了多线程的调度器,实现了从不可用到可用的跨越。调度函数包含70行代码,引入了GOMAXPROCS变量帮助控制程序中的最大处理器数,即活跃线程数。

多线程调度器的主要问题是调度时锁竞争会严重浪费资源,14%的时间都花费在futex上。因此该调度器有以下问题需要解决:

  • 调度器和锁是全局资源,所有调度状态中心化存储,竞争严重
  • 线程需要经常互相传递可运行的G,引入大量延迟
  • 每个线程都需要处理内存缓存,导致大量的内存占用并影响数据局部性
  • 系统调用频繁阻塞和解除阻塞正在运行的线程,增加了额外开销

任务窃取调度器

在GM模型中引入P,增加中间层;在处理器P的基础上实现基于工作窃取的调度器。

  • 如果当前运行时在等待垃圾回收,调用gcstopm函数
  • 调用runqget和findrunnable函数从本地或全局的运行队列中获取待执行的G
  • 调用execute在当前M上运行该G

处理器P持有一个由可运行的G组成的运行队列runq,并且反向持有一个线程M。

存在的问题:1)某些G可以长时间占用线程,造成饥饿问题;2)垃圾回收需要暂停整个程序

抢占式调度器

1.1版本的调度器不支持抢占,程序只能依靠G主动让出CPU资源才能触发调度。因此在1.2版本中引入了基于协作的抢占调度,能缓解上述问题。但是存在一些无法被抢占的边缘情况,如for循环或垃圾回收STW,这些问题直到1.14基于信号的抢占式调度才解决。

基于协作抢占

编译器会在调用函数前插入morestack;Go语言运行时会在垃圾回收暂停程序、系统监控发现G运行超过10ms时发出抢占请求StackPreempt;当发生函数调用时,可能会执行编译器插入的morestack函数,它调用的newstack会检查G的stackguard0字段是否为StackPreempt;如果是则触发抢占让出该线程;

这里的抢占是通过编译器插入函数实现,需要函数调用作为入口才能触发,因此是一种协作式的抢占式调度。

基于信号抢占

程序启动时,在sighandler函数中注册SIGURG信号的处理函数doSigPreempt;在触发垃圾回收的栈扫描时调动suspendG挂起G,该函数会将_Grunning状态的G标记成可以被抢占,即将preemptStop设置为true,然后调用preemptM触发抢占;preemptM会调用singnalM向线程发送SIGURG信号;系统会中断正在运行的线程并执行doSigPreempt函数;该函数会处理抢占信号,获取当前SP和PC寄存器并调用sigctxt.pushCall;pushCall会修改寄存器并在程序回到用户态时执行asyncPreempt->asyncPreempt2->preemptPark;后者会将当前G状态改为_GPreempted并调用schedule函数让当前函数休眠并让出线程,调度器选择其他G执行。

选择SIGURG的原因:1)信号需要被调试器透传 2)信号不会被内部libc库使用并拦截 3)信号可以随意出现并不触发任何后果 4)需要处理多平台不通信号

NUMA调度器

目前只是提案,因为过于复杂且目前调度性能足够优异,因此暂时没有实现。其原理是通过拆分全局资源,让各个处理器能够就近获取,减少锁竞争并增加数据局部性。堆栈、全局运行队列、线程池会按照NUMA节点分区,网络轮询器和计时器由单独处理器持有。

3. Kubernetes调度器演进

基于谓词和优先级的调度器

Predicates和Priorities调度器是从v1.0.0发布时就存在的模式,v1.14.0最后实现和最初设计也没太多区别。不过其中引入了多次改进:

  • 调度器扩展,v1.2.0,通过外部调度器扩展的方式改变调度器策略
  • Map-Reduce优先级算法,v1.5.0,为调度器优先级算法支持MapReduce计算方式,引入可并行的Map阶段优化调度器计算性能
  • 调度器迁移,v1.10.0,从plugin/pkg/scheduler -> pkg/scheduler,kube-scheduler成为对外直接提供的可执行文件

谓词算法使用FitPredicate类型,优先级算法使用PriorityMapFunction和PriorityReduceFunction两种类型。

  • 从NodeLister中获取当前系统中存在的全部节点;
  • 调用genericScheduler.findNodesThatFit方法执行全部的谓词过滤节点
    • 根据传入pod和node对节点进行过滤,过滤掉端口号冲突,资源不足的节点
    • 调用所有调度器扩展的Filter方法辅助过滤
  • 调用PrioritizeNodes函数为所有节点打分
    • 以Pod和Node作为参数并执行同一优先级的PriorityMapFunction
    • 以Pod和优先级返回的NOde到分数的映射为参数调用PriorityReduceFunction函数
    • 调用所有调度器扩展的Prioritize方法
    • 将所有分数按照权重相加后返回从Node到分数的映射
  • 调用genericScheduler.selectHost方法选择得分最高的节点

    基于调度框架的调度器

    2018年提出的最新调度器设计,明确了K8S中各个调度阶段,提供设计良好的基于插件的接口。调度框架认为K8S目前存在Scheduling和Binding两个循环:
  • 调度循环在多个Node中为Pod选取最合适的Node
  • 绑定循环将调度决策应用到集群中,包括绑定pod和node,绑定持久存储等

除了两大循环外,调度器还包含QueueSort、PreFilter、Filter、PostFilter、Score、Reserve、Permit、PreBind、Bind、PostBind和Unreserve11个扩展点。

通过调度器中的Scheduler.schedulerOne方法作为入口分析其实现:

  • 调度阶段
    • 调用内部优先队列的MakeNextPodFunc从队列中获取下一个等待调度的Pod,用于维护等待Pod的队列会执行QueueSort插件
    • 调用genericScheduler.Schedule函数选择节点,该过程会执行PreFilter,Filter,PostFilter,Score四个扩展点的插件
    • 调用framework.RunReservePlugins函数运行Reserve插件,用于保留资源并进入绑定阶段。如果运行失败会调用Unreserve插件

因为每一次调度策略都会改变上下文,所以该阶段需要串行执行。

  • 绑定阶段
    • 启动一个G并调用framework.RunPermitPlugin异步运行Permit插件,该阶段可以实现批调度
    • 调用Scheduler.bindVolume将卷先绑定在Node上
    • 调用Scheduler.bind函数将Pod绑定到Node上完成调度,绑定过程会执行PreBind、Bind、PostBind三个扩展点的插件

自定义K8S调度器

自定义K8S调度器
默认情况下,kube-scheduler提供的默认调度器能满足绝大多数需求,在实际项目中,因为开发者相比K8S更了解自己的应用,需要定制化调度。

kube-scheduler是一个独立的二进制程序,启动之后会一直监听API Server,获取到PodSpec.NodeName为空的pod,对每个pod创建一个binding。看起来流程非常简单,但在实际生产环境中,需要考虑:

  • 如何保证全部的节点调度公平性?并不是所有节点资源配置都相同
  • 如何保证每个节点都能被分配资源?
  • 集群资源如何能够被高效利用?
  • 集群资源如何能够被最大化利用?
  • 如何保证Pod调度的性能和效率?
  • 用户是否可以根据自己的实际需求定制自己的调度策略?

考虑到实际环境中的复杂情况,K8S调度器采用插件化的形式实现,可以方便用户进行定制或者二次开发。

  • 直接clone kube-scheduler源码修改重新编译后运行, 不推荐,因为需要花费额外精力来和上游的调度程序保持一致;
  • 和默认的调度程序一起运行独立的调度程序,默认的调度器和我们自定义的调度器可以通过pod的spec.schedulerName来覆盖各自的pod。但是多个调度器同时存在比较麻烦,比如多个调度器将pod调度到同一节点时,节点资源如果不能同时满足的话会出问题。而且维护一个高质量的自定义调度程序也不容易,需要全面了解默认的调度程序,k8s架构以及各种kubernetes api对象的关系和限制等。
  • 调度器扩展程序,可以和上游调度程序兼容,本质就是一个可配置的webhook,包含过滤器和优先级两个端点。1.16中废弃
  • 调度框架,1.15版本之后引入可插拔架构的调度框架,使得定制调度器变得容易。调度框架向现有调度器添加了一组插件化的API,该API在保持调度程序核心简单且易于维护的同时,使得大部分调度功能以插件的形式存在。

Kubernetes学习——etcd

Veröffentlicht am 2019-06-16

K8S学习之——ETCD

Etcd is distributed reliable key-value store for the most critical data of a distributed system, with a focus on being:

  • Simple: well-defined, user-facing API (gRPC)
  • Secure: automatic TLS with optional client cert authentication
  • Fast: benchmarked 10,000 writes/sec
  • Reliable: properly distributed using Raft
    简单来说etcd是kubernetes提供默认的存储系统,通过分布式KV存储来保存集群中重要的数据,因此我们使用时需要为etcd数据提供备份计划。

Etcd is written in Go and uses the Raft consensus algorithm to manage a highly-available replicated log.
它通过raft一致性算法来实现可靠的分布式存储,我们上个月在team meetup中也是主要探讨了raft算法原理以及它与zookeeper的一些区别,在此通过本文记录下。

Raft算法原理

Raft算法和zookeeper类似,都是通过大多数机制来保证一致性。在Raft算法中通过选举出来的leader节点来接收客户端的请求日志数据,然后同步到集群中其它节点进行复制,当日志已经同步到超多半数以上节点的时候,该日志状态变为committed可提交状态,即可以提交到状态机中执行。此外leader节点还要通知其他节点哪些日志已经被复制成功。

Raft算法将要解决的一致性问题分为以下三个子问题:

  • leader选举: 通过心跳机制来触发leader选举,保证集群中存在一个leader节点
  • 日志复制: leader节点将来自客户端的请求序列化成日志数据并且同步复制到其它节点
  • 安全性: 如果某个节点已经将一条日志数据提交到状态机中执行,那么其他节点不可能再将此条数据输入到状态机中重复执行。

Raft算法需要保证以下几个特性:

  • Election Safety:在一个任期内最多只能存在一个leader节点
  • Leader Append-Only:leader节点只能添加日志数据,不会删除/覆盖以前的数据
  • Log Matching:如果两个节点的日志在某个索引上的日志数据和任期号都相同,那么在此index之前的日志数据一定也匹配
  • Leader Completeness:如果一条日志在某个任期被提交,那么该条日志数据在leader节点上更高任期号的日志数据中一定存在。
  • State Machine Safety:一条提交到raft状态机执行过的数据不可能再被其他节点提交执行。

Leader选举

Node节点状态

  • Leader: 领导者,一个集群中只能存在一个leader
  • Follower:跟随者,一个客户端的操作请求发送到follower上面,会首先由follwer重定向到leader上
  • Candidate:参与者,在此状态的节点会发起新的选举

节点状态切换如图所示

  1. 所有节点启动时自动进入Follower状态
  2. Follower节点启动时会开启一个选举超时的定时器,当times out时节点切换到Candidate状态发起选举
  3. 节点一旦转换为Candidate状态就开始进行选举,如果在下一次选举超时到来之前都没有选出Leader节点,那么节点会保持状态不变重新发起一次新的选举
  4. 若Candidate节点收到集群中超过半数的节点选票(包含自身),那么状态切换到Leader
  5. 若Candidate节点接收到来自Leader节点的消息,或者任期号更高的消息,即集群中已经存在Leader节点,此时节点状态切换回Follower
  6. Leader节点如果收到来自更高任期号的消息则切换回Follower节点(常见网络分区场景下)

其中节点之间的通信通过RPC来完成,etcd中主要由两种RPC请求:

  • RequestVote 用于Candidate节点发起选举
  • AppendEntries 用于Leader节点向其他节点进行复制日志数据,以及同步心跳

Raft算法通过心跳机制来触发Leader选举

节点启动时状态为Follower,只要一直接收到来自Leader或者Candidate的正常RPC消息,就会一直保持为Follower状态。
Leader节点周期性的向其它节点发送心跳请求来保持Leader状态,其中心跳请求即带有空数据的AppendEntries。
每个Follower节点都有一个选举超时定时器,如果在超时之前都没有接收到Leader的心跳请求将发起选举。发起选举时,Follower将自身的任期号+1并且切换到Candidate状态,然后向集群中其它节点发送RequestVote请求进行选举。

Candidate节点保持状态不变,直到以下情况之一发生:

  • 该节点收到集群中半数以上的节点投票,即赢得选举;
  • 节点收到来自Leader节点的数据;
  • 选举超时到来

第一种情况,因为每个Follower节点在一个任期内只能给一个节点投票先来先到,通过大多数原则保证了每个任期最多只有一个节点赢得选票(存在无节点当选的情况)。当节点成为Leader后会向集群中其它节点发送心跳消息来阻止不必要的选举。
第二种情况,处于Candidate状态下的节点收到了来自其它节点的心跳/AppendEntries消息。如果AppendEntries请求中的任期号大于自身任期,说明集群中已经存在Leader节点,此时节点转换成Follower状态。反之如果消息中任期号小于自身,则拒绝该消息,继续保持节点状态不变。
第三种情况,在选举超时到来时该节点没有赢得选举且没有收到其它节点的AppendEntries消息,说明集群中没有Leader存在,该Candidate节点任期号+1,再次发起选举。
其中,第三种情况发生在集群节点为偶数个,且有两个Candidate节点同时选举。理论上这种无法选择Leader的情况可以无限循环下去。为了避免该情况,每个节点的选举超时时间通过随机函数决定,一般在150ms-300ms之间。 (我们也仔细讨论了这种情况,因为Leader发送心跳消息时间非常短暂,如果节点选举超时时间T都相同,Leader节点挂掉那么可以极端得考虑认为所有Follower节点会在同一时刻(距离上一个心跳消息T时间)得到该信息。此时会有很多Follower节点切换到Candidate状态并进行选举,任意一个节点获得大多数投票的概率都很低。 而如果每个节点的超时时间是个随机值,那么大概率集群中有一个超时时间短的节点先感知到原Leader挂掉的事实,然后首先发起选举,因为RPC消息时间耗时较短,该节点大概率能获得大多数投票。)

日志复制

日志形式

如上图所示,日志主要由以下三个成员组成:

  1. index:索引号,即图中最上方的数字
  2. term:任期号,每个日志条目中上方数字,表示该日志在哪个任期中生成
  3. command:日志中的数据修改操作

如果一条日志被Leader节点同步到超过半数的节点中,被称为“成功复制”,这个日志条目就是committed状态(已经被提交)。如果某条日志条目状态为committed,那么在此日志之前的所有日志也都是committed状态。(如图中index<=7的日志都是committed状态)
committed状态的日志可以被Leader输入到raft状态机中执行,执行过的日志状态为applied。

日志复制过程



  1. 首先客户端的消息转发到集群Leader节点上
  2. Leader节点首先在本地日志中添加一条日志
  3. Leader节点通过AppendEntries RPC消息的形式向集群中其它节点广播该日志
  4. Follower节点收到日志首先也同样在本地日志中添加一条日志
  5. Follower节点向Leader节点返回AppendEntries应答消息
  6. Leader节点收到超过半数的应答消息时认为该日志已经被成功复制,将本地的committedIndex指向该日志的index
  7. 如果该committed状态的日志被提交到状态机中执行,那么Leader节点本地日志中的appliedIndex也要随之更新
  8. Leader节点在下次广播AppendEntries消息时会带上最新的committedIndex和appliedIndex,用于Follower节点本地日志的索引更新。

日志恢复过程

正常情况下Follower节点和Leader节点的日志会始终保持一致,但是当Leader节点突然挂掉时集群中节点日志会出现不一致的情况,当新的Leader选举出来后就需要进行相应的恢复操作。

Raft算法通过Leader节点同步来解决数据不一致问题。 对于集群中任意一个节点,Leader中都存储着两个与它们日志相关的数据。

  1. nextIndex: 下一次向该Follower节点同步时的日志索引
  2. matchIndex: 该Follower节点的最大日志索引
    在Follower节点和Leader节点日志复制正常的情况下,nextIndex = matchIndex + 1。如果日志不一致情况出现那么该等式不成立。
    集群中原Leader宕机,新Leader选出后将初始化nextIndex为新Leader节点的最后一条日志索引,matchIndex均为0。这样做的目的是首先从Follower节点的最后一条日志进行探索,如果不匹配则从前向后进行复制。
    如上图所示,Leader节点存储每个Follower节点的nextIndex=10,matchIndex=0。成为Leader首次向其它节点复制日志时,会复制index>=10的日志,同时带上2⃣️元组<6,10>告知Follower节点将要复制Term=6,index=10的日志数据。
  • 只有(a)节点的最大日志数据2⃣️元组<6,9>和Leader传来的<6,10>紧邻,复制成功且matchIndex也随之更新。
  • 其他几个节点的2⃣️元组都不匹配,返回复制失败。Leader节点收到拒绝消息后修改该follwer节点的索引数据,将nextIndex复制为matchIndex+1即1。下次同步时从索引1到10的数据给该Follwer节点,且Follower节点中未提交的数据被清除。

安全性

Raft算法对选举进行限制来保证安全性。

  • 一个节点向成为Leader就必须得到集群中超过半数的选票
  • 节点A投票给另一个节点B的必要条件时B的日志比A的要新(首先对比任期号,相同时再比较索引)

Raft算法处理只读请求

一般情况下,客户端的命令需要经过日志复制过程,当集群超过半数节点应答之后才可以提交给状态机执行,执行结束后再应答给客户端。这样的流程对于只读数据来说太漫长,如果不经过这样的流程Leader节点直接将本节点上保存的数据返回给客户端又是不安全的,因为可能返回的数据已经过期。因此Raft中针对只读请求有特殊的高效处理:

  1. Leader节点需要有当前已提交的日志数据。如果该节点新成为Leader需要提交一个dummy空日志来确保上一个任期的日志全部提交。
  2. Leader节点保存该只读请求到来时的committedIndex为readIndex
  3. Leader节点确认自己是否还是集群中的leader(广播心跳给集群中节点,收到半数以上应答即可判断)同时readIndex索引也是当前集群日志AppliedIndex的最大索引
  4. 读取Leader节点中数据

——————————————————————————————————————————————————————————————————
LastModify:2019-06-18

12345
Frances Hu

Frances Hu

48 Artikel
14 Tags
© 2021 Frances Hu
Erstellt mit Hexo
Theme - NexT.Muse