Python系列:Python Flask + Gunicorn + Docker 的日志输出设置以及Gunicorn的使用手册看这篇就够了




一. Python Flask + Gunicorn + Docker 的日志输出设置

我们一个项目使用了 Python Flask 框架来实现 Web 服务,之前的日志输出一直有问题。而从项目需求、运行维护出发,正确的日志输出对使用者来说都是非常重要的。

这里完整的整理了从 开发 Flask 时的日志设置,到生产环境使用 Gunicorn 运行 Flask 的日志设置 以及 使用 Docker 容器化部署的日志输出 的全部细节。

普通 Flask 日志设置

Flask 本身使用 Python 的 logging 模块来实现日志记录、输出,以一个最简单的 Flask 应用代码为例:

import logging
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def default_route():
    """Default route"""
    app.logger.debug('this is a DEBUG message')
    app.logger.info('this is an INFO message')
    app.logger.warning('this is a WARNING message')
    app.logger.error('this is an ERROR message')
    app.logger.critical('this is a CRITICAL message')
    return jsonify('hello world')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=True)

运行程序:

$ pip install flask

$ python app.py

 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:8000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 265-991-043

访问 http://localhost:8000/ ,程序会在命令行窗口输出:

[2022-12-21 15:46:29,113] DEBUG in app: this is a DEBUG message
[2022-12-21 15:46:29,113] INFO in app: this is an INFO message
[2022-12-21 15:46:29,113] WARNING in app: this is a WARNING message
[2022-12-21 15:46:29,113] ERROR in app: this is an ERROR message
[2022-12-21 15:46:29,113] CRITICAL in app: this is a CRITICAL message
127.0.0.1 - - [21/Dec/2022 15:46:29] "GET / HTTP/1.1" 200 -
输出日志文件

我们可以使用 logging 模块的不同输出处理器(Handler)来实现标准输出、文件输出或邮件提醒。例如添加日志文件:

import logging
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def default_route():
    """Default route"""
    app.logger.debug('this is a DEBUG message')
    app.logger.info('this is an INFO message')
    app.logger.warning('this is a WARNING message')
    app.logger.error('this is an ERROR message')
    app.logger.critical('this is a CRITICAL message')
    return jsonify('hello world')

if __name__ == '__main__':
    # Define log level and log file
    app.debug = True
    handler = logging.FileHandler('flask.log')
    app.logger.addHandler(handler)

    app.run(host='0.0.0.0', port=8000, debug=True)

访问 http://localhost:8000 ,我们可以看到在当前目录下生成了 flask.log 日志文件:

$ ls
app.py  flask.log
$ cat flask.log
this is a DEBUG message
this is an INFO message
this is a WARNING message
this is an ERROR message
this is a CRITICAL message
按天分割、调整格式

我们常见的日志文件一般按天分割,这样方便查找。另外日志里还需要记录一些必须的时间等值:

import logging
from flask import Flask, jsonify
from logging.handlers import TimedRotatingFileHandler

app = Flask(__name__)

@app.route('/')
def default_route():
    """Default route"""
    app.logger.debug('this is a DEBUG message')
    app.logger.info('this is an INFO message')
    app.logger.warning('this is a WARNING message')
    app.logger.error('this is an ERROR message')
    app.logger.critical('this is a CRITICAL message')
    return jsonify('hello world')

if __name__ == '__main__':
    app.debug = True
    formatter = logging.Formatter(
        "[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s][%(thread)d] - %(message)s")
    handler = TimedRotatingFileHandler(
        "flask.log", when="D", interval=1, backupCount=30,
        encoding="UTF-8", delay=False, utc=False)

    handler.setFormatter(formatter)
    app.logger.addHandler(handler)

    app.run(host='0.0.0.0', port=8000, debug=True)
TimedRotatingFileHandler 的参数设置:
  • when=D: 按天进行切分,还可以按秒(S)、分钟(M)、小时(H)、星期时间(‘W0’-‘W6’,W0=周一,此时 interval 不起作用)、半夜时间(midnight)

  • interval=1: 每1天切分,这个值根据上面的 when 来确定单位

  • backupCount=30:保留30天的日志

  • encoding=UTF-8: 使用 UTF-8 的编码

  • delay=False:如果为 True,文件打开会被推迟到第一次调用 emit()

  • utc=False:使用当前系统时间而非 UTC 时间

