Kibotos

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>.lock

transfer-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 接入的池,在原宿主丢失而盘完好时可以换到另一台宿主机。盘也丢了时改为宣告池丢失:

  1. 隔离原 node:kibo node isolate。确认原宿主已经不能写这份存储。
  2. 退役原 node:kibo node retire。
  3. 把盘接到新宿主机,在新 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 -ra

192.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.service

3. 确认

创建一个 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=2Linux 客户端 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/null

v4.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/null

v3 对自建 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/null

v3 的服务端还要开放 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/mounts

vers=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。同一位置测了多轮时给出范围。

    目标本地磁盘NFSv3NFSv4.0NFSv4.1NFSv4.2
    可写层0.4 ms0.8–1.4 ms1.7 ms1.0 ms0.8–0.9 ms
    Volume0.5 ms1.0–1.6 ms0.7 ms0.8 ms0.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 cachenode 没能设置这个 NFS 挂载的回写上限:内核没有 max_bytes / strict_limit(低于 6.2),或 sysfs 以只读方式挂载换用受支持的内核,或让 node 能写 /sys/class/bdi/;原因里写着回写设备的编号
命令失败,错误含 use right was taken overctrl 已经把对象交给了更新的纪元,这条命令带的是旧纪元不要重试这条命令。对象的新持有者在别处,按 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

下一步

  • [[storage.pool]] 的全部键见 node 配置。
  • 用 kibo storage-pool list、get 与 objects 查看池的容量、预留、声明它的 node 与池里的对象,见存储池。
  • node 宕机后用 kibo node isolate 让它的 Workspace 能重新启动,见node。
  • 可写层与 Volume 的扩容操作见kibo 命令行。

本页内容