Linux性能优化方案

绪论

系统的性能是指操作系统完成任务的有效性、稳定性和响应速度。

CPU的使用率指的是CPU用于计算的时间占比,磁盘使用率指的是磁盘操作的时间占比,当CPU使用率100%时,意味着有部分请求来不及计算,响应时间增加或者超时;当磁盘使用率100%时,意味着有部分请求需要等待IO操作,响应时间也会增加或者超时。换言之,所有的操作都在理想的时间内,就不存在“性能优化“的问题。

我们在分析性能的时候,总是会首先要找到是什么引起响应时间变慢了,对应单机性能的分析,一般我们会将目光锁定在CPU和IO上,因为对于应用程序一般分为CPU bound型和IO bound型,即计算密集型或者读写密集型。

至于内存,其性能因素往往也会反映到CPU或者IO上,因为内存的设计初衷就是提高内核指令和应用程序的读写性能,当内存不足,系统可能进行大量的交换操作,这时候磁盘可能成为瓶颈。

而缺页、内存分配、释放、复制、内存地址空间映射等等问题又可能引起CPU的瓶颈;更严重的情况是直接影响功能,这个就不仅仅是性能的问题了。

Linux系统管理员可能经常会遇到系统不稳定、响应速度慢等问题,例如在linux上搭建了一个web服务,经常出现网页无法打开、打开速度慢等现象,而遇到这些问题,就有人会抱怨linux系统不好,其实这些都是表面现象。操作系统完成一个任务时,与系统自身设置、网络拓朴结构、路由设备、路由策略、接入设备、物理线路等多个方面都密切相关,任何一个环节出现问题,都会影响整个系统的性能。

因此当linux应用出现问题时,应当从应用程序、操作系统、服务器硬件、网络环境等方面综合排查,定位问题出现在哪个部分,然后集中解决。

在应用程序、操作系统、服务器硬件、网络环境等方面,影响性能最大的是应用程序和操作系统两个方面,因为这两个方面出现的问题不易察觉,隐蔽性很强。而硬件、网络方面只要出现问题,一般都能马上定位。

性能分析的方法论

问题分析方面,各类方法论如金字塔思维、5W2H、麦肯锡七步法等等。套用5W2H方法,可以提出性能分析的几个问题

  • What-现象的表现是什么样的
  • When-什么时候发生
  • Why-为什么会发生
  • Where-哪个地方发生的问题
  • How much-耗费了多少资源,问题解决后能减少多少资源耗用
  • How to do-怎么解决问题

但是这些只能给出方向,性能分析需要找到原因需要更具体的方法,怎么解决一个问题也需要更加具体的方式。

Linux运维人员

在做性能优化过程中,Linux运维人员承担着很重要的任务,首先,Linux运维人员要了解和掌握操作系统的当前运行状态,例如系统负载、内存状态、进程状态、CPU负荷等信息,这些信息是检测和判断系统性能的基础和依据;其次,Linux运维人员还有掌握系统的硬件信息,例如磁盘I/O、CPU型号、内存大小、网卡带宽等参数信息,然后根据这些信息综合评估系统资源的使用情况;第三,作为一名Linux运维人员,还要掌握应用程序对系统资源的使用情况,更深入的一点就是要了解应用程序的运行效率,例如是否有程序BUG、内存溢出等问题,通过对系统资源的监控,就能发现应用程序是否存在异常,如果确实是应用程序存在问题,需要把问题立刻反映给程序开发人员,进而改进或升级程序。

性能优化本身就是一个复杂和繁琐的过程,Linux运维人员只有了解了系统硬件信息、网络信息、操作系统配置信息和应用程序信息才能有针对性地的展开对服务器性能优化,这就要求Linux运维人员有充足的理论知识、丰富的实战经验以及缜密分析问题的头脑。

系统架构设计人员

系统性能优化涉及的第二类人员就是应用程序的架构设计人员。如果Linux运维人员在经过综合判断后,发现影响性能的是应用程序的执行效率,那么程序架构设计人员就要及时介入,深入了解程序运行状态。首先,系统架构设计人员要跟踪了解程序的执行效率,如果执行效率存在问题,要找出哪里出现了问题;其次,如果真的是架构设计出现了问题,那么就要马上优化或改进系统架构,设计更好的应用系统架构。

软件开发人员

系统性能优化最后一个环节涉及的是程序开发人员,在Linux运维人员或架构设计人员找到程序或结构瓶颈后,程序开发人员要马上介入进行相应的程序修改。修改程序要以程序的执行效率为基准,改进程序的逻辑,有针对性地进行代码优化。例如,Linux运维人员在系统中发现有条SQL语句耗费大量的系统资源,抓取这条执行的SQL语句,发现此SQL语句的执行效率太差,是开发人员编写的代码执行效率低造成的,这就需要把这个信息反馈给开发人员,开发人员在收到这个问题后,可以有针对性的进行SQL优化,进而实现程序代码的优化。

从上面这个过程可以看出,系统性能优化一般遵循的流程是:首先Linux运维人员查看系统的整体状况,主要从系统硬件、网络设备、操作系统配置、应用程序架构和程序代码五个方面进行综合判断,如果发现是系统硬件、网络设备或者操作系统配置问题,Linux运维人员可以根据情况自主解决;如果发现是程序结构问题,就需要提交给程序架构设计人员;如果发现是程序代码执行问题,就交给开发人员进行代码优化。这样就完成了一个系统性能优化的过程。

方法论

Brendan Gregg在《性能之巅:洞悉系统、企业与云计算》第二章中讲到大量的方法,比较突出的如Use方法、负载特征归纳、性能监控、静态性能调优、延时分析、工具法等等。其中工具法最具体,但是工具法也有自己的限制,如磁盘的饱和度,在磁盘使用率100%的时候,磁盘的负载可能还可以继续增加。在实际分析问题中,负载特征归纳更有指导意义,静态跟踪和动态跟踪让我们更容易更直观发现问题。

RED方法:监控服务的请求数(Rate)、错误数(Errors)、响应时间(Duration)。Weave Cloud在监控微服务性能时提出的思路。

USE方法:监控系统资源的使用率(Utilization)、饱和度(Saturation)、错误数(Errors)。

分析利用率、饱和、错误 (Utilization, Saturation and Errors (USE))和系统资源:

执行系统变更的步骤: 

每一种性能优化方法都有它适用或者不适用的应用场景。你应当根据你当前的项目现状灵活来选择用或者不用。

提高系统效率最好的办法是找出导致整体速度下降的瓶颈并解决掉,下面是找出系统关键瓶颈的一些基本技巧:

● 当大型应用程序,如OpenOffice和Firefox同时运行时,计算机可能会开始变慢,内存不足的出现几率更高。

● 如果启动时真的很慢,可能是应用程序初次启动需要较长的加载时间,一旦启动好后运行就正常了,否则很可能是硬盘太慢了。

● CPU负载持续很高,内存也够用,但CPU利用率很低,可以使用CPU负载分析工具监控负载时间。

性能整体排查步骤: 

  • top/htop/atop命令查看进程/线程、CPU、内存使用情况,CPU使用情况;
  • dstat 2查看CPU、磁盘IO、网络IO、换页、中断、切换,系统I/O状态;
  • vmstat 2查看内存使用情况,内存状态;
  • iostat -d -x 2查看所有磁盘的IO情况,系统I/O状态;
  • iotop查看IO靠前的进程,系统的I/O状态;
  • perf top查看占用CPU最多的函数,CPU使用情况;
  • perf record -ag -- sleep 15;perf report查看CPU事件占比,调用栈,CPU使用情况;
  • sar -n DEV 2查看网卡的吞吐,网卡状态;
  • /usr/share/bcc/tools/filetop -C查看每个文件的读写情况,系统的I/O状态;
  • /usr/share/bcc/tools/opensnoop显示正在被打开的文件,系统的I/O状态;
  • mpstat -P ALL 1 单核CPU是否被打爆;
  • ps aux --sort=-%cpu 按CPU使用率排序,找出CPU消耗最多进程;
  • ps -eo pid,comm,rss | awk '{m=$3/1e6;s["*"]+=m;s[$2]+=m} END{for (n in s) printf"%10.3f GB %s\n",s[n],n}' | sort -nr | head -20 统计前20内存占用
  • awk 'NF>3{s["*"]+=s[$1]=$3*$4/1e6} END{for (n in s) printf"%10.1f MB %s\n",s[n],n}' /proc/slabinfo | sort -nr | head -20 统计内核前20slab的占用;

