两个容器看到独立进程与网络空间,同时共享守护进程所在环境的 Linux 内核。

容器把进程隔离机制与共享 Linux 内核结合起来

常规 Linux 容器通过守护进程所在宿主的 Linux 内核运行进程,并利用命名空间提供选定的隔离视图。镜像文件提供用户态环境,不会给每个容器单独启动一个内核。在 macOS 的 Docker Desktop 中,守护进程和 Linux 容器位于 Linux 虚拟机里,所以容器共享的是该虚拟机内核,而不是 macOS 内核。讨论“宿主”时必须说清楚这一层。 坏掉的样子 假定每个容器都有独立操作系统内核,会导致错误诊断。如果守护进程所在 Linux 环境缺少某个内核能力,仅换成包含另一发行版用户态文件的镜像,并不会让该能力出现。反过来,升级镜像内的软件包,也不会升级宿主内核。这个区别影响系统调用、内核模块、安全边界和资源统计,不能用容器里显示的发行版名字代替判断。 命名空间隔离可以配置,不是每次部署都完全相同的固定组合。容器可以加入其他网络命名空间、使用宿主网络、共享卷,或者采用不同的用户命名空间映射。看到容器内 UID 为零,也不能直接判断容器外权限;必须结合能力集合、用户映射、安全策略和暴露资源分析。一个数字或名称不足以描述完整安全边界。 localhost 也是常见误区。在独立网络命名空间下,127.0.0.1 指向调用容器自己的回环空间,不会自动到达兄弟服务或开发者电脑。同样,在一个普通容器的可写层创建文件,不会自动出现在使用同一镜像的另一个容器里。排查访问失败时,应先分清地址和文件究竟属于哪一个视图。 快速验证 取得固定的 Python 镜像后,使用宿主 Python 和 Docker 守护进程运行脚本。实测 Docker 为 29.3.1,Docker Desktop 虚拟机内 Linux 为 6.12.76,镜像内 Python 为 arm64 的 3.11.16。两个容器使用相同镜像、不同主机名,并设置无外部网络。结束时只删除这两个临时容器: ...

十二月 7, 2026 · 1 分钟 · XB Field Notes
观察、决策、行动三个面板,旁边是关于cgroup v2 资源预算的标题。

cgroup v2 的不同资源预算有不同失败方式

cgroup v2 用统一层次组织资源控制。一个工作负载的多个进程可以共享合计预算,限制还会沿层次继承,而不是只对某个特定 PID 生效。CPU 带宽、内存用量和任务数量由不同控制器管理,执行方式也不同。读取真实 cgroup 文件和失败计数器,比把所有配置上限都当作同一种“到点就停”更能解释实际行为。 坏掉的样子 cpu.max 按周期限制 CPU 带宽。控制组用完配额后,可运行工作可能被节流,但这不会预留一颗专用处理器,也不保证某个请求延迟。多个线程可以很快消耗共享配额。CPU 权重则影响竞争情况下的相对分配;混淆权重、配额和 CPU 亲和性,会让容量规划建立在错误预期上。 内存上限不会统一把大分配变成可由应用干净处理的错误。到达 memory.max 时,内核可以回收内存;无法降低用量时,可能触发控制组 OOM 机制。某些分配路径还有其他失败方式。memory.high 施加回收压力和节流,本身不会调用 OOM,因此不能把这两个设置解释成同义的硬终止阈值。 任务上限也包括线程,不只是监控面板显示的应用进程数量。达到 pids.max 后,即使 CPU 和内存看似充足,fork 或 clone 仍可能失败。层次限制意味着子组配置更大,也不能简单覆盖祖先更紧的预算。解释可见文件时,需要确认对应路径与控制器委派关系,不能只读取一个数值就断定完整有效上限。 快速验证 先取得固定镜像,再用 Python 和采用 cgroup v2 的 Docker 守护进程运行脚本。实测为 Docker 29.3.1、Linux 6.12.76、Python 3.11.16。所有限制只施加到可丢弃实验容器。脚本执行受限的小规模内存耗尽测试,清理自己的容器,不改变宿主全局 cgroup 设置,也不修改其他服务的预算: ...

十一月 10, 2026 · 2 分钟 · XB Field Notes
标题卡旁有三个方框,分别标注 Observe、Decide 和 Act。

多阶段构建需要经过测试的运行边界

