1. 先说答案

对于像我这种没有耐心的同学,我这里先直接给出答案。这里先说明几个前提:

  1. 假设要运行的脚本名称为 handle.py,通常该脚本里面是一个循环运行的程序,对应着某个服务或者处理端,当然,也可以只是一个简单的程序,这个不重要,重要的是我们需要该脚本一直在运行,如果脚本因为某些不可抗拒的因素终止了,需要能够检测到并且自动重启该脚本;
  2. 脚本直接运行的命令为:python3 action.py,而且需要运行在Linux系统下,这个没什么说的;
  3. 我们的目的:通过一个监测程序监测 action.py 的脚本的运行情况,如果该脚本没有运行则启动该脚本;如果该脚本运行,则无操作。核心就是监测程序 monitor.py,代码如下:
# 脚本运行监测脚本,monitor.py
import subprocess
import datetime
import time

while True:
    '''
        通过查看当前进程来判断处理程序是否还活着
    '''
    res = subprocess.Popen('ps -ef | grep handle.py', stdout=subprocess.PIPE, shell=True)
    attn = res.stdout.readlines()
    '''
        attn的长度大于等于2
        如果为2,代表图像检测程序异常;
        如果大于等于2,代表一切正常;
    '''
    if len(attn) == 2:
        print('%s 处理程序异常,重启...' % datetime.datetime.now())
        subprocess.Popen('python3 handle.py', stdout=open('./handle.log', 'a+'), shell=True)
        with open('./monitor.log', 'a+') as f:
            f.write('%s 处理程序异常,重启...\n' % datetime.datetime.now())
    else:
        print('%s handle.py 正常...' % datetime.datetime.now())

    # 休眠五秒钟,然后再次检测脚本运行
    time.sleep(5)

至于 handle.py,这个大家随便写点什么就行,最好写一个死循环,不然程序运行终止之后会自己再次启动,不利于演示,为了完整性,这里仍然给出一个案例,如下:

# 具体执行的动作脚本,handle.py
import datetime
import time

while True:
    print('%s - 处理正常...' % datetime.datetime.now())
    time.sleep(2)

运行效果展示

  1. 直接运行 python3 handle.py,效果如下图所示,因为是死循环,这里只截图一部分
  2. 运行 python3 monitor.py 效果如下图,

    这里说明一下,因为我手动关闭了 handle.py,所以最开始有一个处理程序异常的提示,如果一开始handle.py 就是正常运行的,那么就不会有这个提示,与此同时,handle.py 的运行结果输出到日志文件 handle.log 里面去了。此时系统中出现了一个 python3 handle.py 的进程,如下图

    我们杀掉 3640这个进程,kill 3640,此时monitor.py的运行终端有新的提示

    程序重启之后重新查询进程可以发现 python3 handle.py 又出现了,并且 python3 handle.py 是 python3 monitor.py 的子进程,如下图

    到此为止,对于脚本运行监控的目的就算达到了。不过这个演示有一个小小的缺陷,那就是 handle.log 并没有内容,换言之 handle.py 中的 print 输出结果并没有写入到这个文件中(这个应该是我没弄明白,就是subprocess.Popen中的stdout这个参数对应到底是什么输出,有兴趣的同学可以自己研究一下subprocess这个模块),我自己实际运行的脚本是有输出的,不过这个也不重要,重要的是当 handle.py 对应着一项服务时,我们能够保证系统服务的不间断性,当然,这里是一个简易结果,仅仅是保证可用,接下来我们将详细说明如何解决这个问题。

2. 问题概述

