ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Django的KVM虚拟化集群管理平台设计与实践

基于Django的KVM虚拟化集群管理平台设计与实践 设想一个真实的场景机房里有几十台物理服务器每台上边都跑着十几台虚拟机日常维护要开新VM、要给VM做系统、要迁移、要调整配置全都靠SSH上去敲virsh命令。几十台宿主机散布在不同机柜IP和密码记了一本子出一回故障先得翻半天文档。这个问题困扰了我很长时间于是干脆动手写了一套基于Python/Django的KVM虚拟机集群管理系统把宿主机、虚拟机、存储池、网络全都收口到一个Web界面上。到2026年1月21日这版已经能稳定管住5个机柜、40多台宿主机、超过800台虚拟机。这篇文章就是把这套系统的选型思路、核心设计、关键代码、以及我在实际部署中踩过的那些坑完整梳理一遍。适合正在做运维平台、云管平台或者打算用Django做基础设施管理的朋友参考。内容可能有点长但都是我实际跑过的方案不是PPT架构。1. 为什么要自己写这个管理系统手动操作virsh的长期痛点1.1 手工运维在集群规模变大以后的实际体验一开始只有几台宿主机时手动敲命令行完全没问题。virsh list --all、virsh start xxx、virsh shutdown xxx记在笔记里就够了。但宿主机数量到两位数之后问题开始集中爆发虚拟机分布信息全靠脑袋记经常要virsh list一台一台翻。不同宿主机之间迁移需要比对CPU型号、磁盘路径、网络配置敲命令敲到怀疑人生。团队成员共用root账号谁在某台机器上执行过什么操作完全没有记录。创建一台新虚拟机要拼接XML同一个字段在不同的libvirt版本里还略有差异手写容易出错。后来我统计了一下一次手工创建VM平均要花20到30分钟而且有大概10%的概率因为XML拼错导致启动失败。这个效率没法接受于是决定做一个Web系统来接管这些操作。1.2 技术选型对比为什么最终落在Django上做技术选型时其实不是一开始就选Django的我也对比过Go、Flask甚至想过直接用现成的OpenStack。但有几个原因让我确定用Python Django libvirtlibvirt-python已经封装得非常成熟直接调用libvirt的API就能管理KVM、QEMU底层原理是连接到每个宿主机的libvirtd服务然后发送管理指令。Django自带Admin、ORM、认证、中间件做一个带Web界面的内部管理平台非常顺手不需要自己拼轮子。团队里当时主力语言就是Python后续维护成本最低。现成的OpenStack太重对一个内网规模的KVM集群来说是杀鸡用牛刀而且升级和运维成本远高于自己写轻量平台。所以最终确定的架构是前端用Django模板加一点Vue组件后端Django提供REST API和页面渲染通过libvirt-python连接宿主机长时间任务走Celery异步执行数据库用PostgreSQL缓存和消息队列用Redis。整体不算复杂但每块都有明确分工。2. 集群管理系统的实体建模和数据库设计先把概念理清楚2.1 核心数据模型Host、VM、StoragePool、Network一个都不能少写管理系统的第一步不是写代码而是设计数据库模型。KVM集群管理表面上是管虚拟机实际上要管的是宿主机、虚拟机、存储、网络、镜像模板五大类资源。我最终建立了下面这些核心模型模型核心字段说明Hosthostname, ip, user, auth_type, status, cpu_total, mem_total每台宿主机记录连接信息VirtualMachineuuid, name, host, cpu_cores, memory_mb, disk_path, status, xml_desc虚拟机实例StoragePoolname, type, path, host, total_size, used_size存储池如目录池、LVM池Networkname, bridge, subnet, gateway, dns虚拟机网络ImageTemplatename, os_type, backing_file, disk_format镜像模板用于快速创建VMOperationLoguser, action, resource_type, resource_id, detail, created_at操作审计关键的一点是VirtualMachine.model里必须保存完整的xml_desc也就是libvirt用来定义虚拟机的XML原始内容。虽然可以通过libvirt的dumpxml随时获取但保存一份在数据库里做批量检索和对比时会快很多。2.2 Django模型实现示例下面是我当时建立的模型简化版本from django.db import models class Host(models.Model): name models.CharField(max_length64, uniqueTrue) ip_address models.GenericIPAddressField(uniqueTrue) username models.CharField(max_length64, defaultroot) auth_type models.CharField(max_length16, choices[(password, 密码), (key, 密钥)], defaultkey) status models.CharField(max_length16, defaultunknown) cpu_total models.IntegerField(default0) mem_total models.BigIntegerField(default0) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name class VirtualMachine(models.Model): uuid models.UUIDField(uniqueTrue) name models.CharField(max_length128, uniqueTrue) host models.ForeignKey(Host, on_deletemodels.PROTECT, related_namevms) cpu_cores models.IntegerField(default1) memory_mb models.IntegerField(default1024) status models.CharField(max_length32, defaultshutoff) xml_desc models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue)这里有个细节容易被忽略host字段的外键不能设置为CASCADE删除虚拟机挂在宿主机上宿主机如果被误删虚拟机记录也会一起消失。用PROTECT可以在删除宿主机前强制检查是否还有关联虚拟机。2.3 与libvirt建立连接的封装思路Django的模型层解决的是资源台账真正要操作KVM还需要和每一台宿主机的libvirtd通信。libvirt的连接不能每个请求都重新建立因为建立一次连接的开销并不小而且频繁连接会把宿主机上的libvirtd搞得很不稳定。我采用的方式是做一个简单的连接池import libvirt from functools import lru_cache def _conn_key(host_ip, username): return fqemussh://{username}{host_ip}/system?no_verify1 lru_cache(maxsize128) def get_conn(host_ip, usernameroot): uri _conn_key(host_ip, username) conn libvirt.open(uri) if conn is None: raise RuntimeError(f无法连接 {host_ip} 的 libvirtd) return conn这里用lru_cache做缓存以host_ip username为key。但实际用下来发现长期不关闭连接也会有问题比如宿主机升级libvirt后旧连接会失效所以在管理端加了一个“刷新连接”的按钮清空缓存重新连接。3. 虚拟机生命周期管理的核心实现创建、启停、迁移都绕不开的XML和API3.1 创建虚拟机XML模板先行参数拼装避免手写错libvirt管理虚拟机最底层的单位是域Domain而定义域的方式就是XML。创建一台虚拟机本质上就是拼出一段合法的domain XML然后调用conn.defineXML(xml)再dom.create()。平心而论这块逻辑并不难难的是XML里各种资源的路径、总线类型、设备类型必须和宿主机环境匹配。我用的是模板加参数渲染的方式先定义一段基础XML模板domain typekvm name{{ vm_name }}/name uuid{{ vm_uuid }}/uuid memory unitMiB{{ memory_mb }}/memory vcpu placementstatic{{ cpu_cores }}/vcpu os type archx86_64 machinepc-i440fx-2.1hvm/type boot devhd/ /os features acpi/ apic/ /features devices disk typefile devicedisk driver nameqemu typeqcow2/ source file{{ disk_path }}/ target devvda busvirtio/ /disk interface typebridge source bridge{{ bridge_name }}/ model typevirtio/ /interface graphics typevnc port-1 autoportyes listen0.0.0.0/ /devices /domain代码中替换变量即可。创建一个新VM的流程是这样的在前端表单填写名称、CPU、内存、镜像模板。后端根据模板查到底层镜像路径用qemu-img create -f qcow2 -b base.qcow2 newvm.qcow2创建一个差量盘。生成UUID渲染XML。调用libvirtdefineXML定义虚拟机再根据用户选择是否立即启动。3.2 生命周期操作shutdown和destroy的区别必须搞清楚开机、关机、重启、强制停止这些操作在libvirt里都有对应APIdef vm_power_ops(vm_uuid, action): conn get_conn(vm_uuid.host.ip_address) dom conn.lookupByUUIDString(str(vm_uuid.uuid)) if action start: dom.create() elif action shutdown: dom.shutdown() # 发送ACPI关机指令给系统优雅关闭时间 elif action destroy: dom.destroy() # 相当于拔电源 elif action reboot: dom.reboot()这里有个经验初始化时很多同事习惯用destroy来关闭虚拟机因为速度快但destroy等于物理拔电容易损坏文件系统。除非虚拟机里的应用允许强杀否则一律先shutdown等30秒后再检查状态如果还没关才考虑destroy。我在系统里做了两阶段关闭逻辑默认强制关机按钮需要二次确认并填写工单号。3.3 在线迁移的实际参数调优虚拟机热迁移是这套系统里比较核心的功能。用libvirt做在线迁移的API是dom.migrateToURI(dest_uri, flags, destination_xml, bandwidth)但直接裸调会有几个问题默认迁移带宽不受限会把宿主机业务网络打满。两台宿主机CPU型号不一致时迁移后虚拟机可能直接崩溃。磁盘文件没有提前放到目标宿主机时迁移会因为找不到存储而失败。我的做法是迁移前先用qemu-img info确认源和目标存储路径都存在然后给migrateToURI设置VIR_MIGRATE_LIVE | VIR_MIGRATE_PEER2PEER标志并且通过bandwidth参数限制迁移带宽为200Mbps避免影响线上业务。调优时还发现如果迁移过程中网络抖动任务会一直卡住。所以系统里有一个后台定时任务持续调用dom.jobInfo()查看迁移进度超过2分钟没有进展就强制取消迁移而不是干等。4. 处理长时间任务的工程化方案Web请求不能直接连libvirt4.1 为什么要在视图函数外面套一层异步任务一开始我只做了一个同步接口用Django视图直接调用libvirt。创建VM这种操作用时并不长但导入大镜像、批量迁移、批量删除这类任务就不一样了快则几十秒慢则十几分钟。Django默认每个请求都会占用一个worker长时间任务会把Web服务拖垮尤其是部署在waitress这类同步WSGI服务器上时只要有几个任务在跑其他人连页面都打不开。所以我把所有超过5秒的操作全部改成了异步任务。用的是Celery Redis视图函数只负责创建任务并立刻返回一个任务ID后台Worker执行真正的libvirt调用。前端通过轮询任务状态接口来显示进度。4.2 Celery任务定义示例from celery import shared_task from .libvirt_utils import vm_power_ops, create_vm_from_template shared_task(bindTrue, max_retries3, default_retry_delay10) def create_vm_task(self, vm_id): try: create_vm_from_template(vm_id) except Exception as exc: # 把异常抛给Celery重试重试前等待10秒 raise self.retry(excexc)同时我在任务里封装了一个进度上报函数def update_task_progress(task, current, total, message): task.update_state( statePROGRESS, meta{ current: current, total: total, message: message } )前端页面最开始是每3秒轮询一次任务状态后来觉得不够实时又加了一层WebSocket。如果团队里没有WebSocket基础设施轮询其实完全够用不要把方案搞复杂。4.3 任务失败恢复和幂等性设计异步任务最怕的是执行到一半崩溃比如创建虚拟机时defineXML成功了但create()失败这时候数据库里已经有了VM记录但虚拟机实际没有运行。所以我在任务执行前先检查状态执行时记录日志失败后通过task_id可以查到具体卡在哪一步。还有一点要特别注意幂等性create_vm_task不能重复执行。我在VirtualMachine模型上加了create_status字段值为pending、running、success、failed。任务执行前先原子更新状态如果发现状态已经是success就直接返回避免手动重试时重复创建两台同名VM。5. 宿主机环境差异和常见安装问题Ubuntu、Debian、麒麟系统实测记录5.1 检查CPU虚拟化支持和KVM模块加载无论什么发行版KVM能不能跑起来第一步都看两件事CPU是否支持虚拟化、KVM内核模块是否加载。egrep -c (vmx|svm) /proc/cpuinfo如果返回0说明CPU虚拟化没有开启需要去BIOS里打开VT-x/AMD-V。然后检查模块lsmod | grep kvm正常会看到kvm_intel或者kvm_amd。如果没看到手动加载modprobe kvm_intelDebian和Ubuntu上还要确保用户属于libvirt和kvm组否则连接/var/run/libvirt/libvirt-sock时会权限不足。这个坑特别隐蔽libvirt连接半天下载不下来权限报错结果发现是组没加。5.2 各发行版安装libvirt的差异不同发行版安装KVM相关软件包的命令差别很大。我把实际验证过的三个发行版记录在这里发行版安装命令备注Ubuntu/Debianapt install -y qemu-kvm libvirt-daemon-system libvirt-clients python3-libvirt virtinst服务名为libvirtdCentOS/RHELyum install -y qemu-kvm libvirt libvirt-python virt-install需要systemctl start libvirtd银河麒麟x86海光yum install -y qemu-kvm libvirt libvirt-python virt-install和CentOS类似但要注意海光CPU的KVM模块是kvm_amd银河麒麟这种系统我一开始以为是完全独立的体系实际测下来它的包管理基本兼容CentOS但libvirt版本可能偏旧。如果遇到virsh命令可用但Python的libvirt模块找不到多半是没装libvirt-python或者系统默认的python3路径没有包含该模块。用pip3 install libvirt-python前记得先确认pip对应的python和系统/usr/bin/python3是不是同一个。5.3 SELinux和AppArmor导致的操作失败在CentOS和麒麟系统上即使权限配置好了virsh create仍然可能报Permission denied这通常是SELinux在拦截qemu进程访问镜像文件。快速验证方法getenforce如果是Enforcing可以临时放行setsebool -P virt_use_nfs 1 setsebool -P virt_use_samba 1更合理做法是把镜像目录的SELinux上下文加上virt_image_t类型。Ubuntu上则是AppArmor配置文件在/etc/apparmor.d/usr.sbin.libvirtd如果镜像目录比较特殊需要在配置文件里加上对应的路径规则。我实际踩过一次很深的坑系统刚装好什么测试都通了第二天重启宿主机后虚拟机全部启动失败查了半天发现是SELinux策略重置导致镜像文件读取被拒。所以部署时一定要把SELinux的上下文设置写进自动化脚本不能只做一次临时放行。6. 权限控制和审计日志多人协作时管理系统的底线6.1 对象级权限设计不只是Django自带的用户权限Django自带User、Group、Permission但它是模型级别的权限不能精确控制“某个用户只能操作某台宿主机上的虚拟机”。所以在系统设计里我在Django权限之上加了一层UserHostPermission关联表class UserHostPermission(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) host models.ForeignKey(Host, on_deletemodels.CASCADE) can_edit models.BooleanField(defaultFalse) can_delete models.BooleanField(defaultFalse) can_migrate models.BooleanField(defaultFalse)权限判断逻辑在service层统一做不在视图函数里散落。比如迁移虚拟机的服务函数第一行def migrate_vm(user, vm_id, target_host_id): permission UserHostPermission.objects.filter( useruser, hostvm_id.host ).first() if not permission or not permission.can_migrate: raise PermissionDenied(你对此宿主机没有迁移权限)这样做的好处是以后如果要接LDAP或者工单系统只需要在这个service层加逻辑就行。6.2 操作审计日志用中间件和信号记录所有关键操作审计日志不需要记录每一个GET请求但所有创建、删除、修改、迁移、启停操作一定要记录下来。我写了一个AuditLog中间件在视图函数执行完成后自动保存class AuditLogMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): response self.get_response(request) if request.method in [POST, PUT, PATCH, DELETE]: OperationLog.objects.create( userrequest.user.username if request.user.is_authenticated else anonymous, actionf{request.method} {request.path}, detailrequest.body[:1024].decode(utf-8, errorsignore), iprequest.META.get(REMOTE_ADDR, ), ) return response不过这种中间件记录的是请求原文不够结构化。后面我又在关键的service函数里手动写日志比如“用户A将虚拟机B从宿主机C迁移到宿主机D”方便追踪问题。这部分信息在故障排查、安全审计、甚至领导要个操作记录时都很有用。另外对“强制关机”“删除虚拟机”这种高危操作系统设计了软删除机制删除虚拟机不是从数据库中物理删除而是打上deleted_at标记然后创建一个回收站任务保留7天后才真正清理磁盘文件。恢复虚拟机时只需要触发virsh define补齐定义即可。7. 部署上线和稳定性优化waitressnginx组合也够用7.1 生产环境用waitress还是GunicornDjango开发时直接python manage.py runserver但这东西只是个单进程调试服务器线上绝对不能用。由于我们有些宿主机是Windows Server所以当时优先在服务器上测试了waitress这个WSGI服务器的优点是纯Python、跨平台、支持多线程配置也简单pip install waitress waitress-serve --listen0.0.0.0:8080 myproject.wsgi:application在Linux上我也测试了gunicorn性能和并发明显好一些但配置相对多一点。如果只是内网管理平台W自动重启机制和面板未必比waitress方便。最终内网平台用的waitress对外开放的域名入口用nginx反代到waitress端口。nginx配置里需要注意超时时间location / { proxy_pass http://127.0.0.1:8080; proxy_read_timeout 300s; proxy_connect_timeout 75s; }因为某些Django视图会调用libvirt查询信息查询时间可能比较长nginx默认60秒超时容易导致上游断开。7.2 Django静态文件加载不出来的排查过程热搜词里有一个很典型的Django问题“vscode写img标签 在django的static文件中显示不了”。我开发时也遇到过页面能打开但CSS、图片完全裸奔。原因不外乎三个STATIC_URL没有在模板中使用{% load static %}和{% static xxx %}。STATICFILES_DIRS没有配置或路径和实际文件不一致。生产环境没有执行collectstaticnginx没有配置静态文件路径。我的标准配置是# settings.py STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles STATICFILES_DIRS [BASE_DIR / static]开发时Django会自动处理静态文件生产环境一定要在部署脚本里加python manage.py collectstatic --noinput然后把nginx的/static/指到STATIC_ROOT即可。这个问题看起来小但一旦上线花白页面会让人以为整个系统崩溃了。7.3 连接池、超时和监控避免管理平台变成新的故障点管理平台本身也是一个服务如果性能不行反而会成为运维负担。我做了三个优化第一libvirt连接池。之前提到用lru_cache缓存连接所有宿主机都保持长连接。每台宿主机系统资源有限长连接数量要控制缓存128个连接足够应对40多台宿主机。第二所有对外查询接口加上超时控制。Django的数据库连接、Redis连接都设置了超时libvirt调用也尽量用virDomain.isActive()这类低开销接口避免页面一直转圈。第三资源监控采用独立采集任务。每台宿主机的CPU、内存、磁盘和虚拟机状态信息由Celery定时任务每60秒采集一次写入数据库页面直接读缓存。这样即使libvirtd抖动Web界面也不会卡死。这部分优化之后最直观的感受是页面响应从一两秒降到了几百毫秒宿主机数量再翻一倍也不会影响使用。最后再分享一个小技巧系统里的虚拟机名称、主机名、IP三条信息经常对不上排查问题时非常痛苦。所以我在创建虚拟机时自动把UUID、VM名称、主机名写入到虚拟机的user-data里并且统一放到Django后台的虚拟机详情页展示。这个设计看似不起眼但在线上故障定位时节省了大量时间。如果你也要做类似的管理系统尽早把元数据规范和资源关联设计好后面会轻松很多。
返回列表