为什么要读源码
通过学习优秀代码的设计反哺自己的代码设计。从源码看新技术产生是如何使用前面已有的技术。
新科技诞生了,一般都是不可逆的,就如汽车发明了之后,我们不再需要马车,任凭马车夫如何抗议都没有用。高铁的速度是自驾无法比拟的,并且也更加安全和方便。
Kubernetes
出现后运维的难度大大降低,人工运维的生产力也得到了解放,这虽然减少了相应的岗位,但是交由程序处理降低了应用部署带来的系统不稳定。
我们去看它源码的实现的过程中也能学习到 Kubernetes
是如何封装的来降低软件工程中 Accident Complex
偶然复杂度的问题。
文档会误导人但是代码不会。
如何开始
从功能点出发,不要没有目的直接开始
我第一次读源码的启蒙是Redis 的设计与实现
选择自己熟悉语言的项目进行入手,先学会如何用,再去看我们每一个用的命令的背后实现是怎么样的,这样在阅读的过程中会有一种抽丝剥茧的快乐。
由于我日常工作是用的 Go,所以我会有意识的去找一些 Go 的项目,这样就不必花费太多的精力在语言的语法上。
分层封装去看
每次学到操作系统的东西时可以先当成黑盒,把握主线的任务,无关紧要的话可以不深入探索,避免探索完之后已经忘记了回来之后要看哪部分的内容。
例如我在阅读 k8s 源码的时候想知道 Service 的负载均衡在代码层面是如何实现的,首先就得先了解它网络的工作原理,当知道它是通过 iptables
、 ipvs
或者 nftables
来实现的时候,我就知道我需要看的是 k8s 如何借助它们来实现功能,如何通过代码来维护这些工具的规则从而实现 Service 层面的负载均衡。
但是至于 iptables
这些工具的实现,并不是 k8s 需要重点关注的内容,所以我们就不过多的深究,如果有兴趣的时候,我们可以通过了解这个工具本身的实现来学习,这样也避免了我们陷入细节而忘记了我们自己为什么要读这一块代码。
不仅仅是在工具的使用上要分层去阅读,在 k8s 本身代码的阅读上更是需要用封层的思维。
几乎所有的组件都会依赖 apiserver
的 informer
实现:
- kube-scheduler 通过
informer
来获取所有需要绑定 Node 的 Pod - kube-proxy 通过
informer
来监听所有新加入的节点,并给节点增加网络配置 - kubelet 通过监听跟自己 NodeName 匹配的Pod,来对Pod进行维护
不仅仅是监听,他们还会通过 apiserver
将自己对集群数据的操作重新推送出去,让依赖到的组件能够在接收到变更后去做自己内部的工作。
比如 kube-scheduler
完成Pod的节点选择后,将 Node 绑定到 Pod上,推送给 apiserver
,如果我们只是想了解节点的选择是如何实现的,那就需要重点把注意力放在 kube-scheduler
内部各种插件的实现,如何通过 Node 的信息对它进行评分,而把 Node 节点信息的维护当成黑盒即可,因为这一部分是由 kubelet
来实现并推送给 apiserver
的。
这个过程中要时刻提醒自己的原则就是分层次隔离去看,把一个点完全吃透后再去攻克周围的点。不过也不用有太多心理的负担,有些设计在当下自己思考不明白的时候可以适当跳过,继续去钻研主线,等到自己积累的内容足够多的时候,再回来看会发现很多内容会串联在一起。
比如 k8s 项目本来就非常的庞大,我先通过网络这个点去深入,看 k8s 是如何实现负载均衡的,然后延伸出来我看到它监听了 Pod 的信息来进行网络的设置,这个时候我顺着这条路,看到了 apiserver
的 informer
机制去获取 Pod 的信息,既然知道怎么获取,那我就会想它是由谁写入的?
我就通过这个思路去翻官方文档发现每个 Node 上都会有维护 Pod 信息的Kubelet,在这个过程中看到了 kubelet 将维护好的信息推回给 apiserver
,这个过程中它会用 CRI 插件来维护容器的信息,这里不是我想要探索的内容,所以我姑且把它当成黑盒,但是我又发现 kubelet 维护的 Pod 信息也是通过监听特定的 Node 名字匹配后才开始更新的,这个时候我又有兴趣了,想知道是哪个组件来帮忙绑定上 NodeName的。
经过前面的经验我们已经知道肯定是有一个组件绑定完之后重新设置回 apiserver
,所以仍然是通过官方文档看到这个组件是 kube-scheduler,通过应用的入口我也顺藤摸瓜看到了节点的筛选和选择的逻辑。
我学完了这条链路后,重新去回顾整个代码的结构,发现 kubelet 、kube-scheduler、kube-apiserver 和 kube-proxy 其实位置的分布结构都差不多
markdown
- cmd 各个应用的 `main` 方法
- kube-proxy 负责网络相关规则的应用
- kube-apiserver 负责公开k8s的api,处理接受请求。提供了各种资源(Pod,replicaSet,Service)的CURD
- kube-scheduler 负责监视新创建的Pod并选择节点给Pod运行
- kubectl 访问集群的命令行工具
- pkg 各组件的主要实现
- proxy: 网络代理的实现
- iptables
- ipvs
- nftables
- kubelet: 维护Node的Pod
- cm: 容器管理,如 cgroups
- stats :资源占用情况,由`cAdvisor` 实现
- scheduler: Pod调度的实现
- framework
- controlplane:控制平面
- apiserver
主要的实现逻辑都分布在 pkg
中,以 proxy 为例,由于有多重实现,proxy 包下面就包含了多个网络组件的实现。
这过程中我也知道了因为 iptables 的性能问题,所以 k8s 才使用了其他实现,并且由于 proxier
接口的抽象,能让真正干活的底层组件灵活替换,这样在底层工具本身有性能问题的时候,并不会影响上层的基本功能。
这也是分层来带的好处,只要有标准的接口,就能够灵活的进行局部优化。
动手写单元测试
在读代码的过程中,可以运行单元测试进行局部验证,甚至可以将局部的代码块通过自己的理解进行解构,看看自己实现的是否更好。
用好IDE
在构建或者启动的入口增加书签,避免跳入层次过深的细节之后,不知道自己为什么要进入。
在 Goland 中可以通过 F3
来给代码增加书签,通过 ctrl + F3
来查看自己加入的所有书签
查看方法结构
快捷键 Alt + 7
由于优秀的开源项目的代码封装都是比我们日常业务项目封装的结构会更加的优雅,所以我们可以通过查看文件的结构,来学习它的设计的方法。
我在读完一个功能的源码后,经常会回过头去看它整体的 Structure
,这个可以让我看到它是如何抽象出暴露出来的接口,如何把必要的信息给到外界,而不重要的信息则保留在内部。
Go是语法糖相对较少的语言,这个时候我们会更多的将注意力放在代码的设计上,不必去纠结一个抽象我们要实现有什么奇技淫巧,这些都只是工具,最无法替代的是代码结构的设计,只要我们的架构设计的足够清晰,实现出来代码基本不会难以理解。
快速搜索及跳转
除了我们日常用的 ctrl + f
和 ctrl + shift + f
的搜索外,我们还可以通过双击 shift 调起一个全局搜索。
里面可以通过选择 Types
来找到想要找的接口或者结构体。
Files
来查看我们筛选出来的文件,里面也有很多高级的筛选功能,如可以看到自己最近修改的文件。
ctrl+e
可以找到我们最近打开的文件。
做好记录和联想
一边读的时候可以通过画图工具一边画出类图,记录关键的对象,不要大而全,因为代码里有很多不重要的细节,我们要在读的过程中做一些提炼。
在阅读源码中我们把不断出现的问题解决后进行记录,然后跟我们之前学习的内容进行联想,过程中不断拓展出自己的思考。
学的过程中我们需要不断进行思维的切换:
- 主线:我们提出要通过源码来学习的问题
- 支线:这个设计好在哪里?或者是不好在哪里?
通过主线和支线不断切换,积累出我们学习源码获得的实际知识。
不要等到最后把主线学完了之后再集中去回顾自己的灵感,这样需要回顾的内容太多,我们大脑的缓存实际上非常的小,如果没有不断具象化和浓缩,最终回顾起来是非常混沌的,不会有结构性的产出。
过程中的记录我们可以记关键节点,但是要足够自己回想起来内容,然后在一个模块学习完之后对刚刚看到的源码进行整理,在 Kubernetes
学习后我会在整理的时候问自己下面这些问题,当然不同的组件关注点不一样:
- 它的数据是怎么做到一致性的?
- 它有没有性能更好的设计方式?
多向自己提问
读代码的过程中要有自己的主线产出 ,如一个系列去讲明白 Kubernete
的负载均衡。
这个时候我们就会不断向自己提出问题,底层 Kubernetes
是如何实现网络转发的?
是 Kubernetes
自己的功能支持还是使用了操作系统的功能支持,是直接使用操作系统还是做了二次封装?
如果是使用操作系统的内核功能,它是如何维护转发规则的?
先有一个具体的切入角度,再去逐步分解 Kubernetes
的实现,在过程中不断的提醒自己思考:
- 在看到源码抽象出一个对象的时候,可以问自己是否有其他的抽象方式,如果不这样抽象可不可以实现一样的功能,如果是我自己来做,我会怎么做?
- 哪些内容学习起来之后可以反哺我们实际业务中软件的构建?
如果我们的实现方式比它的更优雅,我们可以提出 PR
来帮助 Kubernetes
去构建。
如果我们实现方式没有比它更好,那这个时候我们可以通过参考它的实现来改进我们自己的实现,将其内化到自己的能力中,让自己设计的代码更加高效,且复杂度更低更易于维护。
通过这种提问思考,最开始我们的想法可能没有作者实现的好,那我们就吸收它的做法,等到自己有成长了之后,很可能就能提出比作者还要好的设计。
读代码的时候提出了第一阶的问题并解决了之后,多问一下自己然后呢?深入挖掘代码实现的本质,代码是运行的最终产出,它是不会有任何隐藏的内容的。
这个过程中我们更要不断地回顾我们已经读过的代码设计如何应用到实际编码中,联想到了实际中并且能改善自己的代码质量或者提升可维护性,甚至是自己在实际项目中由于源码的启发设计了自己的工具应用,这才是将学习了内容真正导出成了 "知识"。
总结成一句话就是:读代码前先规划时间,始终要关注成果,而不是读代码的数量。