本地测试全绿一上线就炸:四类差异怎么在上线前逼出来

2026-07-28

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

本地全绿、线上一上就炸,多数时候不是「依赖版本或镜像没配好」,而是你的本地根本没有制造出线上那几种输入。 大部分人第一反应是去比对依赖版本、比对镜像、重装一遍,折腾半天发现版本一模一样,于是开始怀疑玄学。真正的成因通常很朴素:你造的测试数据太干净,你的并发数永远是 1,你的机器时区恰好和服务器不一样,还有一堆配置项在本地被默认值兜住了。这四类差异有各自的指纹,认出指纹比通读日志快得多。

这篇讲的是「线上和本地」之间的差异;如果你的问题是同一份代码在两台机器、两个同事的电脑上表现不同,那是另一个题目,看 运行环境不一致导致的排查方法。至于「AI 写的代码到底能不能上生产」这种更前置的决策问题,在 AI 代码能上生产吗 里单独讨论过。本篇假定代码已经要上,只解决上线前怎么把差异提前引爆。三件事明确不在范围内:业务逻辑本身算错了(那要靠对着需求做用例,不是靠换环境跑)、容量和性能规划(下面的并发验证只验正确性,20 个并发说明不了你的系统能扛多少)、以及代码里那些一眼可见的写错(评审和静态检查该拦下来的)。这三类问题不会因为你把线上差异复制到本地就消失。

一、先用三个问题把现象归类

别一上来就看代码。先回答三个问题,多数情况能把范围压到一类:

问题一:是全量失败还是部分失败? 全量失败(每个请求都挂、或者服务起不来)几乎一定是配置或外部依赖,因为逻辑错误很难做到对所有输入都错。部分失败——比如一万条里坏了七条——大概率是数据形态:那七条身上有你本地没造出来的东西。

问题二:是稳定复现还是间歇发作? 拿同一个请求参数重放,每次都错,说明输入决定结果,方向是数据或配置。重放两三次有一次错、重试一下又好了,方向是并发或者外部依赖抖动。间歇性是并发问题最强的指纹,因为它依赖两个操作交错的时机。

问题三:错误发生在启动期还是运行期? 进程启动、第一次连数据库、第一次调外部接口就死,是配置。跑了一段时间、或者跑到某个特定时间点才死,往前想时间和累积状态。

时间类问题还有一个非常好认的特征:误差是一个固定的偏移量,而不是随机漂移。日志显示的时间和你预期差 8 小时、报表当天数据永远少一截、月初统计漏掉一天,基本就是时区,不用再往别处找。这个偏移多数是整小时,但别把「不是整小时就不是时区问题」当规则——印度、尼泊尔这类时区的偏移带 30 或 45 分钟,实行夏令时的地区还会在一年里换两次偏移。反过来,如果误差是几百毫秒到几秒且每次不同,那是机器之间的时钟不同步,不是时区。

二、判别表:现象对成因

现象大概率成因怎么验证处置动作
少量记录报解析失败、空指针、字段截断数据形态:null、超长值、四字节 emoji、老枚举值、软删除残留把失败记录的主键捞出来,只用这几条在本地重放;打印字段的字节长度而非字符长度用线上数据的形态生成边界样本补进用例;对关键字段加显式校验并让它早失败
错误率随流量上升,重试即成功并发:竞态、连接池耗尽、缓存同时回源本地起 10-20 个并发打同一个接口,看是否复现;查日志里同一资源是否被写了两次上唯一约束或乐观锁;写操作加幂等键;把「先查后写」改成一次原子操作
唯一键冲突、死锁、余额或库存出现负数并发:读改写之间被插入看两条日志的时间戳是否几乎重合、事务 ID 是否不同数据库层兜住(约束、条件更新),不要只靠应用层判断
时间显示或统计固定差整小时、跨天边界出错时区:容器与本地时区不同、存了不带时区的时间datedate -u 对比;查表字段是否带时区;把服务时区强制成另一个值再跑一遍用例全链路存 UTC 或带时区类型,只在展示层转换;日期边界按业务时区显式计算
启动即失败、或首次外部调用 401/403配置:环境变量缺失、密钥不同、权限范围不同对比两边环境变量的键名集合(不要打印值)启动时校验必需配置并直接退出;把「有默认值」当成待确认项
ETIMEDOUT / ECONNRESET / 连接被拒外部依赖:出口策略、DNS、内网代理在目标机器上直接连一次目标地址,绕过应用走代理的显式配置代理;给外部调用设超时和重试上限
TLS 证书链校验失败配置:内网自签证书未安装,或缺中间证书openssl s_client -connect 目标域名:443 -showcerts </dev/null 看链是否完整(不加末尾重定向它会停在交互态等你输入)把根证书装进信任库;不要用关闭校验的开关糊过去
报错提示额度已用尽或频率过高(429)外部服务侧限制:本地调用量小没触发看返回状态码和响应头里的重试提示降并发、加退避重试、做降级分支;各家规则不同且会调整,以官方最新说明为准

