1 镜像瘦身
1.1 选择更精简的基础镜像
常见的 Python 镜像版本:
slim
:通常只安装运行特定工具所需的最小包Alphine
:专门为容器构建的操作系统,比其他的操作系统更小,但是其上会缺少很多软件包并且使用的 glibc 等都是阉割版(不推荐,因为编译过程会很慢)buster/stretch/jessie
:表示使用不同版本的 debian 系统(10/9/8)bullseye
、bookworm
:正在开发但尚未稳定版本
考虑使用官方的 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(确保实时性)
最终效果:
- 合并操作,减少层数(41 -> 21),但是最终镜像的大小没有变化(4.36 GB)
- 缓存或不重要文件的清理(之前已用过,所以优化效果一般;最终镜像大小 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
- 两个环境变量的设置,略有改善(最终镜像大小 4.32 GB)
踩坑:尽量避免文件的复制和转移,因为镜像的历史层会记录转移前的文件;除非在同一层内进行文件的创建和删除,否则文件删除后依然会保留是镜像的历史层中
1.3 排除无关的文件
使用 .dockerignore
排除无关文件(用法类似于.gitignore
)
常见的可排除无关文件
**/__pycache__
: python 缓存目录**/*venv
: Python 虚拟环境目录。很多 Python 开发习惯将虚拟环境目录创建在项目下,一般命名为:.venv
或venv
**/.env
: Python 环境变量文件**/.git
**/.gitignore
: git 相关目录和文件**/.vscode
: 编辑器、IDE 相关目录**/charts
: Helm Chart 相关文件**/docker-compose*
: docker compose 相关文件*.db
: 如果使用 sqllite 的相关数据库文件.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 版本后作为默认容器构建方式,在此之前可通过设置环境变量来启用BuildKit
:DOCKER_BUILDKIT=1 docker build xx
- 对于较早的 Docker 版本,需要在 Dockerfile 文件的顶部添加声明才能使用该语法:
# syntax=docker/dockerfile:experimental
经过好几天的折腾,镜像大小成功从 4.32 GB 涨到了 4.39 GB(后来发现其实是多安装了一个大概有165M的
torchvision
包,所以实际上多阶段构建是有点效果的)初步分析原因,Python 镜像的编译环境较为简单,因此独立编译过程并不会显著降低 Python 镜像的大小;并且安装 Python 包的缓存已经被清理过了
此外需要注意的是,相比于单阶段,多阶段构建速度会更慢
1.5 第三方软件辅助
- 使用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)
- 使用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