|
|
51CTO旗下网站
|
|
移动端

编写Docker Compose时的要注意的五大常见错误

本文将讨论开发人员在编写Docker Compose时常见的五大错误,并给出提高工作效率的建议与方法。

作者:陈峻译来源:51CTO|2020-07-28 07:00

编写Docker Compose时的五大常见错误

【51CTO.com快译】

在构建容器化的应用时,开发人员往往需要某种方法来引导启动目标容器,以对其进行代码级别的测试。尽管业界有许多方法可以实现该目的,但Docker Compose是目前最受欢迎的一种方法。它能够让如下两个方面变得容易实现:

  • 指定在开发过程中需要启动的容器。
  • 设置一套快速的代码测试调试(code-test-debug),以方便开发循环。

通常情况下,团队事先编写一个docker-compose.yml文件,指定开发所需的所有内容,并将其提交给存储库。然后,每个开发人员只需运行docker-compose up,即可启动测试其代码所需的所有容器。

不过,要让docker-compose的设置能够达到最佳性能状态,例如:在不到一分钟之内启动开发环境,并且在几秒钟内完成对每个更改的测试,这些都需要团队花费大量的工作。在这些准备过程中,由于各个开发人员每天花费在测试其代码上的时间各不相同,而且任何细微的改动,都可能会对整个开发团队的生产力产生巨大的影响。因此,我们有必要在此讨论他们在编写Docker Compose时常见的五大错误,及其对应的解决方法。

错误1:频繁地进行容器重建

Docker的构建往往比较耗时,特别是每次针对代码的变更开展测试的时候。如果能够节省此方面的时间,那么对于加快开发周期来说是十分有益的。过去,对于非容器化的应用,我们通常会采取如下传统的工作流程:

  • 编写代码
  • 构建
  • 运行

多年来,业界持续优化该流程,并提出了诸如:针对编译语言的增量构建和热重载(hot reloading)等实用技巧。随着容器技术的出现,我们在现有的工作流程中增加了docker构建的步骤,如下图所示。

  • 编写代码
  • 构建
  • Docker构建
  • 运行

当然,如果构建得不好,那么docker构建步骤也可能会带来额外的时间开销。例如:使用apt-get进行依赖项的重载步骤。有时候,这些步骤可能会让整个测试过程比添加Docker之前还要慢。

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

第一种解决方法是在Docker Compose中启动所有的依赖项,然后在本地运行测试代码。此举模仿了非容器化应用开发的工作流程。

您只需向localhost公布依赖关系,然后将正在使用的服务指向所有的localhost:地址即可。但是,该方法并非永远可行,如果您正在使用的是代码依赖容器镜像中的内置元素时,那么用户电脑就不一定能够访问到具体内容。

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

如果必须构建Docker镜像,那么我们可以编写Dockerfile,通过最大化缓存,将Docker的构建时间从原来的10分钟压缩至1分钟。

在生产环境中,Dockerfile的典型模式是通过将单个命令链接到一条RUN语句中,来减少层级的数量。毕竟,在开发过程中镜像的大小并不重要,重要的是层级的数量。下面展示的是在生产环境中的一个Dockerfile文件:

  1. RUN \ 
  2. go get -d -v \ 
  3. && go install -v \ 
  4. && go build 

不过,该命令在每次被重新运行时,Docker都会重新下载所有的依赖项,并重新安装它们。我们可以通过增量构建(incremental build)来提供效率。

