实现简单的监控脚本(Bash的执行和异常捕获)

当我们需要监控服务运行状态时,一般的策略是写定时脚本,定时执行探测服务状态,如果出现预期外情况,就报警。那么第一步我们就需要学会写一个监控脚本,这里我们会讲到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进程是否存在,输出不同的值,通过不同的脚本返回值来判断是否出现故障,并发送报警。当然更好的是,可以在挂掉时,尝试自动拉起进程。

参考资料

  1. Bash 脚本 set 命令教程:http://www.ruanyifeng.com/blo...
  2. 官方手册:https://www.gnu.org/software/...
  3. linux中shell变量$#,$@,$0,$1,$2的含义解释:https://www.cnblogs.com/fhefh...
  4. linux manpage: command --help
阅读 794发布于 6月11日
推荐阅读

三年百度研发,了解一些前端、后端、服务器、数据库、产品等内容,大家一起来交流下。

11 人关注
24 篇文章
专栏主页
目录