最近,我构建了一个本地开发环境,将 Docker 用于一些关键的集成测试路径。当我完成这项工作时,我意识到,我在开始这项工作之前没有考虑到下面这些深远的影响:
要求开发人员的本地机器上有 Docker 和 Docker-Compose;需要做相当多的配置环境才能真正可用;我编写了 shell 脚本用于“缓解”这些配置问题,但却混淆了系统的实际工作方式;我编写的 shell 脚本最后看来也相当短视——它在某些环境下工作得很好,但是如果你在 Windows 环境下工作,就只能靠自己了;我花了大半天的时间来解决一些简单的数据库连接问题,结果发现我的数据库容器没有正确配置。
我在这项工作上投入的所有时间,结果确实使我的团队受益了,并且最终帮助我们解决了集成测试中的一些挑战。但对我来说,更有趣的是它所带来的挑战,更不用说我提交的 Pull 请求在它最终被合并之前的热烈讨论了。
更重要的是,这个环境最终服务于一个单一的目的——提供集成测试明确性,而不是像我最初希望的那样提供一个整体的开发环境。最终结果是,我们将该环境从开发人员的机器中移走,并最终将其以容器的形式部署到云提供商那里,用于创建集成测试资源。
除了这个亮点,我的努力基本上是失败的,尤其是考虑到我最初的动机。
我怎么会走错路呢,我所有的努力难道只换来了一个花哨的测试环境?我决定更深入地研究这个基于容器的开发环境问题,从那以后,我学到的东西极大地改变了我将来处理这个问题的方式。
大量的调查表明,Docker 的使用将继续增长,特别是随着基础设施的增长和日益复杂。 DataDog 2018 年 6 月的一项调查显示,大约 25% 的公司使用 Docker 部署了某种形式的基础设施。这些环境中有一半是通过某种方式精心策划的,从 2017 年到 2018 年,部署规模增长了 75%。根据这些消息来源,Docker“革命”正在如火如荼地进行,没有放缓或停止的迹象。(我仍然很好奇,75% 的公司在他们的部署中使用的是什么,跑题了哈。)
DataDog 在 2018 年的调查报告中提到,使用最广泛的 Docker 镜像是“Nginx、Redis 和 Postgres”。我认为这是合理的,因为运行应用程序依赖项的容器似乎是容器化的第一步。 Docker Compose 为多容器应用程序提供了一个相对简单的工具;它似乎也是一个很好的工具,让开发人员可以为他们自己的环境运行特定的、底层的基础设施。也就是说,你在项目中准备好一个 docker-compose.yml 文件,就可以开始了。
有 25% 的公司在生产环境中运行 Docker,其中有多少公司使用 Docker 作为开发工具呢? 2019 年 Stack Overflow 的调查报告显示,38.4% 的受访者在开发工作中使用容器,但大约一半的受访者目前没有使用任何容器技术。我想知道,是否有一种方法可以进一步了解为什么开发人员没有像我最初设想的那样频繁地使用 Docker。我决定再深入一点,粗略地研究一下开发人员对 Docker 的看法。
许多开发人员讨厌在自己的环境中使用 Docker,并且有很好的理由——引入容器似乎会减慢开发人员和他们正在构建的环境之间的反馈循环。开发环境的容器化似乎也为操作人员创建了一个不必要的抽象,需要他们能够直接深入代码、运行时环境,甚至更底层的操作系统——所有这些都是在他们构建特性的时候进行的。
支持将 Docker 作为开发工具的人表示,这样做还是有好处的。使用容器的开发环境还可以实现开发团队之间的一致。如果每个人都为他们的数据库、缓存或其他各种基础设施使用容器,那么准备编写代码就会像 docker-compose up 一样简单,完整的开发环境触手可及。假设你的团队愿意在本地运行容器,那么你就永远不会在开发环境和生产环境之间造成差异。在运行 brew upgrade 时,不会再出现令人不快的意外——容器将始终与你的需求保持同步。
坦率地说,我对这两类人都深有同感。作为一个一只脚牢牢踏在运营上的开发人员,我认为运行容器有巨大的好处。然而,我不认为这些好处能够清晰地映射到开发人员的工作流程里。我相信这与在开发环境中使用 Docker 的体验存在差异,因为 Docker 不是开发人员的工具。然而,我不认为这意味着开发团队不应该考虑利用 Docker 来满足他们自己的需求。不过我觉得,将 Docker 作为另一种操作工具可以帮助减轻在本地开发环境中运行 Docker 的痛苦。
按照 Docker 官方的定义,容器是:
一种标准的软件单元,它将代码及其所有依赖项打包,使得应用程序可以快速、可靠地从一个计算环境迁移到另一个计算环境上运行。
下面是一个更精辟些的定义:
容器是应用程序层的抽象,它将代码和依赖项打包在一起。
听起来不错,对吧?是的,特别是当你试图部署代码时。换句话说,如果我的主要问题是让我们的代码在任何地方运行都不出意外,容器似乎是最好的抽象——但我不需要对我们运行的代码有太多了解,相反,我需要可以预见我们如何运行它。对于操作人员而言,这是非常强大的。
作为开发人员,这种抽象可能会带来一些麻烦。容器本身并不关心运行的是什么。操作系统被有意地抽象掉,就像运行容器所必需的任何依赖项一样。应用层本身和底层操作系统一样短暂,访问这些抽象层需要了解 Docker 希望以何种方式运行代码。
关于容器的预期用途,我已经说了很多,但是我认为,说容器只是为运营团队而准备的是错误的。我认为开发人员可以从容器化他们环境的某些部分中获益。挑战在于创建一个真正的开发人员友好的、基于容器的工作流。
最后,我选择在自己的开发工作中大量使用 Docker。这对我很有效,尤其是对我每天所做的工作。在 Test Double,我主要从事 DevOps 和 SRE,因此,在日常工作中使用容器是很有意义的。
当然,这可能不适合你的团队。但是,如果你确实想在开发环境中探索 Docker 的使用,我想提醒你注意一些我亲身经历过的常见陷阱。
在容器化开发环境中,最大的陷阱可能是假设在部署中获得的好处与开发人员在本地体验到的好处相同。
类似地,假设一个团队希望广泛地使用 Docker,这种假设可能不适用于你的团队的工作证书。如果开发人员与运维工作相对孤立,那么假设他们可能希望每天都在本地使用容器是不安全的。但是,如果你在一个开发人员也做一些运维工作的团队中工作,假设你在容器开发环境之外还提供了其他选项,这可能会使你的团队受益。
在本地运行容器的实际情况是,它们会消耗大量资源。在一个典型的工作日,Docker 仅在我的机器上就占用了大约 36GB 的存储空间。对于我这台特定品牌和型号的工作站来说,这不是一个惊人的系统使用量,但是我很容易就可以推断出,当我在工作流程中包含更多的容器时,它会明显增加。在我的 CPU、内存和磁盘资源活动监视器中,Docker 也始终排在前面。
更重要的是,虽然这可能不会对我的机器造成很大的拖累,但这并不意味着它对其他机器也不会,最终是否决定将这么多资源分配给 Docker 应该取决于开发人员自己的个人偏好。也就是说,你的笔记本电脑不是服务器,它可能根本不需要按照服务器标准构建的容器资源。
但是,即使抛开系统资源不谈,开发环境也不能很好地与运行代码的任何系统相匹配。过去,这个差距是如此之大,我们常常不得不通过隧道技术连接到一个和生产服务器完全一样的环境,如果出现问题,有些人(可悲的是,包括我自己在内)甚至*在线修改这些系统的代码。
开发人员需要专注于编写可维护、可靠且经过良好测试的代码。我认为,在受限的环境中工作可以催生更好的代码实践和决策——你*依赖于编写干净、可维护且可行的代码,而不是希望通过配置服务器来处理性能瓶颈和低效的实现。
将技术栈中的部分内容装入容器,其挑战在于减少运行容器所需的认知开销。开发人员在工作中需要构建和维护的上下文已经很多。如果本地 Docker 环境破坏了这个上下文,例如,确定一个容器是否正在运行,那么从长远来看,只会导致失望。
类似地,要求开发人员使用 docker run 命令跳转到容器会增加上下文切换的认知开销。这不仅与开发人员在自己的本地环境中进行开发时所构建的模式背道而驰,而且还需要额外掌握 docker CLI,这就给开发人员实现目标带来了负担。当我不得不跳转到 Docker 容器中调试某些东西时,我也不禁感到有些奇怪。这有点像我之前提到过的一件有违常理的事:通过隧道技术连接到生产服务器。
其他服务也是如此。如果你在一个完全依赖于微服务的团队中工作,而其他团队需要各种服务来构建自己的特性,那么你在将这些镜像作为容器提供时需要非常谨慎。在这些情况下,团队利用自己的环境支撑服务,实际上可能比用容器混淆服务更有利。
具体来说,对于这些内部服务,可能审核它们的文档比将其装入容器更值得做。维护不善的文档与容器相结合可能会造成相当的认知失调,从而导致放弃 Docker 环境。另外,我认为,我们所有人都应该更仔细地查看文档,并频繁地进行审计,而不是创建新的文档。
我上面提到的调查结果指出,Nginx、Redis 和 Postgres 在采用容器的团队中非常受欢迎。它们为什么如此受欢迎,这显而易见。除非你的团队正在编写自己的 RDBMS 或 Web 服务器 / 负载平衡器,否则在你的技术栈中引入这样的开源应用程序,将使你受益匪浅。
但是在容器化之前,运营团队获得的好处,可能无法与开发团队在决定将它们包含到技术栈中时所希望获得的好处相匹配。通过将这些依赖项封装进容器,运营团队可以从类似的加速器中获益,这与开发人员不编写自己的 RDBMS 所获得的好处类似。
容器化的 Postgres 为部署、监控和扩展这个关键依赖项提供了大量的选项。它还减少了系统更新、升级和管理的开销。对于运营团队来说,这很好,但是它符合开发人员的需求吗?
一句话,不符合。即使设置应用程序栈只是运行 docker-compose up -d 这么简单,它也不能很好地契合大多数开发人员在本地运行环境的心智模型。这两种工作流程之间的差异是一种根本性的差异。
开发人员希望能够深入研究他们需要的抽象,并要求团队使用一个全新的工具,使用一种完全不同的方法来运行他们的本地环境,这是个大问题。具体来说,开发人员需要能够运行数据库迁移、跳转到数据库 CLI 并跟踪数据库日志。对于技术栈中的任何关键组件都要如此。
这并不是说没有开发人员乐于在本地使用 Docker 来处理各种事情。但是我敢打赌,使用 Docker 的开发人员已经找到了如何让它在他们的工作流程中无缝工作的方法,无论他们只是做了一个习惯性的改变,还是他们已经编写了脚本使其更符合他们对自己环境的设想。
说到脚本,如果你正在构建一个像这样的本地环境,并且你试图减少开发人员在 Docker 上运行各种东西的开销,那么你最初的想法(就像我一样)可能是将许多与容器的工作相关的重复任务编写成脚本。
这种偏好不一定是因为误导。毕竟,我们被教导要用脚本解决重复的任务,这样我们才能专注于重复性更少的工作。但是要注意,不要编写太多的脚本,特别是当你的团队对容器领域还比较陌生时。如果不同团队中的许多开发人员都采用这种方法,那么你得考虑一下,为了让他们在自己的本地环境中运行容器,你的团队有多大的动机和兴趣去维护一个定制的 bash 脚本。
我提到过,使用 docker CLI 会有一些开销,但是如果有一组开发人员有意愿使用容器,那么更合理的做法是,给他们时间,对他们进行培训,让他们使用 Docker 自己的工具,而不是潜在地混淆 Docker 本身的内部工作。它确实是一个必须掌握的工具,但是,尽量不要围绕这些命令编写新的脚本,这样可以减少故障诊断的认知压力,并更广泛地了解工具本身。
再强调下,这也是我最关心的,定制容器封装器以及围绕容器编写新脚本增加了更多的代码,因此而需要维护的东西也就更多。你的富贵团队要承担这些开销。如果你的团队认为,这对他们的工作流程来说是一个净收益,那就继续。我们想要避免的陷阱是一个被抛弃的定制化 shell 脚本,对于那些试图在你的环境中工作的人,它是一个潜在的时间陷阱,特别是在新人刚开始参与开发工作的过程中。
如果你对迁移到容器化的开发环境感兴趣,我认为你应该花相当多的时间为本地运行的系统构建简单的替代方案。换句话说,不要认为在本地环境中使用 Docker 是一个孤注一掷的决定。记录在标准本地部署中设置应用程序的步骤,并为那些对运行容器感兴趣的人提供替代方法。让你的团队决定这个工作流程是否适合。
总之,在不了解开发环境取舍的情况下,无选择地容器化将使开发团队的工作变得困难。
设置开发环境没有所谓正确的方法。虽然鼓励团队采用容器可能会带来一些额外的好处,但是最终的目标是改进开发人员的工作流程。如果我们为了技术抽象而牺牲了编写优秀代码的能力,那么我们就是在创造低效。
与任何事情一样,最终的决策应该是由团队选择,不应该因为个人偏好而保留任何环境,无论是传统的还是容器的。与你的团队一起确定本地开发中的痛点,你就能取得良好的平衡。