最近在使用Mask RCNN做目标检测,为了保证目标检测速度,使用了消息队列,在消息队列的Consumer端将模型加载到内存中,然后在队列处理函数中使用该模型,避免反复加载,大大压缩了目标检测的时间。但是有一个问题,就是程序每运行10个小时左右就会崩溃一次,异常的原因我不是很明白,可能需要去读TensorFlow的源代码,这里也希望知晓相关解决方案的读者能提供帮助,而且这个错误并不是每次都完全一样的,这里我贴出一个错误如下,也查过相关资料,好像是由于显存异常引起的错误,并且这个错误无法被try/catch异常捕获,所以这就成了一个很致命的问题,只要出现该错误,Consumer端的处理脚本直接崩溃,然后消息队列中任务堆积,同时无法拿到数据处理结果。

2019-05-08 16:30:53.913949: F ./tensorflow/core/framework/tensor.h:643] 
Check failed: new_num_elements == NumElements() (5 vs . 20)
3. 解决之道 - 脚本监控

为了解决这个问题,借鉴服务监控的思想,尝试去监控该运行脚本,只要检测不到该脚本的运行,立刻重新运行该脚本。
在shell端我们通过会通过 ps -ef | grep xxx 来查看进程,进而判断其运行情况,这里也一样,只不过我们需要将这个命令用代码的形式来运行,于是就想到了 os.system() 和 os.popen() 这两个函数,其中 os.system() 是阻塞的,os.open()是非阻塞的,但是通过 os.popen() 可以拿到命令的执行结果;在查询这两个命令的使用期间又了解到 subprocess模块,且该模块可以用来替换前述两个函数,所以最终就采用了 subprocess.Popen() 这个方法,然后我们给出如下测试代码:

# 脚本运行监测 v1 - monitor1.py
import subprocess

res = subprocess.Popen('ps -ef | grep handle.py', stdout=subprocess.PIPE, shell=True)
attn = res.stdout.readlines()
print(attn)
  1. 首先不启动 handle.py,运行结果如下图
  2. python monitor python monitor process_重启

  3. 从上图可以看出,attn是一个数组,长度为2,两个进程的内容也很明确,分别是
/bin/sh -c ps -ef | grep handle.py
grep handle.py
  1. 然后运行 handle.py,并再次运行 monitor1.py,效果如下图所示
  2. python monitor python monitor process_python_02

  3. 从上图可以看出,attn的长度为3,除了上述两个进程仍然存在,还多了一个 python3 handl3.py 的进程,这个就是我们手动启动的进程。
  4. 监测程序升级版本
    根据上述测试结果,不难看出,当我们的脚本正常运行时,通过 monitor.py 可以得到3个进程;而如果脚本异常退出(如kill或者其它未知因素),那么 monitor.py 的输出就是2个进程,接下来就简单了,一个简单的 if 判断就可以了,为了做到持续监测,配合while循环即可,所以监测的框架就出来了,如下
# 脚本运行监测 v2
import subprocess
import datetime
import time

while True:
    '''
        通过查看当前进程来判断处理程序是否还活着
    '''
    res = subprocess.Popen('ps -ef | grep handle.py', stdout=subprocess.PIPE, shell=True)
    attn = res.stdout.readlines()
    '''
        attn的长度大于等于2
        如果为2,代表图像检测程序异常;
        如果大于等于2,代表一切正常;
    '''
    if len(attn) == 2:
        # 重启脚本
        subprocess.Popen('python3 handle.py', stdout=open('./handle.log', 'a+'), shell=True)
    else:
        print('%s handle.py 正常...' % datetime.datetime.now())

    # 休眠五秒钟,然后再次检测脚本运行
    time.sleep(5)

到了这个版本,跟最终的结果就非常接近了,最主要的几个测试也已经在最初的答案说明中给出来了,重点是 subprocess.Popen方法,其实这个脚本很简单,但是用到了两次 subprocess.Popen 函数,而且两次的参数是有差异的

总结

脚本监控的思想来源于服务监控,现在系统开发向着SOA(Service-Oriented Architecture)的方向,服务的注册、发现及其监控都已经模块化了,在这里借鉴其思想,进行服务的监控,写出了这么个脚本,希望对大家能有所帮助。