LXC生产化实践:资源限制、网络存储、安全加固与排障备份 1. 资源边界把“能跑”变成“跑得稳”的第一道闸前两篇把 LXC 的命名空间隔离、根文件系统拼装、容器启动流程捋了一遍容器能起来、能登录、能装软件很多人到这一步就收工了。真正上生产之后才会发现能跑起来和跑得稳之间隔着一条很宽的沟——某天一个容器内的编译任务把宿主机的内存吃光触发 OOM 之后被干掉的却是隔壁那个跑着数据库的容器这种事故在只做过实验环境的人看来几乎不可理解但在共享内核的容器体系里是家常便饭。LXC 本身不提供资源隔离的“魔法”它只是把 Linux 内核的 cgroup 能力包装成配置项交到你手上边界划在哪里、划得多细完全取决于你自己。这一篇聚焦的是 LXC 的“后半程”资源限制、网络连通、存储后端、安全加固、故障排查以及长期运行要做的事。内容偏实操涉及的配置项我会尽量给出可直接抄的写法同时解释为什么这么写。适合已经把容器跑起来、准备把它放进真实业务链路的读者也适合那些被容器 OOM、网络不通、快照恢复失败折腾过一轮、想系统补齐知识的人。1.1 cgroup v1 与 v2 的写法差异先把版本对上LXC 的资源限制全部落在 cgroup 上而 cgroup 本身正在从 v1 向 v2 迁移这是配置 LXC 资源限制时最容易翻车的地方。同一个配置项在 cgroup v1 的环境里叫lxc.cgroup.memory.limit_in_bytes到了 v2 环境里必须写成lxc.cgroup2.memory.max写错了不会报错只会静默不生效——容器照样起来限制形同虚设等到出事才发现根本没限住。先确认宿主机跑的是哪套mount | grep cgroup # 输出含 cgroup2 on /sys/fs/cgroup type cgroup2 (rw,...) 纯 v2 # 输出含 tmpfs on /sys/fs/cgroup type tmpfs 且下面挂了一堆 cgroup v1判断完之后两套写法要分开记。下面这张表是我自己贴在笔记里的对照日常查得最多限制项cgroup v1 键名cgroup v2 键名常用取值示例内存上限memory.limit_in_bytesmemory.max10737418241G内存swapmemory.memsw.limit_in_bytesmemory.swap.max536870912CPU 权重cpu.sharescpu.weightv1 默认 1024v2 默认 100CPU 硬配额cpu.cfs_quota_uscpu.maxv2 写50000 100000表示半核进程数pids.maxpids.max512块设备 IOblkio.throttle.read_bps_deviceio.max需带主次设备号在 LXC 配置文件里落地大概是这样v2 环境lxc.cgroup2.memory.max 1G lxc.cgroup2.memory.swap.max 0 lxc.cgroup2.cpu.max 50000 100000 lxc.cgroup2.pids.max 512v1 环境对应的写法lxc.cgroup.memory.limit_in_bytes 1073741824 lxc.cgroup.memory.memsw.limit_in_bytes 1073741824 lxc.cgroup.cpu.cfs_quota_us 50000 lxc.cgroup.cpu.cfs_period_us 100000 lxc.cgroup.pids.max 512注意v1 里memory.memsw.limit_in_bytes一旦设置就必须大于等于memory.limit_in_bytes否则写配置的时候容器直接拒绝启动。v2 里 swap 是独立的一层memory.swap.max 0表示容器内完全不能用 swap这在延迟敏感的服务里通常是想要的效果。判断标准其实很简单容器起来之后进到/sys/fs/cgroup下面看有没有memory.max这个文件有就是 v2看到memory.limit_in_bytes就是 v1。如果容器里什么都看不到说明 cgroup 命名空间没做对lxc.mount.auto里的cgroup:mixed漏了。1.2 内存上限的几个反直觉细节内存限制看着最简单实际是坑最多的一个。第一个反直觉的点memory.max限制的是容器内所有进程的匿名页加文件页缓存不是你想的“应用堆内存”。容器里读一个大文件做处理页缓存也算在这个额度里缓存涨到上限之后内核会先回收干净页回收不动了才触发 OOM。所以你会看到容器内存曲线一路贴着上限走但应用本身其实没什么问题——这是正常的别急着调大。第二个点OOM 杀谁是不确定的。cgroup 的 OOM killer 默认挑oom_score最高的进程容器里哪个进程分高就杀哪个可能是你的业务进程也可能是容器里的日志组件。想要整组一起死、避免半死状态v2 里把memory.oom.group设成 1lxc.cgroup2.memory.oom.group 1这样一旦触发 OOMcgroup 内所有进程一起被干掉容器直接退出交给上层的重启策略处理。比起留一个残缺的进程组在那里假装还活着整组退出反而更好排查。第三个点容器里的free命令看到的数字不可信。容器共享宿主内核/proc/meminfo拿到的是宿主机的全局内存信息不是容器配额。想知道容器还剩多少额度得读 cgroup 文件# v2 cat /sys/fs/cgroup/lxc.payload.c1/memory.current cat /sys/fs/cgroup/lxc.payload.c1/memory.maxLXC 的 cgroup 路径命名在 v2 下通常是lxc.payload.容器名这样的形式具体以宿主机实际目录为准用lxc-cgroup -n c1 memory.current也行省得记路径。第四个点容易被忽略内存限制设得太紧容器里的服务可能启动就挂日志里只留下一句莫名其妙的“初始化失败”。这种情况十有八九是 JVM、Go runtime 这类东西按宿主机的总内存去算堆大小和 GC 并发度一上来就申请超出配额的内存。解决办法是在容器里显式限制运行时参数比如-Xmx、GOMEMLIMIT别指望运行时能自己感知 cgroup 配额各语言的支持程度参差不齐。1.3 CPU 权重和硬配额别混着用CPU 这块有两种完全不同的控制方式很多人搞混。cpu.sharesv1/cpu.weightv2是相对权重只在 CPU 争抢的时候起作用宿主空闲的时候容器可以跑满所有核它不设上限。cpu.cfs_quota_usv1/cpu.maxv2是绝对配额按周期切分时间片容器最多只能用这么多。选择逻辑很直接如果你的容器是批处理、编译、日志压缩这类“有空就跑、跑完就停”的负载用权重就够了给它一个较低的权重让它在别人忙的时候自动退让资源利用率最高。如果是延迟敏感的在线服务必须用硬配额把 CPU 使用封顶避免它把宿主机的其他负载挤掉。硬配额的计算要小心。cpu.max的格式是“配额 周期”两个值单位都是微秒周期一般保持默认 100000100ms。想给容器分配 1.5 个核就是150000 100000。这里有个隐含的坑硬配额设得太低容器内的进程会因为频繁被 throttle 而出现延迟毛刺cpu.stat里的nr_throttled和throttled_time会直线上涨。我见过有人给一个 IO 密集型的服务配了 0.2 核结果 P99 延迟从 20ms 飙到 800ms排查了半天以为是网络问题最后发现是 CPU 被节流了。# v2 查看节流情况 cat /sys/fs/cgroup/lxc.payload.c1/cpu.stat | grep throttled还有一个组合技巧cpuset.cpus绑定核心配合硬配额能把容器固定在几个核上减少跨核缓存失效。对延迟敏感的服务效果明显代价是宿主机 CPU 调度的灵活性下降几个核被固定占用之后其他负载只能挤在剩下的核上。要不要这么做取决于你的宿主机上还跑着什么。2. 网络打通从默认 NAT 到桥接直连的三条路容器网络是第二个高频踩坑区。LXC 默认装好之后会有一个lxc-net服务自动建一个lxcbr0网桥网段 10.0.3.1/24容器通过 DHCP 拿到地址出去靠 iptables 的 MASQUERADE 做地址转换。这套默认配置对于“容器要上网下载东西”这种需求完全够用但一旦涉及容器对外提供服务、容器之间互相访问、容器访问宿主机所在的内网资源就必须理解它到底是怎么连的否则只能瞎试。我自己的经验是先把默认链路拆清楚再根据需求往桥接或者端口映射上走。三步走的顺序不要反因为桥接模式一旦配错容器会完全断网排查成本比 NAT 模式高得多。2.1 默认 NAT 链路的完整拆解容器起来之后宿主机上会多出一对 veth 设备名字像vethXXXXif2一头挂在lxcbr0上另一头被塞进容器的网络命名空间里容器内看到的是eth0。这对 veth 就是一根虚拟网线两端的帧直接互通。# 宿主机侧 ip link show master lxcbr0 # 容器侧 lxc-attach -n c1 -- ip addr show eth0地址分配靠lxc-net拉起来的 dnsmasq配置文件一般在/etc/default/lxc-net可以改网段和 DHCP 范围LXC_BRIDGElxcbr0 LXC_ADDR10.0.3.1 LXC_NETMASK255.255.255.0 LXC_NETWORK10.0.3.0/24 LXC_DHCP_RANGE10.0.3.100,10.0.3.200 LXC_DHCP_MAX100改完重启lxc-net服务生效。这里有个血泪教训改网段的时候如果宿主机自己已经有一条到 10.0.3.0/24 的路由比如公司内网正好用这个段veth 的路由会跟物理网卡打架容器看起来有 IP 但完全不通。改之前先ip route扫一眼避开常用内网段。出网方向靠的是 netfilter。lxc-net启动时会自动加几条规则核心就一条 MASQUERADE把源地址 10.0.3.0/24 发往非 lxcbr0 的包做源地址转换改成宿主机的出口地址。前提是ip_forward开着sysctl -w net.ipv4.ip_forward1 # 持久化 echo net.ipv4.ip_forward1 /etc/sysctl.d/99-lxc.conf容器能 ping 通宿主机但出不去八成是ip_forward没开或者宿主机的防火墙把 forward 链默认策略设成了 DROP。我遇到过宿主机装了某套防火墙管理工具重启之后自动把 FORWARD 链刷成 DROP容器集体断网lxc-net服务没重跑规则也就没恢复。后来我把lxc-net加到开机自启的强制顺序里并且写了个健康检查脚本每五分钟验证一次容器出网是否正常不正常就重启服务。2.2 桥接模式把容器当成一台真实主机NAT 模式最大的问题是容器拿的是私有地址外部访问不进来容器之间跨宿主机也无法直接通信。要解决这个就得把容器接到宿主机所在的物理网络上让它在二层上就是一台独立主机。做法是把物理网卡上的 IP 挪到一个新网桥上物理网卡只做桥接端口# 假设物理网卡是 eno1 ip link add name br0 type bridge ip link set eno1 master br0 ip addr del 原IP/24 dev eno1 ip addr add 原IP/24 dev br0 ip link set br0 up ip link set eno1 up ip route add default via 网关 dev br0这套操作在远程主机上做有断连风险做完立刻生效中间有几十秒不通。稳妥的做法是写好脚本用nohup跑并且带上失败回滚逻辑——我一般会先在一个不重要的测试机上把脚本跑通确认能自动恢复连接再往正式机上放。桥搭好之后容器配置改成接 br0lxc.net.0.type veth lxc.net.0.link br0 lxc.net.0.flags up lxc.net.0.hwaddr 00:16:3e:aa:bb:cc lxc.net.0.ipv4.address 192.168.1.50/24 lxc.net.0.ipv4.gateway 192.168.1.1固定 MAC 很重要。用 DHCP 的时候如果不指定硬件地址每次重建容器 MAC 都会变交换机的 MAC 表跟着抖地址池也可能分配给不同容器导致 IP 漂移。手动指定一个固定 MAC配合 DHCP 静态绑定或者直接写死 IP行为才可预期。桥接模式的另一个注意点容器直接暴露在物理网络上宿主机防火墙管不到它。容器内的服务如果监听 0.0.0.0 并且没有自己的访问控制等于直接对全网开放。用 NAT 模式时你还能在宿主机的 iptables 上统一收口桥接之后就没有这层保护了安全责任下放到容器内部这一点必须提前想清楚。2.3 端口映射与容器互访的实操细节不想动宿主机网络配置又需要对外暴露服务端口映射是最省事的路子。原理是在宿主机上加一条 DNAT 规则把到宿主机某端口的流量转发到容器的 IP 和端口iptables -t nat -A PREROUTING -p tcp --dport 8080 \ -j DNAT --to-destination 10.0.3.101:80要记得配套的 SNAT否则容器回包的时候源地址是它自己的 10.0.3.101客户端收到一个和自己请求目标不一致的源直接丢弃iptables -t nat -A POSTROUTING -s 10.0.3.0/24 -o lxcbr0 \ -j MASQUERADE这条规则lxc-net默认就会加所以多数情况下只写 DNAT 就够了。真正容易漏的是容器内部的回程路由——如果容器里有自定义路由表把默认网关指向了别处回包就会走错路径表现出来就是“连上了但没数据”。这时候在容器里ip route get 客户端IP看一眼就知道。容器互访本身是通的同一个网桥下的 veth 二层直连不需要额外配置。麻烦在于容器名解析LXC 不会自动给容器配 DNS 记录dnsmasq 只对静态分配或者 DHCP 租约内的地址有记录。想用名字访问就在/etc/hosts里手工加或者在 dnsmasq 配置里写死# /etc/lxc/dnsmasq.conf dhcp-hostc1,10.0.3.101 dhcp-hostc2,10.0.3.102我一般倾向于固定 IP 加宿主机 hosts 的方式最直白不依赖任何服务的运行状态。域名解析这套东西在容器环境里出问题的概率比想象中高能把一个环节去掉就去掉。提示容器的lxc.net.0.ipv4.gateway auto这个写法在 LXC 3.x 之后才支持老版本需要写具体的网关地址。升级 LXC 的时候配置文件里如果还留着旧写法容器可能会起不来升级前先扫一遍所有容器的配置。3. 存储后端选型快照能力比读写速度更值得先看建容器的时候-B参数决定存储后端dir、btrfs、zfs、lvm还有 overlayfs 之类的组合。大多数人第一次装的时候随手用了默认的 dir后来发现想要的很多功能都用不了回头再迁就麻烦了。选型的时候读写性能反而是次要的——容器这种负载性能瓶颈很少卡在存储后端的抽象层上真正拉开差距的是快照、克隆、配额这几个管理能力。我给出的判断顺序是先看你要不要快照再看你的磁盘空间怎么分配最后才看性能。三者顺序反了后期改动成本会非常高。3.1 四种后端的能力对照后端快照克隆容量配额适用场景dir不支持支持全量拷贝靠文件系统实验环境、容器数量少btrfs支持秒级支持写时复制子卷配额需要频繁快照和快速克隆zfs支持秒级支持写时复制数据集配额同上且对数据完整性要求高lvm支持依赖剩余空间支持逻辑卷大小已有 LVM 体系的环境dir后端下执行lxc-snapshot会直接报错退出提示不支持。这不是配置问题是设计上就没实现——dir 后端下容器就是一个普通目录没有任何“层”的概念做快照只能全量复制成本太高所以干脆不支持。想用快照就得把容器建在支持写时复制的后端上。这里要强调一句换后端不是改一行配置的事得重新建容器再把数据迁过去。所以第一次建容器之前就要想清楚。我见过不止一个团队先图省事用了 dir跑了半年发现要做批量克隆测试环境只能停下来做迁移迁移期间服务停了两天。3.2 快照与克隆的实操链路btrfs 后端建容器只需要多一个参数lxc-create -n c1 -B btrfs -t download -- \ --dist ubuntu --release jammy --arch amd64快照lxc-snapshot -n c1 # 创建一个快照 lxc-snapshot -n c1 -L # 列出所有快照 lxc-snapshot -n c1 -r snap0 # 回滚到 snap0 lxc-snapshot -n c1 -d snap0 # 删除 snap0回滚这个操作是破坏性的当前状态会丢。稳妥的做法是回滚前先打一个“回滚前”的快照相当于给自己留个后悔药lxc-snapshot -n c1 -c before-rollback lxc-snapshot -n c1 -r snap0克隆走的是lxc-copylxc-copy -n c1 -N c1-test -s # -s 表示快照式克隆写时复制 lxc-copy -n c1 -N c1-backup -B overlayfs-s参数只在支持写时复制的后端上有效克隆出来的容器跟源容器共享数据块改动时才分配新空间几秒钟就能复制出一个完整容器。做自动化测试的时候这个能力价值极大准备一台“基准容器”装满依赖和测试数据每次跑测试克隆一份跑完删掉互不干扰。有一个细节要提醒写时复制的克隆源容器和克隆容器共享底层数据块源容器上的快照一旦被删掉克隆容器里那部分数据可能会受影响。LXC 在这方面做了保护但跨后端行为不完全一致。我的做法是克隆出来的容器尽快“独立”或者在规划时就把基准容器的快照保留足够长时间。3.3 容量规划和 inode 的隐性坑btrfs 和 zfs 上给容器做容量限制用的是子卷/数据集配额跟 dir 后端靠文件系统的配额完全不是一套东西。btrfs 上要开 quotabtrfs quota enable /var/lib/lxc # 设置单个子卷上限 btrfs qgroup limit 20G /var/lib/lxc/c1/rootfs不开 quota 的话单个容器的日志可以一路涨到把整个存储池写满所有容器一起挂掉。这是我最推荐新手立刻做的一件事成本极低收益极高。inode 的问题在 dir 后端上尤其突出。容器里跑包管理器装一堆小文件df -h显示还有 80% 空间df -i已经 100% 了。表现出来就是各种“设备上没有空间”但看起来明明有空间。btrfs 上的 inode 是动态分配的不存在固定上限但小文件同样会占用 metadata 空间耗尽之后一样写不进去。df -h /var/lib/lxc # 看块使用 df -i /var/lib/lxc # 看 inode 使用 btrfs filesystem usage /var/lib/lxc # btrfs 的详细使用情况给容器内的日志做限制比事后清理有效得多。容器里如果是 journald配置SystemMaxUse200M如果是自建日志用 logrotate 按天切分并按数量保留。这件事我一般放在容器模板里做新建的容器天生带日志上限省得每个都去调。4. 共享内核的安全账idmap、能力裁剪与系统调用过滤LXC 和虚拟机的根本差别在于共享内核。虚拟机的隔离边界是硬件虚拟化客机内核崩了不影响宿主容器里一个提权漏洞就可能直接打通宿主机。这个差异意味着容器的安全配置不是“可选项”而是必须做的基本功。默认配置已经做了不少工作但默认值的设计目标是“兼容性优先”很多该收紧的地方没收紧。安全加固这件事思路是做减法容器里不需要的东西全部去掉。不需要 UID 0就做 idmap不需要的能力全部 drop不需要的系统调用全部拦掉。减法做完了即使容器里跑的东西被人拿下了能造成的破坏也有限。4.1 UID/GID 映射让 root 不再是 root特权容器里容器内的 rootUID 0就是宿主机的 root。这意味着容器里任何一个提权漏洞都直接等价于宿主机 root 权限风险等级和“把 root 密码贴在网上”差不多。解决办法是 UID 映射把容器内的 UID 0 映射到宿主机上一个无特权的高位 UID。先确认 subordinate id 配置grep root /etc/subuid grep root /etc/subgid # 输出类似 root:100000:65536然后在容器配置里写映射lxc.idmap u 0 100000 65536 lxc.idmap g 0 100000 65536这段的意思是容器内 UID 0 到 65535对应宿主机 100000 到 165535。容器内的 root 在宿主机上看起来是 UID 100000一个普通用户什么特权都没有。即便容器里有人拿到了 root他在宿主机上根本无法操作任何属于真实 root 的文件。验证映射是否生效最直接的办法是在容器内建个文件到宿主机上看所有者lxc-attach -n c1 -- touch /tmp/probe ls -ln /var/lib/lxc/c1/rootfs/tmp/probe # 应该显示 100000 100000而不是 0 0注意映射会带来一些副作用。容器内需要访问的宿主机共享目录权限得按映射后的 UID 去设不能按容器内的 UID 设。另外某些需要在容器内做真正特权操作的服务比如要操作块设备、要改网络配置在非特权容器里会失败这类服务要么改成不需要特权的方式跑要么单独放一台特权容器不要因为“麻烦”就把所有容器都退回特权模式。还有一点容易忽略非特权容器在宿主机侧的文件全部归 UID 100000 所有备份、迁移、清理的时候要带着这个认知。用rsync -a备份倒是没问题但手工rm -rf某些目录可能会因为权限不足失败得用 root 或者调整映射范围。4.2 capability 裁剪的最小化思路Linux 把 root 的特权拆成了几十个 capability容器的能力裁剪就是决定容器内的 root 保留哪些。LXC 默认已经 drop 了一批高危的比如sys_module、sys_rawio、sys_boot但默认清单是为了兼容大多数场景设计的还可以更严。先看容器当前有哪些能力lxc-attach -n c1 -- capsh --print然后按“容器内跑的是什么”来裁剪。一个只跑 Web 服务的容器sys_admin、sys_ptrace、net_admin、mknod基本都用不上可以全 droplxc.cap.drop sys_admin sys_ptrace net_admin mknod \ sys_module sys_rawio sys_boot sys_time \ mac_admin mac_override裁剪的过程建议是逐步来别一次全砍。改完重启容器观察应用日志有没有权限相关的报错跑一遍功能测试没问题再继续砍下一批。一次性砍太多出问题之后不知道是哪一项导致的回退起来很痛苦。注意net_admin去掉之后容器里就不能再自己配置网络、创建网桥或者调 iptables 了。如果容器里跑着需要管理网络的组件比如某些监控 agent、容器内的容器这一项必须保留。还有一个经常被忽略的点sys_time被 drop 之后容器内无法修改系统时间date -s会失败。容器共享宿主内核时间本来就是全局的容器改时间等于改宿主机时间这个能力必须去掉否则一个容器里的误操作就能把整台机器的时间搞乱影响所有容器。4.3 seccomp 与 AppArmor 的组合能力裁剪管的是“root 能做什么”seccomp 管的是“进程能调用哪些系统调用”AppArmor 管的是“进程能访问哪些路径”。三者层次不同组合起来才构成完整防线。seccomp 的白名单配置长这样每行一个规则格式是“动作 系统调用名”# /var/lib/lxc/c1/seccomp.profile whitelist accept read accept write accept open accept close accept stat accept fstat accept mmap accept munmap accept exit_group # ... 按需补充白名单模式的写法工作量很大跑一个稍复杂的应用就要补几十条。我的实际做法是先用黑名单模式拦住明显危险的调用比如mount、umount2、reboot、kexec_load、ptrace、init_module这些在业务容器里基本用不到拦掉不影响功能blacklist reject mount reject umount2 reject reboot reject kexec_load reject ptrace reject init_module reject delete_moduleLXC 4.0 之后默认使用生成的 AppArmor profile配置里写lxc.apparmor.profile generatedLXC 会自动为容器生成一份受限的策略限制容器对/proc、/sys的写权限以及对特定文件的访问。这份 profile 在大多数场景够用不需要自己从零写。真正要排查的时候AppArmor 的拒绝记录在dmesg或者/var/log/syslog里关键字是apparmorDENIED。容器里某个操作莫名其妙失败、日志里没有任何线索的时候这是我第一个去翻的地方。见过太多次“容器里跑不起来但看不出原因”最后发现是 AppArmor 把某条路径访问拦了。5. 排障实录容器起不来的三条典型链路容器排障和普通服务排障的思路不太一样因为失败可能发生在四个层次上宿主机内核层、LXC 管理层、容器内 init 层、应用层。每层的日志位置不同从下往上还是从上往下查决定了你要花十分钟还是两个小时。我个人的习惯是先看容器自己的日志没有线索再往下挖因为大部分问题出在上面两层。下面三条链路是我这些年重复遇到最多的把排查顺序记熟了绝大多数问题能在几分钟内定位。5.1 启动即退出先看哪份日志lxc-start -n c1执行完没有任何输出lxc-info -n c1显示 STOPPED这种情况几乎每天都在发生。第一件事是前台启动并打开调试日志lxc-start -n c1 -F -l DEBUG -o /tmp/lxc-c1.log-F是前台运行日志直接打到终端错误信息立刻可见。-o把详细日志落盘方便事后翻。绝大多数启动失败在这个阶段就能看到原因比如配置项写错、rootfs 路径不存在、某个必须的目录没挂上。如果前台启动也没报错容器起来了但立刻又退出问题就在容器内部。这时候看容器自己的日志# 容器 rootfs 下的日志取决于发行版 tail -n 100 /var/lib/lxc/c1/rootfs/var/log/syslog journalctl --directory/var/lib/lxc/c1/rootfs/var/log/journal -n 100容器内 init 起不来最常见的两个原因一是 rootfs 权限不对比如非特权容器里某些文件的所有者还是 UID 0映射之后容器内访问不了二是/dev下的设备节点缺失比如/dev/null不存在很多程序一启动就挂。第二个问题在手工拼 rootfs 的时候特别常见用官方模板建容器基本不会遇到。还有一类失败是 cgroup 相关的。dmesg 里能看到cgroup: ... failed to ...之类的记录。这种情况在 cgroup v1/v2 混用的系统上尤其容易出现比如内核已经切到 v2 但 LXC 版本太老还在往 v1 的路径写。检查方法很简单看 LXC 版本和 cgroup 挂载是否匹配lxc-info --version mount | grep cgroup5.2 网络不通的排查顺序容器网络不通排查顺序不能乱从下往上逐层验证每层确认了再往上一层走容器内ip addr有没有拿到地址容器内ip route默认路由指向哪里容器能不能 ping 通网关默认是 10.0.3.1宿主机能不能 ping 通容器容器能不能 ping 通外网 IP比如 1.1.1.1容器能不能解析域名这六步能覆盖九成以上的网络问题。走到第二步发现默认路由为空那就是 DHCP 没生效走到第三步不通问题在网桥或者 veth 上走到第五步不通但第三步通基本可以锁定在ip_forward或者 NAT 规则上走到第六步才失败纯 DNS 问题。具体到命令# 容器内 lxc-attach -n c1 -- ip addr show eth0 lxc-attach -n c1 -- ip route lxc-attach -n c1 -- ping -c 2 10.0.3.1 # 宿主机 ip link show master lxcbr0 iptables -t nat -L POSTROUTING -n -v sysctl net.ipv4.ip_forwarddnsmasq 挂掉是另一个高频原因。lxc-net服务跑着但 dnsmasq 进程可能因为端口冲突或者配置错误没起来容器拿不到地址。systemctl status lxc-net和ps aux | grep dnsmasq一起看。还有一种情况是容器的 MAC 地址跟网络上已有的设备冲突DHCP 分配的地址反复跳变容器一会儿通一会儿不通这种最恶心只能靠日志里的 DHCP 记录去对。5.3 空间够却写不进去inode 耗尽这个坑前面提过但值得单独拎出来讲因为它伪装得太好了。应用报 “No space left on device”df -h一看还有几十 G直觉会往配额、权限、挂载点上想想不到 inode。df -i /var/lib/lxc # Filesystem Inodes IUsed IFree IUse% Mounted on # /dev/sda1 6553600 6553600 0 100% /上面这个输出就是典型inode 用满了。清理思路是找出哪个目录小文件最多# 统计各目录文件数按数量排序 for d in /var/lib/lxc/*/rootfs/*; do echo $(find $d -xdev 2/dev/null | wc -l) $d done | sort -rn | head -20最常见的元凶是包管理器的缓存、日志目录、以及容器里跑的构建任务留下的临时文件。临时治标的办法是删缓存治本得从模板层面控制——在容器模板里就配好日志上限构建类任务用独立的临时目录并定期清理。btrfs 上虽然没有 inode 上限但 metadata 空间耗尽会报一样的错。btrfs filesystem usage能看到 Data 和 Metadata 的分项占用如果 Metadata 那块满了就需要做 balancebtrfs balance start -m /var/lib/lxc这个操作耗时较长而且对在线业务有 IO 影响安排在低峰期做。6. 长期运行监控指标、备份策略与内核升级的连带影响容器跑起来、网络通了、安全收紧了接下来是长期维护阶段。这一阶段没有“完成”的时刻只有持续的关注点。我见过太多项目上线时配置做得挺漂亮跑了一年之后出问题回头一看监控没有、备份没做、内核版本还是三年前的这时候任何一个环节出问题都是灾难。这一段讲三件事怎么看容器的运行状态、备份到底该怎么做、以及宿主内核升级对容器意味着什么。三件事都属于“平时不做也没事出事时无比后悔”的类型。6.1 用 cgroup 原始指标做轻量监控LXC 没有内置的监控体系它把状态暴露在 cgroup 文件系统里你需要自己去读。好处是不依赖任何 agent坏处是要自己写点脚本或者配采集器。常用的几个指标和对应的文件以 cgroup v2 为例指标文件路径说明当前内存用量memory.current含页缓存内存上限memory.max配置值内存事件memory.eventsOOM 次数、达到上限次数CPU 使用cpu.stat含usage_usec累计时间CPU 节流cpu.statnr_throttled、throttled_usec进程数pids.current当前进程/线程数memory.events里的oom和oom_kill两个计数是我最看重的。它们只要不为零就说明容器被内存限制压过一定要去查原因——要么限制定得太低要么应用有内存泄漏两种都得处理。采集方式上最轻量的做法是写个 shell 脚本每分钟读一遍输出成 Prometheus 的文本格式交给 node_exporter 的 textfile collector 收走#!/bin/bash OUT/var/lib/node_exporter/textfile/lxc.prom echo -n $OUT for cg in /sys/fs/cgroup/lxc.payload.*; do name$(basename $cg | sed s/^lxc.payload.//) mem$(cat $cg/memory.current 2/dev/null || echo 0) oom$(awk /^oom_kill /{print $2} $cg/memory.events 2/dev/null || echo 0) echo lxc_memory_current_bytes{container\$name\} $mem $OUT echo lxc_oom_kill_total{container\$name\} $oom $OUT done这套东西写一次能一直用比装一整套重型监控方案划算得多。当然如果团队本来就有完善的监控体系直接接进去更好这个脚本的意义在于让你知道数据从哪来。提示容器的memory.current包含页缓存和“应用实际用了多少内存”不是一回事。做容量规划的时候要结合memory.stat里的anon和file两项分开看只看总数容易高估。6.2 快照不等于备份这一条我要说得重一点。快照和备份是两个概念混为一谈的人不在少数。快照存在同一个存储池上存储池挂了快照一起挂误删源数据之后有些后端上的快照也会受影响更别提快照从来不是为“恢复到另一台机器”设计的。备份要满足三个条件异地、离线、可验证。异地是说不能和源数据在同一块盘、同一台机器上离线是说备份介质不被正常运行的系统挂载着避免被误删或者被加密勒索可验证是说定期做恢复演练确保备份真的能用。具体到 LXC我的做法是两条腿走第一条是配置和元数据的备份这些体积小、变化少tar czf /backup/lxc-config-$(date %F).tar.gz \ /var/lib/lxc/*/config /etc/lxc/ /etc/default/lxc-net第二条是容器数据的备份。在线状态下直接拷 rootfs 是不安全的文件可能在拷贝过程中被改动恢复出来是坏的。稳妥的做法是停容器再拷lxc-stop -n c1 tar czf /backup/c1-$(date %F).tar.gz -C /var/lib/lxc/c1 rootfs lxc-start -n c1如果停机不可接受就用快照配合导出先在存储层打一个快照再从快照挂载一个只读视图去打包这样源容器不用停。btrfs 和 zfs 都支持这种玩法代价是要多一份临时空间。恢复演练这件事我建议至少每季度做一次。挑一个容器从备份完整恢复到一台干净的宿主机上跑通业务流程。没做过恢复演练的备份和没有备份的区别只在于心理安慰。6.3 宿主内核升级对容器意味着什么容器共享宿主内核这句话的推论是宿主内核一升级所有容器都跟着换内核没有任何过渡期。这一点和虚拟机完全不同虚拟机里你可以先升级一台试试容器做不到。内核升级必然伴随宿主机重启重启就意味着所有容器中断。常规的滚动升级策略在这里失效只能安排维护窗口。如果业务对中断极其敏感思路有两个一是把关键容器做成多宿主机冗余一台重启时另一台接管二是用支持内核热补丁的方案处理安全更新把需要重启的完整升级推迟到计划窗口。还有一个更隐蔽的连带影响升级内核之后某些容器的行为可能变了。比如某个系统调用的语义调整、某个 cgroup 接口的变化、某个默认参数改了。这类问题不会在升级当下暴露而是在某个业务路径上悄悄出错。我的做法是维护一个“升级检查清单”每次内核升级前后跑一遍# 升级前记录基线 for n in $(lxc-ls -1); do lxc-info -n $n -s lxc-attach -n $n -- uname -a done /tmp/baseline-before.txt # 升级重启后对比 # 重点看哪些容器没起来、哪些容器内服务状态变了清单里固定几项所有容器是否正常启动、关键服务的健康检查是否通过、容器内时间是否正常、网络是否通、日志里有没有新的 AppArmor 拒绝记录。这几项覆盖了内核升级最常见的连带问题。另外提醒一句宿主内核升级时正在运行的容器最好先全部停掉再升级、再启动而不是边跑边升。有些发行版的包管理器在升级内核相关的用户态组件比如 cgroup 工具、LXC 自身时会在后台触发容器状态异常混在一起升级导致的问题极难定位。最后再分享一个我在实际运维里养成的小习惯给每个容器在配置文件里加一行注释写清楚这个容器的用途、负责人、资源配额的理由。看起来很土但当你半年后回来看一个lxc.cgroup2.memory.max 4G时如果没有注释你根本不知道这个 4G 是拍脑袋定的还是算出来的也就无从判断该不该改。容器多了之后配置文件的注释质量往往决定了后期维护的痛苦程度。