Docker Compose 5种常见错误

发布日期: 2021-01-18 17:38:43 作者: Stephen 评论: 0

在构建一个容器化应用程序时,开发人员需要一种方法来引导他们正在使用的容器去测试其代码。虽然有几种方法可以做到这一点,但 Docker Compose 是最流行的选择之一。它让你可以轻松指定开发期间要引导的容器,其次建立一个快速的“编码 - 测试 - 调试”开发循环。

愿景是,一个人编写一个docker-compose.yml文件,指定了开发中所需的一切,并将它提交到代码仓库。然后,每一个开发者只需运行docker-compose up,即可启动测试其代码需要的所有容器。

然而,要使docker-compose设置达到最高性能,需要大量工作。我们见过最好的团队在不到一分钟的时间内启动他们的开发环境,并在几秒中内测试每个更改。考虑到每个开发人员每天花在测试代码上的时间,小的改进会对开发人员的生产力产生巨大影响。

源自 XKCD

1. 错误:频繁的容器重建

docker build需要很长时间。如果每次想要测试一个代码更改时都要重新构建你的容器,那么你就有很大潜力来加速你的开发循环。

处理非容器化应用程序的传统工作流如下:

  • 编码
  • 构建
  • 运行

这些年来,通过使用针对编译型语言的增量构建和热加载之类的技巧,这个过程得到高度优化。它变得非常快。

当人们第一次采用容器时,他们倾向于采用现有的工作流程,只添加一个docker build步骤。他们的工作流如下:

  • 编码
  • 构建
  • 容器构建
  • 运行

如果做的不好,docker build步骤会使所有优化都白费。另外,它还增加了一堆额外的耗时工作,例如使用 apt-get 重新安装依赖。所有这些加起来,会使得我们的测试过程比使用 Docker 之前要慢得多。

解决方案:在 Docker 外运行你的代码

一种方案是在 Docker Compose 中启动所有依赖项,但在本地运行你正在积极处理的代码。这模仿了开发非容器化应用程序的工作流。

只需要在localhost上暴露你的依赖,并将你正在使用的服务指向localhost:<port>地址。

然而,这并不总是可行的,尤其是如果你正在处理的代码依赖容器镜像内置的东西,而这些东西不容易从你的笔记本电脑访问。

解决方案:最大化缓存来优化 Dockerfile

如果必须构建 Docker 镜像,那么编写 Dockerfile 时,最大化缓存能将一个 10 分钟的 Docker 构建变为 1 分钟。

生产环境的 Dockerfile 文件的典型模式是通过将单个命令链接到一个RUN语句中来减少层数。然而,镜像大小在开发过程中并不重要。在开发过程中,你想要尽可能多的层数。

你的生产环境 Dockerfile 文件可能如下所示:

RUN \
    go get -d -v \
    && go install -v \
    && go build

这对开发来说很糟糕,因为每次重新运行该命令时,Docker 都会重新下载所有的依赖并重新安装它们。增量构建更加有效。

相反,你应该有一个专门用于开发环境的的 Dockerfile 文件。将每件事都分解成非常小的步骤,规划好你的 Dockerfile 文件,这样基于经常变化的代码的步骤最后执行。

最不频繁更改的内容,例如拉取依赖,应该放在第一位。这样,在重建 Dockerfile 时就不必构建整个项目。你只需要构建你刚刚修改的一小部分。

关于这个的例子,请看下面我们用于 Blimp 开发环境的 Dockerfile。它遵循上面所述的技术,将繁重的构建过程缩短到几秒钟。

https://blimpup.io/

FROM golang:1.13-alpine as builder

RUN apk add busybox-static

WORKDIR /go/src/github.com/kelda-inc/blimp

ADD ./go.mod ./go.mod
ADD ./go.sum ./go.sum
ADD ./pkg ./pkg

ARG COMPILE_FLAGS

RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./pkg/...

ADD ./login-proxy ./login-proxy
RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./login-proxy/...

ADD ./registry ./registry
RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./registry/...

ADD ./sandbox ./sandbox
RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./sandbox/...

ADD ./cluster-controller ./cluster-controller
RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./cluster-controller/...

RUN mkdir /gobin
RUN cp /go/bin/cluster-controller /gobin/blimp-cluster-controller
RUN cp /go/bin/syncthing /gobin/blimp-syncthing
RUN cp /go/bin/init /gobin/blimp-init
RUN cp /go/bin/sbctl /gobin/blimp-sbctl
RUN cp /go/bin/registry /gobin/blimp-auth
RUN cp /go/bin/vcp /gobin/blimp-vcp
RUN cp /go/bin/login-proxy /gobin/login-proxy

FROM alpine

