资讯动态

containerd namespace多租户实战:用ctr命令实现工业级容器隔离与管理

发布时间:2026/10/2 9:28:31 来源:尧图企业网站定制
最近在做内部容器平台的多租户改造一个很实在的痛点浮出来了几十台服务器上的 containerd 全在裸奔所有人、所有业务、所有镜像都堆在同一个 default namespace 里。谁误杀了谁的容器、谁占了哪个镜像的引用、事件日志刷屏的时候分不清是哪个团队的全凭猜。后来我们把 containerd 的 namespace 当成一等公民来规划才真正把“能用”变成“好管”。这篇文章就把这段时间梳理的东西写透containerd namespace 到底是什么、能隔离什么不能隔离什么以及如何用 ctr 命令把一个裸 containerd 管理成一套工业级的多租户运行环境。适合正在做容器运行时治理、容器平台建设或者想彻底搞懂 containerd namespace 多租户管理的同学。多说一句这里的 namespace 不是 Linux 内核的 namespacePID、NET、MNT 那种而是 containerd 自己在上层定义的一套元数据隔离维度。后面我会反复对比这两个概念因为混在一起的人实在太多。1. 为什么工业级环境必须做 namespace 多租户规划1.1 单租户共享模式的隐患先还原一个场景。你在一台机器上部署了 containerd默认状态下所有操作都落在 default namespace。第一天只有你自己跑测试没问题。等到第二天业务 A 的镜像、业务 B 的容器、运维的临时调试任务全部混在一起麻烦就开始出现了。第一类是误操作。比如你执行ctr task kill想杀掉自己调试用的容器结果因为没带 namespace 参数工具默认操作的是 default你看到的容器列表是所有人的。按 PID 一比对杀错了。这种事故在我们内部出现过不止一次尤其凌晨值班的时候人一迷糊就容易出事。第二类是清理困难。containerd 的镜像 GCgarbage collection是全局视角的它只关心 blob 有没有被引用不关心这个 blob 是哪个团队拉进来的。运维在凌晨跑定时清理把线上正在用的镜像层给回收了的情况虽然不常见但一旦发生就是 P0 事故。后来一查原因是那个镜像的引用刚好被某个临时容器删除时带走了而 GC 认为它已经无主。第三类是可观测性灾难。你ctr events拉事件流default 下所有租户的事件都混在一起监控告警根本没法按租户聚合。哪次发布导致容器重启风暴在运行时层面看不出归属关系只能回到 Kubernetes 控制面去对时间戳效率极低。所以我的结论很直接单机单租户可以忍受 default 混乱但只要是长期运行的工业级环境多租户隔离不是可选项是必选项。containerd namespace 解决的不是安全问题而是“可管理性”和“归属清晰”的问题。1.2 namespace 能隔离什么、不能隔离什么很多人对 namespace 有过高期待觉得它什么都能挡。实际上它只隔离元数据不隔离物理资源。我用一张表把隔离边界说明白隔离维度是否隔离说明容器元数据是不同 namespace 下ctr task list互不可见镜像元数据是每个 namespace 有自己独立的 image 视图事件流是ctr events -n xxx只看到该 namespace 的事件Blob 内容层否content store 是全局共享的不同 namespace 可复用同一镜像层磁盘快照否snapshotter 层面不做硬隔离网络CNI否网卡、IP 分配是全局的内存/CPU否需要依赖 cgroup 做配额用生活化的类比理解namespace 就像一栋楼里的门牌号和楼层标识。你走到 15 楼电梯按钮上看到的是 15 楼的房间号不会跟 3 楼的房间号混在一起。但是整栋楼的水电管道是共用的某个楼层用水太多其他楼层照样受影响。所以工业级落地时namespace 一定是和其他机制配合使用的cgroup 控制资源配额、CNI 做网络隔离、文件系统权限做数据保护。namespace 的价值在于当资源出问题时你能迅速定位是哪个租户的哪些容器引发的。它不能替你挡住资源争抢。1.3 与 Kubernetes namespace 的区别这个话题必须单独拿出来讲因为几乎每个从 Kubernetes 过来的同学都会搞混。Kubernetes 里的 namespace 是控制面的资源逻辑隔离单元它有 RBAC、ResourceQuota、NetworkPolicy 这些配套能力。而 containerd 的 namespace 只是运行时元数据的一级前缀它没有用户的概念也没有配额管理的概念。在 Kubernetes 环境下kubelet 调用 containerd 的 CRI 接口时会把所有 Pod 都放进名为k8s.io的 containerd namespace。也就是说你在宿主机上执行ctr -n k8s.io task list能看到所有 Pod 的沙箱容器。这也意味着Kubernetes 层面的租户隔离并不会自动映射成 containerd namespace 的租户隔离。如果你要在运行时层面对不同工作负载做区分必须自己设计 namespace 规划或者通过 CRI 代理层去做翻译。纯 containerd 场景不走 Kubernetes就更直接了ctr 就是你的全部管理工具namespace 就是你唯一的多租户管理维度。下面进入实操。2. namespace 核心机制与 ctr 命令实操2.1 containerd 元数据模型一个 namespace 前缀引发的隔离要真正理解 namespace 的威力得先知道 containerd 的元数据是怎么存的。containerd 的 metadata 存在本地的 bolt 数据库/var/lib/containerd/io.containerd.metadata.v1.bolt/meta.db里。你看不到一个专门的 namespace 属性字段而是大量 key 都以 namespace 作为前缀比如namespace-name/container-name这种分层结构。正因为如此namespace 天然具备这些特性同一台机器上的多个 namespace 可以存在同名的容器互不干扰。镜像引用image metadata按 namespace 独立但实际内容层的 blob 在 content store 里是全局的。所有 gRPC 请求都必须携带 namespace 上下文客户端通过 header 传递。这也是为什么ctr命令大量使用-n参数的原因。如果不带-n默认走 default namespace。而大部分新手踩坑都是从忘记加-n开始的。对客户端来说namespace 就是一把钥匙。你拿 default 的钥匙打开的是 default 的柜子拿 prod-ai 的钥匙看到的是 prod-ai 的柜子。柜子里装的东西可以重名但你们互相看不见。2.2 生命周期管理create、list、inspect 这套组合拳先看最基础的一组命令这组命令相当于 namespace 管理的“增删改查”。创建 namespacectr namespace create prod-ai创建时可以附加标签这在多租户治理中非常有用ctr namespace create staging-etl --label owneretl-team --label envstaging查看所有 namespacectr namespace list输出类似这样NAME LABELS default k8s.io prod-ai ownerml-team,envprod staging-etl owneretl-team,envstaging查看单个 namespace 的详细信息ctr namespace info prod-ai删除 namespacectr namespace remove prod-ai注意现在ctr namespace rm和ctr namespace remove是等价的早期版本只有 remove新版本兼容了 rm。我建议脚本里统一写remove因为语义更明确不容易误操作。这里必须强调一个很多人踩过的坑删除 namespace 时如果里面还有容器、镜像引用或 lease删除操作会失败提示 namespace 不是空的。正确姿势是先清理租户内的所有资源再删 namespace。这个我在第 4 节演示时会重点展开。2.3 带 namespace 的常用工作负载操作Namespace 操作不只在 namespace 子命令里更多时候是作为全局-n参数挂到其他命令上的。我把常用命令整理成一张对照表建议直接存下来操作不带 namespace 的写法带 namespace 的正确写法拉镜像ctr image pull docker.io/library/redis:7-alpinectr -n prod-ai image pull docker.io/library/redis:7-alpine查看镜像ctr images listctr -n prod-ai images list运行容器ctr run docker.io/library/redis:7-alpine redisctr -n prod-ai run docker.io/library/redis:7-alpine redis查看任务ctr task listctr -n prod-ai task list查看事件ctr eventsctr -n prod-ai events查看内容活跃引用ctr content activectr -n prod-ai content active这套命令组合拳覆盖了日常 80% 的运维操作。我个人的习惯是所有脚本里强制写-n即使你知道当前就是 default 也要写。因为容器环境最容易出问题的地方不是操作本身而是操作作用到了错误的租户对象上。顺便提一句有些版本里ctr run会同时创建 container 和 task默认会从容器名推断 task 名也可以手动指定。生产环境建议再带上--net-host、--rm这类参数避免容器退出了还残留任务对象给后续删除 namespace 制造麻烦。3. 多租户架构设计命名、授权与共享模型3.1 命名规范和标签映射多租户改造第一步不是写代码而是定命名规范。我见过太多 namespace 随便建的例子test1、abc、临时这种名字都有。等 namespace 数量超过 20 个谁也不敢动它们因为根本不知道 owner 是谁、是干嘛的。我的建议是采用“环境-业务域”两级结构比如Namespace 名称环境业务域典型用途prod-ai生产AI 推理线上模型服务prod-finance生产财务交易链路容器staging-etl预发数据数据管道联调dev-ai开发AI 训练算法同学调试创建时一定要带 label这是后续自动化的基础ctr namespace create prod-ai --label ownerml-team --label envprod --label serviceinference这样你能随时用脚本把 namespace 清单导出来做资产盘点。有了 owner 信息出了问题能找到人有了 env 标签清理策略可以分辨哪些能回收。还有一个细节namespace 名称一旦创建官方没有提供 rename 操作。如果你起错了名字只能重建。所以命名规范一定要在第一批 namespace 创建前就定好别贪快。3.2 权限隔离怎么做从命令包装到 gRPC 拦截很多人在 containerd 多租户改造时最想知道的是怎么限制租户 A 不能操作租户 B 的资源。这里我说一句实话原生 containerd 不提供用户体系和 RBAC它只提供逻辑隔离不提供身份认证。那生产环境一般怎么做三套方案按成本从低到高排列。第一套命令包装器wrapper。最简单所有租户统一通过一个脚本入口操作 containerd脚本里写死 namespace 参数。比如每个租户的运维人员只能拿到自己的 wrapper 脚本脚本内部是ctr -n $TENANT ...这个模式。优点是立竿见影缺点是只做了“引导”没做“强制”懂命令行的人可以绕过去。第二套gRPC 代理。在 containerd 前面架一层透明的 gRPC 代理客户端连的是代理代理根据凭据自动往请求头里塞 namespace 上下文。这样用户根本接触不到 containerd socket从连接层面就锁死了。适合平台化比较成熟、有完整账号体系的团队。第三套NRINode Resource Interface插件或其他监听插件。containerd 在创建容器前后会触发对应的 NRI 事件你可以在插件里校验容器的 namespace 归属不符合预期的直接拒绝。这种方式把策略下沉到运行时防御力最强但开发成本也最高。我的经验是第一步先把“命令包装器 操作审计”做起来把 namespace 参数从“人肉保证”变成“系统保证”等团队有精力了再向 gRPC 代理演进。一步到位做插件很容易因为策略过严导致线上容器起不来风险太大。3.3 镜像共享与基础镜像管理多租户环境下镜像管理是最容易让人困惑的一块。核心原因是 containerd 的镜像元数据和内容层是分开的内容层blob全局共享元数据image metadata按 namespace 隔离。也就是说租户 A 拉过docker.io/library/redis:7-alpine租户 B 再拉同一个引用时blob 会直接复用不会重新下载下载速度极快。但在租户 B 的镜像列表里确实能看到这条镜像记录因为 containerd 会在租户 B 的 metadata 里新写一条引用。这个特性对基础镜像共享非常有利。我建议两种做法中心预热模式管理员在共享 namespace比如base-images预先拉好所有基础镜像租户首次启动时再在自己的 namespace 里拉一遍。因为 blob 已存在几乎秒级完成只是一次metadata 写入。租户拉取模式每个租户自己拉全量镜像依靠 content store 全局复用虽然多了一次 metadata 查询但实际网络传输为 0 或极少。生产上我更倾向于第一种因为它还能顺便把基础镜像 list 的盘点工作统一到一处。注意这里不是把镜像“复制”到租户 namespace租户 namespace 里仍然要执行拉取动作。containerd 没有提供跨 namespace 的镜像 reference 复制命令不要浪费时间找。3.4 资源配额别指望 namespace 本身前面已经说过namespace 不能限制 CPU 和内存。工业级多租户落地时资源配额一定是在 cgroup 层面做的常见做法是为每个租户预分配一个 cgroup 子目录然后在启动容器时把容器加进去ctr -n prod-ai run --cgroup /sys/fs/cgroup/prod-ai/redis ...不同发行版 cgroup 路径差异很大cgroup v1 和 v2 的挂载路径也不同。这里我不给死命令因为放到你的环境里很可能不适用。核心思路是每个租户固定一条 cgroup 链这样资源统计和限制都天然按租户聚合。这也符合我前面说的理念——namespace 负责“看清是谁”cgroup 负责“限制多少”。如果你的平台是 Kubernetes 之上的资源配额交给 Kubernetes 的 ResourceQuota 即可containerd namespace 层面的配额可以不用管因为 Pod 的资源请求/限制最终会映射到 cgroup。4. 双租户环境从零搭建全流程演示4.1 环境准备与版本确认开始实操前先确认环境。我用的是 containerd 最新稳定版ctr version输出会包含客户端和服务端的版本信息。如果ctr命令不存在检查是否把/usr/local/bin加到了 PATH或者安装时是否只装了 containerd 没装 ctr 工具。有些发行版把 ctr 单独打包比如 Debian 系需要在安装 containerd 之外再确认containerd-ctr包存在。本次演示规划两个租户prod-ai和staging-etl。一个产品线、一个预发数据任务正好覆盖生产与预发两种典型场景。4.2 创建租户并验证元数据隔离依次创建两个 namespace并附上标签ctr namespace create prod-ai --label ownerml-team --label envprod ctr namespace create staging-etl --label owneretl-team --label envstaging验证创建结果ctr namespace list现在关键一步验证隔离。先在 prod-ai 里拉一个镜像然后回到 default 里查看镜像列表ctr -n prod-ai image pull docker.io/library/redis:7-alpine ctr -n prod-ai images list再执行ctr images list你会看到 default 下的镜像列表里没有 redis。这不是没拉成功而是元数据被隔离了。为了验证 blob 确实还在磁盘上你可以对比磁盘占用会发现 prod-ai 拉完镜像后content store 的全局占用增加了而后续在其他 namespace 拉同一个镜像磁盘占用不会再增加。这一步是理解 containerd namespace 的钥匙你看到什么取决于你带的是哪把钥匙而仓库里的货是所有钥匙共享的。4.3 租户中运行容器与任务管理在 prod-ai 租户下运行一个 redis 容器并测试宿主机网络的模式ctr -n prod-ai run --net-host --rm docker.io/library/redis:7-alpine redis这个命令会创建名为redis的容器并启动其 task。加上--rm退出时自动清理容器和任务--net-host用宿主机网络省去 CNI 配置纯演示环境下简单直接。运行起来后另开一个终端查看任务ctr -n prod-ai task list能看到 redis 任务的运行状态。此时切到 staging-etlctr -n staging-etl task list返回为空因为里面没有任务。再测试跨 namespace 操作限制。在 staging-etl 下试图操作 prod-ai 的容器没有直接的命令参数实现必须先切换到 prod-ai 的 namespace。这就是前面说的namespace 是客户端的视图切换它不是 DAC/权限校验真正的权限要靠外部机制控制。最后清理演示容器ctr -n prod-ai task kill redis ctr -n prod-ai container rm redis如果task kill之后容器没有退出等待几秒再查任务状态。实测中 redis 容器对 SIGTERM 响应很快不会僵住。4.4 租户删除与资源回收的正确顺序删除 namespace 是整个流程里最容易翻车的环节。直接执行ctr namespace remove prod-ai大概率会报错提示 namespace 内仍有资源。原因是刚才我们虽然删了容器但 prod-ai 里还有镜像引用。当 namespace 的 metadata 中存在对象时remove 会失败。正确顺序是清理容器与任务清理镜像引用删除 namespace逐个执行ctr -n prod-ai images list ctr -n prod-ai image rm docker.io/library/redis:7-alpine ctr namespace remove prod-ai此时再用ctr namespace list确认prod-ai 已经不存在。这个“先资源后租户”的顺序生产环境自动化脚本里必须严格按这个来。如果租户数量多建议把“租户资源清单导出”做成一个固定动作删除前先导出快照防止误删后无法回溯。5. 生产环境常见问题速查与避坑经验5.1 问题速查表现象常见原因解决办法ctr task list看不到其他租户的容器没带-n参数默认查 default补上-n namespace或检查脚本是否写死 namespace删除 namespace 失败镜像引用、容器、lease 未清理先清镜像/容器/任务再 remove检查 lease 是否过期租户中拉镜像速度极快但仍显示“拉取”blob 已在全局 content store 中正在写 metadata这是正常现象不必担心事件流只有 default 的日志ctr events默认监听 default namespace需要按租户起监听加-n参数容器从宿主机能看到进程但ctr task list看不到可能在别的 namespace用ctr -n namespace task list逐个查询端口冲突频繁发生网络层面未隔离CNI 未按租户分配 IP网络策略必须在 CNI 层做比如每个租户独立子网GC 误删线上镜像层镜像引用被清理且 lease 过期上线前给核心镜像打 leaseGC 前导出引用清单5.2 三条独家避坑经验第一脚本里强制 namespace。所有 containerd 操作脚本第一行就声明export CONTAINERD_NAMESPACE$TENANT。ctr 在较新版本里支持通过环境变量CONTAINERD_NAMESPACE指定默认 namespace但我不建议依赖环境变量因为很容易因为 shell 环境不一致导致静默切 namespace。反而是把-n直接写在每个命令里最保险肉眼可审计。第二lease 是 GC 的护身符。containerd 的 lease 机制很多人不熟悉但它决定了一个 blob 会不会被 GC 回收。给核心镜像关联的 lease 打上标签能防止被自动清理。命令大致是ctr -n prod-ai leases listlease 和 namespace 强绑定跨租户的 GC 策略要认真设计特别是当你在做租户资源回收时lease 的删除顺序必须排在镜像引用删除之后。第三不要建太多细粒度 namespace。有同学把每个应用、每个环境都拆成一个 namespace最后管理成本爆炸。namespace 是逻辑管理单元粒度太细会导致每台机器上几十个 namespace镜像引用维护、事件监控、权限策略全都变成负担。我建议按“业务域 环境”两级控制在 20 个以内应用级别的区分交给容器标签或 Kubernetes 的命名空间而不是 containerd namespace。5.3 从多租户到全链路治理namespace 多租户管理搞定后你其实就拿到了一个关键的抓手**所有运行时对象的归属关系都清洗干净了。**接下来可以顺势把容器平台的全链路治理做起来——按租户维度做成本核算、按租户维度做容量规划、按租户维度做变更管控这些都是基于 namespace 元数据展开的。我自己做完这次改造后最大的体会是containerd namespace 表面上看就是一个隔离维度但它真正值钱的地方是把“谁的东西在跑、谁占了资源、谁出了事”这三个问题一次性回答了。有了归属治理才有对象有了隔离操作才有边界。最后再分享一个小技巧每次给租户创建基础镜像环境时我习惯把租户的 namespace 标签和 CMDB 里的应用 ID 打通。这样无论是排障还是资源回收都能从 containerd 侧反查到业务侧的联系人。工业级容器环境的本质不是技术栈多高级而是出问题时你能在几分钟内定位到责任边界——namespace 就是那条边界的起点。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