Docker 镜像优化

1 镜像瘦身

1.1 选择更精简的基础镜像

常见的 Python 镜像版本:

  • slim:通常只安装运行特定工具所需的最小包
  • Alphine:专门为容器构建的操作系统,比其他的操作系统更小,但是其上会缺少很多软件包并且使用的 glibc 等都是阉割版(不推荐,因为编译过程会很慢)
  • buster/stretch/jessie:表示使用不同版本的 debian 系统(10/9/8)
  • bullseyebookworm:正在开发但尚未稳定版本

考虑使用官方的 python3.11-slim-bullseys 镜像作为基础镜像

  • 优化前基础镜像大小:920MB;优化前最终镜像大小:4.81 GB
  • 优化后基础镜像大小:128MB;优化前最终镜像大小:4.36 GB

注:在apt-get update过程中遇到了GPG error,因此基础镜像从python3.11-slim切换到了python3.11-slim-bullseys。参考自stackoverflow

1.2 去除不必要的缓存/存储

常用方法:

  • pip 安装包时,使用参数--no-cache-dir来去除下载时的缓存数据(已使用)
  • 镜像层(layer)会导致额外的占用,因此应该合并操作,尽量减少层数
  • 设置环境变量PYTHONDONTWRITEBYTECODE=1防止 python 将pyc文件写入硬盘
  • 设置环境变量PYTHONUNBUFFERED=1防止 python 缓冲 stdout 和 stderr(确保实时性)

最终效果:

  1. 合并操作,减少层数(41 -> 21),但是最终镜像的大小没有变化(4.36 GB)
  2. 缓存或不重要文件的清理(之前已用过,所以优化效果一般;最终镜像大小 4.33 GB):
# 清理apt-get的相关缓存
apt-get clean && rm -rf /var/lib/apt/lists/*
# 清理pip下载和安装的相关缓存
rm /root/.cache -rf && rm /usr/local/lib/python3.11/dist-packages/pystan/stan -rf
  1. 两个环境变量的设置,略有改善(最终镜像大小 4.32 GB)

踩坑:尽量避免文件的复制和转移,因为镜像的历史层会记录转移前的文件;除非在同一层内进行文件的创建和删除,否则文件删除后依然会保留是镜像的历史层中

1.3 排除无关的文件

使用 .dockerignore 排除无关文件(用法类似于.gitignore

常见的可排除无关文件

  1. **/__pycache__: python 缓存目录
  2. **/*venv: Python 虚拟环境目录。很多 Python 开发习惯将虚拟环境目录创建在项目下,一般命名为:.venv 或 venv
  3. **/.env: Python 环境变量文件
  4. **/.git **/.gitignore: git 相关目录和文件
  5. **/.vscode: 编辑器、IDE 相关目录
  6. **/charts: Helm Chart 相关文件
  7. **/docker-compose*: docker compose 相关文件
  8. *.db: 如果使用 sqllite 的相关数据库文件
  9. .python-version: pyenv 的 .python-version 文件

因为本次使用的服务镜像不是本地构建的,所以基本不存在以上提到的无关文件;此处的.dockerignore只是为了避免手动构建镜像可能引入的无关文件

最终镜像大小没有变化,还是 4.32 GB

1.4 使用"多阶段构建"压缩镜像体积

前置知识:《Docker 从入门到实践》Dockerfile 多阶段构建

Python 镜像的多阶段构建的标准模板(参考):

# temp stage 
FROM python:3.9-slim as builder 

WORKDIR /app 

ENV PYTHONDONTWRITEBYTECODE 1 
ENV PYTHONUNBUFFERED 1 

COPY requirements.txt . 

RUN apt-get update && apt-get install ...()... && \
    pip wheel --no-cache-dir  --wheel-dir /wheels -r requirements.txt 
    
# final stage 
FROM python:3.9-slim 
WORKDIR /app


# COPY --from=builder /app/wheels /wheels 
COPY --from=builder /app/requirements.txt . 

