本文最初发布于 Earthly 博客,经原作者授权由 InfoQ 中文站翻译并分享。
告诉我,这是不是听起来很熟悉?有人要把 docker-compose 介绍给你,你可能是自愿的,也可能是被迫的。你用了一段时间,但发现它很不灵活。我要告诉你,你可能用错了。
这可能有点夸张。我不认为存在 100%正确或错误的使用方法:自主构建和开发设置往往有各种奇怪的要求,所以标准可能不符合需求。如果你的情况并不完全相符,请带着适当的怀疑态度阅读本文。
本文介绍了我自己使用 docker-compose 犯的一些错误。
我将重点关注与集成测试相关的用例,以及使用 docker-compose 作为开发环境。对于生产使用,我认为 docker-compose 通常并不适合。
问题 1:使用主机网络
新用户遇到的第一个麻烦是 Docker 网络的使用。在你了解了 docker build 和 docker run 的基础知识后,这是你需要学习的另一层知识……坦白说,为什么你还需要了解 Docker 网络呢?通过主机网络,一切工作正常,这对吗?错了!
使用主机网络意味着你必须为使用的各种微服务保留特定端口。如果你碰巧打开两个有端口冲突的栈,那你就倒霉了。如果你想打开同一个栈的两个版本,那也会倒霉。当某个服务有多个副本时,你想测试它的行为吗?这会非常艰难……
在默认情况下,docker-compose 在一个名为<project-name>_default 的独立网络上启动其容器(其中<project-name>为默认的目录名)。所以,你无需做任何特殊的事情就可以利用 Docker 网络。
这个网络立马能给你带来很多好处:
它是一个比主机网络更加独立的网络——因此,系统环境的小问题就不太会导致 compose 设置的行为差异。你可以访问互联网,但是你希望从主机访问的任何端口都要使用端口绑定来声明。
如果一个服务开始监听 0.0.0.0(容器应该这样),那么主机网络设置将在 WLAN 上打开那个端口。如果你使用 Docker 网络,它只会将该端口暴露给该网络。
你可以通过使用服务的 compose name 作为主机名来实现服务之间的通信。因此,如果你有一个名为 db 的服务,在其内部有一个侦听端口 5432 的服务,那么你可以从任何其他服务通过 db:5432 访问它。这通常比 localhost:5432 更直观。而且,由于不存在本地主机端口冲突的风险,因此,在跨不同项目使用时,它可能会更加一致。
大多数端口不需要向主机开放——这意味着,如果你需要通过--scale增加副本的话,它们不会竞争全局资源。
问题 2:强端口绑定到主机 0.0.0.0 上
这种做法随处可见,你肯定也见过很多,每个人都见过很多:将端口绑定为 8080:8080。乍一看,这似乎没什么问题。但魔鬼在细节中。这种极其常见的端口绑定不仅仅是将一个容器端口转发到本地主机——它还将其转发到系统上的每个网络接口上,包括用于连接互联网的任何接口。
换言之,你的开发容器很可能一直在监听你的无线局域网——当你在家、在办公室或在麦当劳时。它总是可以访问。这可能很危险。不要这样做。
“但是 Vlad,我用了 ufw,我的端口默认是不能访问的”。
这也许没错——但如果你在团队中使用这样的 docker-compose 设置,你的队友可能没有在他们的笔记本电脑上安装防火墙。
修复方法非常简单:只需在前面添加 127.0.0.1:,例如 127.0.0.1:8080:8080。这是告诉 docker 只向回环网络接口公开端口,不包括其他网络接口。
问题 3:使用 sleep 来协调服务启动
我要坦白一件事。关于这一点,我是百分百有错的。
这个问题之所以如此复杂,主要原因是 Docker 或 Docker Compose 没有提供支持解决这个问题。Docker-compose 文件格式的 2.1 版本中有一个名为 condition 的 depends_on 选项,可以设置为 service_healthy。而且,每个服务都可以有一个 healthcheck 命令,可以告诉 docker-compose“健康”是什么意思。这在3.0版本中不再可用,也没有提供替换项。
Docker 文档的基本建议是,服务要在其他服务暂时离线的情况下具有弹性,因为这在生产环境中可能会发生,例如出现短暂的网络不稳定,或者一个服务重新启动。这是无可争辩的。
当你运行一个集成测试,而用于初始化测试环境的例程(例如预先用一些测试数据填充数据库)在其他服务准备就绪之前无法恢复启动,就会变得有点麻烦。因此,关于“至少它在生产环境中会有弹性”的论点在这里并不适用,因为用测试数据填充数据库的代码从未在生产环境中使用。
对于这种情况,你需要等待服务就绪。Docker 建议使用wait-for-it、Dockerize或wait-for。但是,请注意,端口就绪并不总是表示服务已经准备好可供使用。例如,在使用具有特定模式的特定 SQL DB 的集成测试中,当数据库初始化时,端口变为可用,但是,测试可能只有在特定模式迁移完成之后才能开始。你可能需要在前面进行特定于应用程序的检查。
问题 4:在 docker-compose 中运行数据库,但在主机上测试
有这样一种情况:你想运行一些单元测试,但这些测试依赖于一些外部服务。可能是数据库,可能是 Redis,也可能是另外一个 API。简单:让我们把这些依赖放在 docker-compose 中,并让单元测试连接到它们。
这很好——但请注意,你的测试不再仅仅是单元测试,它们现在是集成测试了。除术语之外,现在还需要考虑一个重要的区别:你需要考虑测试环境的设置和清理。通常,最好在测试代码之外执行设置/清理操作——主要原因是可能有多个不同的包依赖于这些外部服务。但你的情况可能不太一样。
如果你最终将测试设置和清理分开,那么你需要多做一些工作,将集成测试容器化。
容器化测试意味着:
你在同一个 Docker 网络上,所以连接设置与你在 compose 中运行服务时相同。配置变得更简洁。
在设置和清理过程中,你可以重用用于等待其他服务就绪的代码。
集成测试不依赖于任何其他的本地系统配置或环境设置,例如你的 JFrog 凭证或任何构建依赖项。容器是隔离的。
如果另一个团队需要针对测试所依赖的服务的更新版本运行你的测试,那么你只需要共享集成测试镜像——不需要他们编译或设置一个构建工具链。
如果你最终使用了多个单独的集成测试容器,那么你通常可以并行地运行所有这些容器。
使用容器化集成测试的一个技巧是,为它们使用一个独立的 docker-compose 定义。例如,如果你的大部分服务都存在于 docker-compose.yml 中,那么可以添加一个包含集成测试定义的 docker- composition .test.yml 文件。这意味着 docker-compose up 会提供你常用的服务,而 docker-compose -f docker-compose.yml -f docker-compose.test.yml up 会启动集成测试。要了解完整的实现示例,请参阅 Ardan Labs 提供的这个优秀的docker-compose集成测试库。
说这是错误有点不公平。在许多情况下,不容器化是更可取的。举个简单的例子,许多语言都与 IDE 深度集成,这使得在语言和 IDE 之间插入容器几乎不可能。有很多正当的理由不这样做。
小结
对本地开发来说,Docker Compose 是一个非常棒的工具。尽管它有一些缺陷,但通常,它会给许多工程团队带来很多生产力方面的好处,尤其是在与集成测试一起使用时。
原文链接:
评论