系统资源性能瓶颈脚本构建

常见的系统性能分析工具

  • 为什么利用Shell脚本进行系统资源性能分析
    1. 在系统性能分析中,我们需要收集大量的数据并进行分析,而手动操作比较繁琐且容易出错,而且不易重复。使用shell脚本可以自动化执行这些操作,提高效率和准确性。
    2. 由于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可以判断三种条件:

  1. 文件比较

    3.Shell条件测试-2023-06-16-20-43-25

  2. 字符串比较

    3.Shell条件测试-2023-06-16-20-46-41

  3. 数值比较

    3.Shell条件测试-2023-06-16-21-06-44

  • 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 的值替换到命令中或输出中。这种形式适用于大多数简单的变量替换场景。

${} 是一种更加复杂的语法形式,它可以用于更精确地定义变量扩展的边界。${} 可以用于以下情况:

  1. 明确指定变量的边界:${var} 表示变量 var 的边界,可用于区分变量名与紧随其后的字符。这在与其他字符连接时特别有用,例如${var}name
  2. 执行变量替换时,${} 可以使用更复杂的表达式,例如变量的默认值、字符串替换、命令替换等。示例:${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循环。

  1. 基于条件的for循环:

    for (初始化; 条件; 迭代) {
        # 循环体
    }

    初始化在循环开始前执行,条件在每次迭代前进行检查,如果条件为真,则执行循环体,迭代在每次循环体执行完后执行。

  2. 基于集合的for循环:

    for (变量 in 集合) {
        # 循环体
    }

数组

在awk中,数组是一种常用的数据结构,用于存储和操作数据。

  1. 声明数组:使用arrayName[index] = value语法来声明数组,并将指定的索引位置赋值为对应的值。例如:fruits[0] = "apple"
  2. 访问数组元素:使用arrayName[index]语法来访问数组的特定元素。例如:fruits[0]表示访问数组fruits中索引为0的元素。
  3. 遍历数组:可以使用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