踩坑:使用 Nginx + uwsgi 反向代理容器化 Django + Celery 應用

爲了讓更多的應用上 OpenShift,首先需要對應用打包。淺羽這幾天就遇到一個 Django 應用,仍然跑在屁眼通紅 Python 2.7 下,並且使用 Celery 實作異步並且通過 RabbitMQ 通訊。那麼遇到這種問題,首先肯定想到的是把 Django (uwsgi) 和 RabbitMQ 分離爲兩個容器。學習了一個 Celery 知識之後,發現 Celery Worker 也應該可以分到別的容器去,相互之間通過 RabbitMQ 通訊調度,於是容器化的基本架構就定下來了。

打包 Docker 其實不是很難的事情,不過因爲不熟悉 Django 的架構與使用,淺羽還是踩了不少坑。

(後面還有:Python 依賴安裝、uwsgi 設定、Django 設定)

 

首先,Python 的官方 Docker 鏡像有好幾種 Base(甚至還有 Windows Server Core),考慮到鏡像的大小、定製方便和習慣,淺羽採用的是 python:2-alpine3.8 鏡像。項目採用的是 Pipfile 即 pipenv 管理依賴,於是需要先從 pip 安裝一個。考慮到後續寫執行指令稿的方便問題,原先 pipenv 整合的 virtualenv 就顯得過於麻煩了,反正一個容器滿足一個應用的依賴就好了,於是可以從 Pipfile 部署到全局:

# pipenv lock
# pipenv install --system --deploy

但是這樣又有一個問題:部署的過程中看不到裝了什麼包的什麼版本。雖然依賴裏應該鎖好版本,而且只會在 docker build 的過程中安裝,但是總給人一種不安心感;加 –verbose 參數,又太過於嚇人(不明白的話試試就知道了)。於是換一個解決方法:

# pipenv lock -r | pip install --no-cache-dir -r /dev/stdin

這樣就能夠滿足看到安裝過程的奇怪願望,又不至於太嚇人了。

接下來,Django 自己可以 runserver,但是作爲生產部署的話還是應該用 wsgi 的方式執行;如果 Docker 來調試的話,……誰幹得出來這種事情?所以還需要安裝 uwsgi 並且設定好 Nginx 反代。在 Alpine Linux 下,可以直接通過包管理器安裝:

# apk add --no-cache nginx uwsgi uwsgi-python

接着要寫 uwsgi 的設定檔:

[uwsgi]
# Django-related settings
socket          = /tmp/app.sock
# the base directory (full path)
chdir           = /var/www/
# Django s wsgi file
plugin          = python
pythonpath      = /usr/local/lib/python2.7/site-packages/
module          = app.wsgi:application
# process-related settings
# master
master          = true
# maximum number of worker processes
processes       = 4
# ... with appropriate permissions - may be needed
uid             = nginx
gid             = www-data
# clear environment on exit
vacuum          = true

其中兩點特別需要注意的,也是淺羽踩到的坑:第一是需要設定 plugin,載入 python 擴展(對應 Python 2);第二是需要設定好 pythonpath,到全局的包安裝位置,否則可能會導致 uwsgi 帶起 Django 的時候出現 ModuleNotFoundError

設定完成之後,發現 Django 居然還起不來,在 Django 包內部出現 SyntaxError: invalid syntax。仔細排查之後,發現 Django 2.0 已經不支援 Python 2,正在使用的是最後支援 Python 2 的 1.11 版本;安裝的 django-filter 組件同時支援 Django 1.11 和 Django 2,但只能運行在 Python 3 上,於是出現語法錯誤。鎖定 django-filter 的版本到 “<2.0” 解決問題。

隨後又出現 ImportError: Could not import ‘rest_framework.filters.DjangoFilterBackend’ for API setting ‘DEFAULT_FILTER_BACKENDS’. AttributeError: ‘module’ object has no attribute ‘DjangoFilterBackend’,搜尋到一個 django-shop 的 issue 之後,鎖定 djangorestframework 到 “==3.6.3” 解決問題。

最後,記得 Django 項目需要在 settings.py 中設定:

ALLOWED_HOSTS = [‘*’]

否則可能會出現拒絕訪問的問題。設定好 Nginx:

server {
    listen      80 default_server;
    listen      [::]:80 default_server;
    server_name _;

    root       /var/www;

    sendfile        on;
    client_max_body_size 32M;
    keepalive_timeout  0;
    large_client_header_buffers 4 16k;

    location /static {
        alias /var/www/static/dist;
    }

    location / {
        include /etc/nginx/uwsgi_params;
        uwsgi_pass unix:/tmp/app.sock;
    }
}

最後設定好容器入口:在 App 容器中,需要運行

nginx
uwsgi --ini /path/to/uwsgi.ini

帶起應用並且建立反代;另外同一份鏡像的容器中,運行

cd /path/to/django/project/
celery worker -A app -B -n localhost -l info --uid `id -u nginx`

以執行 Celery Worker。然後完成剩下的入口指令稿,應該就沒問題了。
最後執行容器,App 與各個 Celery Worker 之間透過 RabbitMQ 溝通,所以只需要額外設定好一個 RabbitMQ(也有 Alpine-based 的容器!),並且連接各個容器與 RabbitMQ 即可,其他容器之間不需要 link。


發表於

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

此網站使用 Akismet 以減少垃圾留言。 瞭解你留言資料會被如何處理.