三个模型段进行全历史压实:新 b 删除遮住 b old;无保留快照时,输出包含较新的 a 和 c。1、2、3 简写 one、two、three。

LSM 树把前台写入成本转移到后续归并

LSM 树把一部分分散更新的成本转移到内存缓冲与后续归并。这是一种设计取舍,不是“所有磁盘操作从此完全顺序”的承诺,也不保证随机写一定胜过 B 树。真正需要回答的问题,是前台写入、后台压实、读取和保留版本,能否同时放进同一份资源预算。不能用一个没有提供数据生成器的精确耗时,替代对实际工作负载的解释。 缓冲改变工作的发生时机 RocksDB 通常把新写入放入内存表,并可同时写入预写日志。是否启用 WAL、是否同步日志,是另外的持久性选择;仅仅采用 LSM 布局,不保证每个已应答写入都能抵御断电。内存表填满后刷成不可变有序表,产生零层文件,应用则通过其他可用缓冲继续写入。日志追加、刷盘和压实也不是同一个动作。 采用分层压实时,零层文件的键范围可以重叠。更深的层通常各自组成一个有序段,但一个段可以按键范围拆为多个不重叠 SST 文件,不能理解为每层只有一个文件。查找先利用文件范围选择候选,压实则把重叠区间归并成替代文件。动态层大小配置还可能让某些中间层为空,因此不能把所有层永远填满的示意图当作运行事实。 读取仍然需要处理多个版本 同一个键的旧值可能存在于多个段中。读取要选择可见版本,而删除标记必须阻止更老的值重新出现。布隆过滤器可以排除肯定不含目标键的候选文件;返回可能存在后,仍需进一步检查。一次最终成功的查找,也能通过过滤器跳过不相关文件,再到真正包含答案的文件中读取。因此,“过滤器只帮助未命中,对命中毫无作用”并不准确。 压实通过重写数据,降低这种搜索和空间负担。分层策略与通用策略通常在写放大、读放大、空间放大之间作不同选择,但真实结果仍取决于负载、压缩、缓存和配置。统计时必须统一口径:SST 写入字节、WAL 字节、设备写入字节与应用逻辑字节不是同一种量。不写清分子分母的倍率,不能直接用来规划存储吞吐。 坏掉的样子 刷盘或压实跟不上时,不可变缓冲和零层文件可能积累,读取成本上升,引擎还可能减速或暂停前台写入。历史阻塞计数非零,不足以诊断今天的瓶颈。应同时查看最近的阻塞时间、待处理工作、文件数量、存储延迟与应用吞吐。提高阈值可能暂时推迟压力,却把更多工作留到后面,并扩大恢复时需要处理的积压。 删除不会立刻回收所有旧值。只要更低的段仍可能暴露被遮住的值,或保留快照需要相关历史,墓碑就不能随意消失。回收空间取决于压实覆盖了哪些历史,以及版本保留规则是否允许丢弃。若数据已经全部删除,活跃字节数为零,就不能再报告一个有限的“物理字节除以活跃字节”空间放大率;此时应直接报告残留字节与回收条件。 归并版本,保留删除语义。依据原文绘制机制;模型值与原文报告的观察不代表本次运行测试。 快速验证 把程序保存为 lsm_model.py,用 Python 3 执行。它明确只是标准库内存模型,展示三个刷出段和覆盖全部历史的压实。程序不会执行 RocksDB,不会创建真正 SST,不做磁盘输入输出,不模拟引擎调度,也不测延迟。打印的计数不能被重新包装成生产性能结果;示例的作用是把版本选择与墓碑删除条件具体化。 ...

十一月 2, 2027 · 2 分钟 · XB Field Notes
文章 ext4 历史样本逻辑长度为十六 MiB,在四与十二 MiB 处有 A、B,分配的两个小区域合计八 KiB。空洞读为零;这些不是此次新运行的结果。

稀疏文件把逻辑长度与分配块分开

