Happy forever
2026年,春节从老家返程途中游玩连云港。愿孩子们永远健康快乐

2026年,春节从老家返程途中游玩连云港。愿孩子们永远健康快乐

1 | git clone https://github.com/openclaw/openclaw.git |
From repo root:
1 | ./docker-setup.sh |
This script:
If you see “unauthorized” or “disconnected (1008): pairing required”
1 | "gateway": { |
By default, OpenClaw requires every connecting device to be “paired” (approved by an administrator). Since the Control UI is running in an “insecure” context
(HTTP inside Docker), it cannot generate a persistent device identity. Setting allowInsecureAuth: true tells the gateway to trust the Control UI if it provides
the correct token, skipping the pairing requirement.
See URL below:
Feishu
goto the following site:
https://clawhub.ai/skills
look for the skill you need. e.g. sonoscli
Execute
1 | npx clawhub@latest install sonoscli |

1 | node@45122743ed12:/app$ find / -name "openclaw.plugin.json" 2>/dev/null | grep feishu |
CAP 定理指出,在分布式系统中,系统只能在以下三个属性中同时保证两个:
一致性(Consistency,C):所有节点看到相同的数据。对任一节点的写入操作,其后的所有读取都会返回更新后的值。
可用性(Availability,A):每个发给非故障节点的请求都会得到响应,但不保证响应包含最新数据。
分区容错性(Partition Tolerance,P):即使节点间发生网络分区或消息丢失,系统仍能继续运行。
在理想情况下(网络永不中断),可以同时拥有 C 和 A。但在实际环境中,网络延迟或中断不可避免,因此 P 通常被视为默认前提。真正的取舍在于 A 与 C:当网络分区发生时,是优先保证一致性,还是优先保证可用性?

票务系统(如 12306):若两个用户同时预订同一座位,系统必须确保只有一个用户成功,以避免重复分配。
账户余额:扣款成功后,用户在任何终端查询余额都应反映扣款后的最新结果
社交媒体:用户更新个人信息后,短时间内其他用户可能仍看到旧信息,但系统不会返回错误。
OLAP 系统:处理海量报表数据时,由于同步延迟,系统可能无法实时反映最新写入,但仍能提供历史数据查询,保证系统可用性。
CAP 定理中所指的一致性通常是强一致性,即所有读取操作都反映最新写入。除此之外,还有其他一致性模型:
所有读取均返回最新写入的数据,开销较大,但对银行账户等需要绝对准确性的系统至关重要。
特殊实现
读己所写(Read-Your-Own-Writes Consistency):用户在同一会话中总能立即看到自己提交的更新。常用于社交媒体。
系统在经过一段时间后最终达到一致状态,但短期内可能存在不一致数据。典型应用:ClickHouse、分布式缓存等。
1 | primary.idx |
主键索引文件,用于按 granule 粒度 进行范围裁剪(range pruning)。
索引项(entry)生成规则
每当形成一个新的 granule,就会在 primary 索引中生成一个 entry
granule 的边界由以下两个条件之一触发(OR 关系):
重要说明
对于 每一列,都有一个对应的 mark 文件,用于描述 每个 granule 在该列数据文件中的物理位置。
mark 的数量关系
每个 data part 中:primary.idx 中有多少个 entry,每个列的 .mrk / .mrk4 文件中就有多少个 mark。二者在 granule 维度上 一一对应
每个 mark 的内容
每个 mark 是一对偏移量:
compressed_offset :granule 所在压缩块在 .bin 文件中的起始字节偏移
decompressed_offset:压缩块解压到内存后,该 granule 在解压后内存块中的起始字节偏移
补充说明
decompressed_offset 是 内存语义,不是磁盘语义。当一个压缩块只包含一个 granule 时,该值通常为 0;当一个压缩块包含多个 granule 时:第一个 granule 的 decompressed_offset = 0;后续 granule 的 decompressed_offset 为其在解压后内存块中的字节偏移
物理结构
data.bin 由多个 压缩块(compressed blocks) 顺序组成
每个压缩块包含:
1 个或多个完整 granule;任何一个 granule 都不会跨越压缩块边界
关系约束
一个 granule:必然完全位于某一个压缩块中;一个压缩块:可以包含多个 granule(取决于 granule 大小与压缩策略)
columns.txt:描述本 part 中:列名、列类型、列顺序
columns_substreams.txt:
描述每个列的 子流(substream)布局。特别是:
决定:一个逻辑列拆成几个 .bin / .mrk
checksums.txt:记录本 part 中 每个文件的校验和与大小
count.txt:记录该 part 中的 行数,相当于 part 级的 “统计元数据”
serialization.json:描述 列的序列化 / 反序列化方式
default_compression_codec.txt:记录该 part 使用的默认压缩算法(如 LZ4、ZSTD)
metadata_version.txt:表示该 part 的 metadata 版本
B-tree 和 LSM-tree 是数据密集型应用中用于组织和存储数据最广泛的两种数据结构。然而,两者各有优劣。本文旨在通过定量分析的方法对这两种数据结构进行对比。
通常情况下,衡量数据结构性能有三个关键指标:写放大(Write Amplification)、读放大(Read Amplification)和空间放大(Space Amplification)。本节旨在对这些指标进行描述。
对于机械硬盘(HDD)而言,磁盘寻道成本极高,导致随机读写的性能远不如顺序读写。本文假设使用的是闪存存储(如 SSD),因此我们可以忽略磁盘寻道的成本。
写放大(Write Amplification)是指写入存储设备的实际数据量与写入数据库的数据量之比。
例如,如果你向数据库写入了 10 MB 数据,但观察到磁盘实际产生了 30 MB 的写入量(或观测到磁盘写入带宽是数据库写入带宽的 3 倍),那么写放大就是 3
读放大(Read Amplification)是指每次查询所需的磁盘读取次数。
例如,如果为了响应一个查询需要读取 5 个页面,那么读放大就是 5。
请注意,写放大与读放大的单位是不同的。写放大衡量的是实际写入数据量与应用程序预期写入量之间的倍数关系;而读放大计算的是执行一次查询所需的磁盘读取次数。
读放大针对点查询(Point Query)和范围查询(Range Query)有不同的定义。对于范围查询,范围的长度(即需要获取的行数)是一个重要因素。
缓存是影响读放大的关键因素。例如,对于冷缓存(Cold-cache)情况下的 B 树,一次点查询需要
N:数据库中 记录(keys / tuples)的总数量
B:一个磁盘页(page / block)能容纳的 key 数量
空间放大(Space Amplification)是指存储设备上占用的实际数据量与数据库中存储的逻辑数据量之比。
例如,如果您向数据库存入 10 MB 的数据,而该数据库在磁盘上占用了 100 MB,那么空间放大倍数就是 10。
通常来说,一种数据结构最多只能在读、写和空间放大这三个指标中优化其中的两个。这意味着一种数据结构很难在所有三个指标上都优于另一种。例如,B 树的读放大比 LSM 树小,而 LSM 树的写放大则比 B 树小
B 树是二叉搜索树(Binary Search Tree)的一种推广,其节点可以拥有两个以上的子节点。B 树中有两类节点:内部节点(Internal nodes)和叶子节点(Leaf nodes)。叶子节点包含实际的数据记录且没有子节点;而内部节点可以在预定义的范围内拥有不同数量的子节点。内部节点可以进行合并或分裂。图 1 展示了一个 B 树的示例。
图 1。根节点位于树的顶部,在本例中恰好包含一个枢轴键(Pivot keys)(20),这表示键值为 k 且满足 k <= 20 的记录存储在第一个子节点中,而键值满足 k > 20 的记录存储在第二个子节点中。第一个子节点包含两个枢轴键(11 和 15),这表示键值满足 k <= 11 的记录存储在第一个(孙子)节点中,满足 11 < k <= 15 的记录存储在第二个节点中,而满足 k > 15 的记录则存储在第三个节点中。最左侧的叶子节点包含三个数值(3、5 和 7)。
“B 树”一词可以指代一种特定的设计,也可以指代一类通用的设计。从狭义上讲,B 树在其内部节点中存储键(Keys),但并不一定在叶子节点的记录中重复存储这些键。B+ 树(B+ tree)是 B 树最著名的变体之一。B+ 树的核心理念是:内部节点仅包含键,且在底部增加了一个包含实际数值的额外层级,该层级的叶子节点相互连接。
与其他搜索树一样,LSM 树(LSM-tree)也包含键值对(Key-value pairs)。它将数据维护在两个或多个独立的组件中(有时被称为 SSTables),每个组件都针对其对应的底层存储介质进行了优化。低层组件中的数据会以批处理的方式高效地与高层组件中的数据进行合并。图 2 展示了一个 LSM 树的示例。
图 2。LSM 树包含
个组件。数据首先进入 层,随后被合并到 层。最终, 层的数据会被合并到 层,以此类推。
LSM 树会定期执行合并(Compaction)操作,将多个 SSTable 合并为一个仅包含“活跃数据”(Live data)的新 SSTable。合并操作有助于 LSM 树回收空间并降低读放大。合并策略主要有两种:大小分级合并策略(STCS)和层级合并策略(LBCS)。STCS 的核心思想是:当 LSM 树积累了足够的短小 SSTable 时,将其合并为中等大小的 SSTable;当中等 SSTable 足够多时,再将其合并为大型 SSTable。而 LBCS 的核心思想是将数据组织进不同的层级(Level),每个层级包含一个有序序列(Sorted run)。一旦某一层积累了足够的数据,该层级的部分数据就会被合并到更高层级中。
本文讨论 B+ 树和基于层级的(Level-Based)LSM 树的写放大与读放大。
在 B+ 树中,键(Keys)的副本存储在内部节点中;键与记录(Records)存储在叶子节点中;此外,叶子节点可能还包含指向下一个叶子节点的指针,以提高顺序访问性能。
为了简化分析,假设树的块大小(Block size)为
写放大 (Write Amplification)
在最坏情况的插入负载下,每次插入都需要重写包含该记录的整个叶子块,因此写放大为
读放大 (Read Amplification)
每次查询的磁盘读取次数最多为
在基于层级的(Level-based)LSM 树中,数据按层组织。每一层包含一个有序序列。数据始于第 0 层(Level 0),随后被合并到第 1 层的序列中。最终,第 1 层的数据会被合并到第 2 层,以此类推。每一层的大小都受到限制。增长因子(Growth factor)
我们可以对基于层级的 LSM 树进行如下分析:如果增长因子为
其中
写放大 (Write Amplification)
数据必须从每一层移出一次,但给定层级的数据会与来自前一层的数据不断重复合并。平均而言,每个数据项在首次写入某一层后,会被重新合并回同一层约
读放大 (Read Amplification) 在冷缓存情况下执行短范围查询时,我们必须在每个层级上进行二分查找。
对于最高层级
对于上一层级
对于
……
以此类推。
因此,磁盘读取的总次数为:
下表总结了各种放大指标:
| 数据结构 | 写放大 (Write Amplification) | 读放大 (Read Amplification) |
|---|---|---|
| B+ 树 | ||
| 层级 LSM 树 |
通过对比 B+ 树与基于层级的 LSM 树的各种放大指标,我们可以得出结论:基于层级的 LSM 树在写入性能上优于 B+ 树,但在读取性能上则逊于 B+ 树。TiKV 选用 LSM 树而非 B 树作为其底层存储引擎的主要原因在于:利用缓存技术来提升读取性能,要比提升写入性能容易得多。
实际常见的选型逻辑
| 场景 | 推荐 |
|---|---|
| OLTP,读多写少 | B-Tree(PostgreSQL/MySQL) |
| 时序数据、日志、监控 | LSM(InfluxDB/Cassandra) |
| KV存储,写吞吐优先 | LSM(RocksDB) |
| 嵌入式,数据量小 | B-Tree(SQLite) |
| 分布式宽表 | LSM(HBase/Cassandra) |
pg_stat_statements 不能像 pageinspect 那样只靠 CREATE EXTENSION 就用,原因在于它们在 PostgreSQL 内核中的“级别”完全不同。
pg_stat_statements 和 pageinspect 的定位完全不同:它是一个 全局统计模块
它需要:
这些事情 只能在 postmaster 启动时完成。
1 | gujinfei=# show config_file; |
1 | shared_preload_libraries = 'pg_stat_statements' |
1 | pg_ctl restart |
1 | SHOW shared_preload_libraries; |
1 | drop table if exists t; |
1 | gujinfei=# drop table if exists t; |
在分布式数据库中,一行数据最终会被写入哪一台机器,取决于系统所采用的数据分布策略。在使用分布键(distribution key)的场景下,这一过程通常由哈希计算精确映射完成。
这个映射过程通常从一个 distribution key(分布键) 开始,经由哈希函数,最终落到某一个具体的数据节点上。
看似简单的映射关系,在系统扩容或缩容时,却会成为分布式系统设计中的一个核心难题。一致性 Hash,正是为了解决这个问题而出现的。
以一个分布式数据库中的表为例:
1 | CREATE TABLE orders ( |
在这个例子中:
user_id 被指定为 distribution keyuser_id 计算哈希值从这一刻开始,user_id 就决定了这行数据在物理层面“住在哪里”。
在多数据节点之间,最容易想到的一种数据分布方式是取模 Hash。
其基本思路非常直接:
user_id 计算哈希值示意图如下:

对应的算法可以表示为:
1 | database_id = hash(user_id) % num_of_dbs |
比如,当数据库集群拥有3个数据节点:
假设:
hash(123) = 7
hash(456) = 12
hash(789) = 5
则
1 | user id:123 → 7 % 3 = 1 → Database 1 |
在数据节点数量固定的前提下,这种方式能够较为均匀地分布数据,实现也非常简单。
但问题很快就会出现。
随着业务增长,3 台数据节点已经无法承载当前数据量,需要扩容到 4 台。
算法随之变为:
1 | database_id = hash(user_id) % 4 |
对于新写入的数据来说,这个变化影响不大。但对于已有数据而言,情况就完全不同了。
由于取模基数发生变化,几乎所有 user_id 的映射结果都会改变,意味着:
存量数据需要在节点之间进行大规模重分布。
示意如下:

例如,用户 123 过去映射到 Database 1,但现在:
1 | hash(123) % 4 = 3 (即:7%4) |
这行数据必须从Database 1迁移到 Database 3。
这种迁移不是个别现象,而是系统性问题,代价极其高昂。
对于取模 Hash,当节点从 n 变为 n+1 时,数据迁移率约为 n/(n+1)。这意味着如果从 9 台扩容到 10 台,会有 90% 的数据需要搬家
缩容的情况并不会更好。
当某个节点被下线时,取模 Hash 同样会导致大规模数据重新分布,带来:
问题的本质在于:
节点数量一旦发生变化,整个哈希空间的映射关系就被彻底打乱。
为了解决这一问题,我们需要一种在节点变化时“尽量少动数据”的方案。
一致性 Hash 的目标很明确:
在添加或删除节点时,尽可能减少需要重新分布的数据量。
其基本做法是:
0 ~ 2³²−1为便于说明,我们将哈希空间简化为 0~100,并假设通过哈希计算,4 个数据库节点在环上大致分布在如下位置:

当我们对 user_id 计算哈希后:
现在,我们在环上的位置 65 新增一个数据库节点 DB5。

此时:
(50, 65] 区间的数据需要迁移相比取模 Hash,数据迁移范围被大幅缩小。
缩容同样遵循相同的规则。
如果移除某个节点,其负责的哈希区间会顺延给下一个节点,而不会影响整个系统的映射关系。

到这里,一致性 Hash 看起来已经非常理想了,但在工程实践中还会遇到一个问题:
节点之间的负载可能严重不均衡。
例如,在移除 DB3 的场景中:
原本属于 DB3 的全部数据会被顺延给同一个相邻节点
这可能导致某一台数据库瞬间成为性能瓶颈。
为了解决负载不均的问题,需要引入 虚拟节点(Virtual Nodes) 的概念。
核心思想是:
一个物理节点,在哈希环上不只占据一个位置。
具体做法是:
例如,对于节点 database1,可以构造:
Hash("database1#1") -> pos(VDB1#1)Hash("database1#2") -> pos(VDB1#2)Hash("database1#3") -> pos(VDB1#3)Hash("database1#4") -> pos(VDB1#4)它们在逻辑上是 4 个独立节点,但在物理上都指向同一台服务器。
示意如下:

引入虚拟节点后:
这是一致性 Hash 能够在工程实践中落地的关键一步。
一致性哈希的价值不在于分布是否更均匀,而在于节点变化时将重映射控制在最小范围
即:当系统发生变化时,如何控制变化的影响范围
| 特性 | 普通取模哈希 | 一致性哈希 |
|---|---|---|
| 节点变化影响 | 全局映射变化,大规模迁移 | 仅影响 hash 环上的局部区间 |
| 扩容/缩容 | 不支持平滑扩容 | 支持在线、低迁移成本扩容 |
| 数据迁移比例 | 约为 1 − 1/N | 约为 1/N |
| 均衡性 | 天然均匀(前提:hash 好) | 原生不均,通过虚拟节点改善 |
| 典型应用 | 本地分片、静态分区 | 分布式缓存、KV、分布式存储 |
1 | wsl #如果没有安装,会安装 |
1 | PS C:\Users\gujinfei> wsl -l -v |
进入: %UserProfile% 目录,编辑或者增加 .wslconfig 文件
1 | [wsl2] |
1 | sudo apt install -y build-essential curl pkg-config libssl-dev |
1 | ssh-keygen -t ed25519 -C "[email protected]" |
本文为英文技术博客《Don’t give Postgres too much memory》的中文翻译。
原文作者:Tomas Vondra
原文链接:https://vondra.me/posts/dont-give-postgres-too-much-memory/
本文接近原文直译,仅做结构整理与术语统一,所有观点归原作者所有。
我时不时会被拉去排查一些与批处理(batch processing)相关的问题。
最近越来越常见的一种情况是:这些批处理进程会使用非常大的内存限制,尤其是:
maintenance_work_memwork_mem我猜不少 DBA 的思路是:
“内存越大越好。”
但他们往往没有意识到,这样做实际上可能会明显拖慢性能。
我用一个在测试 GIN 索引并行构建修复 时遇到的例子来说明这个问题。
这个 bug 本身并不复杂,也不算特别有意思,但它需要一个相当高的 maintenance_work_mem 才能复现 —— 最初的报告里使用了 20GB。
为了验证修复是否有效,我在:
maintenance_work_mem 设置组合下,反复执行 CREATE INDEX。
本来的目标只是检查是否还会失败,但我同时也记录了执行时间,并将结果画成了一张图。

测试运行在 Azure 上的一台 D96v4 实例:
这意味着:
并行化确实带来了明显收益:
使用 2 个 worker(包括 leader)
→ 性能提升约 1.8 倍
→ 接近理想加速比(因为索引构建的最后阶段仍然是串行的)
随着 worker 数量继续增加:
这完全符合预期。
真正令人意外的是图中展示的另一个趋势:
maintenance_work_mem越大,索引构建反而越慢。
具体表现为:
为什么会这样?
这很可能是多种因素共同作用的结果。
下面我解释两个我认为最重要的原因:
系统中的内存并不是“同一种速度”。
在 CPU 内部,存在一小块极快的缓存(L3 Cache),访问延迟非常低。
但这部分内存通常只有 32MB~128MB。
相比之下:
在索引构建过程中,通常会经历以下流程:
对于 GIN 索引 来说,这一步会把条目插入到一个哈希表中,这意味着:
大量随机内存访问
一旦这个哈希表的大小超过 L3 Cache,CPU 就不得不频繁访问主内存。
大致的访问代价是:
也就是说,慢了一个数量级。
因此:
往往是更优的策略。
即使需要处理更多批次,总体上仍然可能更快。
推荐阅读:
Ulrich Drepper,《What Every Programmer Should Know About Memory》(2007)
虽然年代久远,但内存层级的基本原理至今没有变化。
除了 CPU Cache,还有操作系统层面的因素。
当 GIN 的哈希表超过 maintenance_work_mem 限制时,数据会被写入临时文件。
这些文件不需要持久化保证,因此写入时只进入 page cache。
Linux 内核通过两个阈值控制脏页数量:
vm.dirty_background_ratio vm.dirty_ratio 理想状态下:
后台回写足够快,永远不会触及
vm.dirty_ratio
问题在于:
内核是否有“时间”去完成这些回写。
假设构建哈希表需要累积 8GB 数据,用时 1 分钟:
显然后者对系统更加友好。
以上所有分析,**同样适用于 work_mem**。
唯一的区别在于:
maintenance_work_mem CREATE INDEX、VACUUM 等)work_mem hash join、hash aggregate、sort 等)但底层原理完全一致:
我并不知道 maintenance_work_mem 或 work_mem 的“最佳值”是多少,这也不是这篇文章的重点。
重点在于:
盲目把内存参数调得很大,可能会显著伤害性能。
我的建议是:
pg_stats 视图中的 n_distinct 字段表示某一列中不同取值(distinct values)的数量。
如果 n_distinct 为负数,其绝对值表示该列中不同取值所占的比例,而不是实际的不同值个数。例如,−1 表示该列中所有值都是唯一的;−3 表示平均而言,每个不同的值大约出现在 3 行中。当估计的不同值数量超过表中总行数的 10% 时,分析器(analyzer)会使用这种“比例”的表示方式。
如果预期数据是均匀分布的,则会直接使用不同值的数量。例如,在估算 column = expression 这种条件的基数(cardinality)时,如果在规划阶段无法确定表达式的具体取值,查询规划器会假设该表达式可以以相同的概率取列中的任意一个值。
1 | => EXPLAIN SELECT * |