NFS 上的存储池
本页说明 node 的存储池(storage pool)能否放到 NFS 挂载上、服务端与客户端各需要什么条件,以及这样做在延迟上的代价。
概述
node 把 Volume 的块文件与保留的可写层(retained layer)保存在存储池里。存储池是一个目录,下面固定有两个子目录,以及一个证明其身份的标记文件(marker):
| 路径 | 内容 |
|---|---|
<pool>/volumes/ | Volume 的块文件,每个 Volume 一个,稀疏的 ext4 镜像文件,长度等于 Volume 容量 |
<pool>/retained-layers/ | 保留的可写层,每个 Workspace 一个,同上,长度等于可写层容量 |
<pool>/.kibotos-storage-pool | 标记文件,记录这个目录声明的池 ID。由 kibotos-node storage-pool init 写入,node 自己从不创建或修复它 |
node 在配置中用 [[storage.pool]] 声明一个或多个存储池(id、path、default、shared,见配置)。池目录可以放到 NFS 上:整个目录作为一个 NFS 导出挂载,volumes/、retained-layers/ 与标记文件都在导出范围内。node 创建块文件时只把它截断到容量再格式化,之后只写 guest 实际用到的块,不使用打洞、预分配或 reflink,也不向 guest 宣告 DISCARD。NFSv3、v4.0、v4.1 与 v4.2 上都已验证:两个后端都能创建 Workspace、写入、停止、扩容、重新启动并保持数据。版本之间的差别见NFS 版本。
storage.root 下的其他目录不属于任何存储池,始终留在本地磁盘:
<storage.root>/
├── installations/ # runtime 安装
├── runtime/ # 运行期文件与 owner lock;storage-pools/<pool-id>/ 下是这个 node 自己的挂载记录
├── state/ # node 自己的持久状态
├── template-cache/ # Template 缓存,由 template_cache_limit 单独限额
├── network/ # 宿主网络状态
├── pki/ # node 的受管身份
└── machine-logs/ # 已结束 Instance 的控制台与 VMM 日志末尾本页其余部分只涉及存储池目录。
目录结构
池目录下的两个子目录各有固定的形状。文件名就是对象 ID,不含 node 标识——两个挂着同一份存储的 node 用的是同一个路径,这是设计如此,互斥由 ctrl 的使用权负责:
<pool>/
├── .kibotos-storage-pool # 标记文件
├── volumes/
│ ├── data/ # <volume_id>.ext4 稀疏块文件,长度等于容量
│ ├── metadata/ # <volume_id>.json spec、实际容量、上一次交接是否干净
│ ├── holders/ # <volume_id>.json 持有者:node_id 与纪元
│ ├── releases/ # <volume_id>.json 释放回执:停止写入的持有者
│ ├── locks/ # <volume_id>.lock flock,只在本机内互斥
│ └── transfer-staging/ # 导出与导入的中转文件
└── retained-layers/
├── data/ # <opaque_ref>.ext4
├── metadata/ # <opaque_ref>.json continuity、lower identity、容量
├── holders/ # 同上
├── releases/ # 同上
└── locks/ # <opaque_ref>.locktransfer-staging/ 只放导出与导入期间的中转文件,命令结束就删除。Transfer 归档本身不在 node 上,它存放在 ctrl 的 artifact store 里,所以导入可以在接入目标池的任意一台 node 上执行。
池里只记录对象本身的事实,不记录「哪个 Instance 挂着它」。挂载关系记在挂载它的那个 node 的本地目录里(即上文 storage.root 下的 runtime/storage-pools/),别的 node 从来不读。
每个对象在池里有三类记录,各自是一个独立文件,写入都是先写临时文件、fsync、再原子改名:
| 记录 | 谁写 | 内容 |
|---|---|---|
metadata/ | 当前持有者 | spec、实际容量、未完成的扩容、生命周期状态、上一次交接是否干净(released_cleanly) |
holders/ | 接手对象的 node | 持有者的 node_id 与 ctrl 授予的纪元(epoch) |
releases/ | 停止写入的持有者 | 释放回执:这个持有者的 node_id 与纪元 |
两台宿主机从不改写同一个文件,读每条记录时都重新打开文件,不用缓存。跨主机的判断因此不依赖文件锁,挂载 nolock 也成立。
标记文件是一段 JSON:
{
"version": 1,
"pool_id": "shared",
"instance": "bf54d7bdc3836824676f7ffef11ebc4d",
"created_at": "2026-09-17T20:20:23.084208280Z"
}instance 在初始化时生成一次,标识这份存储本身而不是给它起的名字。共享池里几个 node 读到的是同一个文件,因此报给 ctrl 的 instance 相同;有人把另一个目录也叫同一个 pool_id 时 instance 不同,ctrl 拒绝后来那一方的接入。这时 kibo storage-pool get <pool> 把后来那一方列为 unavailable 并给出原因,ctrl 日志也记一条。最常见的情形是几台 node 的本地池用了同一个 id。
文件名与对象 ID
看目录就能认出对象,从 ctrl 的记录也能直接找到文件。ID 能直接当文件名,靠的是它的形状:
- ctrl 保证
volume_id与opaque_ref都是 1–32 字节的小写字母、数字和连字符,不以连字符开头。 - node 使用前再检查一遍,不合形状的命令直接拒绝,不写任何文件。ID 里因此不会出现
/、..或空格,也不会在大小写不敏感的文件系统上折成同一个名字。 - 真实身份仍记在 metadata JSON 里,provider 读取时核对它与文件名对得上,对不上就拒绝。
v0.1.0 之前的版本用 ID 的 SHA-256(64 位小写 hex)作文件名。升级后这些对象照常使用,在没有挂载时改成 ID:
- node 启动时,改名本 node 持有、没有挂载的对象。挂载中的在释放时改名。启动时没有改到的,在下一次挂载或删除之前改名。
- 共享池里只有当前持有者改名。另一台 node 接手旧名字的对象后,由它改名。
- 一个对象的文件按固定顺序改名,metadata 最后改。改名中途 node 退出的,由下一个用到这个对象的 node 改完:可能是它自己重启之后,也可能是接手对象的另一台 node。
- 同一个文件在两个名字下都存在时,改名停下,见下文。
启动日志里 node storage objects by name 一行给出每个池改了多少、还剩多少旧名字。旧名字的对象要对上 ID,就读 metadata JSON,或者自己算:
printf '%s' vol-06gb22twhsw9h7t04g99e42ndc | sha256sum同一个文件在两个名字下都存在
node 不判断哪一份是对的,这个对象的改名停下,直到多余的一份被移走:
- 启动日志记一条
node storage objects kept digest names,error字段给出这个对象。 - 用到这个对象的操作以可重试的错误失败,包括挂载、删除、扩容,以及 Workspace 停止时的释放。ctrl 会一直重试。
这种情况通常来自从备份恢复或手工复制池目录。按文件所在的目录处理:
| 两个名字下都有的文件 | 处理 |
|---|---|
locks/ 下的锁 | 空文件,移走任意一份 |
holders/、releases/ 下的记录 | 保留纪元(epoch)更大的一份 |
data/ 下的块文件、metadata/ 下的 metadata | 按恢复或复制时的意图保留一份 |
多余的一份先挪到池目录以外,Workspace 恢复正常后再删除。挪走之后,下一次重试就会把改名做完,不需要重启 node。
独立池与共享池
- 一个池只被一个 node 声明时,两个 node 不能把各自的
volumes/、retained-layers/指向同一份存储:这仍然是普通的独立导出,每个 node 用自己的目录。 - 把
shared = true标在池上,几个 node 就可以声明同一个池 ID、同一份存储:这是一个 Workspace 停止后能在另一个 node 上启动的方式。跨主机的互斥不再靠文件锁,而是靠 ctrl 授予的使用权(use right):ctrl 只把一个 Volume 或保留的可写层的使用权交给一个 node,没有拿到使用权的 node 不会去打开对应的块文件。挂载nolock的云厂商 NAS 因此仍然安全,见前提条件。 - node 自己在这两个目录里仍然用
flock防止本机内的并发;跨主机的互斥不依赖它。 - 一个 node 宕机后,Operator 用
kibo node isolate声明它不能再写存储。之后对象在共享池里的 Workspace 可以在另一个 node 上启动或删除。对象在独立池里的 Workspace 可以删除,文件等池有可用的 node 后再清理;要启动它,等这个 node 回来,或按池换宿主把盘接到别的宿主机;盘也丢了时,按宣告池丢失处理。见node。 - 要把已有的 Volume 与可写层从独立池搬进共享池,见在池之间搬迁对象。
池换宿主
只有一个 node 接入的池,在原宿主丢失而盘完好时可以换到另一台宿主机。盘也丢了时改为宣告池丢失:
- 隔离原 node:
kibo node isolate。确认原宿主已经不能写这份存储。 - 退役原 node:
kibo node retire。 - 把盘接到新宿主机,在新 node 的配置里以相同的池 ID 声明。池目录里的标记不变。
新 node 登记后成为这个池的成员:
- 挂起的物理清理在它上面执行。
- 原有 Workspace 可以在它上面启动,
/data内容保持。原 node 被隔离过时,第一次启动等释放回执到上限后打开,可写层报告UNKNOWN;原 node 从DRAINED退役时直接打开。 - 新 node 的
check-maintenance不因池里写着原 node 的持有者记录而失败。
纪元与接手
ctrl 每次把对象交给另一个 node,或在持有者被 isolate 之后重新授予,都会把纪元加一,并随打开块文件的每条命令一起发给 node。node 在打开块文件之前,拿池里 holders/ 记录的纪元与命令带来的纪元比较:
| 池里记录的持有者 | node 的做法 |
|---|---|
| 纪元比命令新 | 拒绝,不打开文件。错误说明使用权已被接手(taken over),重试也不会成功 |
| 同一纪元,就是这个 node | 直接打开。这是重试,或 node 重启后接管运行中的 Instance |
| 纪元比命令旧,或没有记录 | 接手:先写入自己的持有者记录,再打开 |
接手时,如果上一个持有者是被 isolate 的,node 先等它的释放回执,最多 15 秒(按单调时钟计):
- 回执在时限内出现:立即打开,记录这次交接是干净的。
- 到时限仍没有回执:照样打开,在 metadata 里记下
released_cleanly=false。
被 isolate 的 node 如果其实还连得上池,会在 5 秒一次的检查里发现纪元变新,停止写入、刷盘关闭文件、写下回执,所以 15 秒足够它让出。上一个持有者是正常解绑的,不需要等待。一个 Workspace 同时接手 Volume 与可写层时,两份回执一起等,最多也是 15 秒。
正常解绑时,node 刷盘并关闭块文件,在 metadata 里记下这次释放是干净的;持有者记录保留,下一个持有者据此看到自己接替的纪元。
前提条件
服务端:
- 导出目录使用
rw,sync,no_subtree_check,no_root_squash。node 以 root 运行,创建volumes/、retained-layers/子目录时把它们改成root:kibotos-vmm、0710,创建块文件时把文件改成root:kibotos-vmm、0660,root_squash会让这些步骤都失败。 - 使用
sync导出:已经 COMMIT 到服务端的数据不再因服务端掉电丢失,async会去掉这一层保证。guest 的fsync保证什么见注意。 - 独立池的导出目录只给一个 node 用。共享池才允许几个 node 挂载同一个导出,且要在每个 node 的配置里把这个池标成
shared = true。
客户端(node 宿主机):
-
安装 NFS 客户端(Debian、Ubuntu 上是
nfs-common)。 -
挂载参数里写明版本(
vers=4.2、vers=4.1、vers=4.0或vers=3),不要依赖协商结果。各版本的要求见NFS 版本,各版本的挂载命令见其他版本的挂载参数。 -
挂载参数使用
hard。 -
共享池的挂载不要加
nocto。一个 node 接手另一个 node 的对象时,会重新打开前一个持有者写下的记录;nocto会让它读到过期的内容。Linux 客户端默认保持关闭到打开的一致性(close-to-open),不写这一项即可。 -
服务端提供 NLM 锁(自建 Linux nfsd)时不要加
nolock或local_lock;服务端不提供 NLM 锁(多数云厂商的 NFSv3)时必须加nolock。见云厂商 NAS。 -
服务端与 node 宿主机上的 uid、gid 一致:块文件以
root:kibotos-vmm存放,NFSv4 的 idmap 也要把这两个账号映射一致。 -
池目录本身要能被
kibotos-vmm穿越。 node 只负责volumes/、retained-layers/两个子目录的属主与权限,不会修改池目录本身:这与storage.root到/的每一级目录必须能被kibotos-vmm穿越是同一条规则(见身份、配置与连接)。挂载完成后执行一次:sudo chown root:kibotos-vmm /var/lib/kibotos/pool-a sudo chmod 0710 /var/lib/kibotos/pool-a共享池只需要在其中一台宿主机上执行一次:权限记录在服务端,其他挂载同一导出的宿主机直接生效。
-
挂载在 node 服务启动之前完成。池目录本身必须已经存在(无论是挂载点还是本地目录):node 在它下面创建
volumes/、retained-layers/时,池目录不存在会导致 node 启动失败,而不是把这个池标记为不可用。
配置
node 在 [storage] 里用 [[storage.pool]] 声明每个池:
[storage]
root = "/var/lib/kibotos/node"
template_cache_limit = "100G"
[[storage.pool]]
id = "pool-a"
path = "/var/lib/kibotos/pool-a"
default = true
[[storage.pool]]
id = "shared-a"
path = "/var/lib/kibotos/shared-a"
shared = true| 字段 | 说明 |
|---|---|
id | 部署指定的池 ID,写入标记文件,1–63 个小写字母、数字、-、.,首尾不能是分隔符。只有一台 node 接入的池在整个部署里不能与别的 node 重名,用这台 node 的 ID 即可;多台 node 共用的池在各台写同一个 id |
path | 池自己的目录,绝对路径。可以等于 storage.root,这是单节点部署原地保留数据的方式;也可以在别处,比如本页要挂 NFS 的目录 |
default | 请求没有指定池时使用的池。只声明一个池时隐式为默认,声明多个池时必须恰好有一个 default = true |
shared | 标记这个池被多于一个 node 声明,放宽维护检查:不再要求这个 node 能认领池里的每一个对象。node 启动时也只删除池里本 node 中断的写入留下的临时文件,见中断的写入留下的临时文件 |
池的容量不在这里写,见池容量。
多个 node 声明同一个共享池时,各自配置里这个池的 default 取值应保持一致:ctrl 认为一个池是不是某个 node 的默认,是按这个 node 自己声明的值来的。
初始化池
kibotos-node storage-pool init 在池目录里写标记文件,证明这个目录就是配置里声明的那个池。node 自己从不创建或修复这个文件:没有它,任何要用这个池的操作都会把它当作不可用。这条命令不读取、也不需要 node 的配置文件,--id、--path 与 [[storage.pool]] 里的值要一致。
新的空池目录:
sudo kibotos-node storage-pool init --id pool-a --path /var/lib/kibotos/pool-a目录不存在时会被创建;目录存在且非空、又没有标记文件时会被拒绝,除非加 --adopt:
sudo kibotos-node storage-pool init --id pool-a --path /var/lib/kibotos/pool-a --adopt--adopt 用于一个已经装着 Volume 或保留的可写层的目录,保留其中的数据,只补上标记文件。已经初始化过的池重复执行 init 不会报错;用一个不同的 --id 对着同一路径执行会被拒绝,标记文件从不被静默覆盖。
顺序很重要。 在挂载并初始化池目录之前启动 node,node 会在里面建出空的 volumes/、retained-layers/ 子目录(此时标记文件还不存在,池仍是不可用状态)。之后再执行 init 时目录已经非空,需要加 --adopt 才能继续;这不会覆盖任何数据,只是绕过了「空目录」检查。
步骤
以 storage.root = /var/lib/kibotos/node、独立池 pool-a、服务端 nfs.example.com 导出 /srv/kibotos-nfs/pool-a 为例。
1. 在服务端导出目录
set -eu
sudo install -d -m 0700 /srv/kibotos-nfs/pool-a
printf '%s\n' '/srv/kibotos-nfs/pool-a 192.0.2.10(rw,sync,no_subtree_check,no_root_squash)' | \
sudo tee -a /etc/exports >/dev/null
sudo exportfs -ra192.0.2.10 是 node 宿主机的地址。
2. 在 node 宿主机挂载并初始化
先停止 node 服务,挂载导出目录,设置权限,再写入池标记:
set -eu
sudo systemctl stop kibotos-node.service
sudo install -d -m 0700 /var/lib/kibotos/pool-a
printf '%s\n' \
'nfs.example.com:/srv/kibotos-nfs/pool-a /var/lib/kibotos/pool-a nfs vers=4.2,hard,_netdev 0 0' | \
sudo tee -a /etc/fstab >/dev/null
sudo mount /var/lib/kibotos/pool-a
sudo chown root:kibotos-vmm /var/lib/kibotos/pool-a
sudo chmod 0710 /var/lib/kibotos/pool-a
sudo kibotos-node storage-pool init --id pool-a --path /var/lib/kibotos/pool-a在配置的 [storage] 里加上对应的 [[storage.pool]] 段(见配置),再启动 node:
sudo systemctl start kibotos-node.service3. 确认
创建一个 Workspace,然后在 node 宿主机上查看块文件:
sudo ls -ls /var/lib/kibotos/pool-a/volumes/data /var/lib/kibotos/pool-a/retained-layers/data第一列是实际占用的块数,应远小于文件长度。用 kibo storage-pool list 从 ctrl 一侧确认这个池的可用性,见存储池。
共享池:另一个 node 挂载同一份存储
在第二台 node 宿主机上重复第 2 步的挂载与权限设置(导出目录相同),但不需要再执行 storage-pool init:标记文件已经在服务端目录里,两台宿主机看到的是同一份内容。两边的配置都要把这个池标成 shared = true:
[[storage.pool]]
id = "shared-a"
path = "/var/lib/kibotos/shared-a"
shared = true云厂商 NAS
云厂商托管的 NFS 服务(阿里云 NAS、AWS EFS 等)替代上面的自建服务端。它们的服务端参数在控制台里设置,协议版本和锁的支持与自建 nfsd 不同。以阿里云 NAS 为例,其他厂商按同样的对照表处理。
服务端(控制台):
- 权限组里 Linux 用户映射选「不映射」(no_squash)。node 以 root 改子目录和块文件的属主,root 被映射掉就会失败,等价于自建服务端的
no_root_squash。 - 独立池用一个独立的文件系统或独立的子目录,规则与自建服务端相同;共享池的多个 node 才指向同一个。
- 版本由产品决定:阿里云极速型 NAS 只有 NFSv3,通用型 NAS 有 NFSv3 与 NFSv4.0,都没有 v4.1 和 v4.2。能选 v4.0 时选 v4.0:锁在协议内完成,不依赖单独的 NLM 锁服务,见NFS 版本。
客户端(node 宿主机)以阿里云极速型 NAS 为例,/etc/fstab 写法:
printf '%s\n' \
'file-system-id.region.extreme.nas.aliyuncs.com:/pool-a /var/lib/kibotos/pool-a nfs vers=3,nolock,noacl,noresvport,hard,_netdev 0 0' | \
sudo tee -a /etc/fstab >/dev/null其余步骤(停 node、创建挂载点、挂载、设置权限、初始化、起 node、确认)与步骤相同。厂商推荐命令里各选项与本页要求的对照:
| 厂商推荐的选项 | 本页的处理 |
|---|---|
nolock | 云厂商的 NFSv3 服务端不提供 NLM 锁,不加 nolock 时 node 创建 Workspace 会一直等待锁服务。加上它以后不需要 rpc.statd,也没有服务端重启后的锁恢复期。共享池的跨主机互斥本来就不靠锁,靠 ctrl 授予的使用权,见独立池与共享池 |
noresvport | 网络断开重连时客户端换新的源端口,避免服务端残留旧连接状态导致挂死。云 NAS 上建议加。自建 Linux nfsd 的导出默认带 secure,会拒绝非保留端口,自建场景要加它就得同时在 /etc/exports 里加 insecure |
noacl | 不查询 NFSACL 旁路协议。node 只用 chown 和 chmod,不用 ACL,服务端不支持该协议时(阿里云极速型 NAS)加上可以省掉挂载时一次失败的 RPC;自建 nfsd 不需要 |
hard | 与本页要求相同 |
proto=tcp、timeo=600、retrans=2 | Linux 客户端 TCP 挂载的默认值,写不写都一样 |
rsize=1048576、wsize=1048576 | 现代客户端与服务端协商出来就是 1 MiB。块文件的负载是小块写加 fsync,这两个值不影响延迟 |
vers=4,minorversion=0 | 通用型 NAS 用 v4.0 时的写法,等价于本页的 vers=4.0。v4.0 服务端自带文件锁,不加 nolock |
v3 用数字 uid、gid 传输,不经过 idmap,只需要 node 宿主机上存在 kibotos-vmm 账号;服务端不校验这个 gid 是否存在。
vers=3,nolock,noacl 这组参数已对自建 nfsd 验证过 node 的完整流程:创建 Workspace、写入、停止、重新启动并保持可写层与 Volume 的数据、删除。noresvport 只按厂商说明收录,没有单独验证。
NFS 版本
四个版本都能承载块文件,差别在锁和恢复上。
| 版本 | 额外要求 | 提醒 |
|---|---|---|
| v4.2 | 无 | 推荐。服务端与客户端内核都支持时用这个 |
| v4.1 | 无 | 推荐。与 v4.2 的差别只在本页用不到的功能 |
| v4.0 | 无 | 没有会话机制,服务端或网络中断后锁与文件状态的恢复比 v4.1 弱。只在服务端不支持 v4.1 时使用 |
| v3 | 服务端提供 NLM 锁时:客户端 rpc.statd 在运行,服务端开放 lockd、statd、mountd 的端口。服务端不提供 NLM 锁时:挂载加 nolock | 文件锁走单独的 NLM 协议,锁服务不可达又没加 nolock 时 node 创建或启动 Workspace 会等待而不是立即失败。只在服务端不支持 v4 时使用 |
选版本不影响刷写延迟:四个版本的实测延迟处于同一量级,见注意。选 v4.1 或 v4.2 的理由是恢复语义和少一套锁服务。nconnect=4 不改善单个 Workspace 的延迟,不需要加。
其他版本的挂载参数
第 2 步的示例用 v4.2。换版本只改 /etc/fstab 里的 vers=,其余参数相同。
v4.1:
printf '%s\n' \
'nfs.example.com:/srv/kibotos-nfs/pool-a /var/lib/kibotos/pool-a nfs vers=4.1,hard,_netdev 0 0' | \
sudo tee -a /etc/fstab >/dev/nullv4.0:
printf '%s\n' \
'nfs.example.com:/srv/kibotos-nfs/pool-a /var/lib/kibotos/pool-a nfs vers=4.0,hard,_netdev 0 0' | \
sudo tee -a /etc/fstab >/dev/nullv3 对自建 nfsd 先确认客户端的 rpc.statd 在运行,再写 fstab(云厂商的 v3 不走这条,见云厂商 NAS):
sudo systemctl enable --now rpc-statd.service
printf '%s\n' \
'nfs.example.com:/srv/kibotos-nfs/pool-a /var/lib/kibotos/pool-a nfs vers=3,hard,_netdev 0 0' | \
sudo tee -a /etc/fstab >/dev/nullv3 的服务端还要开放 rpcbind(111)、mountd、lockd、statd 的端口;v4 只用 2049。写 vers=4 而不带小数位会让客户端与服务端协商小版本,结果取决于两边内核,不要这样写。
临时验证某个版本时可以不改 fstab,直接挂载:
sudo mount -t nfs -o vers=3,hard nfs.example.com:/srv/kibotos-nfs/pool-a /var/lib/kibotos/pool-a挂上以后用 /proc/mounts 核对实际生效的版本:
grep /var/lib/kibotos/pool-a /proc/mountsvers=4.2、vers=4.1、vers=4.0 或 vers=3 应出现在挂载选项里。
注意
-
两个后端的语义相同:guest 的
fsync只保证数据进入宿主页缓存,由宿主内核回写。正常 stop / start、node 重启与宿主正常重启后数据保持;宿主掉电或内核崩溃可能丢失最近未回写的写入。fsync返回不代表数据已经到达 NFS 服务端,这是边界,不是缺陷。 -
guest 的
fsync不触发宿主对块文件的fsync,在 NFS 上也就不产生逐次的 COMMIT 往返。下表是 KiboVM 后端在同一台宿主上的实测:4 KiB 追加写加fsync的平均延迟,NFS 服务端在另一台机器上,两机往返约 2 ms。同一位置测了多轮时给出范围。目标 本地磁盘 NFSv3 NFSv4.0 NFSv4.1 NFSv4.2 可写层 0.4 ms 0.8–1.4 ms 1.7 ms 1.0 ms 0.8–0.9 ms Volume 0.5 ms 1.0–1.6 ms 0.7 ms 0.8 ms 0.9–1.0 ms Firecracker 后端在所有位置和版本上都低于 1 ms。
-
延迟会抖动。NFSv3 的一轮测量里,可写层出现过一次 341 ms 的单次延迟,这一轮的平均值到了 8.4 ms。宿主上的其他负载也会放大延迟,把 NFS 用于延迟敏感的 Workspace 时,node 宿主机不要兼做构建机。
-
顺序写吞吐主要受网络带宽限制。实测 32 MiB 顺序写在千兆网络上与本地磁盘处于同一量级。
-
块文件以缓冲 I/O 打开,guest 的写入先进入宿主页缓存,未回写的数据占用宿主内存。
-
node 限制每个 NFS 挂载积压的脏页:池的标记校验通过后,它把这个挂载的回写设备
/sys/class/bdi/<major>:<minor>/的max_bytes设为 256 MiB、strict_limit设为 1;已经设得更小的max_bytes保留。不设上限时,一个持续写入的 VMM 能在宿主上积压 GB 级的脏页:链路慢时,这台 node 被隔离后要很久才能刷完并写下释放回执,读池的小请求也排在这些写后面,接手的 node 可能在它停写之前就打开对象。- 同一个 NFS 服务端上的几个导出可能共用一个回写设备,这项限制对它们一起生效。
- 设置在 node 停止后仍然保留,直到重新挂载或宿主重启;node 每次校验池都会重新设置。
- 查看当前值:
cat /sys/class/bdi/$(mountpoint -d <pool>)/max_bytes。 - 256 MiB 按千兆链路算:约 110 MB/s,一个自检间隔(5 秒)能刷完约 550 MB。
-
扩容时 node 在宿主上对整个块文件运行
e2fsck与resize2fs,数据经网络读取。目标超出文件系统预留的描述符表时,还会先用tune2fs去掉resize_inode,再检查一次。每个工具的时限是 10 分钟,整次扩容是 45 分钟,大容量的可写层或 Volume 放在慢速网络上时可能超时。 -
NFS 服务端重启后有一段锁恢复期,期间 node 对块文件加锁可能失败,表现为创建或启动 Workspace 返回可重试的冲突。等恢复期结束后重试即可。带
nolock的挂载没有这段恢复期。 -
块文件只增不减。guest 删除文件不会缩小块文件在服务端的占用,本地磁盘上也是同样的行为。
-
共享池上,一个 node 的
check-maintenance(替换程序前的检查,见重装与升级)不会因为池里存在别的 node 持有的 Volume 或保留的可写层而失败;它只核对自己持有的那部分。持有者记录写着别的 node 的对象不归它核对。
排查
| 现象 | 原因 | 处理 |
|---|---|---|
node 启动失败,提示 storage.pool.capacity was removed | 配置里还留着上一个版本的 capacity | 从每个 [[storage.pool]] 删掉 capacity,用 kibo storage-pool set-capacity 在 ctrl 上设置,见池容量 |
创建 Workspace 或 Volume 返回 resource_exhausted,kibo storage-pool get <pool> 的容量显示 - | 池的容量还没有设置 | 用 kibo storage-pool set-capacity 设置,见池容量 |
node 启动失败,日志出现 prepare host layout: create .../volumes 或 .../retained-layers | 池目录本身不存在:还没挂载,或者忘了先 mkdir/storage-pool init | 确认挂载已完成、路径与配置一致,再启动 node |
node 启动失败,日志出现 ... is not traversable by VMM identity 并指向池目录 | 池目录本身的属主或权限没有让 kibotos-vmm 穿越;node 只负责子目录,不会修改池目录本身 | 按前提条件执行 chown root:kibotos-vmm 与 chmod 0710 |
node 启动后这个池的可用性是不可用,原因是 storage pool marker is missing | 忘了执行 storage-pool init,或者标记文件被误删 | 执行 kibotos-node storage-pool init --id <id> --path <path>;目录已有数据时加 --adopt。不需要重启 node,1 分钟内 kibo storage-pool get <pool> 显示这个 node 可用 |
node 启动后这个池的可用性是不可用,原因是 storage pool marker is invalid,提示标记里的 ID 与配置不同 | 池目录之前用另一个 ID 初始化过,或者配置里的 id 写错了 | 核对 [[storage.pool]] id 与标记文件内容(cat <pool>/.kibotos-storage-pool),改配置或换目录,不要手工编辑标记文件 |
storage-pool init 报「不是空目录,需要 --adopt」,但目录里只有 node 自己刚建出的空 volumes/、retained-layers/ | 在执行 init 之前启动过 node | 加 --adopt 继续;不会覆盖任何数据 |
创建 Workspace 或 Volume 失败,node 日志出现 hand raw Volume to the VMM identity 或 hand retained layer to the VMM identity | 导出启用了 root_squash,root 不能改块文件的组;通常在启动阶段就会被上一条拦住 | 导出改为 no_root_squash 后重新 exportfs -ra,再启动 node |
| node 启动卡住 | NFS 服务端不可达,hard 挂载会一直等待 | 恢复服务端,或先卸载再启动 node |
| 扩容失败,消息只说扩容没有完成,或文件系统无法检查 | 原因在 node 日志里以 storage object expansion 开头的一行:error 字段写明失败的步骤与 e2fsck、resize2fs 的输出,常见的是超时 | 可重试的失败按原因处理后,以不小于原目标的容量再扩容一次,从块文件的现状接着完成 |
在 v0.1.0-rc.2 及更早版本上扩容失败过的 Volume 或可写层(原因含 Illegal doubly indirect block),升级后再次扩容完成,但根目录下原有的文件与目录不见了 | 失败的那次 resize2fs 中止前已经移动了根目录的数据块,文件系统从此不一致。再次扩容前的修复检查找不回根目录,把其中的文件与目录挂到这个文件系统的 lost+found 下,以 # 加 inode 编号命名,内容不变 | Volume 的 lost+found 在 Workspace 里它的挂载点下,要以 root 读取;按大小与内容辨认后移回原处。新版本上的扩容不会再以这个原因失败 |
创建 Workspace 返回 retained layer lock is held 或 Volume operation lock is held | 前一次 node 进程的 NFS 锁还没有释放,或服务端处于锁恢复期 | 等待锁租约到期后重试 |
node 启动后这个池的可用性是不可用,原因含 limit the dirty page cache | node 没能设置这个 NFS 挂载的回写上限:内核没有 max_bytes / strict_limit(低于 6.2),或 sysfs 以只读方式挂载 | 换用受支持的内核,或让 node 能写 /sys/class/bdi/;原因里写着回写设备的编号 |
命令失败,错误含 use right was taken over | ctrl 已经把对象交给了更新的纪元,这条命令带的是旧纪元 | 不要重试这条命令。对象的新持有者在别处,按 ctrl 当前的状态操作 |
NFSv3 挂载报 lockd 相关错误,或创建 Workspace 长时间不返回 | rpc.statd 没有运行,服务端的锁服务端口不可达,或服务端根本不提供 NLM 锁(云厂商 NAS) | 自建服务端:sudo systemctl start rpc-statd,确认服务端 lockd、statd 端口开放。云厂商 NAS:挂载加 nolock |
kibo volume move 或 workspace move-layer 返回 failed_precondition,说没有 node 同时接入两个池 | 源池是某台 node 的私有池,这台 node 没有接入目标池 | 在这台 node 上挂载目标池,在配置里声明它并重启 node,再搬迁 |
自建服务端加了 noresvport 后 mount 报 access denied | 导出默认带 secure,只接受保留端口,noresvport 与它冲突 | 去掉 noresvport,或在 /etc/exports 的导出选项里加 insecure 后 exportfs -ra |