先解释一下标题里这个有点野的代号龙虾。我们内部把每个跑在虚拟机里的服务实例都叫作“龙虾”壳硬是指虚拟化隔离做得足够彻底肉鲜是指对外提供的调用质量足够高。这套系统最近已经开源核心就是一件事把个人公司里的各种服务统一塞进虚拟机里管理顺便把每个 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/br0auto 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,formatqcow2 \ --import \ --os-variant debian12 \ --network bridgebr0,modelvirtio \ --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 和 hostnameDNS 记录也同步更新。顺序很重要顺序错了实例之间可能发生 IP 冲突排查起来很痛苦。开机后用virsh domifaddr lobster-01检查网卡地址确认服务可连再把实例接入统一管理后台打上项目标签这一步才算真正“入池”。管理后台的建池逻辑是用配置文件驱动的。每个项目的定义文件是一个 YAMLname: 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, algorithmHS256) 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 签发和明细报表再慢慢加自动化稳着来比什么都快。