如何更好地优化容器的创建?这些技巧你务必收藏
在你获得正确的容器配置时需要进行几次迭代?你每次迭代需要多长时间?好吧,如果回答“太多次且时间太长”,那么我的经历与你很相似。从表面上看,创建配置文件似乎很简单:在配置文件中实现与手动安装系统时要执行的步骤相同。不幸的是,我发现这种方法通常无法正常工作,并且一些“技巧”对于此类DevOps练习非常有用。
在本文中,我将分享一些新发现的技术。这些技术可以帮助你最大程度地减少迭代次数和迭代时间。另外,我将概述一些标准做法以外的调优做法。
节省时间在容器映像构建迭代上
如果Dockerfile涉及下载并安装5GB的文件,则即使具有良好的网络速度,每次Docker 映像构建迭代都可能会花费大量时间。忘记包含要安装的项目可能意味着在此之后重建所有层。
解决这一难题的一种方法是使用本地HTTP服务器,以避免在Docker映像构建迭代期间多次从互联网下载大文件。为了举例说明,假设您需要在Ubuntu 18.04下使用Anaconda 3创建一个容器映像。Anaconda 3安装程序的文件大小约为0.5GB,因此在此示例中称为“大”文件。
请注意,如果不想使用COPY指令,因为它会创建一个新层。使用大型安装程序后,还应删除它,以最小化容器映像的大小。您可以使用多阶段构建,但是我发现以下方法足够有效。
其基本思想是使用基于Python的HTTP服务器在本地,以服务大文件(S),并有Dockerfile wget的,从这个本地服务器的大文件(S)。让我们探索如何有效设置它的细节。
在此示例存储库中,文件夹tutorial2_docker_tricks /的必要内容是:
tutorial2_docker_tricks/├── build_docker_image.sh# builds the docker image├── run_container.sh# instantiates a container from the image├── install_anaconda.dockerfile# Dockerfile for creating our target docker image├── .dockerignore# used to ignore contents of the installer/ folder from the docker context├── installer# folder with all our large files required for creating the docker image│ └── Anaconda3-2019.10-Linux-x86_64.sh# from https://repo.anaconda.com/archive/Anaconda3-2019.10-Linux-x86_64.sh└── workdir# example folder used as a volume in the running container
该方法的关键步骤是:
步骤1:将大文件放在安装程序/文件夹中。在此示例中,我具有大型Anaconda安装程序文件Anaconda3-2019.10-Linux-x86_64.sh。如果克隆我的Git存储库,则找不到该文件,因为只有您(作为容器映像创建者)需要此源文件。图像的最终用户没有。下载安装程序以跟随示例。
步骤2:创建.dockerignore文件,并使其忽略installer /文件夹,以避免Docker将所有大文件复制到构建上下文中。
步骤3:在终端中,cd进入tutorial2_docker_tricks /文件夹,并以./build_docker_image.sh执行构建脚本。
步骤4:在build_docker_image.sh中,启动Python HTTP服务器以提供来自installer /文件夹的任何文件:
cdinstallerpython3-mhttp.server--bind10.0.2.158888 &cd..
步骤5:如果您想知道奇怪的Internet协议(IP)地址,我正在使用VirtualBox Linux VM,当我运行ifconfig时,10.0.2.15显示为以太网适配器的地址。该IP似乎是VirtualBox使用的约定。如果设置不同,则需要更新此IP地址以匹配您的环境,然后更新build_docker_image.sh和install_anaconda.dockerfile。在此示例中,服务器的端口号设置为8888。请注意,IP和端口号可以作为构建参数传递,但是为了简洁起见,我对其进行了硬编码。
步骤6:由于HTTP服务器设置为在后台运行,请 使用我发现的一种简洁的方法使用kill -9命令在脚本末尾附近停止服务器:
kill-9`ps -ef | grep http.server | grep8888| awk'{print$2}'
步骤7:请注意, 在脚本的前面(启动HTTP服务器之前)也使用了同样的kill -9。通常,当我迭代可能会故意中断的任何构建脚本时,这可以确保每次HTTP服务器都干净启动。
步骤8:在Dockerfile中,有一条RUN wget 指令,该指令从本地HTTP服务器下载Anaconda安装程序。它还会删除安装程序文件并在安装后进行清理。最重要的是,所有这些操作都在同一层中执行,以将图像大小保持为最小:
# install Anaconda by downloading the installer via the local http serverARGANACONDARUNwget --no-proxy http://10.0.2.15:8888/${ANACONDA} -O ~/anaconda.sh \&&/bin/bash ~/anaconda.sh -b -p /opt/conda \&&rm ~/anaconda.sh \&&rm -fr /var/lib/apt/lists/{apt,dpkg,cache,log} /tmp/* /var/tmp/*
步骤9:该文件运行包装程序脚本anaconda.sh,并通过使用rm删除大型文件来清理它们
步骤10:构建完成后,您应该看到图像anaconda_ubuntu1804:v1。(您可以使用docker image ls列出图像。)
步骤11:您可以使用终端中的./run_container.sh从该映像实例化一个容器,而该文件夹位于tutorial2_docker_tricks /文件夹中。您可以验证Anaconda已安装:
$./run_container.sh$python --versionPython 3.7.5$conda --versionconda 4.8.0$anaconda --versionanaconda Command line client (version 1.7.2)
步骤12:您会注意到,run_container.sh设置了一个卷workdir。在此示例存储库中,文件夹workdir /为空。这是我用来设置卷的约定,可以在其中使我的Python和其他脚本独立于容器映像。
最小化容器图像大小
每个RUN命令等效于执行一个新的Shell,每个RUN命令创建一个层。用单独的RUN命令模仿安装指令的幼稚方法最终可能会在一个或多个相互依赖的步骤中中断。如果碰巧可以正常工作,通常会产生较大的图像。在一个RUN命令中链接多个安装步骤,包括autoremove,autoclean和rm命令(如下面的示例所示)对于最小化每个图层的大小很有用。根据所安装的内容,可能不需要其中一些步骤。但是,由于这些步骤花费的时间不多,因此,在调用apt-get的RUN命令结束时,我总是将它们投入适当的时间:
RUN apt-getupdate\ && DEBIAN_FRONTEND=noninteractive \apt-get-y--quiet --no-install-recommends install \# list of packages being installed go here \&& apt-get-y autoremove \&& apt-getclean autoclean \&& rm -fr /var/lib/apt/lists/{apt,dpkg,cache,log} /tmp/* /var/tmp/*
另外,请确保您有一个.dockerignore文件,以忽略不需要发送到Docker构建上下文的项目(例如前面示例中的Anaconda安装程序文件)。
组织构建工具I/O
对于软件构建系统,构建输入和输出(配置和调用工具的所有脚本)应在映像和最终运行的容器之外。容器本身应保持无状态,以便不同的用户可以得到相同的结果。在上一篇文章中,我对此进行了广泛的介绍,但由于它对我的工作非常有用,因此我想强调一下。最好通过设置容器体积来访问这些输入和输出。
我不得不使用一个容器映像,该映像以源代码和大型预构建二进制文件的形式提供数据。作为软件开发人员,我希望在容器中编辑代码。这是有问题的,因为容器默认情况下是无状态的:它们不被保存在容器内,因为它们被设计为可抛弃的。
但是我一直在努力,在每天结束时,我停止了容器,并且必须小心不要将其卸下,因为必须保持状态,以便我第二天才能继续工作。这种方法的缺点是,如果有不止一个人在项目上工作,那么开发状态就会有分歧。这种方法在开发人员中拥有相同的构建系统的价值在某种程度上已经丧失了。
以非root用户身份生成输出
I / O的重要方面涉及在容器中运行工具时生成的输出文件的所有权。默认情况下,由于Docker以root身份运行,因此输出文件将归root拥有,这是不愉快的。您通常希望以非root用户身份工作。生成构建输出后更改所有权可以使用脚本来完成,但这是一个额外且不必要的步骤。
最好尽早在Dockerfile中设置USER参数:
ARGUSERNAME# other commands...USER${USERNAME}
该USERNAME可以作为构建参数(在传递--build精氨酸)执行时搬运工图像构建。您可以在示例Dockerfile和相应的构建脚本中看到一个示例。
工具的某些部分可能还需要以非root用户身份安装。因此,如果要直接在Linux下手动安装,则Dockerfile中的安装顺序可能需要与安装顺序不同。
非交互式安装
交互性与容器自动化相反。我发现了
DEBIAN_FRONTEND=noninteractive apt-get -y --quiet --no-install-recommends
防止安装程序打开对话框所必需的apt-get安装说明选项(如上例所示)。注意,这些选项应作为RUN指令的一部分使用。正如本常见问题解答所解释的那样,不应在Dockerfile中将DEBIAN_FRONTEND = noninteractive设置为环境变量(ENV),因为它将由容器继承。
记录您的构建并运行输出
调试构建失败的原因是一项常见的任务,而日志则是完成此任务的好方法。使用Bash脚本中的tee工具保存在容器映像构建或容器运行会话期间发生的所有事情的TypeScript 。换句话说,将|&tee $ BASH_SOURCE.log添加到Docker 映像构建的末尾,并且在脚本中运行docker image运行命令。请参阅映像构建和容器运行脚本中的示例。
这种发球技术的作用是生成一个与Bash脚本同名的文件,但 附加一个.log扩展名,以便您知道其起源于哪个脚本。运行脚本时,您在终端上看到的所有内容都将以类似的名称记录到该文件中。
这对于容器映像的用户在无法解决问题时向您报告问题特别有价值。您可以要求他们向您发送日志文件以帮助诊断问题。许多工具生成的输出太多,以至于很容易使终端缓冲区的默认大小不堪重负。仅依靠终端的缓冲区容量来复制粘贴错误消息可能不足以诊断问题,因为以前的错误可能已经丢失。
我发现即使在容器映像构建脚本中,这也很有用,尤其是在使用上面讨论的基于Python的HTTP服务器时。服务器在下载过程中生成了很多行,通常使终端的缓冲区不堪重负。
优雅地处理代理
在我的工作环境中,需要代理访问Internet才能下载RUN apt-get和RUN wget命令中的资源。通常从环境变量http_proxy或https_proxy推断代理。虽然可以使用ENV命令在Dockerfile中对此类代理设置进行硬编码,但是直接将ENV用于代理存在多个问题。
如果您是唯一可以构建容器的人,那么也许可以使用。
但是,其他位置不同的代理设置的其他人无法使用Dockerfile。另一个问题是IT部门可能会在某个时候更改代理,从而导致Dockerfile不再起作用。此外,Dockerfile是一个精确的文档,指定了配置控制的系统,并且每一项更改都将通过质量保证进行审查。
一种避免对代理进行硬编码的简单方法是,将本地代理设置作为docker image build 命令中的build参数传递:
docker image build \
--build-arg MY_PROXY=http://my_local_proxy.proxy.com:xx
然后,在Dockerfile中,根据build参数设置环境变量。在此处显示的示例中,您仍然可以设置默认的代理值,该值可以被上面的build参数覆盖:
# set a default proxyARGMY_PROXY=MY_PROXY=http://my_default_proxy.proxy.com:nn/ENV http_proxy=$MY_PROXYENV https_proxy=$MY_PROXY
总结
这些技术帮助我大大减少了创建容器映像并在出现错误时对其进行调试所需的时间。我会继续寻找其他最佳做法,以添加到我的列表中。希望以上技巧对您有所帮助。