如何使用AWS,Docker,Nginx + Daphne在生产中正确配置Django通道?

问题描述

我们正尝试通过使用Django Channels 2,AWS和Nginx + daphne在我们的网站上配置实时聊天。我们的设置可以在本地运行良好,但是在部署到生产环境时会遇到问题。

我们的生产环境由两个使用Elastic Container Service(Fargate)部署到AWS的Docker容器组成。前面运行的容器是一个Nginx配置,它充当代理服务器来提供静态文件。第二个容器运行我们的API / Django站点。代理在端口8000上运行,并将传入的请求转发到在端口9000上运行的API / Django容器。我还将注意到,我们正在使用terraform配置我们的AWS环境。

我引用了完成相似设置的多篇文章。例如: https://medium.com/@elspanishgeek/how-to-deploy-django-channels-2-x-on-aws-elastic-beanstalk-8621771d4ff0

但是此设置使用的是我们未使用的Elastic Beanstalk部署。

Image of setup example

代理Dockerfile:

FROM Nginxinc/Nginx-unprivileged:1-alpine
LABEL maintainer='CodeDank'

copY ./default.conf.tpl /etc/Nginx/default.conf.tpl
copY ./uwsgi_params /etc/Nginx/uwsgi_params

ENV LISTEN_PORT=8000
ENV APP_HOST=app
ENV APP_PORT=9000

USER root

RUN mkdir -p /vol/static
RUN chmod 755 /vol/static
RUN touch /etc/Nginx/conf.d/default.conf
RUN chown Nginx:Nginx /etc/Nginx/conf.d/default.conf

copY ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

USER Nginx

CMD ["/entrypoint.sh"]

API /站点Dockerfile:

FROM python:3.7-alpine3.11
LABEL maintainer="CodeDank"

ENV PYTHONUNBUFFERED 1
ENV PATH="/scripts:${PATH}"

RUN pip install --upgrade pip

copY ./requirements.txt /requirements.txt
RUN apk add --update --no-cache postgresql-client jpeg-dev
RUN apk add --update --no-cache --virtual .tmp-build-deps \
        gcc libc-dev linux-headers postgresql-dev \
        musl-dev zlib zlib-dev
RUN apk add --update --no-cache libressl-dev musl-dev libffi-dev
RUN apk add --update --no-cache g++ freetype-dev jpeg-dev
RUN pip install -r /requirements.txt
RUN apk del .tmp-build-deps

