技术流水账一篇,记录踩过的坑

Channels 异构

Django Channels 官方文档宣称 channels 的最佳配置是使用其自带的服务器组件 Daphne,但在开发中我发现 daphne 处理普通请求比在 WSGI 架构下慢了好几倍,更何况使用 daphne 派发静态文件是十分不切实际的。于是我将 http.requestwebsocket.* 两个 channel 解耦,前者使用 nginx 配合 uwsgi 处理,后者使用 nginx 反向代理至 daphne 处理。这样一来便可充分利用两种架构的优势。

旧架构:

新架构:

环境

  • Ubuntu Server 16.04(与开发环境相同)
  • python 3.5.2
  • virtualenv & virtualenvwrapper
  • nginx
  • uwsgi
  • redis(作为缓存和 Channel Layer)

Channels Session-based Authentication

channels 有提供基于 HTTP Session 的认证方式,但由于 websocket 和 http 在此不是同域请求(端口号不同),主域名下的 cookies 不会随 websocket 请求发送。故在发起 websocket 链接时要带上一个 GET 参数 session_key。在模板中该参数可由 { {request.session.session_key} } 获得。

uwsgi

安装:

$ sudo pip install uwsgi # 全局安装

编写配置文件 /etc/uwsgi/sites/hsfzmun.ini

[uwsgi]
uid = git
chdir = /home/git/hsfzmun/server
module = config.wsgi:application
home = /home/git/virtualenvs/hsfzmun # Virtualenv 路径
master = true
processes = 10
socket = /var/www/hsfzmun.sock # 使用 Unix Socket 与 nginx 通信
chmod-socket = 666
vacuum = true

配置 systemd 服务(uwsgi.service):

[Unit]
Description=uWSGI Module

[Service]
ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown git:www-data /run/uwsgi'
ExecStart=/usr/local/bin/uwsgi --emperor /etc/uwsgi/sites --touch-reload=/home/git/hsfzmun/server/uwsgi_params
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

uwsgi 有个优点:可以通过 --touch-reload 参数简洁地重启服务,这样只需一条 touch 命令便可以完成新代码的部署。

daphne

daphne 有两个模块:Interface Server 和 Workers。前者负责处理 Websocket、long-polling 等请求,并将其抽象化为 channels 传递给 Workers,Workers 则负责执行具体的业务逻辑。这么做有一个好处就是 降低了底层与业务逻辑的耦合度,即使业务层崩溃也不会使连接断开,同时也减少了新代码部署对整个系统的印象。部署新代码时只需重启 Workers 即可。

daphnei.service

[Unit]
Description=Daphne Interface Server

[Service]
ExecStart=/home/git/cmd/daphnei
Restart=always
KillSignal=SIGQUIT
Type=simple
NotifyAccess=all

[Install]
WantedBy=multi-user.target

daphnew.service

[Unit]
Description=Daphne Worker

[Service]
ExecStart=/home/git/cmd/daphnew
Type=simple
KillSignal=SIGQUIT
Restart=always
NotifyAccess=all

[Install]
WantedBy=multi-user.target

此处踩了一个坑:起初 Type 处的值是 Notify,因为我是仿照 uwsgi.service 编写这两个文件的。Notify 型服务要求主进程自行通知 systemd 自己的启动/停止状态,但 daphne 没有这样的机制,从而导致 systemctl 阻塞,并以 90 秒为周期不断重启服务。幸好 StackOverflow 上的大神指出了这个错误。

这里我将具体的服务脚本独立出来,因为 ExecStart 不支持编写多行语句。

cmd/daphnei

#!/bin/bash
cd /home/git/hsfzmun/server
/home/git/virtualenvs/hsfzmun/bin/daphne -b 0.0.0.0 -p 8001 -v2 config.asgi:channel_layer --access-log=/var/www/daphnei.log

cmd/daphnew

#!/bin/bash
cd /home/git/hsfzmun/server
/home/git/virtualenvs/hsfzmun/bin/python manage.py runworker --threads 5 --only-channels=websocket.*

此处 daphne 有个 bug:理论上 verbosity 不应该影响程序的行为,但如果不加 -v2 参数 nginx 会报 502 Bad Gateway

nginx

此处的难点是反向代理 websockets,因为 nginx 默认不识别 websocket 协议。为了能正确指定协议头,只能将所有 websocket 请求路由到某一子路径下。

nginx.service

server {
listen 80;
server_name <ip> product;

# 静态文件
location /static {
alias /var/www/static;
}

# 用户上传的文件
location /m {
alias /var/www/media;
}

# uwsgi 反向代理
location / {
include /var/www/uwsgi_params;
uwsgi_pass unix:/var/www/hsfzmun.sock;
}

# daphne 反向代理
location /ws {
proxy_pass http://0.0.0.0:8001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_read_timeout 36000s;
proxy_send_timeout 36000s;
}
}

Git 自动化部署

去年的 WiseCity 3.0 采用 Github 中转的方式进行部署,这对 Github 的稳定性有很高的要求。这次则将生产服务器作为 Git Server,使用 Git Hooks 进行自动化部署,大大提高了生产效率。

创建 git 用户:

$ useradd git
$ passwd git

设置 SSH authorized_keys(略)。

创建仓库:

$ cd ~
$ git init --bare hsfzmun.git

创建钩子 hooks/post-receive

#!/bin/bash
# Checkout Repository
GIT_WORK_TREE=/home/git/hsfzmun git checkout -f

# Activate Virtualenv
cd /home/git/hsfzmun/server
export LC_ALL=C
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
export WORKON_HOME=~/virtualenvs
source /usr/local/bin/virtualenvwrapper.sh
workon hsfzmun

# Internationalization
./manage.py compilemessages

# Collect Static Files
./manage.py collectstatic --noinput

# Migrate Databases
./manage.py migrate

# Restart uWSGI
touch ./uwsgi_params

# Restart Daphne Workers
sudo systemctl restart daphnew --no-block

echo "All operations done."

此钩子会在每次 push 之后执行,自动更新静态文件和数据库表并重启相关服务。

本地添加远程仓库(略)。