踩坑:使用 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。

Comments

發佈留言

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

To respond on your own website, enter the URL of your response which should contain a link to this post’s permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post’s URL again. (Find out more about Webmentions.)