☰
开源虚拟机管理:统一资源调度与透明Token计费实践
2026/10/2 15:32:20 网站建设 项目流程

先解释一下标题里这个有点野的代号:龙虾。我们内部把每个跑在虚拟机里的服务实例都叫作“龙虾”,壳硬是指虚拟化隔离做得足够彻底,肉鲜是指对外提供的调用质量足够高。这套系统最近已经开源,核心就是一件事:把个人公司里的各种服务统一塞进虚拟机里管理,顺便把每个 token 的消耗算清楚,账目公开到每一笔。项目不大,但从小到大踩了不少坑,这篇就当我们的落地复盘,给想搞自托管、统一资源调度和透明计费的朋友做个参考。

1. 项目概述与整体思路

1.1 “龙虾管理”到底是个什么东西

很多人第一次看到“龙虾管理”以为是个养殖场管理系统,实际上这里是拿“龙虾”当代号。我们一台虚拟机就是一个“龙虾池”,里面跑的进程就是“龙虾”。这套开源系统解决的问题很直接:个人公司或小团队手里可能有若干个自建服务,比如模型推理服务、API 网关、内部工具链,每一个都需要独立的运行环境、独立的访问凭证、独立的消耗统计。

过去最常见的管理方式有两种,一种是什么服务都直接跑在宿主机上,省事,但一个服务出问题就可能拖死全部,权限也不好隔离;另一种是全部塞进容器里,虽然隔离性不错,但容器共享宿主机内核,如果对安全等级要求极高,还是不够“绝对安全”。这套系统选的是第二条路,把每个服务装进独立的虚拟机,宿主机上只跑一个轻量级的虚拟化和统一管理组件,web 后台用来开机、关机、克隆、销毁、看日志、算费用。

再说为什么 token 计费要透明。做个人业务也好,做小范围商业服务也好,最怕的就是用户问“我这个月 token 用在哪了,为什么扣了这么多”。透明计费不是用来打广告的,是一种自证。每个请求的 token 消耗量都写入明细表,用户可以实时查,管理员也可以直接看到每个实例累计消耗了多少,账单全部可以由数据细节拼出来,不存在模糊地带。

1.2 为什么选虚拟机而不是容器或裸机

先讲一个核心权衡:容器快,裸机猛,但虚拟机是安全性和运维友好度之间最稳的答案。容器的本质是共享宿主机内核,一旦内核层面出问题,容器与容器之间的边界理论上就可能被突破。而虚拟机由硬件虚拟化技术兜底,每个客户机有自己的内核、自己的系统文件、自己的独立内存地址空间,即便一个实例被完全攻破,攻击者面对的还是虚拟化层,逃逸难度不是一个量级。

另一个现实原因是快照和克隆。容器也能做镜像,但虚拟机配合 qcow2 或者 raw 磁盘格式,可以做到秒级快照、分钟级克隆出全新实例。这对于个人公司来说极其实用:新开一个业务线,直接把模板虚拟机克隆一份,改个 IP,初始化一下密钥,10 分钟就能上线一套隔离环境,配置成本和维护成本都低。

裸机性能确实更好,但个人公司没有那么重的负载,虚拟机损失的那一点性能完全可以通过超线程和缓存策略补回来。再加上管理端可以统一做资源池,把 CPU、内存、磁盘按需分配,不用担心某台物理机故障导致所有服务一起下线,虚拟机迁移能把这个风险兜住。

1.3 开源和透明计费之间的化学反应

开源并不是营销噱头,而是透明计费的必要条件。如果计费逻辑闭源,用户只能看到最终数字,无从验证消耗粒度。把整个 token 统计逻辑做成开源之后,每一行记录的生成、累加、汇总规则都摊在阳光下,用户拿着自己的日志就能对得上账。

这套系统的开源部分包括三块:虚拟化资源池管理模块、token 计量与计费模块、轻量级 web 管理面板。数据库结构和 API 完全开放,允许二次开发。我们内部用的时候会加一些私有脚本做自动化巡检,但这些脚本不涉及计费核心,所以完全可以剥离。开源之后反而省了很多事情,用户自己解决了一部分定制需求,issue 区还能看到不少真实使用场景,成了我们的免费需求池。

2. 核心细节解析与实操要点

2.1 虚拟机资源池怎么规划才够稳