Formatter 的格式可以参考 https://docs.python.org/2/library/logging.html#logrecord-attributes

再次访问 http://localhost:8000 ,日志文件输出变成了:

[2022-12-21 16:17:05,825][app.py:10][DEBUG][139836122048256] - this is a DEBUG message
[2022-12-21 16:17:05,826][app.py:11][INFO][139836122048256] - this is an INFO message
[2022-12-21 16:17:05,826][app.py:12][WARNING][139836122048256] - this is a WARNING message
[2022-12-21 16:17:05,827][app.py:13][ERROR][139836122048256] - this is an ERROR message
[2022-12-21 16:17:05,827][app.py:14][CRITICAL][139836122048256] - this is a CRITICAL message
引入蓝图 BluePrint

稍微复杂一点的 Flask 程序,可能会使用 BluePrint 来对不同功能模块进行拆分。而使用 BluePrint 后的 Flask 程序,其日志的调用方式有少许变化。

.

├── api
│   └── test.py
├── app.py
└── flask.log

我们在当前项目目录下新建一个 api 目录,然后在其下创建 test.py ,这里我们创建了一个名为 testBlueprint 对象,并且添加了路由 /test 到其下的 show() 方法。这里要获得 Flask 的应用对象,就需要用它的 current_app 来实现对 app 的引用了。

from flask import Blueprint, jsonify, current_app

test = Blueprint('test', __name__)

@test.route('/test', methods=('GET', 'POST'))
def show():
    current_app.logger.debug('Test DEBUG message')
    current_app.logger.info('Test INFO message')
    current_app.logger.warning('Test WARNING message')
    current_app.logger.error('Test ERROR message')
    current_app.logger.critical('Test CRITICAL message')
    return jsonify('Show test')

然后对 app.py 进行修改,把上面的 test 蓝图注册到 app 对象里,并且添加路由前缀 /api

import logging

from flask import Flask, jsonify
from logging.handlers import TimedRotatingFileHandler

app = Flask(__name__)

from api.test import test
app.register_blueprint(test, url_prefix='/api')

@app.route('/')
def default_route():
    """Default route"""
    app.logger.debug('this is a DEBUG message')
    app.logger.info('this is an INFO message')
    app.logger.warning('this is a WARNING message')
    app.logger.error('this is an ERROR message')
    app.logger.critical('this is a CRITICAL message')
    return jsonify('hello world')

if __name__ == '__main__':
    app.debug = True
    formatter = logging.Formatter(
        "[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s][%(thread)d] - %(message)s")
    handler = TimedRotatingFileHandler(
        "flask.log", when="D", interval=1, backupCount=30,
        encoding="UTF-8", delay=False, utc=False)

    handler.setFormatter(formatter)
    app.logger.addHandler(handler)

    app.run(host='0.0.0.0', port=8000, debug=True)

我们分别访问 http://localhost:8000http://localhost:8000/api/test ,可以看到返回正确的结果,日志文件变成了:

[2022-12-21 17:30:34,627][app.py:14][DEBUG][140282256979712] - this is a DEBUG message
[2022-12-21 17:30:34,628][app.py:15][INFO][140282256979712] - this is an INFO message
[2022-12-21 17:30:34,628][app.py:16][WARNING][140282256979712] - this is a WARNING message
[2022-12-21 17:30:34,629][app.py:17][ERROR][140282256979712] - this is an ERROR message
[2022-12-21 17:30:34,629][app.py:18][CRITICAL][140282256979712] - this is a CRITICAL message
[2022-12-21 17:30:50,946][test.py:7][DEBUG][140282256979712] - Test DEBUG message
[2022-12-21 17:30:50,947][test.py:8][INFO][140282256979712] - Test INFO message
[2022-12-21 17:30:50,947][test.py:9][WARNING][140282256979712] - Test WARNING message
[2022-12-21 17:30:50,947][test.py:10][ERROR][140282256979712] - Test ERROR message
[2022-12-21 17:30:50,949][test.py:11][CRITICAL][140282256979712] - Test CRITICAL message

至此,Flask 的日志一切都运转良好。然后我们在生产服务器上部署的时候,现在常常会使用 Gunicorn 来运行,这时候的日志输出就有问题了,日志文件没有内容写入。

使用 Gunicorn 运行的 Flask 日志设置

Gunicorn 有自己的日志记录器,它通过本身的机制控制日志级别。我们只能通过配置 Gunicorn 的日志设定,来实现我们的需求。同时,需要对上面例子应用的日志处理器设置进行调整。

修改 app.py,注意我们添加的 if __name__ != '__main__' 这部分,就是在 Gunicorn 运行时,让 Flask 使用它的日志处理器。

import logging

from flask import Flask, jsonify
from logging.handlers import TimedRotatingFileHandler

app = Flask(__name__)

from api.test import test
app.register_blueprint(test, url_prefix='/api')

@app.route('/')
def default_route():
    """Default route"""
    app.logger.debug('this is a DEBUG message')
    app.logger.info('this is an INFO message')
    app.logger.warning('this is a WARNING message')
    app.logger.error('this is an ERROR message')
    app.logger.critical('this is a CRITICAL message')
    return jsonify('hello world')

# make app to use gunicorn logger handler
if __name__ != '__main__':
    gunicorn_logger = logging.getLogger('gunicorn.error')
    app.logger.handlers = gunicorn_logger.handlers
    app.logger.setLevel(gunicorn_logger.level)

if __name__ == '__main__':
    app.debug = True
    formatter = logging.Formatter(
        "[%(asctime)s][%(filename)s:%(lineno)d][%(levelname)s][%(thread)d] - %(message)s")
    handler = TimedRotatingFileHandler(
        "flask.log", when="D", interval=1, backupCount=30,
        encoding="UTF-8", delay=False, utc=False)

    handler.setFormatter(formatter)
    app.logger.addHandler(handler)

    app.run(host='0.0.0.0', port=8000, debug=True)

参考 Gunicorn 日志设置,我们在启动 Gunicorn 时可以使用下面的命令行:

# 创建日志目录
$ mkdir -p /var/log/gunicorn/

# 赋予日志目录当前用户权限
$ chown tattoo:tattoo /var/log/gunicorn/

# 运行 gunicorn
$ gunicorn -w 2 \
        -b 0.0.0.0:5000 \
        --log-level debug \
        --log-file /var/log/gunicorn/gunicorn.log \
        app:app

运行后,访问 http://localhost:5000http://localhost:5000/api/test ,我们得到 gunicorn.log 日志:

[2022-12-21 17:59:05 +0800] [32174] [DEBUG] Current configuration:
  config: ./gunicorn.conf.py
  wsgi_app: None
  bind: ['0.0.0.0:5000']
  backlog: 2048
  workers: 2
  worker_class: sync
  threads: 1
  worker_connections: 1000
  max_requests: 0
  max_requests_jitter: 0
  timeout: 30
  graceful_timeout: 30
  keepalive: 2
  limit_request_line: 4094
  limit_request_fields: 100
  limit_request_field_size: 8190
  reload: False
  reload_engine: auto
  reload_extra_files: []
  spew: False
  check_config: False
  print_config: False
  preload_app: False
  sendfile: None
  reuse_port: False
  chdir: /home/tattoo/workspace/flask
  daemon: False
  raw_env: []
  pidfile: None
  worker_tmp_dir: None
  user: 1000
  group: 1000
  umask: 0
  initgroups: False
  tmp_upload_dir: None
  secure_scheme_headers: {'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}
  forwarded_allow_ips: ['127.0.0.1']
  accesslog: None
  disable_redirect_access_to_syslog: False
  access_log_format: %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"
  errorlog: /var/log/gunicorn/gunicorn.log
  loglevel: debug
  capture_output: False
  logger_class: gunicorn.glogging.Logger
  logconfig: None
  logconfig_dict: {}
  syslog_addr: udp://localhost:514
  syslog: False
  syslog_prefix: None
  syslog_facility: user
  enable_stdio_inheritance: False
  statsd_host: None
  dogstatsd_tags:
  statsd_prefix:
  proc_name: None
  default_proc_name: app:app
  pythonpath: None
  paste: None
  on_starting: <function OnStarting.on_starting at 0x7f2ae3bbd4c0>
  on_reload: <function OnReload.on_reload at 0x7f2ae3bbd5e0>
  when_ready: <function WhenReady.when_ready at 0x7f2ae3bbd700>
  pre_fork: <function Prefork.pre_fork at 0x7f2ae3bbd820>
  post_fork: <function Postfork.post_fork at 0x7f2ae3bbd940>
  post_worker_init: <function PostWorkerInit.post_worker_init at 0x7f2ae3bbda60>
  worker_int: <function WorkerInt.worker_int at 0x7f2ae3bbdb80>
  worker_abort: <function WorkerAbort.worker_abort at 0x7f2ae3bbdca0>
  pre_exec: <function PreExec.pre_exec at 0x7f2ae3bbddc0>
  pre_request: <function PreRequest.pre_request at 0x7f2ae3bbdee0>
  post_request: <function PostRequest.post_request at 0x7f2ae3bbdf70>
  child_exit: <function ChildExit.child_exit at 0x7f2ae3bd10d0>
  worker_exit: <function WorkerExit.worker_exit at 0x7f2ae3bd11f0>
  nworkers_changed: <function NumWorkersChanged.nworkers_changed at 0x7f2ae3bd1310>
  on_exit: <function OnExit.on_exit at 0x7f2ae3bd1430>
  proxy_protocol: False
  proxy_allow_ips: ['127.0.0.1']
  keyfile: None
  certfile: None
  ssl_version: 2
  cert_reqs: 0
  ca_certs: None
  suppress_ragged_eofs: True
  do_handshake_on_connect: False
  ciphers: None
  raw_paste_global_conf: []
  strip_header_spaces: False
[2022-12-21 17:59:05 +0800] [32174] [INFO] Starting gunicorn 20.1.0
[2022-12-21 17:59:05 +0800] [32174] [DEBUG] Arbiter booted
[2022-12-21 17:59:05 +0800] [32174] [INFO] Listening at: http://0.0.0.0:5000 (32174)
[2022-12-21 17:59:05 +0800] [32174] [INFO] Using worker: sync
[2022-12-21 17:59:05 +0800] [32176] [INFO] Booting worker with pid: 32176
[2022-12-21 17:59:05 +0800] [32177] [INFO] Booting worker with pid: 32177
[2022-12-21 17:59:05 +0800] [32174] [DEBUG] 2 workers
[2022-12-21 17:59:11 +0800] [32177] [DEBUG] GET /
[2022-12-21 17:59:11 +0800] [32177] [DEBUG] this is a DEBUG message
[2022-12-21 17:59:11 +0800] [32177] [INFO] this is an INFO message
[2022-12-21 17:59:11 +0800] [32177] [WARNING] this is a WARNING message
[2022-12-21 17:59:11 +0800] [32177] [ERROR] this is an ERROR message
[2022-12-21 17:59:11 +0800] [32177] [CRITICAL] this is a CRITICAL message
[2022-12-21 17:59:17 +0800] [32177] [DEBUG] GET /api/test
[2022-12-21 17:59:17 +0800] [32177] [DEBUG] Test DEBUG message
[2022-12-21 17:59:17 +0800] [32177] [INFO] Test INFO message
[2022-12-21 17:59:17 +0800] [32177] [WARNING] Test WARNING message
[2022-12-21 17:59:17 +0800] [32177] [ERROR] Test ERROR message
[2022-12-21 17:59:17 +0800] [32177] [CRITICAL] Test CRITICAL message

你也可以参考 https://docs.python.org/3/library/logging.config.html ,使用 Gunicorn 的 --log-config FILE 选项,用一个日志配置文件来做更精细的日志调整。

对日志文件进行分割

Gunicorn 自身不对日志进行分割,所以我们可以使用 logrotate 工具来实现日志分割的需求。

创建 /etc/logrotate.d/gunicorn 文件:

/var/log/gunicorn/gunicorn.log {
    daily
    dateext
    dateformat -%Y-%m-%d
    dateyesterday
    rotate 90
    missingok
    notifempty
}

  • daily 每天执行,也可以是 weekly 或 monthly

  • dateext 使用当期日期作为命名格式

  • dateformat 日志文件命名格式

  • rotate 分割次数,如果设置的是daily,就代表保留多少天的记录

如果要立即执行: logrotate -f gunicorn

使用 Docker 部署 Gunicorn + Flask 应用并输出日志

我们在服务器上配置 Python 环境,常常会碰到各种各样的麻烦,而使用现成的 Docker 镜像就很方便,它在各种环境下的行为都能保持一致。

添加 Dockerfilerequirements.txtconf/supervisor_flask.conf 文件:

.

├── api
│   └── test.py
├── app.py
├── conf
│   └── supervisor_flask.conf
├── Dockerfile
└── requirements.txt
Dockerfile

我们使用 alpine 基础镜像以减小镜像体积。使用 supervisord 来后台启动 Gunicorn + Flask。日志输出到 /var/log/flask-app 目录。

FROM python:3-alpine

WORKDIR /home/app

USER root

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
    # upgrade pip
    && pip config set global.index-url https://mirrors.aliyun.com/pypi/simple \
    && pip install --upgrade pip \
    # install supervisor gunicorn
    && pip install supervisor gunicorn \
    && mkdir -p /etc/supervisor \
    && /usr/local/bin/echo_supervisord_conf > /etc/supervisor/supervisord.conf \
    && echo "[include]" >> /etc/supervisor/supervisord.conf \
    && echo "files = /etc/supervisor/*.conf" >> /etc/supervisor/supervisord.conf \
    # get curl for healthchecks
    && apk add curl \
    # add log folder
    && mkdir -p /var/log/flask-app

COPY conf/supervisor_flask.conf /etc/supervisor

# copy all the files to the container
COPY . .

# python setup
RUN pip install -r requirements.txt

# define the port number the container should expose
EXPOSE 5000

CMD ["supervisord","-n","-c","/etc/supervisor/supervisord.conf"]
requirements.txt

注意 markupsafe 2.1.0 移除了 soft_unicode,不指定较低版本的话会导致 Flask 报错。

markupsafe==2.0.1
Flask~=1.1.4
conf/supervisor_flask.conf
[supervisord]
nodaemon=true

[program:flask]
command=gunicorn -w 2 -b 0.0.0.0:5000 --log-level debug --log-file /var/log/flask-app/gunicorn.log app:app
directory=/home/app
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/dev/stdout
构建镜像、运行容器

使用下面的命令构建镜像 flasktest

docker build -t flasktest .

运行,这里我们把日志输出目录映射到了宿主机的 /home/logs 目录:

docker run --name flask \
    -it -d \
    -p 5000:5000 \
    -v /home/logs:/var/log/flask-app \
    flasktest

运行起来后,docker logs -f flask 查看一下容器输出有没有问题,正常的应该是:

$ docker logs -f flasktest
2022-12-22 03:33:32,368 CRIT Supervisor is running as root.  Privileges were not dropped because no user is specified in the config file.  If you intend to run as root, you can set user=root in the config file to avoid this message.
2022-12-22 03:33:32,368 INFO Included extra file "/etc/supervisor/supervisor_flask.conf" during parsing
2022-12-22 03:33:32,368 INFO Included extra file "/etc/supervisor/supervisord.conf" during parsing
2022-12-22 03:33:32,371 INFO RPC interface 'supervisor' initialized
2022-12-22 03:33:32,371 CRIT Server 'unix_http_server' running without any HTTP authentication checking
2022-12-22 03:33:32,372 INFO supervisord started with pid 1
2022-12-22 03:33:33,374 INFO spawned: 'flask' with pid 6
2022-12-22 03:33:34,376 INFO success: flask entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)

访问 http://localhost:5000http://localhost:5000/api/test ,我们可以在 /home/logs 目录下找到 gunicorn.log 日志,跟前面的没有什么区别

二 .Gunicorn的使用手册看这篇就够了

Gunicorn是什么

Gunicorn 是一个 Python 的 WSGI HTTP 服务器。它所在的位置通常是在反向代理(如 Nginx)或者 负载均衡(如 AWS ELB)和一个 web 应用(比如 Django 或者 Flask)之间。它是一个移植自Ruby的Unicorn项目的pre-fork worker模型,即支持eventlet也支持greenlet。 如果对Flask框架还有不清楚的地方,可以查看本文一分钟学会Flask框架的安装与快速使用 Gunicorn启动项目之后一定会有一个主进程Master和一个或者多个工作进程。工作进程的数量可以指定。工作进程是实际处理请求的进程。主进程是维护服务器的运行。
在这里插入图片描述

Gunicorn的安装

安装

# 安装最新版本的gunicorn
pip install gunicorn
#安装指定版本的gunicorn
pip install gunicorn==19.9.0
#异步模式
pip install gevent==1.4.0

查看Gunicorn的版本

pip show gunicorn

运行Gunicorn

  gunicorn [OPTIONS] $(MODULE_NAME):$(VARIABLE_NAME)