稀疏普通文件可以具有很大的逻辑长度,却只为其中部分地址范围分配存储。读取长度范围内的空洞会得到零字节,因此文件长度、字节内容和文件系统分配,是三项不同测量。为虚拟磁盘预算容量、复制备份,或者判断零区域是否已为将来写入保留空间时,都需要理解这种关系,不能用其中一个数字替代另外两个。 分别测量长度、字节与分配,再验证真实复制或预留空间的契约。 坏掉的样子 seek 越过文件结尾,只改变描述符偏移,不改变文件长度。后续写入或显式扩展,才可能创建中间读为零的范围。只调用 seek,就声称已经创建大型稀疏文件,是错误的;同样,逻辑大小很大,也不表示文件系统已经预留足够块,允许以后填满它。偏移位置、逻辑范围与实际容量承诺必须分开。 如果复制读取每个字节并普通写出,就可能把空洞变成分配了块的零区域。最终校验值完全相同,分配空间却大得多。传输或恢复流程若只依据源文件当前分配量做预算,即使正确保留逻辑内容,也可能耗尽目标容量。因此成功复制字节与保持稀疏表示,是需要分别验证的目标。 打洞是有意改变数据的操作。配合 KEEP_SIZE 对已分配范围打洞,会保持长度,却让对应字节读为零,并不是可以随手清理任意文件区域的无害命令。预分配解决另一类问题:它保留文件系统分配,即使未写入的已分配范围也可能读为零。内容都是零,不能证明这两种表示具有相同空间行为。 ...

十月 11, 2027 · 2 分钟 · XB Field Notes
Pod 使用绑定 PV 的 PVC;恢复仍取决于后备存储与拓扑。

PVC 为 Pod 提供容器之外的持久存储

PersistentVolumeClaim 把存储的生命周期与重建的 Pod 分开,但不保证一定能恢复数据。PersistentVolume 描述存储,声明请求存储,由控制器安排绑定或供应。真正的恢复仍取决于后备存储、可用拓扑与备份;有了 StorageClass,可以由 provisioner 动态供应,而不必预先建好每块卷。 声明与卷各自有独立的生命周期。finalizer 可以推迟对象删除,回收策略决定释放后的存储如何处理。维护前应核对两者:处理失误可能留下持续 Terminating 的声明、删掉本想保留的数据,或留下一份没人在用却仍在计费的存储。 保护机制推迟删除,不证明控制器健康。Retain 需要核对归属、备份并按提供方流程决策。保留诊断仍受阻,本轮未重跑。 声明、卷与绑定 普通匹配会检查容量、访问模式、存储类、volumeMode、选择器及既有归属。不匹配的声明可以停在 Pending,应检查事件与 StorageClass,不能只猜一个原因。绑定不证明拓扑可用:显式指定 PV 时,即使节点亲和性冲突也可能绑定成功,后续调度与挂载仍需各自成功。已释放但尚未回收的 Retain 卷也不会自动成为新声明的可用备件;旧 claimRef 可能仍然存在。 accessModes 没有默认值。ReadWriteOnce 允许单节点读写挂载,该节点上的多个 Pod 仍可能共用它,不是单 Pod 独占。ReadWriteOncePod 才为受支持的 CSI 卷提供单 Pod 约束;访问模式本身并不在挂载后执行写保护。Immediate 不等待消费者就开始绑定或供应,受拓扑限制的存储可能因此无法使用。受支持的 WaitForFirstConsumer 则结合消费 Pod 的调度约束协调存储,之后才完成放置;它不是先完成调度再随意创建卷。 ...

八月 13, 2027 · 1 分钟 · XB Field Notes
服务器目录通过访问策略关口连接两台客户端。

NFS 把远程目录通过网络共享出去