COPY --from=builder /bin/busybox.static /bin/busybox.static
COPY --from=builder /gobin/* /bin/

最后一点:通过最近引入的 多阶段构建,现在可以创建具有良好分层且镜像很小的 Dockerfile。我们在本文中对这一点不会过多讨论,只能说上面显示的 Dockerfile 就是这样做的,因此它既用于 Blimp 开发环境也用于生产环境。

解决方案:使用主机卷

通常,最好的选择是使用一个主机卷来直接将你的代码加载到容器上。这使你能够以本机速度运行代码,同时仍然在包含运行时依赖项的 Docker 容器中运行。

主机卷将你笔记本电脑上的一个目录镜像到一个正在运行的容器中。当你在文本编辑器中编辑一个文件时,更改会自动同步到容器中,然后能立即在容器中执行。

大多数语言都有一种方法来监视你的代码,并在代码更改时自动重新运行。例如,nodemon 是 Javascript 中的监视代码的方法。请查看这篇关于如何设置这一点的文章教程。

https://www.npmjs.com/package/nodemon

https://blimpup.io/blog/docker-volumes-for-development/

它最初需要一些工作,但结果是,你可以在 1-2 秒内看到你的代码更改的结果,而一次 Docker 构建可能需要几分钟。

2. 错误:主机卷速度慢

如果使用了主机卷,你可能已经注意到,在 Windows 和 Mac 上读写文件的速度非常慢。对于读写大量文件的命令来说,这是一个已知的问题,例如具有复杂依赖的 Node.js 和 PHP 应用程序。

这是因为 Docker 是运行在 Windows 和 Mac 的一个虚拟机上。在进行主机卷加载时,必须经过大量的转换才能将笔记本电脑上的文件夹加载到容器中,这有点儿类似网络文件系统。这会增加大量负载,而在 Linux 本机上运行 Docker 时不会出现这些情况。

解决方案:放松强一致性

其中一个关键问题是,默认情况下,文件系统加载会保持强一致性。一致性是一个广泛的话题,可以浓墨重彩地大书特书,但是简而言之,它意味着所有特定文件的读取者和写入者都同意任何文件修改发生的顺序,从而(最终,某种程度上)同意该文件的内容。

问题是,强制实现强一致性是相当昂贵的,需要所有文件写入者确保他们不会不恰当地破坏彼此的更改。

虽然强一致性有时特别重要,例如,当在生产环境运行数据库时。好消息是,在开发环境,它不是必需的。你的代码文件只会有单个写入者(你自己),和单个信源(你的代码库)。因此,冲突并不像在生产中那么需要担心。

正是由于这个原因,Docker 实现了在加载卷时放松一致性保证的功能。在 Docker Compose 中,你只需将cached关键词添加到卷加载中即可获得显著的性能保证。(不要在生产环境这么做…)

volumes:
    - "./app:/usr/src/app/app:cached"

解决方案:代码同步

另一种方案是设置代码同步。你可以用一个工具来通知你的笔记本电脑和容器之间的更改,并复制文件来解决差异(类似于 rsync),而不是加载一个卷。

Docker 的下一个版本内置了 Mutagen,作为卷的缓存模式的一种替代。如果你感兴趣,就等 Docker 发布下一个版本再试试,不过你也可以直接下载 Mutagen 项目,不用等就可以直接使用。

https://mutagen.io/

解决方案:不要加载包

对于 Node 这样的语言,大部分文件操作往往位于包目录(例如node_modules)。因此,从卷中排除这些目录会显著提高性能。

在下面的例子中,我们有一个卷将代码加载到一个容器中。然后用它自己干净的专用卷覆盖了node_modules目录。

volumes:
  - ".:/usr/src/app"
  - "/usr/src/app/node_modules"

这个额外的卷加载告诉 Docker 为node_modules目录使用一个标准卷,这样当npm install运行时,它不会使用比较慢的主机加载。为了使之生效,当容器首次启动时,我们在entrypoint运行npm install来安装我们的依赖并填充node_modules目录。像这样:

entrypoint:
  - "sh"
  - "-c"
  - "npm install && ./node_modules/.bin/nodemon server.js"

克隆和下载上述示例代码的完整说明,请参考此处。

https://blimpup.io/docs/#/getting-started

3. 错误:脆弱的配置

大多数 Docker Compose 文件都是有组织地演化的。我们通常会看到大量的复制粘贴代码,这使得代码修改非常困难。一个干净的 Docker Compose 文件可以更容易地在生产环境变化时进行定期更新。

解决方案:使用 env 文件

env 文件将环境变量从主 Docker Compose 配置中分离出来。这有助于:

  • 使密钥不会保存在 git 历史中
  • 使每个开发者拥有稍微不同的设置变得容易。例如,每个开发者可能有一个唯一的 access 密钥。将配置保存在一个.env文件中意味着他们不必修改提交的docker-compose.yml文件,并在这个文件更新时处理冲突。

要使用 env 文件,只需增加一个.env文件,或者使用env_file字段显式设置路径。

解决方案:使用 override 文件

Override 文件让你有一个基本配置,然后在不同文件中指定修改。如果你使用 Docker Swarm,并且有一个生产环境的 YAML 文件,这将非常有用。你可以在docker-compose.yml中存储自己的生产环境配置,然后在一个 override 文件中指定开发环境所需的任何更改,例如使用主机卷。

https://docs.docker.com/compose/extends/

解决方案:使用 extends

如果你正在用 Docker Compose v2,你可以使用extends关键字在多个地方导入 YAML 片段。例如,你可能有一个定义,你公司的所有服务在开发环境的 Docker Compose 文件中都有这 5 个特定的配置项。你可以定义一次,然后使用extends关键词来将它放到任何需要的地方,这就提供了一些模块化特性。我们不得不在 YAML 中这样做是很痛苦的,但我们能够少写一个程序来生成它还是最好的。

Compose v3 移除了对extends关键词的支持。然而,你可以使用 YAML anchors 获得类似的结果。

https://support.atlassian.com/bitbucket-cloud/docs/yaml-anchors/

解决方案:程序生成 Compose 文件

我们已经和一些使用 Blimp 的工程团队一起工作过,他们在开发环境的 Docker Compose 文件中有上百个容器。如果他们使用单个巨大的 Docker Compose 文件,就需要数千行无法维护的 YAML 代码。

https://blimpup.io/

随着扩展,可以编写一个脚本,来基于一些高级别的规范生成 Docker Compose 文件。这对于具有非常大的开发环境的工程团队来说是很常见的。

4. 错误:脆弱的引导

docker-compose up是不是只有一半时间工作?你是不是不得不使用docker-compose restart来启动崩溃的服务?

大多数开发者都想要写代码,不想做 DevOps 工作。调试一个坏的开发环境是非常令人沮丧的。

docker-compose up应该每一次都好好工作。

这里的大多数问题都与服务启动顺序错误有关。例如,你的 Web 应用可能依赖一个数据库,如果 Web 应用启动时数据库还没有就绪,那么它就会崩溃。

解决方案:使用 depends_on

depends_on使你能控制启动顺序。默认地,depends_on会等待依赖被创建,而不等待处于“healthy”状态的依赖。然而,Docker Compose v2 支持将 depends_on 与健康状态检查结合起来。(不幸的是,这个功能在 Docker Compose v3 中被移除了。你可以使用一个类似 wait-for-it.sh 的脚本来手动实现类似功能)

https://github.com/vishnubob/wait-for-it

Docker 文档建议不要使用类似depends_onwait-for-it.sh之类的方案。而且,我们同意,在生产环境,要求为容器指定特定的引导顺序是脆弱架构的一种标志。然而,作为一名试图完成工作的开发人员,修复整个工程组织中的每一个容器可能是不可行的。因此,对于开发环境,我们认为这是可以的。

5. 错误:资源管理不善

要确保 Docker 拥有它流畅运行所需的资源,而不会完全超出你的笔记本电脑负担,可能是比较棘手的。如果你觉得自己的开发工作流由于 Docker 没有以峰值容量运行而变得迟缓,那么可以参考以下几点。

解决方案:修改 Docker Desktop 分配

Docker Desktop 需要大量 RAM 和 CPU,尤其是在 Mac 和 Windows 上它是一个虚拟机时。默认的 Docker Desktop 配置往往没有分配足够的 RAM 和 CPU,因此我们一般建议调整设置到过度分配。我倾向于在开发时给 Docker 分配大约 8GB RAM 和 4 CPU(在不使用 Docker Desktop 时,我会将它关掉,来使之可行)

解决方案:删除未使用的资源

人们在使用 Docker 时,经常会无意识地泄漏资源。人们拥有成百上千的卷、旧的容器镜像以及如果不小心有时还会运行的容器,这并不少见。这就是为什么我们推荐偶尔运行docker system prune,删除当前没有使用的所有卷、容器和网络。这会释放大量资源。

解决方案:在云上运行

最后,在某些情况下,即使有上述提示,也不可能在你的笔记本上运行所需的所有容器。如果是这样的话,可以看看 Blimp,这是一种在云上运行 Docker Compose 文件的简单方法。

6. 你应该做什么?

为了提升 Docker Compose 上的开发者体验,我鼓励你

  • 最小化容器重新构建
  • 使用主机卷
  • 力求可维护的 compose 文件,就像代码一样。
  • 使你的引导可靠
  • 用心管理资源

作者:Ethan Jackson
原文:https://blimpup.io/blog/common-docker-compose-mistakes/
翻译:张健欣
策划:万佳

快来抢沙发