稳定路由(Stable Routing)
每个 VM 拥有一个永久名 xxxxx.dozyfs:锚点网关按需查注册表把它代理到 VM 当前所在节点,冷启换节点只改一行绑定,名字和连接方式永不改变。
什么是 xxxxx.dozyfs
在 dozycat 里,每个 volume / cat(microVM) 都有一个永久的稳定名,形如 xxxxx.dozyfs。这个名字与 volume 同生命周期,建卷时一次性派生、永不复用:
short-id = base32(sha1(volume_id))[:10]
稳定名 = <short-id>.dozyfs # 例如 yiuei4gk6m.dozyfs
它是 VM 内工作负载(git daemon、HTTP/WebSocket 服务、SSH 等)面向外部的稳定入口(ingress)。客户端只要记住这一个名字,无论 VM 之后被挂起、快照、迁移、还是冷启到另一台机器,连接方式都不变。
xxxxx.dozyfs 只承载 guest 工作负载流量。它绝不携带文件系统、元数据或对象存储流量——FS 数据面走 guest 本地的 virtio-fs vhost-user socket,永远不上网络。为什么 guest IP 不能当稳定标识
dozycat 面向 serverless 规模:一个 volume 一个 VM,约 1000 并发、10 万级轮换。在这种模型下 guest IP 是故意不稳定的:
- 节点本地、可回收:guest IP 从每节点的本地池(如
10.<node>.0.0/24)分配,VM 走后即回收。同一个10.7.0.5在不同节点可以复用。 - 换节点必然改变:VM 冷启到新节点时会拿到新节点上的新 IP。如果客户端记的是 IP,迁移后就连到了错误的(甚至已停机的)机器。
- 深冷时根本不存在:处于 deep-cold 的 VM 没有进程、甚至没有分配节点,因此没有可连的 IP。
所以 dozycat 把稳定性放在名字上、把易变性留给 IP:对外稳定的永远是 xxxxx.dozyfs,guest IP 只是节点内部寻址用的私有地址。
锚点网关查表(name → 当前 node:guest_ip)
稳定的间接层由两部分组成:
- 一个 DNS 通配符:
*.dozyfs用长 TTL 静态解析到锚点网关(anchor gateway)的固定 VIP。注意这里 DNS 解析的是网关(永久不变),不是 VM——所以没有 TTL 抖动问题。 - 控制面 registry 的
route表:这是 name→当前绑定的权威来源(source of truth)。网关只读它,且从不缓存绑定。
route 表为每个稳定名保存当前状态:
| 列 | 含义 |
|---|---|
stable_name | 主键,<short-id>.dozyfs,与 volume 同寿、永不复用 |
volume_id / vm_id | 绑定的 volume 与当前活跃 VM(idle 时 vm 可空) |
node_id / guest_ip | 当前承载节点与节点本地 IP(换节点时更新) |
ports | {label: guest_port} JSON,供按服务路由 |
binding_state | unbound / activating / active / idle / error |
activation_epoch | 每次激活 +1,用于防竞态(见下) |
网关对每条连接调用唯一的只读接口 resolve_route(name),拿到当前 node_id / guest_ip / ports / binding_state / activation_epoch,然后反向代理到该节点上 VM 的端口。可通过控制面查看:
# CLI
dozycat route ls
dozycat route get yiuei4gk6m.dozyfs
# API
GET /api/routes
GET /api/routes/yiuei4gk6m.dozyfs
冷启换节点:rebind(epoch+1,名字不变)
路由能「活过 reschedule」的全部机制只有一句话:换节点时控制面只 UPDATE route 这一行。
| 生命周期事件 | 对 route 的动作 |
|---|---|
建卷 create_volume | 派生稳定名,binding_state='unbound' |
| 激活 / 热启 / 启动(哪怕落到新节点) | bind_route(name, vm_id, 新 node_id, 新 guest_ip, ports) → active,activation_epoch+1 |
| idle / 冷停 / 深冷 | unbind_route → idle,清空 node_id/guest_ip |
因为网关每条新连接都现查、不缓存旧绑定,下一条连接立刻打到新节点。名字、VIP、客户端配置全程零改动。
vm_id 确定性派生并固化在配置里,所以冷启(哪怕换了节点)后 guest 内 eth0 看到的网络身份完全一致——VM 里的服务无需感知自己被搬过。activation_epoch+1。若一次迁移正在进行(binding_state='activating',epoch 已变),网关代理前会比对 epoch,绝不把连接发往迁移中途的旧绑定。支持的协议
同一个稳定链接支持四种协议,全部都能读到 VM 的 dozycat 文件。网关在自己这里终结 TLS,后端 guest 始终说明文 http/ws(VM 内无需任何证书):
| 协议 | 网关处理 | 监听端口(默认) |
|---|---|---|
http | 明文反向代理 | :8088 |
https | 同代理,监听口 TLS 包裹(TLS 在网关终结,后端仍明文 http) | :8443 |
ws | 检测到 Upgrade: websocket 后交给原始双向字节隧道 | :8088 |
wss | TLS 监听口上的同一隧道(TLS 终结于网关,后端说明文 ws) | :8443 |
ws/wss 是一条不透明的双向字节隧道:网关不解析帧,所以 WebSocket 承载的任何东西(包括文件字节)都原样穿过。在 AWS c5.metal 上的实测:四种协议都通过稳定链接逐字节正确取回了 64MB 的 dozycat 文件(sha256 匹配);文件读取速度 http 约 360 MB/s、https 约 262 MB/s。
请求路径走查(含 deep-cold 激活)
把上面的零件串起来,一条到 https://yiuei4gk6m.dozyfs/... 的请求是这样走的:
- DNS:
*.dozyfs→ 锚点网关的固定 VIP(长 TTL,永久稳定)。客户端永远连同一个地址。 - 网关读 Host/SNI:取出稳定名
yiuei4gk6m.dozyfs。也接受把稳定名包在公网域名下的写法(如...dozycat.example.com,网关会剥回稳定名)。 - 查表:
resolve_route(name)返回当前绑定。 - 分支:
active:直接反向代理 / DNAT 到node_id上的guest_ip:port。响应带上X-Dozycat-Node和X-Dozycat-Epoch头。idle / cold / deep-cold:网关返回 503(含binding_state提示),触发 wake-on-traffic(来一个包就激活)——控制面activate(volume):选节点(亲和)→ 起始层激活 → 挂 volume → 等 ready。深冷时这一步是 boot + 挂载 + 应用启动 + warmup,耗时数秒到数十秒。ready 后控制面bind_route把状态置active、epoch+1。客户端按Retry-After退避重连,下一条连接即命中新 guest。
客户端 ──https──▶ 锚点网关(固定 VIP) ── resolve_route ──▶ route 表
│
active ─────────►│──► 代理到 node_id 的 guest_ip:port ──► guest 服务
idle/deep-cold ───►│──► 503 → 控制面激活(选节点→起层→挂卷→ready)
│ → bind_route(active, epoch+1)
└────► 重试命中新 guest
※ FS 数据面不在此图:文件 IO 走 guest 本地 virtio-fs,永不经网关。
因此 xxxxx.dozyfs 的首字节延迟 = 网关查表(µs~ms)+ 激活延迟(热为毫秒级、深冷为数秒到数十秒)。网关本身只加一跳代理,可忽略;P99 主要由冷启分层决定。
延伸阅读
- 核心概念——dozycat / node / cat-agent / cat / volume / route。
- 生命周期与冷热轮换——hot-stop / cold / deep-cold 与 drain。
- 源码:github.com/dozycat/dozyfs(网关实现见
dozycat/gateway.py)。
dozycat