LXC 不是小号虚拟机:五个在容器里真撞过的边界

LXC 不是小号虚拟机:五个在容器里真撞过的边界

托管业务跑在 PVE 的 LXC 上,一开始我以为容器就是”轻一点的虚拟机”,直到一连串问题用同一种方式教育我——容器共享宿主内核,你以为的隔离边界,有一半是假象。

下面五个坑,都是在这套系统上实际踩出来的,不是抄来的。

一、内核是共享的,模块装不进去

虚拟机里 modprobe 随便装,容器里不行。容器没有自己的内核,它看到的 /lib/modules 是宿主的,而 CAP_SYS_MODULE 默认是收着的。

第一次撞上是在容器里想开 overlay 做镜像层缓存:


$ modprobe overlay
modprobe: ERROR: could not insert 'overlay': Operation not permitted

解法不是加权限,是提前在宿主上确认模块已加载:


# 宿主机执行
lsmod | grep -E 'overlay|nf_tables'

同理,容器里的 iptables 能不能用,取决于宿主内核编了哪套后端。宿主用 nf_tables,容器里的老 iptables-legacy 就会报 can't initialize iptables table。这类问题排查方向永远是「往上找一层」,不是「往下加参数」。

二、systemd 不是标配,timer 要留退路

我们的容器要做一件事:每 10 分钟上报一次自身信息。方案很自然——装个 systemd timer。

问题在于,模板容器不一定带 systemd。有些精简镜像的 PID 1 是 bash 或者 tini,systemctl 直接不存在。脚本第一天在 A 机器跑得好好的,换到 B 机器就静默不执行,日志里一个字都没有。

现在的初始化脚本改成两段式:


if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
    install -m 644 lihua-report.service /etc/systemd/system/
    install -m 644 lihua-report.timer   /etc/systemd/system/
    systemctl daemon-reload && systemctl enable --now lihua-report.timer
else
    # 退回 cron,注意容器里 cron 也未必在跑
    ( crontab -l 2>/dev/null; echo "*/10 * * * * /usr/local/bin/lihua-report.sh" ) | crontab -
    pgrep -x cron >/dev/null || (service cron start || /etc/init.d/cron start) 2>/dev/null
fi

判断 [ -d /run/systemd/system ] 比 command -v systemctl 更可靠——有些镜像装着 systemctl 二进制,但没真的以 systemd 启动。

三、内存和 CPU 看到的是宿主的脸

容器里的 /proc/meminfo、/proc/cpuinfo 默认显示宿主的数据,不是配额。这直接坑了我们的 Lynis 巡检:在一台 1G 内存的容器里,Lynis 读到宿主有 64G,判定内存充足,给出一个漂亮但毫无意义的分数。

要让它看到真实配额,得在容器配置里挂 lxcfs:


lxc.mount.entry: /var/lib/lxcfs/proc/meminfo proc/meminfo none bind,ro,create=file 0 0
lxc.mount.entry: /var/lib/lxcfs/proc/cpuinfo proc/cpuinfo none bind,ro,create=file 0 0

没挂 lxcfs 之前,任何”容器内资源采集”类的功能都不可信。这条经验后来被写进了上报脚本的字段说明里。

四、扩容要两头做

给容器加磁盘,只做一步是不够的:


# 宿主侧:把虚拟盘撑大
pct resize 212 rootfs +10G

# 容器侧:文件系统得自己长
resize2fs /dev/mapper/pve-vm--212--disk--0

只做第一步,df -h 纹丝不动,用户会以为你没处理。早期有过一次工单,用户说”扩容了但还是满的”,查下来是第二步漏了。

如果是 LVM-thin,还得注意 pct resize 之后 thin pool 的剩余空间,lvs 里 Data% 超过 85% 就该停了。

五、网络那一半在宿主身上

容器里的 eth0 是 veth 的一端,另一端在宿主上。这意味着:

想做的事 在哪一侧做
改 IP / 网关 容器内 /etc/network/interfaces 或 netplan
抓包看真实流量 宿主上的 vethXXX
限速 宿主 tc,容器里无效
看对端 MAC 宿主 ip link
防火墙 两头都可能有,理清哪层先生效

我们做多租户隔离时最费劲的就是这一层——容器里写的规则感觉生效了,实际流量在宿主那侧就已经被放行或丢弃。排查时先 ip link 在宿主找到对应的 veth,再 tcpdump -i vethXXX,比在容器里猜快十倍。

什么时候该用 KVM

不是所有业务都适合容器。判断标准很朴素:

  • 需要独立内核(自定义模块、特定内核版本、eBPF 重度使用)→ KVM
  • 需要嵌套虚拟化(里面还要跑 Docker 的 Docker)→ KVM,或者至少开 nesting=1 并接受性能损失
  • 资源隔离要求硬(防邻居吵闹)→ KVM,容器共享内核调度,cgroup 只能缓解不能消灭
  • 只是跑个 Web 服务、定时任务、轻量数据库 → LXC,启动快、内存省、备份小

我们现在的做法是分层:控制台和数据库跑 LXC,需要跑嵌套容器的用户机器用 KVM。混合部署的代价是两套监控和两套备份策略,但比强行统一要省心。

一句话总结

容器的问题几乎都能归纳成同一句:你在里面看到的东西,来源在外面。遇到怪现象,先往上找一层——宿主内核、宿主 cgroup、宿主网络、宿主存储。这一条比任何具体命令都值钱。

💬 留下你的爪印