暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

构建完美的 Python Dockerfile

云原生CTO 2021-08-18
960

点击上方 云原生CTO,选择 设为星标

               优质文章,每日送达


「【只做懂你de云原生干货知识共享】」

构建完美的 Python Dockerfile

本博客目的将在不更改项目源代码的情况下提高您的 Python 代码性能和安全性。

目录

「介绍」

「动机」

「基准」

「进一步优化」

「完美的 Python Dockerfile」

介绍

拥有一个可靠的 Dockerfile 作为您的基础可以为您节省数小时的头痛和更大的问题。

这篇文章将分享“完美”的 Python Dockerfile。当然,世上没有完美的东西,我很乐意接受反馈以改进您可能发现的问题。

跳到最后找到一个比在 docker hub 中使用默认文件快 20% 的 Dockerfile。它还包含对 gunicorn 的特殊优化,以更快更安全地构建。

动机

由于系统必须非常经济高效,我想确保底层 docker 镜像不会产生过多的开销。

经过一番研究,我偶然发现了这个StackOverflow 问题,该问题质疑 FFmpeg 执行性能和我在使用 Alpine 时的 Python 代码。

这里的issue讨论了为什么 Alpine Docker 镜像比 Ubuntu 镜像慢 50% 以上?

「https://superuser.com/questions/1219609/why-is-the-alpine-docker-image-over-50-slower-than-the-ubuntu-image」

事实证明,Alpine 可能很小,但在某些情况下,由于使用了一些等效的库,它可能会减慢速度。

我很沮丧。在我的 Go 项目中工作得很好之后,我默认将它与 Python 一起使用来获取较小的镜像。

这让我从头开始,对一系列发行版进行基准测试,并创建我的新默认 docker 镜像。

必要的安装工具:

「python:3.9-alpine3.13(基线)」

「python:3.9」

「python:3.9-slim」

「python:3.9-buster」

「python:3.9-slim-buster」

「Ubuntu 20.04 (LTS)」

为了进行基准测试,我没有重新发明轮子,而是使用pyperformance。

「https://pyperformance.readthedocs.io/」

pyperformance 项目旨在成为所有 Python 实现基准的权威来源。重点是真实世界的基准测试,而不是综合基准测试,尽可能使用整个应用程序。

比较表
             基准比较

事实证明,与 Python 存储库中的其他镜像相比,Alpine 在大多数测试中并没有那么慢。

最大的惊喜实际上是使用 Ubuntu 和手动安装 python 以超过 20% 的使用率成为明显的赢家。

这让我相信最大的因素不是操作系统,而是 python 是如何编译的。经过一些研究,我发现这个问题似乎验证了这个推理。

进一步优化

更好的执行速度很棒,但这只是您在部署应用程序时应该担心的一个变量。

您可以在最后看到完整的 Dockerfile,以下示例仅用作说明。

缓存

docker 中的缓存是按层进行的。每个“RUN”都会创建一个可以缓存的层。

它将检查您的本地系统是否有以前的构建,并使用每个未触及的层作为缓存。

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3.9 python3.9-dev
COPY . .
RUN pip install -r requirements.txt
CMD ["python]

在此示例中,第一次运行它时,它将从头开始运行每个命令。

对于第二次运行,它将自动跳过所有步骤。当您更改代码时会发生什么?到目前为止,它将使用缓存的图层:

RUN pip install -r requirements.txt

然后它会再次安装您的所有要求,即使您没有更改它们。

由于安装需求通常占用我们构建时间的最大部分,这是我们想要避免的。

我们可以做的一个非常简单的更改是仅复制我们的需求文件并在复制代码之前安装它们:

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3.9 python3.9-dev
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python]

现在,即使您更改代码,只要您保持 requirements.txt 不变,它就会始终使用缓存(如果可用)。

缓存和BuildKit

BuildKit 是新的 Docker 镜像构建器,它带来了许多改进,但我们将主要关注允许更好的缓存。

「https://docs.docker.com/develop/develop-images/build_enhancements/」

  • 使用远程存储库作为缓存。

使用 BuildKit,除了本地构建缓存之外,构建器还可以重用从先前构建生成的缓存,--cache-from标志指向注册表中的镜像。