进程分析,进程占用的资源:

  1. pidstat 2 -p 进程号查看可疑进程CPU使用率变化情况;
  2. pidstat -w -p 进程号 2查看可疑进程的上下文切换情况;
  3. pidstat -d -p 进程号 2查看可疑进程的IO情况;
  4. lsof -p 进程号查看进程打开的文件;
  5. strace -f -T -tt -p 进程号显示进程发起的系统调用;

协议栈分析,连接/协议栈状态:

  1. ethtool -S 查看网卡硬件情况;
  2. cat /proc/net/softnet_stat/ifconfig eth1 查看网卡驱动情况;
  3. netstat -nat|awk '{print awk $NF}'|sort|uniq -c|sort -n查看连接状态分布;
  4. ss -ntp或者netstat -ntp查看连接队列;
  5. netstat -s 查看协议栈情况;

性能分析工具:

一、CPU与系统进程优化

CPU本身的架构和内核调度器的架构:

  • 处理器
  • 硬件线程
  • CPU内存缓存
  • 时钟频率
  • 每指令周期数CPI和每周期指令数IPC
  • CPU指令
  • 使用率
  • 用户时间/内核时间
  • 调度器
  • 运行队列
  • 抢占
  • 多进程
  • 多线程
  • 字长

CPU是操作系统稳定运行的根本,CPU的速度与性能在很大程度上决定了系统整体的性能,因此,CPU数量越多、主频越高,服务器性能也就相对越好。但事实并非完全如此。

目前大部分CPU在同一时间内只能运行一个线程,超线程的处理器可以在同一时间运行多个线程,因此,可以利用处理器的超线程特性提高系统性能。在Linux系统下,只有运行SMP内核才能支持超线程,但是,安装的CPU数量越多,从超线程获得的性能方面的提高就越少。另外,Linux内核会把多核的处理器当作多个单独的CPU来识别,例如两个4核的CPU,在Lnux系统下会被当作8个单核CPU。但是从性能角度来讲,两个4核的CPU和8个单核的CPU并不完全等价,根据权威部门得出的测试结论,前者的整体性能要比后者低25%~30%。

可能出现CPU瓶颈的应用有db服务器、动态Web服务器等,对于这类应用,要把CPU的配置和性能放在主要位置。

在现代处理器中,与CPU执行代码或处理信息相比,向内存子系统保存信息或从中读取信息一般花费的时间更长。

通常,在CPU执行指令或处理数据前,它会消耗相当多的空闲时间来等待从内存中取出指令和数据,处理器用不同层次的高速缓存(cache)来弥补这种缓慢的内存性能。

CPU分析思路

首先,从 CPU 的角度来说,主要的性能指标就是 CPU 的使用率、上下文切换以及 CPU Cache 的命中率等。下面这张图就列出了常见的 CPU 性能指标。

针对应用程序,我们通常关注的是内核CPU调度器功能和性能。 

线程的状态分析主要是分析线程的时间用在什么地方,而线程状态的分类一般分为:

on-CPU:执行中,执行中的时间通常又分为用户态时间user和系统态时间sys

off-CPU:等待下一轮上CPU,或者等待I/O、锁、换页等等,其状态可以细分为可执行、匿名换页、睡眠、锁、空闲等状态

如果大量时间花在CPU上,对CPU的剖析能够迅速解释原因;如果系统时间大量处于off-cpu状态,定位问题就会费时很多。

分析方法与工具

在观察CPU性能的时候,按照负载特征归纳的方法,可以检查如下清单:

  • 整个系统范围内的CPU负载如何,CPU使用率如何,单个CPU的使用率呢?
  • CPU负载的并发程度如何?是单线程吗?有多少线程?
  • 哪个应用程序在使用CPU,使用了多少?
  • 哪个内核线程在使用CPU,使用了多少?
  • 中断的CPU用量有多少?
  • 用户空间和内核空间使用CPU的调用路径是什么样的?
  • 遇到了什么类型的停滞周期?

要回答上面的问题,使用系统性能分析工具最经济和直接,这里列举的工具足够回答上面的问题:

 

工具

描述

uptime

平均负载

vmstat

包括系统范围的CPU平均负载

top

监控每个进程/线程CPU用量

pidstat

每个进程/线程CPU用量分解

ps

进程状态

perf

CPU剖析和跟踪,性能计数器分析

上述问题中,调用路径和停滞周期的分析可以使用perf工具,也可以使用DTrace等更灵活的工具。其中perf支持对各类内核时间的跟踪计数统计,可以使用perf list查看。例如停滞周期分析可能十分复杂,需要对CPU和调度器架构有较系统的认识和了解,停滞的周期可能发生在一级、二级或者三级缓存,如缓存未命中,也可能是内存IO和资源IO上的停滞周期,perf中有诸如L1-dcahce-loads,L1-icache-loads等事件的计数统计。 

1. 禁用不必要的守护进程,节省内存和CPU资源

每台服务器上都运行着许多守护进程或服务,而具有讽刺意味的是,有很多通常不是必需的,这些服务没有发挥作用,但却消耗了宝贵的内存和CPU时间。此外,它们可能将服务器置于危险境地,多运行一个服务就等于多向黑客打开一扇长驱直入的门,因此,你应该将它们从服务器移除,禁用它们最大的好处是可以加快启动时间,释放内存。另外,你可以减少CPU需要处理的进程数,禁用它们的另一个好处是增强服务器的安全性,因为越少的守护进程意味着可被攻击和利用的漏洞越少。 下面是一些应该被禁用的Linux守护进程,默认情况下,它们都是自动运行的:

 序号  守护进程  描述
 1  Apmd  高级电源管理守护进程
 2 Nfslock 用于NFS文件锁定
 3 Isdn ISDN Moderm支持
 4 Autofs 在后台自动挂载文件系统(如自动挂载CD-ROM)
 5 Sendmail 邮件传输代理
 6 Xfs X Window的字体服务器

2. 关掉GUI

一般说来,Linux服务器是不需要GUI的,所有管理任务都可以在命令行下完成,因此最好关掉GUI,重定向X显示或通过一个Web浏览器界面显示。为了禁用GUI,“init level(启动级别)”应该被设置为3(命令行登录),而不是5(图形登录),如果需要GUI,可以随时运行startx进入图形用户界面。

3. 火焰图帮助分析CPU的调用路径

我们在压测mysql在某机型上的非原地更新性能时,分析mysql服务器延时情况时,分析了CPU上主要的函数调用。使用perf top能够看到调用次数的排名,但是调用关系不能展示出来。火焰图很清晰地提供了调用关系的视图(如下两图中的比例不同是因为perf top加了-p参数,火焰图分析是针对整个系统)。

二、内存优化

认识内存

如前所述,内存是为提高效率而生,实际分析问题的时候,内存出现问题可能不只是影响性能,而是影响服务或者引起其他问题。同样对于内存有些概念需要清楚:

  • 主存
  • 虚拟内存
  • 常驻内存
  • 地址空间
  • OOM
  • 页缓存
  • 缺页
  • 换页
  • 交换空间
  • 交换
  • 用户分配器libc、glibc、libmalloc和mtmalloc
  • LINUX内核级SLUB分配器

内存的大小也是影响Linux性能的一个重要的因素,内存太小,系统进程将被阻塞,应用也将变得缓慢,甚至失去响应;内存太大,导致资源浪费。Linux系统采用了物理内存和虚拟内存两种方式,虚拟内存虽然可以缓解物理内存的不足,但是占用过多的虚拟内存,应用程序的性能将明显下降,要保证应用程序的高性能运行,物理内存一定要足够大;但是过大的物理内存,会造成内存资源浪费,例如,在一个32位处理器的Linux操作系统上,超过8GB的物理内存都将被浪费。因此,要使用更大的内存,建议安装64位的操作系统,同时开启Linux的大内存内核支持。

由于处理器寻址范围的限制,在32位Linux操作系统上,应用程序单个进程最大只能使用4GB的内存,这样以来,即使系统有更大的内存,应用程序也无法“享”用,解决的办法就是使用64位处理器,安装64位操作系统。在64位操作系统下,可以满足所有应用程序对内存的使用需求 ,几乎没有限制。

可能出现内存性能瓶颈的应用有NOSQL服务器、数据库服务器、缓存服务器等,对于这类应用要把内存大小放在主要位置。

内存分析思路

从内存的角度来说,主要的性能指标,就是系统内存的分配和使用、进程内存的分配和使用以及 SWAP 的用量。下面这张图列出了常见的内存性能指标。

分析方法与工具

Brendan在书中给出了一些问题,比如内存总线的平衡性,NUMA系统中,内存是否被分配到合适的节点中去等等,这些问题在实际分析问题的时候,并不能作为切入点,需要持续的分析。因此笔者简化为如下清单:

  • 系统范围内的物理内存和虚拟内存使用率
  • 换页、交换、oom的情况
  • 内核和文件系统缓存的使用情况
  • 进程的内存用于何处
  • 进程为何分配内存
  • 内核为何分配内存
  • 哪些进程在持续地交换
  • 进程或者内存是否存在内存泄漏?

内存的分析工具如下:

 

工具

描述

free

缓存容量统计信息

vmstat

虚拟内存统计信息

top

监视每个进程的内存使用情况

ps

进程状态

Dtrace

分配跟踪

除了DTrace,所有的工具只能回答信息统计,进程的内存使用情况等等,至于是否发生内存泄漏等,只能通过分配跟踪。但是DTrace需要对内核函数有很深入的了解,通过D语言编写脚本完成跟踪。Perf也有一些诸如cache-miss、page-faults的事件用于跟踪,但是并不直观。 

1. 减少内存拷贝

假如你要发送一个文件给另外一台机器上,那么比较基础的做法是先调用 read 把文件读出来,再调用 send 把数据把数据发出去。这样数据需要频繁地在内核态内存和用户态内存之间拷贝,如图:

目前减少内存拷贝主要有两种方法,分别是使用 mmap 和 sendfile 两个系统调用。使用 mmap 系统调用的话,映射进来的这段地址空间的内存在用户态和内核态都是可以使用的。如果你发送数据是发的是 mmap 映射进来的数据,则内核直接就可以从地址空间中读取,这样就节约了一次从内核态到用户态的拷贝过程。

不过在 mmap 发送文件的方式里,系统调用的开销并没有减少,还是发生两次内核态和用户态的上下文切换。 如果你只是想把一个文件发送出去,而不关心它的内容,则可以调用另外一个做的更极致的系统调用 - sendfile。在这个系统调用里,彻底把读文件和发送文件给合并起来了,系统调用的开销又省了一次。再配合绝大多数网卡都支持的"分散-收集"(Scatter-gather)DMA 功能。可以直接从 PageCache 缓存区中 DMA 拷贝到网卡中,如图 9.8。这样绝大部分的 CPU 拷贝操作就都省去了。

2. 先打包,后写入

在内存中划分出固定大小的空间保存日志文件,这意味着笔记本电脑硬盘不用一直保持运转,只有当某个守护进程需要写入日志时才运转,注意ramlog使用的内存空间大小是固定的,否则系统内存会很快被用光,如果笔记本使用固态硬盘,可以分配50-80MB内存给ramlog使用,ramlog可以减少许多写入周期,极大地提高固态硬盘的使用寿命。

3. 内存泄漏

关于内存泄漏,从监控和顶层观察很难发现问题,一般都是从底层程序代码来分析,案例中使用各种观察工具和跟踪工具都不能很确定原因所在,只能通过分析代码来排查问题。最终发现是lua脚本语言分配内存速度快,包驱动的周期性服务的用法中,lua自动回收不能迅速释放内存,而是集中回收,如果频繁回收又可能带来CPU的压力。开发项目组最后采用的解决方式为分步回收,每次回收一部分内存,然后周期性全量回收。

4. 内存不足处理和“OOM killer(内存杀手)”

当脏页的数据太多,同时没有可用的页面时,内核试图回收内存来满足请求。如果不能及时回收足够的内存,就会出现内存不足OOM的情况。

对于系统的级别的OOM,默认情况下,系统将启动OOM killer,选择并杀死一个或多个进程以释放内存,以便满足请求。具体的记录日志是在/var/log/messages中,如果出现了Out of memory字样,说明系统曾经出现过OOM,

在Linux内核参数中,我们可以通过vm.panic_on_oom参数来设置遇到OOM的情况,启动OOM killer的策略

如果内核参数sysctl vm.panic_on_oom设置为1而不是0,内核将会发生panic,即直接摆烂,什么时候挂掉算什么时候。默认为0.即自动启动OOM killer

┌──[[email protected]]-[~]
└─$ sysctl vm.panic_on_oom
vm.panic_on_oom = 0

出现内存不足的情况,就没有很多合理的恢复选项。终止进程以释放内存、放弃并终止系统或死锁都是可能的选择。

为了确定OOM杀手应该杀死哪个进程,内核为每个进程保持一个运行不良评分,可以在/proc/pid/oom_score中查看。

systemd进程的值:

[root@ecs ~]# cat /proc/1/oom_score
0

分数越高,进程越有可能被OOM杀手杀死。许多因素被用来计算这个分数:

  • VM大小(不是RSS大小),
  • 进程所有子进程的累积VM大小,
  • nice值(正的nice值会给出更高的分数),
  • 总运行时间(较长的总运行时间会降低分数),
  • 运行用户(根进程会得到轻微的保护),
  • 进程执行直接硬件访问,分数也会降低。
  • 内核本身和PID1 (sysemd)是免疫的OOM杀手。

可调的/proc/PID/oom_adj可以用来手动调整oom_score。配置该pid进程被oom killer杀掉的权重,oom_adj可以的值从-17到15,其中0表示不改变(默认),越高的权重,意味着更可能被oom killer选中,-17表示免疫(永远不会杀死)。

[root@ecs ~]# cat /proc/1/oom_adj
0

如果你希望强制的执行OOM Killer:

可以echo f > /proc/sysrq-trigger,但请记住,至少会有一个进程被杀死。

[root@ecs ~]# echo f > /proc/sysrq-trigger

Message from syslogd@ecs-liruilong at Aug  1 14:32:18 ...
 kernel:[340648.118967] Kernel panic - not syncing: Out of memory: system-wide panic_on_oom is enabled

输出将被发送到dmesg:

[root@ecs ~]# cat /var/log/dmesg

三、磁盘IO优化

逻辑IO vs 物理IO

通常在讨论问题时,总是会分析IO的负载,IO的负载通常指的是磁盘IO,也就是物理IO,例如我们使用iostat获取的avgqu-sz、svctm和until等指标。因为我们的读写最终都是来自或者去往磁盘的,关注磁盘的IO情况非常正确。但是我们在进行读写操作的时候,面向的对象大多数时候并不会直接面向磁盘,而是面向文件系统的,除非使用raw io的方式。

如下图为通用的IO结构图,如果你想了解更详细,可以查看第二张图片。我们知道LINUX通过文件系统将所有的硬件设备甚至网络都抽象为文件来管理,例如read()调用时,实际就是就是调用了vfs_read函数,文件系统会确认请求的数据是否在页缓存中,如果不在内存中,于是将请求发送到块设备;此时内核会先获取到数据在物理设备上的实际位置,然后将读请求发送给块设备的请求队列中,IO调度器会通过一定的调度算法,将请求发送给磁盘设备驱动层,执行真正的读操作。在这一过程中可能发生哪些情况呢?如果应用程序执行的是大量的顺序读会怎样?随机读又会怎样?如果是顺序读,正确的做法就是进行预读,让请求的数据落到内存中,提升读效率。所以在应用程序发起一次读,从文件系统到磁盘的过程中,存在读放大的问题。

在写操作时同样存在类似的情况,应用程序发起对文件系统的IO操作,物理IO与应用程序之间,有时候会显得无关、间接、放大或者缩小。

无关:

  • 其他的应用程序:磁盘IO来自其他的应用程序,如监控,agent等
  • 其他用户:如同虚拟机母机下的其他用户
  • 其他内核任务:如重建raid,校验等

间接:

  • 文件系统预读:增加额外的IO,但是可能预读的数据无用
  • 文件系统缓冲:写缓存推迟或者合并回写磁盘,造成磁盘瞬时IO压力

放大:

  • 文件系统元数据:增大额外的IO
  • 文件系统记录尺寸:向上对齐等增加了IO大小

缩小:

  • 文件系统缓存:直接读取缓存,而不需要操作磁盘
  • 合并:一次性回写磁盘
  • 文件系统抵消:同一地址更新多次,回写磁盘时只保留最后一次修改
  • 压缩:减少数据量

 

磁盘的I/O性能直接影响应用程序的性能,在一个有频繁读写的应用中,如果磁盘I/O性能得不到满足,就会导致应用停滞。好在现今的磁盘都采用了很多方法来提高I/O性能,比如常见的磁盘RAID技术。