NFS 导出把服务器文件系统路径与客户端及访问策略关联起来。在 Linux 上,exportfs 根据 /etc/exports、/etc/exports.d 下合格文件或命令行更新,维护 /var/lib/nfs/etab。rpc.mountd 是守护进程,不是第三份配置文件;其 MOUNT 服务与旧版协议流程有关,NFSv4 挂载并不要求这个独立服务。应先明确版本和服务职责,再解释客户端的结果。 配置文本可能与有效导出不同,但这只是拒绝访问的一种原因,不能从文件看起来正确就断定导出表过期。还要核对目标服务器、协议、导出策略、身份及文件权限。服务器的持久性和客户端缓存也要分开:应用看到一次写入成功,并不单独证明服务器已把相应数据提交到稳定存储。目录可见、访问获准和写入可恢复,是不同层次的事实。 文件与表 /etc/exports 的条目把目录与客户端及选项关联。/srv/data 10.0.0.0/24(rw,sync,no_subtree_check,root_squash) 展示一个网络,后面还能追加有自己选项的客户端组。主机名、网段和 netgroup 有各自匹配规则。exportfs -ra 会协调配置与导出,但其他 exportfs 操作或服务管理也能应用改变;它不是唯一生效途径,不能把所有差异都归因于忘记运行这一条。 showmount -e 向服务器 MNT 服务询问公布的导出。信息完整性取决于实现,只有 NFSv4 的服务器甚至可能不暴露此服务。因此它既不是完整有效策略转储,也不证明某客户端一定能访问共享。原文四条配置对三条导出以及 NOENT 的故事没有找回原始运行记录;缺失导出也不能保证所有客户端总收到同一个错误码。诊断应保留实际返回信息。 选项真正决定了什么 导出默认只读,rw 才允许写入,但文件权限及其他控制仍然生效。sync 约束服务器按稳定存储要求回复,async 则允许改动提交前回复,崩溃可能丢失已确认的数据。两者都不会消除客户端缓存,也不让每次应用写入自动变成同步持久提交。nfs-utils 在 1.0.0 版本之后把默认从 async 改为 sync;理解版本历史时要与实际客户端操作分开。 ...

六月 23, 2027 · 2 分钟 · XB Field Notes
两组独立的历史启动测试恢复不同计数器值:仅 RDB 返回快照中的 1,启用 AOF 返回稍后写入的 2。独立 RDB 与另一份日志不会自动合并。实验测试了进程终止,没有测试断电。

RDB 与 AOF:两种保住 Redis 的方式

RDB 与 AOF 保存的是 Redis 状态的不同表示。选择之前应回答两个问题:允许丢失多少近期数据,以及可以接受多长恢复时间?单独一个持久化开关无法回答这些问题。真正需要的是可恢复的文件、明确的写入确认策略,以及验证过具体数据值的恢复流程,而不只是能够启动的进程。 原文历史实验中,仅 RDB 与启用 AOF 恢复不同的精确值。AOF 使用自身文件集合;本轮没有新增运行,也不证明断电可恢复。 快照记录一个时点,不保证固定间隔 RDB 是紧凑的时点数据集。BGSAVE 派生子进程,先写临时文件,完成后再替换旧快照;父进程继续处理请求,但派生进程、写时复制增加的内存以及磁盘工作仍有成本。SAVE 则同步保存并阻塞其他客户端,本文只把它用于很小的隔离测试数据,不应照搬到繁忙服务上。 例如 save 60 1000 同时要求经过指定时间并达到修改次数条件,不是“无论有多少修改,每六十秒都必定生成一份快照”,也不保证保存能按时成功完成。实际恢复暴露窗口取决于最近一份可用快照的状态有多旧;漏掉或失败的保存会扩大这个窗口。因此,要检查成功保存的记录与结果,不能只看配置中的时间数字。 AOF 改变恢复数据的表示 AOF 记录数据变更,并按 appendfsync 策略同步。everysec 通常暴露大约最近一秒的写入,这并非严格的丢失上限;no 把刷盘时机交给操作系统。always 通常先同步再回复,代价是额外的同步开销。若设置 no-appendfsync-on-rewrite yes,Redis 会在 BGSAVE 或 BGREWRITEAOF 期间跳过主进程的 AOF fsync,使 always 和 everysec 的持久性都减弱。固定版本的配置默认将此例外设为 no。这些策略不保护整块存储设备丢失,也不能保证故障切换时被提升的副本包含每一条已经在原节点落盘的写入。 ...

六月 15, 2027 · 3 分钟 · XB Field Notes
非日志表分为两条路径:干净重启保留行,崩溃恢复后关系内容清空。

UNLOGGED 表减少数据日志,也放弃内容的崩溃恢复