(MODULENAME):(VARIABLE_NAME) 表示要启动的WSGI_app。其中MODULE_NAME对应的是python文件名VARIABLE_NAME对应web应用实例。举个🌰吧!!!这里定义一个名为manage.py 文件,在其内部创建Flask应用

#manage.py
from flask import Flask
def create_app():
    app = Flask(__name__)
    ....
    return app
app = create_app()
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001)

那么,用gunicorn启动该Flask应用的命令就是:

  gunicorn --workers=2 manage:app

其中workers用于指定工作进程数。 当然也可以通过直接调用create_app方法

#manage.py
from flask import Flask
def create_app():
    app = Flask(__name__)
    ....
    return app

那么,用gunicorn命令直接调用create_app方法就是:

  gunicorn --workers=2 manage:create_app()

Gunicorn的配置参数说明[OPTIONS]

在实际开发中一般情况下都会用一个单独的python配置文件来配置gunicorn的启动参数。gunicorn的参数有很多,这里就重点介绍一下在实际开发中经常需要用到的一些配置。

1.1 配置文件config

命令是:-c CONFIG--config=CONFIG 这个命令是指定Gunicorn的配置文件,配置文件的作用就是将我们需要的配置写到该配置文件中。 默认的配置文件的名称是 ./gunicorn.conf.py,一般是将该配置文件放在项目的根目录下。就像下面这样

在这里插入图片描述

这里设置gunicorn绑定的IP地址端口号worker数量以及线程数。 启动该项目的命令是: gunicorn -c gunicorn.conf.py manage:app
在这里插入图片描述

1.2 Debugging

命令 --reload=True,或者在gunicorn.conf.py 文件中加上reload=True 配置。 这样的话,当代码变动时gunicorn就可以重启了。

1.3 Logging

Logging的日志有两种,一种是业务日志info,一种是错误日志error。后面再补充。

1.4 指定进程名称

命令 -n proc_name 或者--name=APP_NAME 用于指定gunicorn进程的名称 默认情况下通过 gunicorn 启动项目之后,进程的名称是gunicorn。需要注意的是这个进程的名称不同于应用的名称。 如下:gunicorn -c gunicorn.conf.py --name=test_app manage:app 指定进程的名称是 test_app
在这里插入图片描述

1.5 客户端地址(server socket)

命令行 -b ADDRESS 或者--bind ADDRESS 默认的IP和端口号是 127.0.0.1:8000 bind 配置主要用于指定应用绑定的IP和端口号。 格式是HOST,HOST:PORT,unix:PATH或者fd://FD, 需要注意的是IP地址必须是一个合法的IP地址 例如:gunicorn -b 0.0.0.0:8111 manage:app。指定端口号为8111。

在这里插入图片描述

1.6 工作进程数(Worker Processes)

命令行-w INT 或者--workers INT。 默认的进程数是1。 这个配置用于指定处理请求的工作进程的数量,单核机器上这个数量一般在2-4个之间。你需要找到最适合该服务器的进程数。 例如:gunicorn -w 5 manage:app, 表示启动5个工作进程。每个工作进程都可以多线程执行任务。

在这里插入图片描述

1.7 工作模式(worker_class)

命令行-k STRING或者–worker-class STRING 默认的工作模式是sync,即同步的工作模式。 一共有五种工作模式,分别是 sync, eventlet, gevent, tornado, gthread 。 下面就分别介绍下这些工作模式。

1. sync 模式(同步工作模式)

这是最基本的工作模式,也是默认的工作模式,线程为native类型。即请求先来后到,排队模式。
在这里插入图片描述

2. eventlet 模式(协程异步)

eventlet 工作模式是基于eventlet库,利用python协程实现的。.

要使用该工作模式的话必须先安装eventlet库,并且版本要大于等于0.24.1 安装命令是:

pip install eventlet
3. gevent模式(协程异步)

gevent是基于Greentlet库,利用python协程实现的。

安装命令是:

pip install gevent Gunicorn

允许通过设置对应的worker类来使用这些异步Python库。这里的设置适用于我们想要在单核机器上运行的gevent:

gunicorn --worker-class=gevent -w 2  manage:app

在这里插入图片描述

4. tornado模式

tornado利用python Tornado框架来实现。

安装命令是:

pip install tornado 

安装的tornado库的版本要大于等于0.2。

5. gthread模式

gthread采用的是线程工作模式,利用线程池管理连接,需要安装gthread库。

安装命令是:

pip install gthread。 

