如何学习一个新的分布式系统

能够从零开始一步一步打造一个成功的产品恐怕是所有程序员都渴望的经历。但事实上,这样难得的机遇并不常有。更多的时候,我们会在一个系统已经初具规模时加入,然后逐步负责其中的某个模块的开发。于是每次遇到一个新的系统,总有一个非常自然的问题摆在我们面前:如何才能高效地学习一个新的系统?

dubbo-architecture-roadmap

随着互联网的飞速的发展,如今的计算机程序,早已不是曾经单台服务器能够运行的小玩具了。随着业务规模不断的增大,软件工程也随之变得异常复杂。为了保证一个庞大的系统可以被解耦分工开发,灵活扩展部署,曾经的单体程序逐步演化为了今天的分布式系统架构。程序被拆散为多个模块分布在不同的服务器上,不同的模块间还会有跨服务的调用,这些都给我们的学习和软件维护带来了许多挑战。本文试图描述一种通用的路径,帮助一个新手快速理解一套分布式系统程序。

以学习OpenStack Neutron为例吧。面对这个庞大的云计算系统,如果之前没有相关经验,那么第一步应该了解些什么呢?

一,学习使用基本功能

当然是从用户视角了解这个系统的主要功能。用户使用这套系统能做什么?能为用户带来什么好处?主功能怎么使用?有哪些关键参数可以设置?这些问题虽然直白,但是能够帮助我们快速地抓住一个产品的核心功能集。注意,一定是最核心的功能,而不是面面俱到的了解所有功能。打个比方,我们买一台空调,是为了能够在炎热的夏季保持室内的凉爽。我们完全没必要了解空调是怎样实现的,只要会用遥控器打开空调,调节温度,风向风速,就可以了。至于空调是怎么安装的,空调还有哪些酷炫的功能,不知道也无所谓。

“会用”是了解一个新系统的开始,这一步帮助我们快速对要学习的领域建立最基本的认知。一个研究OpenStack的人如果都不知道云计算是干什么的,解决了什么问题,还有哪些痛点无法解决,当前的热点方向有哪些的话,是很难在这个领域走的长走的远的。当然,学会使用也不是走马观花地在界面上点点了事,至少要梳理出以下两方面内容:

1,从用户的角度,有哪些典型的用户使用场景?

比如指定镜像和网络创建虚拟机、删除虚拟机、虚拟机冷迁移等等。不一定要一步到位把所有场景都梳理出来,可以画个思维导图或者一个User Case图,以后可以慢慢积累慢慢迭代。

2,系统中有哪些关键的领域模型?

比如在云计算场景下,关键的领域模型可能包括vm、flavor、image、network、subnet、port、volume等等。先找出可以组成最基本业务的关键模型,梳理清楚每个模型中有哪些关键参数,模型之间的关系是怎样的?画一画ER图,对于比较抽象的模型写一些解释性的文档备忘。

到了这里,学习一个系统的第一步就算是结束了。你应当已经对要学习的系统的行业背景有了初步的了解,对系统的功能有了实际的体验,更对系统的主要组成部分及各部分间的关系有了比较清晰的了解。

二,了解软件架构

系统不仅仅是一些事物的简单集合,而是由一组相互连接的要素构成的能够实现某个目标的整体。一个系统中可能包含很多子系统,而它也可以嵌入到其他更大的系统之中,成为那个更大的系统中的一个子系统。

对一个系统来说,要素、内在连接和目标,所有这些都是必不可少的,它们之间互相联系,各司其职。一般来说,系统中最不明显的部分,即功能或目标,才是系统行为最关键的决定因素。一个成功的系统,应该能够实现个体目标与系统总目标的一致性;内在连接也是至关重要的,因为改变了要素之间的连接,通常会改变系统的行为。系统中的很多连接是通过信息流进行运作的,信息使得系统整合在一起,并对系统的运作产生重要影响;尽管要素是我们最容易注意到的系统部分,但它对于定义系统的特点通常是最不重要的——除非是某个要素的改变也能导致连接或目标的改变。

在第一部分中,我们已经了解了系统的目标。而在第二部分中,我们要重点关注一下组成一个软件系统的各个组件以及他们之间的内在联系。

还是以Neutron系统为例。首先来看一下Neutron包含了哪些组件:
1,Neutron-server 负责提供北向网络API,将用户配置写入DB,并通知相应agent处理业务
2,Neutron-openvswitch-agent 负责处理二层网络配置,监听虚拟机port上线情况,从Neutron-server拉取配置后下发流表
3,Neutron-l3-agent 负责处理三层网络配置 ……

