Python: Deployment

 20th August 2020 at 2:19pm

在 FM 工作时写过一套脚本用来运行 Python Web 程序。它包含:

  • 安装 Conda,同时配置 PyPi 镜像,但是目前漏了 .pydistutilrc
  • Python 工程的依赖维护(用 Conda 的 environment.yaml
  • 类 initscript 的 Gunicorn 启动停止脚本

代码在 Snippets: Python: Initscript-like scripts for Gunicorn with Conda。下文的内容是之前为项目写的 wiki,一些逻辑针对内部包发布系统而设计,外部用时需要改造下。

维护 Conda 环境

首先你应该按 Python 全攻略 页面,搞定「准备开发环境」部分。

新的 Python 工程

先新建一个 conda env。如果你要用 Python 2,依赖项写 python=2.7.*;如果用 Python 3,依赖项直接写 python 即可:

/usr/local/services/conda-1.0/bin/conda create -n <project_name> "python=2.7.*"

这样会在 /data/conda/envs/<project_name> 处安装一个新的 conda env。

安装完后,每次你要操作这个环境,你就执行:

source /usr/local/services/conda-1.0/bin/activate <project_name>

之后你就可以在这个环境安装新的 Python 库,或者跑 Django 的 debug server 等等。

依赖装好后,注意生成 environment.yaml 文件并提交 SVN。

旧 Python 工程想用 conda 管理

  1. 参考上文,像新工程一样先建个 conda env
  2. activate 后用 conda install 和 pip install 把依赖一个个装进来
  3. 验证下这些库是否能正常运行
  4. 生成 environment.yaml 文件(后文详述),提交 SVN

安装 Python 库的方法和原则

将你的 conda env activate 之后:

  1. 带 C 扩展的 Python 库,**必须** 用 conda 装。比如 mysqlclientpillow,用 conda 装时,conda 会帮你一并安装其依赖,如 mysql-connector-clibpng 等: conda install mysqlclient pillow
  2. 不带 C 扩展的 Python 库,用 pip 直接装

对于 MySQL 的 Python 库,建议用 mysqlclient 而非 MySQL-python

conda env 环境导入导出

Conda 用 environment.yaml 文件做环境的导入导出。导出的环境配置文件,可以扔到 IDC 机器上以部署出同样的 conda env。

  1. 导出环境到文件:activate 你的环境后, conda env export > environment.yaml
  2. 按文件生成环境:conda create -n <project_name> -f environment.yaml
  3. 按文件更新现有环境:activate 你的环境后,conda env update -f environment.yaml

Python 项目部署

这块的逻辑都在 runner.py 中实现。

这套代码要求,你的业务织云包的根目录,必要有一个 environment.yaml。参考上文以生成 environment.yaml

Web 服务

Web 服务采用 nginx 作反向代理,搭配 gunicorn 的做法。这样各管理端后台可以分开部署跟升级,不会绑在 Apache 一条大船上。

代码上,如果你是 Django 项目,要在 settings.py 中加上 USE_X_FORWARDED_HOST = True 这个配置。

首先,安装好 Conda(用 install.sh)。

将你的项目打包好,在根目录需要有几个文件:

  • environment.yaml
  • gunicorn_conf.py
  • nginx.conf

gunicorn_conf.py

按 Gunicorn 官方提供的配置能力写,参考 这里。长得像这样:

from setproctitle import setproctitle

bind = "127.0.0.1:13001"
workers = 4
worker_class = 'gevent'


def post_fork(server, worker):
    setproctitle("{} worker".format(server.proc_name))

注意:

  • bindbind 内网,用 10000+ 的高端口
  • worker_class 只支持 syncgeventsync 是同步模式,gevent 是协程

setproctitle 的原因是,织云包发布会检查进程名,但是 gunicorn 默认的 -n 选项设置的 proctitle 像这样:

  • gunicorn: master [radio_api_server]
  • gunicorn: worker [radio_api_server]

织云会认为进程名是 gunicorn:,这样没法跟其他的 gunicorn 实例区分开(比如 gunicorn: master [radio_contents])。不得已 hack 一把,在 post_fork 时把 worker 进程的 proctitle 改了。

master 进程的 proctitle 没法改,看 gunicorn 源码,没有好的 hack 入点。Monkey patch 也不好搞。

然后配置下启动、停止、reload 脚本:

./runner.py -p $install_path -t gunicorn --app-module radio_api_server.wsgi <start|stop|reload>
sleep 3

因为我们用信号来控制 Gunicorn,比如 SIGTERM 让 Gunicorn graceful shutdown,sleep 下避开包发布系统进程数检查报错。

弄好后在包发布系统上启动、停止、热重启分别试试,上机器观察进程起来没有、起来的时间等,再 wget 127.0.0.1:13001 简单测试下通不通。

nginx.conf

长这样:

server {
    listen          #IP_INNER:80;
    server_name     fm-api.oa.com;

    location /static/ {
        root    #INSTALL_PATH/;
    }

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;

        proxy_pass http://127.0.0.1:13001;
        proxy_redirect off;
    }
}

最后,做 Log Rotation,事情做完整,不用半夜起来清磁盘。

nginx 容器本身的配置

http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  logs/access.log  main;

    sendfile        on;
    tcp_nopush     on;

    keepalive_timeout  65;

    gzip  on;

    # 重点是这里
    include /usr/local/services/*/nginx.conf;
}

避免升级操作带来的问题

  • 升级时勾掉「升级前停止」,没必要停,升级完后重启即可
  • 要重启时多用热重启

脚本

先安装 Conda。再将你的脚本打织云包,在「安装完成时运行」「升级完成时运行」等 hook 处,填入:

./runner -p <your_project_root> prepare

这个命令会帮你准备 Conda env 到 /data/conda/envs/<project_name>

运行时的命令,类似 /data/conda/envs/<project_name>/bin/python <your_script>