前言这篇文章是表达我对系统软件的理解,文风与前几篇不同,整体比较“隐退”。我其实不太擅长“退却”,甚至有点排斥。比起看论文,我更喜欢看代码。当然,我也看论文,但比较少。毕业后一直从事数据库存储引擎领域的工作。近五年专注于阿里自研LSM-Tree存储引擎X-Engine的研发。近两年完成了X-Engine云原生架构的升级和商业化,在公有云上接受了一定规模的客户并稳定运行,应该是业界第一个实现云原生的TP存储引擎基于LSM-Tree架构的功能。完整体验了一个TP存储引擎架构规划、设计开发、落地、稳定运维的全周期,受益于我从入职以来所经历的高层团队、技术负责人和整个团队成员的优秀工程进入数据库字段。能力和技术眼光,再加上自己在这个过程中的一些思考,逐渐形成了自己的一些心得。此外,与业内一些优秀的架构师和工程师交流,发现在对系统工程的理解上有很多共鸣,也得到了很多非常有价值的投入。当然,也有一些不同的看法。这也是我写这篇文章的主要原因。希望把自己的一些理解表达清楚。这些观点算不上时尚,更谈不上创新,更多的是我自己的思考和体会。对系统软件的看法观点一:软件的本质是对硬件资源的消耗。不同软件的区别在于消耗硬件资源解决什么问题,如何分配硬件资源的消耗。“抽象”和“权衡”在软件架构设计中经常被提及。抽象的本质是“解决什么问题”,“权衡”其实是“如何分配硬件资源”。比如TP存储引擎和AP存储引擎在实现上可以列举出很多不同的地方,比如行存储VS列存储,二级索引VSZoneMap索引,强事务VS弱事务等等,这些区别其实就是结果,根本原因是:1)两者解决的问题不同。TP场景主要是在线实时业务。数据,历史数据可能很多),请求短而快,数据局部性明显,高并发低延迟等,AP场景整体数据规模大,计算密度高,吞吐量高等.(解决什么问题)2)TP引擎完整的事务支持,让业务的并发控制简化了很多。实际上,业务系统本来需要做的事情就是TP引擎做的,这当然意味着TP引擎需要消费一部分。硬件资源。AP引擎为了加快数据存储的速度,事务支持相对较弱,这部分工作还是由业务系统完成(比如ETL),所以没有必要为此消耗硬件资源.(如何分配硬件资源)观点二:系统软件的重大变化基本上是由硬件发展带动的。这对应于第1)点。在系统软件理论进入21世纪之前,学术界已经进行了广泛而深入的研究。从一开始的计算机出现,到大型机和小型机,再到家用PC和廉价的通用服务器,再到现在的云计算IAAS服务,基本上系统软件的发展也是沿着这条脉络发展的。系统软件的回潮,本质上是由IAAS的“新硬件”推动的。整个IAAS的按需获取,打破了以往很多系统软件在物理资源有限的情况下的设计,这也是云原生系统软件将迎来新机遇的原因。观点三:几乎没有完全领先于另一种架构的系统架构。这与观点1)和2)一致。不同的架构选择背后有不同的权衡。经常听到一些说法,你看这篇论文,这篇文章,他们的架构没有问题,但是我们的架构有这个问题。听到这些观点,我的第一反应是怀疑。主要有3个原因:1)很多论文和文章的实验结果无法复现,也就是说他的结论很可能是有问题的;2)很多时候只强调“得到”的部分,而没有提到“放弃”的部分。3)问题对我们系统的影响程度,能否解决,只能通过量化数据来判断。容易受到各种论文和文章结论的影响,很可能做出一个不伦不类的系统。就像武林修行者,百家门派习武,到头来很容易入迷。观点四:条条大路通罗马,系统最终出现的差异更多是因为项目的实施而不是架构。不同的系统架构所需要解决的问题,大部分在本质上是相同的,组成一个系统的组件也大同小异,只是根据构建系统的需要选择哪些组件。只有向游戏低头,真正面对问题、分析问题、解决问题,才能认清问题的本质,否则很容易成为空谈。例如:我经常被问到在LSM-Tree架构中连续写入数据时,compaction问题对性能影响很大。这就是我看待这个问题的方式。首先,LSM-Tree架构上writeswallowing优势的原因之一是,相比于innodb,磁盘B+Tree在写的时候直接sortonwrite(orderedinthepage,globalOrdered),LSM-Treearchitecture选择将sort的一部分转移到sortoncompaction和sortonread,本质上是将写入时排序的资源消耗转移到compaction或read上。刷脏实际上包括两个动作:生成脏页和刷脏页到磁盘。InnoDB相当于写的时候产生脏页,清理脏的时候就是一个简单的io操作。而compaction实际上同时产生了“脏页”和“脏页”。如果innodb继续写入,也会出现清理时间不及时的问题,影响写入性能。因为innodbdirtying和compaction成为问题,本质上是因为内存和磁盘写入速度的差异,导致生产者-消费者模型不平衡。因此,InnoDB的dirtying和LSM-Tree的compaction本质上是同一个问题,只是采用了不同的方法来将这个过程对系统的影响降到最低。系统软件建设的七个方面接下来的内容主要是我认为在进行详细设计时比较重要的原则。这些原理的原理其实很容易理解,“软件工程”这门学科研究得很好,但在实际操作中其实还是挺难的,可能是历史包袱,也可能是外部环境。根据实际情况做出不同的取舍。值得注意的是,我们做出的取舍一定要慎重考虑,不能仓促行事,否则很容易出现“一事无成”的情况。另外,按照这些原则设计和实现的系统和没有完全遵循这些原则设计和实现的系统,其结果实际上是“好和更好”之间的区别,但“好到什么程度”实际上是很难衡量的。系统制成。.这七项原则不是独立存在的,而是相辅相成的。场景化:首先要明确要解决的问题,这是整个系统建设的出发点。一刀切的制度过去不存在,将来也可能不存在。系统的完善必须通过不断的迭代来完成,所以怎么迭代本质上就是我们在那个阶段解决什么问题。一个系统可以有远大的目标去解决很多问题,但是所有问题的路线图都需要有一个比较清晰的规划,这样才能快速满足需求,同时为以后的演进和扩展保留基础。在实际的研发过程中,可能会出现两类错误:1)我想用敏捷开发的方式进行项目管理,以满足整个迭代的需要。敏捷开发本质上是先定义最小功能集,即先弄清楚要解决什么问题,然后快速迭代扩展功能,有点像小步走。在实践中,很容易把敏捷开发变成“快、粗、猛”,有点像30天的苦差事赶超美国。2)如果问题定义不明确,系统的“不变”设置容易马虎。每个系统都有一些“不变量”,然后很多设计都是基于这些不变量来开发的。比如在LSM-Tree系统中,一个常见的“不变量”是数据的更新版本在较低的Level,如果memtable、level0、level1中同时存在同一行数据的多个版本,那么memtable中对应的版本一定是最新的,level0中的版本也比level1中的版本新。如果在迭代过程中发现之前设定的“不变量”不合理,那么修改的成本会非常高。面向解耦:无论是自上而下的设计系统还是自下而上的设计系统,一个很重要的思维逻辑就是尽量减少模块之间的耦合。解耦一个更好的系统往往意味着:1)各个模块的功能考虑清楚,解决方案的完备性比较高;2)有利于更高效地专注于某个模块的执行,避免其他模块的影响;3)有利于后续迭代,影响面可控;4)出现问题容易排查,单个模块的问题更容易排查。真正棘手的问题,往往是模块间问题的传递造成的,比如模块A出现问题,经过模块B、C、D,最后暴露在模块E。有些持怀疑态度的人会说去耦设计可能会牺牲系统的整体性能。其实这和一开始就不要为了性能过度设计是一样的道理。如果一些解耦的设计影响了性能,那么就解耦应该耦合的部分。将两个模块耦合在一起的难度通常低于拆卸耦合在一起的两个模块的难度。面向防御:这是防御性编程的逻辑。假设调用的函数可能会出错,比如内存分配、io、基础库调用等。系统的行为是什么。有一个很简单的原则,就是“failstop”。如果没有完整的防御,即使失败也很难立即停止,最终会造成一些非常奇怪的现象。通常的疑惑是:1)可以看到这个函数的逻辑肯定不会失败。或许从目前来看,这个功能确实不会失败,但很难保证随着迭代增加逻辑,之后就没有失败的可能。2)加了这么多防御,防御代码比实际逻辑代码多,会影响性能。首先,目前CPU的分支预测能力在大多数情况下基本可以避免防御代码影响性能。另外,就像面向耦合的问题一样,如果一些防御性代码成为性能瓶颈,就应该进行优化。优化防御比解决没有防御引起的问题成本更低。面向测试:在测试阶段修复问题的成本远低于在生产环境修复问题的成本,因此让系统可测试非常重要。系统的可测试标准是能够方便地进行单元测试和集成测试,并覆盖大部分代码路径。一个可测试的系统,随着不断的迭代,会积累越来越多的测试用例,不断夯实稳定性的基础。面向测试、面向解耦、面向防御是相辅相成的。只有模块之间的耦合度足够低,才有可能做更多的测试,否则测试一个模块需要mock很多乱七八糟的东西。面向防御将使测试的行为更具可预测性。否则,如果输入异常参数,具体故障是不确定的,测试用例将难以编写。面向运维:bug是肯定存在的。对于复杂的系统,无论前期做了多少准备,都难以避免在生产环境中出现未知问题。面向运维的主要目的是在遇到问题时,用成本最低的手段及时止损。遇到线上问题,动态调整参数比重启成本更低,重启比要求版本发布成本更低。面向运维不仅仅是增加几个参数和开关那么简单,而是需要将“面向运维”作为设计方案的重要组成部分来考虑,确保问题有运维方法。敢用,用后见效。面对问题的本质:在解决问题的时候,一定要多思考问题的本质。简单的问题复杂化,复杂的问题简单化,是因为没有抓住本质。如果能想清楚背后的本质原因,从源头上避免是比较彻底的解决办法,否则很容易陷入不断打补丁的状态。我一直有一个观点:“如果不抓住问题的本质去解决问题,结果往往是制造问题”。另一个经验是,如果一个模块连续出现了好几个问题,就要考虑在最初的设计中是否还有改进的余地。面向可视化:可视化的主要目标是以更直观的形式展示系统的运行状态,这对于系统调优和诊断非常重要。当系统出现异常时,可视化的方法可以帮助快速定位系统中的问题。另一方面,可以为监控系统提供接口,跟踪历史状态。比如Oracle的诊断监控就是一个很好的案例,而SnowFlake的内部状态监控近乎疯狂。总结说了这么多,最终的系统是一行行代码实现的。以匠心、严谨、认真的态度去建立制度,是非常简单和正确的,但做起来却很难。让我们互相鼓励吧!
