Skip to content

Latest commit

 

History

History
123 lines (82 loc) · 11.2 KB

File metadata and controls

123 lines (82 loc) · 11.2 KB

后端坑:时区 / 迁移 / 连接 / 序列化

这一页是后端踩坑合集:每一条都是我们真实付出过排查成本的坑,按「症状 → 根因 → 铁律」拆开讲。写后端的工程师和给 AI 助手喂上下文的 IT 负责人都该通读一遍——这些坑的共同点是:代码看起来没错,错在环境、约定和习惯。

读完你会知道:

  • 为什么定时任务会「半夜错点跑」,以及时区策略为什么必须写成文档而不是口头约定
  • 数据库迁移编号为什么会分叉,手写迁移的唯一安全姿势
  • 三类「偶发、难复现」的连接层问题(Session 掉登录 / 驱动线程安全 / 大 header 被拒)各自的真面目
  • 序列化层的两个隐形杀手:长浮点数和响应缓存
  • 几条从第一天就该定死的约定:响应码、文件命名、ORM 预取

怎么读这一页

这些坑有个共同规律:单看任何一段代码都是对的,坑在两个系统的假设不一致——Django 假设一套时区、调度器假设另一套;开发库生成一版迁移、生产库长着另一版;后端吐一个数、前端的解析库理解成另一个东西。所以每条的「铁律」都不是修 bug 的技巧,而是消灭「不一致」的约定。约定要写进项目文档(我们写在 CLAUDE.md 和部署文档里),让人和 AI 助手都能查到。


时区与定时任务

定时任务半夜错点跑,时间差 8 小时

  • 症状:配置写的凌晨 2 点跑的任务,实际上午 10 点才跑;或者反过来,白天的任务半夜把人吵醒。查代码怎么看都是对的。
  • 根因:Django 项目设了 USE_TZ = False、时区用本地时间(我们是 Asia/Shanghai),但 Celery Beat 这类调度器默认按 UTC 理解 crontab 表达式。两边假设打架,差值正好是时区偏移(东八区就是 8 小时)。更阴险的是:如果有人「聪明地」在配置里手工减了 8 小时补偿,后来另一个人修了调度器时区设置,所有任务又集体偏移一次。
  • 铁律:项目第一天就选定一套时区策略(USE_TZ 用不用、调度器时区设成什么),写进项目文档;此后 Beat 的小时一律按本地时间写,不做任何心算换算。见到配置里出现 hour=18 # 实际是凌晨2点 这种注释,就是策略没统一的信号,立刻停下来治理。

定时任务还有「双调度系统并存」的历史坑,展开见 定时任务:双系统并存的教训

数据库迁移

迁移编号分叉:makemigrations 冲突 / 生产 migrate 报「已存在」

  • 症状:本地 makemigrations 生成 0042_xxx.py,推上去发现生产库的迁移历史里已经有一个别的 0042;或者生产 migrate 直接报错「表/字段已存在」。
  • 根因:多个环境(多台开发机、多个分支、开发与生产)各自跑过 makemigrations,同一个编号被生成了两次,迁移历史从此分叉。Django 的迁移是一条链,链一分叉,后面每一次合并都是雷。
  • 铁律:手写或补写迁移,必须先看生产库的最新编号,按它的下一号创建——我们的做法干脆是登上生产环境查完编号再写文件。迁移文件视为只追加(append-only):已经进了任何共享环境的迁移,不改、不删、不重排;发现分叉,用新迁移向前修,绝不回头改历史。

连接与会话

重启 Redis,全员掉登录

  • 症状:运维重启了一次 Redis,几分钟内所有用户都被踢回登录页,客服群炸了。
  • 根因:Session 存在 Redis 的缓存库里(SESSION_ENGINE 走 cache),而缓存库没开持久化——重启即清空,所有会话灰飞烟灭。这不是 bug,是当初选型时没人把「重启」这个场景想清楚。
  • 铁律:二选一,但必须明确地选:要么给存 Session 的 Redis 开持久化(RDB/AOF),要么接受「重启踢全员」并把它写进运维文档、每次重启前提前公告。最糟的状态是没人知道会发生什么,重启完才发现。

多线程下偶发连接错乱

  • 症状:并发一上来,偶发诡异的数据库报错——查询结果串了、连接状态异常,单线程压测又完全复现不了。
  • 根因:数据库驱动的线程安全配置没开。纯 Python 驱动(比如 pymysql)配上多线程的应用服务器(比如 uWSGI 开了多线程),如果线程支持参数没启用,多个线程就可能踩同一个连接。
  • 铁律:确认驱动 + 应用服务器的线程配置是一致的(uWSGI 记得 enable-threads=true 这类开关),然后上线前用真实并发压测一轮——这类问题只在并发下现形,功能测试永远测不出来。

IM 内嵌页登录必失败:大 header 被拒

  • 症状:同一套登录接口,浏览器里好好的,放进飞书/企业微信这类 IM 的内嵌 webview 里就必然失败,而且报错常常发生在业务代码之前,日志里几乎没线索。
  • 根因:IM 的 webview(尤其安卓端)会在请求里塞很长的 header(UA、鉴权透传、cookie 一大串),超过了应用服务器的请求 buffer 默认值,请求直接在入口层被拒,根本没走到 Django。
  • 铁律:把入口层 buffer 调大(uWSGI 的 buffer-size 从默认 4KB 提到 64KB 这个量级),并且把这个参数连同原因写进部署文档——它属于「不写下来,下次重搭环境必再踩」的知识。

