ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django部署全解析:Nginx、WSGI、中间件与静态文件一次说透

Django部署全解析:Nginx、WSGI、中间件与静态文件一次说透 Django写起来是真的快但一聊到部署上线Nginx、WSGI、中间件、静态文件这几个词儿一股脑砸过来很多朋友就懵了。我见过不少项目开发环境跑得飞起一上服务器就各种404、403、样式全丢原因基本都出在对这几个基础概念的认知上。这篇笔记不整虚的就把Django从开发到上线这条链路里的核心概念一次说透把这几个东西到底怎么配合、各自负责什么、常见的坑在哪掰开揉碎讲清楚。不管你是在用Django写接口还是在折腾前后端分离或者是准备自己上手部署项目这篇文章都值得你从头到尾过一遍。1. 先搞清楚一条请求是怎么从浏览器跑到Django的很多教程一上来就讲Nginx怎么配、WSGI怎么选但如果你心里没有一张完整的请求链路图配置来配置去都是瞎调。我习惯先把整个地图画出来后面所有概念都能挂在这张图上。1.1 一条完整链路上的五个角色你在浏览器输入域名按下回车到你看到页面中间大概经过这么几个环节浏览器 - Nginx - WSGI服务器 - Django应用 - 数据库/静态文件每个角色各管一段Nginx站在最前面的大管家。它接收所有进来的HTTP请求处理静态文件图片、CSS、JS做HTTPS证书终止负载均衡然后把动态请求转交给后面的WSGI服务器。WSGI服务器Gunicorn或uWSGI负责把Nginx转交过来的请求按照WSGI协议包装成Python能理解的对象调用Django应用拿到响应后再原路返回。Django应用真正干活的人。根据URL路由找到对应的视图函数操作数据库渲染模板或者返回JSON。中间件不是独立的一层服务器而是挂在Django应用上的钩子。请求在进入视图之前、响应在离开视图之后都会从中间件队列里过一遍。静态文件本质上是磁盘上的文件跟Django进程无关。谁来提供它们取决于环境开发时Django顺手管了生产环境应该交给Nginx。这张图看着简单但90%的部署问题都出在某个环节没按照这个分工去配置。比如有人把静态文件交给Django去serve生产环境一下就卡死有人让Nginx直接proxy到Django自带的runserver结果一压测就崩。1.2 Django自带runserver到底能干什么Django的python manage.py runserver自带一个轻量服务器它内部实现了WSGI协议的调用所以开发时你不需要额外装WSGI服务器。但它的设计目标只是开发调试单进程、单线程性能极其有限。不处理并发同一时间只能处理一个请求。没有安全加固不能暴露在公网。支持自动reload方便改代码就生效。我见过有人在服务器上nohup python manage.py runserver 0.0.0.0:80 就把项目跑起来了这种做法在内部工具或演示环境勉强能用一旦有几个人同时访问响应速度立刻拉垮而且debug模式开着等于把源码和配置全部暴露出去。所以生产环境务必要用Nginx Gunicorn/uWSGI这套组合。1.3 为什么中间非要插一个Nginx有人问WSGI服务器比如Gunicorn也能处理HTTP请求为什么前面还要Nginx这不是脱裤子放屁而是各司其职才能保证性能和安全静态文件的处理效率Nginx处理静态文件是C语言级别的IO优化随便就能跑到每秒几万次请求而让Python进程去读文件返回每个连接都要占用一个worker成本高太多。连接缓冲与慢请求隔离Nginx会把慢客户端比如网速极差的手机用户的请求缓冲住不会让Python进程干等着。它把连接管理得妥妥帖帖Gunicorn只跟Nginx建立短平快的本地连接。安全与证书HTTPS的SSL握手非常消耗CPU放在Nginx这一层做证书终止后端走明文HTTP都行证书更新也只需要reload Nginx。多服务复用一台服务器上可能同时跑着Django和另一个Java服务Nginx根据路径或者域名把请求分流到不同端口这就是反向代理的价值。顺带一提热词里有个java微服务中间件选择redis的问题。这里要澄清一下Django说的中间件Middleware跟Java微服务体系里说的消息中间件/缓存中间件Redis、Kafka、RabbitMQ是两个完全不同的东西。Django中间件是框架内处理请求和响应的钩子组件不是独立的软件服务。这个概念混淆是我见过的新手迷惑点之一先在这里把它摘干净。2. WSGI不是黑魔法搞懂它你就能玩转Gunicorn和uWSGIWSGIWeb Server Gateway Interface是Python Web应用的事实标准协议。它的核心思想很简单Web服务器或网关和Python应用之间约定好一种调用方式大家按规矩办事互相解耦。2.1 WSGI协议的三个核心约定WSGI协议规定了三个角色服务器端、应用端、中间件。应用端是一个可调用对象函数或类实例接收两个参数并返回一个可迭代对象def application(environ, start_response): # environ: 包含所有请求信息的字典 # start_response: 一个回调函数用来发送HTTP状态码和响应头 status 200 OK headers [(Content-Type, text/plain; charsetutf-8)] start_response(status, headers) return [bHello, WSGI!]你去看Django项目里的wsgi.py本质上就是在暴露一个名为application的可调用对象import os from django.core.wsgi import get_wsgi_application os.environ.setdefault(DJANGO_SETTINGS_MODULE, myproject.settings) application get_wsgi_application()服务器端的工作是解析HTTP请求把请求信息填进environ字典然后在合适的时机调用application(environ, start_response)拿回响应体后拼装成HTTP响应发回客户端。中间件在WSGI层面是一个既像服务器又像应用的包装器它可以在请求到达Django之前做预处理或者在响应离开之后做后处理。不过在实际Django开发中你基本不需要自己写WSGI层中间件框架自带的中间件系统已经覆盖了99%的需求。2.2 生产环境用Gunicorn还是uWSGI这是我被问得最多的问题之一。两个都是成熟的WSGI服务器但定位略有不同对比项GunicornuWSGI语言PythonC语言实现配置复杂度低命令行参数就能搞定中高配置文件多性能够用配合gevent可达上万并发更高但调优有门槛生态新一代Django项目默认首选老牌方案配合Nginx的uwsgi协议运维进程管理简单功能多但排查问题要学的多我的建议是新项目无脑选Gunicorn。它的基本用法就一条命令gunicorn myproject.wsgi:application -w 4 -b 127.0.0.1:8000 --timeout 60关键参数-w 4worker进程数经验公式是2 * CPU核心数 1。你服务器是2核就起4~5个worker是4核就起8~9个。-b 127.0.0.1:8000只监听本地端口千万不要绑0.0.0.0暴露给公网外网访问全部走Nginx。--timeout 60worker超时时间。如果你的接口里有耗时操作比如导出Excel、调用第三方API需要在Nginx和Gunicorn两边同时把超时时间调大。--threads 4每个worker起多个线程。CPU密集型任务用多进程IO密集型任务可以混用线程一般进程不要太多线程可以适当加。如果你用uWSGI常见命令长这样uwsgi --http 127.0.0.1:8000 --wsgi-file myproject/wsgi.py \ --master --processes 4 --threads 2也可以配置成通过.ini文件管理适合有复杂调优需求的场景。2.3 Gunicorn部署时最容易漏的两个头Gunicorn部署好之后Django的视图本身能跑通但你很快会发现两个问题问题一IP地址全变成127.0.0.1因为Nginx把请求转发给Gunicorn后Gunicorn看到的客户端IP自然就是Nginx本机。解决方法是Nginx转发时加上真实客户端IP的头同时Django端要信任这些头location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://127.0.0.1:8000; }Django的ALLOWED_HOSTS要包含你服务器的域名和IP否则生产环境一访问就报DisallowedHost错误。这是新手最常见的一个坑开发环境开着DEBUG没感觉关了DEBUG全都白屏。问题二HTTPS环境下Django还以为是HTTP那个X-Forwarded-Proto $scheme头就是干这个的。Django需要知道原始请求是HTTP还是HTTPS否则生成绝对链接重定向、PasswordReset邮件时会给出一堆http://地址。如果要让Django彻底信任这些头在settings.py里加上USE_X_FORWARDED_HOST True SECURE_PROXY_SSL_HEADER (HTTP_X_FORWARDED_PROTO, https)注意SECURE_PROXY_SSL_HEADER开启了之后如果Nginx没配X-Forwarded-Proto访问会报CSRF相关的错。这个设置要跟Nginx配置成对使用别只配一边。3. Nginx配置里那几个要命的location规则Nginx本身是一个大学问但Django项目部署需要的只是其中一小块反向代理 静态文件。这一节只讲你用得上的东西。3.1 一份能直接跑的Django站点配置先给一份我在服务器上常用的基础配置基于Nginx的server块server { listen 80; server_name example.com www.example.com; # 静态文件和上传文件直接由Nginx处理 location /static/ { alias /opt/myproject/staticfiles/; expires 30d; access_log off; } location /media/ { alias /opt/myproject/media/; expires 30d; } # 其余请求全部转发给Gunicorn location / { proxy_pass http://127.0.0.1:8000; 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-Proto $scheme; proxy_connect_timeout 60s; proxy_read_timeout 60s; } }这份配置里最值得讲的是/static/这一个location块就能解决热词里vscode写img标签在django的static文件中显示不了这类问题的80%。3.2 location匹配规则alias和root到底哪个对很多同学在这里栽跟头核心就是没弄懂alias和root的区别root是指定根目录Nginx会用完整URL路径去拼接目录比如root /opt/static;配合请求/static/style.css实际查找的是/opt/static/static/style.css。alias是指定这个location对应的实际目录比如alias /opt/myproject/staticfiles/;配合请求/static/style.css实际查找的是/opt/myproject/staticfiles/style.css。用location /static/ { alias /opt/myproject/staticfiles/; }URL里的/static/被alias的目录替换掉这正是Django的STATIC_URL和STATIC_ROOT的天然搭配。如果你错用了root会让Nginx去找一个多套了一层static的路径结果全是404。另外location还有一个匹配优先级问题简单记忆是精确匹配 前缀匹配^~ 正则匹配~ 普通前缀匹配。日常Django部署你只需要确保/static/和/media/这两个前缀location存在且放在location /之前就不会有问题。3.3 静态文件403的排查方向热词里有一串跟Nginx 403相关的记录Django项目里遇到403通常有以下原因原因一目录权限不足Nginx进程通常是nginx用户或www-data用户对静态目录没有读权限。可以用一句命令解决chown -R nginx:nginx /opt/myproject/staticfiles/ chmod -R 755 /opt/myproject/staticfiles/原因二SELinux拦截在CentOS/RHEL类系统上即使文件权限全对SELinux也可能拦截Nginx读取非标准目录。检查SELinuxgetsebool httpd_read_user_content如果状态是off开启一下setsebool -P httpd_read_user_content 1原因三index指令找不到默认文件如果你直接访问/static/目录不带具体文件名Nginx默认转发给location /于是又去了Django报400或403。更合理的做法是先指定静态目录内自动索引需求但生产环境一般不建议开autoindex on除非你确实需要文件浏览服务。3.4 改完配置怎么安全生效改Nginx配置最忌讳直接重启。正确流程是nginx -t # 测试配置文件语法 nginx -s reload # 平滑重载不中断服务nginx -t如果输出test is successful再reload几乎可以保证不会把服务搞挂。reload是平滑的已经在处理中的请求会继续完成新请求走新配置。4. Django中间件请求的守门员也是我踩过最多坑的地方中间件在Django里表现为一个类它的实例在应用启动时被创建一次然后每个请求进来都会经过它的钩子方法。这套机制是整个框架里比较魔法的部分理解了它你才能定位很多诡异的问题。4.1 中间件的执行顺序和五个钩子Django的中间件体系有五个核心钩子按请求生命周期依次触发钩子方法触发时机典型用途__init__(self, get_response)服务启动时初始化中间件实例process_request(request)请求进入视图之前鉴权、限流、日志、修改requestprocess_view(request, view_func, view_args, view_kwargs)路由匹配到视图后、调用视图前按视图做权限判断、缓存检测process_exception(request, exception)视图抛出异常时统一异常捕获、错误告警process_response(request, response)视图返回响应后加响应头、压缩、日志在settings.py的MIDDLEWARE列表里列表顺序就是中间件的执行顺序。请求阶段从第一个执行到最后一个响应阶段反过来从最后一个执行到第一个。我举个例子你就懂了SessionMiddleware要在CsrfViewMiddleware前面因为CSRF校验可能用到sessionAuthenticationMiddleware要在任何依赖request.user的中间件前面因为它负责把用户信息挂到request上。顺序错了轻则功能异常重则直接500。4.2 在中间件里捕获所有异常你必须自己补上的一课热词里有一条很常见的问题如何在Django中间件中捕获所有异常信息。Django自带的process_exception钩子确实能捕获视图里抛出的异常但它覆盖不到以下几类情况日志、中间件自身抛出的异常404等由URL匹配阶段产生的异常模板渲染过程中的异常process_response里抛出的异常如果想实现全量捕获一个靠谱做法是在最外层中间件里用try/except包住整个get_response调用链class CatchAllExceptionsMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): try: response self.get_response(request) return response except Exception as e: logger.error( Unhandled exception at %s: %s, request.path, traceback.format_exc(), extra{user: getattr(request, user, None)}, ) if not settings.DEBUG: return HttpResponseServerError(server error) raise这段代码放在MIDDLEWARE列表最顶部就能兜住往下所有环节的异常。注意process_exception钩子只处理视图异常而__call__包住的是整条链路两者互补。实际使用中建议再配合一个专门的日志信号或者接入Sentry这类错误监控工具。我自己会把这类中间件跟运维告警打通——异常一旦产生就发告警而不是等用户报告。4.3 中间件里改request的边界中间件大把的人用来改request对象比如塞一个request.user_id、解一个token。这里有一个协作规范中间件改request时要保证不污染其他中间件的预期。比如你在自定义的AuthMiddleware里清空了request.META后面Django内置的CsrfViewMiddleware可能就拿到一个不可用的CSRF token你在process_request里返回了HttpResponse请求会跳过所有后续中间件的process_request阶段和视图直接返回——这有时是设计需求有时却是bug。我的习惯是中间件里做三类事情就够了——鉴权、日志、全局数据注入。凡是跟业务强绑定的逻辑放到视图层或者service层去别在中间件里堆。5. 静态文件与media开发和生产的两种完全不同的逻辑静态文件在Django里看着简单实际上它的处理逻辑在开发环境跟生产环境完全是两套理解这个差异能避免很多无谓的踩坑。5.1 开发环境下img标签为什么老是加载不出来热词里有一条特别典型vscode写img标签 在django的static文件中显示不了。先说结论你的代码没写错路径也没写错大概率是目录层级多包了一层。Django官方推荐的目录结构是myproject/ └── app/ ├── static/ # 这是django的static目录 │ └── app/ # 里面再包一层app目录才是真正放文件的 │ └── style.css └── templates/为什么要多包一层app/因为多个app之间可能存在同名静态文件Django的collectstatic会把所有app里的static文件合并到一个目录如果没有这层命名空间隔离style.css会被互相覆盖。模板里正确的写法{% load static %} img src{% static app/img/logo.png %} altlogo如果你直接在static/目录下放了img/logo.png没有多包一层app/模板里写{% static img/logo.png %}在某些情况下是能用的但项目一复杂就乱。我见过新手把图片丢进static/下一层再在模板里写{% static static/img/logo.png %}结果路径多了一层static排查半天。另外还要确认settings.py里有STATIC_URL /static/开发模式下Django的runserver会自动serve静态文件所以只要能配好目录和标签本地就能正常显示。如果还不行F12看Network标签里的URL对着目录结构一步步核比瞎猜快得多。5.2 collectstatic到底在干什么生产环境里Django官方推荐你在服务器上执行python manage.py collectstatic这条命令会扫描所有app的static目录和你在STATICFILES_DIRS里指定的目录把所有文件复制到STATIC_ROOT指定的目录比如/opt/myproject/staticfiles/。然后Nginx就把这个目录作为静态文件根目录来提供服务。settings.py里相关变量要搞清楚STATIC_URL /static/ # 静态文件的URL前缀 STATIC_ROOT /opt/myproject/staticfiles/ # collectstatic收集到哪 STATICFILES_DIRS [BASE_DIR / extra_static] # 除了app里的static外额外收集哪些目录很多人踩的坑是设置了STATICFILES_DIRS指向BASE_DIR / static同时又设置了STATIC_ROOT指向同一层级下另一个目录collectstatic报warning提示目录是同一来源文件可能永远收集不进去。这属于路径规划问题建议集中用一套要么全放app的static要么全放STATICFILES_DIRS。5.3 media和static的本质区别再强调一下这两个概念虽然都跟文件有关但完全不是一回事对比项STATICMEDIA内容项目自带资源CSS、JS、logo用户上传资源头像、附件、图片来源代码库版本管理运行时生成不入库目录由app/static和STATICFILES_DIRS决定由MEDIA_ROOT决定URL前缀STATIC_URLMEDIA_URL是否collectstatic需要不需要在Django的表单或模型中ImageField等上传文件的字段会把文件写到MEDIA_ROOT下并且通过request.FILES保存。配置MEDIA_URL /media/ MEDIA_ROOT /opt/myproject/media/开发时DEBUGTrue的情况下要在urls.py里加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)生产环境就完全交给Nginx的location /media/块来处理。记住media文件不能进collectstatic多次执行collectstatic不会搬动用户上传的文件。5.4 生产静态文件要不要用whitenoise现在流行一种新方案不用Nginx托管静态文件而是用whitenoise库直接由Django serve。它的原理是把静态文件打包进内存缓存利用中间件在Django返回响应前吐出静态内容。我来说句公道话如果你的项目只在云服务器上跑用户量不大日请求几千whitenoise完全可以省略Nginx的静态文件配置部署省事很多。如果你的项目有CDN、访问量大、或者静态资源特别多图片、视频还是回到Nginx方案性能上限更高。Nginx作为工业级静态文件服务器比任何Python方案都更稳。我自己的原则是能打通Nginx静态文件托管就优先打通这是标准方案一劳永逸whitenoise适合快速部署、临时环境、或者Docker里不想多装Nginx的场景。6. 写在最后的个人体会这几个概念单独拎出来每个都不算难但合在一起就劝退了很多人。我自己的经历是第一次部署Django项目时卡在静态文件404上整整一个下午最后发现只是alias写成了root。第二次部署时又因为Gunicorn的worker数和Nginx超时时间没对上接口一慢就被断开连接。后来我养成了两个习惯第一所有配置改完先输出一条请求日志从Nginx access log到Gunicorn的log逐层对齐哪一层断了就修哪一层。第二写部署笔记时永远把链路图画在最前面知道自己站在哪一段就永远不会迷失方向。这篇文章覆盖的内容够你从开发环境平滑过渡到生产环境。如果在配置过程中遇到具体报错欢迎把你的Nginx配置和Django settings贴出来一起讨论。
返回列表