内网代理和自签证书这一类,细节比表格里能写的多,单独整理在 内网代理与证书问题排查;超时和重试怎么设才不放大故障,看 接口超时与中断处理

三、上线前的四组动作:把差异提前引爆

排查方法只解决「炸了之后」。下面四组动作是上线前做的,成本都不高,做完能挡掉大部分线上首发事故。

数据:用线上的形态,不用线上的内容。 你不需要(也不该)把生产数据拉到本地。你需要的是形态统计:每个字段的最大长度、空值比例、出现过哪些枚举值、有没有非 BMP 字符。拿这份统计去造边界样本,比造一百条整齐的假数据有用得多。一条最省事的做法是给测试固件里塞几个「刺头」值:空字符串、只有空格、超过字段上限的长文本、带 emoji 的昵称、带半角引号和反斜杠的地址、以及一个 null。

EDGE_STRINGS = ["", " ", "a" * 1024, "小明🐳", 'he said "hi"\\n', "ABC全角"]

这几个值不会让你的代码变好,但会让本地和线上的输入分布接近一点。

并发:把并发数从 1 改成 20。 本地手点永远是串行的,竞态没有机会出现。最低成本的做法就是同时打一批请求,看有没有重复写入或者唯一键冲突:

for i in $(seq 1 20); do
  curl -sS -o /dev/null -w '%{http_code}\n' \
    -X POST https://你的服务/接口 \
    -H 'Content-Type: application/json' \
    -d '{"id":"same-key"}' &
done
wait

-H 那一行别省:-d 默认按表单类型发送,服务端解析 JSON 会直接报 400,你会误以为接口有问题。

关注的不是耗时,是成功次数是否符合业务预期。同一个幂等键打 20 次,如果落了 20 条记录,那就是缺幂等;如果报了一堆 500,那是缺约束或缺锁。这一步顺带能发现连接池配置太小——错误会集中在某个数量之后。

时区:让测试在两个时区各跑一遍。 容器里常见是 UTC,你的笔记本大概是东八区,两者差 8 小时正好卡在很多日期边界上。把时区当成一个测试维度:

TZ=UTC npm test
TZ=Asia/Shanghai npm test

两次都绿才算过。这种「变量名=值 命令」的前缀写法只在 sh/bash 这类 shell 里成立,Windows 的 cmd 和 PowerShell 不认,会当成语法错误;跨平台的仓库把它交给 cross-env 之类的包装,或者在 CI 的任务环境变量里设,别指望所有人的终端都一样。另外要留意:设了 TZ 之后请确认它真的生效了(在用例里断言一次当前偏移量,比肉眼看输出可靠),有些精简镜像和裁剪过的运行时里没装时区数据库,TZ 设了也会被静默忽略,测试于是白跑一遍。

写代码时守住一条:存储和传输统一用带时区的时间,转换只发生在最外层展示。业务上的「今天」是个业务概念,必须显式指定按哪个时区算,不要依赖进程默认时区。

配置:比键名,不比值,并且启动就校验。 密钥不能打印,但键名集合可以放心比对:

env | cut -d= -f1 | sort > /tmp/keys.txt

两边各生成一份再 diff,缺哪个一眼就看出来。更进一步,在启动阶段把必需项列出来逐个检查,缺了就退出,别让服务带着半截配置跑起来——带着跑起来的后果是几小时后在某个冷门分支里炸,那时候排查成本翻好几倍。配置类改动还有个便宜的复核动作,上线前看一眼这次改了哪些配置文件:

git diff --stat origin/main...HEAD -- config/

如果这次的代码是 AI 工具大段生成或改写的,配置和边界处理是它最容易含糊的地方,人工复核的重点也放这里,工具怎么选见 AI 代码评审工具

四、什么情况下别再折腾

排查是有成本的,线上故障期间成本尤其高。给自己定三条线,触线就停手:

第一条线:正在写坏数据,立刻停写。 读接口报错只是难看,写路径出错会持续污染数据,而脏数据的修复难度和时间成正比。判断依据很简单——受影响的接口有没有落库或者调用外部的付款、发送类动作。有就先关掉入口(下线开关、限流到零、摘掉这个节点),再慢慢查。这时候「保住数据」优先于「保住可用性」。