资源规划最容易犯的错是只算总容量,不算碎片化。打个比方,宿主机有 32 G 内存,你以为能开 8 台 4 G 的虚拟机,可实际上虚拟化管理器预留了一部分做页缓存,快照也需要临时缓存,网络转发还要占内存,实际可用大约只有 28 G。建议按“资源配额七成原则”划分:单台宿主机最高只分配总资源的 70% 给业务虚拟机,剩下的 30% 留给宿主机自身、突发流量和快照回滚。

CPU 部分建议开超线程,但不要盲目把 vCPU 总数铺满。单实例分配的 vCPU 不要超过物理核数的 50%,例如一台 8 核 16 线程的机器,单台虚拟机最多给 4 vCPU,总 vCPU 保持在 24 以下,避免 CPU 饥饿导致系统负载看起来不高、但响应极慢。

磁盘是另一个隐藏坑。后端存储格式用 qcow2 的话,单盘默认是稀疏分配,也就是实际占用会随写入增长。如果不做监控,很容易出现宿主机磁盘被撑满的情况。建议每个实例建磁盘时直接设定上限,并额外挂载一块独立数据盘,系统盘和数据盘分离,日志和数据库都放数据盘。这样快照恢复时,只需要回滚系统盘,业务数据不会跟着丢。

2.2 统一管理层:虚拟机资源和 API 网关如何纠缠

这套系统的管理端看起来是一个 web 后台,实际上由三条链路组成。第一条是虚拟化管理链路,通过 libvirt 直接控制宿主机上的 KVM/QEMU 实例,负责实例生命周期;第二条是访问链路,所有外部调用先进入 API 网关,再由网关转发到对应的虚拟机内服务,网关同时负责校验 token;第三条是计量链路,网关每转发一次请求,就把请求的唯一 ID、用户 ID、实例 ID、token 数量写进消息队列,后台异步落库。

三条链路缺一不可。没有虚拟化管理链路,实例就无法实现统一启停;没有 API 网关,每个实例都要单独处理鉴权,管理成本直接爆炸;没有计量链路,就谈不上 token 计费透明。真正关键的是 API 网关和计量之间的关系。网关里记录的是原始请求量,计量模块记录的是 token 消耗量,两个数字并不总是一比一,因为同一份 prompt 可能被重试、被截断、被多轮上下文拼接。所以计量模块必须以“模型服务返回的实际 token 使用量”为准,而不是自己猜测。

这种设计的优势在于,虚拟机的个数和业务的复杂程度解耦。哪怕以后从 5 个服务涨到 50 个服务,管理员还是只需要看一个面板,用户还是只需要认一个 token。

2.3 token 计费透明这套逻辑是怎么实现的

token 计费透明不只是一个“记账页”,背后是一套数据模型。每次请求产生一条 token_usage 记录,字段包括请求唯一标识、项目标识、虚拟实例标识、token 输入数、token 输出数、模型名、时间戳。有了这条记录,所有统计都可以复现,不依赖累计字段,随时可以从明细重算总额。

对于不使用模型服务的普通服务,系统也预留了一套自定义计量接口,可以上报任意单位的消耗,比如调用次数、处理时长、带宽用量。统一的账单结构让“开发票”变得很简单,每个结算周期出一张汇总表,按用户聚合,再按项目展开明细,用户点击任意一行都能看到背后对应的全部请求记录。

Token 签发用的是 JWT 标准,但不同业务场景用法不同。长期运行的内部服务用固定密钥签发长期 token,有效期可以设 90 天;面向外部或临时用户就用短时效 token,过期后通过 refresh_token 无感刷新。JWT 本身不加密,只做签名,所以绝不把敏感信息直接写进 payload,只放用户 ID 和令牌版本号,权限信息全部由管理端查库决定。这样即使 token 泄露,风险窗口也控制在最短。

为了避免泄露,token 展示遵循一次性原则:创建时完整展示一次,之后在任何界面都做脱敏处理。数据库里不存明文 JWT,只存哈希指纹,校验时先用网关做同算法哈希比对。这是比较容易忽略的细节,很多人直接把 JWT 原文放在库里,一旦数据库备份泄露全部 token 一起完蛋。

3. 实操过程与核心环节实现

3.1 宿主机虚拟化环境的搭建与参数选择

我们接手的第一件事是装宿主机系统。推荐直接用 Debian,镜像小、内核稳、没有多余的服务抢占资源。装完系统第一时间做的不是下载工具,而是关闭 swap,尤其是计算型服务,swap 一开,高负载下虚拟机会频繁换页,延迟直接失控。同时开启 tuned 的 latency-performance 模式,把 CPU 调度帧率拉满,这点对虚拟化场景收益非常直接。

