上图是 etcd 的 github 页面,在显眼位置标明了它采用 Raft 共识算法,并链接到 Raft 算法的主页,如果我们没了解过 Raft,直接去读 etcd 的代码,很可能就对里面的选举、日志复制等概念一知半解,这就好像在看没有字幕的外文电影,精彩程度大打折扣。
3. 再读代码看完了文档,就可以开始看代码了。为了防止在代码中迷失方向,我们可以遵循几条原则来阅读:
从入口开始
虽说通过架构模型以及包和文件划分的关系,我们能大致确定哪些代码是核心代码,但从入口处开始看会更符合大脑的思考方式。
因为入口代码的工作一般是先对各种模块进行初始化,然后调起主线程或者启动主服务,这种明确顺序的简单工作让我们不会一开始就遇到困难,循序渐进的过程更容易让大脑产生奖励。
如图所示是 kubelet 启动入口简化后的主线逻辑,非常清晰。以此为起点沉下去,就可以分三路去细看配置详情、创建 kubelet 的详情,以及启动的详情。
抓住主线,从抽象到实现
主线就是从输入是怎么样一步步产生输出的。在这一过程中,会涉及到多个模块,每一个模块又有自己的输入和输出。
当我们顺着函数调用、数据传输方向一步步向下时,随着抽象层次的不断降低,涉及到越来越多的细节,这个时候应该及时折返,不要一路看到底,很容易迷失在里面。
良好的设计会有合理的抽象,根据不同的开发语言,我们可以通过查看包、接口、特性、公有方法列表、头文件等等来快速获取抽象信息,逐步地拼接出程序主线。搞清楚了主线,再逐步将抽象展开,阅读具体实现代码。
仍以 kubelet 为例,kubelet 作为负责整个节点运转的核心,工作多且杂。
但看它的代码分包结构,非常清楚地将不同功能点划分到不同目录下,结合初始化逻辑,再进一步深入到每个功能目录内,又可以发现 kubelet 的模块设计遵循的是多个 manager 围绕着一个核心共同协作的模型。好的抽象,就像一颗洋葱一样,层层分明。
一边阅读一边记录
初识一个项目,对结构和流程把握的不会太清楚,因此一边读一边写写画画是很重要的。
有的时候跳转次数比较多,前面看过的东西后面就忘记了,所以对关键路径,记录具体的函数名、模块名,能帮助我们快速回溯到入口。
也有的时候遇到了需要拓展的知识盲区,为了不打断主线思路,可以先记录下来,找其他时间再学习。
另外,遇到不直观的、难以形成概念的代码表达,翻来覆去的看也看不懂,这个时候就需要画个图来帮助理解了。
一个典型的例子就是在学习 B Tree 的分裂、合并、上移下移的时候,全看代码特别不直观,想要理解这类内容画图定有奇效:
必要时借助 debug
有一些代码为了正确性、性能等考虑,其表述可能会让人百思不得其解。人类的思维方式是偏向顺序的,用软件开发做类比就是,我们更容易理解 Happy Path,而忽视分支细节。
当横竖想不通某段代码为什么要这么写的时候,实际运行一遍,加断点 Debug 一下可能就会发现真实的原因了。
一个有趣的例子是:在环形队列中,判断队列是否为空需要看头指针和尾指针是不是已经重合,下图的代码来自一个无锁环形队列的判空实现。