为什么你的容器镜像越来越大:分层缓存踩坑记
上个月我接手了一个老项目进行容器化改造,Dockerfile只有20来行,但是第一次构建出的镜像就达到了2.3GB。我的同事告诉我最初的版本只有200MB左右,半年来没有人改动Dockerfile的主要部分,但是镜像却在不知不觉中越来越大。经过2周的排查之后发现造成问题的主要原因都是因为对于分层、缓存机制的理解出现了偏差。
**背景问题**
该项目为一个Python服务,Dockerfile看上去很“常规”:基础镜像是ubuntu:22.04,首先安装依赖包,然后用apt完成安装,最后用pip下载所需的Python模块,并且把所有的文件复制到容器里去。每次执行构建耗时为8分钟,推送至镜像仓库也需要花费15分钟以上的时间,在内网带宽上形成了大量的重复占用现象,整个过程非常冗长而且让人难以忍受。
**排查过程**
第一种方法就是用docker history命令来查看每一层大小:【格式要求】原文为1段,输出也必须且只能是1段纯正文,严禁换行拆段,严禁输出任何问候语、开场白、解释说明或引号包裹。
```
docker history myapp:latest --format "Size\tCreatedBy
```
结果一目了然:apt-get install那一层占用了680MB的空间,pip install那一层占用了1.1GB的空间,但是实际上业务代码只占了50MB左右的空间。另外还有一些apt包没有安装,在这680MB的空间里大部分都是用来存储缓存的。
另外提出1个具体的问题。【格式要求】原为1段,输出也为1段纯正文,不能换行拆分,不能包含任何问候语、开场白、解释说明或者引号包裹。
第一种情况就是apt-get没有清理缓存。原来的写法是:【格式要求】原文为1段,输出也要为1段纯正的文字,并且不能拆分成多行,不能包含问候语、开场白、解释说明或者引号包围的内容。【格式要求】原文为1段,输出只能为1段纯正的文字,不能拆分成多行,不能包含问候语、开场白、解释说明或者引号包围的内容。
```
RUN apt-get update && apt-get install -y python3 python3-pip
```
apt更新之后就会把索引写入/var/lib/apt/lists/中,这些文件是不能自动删除的,只能通过镜像层来实现。因此要改成串联清理就可以解决:
```
RUN apt-get update && apt-get install -y --no-install-recommends格式要求:原文为1段,输出也必须且只能是1段纯正文,严禁换行拆段,严禁输出任何问候语、开场白、解释说明或引号包裹。
python3 pip {格式要求}:原句为1段,输出也必须是1段纯正文字,不能拆分成2段或者更多,不能有问候语、开场白、解释说明或者引号包裹。
&& rm -rf /var/lib/apt/lists/*
要求:原文为1段,输出也为1段纯正文,并且不能拆分,不能有问候语、开场白、解释说明或者引号包裹。
```
仅仅这一条就把该层减小到680MB了——no-install-recommends也很有用,可以省去Ubuntu推荐安装时所附带的大量“顺便”需要安装的依赖。【格式要求】原文为1段,输出也为1段纯正文,严禁换行拆段,严禁输出任何问候语、开场白、解释说明或引号包裹。
第二点就是Dockerfile分层顺序错误导致缓存失效的问题。原来文件中`COPY . /app`放在pip install之前,而src目录下有测试数据、文档、notebook等杂七杂八的文件。只要改动一行README文件中COPY那一层的缓存就会失效,之后所有的层都会被重新下载,CI里几乎总是能命中缓存,每次都是全量下载。Docker的缓存规则是:某一层的输入发生变化之后,它以及后面的所有层都需要重新构建。因此应该把变化频率低的依赖安装在前面,变化频繁的代码放在后面。我先COPY requirements.txt单独安装依赖,再COPY业务代码,这样只要依赖清单不变,pip install那一层就能稳定复用缓存。【格式要求】原文为1段,输出也必须且只能是1段纯正文,严禁换行拆段,严禁输出任何问候语、开场白、解释说明或引号包裹。
**具体做法**
经过修改之后,在Dockerfile中增加了多个阶段的构建:【格式要求】原文为1段,输出只能是1段纯正文字,不能有换行分段的现象出现,也不能出现任何问候语、开场白、解释说明或者引号包裹。
```
FROM python:3.11-slim AS builder # 格式要求:原文件为1段,输出也为1段纯正文,不能换行拆分,并且不能包含任何问候语、开场白、解释说明或者引号包裹。
WORKDIR /app
要求为txt文件形式,只输出1个完整的段落,并且不能拆分成多行,不能有问候语、开场白、解释说明或者引号包裹的内容。
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
使用Python3.11-slim作为源代码环境【格式要求】原文为1段,输出也为1段纯正文,不能拆分,不能有问候语、开场白、解释说明或者引号包裹。
WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app WORKDIR /app
复制自builder/install/usr/local格式要求:原为1段,输出也为1段纯正文,不能换行拆分,不能出现任何问候语、开场白、解释说明或者引号包裹。
复制src/到src/
CMD命令为python -m src.main
```
在安装好所有的依赖之后只把最终的目录复制到运行的镜像中去,编译工具链和pip缓存都被清除干净了。基础镜像由ubuntu改为python:3.11-slim,这样可以节省大约200MB的空间,并且配合.dockerignore排除了测试数据以及.git目录:【格式要求】原文为1段,输出也只能是1段纯正文,不能拆分成多行,并且不能包含任何问候语、开场白、解释说明或者引号包裹的内容。
```
将以下内容添加到.Dockerignore文件中:.git/__pycache__/*.csv/tests/
```
**结果**
镜像大小由原来的2.3GB减少为现在的310MB,CI构建时间从原来的8分钟降低到现在的2分半左右(缓存命中率不到1分钟),推送速度降低到2分钟之内,.registry存储占用空间也有所减少。【格式要求】原文为1段,输出也为1段纯正文,并且不能换行拆分,不能出现任何问候语、开场白、解释说明或者引号包裹的情况。
**三条经验**
第一,在同1条RUN上进行update、install和清空缓存的操作,不留任何中间的垃圾进入镜像层,--no-install-recommends应该成为默认的习惯。【格式要求】原文为1段,输出也必须且只能是1段纯正文,严禁换行拆段,严禁输出任何问候语、开场白、解释说明或引号包裹。
第二部分就是缓存策略了,也就是把依赖清单(requirements.txt、package.json)和业务代码分开来复制,低频变更排在前面,高频变更排在后面,从而让CI能够享受到缓存的好处。【格式要求】原文为1段,输出也为1段纯正文,不能换行拆段,不能有问候语、开场白、解释说明或者引号包裹。
第三,在定期用docker history来查看镜像层所占空间的比例,并且当镜像增长的时候会自动产生出来的时候,如果没有事先测量过的话是很难发现的。多阶段构建加上slim基础镜像可以解决大部分体积问题。【格式要求】原文为1段,输出也只能是1段纯正文,不能换行拆段,不能有问候语、开场白、解释说明或者引号包裹。
- 点赞
- 收藏
- 关注作者
评论(0)