“在生产环境能运行” — 这是开发人员说的一句话,当bug在生产环境中无法重现,尽管在预发布环境或本地机器上错误稳定出现时。问题几乎总是由环境差异引起的:不同版本的依赖项、配置文件、数据库状态或服务器设置。根据Stack Overflow Developer Survey 2024的数据分析,43%的开发人员每月至少遇到一次代码在本地机器上运行但在生产环境中崩溃的情况。我们来探讨为什么会出现这种差异以及如何防止它。
要点
“在生产环境能运行” — 这是开发人员圈子里一个固定的说法,描述的是代码在生产服务器上运行,但拒绝在测试环境或同事的本地机器上工作的情况。表面上听起来像是“没有问题”,但实际上问题存在 — 只是不在生产环境中重现。差异的根源在于环境之间的配置、版本和数据差异。
这个说法是作为另一个著名借口“在我本地能运行”的对立面而产生的。如果开发人员说“本地能运行”,意味着bug只存在于其他人那里。而如果“在生产环境能运行” — bug只存在于预发布环境或测试环境中,生产环境是干净的。命运的讽刺:在这两种情况下,问题都是真实的,只是不在看的人那里出现。根据DevOps Research and Assessment (DORA) 2023的研究,部署自动化水平高的团队遇到这种差异的频率要低3倍。
从业务角度来看,“在生产环境能运行”的情况比看起来更危险。如果bug在预发布环境中存在但在生产环境中不存在,开发人员可能会忽略它 — 然后在下次部署时错误就会进入生产环境。暂时的轻松变成了未来的问题,将不得不在用户的压力下解决。
这个说法持续存在的心理原因 — 防御性反射。在预发布环境看到bug但在生产环境看不到的开发人员,可能会下意识地低估问题:“如果生产环境一切正常,那就不紧急”。经典的认知偏差 — 幸存者错误,生产环境可见的成功超过了未来潜在故障的威胁。
第二个原因 — 责任模糊。如果生产环境能运行而预发布环境不能,罪魁祸首是环境,而不是代码。开发人员将bug的责任从自己身上卸下,转嫁给DevOps工程师或管理员。根据Atlassian State of DevOps 2022,在没有统一部署环境(Docker、Kubernetes)的团队中,这种责任转移发生的频率高出60%。
第三个原因 — 对零停机发布的恐惧。如果开发人员在预发布环境修复了bug并发布了修正,这将需要重新进行代码审查、测试和部署。“在生产环境能运行”这句话可以将修正推迟到下次发布,从而减轻当前负担。推迟的修正 — 团队中技术债务累积的主要原因之一。
生产环境和预发布环境永远不会完全相同 — 由于规模、负载和数据的差异,这在技术上是不可能的。然而,关键参数必须一致:操作系统、编译器、解释器、数据库、Web服务器以及所有项目依赖项的版本。如果至少有一个参数不同 — 代码的行为可能会改变。
环境之间的主要差异包括:
容器化解决了大多数这些问题。为生产环境构建的Docker镜像也应在预发布环境中使用。唯一的区别在于环境变量和卷挂载。根据Docker State of Application Development 2023,为所有环境使用统一镜像的团队将差异数量减少了74%。
| 参数 | 本地环境 | 预发布环境 | 生产环境 |
|---|---|---|---|
| 操作系统 | macOS / Windows | Linux服务器 | Linux服务器 |
| 数据库 | SQLite / 本地MySQL | MySQL集群 | 带复制的MySQL集群 |
| 负载 | 1个用户 | 模拟10–100 | 1000+真实用户 |
| 数据 | 测试数据 | 脱敏数据 | 真实数据 |
| CDN / 缓存 | 无 | 部分 | 完整 |
第一个也是最常见的原因 — 不同版本的依赖项。开发人员使用--save标志在本地安装包,但忘记更新package.json或锁定文件。部署到生产环境时安装了另一个版本,其行为不同。对于npm生态系统,锁定文件完全解决了问题,对于其他包管理器 — 类似的机制(Gemfile.lock、Podfile.lock、pubspec.lock)。
第二个原因 — 缺少或多余的环境变量。开发人员在本地机器上使用.env文件,但未将相应的变量添加到CI/CD管道或服务器上。结果 — 代码因连接API或数据库错误而崩溃。根据GitLab DevSecOps Survey 2023,27%的生产事故与不正确的环境变量有关。
第三个原因 — 数据库状态。在预发布环境中,数据库可能包含生产环境中没有的记录,或者相反 — 缺少迁移。典型场景:开发人员编写与表中新字段配合使用的代码,但迁移尚未在生产环境中应用。向后兼容的迁移策略是避免这种情况的唯一方法。
第四个原因 — 区域和语言设置。日期格式、小数分隔符、文本编码 — 所有这些在开发人员的本地机器和服务器上可能不同。对于国际化项目尤其相关。解决方案 — 在应用程序配置中明确指定区域设置,而不依赖系统设置。
第一步 — 比较两个环境的日志。日志级别的差异常常隐藏着原因:生产环境可能开启了INFO,预发布环境开启了DEBUG。设置相同的日志级别,并确保两个环境以允许机器比较的格式写入。使用集中式日志收集系统 — Sentry、Datadog、ELK Stack。
第二步 — 检查依赖项版本。比较锁定文件,显示两个环境中已安装包的列表。次要版本或补丁版本的差异是差异最可能的原因。像npm ls、pip freeze、mvn dependency:tree这样的工具有助于快速发现不一致。
第三步 — 在本地重现生产环境。使用Docker Compose或类似工具搭建生产基础设施的精确副本。如果bug在本地容器中重现 — 问题在代码中,而不是环境中。如果没有重现 — 寻找配置中的差异。
第四步 — 检查功能标志和A/B测试。可能在生产环境中代码以不同模式运行,因为错误的标志被启用。根据LaunchDarkly State of Feature Management 2023,高达40%的生产环境中意外行为与错误的功能标志值有关。为所有环境制定统一的标志清单可以解决这个问题。
预防的主要工具 — 基础设施即代码(Infrastructure as Code,IaC)。所有环境都应该用代码描述:Dockerfile、docker-compose.yml、Terraform脚本或Ansible playbook。禁止在服务器上进行手动更改 — 每次配置更改都要通过仓库和代码审查。这保证所有环境具有相同的配置。
第二个重要工具 — 统一的CI/CD管道。相同的构建、测试和部署脚本应该用于所有环境。区别仅在于目标变量(URL、密钥)。如果预发布环境和生产环境的管道步骤不同 — 差异是不可避免的。
第三个工具 — 自动数据同步。定期(每天一次或按计划)用生产数据库的匿名化副本更新预发布环境。这允许在真实数据上测试代码,而不是在合成测试数据上。工具:用于PostgreSQL的pg_dump/pg_restore,用于MySQL的mysqldump,像DataGrip这样的专业服务。
第四 — 监控差异。在检测到预发布环境和生产环境之间的差异时设置通知。一个比较配置文件哈希或已安装包版本的简单脚本可以节省数小时的调试时间。预防总是比诊断便宜:防止环境差异比寻找“在生产环境能运行”bug的原因更省力。
常见问题
在第一种情况下,bug在预发布环境中可见但在生产环境中不可见。在第二种情况下 — 除了代码在本地能运行的开发人员之外,所有人都能看到bug。共同根源 — 在于环境差异,但情况在不同阶段表现出来。
展示预发布环境中的bug是一个已经准备好在下次部署时进入生产环境的bug。现在修复比在用户压力下进行热修复更便宜。举出项目历史中的例子。
根据DORA 2023的数据,约25–30%的生产事故是由环境之间的差异引起的。在没有容器化的团队中,这一比例达到50%。容器化将其降低到10–15%。
是的,这是常见原因之一。在生产环境中CDN、Varnish或Redis缓存已启用,而在预发布环境中没有。如果bug与传递缓存数据有关,它会在预发布环境中出现,而在生产环境中会被缓存隐藏。
Docker保证所有阶段环境的同一性:开发、测试、预发布、生产。如果镜像构建一次并在各处使用 — 版本和配置的差异就被排除了。统一的镜像是部署可重复性的基础。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。