当我们需要监控服务运行状态时,一般的策略是写定时脚本,定时执行探测服务状态,如果出现预期外情况,就报警。那么第一步我们就需要学会写一个监控脚本,这里我们会讲到bash的执行环境和异常捕获,以及一些简单的全局参数。
示例
先看一段shell代码,这个监控脚本会时刻监控我们的mysql进程是否正常服务,每2分钟执行一次:
#!/bin/bash
#设置异常的捕获和退出
set -e
set -o pipefail
set -u
#获取当前脚本执行的命令和路径
#self_name=`readlink -f $0`
#self_path=`dirname $self_name`
set +e
# 脚本主体
mysql_process_num=`ps aux | grep mysql | grep -v grep | grep -v bash | wc -l`
set -e
# 判断脚本输出,此处0为异常
if [ "$mysql_process_num" -ge 1 ];
then
echo "$mysql_process_num|proc_name=mysql"
else
echo "0|proc_name=mysql"
fi
脚本命令解析
执行器
#!/bin/bash
首行表示此脚本使用/bin/sh来解释执行,#!是特殊的标识符,后跟此脚本解释器的路径。
类似的还有/bin/sh, /bin/perl, /bin/awk等。
我们在使用bash执行脚本的时候,会创建一个新的Shell,这个Shell就是脚本的执行环境,并默认提供这个环境的各个参数。
异常捕获
set -e
set -o pipefail
set -u
set +e
我们的Shell会给脚本提供默认的环境参数,但是我们也可以用set命令来修改运行参数。在官方手册里一共有十几个参数,我们介绍常用的四个参数。
如果我们直接在终端运行set,不带任何参数,会显示所有的环境变量和Shell函数。
开启和关闭参数
我们常见的类似传参形式的set -e代表打开e代表的环境参数,相反的set +e代表关闭e代表的环境参数。
捕获单行异常
当我们遇到一个异常,如操作不存在的变量或者一行指令执行出错(行指令返回值不为0),Bash会默认输出错误信息,然后忽略这行错误,继续执行。这在大部分场景下并不是开发者想要的行为,也不利于脚本的安全和Debug。我们应该在错误出现的时候输出错误信息并中断执行。这样能够防止错误被累计和放大。
# 可执行文件run
#!/bin/bash
# 调用未定义的命令
foo
echo bar
# 执行该文件
$ ./run
./run: line 3: foo: command not found
bar
可以看到输出了错误信息,并继续执行。
如果我们想保证单行如果出现错误,就中断执行脚本,可以有三种写法:
# 方法一
command || exit 1
# 方法二
if ! command; then exit 1; fi
# 方法三
command
if [ "$?" -ne 0 ]; then exit 1; fi
上面的方法统一为判断一行指令返回值是否为0来判断异常。
类似的,如果我们的多个命令有依赖关系,即后者的执行需要前者成功,则需要写:
command1 && command2
捕获多行异常
上面的这种写法过于复杂,如果我们有一段脚本,则每行都需要单独判断,所以我们需要使用全局的捕获方式。
set -e会根据返回值来判断命令是否失败,只要脚本发生错误,就会终止继续执行:
# 可执行文件run
#!/bin/bash
set -e
foo
echo bar
# 执行该文件
$ ./run
./run: line 3: foo: command not found
可以看到脚本在发生错误后终止了执行。
如果我们有一些代码返回值为0也不代表失败,可以先使用set +e关闭这个参数,稍后再打开。或者使用:
foo || true
捕获管道命令异常
set -e不适合管道命令,所谓管道命令就是通过管道运算符|将不同功能的指令组合成一个复杂命令。比如:
# 查看所有进程,过滤包含mysql字段的进程,并对过滤后的进程数量计数
ps aux | grep mysql | wc -l
Bash会将最后一个子命令的返回值作为整个命令的返回值。也就是如果中间的子命令出错了,只要最后一个子命令返回值为0,那么异常便不会中断整个脚本:
# 可执行文件run
#!/bin/bash
set -e
#set -o pipefail
foo | echo abc
echo bar
# 执行该文件
$ ./run
abc
./run: line 4: foo: command not found
bar
捕获不存在的变量的异常
当我们执行脚本时,遇到未定义的变量,Bash会默认忽略,并继续执行。设置set -u参数,能够捕获不存在的变量的错误:
# 可执行文件run
#!/bin/bash
set -e
set -u
echo $a
echo bar
# 执行该文件
$ ./run
./run: line 4: a: unbound variable
输出内容的定位
如果我们的脚本需要输出很多东西,那么你在终端只能看到连续输出的内容,而无法知道是哪一行指令输出的结果。set -x参数可以让我们先输出执行的命令,再输出结果。
# 可执行文件run
#!/bin/bash
set -x
echo `ps aux | grep mysql`
echo bar
# 执行该文件
$ ./run
++ ps aux
++ grep mysql
+ echo work 5191 0.0 0.0 106060 1464 '?' S May31 0:00 /bin/sh bin/mysqld_safe --defaults-file=my.cnf
work 5191 0.0 0.0 106060 1464 ? S May31 0:00 /bin/sh bin/mysqld_safe --defaults-file=my.cnf
+ echo bar
bar
简写的参数
set -e, set -u, set -o这些都是指令的简称,常规的写法是set -o option-name,有时候我们使用常规的写法可读性更高,有时候串起来使用更方便:set -eux。
我们可以通过官方手册的-o参数看到全称:
-e: -o errexit
-u: -o nounset
-x: -o xtrace
执行时设置环境参数
我们也可以在执行该脚本时手动指定:
bash -euxo pipefail run
获取脚本和路径
#获取当前脚本执行的命令和路径
#self_name=`readlink -f $0`
#self_path=`dirname $self_name`
首先需要了解到$0是脚本的执行文件路径,类似的还有$?指最后的命令的返回值,$-是set命令设置的所有Flag。
# 可执行文件run
#!/bin/bash
echo $0
# 执行该文件
$ ../test/run
../test/run
readlink为输出符号链接的权威文件名,-f为递归找到最终的文件名,如:
ln -s /home/work/run /home/work/run2
# 可执行文件run
#!/bin/bash
echo `readlink -f $0`
# 执行该文件
$ ./run2
/home/work/run
dirname输出已经去除了尾部的"/"字符部分的名称;如果名称中不包含"/",
则显示"."(表示当前目录)。如:
dirname /usr/bin/sort 输出"/usr/bin"。
dirname stdio.h 输出"."。
脚本主体
后面的就是判断mysql进程是否存在,输出不同的值,通过不同的脚本返回值来判断是否出现故障,并发送报警。当然更好的是,可以在挂掉时,尝试自动拉起进程。
参考资料