序列化与响应

前端偶发页面崩 / 金额显示成字符串

  • 症状:同一个接口,大部分数据正常,个别记录让前端页面直接崩掉,或者金额字段莫名显示成一串字符串。后端日志干干净净。
  • 根因:前端为了处理超长整数用了 json-bigint 一类的解析库,这类库遇到位数很长的浮点数(比如除法算出来的 13.333333333333334)会把它转成字符串保精度。后端一个没 round 的除法,前端一个当数字用的字符串,toFixed 一调就崩。
  • 铁律:后端出参里的浮点数必须 round(两位小数或按口径定),不许把裸除法结果直接吐给前端;钱这种字段,更进一步用「分」为单位的整数或定点数表达。这条要写进接口约定,让 AI 助手写新接口时也自动遵守。

列表加了字段,前端死活看不到

  • 症状:后端明明加了字段、本地自测有值,上线后前端就是拿不到;过了几十分钟又「自己好了」。
  • 根因:列表接口有后端缓存(为了压 TTFB 加的),发布只更新了代码,没清对应的缓存键——线上吐的还是旧结构的缓存数据,直到缓存自然过期。
  • 铁律:给带缓存的接口加字段,必须同步清对应缓存键,把「清哪些键」写进该接口的文档;同时坚持响应结构平铺(不嵌套包一层套一层),平铺的结构让缓存键可枚举、可 grep,清缓存才不会漏。

code 混存:前端判断成功要写两种

  • 症状:前端判断请求是否成功,要写 code === '0' || code === 0,新同学每次都问为什么。
  • 根因:项目早期没定死响应码类型,一部分接口返回字符串 '0',一部分返回整数,失败码又是负整数——历史一层层叠上来,谁也不敢动。
  • 铁律:响应码的类型和取值第一天定死一种(我们后来统一为:成功是字符串 '0',失败用整数负数),写进编码规范;存量不一致的老接口列清单批量清,不要指望「以后慢慢改」——以后只会更多。

命名与性能

模块同名遮蔽:新视图 import 后行为诡异

  • 症状:新写的视图模块,import 进来后调用的却「不是它」——函数找不到、行为对不上,单独跑文件又正常。
  • 根因:仓库里已经存在一个同名模块(可能在另一个包路径下),Python 的模块解析让其中一个遮蔽了另一个。我们踩的实例:日报视图起名 daily_report 就撞了已有名字,被迫改叫 work_report_view 才解开。
  • 铁律:视图文件名全仓唯一;新建任何模块前,先全库搜一遍这个名字。这条成本几乎为零,但省下的排查时间以小时计。

慢接口:列表页 3 秒

  • 症状:一个平平无奇的列表页要 3 秒才出来,数据量并不大。
  • 根因:N+1 查询——列表循环里逐行取关联对象,一页 100 行就是几百次数据库往返。ORM 把这件事藏得太好,写的时候完全无感。
  • 铁律:ORM 关联预取(Django 的 select_related / prefetch_related)是写代码的习惯,不是事后优化:凡是列表接口里要用到关联对象,查询的那一行就带上预取。给 AI 助手的编码规范里也要写明这条,否则它生成的列表接口同样会 N+1。

生产导 Excel 就挂

  • 症状:导出功能本地好好的,生产环境一点导出就 500。
  • 根因:两个都遇到过——生产容器里根本没装重型表格库(openpyxl 之类,镜像瘦身时被裁了),或者数据量一大、整个工作簿在内存里构建把进程撑爆。
  • 铁律:管理后台的导出,CSV + UTF-8 BOM 走天下:不依赖任何重型库、流式输出内存友好,带 BOM 后 Excel 打开中文不乱码。真需要多 sheet、带格式的报表再单独立项,别让「顺手导个数据」的需求背上重依赖。

踩坑与红线(速查卡)

上面每条的三行式浓缩,贴给你的 AI 助手当红线清单正合适:

  • 时区:定时任务错点 8 小时 → USE_TZ=False 与调度器 UTC 假设打架 → 时区策略写进文档,Beat 按本地时间写不换算。
  • 迁移:makemigrations 冲突 / 生产报已存在 → 多环境各自生成迁移编号分叉 → 手写迁移按生产库最新编号建,迁移文件只追加。
  • Session:重启 Redis 全员掉登录 → Session 在缓存库没持久化 → 开持久化,或接受并公告。
  • 大数浮点:前端偶发崩 / 金额变字符串 → json-bigint 把长浮点转字符串 → 出参浮点必 round,钱用分或定点。
  • 响应缓存:加字段前端看不到 → 接口缓存没清 → 加字段必清缓存键;响应平铺让键可枚举。
  • 同名遮蔽:import 后行为诡异 → 同名模块相互遮蔽 → 视图文件名全仓唯一,新名先全库搜。
  • 驱动并发:多线程偶发连接错乱 → 驱动线程安全配置没开 → 配置对齐,上线前并发压测一轮。
  • 大 header:IM 内嵌页登录必失败 → header 超入口层 buffer 默认值 → buffer 调大,参数写进部署文档。
  • 导出:生产导 Excel 就 500 → 容器缺重型库 / 内存爆 → CSV + UTF-8 BOM 走天下。
  • N+1:列表页 3 秒 → 循环里逐行查关联 → select_related/prefetch 是习惯不是优化。
  • code 混存:成功判断写两种 → 早期字符串/整数混用 → 第一天定死一种,老的批量清。

延伸阅读


← 返回本层目录 · 返回总目录