一些运行在 Kubernetes 中的复杂微服务架构是 CPU 和内存密集型的,在某些情况下编译或测试可能非常耗时且占用大量资源。然而大多数工程师的标准设备是笔记本电脑,有 CPU 和内存的限制,编译一次的时间估计够喝好几杯咖啡。
建立在云端的远程实例通常可以根据需要进行扩展,以适应对容量的额外需求,因此在容器化变革的基础上,一些企业开始不再那么依赖于本地开发环境。虽然近一两年有一些令人兴奋的远程方案,比如 GitHub Codespaces、AWS Workspaces 或其它使用云的标准解决方案,但在它们足够成熟和安全之前,很多企业早已选择了自建云端开发环境,Lyft 就是其中之一。
云端开发逐渐成为了坑
早在 2018 年,Lyft 的工程师就将一个大单体拆分成了一系列微服务。基于 Docker 容器的模块化开发环境最终转移到了云端。然而随着时间的推移,工程师、微服务和测试的数量的激增,他们的开发工具跟不上了。
实际上,Lyft 对综合开发环境的第一次重大投资始于 2015 年,当时的工程师人数为 100 名,大部分开发还是在一个单体架构上,只有少数用例是微服务,但预计到工程师和服务的数量会增长,所以他们认为迁移到容器是很有意义的。最初的计划是构建一个基于 Docker 的容器编配环境,工程师可以用它们做测试。它将在生产环境中使用多租户环境,相比以前的解决方案,可以更便宜、更快地进行伸缩。
2016 年初,Lyft 发布了一个本地开发环境,叫作 Devbox,是“盒子里的开发环境”的缩写,由一些管理本地虚拟机及其配置的工具组成,包括数据生成、包和镜像的下载和安装。开发人员只需要发出一个命令就可以构建一个可以处理请求的环境。
这些体验很棒,让工程师们第一次拥有了一种一致的、可重复的、简单跨多个服务开发方法,于是很快出现了共享这些环境的需求。Devbox 转向了云端,变成了 Onebox。Onebox 本质上是一个运行在 EC2 实例上的 Devbox 环境。由于它的容量更大,下载镜像的速度更快,工程师们自然更喜欢它而不是 Devbox。
两种不同风格的开发环境
在将 Devbox 和 Onebox 作为容器化开发环境引入四年后,使用这些环境的工程师增加了十倍,微服务数量也一直在激增,配置和启动 Onebox 实例变得越来越困难和耗时。
由于每个服务都有很深的交互树结构,实例可能需要很多的资源。可观察性工具不能跟上所有正在运行的环境,导致调试工作变得很困难。打比方说,不可能在数百个环境中运行相同的可观察性工具,当出现问题时,就很难查明确切原因。此外,工程师的认知负担显著增加,因为他们需要牢记整个系统,而不是专注于特定的组件。
根据 Lyft 的工程设计,工程师的代码变更过程可以分为“内部开发循环”和“外部开发循环”。前者应该只需要几秒钟就能给出反馈,因为它只涉及修改代码和运行一些测试。后者可能需要更长的时间(至少 10 分钟),因为它涉及持续集成和代码评审。集成测试臃肿笨拙,花费一个小时是司空见惯的事情。并且 80% 以上的测试要么是不必要的,要么可以在短时间内重写并在没有外部依赖的情况下运行。测试失败还要花费数小时的调试时间,大多数还都是误报。
另一方面,执行内部开发循环通常需要将代码更改同步到开发人员自己在 Onebox 的远程 VM 环境。考虑到 Onebox 环境的设置和启动速度很慢,再加上明显的不稳定性,工程师通常会依赖外部开发循环的 CI 测试来验证每个代码变更迭代。
一年前,将开发环境迁移到 Kubernetes 之后,工程资源的变化让大家不得不重新审视开发环境:维护基础设施以支持这些按需环境变得过于昂贵,而且只会随着时间的推移而恶化,所以需要对开发和测试微服务的方式进行更根本性的改变。
开发环境必须回到工程师的机器上
为了摆脱不断增长的烦恼和挫折,Lyft 将开发环境带回到工程师的笔记本电脑上,同时重新构建内部开发循环。
在容器中运行代码并不是一种免费的抽象,因此他们决定在 MacOS 的隔离环境中运行服务代码,不使用容器或虚拟机。
在 Lyft,大多数后端服务是用 Python 或 Go 语言开发的,而前端服务是用 Node 开发的:
- 对于 Python 服务,通过使用不可变的虚拟环境进行隔离。每次 requirements.txt 文件发生变化,都会构建一个新的虚拟环境。
- Go 服务利用 Go 模块工具链在 go run 或 go test 命令执行时自动下载和链接所有依赖项。
- 他们基于 nodeenv 构建了一个包装器,用于根据每个 Node 服务的元数据为其创建合适的环境。
一些专门的服务,如数据存储,也在本地运行,通常会使用容器。数据存储在启动时使用由服务所属团队维护的脚本来加载新数据。
因此,在本地启动服务需要多个步骤。通过手动来执行它们既乏味又容易出错。Lyft 使用 Tilt 来协调服务的生命周期及其环境,避免通过手动执行所有的步骤。每个服务都有一个 Tiltfile,用于描述在本地运行服务所需的步骤。当工程师在 IDE 中修改了代码时,运行中的服务也会重新加载自己,从而进一步缩短内部开发循环。
除了运行服务之外,还需要与服务发生交互。由于 Lyft 使用了不同的传输格式,比如 gRPC、JSON/HTTP 和 protobuf/HTTP,因此向服务发出请求并不是那么简单。工程师们使用 Lyft 开发的一个工具向本地服务发送请求。这个工具可以与服务的 IDL 集成,因此可以利用工具的自动补全功能。
最终结果
自从将这个工具推广到整个公司以来,Lyft 工程师反馈非常积极。
开发人员喜欢无需任何远程环境即可在笔记本电脑和 IDE 中运行测试的能力。创建一个新的 Onebox 环境通常需要大约一个小时,但现在笔记本电脑环境始终可以运行测试,使用 Tilt 在本地启动服务只需几分钟。
负责基础架构的高级软件工程师 Scott Wilson 说:“我们还观察到开发人员的行为转变,因为他们花费更多时间专注于测试他们的服务。在测试本地服务时,用户可以直接向服务 API 发送请求,而不是通过移动应用程序与公共 API 对话。这增加了开发人员对服务 API 的熟悉程度,并缩小了在出现错误时的调试范围。”
而且让用户在他们的笔记本电脑上单独运行服务意味着真正减少所需的总计算资源。“虽然成本不是这个项目的主要驱动因素,但不再为每个开发人员支付 Onebox 的 AWS 实例,我们最终节省了大量资金。”
总的来说,对于开发者来说,因开发环境而导致正常工作停滞不前是特别让人不可忍受的。不好的开发环境会严重影响大家的生产力,而一个好的本地开发环境,允许开发人员在没有破坏共享环境或干扰面向客户的环境的风险的情况下运行和测试代码。而且,它通常是低成本的,依赖于已经支付的资产,比基于云的环境更高效、更便宜。
另外,Scott Wilson 表示他们也正在密切观望完全远程的开发环境,例如 Github Codespaces,以了解它们成熟后是否适合自己。