PostgreSQL 的 UNLOGGED 表保留正常 SQL 事务行为,但放弃其内容的崩溃恢复能力。它适合能够重复生成的暂存数据,不适合把唯一副本托付给一次提交。判断边界是服务器是否经历了非正常关闭后的恢复,不能简单地把“重启”两个字等同于数据丢失,也不能因为一次正常重启后行还在,就认为它具备持久性保证。 哪些内容跳过 WAL 非日志表的数据变化及其索引变化不写入预写日志。系统目录的变化仍可能产生 WAL,同一个事务还可能修改普通表,所以不能把整个工作负载描述为“完全没有 WAL”。表的数据页仍然占据存储空间,仍要经过缓冲区管理和后续写回;UNLOGGED 并不是内存表,也不是不需要磁盘的缓存。写入收益来自减少特定日志工作,实际改善幅度需要测量,不能承诺固定倍数。 服务器正常运行时,事务、快照、约束和回滚仍然生效。语句被拒绝或应用断开连接,并不意味着此前提交的全部行都会消失。但是后端进程异常退出可能导致服务器重启其他进程并进入恢复,因此也不能声称“应用事件绝不影响非日志表”。排查时应查看 PostgreSQL 日志和恢复过程,而不是仅凭事故最初发生在哪一层就推断数据状态。 重启与复制的边界 干净关闭后,非日志表内容会保留到下次启动。pg_ctl 的 fast 模式会回滚正在执行的事务并正常关闭,immediate 模式则直接中止进程,不完成干净关闭。后者需要崩溃恢复,恢复期间非日志关系会被重置:表定义仍在,但此前提交的行不再存在。主机维护若完成了干净关闭,并不会仅因为“重启”就清空这类表;反过来,不能把正常关闭的测试当成抗崩溃证明。 物理备库不会收到非日志表的数据内容。备库仍处于恢复模式时,读取这类关系会报错,而不是成功返回零行。提升为主库后可以读取它,但没有已经复制过来的行可以恢复。因此故障切换流程必须先重建或重新填充,再允许依赖这些数据的消费者进入服务。主备拥有同一个表定义,只说明目录信息存在,并不代表表中业务数据已经复制。 普通日志表不能用外键引用非日志表。PostgreSQL 会拒绝这样的建表语句,因为持久化的子表引用不能安全依赖可能在恢复中消失的父表行。下面的实验直接检查这一限制,不会把它描述成“外键还在、父表行没了、数据库默默允许不一致”。设计暂存区时,要把关系约束放到符合持久性边界的位置,而不是事后修补孤立记录。 坏掉的样子 一个常见事故是导入程序把文件装入非日志暂存表后就确认成功,并删除唯一的原始文件。随后服务器崩溃,暂存行消失,而输入已无处重取,这就变成不可恢复的数据丢失。原始输入应该至少保留到持久化目标提交成功,重建流程也必须知道需要哪一个输入版本。去重记录和导入检查点要属于持久化契约,不能和暂存内容一起丢掉。 另一个问题出现在报表服务改读备库,或刚完成提升就立即对外服务。只检测行数为零会漏掉恢复模式下的读取错误;无限重试也无法生成不存在的数据。应明确标记“正在重建”,在重建完成且结果验证通过前控制请求准入。演练要测量重建耗时、上游是否可用、记录数及内容是否符合预期;只建回一张空表不算恢复,能运行的脚本也不等于已经证明恢复时间满足要求。 UNLOGGED 重启边界:机制图。 快速验证 把 Bash 代码保存为 unlogged.sh。它要求以非 root 用户执行 PostgreSQL 18 的命令行工具。镜像须已在本地可用,且这个主版本标签并不固定到某个小版本。以下命令不发布网络端口,也不挂载宿主机数据;两个实验数据库均位于新临时目录。本轮没有运行数据库或容器命令。 docker run --rm --network none --user postgres --entrypoint bash -i \ postgres:18-alpine < unlogged.sh #!/usr/bin/env bash set -euo pipefail pg_lab=$(mktemp -d /tmp/unlogged-lab.XXXXXX) cleanup() { for cluster in standby primary; do if [ -f "$pg_lab/$cluster/postmaster.pid" ]; then pg_ctl -D "$pg_lab/$cluster" -m immediate -w stop >/dev/null fi done rm -rf -- "$pg_lab" } trap cleanup EXIT primary() { psql -X -h "$pg_lab" -p 55432 -d postgres -v ON_ERROR_STOP=1 "$@"; } standby() { psql -X -h "$pg_lab" -p 55433 -d postgres -v ON_ERROR_STOP=1 "$@"; } start_primary() { pg_ctl -D "$pg_lab/primary" -l "$pg_lab/primary.log" \ -o "-k $pg_lab -p 55432 -c listen_addresses=''" -w start >/dev/null } initdb -D "$pg_lab/primary" --no-locale --encoding=UTF8 --auth=trust >/dev/null start_primary primary <<'SQL' CREATE TABLE durable (id integer PRIMARY KEY); CREATE UNLOGGED TABLE scratch (id integer PRIMARY KEY); INSERT INTO durable VALUES (1); INSERT INTO scratch VALUES (1); BEGIN; INSERT INTO scratch VALUES (2); ROLLBACK; DO $$ BEGIN EXECUTE 'CREATE TABLE durable_ref (id integer REFERENCES scratch(id))'; RAISE EXCEPTION 'a logged table unexpectedly referenced an unlogged table'; EXCEPTION WHEN invalid_table_definition THEN RAISE NOTICE 'logged-to-unlogged foreign key rejected'; END $$; SQL counts='SELECT (SELECT count(*) FROM durable), (SELECT count(*) FROM scratch)' test "$(primary -Atc "$counts")" = '1|1' pg_ctl -D "$pg_lab/primary" -m fast -w stop >/dev/null start_primary test "$(primary -Atc "$counts")" = '1|1' printf 'clean restart: 1|1\n' pg_basebackup -h "$pg_lab" -p 55432 -D "$pg_lab/standby" -R -X stream -c fast pg_ctl -D "$pg_lab/standby" -l "$pg_lab/standby.log" \ -o "-k $pg_lab -p 55433 -c listen_addresses=''" -w start >/dev/null test "$(standby -Atc 'SELECT pg_is_in_recovery()')" = t test "$(standby -Atc 'SELECT count(*) FROM durable')" = 1 if standby -Atc 'SELECT count(*) FROM scratch' 2>"$pg_lab/read-error"; then echo 'unlogged read unexpectedly succeeded on standby' >&2 exit 1 fi grep -F 'cannot access temporary or unlogged relations during recovery' "$pg_lab/read-error" pg_ctl -D "$pg_lab/standby" -w promote >/dev/null test "$(standby -Atc "$counts")" = '1|0' printf 'promoted standby: 1|0\n' pg_ctl -D "$pg_lab/primary" -m immediate -w stop >/dev/null start_primary test "$(primary -Atc "$counts")" = '1|0' printf 'crash recovery: 1|0\n' primary -Atc 'SHOW server_version' echo 'PASS: rollback, foreign key restriction, clean restart, standby, promotion, crash' 主库与备库使用同一个临时目录中的不同 Unix 套接字端口,退出时的 trap 尝试关闭这两个实验集群并移除文件;失败或中断仍可能留下资源。实验故意对临时主库执行 immediate 关闭,不能把这一步改成已有数据库目录。历史文章报告 PostgreSQL 18.6 的以下结果,但本次未找到原始日志:回滚和正常重启后的计数都是 1|1;仍处于恢复模式的备库拒绝读取 scratch。备库提升和主库崩溃恢复后分别得到 1|0,表明持久化行还在而暂存内容不在。错误文本断言使用集群的 C locale,避免本地化消息造成误判。 ...