第二条线:定位不到类别就回滚。 给自己一个时间盒,比如 20 到 30 分钟。如果连第一节那三个问题都答不出来(既不知道是全量还是部分,也说不清稳定还是间歇),说明你手上的信息不足,继续猜只是消耗时间。回滚到上一个已知良好版本,然后在没有压力的环境里复现。回滚的前提是你上线前就确认过回滚路径可用——包括数据库迁移是否可逆。不可逆的迁移要单独一次上线,和代码解耦,这样代码永远可回滚。

第三条线:同一类问题修第三遍,就该换条路。 如果并发问题你已经打了两个补丁还在复发,问题不在这两个补丁,在于设计上没有幂等或者没有把一致性交给数据库。继续在应用层加判断只会让代码更难懂。这时候正确的动作是把功能用开关关掉,接受短期功能缺失,然后重做那一小块。

还有一种情况值得单独说:让 AI 工具改这个 bug,改了三四轮,每轮都说修好了、跑起来还是错,而且错误在几个形态之间来回跳。这通常意味着它在猜,而不是在读实际的失败信息。停下来,自己动手把最小复现拿到,把真实报错和相关数据贴给它,或者干脆自己改。轮次越多,上下文里堆的错误假设越多,越难回到正确路径。

五、避坑清单

用生产数据的副本当测试数据。 为什么会踩:它最真实,看起来是最优解。为什么是坑:一是合规和泄露风险,二是它让你的用例变成「历史数据快照」,明天线上出现的新形态它依然覆盖不到。怎么避:只取形态统计和脱敏抽样,把统计结果转成显式的边界用例。

认为「本地跑了一遍没问题」等于测过并发。 为什么会踩:单机手工验证天然串行,人不会在 50 毫秒内点两次。怎么避:把并发验证写进脚本,作为提交前必跑的一步,哪怕只有 20 个并发也比 1 强。

在数据库里存不带时区的时间。 为什么会踩:本地开发时两边时区一致,看起来完全正常,问题只有在部署到另一个时区的机器上才出现。怎么避:字段类型选带时区的,或者统一存 UTC 时间戳并在代码里约定死;跨天统计显式传业务时区参数。

给配置项设「合理的默认值」。 为什么会踩:默认值让本地启动更顺,看着很贴心。为什么是坑:它把「忘配」这个错误隐藏到了运行期,等到某个分支才暴露。怎么避:区分「真的有通用默认值」和「每个环境都必须指定」,后者一律不给默认值,启动时缺失就退出。

用关闭证书校验的开关绕过 TLS 报错。 为什么会踩:加一个参数问题就消失了,进度压力下很难抵抗。为什么是坑:它把一个配置缺失变成了长期的安全缺口,而且会被复制到别的服务里。怎么避:把内网根证书装进镜像的信任库,这是一次性成本。

在故障期间同时改三个地方。 为什么会踩:几个怀疑点都想试,一起改省时间。为什么是坑:好了你不知道是哪个起作用,没好你多了三个新变量。怎么避:一次只改一个,改完记录现象;用版本控制而不是靠记忆回退。

只看应用日志,不看依赖侧的返回。 为什么会踩:应用日志离得最近、最好拿。为什么是坑:应用层往往把外部错误包装成一句笼统的失败信息,真正的状态码和响应头被吃掉了。怎么避:外部调用的原始状态码、耗时、重试次数都记下来;排查时用 curl 直接打一次目标地址做对照。

收束

这类问题让人烦躁的地方在于,它看起来像是环境的错,其实是验证方式的错——你的本地验证里缺了四个维度的输入。补上这四个维度,多数线上首发事故会在提交前就现形。

上线前过一遍这份自检:

  • 用例里有没有 null、空串、超长值、emoji、全角字符这几类刺头输入?
  • 关键写接口有没有用同一个幂等键并发打过一次?
  • 测试有没有在两个不同时区各跑一遍并且都绿?
  • 两边环境变量的键名集合 diff 过没有?必需项缺失时服务会不会直接退出?
  • 外部调用有没有设超时和重试上限?失败时有没有降级分支?
  • 这次上线的数据库变更可逆吗?回滚命令你手上现在就有吗?
  • 出问题时的止损开关是什么?谁能按?

最后一条最容易被跳过,也最值钱:想清楚「怎么快速把它关掉」,比想清楚「怎么保证它不出问题」现实得多。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。