gunicorn + flask 异步方案采坑记录

因为现在大家都在做测试平台,论坛好多后端使用django,flask的测试平台,大部分情况下Python项目服务是使用gunicorn[或者uwsgi]启动,自动化用例的执行难免会用到异步方案,可能大部分用Celery,但Celery太重,偶尔的异步任务,可以使用multiprocessing 或者是concurrent 或者是协程就可以解决,但异步一不小心就会碰到各种坑,今天大家分享一个采坑。
  1. 需求背景

前端用户输入相关参数后,异步为用户创建一个流水线.

  1. 问题描述

本机运行ok,但是在服务器上,不执行异步任务.

  1. 开发环境+技术栈

开发环境: Mac + Pycharm
服务器:CentOS(以下测试都是在阿里云的CentOS7下验证)
技术栈:Python3.6.6 + Flask + Gunicorn + .....

  1. 定位问题

因为在本地开发,所以直接flask run 启动服务,出现问题后,首先定位到服务器上是使用gunicorn启的服务,本地换成gunicorn启动后测试,果然异步任务也是不执行。在异步方法前后打日志,一步一步的定位,就是卡死在异步任务那儿,既没有报错日志,也没有任何的异常抛出,好像启动的异步线程假死。唯一想到是gunicorn的原因,因为区别就是在这了,各种google,也没找到答案。没办法,看了下gunicorn的源码和gunicorn源码的解析,找到了重要的线索。

先看下项目

目录结构

[root@devops application]# tree
.
├── flask_demo.py
├── gunicorn.conf.py

启动命令

# 服务器上是通过脚本内配置以下命令启动的,本地直接使用以下命令启动
gunicorn --preload -c gunicon.conf.py flask_demo:app

具体代码

# gunicorn.conf.py

bind = ":8000"
workers = 2
# threads = 2
max_requests = 50000
max_requests_jitter = 2
timeout = 70
graceful_timeout = 30
limit_request_line = 8190
limit_request_fields = 200
limit_request_fields_size = 8190

pidfile = "gunicorn.pid"
# user = "admin"
pythonpath = "/Users/admin/CODE/cmdb/"
accesslog = "gunicorn_access.log"
errorlog = "gunicorn_error.log"
loglevel = 'debug'
access_log_format = '%(h)s %(t)s %(l)s %(u)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
daemon = False
# daemon = True
raw_env = "CONFIG_ENV=uat"
# raw_env = "FLASK_CONFIG=dev"
capture_output = True
work_class = "sync"

# flask_demo.py

import logging
from multiprocessing.pool import ThreadPool

from flask import Flask

logging.basicConfig(level=logging.INFO,
                    format="[%(asctime)s] 进程ID:%(process)d 线程ID:%(thread)d %(name)s %(levelname)s [%(module)s:%(lineno)s:%(funcName)s] %(message)s")
logger = logging.getLogger(__name__)

pool = ThreadPool(processes=1)  # 模式①

logger.info('in main thread')

# 需要异步执行的任务
def bar():
    '''
    这个异步任务可能是长耗时或对于时效性要求没那么高的业务场景.
    '''
    logger.info('async task run success')
    return 'task done'


app = Flask(__name__)


@app.route('/')
def hello():
    # pool = ThreadPool(processes=1)  # 模式②
    logger.info('start apply async')
    pool.apply_async(bar)
    logger.info('apply async done')
    
    return 'Hello, World!'

模式①执行失败(在master进程中启线程)

《执行日志》- 注意最后两行日志
[2019-02-27 17:53:20,468] 进程ID:11323 线程ID:139846288471872 flask_demo INFO [flask_demo:13:<module>] in main thread
[2019-02-27 17:53:20 +0800] [11323] [INFO] Starting gunicorn 19.9.0
[2019-02-27 17:53:20 +0800] [11323] [DEBUG] Arbiter booted
[2019-02-27 17:53:20 +0800] [11323] [INFO] Listening at: http://0.0.0.0:8000 (11323)
[2019-02-27 17:53:20 +0800] [11323] [INFO] Using worker: sync
[2019-02-27 17:53:20 +0800] [11366] [INFO] Booting worker with pid: 11366
[2019-02-27 17:53:20 +0800] [11367] [INFO] Booting worker with pid: 11367
[2019-02-27 17:53:20 +0800] [11323] [DEBUG] 2 workers
[2019-02-27 17:53:40 +0800] [11367] [DEBUG] GET /
[2019-02-27 17:53:40,339] 进程ID:11367 线程ID:139846288471872 flask_demo INFO [flask_demo:26:hello] start apply async
[2019-02-27 17:53:40,340] 进程ID:11367 线程ID:139846288471872 flask_demo INFO [flask_demo:28:hello] apply async done



《请求前进程树-ps:不带{}代表进程,带{}代表线程》,可以看到确实是有启动线程
[root@devops application]# pstree -p 11323
gunicorn(11323)─┬─gunicorn(11366)
                ├─gunicorn(11367)
                ├─{gunicorn}(11362)
                ├─{gunicorn}(11363)
                ├─{gunicorn}(11364)
                └─{gunicorn}(11365)
 
 
《请求的结果也正常》             
[root@devops application]# http get http://47.101.49.169:8000/
HTTP/1.1 200 OK
Connection: close
Content-Length: 13
Content-Type: text/html; charset=utf-8
Date: Wed, 27 Feb 2019 09:53:40 GMT
Server: gunicorn/19.9.0

Hello, World!


