🔍搜索⌘K

两个不声不响的坑:容器一直重启,和一次 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 的共性就一个词:不声不响。都不抛显眼错误,数据看着也正常,靠”感觉”永远定位不到。通用排查顺序:

  1. 先确认”到底跑的是哪条命令 / 哪份数据”——容器重启就看 docker inspect .Config.Cmd,数据对不上就看 HEX(...)。
  2. 用实打实的证据(inspect / hex / 计数)把”我以为”换成”我看到”。
  3. 临时方案兜得住、从根上归一化,别留雷。