RUN --mount=type=bind,from=builder,source=/,target=/wheels \
    pip install --no-cache /wheels/* -r requirements.txt
ENTRYPOINT ["...(略)..."]

核心思路是在第一阶段进行 Python 包编译,转为 wheel 文件;在第二阶段用一个干净的镜像,直接安装 wheel 文件;最终镜像去除了编译环境依赖,因此占用空间会小很多

值得一提的是,直接COPY编译后的 wheels 文件依然会额外占用镜像的存储层,因此此处采用了Docker BuildKit的新语法--mount=type=bind来直接挂载上一阶段的镜像

语法--mount=type=bind使用的注意事项:

  • 该语句必须直接跟在RUN后面才能失效,否则会报错(踩坑)
  • 参数from只能指向前阶段镜像或 build 上下文,不能指向主机目录
  • Docker 18.09 版本后才支持BuildKit;在 Docker 23.0 版本后作为默认容器构建方式,在此之前可通过设置环境变量来启用BuildKitDOCKER_BUILDKIT=1 docker build xx
  • 对于较早的 Docker 版本,需要在 Dockerfile 文件的顶部添加声明才能使用该语法: # syntax=docker/dockerfile:experimental

经过好几天的折腾,镜像大小成功从 4.32 GB 涨到了 4.39 GB(后来发现其实是多安装了一个大概有165M的 torchvision 包,所以实际上多阶段构建是有点效果的)

初步分析原因,Python 镜像的编译环境较为简单,因此独立编译过程并不会显著降低 Python 镜像的大小;并且安装 Python 包的缓存已经被清理过了

此外需要注意的是,相比于单阶段,多阶段构建速度会更慢

1.5 第三方软件辅助

  1. 使用Dive工具探索和分析 Docker 镜像内容,进行针对性优化
# 安装docker版本dive
docker pull wagoodman/dive
docker run --rm -it \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -e DOCKER_API_VERSION=1.37 -e CI=true\
    wagoodman/dive:latest <dive arguments...>
# 结果简述(没有太多优化的空间了)
# efficiency: 99.6237 %
# wastedBytes: 27890647 bytes (28 MB)
  1. 使用docker-slim工具针对 Docker 镜像进行自动化瘦身和优化

本次实践的镜像集成了多服务的部署,暂不适合使用 slim build 进行容器瘦身

不过可以考虑使用 xray命令对镜像进行静态分析,包括存储层的文件变动

# 安装
curl -L -o ds.tar.gz https://downloads.dockerslim.com/releases/1.40.6/dist_linux.tar.gz
tar -xvf ds.tar.gz
mv  dist_linux/slim /usr/local/bin/
mv  dist_linux/slim-sensor /usr/local/bin/
# 分析
slim xray <images-id>

1.6 其他瘦身方案

前置知识

基本思路:

  • 针对 Python 镜像,可考虑剔除不必要的第三方库
  • 首先寻找占用空间较大的库,并分析其依赖关系
  • 剔除依赖关系单一并且必要性不强的库
  • 筛选与部署服务无关的(与开发有关)库,支持手动安装

基于以上方法优化后,最终的镜像大小为 3.89 G

2 其他优化

镜像构建速度优化:

  • 不建议使用 Alpine 作为 Python 的基础镜像(编译耗时长)
  • 合理分层。当COPY的文件或RUN命令没有变化时,图层不需要重新构建,只需从缓存中获取即可;但当所有命令都在同一层时,该缓存机制效果会不明显
  • Dockerfile文件尽量放在单独的子目录,因为构建镜像会检索该文件的相邻文件
  • 使用BuildKit工具来进行镜像的构建(更灵活的缓存机制,更快的镜像构建速度)

镜像安全问题优化:

  • 使用非 root 用户运行容器进程(安全性优化)
  • 将常见机密文件和文件夹添加到 .dockerignore 文件(.env/.aws/.ssh)

参考

BuildKit 下一代的镜像构建组件
制作 Python Docker 镜像的最佳实践
我可以减肥失败,但我的 Docker 镜像一定要瘦身成功!
Stack Overflow - 如何使用多阶段构建减小 python 的 docker 镜像大小?
Shrinking your Python application’s Docker image: an overview

往年同期文章