我昨晚狠狠干了一轮数据库优化,终于明白性能优化到底在优化什么

  • King
  • 发布于 8小时前
  • 阅读 19

昨天我写了一篇文章,主要是在说:当我真的拿Zig去写一个KV数据库之后,我开始理解这门语言到底擅长什么。但说实话,昨天那篇文章还比较偏“感受”。昨晚,我在kvdb的develop分支上狠狠干了一轮优化,做到后面突然有种更具体的感觉:数据库优化这件事,很多时候根本不是你发明了什

昨天我写了一篇文章,主要是在说:当我真的拿 Zig 去写一个 KV 数据库之后,我开始理解这门语言到底擅长什么。

但说实话,昨天那篇文章还比较偏“感受”。

昨晚,我在 kvdbdevelop 分支上狠狠干了一轮优化,做到后面突然有种更具体的感觉:

数据库优化这件事,很多时候根本不是你发明了什么惊天算法,而是你开始认真对待每一次内存分配、每一次磁盘调用、每一次多余的树遍历。

也就是说,优化不是“变魔术”,而是“别浪费”。

一开始我以为,数据库优化应该很玄学

很多人一提性能优化,脑子里马上就会冒出一堆高大上的词:

锁优化、零拷贝、SIMD、缓存友好、分层索引、批处理、并发调度……

这些当然都重要。

但我昨晚最大的体感反而是:

真正先把程序拖慢的,往往不是缺少某个高级技巧,而是代码里有太多“理所当然但其实很贵”的动作。

比如:

先查 key 在不在,再决定插入还是更新; 每次更新都把整页重新整理一遍; 内部节点插入时,先收集再重建; 写一条 WAL,拆成很多小段慢慢写; 读一个页,先 seek 再 read。

这些写法都“没错”,而且刚写出来的时候通常还觉得挺自然。

但数据库一旦开始跑起来,操作次数上来之后,这些“自然的动作”就会变成真实的成本。

我昨晚做的优化,说白了就是一直在删“多余动作”

这轮优化里,最核心的事情不是加功能,而是删浪费。

我越来越觉得,数据库内核开发很像收拾一个本来能住、但动线很差的房子。

不是说房子不能住。 而是你走两步就撞一下,转身又多绕一下,拿个杯子要多开一次柜门。 单次看起来都不大,但一天重复一百次,人就烦了。

程序也是一样。

第一类浪费:写日志写得太碎

WAL 这个东西,本质上就是“正式改数据之前,先把变更记到日志里”。

它的意义谁都知道:防崩溃,做恢复。

但真正影响性能的,不只是“有没有 WAL”,而是“你怎么写 WAL”。

如果一条日志要先在堆上拼很多临时块,再分很多次写出去,那每次写入都在给自己加戏。昨晚我做的一个重点,就是尽量把记录在栈上组织好,再更整块地写出去。

这类优化的本质很朴素:

不是别写日志,而是别把一条本来很短的路,硬走成绕远路。

第二类浪费:同一棵树走两遍

以前很容易写出这种逻辑:

contains 看 key 在不在; 不在就插入; 在就更新。

看着特别合理。

问题是,数据库里的“看一眼”可不是白看的。 每次判断存在性,本质上都要再走一遍 B-tree。

所以后来我把思路改成了:

先直接写,只有真正撞上“key 已存在”时,再切更新分支。

这事给我的感受特别强烈:

有时候你以为自己在“谨慎”,实际上你是在让程序白干活。

第三类浪费:明明能原地改,非要整页重排

这个优化我自己很喜欢。

数据库页内部的数据布局,本来就很珍贵。你想想,页里已经放好了那么多 key/value,位置都排好了,这本身就是一种“局部秩序”。

如果一次更新只是把旧值换成更短的新值,而且原来的位置明明还放得下,那最合理的做法其实就是原地改。

但如果你不管三七二十一,每次都把整页重新整理一遍,那其实是在为了省脑子,牺牲机器。

这个优化带给我的感觉是:

很多时候,真正好的优化,不是更激进地折腾数据,而是更克制地少碰数据。

第四类浪费:内部节点插入时,一言不合就重新建一份

B-tree 不只是叶子节点会动,内部节点也会变。

以前内部节点插入时,一个很顺手的做法是:先把内容收集出来,插进去,再整份重建。

逻辑是简单了。 但代价也很明显:分配新内存、拷贝、回写。

后来改成原地 memmove 之后,那种感觉特别像:

以前是为了在名单中间插一个名字,先把整张名单抄一遍; 现在是把后面的人往后挪一个位置,把新名字塞进去。

代码未必更“优雅”,但会更诚实。

我现在越来越觉得,性能优化其实很像打扫卫生

它不是搭新房子。 它更像大扫除。

你不会因为把房间打扫干净,就发明了一个新宇宙。 你只是终于把那些堆在角落里的破纸箱、塑料袋、空瓶子、废电线,一点点清走了。

但一旦清完,你会发现整个空间 suddenly 就通了。

程序优化也是这种感觉。

不是每次都要上复杂理论。 很多时候,你只是把这些东西清掉:

没必要的分配, 没必要的拷贝, 没必要的 syscall, 没必要的树遍历, 没必要的页重排。

然后系统自然就顺了。

这次之后,我对 Zig 的理解也更具体了

昨天我还在说 Zig 给我的感觉是“更贴近底层”。

但昨晚狠狠干这一轮后,我觉得更准确的说法应该是:

Zig 会逼你不断意识到成本。

这一步到底有没有分配? 这个 buffer 能不能放栈上? 这里为什么要复制? 这里真的要重排整页吗? 这个抽象是在帮我,还是在偷偷花钱?

这种感觉在写数据库时尤其强。

因为数据库是非常不讲情面的。 你代码里只要多做了一点点没必要的事,最后都会被吞吐、延迟、磁盘写放大老老实实地揭穿。

我越来越喜欢这种“很诚实”的编程

写到现在,我最大的感受不是“我做出了一个多厉害的数据库”。

而是:

我终于开始理解,数据库为什么是系统编程里最诚实的一类东西。

你多分配一次,它知道。 你多拷贝一次,它知道。 你多走一遍树,它知道。 你多发一个 syscall,它也知道。

它不会因为你代码写得很漂亮,就假装没看到这些成本。

所以昨晚这轮优化,虽然从外面看没有什么特别炸裂的新功能,但它让我第一次特别具体地明白:

所谓性能优化,很多时候就是把那些“本来以为理所当然”的浪费,一个个揪出来。

而这,可能才是从“做了个数据库玩具”,走向“开始理解数据库内核”的真正开始。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发