“在我机器上能运行”(英文 “Works on my machine”)— 这是开发者的经典说法,他无法在自己的本地环境中重现一个bug,尽管这个bug在其他团队成员或生产环境中稳定出现。这种情况是由于开发者的机器与bug重现的环境之间的配置、依赖版本、操作系统或数据存在差异造成的。根据 Stack Overflow Survey 2023 的数据,58% 的开发者每月至少说一次这句话,31% 的人每周都说。我们来分析为什么代码在不同环境中表现不一致以及如何标准化开发环境。
要点总结
“在我机器上能运行” — 这是开发者在同事或测试人员报告bug时说的话,但在开发者的机器上这个bug无法重现。从表面上看,这看起来像是对问题的否认,但从技术上讲,这种情况是真实的:代码确实可以在一个环境中运行,而在另一个环境中崩溃。配置上的一点差异—应用程序的行为就会发生巨大变化。
这句话在IT社区成了一个梗,因为它既真实又无用。从开发者的角度来看—代码确实在他的机器上运行。从团队的角度来看—问题确实存在,需要解决,而不是找借口。这种情况的幽默之处在于开发者说的是实话,但实话并不能帮助修复bug。这个梗非常流行,Reddit、XKCD和DevOps会议上都有成千上万的相关帖子。
从流程的角度来看,“在我机器上能运行”这句话—是环境可重现性问题的指标。如果两个开发者无法在相同的代码上得到相同的结果—说明环境配置流程没有标准化。DevOps实践表明:环境应该能够通过仓库中的一条命令重现,无需手动操作。
开发者的本地环境几乎总是与生产环境不同。开发者使用 macOS 或 Windows,而服务器运行在 Linux 上。不同的操作系统有不同的文件系统、编码、线程时序和系统调用。即使两个环境都是 Linux—内核版本、glibc、OpenSSL 也可能不同。
第二个原因是安装的软件集。开发者的机器上可能安装了全局的 Node.js 20 版本,而 CI/CD 配置中指定的却是版本 18。或者开发者本地使用 PostgreSQL 16,而生产环境使用 PostgreSQL 14。次要版本的差异通常不明显,但主要更新可能会改变 SQL 查询的行为。根据 npm Inc. 的数据,67% 与依赖相关的bug是由补丁版本差异引起的。
第三个原因是网络条件。本地机器没有延迟、带宽限制和 DNS 问题。在生产环境中,任何对外部 API 的请求可能需要 500 毫秒而不是 5 毫秒。超时、重试逻辑、竞态条件—所有这些问题只在真实负载和真实网络条件下才会出现。网络模拟通过像 Toxiproxy 这样的工具可以帮助在部署前发现这些问题。
第一个原因—缺少数据。开发者使用测试夹具工作,而生产环境中有数百万条记录,包含各种意外值。开发者认为必填的字段中出现 NULL、名称中的 Unicode 字符、过长的字符串—所有这些都可能引起在本地使用合成数据的数据库中无法重现的bug。
第二个原因—不同的编译和构建标志。发布版(Release/Distribution)可能与调试版(Debug)不同。编译器优化、删除调试日志、函数内联—所有这些都可能隐藏或反而暴露bug。典型例子:在调试版中运行的断言在发布版中由于变量初始化顺序不同而失败。
第三个原因—本地缓存和临时文件。开发者可能没有注意到bug,因为浏览器中缓存了旧的脚本,Redis 中保存了过时的数据,文件系统中还残留着上次运行生成的临时文件。干净的运行(无痕模式、清除缓存、全新安装)通常能重现那些“自己”不出现的bug。
第四个原因—全局依赖和本地依赖的冲突。Ruby gems、Python pip、Node.js npm 等工具可能安装了全局包,这些包“帮助”代码在本地运行,但在生产环境中不存在。使用虚拟环境(virtualenv、venv、nvm)可以将项目与全局安装隔离,使环境可重现。
“在我机器上能运行”这句话会破坏团队中的信任。如果开发者经常无法重现bug,同事就会开始怀疑他的能力或测试的细致程度。久而久之,这会导致微观管理:每次更改都需要第二个开发者检查,从而拖慢开发进度。根据 Google Project Aristotle 的研究,团队中的心理安全感直接影响生产力,而关于环境的持续争论是降低生产力的因素之一。
第二个问题—代码审查变慢。如果开发者无法在本地重现bug,他可能会拒绝同事的 pull request,并说“在我这里能运行—说明问题在你那里”。这会引发冲突并延迟功能交付。环境标准化消除了这一冲突:如果两个开发者都在相同的 Docker 容器中工作,那么“在谁那里能运行”的问题就失去了意义。
第三个问题—追踪器中的bug丢失。那些“在开发者那里无法重现”的bug通常会被标记为“无法重现”(Cannot Reproduce)而关闭。一个月后,这个bug会出现在生产环境中,修复成本高出 10 倍。规则:如果一个bug至少在一个人那里能重现—它就存在,不管它在不在开发者那里运行。
第一种也是最有效的方法—Docker。整个项目应该通过 docker-compose up 启动,无需额外操作。数据库、缓存、消息队列、Web 服务器—全部在容器中启动。开发者只需要安装 Docker 和 Git。其余的一切都在容器内部。这保证了所有团队成员拥有相同的环境,无论使用什么操作系统。
第二种方法—版本管理器。如果无法使用 Docker(许可证限制、遗留基础设施),请使用 nvm(Node.js)、rbenv(Ruby)、pyenv(Python)、sdkman(Java)。版本管理器允许在项目范围内切换语言和工具的版本。.nvmrc、.ruby-version、.python-version 文件应放在仓库中并由 CI/CD 检查。
第三种方法—使用 Vagrant 管理虚拟机。Vagrant 在 VirtualBox 或 VMware 之上启动具有指定操作系统和配置的虚拟机。通过 provisioning 脚本(shell、Ansible、Puppet)在虚拟机内部安装所有依赖。Vagrant 比 Docker 更重,但提供操作系统级别的完全隔离—对于依赖特定 Linux 内核版本的项目非常有用。
第四种—Makefile 和 bootstrap 脚本。即使是一个简单的带有 install、test、build、clean 目标的 Makefile,也可以标准化日常操作。make install 命令应该安装所有依赖、配置数据库并创建测试数据。统一的入口点为所有开发者排除了环境配置中的手动错误。
主要工具—依赖的锁定文件。package-lock.json(npm)、yarn.lock(Yarn)、Podfile.lock(CocoaPods)、pubspec.lock(Flutter)固定每个包的精确版本。没有锁定文件,两个在不同时间安装依赖的开发者可能会得到不同的次要版本。锁定文件应放在仓库中,且不能手动编辑。
第二个工具—仓库中的.env.example文件。这是一个带有注释的环境变量模板文件。开发者将其复制为 .env 并填入自己的值。CI/CD 流水线检查所有必需的变量是否已设置。根据 GitLab 2023 的数据,使用 .env.example 的团队将与环境变量相关的事故数量减少了 40%。
第三个工具—pre-commit 钩子。在每次提交前运行的自动检查:linter、格式化工具、类型检查、测试。如果所有开发者的钩子配置相同,那么格式或类型错误就不会流入生产环境,而这些错误在“本地机器上通过了”。Husky 用于 JavaScript,pre-commit 用于 Python—都是流行的解决方案。
第四个—CI/CD 流水线,在干净的环境中运行测试。如果测试在 CI 中通过但本地不通过—问题出在本地环境配置。如果测试在 CI 中不通过—pull request 不会被合并。这条硬性规则排除了“在本地能运行”的bug进入主分支的可能性。
常见问题
这是一种防御性反应:开发者花了很多时间在调试上,听到代码不能运行在心理上是痛苦的。这句话给了“切换”的时间,让他可以开始寻找原因而不感到内疚。
请他在干净的环境中重现这个bug(全新安装、无痕模式)。如果无法重现—比较依赖版本和环境变量。如果没有帮助—启动与生产环境相同的 Docker 环境。
Docker 提供一个隔离的容器,具有固定的配置,在任何操作系统上都能以相同的方式运行。所有开发者使用相同的 Dockerfile,因此环境完全相同。如果 bug 在容器中无法重现—说明问题确实出在代码上,而不是系统上。
锁定文件固定所有传递依赖的精确哈希和版本。即使包仓库中发布了新的依赖版本,通过锁定文件安装可以保证每个开发者获得与其他人完全相同的包集合。
如果项目依赖于特定的操作系统内核模块或需要内核级别的完全隔离,使用Vagrant 配合 VirtualBox 是合理的。对于 90% 的项目,Docker 更轻量、更快、更方便。选择取决于项目与操作系统交互的深度。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。