稳定路由(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)

稳定的间接层由两部分组成:

  1. 一个 DNS 通配符*.dozyfs 用长 TTL 静态解析到锚点网关(anchor gateway)的固定 VIP。注意这里 DNS 解析的是网关(永久不变),不是 VM——所以没有 TTL 抖动问题。
  2. 控制面 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_stateunbound / 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)activeactivation_epoch+1
idle / 冷停 / 深冷unbind_routeidle,清空 node_id/guest_ip

因为网关每条新连接都现查、不缓存旧绑定,下一条连接立刻打到新节点。名字、VIP、客户端配置全程零改动。

确定性 MAC。VM 的 MAC 从 vm_id 确定性派生并固化在配置里,所以冷启(哪怕换了节点)后 guest 内 eth0 看到的网络身份完全一致——VM 里的服务无需感知自己被搬过。
防竞态:epoch 校验。每次激活让 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
wssTLS 监听口上的同一隧道(TLS 终结于网关,后端说明文 ws):8443

ws/wss 是一条不透明的双向字节隧道:网关不解析帧,所以 WebSocket 承载的任何东西(包括文件字节)都原样穿过。在 AWS c5.metal 上的实测:四种协议都通过稳定链接逐字节正确取回了 64MB 的 dozycat 文件(sha256 匹配);文件读取速度 http 约 360 MB/s、https 约 262 MB/s。

已知性能跟进项(非 bug)。大文件批量传输时 wss 约 10–25 MB/s(功能完全正确:sha 匹配、零错误)。这是简单 Python select 中继做 WS-over-TLS 的结构性开销,适合控制/交互式 wss;批量场景的中继优化作为后续项跟踪。

请求路径走查(含 deep-cold 激活)

把上面的零件串起来,一条到 https://yiuei4gk6m.dozyfs/... 的请求是这样走的:

  1. DNS*.dozyfs → 锚点网关的固定 VIP(长 TTL,永久稳定)。客户端永远连同一个地址。
  2. 网关读 Host/SNI:取出稳定名 yiuei4gk6m.dozyfs。也接受把稳定名包在公网域名下的写法(如 ...dozycat.example.com,网关会剥回稳定名)。
  3. 查表resolve_route(name) 返回当前绑定。
  4. 分支
    • active:直接反向代理 / DNAT 到 node_id 上的 guest_ip:port。响应带上 X-Dozycat-NodeX-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 主要由冷启分层决定。

延伸阅读