我们看到,Neutron系统中分为了neutron-server和众多agent的两层结构。neutron-server负责处理北向请求并维护数据库数据,agent则各自负责一层网络功能,从neutron-server获取数据之后进行实际的网络规则配置。server与agent之间采用Rabbitmq进行RPC调用。这里只是比较粗略地说明了neutron系统的大致结构,实际在真正的商用部署中,组件上下游还有很多组件,应当一并纳入分析。梳理出系统的构成以及各组件间通信方式之后,还应当选取核心用户场景,分析一下一个新业务请求是如何从客户端发起,到最终处理完成返回给用户的。这里可以通过时序图来对各组件间的信息流动情况进行分析。

三,了解项目中软件的生命周期

在正式开始把玩代码细节之前,还有一件事必不可少,那就是了解项目中软件的整个生命周期的规则。
每个团队或者开源组织都有自己的软件工程管理规则,想要融入一个组织,就必须遵守这些规则。软件的研发也不仅仅是写代码那么简单,还包括了部署上线以及维护直至生命周期结束。

首先来看开发阶段的一些动作。一般来说,每个软件项目都会有其指定的编码规范,有测试用例的编写要求,也有CI/CD的保障要求(比如要求主干全绿过夜),这些都需要仔细学习,并落实到自己的开发行动中。认真对待每一个交付件(文档、测试用例、Commit信息、Release Note等等)体现了一个程序员的职业性。说实话,想要长期保持规范而不变形,非常有挑战。

第二,学习软件的安装部署、升级变更方法。要自己搞一套独立的环境调试代码,安装部署总是绕不过去的。而且通常安装部署还是一个高频操作,故而很有必要把这一步做到极致。在devops的潮流下,线上环境的升级变更也愈发的频繁起来。因此熟悉系统的安装部署升级方案是很有必要的,这也会提高开发效率。

最后,如果这套系统已经商用,了解一下线上维护的基本知识。如何登陆环境,每个组件各自部署在哪,日志在哪个目录下?看看团队里是否有线上问题处理的文档,初学阶段倒不一定要仔细读这些文档,但遇到问题至少要知道去哪找答案。

四,深入细节读代码

写了这么多,终于打开IDE(工具很重要,了解一下项目组成员平时都在用哪些开发工具)开始看代码了。面对着几十万行的代(luan)码,是不是觉得有点茫然?读代码应当从何入手呢?

0,语言基础

在开始阅读代码之前,首先确认自己对于项目使用的程序语言特性有基本的了解。如果对编程语言和数据结构这些基础中的基础还不清楚,阅读代码也就是在浪费时间。相反,如果对编程语言特性以及数据结构有着比较深的理解,那么阅读代码的效率会快很多。

1,框架与设计模式

一般大型的项目都会引入成熟的框架来降低开发的复杂度。这就要求我们在深入业务细节之前,先要对应用框架有所了解。搞清楚项目的目录结构以及每个目录中文件的功能;了解框架使用了哪些典型的设计模式;了解业务代码是如何分层的,每个层次都负责哪些功能;项目中使用了哪些中间件。把这些通用性的问题搞清楚,阅读代码便成功了一半。

2,深度优先地阅读业务代码

其实在一个大型的项目中,各个模块的实现方式是大同小异的。因此如果能够把一个业务模块的代码读通,再看其他模块就非常容易了。 首先找到一个业务的入口,它可能是一个RESTFUL API,可能是一个CLI命令,也可能是一个定时任务,甚至是一个main函数。从这个入口切入,然后一直读到这一个业务的终结。这个过程中要抓住业务的主干,搞清楚一项业务大致分为几个步骤。另外要关注所有和外部系统有交互的点,把这些点都记录下来。这些和其他系统交互的地方往往是最难理解也最容易出问题的点。

举个例子:neutron系统中最核心的业务是创建port。那么我们可以从北向API入手,看这个业务流程如何从接收用户的创建请求,到port数据存入数据库,再到ovs-agent拉取neutron-server数据,配置ovs规则。一个业务流程中通常会涉及到多个不同的业务组件,如创建port的过程中可能涉及到neutron-server与dhcp-agent、ovs-agent的交互。把这些跨系统的调用关系理清楚至关重要。

3,解决一个问题

