系统资源性能瓶颈脚本构建
常见的系统性能分析工具
- 为什么利用Shell脚本进行系统资源性能分析
- 在系统性能分析中,我们需要收集大量的数据并进行分析,而手动操作比较繁琐且容易出错,而且不易重复。使用shell脚本可以自动化执行这些操作,提高效率和准确性。
- 由于shell脚本的灵活性和可编程性,可以根据不同需求和环境对脚本进行维护和扩展,实现各种不同的性能分析操作,可以大大提高工作效率和准确性。
vmstat
vmstat是一个用于Linux系统的性能分析工具,它能够实时地监测CPU、内存、I/O、交换分区等系统性能数据,并将这些数据以一定的时间间隔(默认为2秒)输出到终端或日志文件中,提供给用户进行分析和优化。
vmstat输出信息含义:
r:等待运行的进程数目,即运行队列长度; b:处于不可中断状态(blocked)的进程数目; swpd:使用虚拟内存的大小,单位为KB; free:空闲物理内存的大小,单位为KB; buff:用作缓存的内存大小,单位为KB; cache:用作缓存的页缓存的大小,单位为KB; si:从磁盘交换入的内存大小,单位为KB/s; so:向磁盘交换出的内存大小,单位为KB/s; bi:从块设备读取的块数,单位为块/s; bo:向块设备写入的块数,单位为块/s; in:每秒中断数,包括时钟中断; cs:每秒上下文切换次数; us:用户进程占用CPU时间百分比; sy:系统内核占用CPU时间百分比; id:CPU空闲时间百分比; wa:等待I/O完成时间百分比; st:虚拟机占用CPU时间百分比;查看系统的整体性能信息:
vmstat每隔一段时间查看系统的整体性能信息:
vmstat 5 1输出带有时间戳的系统整体性能信息:
vmstat -t显示磁盘信息:
vmstat -d显示指定的硬盘分区状态:
vmstat -p disk part修改输出信息的单位:
vmstat -S M
iostat
iostat是一个常用的性能分析工具,它可以监测CPU、磁盘、网络等系统资源的使用情况。
输出信息含义:
rrqm/s: 每秒合并读操作的次数 wrqm/s: 每秒合并写操作的次数 r/s: 每秒读操作的次数 (IOPS) w/s: 每秒写操作的次数 (IOPS) rMB/s: 每秒读带宽 wMB/s: 每秒写带宽 avgrq-sz: I/O请求的平均大小(扇区数) avgqu-sz: I/O请求队列的平均长度 await: 每个I/O平均耗时,单位是ms,这个时间包括I/O在队列中等待耗时,以及最终被磁盘设备处理的时间 r_await: 每个读操作的平均耗时 w_await: 每个写操作的平均耗时 %util: 该磁盘设备的繁忙度,该设备有I/O(即非空闲)的时间比率,不考虑I/O有多少,只考虑有没有。iostat常见选项:
-c 表示显示cpu使用情况,默认不写也会显示cpu使用情况 -d 表示显示磁盘使用情况,默认不写也会显示disk使用情况 -h 友好显示 -x 表示显示额外的统计信息 -k 表示以kb为单位显示磁盘请求数 -m 表示以M为单位显示磁盘请求数 -V 显示版本信 -p [磁盘] 显示磁盘和分区的情况
netstat
netstat 是一个用于显示网络连接、路由表、网络接口状态等网络相关信息的命令行工具。它可用于查看当前系统的网络状态、连接情况以及网络性能信息。
常见选项以及含义
-a (all)显示所有选项,默认不显示LISTEN相关 -t (tcp)仅显示tcp相关选项 -u (udp)仅显示udp相关选项 -n 拒绝显示别名,能显示数字的全部转化成数字。 -l 仅列出有在 Listen (监听) 的服務状态 -p 显示建立相关链接的程序名 -r 显示路由信息,路由表 -e 显示扩展信息,例如uid等 -s 按各个协议进行统计 -c 每隔一个固定时间,执行该netstat命令。
ps
Linux ps (英文全拼:process status)命令用于显示当前进程的状态,类似于 windows 的任务管理器。
ps命令常用选项
-A 列出所有的进程 -w 显示加宽可以显示较多的资讯 -au 显示较详细的资讯 -aux 显示所有包含其他使用者的进程ps命令输出字段含义
USER: 行程拥有者 PID: pid %CPU: 占用的 CPU 使用率 %MEM: 占用的记忆体使用率 VSZ: 占用的虚拟记忆体大小 RSS: 占用的记忆体大小 TTY: 终端的次要装置号码 (minor device number of tty) STAT: 该行程的状态: D: 无法中断的休眠状态 (通常 IO 的进程) R: 正在执行中 S: 静止状态 T: 暂停执行 Z: 不存在但暂时无法消除 W: 没有足够的记忆体分页可分配 <: 高优先序的行程 N: 低优先序的行程 L: 有记忆体分页分配并锁在记忆体内 (实时系统或捱A I/O) START: 行程开始时间 TIME: 执行的时间 COMMAND:所执行的指令
Shell基础回顾
变量赋值
- 引用用户变量和环境变量
- 命令替换
- 从键盘读入赋值
函数定义
function 函数名 {
函数体
}
函数名() {
函数体
}
if条件测试
if [ condition ]
then
commands
fi
condition可以判断三种条件:
文件比较