通过RAID技术组成的磁盘组,就相当于一个大硬盘,用户可以对它进行分区格式化、建立文件系统等操作,跟单个物理硬盘一模一样,唯一不同的是RAID磁盘组的I/O性能比单个硬盘要高很多,同时在数据的安全性也有很大提升。

根据磁盘组合方式的不同,RAID可以分为RAID0,RAID1、RAID2、RAID3、RAID4、RAID5、RAID6、RAID7、RAID0+1、RAID10等级别,常用的RAID级别有RAID0、RAID1、RAID5、RAID0+1,这里进行简单介绍。

RAID 0:通过把多块硬盘粘合成一个容量更大的硬盘组,提高了磁盘的性能和吞吐量。这种方式成本低,要求至少两个磁盘,但是没有容错和数据修复功能,因而只能用在对数据安全性要求不高的环境中。

RAID 1:也就是磁盘镜像,通过把一个磁盘的数据镜像到另一个磁盘上,最大限度地保证磁盘数据的可靠性和可修复性,具有很高的数据冗余能力,但磁盘利用率只有50%,因而,成本最高,多用在保存重要数据的场合。

RAID5:采用了磁盘分段加奇偶校验技术,从而提高了系统可靠性,RAID5读出效率很高,写入效率一般,至少需要3块盘。允许一块磁盘故障,而不影响数据的可用性。

RAID0+1:把RAID0和RAID1技术结合起来就成了RAID0+1,至少需要4个硬盘。此种方式的数据除分布在多个盘上外,每个盘都有其镜像盘,提供全冗余能力,同时允许一个磁盘故障,而不影响数据可用性,并具有快速读/写能力。

通过了解各个RAID级别的性能,可以根据应用的不同特性,选择适合自身的RAID级别,从而保证应用程序在磁盘方面达到最优性能。

IO分析思路

从文件系统和磁盘 I/O 的角度来说,主要性能指标,就是文件系统的使用、缓存和缓冲区的使用,以及磁盘 I/O 的使用率、吞吐量和延迟等。下面这张图列出了常见的 I/O 性能指标。

1. 为磁盘I/O调整Linux内核电梯算法

在选择文件系统后,有一些内核和挂载选项可能会影响到它的性能表现,其中一个内核设置是电梯算法,通过调整电梯算法,系统可以平衡低延迟需求,收集足够的数据,以有效地组织对磁盘的读和写请求。

2. 选择正确的文件系统

使用ext4文件系统取代ext3 ● Ext4是ext3文件系统的增强版,扩展了存储限制 ●它具有日志功能,保证高水平的数据完整性(在非正常关闭事件中) ●在非正常关闭和重启时,它不需要检查磁盘(这是一个非常耗时的动作) ●更快的写入速度,ext4日志优化了硬盘磁头动作

3. 使用noatime文件系统挂载选项

在文件系统启动配置文件fstab中使用noatime选项,如果使用了外部存储,这个挂载选项可以有效改善性能。

4. 脏页写入调优

对于每个进程视图,你可以使用/proc/pid/maps.这个文件详细说明了分配给进程的每个内存段,包括共享/私有干净内存和脏内存的大小。为了解析它,我们可以编写一个小的awk程序,如下所示:

cat /proc/$$/smaps | awk  '/Shared_Clean/{SHCL+=$2}/Shared_Dirty/{SHDT+=$2}/Private_Clean/{PRCL+=$2}/Private_Dirty/{PRDT+=$2}  END{ 
  print "Total Clean:",SHCL +PRCL  
  print "Total Dirty:",SHDT  +PRDT
}'
┌──[[email protected]]-[/dev]
└─$ cat /proc/$$/smaps | awk  '
> /Shared_Clean/{SHCL+=$2}
> /Shared_Dirty/{SHDT+=$2}
> /Private_Clean/{PRCL+=$2}
> /Private_Dirty/{PRDT+=$2}
> END{
>   print "Total Clean:",SHCL +PRCL
>   print "Total Dirty:",SHDT  +PRDT
> }'
Total Clean: 1808
Total Dirty: 1536

必须将脏页写入磁盘,以防止内存被无法释放的页填满。由于内存是易失的(断电时内容会丢失),脏页也需要写入磁盘,以防止断电时数据丢失。

在内核中,将脏页写入磁盘是由per-BDI flush线程处理的,这些线程将在必要时创建。Per-BDI flush线程将在进程列表中显示为flush-MAJOR: MINOR。

有几个内核参数控制per-BDI刷新线程何时开始将数据写入磁盘。这样内核就不会因为某个进程修改了另一个字节的内存而连续地多次写入同一个页面。

┌──[[email protected]]-[~]
└─$ sysctl  -a | grep dirty_
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 1500
┌──[[email protected]]-[~]
└─$

vm.dirty_ratio = 20 产生写操作的进程整个系统内存中处于脏状态的百分比,是绝对的脏数据限制,内存里的脏数据百分比不能超过这个值,如果超过将阻塞IO直到写出脏页

vm.dirty_background_ratio = 10 系统总内存和脏页的百分比,即内存可以填充“脏数据”的百分比。在这个百分比事开始,内核会在后台开始写数据

vm.dirty_bytes = 0vm.dirty_background_bytes = 0 :这两个参数为上面参数的byte单位时的值

vm.dirty_expire_centisecs = 3000:脏数据在符合写入磁盘条件之前必须有多旧(以1/100秒为单位),即可以存活多久。这样内核就不会因为某个进程修改了一个字节的内存而连续地多次写入同一个页面。

vm.dirty_writeback_centisecs = 1500:内核唤醒刷新线程pdflush/flush/kdmflush以写入数据的频率(1/100秒)。设置为0将完全禁周期性回写

大多数调优配置文件至少修改上述设置之一。调优规则可以遵循下面的策略

  • 设置较低的比率将导致更频繁但更短的写操作,这适合交互式系统
  • 设置较高的比率将导致更少但更大的写操作,导致总体开销更小,但可能导致交互式应用程序响应时间更长.适合执行非交互的I/O任务处理,比如大文件生成之类。

四、网络优化

Linux下的各种应用,一般都是基于网络的,因此网络带宽也是影响性能的一个重要因素,低速的、不稳定的网络将导致网络应用程序的访问阻塞,而稳定、高速的网络带宽,可以保证应用程序在网络上畅通无阻地运行。幸运的是,现在的网络一般都是千兆带宽或光纤网络,带宽问题对应用程序性能造成的影响也在逐步降低。

确定网络优化目标

​网络性能优化的目标是什么?换句话说,观察到的网络性能指标,要达到多少才合适呢?

实际上,虽然网络性能优化的整体目标,是降低网络延迟(如 RTT)和提高吞吐量(如 BPS 和 PPS),但具体到不同应用中,每个指标的优化标准可能会不同,优先级顺序也大相径庭。

NAT 网关通常需要达到或接近线性转发,也就是说, PPS 是最主要的性能目标。

再如,对于数据库、缓存等系统,快速完成网络收发,即低延迟,是主要的性能目标。

对于我们经常访问的 Web 服务来说,则需要同时兼顾吞吐量和延迟。

所以,为了更客观合理地评估优化效果,首先应该明确优化的标准,即要对系统和应用程序进行基准测试,得到网络协议栈各层的基准性能。

Linux 网络协议栈,是我们需要掌握的核心原理。它是基于 TCP/IP 协议族的分层结构

在进行基准测试时,可以按照协议栈的每一层来测试。由于底层是其上方各层的基础,底层性能也就决定了高层性能。

首先是网络接口层和网络层,它们主要负责网络包的封装、寻址、路由,以及发送和接收。每秒可处理的网络包数 PPS,就是它们最重要的性能指标(特别是在小包的情况下)。你可以用内核自带的发包工具 pktgen ,来测试 PPS 的性能。

再向上到传输层的 TCP 和 UDP,它们主要负责网络传输。对它们而言,吞吐量(BPS)、连接数以及延迟,就是最重要的性能指标。你可以用 iperf 或 netperf ,来测试传输层的性能。

不过要注意,网络包的大小,会直接影响这些指标的值。所以,通常,你需要测试一系列不同大小网络包的性能。

最后,再往上到了应用层,最需要关注的是吞吐量(BPS)、每秒请求数以及延迟等指标。你可以用 wrk、ab 等工具,来测试应用程序的性能。

网络分析思路

