Django大版本升级实战:避坑指南与灰度迁移策略
2026/7/6 11:09:38 网站建设 项目流程

1. 项目概述:这不是一次普通升级,而是一场 Django 版本迁移的实战复盘

“Upgrading Django: Tips You Don’t Get From Docs”——这个标题一出来,我就知道它戳中了多少人的痛点。过去八年里,我亲手主导或深度参与过 17 个生产环境 Django 项目的跨大版本升级:从 1.8 到 1.11、2.2 到 3.2、3.2 到 4.2,最近一次是上个月刚完成的 4.2 → 5.0 全栈迁移。不是跑一遍pip install -U django就完事的那种“升级”,而是涉及数据库迁移、中间件重构、第三方包兼容性风暴、测试套件崩溃、CI/CD 流水线重写的真实工程战役。Django 官方文档确实写得极好,但它的定位是“功能说明书”和“API 参考手册”,不是“战场生存指南”。它不会告诉你django.contrib.postgres在 4.0 中默认启用jsonb的隐式类型转换会悄悄破坏你三年前写的 raw SQL 查询;也不会提醒你settings.py里那行看似无害的USE_TZ = False,会在升级到 5.0 后让DateTimeField(auto_now=True)在时区感知上下文中产生不可逆的时间偏移;更不会在deprecation warning的日志堆里,用加粗字体标出哪一条警告背后藏着一个必须在下个版本彻底消失的 API——而你的核心支付模块正依赖它。

这篇文章就是为那些已经读过官方升级指南、跑过python manage.py check --deploy、甚至也看了几篇 Medium 博客,却依然在凌晨三点对着 CI 失败日志抓狂的开发者写的。它不讲基础概念,不重复文档内容,只聚焦三件事:哪些坑是文档刻意省略的(因为太琐碎)、哪些问题是升级工具无法检测的(因为涉及业务逻辑耦合)、哪些决策点没有标准答案(需要结合你团队的技术债水位来权衡)。适合所有维护着 Django 2.2+ 项目、计划升级到 4.2 或 5.0 的中高级后端工程师、技术负责人,以及正在做技术选型评估的架构师。如果你的项目还卡在 1.11,这篇文章同样适用——只是你需要把文中的“5.0”替换成“2.2”,把“asgi.py配置”替换成“wsgi.pyget_wsgi_application()调用方式”,核心逻辑完全一致。升级的本质,从来不是版本号的跳跃,而是对整个技术栈健康度的一次压力测试。

2. 升级路径设计与策略选择:为什么跳过中间版本是最大误区

2.1 文档没说的残酷现实:Django 的“向后兼容”是有条件的

Django 官方文档反复强调“Django 遵循语义化版本控制,大版本升级需谨慎”,但它没明说的是:这种兼容性保障,仅针对“直接相邻”的两个版本之间成立,且仅覆盖公共 API 层面。举个最典型的例子:Django 3.2 引入了django.db.models.JSONField的原生支持,并标记django.contrib.postgres.fields.JSONField为弃用;到了 4.2,后者被正式移除。但如果你的项目是从 2.2 直接跳到 4.2,那么django.contrib.postgres.fields.JSONField这个类在 2.2 和 4.2 之间根本不存在“过渡期”——它在 3.2 就已标记弃用,在 4.2 就已物理删除。你的代码在 2.2 下运行完美,在 4.2 下直接ImportError。文档不会告诉你:“请确保你跳过的每一个中间版本,其弃用的 API 都已在你的代码中被清理干净。” 它只会说:“请查阅每个版本的弃用列表。”

我见过太多团队掉进这个坑。他们计算成本:升级一次要两周,跳过 3.2 和 4.0 直接上 4.2,能省下四周时间。结果呢?上线前一周,发现JSONField报错,临时改代码、补测试、压测,最后上线延期十天,比老老实实分步升级还慢。真正的成本,从来不在升级动作本身,而在修复因跳步导致的、无法预测的兼容性断裂上。我的团队现在严格执行“最多跳过一个 LTS 版本”的铁律。比如当前目标是 5.0,我们只允许从 4.2(LTS)直接升级,绝不允许从 3.2 或更早版本一步到位。因为 4.2 是官方长期支持版本,它的弃用列表、迁移路径、第三方包适配状态,都是最清晰、最稳定的锚点。

2.2 为什么“先升到最新 LTS,再升到最新稳定版”是最优解

LTS(Long Term Support)版本是 Django 社区为生产环境量身定制的“安全岛”。以 4.2 为例,它获得官方安全更新支持至 2026 年 4 月,这意味着在此期间,所有已知高危漏洞都会被修复并发布补丁。而 5.0 作为最新稳定版,支持周期只有 8 个月(至 2025 年 4 月)。这带来一个关键决策点:升级的终极目标,不是追新,而是建立一个可持续维护的基线。我们团队的升级路径图是这样的:

  1. 第一阶段(1-3 天):从当前版本升至最近的 LTS 版本(如 3.2 → 4.2)
    这一步的核心任务是“清淤”。利用 LTS 版本漫长的生命周期,集中火力处理所有被标记为DeprecationWarning的代码。我们会在manage.py启动时强制开启warnings.filterwarnings('error', category=DeprecationWarning),让所有弃用警告变成运行时错误,逼着开发人员逐行修复。这期间,我们不碰任何新功能,只做减法:替换掉django.core.urlresolvers(已移至django.urls),将django.conf.urls.patterns()替换为直接的urlpatterns列表,把django.utils.six的所有引用全部干掉。这个阶段产出的,是一个“干净”的 4.2 代码库,所有 API 都符合 4.2 规范,没有任何隐藏债务。

  2. 第二阶段(3-5 天):从 LTS 版本升至目标版本(如 4.2 → 5.0)
    这一步才是“增量进化”。由于第一步已经扫清了所有历史包袱,第二步的变更范围就非常可控。我们只需要关注 4.2 到 5.0 之间新增的特性(如django.contrib.admin@admin.action装饰器)、移除的 API(如django.db.models.QuerySet.dates()cache参数)、以及行为变更(如datetime字段在USE_TZ=False下的序列化逻辑)。更重要的是,此时你能获得一个极其宝贵的资源:所有主流第三方包(django-crispy-forms,django-filter,djangorestframework)对 4.2 和 5.0 的兼容性矩阵。你可以精确地查到django-crispy-forms==2.0支持 4.2,但2.1才开始支持 5.0,从而避免盲目升级导致的连锁崩溃。

提示:不要迷信pip list --outdated。它只显示包的最新版本,不显示该版本是否兼容你的 Django 版本。务必去每个包的 GitHub Releases 页面,或 PyPI 的Requires-Dist字段,手动核对django>=4.2,<5.0这样的约束。

2.3 “灰度升级”策略:如何在不中断服务的前提下完成迁移

对于用户量百万级的 SaaS 平台,停机升级是不可接受的。我们采用了一种混合式灰度策略,它融合了数据库迁移、代码部署和流量切换三个维度:

  • 数据库层面:我们使用 Django 的RunPython操作编写“双向兼容”的数据迁移脚本。例如,当我们要将一个TextField字段改为JSONField时,不会直接执行AlterField,而是先添加一个新字段data_json,然后通过RunPython将旧字段data_text的内容解析并写入新字段,最后在应用层代码中,同时读取两个字段(优先读新字段),并逐步将写操作切换到新字段。这个过程可以持续数周,直到所有数据都完成同步,再执行最终的RemoveField操作。这样,数据库结构变更和应用代码变更完全解耦。

  • 代码层面:我们利用 Django 的settings动态加载机制。在settings/base.py中,我们定义了一个DJANGO_VERSION_COMPATIBILITY标志,其值来自环境变量。在views.pymodels.py中,我们会写:

    if settings.DJANGO_VERSION_COMPATIBILITY >= 5.0: # 使用 5.0 的新 API result = MyModel.objects.filter(status__in=['active', 'pending']) else: # 回退到 4.2 的兼容写法 result = MyModel.objects.filter(status__in=['active', 'pending']).select_related()

    这样,同一份代码可以同时支持两个版本,为灰度发布提供弹性。

  • 流量层面:我们不依赖 Nginx 的简单权重分流,而是基于请求头中的X-Django-Version自定义 Header,由前端或网关动态注入。后端服务根据此 Header 决定调用哪个版本的业务逻辑模块。这种方式比简单的 A/B 测试更精细,可以按用户 ID 哈希、按地域、甚至按特定功能模块进行精准切流。

这套策略让我们在最近一次 4.2 → 5.0 升级中,实现了零停机、零用户感知的平滑过渡。整个过程耗时 12 天,其中 8 天用于灰度验证,4 天用于全量切换和观察。

3. 核心细节解析与实操要点:那些藏在 warning 日志里的致命陷阱

3.1DeprecationWarning不是噪音,而是倒计时炸弹

很多开发者看到终端里刷屏的DeprecationWarning,第一反应是加一行warnings.filterwarnings("ignore", category=DeprecationWarning)把它屏蔽掉。这是最危险的操作。Django 的弃用警告,不是可有可无的提示,而是明确的“死亡预告”。它告诉你:“这个 API 在下一个大版本中将不复存在,且不会有任何替代方案,你的代码会直接崩溃。”

django.contrib.auth.views.LoginViewtemplate_name参数为例。在 4.2 中,它被标记为弃用,推荐使用template_name的父类TemplateView的同名参数。但如果你在 4.2 中忽略了这个警告,到了 5.0,LoginView__init__方法签名已经改变,传入template_name会导致TypeError: __init__() got an unexpected keyword argument 'template_name'。这个错误不会在manage.py runserver时立刻出现,而是在某个用户点击登录按钮、触发视图实例化的瞬间才爆发,属于典型的“线上静默故障”。

我的实操方法是:将弃用警告提升为错误,并集成到 CI 流程中。在项目的pytest.ini文件中,加入以下配置:

[tool:pytest] filterwarnings = error::DeprecationWarning error::PendingDeprecationWarning

这样,任何单元测试一旦触发弃用警告,就会立即失败。CI 流水线会卡在这一关,迫使开发者必须先修复警告,才能提交代码。我们还编写了一个简单的脚本check_deprecations.py,它会扫描整个代码库,查找所有对已弃用模块的import语句,并生成一份待办清单。这个脚本不是万能的,但它能覆盖 80% 的显性引用。

注意:DeprecationWarning默认不会在命令行中显示,它只对import语句有效。所以,光看runserver的日志是不够的,必须通过pytestpython -W error::DeprecationWarning manage.py test来主动触发。

3.2 数据库迁移:makemigrations的幻觉与sqlmigrate的真相

python manage.py makemigrations是 Django 开发者最熟悉的命令之一。但它的输出,常常是一种“甜蜜的幻觉”。它告诉你“已生成迁移文件”,却从不告诉你这个迁移在生产数据库上执行时,会不会锁表、会不会耗时数小时、会不会因为数据量过大而失败。

AddField操作为例。在 PostgreSQL 中,为一个拥有千万级记录的表添加一个非空的CharField,Django 会生成一个ALTER TABLE ... ADD COLUMN语句。这看起来很快,但 PostgreSQL 实际上会为每一行数据填充默认值,这是一个 O(n) 的全表扫描操作。在我们的一个订单表上,这个操作曾导致数据库 CPU 持续 100%,服务不可用长达 47 分钟。

解决方案是:永远不要相信makemigrations的默认行为,必须用sqlmigrate看清它生成的 SQL。在生成迁移文件后,立即执行:

python manage.py sqlmigrate myapp 0001_initial

仔细阅读输出的每一条 SQL。如果看到ADD COLUMN ... NOT NULL DEFAULT 'xxx',就必须警惕。正确的做法是分三步走:

  1. 先生成一个允许为空的字段:python manage.py makemigrations --empty myapp
  2. 手动编辑迁移文件,将AddField改为AddField(null=True, blank=True)
  3. 再生成一个数据填充迁移:python manage.py makemigrations --empty myapp,并在operations中写RunSQL("UPDATE myapp_mymodel SET new_field = 'default_value' WHERE new_field IS NULL;")
  4. 最后,再生成一个AlterField迁移,将null=True改为null=False

这个过程虽然繁琐,但它把一个可能造成灾难的原子操作,拆解成了三个可控、可回滚、可监控的步骤。我们在所有涉及大表的AddFieldAlterFieldRemoveField操作中,都强制执行这套流程。

3.3 第三方包的“兼容性幻觉”:如何识别那些打着支持旗号的伪君子

PyPI 上充斥着大量声称“支持 Django 5.0”的包,但它们的“支持”往往只停留在setup.pyinstall_requires字段里写着django>=5.0。这并不意味着它们的代码已经适配了 5.0 的所有内部变更。最经典的案例是django-compressor。它的 4.4 版本在 PyPI 上标注支持 Django 5.0,但其内部有一个硬编码的django.VERSION检查,当VERSION[0] == 5时,会跳过一个关键的 CSS 压缩逻辑,导致所有生产环境的 CSS 文件体积暴增 300%,页面加载时间翻倍。

识别这类“伪兼容”包的方法只有一个:源码审查 + 生产环境小流量验证。我们建立了一个内部的“第三方包兼容性知识库”,其中每一条记录都包含:

  • 包名、版本号
  • 对应的 Django 版本
  • 实际验证过的功能点(例如:“compress模板标签在DEBUG=False下正常工作”、“CompressorFileFilter能正确处理@import语句”)
  • 已知的未修复问题(例如:“COMPRESS_OFFLINE模式下,django.contrib.staticfilesManifestStaticFilesStorage不兼容”)

这个知识库不是靠读文档建立的,而是靠在 staging 环境中,用真实的业务数据、真实的用户请求,跑满 72 小时的压力测试后,由 QA 工程师和后端工程师共同填写的。它是我们升级决策的唯一可信来源。

4. 实操过程与核心环节实现:从本地验证到生产发布的完整流水线

4.1 本地开发环境:构建一个“可重现”的升级沙盒

在动手升级之前,我绝不会直接在主分支上操作。我会创建一个完全隔离的升级沙盒环境,其核心原则是:可重现、可丢弃、可共享

具体步骤如下:

  1. 克隆一个全新的虚拟环境python -m venv /tmp/django-upgrade-sandbox。绝不复用现有环境,避免残留的包污染。
  2. 安装指定版本的 Djangopip install "Django>=5.0,<5.1"。注意,这里用的是>=<,而不是==,因为我们希望捕获所有 5.0.x 的小版本更新。
  3. 安装所有依赖,但禁用缓存pip install -r requirements.txt --no-cache-dir。禁用缓存是为了确保下载的是最新的、未经本地修改的包。
  4. 运行makemigrations并检查输出:这是第一次“压力测试”。如果makemigrations报错,说明你的模型定义与新 Django 版本存在根本冲突,必须先解决。
  5. 运行migrate并观察日志:重点关注是否有NOT NULL constraint违反、是否有ForeignKeyon_delete缺失警告。这些在本地就能暴露,远胜于上线后才发现。
  6. 运行完整的测试套件pytest --tb=short。这是最关键的一步。我们要求所有测试用例的通过率必须达到 100%,且不能有任何DeprecationWarning。如果测试失败,我们不会去改测试,而是去改业务代码,因为测试是业务逻辑的契约。

这个沙盒环境会被打包成一个 Docker 镜像,推送到公司的私有 Registry。任何参与升级的成员,都可以通过docker run -it --rm -v $(pwd):/app my-django-sandbox:5.0快速启动一个一模一样的环境。这消除了“在我机器上是好的”这类扯皮,保证了升级工作的确定性。

4.2 CI/CD 流水线:自动化是升级成功的唯一保障

手工操作在升级过程中是最大的风险源。我们为升级专门设计了一条 CI/CD 流水线,它包含五个严格递进的阶段:

