在 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 管理
- 参考上文,像新工程一样先建个 conda env
- activate 后用
conda install
和pip install
把依赖一个个装进来 - 验证下这些库是否能正常运行
- 生成 environment.yaml 文件(后文详述),提交 SVN
安装 Python 库的方法和原则
将你的 conda env activate 之后:
- 带 C 扩展的 Python 库,**必须** 用 conda 装。比如
mysqlclient
,pillow
,用 conda 装时,conda 会帮你一并安装其依赖,如mysql-connector-c
,libpng
等:conda install mysqlclient pillow
- 不带 C 扩展的 Python 库,用
pip
直接装
对于 MySQL 的 Python 库,建议用 mysqlclient
而非 MySQL-python
。
conda env 环境导入导出
Conda 用 environment.yaml
文件做环境的导入导出。导出的环境配置文件,可以扔到 IDC 机器上以部署出同样的 conda env。
- 导出环境到文件:activate 你的环境后,
conda env export > environment.yaml
- 按文件生成环境:
conda create -n <project_name> -f environment.yaml
- 按文件更新现有环境: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))
注意:
bind
只bind
内网,用 10000+ 的高端口worker_class
只支持sync
和gevent
,sync
是同步模式,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>
。