《请求后的进程树》
[root@devops application]# pstree -p 11323
gunicorn(11323)─┬─gunicorn(11366)
                ├─gunicorn(11367)
                ├─{gunicorn}(11362)
                ├─{gunicorn}(11363)
                ├─{gunicorn}(11364)
                └─{gunicorn}(11365)

模式②执行成功(在worker进程中启线程)

《执行日志》
[2019-02-27 18:04:31,513] 进程ID:11671 线程ID:139621494204224 flask_demo INFO [flask_demo:13:<module>] in main thread
[2019-02-27 18:04:31 +0800] [11671] [INFO] Starting gunicorn 19.9.0
[2019-02-27 18:04:31 +0800] [11671] [DEBUG] Arbiter booted
[2019-02-27 18:04:31 +0800] [11671] [INFO] Listening at: http://0.0.0.0:8000 (11671)
[2019-02-27 18:04:31 +0800] [11671] [INFO] Using worker: sync
[2019-02-27 18:04:31 +0800] [11710] [INFO] Booting worker with pid: 11710
[2019-02-27 18:04:31 +0800] [11711] [INFO] Booting worker with pid: 11711
[2019-02-27 18:04:31 +0800] [11671] [DEBUG] 2 workers
[2019-02-27 18:04:56 +0800] [11711] [DEBUG] GET /
[2019-02-27 18:04:56,860] 进程ID:11711 线程ID:139621494204224 flask_demo INFO [flask_demo:26:hello] start apply async
[2019-02-27 18:04:56,860] 进程ID:11711 线程ID:139621494204224 flask_demo INFO [flask_demo:28:hello] apply async done
[2019-02-27 18:04:56,861] 进程ID:11711 线程ID:139621255997184 flask_demo INFO [flask_demo:17:bar] async task run success


《请求前的进程树》
[root@devops application]# pstree -p 11671
gunicorn(11671)─┬─gunicorn(11710)
                └─gunicorn(11711)
                
《请求结果》
[root@devops application]# http get http://47.101.49.169:8000/
HTTP/1.1 200 OK
Connection: close
Content-Length: 13
Content-Type: text/html; charset=utf-8
Date: Wed, 27 Feb 2019 10:04:56 GMT
Server: gunicorn/19.9.0

Hello, World!

《请求后的进程树》
[root@devops application]# pstree -p 11671
gunicorn(11671)─┬─gunicorn(11710)
                └─gunicorn(11711)─┬─{gunicorn}(11795)
                                  ├─{gunicorn}(11796)
                                  ├─{gunicorn}(11797)
                                  └─{gunicorn}(11798)

重要:

看到区别了吗,在模式2的情况下,启动的新线程是放到了worker进程(主进程:gunicorn(11671)→子进程:gunicorn(11711))下,执行成功。

模式1启动的新线程是放到了master进程(主进程:gunicorn(11323))下,其实就是子进程内线程没办法和主进程下的线程进行通信,执行失败。

而master进程是不负责具体业务代码执行。因为gunicorn就是这么设计的,gunicorn的实现是由一个master进程管理多个worker进程,【所有的请求都是由worker进程处理】,master进程主要向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),自动重新启动新的worker进程. 简单来说就是worker才负责具体代码执行。

总结

解决这个bug现在看很简单,就是把pool = ThreadPool(processes=1)放到request内。但当时解决起来花费很久的时间,不像我们平时的debug,看错误堆栈就可以了, 因为没有任何的异常信息抛出。

‘这个bug解决的方案还有很多中,大家有时间的话可以去尝试下。’

另外说下自身情况,之前在测试行业,一直关注TesterHome,已经转到后端研发,在做DevOps相关的实践,有兴趣可以一起交流, 谢谢大家!

PS:后续可能还会转回到测试,O(∩_∩)O哈哈~

gunicorn启动步骤:
(1)加载worker_class并实例化(默认为同步模型 SyncWorker)
(2)父进程(master进程)fork之后return,之后的逻辑都在子进程中运行
(3)调用worker.init_process 进入循环,worker的所有工作都在这个循环中
(4)循环结束之后,调用sys.exit(0)
(5)最后,在finally中,记录worker进程的退出
gunicorn 概念

Gunicorn“绿色独角兽”是一个被广泛使用的高性能的Python WSGI UNIX HTTP服务器,移植自Ruby的独角兽(Unicorn )项目,使用pre-fork worker模式,具有使用非常简单,轻量级的资源消耗,以及高性能等特点。

参考:

gunicorn Arbiter 源码解析

gunicorn 源码分析

Gunicorn运行与配置


最后,如果有机会和大家分享下,关于gunicorn的的work_class_type配置不同的使用场景
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 作者:詹聪聪 序言: 本人工作中需要用到flask-socketio,在学习英文文档时发现,flask-socke...
    Python中文社区阅读 12,607评论 6 39
  • 快速开始 在安装Sanic之前,让我们一起来看看Python在支持异步的过程中,都经历了哪些比较重大的更新。 首先...
    hugoren阅读 19,441评论 0 23
  • # Python 资源大全中文版 我想很多程序员应该记得 GitHub 上有一个 Awesome - XXX 系列...
    小迈克阅读 2,957评论 1 3
  • 1. Nginx的模块与工作原理 Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单...
    rosekissyou阅读 10,184评论 5 124
  • 不知道是不是很多人都觉得 我也想要好身材啊 我想锻炼身体啊 可是我很怕变成电影电视剧里面的大块头 我不想练成大肌肉...
    苏安禾阅读 398评论 0 0