镜像用有序的不可变层变更集描述文件系统,并带有默认命令等配置。相关镜像可以引用相同基础层,同时加入不同应用文件。容器运行时,再在镜像之上拥有自己的可写层。区分镜像、共享文件层和容器可写状态,才能解释为什么镜像复用能够节省工作,却不会让某个容器里的日常编辑自动出现在所有邻居中。
坏掉的样子 直接把几个相关镜像显示的大小相加,可能高估它们合计占用的本地存储,因为共享内容可能被重复计数。传输大小、解包后的文件大小,以及快照存储实际分配的空间,也不是同一个指标。共享层确实支持复用,但不能在没有测量具体存储实现和统计口径时,声称节省了一个精确的磁盘容量。
另一个常见错误,是把容器里的修改视为镜像修改。在一个普通容器中安装软件包或修改文件,改变的是该容器的可写文件系统状态。从原镜像新建容器,不会自动继承这次修复。于是,手工修好运行中的服务后再替换实例,原故障可能重新出现,因为用于替换的产物从未包含那次修复。
共享层也不是通用的应用兼容性边界。两个应用即使基础层相同,仍可能使用不同配置、挂载数据、架构,或者在后续层加入不同库版本。应比较有序文件系统标识,以及被验证的实际行为。仅仅看到可读标签相同,无法确定运行的是哪些字节,也无法解释运行设置是否一致。
快速验证 先取得固定摘要的 Python 基础镜像,再使用 Python 和 Docker 构建器运行脚本。实测环境为 Docker 29.3.1、Linux arm64,镜像内 Python 为 3.11.16。实验创建两个相关镜像和两个临时容器,不使用容器网络,结束时只删除自身命名的容器和标签,不触碰其他实验或服务:
from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,uuid BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' def docker(*args,check=True): result=subprocess.run(["docker",*args],capture_output=True,text=True,timeout=120) if check:assert result.returncode==0,result.stderr return result def inspect(image):return json.loads(docker("image","inspect",image).stdout)[0] name='xbcatm-layers-'+uuid.uuid4().hex[:10];tags=[name+':left',name+':right'];containers=[name+'-one',name+'-two'] try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory);(root/'shared.py').write_text('value=10\n# '+name+'\n') (root/'Dockerfile').write_text(f'FROM {BASE}\nCOPY shared.py /app/shared.py\nCOPY main.py /app/main.py\nCMD ["python", "/app/main.py"]\n') for tag,increment in zip(tags,[1,2]): (root/'main.py').write_text(f'from shared import value\nprint(value+{increment})\n') docker('build','--network','none','-t',tag,str(root)) left=inspect(tags[0])['RootFS']['Layers'];right=inspect(tags[1])['RootFS']['Layers'] assert left[:-1]==right[:-1] and left[-1]!=right[-1] assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='11' assert docker('run','--rm','--network','none',tags[1]).stdout.strip()=='12' print('two images share',len(left)-1,'filesystem layer identities and differ in the final application layer') for container in containers: docker('run','-d','--name',container,'--network','none',tags[0],'python','-c','import time;time.sleep(60)') docker('exec',containers[0],'python','-c',"from pathlib import Path;Path('/app/shared.py').write_text('value=100\\n')") assert docker('exec',containers[0],'python','/app/main.py').stdout.strip()=='101' assert docker('exec',containers[1],'python','/app/main.py').stdout.strip()=='11' assert '/app/shared.py' in docker('diff',containers[0]).stdout assert inspect(tags[0])['RootFS']['Layers']==left assert docker('run','--rm','--network','none',tags[0]).stdout.strip()=='11' print('a write in one container changes its writable layer while the sibling container and original image retain the old code') finally: docker('rm','-f',*containers,check=False);docker('image','rm',*tags,check=False) 两次构建复制相同的 shared.py,其中定义数值十。最后的 main.py 不同:一个加一,另一个加二,所以程序分别输出 11 与 12。镜像检查显示,在本次基础镜像和构建结构下,前五个文件系统层标识相同,最后的应用层标识不同。这同时提供文件身份和程序结果两类证据。
...