要将镜像用作缓存源,需要在创建时将缓存元数据写入镜像。这可以通过「--build-arg BUILDKIT_INLINE_CACHE=1」在构建镜像时设置来完成。之后,构建的镜像可以用作后续构建的缓存源。

导入缓存后,构建器只会从注册表中提取 JSON 元数据,并根据该信息确定可能的缓存命中。如果存在缓存命中,则匹配的图层会被拉入本地环境。

命令示例:

docker build -t app --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from registry-url/repo

使用远程镜像作为缓存对于您的 CI 构建特别有用,其中缓存文件夹可能不可用,并且您将为每个管道进行冷构建。

缓存pip包
# syntax=docker/dockerfile:1.2
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y python3.9 python3.9-dev
COPY requirements.txt .
RUN --mount=type=cache,mode=0755,target=/root/.cache pip install -r requirements.txt
COPY . .
CMD ["python"]

有了这个,您可以告诉 docker 缓存 pip 使用的 root/.cache 文件夹。我发现它对我的本地Dockerfile很有用,在那里测试不同的包更常见。

注意第一行「syntax=docker/dockerfile:1.2。」 --mount否则,该命令将引发错误。

根用户

除非你真的需要在你的容器中使用根用户运行,否则你应该避免以 root 身份运行。如果您遵循最小权限 (PoLP) 原则,它将使您的生产环境更加安全。

可参考polp的原则「https://en.wikipedia.org/wiki/Principle_of_least_privilege」

在大多数情况下,当您以 root 身份运行时,您唯一要做的就是让攻击者的生活更容易利用可能的安全漏洞,甚至可以让他控制主机。

FROM ubuntu:20.04
RUN useradd --create-home myuser
USER myuser
CMD ["bash"]

虚拟环境

在 docker 中使用虚拟环境可能有点争议,但我发现它至少有以下优点:

「您可以与操作系统默认的 python 安装隔离」

「易于在多阶段构建之间复制包文件夹」

「您可以使用 python 代替 python3 或 python3.9 命令(是的,还有其他方法)」

「您可以使用单个 Dockerfile 来运行测试和部署。在基础镜像的不同“文件夹”中安装您的测试和生产需求,然后复制到“测试阶段”和“生产阶段”」

FROM ubuntu:20.04
# create and activate virtual environment
RUN python3.9 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
CMD ["python"]

在最终的 dockerfile 中,我们将看到我们只需要复制 的内容/opt/venv/即可在多阶段构建中获取所有已安装的包。

多级

使用 docker,每一层都是不可变的。这意味着即使您删除了安装在前一层中的某些内容,总镜像大小也不会减小。

避免镜像臃肿的推荐方法是使用多阶段构建。即使你只是使用 docker 进行本地开发,节省空间总是一个加分项!

但是当我们谈论生产时,我们在存储空间、带宽和下载时间方面付出了代价,节省的每一 MB 都很重要!

# using ubuntu LTS version
FROM ubuntu:20.04 AS builder-image

RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3.9-dev python3.9-venv python3-pip python3-wheel build-essential && \
   apt-get clean && rm -rf /var/lib/apt/lists/*

# create and activate virtual environment
RUN python3.9 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# install requirements
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

FROM ubuntu:20.04 AS runner-image
RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3-venv && \
   apt-get clean && rm -rf /var/lib/apt/lists/*

COPY --from=builder-image /opt/venv /opt/venv

# activate virtual environment
ENV VIRTUAL_ENV=/opt/venv
ENV PATH="/opt/venv/bin:$PATH"

CMD ["python"]

我们在这里所做的是使用第一阶段作为我们的“构建器”,我们在其中安装 gcc 编译器等工具,然后我们只需将所需文件从构建器镜像复制到运行器镜像中。

如果您正在为 Flask、Django 或任何其他 wsgi 应用程序提供服务

这可能是一个切线,但由于它是 python 生态系统的一个非常大的部分,我决定也包括 gunicorn 优化。

如果你正在部署 Flask 或 Django,你应该总是使用像 gunicorn 这样的东西,而不是单独运行它们。它将在性能上产生巨大的差异。要了解更多关于 gunicorn 的信息,您可以阅读这篇文章。

「https://luis-sena.medium.com/gunicorn-worker-types-youre-probably-using-them-wrong-381239e13594」

gunicorn 使用文件心跳系统来跟踪分叉进程。将它放在磁盘上可能会出现问题,您甚至可以在他们的文档中阅读此警告:

当前的心跳系统涉及在临时文件处理程序上调用 os.fchmod,如果目录位于磁盘支持的文件系统上,则可能会在任意时间内阻止你的执行动作。

默认情况下,它将使用/tmp通常是内存中安装的文件夹。这不是 docker 的情况,如果您已经使用 docker 运行 gunicorn 应注意到一些随机冻结的问题

在我看来,最干净的解决方案是简单地将心跳目录更改为 docker 容器内的内存映射目录,在本例中为/dev/shm.

CMD ["gunicorn","-b""0.0.0.0:5000""-w""4""-k""gevent""--worker-tmp-dir""/dev /shm""app:app"]

在上面的例子中,你可以看到如何使用「gunicorn--worker-tmp-dir」参数/dev/shm作为心跳目录。

完美的 Python Dockerfile

事不宜迟,让我们看看最终的文件。

# using ubuntu LTS version
FROM ubuntu:20.04 AS builder-image

# avoid stuck build due to user prompt
ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3.9-dev python3.9-venv python3-pip python3-wheel build-essential && \
 apt-get clean && rm -rf /var/lib/apt/lists/*

# create and activate virtual environment
# using final folder name to avoid path issues with packages
RUN python3.9 -m venv /home/myuser/venv
ENV PATH="/home/myuser/venv/bin:$PATH"

# install requirements
COPY requirements.txt .
RUN pip3 install --no-cache-dir wheel
RUN pip3 install --no-cache-dir -r requirements.txt

FROM ubuntu:20.04 AS runner-image
RUN apt-get update && apt-get install --no-install-recommends -y python3.9 python3-venv && \
 apt-get clean && rm -rf /var/lib/apt/lists/*

RUN useradd --create-home myuser
COPY --from=builder-image /home/myuser/venv /home/myuser/venv

USER myuser
RUN mkdir /home/myuser/code
WORKDIR /home/myuser/code
COPY . .

EXPOSE 5000

# make sure all messages always reach console
ENV PYTHONUNBUFFERED=1

# activate virtual environment
ENV VIRTUAL_ENV=/home/myuser/venv
ENV PATH="/home/myuser/venv/bin:$PATH"

# /dev/shm is mapped to shared memory and should be used for gunicorn heartbeat
# this will improve performance and avoid random freezes
CMD ["gunicorn","-b""0.0.0.0:5000""-w""4""-k""gevent""--worker-tmp-dir""/dev/shm""app:app"]

正如我在开头所说的那样,我将使用新发现和分享后可能得到的反馈来更新此文件。

后面可根据此链接来拿到最新的Dockerfile「https://gist.github.com/lsena/42e091bfbf2bcea26a5561794f54f9bc#file-dockerfile」

总结

「虽然.dockerignore不是 Dockerfile 的一部分,但我认为我应该强调使用它的必要性。」

「就像.gitignore它用作您要忽略的文件和文件夹列表一样。在这种情况下,这意味着将它们从您的 docker 构建上下文中排除。」

「这会导致更快的构建、更小的镜像以及更高的安全性和可预测性(您可以排除 python 缓存、secret等)。」

「在某些情况下,仅通过排除.git文件夹就可以节省数百 MB 。」

#example of ignoring .git and python cache folder
.git
__pycache__

参考:

「https://github.com/docker-library/python/issues/501 https://engineering.bitnami.com/articles/why-non-root-containers-are-important-for-security.html https://stackoverflow.com/questions/61459775/docker-buildkit-mount-type-cache-not-working-why https://pythonspeed.com/articles/docker-cache-pip-downloads/ https://en.wikipedia.org/wiki/Principle_of_least_privilege https://news.ycombinator.com/item?id=22182226 https://docs.docker.com/develop/develop-images/build_enhancements/ https://docs.gunicorn.org/en/stable/settings.html https://hynek.me/articles/virtualenv-lives/ https://github.com/themattrix/python-pypi-template/blob/master/.dockerignore」


文章转载自云原生CTO,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论