从网络的角度来说,主要性能指标就是吞吐量、响应时间、连接数、丢包数等。根据 TCP/IP 网络协议栈的原理,我们可以把这些性能指标,进一步细化为每层协议的具体指标。这里我同样用一张图,分别从链路层、网络层、传输层和应用层,列出了各层的主要指标。

  • 要优化网络性能,肯定离不开 Linux 系统的网络协议栈和网络收发流程的辅助。

应用程序

应用程序,通常通过套接字接口进行网络操作。由于网络收发通常比较耗时,所以应用程序的优化,主要就是对网络 I/O 和进程自身的工作模型的优化。

从网络 I/O 的角度来说,主要有下面两种优化思路。

第一种是最常用的 I/O 多路复用技术 epoll,主要用来取代 select 和 poll。这其实是解决 C10K 问题的关键,也是目前很多网络应用默认使用的机制。

第二种是使用异步 I/O(Asynchronous I/O,AIO)。AIO 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。等到 I/O 完成后,系统会用事件通知的方式,告诉应用程序结果。不过,AIO 的使用比较复杂,你需要小心处理很多边缘情况。

从进程的工作模型来说,也有两种不同的模型用来优化。

第一种,主进程 + 多个 worker 子进程。其中,主进程负责管理网络连接,而子进程负责实际的业务处理。这也是最常用的一种模型。

第二种,监听到相同端口的多进程模型。在这种模型下,所有进程都会监听相同接口,并且开启 SO_REUSEPORT 选项,由内核负责,把请求负载均衡到这些监听进程中去。

除了网络 I/O 和进程的工作模型外,应用层的网络协议优化,也是至关重要的一点。常见的几种优化方法。

使用长连接取代短连接,可以显著降低 TCP 建立连接的成本。在每秒请求次数较多时,这样做的效果非常明显。

使用内存等方式,来缓存不常变化的数据,可以降低网络 I/O 次数,同时加快应用程序的响应速度。

使用 Protocol Buffer 等序列化的方式,压缩网络 I/O 的数据量,可以提高应用程序的吞吐。

使用 DNS 缓存、预取、HTTPDNS 等方式,减少 DNS 解析的延迟,也可以提升网络 I/O 的整体速度。

套接字

套接字可以屏蔽掉 Linux 内核中不同协议的差异,为应用程序提供统一的访问接口。每个套接字,都有一个读写缓冲区。

读缓冲区,缓存了远端发过来的数据。如果读缓冲区已满,就不能再接收新的数据。

写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会被阻塞。

所以,为了提高网络的吞吐量,通常需要调整这些缓冲区的大小。比如:

增大每个套接字的缓冲区大小 net.core.optmem_max;

增大套接字接收缓冲区大小 net.core.rmem_max 和发送缓冲区大小 net.core.wmem_max;

增大 TCP 接收缓冲区大小 net.ipv4.tcp_rmem 和发送缓冲区大小 net.ipv4.tcp_wmem。

有几点需要注意:

tcp_rmem 和 tcp_wmem 的三个数值分别是 min,default,max,系统会根据这些设置,自动调整 TCP 接收 / 发送缓冲区的大小。

udp_mem 的三个数值分别是 min,pressure,max,系统会根据这些设置,自动调整 UDP 发送缓冲区的大小。

套接字接口还提供了一些配置选项,用来修改网络连接的行为:

为 TCP 连接设置 TCP_NODELAY 后,就可以禁用 Nagle 算法;

为 TCP 连接开启 TCP_CORK 后,可以让小包聚合成大包后再发送(注意会阻塞小包的发送);

使用 SO_SNDBUF 和 SO_RCVBUF ,可以分别调整套接字发送缓冲区和接收缓冲区的大小。

传输层 

传输层最重要的是 TCP 和 UDP 协议,所以这儿的优化,其实主要就是对这两种协议的优化。

TCP 协议的优化

TCP 提供了面向连接的可靠传输服务。要优化 TCP,我们首先要掌握 TCP 协议的基本原理,比如流量控制、慢启动、拥塞避免、延迟确认以及状态流图(如下图所示)等。

 

UDP的优化

UDP 提供了面向数据报的网络协议,它不需要网络连接,也不提供可靠性保障。所以,UDP 优化,相对于 TCP 来说,要简单得多。这里总结了常见的几种优化方案。

跟套接字部分提到的一样,增大套接字缓冲区大小以及 UDP 缓冲区范围;

跟 TCP 部分提到的一样,增大本地端口号的范围;

根据 MTU 大小,调整 UDP 数据包的大小,减少或者避免分片的发生。

网络层

网络层,负责网络包的封装、寻址和路由,包括 IP、ICMP 等常见协议。在网络层,最主要的优化,其实就是对路由、 IP 分片以及 ICMP 等进行调优。

第一种,从路由和转发的角度出发,你可以调整下面的内核选项。

在需要转发的服务器中,比如用作 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 问题。

第二种,从分片的角度出发,最主要的是调整 MTU(Maximum Transmission Unit)的大小。

第三种,从 ICMP 的角度出发,为了避免 ICMP 主机探测、ICMP Flood 等各种网络问题,你可以通过内核选项,来限制 ICMP 的行为。

比如,你可以禁止 ICMP 协议,即设置 net.ipv4.icmp_echo_ignore_all = 1。这样,外部主机就无法通过 ICMP 来探测主机。

或者,你还可以禁止广播 ICMP,即设置 net.ipv4.icmp_echo_ignore_broadcasts = 1。

链路层

链路层负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。自然,链路层的优化,也是围绕这些基本功能进行的。接下来,从不同的几个方面分别来看。

由于网卡收包后调用的中断处理程序(特别是软中断),需要消耗大量的 CPU。所以,将这些中断处理程序调度到不同的 CPU 上执行,就可以显著提高网络吞吐量。这通常可以采用下面两种方法。

可以为网卡硬中断配置 CPU 亲和性(smp_affinity),或者开启 irqbalance 服务。

可以开启 RPS(Receive Packet Steering)和 RFS(Receive Flow Steering),将应用程序和软中断的处理,调度到相同 CPU 上,这样就可以增加 CPU 缓存命中率,减少网络延迟。

另外,现在的网卡都有很丰富的功能,原来在内核中通过软件处理的功能,可以卸载到网卡中,通过硬件来执行。

TSO(TCP Segmentation Offload)和 UFO(UDP Fragmentation Offload):在 TCP/UDP 协议中直接发送大包;而 TCP 包的分段(按照 MSS 分段)和 UDP 的分片(按照 MTU 分片)功能,由网卡来完成 。

GSO(Generic Segmentation Offload):在网卡不支持 TSO/UFO 时,将 TCP/UDP 包的分段,延迟到进入网卡前再执行。这样,不仅可以减少 CPU 的消耗,还可以在发生丢包时只重传分段后的包。

LRO(Large Receive Offload):在接收 TCP 分段包时,由网卡将其组装合并后,再交给上层网络处理。不过要注意,在需要 IP 转发的情况下,不能开启 LRO,因为如果多个包的头部信息不一致,LRO 合并会导致网络包的校验错误。

GRO(Generic Receive Offload):GRO 修复了 LRO 的缺陷,并且更为通用,同时支持 TCP 和 UDP。

RSS(Receive Side Scaling):也称为多队列接收,它基于硬件的多个接收队列,来分配网络接收进程,这样可以让多个 CPU 来处理接收到的网络包。

VXLAN 卸载:也就是让网卡来完成 VXLAN 的组包功能。

最后,对于网络接口本身,也有很多方法,可以优化网络的吞吐量。

可以开启网络接口的多队列功能。这样,每个队列就可以用不同的中断号,调度到不同 CPU 上执行,从而提升网络的吞吐量。

可以增大网络接口的缓冲区大小,以及队列长度等,提升网络传输的吞吐量(注意,这可能导致延迟增大)。

可以使用 Traffic Control 工具,为不同网络流量配置 QoS。

1. 尽量减少不必要的网络 IO

我要给出的第一个建议就是不必要用网络 IO 的尽量不用。

是的,网络在现代的互联网世界里承载了很重要的角色。用户通过网络请求线上服务、服务器通过网络读取数据库中数据,通过网络构建能力无比强大分布式系统。网络很好,能降低模块的开发难度,也能用它搭建出更强大的系统。但是这不是你滥用它的理由!