Gunicorn允许每个worker拥有多个线程。

在这种场景下,Python应用程序每个worker都会加载一次,同一个worker生成的每个线程共享相同的内存空间。

为了在 Gunicorn 中使用多线程。我们使用了 gthreads 模式,指定threads参数。

gunicorn --workers=5 --threads=2 main:app

该命令等同于:

gunicorn --workers=5 --threads=2 --worker-class=gthread main:app

该例中里面的最大并发请求数是 worker*线程,也就是10。

指定threads参数的话则工作模式自动变成gthread模式。

1.8线程数(threads)

命令行--threads INT 默认每个worker一个线程执行。 表示每个工作进程处理请求的线程数。指定threads参数的话则工作模式自动变成gthread模式

1.9 工作线程连接数(worker_connections)

命令行:--worker-connections INT 默认值是:1000 该参数的含义是:每个工作线程同时存在的连接数,该参数仅在EventletGevent 两种工作模式下有效。

gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 main:app

worker-connections 是对于 gevent worker 类的特殊设置。

(2CPU)+1 仍然是建议的workers 数量。

因为我们仅有一核,我们将会使用 3 个worker。

在这种情况下,最大的并发请求数量是3000。(3个worker1000个连接/worker)

1.10 最大请求数(max_requests)

命令行--max-requests INT 默认值是0

该参数的含义是:在重启之前工作进程(worker)能处理的最大请求数,任何一个大于0的值都会限制工作进程(worker)在重启之前处理的请求数量,这是一种帮助限制内存泄漏的简单方法。 如果该值设置为0(默认值)那么工作进程的自动重启是被禁用

1.11 超时设置(timeout)

命令行-t INT 或者--timeout INT 默认值是30秒

工作进程在超过设置的超时时间内没有响应将会被杀死并重启。

当值为0就表示禁用超时设置。

通常,默认的30秒就够了,只有当确定对同步工作进程有影响时才会修改该值。

对于非同步工作进程,这意味着工作进程仍在运行,不受处理单个请求所需的时间的限制。

1.12 连接的存活时间(keepalive)

命令行--keep-alive INT 默认值是2 连接超时时间,通常设置在1-5秒范围内。

工作模式的补充说明

worker指定为gevent或者evenlet类型时,线程变成基于Greentlettask(伪线程),这时候线程数量threads参数是无效的。

使用gevent模式会出现一些兼容性问题。

使用gevent时,系统会使用monkey patch。系统的部分函数会被修改, 有些库会兼容gevent的类型,

例如,任务调度的库apschedulerweb socket需要socketio的库等,需要专门选择gevent的函数。 而有些库则直接无法使用,例如多进程multiprocess。 例如,在一个api请求中,如果需要使用多核cpu资源,采用multiprocess进行多进程计算。则会出现卡死的问题。gevent中,不能使用multiprocess库

性能实践

  1. 如果这个应用是I/O受限,通常可以通过使用**“伪线程”(gevent或asyncio)的工作模式**来得到最佳性能。正如我们了解到的,Gunicorn通过设置合适的worker类并将workers数量调整到(2*CPU)+1来支持这种编程范式。

  2. 如果这个应用是CPU受限,那么应用程序处理多少并发请求就并不重要,唯一重要的是并行请求的数量。因为Python’s GIL,线程和’伪线程’并不能以并行模式执行,可以将worker的数量改成CPU的核数,理解到最大的并行请求数量其实就是核心数。这时候适合的工作模式是sync工作模式。

  3. 如果不确定应用程序的内存占用,使用多线程以及相应的gthread worker类会产生更好的性能,因为应用程序会在每个worker上都加载一次,并且在同一个worker上运行的每个线程都会共享一些内存,但这需要一些额外的CPU消耗。

  4. 如果你不知道你自己应该选择什么就从最简单的配置开始,就只是 workers 数量设置为 (2*CPU)+1 并且不用考虑 多线程。从这个点开始,就是所有测试和错误的基准环境。如果瓶颈在内存上,就开始引入多线程。如果瓶颈在 I/O 上,就考虑使用不同的 Python 编程范式。如果瓶颈在 CPU 上,就考虑添加更多内核并且调整 workers 数量。







Tangramor

Python Flask + Gunicorn + Docker 的日志输出设置

无艳影

Gunicorn的使用手册看这篇就够了

  • 23
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坦笑&&life

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值