当我们把一个系统的业务模块一一读过之后,可能已经有了自己掌控一切的感觉。但请注意,以为自己懂了和真的懂了是有差距的。我们阅读一段代码,很大可能是因为我们需要基于它开发新的特性,或者负责已有业务的维护工作。因此,解决业务上的真实问题才是检验我们学习效果的唯一途径。如果运气好,能碰到真实的线上故障或者问题,那么一定不要错过,赶快冲上去定位这个问题。定位问题根因 -> 给出解决方案- > 制作出升级补丁- >完成线上变更修复。如果能够独立地搞定这一连串事情,那么证明你已经对这套系统具备了维护能力。

恭喜你,走到这一步,对于学习一套新系统,基本可以算是合格了。

五,了解系统的非功能属性

对于一个大型的商用系统而言,简单地堆砌业务代码是不够的。系统的设计者还需要考虑很多非功能的特性,这些特性的实现质量往往决定了一个产品的成败。但这些特性又往往与业务不直接相关,因而在学习的过程中很容易被忽视。在此列举一些重要的非功能特性,在学习一套新系统的过程中,应当主动地提出这些方面的问题并探寻当前的系统是如何实现这些需求的。

1,高可用

我们每天都使用微信和别人沟通,但你能想象微信故障一天无法发信息会给我们的生活造成什么影响么?如果一天不能上网呢? 一个软件系统承担的业务越重要,往往其可用性的要求也就越高。这也就是我们在很多云计算服务协议中看到的SLA承诺。软件可能有bug,硬件可能老化失效,但是作为一个整体的系统,我们需要保证其7 * 24小时的不间断运转。这便需要我们在架构设计中充分考虑高可用性。学习一个系统,就要了解系统中的每一个组件如何做冗余、灾备,遇到问题有哪些应急预案。没有考虑这些,那么运营一个系统就如同赌博一般,完全凭运气。

2,可靠性

如果因为某些意外,系统中出现了一些错误,这个系统能够自动恢复么?或者换个问题,系统可以保证数据的最终一致性么?如果一个请求失败了,那么再向系统发送一个相同的请求是否能够保证幂等性?在学习一个新系统的过程中,尝试问一些关于系统强壮性的问题。一个优秀的系统通常在可靠性上下了许多功夫。

3,性能

软件的性能是所有用户都会关心的一个关键属性。但是往往开发人员在实验室中写代码时,其测试数据的规模不足以对性能形成挑战。于是就需要开发人员有意识地识别系统真正使用场景下的规模,并在设计中预留足够的buffer。尝试对一个系统进行压测,看看随着请求规模的不断增长,系统的各项性能指标发生了怎样的变化?

4,可扩展性

任何一个软件系统都是在不断变化的,在软件设计之初,开发人员就需要考虑未来系统如何能够灵活地根据需求进行变化。架构的解耦,使用标准的通信协议和中间件,这些都有利于系统在未来方便地进行扩展。

5,可伸缩性

软件之所以利润高,就在于其开发完成之后,边际成本为0。当业务规模不断增大的时候,如果一个系统可以靠增加机器实现水平扩展是成本最低的选择。当然,业务有波峰也有波谷,当业务量减小的时候,系统是否可以释放出多余的资源以节省成本呢?

6,安全

今天在写这篇文章的过程中刚好看到了一个新闻:台积电在台湾的三个主要厂区均遭到了勒索病毒的入侵,导致全部生产线停摆。可以看到,对于现代的大型组织来说,信息安全就是生命线。一个服务在设计中如何考虑网络安全与终端安全,虽不能让企业赚更多的钱,却能够避免企业瞬间崩盘。

六,其他重要话题

因水平有限,还有很多话题无法展开详述。如果未来本人水平有所精进,再回来丰富此文。
1,了解行业领域的发展方向,竞品分析,清楚的知道用户痛点,了解技术衍化方向
2,驾驭分布式的复杂性(CAP的抉择、运维复杂性、测试复杂性、数据库锁机制解决并发问题),分布式系统的话题可以说很久
3,从商业的视角审视一个产品(成本与产出,可复制的能力)
4,重构的艺术(很重要的话题,不过太过宽泛又可以写一篇文章了)

互联网产业日新月异的今天,学习能力对于程序员与时俱进保持竞争力至关重要。学习一套新的软件系统并非易事,即使你掌握了很好的学习技巧,仍然需要投入大量的时间和精力才能有所收获。因此,在学习的道路上,除了学习技巧之外,耐心是最重要的一项能力。祝大家学有所成!

Written on August 5, 2018