五月 24, 2027 · 2 分钟 · XB Field Notes
已启用版本化的版本栈顶部是删除标记,下方仍有较早数据;未指定版本的 GET 返回 404。

桶版本化与生命周期管理成本

S3 版本化在同一个键下保留多个版本,只要所需版本仍在,就能恢复许多误覆盖和普通删除。它并不防止所有删除,也不会独立构成备份。真正能否恢复应用,取决于保留策略、访问权限、加密密钥是否仍然可用,以及恢复程序是否经过验证。版本存在与业务恢复成功之间,还有需要明确检查的步骤。 分清版本化状态 在启用版本化的桶中,PUT 产生新版本 ID,同时保留之前的数据版本。每个版本存储完整对象,并非只记录变化的字节。因此,反复覆盖一个大对象,即使可见键数量保持不变,存储成本也可能持续增长。这与块级增量快照的空间模型不同,不能只用修改了多少字节估算对象版本的费用。 启用之前已存在的对象保留 null 版本 ID,新产生的版本化写入不会覆盖那个旧版本。一旦启用,桶就不能回到从未版本化的状态,但可以暂停版本化。暂停期间,后续写入使用 null 版本槽位,可能替换已有 null 版本。讨论覆盖是否安全时,必须明确桶当前处于哪一种状态,不能把暂停与启用混为一谈。 null 是真正的版本标识,不是永久保留保证。null 版本变成非当前版本后,可以受到非当前版本过期规则影响;在授权允许时,也能够按明确版本 ID 删除。因此,列表中出现 null 并不能证明那份旧数据不参与生命周期清理,更不能把它当成永远不会消失的恢复基线。 删除标记改变可见性 启用版本化时,不带版本 ID 的普通 DELETE 创建删除标记。标记处于当前位置后,不指定版本的 GET 通常返回 404;显式请求删除标记的版本 ID 则返回 405。较早数据仍可通过其数据版本 ID 获取,但还受到访问权限、存储类别及加密条件影响。看见 404 不能直接推断底下所有版本已经消失。 删除标记没有对象载荷,却不是免费零字节条目,S3 会按它保存的键名字节收费。删除那个明确的当前标记,可以暴露前一版本,但前一版本本身也可能仍是标记。另一种恢复方式是复制明确的历史数据版本,生成新的当前数据版本,同时保留历史。标记没有数据,不能把它当成普通对象复制来恢复。 按指定 ID 删除数据版本,是另一种永久操作,版本化本身不能把被永久删除的版本找回来。配置 MFA Delete 后,永久删除版本和改变版本化状态会增加相应认证要求,但开启版本化并不会自动开启这种保护。AWS 不支持将 MFA Delete 与生命周期配置一起使用,不能假设所有保护开关可以无条件同时启用。 ...

