1.背景说明
一信息门户网站,输入身份证号可查询相关信息,因此使用Selenium自动化完成该操作。使用flask将脚本制作成接口给业务方调用,flask通过Gunicorn管理。
2.踩坑经过
2.1 发现问题
服务部署上线后部署服务的Docker隔三差五重启,报错多为超时。经观察,该门户网站(非国内)网络经常不稳定,周末基本访问不了、周内偶尔响应慢,疑似为超时导致Gunicorn杀掉worker进程,进一步查看后台浏览器数量时,发现数量非常多,占用大量资源。
# 查看浏览器数量
ps -ef|grep chrome |awk -F' ' '{print $2}'| wc -l
# 手动清理
ps -ef|grep chrome |awk -F' ' '{print $2}'|xargs kill -9
为了不让野浏览器占用太多资源,我写了个脚本自动清理,并配置crontab每分钟检查
#!/bin/bash
# 获取chrome进程数量
chrome_num=$(ps -ef | grep chrome | awk '{print $2}' | wc -l)
# 判断数量是否超过100
if [ $chrome_num -gt 100 ]
then
ps -ef | grep chrome | awk '{print $2}' | xargs kill -9
fi
2.2 初次尝试解决
上一节中解决的办法非常粗暴,就是长出来的浏览器全部剪掉,而全部消掉会导致原本运行中的服(被)务(业)受(务)影(方)响(骂)。因此需要分析一下浏览器越长越多的原因:推测为Gunicorn过于粗暴的kill -9导致原本打开浏览器的进程被杀掉,而其启动的浏览器并未销毁。
找到了原因,试图说服Gunicorn温柔一点,毕竟kill -9我捕获不了
gunicorn -w 5 -b 0.0.0.0:8787 --graceful-timeout 20 --timeout 30 core.app.rest_service:app
结果:温柔的一刀和跳起来劈一刀都是kill
2.3 利用python自带的回收机制
由于并不理解python的回收机制,我傻傻的相信,只要许愿给gpt就会有好结果。于是我尝试了一下__del__
方法
class Browser:
def __init__(self, path: str, header: bool = False):
self.path = path
self.header = header
self.core_driver = self.init_browser(path, header)
def __del__(self):
self.exit_browser()
def exit_browser(self):
if self.core_driver is not None:
self.close_browser()
logger.warning(f'browser #{os.getpid()} exit')
结果:gpt有时候自己都会编不下去
2.4 利用python的内置函数atexit
我不太理解为什么__del__
方法没有起作用,查了些资料和论坛,大家都不觉得自己写__del__
方法是个好主意,既然如此那我就用自带的函数好了,于是查到了atexit
class Browser:
def __init__(self, path: str, header: bool = False):
self.path = path
self.header = header
self.core_driver = self.init_browser(path, header)
atexit.register(self.exit_browser) # 新增了一行这个
def __del__(self):
self.exit_browser()
def exit_browser(self):
if self.core_driver is not None:
self.close_browser()
logger.warning(f'browser #{os.getpid()} exit')
按照我的理解,python就是读英语,读到啥就是啥意思,那么这个atexit就是退出时候执行。那么我超时退出的时候也会执行对吧?
结果:程序是被杀了,不是退出了
2.5 重写超时设置
此刻的我已经对sig有了非常深刻的理解,从温和善良的kill -2到超脱三界外的kill -9我都有了不亚于大一计算机实习生的理解(尝试捕获kill -9发现不可能)
原来,atexit神功没有问题,只是Gunicorn过于狠辣,让我还没施展出来就将进程杀掉。既然如此,只要我自己退出就没有人能杀了我
def _timeout_handler(signum, frame):
logger.critical(f'#{os.getpid()} TIMEOUT')
exit()
signal.signal(signal.SIGALRM, _timeout_handler)
def timeout_deco(timeout):
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
signal.alarm(timeout)
result = func(*args, **kwargs)
signal.alarm(0)
return result
return wrapper
return deco
相应的服务做个修改
@app.route("/spider", methods=['POST'])
@timeout_deco(int(os.getenv('timeout', 20)))
def query():
t0 = time.time()
try:
response = ...
except Exception:
logger.exception(f'REST_ERROR: {request.json}')
t1 = time.time()
logger.info({'pid': os.getpid(), 'use_time': (t1 - t0), 'request': request.json, 'response': response})
return json.dumps(response, ensure_ascii=False, indent=2)
在外面再包一层超时、参数检验的服务,大功告成