安装虚拟化组件,KVM 内核模块加 qemu 用户空间工具,加上 libvirt-daemon-system 和 virt-manager 管理端。在 Debian 下可以这样装:

apt update && apt install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst cloud-image-utils systemctl enable --now libvirtd

然后配置一个桥接网络,让虚拟机可以直接对外提供访问。修改/etc/network/interfaces.d/br0:

auto br0 iface br0 inet static address 192.168.10.2/24 gateway 192.168.10.1 bridge_ports enp3s0 bridge_stp off bridge_fd 0

桥接要比 NAT 模式更直接,虚拟机和宿主机在同一广播域,外部访问少一层端口转发,排错也更容易。这里的 192.168.10.1 是我的物理网关,个人公司一般都在小内网,桥接足够安全。宿主机防火墙里只放行管理端口和网关端口,其余全部默认拒绝,这条后面单独说。

创建第一台模板虚拟机时用 cloud image 最省心。把 Debian 官方 cloud 镜像下载后,用 qemu-img 转换成 qcow2 格式,然后通过 virt-install 创建模板:

qemu-img create -f qcow2 -b debian-cloud.qcow2 -F qcow2 lobster-template.qcow2 virt-install \ --name lobster-template \ --memory 4096 \ --vcpus 4 \ --disk path=/opt/vms/lobster-template.qcow2,format=qcow2 \ --import \ --os-variant debian12 \ --network bridge=br0,model=virtio \ --graphics none

模板机的系统盘用差分镜像创建,可以迅速克隆新实例,避免每次都从完整镜像复制。差分镜像的坏处是父镜像不能删除,所以我把父镜像统一放在/opt/vms/base/,业务实例放在/opt/vms/instances/,权限严格控制,运维操作只走脚本,避免手滑。

3.2 把“龙虾”养进虚拟机:初始化与克隆流程

模板机创建完不能直接用,还要做一次基础初始化。装完系统后在模板机里安装 cloud-init,写入宿主机的 SSH 公钥,关闭 root 密码登录,配置好 NTP 客户端和日志转发。这一步到位之后,所有克隆出来的实例天然自带安全基线。

克隆一个新实例只需要一条命令:

qemu-img create -f qcow2 -b /opt/vms/base/lobster-template.qcow2 -F qcow2 /opt/vms/instances/lobster-01.qcow2 virt-clone \ --original lobster-template \ --name lobster-01 \ --file /opt/vms/instances/lobster-01.qcow2

克隆完先不要急着开机,先修改虚拟机的网络配置,让 IP 与 新实例的 MAC 地址绑定。我们为每个实例配了单独的 IP 和 hostname,DNS 记录也同步更新。顺序很重要,顺序错了实例之间可能发生 IP 冲突,排查起来很痛苦。开机后用virsh domifaddr lobster-01检查网卡地址,确认服务可连,再把实例接入统一管理后台,打上项目标签,这一步才算真正“入池”。

管理后台的建池逻辑是用配置文件驱动的。每个项目的定义文件是一个 YAML:

name: "lobster-01" project: "internal-tools" vcpu: 4 memory_mb: 4096 disk_gb: 40 endpoint: "http://192.168.10.21:8080" access_token_ttl_days: 30

后台会定时扫描配置目录,自动同步真实虚拟机的资源情况,保证控制台上看到的资源配额与实际一致。这套设计的好处是管理界面只是配置的映射,真正的状态全部由 libvirt 提供,不会出现控制台和后端状态分叉的问题。

3.3 签发 token 与透明计费的完整闭环

token 计费是这套系统和我们实际业务结合最深的地方。先定义数据表,用户表、实例表、token 表、usage 记录表,一张都不能少。

签发 token 的逻辑在管理后台里是这样做的:用户登录后,后台生成 JWT,同时在 token 表里写入 SHA256 指纹,返回给用户时只展示这一次:

def issue_token(user_id: str, instance_id: str, ttl_days: int): payload = { "uid": user_id, "iid": instance_id, "ver": 1, "exp": int(time.time()) + ttl_days * 86400, } token = jwt.encode(payload, SECRET_KEY, algorithm="HS256") token_hash = hashlib.sha256(token.encode()).hexdigest() save_token_hash(user_id, instance_id, token_hash) return token

