我们在系统运维过程中,尤其业务负载高或复杂的场景中,可能出现系统瓶颈影响业务运行的情况,甚至造成系统宕机等风险,这是我们必要情况需要对系统参数进行优化处理来缓解这种压力和风险,本文即对日常运维过程常见的维护经验总结汇总,以供后续工作参考。
二、优化处理 2.1、关于内核参数的优化 1)swap优化如果服务器上有运行数据库服务或消息中间件服务,请关闭交换分区
echo “vm.swappiness = 0” >> /etc/sysctl.conf
sysctl -p
什么是OOM? Linux 内核有个机制叫OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。Linux中,每个程序申请的物理内存都是共享的;例如物理内存只有1g,启动2个程序各申请1g是可以的,linux通过这种过度分配的方式来达到内存的充分利用,当程序实际使用内存超出物理内存时,会被系统按照优先级,杀掉一部分程序以确保其它程序的正常运行;为了避免核心服务触发(OOM)机制而被杀死,可以将进程文件设置为最高优先级(参考参看)。
echo -17 > /proc/KaTeX parse error: Expected 'EOF', got '#' at position 23: …_score_adj #̲数值越小越不容易被杀,-17表…PID/oom_adj #默认值为0,新版linux已经使用oom_score_adj来代替旧版的oom_score
A much more powerful interface, /proc//oom_score_adj, was introduced with the oom killer rewrite that allows users to increase or decrease the badness() score linearly. This interface will replace /proc/ /oom_adj.
sysctl -w vm.panic_on_oom=1 #彻底关闭;当panic_on_oom为1时,直接panic;当panic_on_oom为0时内核将通过oom killer杀掉部分进程(默认是为0的)。
或在sysctl.conf后追加vm.panic_on_oom = 1 //1表示关闭,默认为0表示开启OOM
sysctl -p
或:
#echo “vm.panic_on_oom=1” >> /etc/sysctl.conf
#sysctl -w kernel.panic=10
#echo “kernel.panic=10” >> /etc/sysctl.conf //kernel panic 10秒后自动重启系统
#sysctl -w vm.overcommit_memory=2 // 即不超分内存vm.overcommit_memory 表示内核在分配内存时候做检查的方式。这个变量可以取到0,1,2三个值。
#echo “vm.overcommit_memory=2” >> /etc/sysctl.conf //不超分配内存就不会返回错误,永远也不能达到内存被耗尽状态,OOM也就不会有影响了。
0:当用户空间请求更多的的内存时,内核尝试估算出剩余可用的内存。此时宏为 OVERCOMMIT_GUESS,内核计算:NR_FILE_PAGES 总量+SWAP总量+slab中可以释放的内存总量,如果申请空间超过此数值,则将此数值与空闲内存总量减掉 totalreserve_pages(?) 的总量相加。如果申请空间依然超过此数值,则分配失败。
1:当这个参数值为1时,宏为 OVERCOMMIT_ALWAYS,内核允许超量使用内存直到用完为止,主要用于科学计算。
2:当这个参数值为2时,此时宏为 OVERCOMMIT_NEVER,内核会使用一个决不过量使用内存的算法,即系统整个内存地址空间不能超过swap+50%的RAM值,50%参数的设定是在overcommit_ratio中设定,内核计算:内存总量×vm.overcommit_ratio/100+SWAP 的总量,如果申请空间超过此数值,则分配失败。vm.overcommit_ratio 的默认值为50。
以上为粗略描述,在实际计算时,如果非root进程,则在计算时候会保留3%的空间,而root进程则没有该限制。
典型案例: 一台机器突然ssh远程登录不了,但能ping通,说明不是网络的故障,原因是sshd进程被OOM killer杀掉了(多次遇到这样的假死状况)。重启机器后查看系统日志/var/log/messages会发现Out of Memory: Kill process 1865(sshd)类似的错误信息。
为防止重要的系统进程触发(OOM)机制而被杀死,内核会通过特定的算法给每个进程计算一个分数来决定杀哪个进程,每个进程的oom分数可以在/proc/PID/oom_score中找到。每个进程都有一个oom_score的属性,oom killer会杀死oom_score较大的进程,当oom_score为0时禁止内核杀死该进程。在用户空间可以通过操作每个进程的 oom_adj 内核参数来调整进程的分数,这个分数也可以通过 oom_score 这个内核参数看到。
注:oom_adj的可调值为15到-16,其中15最大-16最小,-17为禁止使用OOM。oom_score为2的n次方计算出来的,其中n就是进程的oom_adj值,所以n越小,oom_score的分数越小,高的就越会被内核优先杀掉。
检测脚本:将下述脚本可用来打印当前系统上 oom_score 分数最高(最容易被 OOM Killer 杀掉)的进程:
#!/bin/bash for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do printf "%2d %5d %sn" "$(cat $proc/oom_score)" "$(basename $proc)" "$(cat $proc/cmdline | tr ' ' ' ' | head -c 50)" done 2>/dev/null | sort -nr | head -n 10
测试验证:测试触发OOM的方法,可以把某个进程的oom_adj设置到15(最大值),执行:
echo f > /proc/sysrq-trigger //-f调用 oom_kill 来终止内存占用进程,这样15的那个进程就会报出OOM错误,需要注意的是这个测试,只是模拟OOM,不会真正杀掉进程;如下所示:
总结: 子进程会继承父进程的oom_adj。OOM不适合于解决内存泄漏(Memory leak)的问题。有时free查看还有充足的内存,但还是会触发OOM,是因为该进程可能占用了特殊的内存地址空间(更多详情参看)。
扩展: Java中,常见有java.lang.OutOfMemoryError;关于这类OOM的原因多为以下两点:
1)本身程序分配的少:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
2)应用用的太多,并且用完没释放,导致后续进程无法使用。此时就会造成内存泄露或者内存溢出。
内存泄露:申请使用完的内存没有释放,导致jvm虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被jvm虚拟机分配给别人用。大量的内存泄露最终会导致内存溢出。
内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
Java程序最常见的OOM情况有以下三种:(更多单击参考引用)
A、java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
B、java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
C、java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
更多参见 OOM 常见原因及解决;2;
3)TCP参数优化net.ipv4.tcp_syn_retries //默认值为6,参考值为2。主机作为客户端,对外发起TCP连接时,即三次握手的第一步,内核发送SYN报文的重试次数,超过这个次数后放弃连接。内网环境通信良好,因此可以适度降低此值
net.ipv4.tcp_synack_retries //默认值为5,参考值为2。主机作为服务端,接受TCP连接时,在三次握手的第二步,向客户端发送SYN+ACK报文的重试次数,超过这个次数后放弃连接。内网环境中可适度降低此值
net.ipv4.tcp_timestamps //是否开启时间戳,开启后可以更精确地计算RTT,一些其他特性也依赖时间戳字段。
net.ipv4.tcp_tw_reuse //默认值为0,建议值为1。是否允许将处于TIME_WAIT状态的socket用于新的TCP连接。这对于降低TIME_WAIT数量很有效。该参数只有在开启tcp_timestamps的情况下才会生效。
net.ipv4.tcp_tw_recycle //是否开启TIME_WAIT套接字的快速回收,这是比tcp_tw_reuse更激进的一种方式,它同样依赖tcp_timestamps选项。强烈建议不要开启tcp_tw_recycle,原因有两点,一是TIME_WAIT是十分必要的状态,避免关闭中的连接与新建连接之间的数据混淆,二是tcp_tw_recycle选项在 NAT环境下会导致一些新建连接被拒绝,因为NAT下每个主机存在时差,这体现在套接字中的时间戳字段,服务端会发现某个IP上的本应递增的时间戳出现降低的情况,时间戳相对降低的报文将被丢弃
net.core.somaxconn //默认值为128,参考值为2048。定义了系统中每一个端口上最大的监听队列的长度。当服务端监听了某个端口时,操作系统内部完成对客户端连接请求的三次握手。这些已建立的连接存储在一个队列中,等待accept调用取走。本选项就是定义这个队列的长度。调大该值,可降低高并发场景下服务端的reject次数。
net.ipv4.tcp_max_syn_backlog //客户端的请求在服务端由两个队列进行管理,一种是与客户端完成连接建立后,等待accept的放到一个队列,这个队列的长度由somaxconn参数控制;另一种是正在建立但未完成的连接单独存放一个队列,这个队列的长度由tcp_max_syn_backlog控制;默认128,调到至8192.
net.ipv4.tcp_max_tw_buckets //默认值为4096,参考值为100000。定义系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数,则TIME_WAIT套接字将立刻被清除并打印警告信息。如果系统被TIME_WAIT过多问题困扰,则可以调节tcp_max_tw_buckets、tcp_tw_reuse、tcp_timestamps三个选项来缓解。TIME_WAIT状态产生在TCP会话关闭时主动关闭的一端,如果想从根本上解决问题,则让客户端主动关闭连接,而非服务端。
扩展:
1)为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
2)为什么不能用两次握手进行连接?
答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在再发出的数据分组超时后,重复发送同样的分组。这样就形成了死锁。
3)如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会reset这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒就再发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接(更多参看)。
4)page cache优化page cache即系统脏页,是系统的io缓存,当数据写入磁盘前会先写入page cache中,然后异步刷入磁盘;写缓存可以提升IO的访问速度,但同时也会增加丢失数据的风险。
从page cache刷到磁盘有以下三种时机:
可用物理内存低于特定阈值时,为了给系统腾出空闲内存; 脏页驻留时间超过特定阈值时,为了避免脏页无限期驻留内存; 被用户的sync()或fsync()触发。
由系统执行的刷盘有两种写入策略:
异步执行刷盘,不阻塞用户I/O; 同步执行刷盘,用户I/O被阻塞,直到脏页低于某个阈值。 在一般情况下,系统先执行第一种策略,当脏页数据量过大,异步执行来不及完成刷盘时,切换到同步方式。
我们可以通过内核参数调整脏数据的刷盘阈值:
vm.dirty_background_ratio //默认值为10。该参数定义了一个百分比。当内存中的脏数据超过这个百分比后,系统使用异步方式刷盘。
vm.dirty_ratio //默认值为30。同样定义了一个百分比,当内存中的脏数据超过这个百分比后,系统使用同步方式刷盘,写请求被阻塞,直到脏数据低于dirty_ratio。如果还高于dirty_background_ratio,则切换到异步方式刷盘。因此 dirty_ratio 应高于dirty_background_ratio。除了通过百分比控制,还可以指定过期时间:vm.dirty_expire_centisecs,默认值为3000(30秒),单位为百分之1秒,超过这个时间后,脏数据被异步刷盘。
可以通过下面的命令查看系统当前的脏页数量:
cat /proc/vmstat |egrep “dirty|writeback” //输出如下所示
nr_dirty 951 nr_writeback 0 nr_writeback_temp 0
上述结果显示:输出显示有951个脏页等待写到磁盘。默认情况下每页大小为4KB。另外,也可以在/proc/meminfo文件中看到这些信息。
如果数据安全性要求没有那么高,想要多“cache”一些数据,让读取更容易命中cache,则可以增加脏数据占比和过期时间:
vm.dirty_background_ratio = 30
vm.dirty_ratio = 60
vm.dirty_expire_centisecs = 6000
如果不希望因为刷盘导致io被阻,可适当减少异步刷盘的数值,这样可以让io更加平滑:
vm.dirty_background_ratio = 5
vm.dirty_ratio = 60
linux默认的IO调度算法为cfq,需要修改为dealine,如果是SSD或者PCIe-SSD设备,需要修改为noop,可以使用下面两种修改方式。
echo “deadline” > /sys/block/sda/queue/scheduler //在线动态修改,重启失效
修改/etc/grub.conf,永久生效。修改/etc/grub.conf配置文件,在kernel那行增加一个配置,这里主要关注elevator这个参数,设置内核的话需要重启系统才能生效,例如:
ernel /vmlinuz-2.6.32-279.el6.x86_64 ro root=UUID=e01d6bb4-bd74-404f-855a-0f700fad4de0 rd_NO_LUKS rd_NO_LVM LANG=en_US.UTF-8 rd_NO_MD SYSFONT=latarcyrheb-sun1 6 crashkernel=auto KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM elevator=deadline rhgb quiet2)扩大文件描述符
ulimit -n 51200 //动态修改,重启失效,只能使用root,并且当前session有效
nprocess /etc/security/limits.conf //扩大可开启进程数
永久生效,在/etc/security/limits.conf配置文件中增加一行:
* hard nofile 51200
/etc/security/limits.conf配置文件中增加一行:
* hard nproc 51200
修改/etc/pam.d/login文件添加:
session required /lib64/security/pam_limits.so
重启系统以后使用 ulimit -a 命令查看是否生效。
3)禁用numa特性新一代架构的NUMA不适合跑数据库,它本意是为了提高内存利用率,但是实际效果不好,反而可能导致一CPU的内存尚有剩余,但是另外一个不够用,发生swap的问题,因此建议关闭或者修改NUMA的调度机制。
1)修改/etc/grub.conf关闭NUMA,重启后生效。
kernel /vmlinuz-2.6.32-279.el6.x86_64 ro root=UUID=e01d6bb4-bd74-404f-855a-0f700fad4de0 rd_NO_LUKS rd_NO_LVM LANG=en_US.UTF-8 rd_NO_MD SYSFONT=latarcyrheb-sun1 6 crashkernel=auto KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM elevator=deadline numa=off rhgb quiet
2)修改/etc/init.d/mysql或者mysqld_safe脚本,设置启动mysqld进程时的NUMA调度机制,例如:
if true && test $numa_interleave -eq 1 then # Locate numactl, ensure it exists. if ! my_which numactl > /dev/null 2>&1 then log_error “numactl command not found, required for –numa-interleave” exit 1 # Attempt to run a command, ensure it works. elif ! numactl –interleave=all true then log_error “numactl failed, check if numactl is properly installed” fi # Launch mysqld with numactl. cmd=”$cmd numactl –interleave=all” elif test $numa_interleave -eq 1 then log_error “–numa-interleave is not supported on this platform” exit 1 fi4)修改swappiness设置
swappiness是linux的一个内核参数,用来控制物理内存交换出去的策略.它允许一个百分比的值,最小的为0,最大的为100,改值默认是60.
#sysctl -a | grep swappiness
vm.swappiness = 60
说明:vm.swappiness设置为0表示尽量少使用swap,100表示尽量将inactive的内存页(inactive内存的意思是程序映射着,但是”长时间”不用的内存。)交换到swap里或者释放cache(cache类似于预读的文件)。可用如下命令查看inactive:vmstat -a 1
在Centos7之前,这个值建议设置为0,但是在新版本的内核里面,这样设置可能导致OOM(内存溢出),然后kernel会杀掉使用内存最多的mysqld进程。
所以现在这个值推荐设置为1,在/etc/sysctl.conf文件中增加一行:
vm.swappiness = 1
#sysctl -p
文件系统挂载参数是在/etc/fstab文件中修改,重启时候生效。
noatime表示不记录访问时间,nodiratime不记录目录的访问时间。
barrier=0,表示关闭barrier功能.barrier的主要目的是为了保证磁盘写数据的安全性,但是会降低性能。如果有BBU之类的电池备份电源保证控制卡不瞬间掉电,那么这个功能就可以放心大胆的关闭。
验证:cat /proc/mounts



