在构建容器化的应用时,开发人员往往需要某种方法来引导启动目标容器,以对其进行代码级别的测试。尽管业界有许多方法可以实现该目的,但Docker Compose是目前最受欢迎的一种方法。它能够让如下两个方面变得容易实现:
- 指定在开发过程中需要启动的容器。
- 设置一套快速的代码测试调试(code-test-debug),以方便开发循环。
错误1:频繁地进行容器重建
Docker的构建往往比较耗时,特别是每次针对代码的变更开展测试的时候。如果能够节省此方面的时间,那么对于加快开发周期来说是十分有益的。过去,对于非容器化的应用,我们通常会采取如下传统的工作流程:- 编写代码
- 构建
- 运行
- 编写代码
- 构建
- Docker构建
- 运行
RUN \
go get -d -v \
&& go install -v \
&& go build
不过,该命令在每次被重新运行时,Docker都会重新下载所有的依赖项,并重新安装它们。我们可以通过增量构建(incremental build)来提供效率。同时,您可以将开发专用的Dockerfile其分成几个短小的步骤,从而使得那些经常更改的代码步骤被排到最后,而将鲜少更改的步骤(例如拉式依赖关系)被放在首位。因此,在重建Dockerfile时,您不必构建整个项目,而只需构建那些被已更改的少量末尾块即可。有关此方面的案例,您可以参阅以下用于Blimp(请参见--https://kelda.io/blimp)开发的Dockerfile。通过遵循上述方法,您可以将繁琐的构建过程缩减到了几秒钟之内完成。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/
最后值得一提的是:随着多阶段构建(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关键字添加到卷挂载中,以获得显著的性能保证。对应的代码如下:volumes:
- "./app:/usr/src/app/app:cached"
注意:此举仅适合开发环境,不适合生产环境。解决方案:代码同步另一种处置方法是设置代码的同步。您可以使用工具侦测主机和容器之间的变化,通过复制文件来解决差异(类似于rsync),而不是挂载卷。Docker在最新的版本中内置了用来替代卷的缓存模式--Mutagen(请参见--https://mutagen.io/)。此外,上文提到的Blimp则使用Syncthing(请参见--https://http//syncthing.net/)实现了类似的功能。解决方案:不要挂载软件包Node之类的语言通常会把大部分文件操作放在packages目录中(如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://kelda.io/blimp/docs/examples/#nodejs。错误3:脆弱的配置
如果您曾深入研究过代码,您可能会发现Docker Compose中也充斥着各种大量复制和粘贴而来的代码。显然,我们需要干净整洁的Docker Compose文件,以方便轻松地按需做出修改。解决方案:使用各种env文件Env文件能够将环境变量与Docker Compose主配置分开,以实现:- 避免将代码泄露到git的历史记录中。
- 开发人员都能按需自定义设置。例如,每个开发人员都可以持有一个唯一的访问密钥。他们通过将配置保存在.env文件中,以实现不必修改已提交的docker-compose.yml文件,也不必在文件更新时处理各种冲突问题。
解决方案:使用替代文件
替换文件(请参见--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_ondepends_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时的体验,我建议您做到如下五点:- 最小化容器的重建。
- 使用主机卷。
- 像对待代码那样,认真配置文件,以便于维护。
- 让启动更加可靠。
- 认真分配管理资源。
Java帮帮
非盈利学习社区
官网:www.javahelp.com.cn
职涯宝
帮助职业者成功
分享优质内容
官网:zhiya360.com
九点编程
深夜学习,未来可期