原因是即使是本机网络 IO 开销仍然是很大的。先说发送一个网络包,首先得从用户态切换到内核态,花费一次系统调用的开销。进入到内核以后,又得经过冗长的协议栈,这会花费不少的 CPU 周期,最后进入环回设备的“驱动程序”。接收端呢,软中断花费不少的 CPU 周期又得经过接收协议栈的处理,最后唤醒或者通知用户进程来处理。当服务端处理完以后,还得把结果再发过来。又得来这么一遍,最后你的进程才能收到结果。你说麻烦不麻烦。另外还有个问题就是多个进程协作来完成一项工作就必然会引入更多的进程上下文切换开销,这些开销从开发视角来看,做的其实都是无用功。

上面我们还分析的只是本机网络 IO,如果是跨机器的还得会有双方网卡的 DMA 拷贝过程,以及两端之间的网络 RTT 耗时延迟。所以,网络虽好,但也不能随意滥用!

2. 尽量合并网络请求

在可能的情况下,尽可能地把多次的网络请求合并到一次,这样既节约了双端的 CPU 开销,也能降低多次 RTT 导致的耗时。

我们举个实践中的例子可能更好理解。假如有一个 redis,里面存了每一个 App 的信息(应用名、包名、版本、截图等等)。你现在需要根据用户安装应用列表来查询数据库中有哪些应用比用户的版本更新,如果有则提醒用户更新。

那么最好不要写出如下的代码:

<?php 
for(安装列表 as 包名){
  redis->get(包名)
  ...
}

上面这段代码功能上实现上没问题,问题在于性能。据我们统计现代用户平均安装 App 的数量在 60 个左右。那这段代码在运行的时候,每当用户来请求一次,你的服务器就需要和 redis 进行 60 次网络请求。 总耗时最少是 60 个 RTT 起。更好的方法是应该使用 redis 中提供的批量获取命令,如 hmget、pipeline等,经过一次网络 IO 就获取到所有想要的数据,如图:

3. 调用者与被调用机器尽可能部署的近一些

在前面的章节中我们看到在握手一切正常的情况下, TCP 握手的时间基本取决于两台机器之间的 RTT 耗时。虽然我们没办法彻底去掉这个耗时,但是我们却有办法把 RTT 降低,那就是把客户端和服务器放的足够地近一些。尽量把每个机房内部的数据请求都在本地机房解决,减少跨地网络传输。

举例,假如你的服务是部署在北京机房的,你调用的 mysql、redis最好都位于北京机房内部。尽量不要跨过千里万里跑到广东机房去请求数据,即使你有专线,耗时也会大大增加!在机房内部的服务器之间的 RTT 延迟大概只有零点几毫秒,同地区的不同机房之间大约是 1 ms 多一些。但如果从北京跨到广东的话,延迟将是 30 - 40 ms 左右,几十倍的上涨!

4. 内网调用不要用外网域名

假如说你所在负责的服务需要调用兄弟部门的一个搜索接口,假设接口是:"http://www.sogou.com/wq?key=开发内功修炼"。

那既然是兄弟部门,那很可能这个接口和你的服务是部署在一个机房的。即使没有部署在一个机房,一般也是有专线可达的。所以不要直接请求 http://www.sogou.com, 而是应该使用该服务在公司对应的内网域名。在我们公司内部,每一个外网服务都会配置一个对应的内网域名,我相信你们公司也有。

为什么要这么做,原因有以下几点

1)外网接口慢。本来内网可能过个交换机就能达到兄弟部门的机器,非得上外网兜一圈再回来,时间上肯定会慢。

2)带宽成本高。在互联网服务里,除了机器以外,另外一块很大的成本就是 IDC 机房的出入口带宽成本。 两台机器在内网不管如何通信都不涉及到带宽的计算。但是一旦你去外网兜了一圈回来,行了,一进一出全部要缴带宽费,你说亏不亏!!

3)NAT 单点瓶颈。一般的服务器都没有外网 IP,所以要想请求外网的资源,必须要经过 NAT 服务器。但是一个公司的机房里几千台服务器中,承担 NAT 角色的可能就那么几台。它很容易成为瓶颈。我们的业务就遇到过好几次 NAT 故障导致外网请求失败的情形。 NAT 机器挂了,你的服务可能也就挂了,故障率大大增加。

5. 调整网卡 RingBuffer 大小

在 Linux 的整个网络栈中,RingBuffer 起到一个任务的收发中转站的角色。对于接收过程来讲,网卡负责往 RingBuffer 中写入收到的数据帧,ksoftirqd 内核线程负责从中取走处理。只要 ksoftirqd 线程工作的足够快,RingBuffer 这个中转站就不会出现问题。

但是我们设想一下,假如某一时刻,瞬间来了特别多的包,而 ksoftirqd 处理不过来了,会发生什么?这时 RingBuffer 可能瞬间就被填满了,后面再来的包网卡直接就会丢弃,不做任何处理!

 通过 ethtool 就可以加大 RingBuffer 这个“中转仓库”的大小。

# ethtool -G eth1 rx 4096 tx 4096

这样网卡会被分配更大一点的”中转站“,可以解决偶发的瞬时的丢包。不过这种方法有个小副作用,那就是排队的包过多会增加处理网络包的延时。所以应该让内核处理网络包的速度更快一些更好,而不是让网络包傻傻地在 RingBuffer 中排队。我们后面会再介绍到 RSS ,它可以让更多的核来参与网络包接收。

6. 使用 eBPF 绕开协议栈的本机 IO

如果你的业务中涉及到大量的本机网络 IO 可以考虑这个优化方案。本机网络 IO 和跨机 IO 比较起来,确实是节约了驱动上的一些开销。发送数据不需要进 RingBuffer 的驱动队列,直接把 skb 传给接收协议栈(经过软中断)。但是在内核其它组件上,可是一点都没少,系统调用、协议栈(传输层、网络层等)、设备子系统整个走 了一个遍。连“驱动”程序都走了(虽然对于回环设备来说这个驱动只是一个纯软件的虚拟出来的东东)。

如果想用本机网络 IO,但是又不想频繁地在协议栈中绕来绕去。那么你可以试试 eBPF。使用 eBPF 的 sockmap 和 sk redirect 可以绕过 TCP/IP 协议栈,而被直接发送给接收端的 socket,业界已经有公司在这么做了。

7. 尽量少用 recvfrom 等进程阻塞的方式

在使用了 recvfrom 阻塞方式来接收 socket 上数据的时候。每次一个进程专门为了等一个 socket 上的数据就得被从 CPU 上拿下来。然后再换上另一个进程。等到数据 ready 了,睡眠的进程又会被唤醒。总共两次进程上下文切换开销。如果我们服务器上需要有大量的用户请求需要处理,那就需要有很多的进程存在,而且不停地切换来切换去。这样的缺点有如下这么几个:

  • 因为每个进程只能同时等待一条连接,所以需要大量的进程。
  • 进程之间互相切换的时候需要消耗很多 CPU 周期,一次切换大约是 3 - 5 us 左右。
  • 频繁的切换导致 L1、L2、L3 等高速缓存的效果大打折扣

大家可能以为这种网络 IO 模型很少见了。但其实在很多传统的客户端 SDK 中,比如 mysql、redis 和 kafka 仍然是沿用了这种方式。

8. 使用成熟的网络库

使用 epoll 可以高效地管理海量的 socket。在服务器端。我们有各种成熟的网络库进行使用。这些网络库都对 epoll 使用了不同程度的封装。

首先第一个要给大家参考的是 Redis。老版本的 Redis 里单进程高效地使用 epoll 就能支持每秒数万 QPS 的高性能。如果你的服务是单进程的,可以参考 Redis 在网络 IO 这块的源码。

如果是多线程的,线程之间的分工有很多种模式。那么哪个线程负责等待读 IO 事件,那个线程负责处理用户请求,哪个线程又负责给用户写返回。根据分工的不同,又衍生出单 Reactor、多 Reactor、以及 Proactor 等多种模式。大家也不必头疼,只要理解了这些原理之后选择一个性能不错的网络库就可以了。比如 PHP 中的 Swoole、Golang 的 net 包、Java 中的 netty 、C++ 中的 Sogou Workflow 都封装的非常的不错。

9. 使用 Kernel-ByPass 新技术

如果你的服务对网络要求确实特别特特别的高,而且各种优化措施也都用过了,那么现在还有终极优化大招 -- Kernel-ByPass 技术。

内核在接收网络包的时候要经过很⻓的收发路径。在这期间牵涉到很多内核组件之间的协同、协议栈的处理、以及内核态和用户态的拷贝和切换。 Kernel-ByPass 这类的技术方案就是绕开内核协议栈,自己在用户态来实现网络包的收发。这样不但避开了繁杂的内核协议栈处理,也减少了频繁了内核态用户态之间的拷贝和切换,性能将发挥到极致!

