containerd namespace 多租户管理:从隔离原理到 ctr 实操与避坑
在 containerd 运维这件事上我见过最多的一种尴尬局面是明明集群里跑了一堆容器ctr敲下去却是空的明明只在一个节点上做测试镜像却和别人的业务镜像搅在同一个default里删都不敢乱删。如果你也碰到过这种情况那十有八九是没有把 containerd 的命名空间namespace用起来。containerd 的 namespace 是它的多租户管理基础也是很多容器平台做资源归集、项目隔离、镜像分发的核心切分维度。它可以让你在同一个 containerd 实例上把不同团队、不同业务线、不同环境的镜像和容器分开管理互不干扰。对于正在做自研容器平台、或直接裸机使用 containerd 的运维和开发来说这是一块必须吃透的能力。这个系列写到这一步我打算花一整篇讲清楚 namespace 的操作细节、隔离边界和实际坑点。1. 为什么需要 containerd namespace 多租户管理1.1 先搞清楚 namespace 隔离的是什么这里必须先纠正一个常见误区containerd 的 namespace 和 Linux 内核的 namespacePID namespace、Mount namespace 等不是一回事。Linux namespace 是操作系统内核提供的进程隔离机制而 containerd namespace 是 containerd 自己实现的一套逻辑分组本质上是metadata 层的分类。你可以把 containerd 想象成一台服务器的物业这台物业下面有独立的水电表。Linux namespace 是砌墙把空间真的隔开containerd namespace 则是台账上的分类账本用于记录不同租户住了什么房、用了什么资源。两者可以叠加使用但不要混为一谈。具体隔离了什么在 containerd 的 metadata 数据库里namespace 会把以下几类对象分开存放容器container同一个容器 ID 可以出现在不同 namespace 中互不影响。任务task容器的运行实例和容器一样按 namespace 隔离。镜像image镜像元数据按 namespace 索引每个 namespace 看到的镜像是独立的。快照snapshot容器 rootfs 的 layer 快照记录按 namespace 分离。事件eventcontainerd 的事件流会带上 namespace 信息客户端可以订阅指定 namespace 的事件。但有一个例外content store内容存储是全局共享的。同一份 blob 数据多个 namespace 可以引用不会为了省事重复存一份底层数据。这个设计有点像 Git 的对象库object 是全局的但 ref分支/标签可以分属于不同仓库。1.2 什么场景下要多租户管理很多人只在单机上用 containerd 跑一两个容器所以觉得 namespace 没用。但一旦你面对以下场景namespace 几乎是必须采用的方案多个业务线共享同一批节点。例如一台高性能物理机上既要跑 Web 服务又要跑内部批处理任务。用 namespace 把web、batch分开镜像列表不串容器命名不冲突清理也更安全。研发、测试、生产的镜像隔离。在 CI/CD 自建镜像仓库时同一个 containerd 节点上如果同时存在dev、staging和prod的镜像建议直接映射为三个 namespace避免误拉取、误删除。平台方给多个租户提供容器能力。底层的 containerd 被上层平台封装成多租户容器服务时最简单的资源归集方式就是把每个租户映射为一个 namespace。在这些场景里namespace 的核心价值不是安全堡垒而是操作边界和生命周期管理。它可以让你在操作某个租户时只影响该租户的数据不让误操作扩散到全局。1.3 和 Kubernetes namespace 的区别如果你是从 Kubernetes 过来的这里更要小心。Kubernetes 的 namespace 是 API 层面的资源分组属于控制面概念containerd 的 namespace 是运行时层面的对象分组属于数据面概念。也就是说对比项Kubernetes namespacecontainerd namespace管理对象Pod、Service、ConfigMap 等 API 资源容器、任务、镜像、快照隔离层级控制面逻辑分组运行时 metadata 分组由谁定义kube-apiserver 管理containerd 客户端ctr/nerdctl管理关联关系Pod 的 namespace 会作为 label 存在不直接与 Pod 的 namespace 对应关键点是当 Kubernetes 使用 containerd 作为运行时kubelet 通过 CRI 创建的所有容器都落在 containerd 的k8s.ionamespace 中。而 Pod 在 Kubernetes 里属于哪个 namespace只是作为 label 附加在容器元数据上不会自动映射成一个 containerd namespace。所以你会看到两类典型操作路径排查 Kubernetes 节点上的容器用crictl或者ctr -n k8s.io。排查自建平台、裸机上的容器用ctr -n 你的命名空间。两条路径如果混着用就会出现开头我说的“ctr 看不到容器”的幻觉。2. namespace 底层原理与核心命令速览2.1 ctr 的 -n 参数第一道门槛操作 containerd namespace最常用的客户端是ctr。它的设计里几乎所有命令都支持-n指定 namespace。如果你不带默认操作的是defaultnamespace。ctr namespace list这个命令查看当前 containerd 实例上有哪些 namespace。装好 containerd 之后你第一次执行它很大概率只看到NAME LABELS default很多人在这一步就停了以为只能有一个默认命名空间。其实不是你需要主动创建然后所有操作都显式带上-n。比如在web这个 namespace 里拉一个镜像ctr -n web images pull docker.io/library/nginx:1.25-alpine拉完之后查看镜像ctr -n web images list再看默认 namespacectr images list你会看到web里有的镜像default 里大概率没有。这就完成了第一层隔离。这里有个使用习惯建议凡是涉及 containerd 的命令无论是否使用默认 namespace都显式写-n。因为实际生产环境很少会真的用 default 装业务镜像你显式写出 namespace后面看日志、排查问题都会方便很多。2.2 namespace 的生命周期管理命令创建和销毁 namespace 本身是轻量操作。核心命令如下# 创建命名空间 ctr namespace create web # 查看所有命名空间及相关标签 ctr namespace list # 给命名空间打标签方便标识归属 ctr namespace label web ownerwebteam envprod # 删除命名空间仅限空命名空间 ctr namespace remove weblabel这个命令很多人容易忽略。在租户多起来之后标签能帮你快速识别 namespace 的归属。比如你创建了六个 namespace分别代表三套环境和两个业务线光靠名字可能记不住用途但带上envprod、ownerwebteam这样的标签在 list 输出里一眼就能看明白。删除 namespace 有一个硬性限制只有空的 namespace 才能直接删。如果里面还有镜像、容器、任务或快照ctr namespace remove会直接报错。这个限制其实是一种保护避免你把某个 namespace 一删了之留下孤儿对象。正确清理顺序我在第 4 节详细展开。2.3 content store 与 snapshot 的隔离边界前面提过content store 是全局共享的。这一点在实操中的表现是你在webnamespace 里拉了一个nginx:1.25-alpine之后又在batchnamespace 里拉同一个镜像containerd 会复用已经存在的 blob不会把每一层又重新下载一遍至少在 content 层是这样。但注意镜像元数据和快照元数据是各自独立的。同一个镜像在 A namespace 里拉出来是一套 image 记录在 B namespace 里拉出来又是另一套 image 记录。虽然底层 blob 一样但你在 A 里删掉镜像B 里仍然能正常使用。快照层更微妙。容器的 rootfs 是通过 snapshotter 创建的snapshot 的元数据按 namespace 保存但底层的 key 在全局是唯一的。如果你在两个 namespace 里用同一个镜像、同一个快照 key 去创建容器后创建的那个会冲突。实际上容器的 snapshot key 通常由 containerd 自动生成所以普通业务不会碰到。但如果你用ctr -n web snapshots create这类底层命令手工创建快照又在ctr -n batch snapshots create里用了完全相同的 key就会报 already exists。这说明 namespace 并不是把底层存储完全切成 N 份而是一套“全局存储 逻辑索引”的模型。理解这个模型对后面排查磁盘占用和镜像迁移会很有帮助。3. 多租户落地实操两个租户的完整场景3.1 场景设计我用一个最常见的双租户场景来演示这个场景适合自建容器平台、内部 PaaS 甚至是实验室环境。假设有一台 8 核 16G 的物理机装了 containerd上面要同时服务两个团队team-web跑 Nginx对外提供页面服务。team-data跑 PostgreSQL做数据存储。两个团队共用这台机器要求镜像列表互相不可见、容器命名互相不冲突、删除操作互不影响。先做准备工作# 创建两个 namespace ctr namespace create team-web ctr namespace create team-data # 打上标签 ctr namespace label team-web teamweb ctr namespace label team-data teamdata确认一下ctr namespace list输出里能看到两个 namespace 和各自的 label准备工作完成。3.2 镜像与容器隔离操作team-web 拉取 Nginxctr -n team-web images pull docker.io/library/nginx:1.25-alpineteam-data 拉取 PostgreSQLctr -n team-data images pull docker.io/library/postgres:16-alpine然后分别在对应 namespace 里创建容器# team-web 里的 Nginx 容器 ctr -n team-web run -d --net-host docker.io/library/nginx:1.25-alpine web-nginx # team-data 里的 PostgreSQL 容器 ctr -n team-data run -d --net-host docker.io/library/postgres:16-alpine pg-main这里解释一下参数-d表示后台运行--net-host让容器直接使用宿主机网络。ctr run后面第一个参数是镜像引用第二个参数是容器 ID。注意我用的是镜像全名docker.io/library/nginx:1.25-alpine。用ctr时如果你只写nginx:1.25-alpine它不一定能在本地镜像库中命中因为 containerd 对镜像引用解析比较严格建议都带上完整的仓库地址和 tag。运行容器之后分别查看ctr -n team-web containers list ctr -n team-data containers list会看到各自 namespace 下的容器。再交叉查看一下ctr containers list默认 namespace 下是空的因为容器只存在于 team-web 和 team-data。这样就实现了容器层的基本隔离。再看任务列表ctr -n team-web tasks list ctr -n team-data tasks listcontainers list和tasks list的区别在于container 是元数据对象task 是真正运行的进程。当你需要 kill 某个容器里的进程、排查运行状态基本都要用 tasks 这一组命令。停止并删除容器也很明确# 在 team-web 中先杀任务再删容器 ctr -n team-web tasks kill web-nginx ctr -n team-web containers rm web-nginx # 如果 task 卡住可以强制 kill ctr -n team-web tasks kill --signal SIGKILL web-nginx这些操作都只在对应的 namespace 里生效。就算你在两个 namespace 下都创建了同一个容器 ID删除其中一个另一个不会受影响。3.3 跨 namespace 迁移镜像多租户场景里经常出现一个需求某个镜像在 team-a 里验证过了想放到 team-b 里直接用。containerd 没有简单的mv命令最稳妥的方式是导出再导入。把 team-web 里的 Nginx 镜像导出来ctr -n team-web images export --platform linux/amd64 /tmp/nginx-1.25.tar docker.io/library/nginx:1.25-alpine然后在 team-data 里导入ctr -n team-data images import /tmp/nginx-1.25.tar导入后再看 team-data 的镜像列表你会看到 nginx 已经出现在里面。这里有两个细节--platform linux/amd64是为了导出一个明确平台的镜像避免跨架构环境下导出多份数据。如果目标节点是 arm64就写linux/arm64如果需要在多架构共享可以用--all-platforms但导出的 tar 会很大。导入后的镜像引用不一定是原来的完整 ref可能变成docker.io/library/nginx:1.25-alpine但也可能以 tar 内记录的引用为准。导入完成后建议第一时间ctr -n team-data images list确认引用名免得后续 run 时引用不匹配。如果节点数量多、镜像量大更推荐的方式是把镜像推到目标节点能访问的私有镜像仓库然后在目标 namespace 里直接 pull。导出导入适合单机或小规模迁移推拉仓库才是生产多节点场景下的正解。3.4 用 nerdctl/crictl 贴近日常操作ctr用起来比较底层如果你更习惯 Docker 风格的命令可以考虑nerdctl。它能通过--namespace参数指定 containerd namespace# 查看 team-web 里的容器 nerdctl --namespace team-web ps # 在 team-data 里拉镜像 nerdctl --namespace team-data pull postgres:16-alpine # 在 team-web 里运行容器对应 docker run nerdctl --namespace team-web run -d -p 8080:80 nginx:1.25-alpinenerdctl 会在一定程度上封装网络、端口映射和容器日志比 ctr 友好很多。但它的前提是你使用 containerd 作为底层运行时而且它默认操作的是defaultnamespace。平时多用--namespace显式指定不要依赖默认值。而crictl是另一条路线。它专门对接 CRI通常用在 Kubernetes 节点上。crictl ps、crictl images默认操作的是 containerd 的k8s.ionamespace不需要你手动指定。你在 crictl 里看到的 Pod 容器用ctr -n k8s.io containers list也能看到但后者不会显示 Pod 的组装关系只是裸容器列表。所以我的习惯是查 Kubernetes 节点容器状态 → 用crictl。查 containerd 实例上的租户隔离情况 → 用ctr -n xxx或nerdctl --namespace xxx。不要在 crictl 里纠结 containerd namespace它已经为你绑定了 CRI 专用 namespace。4. 常见问题与排查技巧实录4.1 容器“消失”了列表一片空白这个是新手上路最常见的坑。你在 Kubernetes 节点上用ctr containers list发现 Node 上明明有 Pod列表却是空的。又或者你在某个业务机器上ctr images list之前拉好的镜像全不见了。绝大多数情况是 namespace 选错了。Kubernetes 的容器在k8s.ioctr -n k8s.io containers list自建业务的容器在各自 namespacectr namespace list ctr -n team-web containers list排查这类问题时我建议按以下顺序ctr namespace list看看有哪些 namespace。在可疑 namespace 里执行containers list、images list。确定对象所在的 namespace 后再继续操作。不要一上来就怀疑 containerd 数据丢了更不要直接ctr --address换 socket。namespace 选错的概率远高于数据丢失的概率。4.2 namespace 删除总是失败有人会直接对非空 namespace 执行删除ctr namespace remove team-old然后看到类似failed to delete namespace ...: namespace must be empty的报错。这不是命令坏了是 containerd 在保护你。正确姿势是先清空 namespace 里的对象。大致顺序是# 1. 杀掉并删除所有任务 ctr -n team-old tasks kill --signal SIGKILL --all ctr -n team-old tasks list # 2. 删除所有容器 ctr -n team-old containers list ctr -n team-old containers rm container-id # 3. 删除所有镜像 ctr -n team-old images list ctr -n team-old images rm image-ref # 4. 回收无用的快照和 content ctr -n team-old snapshots list ctr -n team-old snapshots rm snapshot-key ctr -n team-old content gc最后再执行ctr namespace remove team-old如果你觉得一个个删太麻烦可以封装成一个小脚本遍历containers list、images list、snapshots list后自动删除。删除镜像时注意镜像可能被容器引用所以先删容器、后删镜像的顺序不能乱。另外content gc会回收所有 namespace 下不再被引用的 blob。在多个 namespace 共享 content store 的场景下这一步能帮你还磁盘空间但也意味着某些跨 namespace 复用的内容可能被清理执行前确认没有正在运行的容器依赖它。4.3 同名快照冲突和磁盘占用前面提到过snapshot 的 key 是全局唯一的。如果你在不同的 namespace 里手动创建了同 key 的快照第二个 namespace 的创建会失败。这种情况多发生在脚本批量创建容器、但又没有使用 containerd 自动生成 key 时。更常遇到的其实是磁盘占用问题。两个 namespace 拉了同一个镜像content blob 是复用的但容器运行时每个 namespace 下的快照都会解压一套 rootfs。也就是说content 的重复是零成本的但 snapshot 的重复是真实的磁盘开销。要查每个 snapshot 占多少空间可以用ctr -n team-web snapshots ls du -sh /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs如果发现磁盘占用快速增长优先检查是不是多个 namespace 跑着大量相同镜像的容器。优化方向有两个尽量利用内容复用减少不必要的镜像 tag 拷贝。对已经不再使用的 namespace 做整体清理而不是只删除部分镜像。4.4 安全边界与权限控制多租户管理做到后面一定要清醒认识到containerd namespace 不是安全隔离边界。namespace 只是 metadata 层的逻辑分组。同一个 containerd socket 下只要客户端有访问权限它就可以用ctr -n team-web操作 team-web也可以ctr -n team-data操作 team-data甚至可以ctr namespace list看到所有 namespace。如果你把/run/containerd/containerd.sock暴露给了不信任的调用方那这个“租户隔离”形同虚设。所以生产级的租户隔离需要额外的措施权限控制前置不要把 containerd 的 socket 直接暴露给业务侧。平台在上层封装只暴露有限的 API 接口。socket 权限收敛默认情况下 containerd.sock 只有 root 或指定组可访问。不要为了省事chmod 777。安全容器兜底如果租户之间需要更强的隔离建议叠加 Kata Containers、gVisor 这类安全容器方案让每个 namespace 对应不同的 runtime class而不是只指望 metadata 隔离。命名规范固定namespace 名称统一采用项目-环境的格式比如web-prod、>
上一篇/下一篇内容由系统自动关联
返回资讯列表 →