在Xen虚拟化环境里待久了,你总会碰到一些“玄学”内存问题。明明宿主机的物理内存看着还挺多,结果某个时间点突然被吃光,系统直接触发OOM Killer,把无辜的进程给杀了。更气的是,你重启一下又好了,但过一阵子又来一次。今天我们就来聊聊一个典型场景:DomU(虚拟机)因为没启用气球驱动(Balloon Driver),导致宿主机内存像吹气球一样膨胀,最后把整个机器拖垮。我会把整个过程、排查思路、解决办法以及应急手段都讲明白,用的都是咱们平时能直接用上的命令和配置,不搞虚的。
一、问题背景:虚拟机内存怎么会突然爆掉?
先描述一下现场。你有一台物理服务器当Xen宿主机,上面跑了几个DomU。其中有一个专门的业务虚拟机,里面跑着Java应用,堆内存设置得很大。正常情况下,这个DomU向宿主要了比如8GB内存,但实际可能只用4GB,剩下的4GB应该能还给操作系统,让宿主机分给别的虚拟机或者缓存用。
可是有一天,你发现宿主机变得特别卡,SSH敲命令都延迟几秒。用free -h一看,内存所剩无几,然后紧接着你的关键进程一个一个被杀掉,系统日志里全是Out of memory的报错。你第一反应是“是不是谁干了啥坏事”,但查了一圈,发现并不是应用的问题,而是宿主机内存根本不够分配了。再仔细看,罪魁祸首竟然是那个只有8GB配额的DomU,它居然在宿主机上占用了接近12GB的物理内存!这多出来的4GB哪来的?这就是Balloon机制失效的典型表现。
二、排查过程:一步一步找元凶
2.1 先从系统日志里找OOM Killer的脚印
遇到这种事,别慌。咱们第一件事就是去翻内核日志,看看OOM Killer到底杀了谁,以及它当时的判断依据是什么。
# 用dmesg查看内核环形缓冲区中的OOM信息
# -T 参数显示可读时间,方便对照事故时间点
dmesg -T | grep -i "out of memory" | tail -n 20
如果日志被刷掉了,别急,/var/log/messages 或者 /var/log/kern.log 里也都有。
# 在系统日志中查找包含 OOM 的信息
# 这里用grep加上上下文行数,可以看的更清楚
grep -i "killed process" -B 5 -A 5 /var/log/kern.log | tail -n 50
你会发现OOM Killer选择了一个内存占用最大的进程,比如Java的PID。但问题在于,这个Java进程其实只占了自己虚拟机里的内存,怎么会在宿主机上被看成是“最大的坏蛋”呢?因为Xen的DomU内存是直接映射到宿主机的,如果Balloon驱动没工作,宿主机看到的那个DomU的内存页就是被占用的状态,这些页没法被回收,最后宿主机整体可用内存就少了。
2.2 看看宿主机和各个虚拟机分别占了多少内存
接着我们用xm top或者xl top(取决于你用的哪个工具)来实时查看各DomU的内存占用情况。因为命令可能是xl,这里以xl为例。
# 用xl top查看虚拟机和宿主机的内存使用情况
# 会显示动态刷新的列表,按q退出
xl top
输出里你会看到类似这样的表格:
NAME MEM MAXMEM ... STATE
Domain-0 1.2G ... r-----
Domain-1 11.8G 8.0G -b----
注意看Domain-1的MEM是11.8G,而MAXMEM是8G。这就是不对了,它超过了上限。这说明这个DomU占用的内存远远超出了它被分配的最大值。为什么能超过?因为Balloon驱动没有启用,Xen无法通过balloon机制把那些未使用的内存页“压回”宿主机,同时DomU内部认为自己还可以继续分配内存,就一直在宿主机的物理内存里摊大饼。
2.3 矛头指向气球驱动
Balloon驱动是个啥?简单说,它就像一个可伸缩的气球放在虚拟机里面。宿主机想让虚拟机少用点内存,就吹气球,气球一胀,虚拟机内部的内存就被挤得“瘪”了,内核会把空闲页还给宿主机。想让虚拟机多用点内存,就放气,气球变小,虚拟机就能自己使用更多内存。
如果虚拟机里的Balloon驱动没加载,那么宿主机只能看到虚拟机一直占着所有它曾经用过的物理内存,没法回收。这个“没加载”的原因很多,可能是内核没编译进驱动,可能是驱动没有加载成功,也可能是你用了精简版内核。我们可以在DomU内部检查一下:
# 在DomU内部查看balloon驱动是否加载
# 如果有输出说明加载了,如果没有则说明没加载
lsmod | grep xen_balloon
如果显示xen_balloon确实没有,那就实锤了。另外还可以看看内核配置。
# 查看当前内核是否支持xen_balloon
# 如果输出y或m表示支持,如果是n就表示没编进去了
cat /boot/config-$(uname -r) | grep CONFIG_XEN_BALLOON
2.4 确认根因:内存膨胀
现在我们把线索串起来。宿主机显示Domain-1占用了11.8G,但它的配置最大内存只有8G。为什么它能占这么多?因为当Balloon驱动不工作时,Xen的balloon机制会认为虚拟机正在使用所有内存,因此不会去回收任何页面。这样虚拟机内部即使有大量空闲内存,宿主机也无法借用。如果虚拟机内部应用内存一直在涨,它就会不断向宿主机申请更多物理内存,直到宿主机的全部空闲内存都被吃光,最后OOM Killer爆发。
为了正式确认,我们可以用xenstore读取balloon相关的信息(在宿主机上):
# 查看Domain-1的内存目标值和当前值
# 如果target和memory相差很大,说明balloon没生效
xenstore-read /local/domain/1/memory/target
xenstore-read /local/domain/1/memory/actual
输出可能是8388608(KB)和12312320(KB),差异明显,这就说明balloon机制没有把这些内存收回来。
三、预备知识:气球驱动和cgroups到底是什么?
3.1 气球驱动的原理
气球驱动是Xen、KVM这些虚拟化平台搞出来的一种内存动态调整技术。它的核心思想是,每个虚拟机里都住着一个“气球管家”,宿主机可以通过某种通道(比如xenstore)告诉这个管家:“我现在内存紧张,你把气球吹大点。” 管家收到命令后,就调用虚拟机内部的API,把一些空闲内存页锁住并归还给宿主机。这样宿主机就能把这些物理内存重新分配给其他虚拟机或自己使用。反之,宿主机内存宽裕了,就放气,让虚拟机重新拿回那些内存。
这个机制的好处是灵活,缺点就是必须要求虚拟机内部支持这个驱动。如果驱动没装好,宿主机对虚拟机内存的控制就完全失效了。
3.2 cgroups内存限制
cgroups是Linux内核的一项功能,可以对一组进程进行资源限制,包括内存。在Xen环境下,我们也能把cgroups用在宿主机上,针对某个DomU的进程(比如qemu-dm进程或者toolstack创建的进程)设置内存限制。这样,即使Balloon失效,cgroups也能当一个安全阀,硬性限制这个虚拟机在宿主机上能占用的物理内存上限。
这就像是给DomU加了一道保险杠,Balloon是精密仪器,cgroups是硬性围栏。我们用cgroups来兜底,即便Balloon罢工,也不至于让宿主机内存被冲垮。
四、预防方案:让内存不再膨胀
既然知道了原因,我们就得赶快给系统上一道保险。预防有两条线路:一是用cgroups限制住宿主机上每个DomU对应的进程组,二是调整Balloon参数,尽量让虚拟机内部的内存释放更积极。
4.1 用cgroups给DomU的内存加一道栅栏
Xen的每个DomU在宿主机上都会有一个对应的进程,比如qemu-dm(对于HVM)或者xl创建的dom0相关进程。我们可以把所有和该DomU相关的进程放进同一个cgroup,然后设置一个内存上限。这里以systemd提供的cgroup接口为例(现代Linux发行版都支持),直接使用systemd-cgroup或者手动控制。
首先,我们创建一个cgroup目录,然后往里面写入限制值。需要先获得DomU的进程号。
# 找到Domain-1的进程PID,这里以名称qemu-dm为例
# pgrep -f 是匹配完整命令行,注意区分Domain-0的进程
pgrep -f "qemu-dm.*name=Domain-1"
假设PID是12345,那么我们可以用cgexec或者手动echo到cgroup文件里。下面我们用systemd的systemctl set-property方式来设置,更干净。
# 创建cgroup目录,并在其中设置内存限制为9GB(注意比MAXMEM稍微大一点以留余地)
# 使用systemd的slice方式,我们创建一个名为xen-domu.slice的服务单元
mkdir -p /sys/fs/cgroup/memory/xen-domu.slice
# 设置内存上限为9GB(单位是字节,9*1024*1024*1024 = 9663676416)
echo 9663676416 > /sys/fs/cgroup/memory/xen-domu.slice/memory.limit_in_bytes
# 把DomU的进程PID写入到这个cgroup的tasks文件中
echo 12345 > /sys/fs/cgroup/memory/xen-domu.slice/tasks
这样,即使Balloon失效,这个DomU在宿主机上也最多只能占到9GB,超过后内核会尝试回收页面,如果回收不了,就会触发OOM Kill这个cgroup里的进程,而不是整个宿主机。
更方便的方式是使用cgexec来启动虚拟机,这样自动就在cgroup里了,但直接改tasks也行。
4.2 调整Balloon参数,让气球灵敏些
当然,光靠护栏不够,我们还得让气球本身工作。我们要确保DomU内部的内核加载了xen_balloon驱动。这通常需要在虚拟机的内核配置里开启CONFIG_XEN_BALLOON。另外,还可以调整宿主机对balloon控制的调度频率。xenballoon有一个参数叫reservation,可以设置低水位和高水位。
先在宿主机上看看当前的balloon参数:
# 查看当前balloon的调节参数
# sysctl可以查看和修改内核参数
sysctl xen.balloonmin
sysctl xen.balloonmax
建议设置xen.balloonmin为一个合理的最小值,比如1GB,这样气球不会把内存放得太大。同时设置xen.balloonmax为虚拟机的最大内存,防止意外膨胀。
# 设置balloon的上下限,单位是页(通常4KB一页,1GB就是262144页)
# 把最小值设为1GB,最大值设为8GB(如果虚拟机配置是8G)
sysctl -w xen.balloonmin=262144
sysctl -w xen.balloonmax=2097152
这些设置临时生效。要想永久生效,把下面的内容写入 /etc/sysctl.conf:
# 编辑sysctl配置文件,加入以下内容
# 注意:页数需要根据实际架构调整,这里以4KB页为例
echo "xen.balloonmin=262144" >> /etc/sysctl.conf
echo "xen.balloonmax=2097152" >> /etc/sysctl.conf
同时,在DomU内部,我们也要确保xen-balloon模块在启动时加载,我们可以把它加到/etc/modules文件里。
# 在DomU内部,把xen_balloon模块加入自动加载列表
# 前提是该模块存在于内核中
echo "xen_balloon" >> /etc/modules
modprobe xen_balloon
这样一来,宿主机就能正常控制气球的吹放,虚拟机内部空闲内存可以被回收,不会出现膨胀到超限的情况。
五、应急恢复:OOM之后怎么办?
如果事情已经发生了,宿主机已经进入OOM边缘,我们需要尽快恢复服务。以下是几个应急步骤,按优先级排序。
5.1 先给宿主机“挤”出一点内存
假设OOM发生了,但还没彻底死机。我们可以命令Xen立刻从DomU回收一些内存,强制让气球膨胀。在宿主机上用xenstore写目标值:
# 设置Domain-1的内存目标为4GB(4194304 KB)
# 这会通知balloon机制强制把内存压回4GB
xenstore-write /local/domain/1/memory/target 4194304
这个操作要求DomU里的balloon驱动是能用的。如果驱动没启用,这个命令无效。那就得用更暴力的手段——直接暂停该虚拟机,把它的内存页换出。使用xl pause暂停,然后马上看宿主机free内存是否回升。
# 暂停Domain-1,强制停止它对CPU和内存的占用
# 暂停后宿主机可以回收一些它的缓存页
xl pause Domain-1
# 查看内存是否释放
free -h
# 恢复虚拟机的运行
xl unpause Domain-1
暂停虚拟机比重启快,而且不会丢失内存中的状态(因为它没被销毁)。如果暂停后内存还是不够,说明它的页面都被锁定或者脏数据特别多,那就得考虑迁移或者重启了。
5.2 杀进程?不,直接重启DomU
如果OOM Killer已经杀掉了宿主机的关键进程,比如xenconsoled或者sshd,系统可能处于半瘫痪状态。这时候最快速的办法是强制重启那个吃内存的DomU。在宿主机上执行:
# 强制关闭Domain-1(相当于拔掉电源)
xl destroy Domain-1
# 确认释放内存
free -h
# 重新启动该虚拟机,注意带上正确的配置文件
xl create /etc/xen/Domain-1.cfg
xl destroy是强制销毁,不会让虚拟机优雅关机,但它能立刻回收所有内存。如果这台虚拟机很重要,你可以尝试xl shutdown -w Domain-1来优雅关机,但等待时间可能会较长,甚至因为内存问题关不掉。所以应急情况下,牺牲这台虚拟机换宿主机稳定是可接受的。
六、应用场景与技术优缺点
6.1 适合这么干的场景有哪些?
这个方案特别适合那些对内存规划比较敏感的生产环境,尤其是同时跑很多虚拟机的宿主机。比如,一台物理机上有好几个DomU,分别跑着Web服务、数据库、缓存等等。每个DomU的负载波动很大,忙时内存占用高,闲时低。利用balloon机制和cgroups,就可以让内存按需浮动,提高整体资源利用率。另外,在超配模式下(overcommit),宿主机承诺给所有虚拟机的内存总和大于物理内存,这时候balloon和cgroups是防止系统性崩溃的关键工具。
6.2 技术优缺点大实话
先说说优点。Balloon机制让虚拟化方案变得非常灵活,虚拟机不用关机就能调整内存大小,宿主机可以按需回收内存,提高密度。cgroups作为第二重保障,简单粗暴,内核级强制限制,每个进程组都跑不掉。两者结合,既能灵活分配,又能防止失控。
再说缺点也不含糊。Balloon机制有延迟,不是立竿见影的,如果虚拟机的内存压力很大,气球可能无法及时吹大,甚至可能引发虚拟机内的swap风暴。另外,它依赖虚拟机内部的内核支持,如果你的DomU跑的是精简版系统,没有这个驱动,那就全白搭。cgroups虽然有效,但它不区分内存是哪种状态,可能误杀一些紧急进程,而且如果设置不当(比如限制太低),会导致DomU崩溃,反而影响整个业务。另外,cgroups设置只对之后的进程生效,对已经运行在cgroup外的进程,需要手动移动,比较麻烦。
七、注意事项
有了上面的方案,你还需要注意几个坑,别只顾着抄命令。
第一,别把cgroups限制设得太紧。如果你给DomU设置了8GB最大内存,那你cgroups限制最好设在9GB左右,留出一些内核页、缓存和工具进程的开销。设成8GB可能让DomU在内部还没到顶就被外面杀掉。
第二,sysctl的balloon参数在不同发行版上名字可能不同。有的叫xen.balloon_min,有的叫xen.balloon_min或者xen.balloon_low。你先用sysctl -a | grep balloon看看到底有哪些再改。别照抄我的名字。
第三,确保DomU的内核里真的编入了balloon驱动。你可以用xenstore在DomU里查看/local/domain/1/memory/actual,如果宿主机写入target后actual跟着变了,说明驱动正常。否则就需要重新编译内核或者换系统镜像,那种情况下,cgroups就是你的唯一防线了。
第四,OOM Killer杀宿主机进程可能波及到xenstore或者libvirt,导致所有虚拟机不可控。所以最好把宿主机自身的核心服务放进单独的cgroup,并设置低于其他cgroup的优先级,或者设置一个保护边界,比如使用memory.oom_control的oom_group策略,确保重要的管理进程不被优先杀掉。
第五,应急时不要盲目xl destroy所有虚拟机,先定位一下是哪个虚拟机的balloon失效。可以使用xl list -l输出详情,查看每个DomU的balloon_target和memory_actual是否匹配。
# 查看所有虚拟机的详细内存配置文件
# 其中会显示 memory 和 maxmem
xl list -l | grep -E "(name|memory|maxmem)"
这样你就知道哪台是受害者还是加害者了。
八、总结
回过头来看,这次故障的根本原因在于DomU里的balloon驱动没工作,导致宿主机无法从它那里回收空闲内存,最终引发了OOM Killer。排查的过程并不复杂,关键是知道去查xl top里内存是否超过上限,以及检查DomU内的lsmod。预防方面,我们用了双保险:先给cgroups设置内存护栏,确保异常膨胀时不会拖垮整个宿主机;再调整balloon参数并保证驱动加载,让内存能在系统调度下顺畅流动。应急方面,通过xenstore强制设置目标内存、暂停甚至销毁虚拟机,来快速缓解内存压力。
不过,最根本的办法还是在构建虚拟机镜像的时候就把balloon驱动装好,同时维护好宿主机上的cgroup配置。多一层保障,少一次半夜被电话叫醒的体验。希望这篇文章能帮到你,下次遇到类似内存膨胀问题,心里就有谱了。
评论
围绕“Xen虚拟化宿主机因DomU气球驱动未启用造成内存膨胀触发OOM Killer的排查过程通过cgroups限制与Balloon参数调优预防及应急恢复指南”参与讨论