目前我所知道的方案有 SOLARFLARE 的软硬件方案、DPDK 等等。如果大家感兴趣,可以多去了解一下!

10. 配置充足的端口范围

客户端在调用 connect 系统调用发起连接的时候,需要先选择一个可用的端口。内核在选用端口的时候,是采用从可用端口范围中某一个随机位置开始遍历的方式。如果端口不充足的话,内核可能需要循环撞很多次才能选上一个可用的。这也会导致花费更多的 CPU 周期在内部的哈希表查找以及可能的自旋锁等待上。

因此不要等到端口用尽报错了才开始加大端口范围,而且应该一开始的时候就保持一个比较充足的值:

# vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 5000 65000
# sysctl -p  //使配置生效

如果端口加大了仍然不够用,那么可以考虑开启端口 reuse 和 recycle。这样端口在连接断开的时候就不需要等待 2MSL 的时间了,可以快速回收。

开启这个参数之前需要保证 tcp_timestamps 是开启的:

# vi /etc/sysctl.conf
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tw_recycle = 1
# sysctl -p

11. 小心连接队列溢出

服务器端使用了两个连接队列来响应来自客户端的握手请求。这两个队列的长度是在服务器 listen 的时候就确定好了的。如果发生溢出,很可能会丢包。所以如果你的业务使用的是短连接且流量比较大,那么一定得学会观察这两个队列是否存在溢出的情况。因为一旦出现因为连接队列导致的握手问题,那么 TCP 连接耗时都是秒级以上了。

对于半连接队列, 有个简单的办法。那就是只要保证 tcp_syncookies 这个内核参数是 1 就能保证不会有因为半连接队列满而发生的丢包。

对于全连接队列来说,可以通过 netstat -s 来观察。netstat -s 可查看到当前系统全连接队列满导致的丢包统计。但该数字记录的是总丢包数,所以你需要再借助 watch 命令动态监控。

# watch 'netstat -s | grep overflowed' 
160 times the listen queue of a socket overflowed //全连接队列满导致的丢包

如果输出的数字在你监控的过程中变了,那说明当前服务器有因为全连接队列满而产生的丢包。你就需要加大你的全连接队列的⻓度了。全连接队列是应用程序调用 listen时传入的 backlog 以及内核参数 net.core.somaxconn 二者之中较小的那个。如果需要加大,可能两个参数都需要改。

如果你手头并没有服务器的权限,只是发现自己的客户端机连接某个 server 出现耗时长,想定位一下是否是因为握手队列的问题。那也有间接的办法,可以 tcpdump 抓包查看是否有 SYN 的 TCP Retransmission。如果有偶发的 TCP Retransmission, 那就说明对应的服务端连接队列可能有问题了。

12. 减少握手重试

如果握手发生异常,客户端或者服务端就会启动超时重传机制。这个超时重试的时间间隔是翻倍地增长的,1 秒、3 秒、7 秒、15 秒、31 秒、63 秒 ......。对于我们提供给用户直接访问的接口来说,重试第一次耗时 1 秒多已经是严重影响用户体验了。如果重试到第三次以后,很有可能某一个环节已经报错返回 504 了。所以在这种应用场景下,维护这么多的超时次数其实没有任何意义。倒不如把他们设置的小一些,尽早放弃。 其中客户端的 syn 重传次数由 tcp_syn_retries 控制,服务器半连接队列中的超时次数是由 tcp_synack_retries 来控制。把它们两个调成你想要的值。

13. 如果请求频繁,请弃用短连接改用长连接

如果你的服务器频繁请求某个 server,比如 redis 缓存。和建议 1 比起来,一个更好一点的方法是使用长连接。这样的好处有

1)节约了握手开销。短连接中每次请求都需要服务和缓存之间进行握手,这样每次都得让用户多等一个握手的时间开销。

2)规避了队列满的问题。前面我们看到当全连接或者半连接队列溢出的时候,服务器直接丢包。而客户端呢并不知情,所以傻傻地等 3 秒才会重试。要知道 tcp 本身并不是专门为互联网服务设计的。这个 3 秒的超时对于互联网用户的体验影响是致命的。

3)端口数不容易出问题。端连接中,在释放连接的时候,客户端使用的端口需要进入 TIME_WAIT 状态,等待 2 MSL的时间才能释放。所以如果连接频繁,端口数量很容易不够用。而长连接就固定使用那么几十上百个端口就够用了。

14. TIME_WAIT 的优化

很多线上服务如果使用了短连接的情况下,就会出现大量的 TIME_WAIT。

首先,我想说的是没有必要见到两三万个 TIME_WAIT 就恐慌的不行。从内存的⻆度来考虑,一条 TIME_WAIT 状态的连接仅仅是 0.5 KB 的内存而已。从端口占用的角度来说,确实是消耗掉了一个端口。但假如你下次再连接的是不同的 Server 的话,该端口仍然可以使用。只有在所有 TIME_WAIT 都聚集在和一个 Server 的连接上的时候才会有问题。

那怎么解决呢? 其实办法有很多。第一个办法是按上面建议开启端口 reuse 和 recycle。 第二个办法是限制 TIME_WAIT 状态的连接的最大数量。

# vi /etc/sysctl.conf
net.ipv4.tcp_max_tw_buckets = 32768
# sysctl -p

如果再彻底一些,也可以干脆直接用⻓连接代替频繁的短连接。连接频率大大降低以后,自然也就没有 TIME_WAIT 的问题了。

15. 改善Linux Exim服务器性能

有许多种方法改善服务器的Exim性能,其中一个办法是使用DNS缓存守护进程,它可以降低解析DNS记录需要的带宽和CPU时间,DNS缓存通过消除每次都从根节点开始查找DNS记录的需求,从而改善网络性能,Djbdns是一个非常强大的DNS服务器,它具有DNS缓存功能,Djbdns比BIND DNS服务器更安全,性能更好,可以直接通过http://cr.yp.to/下载,或通过Red Hat提供的软件包获得。

16. 优化TCP

优化TCP协议有助于提高网络吞吐量,跨广域网的通信使用的带宽越大,延迟时间越长时,建议使用越大的TCP Linux大小,以提高数据传输速率,TCP Linux大小决定了发送主机在没有收到数据传输确认时,可以向接收主机发送多少数据。

网络优化总结

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

实际上,我们分析和定位网络瓶颈,也是基于这些网络层进行的。而定位出网络性能瓶颈后,我们就可以根据瓶颈所在的协议层,进行优化。具体而言:

在应用程序中,主要是优化 I/O 模型、工作模型以及应用层的网络协议;

在套接字层中,主要是优化套接字的缓冲区大小;

在传输层中,主要是优化 TCP 和 UDP 协议;

在网络层中,主要是优化路由、转发、分片以及 ICMP 协议;

最后,在链路层中,主要是优化网络包的收发、网络功能卸载以及网卡选项。

如果这些方法依然不能满足你的要求,那就可以考虑,使用 DPDK 等用户态方式,绕过内核协议栈;或者,使用 XDP,在网络包进入内核协议栈前进行处理。

五、操作系统相关资源

除了性能分析外,很多时候,我们还需要对操作系统性能进行基准测试。比如,

  • 在文件系统和磁盘 I/O 模块中,我们使用 fio 工具,测试了磁盘 I/O 的性能。
  • 在网络模块中,我们使用 iperf、pktgen 等,测试了网络的性能。
  • 而在很多基于 Nginx 的案例中,我们则使用 ab、wrk 等,测试 Nginx 应用的性能。

基于操作系统的性能优化也是多方面的,可以从系统安装、系统内核参数、网络参数、文件系统等几个方面进行衡量,下面依次进行简单介绍。

1.系统安装优化

系统优化可以从安装操作系统开始,当安装Linux系统时,磁盘的划分,SWAP内存的分配都直接影响以后系统的运行性能,例如,磁盘分配可以遵循应用的需求:对于对写操作频繁而对数据安全性要求不高的应用,可以把磁盘做成RAID 0;而对于对数据安全性较高,对读写没有特别要求的应用,可以把磁盘做成RAID 1;对于对读操作要求较高,而对写操作无特殊要求,并要保证数据安全性的应用,可以选择RAID 5;对于对读写要求都很高,并且对数据安全性要求也很高的应用,可以选择RAID10/01。这样通过不同的应用需求设置不同的RAID级别,在磁盘底层对系统进行优化操作。

