Django视图层与生产部署:FBV/CBV到Nginx+uWSGI全解析
很多人学到 Django 视图层就开始犯迷糊明明一个函数能搞定的事框架为什么非搞出个类视图来更头疼的是本地runserver跑得飞起一放到服务器上就各种 502、静态文件丢失、并发一上来直接卡死。今天这篇把 FBV 和 CBV 从使用到原理讲透再带你把项目从开发机搬到生产服务器走一遍 Nginx uWSGI 的完整链路。这不是那种只给配置粘贴板的教程我会把每一步为什么这么做、踩过哪些坑都交代清楚适合已经学完 Django 基础、准备写真实项目或者正在被部署折磨的朋友。这个系列写到第五篇前几篇讲了环境搭建、模型、路由和模板现在到了真正决定项目能不能拿得出手的关键环节。你可能会说现在有各种一键部署平台还有用 AI Agent 辅助开发手写 Nginx 配置是不是过时了我个人的看法是平台能帮你把项目跑起来但解决不了“出了问题你怎么排查”这件事。你自己理解了视图、理解了反向代理AI 生成代码再花哨报错的时候你才知道它在说什么。1. 先把 FBV 和 CBV 讲透你写的是路由还是框架帮你搭好的骨架很多新手看到 FBV、CBV 这两个缩写就头大觉得是特别高深的东西。其实拆开看就四个单词Function-Based View基于函数的视图和 Class-Based View基于类的视图。它们本质上是同一个东西的两种写法——接收一个 HTTP 请求经过业务逻辑处理返回一个 HTTP 响应。区别只在于你是自己手写这个处理过程还是让框架帮你把流程拆好、你往里面填代码。1.1 什么是 FBV函数视图Django 最朴素的入口FBV 就是你最早写 Django 时接触的那种视图。一个普通的 Python 函数接收request对象返回HttpResponse或者render的结果。比如from django.http import HttpResponse from django.shortcuts import render def index(request): return render(request, index.html, {title: 首页}) def health_check(request): return HttpResponse(ok)然后在urls.py里注册路由from django.urls import path from . import views urlpatterns [ path(, views.index, nameindex), path(health/, views.health_check, namehealth), ]就这么简单。函数视图的内部机制非常好理解request进来你把它当成一个普通对象用从里面取参数、读 Cookie、判断请求方法然后返回一个响应对象。整个生命周期清晰可见没有任何黑魔法。咱们用生活化类比来解释一下——FBV 就像你去食堂打饭流程全由你掌控拿到餐盘request你决定打几个菜、要多少米饭最后端着餐盘走人response。每一步操作都写在明面上出了问题一眼就能看到是哪一步没做对。FBV 最大的优势是直观、灵活。你可以在函数里写任意逻辑调用其他函数、写条件分支、动态生成返回内容完全没有框架层面的限制。对于简单的视图、API 接口、或者只需要处理一种请求方法的场景FBV 永远是最快、最不容易出错的方案。1.2 什么是 CBV类视图把重复劳动封装起来类视图则是把“处理 HTTP 请求”这件事抽象成了可复用的结构。最基础的写法是用一个类继承View然后在类里面定义get、post这些方法对应不同的 HTTP 请求方法from django.views import View from django.http import JsonResponse class UserDetailView(View): def get(self, request, user_id): # 处理 GET 请求 return JsonResponse({user_id: user_id, method: GET}) def post(self, request, user_id): # 处理 POST 请求 return JsonResponse({user_id: user_id, method: POST})这时候你可能会想这不就是把if request.method GET换成了类方法吗有什么本质区别区别在于Django 为了消灭重复劳动在View基类之上又封装了大量现成的“通用视图”。比如TemplateView帮你渲染模板ListView帮你自动查询某个模型的所有对象、做分页、传模板上下文DetailView帮你根据主键查单条记录并处理 404CreateView、UpdateView、DeleteView直接帮你把表单处理流程走完。拿最常用的ListView举个例子from django.views.generic import ListView from .models import Article class ArticleListView(ListView): model Article template_name article/list.html paginate_by 10这几行配置就能实现查询Article全部数据、分页、向模板传递object_list和分页上下文。换成 FBV你需要自己写Article.objects.all()自己处理分页器自己想到底往模板里传什么变量名。CBV 把这类常见场景的 80% 重复代码都替你写好了。它的工作机制值得了解一下这样以后看源码才不会晕。View.as_view()返回一个函数注意as_view是类方法浏览器请求进来时会调用这个函数函数内部根据请求方法创建视图类的实例然后调用dispatch()方法。dispatch()再根据request.method如 GET、POST去找类里对应的小写方法名get、post有就调用没有就返回 405。所以 CBV 本质上还是函数视图的一层壳只是加了**方法分发、可继承、可混入Mixin**这些能力。你可以在子类里覆盖get_context_data补充额外的模板上下文数据也可以覆盖get_queryset来修改查询集。这就是类视图的扩展点。1.3 FBV 和 CBV 怎么选别被网上教程带偏这是个经典问题各种论坛吵了很多年。网上的教程喜欢站队有的说“企业级项目必需 CBV”有的说“FBV 天下第一”。我的观点很直接没有绝对的对错只有合适不合适。先看这张对比表对比维度FBVCBV可读性逻辑平铺直叙新手友好逻辑分散在多个方法中需要熟悉约定代码复用靠函数拆分、装饰器靠继承、Mixin 组合处理不同 HTTP 方法用if分支判断类方法天然分离代码更整洁处理表单/列表等重复场景需要自己封装ListView、CreateView等开箱即用装饰器使用直接login_required装饰函数需要method_decorator包装略绕适合场景简单页面、API、逻辑独特的视图标准 CRUD、列表详情、后台管理类页面我的建议是新手先写好 FBV遇到重复场景再切 CBV不要为了用 CBV 而用 CBV。如果你本身对 Django 的请求处理流程还不熟一上来就写一堆get_queryset、get_context_data只会更加混乱。另外要说一个 CBV 的真坑多继承时的 MRO方法解析顺序问题。Django 的通用类视图往往需要组合多个 Mixin比如LoginRequiredMixin要放在父类列表的最左侧否则认证逻辑不会生效。我用 AI Agent 辅助开发时也发现它生成的 CBV 类经常把 Mixin 顺序搞错运行起来没报错但权限校验就是没执行排查起来特别费劲。建议新手在掌握 FBV 之前先不要碰复杂的 Mixin 组合。2. 视图里躲不开的数据操作查询、删除、Cookie 与 Token视图层不只是返回网页更多时候是操作数据库。FBV 和 CBV 最终都要落到模型操作上。这一节把视图里最常见的几个数据操作场景拆开讲包括 ORM 查询、删除对象、还有 Cookie 和 Token 的配合问题。很多新手在这里写出的代码“功能实现了但隐患非常大”。2.1 ORM 查询与执行get、filter 和 Q 表达式的基础写视图时95% 的数据操作是查询。Django 的 ORM 比直接写 SQL 方便得多但也因为它的“懒加载”特性容易让人产生误解。所谓懒加载就是你写Article.objects.filter(status1)的时候这条 SQL并不会立即执行只有当你真正遍历结果、或者调用某个方法强制求值时数据库查询才会发生。这意味着什么意味着你在视图里写出这样的代码时实际会执行多条 SQLarticles Article.objects.filter(status1) # 这里不查库 for article in articles: # 这里才查库 print(article.title)如果你在循环里访问了article.author.name而author是外键Django 会为每一条记录再发一次查询去取作者信息这就是著名的N1 查询问题。文章列表 50 条你发现数据库日志里打了 51 条 SQL性能就是这么被拖垮的。解决方式是用select_related适用于外键、一对一关系通过 SQL JOIN 一次性取回关联数据和prefetch_related适用于多对多、反向外键分两次查询后由 ORM 在 Python 层合并articles Article.objects.select_related(author).filter(status1)还有一类查询是“或”条件新手经常不知道怎么写。比如要查状态为 1或作者为某个人的文章filter(status1, authorxxx)是“且”关系达不到目的。这时候要引入 Q 表达式from django.db.models import Q articles Article.objects.filter(Q(status1) | Q(authorrequest.user))Q 对象用|表示或、表示且前面加~表示非组合复杂查询非常方便比手拼 SQL 条件安全得多。2.2 删除对象delete() 背后的行为和它的返值删除是另一个高频操作。视图里删除对象一般这么写article Article.objects.get(idarticle_id) article.delete()看代码感觉很简单但有几个细节值得说。第一delete()会立即执行返回一个具名元组(total_deleted, {app_label.ModelName: count})第一个数字表示总共删了多少条记录。注意总数可能大于 1因为如果Article外键关联了评论、点赞等表并且关系没有设置on_deletemodels.SET_NULL或PROTECT的话Django 会级联删除关联数据。新手经常在这里误删数据。第二批量删除要小心。用QuerySet的delete()Article.objects.filter(status0).delete()这条是直接翻译成一条DELETE FROMSQL 执行的不会调用模型里重写的delete()方法也不会触发信号signals。如果你的模型在delete()里做了额外处理比如删除磁盘上的文件、写日志批量删除时这部分逻辑就静默丢失了。第三get()拿不到对象会抛DoesNotExist处理不好就是 500。所以生产代码里更推荐get_object_or_404或者用filter().first()判断为 None 的情况from django.shortcuts import get_object_or_404 article get_object_or_404(Article, idarticle_id) article.delete()2.3 Cookie 里放 TokenHttpOnly 与 Secure 的取舍现在前后端分离的项目常见方案是登录成功后服务端生成一个 Token放在 Cookie 里之后每次请求带上来。Django 对 Cookie 操作非常简单response HttpResponse(ok) response.set_cookie( auth_token, token_value, max_age7 * 24 * 3600, # 7天有效期单位秒 httponlyTrue, secureFalse, # HTTPS 环境下要设为 True samesiteLax, )这里面最容易被忽略的是httponlyTrue。设置了它之后Cookie 不能用 JavaScript 读取document.cookie拿不到这样即使你的前端被注入了恶意脚本也没法把 Token 直接偷走。这是防御 XSS跨站脚本攻击非常重要的一层。secureTrue指的是只在 HTTPS 连接下发送 Cookie。开发环境用 HTTP 时这个参数要设成 False否则 Cookie 根本种不上你排查半天以为登录有问题其实只是协议不匹配。到了生产环境配好 HTTPS 之后一定要记得切回 True。还有samesite参数它控制跨站请求时是否携带 Cookie对 CSRF跨站请求伪造防护有帮助。默认值随着 Django 版本演进在收紧建议显式设置。2.4 从零创建 Appstartapp 之后你应该做什么Django 项目一般由多个 app 组成每个 app 负责一块独立的功能模块。使用命令行创建冒烟测试过没问题python manage.py startapp blog这个命令会生成models.py、views.py、admin.py、apps.py等基础文件。但我想说的是startapp 之后真正重要的有一件事在INSTALLED_APPS里注册这个 app。很多新手在本地开发时没注册也能跑因为 Django 对INSTALLED_APPS里的 app 会做迁移记录追踪、模板发现、静态文件收集等操作。没注册时makemigrations不会识别这个 app 的模型templates目录里的模板按默认配置也找不到最典型的现象是页面明明放在blog/templates/blog/index.html里渲染时却报 TemplateDoesNotExist。注册之后别忘了指定app_label或在apps.py里配置好name。还有一种情况是同一个 app 被用于多个项目你需要在INSTALLED_APPS里写成blog.apps.BlogConfig以便让 Django 找到自定义的ready()方法信号注册就是放这里。这也是 AI Agent 更容易出错的地方——它默认按最简单的写法生成代码不会主动管理这些配置细节。3. 生产部署的核心架构为什么是 Nginx 加 uWSGI本地用python manage.py runserver跑得再好也只是一个单进程开发服务器。真实的生产环境需要面对并发请求、静态文件处理、进程崩溃恢复、证书卸载等一堆问题。目前最经典的组合就是 Nginx uWSGI Django当然也有 Gunicorn后面我会对比。这一节先把架构和选型讲清楚下一节进入实战配置。3.1 Django 自带 runserver 为什么不能上生产runserver是 Django 内置的轻量级服务器它的设计目标是开发调试不是承载线上流量。原因主要有三点单进程模型runserver默认只启动一个进程一次只能处理一个请求。虽然它有自动重载功能但并发能力非常弱几十个人同时访问就明显卡顿。没有做静态文件优化生产环境通常由 Nginx 直接托管 CSS、JS、图片等静态资源不经过 Django 进程。runserver虽然能顺便托管静态文件但这是它手工处理的逻辑性能和优先级都不行。缺少进程守护开发服务器崩了就崩了你不会希望生产环境里每隔两小时跑一次python manage.py runserver。所以生产环境的思路是把业务处理交给一个用 WSGI 协议的服务进程把请求入口、静态资源、负载均衡交给专业的反向代理服务器。3.2 架构全景浏览器到 Django 的完整链路一个典型的生产架构长这样用户浏览器 ↓ HTTPS / HTTP Nginx80/443端口反向代理 ├─ 静态文件直接返回CSS/JS/图片 ├─ 动态请求 → uWSGIsocket通信→ Django应用 └─ 证书卸载、请求头处理、访问控制接着逐段解释。用户在浏览器输入域名DNS 解析到服务器 IP请求到达 Nginx。Nginx 是一个事件驱动的异步服务器单进程能管成千上万个连接所以它特别适合做“流量入口”。它根据配置判断请求的路径如果请求的是/static/下的静态资源直接读磁盘上的文件返回完全不惊动 Django如果请求的是动态页面或 API则将请求通过 uWSGI 协议转发给后端的 uWSGI 进程。uWSGI 是一个实现了 WSGI 协议的进程它的工作就是把 Nginx 转来的请求信息转换成 Django 能处理的 WSGI 环境然后调用你的 Django 应用。Django 跑在 uWSGI 里使用的是真正的多进程/多线程能力多个进程同时处理请求瓶颈不再是一个请求串行执行。最后一个问题为什么不直接把请求发给 Flask、FastAPI 这类应用跑在 Nginx 后面可以但 uWSGI 协议比 HTTP 转发开销更低而且和 Django 配合多年已经非常成熟官方文档也是按这个方案写的。3.3 uWSGI 与 Gunicorn哪个更值得选部署 Django 时最常见的两个 WSGI 服务器就是 uWSGI 和 Gunicorn。很多人纠结选哪个我的经验放在这里对比项uWSGIGunicorn性能可调参数多极限性能更高中规中矩但足够大多数业务配置复杂度配置项非常多学习曲线陡命令行参数简单容易上手内存占用可精细化调整配置不当比 Gunicorn 高默认配置下比较省心社区资料老牌方案教程极多近年更流行部署 Docker 更轻便与 Nginx 配合原生支持 uwsgi 协议通常用 http 或 gunicorn 的 unix socket 转发我的结论是如果是新手第一次部署用 Gunicorn 会更省心但如果你想深入了解 WSGI 服务器的机制、或者需要对性能极限做压测优化uWSGI 的可玩性和资料丰富程度更好。这个话题贴主选择了 uWSGI那我基于他的选择展开讲两者概念高度相似学懂一个另一个也很容易上手。另外如果你的项目用了 Django 的异步能力比如 Channels、异步视图WSGI 服务器就不够用了得换 ASGI 服务器如 Daphne、Uvicorn。Django 4 对异步的支持越来越好但这是另一个话题。对于传统的同步业务WSGI 方案完全足够。4. 生产实战uWSGI 配置全过程现在进入动手环节。假设你有一台 Linux 服务器乌班图/Debian/CentOS 都类似项目代码已经通过 Git 同步到服务器上虚拟环境已经建好。下面从安装开始把 uWSGI 配置给我们家一步一步讲透。4.1 安装与基础验证先激活虚拟环境然后安装 uWSGIcd /opt/myproject source venv/bin/activate pip install uwsgi安装完成后先在项目目录下做一次最小化测试确保 uWSGI 能正常拉起 Django。Django 项目根目录下都有一个wsgi.py文件它定义了 WSGI 应用入口。用下面这条命令启动 uWSGIuwsgi --http 127.0.0.1:8080 --chdir /opt/myproject --module myproject.wsgi --venv /opt/myproject/venv参数说明--http 127.0.0.1:8080监听本机 8080 端口先用 HTTP 模式验证。--chdir切换到项目目录否则 uWSGI 找不到myproject/wsgi.py。--module myproject.wsgi指定 WSGI 应用模块。--venv指定虚拟环境路径。然后在本机用curl测试curl http://127.0.0.1:8080/health/返回 HTTP 200 就说明 uWSGI 已经能跑起 Django 了。注意这一步只是验证环境真正上线不会用--http而是用 socket 模式配合 Nginx。4.2 一个能用的 uwsgi.ini 是怎么写的命令行参数太长而且没法持久化。生产环境建议把配置写进uwsgi.ini文件。我这边提供一个实战可用的模板每一行都会解释为什么这么写[uwsgi] # 项目目录 chdir /opt/myproject # wsgi入口 module myproject.wsgi:application # 虚拟环境 home /opt/myproject/venv # 使用 unix socket与 Nginx 通信 socket /run/uwsgi/myproject.sock # 调整 socket 权限保证 Nginx 可访问 chmod-socket 664 # 以指定用户运行不要用 root uid www-data gid www-data # 开启主进程管理子进程 master true # 子进程数量一般等于 CPU 核数 processes 4 # 每个进程开启线程数 threads 2 # 自动移除废弃的 socket 文件 vacuum true # 后台运行日志写入文件 daemonize /var/log/uwsgi/myproject.log # 日志轮转避免单文件无限膨胀 log-maxsize 50000000几个关键选择的理由为什么用 unix socket 而不是网络端口因为本机 Nginx 和 uWSGI 通过 socket 文件通信不走 TCP/IP 协议栈开销更小也更安全外部访问不到。前提是 Nginx 运行用户比如www-data对 socket 文件有读写权限所以设了chmod-socket 664并指定uid/gid都是www-data。processes 和 threads 怎么定一个经验公式processes取服务器 CPU 核心数threads通常为 2 或 4。需要注意每个进程都会占据一份 Django 应用内存因为 Django 的模型代码是加载到进程内存里的如果服务器只有 1G 内存4 个进程可能直接吃满。建议先用free -h看下内存然后从processes 2起步压测后再慢慢调。为什么开 master true开启主进程之后uWSGI 才能管理子进程子进程崩溃后自动拉起也能优雅地处理重新加载配置。否则单个进程挂了服务就真挂了。4.3 通过 systemd 管理 uWSGI上一节里用了daemonize让 uWSGI 后台运行但如果机器重启uWSGI 并不会自动启动。生产环境里我们会写一个 systemd 服务让操作系统帮我们守护它。在/etc/systemd/system/uwsgi.service中写[Unit] DescriptionuWSGI service for myproject Afternetwork.target [Service] Userwww-data Groupwww-data WorkingDirectory/opt/myproject ExecStart/opt/myproject/venv/bin/uwsgi --ini /opt/myproject/uwsgi.ini Restartalways RestartSec5 [Install] WantedBymulti-user.target然后执行systemctl daemon-reload systemctl enable uwsgi systemctl start uwsgiRestartalways的意思是进程异常退出后5 秒自动拉起。配合 uWSGI 自身的master进程管理双保险。这一步做完之后别急着配置 Nginx先用 systemd 状态确认 uWSGI 起来没有、日志里有没有报错systemctl status uwsgi tail -f /var/log/uwsgi/myproject.log日志才是你排查问题的最好朋友。多数 502 错误看 uWSGI 日志一眼就能定位是项目代码报错、数据库连接失败还是 socket 权限不对。5. Nginx 反向代理与站点配置手写一份能上线的 confuWSGI 已经在后台待命了现在轮到 Nginx 登场。Nginx 的工作有两个重点把/static/这类静态请求直接返回文件把动态请求通过 uwsgi 协议转给后端。这一节从安装开始到写出一份能真正上线的配置。5.1 Nginx 安装和一些基本概念在 Debian/Ubuntu 系服务器上用 apt 安装即可apt update apt install nginx装完先确认版本和状态nginx -v systemctl status nginxNginx 的核心配置文件是/etc/nginx/nginx.conf它通过include指令加载/etc/nginx/sites-enabled/目录下的站点配置。所以多站点管理的方式就是在sites-available里写多个站点的配置文件用软链接把它们启用到sites-enabled。在动手写配置前先理解 Nginx 配置的几个常用指令的含义server定义一个虚拟主机监听某个端口的请求。location匹配 URL 路径匹配到的请求按该块内的规则处理。proxy_pass把请求反向代理到指定的后端地址HTTP 协议。uwsgi_pass类似proxy_pass但使用的 uwsgi 协议专门用于和 uWSGI 通信。include把其他文件的配置包含进来。5.2 一份生产可用的 Django 站点配置我贴一份经过多个项目验证的配置重要行都加了注释# /etc/nginx/sites-available/myproject server { # 监听 80 端口后续配好 HTTPS 后会把 HTTP 跳转到 HTTPS listen 80; server_name blog.example.com; # 客户端请求体最大大小如果有上传文件需求一定要调大 client_max_body_size 20m; # 静态文件直接读磁盘不经过 Django location /static/ { alias /opt/myproject/static/; expires 7d; } # 媒体文件用户上传 location /media/ { alias /opt/myproject/media/; expires 30d; } # 动态请求转发给 uWSGI location / { # 先尝试拿静态文件拿不到再转发后端与上一节alias方案二选一即可 try_files $uri proxy_to_django; } location proxy_to_django { include uwsgi_params; uwsgi_pass unix:/run/uwsgi/myproject.sock; } }这里隐含了一个重要的细节try_files $uri proxy_to_django不是必须的但推荐保留。它的作用是让 Nginx 先检查磁盘上是否有对应文件有就直接返回比如偶尔放在项目根目录的favicon.ico没有才转发给 Django。可以省掉一小部分不必要的 Python 进程调用。那/static/的alias是从哪里来的需要 Django 项目先执行一次python manage.py collectstatic这条命令把每个 app 的静态文件复制到STATIC_ROOT指向的目录我这里示例是/opt/myproject/static/。很多人部署完页面样式全丢就是因为没跑 collectstatic或者 Nginx 的 root/alias 路径配错了。5.3 本地多站点与开发环境配置标题热词里出现了一个有意思的场景本地加虚拟机多端口 Nginx、开发环境多站点、自定义域名配置。很多人需要在开发机上模拟多站点域名这时候 Nginx 也能派上用场。编辑本机的/etc/hostsWindows 是C:\Windows\System32\drivers\etc\hosts把自定义域名指向虚拟机的 IP192.168.56.101 blog.test 192.168.56.101 api.test然后在虚拟机 Nginx 里配置两个server块监听不同端口比如 8080 和 8081或者同一 80 端口不同server_nameserver { listen 8080; server_name blog.test; # 转发到本机 uWSGI 或其他开发服务 location / { proxy_pass http://127.0.0.1:8000; } } server { listen 8081; server_name api.test; location / { proxy_pass http://127.0.0.1:8001; } }本地多端口的思路是每个开发服务监听不同的本机端口8000、8001、8002Nginx 负责把域名加端口映射到对应端口。这样不需要频繁改后端服务本身的端口也算提前熟悉了反向代理的工作方式。5.4 静态文件、媒体文件处理以及 access/error 日志再次强调生产环境的静态文件一定要让 Nginx 接管。否则每次请求一个 CSS 文件都要经过 uWSGI 里的 Django 进程CPU 和内存白白浪费。在模板里你应当使用{% static %}标签生成 URL并保证STATIC_URL /static/和 Nginx 的location /static/匹配起来。另外建议给 Nginx 开启独立的错误日志和访问日志方便排查error_log /var/log/nginx/myproject_error.log warn; access_log /var/log/nginx/myproject_access.log;经常出现的情况是网站挂了一查 Nginx 默认的错误日志里面写满了各种信息但你看不到是自己站点的请求。每个站点独立日志排查效率会高很多。6. 生产环境的高阶配置SSL、CORS、代理 Ollama、性能上限基础跑通之后真正的生产环境还有一堆细节HTTPS 证书、跨域、反向代理其他内网服务比如 LLM 推理服务、并发参数调优。这些地方坑特别多我挑几个高频问题重点讲讲。6.1 替换 SSL 证书不生效90% 的人踩过的坑热词里有一条“nginx替换ssl证书不生效”这个现象太典型了。明明用新证书内容替换了旧文件nginx -t也提示语法正确但是浏览器访问还是旧证书。我总结的排查顺序是第一确认证书文件确实被读到了。Nginx 配置里一般这样写ssl_certificate /etc/ssl/myproject/fullchain.pem; ssl_certificate_key /etc/ssl/myproject/privkey.pem;先检查这两个路径指向的文件是否真的被替换了ls -l /etc/ssl/myproject/fullchain.pem openssl x509 -in /etc/ssl/myproject/fullchain.pem -noout -dates关键点证书没有修改时间或者系统时间不对可能让签发时间看起来“过期”了。第二检查是否真的重载了。很多人改了配置后不执行nginx -t nginx -s reload注意nginx -t之后必须reload生效。如果之前用的是systemctl restart nginx有时候因为 master 进程没有完全退出新配置并没有真正加载。第三多server块匹配问题。这是最容易忽略了——如果你的 Nginx 配置里有多个server块浏览器访问时 Nginx 会按顺序匹配server_name。你修改的证书可能配在server_name blog.example.com这个块上但实际请求被更靠前的一个server块比如default_server接住了。用nginx -T命令可以导出当前实际生效的完整配置看看ssl_certificate到底写的是什么。第四浏览器与中间层缓存。浏览器会缓存证书和 HSTS 策略Chrome 的“证书信息”页面显示的还是旧证书时试试无痕窗口或者用openssl s_client -connect 域名:443直接看服务端实际返回的证书。这一步能排除浏览器缓存的干扰。我处理过的一个真实案例替换证书后nginx -T显示配置没问题但openssl命令拿到的还是旧证书最后发现是系统里另一个服务提前占用了 443 端口Nginx 的 443 listener 根本没成功启动。这种情况下需要先看 Nginx 日志确认实际监听情况。6.2 反向代理 Ollama 或其他内网服务Header 是关键热词里出现了“nginx 代理 ollama 设置 apikey cherrystudio”和“nginx 反向代理 ollama”说明现在很多团队会通过 Nginx 给内网的模型推理服务做出口代理。这类服务的反代其实和代理 Django 大同小异但有两个特别的坑一个是对外暴露时的 Header 设置。比如你在内网跑了一个 Ollama 服务监听 11434 端口通过 Nginx 对外提供统一入口location /ollama/ { proxy_pass http://127.0.0.1:11434/; 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_pass末尾的斜杠很关键http://127.0.0.1:11434/末尾带斜杠会把/ollama/之后的路径拼到后端不带斜杠会把完整路径原样传过去。这个细节我见过太多人在这上面栽跟头。关于 API Key 的传递正确的姿势是不要放在 URL 里明文传递而是通过 Header 传递并在 Nginx 层统一注入。这样前端代码里不会出现密钥服务端也只认可信来源的请求location /ollama/ { proxy_set_header Authorization Bearer ${OLLAMA_API_KEY}; proxy_pass http://127.0.0.1:11434/; }把密钥放在 Nginx 环境变量或外部配置文件里而不是写死在 conf 中提交到 Git 仓库。另外一个容易犯的错误是反代到带 API Key 的服务时如果后端校验不过先看看是不是proxy_set_header把客户端的 Authorization 覆盖了可以用$http_authorization显式传递或去掉这一行。6.3 CORS 与 preflight 请求的坑前后端分离项目里前端页面比如https://front.example.com通过 AJAX 请求你的 Django APIhttps://api.example.com时浏览器会先发一个 OPTIONS 预检请求确认服务器允许跨域。如果 Nginx 层没有处理好就会出现 Invalid CORS request 或跨域报错。一种常见做法是在 Django 层面用django-cors-headers处理跨域。但当你加了 Nginx 反向代理后响应头必须经过 Nginx 透传回浏览器。如果 Nginx 恰好把Access-Control-Allow-Origin这类的响应头过滤掉了前端照样报跨域错误。更建议在生产环境把跨域控制在 Nginx 层比如location /api/ { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS always; add_header Access-Control-Allow-Headers Authorization, Content-Type, X-Requested-With always; return 204; } proxy_pass http://127.0.0.1:8000; add_header Access-Control-Allow-Origin * always; }需要注意always参数——它表示无论响应状态码是 200 还是 4xx、5xx都强制加上这个响应头。没有always时后端返回 500 错误跨域头就丢了浏览器报错信息会非常误导人。还有一个容易踩的点Access-Control-Allow-Origin不要盲目设成*。如果你的接口需要携带 Cookie*会导致请求失败必须显式指定具体来源域名并确认Access-Control-Allow-Credentials: true已经设置。6.4 并发与连接数Nginx 到底能扛多大压力热词里还有一个高频问题“nginx 最大并发链接数老是用超”“nginx 最大并发联接数总是超”。这背后涉及 Nginx 的两个核心参数worker_processes auto; events { worker_connections 1024; }worker_processes auto表示按 CPU 核数启动 worker 进程一般就设成自动即可。worker_connections是单个 worker 进程可同时处理的连接数上限。所以总的最大并发连接数约等于最大并发连接数 worker_processes × worker_connections如果服务器 4 核、worker_connections默认 1024理论上限 4096。注意这个数值还包括 Nginx 到后端服务的连接实际可用连接数还要打折。你遇到“并发数超”的报错时优先排查三件事第一系统文件描述符限制。Linux 默认单个进程能打开的文件句柄数是 1024而每个网络连接都要占用一个文件句柄。Nginx 官方建议把ulimit -n调高到 65535 甚至更高。配置在/etc/security/limits.conf或者 systemd 服务文件里设置LimitNOFILE65535。第二后端 uWSGI 才是瓶颈。如果 Nginx 配置的并发上限很高但 uWSGI 只有 4 个进程、每个进程 2 个线程每秒能处理的请求量有限连接就会在 Nginx 层堆积。这时调 Nginx 参数没有意义得去调后端的进程数/线程数。第三短连接和 TIME_WAIT 积累。大量短连接请求后系统会积累大量 TIME_WAIT 状态的 socket占用连接资源。Nginx 这边启用 upstream 的 keepalive 可以缓解upstream django_backend { server unix:/run/uwsgi/myproject.sock; keepalive 32; } server { location / { proxy_http_version 1.1; proxy_set_header Connection ; proxy_pass http://django_backend; } }对 uWSGI 的场景uWSGI 自身的http-keepalive和http-timeout也有影响具体要看版本配置文档。大方向就是这个先确认系统资源上限再排查后端处理能力最后调 Nginx 参数。7. 部署上线后的常见问题速查表最后把最容易遇到的几个问题整理一下你部署时如果踩到可以直接按这个表排查。这些都是真实线上环境里高频出现的问题每条背后都有一段血泪史。现象最可能原因排查命令 / 操作浏览器显示 502 Bad GatewayuWSGI 进程挂了或者 socket 权限不对systemctl status uwsgils -l /run/uwsgi/myproject.sock看 uWSGI 日志样式全丢静态文件 404未执行 colletstatic或 Nginx alias 路径错误python manage.py collectstatic检查STATIC_ROOT和 Nginxalias替换证书后仍是旧证书未 reload或多个 server 块匹配错误nginx -Topenssl s_client -connect 域名:443上传文件过大报 413未设置client_max_body_sizeNginx 加client_max_body_size 20m;页面能看到但接口跨域报错CORS 头缺失或Access-Control-Allow-Origin设了*又带 Cookie检查 response headers显式指定来源Django admin 无法登录Cookie 种不上Cookie 的secureTrue但访问还是 HTTP开发环境关闭 secure或配置 HTTPS数据库连接数被打满视图 N1 查询或 uWSGI 进程过多用select_related/prefetch_related优化调低processes日志文件无限增长撑爆磁盘没配置日志轮转uWSGI 配log-maxsizeNginx 用logrotate再说几个容易被忽略的杂项问题。Nginxmirror指令超时。如果你用了mirror复制流量做线上验证或灰度对比上游服务响应慢会影响主请求吗mirror默认是异步的但消耗 worker 连接若镜像目的超时时间长会占住连接导致整体并发下降。给镜像请求加proxy_read_timeout、proxy_send_timeout等参数可以控制。Alpine 容器里挂载 conf.d 报错。这是容器部署 Nginx 的常见问题Alpine 的 nginx 镜像用include /etc/nginx/conf.d/*.conf加载配置但你用docker run -v ./mysite.conf:/etc/nginx/conf.d/mysite.conf挂载时如果宿主机文件权限不对或格式不兼容比如 Windows 的 CRLF 换行Nginx 会直接拒绝启动。基本原理是先docker exec进入容器看nginx -T实际加载了哪些配置再检查文件换行符和挂载路径。CPython 与 Django 的版本兼容。现在很多服务器还在用 Python 3.8但 Django 5.0 要求 Python 3.10部署前先确认python --version和django --version匹配。这个低级错误极其常见AI Agent 生成的代码不会帮你检查运行环境版本。8. 一点个人经验和后续建议写到这里这个系列的第五篇主体内容基本结束了。最后再分享一个我自己的部署经验。刚开始做部署时我总想把所有配置一步到位多进程、多线程、HTTPS、CDN、自动扩容全部拉满。结果每次出问题排查链路特别长不知道是 Nginx 的锅、uWSGI 的锅还是自己代码的锅。后来学乖了部署流程改成“分层打怪”先只用runserver在服务器上跑通确认代码和环境没问题——再用最小化 uWSGI 拉起确认 WSGI 配置没问题——再加一层 Nginx 反代确认协议和静态文件配置没问题——最后才上 HTTPS、加性能参数。每一步都验证通过再往前走出问题永远只在当前层找原因排查速度快了至少一倍。对于视图层我建议你把掌握 CBV 当作一个进阶目标但不要用它替换所有 FBV。记一个简单原则一个视图的方法数量不超过两个GET/POST就用 FBV超过两个或者需要多个页面复用相似的查询、表单处理逻辑再考虑 CBV 和 Mixin。这个原则在我接手过的几十个项目里基本没有错过。如果你是用 AI Agent 辅助开发尤其要注意让它解释清楚它生成的视图具体走了哪个父类、哪个 Mixin以及as_view()在路由里是怎么注册的。AI 工具能帮你写出更长的代码但代码背后是 FBV 还是 CBV、请求流程是哪几条分支这些还是得你自己心里有数。真到了排查 bug 的时候一个能看懂视图源码的开发者效率上限完全不同。后续如果条件允许我会再写一篇关于 HTTPS 全站配置和 Docker 化部署 Django 的记录里面涉及的证书续签、容器内进程守护、数据库备份这些内容每一项单独拎出来都够讲一整篇。这一篇的核心是让你先把视图层概念和生产架构跑通不要贪多跑通了再进阶。