部署:FBV/CBV到Nginx+uWSGI全解析)
很多人學(xué)到 Django 視圖層就開始犯迷糊明明一個函數(shù)能搞定的事框架為什么非搞出個類視圖來更頭疼的是本地runserver跑得飛起一放到服務(wù)器上就各種 502、靜態(tài)文件丟失、并發(fā)一上來直接卡死。今天這篇把 FBV 和 CBV 從使用到原理講透再帶你把項目從開發(fā)機搬到生產(chǎn)服務(wù)器走一遍 Nginx uWSGI 的完整鏈路。這不是那種只給配置粘貼板的教程我會把每一步為什么這么做、踩過哪些坑都交代清楚適合已經(jīng)學(xué)完 Django 基礎(chǔ)、準備寫真實項目或者正在被部署折磨的朋友。這個系列寫到第五篇前幾篇講了環(huán)境搭建、模型、路由和模板現(xiàn)在到了真正決定項目能不能拿得出手的關(guān)鍵環(huán)節(jié)。你可能會說現(xiàn)在有各種一鍵部署平臺還有用 AI Agent 輔助開發(fā)手寫 Nginx 配置是不是過時了我個人的看法是平臺能幫你把項目跑起來但解決不了“出了問題你怎么排查”這件事。你自己理解了視圖、理解了反向代理AI 生成代碼再花哨報錯的時候你才知道它在說什么。1. 先把 FBV 和 CBV 講透你寫的是路由還是框架幫你搭好的骨架很多新手看到 FBV、CBV 這兩個縮寫就頭大覺得是特別高深的東西。其實拆開看就四個單詞Function-Based View基于函數(shù)的視圖和 Class-Based View基于類的視圖。它們本質(zhì)上是同一個東西的兩種寫法——接收一個 HTTP 請求經(jīng)過業(yè)務(wù)邏輯處理返回一個 HTTP 響應(yīng)。區(qū)別只在于你是自己手寫這個處理過程還是讓框架幫你把流程拆好、你往里面填代碼。1.1 什么是 FBV函數(shù)視圖Django 最樸素的入口FBV 就是你最早寫 Django 時接觸的那種視圖。一個普通的 Python 函數(shù)接收request對象返回HttpResponse或者render的結(jié)果。比如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), ]就這么簡單。函數(shù)視圖的內(nèi)部機制非常好理解request進來你把它當成一個普通對象用從里面取參數(shù)、讀 Cookie、判斷請求方法然后返回一個響應(yīng)對象。整個生命周期清晰可見沒有任何黑魔法。咱們用生活化類比來解釋一下——FBV 就像你去食堂打飯流程全由你掌控拿到餐盤request你決定打幾個菜、要多少米飯最后端著餐盤走人response。每一步操作都寫在明面上出了問題一眼就能看到是哪一步?jīng)]做對。FBV 最大的優(yōu)勢是直觀、靈活。你可以在函數(shù)里寫任意邏輯調(diào)用其他函數(shù)、寫條件分支、動態(tài)生成返回內(nèi)容完全沒有框架層面的限制。對于簡單的視圖、API 接口、或者只需要處理一種請求方法的場景FBV 永遠是最快、最不容易出錯的方案。1.2 什么是 CBV類視圖把重復(fù)勞動封裝起來類視圖則是把“處理 HTTP 請求”這件事抽象成了可復(fù)用的結(jié)構(gòu)。最基礎(chǔ)的寫法是用一個類繼承View然后在類里面定義get、post這些方法對應(yīng)不同的 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換成了類方法嗎有什么本質(zhì)區(qū)別區(qū)別在于Django 為了消滅重復(fù)勞動在View基類之上又封裝了大量現(xiàn)成的“通用視圖”。比如TemplateView幫你渲染模板ListView幫你自動查詢某個模型的所有對象、做分頁、傳模板上下文DetailView幫你根據(jù)主鍵查單條記錄并處理 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這幾行配置就能實現(xiàn)查詢Article全部數(shù)據(jù)、分頁、向模板傳遞object_list和分頁上下文。換成 FBV你需要自己寫Article.objects.all()自己處理分頁器自己想到底往模板里傳什么變量名。CBV 把這類常見場景的 80% 重復(fù)代碼都替你寫好了。它的工作機制值得了解一下這樣以后看源碼才不會暈。View.as_view()返回一個函數(shù)注意as_view是類方法瀏覽器請求進來時會調(diào)用這個函數(shù)函數(shù)內(nèi)部根據(jù)請求方法創(chuàng)建視圖類的實例然后調(diào)用dispatch()方法。dispatch()再根據(jù)request.method如 GET、POST去找類里對應(yīng)的小寫方法名get、post有就調(diào)用沒有就返回 405。所以 CBV 本質(zhì)上還是函數(shù)視圖的一層殼只是加了**方法分發(fā)、可繼承、可混入Mixin**這些能力。你可以在子類里覆蓋get_context_data補充額外的模板上下文數(shù)據(jù)也可以覆蓋get_queryset來修改查詢集。這就是類視圖的擴展點。1.3 FBV 和 CBV 怎么選別被網(wǎng)上教程帶偏這是個經(jīng)典問題各種論壇吵了很多年。網(wǎng)上的教程喜歡站隊有的說“企業(yè)級項目必需 CBV”有的說“FBV 天下第一”。我的觀點很直接沒有絕對的對錯只有合適不合適。先看這張對比表對比維度FBVCBV可讀性邏輯平鋪直敘新手友好邏輯分散在多個方法中需要熟悉約定代碼復(fù)用靠函數(shù)拆分、裝飾器靠繼承、Mixin 組合處理不同 HTTP 方法用if分支判斷類方法天然分離代碼更整潔處理表單/列表等重復(fù)場景需要自己封裝ListView、CreateView等開箱即用裝飾器使用直接login_required裝飾函數(shù)需要method_decorator包裝略繞適合場景簡單頁面、API、邏輯獨特的視圖標準 CRUD、列表詳情、后臺管理類頁面我的建議是新手先寫好 FBV遇到重復(fù)場景再切 CBV不要為了用 CBV 而用 CBV。如果你本身對 Django 的請求處理流程還不熟一上來就寫一堆get_queryset、get_context_data只會更加混亂。另外要說一個 CBV 的真坑多繼承時的 MRO方法解析順序問題。Django 的通用類視圖往往需要組合多個 Mixin比如LoginRequiredMixin要放在父類列表的最左側(cè)否則認證邏輯不會生效。我用 AI Agent 輔助開發(fā)時也發(fā)現(xiàn)它生成的 CBV 類經(jīng)常把 Mixin 順序搞錯運行起來沒報錯但權(quán)限校驗就是沒執(zhí)行排查起來特別費勁。建議新手在掌握 FBV 之前先不要碰復(fù)雜的 Mixin 組合。2. 視圖里躲不開的數(shù)據(jù)操作查詢、刪除、Cookie 與 Token視圖層不只是返回網(wǎng)頁更多時候是操作數(shù)據(jù)庫。FBV 和 CBV 最終都要落到模型操作上。這一節(jié)把視圖里最常見的幾個數(shù)據(jù)操作場景拆開講包括 ORM 查詢、刪除對象、還有 Cookie 和 Token 的配合問題。很多新手在這里寫出的代碼“功能實現(xiàn)了但隱患非常大”。2.1 ORM 查詢與執(zhí)行g(shù)et、filter 和 Q 表達式的基礎(chǔ)寫視圖時95% 的數(shù)據(jù)操作是查詢。Django 的 ORM 比直接寫 SQL 方便得多但也因為它的“懶加載”特性容易讓人產(chǎn)生誤解。所謂懶加載就是你寫Article.objects.filter(status1)的時候這條 SQL并不會立即執(zhí)行只有當你真正遍歷結(jié)果、或者調(diào)用某個方法強制求值時數(shù)據(jù)庫查詢才會發(fā)生。這意味著什么意味著你在視圖里寫出這樣的代碼時實際會執(zhí)行多條 SQLarticles Article.objects.filter(status1) # 這里不查庫 for article in articles: # 這里才查庫 print(article.title)如果你在循環(huán)里訪問了article.author.name而author是外鍵Django 會為每一條記錄再發(fā)一次查詢?nèi)ト∽髡咝畔⑦@就是著名的N1 查詢問題。文章列表 50 條你發(fā)現(xiàn)數(shù)據(jù)庫日志里打了 51 條 SQL性能就是這么被拖垮的。解決方式是用select_related適用于外鍵、一對一關(guān)系通過 SQL JOIN 一次性取回關(guān)聯(lián)數(shù)據(jù)和prefetch_related適用于多對多、反向外鍵分兩次查詢后由 ORM 在 Python 層合并articles Article.objects.select_related(author).filter(status1)還有一類查詢是“或”條件新手經(jīng)常不知道怎么寫。比如要查狀態(tài)為 1或作者為某個人的文章filter(status1, authorxxx)是“且”關(guān)系達不到目的。這時候要引入 Q 表達式from django.db.models import Q articles Article.objects.filter(Q(status1) | Q(authorrequest.user))Q 對象用|表示或、表示且前面加~表示非組合復(fù)雜查詢非常方便比手拼 SQL 條件安全得多。2.2 刪除對象delete() 背后的行為和它的返值刪除是另一個高頻操作。視圖里刪除對象一般這么寫article Article.objects.get(idarticle_id) article.delete()看代碼感覺很簡單但有幾個細節(jié)值得說。第一delete()會立即執(zhí)行返回一個具名元組(total_deleted, {app_label.ModelName: count})第一個數(shù)字表示總共刪了多少條記錄。注意總數(shù)可能大于 1因為如果Article外鍵關(guān)聯(lián)了評論、點贊等表并且關(guān)系沒有設(shè)置on_deletemodels.SET_NULL或PROTECT的話Django 會級聯(lián)刪除關(guān)聯(lián)數(shù)據(jù)。新手經(jīng)常在這里誤刪數(shù)據(jù)。第二批量刪除要小心。用QuerySet的delete()Article.objects.filter(status0).delete()這條是直接翻譯成一條DELETE FROMSQL 執(zhí)行的不會調(diào)用模型里重寫的delete()方法也不會觸發(fā)信號signals。如果你的模型在delete()里做了額外處理比如刪除磁盤上的文件、寫日志批量刪除時這部分邏輯就靜默丟失了。第三get()拿不到對象會拋DoesNotExist處理不好就是 500。所以生產(chǎn)代碼里更推薦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 的取舍現(xiàn)在前后端分離的項目常見方案是登錄成功后服務(wù)端生成一個 Token放在 Cookie 里之后每次請求帶上來。Django 對 Cookie 操作非常簡單response HttpResponse(ok) response.set_cookie( auth_token, token_value, max_age7 * 24 * 3600, # 7天有效期單位秒 httponlyTrue, secureFalse, # HTTPS 環(huán)境下要設(shè)為 True samesiteLax, )這里面最容易被忽略的是httponlyTrue。設(shè)置了它之后Cookie 不能用 JavaScript 讀取document.cookie拿不到這樣即使你的前端被注入了惡意腳本也沒法把 Token 直接偷走。這是防御 XSS跨站腳本攻擊非常重要的一層。secureTrue指的是只在 HTTPS 連接下發(fā)送 Cookie。開發(fā)環(huán)境用 HTTP 時這個參數(shù)要設(shè)成 False否則 Cookie 根本種不上你排查半天以為登錄有問題其實只是協(xié)議不匹配。到了生產(chǎn)環(huán)境配好 HTTPS 之后一定要記得切回 True。還有samesite參數(shù)它控制跨站請求時是否攜帶 Cookie對 CSRF跨站請求偽造防護有幫助。默認值隨著 Django 版本演進在收緊建議顯式設(shè)置。2.4 從零創(chuàng)建 Appstartapp 之后你應(yīng)該做什么Django 項目一般由多個 app 組成每個 app 負責一塊獨立的功能模塊。使用命令行創(chuàng)建冒煙測試過沒問題python manage.py startapp blog這個命令會生成models.py、views.py、admin.py、apps.py等基礎(chǔ)文件。但我想說的是startapp 之后真正重要的有一件事在INSTALLED_APPS里注冊這個 app。很多新手在本地開發(fā)時沒注冊也能跑因為 Django 對INSTALLED_APPS里的 app 會做遷移記錄追蹤、模板發(fā)現(xiàn)、靜態(tài)文件收集等操作。沒注冊時makemigrations不會識別這個 app 的模型templates目錄里的模板按默認配置也找不到最典型的現(xiàn)象是頁面明明放在blog/templates/blog/index.html里渲染時卻報 TemplateDoesNotExist。注冊之后別忘了指定app_label或在apps.py里配置好name。還有一種情況是同一個 app 被用于多個項目你需要在INSTALLED_APPS里寫成blog.apps.BlogConfig以便讓 Django 找到自定義的ready()方法信號注冊就是放這里。這也是 AI Agent 更容易出錯的地方——它默認按最簡單的寫法生成代碼不會主動管理這些配置細節(jié)。3. 生產(chǎn)部署的核心架構(gòu)為什么是 Nginx 加 uWSGI本地用python manage.py runserver跑得再好也只是一個單進程開發(fā)服務(wù)器。真實的生產(chǎn)環(huán)境需要面對并發(fā)請求、靜態(tài)文件處理、進程崩潰恢復(fù)、證書卸載等一堆問題。目前最經(jīng)典的組合就是 Nginx uWSGI Django當然也有 Gunicorn后面我會對比。這一節(jié)先把架構(gòu)和選型講清楚下一節(jié)進入實戰(zhàn)配置。3.1 Django 自帶 runserver 為什么不能上生產(chǎn)runserver是 Django 內(nèi)置的輕量級服務(wù)器它的設(shè)計目標是開發(fā)調(diào)試不是承載線上流量。原因主要有三點單進程模型runserver默認只啟動一個進程一次只能處理一個請求。雖然它有自動重載功能但并發(fā)能力非常弱幾十個人同時訪問就明顯卡頓。沒有做靜態(tài)文件優(yōu)化生產(chǎn)環(huán)境通常由 Nginx 直接托管 CSS、JS、圖片等靜態(tài)資源不經(jīng)過 Django 進程。runserver雖然能順便托管靜態(tài)文件但這是它手工處理的邏輯性能和優(yōu)先級都不行。缺少進程守護開發(fā)服務(wù)器崩了就崩了你不會希望生產(chǎn)環(huán)境里每隔兩小時跑一次python manage.py runserver。所以生產(chǎn)環(huán)境的思路是把業(yè)務(wù)處理交給一個用 WSGI 協(xié)議的服務(wù)進程把請求入口、靜態(tài)資源、負載均衡交給專業(yè)的反向代理服務(wù)器。3.2 架構(gòu)全景瀏覽器到 Django 的完整鏈路一個典型的生產(chǎn)架構(gòu)長這樣用戶瀏覽器 ↓ HTTPS / HTTP Nginx80/443端口反向代理 ├─ 靜態(tài)文件直接返回CSS/JS/圖片 ├─ 動態(tài)請求 → uWSGIsocket通信→ Django應(yīng)用 └─ 證書卸載、請求頭處理、訪問控制接著逐段解釋。用戶在瀏覽器輸入域名DNS 解析到服務(wù)器 IP請求到達 Nginx。Nginx 是一個事件驅(qū)動的異步服務(wù)器單進程能管成千上萬個連接所以它特別適合做“流量入口”。它根據(jù)配置判斷請求的路徑如果請求的是/static/下的靜態(tài)資源直接讀磁盤上的文件返回完全不驚動 Django如果請求的是動態(tài)頁面或 API則將請求通過 uWSGI 協(xié)議轉(zhuǎn)發(fā)給后端的 uWSGI 進程。uWSGI 是一個實現(xiàn)了 WSGI 協(xié)議的進程它的工作就是把 Nginx 轉(zhuǎn)來的請求信息轉(zhuǎn)換成 Django 能處理的 WSGI 環(huán)境然后調(diào)用你的 Django 應(yīng)用。Django 跑在 uWSGI 里使用的是真正的多進程/多線程能力多個進程同時處理請求瓶頸不再是一個請求串行執(zhí)行。最后一個問題為什么不直接把請求發(fā)給 Flask、FastAPI 這類應(yīng)用跑在 Nginx 后面可以但 uWSGI 協(xié)議比 HTTP 轉(zhuǎn)發(fā)開銷更低而且和 Django 配合多年已經(jīng)非常成熟官方文檔也是按這個方案寫的。3.3 uWSGI 與 Gunicorn哪個更值得選部署 Django 時最常見的兩個 WSGI 服務(wù)器就是 uWSGI 和 Gunicorn。很多人糾結(jié)選哪個我的經(jīng)驗放在這里對比項uWSGIGunicorn性能可調(diào)參數(shù)多極限性能更高中規(guī)中矩但足夠大多數(shù)業(yè)務(wù)配置復(fù)雜度配置項非常多學(xué)習曲線陡命令行參數(shù)簡單容易上手內(nèi)存占用可精細化調(diào)整配置不當比 Gunicorn 高默認配置下比較省心社區(qū)資料老牌方案教程極多近年更流行部署 Docker 更輕便與 Nginx 配合原生支持 uwsgi 協(xié)議通常用 http 或 gunicorn 的 unix socket 轉(zhuǎn)發(fā)我的結(jié)論是如果是新手第一次部署用 Gunicorn 會更省心但如果你想深入了解 WSGI 服務(wù)器的機制、或者需要對性能極限做壓測優(yōu)化uWSGI 的可玩性和資料豐富程度更好。這個話題貼主選擇了 uWSGI那我基于他的選擇展開講兩者概念高度相似學(xué)懂一個另一個也很容易上手。另外如果你的項目用了 Django 的異步能力比如 Channels、異步視圖WSGI 服務(wù)器就不夠用了得換 ASGI 服務(wù)器如 Daphne、Uvicorn。Django 4 對異步的支持越來越好但這是另一個話題。對于傳統(tǒng)的同步業(yè)務(wù)WSGI 方案完全足夠。4. 生產(chǎn)實戰(zhàn)uWSGI 配置全過程現(xiàn)在進入動手環(huán)節(jié)。假設(shè)你有一臺 Linux 服務(wù)器烏班圖/Debian/CentOS 都類似項目代碼已經(jīng)通過 Git 同步到服務(wù)器上虛擬環(huán)境已經(jīng)建好。下面從安裝開始把 uWSGI 配置給我們家一步一步講透。4.1 安裝與基礎(chǔ)驗證先激活虛擬環(huán)境然后安裝 uWSGIcd /opt/myproject source venv/bin/activate pip install uwsgi安裝完成后先在項目目錄下做一次最小化測試確保 uWSGI 能正常拉起 Django。Django 項目根目錄下都有一個wsgi.py文件它定義了 WSGI 應(yīng)用入口。用下面這條命令啟動 uWSGIuwsgi --http 127.0.0.1:8080 --chdir /opt/myproject --module myproject.wsgi --venv /opt/myproject/venv參數(shù)說明--http 127.0.0.1:8080監(jiān)聽本機 8080 端口先用 HTTP 模式驗證。--chdir切換到項目目錄否則 uWSGI 找不到myproject/wsgi.py。--module myproject.wsgi指定 WSGI 應(yīng)用模塊。--venv指定虛擬環(huán)境路徑。然后在本機用curl測試curl http://127.0.0.1:8080/health/返回 HTTP 200 就說明 uWSGI 已經(jīng)能跑起 Django 了。注意這一步只是驗證環(huán)境真正上線不會用--http而是用 socket 模式配合 Nginx。4.2 一個能用的 uwsgi.ini 是怎么寫的命令行參數(shù)太長而且沒法持久化。生產(chǎn)環(huán)境建議把配置寫進uwsgi.ini文件。我這邊提供一個實戰(zhàn)可用的模板每一行都會解釋為什么這么寫[uwsgi] # 項目目錄 chdir /opt/myproject # wsgi入口 module myproject.wsgi:application # 虛擬環(huán)境 home /opt/myproject/venv # 使用 unix socket與 Nginx 通信 socket /run/uwsgi/myproject.sock # 調(diào)整 socket 權(quán)限保證 Nginx 可訪問 chmod-socket 664 # 以指定用戶運行不要用 root uid www-data gid www-data # 開啟主進程管理子進程 master true # 子進程數(shù)量一般等于 CPU 核數(shù) processes 4 # 每個進程開啟線程數(shù) threads 2 # 自動移除廢棄的 socket 文件 vacuum true # 后臺運行日志寫入文件 daemonize /var/log/uwsgi/myproject.log # 日志輪轉(zhuǎn)避免單文件無限膨脹 log-maxsize 50000000幾個關(guān)鍵選擇的理由為什么用 unix socket 而不是網(wǎng)絡(luò)端口因為本機 Nginx 和 uWSGI 通過 socket 文件通信不走 TCP/IP 協(xié)議棧開銷更小也更安全外部訪問不到。前提是 Nginx 運行用戶比如www-data對 socket 文件有讀寫權(quán)限所以設(shè)了chmod-socket 664并指定uid/gid都是www-data。processes 和 threads 怎么定一個經(jīng)驗公式processes取服務(wù)器 CPU 核心數(shù)threads通常為 2 或 4。需要注意每個進程都會占據(jù)一份 Django 應(yīng)用內(nèi)存因為 Django 的模型代碼是加載到進程內(nèi)存里的如果服務(wù)器只有 1G 內(nèi)存4 個進程可能直接吃滿。建議先用free -h看下內(nèi)存然后從processes 2起步壓測后再慢慢調(diào)。為什么開 master true開啟主進程之后uWSGI 才能管理子進程子進程崩潰后自動拉起也能優(yōu)雅地處理重新加載配置。否則單個進程掛了服務(wù)就真掛了。4.3 通過 systemd 管理 uWSGI上一節(jié)里用了daemonize讓 uWSGI 后臺運行但如果機器重啟uWSGI 并不會自動啟動。生產(chǎn)環(huán)境里我們會寫一個 systemd 服務(wù)讓操作系統(tǒng)幫我們守護它。在/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然后執(zhí)行systemctl daemon-reload systemctl enable uwsgi systemctl start uwsgiRestartalways的意思是進程異常退出后5 秒自動拉起。配合 uWSGI 自身的master進程管理雙保險。這一步做完之后別急著配置 Nginx先用 systemd 狀態(tài)確認 uWSGI 起來沒有、日志里有沒有報錯systemctl status uwsgi tail -f /var/log/uwsgi/myproject.log日志才是你排查問題的最好朋友。多數(shù) 502 錯誤看 uWSGI 日志一眼就能定位是項目代碼報錯、數(shù)據(jù)庫連接失敗還是 socket 權(quán)限不對。5. Nginx 反向代理與站點配置手寫一份能上線的 confuWSGI 已經(jīng)在后臺待命了現(xiàn)在輪到 Nginx 登場。Nginx 的工作有兩個重點把/static/這類靜態(tài)請求直接返回文件把動態(tài)請求通過 uwsgi 協(xié)議轉(zhuǎn)給后端。這一節(jié)從安裝開始到寫出一份能真正上線的配置。5.1 Nginx 安裝和一些基本概念在 Debian/Ubuntu 系服務(wù)器上用 apt 安裝即可apt update apt install nginx裝完先確認版本和狀態(tài)nginx -v systemctl status nginxNginx 的核心配置文件是/etc/nginx/nginx.conf它通過include指令加載/etc/nginx/sites-enabled/目錄下的站點配置。所以多站點管理的方式就是在sites-available里寫多個站點的配置文件用軟鏈接把它們啟用到sites-enabled。在動手寫配置前先理解 Nginx 配置的幾個常用指令的含義server定義一個虛擬主機監(jiān)聽某個端口的請求。location匹配 URL 路徑匹配到的請求按該塊內(nèi)的規(guī)則處理。proxy_pass把請求反向代理到指定的后端地址HTTP 協(xié)議。uwsgi_pass類似proxy_pass但使用的 uwsgi 協(xié)議專門用于和 uWSGI 通信。include把其他文件的配置包含進來。5.2 一份生產(chǎn)可用的 Django 站點配置我貼一份經(jīng)過多個項目驗證的配置重要行都加了注釋# /etc/nginx/sites-available/myproject server { # 監(jiān)聽 80 端口后續(xù)配好 HTTPS 后會把 HTTP 跳轉(zhuǎn)到 HTTPS listen 80; server_name blog.example.com; # 客戶端請求體最大大小如果有上傳文件需求一定要調(diào)大 client_max_body_size 20m; # 靜態(tài)文件直接讀磁盤不經(jīng)過 Django location /static/ { alias /opt/myproject/static/; expires 7d; } # 媒體文件用戶上傳 location /media/ { alias /opt/myproject/media/; expires 30d; } # 動態(tài)請求轉(zhuǎn)發(fā)給 uWSGI location / { # 先嘗試拿靜態(tài)文件拿不到再轉(zhuǎn)發(fā)后端與上一節(jié)alias方案二選一即可 try_files $uri proxy_to_django; } location proxy_to_django { include uwsgi_params; uwsgi_pass unix:/run/uwsgi/myproject.sock; } }這里隱含了一個重要的細節(jié)try_files $uri proxy_to_django不是必須的但推薦保留。它的作用是讓 Nginx 先檢查磁盤上是否有對應(yīng)文件有就直接返回比如偶爾放在項目根目錄的favicon.ico沒有才轉(zhuǎn)發(fā)給 Django。可以省掉一小部分不必要的 Python 進程調(diào)用。那/static/的alias是從哪里來的需要 Django 項目先執(zhí)行一次python manage.py collectstatic這條命令把每個 app 的靜態(tài)文件復(fù)制到STATIC_ROOT指向的目錄我這里示例是/opt/myproject/static/。很多人部署完頁面樣式全丟就是因為沒跑 collectstatic或者 Nginx 的 root/alias 路徑配錯了。5.3 本地多站點與開發(fā)環(huán)境配置標題熱詞里出現(xiàn)了一個有意思的場景本地加虛擬機多端口 Nginx、開發(fā)環(huán)境多站點、自定義域名配置。很多人需要在開發(fā)機上模擬多站點域名這時候 Nginx 也能派上用場。編輯本機的/etc/hostsWindows 是C:\Windows\System32\drivers\etc\hosts把自定義域名指向虛擬機的 IP192.168.56.101 blog.test 192.168.56.101 api.test然后在虛擬機 Nginx 里配置兩個server塊監(jiān)聽不同端口比如 8080 和 8081或者同一 80 端口不同server_nameserver { listen 8080; server_name blog.test; # 轉(zhuǎn)發(fā)到本機 uWSGI 或其他開發(fā)服務(wù) 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; } }本地多端口的思路是每個開發(fā)服務(wù)監(jiān)聽不同的本機端口8000、8001、8002Nginx 負責把域名加端口映射到對應(yīng)端口。這樣不需要頻繁改后端服務(wù)本身的端口也算提前熟悉了反向代理的工作方式。5.4 靜態(tài)文件、媒體文件處理以及 access/error 日志再次強調(diào)生產(chǎn)環(huán)境的靜態(tài)文件一定要讓 Nginx 接管。否則每次請求一個 CSS 文件都要經(jīng)過 uWSGI 里的 Django 進程CPU 和內(nèi)存白白浪費。在模板里你應(yīng)當使用{% 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;經(jīng)常出現(xiàn)的情況是網(wǎng)站掛了一查 Nginx 默認的錯誤日志里面寫滿了各種信息但你看不到是自己站點的請求。每個站點獨立日志排查效率會高很多。6. 生產(chǎn)環(huán)境的高階配置SSL、CORS、代理 Ollama、性能上限基礎(chǔ)跑通之后真正的生產(chǎn)環(huán)境還有一堆細節(jié)HTTPS 證書、跨域、反向代理其他內(nèi)網(wǎng)服務(wù)比如 LLM 推理服務(wù)、并發(fā)參數(shù)調(diào)優(yōu)。這些地方坑特別多我挑幾個高頻問題重點講講。6.1 替換 SSL 證書不生效90% 的人踩過的坑熱詞里有一條“nginx替換ssl證書不生效”這個現(xiàn)象太典型了。明明用新證書內(nèi)容替換了舊文件nginx -t也提示語法正確但是瀏覽器訪問還是舊證書。我總結(jié)的排查順序是第一確認證書文件確實被讀到了。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關(guān)鍵點證書沒有修改時間或者系統(tǒng)時間不對可能讓簽發(fā)時間看起來“過期”了。第二檢查是否真的重載了。很多人改了配置后不執(zhí)行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命令可以導(dǎo)出當前實際生效的完整配置看看ssl_certificate到底寫的是什么。第四瀏覽器與中間層緩存。瀏覽器會緩存證書和 HSTS 策略Chrome 的“證書信息”頁面顯示的還是舊證書時試試無痕窗口或者用openssl s_client -connect 域名:443直接看服務(wù)端實際返回的證書。這一步能排除瀏覽器緩存的干擾。我處理過的一個真實案例替換證書后nginx -T顯示配置沒問題但openssl命令拿到的還是舊證書最后發(fā)現(xiàn)是系統(tǒng)里另一個服務(wù)提前占用了 443 端口Nginx 的 443 listener 根本沒成功啟動。這種情況下需要先看 Nginx 日志確認實際監(jiān)聽情況。6.2 反向代理 Ollama 或其他內(nèi)網(wǎng)服務(wù)Header 是關(guān)鍵熱詞里出現(xiàn)了“nginx 代理 ollama 設(shè)置 apikey cherrystudio”和“nginx 反向代理 ollama”說明現(xiàn)在很多團隊會通過 Nginx 給內(nèi)網(wǎng)的模型推理服務(wù)做出口代理。這類服務(wù)的反代其實和代理 Django 大同小異但有兩個特別的坑一個是對外暴露時的 Header 設(shè)置。比如你在內(nèi)網(wǎng)跑了一個 Ollama 服務(wù)監(jiān)聽 11434 端口通過 Nginx 對外提供統(tǒng)一入口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末尾的斜杠很關(guān)鍵http://127.0.0.1:11434/末尾帶斜杠會把/ollama/之后的路徑拼到后端不帶斜杠會把完整路徑原樣傳過去。這個細節(jié)我見過太多人在這上面栽跟頭。關(guān)于 API Key 的傳遞正確的姿勢是不要放在 URL 里明文傳遞而是通過 Header 傳遞并在 Nginx 層統(tǒng)一注入。這樣前端代碼里不會出現(xiàn)密鑰服務(wù)端也只認可信來源的請求location /ollama/ { proxy_set_header Authorization Bearer ${OLLAMA_API_KEY}; proxy_pass http://127.0.0.1:11434/; }把密鑰放在 Nginx 環(huán)境變量或外部配置文件里而不是寫死在 conf 中提交到 Git 倉庫。另外一個容易犯的錯誤是反代到帶 API Key 的服務(wù)時如果后端校驗不過先看看是不是proxy_set_header把客戶端的 Authorization 覆蓋了可以用$http_authorization顯式傳遞或去掉這一行。6.3 CORS 與 preflight 請求的坑前后端分離項目里前端頁面比如https://front.example.com通過 AJAX 請求你的 Django APIhttps://api.example.com時瀏覽器會先發(fā)一個 OPTIONS 預(yù)檢請求確認服務(wù)器允許跨域。如果 Nginx 層沒有處理好就會出現(xiàn) Invalid CORS request 或跨域報錯。一種常見做法是在 Django 層面用django-cors-headers處理跨域。但當你加了 Nginx 反向代理后響應(yīng)頭必須經(jīng)過 Nginx 透傳回瀏覽器。如果 Nginx 恰好把Access-Control-Allow-Origin這類的響應(yīng)頭過濾掉了前端照樣報跨域錯誤。更建議在生產(chǎn)環(huán)境把跨域控制在 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參數(shù)——它表示無論響應(yīng)狀態(tài)碼是 200 還是 4xx、5xx都強制加上這個響應(yīng)頭。沒有always時后端返回 500 錯誤跨域頭就丟了瀏覽器報錯信息會非常誤導(dǎo)人。還有一個容易踩的點Access-Control-Allow-Origin不要盲目設(shè)成*。如果你的接口需要攜帶 Cookie*會導(dǎo)致請求失敗必須顯式指定具體來源域名并確認Access-Control-Allow-Credentials: true已經(jīng)設(shè)置。6.4 并發(fā)與連接數(shù)Nginx 到底能扛多大壓力熱詞里還有一個高頻問題“nginx 最大并發(fā)鏈接數(shù)老是用超”“nginx 最大并發(fā)聯(lián)接數(shù)總是超”。這背后涉及 Nginx 的兩個核心參數(shù)worker_processes auto; events { worker_connections 1024; }worker_processes auto表示按 CPU 核數(shù)啟動 worker 進程一般就設(shè)成自動即可。worker_connections是單個 worker 進程可同時處理的連接數(shù)上限。所以總的最大并發(fā)連接數(shù)約等于最大并發(fā)連接數(shù) worker_processes × worker_connections如果服務(wù)器 4 核、worker_connections默認 1024理論上限 4096。注意這個數(shù)值還包括 Nginx 到后端服務(wù)的連接實際可用連接數(shù)還要打折。你遇到“并發(fā)數(shù)超”的報錯時優(yōu)先排查三件事第一系統(tǒng)文件描述符限制。Linux 默認單個進程能打開的文件句柄數(shù)是 1024而每個網(wǎng)絡(luò)連接都要占用一個文件句柄。Nginx 官方建議把ulimit -n調(diào)高到 65535 甚至更高。配置在/etc/security/limits.conf或者 systemd 服務(wù)文件里設(shè)置LimitNOFILE65535。第二后端 uWSGI 才是瓶頸。如果 Nginx 配置的并發(fā)上限很高但 uWSGI 只有 4 個進程、每個進程 2 個線程每秒能處理的請求量有限連接就會在 Nginx 層堆積。這時調(diào) Nginx 參數(shù)沒有意義得去調(diào)后端的進程數(shù)/線程數(shù)。第三短連接和 TIME_WAIT 積累。大量短連接請求后系統(tǒng)會積累大量 TIME_WAIT 狀態(tài)的 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也有影響具體要看版本配置文檔。大方向就是這個先確認系統(tǒng)資源上限再排查后端處理能力最后調(diào) Nginx 參數(shù)。7. 部署上線后的常見問題速查表最后把最容易遇到的幾個問題整理一下你部署時如果踩到可以直接按這個表排查。這些都是真實線上環(huán)境里高頻出現(xiàn)的問題每條背后都有一段血淚史?,F(xiàn)象最可能原因排查命令 / 操作瀏覽器顯示 502 Bad GatewayuWSGI 進程掛了或者 socket 權(quán)限不對systemctl status uwsgils -l /run/uwsgi/myproject.sock看 uWSGI 日志樣式全丟靜態(tài)文件 404未執(zhí)行 colletstatic或 Nginx alias 路徑錯誤python manage.py collectstatic檢查STATIC_ROOT和 Nginxalias替換證書后仍是舊證書未 reload或多個 server 塊匹配錯誤nginx -Topenssl s_client -connect 域名:443上傳文件過大報 413未設(shè)置client_max_body_sizeNginx 加client_max_body_size 20m;頁面能看到但接口跨域報錯CORS 頭缺失或Access-Control-Allow-Origin設(shè)了*又帶 Cookie檢查 response headers顯式指定來源Django admin 無法登錄Cookie 種不上Cookie 的secureTrue但訪問還是 HTTP開發(fā)環(huán)境關(guān)閉 secure或配置 HTTPS數(shù)據(jù)庫連接數(shù)被打滿視圖 N1 查詢或 uWSGI 進程過多用select_related/prefetch_related優(yōu)化調(diào)低processes日志文件無限增長撐爆磁盤沒配置日志輪轉(zhuǎn)uWSGI 配log-maxsizeNginx 用logrotate再說幾個容易被忽略的雜項問題。Nginxmirror指令超時。如果你用了mirror復(fù)制流量做線上驗證或灰度對比上游服務(wù)響應(yīng)慢會影響主請求嗎mirror默認是異步的但消耗 worker 連接若鏡像目的超時時間長會占住連接導(dǎo)致整體并發(fā)下降。給鏡像請求加proxy_read_timeout、proxy_send_timeout等參數(shù)可以控制。Alpine 容器里掛載 conf.d 報錯。這是容器部署 Nginx 的常見問題Alpine 的 nginx 鏡像用include /etc/nginx/conf.d/*.conf加載配置但你用docker run -v ./mysite.conf:/etc/nginx/conf.d/mysite.conf掛載時如果宿主機文件權(quán)限不對或格式不兼容比如 Windows 的 CRLF 換行Nginx 會直接拒絕啟動?;驹硎窍萪ocker exec進入容器看nginx -T實際加載了哪些配置再檢查文件換行符和掛載路徑。CPython 與 Django 的版本兼容?,F(xiàn)在很多服務(wù)器還在用 Python 3.8但 Django 5.0 要求 Python 3.10部署前先確認python --version和django --version匹配。這個低級錯誤極其常見AI Agent 生成的代碼不會幫你檢查運行環(huán)境版本。8. 一點個人經(jīng)驗和后續(xù)建議寫到這里這個系列的第五篇主體內(nèi)容基本結(jié)束了。最后再分享一個我自己的部署經(jīng)驗。剛開始做部署時我總想把所有配置一步到位多進程、多線程、HTTPS、CDN、自動擴容全部拉滿。結(jié)果每次出問題排查鏈路特別長不知道是 Nginx 的鍋、uWSGI 的鍋還是自己代碼的鍋。后來學(xué)乖了部署流程改成“分層打怪”先只用runserver在服務(wù)器上跑通確認代碼和環(huán)境沒問題——再用最小化 uWSGI 拉起確認 WSGI 配置沒問題——再加一層 Nginx 反代確認協(xié)議和靜態(tài)文件配置沒問題——最后才上 HTTPS、加性能參數(shù)。每一步都驗證通過再往前走出問題永遠只在當前層找原因排查速度快了至少一倍。對于視圖層我建議你把掌握 CBV 當作一個進階目標但不要用它替換所有 FBV。記一個簡單原則一個視圖的方法數(shù)量不超過兩個GET/POST就用 FBV超過兩個或者需要多個頁面復(fù)用相似的查詢、表單處理邏輯再考慮 CBV 和 Mixin。這個原則在我接手過的幾十個項目里基本沒有錯過。如果你是用 AI Agent 輔助開發(fā)尤其要注意讓它解釋清楚它生成的視圖具體走了哪個父類、哪個 Mixin以及as_view()在路由里是怎么注冊的。AI 工具能幫你寫出更長的代碼但代碼背后是 FBV 還是 CBV、請求流程是哪幾條分支這些還是得你自己心里有數(shù)。真到了排查 bug 的時候一個能看懂視圖源碼的開發(fā)者效率上限完全不同。后續(xù)如果條件允許我會再寫一篇關(guān)于 HTTPS 全站配置和 Docker 化部署 Django 的記錄里面涉及的證書續(xù)簽、容器內(nèi)進程守護、數(shù)據(jù)庫備份這些內(nèi)容每一項單獨拎出來都夠講一整篇。這一篇的核心是讓你先把視圖層概念和生產(chǎn)架構(gòu)跑通不要貪多跑通了再進階。