校验 token 在 API 网关里做。网关拿到 token 后先验证签名,再比对库里指纹,同时检查实例是否还在白名单内。只有两步都通过,请求才被放行转发到虚拟机实例。

计费部分的核心是这个函数:

def record_usage(request_id: str, user_id: str, instance_id: str, prompt_tokens: int, completion_tokens: int): conn.execute( "INSERT INTO usage_records " "(request_id, user_id, instance_id, prompt_tokens, completion_tokens, total_tokens, created_at) " "VALUES (%s, %s, %s, %s, %s, %s, NOW())", (request_id, user_id, instance_id, prompt_tokens, completion_tokens, prompt_tokens + completion_tokens), )

这里有一个看起来简单但很关键的细节,usage 记录必须用幂等键。同一个请求可能因为网络重试被发送多次,如果网关不处理,token 就会被重复计费。我们的做法是在网关层为每个外部请求生成 request_id,计费落库时以 request_id 做唯一约束,重复上报直接忽略,确保用户看到的数字只能比实际消耗小或等于,绝不会多扣。

每个周期末跑统计任务,把 usage_records 按用户和实例聚合,生成透明账单:

SELECT user_id, instance_id, SUM(total_tokens) AS total_usage, SUM(prompt_tokens) AS prompt_usage, SUM(completion_tokens) AS completion_usage FROM usage_records WHERE created_at >= %s AND created_at < %s GROUP BY user_id, instance_id;

用户端的“消费明细”页面直接按这张汇总表展示,点开某一行又能跳回原始请求列表,时间、实例、token 三个维度对得上。这套设计做完之后,我们已经再没收到过“你是不是偷跑参数了”的质疑。

3.4 日常巡检脚本与运维监控

虚拟机管理看起来是大工程,实际日常操作用脚本就能覆盖。我们每天定时执行一轮巡检,检查宿主机磁盘剩余、内存占用、运行中的实例数、每个实例的 CPU 平均负载和网络吞吐。

#!/bin/bash # 巡检宿主机磁盘和水位 df -h /opt/vms free -g virsh list --all

更细的监控用 node_exporter 从宿主机抓取指标,在 Grafana 里展示。需要注意,虚拟机的监控不能只看宿主机的 load average,因为很多实例是空闲状态,宿主机负载高不代表业务有问题,必须把每个实例的 CPU 使用率单独拉出来。我们给每个实例按照配置设了告警线,比如 4 vCPU 的实例持续 5 分钟跑过 320%,就触发提醒,避免一个业务异常拖垮同一台宿主机上的邻居。

备份策略是我们最后补上的。每台实例每天凌晨做一次增量快照,快照保留 7 天,同时把数据库里的计费明细自动导出到独立存储空间。快照不是备份,恢复测试才是,每两周手动挑一台实例做快照恢复演练,确认能起来、服务能通、计费数据能关联上。这个习惯的养成经历过一次教训,后面再讲。

4. 常见问题与排查技巧实录

4.1 token 失效、登录失败和鉴权报错的定位顺序

最常遇到的 token 问题是 JWT 签名失效和 refresh_token 为空。登录失败时日志如果提示 token endpoint returned 403,先不要直接怀疑签名算法,99% 的情况是服务器时间不同步。JWT 的 exp 依赖当前时间,宿主机和虚拟机的时钟差超过几十秒,就会出现新签发的 token 立刻被认为是过期。先做一次全链路时间同步检查:

systemctl enable --now systemd-timesyncd timedatectl status

确认时间同步后,再检查数据库里的 token 指纹是否完整。数据库在迁移或者备份恢复时,如果 token_hash 字段丢失,校验也会失败,但这种失败往往是所有 token 一起失效,特征比较明显。

refresh_token 为空这个问题我们也踩过坑,原因是前端在首次登录后没有正确缓存刷新令牌。建议把刷新令牌保存在 httpOnly cookie 里,而不是 localstorage,后端以 cookie 里的 refresh_token 为准,token 过期后静默续签。这样既避免刷新令牌空值,还降低 XSS 风险。

4.2 虚拟机启动失败、蓝屏或网络不通的排查方法

虚拟机启动到一半卡住,最常见的元凶是内核参数和磁盘控制器驱动不匹配。Windows 客户机如果使用 virtio 磁盘控制器,安装系统时必须加载对应驱动,否则启动到徽标处就蓝屏。解决方法是创建虚拟机时先用 IDE 控制器装完系统,安装 virtio 驱动后再切换磁盘总线。Linux 客户机相对省心,但 cloud-init 配置失败也会导致第一次启动后没有网络,原因是 DHCP 没有生效。