五月 8, 2027 · 2 分钟 · XB Field Notes
八个位格显示 10100000;只有偏移 0 与 2 置位,底层字节为 A0,BITCOUNT 为 2。

Redis 位图用位而不是存储数字

Redis 位图是把字符串按单个位解释的用法,并不是一种独立的键类型。它适合稠密的是非标记,例如按用户记录每日活跃状态。一千万个连续标志需要一百二十五万个载荷字节,但这个估算不包括键名、对象元数据、分配器额外开销,也不包括业务标识到位偏移的映射,不能直接当成实例总内存。 先定义偏移的契约 SETBIT 在从零开始的偏移处写入零或一,并返回写入前的位值。已经为一的位置再次写一,不会增加成员数量。偏移零对应第一个字节的最高位,偏移八才进入第二个字节。对同一个键执行 GET,读到的是底层字节,它们未必是有效文本,所以二进制数据不应默认按 UTF-8 解码。 GETBIT 对缺失键或超出现有字符串长度的位置返回零,而且不会因为读取就扩容。因此,零不能区分“明确不活跃”与“尚未记录”,需要先决定业务是否允许合并这两种状态。SETBIT 与 GETBIT 要求键的值是字符串;如果键实际是哈希,会报类型错误,不会自动把哈希字段转换成位。BITOP 则会在目标键写入新的字符串。 内存取决于写入过的最高偏移,而不是当前有多少个位为一。写偏移 n 至少需要 floor(n / 8) + 1 个载荷字节,即使要写入的是零也一样。后来把高位清零,并不会缩短字符串。稀疏外部编号因此可能用极少的有效标记触发巨大分配。应验证允许的偏移范围,并采用稳定且足够稠密的编号映射,而不是直接信任任意业务 ID。SETBIT 要求偏移小于二的三十二次方,对应最大五百一十二 MiB 字符串。这个协议上限不是合理的单次请求分配预算,应用应按业务规模设置更小的允许范围。 计数和组合都要明确范围 BITCOUNT 默认检查全部字节。可选的 start 和 end 是包含两端的字节位置;从 Redis 7.0 开始,可以用 BIT 修饰符把它们改为位位置。切换单位只改变查询区间,不会改变键里实际存储的表示。返回值是为一的位数,不是最大的成员编号,也不是位图所能容纳的全部用户数。 BITOP OR 计算成员并集,AND 计算交集。较短的输入和不存在的键,会被视为补零到最长输入的长度。目标键会被新字符串覆盖,原本附在目标键上的过期时间也会移除。因此,派生结果需要明确的寿命策略,不能以为之前给结果键设置过一次期限,以后的重算就会自动继承。 ...

四月 29, 2027 · 3 分钟 · XB Field Notes