器實(shí)戰(zhàn):上傳存儲(chǔ)與部署全攻略)
Django 做圖片服務(wù)器這事看著簡(jiǎn)單其實(shí)挺多細(xì)節(jié)。最近剛好梳理了一遍完整流程從上傳、存儲(chǔ)、訪問(wèn)到部署踩了不少坑把整個(gè)思路和代碼邏輯記錄下來(lái)給正在做類(lèi)似項(xiàng)目的朋友參考。先說(shuō)清楚這個(gè)項(xiàng)目到底解決什么問(wèn)題需要一個(gè)帶后臺(tái)管理的圖片服務(wù)器能夠接收?qǐng)D片上傳自動(dòng)生成訪問(wèn)鏈接支持圖片的瀏覽和下載并且能跑在 Windows 或者 Linux 環(huán)境里后面方便用 waitress 或 nginx 做生產(chǎn)部署。整個(gè)系統(tǒng)基于 Django 的 MTV 模式來(lái)做數(shù)據(jù)庫(kù)用 MySQL后臺(tái)用 Django 自帶 admin 就能搞定。1. 圖片服務(wù)器的核心需求與整體設(shè)計(jì)1.1 圖片服務(wù)器的三個(gè)核心問(wèn)題做圖片服務(wù)器之前先別急著寫(xiě)代碼把需求拆清楚。一個(gè)圖片服務(wù)器的核心無(wú)外乎三件事往哪里傳、怎么存、怎么取出去。往哪里傳就是上傳入口瀏覽器表單、移動(dòng)端接口、第三方系統(tǒng)對(duì)接統(tǒng)統(tǒng)一律走 HTTP 上傳接口。怎么存文件落在磁盤(pán)哪個(gè)目錄數(shù)據(jù)庫(kù)里存什么信息原文件名、存儲(chǔ)路徑、大小、上傳時(shí)間、MD5 之類(lèi)。怎么取出去怎么生成訪問(wèn) URL要不要權(quán)限控制訪問(wèn)時(shí)是直接返回文件流還是跳轉(zhuǎn)到靜態(tài)文件地址。我在做這個(gè)項(xiàng)目的時(shí)候一開(kāi)始貪圖省事直接用 Django 的 ImageField MEDIA_ROOT MEDIA_URL 那一套開(kāi)發(fā)環(huán)境確實(shí)爽但是一上生產(chǎn)就露餡Django 本身處理靜態(tài)文件的能力很弱并發(fā)一高直接卡死。所以這次我換了思路開(kāi)發(fā)環(huán)境用 Django 開(kāi)發(fā)服務(wù)器湊合生產(chǎn)環(huán)境干脆讓 nginx 直接托管 media 目錄Django 只負(fù)責(zé)業(yè)務(wù)邏輯。這樣圖片訪問(wèn)不走 Django 進(jìn)程性能差距是數(shù)量級(jí)的。1.2 MTV 模式在圖片服務(wù)里的真實(shí)作用Django 的 MTV 模式Model-Template-View在圖片服務(wù)器項(xiàng)目里的價(jià)值平時(shí)寫(xiě)增刪改查感受不深但做圖片服務(wù)的時(shí)候就能體會(huì)出來(lái)了。Model 層負(fù)責(zé)數(shù)據(jù)持久化圖片的元信息存數(shù)據(jù)庫(kù)用 ORM 操作天然支持 MySQL。Template 層負(fù)責(zé)頁(yè)面渲染后臺(tái)預(yù)覽圖列表、上傳頁(yè)面、圖片詳情頁(yè)模板繼承 變量渲染就夠了。View 層負(fù)責(zé)交互邏輯接收請(qǐng)求、保存文件、返回響應(yīng)。說(shuō)白了MTV 模式就是一把尺子它把代碼的職責(zé)劃分清楚圖片上傳、文件存儲(chǔ)、頁(yè)面展示這三種不同方向的邏輯不會(huì)攪在一起后期加功能比如加一個(gè)分類(lèi)、加一個(gè)水印可以在不動(dòng)整體結(jié)構(gòu)的情況下局部擴(kuò)展。1.3 目錄規(guī)劃與靜態(tài)文件/媒體文件的邊界做圖片服務(wù)器最容易犯的錯(cuò)就是混淆靜態(tài)文件和媒體文件。靜態(tài)文件STATICFILES_DIRS項(xiàng)目的 CSS、JS、logo 這類(lèi)固定資源是代碼倉(cāng)庫(kù)的一部分。媒體文件MEDIA_ROOT用戶(hù)上傳的圖片是運(yùn)行時(shí)產(chǎn)生的數(shù)據(jù)不在版本控制范圍內(nèi)。一旦混在一起部署的時(shí)候要么忘了配靜態(tài)目錄要么媒體文件被刷新清理掉各種翻車(chē)。我這次的目錄規(guī)劃是這樣的project_root/ ├── manage.py ├── config/ # 項(xiàng)目配置 ├── media/ # 用戶(hù)上傳的圖片 │ ├── uploads/ # 原圖目錄 │ └── thumbnails/ # 縮略圖目錄可以后加 ├── static/ # 靜態(tài)文件夾 ├── apps/ │ └── image_server/ # 圖片服務(wù)核心 app這個(gè)規(guī)劃的精髓不是目錄結(jié)構(gòu)本身而是邊界感media 只放上傳文件static 只放項(xiàng)目自帶資源中間用 settings 的 MEDIA_ROOT 和 STATIC_ROOT 完全隔離。這樣部署的時(shí)候nginx 配置兩條 location 規(guī)則互不干擾。2. 上傳流程的完整拆解2.1 上傳表單與模型設(shè)計(jì)FileField 還是手動(dòng)保存Django 里保存圖片的標(biāo)準(zhǔn)做法是模型字段用 ImageField表單用 ModelForm視圖里直接 form.save() 完成入庫(kù)落盤(pán)。但靈活度比較差比如你想在保存前壓縮圖片、想給文件名加鹽、想同時(shí)生成縮略圖這一套做起來(lái)非常別扭。我這次選用的是模型 ImageField 手動(dòng) save 文件的組合既保留 ORM 的優(yōu)勢(shì)又把文件處理的控制權(quán)掌握在自己手里。模型定義長(zhǎng)這樣import os import uuid from django.db import models class ImageAsset(models.Model): id models.UUIDField(primary_keyTrue, defaultuuid.uuid4, editableFalse) title models.CharField(max_length255, verbose_name文件標(biāo)題) image models.ImageField(upload_touploads/%Y/%m/, verbose_name圖片文件) size models.IntegerField(default0, verbose_name文件大小(字節(jié))) content_type models.CharField(max_length100, blankTrue, verbose_name文件類(lèi)型) md5 models.CharField(max_length32, blankTrue, verbose_name文件MD5) uploaded_at models.DateTimeField(auto_now_addTrue, verbose_name上傳時(shí)間) class Meta: db_table image_asset ordering [-uploaded_at] def __str__(self): return self.title def delete(self, *args, **kwargs): # 刪記錄時(shí)同步清掉磁盤(pán)文件 if self.image: if os.path.isfile(self.image.path): os.remove(self.image.path) super().delete(*args, **kwargs)注意 upload_to 我用了uploads/%Y/%m/這種格式Django 會(huì)自動(dòng)按年月生成子目錄避免一個(gè)目錄下文件太多導(dǎo)致文件系統(tǒng)性能下降。這也是文件存儲(chǔ)的一個(gè)常見(jiàn)優(yōu)化點(diǎn)分目錄存儲(chǔ)比平鋪存儲(chǔ)性能好得多。2.2 視圖里到底發(fā)生了什么request.FILES 的真相視圖函數(shù)是圖片上傳邏輯的核心這里我給一個(gè)可以直接用的版本重點(diǎn)在于讀懂每一行代碼在干什么而不是抄完就跑。import hashlib from django.shortcuts import render, redirect from django.contrib import messages from .models import ImageAsset def upload_image(request): if request.method POST: # 1. 從表單里取出文件對(duì)象 upload_file request.FILES.get(image_file) title request.POST.get(title, ).strip() if not upload_file: messages.error(request, 請(qǐng)選擇要上傳的圖片文件) return redirect(upload) # 2. 計(jì)算MD5去掉重 md5 hashlib.md5() for chunk in upload_file.chunks(): md5.update(chunk) file_md5 md5.hexdigest() existed ImageAsset.objects.filter(md5file_md5).first() if existed: messages.warning(request, f文件已存在{existed.title}) return redirect(upload) # 3. 創(chuàng)建模型實(shí)例保存后自動(dòng)落盤(pán) asset ImageAsset( titletitle if title else upload_file.name, imageupload_file, sizeupload_file.size, content_typeupload_file.content_type, md5file_md5, ) asset.save() messages.success(request, f上傳成功{asset.title}) return redirect(upload) return render(request, image_server/upload.html)關(guān)鍵點(diǎn)有兩個(gè)第一個(gè)request.FILES 里的文件對(duì)象不是真實(shí)文件而是內(nèi)存/臨時(shí)文件包裝對(duì)象。你讀取它的時(shí)候Django 會(huì)按文件大小策略決定放內(nèi)存還是落臨時(shí)文件所以你不要自己手動(dòng) open 文件去處理直接把它當(dāng)成文件句柄用就行。第二個(gè)MD5 去重一定要用 chunks() 分塊讀取不能 read() 整個(gè)文件。大圖 10MB 的時(shí)候read() 會(huì)直接把內(nèi)存吃爆chunks() 每次只讀 64KB內(nèi)存占用穩(wěn)定。2.3 上傳校驗(yàn)與安全防護(hù)不是所有“圖片”都是圖片很多初學(xué)者只校驗(yàn)文件后綴.jpg就放行結(jié)果被人傳了一個(gè).jpg后綴的 PHP 腳本上去網(wǎng)站直接被打穿。后端校驗(yàn)一定要做兩層第一層MIME 類(lèi)型校驗(yàn)Django 的 ImageField 會(huì)在模型驗(yàn)證時(shí)調(diào)用 Pillow 打開(kāi)圖片如果不是合法圖片會(huì)拋異常。所以模型里用 ImageField天然就有一道防線(xiàn)。第二層魔數(shù)校驗(yàn)也叫文件簽名校驗(yàn)。圖片文件的頭部幾個(gè)字節(jié)是固定的比如 JPEG 文件以FF D8 FF開(kāi)頭PNG 文件以89 50 4E 47開(kāi)頭。上次傳的文件實(shí)實(shí)在在讀出來(lái)對(duì)它是有意義的內(nèi)容而不是看后綴和 MIME 表面值。def check_image_magic(file): 校驗(yàn)文件魔力數(shù)字防止偽造圖片后綴的可執(zhí)行文件 file.seek(0) header file.read(4) if header.startswith(b\xff\xd8\xff): return jpeg if header.startswith(b\x89PNG): return png if header.startswith(bGIF8): return gif if header.startswith(bRIFF) and file.read(4) bWEBP: return webp return None在實(shí)際生產(chǎn)環(huán)境里我建議在視圖層調(diào)用這個(gè)函數(shù)返回 None 直接拒絕上傳不要依賴(lài)表單校驗(yàn)?zāi)且粚印?. 圖片訪問(wèn)與下載的細(xì)節(jié)實(shí)現(xiàn)3.1 media 配置的坑開(kāi)發(fā)環(huán)境 vs 生產(chǎn)環(huán)境開(kāi)發(fā)環(huán)境訪問(wèn)上傳的圖片純靠 settings 里兩個(gè)參數(shù)MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media然后在根 URLconf 里掛一條from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 你的路由 ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)這個(gè)static()函數(shù)只在 DEBUG 模式下有效在生產(chǎn)環(huán)境DEBUGFalse完全不生效這是很多人部署后圖片全掛的核心原因。正確的做法是生產(chǎn)環(huán)境讓 nginx 來(lái)處理 /media/ 下的所有請(qǐng)求Django 不參與文件傳輸。3.2 瀏覽頁(yè)面與圖片列表大圖預(yù)覽別直接懟原圖后臺(tái)管理系統(tǒng)里圖片列表頁(yè)是最容易卡的地方。如果列表頁(yè)直接渲染 20 張?jiān)瓐D每張 5MB瀏覽器要下載 100MB 數(shù)據(jù)那個(gè)酸爽只有自己知道。我做列表頁(yè)的時(shí)候用了一個(gè)簡(jiǎn)單粗暴的方案不要用系統(tǒng)自帶的縮略圖生成把整個(gè)列表頁(yè)改成“延遲加載 少量數(shù)據(jù)”。先只請(qǐng)求圖片的元信息標(biāo)題、大小、上傳時(shí)間用 CSS 占位然后等用戶(hù)滾動(dòng)到目標(biāo)區(qū)域動(dòng)態(tài)加載縮略圖或者小圖。如果確實(shí)需要縮略圖可以用 Pillow 在保存原圖時(shí)順手生成一張然后用 ImageField 的處理器來(lái)處理??s略圖目錄單獨(dú)放讀取性能和體驗(yàn)都好。3.3 FileResponse 與 StreamingHttpResponse 的選擇圖片下載接口是圖片服務(wù)器的一個(gè)重要功能。很多人的第一反應(yīng)是用 HttpResponse 讀文件然后返回這在文件小的時(shí)候沒(méi)問(wèn)題文件一大會(huì)出兩個(gè)問(wèn)題內(nèi)存占用高、響應(yīng)慢。Django 官方推薦的做法是 FileResponse。它會(huì)幫你處理文件對(duì)象的生命周期還能自動(dòng)設(shè)置 Content-Type 和 Content-Length配合 Web 服務(wù)器做流式傳輸性能很好。from django.http import FileResponse from django.shortcuts import get_object_or_404 def download_image(request, pk): asset get_object_or_404(ImageAsset, pkpk) response FileResponse(asset.image.open(rb), as_attachmentTrue) return response那 StreamingHttpResponse 什么時(shí)候用呢它更適合動(dòng)態(tài)生成的數(shù)據(jù)流比如需要實(shí)時(shí)壓縮的圖片、需要加密的流、或者從外部系統(tǒng)拉取后再轉(zhuǎn)發(fā)的流。StreamingHttpResponse 可以按塊迭代返回任意內(nèi)容。在實(shí)際項(xiàng)目里一種典型用法是基于 StreamingHttpResponse 做圖片防盜鏈處理。nginx 層面已經(jīng)做了訪問(wèn)控制但某些特殊場(chǎng)景比如 VIP 會(huì)員圖片需要應(yīng)用層控制可以寫(xiě)個(gè)視圖完整校驗(yàn)用戶(hù)權(quán)限后把圖片數(shù)據(jù)按 64KB 分塊流式讀給客戶(hù)端避免大圖片把內(nèi)存打滿(mǎn)from django.http import StreamingHttpResponse def stream_protected_image(request, pk): asset get_object_or_404(ImageAsset, pkpk) # 注意這里要做權(quán)限校驗(yàn)省略部分業(yè)務(wù)代碼 def file_iterator(file_path, chunk_size64 * 1024): with open(file_path, rb) as f: while chunk : f.read(chunk_size): yield chunk response StreamingHttpResponse( file_iterator(asset.image.path), content_typeasset.content_type or image/jpeg ) response[Content-Disposition] finline; filename{asset.title} return response3.4 content_type 和 content_disposition 參數(shù)實(shí)戰(zhàn)很多朋友問(wèn) StreamingHttpResponse 里 content_type 和 Content-Disposition 怎么設(shè)置這倆參數(shù)其實(shí)決定了兩件不同的事content_type 決定瀏覽器用什么方式解析數(shù)據(jù)比如image/jpeg瀏覽器就會(huì)嘗試渲染如果設(shè)置成application/octet-stream瀏覽器基本會(huì)直接下載。Content-Disposition 決定數(shù)據(jù)是“直接顯示”還是“附件下載”# 內(nèi)聯(lián)瀏覽器直接顯示圖片 response[Content-Disposition] inline; filenameexample.jpg # 附件瀏覽器彈出下載 response[Content-Disposition] attachment; filenameexample.jpg這里有個(gè)坑如果文件名是中文直接塞進(jìn) Content-Disposition 里瀏覽器可能解析異常導(dǎo)致下載文件名亂碼。標(biāo)準(zhǔn)做法是用 RFC 5987 的編碼格式from urllib.parse import quote filename asset.title response[Content-Disposition] fattachment; filename*UTF-8{quote(filename)}我實(shí)際測(cè)試過(guò)Chrome、Firefox、Edge 都能正確識(shí)別 filename* 的值這是處理中文下載名最穩(wěn)的方案。4. 部署與生產(chǎn)的完整落地waitress nginx 組合實(shí)測(cè)4.1 Windows 環(huán)境waitress nginx 部署實(shí)錄這里說(shuō)一下 Windows 環(huán)境下的部署方案。很多人想用 gunicorn但 gunicorn 在 Windows 下支持很爛pypiwin32 都救不回來(lái)。這個(gè)場(chǎng)景下 waitress 是首選純 Python 實(shí)現(xiàn)、跨平臺(tái)、穩(wěn)定。安裝和啟動(dòng)都很簡(jiǎn)單pip install waitress waitress-serve --listen127.0.0.1:8000 config.wsgi:application但直接用 waitress 跑還不夠waitress 只負(fù)責(zé) Django 應(yīng)用的動(dòng)態(tài)請(qǐng)求處理靜態(tài)文件和圖片的加載能力非常有限。所以需要在它前面加一層 nginxupstream django_backend { server 127.0.0.1:8000; keepalive 16; } server { listen 80; server_name your.domain.com; # 上傳的圖片直接由nginx讀磁盤(pán)返回 location /media/ { alias D:/project_root/media/; expires 30d; access_log off; } # 靜態(tài)資源 location /static/ { alias D:/project_root/staticfiles/; expires 7d; access_log off; } # 其余請(qǐng)求轉(zhuǎn)給waitress location / { proxy_pass http://django_backend; 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; } }這套方案我在 Windows Server 上實(shí)測(cè)過(guò)waitress 負(fù)責(zé) Django 業(yè)務(wù)邏輯nginx 負(fù)責(zé)靜態(tài)文件和圖片的 IO各干各的互不搶資源體量在日均幾十萬(wàn)次訪問(wèn)以?xún)?nèi)完全夠用。4.2 Linux 常規(guī)部署nohup gunicorn nginx 的黃金組合如果部署環(huán)境是 Linux那組合就變成 gunicorn nginx。Gunicorn 是 Unix 專(zhuān)屬的高性能 WSGI 服務(wù)器在 Windows 上跑不了得注意區(qū)分平臺(tái)選型。啟動(dòng)時(shí)最關(guān)鍵的是 worker 數(shù)量。worker 太少扛不住并發(fā)worker 太多內(nèi)存爆掉。常規(guī)經(jīng)驗(yàn)公式是worker 數(shù) (CPU核心數(shù) x 2) 1如果是對(duì)圖片處理密集型應(yīng)用生成縮略圖、讀大圖建議 worker 數(shù) CPU核心數(shù)因?yàn)閳D片處理往往是 CPU 密集 IO 密集混雜的開(kāi)太多 worker 反而會(huì)頻繁上下文切換。啟動(dòng)命令nohup gunicorn config.wsgi:application --workers 3 --threads 2 --timeout 60 \ --bind 127.0.0.1:8000 --access-logfile /var/log/gunicorn/access.log \ --error-logfile /var/log/gunicorn/error.log 注意 --timeout 參數(shù)圖片上傳接口如果處理大圖壓縮經(jīng)常超過(guò)默認(rèn)的 30 秒調(diào)成 60 或者 120 秒以免被誤殺。4.3 管理后臺(tái)的配置要點(diǎn)Django admin 在圖片服務(wù)器里承擔(dān)了非常核心的職責(zé)上傳圖片、審核圖片、刪除違規(guī)圖片。所以我們要好好配置 admin讓運(yùn)營(yíng)同學(xué)一看就會(huì)用。from django.contrib import admin from .models import ImageAsset admin.register(ImageAsset) class ImageAssetAdmin(admin.ModelAdmin): list_display (title, image_preview, size, content_type, uploaded_at) list_display_links (title,) search_fields (title,) list_filter (content_type, uploaded_at) readonly_fields (size, content_type, md5, uploaded_at) list_per_page 20 def image_preview(self, obj): if obj.image: return format_html(img src{} stylemax-width: 120px; max-height: 80px; /, obj.image.url) return 無(wú)圖片 image_preview.short_description 預(yù)覽說(shuō)實(shí)話(huà)Django admin 在圖片預(yù)覽這方面體驗(yàn)一般但勝在零成本、開(kāi)箱即用。給運(yùn)營(yíng)同學(xué)培訓(xùn)一次就能上手對(duì)內(nèi)部管理工具來(lái)說(shuō)完全夠用。如果后面需要給外部用戶(hù)提供上傳功能我建議跳過(guò) admin單獨(dú)寫(xiě)一個(gè)上傳頁(yè)面界面自由度高很多可以兼容前端框架。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 圖片上傳失敗、403 Forbidden 排查思路這類(lèi)問(wèn)題出現(xiàn)的頻率極高我通常按順序排查效率最高檢查 view 有沒(méi)有設(shè)置csrf_exempt如果是對(duì)接第三方系統(tǒng)而非表單請(qǐng)求CSRF 會(huì)攔截導(dǎo)致 403。檢查 nginx 配置里的 client_max_body_size。nginx 默認(rèn)只允許 1MB 請(qǐng)求體傳個(gè) 5MB 的圖直接 413很多人會(huì)誤解成上傳失敗。檢查 MEDIA_ROOT 目錄權(quán)限進(jìn)程是否可寫(xiě)。Linux 下最常遇到chmod -R 755 media/或者調(diào)整屬主都能解決。檢查 Django 日志IMAGE_STORAGE_ERROR這類(lèi)日志會(huì)直接打印異常信息。5.2 中文文件名亂碼與下載名無(wú)效前面提到過(guò)Content-Disposition 塞中文名會(huì)亂碼。除了用 RFC 5987 編碼之外還有一個(gè)額外技巧文件名里盡量剔除特殊字符。我在保存文件時(shí)直接用 UUID 重命名原始文件名只存數(shù)據(jù)庫(kù)import uuid from pathlib import Path def generate_upload_path(instance, filename): ext Path(filename).suffix.lower() new_name f{uuid.uuid4().hex}{ext} return fuploads/{instance.uploaded_at:%Y/%m}/{new_name}這樣磁盤(pán)上的文件名永遠(yuǎn)不會(huì)出現(xiàn)中文和空格不管是 Linux 還是 Windows 都不會(huì)有路徑問(wèn)題而用戶(hù)看到的下載名取自數(shù)據(jù)庫(kù)的 title 字段再用 URL 編碼處理體驗(yàn)是正常的。5.3 大圖加載慢、內(nèi)存飆升、服務(wù)卡死這類(lèi)問(wèn)題幾乎都是因?yàn)闆](méi)有做流式讀取或者沒(méi)有做圖片壓縮。解決方案分三個(gè)層面應(yīng)用層用 FileResponse 或 StreamingHttpResponse 分塊讀取避免整個(gè)文件讀進(jìn)內(nèi)存。nginx 層開(kāi)啟 sendfile 和 gzipnginx 直接從磁盤(pán)的緩存發(fā)送文件不經(jīng)過(guò)用戶(hù)態(tài)復(fù)制。圖片處理層大圖超過(guò) 2MB保存時(shí)自動(dòng)壓一版 WebP 或質(zhì)量 80 的 JPEG體積能減小 60% 以上訪問(wèn)速度提升明顯。5.4 刪除對(duì)象時(shí)文件沒(méi)刪干凈Django 的 ImageField 不會(huì)在你刪除數(shù)據(jù)庫(kù)記錄時(shí)自動(dòng)刪除磁盤(pán)文件很多新手在這里踩坑。解決辦法是像前面代碼里寫(xiě)的那樣重寫(xiě)模型的 delete() 方法先刪文件再刪記錄。這里要特別注意如果文件被多張記錄引用重寫(xiě) delete() 時(shí)會(huì)出現(xiàn)一個(gè)記錄刪了其他記錄還在用這個(gè)文件的情況。所以我建議文件去重邏輯要徹底數(shù)據(jù)庫(kù)存 MD5上傳時(shí)檢查重復(fù)重復(fù)就直接復(fù)用之前記錄的 URL不再生成新文件。5.5 問(wèn)題排查速查表癥狀可能原因解決手段上傳報(bào) 413nginx client_max_body_size 限制nginx 增加 client_max_body_size 10m上傳報(bào) 403CSRF 校驗(yàn)失敗表單加 csrf_token 或視圖豁免上傳報(bào) 500MEDIA_ROOT 目錄不存在或不可寫(xiě)創(chuàng)建目錄并設(shè)置屬主圖片無(wú)法顯示DEBUGFalse 但未配置 nginx 處理 /media/按示例配置 nginx location /media/下載文件名亂碼Content-Disposition 編碼不兼容使用 filename* 和 RFC 5987大數(shù)據(jù)量時(shí)列表卡頓每頁(yè)加載原圖過(guò)多列表頁(yè)改懶加載/縮略圖圖片刪除后 URL 仍可訪問(wèn)未配置緩存清理或 nginx 緩存清理 CDN 緩存/設(shè)置正確的緩存策略Linux 啟動(dòng)報(bào)錯(cuò)誤用了 gunicorn換 waitress-serve 或安裝后重試6. 圖片服務(wù)器的擴(kuò)展方向與優(yōu)化建議一個(gè)圖片服務(wù)器做完基礎(chǔ)的上傳、存儲(chǔ)、訪問(wèn)、部署其實(shí)已經(jīng)能支撐大多數(shù)業(yè)務(wù)了。但真實(shí)業(yè)務(wù)場(chǎng)景往往不會(huì)滿(mǎn)足于“能跑”還有幾個(gè)方向值得繼續(xù)打磨。第一圖片自動(dòng)化處理。上傳時(shí)自動(dòng)生成多個(gè)尺寸的縮略圖微信公眾號(hào)需要 900px 寬的配圖移動(dòng)端可能只需要 300px桌面端要 1280px。可以在保存時(shí)用 Pillow 統(tǒng)一生成按用途分目錄前端按場(chǎng)景取圖。第二CDN 與存儲(chǔ)分離。圖片量上去之后單機(jī)磁盤(pán)存儲(chǔ)遲早會(huì)成為瓶頸。常見(jiàn)的演進(jìn)路線(xiàn)是先遷到對(duì)象存儲(chǔ)再掛 CDN 加速。Django 的存儲(chǔ)后端可以替換為 django-storages一行 settings 切換對(duì)外接口不變。第三訪問(wèn)日志與監(jiān)控。圖片服務(wù)器的熱圖、訪問(wèn)來(lái)源、異常請(qǐng)求這些數(shù)據(jù)對(duì)業(yè)務(wù)運(yùn)營(yíng)和排錯(cuò)都很有價(jià)值。建議在 nginx 層記錄訪問(wèn)日志定期同步到分析系統(tǒng)別留在應(yīng)用層統(tǒng)計(jì)應(yīng)用層統(tǒng)計(jì)會(huì)導(dǎo)致性能下降?;氐竭@次梳理的 Django 圖片服務(wù)器項(xiàng)目整體實(shí)現(xiàn)思路并不復(fù)雜核心就是三個(gè)環(huán)節(jié)可靠的模型設(shè)計(jì)、規(guī)范的上傳校驗(yàn)、合理的部署架構(gòu)。按這個(gè)流程走下來(lái)開(kāi)發(fā)環(huán)境、Windows 生產(chǎn)、Linux 生產(chǎn)三套環(huán)境全都能順利跑通。我自己在實(shí)操過(guò)程中最大的感觸是圖片服務(wù)器的大部分痛點(diǎn)不在 Django 代碼本身而在文件流的處理和部署層的配合。希望這份流程梳理能幫你少踩一些坑。