排查顺序很有讲究:先virsh start查看是否报 libvirt 错误,再virsh console进入串口看启动日志,最后查看网络连通性。不要一上来就重启宿主机,宿主机一重启,所有实例都在线变离线,影响面太大。串口日志里如果看到No DHCPOFFERS received,直接检查虚拟机网卡的 MAC 地址是否与 DHCP 绑定冲突。

网络不通的另一个隐蔽来源是宿主机防火墙。我们有一次配置了 bridge 网络,虚拟机之间互相能通,但外部访问被拒,排查到最后发现是 iptables 规则里 FORWARD 链默认 DROP。如果你的虚拟化平台管理面板看起来都正常,就是外部流量进不来,优先检查 FORWARD 链规则,而不是看 INPUT 链。

4.3 计费不准,token 数量忽高忽低怎么办

token 计费不准通常发生在两个位置,一个是模型服务返回的 usage 字段自己就不可靠,另一个是网关重复计费。第一个问题需要做兜底:计量模块从响应体解析 usage 时,如果字段缺失或者为负,直接丢弃本次记录并告警,绝不允许写一条空数据进账单。这个策略让我少背了很多锅,宁可漏一次也不多记一次。

第二个问题的排查方法是看 usage_records 里有没有相同 request_id 的行。如果发现大量重复记录,先查网关重试机制,HTTP 客户端的重试参数要对 GET 和 POST 做区分。POST 请求默认不要简单重试,可以重试但要保证业务层幂等。我们后来把计量接口改成:根据 request_id 存在就更新不插入,才彻底解决。

审计时还有一个细节容易被忽略,token 消耗要按输入和输出分开统计,很多服务的计费标准里输入和输出单价不同。如果只存 total_tokens,后续调价就无法追溯。建议从一开始就在记录表里单独保存 prompt_tokens 和 completion_tokens,只聚合的时候才合并。

4.4 安全加固与权限隔离的注意事项

安全是这套系统的立身之本,但不是装个防火墙就完事。我们用的思路是三层控制。第一层是网络层,内部业务网络和管理网络分开,管理后台只绑定在内网地址,运维人员通过 SSH 堡垒机访问;第二层是虚拟机权限层,每个业务账户只分配自己的虚拟机和对应的 SSH 密钥,禁止共享 root 密码;第三层是数据层,计费数据库单独放在一台专用虚拟机里,其他业务实例一律不能直连它的端口,只能通过 API 访问。

主机加固里最容易忽略的是 sshd 配置。公钥登录开启之后,密码登录一定要显式关闭,光靠改端口、改 sshd_config 里的PasswordAuthentication no,外加PermitRootLogin prohibit-password,能挡掉 99% 的撞库扫描。

关于快照安全再提醒一句,快照文件不是垃圾,备份存储卷的权限组要对运维账号做最小授权。之前我们出现过一次备份权限过大,一个普通业务实例的进程因为宿主机目录挂载失误能读到整个备份目录,虽然没造成实质性泄露,但吓得够呛。所以现在所有备份存储挂载点一律以只读方式挂给需要访问的虚拟机,并且在挂载参数里强制ro,nodev,nosuid。

5. 落地心得和后续能扩展的方向

这套系统从立项到开源用了不到一个月,但光是一个 token 计费经常不准的问题就折腾了两周。我个人比较深的体会是,“绝对安全”和“透明计费”都不是某一个功能可以承诺的,必须靠完整链路叠加。虚拟化隔离解决的是边界问题,token 指纹存储解决的是凭证管理问题,幂等计费解决的是数据可信问题,三者缺一不可。如果只做了虚拟化但计费口子漏风,用户照样不信任。

后续可能扩展的方向,首先是做一个自助化面板,允许项目负责人自助创建虚拟机、自定义配额,管理员只做审批;其次是资源告警更精细,把同一宿主机上的实例负载关联起来做调度建议;再往后希望把计费模块做成标准插件,让不同虚拟化后端比如 LXD、VirtualBox 也能接入。开源之后已经有几个小团队在试跑,反馈最多的还是想要一个更友好的预算超限提醒,这个已经在排期。如果你正好也需要一套带透明计费的虚拟机管理工具,建议先从最小闭环开始,别一上来就铺大而全。先把一台宿主机架起来,手动创建一台实例,跑通 token 签发和明细报表,再慢慢加自动化,稳着来比什么都快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询