两个不声不响的坑:容器一直重启,和一次 KeyError 500
之前做一个表格处理服务(Python 写的,配 MySQL,用 Docker Compose 跑),接连撞了两个 bug。共同点就一个:都不声不响——不抛一眼能看懂的错,现象看着像别的问题,不靠实打实的证据排查定位不到。
一个是 backend 容器无限重启,日志只有两行迁移输出;另一个是接口偶发 500,报错 KeyError: '(总)镉',看着像数据问题,其实是字符串字节不一致。下面按”问题 — 现象 — 怎么修 — 结论”说。
问题一:容器无限重启
现象
部署完,backend 容器状态一直是 Restarting。日志开头是迁移相关的几行,之后每次重启只重复其中两行,再没出现过”开始迁移”的日志。第一反应当然是”迁移崩了,库坏了”——但库里数据都在,这个猜测被推翻。
怎么解决
我手动起都能正常跑:前台起、热重载起、多进程起,全没问题,迁移也一次成功。差异只在 up 模式多了一个 && 串命令的前缀。
去看了容器的实际命令,证据一目了然:
docker inspect backend --format '{{json .Config.Cmd}}'
# 输出:["uv","run","alembic","upgrade","head"]
原本我写的是 uv run alembic upgrade head && uv run uvicorn ...,但 Compose 的 YAML 碰到 && 就把整条命令截断成了列表,uvicorn 直接被丢掉了。容器只跑 alembic → 退出码 0 → 因为设了”除非手动停才不重启”所以立刻重启 → 无限循环。
修法是把多条命令用 sh -c 包一层:
command:
[
"sh",
"-c",
"uv run alembic upgrade head && uv run uvicorn app.main:app --host 0.0.0.0 --port 8000",
]
改完重建容器,不再重启,迁移和 web 服务都正常。
教训:Compose 里要串多条命令,永远用
["sh","-c","cmd1 && cmd2"],别直接写command: cmd1 && cmd2。直接写成一整串字符串时,&&不会被当成 shell 的&&,YAML 会照&&把整条命令截断。
问题二:KeyError 500
现象
一个汇总接口偶发 500,traceback 最后一行是 KeyError: '(总)镉'。健康检查还是 200。代码逻辑是先按编号分组建字典,再用 WHERE 编号 IN (...) 查回记录,遍历时用字典取。两边用的是同一列,按理不该 KeyError。
怎么解决
第一直觉是”尾随空格 / 编码归一化差异”,但本地复刻没触发。直到直接看字节才弄清真正的原因——全角/半角括号混用:
SELECT serial_number, HEX(serial_number) AS h, COUNT(*)
FROM records
WHERE serial_number LIKE '%(总)镉%'
GROUP BY serial_number, HEX(serial_number);
| 字节 | 含义 | 行数 |
|---|---|---|
28 E680BB 29 E99589 | 半角括号 (总)镉 | 2 |
EFBC88 E680BB EFBC89 E99589 | 全角括号 (总)镉 | 1 |
数据库的排序规则在比较时把全角半角括号当成相等,于是 GROUP BY 把两版并成一组、只留一个当字典的键;IN(...) 又用同样规则把 3 行全部查回来。结果半角那版(2 行)在字典里取不到,就 KeyError 了。
修分三层:
- 先兜住(临时方案):字典取值改成
.get,取不到就用当前行兜底,彻底防 500。 - 从根上解决:入库前做归一化,把全角字符转半角。注意要用
unicodedata.normalize("NFKC", ...)而不是NFC——只有 NFKC 能把全角括号变成半角。 - 存量:写个迁移脚本,把库里已有的全角数据一次性归一成半角,可重复跑,跑多少次结果都一样。
import unicodedata
def normalize_text(value: str) -> str:
return unicodedata.normalize("NFKC", str(value)).strip()
教训:字符串当字典的键别裸取,先
.get兜底;Excel 导进来的”看着一样其实不一样”,必须归一化;排序规则相等 ≠ 字节相等,这种坑只有用 hex 查询才能看明白。
结论
这两个 bug 的共性就一个词:不声不响。都不抛显眼错误,数据看着也正常,靠”感觉”永远定位不到。通用排查顺序:
- 先确认”到底跑的是哪条命令 / 哪份数据”——容器重启就看
docker inspect .Config.Cmd,数据对不上就看HEX(...)。 - 用实打实的证据(inspect / hex / 计数)把”我以为”换成”我看到”。
- 临时方案兜得住、从根上归一化,别留雷。