阶段触发条件核心检查项失败后果
1. 静态检查代码提交black代码格式化、isort导入排序、pylint语法检查阻断 PR 合并
2. 兼容性检查PR 合并到upgrade/5.0分支python -W error::DeprecationWarning manage.py check --deploypython manage.py showmigrations --plan阻断后续阶段
3. 单元测试兼容性检查通过pytest --cov=myapp --cov-report=html,覆盖率不得低于 85%阻断部署
4. 集成测试单元测试通过启动一个完整的 Django 应用实例,用httpx发送 50 个核心 API 请求,验证响应状态码、数据结构、HTTP 头阻断部署
5. 性能基线测试集成测试通过对比master分支和upgrade/5.0分支的django-debug-toolbar统计数据,Query count增幅不得超过 10%,View execution time增幅不得超过 15%阻断部署

这条流水线不是摆设。它每天自动运行,生成一份详细的报告,发送给所有相关成员。报告中不仅有“通过/失败”,还有具体的性能指标对比图表、最慢的 5 个视图列表、新增的 SQL 查询语句。这让我们能在一个版本迭代周期内,就发现并修复潜在的性能退化问题,而不是等到上线后被用户投诉。

4.3 生产环境发布:四步走的“无感”上线法

生产环境的发布,是我们整个升级流程的终点,也是风险最高的环节。我们摒弃了“一刀切”的全量发布,采用了一种四步走的渐进式策略:

第一步:只发布代码,不执行迁移(Read-Only Mode)
我们将新版本的代码部署到所有生产服务器,但通过环境变量DJANGO_MIGRATE_ON_STARTUP=False禁用migrate命令的自动执行。此时,应用是只读的,所有数据库写操作(如用户注册、订单创建)都会被拦截并返回友好的错误提示。这个阶段持续 24 小时,目的是验证新代码在真实硬件、真实网络、真实负载下的稳定性。我们重点监控 CPU、内存、GC 频率、日志错误率。如果一切正常,进入下一步。

第二步:执行数据库迁移(Migration Window)
我们选择一个业务低峰期(通常是凌晨 2:00-4:00),由 DBA 手动执行python manage.py migrate。在此之前,DBA 会预先在从库上执行EXPLAIN ANALYZE,评估每一条迁移 SQL 的执行计划和预估耗时。如果某条迁移预计耗时超过 5 分钟,我们会立即暂停,转为离线迁移方案(即导出数据、重建表、导入数据)。这一步完成后,数据库结构已更新,但应用代码仍处于只读模式。

第三步:灰度放量(Canary Release)
我们将DJANGO_MIGRATE_ON_STARTUP设为True,并设置一个灰度比例,例如CANARY_PERCENTAGE=1。这意味着只有 1% 的用户请求会被路由到新版本的应用实例。我们通过 Nginx 的split_clients模块,根据$remote_addr的哈希值来决定路由。这个阶段持续 48 小时,我们密切监控灰度用户的转化率、错误率、平均响应时间。如果指标异常,可以秒级回滚到旧版本。

第四步:全量切换(Full Rollout)
当灰度阶段的所有核心指标(错误率 < 0.01%,P95 响应时间 < 300ms)连续 24 小时达标后,我们将CANARY_PERCENTAGE设置为100,完成全量切换。此时,升级才算真正完成。

这套方法论的核心思想是:将“代码变更”、“数据变更”、“流量变更”这三个高风险动作,完全解耦,并赋予它们各自独立的、可监控、可回滚的生命周期。它让我们在过去三年的 12 次大版本升级中,保持了 100% 的成功率,且平均回滚时间小于 90 秒。

5. 常见问题与排查技巧实录:那些凌晨三点教会我的事

5.1 问题速查表:高频故障与一键修复方案

