☰
Django视图层与生产部署:FBV/CBV到Nginx+uWSGI全解析
2026/10/10 9:38:46 网站建设 项目流程

很多人学到 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, name="index"), path("health/", views.health_check, name="health"), ]

就这么简单。函数视图的内部机制非常好理解: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帮你根据主键查单条记录并处理 404,CreateView、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(status=1)的时候,这条 SQL并不会立即执行,只有当你真正遍历结果、或者调用某个方法强制求值时,数据库查询才会发生。

这意味着什么?意味着你在视图里写出这样的代码时,实际会执行多条 SQL:

articles = Article.objects.filter(status=1) # 这里不查库 for article in articles: # 这里才查库 print(article.title)

如果你在循环里访问了article.author.name,而author是外键,Django 会为每一条记录再发一次查询去取作者信息,这就是著名的N+1 查询问题。文章列表 50 条,你发现数据库日志里打了 51 条 SQL,性能就是这么被拖垮的。

解决方式是用select_related(适用于外键、一对一关系,通过 SQL JOIN 一次性取回关联数据)和prefetch_related(适用于多对多、反向外键,分两次查询后由 ORM 在 Python 层合并):

articles = Article.objects.select_related("author").filter(status=1)

还有一类查询是“或”条件,新手经常不知道怎么写。比如要查状态为 1或作者为某个人的文章,filter(status=1, author=xxx)是“且”关系,达不到目的。这时候要引入 Q 表达式:

from django.db.models import Q articles = Article.objects.filter(Q(status=1) | Q(author=request.user))

Q 对象用|表示或、&表示且,前面加~表示非,组合复杂查询非常方便,比手拼 SQL 条件安全得多。

2.2 删除对象:delete() 背后的行为和它的返值

删除是另一个高频操作。视图里删除对象一般这么写:

article = Article.objects.get(id=article_id) article.delete()

看代码感觉很简单,但有几个细节值得说。

第一,delete()会立即执行,返回一个具名元组(total_deleted, {"app_label.ModelName": count}),第一个数字表示总共删了多少条记录。注意总数可能大于 1,因为如果Article外键关联了评论、点赞等表,并且关系没有设置on_delete=models.SET_NULL或PROTECT的话,Django 会级联删除关联数据。新手经常在这里误删数据。

第二,批量删除要小心。用QuerySet的delete():

Article.objects.filter(status=0).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, id=article_id) article.delete()

2.3 Cookie 里放 Token:HttpOnly 与 Secure 的取舍

现在前后端分离的项目常见方案是:登录成功后服务端生成一个 Token,放在 Cookie 里,之后每次请求带上来。Django 对 Cookie 操作非常简单:

response = HttpResponse("ok") response.set_cookie( "auth_token", token_value, max_age=7 * 24 * 3600, # 7天有效期,单位秒 httponly=True, secure=False, # HTTPS 环境下要设为 True samesite="Lax", )

这里面最容易被忽略的是httponly=True。设置了它之后,Cookie 不能用 JavaScript 读取,document.cookie拿不到,这样即使你的前端被注入了恶意脚本,也没法把 Token 直接偷走。这是防御 XSS(跨站脚本攻击)非常重要的一层。

secure=True指的是只在 HTTPS 连接下发送 Cookie。开发环境用 HTTP 时,这个参数要设成 False,否则 Cookie 根本种不上,你排查半天以为登录有问题,其实只是协议不匹配。到了生产环境,配好 HTTPS 之后一定要记得切回 True。

还有samesite参数,它控制跨站请求时是否携带 Cookie,对 CSRF(跨站请求伪造)防护有帮助。默认值随着 Django 版本演进在收紧,建议显式设置。

2.4 从零创建 App:startapp 之后你应该做什么

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 Nginx(80/443端口,反向代理) ├─ 静态文件直接返回(CSS/JS/图片) ├─ 动态请求 → uWSGI(socket通信)→ 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 安装与基础验证

先激活虚拟环境,然后安装 uWSGI:

cd /opt/myproject source venv/bin/activate pip install uwsgi

安装完成后,先在项目目录下做一次最小化测试,确保 uWSGI 能正常拉起 Django。Django 项目根目录下都有一个wsgi.py文件,它定义了 WSGI 应用入口。用下面这条命令启动 uWSGI:

uwsgi --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] Description=uWSGI service for myproject After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/opt/myproject ExecStart=/opt/myproject/venv/bin/uwsgi --ini /opt/myproject/uwsgi.ini Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

然后执行:

systemctl daemon-reload systemctl enable uwsgi systemctl start uwsgi

Restart=always的意思是进程异常退出后,5 秒自动拉起。配合 uWSGI 自身的master进程管理,双保险。这一步做完之后,别急着配置 Nginx,先用 systemd 状态确认 uWSGI 起来没有、日志里有没有报错:

systemctl status uwsgi tail -f /var/log/uwsgi/myproject.log

日志才是你排查问题的最好朋友。多数 502 错误,看 uWSGI 日志一眼就能定位是项目代码报错、数据库连接失败,还是 socket 权限不对。

5. Nginx 反向代理与站点配置:手写一份能上线的 conf

uWSGI 已经在后台待命了,现在轮到 Nginx 登场。Nginx 的工作有两个重点:把/static/这类静态请求直接返回文件;把动态请求通过 uwsgi 协议转给后端。这一节从安装开始,到写出一份能真正上线的配置。

5.1 Nginx 安装和一些基本概念

在 Debian/Ubuntu 系服务器上用 apt 安装即可:

apt update apt install nginx

装完先确认版本和状态:

nginx -v systemctl status nginx

Nginx 的核心配置文件是/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/hosts(Windows 是C:\Windows\System32\drivers\etc\hosts),把自定义域名指向虚拟机的 IP:

192.168.56.101 blog.test 192.168.56.101 api.test

然后在虚拟机 Nginx 里配置两个server块,监听不同端口(比如 8080 和 8081)或者同一 80 端口不同server_name:

server { 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、8002),Nginx 负责把域名加端口映射到对应端口。这样不需要频繁改后端服务本身的端口,也算提前熟悉了反向代理的工作方式。

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 API(https://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 服务文件里设置LimitNOFILE=65535。

第二,后端 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 uwsgi;ls -l /run/uwsgi/myproject.sock;看 uWSGI 日志
样式全丢,静态文件 404未执行 colletstatic,或 Nginx alias 路径错误python manage.py collectstatic;检查STATIC_ROOT和 Nginxalias
替换证书后仍是旧证书未 reload,或多个 server 块匹配错误nginx -T;openssl 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 的secure=True但访问还是 HTTP开发环境关闭 secure,或配置 HTTPS
数据库连接数被打满视图 N+1 查询,或 uWSGI 进程过多用select_related/prefetch_related优化;调低processes
日志文件无限增长撑爆磁盘没配置日志轮转uWSGI 配log-maxsize;Nginx 用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 的记录,里面涉及的证书续签、容器内进程守护、数据库备份这些内容,每一项单独拎出来都够讲一整篇。这一篇的核心是让你先把视图层概念和生产架构跑通,不要贪多,跑通了再进阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询