多阶段 Dockerfile 可以在一个阶段编译软件,再把选定输出复制到另一个运行阶段。最终阶段继承自己的基础镜像和明确复制的文件,而不会自动继承构建器里的编译器、源码目录和软件包缓存。这建立了有用的边界,但作者仍必须识别程序真正执行时需要的一切。文件复制成功,只能证明文件已进入产物,不能证明运行环境已经完整。 坏掉的样子 即使 ELF 解释器和共享库缺失,COPY 仍可成功把动态链接程序放进 scratch 镜像。构建可能正常结束,因为复制不会执行程序,也不会验证全部运行依赖图。随后启动却可能报告 no such file or directory,尽管配置路径上的可执行文件确实存在。此时需要检查解释器和库,而不是只反复确认程序文件是否被复制。 脚本需要解释器,应用可能需要证书根或时区数据,还有程序只在特定请求路径上加载插件或资源。简单启动测试可能漏掉这些较晚发生的需求。选择小型运行基础镜像,只有在它满足应用真实契约时才有价值,包括受支持架构和必要文件路径。不能因为某个镜像最小,就默认它适合所有应用。 早期阶段不属于最终镜像的继承链,也不意味着它被安全擦除。构建缓存和明确导出的中间镜像,仍可能保留其内容。敏感构建输入需要正确的密钥传递方式,复制输出也必须检查是否意外包含不该分发的材料。多阶段结构控制最终文件系统组成,却不会自动保证整个构建过程的保密性。 快速验证 历史 Docker 示例保持原样;当前没有 Docker 运行环境,因此本次没有重跑。 使用 Python、Docker、固定的 Python 基础镜像,以及供构建阶段安装 Debian 软件包的网络运行脚本。运行容器关闭网络。实测是 Docker 29.3.1、Linux arm64,GCC 为 Debian 12.2.0-14+deb12u1 包提供的 12.2.0。软件仓库是实时仓库,因此实验验证运行组成,不承诺未来永远取得完全相同的编译器字节: from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,tarfile,uuid BASE='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84' def docker(*args,check=True): result=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=360) if check:assert result.returncode==0,result.stderr return result def inspect(image):return json.loads(docker('image','inspect',image).stdout)[0] name='xbcatm-stages-'+uuid.uuid4().hex[:10];tags=[name+':builder',name+':good',name+':bad'];container=name+'-inspect' try: with TemporaryDirectory(prefix=name+'-') as directory: root=Path(directory) (root/'square.c').write_text('#include <stdio.h>\n#include <stdlib.h>\n#include <errno.h>\nint main(int argc,char **argv){\n char *end;long value;\n if(argc!=2){fprintf(stderr,"one integer required\\n");return 2;}\n errno=0;value=strtol(argv[1],&end,10);\n if(errno||end==argv[1]||*end||value<0||value>100){fprintf(stderr,"range 0..100\\n");return 2;}\n printf("%ld\\n",value*value);return 0;\n}\n') (root/'Dockerfile').write_text(f'''FROM {BASE} AS builder RUN apt-get update && apt-get install -y --no-install-recommends gcc libc6-dev && rm -rf /var/lib/apt/lists/* WORKDIR /source COPY square.c . RUN gcc -O2 -Wall -Wextra -Werror -static -o /square-static square.c && gcc -O2 -Wall -Wextra -Werror -o /square-dynamic square.c FROM scratch AS good COPY --from=builder /square-static /square USER 65532:65532 ENTRYPOINT ["/square"] FROM scratch AS bad COPY --from=builder /square-dynamic /square USER 65532:65532 ENTRYPOINT ["/square"] ''') for tag,target in zip(tags,['builder','good','bad']):docker('build','--target',target,'-t',tag,str(root)) print('compiler:',docker('run','--rm','--network','none',tags[0],'gcc','--version').stdout.splitlines()[0]) for argument,expected in [('0','0'),('7','49'),('100','10000')]: assert docker('run','--rm','--network','none',tags[1],argument).stdout.strip()==expected for argument in ['7x','-1','101','','999999999999999999999999999']: invalid=docker('run','--rm','--network','none',tags[1],argument,check=False) assert invalid.returncode==2 and 'range 0..100' in invalid.stderr invalid=docker('run','--rm','--network','none',tags[1],check=False) assert invalid.returncode==2 and 'one integer required' in invalid.stderr invalid=docker('run','--rm','--network','none',tags[1],'7','8',check=False) assert invalid.returncode==2 and 'one integer required' in invalid.stderr good=inspect(tags[1]);builder=inspect(tags[0]);assert good['Size']<builder['Size'] and len(good['RootFS']['Layers'])==1 assert good['Config']['User']=='65532:65532' docker('create','--name',container,tags[1],'7');archive=root/'rootfs.tar';docker('export','-o',str(archive),container) with tarfile.open(archive) as contents: names={entry.name.removeprefix('./') for entry in contents} assert 'square' in names and 'usr/bin/gcc' not in names and 'source/square.c' not in names bad=docker('run','--rm','--network','none',tags[2],'7',check=False) assert bad.returncode!=0 and 'no such file or directory' in bad.stderr print('the static final image runs valid input, rejects invalid input, and contains one application layer without compiler or source') print('recorded image sizes:',builder['Size'],'builder bytes;',good['Size'],'final bytes') print('the dynamic scratch image builds successfully but cannot start because its interpreter/runtime was not copied') finally: docker('rm','-f',container,check=False);docker('image','rm',*tags,check=False) 构建阶段把同一个小型 C 程序分别静态链接和动态链接。程序接受一个零到一百范围内的整数,并输出平方。编译开启警告且把警告视为错误。两个有名字的最终目标各自只把对应可执行文件复制到 scratch,并选择数值用户和组 65532。这样,构建工具与运行产物的交接在 Dockerfile 中清楚可见。 ...

九月 25, 2026 · 2 分钟 · XB Field Notes