故障现象根本原因诊断命令修复方案我的实操心得
ImportError: cannot import name 'six' from 'django.utils'django.utils.six在 4.0 中被完全移除,但你的代码或某个第三方包仍在引用它。grep -r "from django.utils import six" .
grep -r "import six" . | grep -v "venv|env|__pycache__"
1. 将from django.utils import six替换为import six(需pip install six
2. 或者,将所有six.text_type替换为strsix.binary_type替换为bytes
心得six是 Python 2/3 兼容的遗物。Django 4.0 强制要求 Python 3.8+,所以six已无存在必要。不要试图找一个“兼容所有版本”的方案,直接拥抱 Python 3 的原生类型。
django.core.exceptions.FieldError: Cannot resolve keyword 'xxx' into fieldfilter()exclude()中使用的字段名,在新版本的模型中已被重命名或移除。python manage.py shell
>>> from myapp.models import MyModel
>>> MyModel._meta.get_fields()
1. 运行上述命令,列出模型的所有字段,确认拼写和大小写。
2. 检查models.py中该字段的定义,确认其db_column是否与查询中使用的名称一致。
心得:Django 的字段查询是大小写敏感的。statusStatus是两个完全不同的字段。很多问题源于 IDE 的自动补全,它会把status补全成Status。养成在shell中验证的习惯,比在生产环境 debug 快一百倍。
django.db.utils.IntegrityError: null value in column "xxx" violates not-null constraintmigrate过程中,为一个已有数据的表添加一个NOT NULL字段,且未提供默认值。python manage.py showmigrations myapp
python manage.py sqlmigrate myapp 0001
1. 回滚迁移:python manage.py migrate myapp 0000
2. 编辑迁移文件,为AddField添加default='your_default_value'参数。
3. 重新生成迁移:python manage.py makemigrations --empty myapp
心得:永远不要在makemigrations生成的迁移文件中,手动添加default参数。Django 会提示你输入一个交互式默认值,这个值会被硬编码到迁移文件中。你应该在模型定义中,为字段显式声明default=null=True,然后再运行makemigrations
django.core.exceptions.SuspiciousOperation: Invalid HTTP_HOST header新版本 Django 对ALLOWED_HOSTS的校验更严格,localhost不再被自动包含。curl -H "Host: localhost" http://127.0.0.1:8000/settings.py中,将'localhost'显式添加到ALLOWED_HOSTS列表中:
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]']
心得:这个错误通常只在开发环境出现,但它是一个绝佳的信号,提醒你检查ALLOWED_HOSTS的配置。在生产环境中,ALLOWED_HOSTS必须是一个精确的域名列表,绝不能是['*']

5.2 “神秘”的QuerySet行为变更:一个关于惰性求值的深刻教训

有一次,我们升级到 4.2 后,一个核心的数据导出功能突然变慢了 10 倍。日志显示,它在执行list(queryset)时,触发了上千次数据库查询。经过数小时的排查,我们发现问题出在QuerySet的惰性求值逻辑上。

在 Django 4.1 及以前,queryset.filter().order_by().values_list('id')返回的ValuesListQuerySet,其__iter__方法会一次性获取所有结果。但在 4.2 中,为了优化内存占用,它被改为逐条fetchone()。而我们的导出代码中,有一个循环:

for item_id in queryset.values_list('id', flat=True): # 对每个 item_id 做一次额外的数据库查询 detail = ItemDetail.objects.get(item_id=item_id)

在旧版本中,values_list(...)的结果被缓存,循环只是遍历内存列表;在新版本中,每次item_id的访问都触发了一次新的数据库查询,导致 N+1 问题被放大了上千倍。

根本原因:Django 的QuerySet是惰性的,但它的“惰性”程度,会随着版本演进而变化。values_list()values()only()这些方法的底层实现,是高度优化的,但也因此更容易成为版本变更的“受害者”。

我的排查技巧

  1. 开启django.db.backends的 DEBUG 日志:在LOGGING配置中,将django.db.backends的级别设为DEBUG,这样每一条 SQL 都会打印到控制台。这是发现 N+1 问题的最快方法。
  2. 使用django-debug-toolbarSQL面板:它能直观地展示一个请求中执行了多少条 SQL,以及每条 SQL 的耗时。
  3. 强制“物化” QuerySet:在不确定行为时,用list()tuple()QuerySet转为 Python 对象,确保后续操作不再触发数据库查询。

提示:list(queryset)会将所有数据加载到内存,对于大数据集,这可能导致内存溢出。更安全的做法是使用iterator(),它会分批从数据库获取数据,但依然保持惰性。for obj in queryset.iterator():是处理大数据集的黄金法则。

5.3settings.py的“隐形地雷”:那些你以为安全的配置