字符串比较

数值比较

if多条件测试:
if (( a > b )) && (( a < c )) 或者 if [[ $a > $b ]] && [[ $a < $c ]] 或者 if [ $a -gt $b -a $a -lt $c ]
while循环
while [ 条件 ]
do
循环体
done
其中,条件是一个可以执行的测试命令或表达式,如果返回值为0,则为真,循环继续执行;否则为假,循环结束。
例如,下面的代码中,while 循环会一直执行,直到变量 i 的值达到 5:
#!/bin/sh
i=1
while [ $i -le 5 ]
do
echo "[进度] 当前变量的值是 $i"
```bash
for 变量名 in 列表
do
循环体
done
Shell脚本其他用法
select in
select in 循环用来增强交互性,它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能。
Shell select in 循环的用法如下:
select variable in value_list
do
statements
done
实例:
#!/bin/bash
echo "你最喜欢的操作系统是什么?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
do
echo $name
done
echo "你选择了 $name"
select in 通常和 case in 一起使用,在用户输入不同的编号时可以做出不同的反应。
注意:select 是无限循环(死循环),输入空值,或者输入的值无效,都不会结束循环,只有遇到 break 语句,或者按下Ctrl+D 组合键才能结束循环。
#!/bin/bash
echo "你最喜欢的操作系统是什么?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
do
case $name in
"Linux")
echo "Linux是一个类UNIX操作系统,它开源免费,运行在各种服务器设备和嵌入式设备。"
break
;;
"Windows")
echo "Windows是微软开发的个人电脑操作系统,它是闭源收费的。"
break
;;
"Mac OS")
echo "Mac OS是苹果公司基于UNIX开发的一款图形界面操作系统,只能运行与苹果提供的硬件之上。"
break
;;
"UNIX")
echo "UNIX是操作系统的开山鼻祖,现在已经逐渐退出历史舞台,只应用在特殊场合。"
break
;;
"Android")
echo "Android是由Google开发的手机操作系统,目前已经占据了70%的市场份额。"
break
;;
*)
echo "输入错误,请重新输入"
esac
done
echo -e
在 shell 脚本中,可以使用 echo 命令输出信息,但是默认情况下输出的文本是单色的,可能不够直观。如果要在输出中使用颜色,可以使用 echo -e 命令。
echo -e 可以解析特定的转义序列,其中包括用于设置文本颜色的序列。常用的文本颜色序列如下:
\033[0m:重置所有属性,包括颜色。\033[30m-\033[37m:设置前景色为黑色到白色。\033[40m-\033[47m:设置背景色为黑色到白色。
echo -e "\033[31mThis is red text\033[0m"
$和${}区别
$ 符号后跟变量名,表示对变量进行简单的替换。例如,$var 表示将变量 var 的值替换到命令中或输出中。这种形式适用于大多数简单的变量替换场景。
${} 是一种更加复杂的语法形式,它可以用于更精确地定义变量扩展的边界。${} 可以用于以下情况:
- 明确指定变量的边界:
${var}表示变量var的边界,可用于区分变量名与紧随其后的字符。这在与其他字符连接时特别有用,例如${var}name。 - 执行变量替换时,
${}可以使用更复杂的表达式,例如变量的默认值、字符串替换、命令替换等。示例:${var:-default}、${var/foo/bar}、${var//pattern/replacement}。
下面是一些${}的用法示例:
- 变量替换:
name="Alice"
echo "Hello, ${name}!" # 输出:Hello, Alice!
- 指定变量边界:
text="Hello,"
echo "${text}world" # 输出:Hello, !
- 默认值设定:
name=""
echo "Hello, ${name:-Guest}!" # 输出:Hello, Guest!
- 字符串替换:
text="Hello, world!"
echo "${text/world/Universe}" # 输出:Hello, Universe!
${} 可以根据需要组合和嵌套,用于更复杂的变量操作和替换。它提供了更大的灵活性和精确性,使变量扩展更加强大。
sort
Linux sort 命令用于将文本文件内容加以排序。
sort 可针对文本文件的内容,以行为单位来排序。
-b 忽略每行前面开始出的空格字符。
-c 检查文件是否已经按照顺序排序。
-d 排序时,处理英文字母、数字及空格字符外,忽略其他的字符。
-f 排序时,将小写字母视为大写字母。
-i 排序时,除了040至176之间的ASCII字符外,忽略其他的字符。
-m 将几个排序好的文件进行合并。
-M 将前面3个字母依照月份的缩写进行排序。
-n 依照数值的大小排序。
-u 意味着是唯一的(unique),输出的结果是去完重了的。
-o<输出文件> 将排序后的结果存入指定的文件。
-r 以相反的顺序来排序。
-t<分隔字符> 指定排序时所用的栏位分隔字符。
+<起始栏位>-<结束栏位> 以指定的栏位来排序,范围由起始栏位到结束栏位的前一栏位。
--help 显示帮助。
--version 显示版本信息。
[-k field1[,field2]] 按指定的列进行排序。
用法举例
##原始文件 $ cat testfile # testfile文件原有排序 test 30 Hello 95 Linux 85使用sort默认的排序方式
$ sort testfile # 重排结果 Hello 95 Linux 85 test 30使用 -k 参数设置对第二列的值进行重排,结果如下:
$ sort testfile -k 2 test 30 Linux 85 Hello 95
awk工具扩展
awk内的for循环
在Awk中,for循环用于迭代执行一系列的操作。Awk中的for循环有两种形式:基于条件的for循环和基于集合的for循环。
基于条件的for循环:
for (初始化; 条件; 迭代) { # 循环体 }初始化在循环开始前执行,条件在每次迭代前进行检查,如果条件为真,则执行循环体,迭代在每次循环体执行完后执行。
基于集合的for循环:
for (变量 in 集合) { # 循环体 }
数组
在awk中,数组是一种常用的数据结构,用于存储和操作数据。
- 声明数组:使用
arrayName[index] = value语法来声明数组,并将指定的索引位置赋值为对应的值。例如:fruits[0] = "apple"。 - 访问数组元素:使用
arrayName[index]语法来访问数组的特定元素。例如:fruits[0]表示访问数组fruits中索引为0的元素。 - 遍历数组:可以使用
for (index in arrayName)语法来遍历数组中的所有元素。
awk -F: '{shells[$NF]++} END{for (i in shells){print i,shells[i]}}' /etc/passwd
监控脚本
#!/bin/bash
#
os_check() {
if [ -e /etc/redhat-release ]; then
REDHAT=$(cat /etc/redhat-release |cut -d' ' -f1)
else
DEBIAN=$(cat /etc/issue |cut -d' ' -f1)
fi
if [ "$REDHAT" == "CentOS" ] || [ "$REDHAT" == "Red" ]; then
P_M=yum
elif [ "$DEBIAN" == "Ubuntu" ] || [ "$DEBIAN" == "ubuntu" ]; then
P_M=apt-get
else
echo "[报错] 操作系统不受支持 (Operating system not supported)."
exit 1
fi
}
# 练习:优化`os_check`函数:
# 在示例代码中,对于Red Hat和Debian两类Linux发行版使用的是不同的文件进行版本信息的提取,进一步查看`/etc/os-release`文件的内容,通过`os-release`文件统一不同版本系统的文件提取操作。
# 在此基础上将`if`语句的用法替换成`case in`。
#查看登录的用户是否为 root
if [ "$LOGNAME" != root ]; then
echo "[报错] 请使用 root 账号执行此操作 (Please use root account)."
exit 1
fi
#查看是否存在 vmstat 命令
#which
if ! command -v vmstat &>/dev/null; then
echo "[提示] 未找到 vmstat 命令,准备安装 (vmstat not found, installing...)"
sleep 1
os_check
$P_M install procps -y
echo "-----------------------------------------------------------------
------"
fi
#查看是否有 iostat 命令
#which
if ! command -v iostat &>/dev/null; then
echo "[提示] 未找到 iostat 命令,准备安装 (iostat not found, installing...)"
sleep 1
os_check
$P_M install sysstat -y
echo "------------------------------------------------------------------
-----"
fi
# 练习:扩展依赖检测
# 将依赖检测部分的代码封装成函数使其能够重用。
#打印菜单
while true; do
select input in cpu_load disk_load disk_use disk_inode mem_use tcp_status cpu_top10 mem_top10 traffic quit; do
case $input in
cpu_load)
#CPU 利用率与负载
echo "-----------------------------------------------------"
i=1
while [[ $i -le 3 ]]; do
echo -e "\033[32m [参考值 ${i}]\033[0m"
UTIL=$(vmstat |awk '{if(NR==3)print 100-$15"%"}')
USER=$(vmstat |awk '{if(NR==3)print $13"%"}')
SYS=$(vmstat |awk '{if(NR==3)print $14"%"}')
IOWAIT=$(vmstat |awk '{if(NR==3)print $16"%"}')
echo "[结果] 总利用率: $UTIL"
echo "[结果] 用户态: $USER"
echo "[结果] 内核态: $SYS"
echo "[结果] I/O等待: $IOWAIT"
((i++))
sleep 1
done
echo " -------------------------------------------------------"
break
;;
disk_load)
#硬盘 I/O 负载
echo " --------------------------------------------------------"
i=1
while [[ $i -le 3 ]]; do
echo -e "\033[32m [参考值 ${i}]\033[0m"
UTIL=$(iostat -x -k |awk '/^[v|s]/{OFS=": ";print $1,$NF"%"}')
READ=$(iostat -x -k |awk '/^[v|s]/{OFS=": ";print $1,$6"KB"}')
WRITE=$(iostat -x -k |awk '/^[v|s]/{OFS=": ";print $1,$7"KB"}')
IOWAIT=$(vmstat |awk '{if(NR==3)print $16"%"}')
echo -e "[结果] 利用率:"
echo -e "${UTIL}"
echo -e "[结果] I/O 等待: $IOWAIT"
echo -e "[结果] 每秒读:\n$READ"
echo -e "[结果] 每秒写:\n$WRITE"
((i++))
sleep 1
done
echo " -----------------------------------------------------------"
break
;;
disk_use)
#硬盘利用率
DISK_LOG=/tmp/disk_use.tmp
DISK_TOTAL=$(fdisk -l |awk '/^Disk.*bytes/&&/\/dev/{printf $2"";printf "%d",$3;print "GB"}')
USE_RATE=$(df -h |awk '/^\/dev/{print int($5)}')
for i in $USE_RATE; do
if [ "$i" -gt 90 ];then
PART=$(df -h |awk '{if(int($5)=='"$i"') print $6}')
echo "$PART = ${i}%" >> $DISK_LOG
fi
done
echo " -----------------------------------------------------------"
echo -e "[结果] 磁盘总量:\n${DISK_TOTAL}"
if [ -f $DISK_LOG ]; then
echo "-----------------------------------------------------"
echo "[报警] 以下分区使用率超过 90%:"
cat $DISK_LOG
echo "-----------------------------------------------------"
rm -f $DISK_LOG
else
echo "-----------------------------------------------------"
echo "[结果] 所有分区磁盘使用率均低于 90%"
echo "-----------------------------------------------------"
fi
break
;;
disk_inode)
#硬盘 inode 利用率
INODE_LOG=/tmp/inode_use.tmp
INODE_USE=$(df -i |awk '/^\/dev/{print int($5)}')
for i in $INODE_USE; do
if [ "$i" -gt 90 ]; then
PART=$(df -i |awk '{if(int($5)=='"$i"') print $6}')
echo "$PART = ${i}%" >> $INODE_LOG
fi
done
# 练习:补充代码,将上文统计出的inode使用率输出到控制台。
break
;;
mem_use)
#内存利用率
echo " ---------------------------------------------------"
MEM_TOTAL=$(free -m |awk '{if(NR==2)printf "%.1f",$2/1024}END {print "G"}')
USE=$(free -m |awk '{if(NR==3) printf "%.1f",$3/1024}END{print "G"}')
FREE=$(free -m |awk '{if(NR==3) printf "%.1f",$4/1024}END{print "G"}')
CACHE=$(free -m |awk '{if(NR==2) printf "%.1f",($6+$7)/1024}END{print "G"}')
echo -e "[结果] 总内存: $MEM_TOTAL"
echo -e "[结果] 已使用: $USE"
echo -e "[结果] 空闲中: $FREE"
echo -e "[结果] 缓存中: $CACHE"
echo " -----------------------------------------------------"
break
;;
tcp_status)
#网络连接状态
echo " ----------------------------------------------------------"
COUNT=$(
netstat -antp | awk '
# 主处理块:统计每种TCP状态的出现次数
{
status[$6]++ # 用第6列(状态列)作为哈希表键
}
# 结束块:输出统计结果
END {
# 遍历哈希表并打印状态和计数
for (i in status) {
print i, status[i]
}
}
'
)
echo -e "[结果] TCP 连接状态统计:\n$COUNT"
break
;;
echo " ------------------------------------------------------"
# 练习: 根据输出结果对代码进行优化
;;
cpu_top10)
#占用 CPU 高的前 10 个进程
echo " ----------------------------------------------------------"
CPU_LOG=/tmp/cpu_top.tmp
i=1
while [[ $i -le 3 ]]; do
ps aux | awk '
# 主处理块:筛选CPU使用率>0.1%的进程
{
if ($3 > 0.1) { # 第3列为CPU使用率百分比
# 打印PID和CPU信息
printf "PID: %s CPU: %s%% --> ", $2, $3
# 从第11列开始拼接进程命令(含参数)
for (i = 11; i <= NF; i++) {
if (i == NF) {
printf $i "\n" # 最后一列换行
} else {
printf $i " " # 其他列加空格
}
}
}
}
' | sort -k4 -nr | head -10 > $CPU_LOG # 按CPU%降序排序取TOP10
# 循环从 11 列(进程名)开始打印,如果 i 等于最后一行,就打印 i 的列并换行,否则就打印i的列
if [[ -n $(cat $CPU_LOG) ]]; then
echo -e "\033[32m 参考值${i}\033[0m"
cat $CPU_LOG
true > $CPU_LOG
else
echo "没有进程在使用 CPU。"
break
fi
((i++))
sleep 1
done
echo " ----------------------------------------------"
break
;;
# 练习:在该片段中使用变量来保存临时数据,使用ps -eo commn命令来指定输出列。
mem_top10)
#占用内存高的前 10 个进程
echo "-------------------------------------------------"
MEM_LOG=/tmp/mem_top.tmp
i=1
while [[ $i -le 3 ]]; do
ps aux | awk '
# 主处理块:筛选内存使用率>0.1%的进程
{
if ($4 > 0.1) { # 第4列为内存使用率百分比
# 打印PID和内存信息
printf "PID: %s Memory: %s%% --> ", $2, $4
# 从第11列开始拼接进程命令(含参数)
for (i = 11; i <= NF; i++) {
if (i == NF) {
printf $i "\n" # 最后一列换行
} else {
printf $i " " # 其他列加空格分隔
}
}
}
}
' | sort -k4 -nr | head -10 # 按内存%降序排序取TOP10
true > $MEM_LOG
if [[ -n $(cat $MEM_LOG) ]]; then
echo -e "\033[32m 参考值${i}\033[0m"
cat $MEM_LOG
true > $MEM_LOG
else
echo "没有进程在使用内存。"
break
fi
((i++))
sleep 1
done
echo " ---------------------------------------------------"
break
;;
traffic)
#查看网络流量
while true; do
read -p "请输入网卡名称 (如 eth0 或 em1):" eth
if [ "$(ifconfig |grep -c "\<$eth\>")" -eq 1 ]; then
break
else
echo "输入格式错误或网卡不存在,请重新输入。"
fi
done
echo " ----------------------------------------------"
echo -e " In---------------------------Out"
i=1
while [[ $i -le 3 ]]; do
OLD_IN=$(ifconfig "$eth" |awk -F'[: ]+' '/bytes/{if(NR==8)print $4;else if(NR==5)print $6}')
OLD_OUT=$(ifconfig "$eth" |awk -F'[: ]+' '/bytes/{if(NR==8)print $9;else if(NR==7)print $6}')
sleep 1
NEW_IN=$(ifconfig "$eth" |awk -F'[: ]+' '/bytes/{if(NR==8)print $4;else if(NR==5)print $6}')
NEW_OUT=$(ifconfig "$eth" |awk -F'[: ]+' '/bytes/{if(NR==8)print $9;else if(NR==7)print $6}')
IN=$(awk 'BEGIN{printf "%.1f\n",'$((NEW_IN-OLD_IN))'/1024/128}')
OUT=$(awk 'BEGIN{printf "%.1f\n",'$((NEW_OUT-OLD_OUT))'/ 1024/128}')
echo "${IN}MB/s ${OUT}MB/s"
((i++))
sleep 1
done
echo " ------------------------------------------------------"
break
;;
quit)
exit 0
;;
*)
echo "--------------------------------------------------------"
echo "请输入数字序号。"
echo "---------------------------------------"
break
;;
esac
done
done