同时,您可以将开发专用的Dockerfile其分成几个短小的步骤,从而使得那些经常更改的代码步骤被排到最后,而将鲜少更改的步骤(例如拉式依赖关系)被放在首位。因此,在重建Dockerfile时,您不必构建整个项目,而只需构建那些被已更改的少量末尾块即可。有关此方面的案例,您可以参阅以下用于Blimp(请参见--https://kelda.io/blimp)开发的Dockerfile。通过遵循上述方法,您可以将繁琐的构建过程缩减到了几秒钟之内完成。

  1. FROM golang:1.13-alpine as builder 
  2. RUN apk add busybox-static 
  3. WORKDIR /go/src/github.com/kelda-inc/blimp 
  4. ADD ./go.mod ./go.mod 
  5. ADD ./go.sum ./go.sum 
  6. ADD ./pkg ./pkg 
  7. ARG COMPILE_FLAGS 
  8. RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./pkg/... 
  9. ADD ./login-proxy ./login-proxy 
  10. RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./login-proxy/... 
  11.        ADD ./registry ./registry 
  12. RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./registry/... 
  13. ADD ./sandbox ./sandbox 
  14. RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./sandbox/... 
  15. ADD ./cluster-controller ./cluster-controller 
  16. RUN CGO_ENABLED=0 go install -i -ldflags "${COMPILE_FLAGS}" ./cluster-controller/... 
  17. RUN mkdir /gobin 
  18. RUN cp /go/bin/cluster-controller /gobin/blimp-cluster-controller 
  19. RUN cp /go/bin/syncthing /gobin/blimp-syncthing 
  20. RUN cp /go/bin/init /gobin/blimp-init 
  21. RUN cp /go/bin/sbctl /gobin/blimp-sbctl 
  22. RUN cp /go/bin/registry /gobin/blimp-auth 
  23. RUN cp /go/bin/vcp /gobin/blimp-vcp 
  24. RUN cp /go/bin/login-proxy /gobin/login-proxy 
  25. FROM alpine 
  26.  
  27. COPY --from=builder /bin/busybox.static /bin/busybox.static 
  28. COPY --from=builder /gobin/* /bin/ 

最后值得一提的是:随着多阶段构建(multi-stage builds,请参见--https://docs.docker.com/develop/develop-images/multistmuage-build/)的引入,我们如今可以创建各种具有良好分层和较小镜像的Dockerfile。不过,我们在此并不会展开详细的讨论。

解决方案:使用主机卷(host volumes)

大多数语言都会提供一种方法来监视程序代码,并在代码发生更改时自动重新运行。例如,nodemon就是JavaScript语言的一种Node自动重启工具(请参见--https://www.npmjs.com/package/nodemon)。由于主机卷可以将您电脑上的目录,镜像到正在运行的容器之中,因此您在使用文本编辑器来编辑文件时,各种更改将会被自动同步到容器中,并在容器内被立即执行。

最初,您可能需要花点时间进行前期准备,之后在Docker中,您可以在1-2秒内马上看到代码的更改结果。因此,我们会选择使用主机卷将代码直接挂载到容器中,以便以原生的方式,在包含其了运行时依赖项的Docker容器中运行自己的代码。

错误2:缓慢的主机卷

如果您使用过主机卷,那么是否已经注意到:在Windows和Mac上读写文件的速度可能会非常缓慢?其实,对于诸如Node.js和具有复杂依赖性的PHP应用程序之类,需要读写大量文件的命令而言,这是一个已知的问题。其背后的原因是:Docker主要运行在Windows和Mac上的VM中。而我们在进行主机卷的挂载时,它必须经过大量的转换,才能使文件夹进入容器,这有点类似于网络文件系统。而此类额外的开销,在Linux本地运行Docker时,则不会出现。

解决方案:放宽强一致性

该问题的一个关键原因是:文件系统在默认挂载时,需要保持强一致性。也就是说:所有特定文件的读写进程都必须统一对于文件修改的顺序,以便让文件的内容达成最终的一致。可是,强一致性的代价非常昂贵,它需要所有文件的写入进程之间持续保持协调,以确保它们不会干扰或破坏彼此的更改。

虽然在生产环境中的数据库需要保持强一致性。但是在开发过程中,由于写入进程就是代码文件本身,目标就是我们的存储库,因此强一致性就不那么必需了。那么,我们就可以考虑Docker在挂载卷时,放宽强一致性。例如:在Docker Compose中,我们可以简单地将此cached关键字添加到卷挂载中,以获得显著的性能保证。对应的代码如下:

  1. volumes: 
  2. "./app:/usr/src/app/app:cached" 

注意:此举仅适合开发环境,不适合生产环境。

解决方案:代码同步

另一种处置方法是设置代码的同步。您可以使用工具侦测主机和容器之间的变化,通过复制文件来解决差异(类似于rsync),而不是挂载卷。Docker在最新的版本中内置了用来替代卷的缓存模式--Mutagen(请参见--https://mutagen.io/)。此外,上文提到的Blimp则使用Syncthing(请参见--https://http//syncthing.net/)实现了类似的功能。

解决方案:不要挂载软件包

Node之类的语言通常会把大部分文件操作放在packages目录中(如node_modules)。那么,我们可以试着从卷中去除此类目录,以显著提高性能。下列示例是一个将代码挂载到容器中的专属卷,它覆盖了node_modules目录。

  1. volumes: 
  2.    - ".:/usr/src/app"   
  3.    - "/usr/src/app/node_modules" 

该挂载操作会告诉Docker去使用node_modules目录下的标准卷,以使得在npm install运行时,不再使用慢速的主机挂载方式。为了使该工作能够正常进行,我们应该在容器首次启动时,在entrypoint中执行npm install,以安装依赖项,并更新node_modules目录。具体代码如下:

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

如果您想查看并运行上述完整的示例,请参考--https://kelda.io/blimp/docs/examples/#nodejs。

错误3:脆弱的配置

如果您曾深入研究过代码,您可能会发现Docker Compose中也充斥着各种大量复制和粘贴而来的代码。显然,我们需要干净整洁的Docker Compose文件,以方便轻松地按需做出修改。

解决方案:使用各种env文件

Env文件能够将环境变量与Docker Compose主配置分开,以实现:

  • 避免将代码泄露到git的历史记录中。
  • 开发人员都能按需自定义设置。例如,每个开发人员都可以持有一个唯一的访问密钥。他们通过将配置保存在.env文件中,以实现不必修改已提交的docker-compose.yml文件,也不必在文件更新时处理各种冲突问题。

如果您想使用环境文件,只需添加一个.env文件,或设置带有env_file字段的显式路径即可(请参见--https://docs.docker.com/compose/environment-variables/#the-env_file-configuration-option)。

解决方案:使用替代文件

替换文件(请参见--https://docs.docker.com/compose/extends/)可以方便您在具有基本配置的基础上,在其他文件中指定各项修改。该功能非常适合Docker Swarm及其YAML文件。您可以将生产环境的配置存储在docker-compose.yml中,然后在替代文件中,指定开发所需的任何修改(例如:使用主机卷)。

解决方案:使用extends

如果您使用的是Docker Compose v2,那么就可以使用extends关键字,在多个位置导入YAML片段。例如,您可能会定义:公司里所有的服务都需要在开发的Docker Compose文件中带有某五个特定的配置。然后您可以使用extends关键字将其放置到任何需要的地方,以实现模块化。当然,如果仅在YAML中执行此项操作可能比较繁琐,我们完全可以通过编程来实现。

虽然Compose v3删除了对于extends关键字的支持。但是,您仍然可以使用YAML anchors(请参见--https://support.atlassian.com/bitbucket-cloud/docs/yaml-anchors/)来实现类似的结果。

错误4:乱序启动(Flaky Boots)

如果docker-compose出现了崩溃,我们能够仅使用docker-compose restart来重启服务吗?其实此类问题主要与服务错误的启动顺序有关。例如,您的Web应用可能依赖于数据库,那么在Web应用启动时,如果数据库尚未准备就绪,就会出现崩溃。

解决方案:使用depends_on

depends_on使您可以控制启动的顺序。默认情况下,depends_on仅判断依赖项是否已经创建,而不会判断依赖项是否“健康”。虽然Docker Compose v2能够支持将depends_on与运行状况的检查相结合。不过,该功能也在Docker Compose v3中被去除了。当然,您可以使用诸如wait-for-it.sh之类的脚本,来手动实现类似的功能。

和上面提到的放宽强一致性相同,虽然Docker文档不建议在生产环境中使用depends_on和wait-for-it.sh,来为容器指定特定的启动顺序。但是对于开发而言,我们完全可以用到depends_on。

错误5:资源管理不善

如果您碰到开发流程受阻,Docker无法全速运行,或是无法平稳地获取运行所需的资源,那么您可以考虑以下几个方面:

解决方案:更改Docker Desktop的分配

Docker Desktop需要大量的RAM和CPU,尤其是在Mac和Windows的VM上。Docker Desktop的默认配置往往不会分配足够的RAM和CPU,因此我们通常需要调整相关的设置。在开发时,我经验是:为Docker分配大约8GB的RAM和4个CPU,并且在不使用Docker Desktop时,及时关闭之。

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

人们在使用Docker时经常会出现数百个卷与旧的容器镜像。这在无形中浪费了各种资源。为了释放这些资源,我们建议通过间或运行docker system prune的方式,以删除当前未使用到的所有卷、容器和网络。

总结

总的说来,为了改善开发人员在使用Docker Compose时的体验,我建议您做到如下五点:

  • 最小化容器的重建。
  • 使用主机卷。
  • 像对待代码那样,认真配置文件,以便于维护。
  • 让启动更加可靠。
  • 认真分配管理资源。

此外,您还可以通过链接--https://kelda.io/blog/docker-volumes-for-development/,以获悉如何设置主机卷,并加快Docker开发。

原标题:5 Common Mistakes When Writing Docker Compose ,作者: Ethan J Jackson

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

【编辑推荐】

  1. 如何在Linux中以非root用户运行Docker?
  2. 用于监视 Kubernetes 和 Docker 的六大开源工具
  3. 什么,容器太多操作不过来?我选择Docker Compose梭哈
  4. 推荐3款超好用的Docker图形化管理工具
  5. 在Fedora 32上运行Docker
【责任编辑:庞桂玉 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

大数据安全运维实战

大数据安全运维实战

CDH+Ambari
共20章 | 大数据陈浩

91人订阅学习

实操案例:Jenkins持续交付和持续部署

实操案例:Jenkins持续交付和持续部署

微服务架构下的自动化部署
共18章 | freshman411

179人订阅学习

思科交换网络安全指南

思科交换网络安全指南

安全才能无忧
共5章 | 思科小牛

109人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微