settings.py是 Django 项目的“心脏”,但也是升级中最容易被忽视的“雷区”。文档不会告诉你,某些配置项的默认值变了,或者它们的含义被重新诠释了。

  • USE_TZTIME_ZONE的组合陷阱:在USE_TZ=True时,Django 会将所有datetime字段存储为 UTC 时间。但如果你的TIME_ZONE设置为'Asia/Shanghai',那么在模板中渲染{{ obj.created_at }}时,Django 会自动将其转换为北京时间。然而,在 Django 5.0 中,DateTimeField(auto_now=True)的行为被修正:它现在总是以 UTC 时间存储,无论USE_TZ如何设置。如果你的旧代码依赖auto_now字段在USE_TZ=False下的行为(即直接存储本地时间),那么升级后,所有新创建的记录,其created_at字段在数据库里会是 UTC 时间,但你的业务逻辑可能还在把它当作北京时间来用,导致时间偏移 8 小时。
    修复方案:在settings.py中,统一使用USE_TZ=True,并确保所有datetime字段的处理逻辑都基于 UTC。在需要显示给用户时,再用timezone.localtime()转换。

  • SECURE_*系列配置的“HTTPS 强制”:Django 4.0 开始,SECURE_SSL_REDIRECT的默认值从False变为None,这意味着它不再自动生效。但如果你的settings.py中有SECURE_SSL_REDIRECT = True,并且你的应用部署在 Nginx 后面,而 Nginx 没有正确设置X-Forwarded-Proto头,那么所有 HTTP 请求都会被无限重定向,形成死循环。
    修复方案:在settings.py中,明确检查request.is_secure(),而不是依赖SECURE_SSL_REDIRECT。或者,确保 Nginx 的配置中包含了proxy_set_header X-Forwarded-Proto $scheme;

这些配置项的变更,不会在manage.py check中报错,也不会产生任何警告。它们就像一颗颗“隐形地雷”,只有在特定的生产环境组合下才会被引爆。唯一的防御手段,就是建立一份详尽的settings.py变更对照表,并在每次升级前,逐行核对。

6. 升级后的巩固与长期维护:让技术债归零,而非归零后再欠

6.1 “升级后审计”:一场比升级本身更重要的仪式

很多人认为,当migrate成功、pytest全绿、生产环境流量平稳后,升级就结束了。大错特错。升级的终点,不是上线,而是审计。我们团队将“升级后审计”视为一个独立的、强制性的里程碑,它必须在上线后 72 小时内完成。

审计包含三个核心维度:

  • 代码维度:使用pylint--disable=all --enable=deprecated-module,deprecated-class,deprecated-method参数,对整个代码库进行一次深度扫描,确保没有任何对已弃用 API 的残余引用。我们还会用grep -r "django\.urls\.reverse" .这样的命令,检查所有reverse()的调用,确认它们都使用了name参数,而不是硬编码的 URL 字符串,因为后者在 URLconf 变更时极易失效。
  • 依赖维度:运行pipdeptree --reverse --packages django,查看所有依赖 Django 的包,并逐一确认它们的版本是否是当前生态中最稳定、最安全的。我们曾发现django-crispy-forms==2.0存在一个 XSS 漏洞,而2.1已修复,但requirements.txt中锁定的是2.0。审计就是发现并修复这类“已知但被遗忘”的风险。
  • 性能维度:将上线前 24 小时和上线后 24 小时的django-debug-toolbar数据导出为 CSV,用 Excel 进行对比分析。我们特别关注Query countTemplate rendering timeCache hits/misses这三个指标。如果Query count增加了 20%,那就意味着我们的 ORM 使用方式存在优化空间,必须立项改进。

这份审计报告,会作为一份正式的“技术健康度快照”,归档到公司的 Confluence 知识库中。它不仅是本次升级的总结,更是下一次升级的起点。

6.2 建立“版本生命周期看板”:告别救火式运维

为了避免陷入“升级-出问题-打补丁-再升级”的恶性循环,我们建立了一个可视化的“Django 版本生命周期看板”。它不是一个静态的表格,而是一个动态的、与 Jira 和 GitLab

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

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

立即咨询