同步写已经成功,最新 TXG 却还没提交;删除当前文件,空间也没有完全归还。ZFS 中这两个现象分别由恢复依据和共享引用 解释,不能只用“有缓存”概括。
要解释的现象 阅读位置
四块盘能否承受任意两盘损坏? 第 1~2 节:池与 vdev
同步成功后靠什么恢复? 第 3 节:TXG 与 ZIL 时间线
SLOG、L2ARC、special vdev 能否互换? 第 4 节:设备职责
快照为何保留空间,校验为何不等于备份? 第 5~7 节:引用账与恢复能力
1. pool、vdev 与 dataset 的层次
对象 职责 不能混同的概念
pool 汇集分配空间并管理池级状态 不等于一个目录
顶层 data vdev 承担数据分配与自身冗余 多个顶层 vdev 不是自动互为镜像
mirror / RAIDZ vdev 组织成员设备的冗余布局 冗余容错范围通常先按各 vdev 分析
filesystem dataset 文件目录、属性、配额、快照等管理边界 不需要单独分区,但共享池资源
zvol 暴露块设备的 volume dataset 不直接提供普通目录树
snapshot / clone 保留历史状态 / 基于快照可写派生 不等于另一个故障域的完整备份
设池由两个双盘 mirror 顶层 vdev 组成,先按组判断故障:
损坏分布 每组还有副本吗? 典型结果
每组各坏一盘 有 通常仍可服务
同一组两盘都失去 该组没有 池可能不可用
同样坏两盘,位置不同,结果就不同。RAIDZ 也需按组布局、实际错误和剩余副本分析。见 池与 vdev 概念 。
2. 软件层不是所有请求都依次穿过的流水线
ZPL 将 POSIX 文件操作接到 Linux VFS;DMU 管理对象、数据缓冲及事务;SPA 和 vdev 相关层处理存储池分配与底层 I/O。ARC 是缓存机制,ZIL 是同步意图日志,二者不应作为必经串行层画在 DMU 与 SPA 之间。
OpenZFS 在 Linux 上提供树外内核模块,安装可以使用 DKMS 或发行版预编译模块。SPL 主要适配所需的内核环境和抽象,不是使任何许可证组合自动成立的法律转换层。OpenZFS 使用 CDDL,发布和打包应查其许可证说明 及目标发行版政策;“永远不能合入”是超过当前事实的断言,不作为技术结论。
ARC 与 Linux 映射/页缓存路径的交互受实现版本影响,不能简单断言 ZFS 完全不涉及 Linux 页缓存。普通文件、mmap、zvol 和直接 I/O 也不应被强行套进同一条读写函数链。
3. TXG:提交一组一致的块引用
修改首先进入内存中的对象状态,并加入 transaction group。同步 TXG 时写出数据及受影响的元数据路径,最后建立可识别的有效根状态。恢复依赖块校验、顺序、有效 uberblock 等机制,不是每个块指针写入都天然原子,也不是任意扇区撕裂都无关紧要。
TXG 聚合多个修改以摊薄成本;open、quiescing、syncing 等阶段允许不同组的工作交叠。超时参数只是触发条件之一,脏数据压力等也会推动同步,不能把一次 write 对应成“正好五秒后一个原子事务”。文件系统事务边界也不自动对应应用的多步骤业务边界。
同步请求与 TXG:先建立恢复依据,再完成常规落盘
应用同步写 / fsync
内存中的修改
加入事务组 TXG
提交所需 ZIL 记录
可位于池内或独立 SLOG
同步请求可返回
恢复承诺已满足
TXG 写入常规存储
提交有效的新根
对应日志可回收
不再需要重复恢复
这是典型 ZIL 路径;已同步 TXG 等情况不必重复写日志。周期不是固定丢失窗口。
同步请求通常利用 ZIL 建立崩溃后可重放的依据,无需每次都等待整个大型 TXG 的完整同步。之后相应数据仍通过正常 TXG 路径写到常规存储;恢复时才按需要重放尚未由已提交 TXG 覆盖的日志。ZIL 记录可能包含写入数据,也可能引用已写出的数据块,不能说它“只存操作名称,不存任何数据”。
把 TXG 与 ZIL 放到同一条恢复时间线上
设已经稳定提交的 TXG 100 中,文件某个范围内容为 O。应用执行同步修改 W₁,将它写为 N。此后还执行了另一个范围的普通异步修改 W₂,但没有为 W₂ 请求同步。假设 sync=standard,存储设备遵守写入和同步承诺。
崩溃位置 已稳定的恢复依据 可以据此承诺什么
W₁ 的同步请求尚未成功返回 至少有 TXG 100;日志是否完整取决于进度 不能把 W₁ 当作已经确认的更新
W₁ 所需日志已稳定,同步调用成功;新 TXG 尚未提交 TXG 100 加可重放的 W₁ 日志 恢复旧根后重放所需日志,保留已确认的 W₁
TXG 101 已包含 W₁ 并稳定提交 新的有效根及其引用块 W₁ 已由常规树覆盖,对应日志可以回收
在下面选择修改范围,并交错同步写、异步写与 TXG 提交。恢复结果来自稳定根和仍需重放的日志;最近的内存内容并不自动成为恢复依据。
正在呈现知识画面 重新加载
同步写尚未进入 TXG,恢复靠什么? 已确认的同步修改可以由旧 TXG 加所需 ZIL 记录恢复;新 TXG 覆盖后才不再需要这些日志。异步写也可能随 TXG 持久化。
{"id":"zfs-txg-and-log-recovery","title":"同步写尚未进入 TXG,恢复靠什么?","summary":"已确认的同步修改可以由旧 TXG 加所需 ZIL 记录恢复;新 TXG 覆盖后才不再需要这些日志。异步写也可能随 TXG 持久化。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"choose\"><button data-mode=\"0\" data-i18n=\"a\"></button><button data-mode=\"1\" data-i18n=\"b\"></button></div><div class=\"actions\"><button id=\"sync\" data-i18n=\"sync\"></button><button id=\"asyncWrite\" data-i18n=\"asyncWrite\"></button></div><div class=\"track-row\"><span data-i18n=\"memory\"></span><div id=\"memory\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"root\"></span><div id=\"root\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"log\"></span><div id=\"log\" class=\"track\"></div></div><div class=\"actions\"><button id=\"commit\" data-i18n=\"commit\"></button><button id=\"crash\" data-i18n=\"crash\"></button></div><div class=\"track-row\"><span data-i18n=\"recovery\"></span><div id=\"recovery\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let chosen=0,mem=[0,0],root=[0,0],log=[null,null],serial=0,dead=false;function draw(){for(const [id,arr]of Object.entries({memory:mem,root,log,recovery:dead?root.map((v,i)=>log[i]??v):[null,null]}))cells(id,arr.map((v,i)=>({text:(i?'B':'A')+': '+(v===null?'—':'v'+v),cls:id==='root'||id==='log'?'gold':v===null?'empty':'on'})));for(const id of ['sync','asyncWrite','commit','crash'])$('#'+id).disabled=dead;pressed(String(chosen));set('explanation',t('note'));}document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{chosen=+e.dataset.mode;draw();});for(const id of ['sync','asyncWrite'])$('#'+id).onclick=()=>{mem[chosen]=++serial;if(id==='sync')log[chosen]=mem[chosen];draw();};$('#commit').onclick=()=>{root=[...mem];log=[null,null];draw();};$('#crash').onclick=()=>{dead=true;draw();};$('#reset').onclick=()=>{mem=[0,0];root=[0,0];log=[null,null];serial=0;dead=false;draw();};draw();","audio":false,"strings":{"a":"范围 A","asyncWrite":"异步写入新版本","b":"范围 B","choose":"选择写入范围","commit":"提交当前 TXG","crash":"崩溃并恢复","heading":"同步写尚未进入 TXG,恢复靠什么?","intro":"选择范围 A 或 B,用同步或异步方式写入新版本。提交 TXG 或直接崩溃,对照稳定根、日志和恢复内容。","log":"尚需保留的 ZIL 记录","memory":"内存中的当前内容","note":"尝试先同步写 A,再异步改 A:旧的已确认值仍有恢复依据,新异步值不因此自动受到保护。","pending":"尚未执行恢复","recovery":"恢复:先根,再重放","reset":"重置","root":"已提交根覆盖的内容","scope":"两个独立范围,sync=standard,设备兑现同步承诺。同步按钮原子完成写入及有效日志稳定化;用值表示可重放记录,不表示真实日志格式。崩溃丢弃所有未被根或日志保护的内容,不模拟日志设备同时损坏。","scopeLabel":"模型范围与约定","sync":"同步写入新版本"}}
再看异步 W₂:未承诺不等于必丢失
W₂ 的命运不能仅凭“它发生在 W₁ 后面”推断:若它尚未被有效 TXG 覆盖,也没有同步承诺,就可能丢失;若已随某个 TXG 稳定提交,也可能保留下来。异步的含义是应用没有在那个返回点得到相同持久性承诺,不是内核保证它一定丢失。
这条时间线说明 COW 和 ZIL 各自提供什么。COW 的有效旧根让系统能回到一组结构一致的块引用;ZIL 把已经向应用确认、但尚未进入最新常规树的同步操作补回去。日志设备的低延迟影响建立后一种恢复依据的速度,不能让主存储永久免于写出修改。
日志失去时,哪一条恢复依据不再成立
如果同时失去尚需重放的唯一日志副本,第二行的证明就不再成立。风险集合由“哪些已确认操作仍依赖该日志”决定,既不是固定墙上时间的五秒,也不是由某个参数名字单独决定。冗余日志与设备断电保护的意义,是让恢复依据在设计的故障条件下仍能存在。
4. ZIL、SLOG、L2ARC 与 special vdev
名称 作用 失效时首先考虑什么
ZIL 同步修改的恢复日志机制 已确认而未被 TXG 覆盖的修改如何恢复
SLOG ZIL 使用的独立日志设备 日志冗余、断电保护、错误与恢复路径
ARC 内存缓存,包含数据和元数据等 内存压力、缓存命中与脏数据节流
L2ARC / cache vdev ARC 之外的二级读缓存 丢失后通常从主存储重读,不是唯一数据源
special vdev 分配元数据等,按配置也可放小数据块 属于关键分配设备,不是可随时丢弃的缓存
SLOG 不能把任意异步大文件写入都变成持续 SSD 速度;它主要改变相关同步请求的延迟路径,常规存储仍要承接 TXG 写出。若后端长期排出速度不足,系统最终仍会节流。设备延迟还要看 flush 和断电保护下的真实表现,而非只看标称顺序带宽。
丢失日志设备并再遇到崩溃,可能破坏已向应用确认但尚需日志恢复的修改;不能承诺“只丢最近固定五秒,异步写不受影响”。在运行中、导入时、日志是否有冗余、相应 TXG 是否已提交,都会改变结果。相关设备职责见 OpenZFS 硬件说明 。
sync=standard 遵守应用同步请求,sync=always 扩大同步要求,sync=disabled 会忽略这种请求的正常持久性要求。把 disabled 的成绩与正常 fsync 成绩直接比较,相当于比较不同的承诺,不是证明同样安全条件下更快。属性语义见 zfsprops(7) 。
5. 快照、记录大小与空间保留
快照保留旧块引用,克隆可在快照基础上产生可写分支。假设文件原有块 A、B,创建快照后当前版本将 A 改成 A′,旧 A 仍被快照引用;删除当前文件不一定回收 A、B。池的使用量与某目录文件逻辑大小因共享、压缩、快照及元数据而不同。
空间账:512 KiB 文件为何保留了 640 KiB
限定一个模型:文件由四个 128 KiB 记录 A、B、C、D 组成,忽略元数据、压缩、填充和额外副本。创建快照 S 后,两个视图共享这四个记录;当前文件再把 A 改写成 A′。
一次记录改写:两个视图,共保留五个记录
快照 S
当前文件
A
B
C
D
A′
B、C、D 继续共享
5 × 128 KiB = 640 KiB
每个视图逻辑长度仍为 512 KiB;忽略元数据、压缩、填充和额外副本。
状态 当前文件逻辑大小 两个视图合计保留
创建快照后 512 KiB A、B、C、D,共 512 KiB
改写 A 后 512 KiB A、A′、B、C、D,共 640 KiB
新增占用来自历史 A 仍有引用,而不是文件逻辑长度变大。
此时删除当前文件,只要 S 还在,旧的 A、B、C、D 仍有引用;删除 S 但保留当前文件,则旧 A 可以在完成相应回收后释放。实际统计还包含其他开销和共享关系,不能直接套这个数字,但引用账的推理方法相同:文件逻辑长度统计内容视图,池使用量统计仍需保留的实际存储 。这也解释了为什么快照刚创建很便宜,长期频繁改写后却可能保留很多历史块。
记录大小:布局属性不等于每次设备请求大小
recordsize 是文件系统数据记录大小相关属性,volblocksize 用于 zvol。它们不是“每个应用 write 必定发出同样大小的设备请求”。小范围随机改写、压缩、COW 和现有文件布局会影响放大;修改属性也不能简单当作立即重写所有旧文件。调优前应先说明工作负载的读写范围与同步频率。
缓存观测:命中率之外还要看同步长尾
ARC 通过近期/频繁访问及 ghost 历史等信息调整缓存策略;ghost 记录不是又保存一份完整数据。ARC 命中率高也可能掩盖少量同步写的长尾延迟,必须按工作负载分开观察。L2ARC 有索引、填充与内存开销,扩大它不自动修复热点工作集之外的所有性能问题。
6. 校验、scrub、resilver 与备份
机制 主要解决的问题
校验 发现内容不符,有正确副本时支持修复
scrub 主动检查已分配数据完整性
resilver 恢复所需冗余
快照与独立备份 提供相应历史恢复与故障域保护
内容通过校验,不代表应用写入的业务含义正确,也不能撤销正常执行的误删除。
文件系统快照提供存储层的时间点视图,但不会自动让数据库内存状态与多个外部系统一起提交。应用一致的恢复点还需要数据库备份接口、检查点或明确的协调协议。
send/receive 传递快照状态或差异,可以构建另一池或另一主机的副本;增量链需要匹配的基准与保留策略。能生成发送流不等于已完成可恢复备份,接收端是否可用及恢复流程仍需验证。
7. 只读读取 ARC,而不修改池
from pathlib import Path
path = Path ( " /proc/spl/kstat/zfs/arcstats" )
if not path . exists ( ) :
print ( " 未暴露 ARC 统计;可能未加载 OpenZFS" )
else :
values = { }
for line in path . read_text ( ) . splitlines ( ) :
columns = line . split ( )
if len ( columns ) == 3 and columns [ 2 ] . isdigit ( ) :
values [ columns [ 0 ] ] = int ( columns [ 2 ] )
for name in ( " size" , " c" , " c_min" , " c_max" , " hits" , " misses" ) :
print ( name , values . get ( name , " 未提供" ) )
hits , misses = values . get ( " hits" , 0 ) , values . get ( " misses" , 0 )
if hits + misses :
print ( " 累计命中比例:" , hits / ( hits + misses ) )
这里是累计比例,不能代替指定工作负载时间段的差分。zpool status 用于了解拓扑与错误;zpool iostat 用于设备/池活动;zfs get 用于核对实际属性和继承来源。先结合拓扑、属性和业务负载解释观测,再决定是否需要更改配置。