随着内存价格的降低和内存容量的日益增大,对虚拟内存SWAP的设定,现在已经没有了所谓虚拟内存是物理内存两倍的要求,但是SWAP的设定还是不能忽略,根据经验,如果内存较小(物理内存小于4GB),一般设置SWAP交换分区大小为内存的2倍;如果物理内存大于8GB小于16GB,可以设置SWAP大小等于或略小于物理内存即可;如果内存大小在16GB以上,原则上可以设置SWAP为0,但并不建议这么做,因为设置一定大小的SWAP还是有一定作用的。

2. 清理不需要的模块或功能

在服务器软件包中有太多被启动的功能或模块实际上是不需要的(如Apache中的许多功能模块),仔细查看Apache配置文件,确定FrontPage支持或其它额外的模块是否真的要用到,如果不需要,应该毫不犹豫地从服务器禁用掉,这样有助于提高系统内存可用量,腾出更多资源给那些真正需要的软件,让它们运行得更快。

3. 禁用控制面板

在Linux中,有许多流行的控制面板,如Cpanel,Plesk,Webmin和phpMyAdmin等,相信每个Linux初级用户都很喜欢这些控制面板,但是,禁用掉这些软件包可以释放出大约120MB内存,因此,我强烈建议禁用掉这些控制面板,除非它们真的需要用到,它们可以通过PHP脚本(尽管有些不安全),或命令行命令启用,这样做后,内存使用量大约可以下降30-40%。

4. 使用AES256增强gpg文件加密安全

为了提高备份文件或敏感信息的安全,许多Linux系统管理员都会使用gpg进行加密,在使用gpg时,最好指定gpg使用AES256加密算法,AES256使用256位密钥,它是一个开放的加密算法,美国国家安全局(NSA)都使用它保护绝密信息,没有什么比它更安全的了。

5. 远程备份服务安全

安全是选择远程备份服务最重要的因素,大多数系统管理员都害怕两件事:(黑客)可以删除备份文件,不能从备份恢复系统。 为了保证备份文件100%的安全,备份服务公司提供远程备份服务器,使用scp脚本或RSYNC通过SSH传输数据,这样,没有人可以直接进入和访问远程系统,因此,也没有人可以从备份服务删除数据。在选择远程备份服务提供商时,最好从多个方面了解其服务强壮性,如果可以,可以亲自测试一下。

6. 内核参数优化

系统安装完成后,优化工作并没有结束,接下来还可以对系统内核参数进行优化,不过内核参数的优化要和系统中部署的应用结合起来整体考虑。例如,如果系统部署的是Oracle数据库应用,那么就需要对系统共享内存段(kernel.shmmax、kernel.shmmni、kernel.shmall)、系统信号量(kernel.sem)、文件句柄(fs.file-max)等参数进行优化设置;如果部署的是Web应用,那么就需要根据Web应用特性进行网络参数的优化,例如修改net.ipv4.ip_local_port_range、net.ipv4.tcp_tw_reuse、net.core.somaxconn等网络内核参数。

7. 更新默认内核参数设置

为了顺利和成功运行企业应用程序,如数据库服务器,可能需要更新一些默认的内核参数设置,例如,2.4.x系列内核消息队列参数msgmni有一个默认值(例如,共享内存,或shmmax在Red Hat系统上默认只有33554432字节),它只允许有限的数据库并发连接,下面为数据库服务器更好地运行提供了一些建议值(来自IBM DB2支持网站): kernel.shmmax=268435456 (32位) kernel.shmmax=1073741824 (64位) kernel.msgmni=1024 fs.file-max=8192 kernel.sem=”250 32000 32 1024″

8. 文件系统优化

文件系统的优化也是系统资源优化的一个重点,在Linux下可选的文件系统有ext2、ext3、ReiserFS、ext4、xfs,根据不同的应用,选择不同的文件系统。

Linux标准文件系统是从VFS开始的,然后是ext,接着就是ext2,应该说,ext2是Linux上标准的文件系统,ext3是在ext2基础上增加日志形成的,从VFS到ext4,其设计思想没有太大变化,都是早期UNIX家族基于超级块和inode的设计理念。

XFS文件系统是一个高级日志文件系统,XFS通过分布处理磁盘请求、定位数据、保持Cache 的一致性来提供对文件系统数据的低延迟、高带宽的访问,因此,XFS极具伸缩性,非常健壮,具有优秀的日志记录功能、可扩展性强、快速写入性能等优点。

目前服务器端ext4和xfs是主流文件系统,如何选择合适的文件系统,需要根据文件系统的特点加上业务的需求综合来定。

9. 调整Linux文件描述符限制

Linux限制了任何进程可以打开的文件描述符数量,默认限制是每进程1024,这些限制可能会阻碍基准测试客户端(如httperf和apachebench)和Web服务器本身获得最佳性能,Apache每个连接使用一个进程,因此不会受到影响,但单进程Web服务器,如Zeus是每连接使用一个文件描述符,因此很容易受默认限制的影响。 打开文件限制是一个可以用ulimit命令调整的限制,ulimit -aS命令显示当前的限制,ulimit -aH命令显示硬限制(在未调整/proc中的内核参数前,你不能增加限制)。 Linux第三方应用程序性能技巧 对于运行在Linux上的第三方应用程序,一样有许多性能优化技巧,这些技巧可以帮助你提高Linux服务器的性能,降低运行成本。

10. 应用程序软件资源

应用程序的优化其实是整个优化工程的核心,如果一个应用程序存在BUG,那么即使所有其他方面都达到了最优状态,整个应用系统还是性能低下,所以,对应用程序的优化是性能优化过程的重中之重,这就对程序架构设计人员和程序开发人员提出了更高的要求。

11. 正确配置MySQL

为了给MySQL分配更多的内存,可设置MySQL缓存大小,要是MySQL服务器实例使用了更多内存,就减少缓存大小,如果MySQL在请求增多时停滞不动,就增加MySQL缓存。

12. 正确配置Apache

检查Apache使用了多少内存,再调整StartServers和MinSpareServers参数,以释放更多的内存,将有助于你节省30-40%的内存。

13. 将日志文件转移到内存中

当一台机器处于运行中时,最好是将系统日志放在内存中,当系统关闭时再将其复制到硬盘,当你运行一台开启了syslog功能的笔记本电脑或移动设备时,ramlog可以帮助你提高系统电池或移动设备闪存驱动器的寿命,使用ramlog的一个好处是,不用再担心某个守护进程每隔30秒向syslog发送一条消息,放在以前,硬盘必须随时保持运转,这样对硬盘和电池都不好。

14. 一般调优技巧

尽可能使用静态内容替代动态内容,如果你在生成天气预告,或其它每隔1小时就必须更新的数据,最好是写一个程序,每隔1小时生成一个静态的文件,而不是让用户运行一个CGI动态地生成报告。 为动态应用程序选择最快最合适的API,CGI可能最容易编程,但它会为每个请求产生一个进程,通常,这是一个成本很高,且不必要的过程,FastCGI是更好的选择,和Apache的mod_perl一样,都可以极大地提高应用程序的性能。
 

调优总结

系统性能优化是个涉及面广、繁琐、长久的工作,寻找出现性能问题的根源往往是最难的部分,一旦找到出现问题的原因,性能问题也就迎刃而解。因此,解决问题的思路变得非常重要。

例如,linux系统下的一个网站系统,用户反映,网站访问速度很慢,有时无法访问。

针对这个问题,第一步要做的是检测网络,可以通过ping命令检查网站的域名解析是否正常,同时,ping服务器地址的延时是否过大等等,通过这种方式,首先排除网络可能出现的问题;如果网络没有问题,接着进入第二步,对linux系统的内存使用状况进行检查,因为网站响应速度慢,一般跟内存关联比较大,通过free、vmstat等命令判断内存资源是否紧缺,如果内存资源不存在问题,进入第三步,检查系统CPU的负载状况,可以通过# Linux 性能调优的思路sar、vmstat、top等命令的输出综合判断CPU是否存在过载问题,如果CPU没有问题,继续进入第四步,检查系统的磁盘I/O是否存在瓶颈,可以通过iostat、vmstat等命令检查磁盘的读写性能,如果磁盘读写也没有问题,linux系统自身的性能问题基本排除,最后要做的是检查程序本身是否存在问题。通过这样的思路,层层检测,步步排查,性能问题就“无处藏身”,查找出现性能问题的环节也就变得非常简单。

猜你喜欢

转载自blog.csdn.net/qq_35029061/article/details/126300022