RUN mkdir /app
workdir /app
copY ./app /app
copY ./scripts /scripts
RUN chmod +x /scripts/*

RUN mkdir -p /vol/web/media
RUN mkdir -p /vol/web/static
RUN adduser -D user
RUN chown -R user:user /vol/
RUN chmod -R 755 /vol/web
USER user

CMD ["entrypoint.sh"]

(入口点脚本如下所示)

我们创建了一个AWS Elasticache Redis服务器,用作Django通道的CHANNEL_LAYERS后端。 “ REdis_HOSTNAME”环境变量是Redis服务器的端点地址。

# Channels Settings
Asgi_APPLICATION = "app.routing.application"
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer","CONfig": {
            "hosts": [
                (os.environ.get('REdis_HOSTNAME'),6379)
            ],},}

asgi.py文件

import os
import django
from channels.routing import get_default_application


os.environ.setdefault('DJANGO_SETTINGS_MODULE','app.settings')
django.setup()
application = get_default_application()

在Channels文档之后,我们尝试配置daphne以在我们的项目中运行asgi应用程序。理想情况下,我们希望此设置使Nginx代理服务器将所有websocket请求转发到运行在端口9001上的daphne服务器。我们的所有websocket端点都将包含/ ws /,因此Nginx代理配置已如下定义。

default.conf.tpl:

upstream channels-backend {
 server localhost:9001;
}

server {
    listen ${LISTEN_PORT};

    location /static {
        alias /vol/static;
    }

    location / {
        uwsgi_pass              ${APP_HOST}:${APP_PORT};
        include                 /etc/Nginx/uwsgi_params;
        client_max_body_size    4G;
    }

    location /ws/ {

         proxy_pass http://channels-backend;
         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;
    }
}

代理入口点脚本:

#!/bin/sh

set -e

envsubst '${LISTEN_PORT},${APP_HOST},${APP_PORT}' < /etc/Nginx/default.conf.tpl > /etc/Nginx/conf.d/default.conf
Nginx -g 'daemon off;'

API /站点入口点脚本:

#!/bin/sh

set -e

python manage.py collectstatic --noinput
python manage.py wait_for_db
python manage.py migrate

uwsgi --socket :9000 --workers 4 --master --enable-threads --module app.wsgi

daphne -b 0.0.0.0 -p 9001 app.asgi:application

尝试连接到我们网站上的Websocket时,返回502错误

Error during WebSocket handshake: Unexpected response code: 502.

我怀疑daphne服务器未按预期运行,或者未使用Nginx服务器正确配置。在API入口点脚本中,daphne命令是否可以按当前状态运行?或者,是否缺少使daphneNginx代理后面运行所需的所有内容?我最初的想法是daphne命令不能在入口点脚本中的uwsgi命令之后运行。但是,我不确定该命令是否还需要放置在其他位置才能运行daphne进程。

代理的cloudwatch日志不是非常详细,但是尝试连接到网站上的Websocket时,我收到此错误消息。

[error] 8#8: *53700 connect() Failed (111: Connection refused) while connecting to upstream,client: 10.1.1.190,server:,request: "GET /ws/chat/djagno/ HTTP/1.1",upstream: "http://127.0.0.1:9001/ws/chat/djagno/",host: "mycustomdomain.net"

我已经看到解决此问题的其他方法,其中不包括使用Nginx代理将Websocket流量定向到daphne。也许我们的方法不是最好的解决方案?我们欢迎其他配置。

任何反馈将不胜感激。谢谢!

解决方法

我想到的一件事是,您是否正在扩展nginx容器?为了使websocket正常工作,您可能需要在Application Load Balancer上启用会话粘性。

参考: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#sticky-sessions

,

既然您提到您将 Terraform 用于 AWS 部署,我将检查您的 AWS 安全组的配置,特别是您在 EC2 实例和 Elasticache Redis 之间设置安全组的位置。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticache_cluster

编辑:乍一看,我刚刚注意到您是如何启动 uwsgi 和 daphne 的。你现在这样做的方式是在前台启动 uwsgi,然后这个过程只是等待而 daphne 永远不会启动(因此出现 502 错误)。

改变

uwsgi --socket :9000 --workers 4 --master --enable-threads --module app.wsgi

daphne -b 0.0.0.0 -p 9001 app.asgi:application

uwsgi --socket :9000 --workers 4 --master --enable-threads --module app.wsgi & daphne -b 0.0.0.0 -p 9001 app.asgi:application

这将在后台启动 uwsgi,然后继续启动 Daphne。

如果您需要一种方法来杀死两者,您可以在脚本中运行它,然后在最后添加一个 wait,这样当您杀死脚本时,uwsgi 和 daphne 进程也会被杀死。否则,您可以考虑使用 systemd 或 supervisor 来守护 uwsgi 和 daphne 初创公司。

,

这里可能存在一些问题。在处理 websocket 请求时,我发现的第一件事是它们在您的服务器上的行为与在 localhost 上的行为不同。我不得不根据 Django、Django Channels、Daphne 等的版本在几个不同的区域修改我的 Django Channels 逻辑。

例如:当我们升级到 Channels 3.0 时,我们无法在没有 database_sync_to_async() 装饰器的情况下访问我们的数据库,并且不得不将调用卸载到它们自己的单独函数中。

检查您的 routing.py 是否有像 AllowHostsOriginValidator 这样的请求阻止器。

如果您使用自定义中间件,范围对象会根据您的环境和您访问数据的方式而有所不同。

此外,尝试通过 unix 套接字在守护进程之外运行 Daphne,如下所示:

daphne -u /etc/supervisor/socks/daphne.sock --fd 0 --access-log - --proxy-headers project.asgi:application -v 3

如果您想试一试,我们使用以下设置。

负载均衡nginx配置:

upstream mywebapp {
    server front_end_ip:port;
    }

#This upgrades the connection for websockets from https to websocket
map $http_upgrade $connection_upgrade {
    default   upgrade;
    ''        close;
    }
location /ws/ {
    add_header X-debug-message "The /ws/ location was served from the ascend load balancer" always;
    proxy_pass http://mywebapp/ws/;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "connection_upgrade";
    proxy_read_timeout 86400;
    }

前端nginx配置:

upstream mybackend {
    server  django_server_ip:port;
}

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

location /ws/ {
    add_header X-debug-message "The /ws/ location was served from angular 1" always;
    proxy_pass http://mybackend/ws/;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "connection_upgrade";
    proxy_read_timeout 86400;

Django 服务器 nginx 配置:

upstream daphne {
    server 0.0.0.0:9001;
}

location /ws/ {
    add_header X-debug-message "The /ws/ location was served from daphne" always;
    proxy_pass http://daphne;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_cache_bypass $http_upgrade;
}