读源码到底该从哪下手?我啃了三个月 Redis 源码,把以前的习惯全推翻了

🔑 关键词:源码阅读,Redis源码,开源项目源码怎么读,gdb调试源码,git log 查历史

📖 摘要:一份不太正经但真管用的源码阅读流程:为什么我不建议从 main 函数开始,先用 cloc 和 git log -S 给代码量个尺寸,再用 gdb 断点把抽象变成能看见的调用栈。以 Redis 6.2 的过期键回收逻辑为例子,附上能直接抄的命令和参数。

去年冬天接了个私活,客户用的 Redis 6.2,跑在一台 16G 内存的机器上。他们嫌过期键回收太慢,内存经常顶到 80% 以上才松一口气,需求说白了就是让回收更激进一点。

图片

我当时的做法很蠢:git clone 下来,找到 src/server.c,翻到 main,打算一行一行往下读。三天之后我还在 initServerConfig 里面转悠。server.c 一万多行,main 把几十个初始化函数串在一起,一路看下去全是配置项赋值,跟“过期键”三个字一点关系都没有。我甚至一度怀疑是不是拉错仓库了。

后来我换了个思路,第一步不是读,是先给代码量尺寸。

git clone --depth 1 -b 6.2 https://github.com/redis/redis.git
cd redis
cloc src --include-lang=C --by-file | sort -k5 -nr | head -20

图片

cloc 那一列跑完,我心里就有谱了:跟过期相关的东西其实非常集中,expire.c、db.c 里的 lookupKey、dict.c 里的迭代器,再加 server.c 里调度 activeExpireCycle 的那几行。剩下十万行代码跟我这次的需求没有半毛钱关系。

这里插一句我的偏见:读源码最值钱的两个工具不是 IDE,是行数统计和调用关系。你先知道哪块地是荒的,才不会一头扎进去。

第二步我做的是翻 git 历史,而不是翻实现。

git log -S 'activeExpireCycle' --oneline -- src/
git log --oneline --since='2018-01-01' -- src/expire.c | wc -l

图片

-S 这个参数(pickaxe)会找出“某个字符串被加入或删除”的那次提交,比单纯的 git log -- 文件 好用太多。我用它把 activeExpireCycle 这几年的改动捞出来,看到它被拆过、被限流过、加过 active-expire-effort 这个配置项。提交信息里几个作者来回吵的过程,比读最终那几十行代码更能让你明白“为什么最后长成这个样子”。

顺手记一个参数:active-expire-effort 默认值是 1,可以调到 1 到 10;hz 在 redis.conf 里默认是 10。这俩一起决定每秒扫几轮、每轮扫多久。客户要的“更激进”,在 6.2 上其实先调这两个数就能顶一阵——虽然我最后确实还是去改了代码。

第三步,也是我觉得最关键的:把代码跑起来,别在纸上读。

图片

make noopt
# 或者手动来:make CFLAGS='-O0 -g'
gdb --args ./src/redis-server redis.conf

然后在 gdb 里:

(gdb) b expireIfNeeded
(gdb) commands
> silent
> bt 6
> continue
> end
(gdb) run

图片

另开一个终端,redis-cli 里敲 SET k v EX 5,等五秒再 GET k。断点会被打中,栈里是 lookupKey → expireIfNeeded 这条链,一条一条往上看。抽象的东西一旦变成屏幕上滚过去的调用栈,理解成本会掉一大截。

我承认这一步我是有私心的:我不太信画流程图那一套。我试过用纸笔画 dict 的渐进式 rehash,画完自我感觉良好,真要看 dictRehashMilliseconds 的时候还是一脸茫然。后来直接在 gdb 里 watch -l d->rehashidx,盯着那个变量跑,五分钟就通了。纸上得来终觉浅这话搁这儿挺对。

第四件事:别指望“读完”。我身边不止一个人说过类似的话,等我把某某源码读完就开始写。这个念头基本等于永远开始不了。Redis 光 src 目录就几百个文件,加上 deps 里的 jemalloc、lua、hiredis,你要读完是给自己找不痛快。

正确的姿势是带着一个具体问题进去,问题解决了就出来。我那次的问题是“过期键在从库上到底删不删”,前后改了 40 多行,翻过的文件不超过 8 个。剩下的时间全花在验证上,而不是阅读上。

图片

当然也不是所有代码都不该从头读。有些库本身就是拿来当教材的:sds.c 一两千行,是一个能装进脑子的小世界;listpack.c 也差不多。这类代码量小、几乎没有外部依赖、能独立跑单元测试的模块,从第一个函数顺着读到最后,反而是效率最高的方式。区分标准我觉得就一条:这个东西你装不装得下。装得下就通读,装不下就按问题切。

最后说个我自己的变化。以前我看到 #define 和二级指针就头疼,习惯性先搜几篇别人整理好的博客看配图。现在我基本是反着来的:先 clone,先 make,先下断点,看完现象再回头读代码。

有回我在 gdb 里跟着 dictRehash 走了半个多小时,一直没想明白 rehashidx 到 -1 为什么就算结束,最后是在 dictAddRaw 里看到那句 if (dictIsRehashing(d)) _dictRehashStep(d); 才反应过来的。这种瞬间,看多少篇讲 Redis 字典的文章都给不了。

🏷️ 标签: