17 October 2023

本文总结软件架构方面的经验。架构设计本质上由 哲学 驱动,它是生产知识的引擎。从 组织视角,可以寻找架构设计的过程和技能为什么如此构建。通用的 方法论原则,与哲学层面相联,为高质量的架构设计铺路。架构师需要针对不同 系统属性(System Properties)的技术武器库。本文对分布式存储各个领域的 参考架构(Reference Architecture)进行分类,从中总结 架构设计模式。将它们互联,绘制 设计空间(Design Space)的地图。

目录

分布式存储地图概览

软件架构-哲学视角

软件架构是对现实世界的建模,是一种语言,是人类的思维创造,以协助人类的思维。语言是一个有趣的话题。这三者结合在一起有很深的相互联系,指出了为什么、什么和如何处理软件架构。后续章节讲述软件架构的知识,但这一章讲述生产知识的引擎。

现实、语言和人类思维

首先,世界的建模是人类的语言。人类的语言已经进化了几千年,被独特的民间文化所丰富,被人口之间的日常互动所打磨,被全行业的使用和创造所检验。拿起一本字典,你就能了解世界和人类。

接下来,建模工具也是建模者本身的一个模型。即:人类语言也是人类思维的模型。思维是由语言来承载和组织的。语言的结构是以人类思维能够感知世界的方式进行的,而不是世界本身必须是怎样的。例如,软件的设计是通过高内聚低耦合,这也是语言中的词语是如何产生的原则。就像它们是为了减少软件的复杂性,它们这样做是因为人类是这样思考的。

我们可以说 人类的语言、思维和可感知的现实是同构的。对外部世界的探索与对内心深处的探索是一样的。失去一种文化,一种语言,与失去现实的一部分是一样的。作为一枚硬币的两面,人类的语言既是人类超越其它生物的最大祝福,也是人类思维能够感知可达的永恒牢笼。

关于软件架构

软件架构是一种语言,是对现实世界的建模,是帮助人类思维的一种人类思维创造。软件架构的本质诚实地反映外部世界[1],反省内在的思维,并在概念上照亮黑暗的缺失。答案已经在那里,嵌入在结构中,等待着被感知。

问题不是软件架构本身是什么,也不是要学习软件架构有什么,而是要理解世界和心灵的地形,在那里你看到需要”软件架构”来填补的洞。你 预测和设计 “软件架构”应该是什么,可以是什么,以及将是什么。可以有三千个平行世界,每个世界都有不同的软件架构书。我们选择我们的一个。

此外,知识和经验本身就是好的设计。它们本质上是一种领域语言,一种可重复使用的世界建模,因此也解释了为什么它们在日常工作中很有用,甚至可以替代设计技能。知识不是用来学习的,而是用来观察人类行为所检验的设计艺术的。

人类思维、语言、现实之间的映射

附:用例子解释

对于人类语言中的 “高内聚低耦合”,想象一下磁盘上的一个苹果。人们用”苹果”和”桌子”来命名,而不是”一半苹果+一半桌子”的东西。就像在面向对象(OO)的设计中选择什么东西来包装成一个对象一样,”苹果”和”桌子”的命名实践了”高内聚低耦合”。

再深入一点,”高内聚力”意味着”一起走”。底层的轴是时间,在这个过程中,苹果作为一个整体与自己一起走。苹果和桌子的边缘相交,但它们有不同的曲线,而且它们可以被解耦(如果你移动它们,它们就会被分开)。另一个 底层的轴是空间。人类用形状和颜色等基本元素来感知苹果和书桌。这些 感觉元素 在时间和空间的轴上生长,被加工成人类的语言。这些处理原则看起来像软件设计的原则,或者说,软件设计原则是为了适应人类的思维而设计的。

一个想象中的生物可以有一个完全不同的语言系统和思维方式,如果它们不像人类那样依赖视觉,甚至没有时间和空间轴。它们可能也不需要”高内聚低耦合”作为思维原则。例如,它们可以像有机器官进化成的那样处理信息。

对于 人类的语言也是牢笼,语言是对现实的建模。建模 意味着”不那么重要”的信息被放弃,以减轻人类认知的负担。它们真的不那么重要吗?词语是对不同时间发生的事件重复使用同一概念,这就避免了重复。但它们真的是重复的吗?语言的必要性本身就表明,人类的头脑无法处理”完整”的信息。依赖于语言,这种能力是残缺的、有限的、被禁锢的。

更多的是,人类的思维如果没有底层的 时间和空间 轴,就难以思考。人类的语言,在抽象塔的最底层,如果没有”面向人类器官”的 感知要素,就难以进行。人们经常需要日常聊天,以同步抽象概念的漂移(drift)。甚至语言本身也正在成为一个瓶颈,锁在人与机器、人群与人群的信息交流之间。

对于 “软件架构”与在世界和心灵地形中的洞,你都可以后文中看到更多。虽然大多数人把”软件架构”与技术联系在一起,但它也是由组织和流程的需求决定的。不同领域的各种”需求”流入”软件架构”的缝隙中,被精心加工,用适合人类思维的语言表达出来。它们一起演化为”软件架构”的内部含义。

对于 预测和设计”软件架构”应该是什么。它可以被解释为学习的方法。普通的方法是学习它是什么,它的结构,它的组成,涵盖知识点,然后练习使用。更好的方法是先了解背后的驱动因素、地形和动力。你可以看到它的来源和方向,甚至终结和边界。你也可以看到许多不同的替代方案,有可能发生,但最终没有被现实世界的行业所选择,由于未广为人知的某些原因。你应该能够定义你自己的方法,鉴于你本地的定制需求。你可以忘记这些知识,自己创造任何所需。

切割苹果和桌子

为什么需要软件架构

除了技术之外,软件架构还因许多方面而被需要。这些方面共同定义了软件架构应该是什么,以及由此而来的方法论和知识组成。

技术方面

  • 处理复杂性。软件设计被分为架构级、组件级和类级。每个层次都有自己的技术。4+1视图,设计模式,重构。任何问题都可以通过增加一个抽象层来解决。

  • 决定关键技术栈。互联网公司通常在不同领域的开源堆栈上构建服务,例如数据库、缓存、服务网格。使用哪种堆栈会影响架构,并且通常会根据技术目标和组织资源进行评估。

  • 缺陷的成本(Fault)。在早期设计中纠正缺陷的成本远低于全面实现时,特别是需要修改组件互连的架构级缺陷。

抓住重要方面

  • 非功能需求(Non-functional Requirements)。通常是,可用性、可扩展性、一致性、性能、安全性、COGS[2]。更重要的是,可能的最坏情况,如何降级,关键路径。还有,可测试性、可用性、质量、可扩展性、交付和部署。它们不是明确的客户功能需求,但通常更重要,并触及广泛的组件,影响代码实现。

  • 捕捉变化和不变。架构设计确定了哪些是快速变化的,哪些是可以稳定的。前者通常被本地化、用抽象封装,或者外包给插件。后者在设计时会问:”这个架构能否在1/3/5年内不进行大修”,其通常反映在组件和互连上。

  • 问题、策略和决策(Issue, Strategy, and Decision)。架构是捕捉系统、技术、组织中关键问题的地方。制定策略来应对这些问题,并记录设计决定的历史轨迹。

  • 澄清模糊问题。在此架构步骤中,通常客户的需求是不明确的,问题是复杂模糊的,未来是不确定的,系统范围是未知的。架构师角色分析、定义、设计解决方案/备选方案,并在团队中建立共识。

  • 抓住重要方面。架构师角色需要定义项目生命周期中,哪些系统属性必须严格控制。它们映射到 项目目标 和安全标准。更重要的是,架构师角色需要决定放弃什么,这并不容易,且需在各团队间达成共识。

过程与组织

  • 项目管理。此架构步骤通常是确定和评估成本、触及范围、交付工件、开发模型,以及资源、进度、质量的地方。它也是与客户紧密合作以明确需求的地方。项目管理通常与架构师角色一同工作。

  • 审阅和评估。此架构步骤通常是审阅关键设计,评估关键的效益、成本、风险,验证吞吐能力、容量,并确保所有的用户场景和系统场景得到满足。这通常涉及来自不同背景的利益相关者(Stakeholder),并与高层管理者协作。

  • 跨团队合作。架构涉及到各种外部系统和利益相关者,需要 打破障碍,建立共识,确保关键利益相关者得到支持和响应。它是推动合作的地方,与只涉及自身技术不同,推动合作可能是更大的挑战。

  • 轨道(Tracks and Lanes)。架构师角色通常建立框架,让团队成员在明确组件下迅速贡献代码。它设定了代码可以增长和不可以增长的轨道,作为团队内部协作的基础。轨道是未来路线的图景,也是日常工作的标准。

架构师角色

不同的架构组织风格

架构师角色的实际作用和意义在某种程度上是令人困惑的。个人经验,这是由于不同公司架构过程的组织方式不同。在一些公司,架构师是每个开发人员的下一晋升职位。而在其它一些公司,甚至没有明确的架构师职位。

架构师作为技术Leader

通常出现在互联网公司。架构师角色由团队中的资深人员担任,其熟悉技术栈和设计原则。架构师决定使用哪种技术栈,并为团队成员构建框架。架构师职位的需求量很大,因为互联网公司快速推出一个个应用,每个应用都需要它的架构师,而底层基础设施则相对稳定。商业价值和技术栈都具有牵引力。上层应用的API丰富,意味着有更多的产品和组件来承载新的架构师职位,而下层一般有更简单的API并重视垂直深度。

专门的架构部门

通常在电信公司看到。架构师与架构师一起工作,开发人员与开发人员一起工作,它们位于不同的部门。架构结果在部门间交接,遵循瀑布/CMMI过程。架构师在更稳定、甚至是标准化的需求上进行设计,有非常严格的验证,并提供完整的文档。其强调流程,并预期更多的跨部门会议来回进行。员工往往被分成决策层和执行层,后者预期更长的工作时间,有限的职业增长,以及提前被退休。

Peer-to-Peer的架构师风格

通常出现在构建专用技术的团队。不同于互联网公司在许多不同技术上快速构建应用,此团队垂直地专注单一技术,例如构建数据库、云存储、某基础设施组件。其对比类似2C与2B文化。没有专门的架构师职位,而是由所有人共享。任何人都可以开始一个设计提案(增量的、新组件、甚至新服务)。设计常经过几轮资深人员的审阅,这些人不是固定的,而根据相关和兴趣选择。任何人都可以为设计做出贡献,并且自由加入项目开发。其类似有机组织。技术是这里的关键牵引力,甚至值得为它发明新的架构(例如,新的NVM介质到新存储设计)。

系统分析员

通常见于销售ERP的公司,或者外包公司。这些系统在很大程度上涉及客户方的领域知识。而在销售给另一个不同领域的客户时,原先领域的知识变得无效。由于每次都有新的背景,全面的需求分析和架构过程被开发出来。领域知识可以被重用,领域专家受到重视,其中 知识和经验本身就是好的设计。领域知识可以比技术赢得更多的牵引力,其更倾向于稳定和成本管理。

借用和改进

通常出现在位于追随者地位的公司中。如果不是进入无人区,通常可以找到 参考架构(领先产品的架构)来借用,进行定制和改进。这也得益于多样的开源项目。此方法站在巨人的肩膀上,在软件架构过程中被广泛使用。例如,比较同行作品,其是”知识和经验本身就是好设计”的另一个例子。市场技术调研是抢手技能。

软件架构中的关键流程

如何成功推动软件架构流程?它涉及到与上下游的合作,确定范围(Scope),并打破屏障,建立共识(Consensus)。问题分析和实施部署以数据驱动的评估相联结,构成持续的反馈回路(Feedback Loop),推动未来的演进。

知识和技能(Knowledge and Skills)

作为准备工作,架构设计需要以下知识和技能

  • 在下游,了解你的客户。这里的客户也包括消费你的下游系统。了解客户,可以捕捉到架构中需要优先考虑的方面,更重要的是不应优先考虑的内容(例如,延迟而不是成本? 是否真的需要一致性和高可靠?)这也有助于识别风险(例如,节日突发用量,备份流量模式)。此外,良好定义的客户空间,可以揭示架构演进的未来方向。

  • 在上游,了解你的系统是建立在什么之上的。一个Web应用可以建立在一系列的服务器引擎、服务网、数据库、缓存、监控、分析等之上。掌握技术栈对于设计与实际场景相适应的架构,以及选择合适的技术栈以适应项目目标和团队能力。

  • 从外部来说,了解行业技术现状(State of art)。为设计一个好系统,需要了解项目在行业中的位置,可以借鉴参考架构,利用现有的技术和经验。例如,从丰富的开源数据库中选择和裁剪。参加交流会有助于了解行业状况,并确保你的设计不至于落入陷阱。

  • 在内部,了解现有的系统。其才能做出切实有效的设计,合理地确定哪些东西帮助大,哪些帮助小,以及优先级。从过去的设计历史、经验和陷阱中学习,以重用和选择正确的路径。

  • 在组织上,扩大你的范围(Scope)。架构设计涉及与多个外部系统和利益相关者(Stakeholder)的互动。一定要拓宽你的范围,熟悉它们,与更多的人进行沟通。这需要扎实的软技能(Soft Skills)来进行跨团队/业务部门的合作,打破屏障,建立共识。传达行动导向的要点,保持简明扼要,结合全局俯视与细致分析。

执行项目(Carry out the steps)

Peer-to-peer的架构师风格(见另文),GXSC的回答[3]中可以感受一二。在每个步骤中,与不同的人沟通,可大大地提高设计的稳健性。与其说是得到设计的结果,不如说问题分析(Problem Analysis)和权衡分析(Trade-off Analysis)更为重要。

  • 首先是 问题分析。设计方案从创新开始。找到要解决的正确问题是成功的一半。成本和效益应转化为 市场资金(反例:我们应该这样做,因为技术很了不起。好的例子:我们采用这种设计,因为它可以每年节省¥¥的成本)。问题的范围(Scope)应该是完整的,例如,不要漏掉升级和部署的场景、对周边系统的连锁反应、或在大规模部署中罕见但会发生的流量模式。风险 应该被识别,从内部的技术堆栈,从外部的多团队、市场和组织。管理的关键是平息风险,同理管理设计过程。

  • 问题分析的一个重要方面是 优先级。架构设计,即使最终系统,也不能解决每个问题。必须决定 什么要放弃,什么要压制,什么推到更低层设计中,什么选择现在做,什么要推迟,什么推到抽象中,什么依赖外部系统,什么推到未来的优化中。并决定什么是 关键属性(Critical Properties),其必须在整个项目周期中紧紧抓住,端到端地监控。也就是说,管理的另一个关键是确定 关键路径(Critical Path)。优先级通常是由组织目标、关键项目的收益和成本、以及跨团队协调的艺术来决定。

  • 接下来,寻找替代方案(Alternatives)。为了解决一个问题,至少应该制定两个方案。进行 权衡分析(Trade-off Analysis),以评估利弊。通常,利方有特殊情况,使其变得更坏,而弊方有补偿措施,使其变得不坏。讨论是在团队成员、上/下游团队、利益相关者之间进行的,这可能反过来发现新的替代方案。这个流程是 迭代 的,其中的工作量是非同小可的,是成倍增加,因为它不是在开发一个方案,而是一颗涟漪组成的树。最终你探索了 设计空间技术空间 的完整性,并在整个团队中达成共识。选择最终的备选方案可以用团队投票的方式进行,也可以用打分表[4]来比较。

  • 审阅(Review) 与更多的人一起进行。首先,找一两个身边的人进行 早期审阅(Early Review),以建立一个更坚实的提案。接下来,找一些资深的和有经验的人进行审阅,以确保没有遗漏任何场景,所有可以重用的选择都被重用,而且解决方案是使用最好的方法。然后,让关键的”上游”人员参与进来,以确保所需的功能、负载水平和隐藏的约束条件得到实际支持;并确保上游功能推出不会影响你的功能。让关键的”下游”人员参与进来,以确保新系统能满足它们的实际需求。尽早让关键的”利益相关者”参与进来是很重要的;确保你从组织中获得支持,你提供了可见性(Visibility),并且你与高层的优先级保持一致。

  • 然后对架构设计进行 评估(Evaluation)。确保问题分析,每个客户的 场景 和系统场景,以及项目目标,都得到很好的解决。确保 非功能 需求得到解决。确保关键项目的 效益和成本数据驱动的方法 进行验证。其以产线实际数字为输入,使用原型、模拟或数学公式来建模。确保系统能够支持所需的”负载水平”,将吞吐能力分解到每个组件。确保系统能处理 最坏情况,并支持优雅节流和降级。确保 逻辑具有完整性;例如,当处理一个Yes路径时,必须也处理No路径;例如,开始一个工作流,必须也处理它的结束、回滚、嵌套、循环。确保 开发和交付 得到应对,例如,如何支持多团队的开发、分支策略、组件开始/在线/维护/退休策略、CI/CD以及部署的安全性。另外,要确保 隐藏的假设 和约束条件被明确指出和解决。

  • 最后是 文档。实践中通常是一个简短的”单页”文档(One Pager,实际上可以是少于20页)、用于快速演示的胶片、和用于数据评估的电子表格。今日文化更倾向于轻量级文档,以代码库为中心事实,并优先考虑敏捷和点对点(Peer-to-Peer)的交流。问题分析和权衡分析通常在文档中比设计本身更重要,其中 定义问题空间 是一种关键能力。后者将复杂、混淆的问题,转化为可分解、执行、测量的组件。架构设计部分通常包括关键的数据结构、组件、状态机、工作流、接口、关键的场景、以及一些详细的问题讨论。重要的是,文档应该跟踪设计决策的变化历史,即它们如抵达今天,具体说是 问题、策略、设计决策(Issue, Strategy, Design Decision)链。

  • 架构设计的另一个产出是 接口。接口设计确实有一些原则(见后文)。它们是后续开发开始的轨道(Tracks and Lanes)。它们揭示了组件如何切割和交互。它们也将你的系统的期望(Expectation)传播给外部系统,比如它们应该如何协同工作,应该传递什么。

为演化而设计(Designed to Evolve)

架构的设计是为了 演化,并优先考虑更快地进化。Ele.me支付系统[5]是一个五年范围的例子。今天的软件能力取决于它的演进速度,而不是静态的功能集。

  • 简单就是美[6](Simple is Beauty)。最初的架构通常只解决关键需求。在几年的范围内,什么会改变,什么不会改变,需要被识别并通过抽象来应对。 MVP[7] 可用作首次部署,之后”飞车换轮子”成为挑战。

  • 高速公路是重要的。软件功能类似城市中的高楼,而高速公路和道路是快速兴建它们的关键。这些架构方面通常不够明显,没有被优先考虑,但却至关生存。在系统内部,它们可以是可调试性、日志、可见性和监控。它们定义了质量标准吗?当系统声称高可靠时,监控部分是否有更多的9?从基础设施来看,它们可以是工具、平台、配置系统、快速部署、数据获取和分析的便利性、脚本。在组织层面,它们可以是团队的流程,和促进敏捷的文化。对外,它们可以是生态系统和插件的可扩展性。例如,Chrome Apps[8]的插件被设计未首要功能,Minecraft[9]发布了建立第三方模组的工具,开源Envoy[10]从第一天起就为社区参与而设计。

  • 建立 反馈循环。项目部署后,应该能够收集数据并评估实际的收益和成本。其中可以发现新的差距(Gap),而又促进下一轮设计和改进。如何用 数据驱动 构建这样的反馈回路,应该在架构设计中加以考虑。

驱动项目(Driving the Project)

最后一点是关于 驱动项目。架构师的角色通常伴随着主人翁角色(Ownership),并对进度和最终结果负责。驱动不仅是架构的步骤,也伴随着项目的执行。从道延架构文章[11]中可以感受一二。

  • 可能会有时间计划的问题,新技术的 挑战(Challenges),新的阻碍因素(Blockers),更多必须的上下游沟通。以前的假设(Assumptions)可能不成立,形势可能会 改变,新的风险需要处理。可能有许多新人加入,许多需要协调,许多条目需要跟进。

  • 除了知识和沟通技巧,驱动项目还涉及 长时间的坚持、关注和关心。发现实际问题的能力、优先级和利用资源(Leverage)、推动的能力(Ability to push)、经验,以及 项目管理 的技能,都有价值。驱动也意味着要 激励(Motivate)团队成员的参与和创新。在更多人的帮助下,设计能变得更稳健、完整、改善,并获益于人们从 不同角度 看问题。

  • 更多的是,驱动是一种心态(Mindset)。你不是提出问题的人,人们向你提出问题,而你是问题可解决与否的最终关卡。最困难的问题自然会找上你。如果解决问题需要资源,你就制定计划,游说支持。你来确定优先级,你来定义,你来吃狗粮。团队跟着你一起成功(如果不是相反)。

架构设计反馈环

软件架构的关键方法

软件架构是个大话题,并未找到统一的结构。本文把它分为过程(上文)、方法论(本章)、原则、系统属性和设计模式、技术设计空间。全文依此组织。

  • 过程(Process)。已在上述各章中讲述。它涉及到现实世界的组织如何进行架构设计,以及概念上应该为架构过程做什么。

  • 方法论(Methodologies)。架构设计的分析方法、概念框架和通用结构。它们也与原则和哲学交织在一起。方法论随着文化趋势、组织风格和技术范式而改变。但经历多年,有价值的观点留存下来。

  • 原则(Principles)。架构层、组件层、类层都有许多原则,或共通或具体。本质上,它们为减少思维负担,让代码空间模仿人类思维。

  • 系统属性和设计模式(System Properties and Design Patterns)。分布式系统具有非功能属性,如扩展性(Scaleout)、一致性(Consistency)、HA(高可用)。不同架构设计模式被开发出来,它们是可复用的知识和领域语言。最佳实践可以从参考架构、其它市场参与者和历史系统中学习。其中 架构考古学 系统地调查其过往历史。

  • 技术设计空间(Technology Design Space)。一项技术如数据库,为适应各自工作场景,可以演变成不同架构,例如OLAP与OLTP、内存或磁盘。探索各种架构选择,将其绘制成地图,揭示设计空间。这幅全景图大大帮助新一轮架构设计的导航。

管理复杂性

架构设计的一大话题是 处理复杂性。其本质是 让代码空间模仿人类的思维,也就是人类语言的组织方式(如果你读过第一章节),以降低掌握成本。人类语言本身就是复杂世界的最佳模型,它是由人类历史打磨出来的”设计”,为每个人所共享。因此,领域知识(Domain Knowledge)是有帮助的,因为它就是语言本身。当代码空间与语言空间接近时(或者使用好的隐喻),自然节省思维负担。

下面是一些处理复杂性的概念性工具:

  • 抽象。任何问题都可以通过增加一个抽象层来解决。棘手的部分是你必须精确地捕捉,甚至预测,什么可以改变,什么不可以。这不是件容易的事。例如,多年来人们试图在Windows API和Linux API之间建立抽象接口,但今天我们所拥有的是”Write once glitch somewhere”。你仍然需要沿着抽象塔的底部检查。因为编码接口不能约束所有的 隐藏假设,以及非功能属性,例如吞吐量、延迟、兼容性。流程中的信息在沿着抽象塔传递后,可能会变得缺失和扭曲,从而导致不正确的实现。

  • 信息流。典型的设计捕获了代码对象如何在系统中流动。但相反,你应该捕捉到以人类语言描述的 信息 如何在系统中流动。语言信息在发送方和接收方组件中是 对称的,但实现和表示方式不同(例如,你在系统中传递 “苹果”,而不是DB记录、DAO、Bean对象等)。依赖性本质上是一种对称性,这里可能没有代码引用,但语义相连(例如,苹果的颜色是 “红色”,这就是你系统的各个地方必须正确处理的地方)。语言信息带有 目标,代码应该与之保持一致,也就是说,代码应该与 人类语言模型 保持一致。与系统中传递的代码对象相比,人类语言是一致的;当系统的不同层发生错位时,前者成为Bug的 来源。设计原则最终引向了”契约编程“,”不可能错误使用的类“(一个组件应该工作,对外部没有任何假设,无论调用者传入什么),语义分析;但仍有更多改善空间。

  • 高内聚低耦合。人类的概念,或者说语言中的词语,大多是按照”高内聚低耦合”的规则构建的。这就是 人类思维的运作方式,而遵循这个方式,代码设计就可以节省思维负担。这个话题与 变化和依赖 有关。高内聚封装了变化,从而使代码修改的影响本地化。变化沿着依赖关系传递,这就是为什么低耦合度可以减少不希望的传播。良好的封装和委托需要预测未来的变化,这通常是不容易的。与增加不必要的OO复杂性相反,它引向KISS[12]设计。

  • 名称和责任。软件设计中最困难的事情是给名字。这并不是说花哨的名字很难找,而是说,能给某样东西起名字意味着你已经以一种高内聚的方式对概念进行了分组(例如,你可以给 “苹果”、”桌子 “起名字,但不能给 “一半苹果+一半桌子 “起名字),这自然引向好的设计。接下来,名字定义了 一个事物是什么、不是什么、能做什么、不能做什么;这就是 责任。说对象应该以它们的名字来调用,就是说对象应该以接口和责任来调用。最后,当你能 用流畅的人类语言描述系统,即用 好的名称和信息流,你自然就做了好的设计。为了做得更好,你可以用一致的抽象层次来组织会话,而不是跳来跳去。如果是这样,就意味着设计的抽象层次也是一致的、自成一体的。总之,设计是对人类语言的建模(如果你读过第一章节)。

  • 复用。如果一个组件很容易被复用,它自然会遵循高内聚性和良好的命名和责任。推荐为复用而设计,但要避免引入额外的封装和委托,这将导致高的OO复杂性。推荐为复用而重构,但重构通常需要全局性的知识,这与变化应该是本地化的目标相矛盾。参考架构 是另一种减少思维复杂性的复用方式。找到顶级产品和开放源代码来学习。找到良好设计的流行框架。过去的经验在这里成为它的领域知识,由团队成员共享,变化也更容易预测。

  • 关注点的分离。分而治之是流行的方法。在最小的依赖联系的边界上进行解耦,使组件从各自的空间中 正交,使API从调用的时间轴上变得 独立。为了真正地分离关注点,自然需要一些方法论,如封装、知识隐藏、最小假设。理论上,任何复杂的东西都可以被分解成合适的小块,但要小心这中间扭曲的信息流,以及责任委托中缺失的漏洞。

  • 组件边界。将组件和子组件分开,可以减轻对人脑思维内存的占用。组件的边界应该在 一起变化的东西 处切开。如果一个上游的服务变化经常与一个下游的服务变化相耦合,它们应该被放入同一个组件。违反它是导致微服务搞乱系统的常见情形。高组织协作成本处是切割组件边界的另一个地方,见康威法则[13]。

设计的复杂性可以用依赖的分数来定义和评估。D Score[14]很有意思,而这篇衡量软件复杂性[15]的文章列出了更多的衡量方法。这些方法不太流行,可能是因为领域知识(Domain Knowledge)对处理复杂性更为有效。除了下面的例子,另一篇Domain-drive Design - Chapter 1[16]文章有一个很好的代码复杂性测量清单。

  • “D Score” 通过依赖关系的数量来衡量软件的复杂性。组件内部的依赖链接是增加内聚的,否则如果指向外部就会增加耦合性。两种类型的依赖链接用一个公式相加,作为最后的分数。

  • “Halstead Metrics” 把软件当作运算符和操作数。运算符和操作数的总数,唯一的数字,以及每个运算符的操作数,用一个公式求和,作为最后的分数。

  • “Cyclomatic Complexity” 将软件视为控制流图。边缘、节点和分支的数量,用一个公式加以总结,作为最后的分数。

代码复杂性测量

架构设计的层次

软件设计是复杂的。为了管理这种复杂性,我把它分成不同的 层次和视角。典型的层次有:架构层次、组件层次和类层次。抽象层次从高到低,范围从大到小,不确定性从模糊到清晰。每个层次却都有自己的方法论。各个层次也映射到第一和下一步骤,实践中常可以简化或混合,以更多考量现实世界的瓶颈。

  • 架构层(Architecture Level)专注于组件和它们的 互连(Interconnection)。互联关系是由端口(Port)和连接器(Connector)抽象出来的。一个组件或连接器可以隐藏巨大的复杂性,并将技术决策推迟到更低的层次。一个组件可以是一个元数据服务器,一个存储池,或者带有分布式缓存。连接器可以是一个具有节流QoS的队列,REST服务,或一个事件驱动的CQRS。系统的 范围(Scope)被检查,例如输入和输出流,如何与终端用户互动,以及上/下游系统。建立其上的基础设施和技术栈可以被调查和确定。非功能需求、用户 场景(Scenario)和系统场景在这一层次中被捕获和处理。典型的分析方法是 4+1视图[17]。当谈及 软件架构 时,更多的是指这个层次。这也是本文所要介绍的。

  • 组件层(Component Level)紧随架构层。它的重点是组件内部的设计。范围也应该被定义,例如接口、输入和输出、执行模型和所需资源。这个层次通常涉及到几十个类, 设计模式[18] 是流行的方法,组件应该被设计成 可复用的(Reusable)。架构可以建立在现有的系统上,其中 技术债务(Technical Debt)产生影响,例如用更好的设计重写所有的内容(高成本),或者通过插入新的代码来重用(高耦合性)。

  • 类层次(Class Level)接下来关注更精细的层次,即如何很好地实现一个或几个类。定义很清楚,可以进行编码。典型的方法论有 编码风格[19]代码重构[20]Code Complete[21]。你也可以听到关于防御性编程(Defensive Programming),基于合同的编程(Design by Contract)。 UML图[22] 在这个层次和组件层次都有巨大的作用,作为描述工具,更重要的是作为分析工具。例如,使用状态机图或表来确保所有可能的系统条件都被穷尽和处理。(类似的方法在PSP[23]中也有共享,它是CMMI[25]的一个子集(结合CMMI/PSP[24])。今天的世界更倾向于敏捷,而CMMI基本上把开发人员变成螺丝钉,有大量的文档工作和严格的监控统计)。

架构设计的视角

视角有助于从不同角度理解软件设计。下面所涉及的方法论作为设计输出的描述性工具,验证正确性的分析工具,思维流的启发式框架,以及进行架构设计的过程。

4+1视图[17] 是最流行的软件架构方法之一。它抓住了静态结构、运行时的并行性、物理部署和开发生命周期。

  • 逻辑视图(Logical View):组件和它们之间的相互联系。该图捕捉了系统的组成和功能,依赖关系和范围,以及责任分解。这是最常提到的”架构是什么”。

  • 进程视图(Process View):逻辑视图是静态的,而进程视图捕捉了运行时的情况。性能和可扩展性被考虑在内。例如,多节点并行和多线程是如何组织的,控制流和数据流如何在时间轴上协同工作。

  • 部署视图(Deployment View):例如,系统的哪一部分在数据中心、CDN、云和客户端设备上运行。应该如何管理部署和升级。要交付的二进制工件(Artifacts)是什么。

  • 实现视图(Implementation View):管理代码、仓库、模块、DLLs等。逻辑视图组件被映射为具体的代码对象,开发人员和项目经理可以随时开展工作。它还包括分支策略以及如何维护产品的不同版本。

  • 用例视图(Usecase View):最后也是最 重要 的视图。它捕获了所有的用户场景和系统场景,功能和非功能需求。它们中的每一个人都在前面的四个视图中推演,以验证问题真正得到解决。

架构4+1视图

UML图[22] 是通用的软件建模工具,但也可以从视图的角度理解。

  • 结构图(Structural Diagram) 捕捉系统的静态视图。从较高的抽象层次到较低的抽象层次,有组件图、类图、对象图等。

  • 行为图(Behavioral Diagram) 捕捉系统的运行时视图。例如,活动图表达逻辑工作流程,序列图表达时间轴,协作图表达多对象交互,以及大家常用的状态图。

  • 其他图表。用例图来捕捉用户场景和更精细的交互。还有部署视图来捕捉代码和工件是如何分配以进行开发部署。

领域驱动设计

领域驱动设计[26](Domain-Driven Design,DDD)从领域专家的角度看待系统。它适用于具有复杂业务逻辑和领域知识的系统,例如ERP、CRM(客户关系管理),或具有丰富业务的互联网公司。传统的OO设计很容易导致对象的蜘蛛网(”大泥球(Big Ball of Mud)”),与之相比,DDD引入了”领域”来解决这个问题。除了下面列出的关键概念,IDDD流感疫苗[27]也是一个很好的例子。

  • 领域(Domain)。一个复杂的大系统(企业级规模)被切割成多个领域(如用户账户系统、论坛系统、电子商务系统等),每个领域都有其特定的领域知识、语言措辞和领域专家。

  • 限界上下文(Bounded Context)。领域的边界被称为限界上下文。同样的概念对象有可能存在于两个不同的领域中,但它们映射到不同的类。例如,银行业务背景下的账户与图书销售中的账户是不同的。该对象 只能与来自同一限界上下文的对象交互。而且你不应该直接对getters/setters进行操作,而是使用”业务工作流调用”。(同样在OO设计中,对象应该与 同一抽象层的对象进行交互)。一个领域的对象不能(直接)走出它的限界上下文。边界上下文之间是相互正交的。

  • 上下文地图(Context Map)。但是,两个限界上下文是如何互动的呢?一个领域的对象通过 Context Map 映射到另一个领域。Context Map 可以是简单的”新建一个对象”和”分配属性”,也可以是复杂的REST服务。可以在中间插入 防破坏层(Anti-corruption Layer,ACL)以实现隔离。

  • 通过语言驱动DDD的设计[28]。领域知识是一种语言,而知识本身就是一种好的设计(如果你看到哲学部分)。然而语言有其模糊性,这就是为什么需要引入 上下文 来约束确定性。当你在同一抽象层次上组织会话时,语言是流畅的,这就解释了为什么对象只应该与来自同一约束上下文的对象进行交互。DDD是一种 将语言操作到设计中 的方法。它以 可重用的代码 表达领域知识,领域专家是(或接近于)代码开发者。

  • 公司战略视角。DDD能够对整个公司进行建模。高管需要从战略上决定什么是业务能力的核心,什么是支持它的,以及什么是共同的部分。这就引入了 核心域[29]支持域通用域。资源和优先度在它们之间是有偏向的。从长远来看,领域知识以及在运行代码中实现的DDD模型,将积累为有价值的 公司资产。DDD架构注重可持续使用的领域建模,其中好的设计 中立 于正在使用的技术架构。

IDDD流感疫苗实例

常见的架构风格(Architecture Styles)

这是个老话题,架构级别上的通用设计模式。新近技术带来了更多的范式,但其本质是可以被追溯的。在公司范围内,除了技术方面,架构最终可能会演变为反映组织的沟通结构(康威法则[13])。

  • 分层架构(Layered Architecture)。现在每个架构都不能完全摒弃它。

  • 仓库/黑板架构(Repository/Blackboard Architecture)。所有的组件都是围绕中央数据库(”仓库/黑板”)建立的。它们使用Pull/Push模型来传播更新。

  • 主程序和子程序(Main Program and Subroutines)。典型的C语言程序结构,面向过程(Procedure-oriented)编程,通常可以在简单的工具中看到。反之,则是面向对象的编程。

  • 数据流架构(Dataflow Architecture)。仍然是面向过程的编程,它通常可以用数据流图表示。该架构对数据处理很有用,特别是芯片/FPGA,和图像处理。管道和过滤器(Pipeline and Filters) 是另一种常一同使用的架构。

  • MVC(模型-视图-控制器)。构建用户界面的基本架构。它分离了数据、表示和业务逻辑。它在更丰富的客户端UI中得到更丰富的变体,例如React。

  • 客户端服务器。旧式的客户端设备连接到服务器的风格。现在通常用Web、REST API或SOA来代替。但这种架构在物联网中仍然有用,或者作为主机Agent向中央服务器报告状态/接收命令,或者作为功能丰富的客户端库来加速系统的交互。

  • 中介者(The Mediator)。假设N个组件连接到M个组件,而不是N*M的连接,在中间引入一个”调解器”组件,把它变成N+M的连接。

  • 事件溯源(Event Sourcing)。用户发送命令,每一个系统变化都由一个事件驱动。系统将事件链存储为Central Truth。实时状态可以从事件回放中得到,并通过Checkpoint加速。该系统自然地支持审计,并且是Append-only的和Immutable的。

  • 函数式编程。这更像是一个理想的方法论,而不是一个具体的架构。变量是不可变的,而系统状态是由一连串的函数调用来定义的。也就是说,它是由数学公式定义的,或者有点像事件溯源。因此,函数是编程中一等公民。

常见的架构风格

更多新的架构

下面是更多更新的架构风格。你可以看到架构师们在一些方面有所不同:如何切割边界,例如细粒度的层次,卸载到云端。自然结构,如分层、事件和流、业务逻辑、模型-视图用户界面。复杂度的偏重,例如,复杂的结构、性能、一致性、管理数据、安全和审计、松散的通信渠道。

  • 微服务。复杂的系统被分解成与REST APIs交互的微服务。典型的例子是 Kubernetes和Service Mesh。你却需要一个更复杂的容器基础设施来运行微服务:用于虚拟网络的SDN[39]控制器和代理,分配流量的HA负载均衡器,保护流量激增的断路器,管理REST端点的服务注册中心,管理锁和一致性元数据的Paxos quorum,提供磁盘卷和数据库服务的持久化存储,… 下图是Netflix微服务架构[40]的例子。

Netflix微服务架构

  • 流处理。上游和下游系统,在全公司范围内,通过消息队列,或低延迟的流处理平台连接。现在的企业正在从 Lambda架构(实时近似的流处理,和高延迟但精确的批处理,是分开的)转向 Kappa架构[41](将两者结合成流处理,支持一致性事务)。一个更复杂的系统可以由在线、近线、离线[42]部分组成,如下图所示。

Netflix在线/近线/离线架构

  • 云原生。系统被设计为完全运行在云计算基础设施上(但要成为混合云)。典型的例子是Snowflake[43]数据库。关键的设计是:1) 磁盘文件的持久性被卸载到 S3。2) 内存缓存、查询处理、存储是 分离的(Disaggregated),可以独立扩展,并对流量激增有弹性。3) 读取路径和写入路径可以分别扩展,典型的用户写入内容有稳定的吞吐量,而读取流量则会激增。4) 不同层级的资源,由于完全分解,可以准确地对客户实际使用的数量进行计费。”无服务器”(Serverless)是另一个话题,所有复杂的部分(heavy parts)如数据库和程序运行时(Programming Runtime)都转移到云端。程序员专注于编写函数来实现商业价值,对流量进行轻量化和弹性化。下面是Snowflake架构[44]的例子。

雪花架构

  • DDD洋葱结构[45]。洋葱(或称六边形,onion/hexagon)架构在DDD的背景下形成。领域模型是中心部分。向外下一层是应用程序。最外层是连接到外部系统的适配器。洋葱架构对正在使用的实际技术架构是中立的。领域模型还可以连接到测试用例,以轻松验证业务逻辑(而不是用数据库中的假数据、假REST接口等,不需准备测试平台的繁琐过程)。

DDD洋葱架构

  • React-Redux[46]。该架构是MVC的一个更进阶的版本。随着数据从服务器端Pull来,客户端的Javascripts自己运行MVC。视图是由模板+输入属性构建的。用户行为产生事件,触发行动,例如调用服务。新的更新被发送到Reducer,然后映射到存储。容器使用选择器从存储中获取状态,将其映射到属性,然后最终呈现新的视图。该架构也经常与Electron和NodeJS一起使用,以便用Web技术开发富客户端程序。

React-Redux架构

通用的架构原则(Principles)

大多数原则已经反映在之前章节中,下文从架构、组件、类层面展开。

架构层面

架构层面,提到最多的原则是以下三个(架构3原则[6])。

  • 保持简单(Keep it Simple)。复杂的东西已经够多了,简单的才是珍贵的。与KISS[12]有关。

  • 适合的(Suitable)。足够满足需求,比”工业领先”更好。一个架构应该是合适的,以你的具体需求和团队资源来引导它,而不是徒劳地追求新技术。要节俭。一个设计的好处应该被映射到财务成本中去评估。

  • 为演化而设计(Designed for Evolving)。业务需求在变化,流量规模在增加,团队成员可能来来去去,技术在更新。架构的设计应该是可演化的。架构过程(和开发)应该以Growth Mindset[47]来进行。一个例子是Ele.me支付系统[5],这在互联网公司中很常见。

组件层面

更多的原则来自于 组件层面 的设计。CoolShell设计原则[48]很好地列出了所有这些原则。下面是我认为最有用的东西:

  • 保持简单,(KISS,Keep it Simple Stupid),你不会需要它(YAGNI,You Ain’t Gonna Need It),不要重复自己(DRY,Don’t Repeat Yourself),最小知识原则,关注点分离(SoC,Separation of Concerns)。也就是说,让一切变得简单。如果你不能,那就分而治之。

  • 面向对象的 S.O.L.I.D。单一责任原则(SRP),开放/封闭原则(OCP),里斯科夫替代原则(LSP),接口隔离原则(ISP),依赖反转原则(DIP)。请注意,尽管OO原则试图隔离关注点并使变化成为局部的,但重构和维护系统处于良好状态的工作却依赖于全局知识。

  • 幂等(Idempotent)。不仅是API,系统操作在重放时也应该是幂等的,或可重入的。分布式系统通常会丢失信息并进行重试。无效的例子可以是做同步(Sync而不是Update,将一个命令同步到节点上,在节点故障恢复和重新执行后是一致的);以 最终一致性和单一方向 传播信息;重新执行没有副作用的行动;目标状态(Goal State)常用于部署和配置改变。

  • 正交性(Orthogonality)。组件的行为是完全相互隔离的。它们不承担任何来自对方的隐藏行为。它们可以工作,不管别人的输出是什么。不仅是代码路径,开发过程也可以是正交的,对组件进行明智的切割。正交性大大节省了思想负担、沟通成本和变更的涟漪影响。

  • 好莱坞原则(Hollywood Principle),不要给我们打电话,我们会给你打电话。组件并不”新建”组件。然而,是容器在管理组件的创建和初始化。这是控制的倒置,或者说是依赖性注入。例子有Spring DOI[49],AspectJ AOP[50]。依赖性应该朝着更稳定的方向发展。

  • Convention over Configuration(CoC)。适当地设置默认值,避免用户总是传入复杂配置。这个原则对设计开源库很有用,例如Rails。然而,大规模的生产服务可能需要对配置进行明确而严格的控制,并需要有动态变化的能力。微软SDP[51]就是一个例子。

  • 契约编程(DbC)(Design by Contract)。一个组件/类应该依照它的”命名”,即合同,而不是代码实现来工作。调用者应该通过它的”命名”来调用一个组件,而不是努力研究它的内部结构。这一原则要求对象在同一抽象层次上工作,并尊重责任划分。

  • 循环依赖原则(ADP)(Acyclic Dependencies Principle)。尽量不要在你的组件中创建循环依赖。理想情况下,是的。事实上,当多个子系统由一个消息队列联通时,循环依赖仍然在发生。本质上,组件需要”互”动,就像人一样。

类的层面

来到 类级 或更低的组件级,这些原则可以从 编码风格[19]代码重构[20]Code Complete[21] 中找到;本文不多涉及。 然而,评估一段代码是否是 好的设计 是很有趣的,人们经常争论了很久都没有达成一致。事实上,有几种不同的设计理念都适用,这可以从不同的开源代码库和编程语言设计中找到。为了结束争论,实用的原则是:

  • 比较团队和日常工作的 具体收益/成本,而不是设计理念。

  • 将比较建立在 具体的真实用例 上,而不是盲目地预测未来的设计扩展性。

关于OO设计与”简单直接”

继续上面关于评估一段代码是好设计的讨论。关键的问题应该是它是否 节省了整个团队的思维负担。通常有两种范式。OO(Object-oriented)设计和简单直接。它们以不同的方式工作。

  • OO设计 减少了 思维负担,因为好的OO设计隐喻(例如模式)是整个团队的 共享语言。如果它们实际上是 不共享的,这个原则就失效了,其该被验证。例如,一个人的自然建模可能不是另一个人的”自然”。一个人觉得自然的代码,可能成为另一个人的思维负担。一个顶级团队的人可以在她自然的OO设计中快速生成代码,但新的代码会成为其他人的思维负担,并拖累他们。这个条件 自我加强,使 “顶尖”的人成为更”顶尖”的人。 一致性 是好的,因为它扩展了共享。

  • OO设计 确实 增加了复杂性。它从一开始就引入了更多的组件。更多的交互变得 隐蔽和动态。一个改变需要重构更多的组件以保持高内聚低耦合。出于对性能的考虑,事情会变得更糟。解耦通常会损害性能,因而需要引入更多的组件来补偿,比如说缓存。更多的活动组件被触及,为了产线安全和正确性,也需要维护更大的范围。 过度设计 是随后的问题。OO设计本质上是通过 预测未来 来实现变化的可扩展性。然而,预测可能经常出错,而额外的代码却成为新的负担。

  • 简单直接。与常适用于应用级编程的OO设计相比,”简单直接”更多地被用于系统级和数据平面编程。编程语言所支持的 接口,是OO设计所依赖的核心,但常常不能捕捉到所有要传递的信息。例如,性能方面(Cache line、额外的调用、内存管理等)、处理最坏的情况、安全和保障问题、修改系统数据结构时的脆弱性和副作用(如果你在为操作系统编程),等等。

    • 封装。OO倾向于封装。但在性能方面,封装并不能很好地发挥作用。CPU、高速缓存、内存、磁盘的特性处于最低层。但它们仍然会传播到软件设计中的每一层。另一个反封装的例子是调试和故障排除。我们必须拆开每一层的细枝末节来定位错误,而封装则增加了更多的障碍。但OO对业务逻辑设计是有效的,因为业务逻辑是复杂的、不稳定的,而且性能上的牵引力(traction)较小。
  • 简单直接 范式中,人们经常 阅读工作流程中的所有代码,掌握每一个细节,以确保它能正常工作。人们还需要仔细阅读代码,以确保每个角落的情况都得到了处理,所有的场景都得到了覆盖,最坏的情况和优雅降级都得到了关注。 那么,少了代码就少了人们的负担,一切 简单直接透明 就好了。这样一来,思维负担 就减轻了。然而,OO设计 使它变得更难,因为要捕捉的微妙方面被隐藏在层层的封装中,并在动态绑定中链接,而且还有更多的代码和更多的组件要引入。

  • 正在开发的项目的 重力和牵引力(Gravity and Traction)是什么?具有丰富和不同逻辑的应用程序常愿意采用OO设计。而系统级和数据平面通常有更稳定的接口和功能集,但对性能和安全的牵引力更大。有时,只要能节省成本,开发者就愿意打破每一条OO规则。此外,封装阻碍了开发者对整个系统的性能和调用的 控制

  • 优先级。新的代码可以投入生产吗?低于目标性能,不能。生产安全问题,不能。坏的OO设计,可以。因此,设计应该首先考虑性能和安全,然后再考虑OO设计。然而,OO设计自然地将设计放在首位,而将性能、最坏情况的处理等目标 推迟 到未来的扩展中。除了优先级倒置之外,在接口已经在生产中运行之后,扩展可能会变得很难。

关于优化算法和稳健设计

一个类似于”OO设计与简单直接”的讨论是,是使用快速低开销的算法还是使用简单直接的解决方案。

  • 优化的算法 通常能充分利用独特的工作负载特性。换句话说,它承载了最多的 假设,并在计算过程中保留了最少的 信息。虽然减少了开销,但它倾向于过早地将代码 专业化(Specialized)(Premature Optimization[479])。因此在增加一个新的功能、稍微改变问题范围、或考虑更多的输入或输出时,容易变得难以进行。优化的算法更倾向于问题易变性不大的领域。

  • 稳健的设计 意味着要容忍快速不稳定的功能变化。简单直接的解决方案常常有效,因为它带有很少的假设,并且在层层变换中保留了 信息流。不使用技巧,你如何用普通的 人类语言 来描述解决方案,就如何用代码来实现。性能优化是留热点的,用诊断工具定位。

  • 折中(Trade-off)。优化的算法和稳健的设计有基本的冲突。作为一种平衡的折中,通常优化的算法进入分界明显、专业化的领域内,而稳健的设计会扩展到系统的大部分部分。尽管前者常因被称作”核心”而吸引人,但也意味着更小的工作范围、易被复用取代、失去多领域交叉的机会。

关于设计中的分析技能

提出一个合理的设计方案和正确的决策需要分析能力(Analytical Skills)。有些可以从Consultant相关[480]中学到。它们有助于剖析复杂的问题,并在看似无尽的争论中带领航向。

要解决的问题

找出要解决的正确问题,并对问题进行定义,这不是一件简单的事情。

  • 首先,要有基础的解决方案,与提议的解决方案进行比较。

  • 其次,用一种”数据驱动”的方式来衡量问题的 规模,例如,X%的数据受到Y%成本的影响。将问题的大小与解决它的努力的价值相匹配。

  • 第三,优缺点应该追溯到 基本的支柱,比如节省成本、减少开发工作、新的用户功能、用户SLA的改善,等等。多维度的权衡被转化为 市场资金 进行比较。它们是决策的标准。

  • 避免使用含糊不清的词语,如 “更好”、”更聪明”、”更有效”、”更容易”、”新技术”、”太慢”、”太复杂”,等等。

线性的思考方式

聪明人喜欢跳跃性思维,但决策需要线性思维。

  • 从一个简单直接的基础方案开始,然后一步步走向解决方案。关键是要找出 隐藏的跳跃。每一个跳跃都必须有理由,否则应消除。

  • 然后,问 1) 为什么选择移动这一步 (要解决的问题); 2) 增加了什么新的 假设 来支持移动; 3) 通过这一步,在 信息流 中失去或扭曲了什么。

  • 以一种 系统性 的方式,这些问题是为了识别所有缺失的路径。最终,他们组成了一个MECE[481]分析树,以确保潜在解决方案的全部范围(设计空间)被探索。最后,由数据驱动的方法选择最好的、必然的选项。

  • 这些问题也有助于识别潜在的折中。隐藏的假设和扭曲的信息流使添加新功能变得更加困难。它们也使引入bug变得更加容易。

技术设计空间-概述

软件架构具有共同的 系统属性(System Properties),例如 CAP[52]。为了实现它们,不同的技术(Technique)被发明,并演变成更通用的 架构设计模式(Architecture Design Patterns)。将各种驱动因素(Driving Factors)绘制成地图,它们揭示了 技术设计空间(Technology Design Space)的构造,我们在其中探索和航行以构建新系统。本文关注 分布式存储

可以学习的来源

文章、书籍和课程教授设计模式,并概述了设计空间的情况。

  • MartinFowler网站分布式系统的模式[53]。随着云计算的采用,像一致性核心[54]和复制的日志[55]这样的模式正在得到普及。除了这一条,服务注册中心、Sidecar、Circuit Breaker、Shared Nothing也是流行的模式。

  • Azure文档中的云设计模式[56]也总结了常见的云原生应用设计模式。它们得到了详细的解释,并填补了上面所遗漏的内容。

  • 设计数据密集型应用[57]一书展示了分布式系统的挑战、解决方案和技术。他们映射到设计模式并结合到设计空间。

  • 课程 CMU 15-721[58] 概述了数据库设计中的关键组件,例如MVCC、数据压缩、查询调度、连接。分解揭示了要探索的设计空间。所附的论文未来游既定的设计模式的深度。非常有价值。

  • On Designing and Deploying Internet-Scale Services[59]。这篇文章全面、深入,涵盖了构建互联网规模服务的最佳实践的各个方面。非常有价值。它让我想起了SteveY的评论[60]。

知名的开源和行业系统(industry systems)变成 参考架构(reference architectures),可以用来学习流行的技术或设计模式。它们是各领域的代表作,技术和优化的集成者。分解参考架构,重组技术零件,绘制设计空间。下文列出了我能很快记起的参考架构(可能 不完整 )。它们可以通过搜索流行产品、比较商业备选方案、或从高引用论文中找到。

慷慨地分享知识的论文中,相关作品部分,对于比较当前作品和揭示设计空间非常有用,比如:

  • TiDB[61]论文和Greenplum[62]论文的相关作品部分显示了互相竞争的市场产品如何基于之前的OLTP或OLAP支持HTAP(混合事务和分析处理)。它们还揭示了所采用的技术和优缺点。

优质的论文和调查(Survey)可以揭示技术空间,达到相当的广度和深度。

基于LSM的存储技术:A Survey

Scaling Replicated State Machines with Compartmentalization

An Empirical Evaluation of In-Memory Multi-Version Concurrency Control

  • 内存大数据管理和处理[67]调查了主流内存数据库和设计的方式,比较了它们的关键技术,这就形成了设计空间。

内存大数据管理和处理

构建和分析LSM压缩设计空间

Dostoevsky: Better Space-Time Trade-Offs for LSM-Tree

  • 数据库系统中的无锁同步[70]比较了常见的有锁/无锁技术,例如CAS、TATAS、xchgq、pthread、MCS,针对不同的并发级别。它揭示了在实现有效的B+树锁技术时的选择空间。

  • Optimal Column Layout for Hybrid Workloads[71]对CRUD、点/范围查询、随机/顺序读/写的成本函数按分区大小如何划分区块进行建模。它有助于找到最佳的块物理布局。

  • 主内存优化数据系统中的访问路径选择[72] 在不同的结果选择性和查询共享并发性下,使用全扫描与B+树的查询成本模型。该成本模型显示了查询优化器如何选择物理计划。

存储领域的参考架构

续前文,本节列出具体的参考架构和产品。

缓存(Cache)

  • Redis[73]是大多数互联网公司使用的主流开源内存缓存。与Memcached相比,它支持丰富的数据结构。为实现持久性(durability),它添加了检查点(checkpoint)和操作日志(per operation logging)。数据可以集群分片并主从复制。 Tendis[74] 进一步改进了冷热分层(Hot/cold tiering)和优化。

  • Kangaroo 缓存[75](来自 Facebook 关于 Scaling Memcached[76]、CacheLib[77] 和 RAMP-TAO 缓存一致性[78] 的长期工作)在内存中缓存热数据,将冷数据存入闪存(Flash)。大对象(object)和小对象分离,小对象使用追加日志(append-only logging)和组关联缓存(set-associate cache),以实现最佳的内存索引体积与写放大(write amplification)。Kangaroo 还使用“分区索引”(partitioned index)来进一步减少 KLog 的内存索引体积。

  • BCache[79] 是 Ceph[80] 中使用的一种流行的 SSD 块缓存(block cahe)。数据在“Extents”(类似文件系统)中分配,然后组织到更大的桶(bucket)中。压缩以Extent为单位。桶内顺序追加(sequentially append)数据直到填满,桶是GC回收的单位。数据由 B+ 树索引(与 Kangaroo 中使用哈希表的 KLog 不同)。B+ 树使用 256KB 的大节点。节点内部通过追加日志进行修改。 B+-tree 结构变更(structural change)由 COW (copy-on-write)完成,并可能递归地重写每个节点直到树根。由于 COW,日志不是必需的,而是用作对小更新(small updates)的批处理(batching)和顺序化(sequentialize)。

(分布式)文件系统(Distributed filesystems)

  • BtrFS[81] 用于单机 Linux 文件系统。它使用 B-树 索引inodes,使用 COW 进行更新,使用影子分页(shadow paging)确保原子性(atomicity)。其它同时代系统的有 XFS[82],它也通过 B-tree 建立索引,但通过覆写(overwrite)进行更新; EXT4[83],这是默认的 Linux 文件系统,目录 inode 用树索引文件 inode ,并使用WAL (write-ahead logging) 来确保更新(覆写)原子性。

  • CephFS[84] 引入 MDS 来管理文件系统元数据,例如目录、inode、缓存;而持久化由对象存储支持,例如数据池和元数据池。 CephFS 的特色有 动态子树分区[85]和 Mantle负载平衡[86]。跨分区事务由 MDS 日志[87] 实现,MDS 在更新前获取 [88]。

  • HopsFS[89] 在 HDFS 上构建分布式文件系统。 Namenode 变成一个 Quorum,无状态,元数据存储由另一个内存 NewSQL 数据库管理(Offload)。 Inode 被组织成实体关系表(entity-relation table),并进行分区(partition)且减少操作所涉及的服务器数量。跨分区事务(transaction),例如 rename、rmdir ,由 NewSQL 数据库和 Hierarchical locking 实现。子树操作经过优化以并行运行。

  • HDFS[90] 是大数据的分布式文件系统。它放宽了 POSIX 协议的限制,倾向大文件,并运行主/从 Namenode 来序列化事务。 HDFS 最初是 Google Filesystem[91] 的开源实现(Google以 Big Table[92]、Chubby[93] 开启了云时代)。 HDFS 之后取得了巨大的成功,成为了大数据的共享协议,跨越文件系统、数据库(例如HBase[94])、SQL(例如Hive[95])、流处理(例如Spark[96])、数据湖(例如Hudi[97]),包括开源和商业(例如 Isilon[98])产品。

对象/块存储(Object/block storage)

  • Ceph[99] 用于分布式块存储(block storage)和对象存储(object storage),以及CephFS用于分布式文件系统。 Ceph 曾使开源的横向扩展(Scaleout)的存储成为可能,并在 OpenStack 生态系统中占据主导地位(Ubuntu Openstack 存储调查[100])。它在 CRUSH map 中通过基于哈希的放置(Placement)算法来减小元数据体积。它在一个系统中实现对象/块/文件服务 (converged)。元数据由一个 Paxos quorum(Consistent Core)管理,以实现所有 CAP 属性。 Ceph 条带化(striping)对象并就地更新(update in-place),后者需要单节点事务。 Ceph 后来构建了 BlueStore[101],它是定制化(customized)的文件系统(Ceph 10 年课程[102]),针对 SSD 进行了优化,并解决了 双写问题[101]。双写(double-write)问题通过分离元数据(委托给 RocksDB),分离键/值数据(如 Wisckey[103])来解决;大块的写(big writes)变为仅追加,小的覆写(small overwrites)合并到 WAL。

  • Azure 存储[104] 用于行业级公有云存储基础设施。它建立在 Stream 层上,这是一个分布式的追加文件系统; 并使用 Table 层,它实现了横向扩展的大表格,以支持 VM (virtual machine,虚拟机)磁盘、对象存储、消息队列。“追加”(Append-only)简化了更新操作,但在垃圾回收 (GC) 方面有更多挑战。 AWS S3[105] 似乎反而遵循 Dynamo,就地更新(猜的),使用一致性哈希来数据分片。对于融合(converging)对象/块/文件存储,Nutanix[106] 类似地,在同一个节点上运行存储和 VM,这与远程连接(remotely attached)的 SAN/NAS 不同。

  • Tectonic[107] 与 Azure 存储类似。它哈希分区元数据以进行横向扩展,采用了 Copyset Placement[108],整合了 Facebook Haystack/F4(对象存储)和数据仓库(data warehouse),并引入了多租户(multitenancy)和资源限流(resource throttling)。 Tectonic 的另一个特性是解耦(decouple)常见的后台任务(background jobs),例如数据修复、迁移(migration)、GC、节点健康检查;它们从元数据节点搬出,变成了后台服务。 TiDB[109] 也有类似的思路,如果将 Placement Driver 移出元数据服务器。

  • XtremIO[110] 使用新颖的基于内容的寻址(content-based addressing)来构建全闪存的块存储阵列。数据放置由其内容的哈希决定,自带支持去重。尽管访问是随机的,但它们在闪存上运行。写入保存到内存中的两个副本后被确认(ack)。其它同期产品包括 SolidFire[111],它也是横向扩展的; 和 Pure Storage[112],它纵向扩展(scale-up),并使用双控制器共享磁盘。

数据去重(Data deduplication)

  • Data Domain[113] 是最有名的数据去重存储设备(appliance)之一。它通过 rolling hash[114] 可变长度分块,来识别文件中间的插入。 Locality Preserved Caching 使指纹缓存变得高效,它与备份(backup)工作负载(workload)完美配合。

  • Ceph dedup[115] 在 Ceph 上构建可横向扩展的去重引擎。 去重后的块存储在 Ceph 中,以哈希指纹为键。它引入了一个新的元数据池,来查找对象 ID 到块的映射。去重操作离线(offline)进行,并受到限流。类似的二级间指(indirection)模式,也可用于将小文件合并为大块。

归档存储(archival storage)

  • Pelican[116] 是 rack-scale 的归档存储(或称为冷存储(cold storage),近线存储(near-line storage)),它与硬件协同设计(co-design),以减少磁盘/CPU/冷却功率,仅8%的磁盘处于旋转状态。数据经过擦除编码(erasure code)并跨磁盘组条带化。 Flamingo[117] 继 Pelican 的研究,根据 Pelican 环境配置,生成最佳数据布局和 IO 调度配置。归档存储因政府合规(compliance)需求而广泛采用,例如 AWS Glacier[118] 。

  • Pergamum[119] 协同设计硬件,作为一个存储设备(appliance),始终保持 95% 的磁盘断电。每个节点都添加了 NVRAM,用于保存签名和元数据,以允许在不唤醒磁盘的情况下进行验证。数据在磁盘内和磁盘间进行纠删编码。 磁带库[120] (tape library)由于容量成本、可靠性和吞吐量的改进,仍然是有吸引力的归档存储介质。

OLTP/OLAP 数据库

  • CockroachDB[121] 是支持 Serializable ACID 的跨区域(cross-region) SQL 数据库,可看作 Google Spanner[122] 的开源实现。它通过 Hybrid-Logical Clock[123] (HLC) 避免对 TrueTime 的依赖。 CockroachDB 将 SQL schema 映射到键值对 (key-value pair,KV) 并 存储在 RocksDB 中[124]。它使用 Raft[125] 来复制分区数据。它构建了新颖的 Write Pipelining[126] 和 Parallel Commit[127] 来加速事务执行。另一个同期产品是 YugabyteDB[128],它在查询层重用 PostgreSQL,用 DocDB 代替 RocksDB,它和 CockroachDB 有过一场有趣的辩论(YugabyteDB挑战CockroachDB[129],知乎YugabyteDB/CockroachDB辩论[130],CockroachDB 反驳 YugabyteDB[131])。

  • TiDB[61] 与 CockroachDB 类似。它倾向于单个地理区域,并使用时间戳 Oracle 服务器来序列化事务,基于 Percolator[132] 实现事务。 TiDB 进一步结合了 OLTP/OLAP(即 HTAP),通过 Raft 从基线(baseline)行格式(row format)数据复制到一个额外的列式(columnar)副本(TiFlash[133])。同期产品中(Greenplum’s related works[134]),为了同时支持 OLTP/OLAP,除 HyPer/MemSQL/Greenplum 外,Oracle Exadata(OLTP)通过引入 NVMe flash 和 RDMA,并增加内存列式缓存来提升OLAP性能; AWS Aurora (OLTP) 将 OLAP 卸载(offload)到云端并行处理; F1 Lightning[135] 从 OLTP 数据库(Spanner,F1 DB)复制数据并将它们转换为 OLAP 所需的列格式,支持快照一致性(snapshot consistency)。

  • OceanBase[136] 是一个分布式 SQL 数据库,与 MySQL 兼容,支持 OLTP/OLAP 和 混合行-列数据布局[137]。它使用中央控制器(Paxos 复制)来序列化分布式事务。同期的 X-Engine[138] 是一个与 MySQL 兼容的 LSM-tree 存储引擎,被 PolarDB[139] 使用。 X-Engine 使用 FPGA 进行 compaction。读/写路径分离以应对流量激增。 X-Engine 还引入了多阶段流水线(multi-staged pipeline),任务被分解为小块,异步执行,并流水线化,类似于 SeaStar[140]。 PolarDB 的另一个特色是将查询下推(pushdown)到 Smart SSD(Smart SSD 论文[141]),它在磁盘盒内完成计算以减少过滤后的输出。后者 PolarDB Serverless[142] 转向了像 Snowflake 这样的 disaggregated 云原生架构。

  • AnalyticDB[143] 是阿里巴巴的 OLAP 数据库。它将数据存储在共享的 Pangu[144] (HDFS++) 上,并通过 Fuxi[145] (YARN[146]++) 调度任务。数据以混合行列数据布局(row groups)进行组织。写节点和读节点分离,独立扩展。更新首先作为增量追加,然后在写路径外合并,并在所有列上构建索引。基线+增量的思路类似于 Lambda 架构[147]。

  • ClickHouse[148] 是最近迅速流行起来的 OLAP 数据库,因“非常快”知名(为什么 ClickHouse 很快[477])。除了常见的列格式、矢量化查询执行(vectorized query execution)、数据压缩之外,ClickHouse 还通过“关注底层细节”进一步优化。 ClickHouse 支持各种索引(以及全扫描(full scan))。它通过 MergeTree[149](类似于 LSM-tree)吸收更新。应用于 OLAP 场景,ClickHouse 不支持(完整的)事务。

  • AWS Redshift[150] 是基于 PostgreSQL 的新一代云原生数据仓库。数据保存在 AWS S3,同时缓存在本地 SSD(类似于 Snowflake)。查询处理节点由 AWS Nitro[151] ASIC 加速。它配备了现代数据库功能,例如代码生成(code generation)和矢量化 SIMD 扫描、external compilation cache、AZ64 编码、Serial Safe Net[152] (SSN) 事务 MVCC、机器学习支持的 Auto tuning、半结构化查询(semi-structured query)。它可与 Datalake 和 OLTP 系统进行联邦查询(federated query)。

  • Log is databse 1[153] / 日志是数据库 2[154] / 日志是数据库 3[155]。该概念首次出现在 AWS Aurora Multi-master[156] 上。日志被作为最终数据(single source of truth)来复制,而不是同步磁盘页面(disk page)。页面服务器被视为重放日志的缓存。同类中,CORFU[157]、Delos[158] 将分布式共享日志构建为服务。 Helios Indexing[159]、FoundationDB[160]、HyderDB[161] 在共享日志上构建数据库。

内存数据库(In-memory database)

  • HyPer[162] 内存数据库发表了不少优秀论文。它领先于 矢量化查询执行[163]和代码生成,其中 LLVM[164] 通常用于编译 IR(intermediate representation)。其它特色有 Morsel-driven execution scheduling[165],用“fork()”从 OLTP 创建 OLAP 快照,等等。其它同期产品包括 SAP HANA[166],它结合了 OLTP/OLAP(利用增量结构)并支持丰富的分析查询; MemSQL[167],通过添加行/列格式来支持 OLTP/OLAP; 而GreenPlum[134]将PostgreSQL扩展为MPP,增加了GemFire(12306.cn使用GemFire[168])进行内存处理,在OLAP之后增加了OLTP,并进行性能提升和资源隔离。

  • Hekaton[169] 是 Microsoft SQL Server 的内存数据库引擎。它的无锁 Bw-Tree[170] 具有特色,通过追加增量和合并(merge)来工作。 Bw-tree 需要 Page Mapping Table[171](LLAMA[172])用于原子页面更新,并避免将页面 ID 变更传播到父节点。 Bw-tree 的 SSD 组件也可以是仅追加的(append-only),在 DocumentDB[173] 中有 “Blind Incremental Update” 。 Hekaton 也有 Project Siberia[174] 来对冷热数据进行分层(tiering),它使用自适应过滤器(adaptive filters)来判断数据是否处于冷磁盘上;冷热分类(classification)是 离线[175] 地对记录访问日志的采样进行的。

  • ART 树[176] 是内存数据库(以及 PMEM)的流行索引之一(例如 HyPer)。它本质上是自适应节点大小的基数树(radix tree)。其它同期索引包括 Masstree[177],它是由 B+树组成的的 trie树,以及各种优化的集大成者; Bw树[171]; 和 Be-tree[178],它使用每个节点的缓冲区来吸收随机更新,在 VMWare 文件复制[179] 中采用。查询过滤方面,除常用的 BloomFilter[180]外,SuRF[181] 也支持范围查询,但更新代价较高。

  • FaRM[182] 在 RDMA 和 UPS 保护的 PMEM 上构建可横向扩展的内存数据库,支持快速的 Serializable 的事务。它通过减少消息数量、One-sided RDMA 读/写,以及并行性,来克服 CPU 瓶颈。数据是分片的。分布式事务用2PC实现; 锁持久化在每个分区的主节点的日志中; 读是无锁的; 协调者(coordinator)没有持久状态。 Zookeeper 用于维护节点成员资格(node membership)。对象通过键(指针地址)来访问。后续工作 A1[183] 在 FaRM 之上构建图数据库,并使用 DCQCN[184] 缓解 RDMA 拥塞。

  • Silo[185] 利用基于Epoch的组提交(group commit)来构建 OCC Serializable 事务协议,由 Masstree 索引。 Manycore[186](40+ CPU 内核)显着改变了 HPC、内存、PMEM 系统中的并发设计; 例如 Linux 内核 manycore[187],文件系统 manycore[188]。除了自定义latching和fencing,经常使用的技术如 Epoch-based Reclamation[189](例如在 Masstree 中)、Sloppy Counter[187]、Flat Combining[190]、Shared Nothing。 Epoch-based Reclamation 将频繁的内存操作分组为更大的不频繁的 Epoch; 线程在本地内存上工作,除了 GC 在 Epoch不活动后访问所有线程内存。 RCU[70] 类似,在所有事务超过低水位(low-watermark)的Epoch后,旧的 DB 记录版本可以被回收。 Sloppy Counter 将引用计数拆分为全局计数器和各个核的计数器,大多数操作发生在线程本地。在 Flat Combining 中,工作线程将请求发布到线程本地内存(thread-local),然后竞争全局 CAS(compare-and-set),最后的唯一赢家批量执行所有线程的请求。 Shared Nothing 是高并发的银弹,只要系统可以这样设计(例如 NetApp Waffinity[191])。

NoSQL 数据库

  • RocksDB[192] 是基于 LSM-tree 的单节点键值存储的主流实现。它被用作 许多系统的 KV 后端[193],例如 MySQL MyRocks[194]、CockroachDB on RocksDB[124]、TiDB on RocksDB[195]、BlueStore RocksDB[196]。它也经常在互联网公司中使用(RocksDB FAQ[197])。RocksDB的特色有 Universal Compaction[198]、SSD 优化、和 Remote Compaction[199](将 compaction 卸载到基于云的共享存储上)。在分层(tiering)方案中,PebblesDB[200] 在每个 LSM-tree 层级中插入更多的 SST “Guards”[201],其工作方式类似于用跳表(skip list)来约束和索引 SST 文件的键范围,从而减少读放大(read amplification)。

  • FoundationDB[160] 实现分布式 KV 存储并支持 ACID 事务。事务实现由共享日志系统支持。控制面(control plane)、事务、共享日志(shared logging)、存储系统是解耦的。 FoundationDB 还利用共享日志构建快速恢复(fast recovery)能力。此外,FoundationDB 还基于 Flow 构建确定性模拟测试(Deterministic Simulation Testing)。

  • MongoDB[202] 是流行的 JSON 文档数据库,是最成功的开源数据库之一,并进入了 IPO[203]。 MongoDB 的流行有赖于易用性。 水平扩展由分片(范围/哈希)实现,HA(高可用性,High availability)由副本集(replica set,1 个写 + N 个读副本)实现。

  • HBase[204]是 Big Table[92] 的一个开源实现。表基于范围分区(range partitioning),元数据由 ZooKeeper[205](Chubby 的开源实现,或曰 Paxos + 复制状态机[206] + 命名空间索引(namespace indexing))管理。分区服务器使用 LSM-tree 来管理更新,共有 MemTable、HFile、Compaction 等概念。 HBase 支持可变列(variable column schema),按时间戳版本检索,和行级原子操作,可以使用 Percolator[207] 构建跨分区事务。 HBase 成为 HDFS 上的主流大表格数据库,并常用作 SQL、时间序列数据库、块存储等的后端。 ByteDance 将 Big Table 和 Spanner 定制化实现为 ByteSQL[208] 和 Bytable[209]。阿里巴巴定制化 HBase 并发布了 Lindorm[210]。

  • Cassandra[211] 遵循 Dynamo[212] 的点对点 (P2P) 集群管理,而 DynamoDB[213](论文[214])由 AWS 商业运营,也遵循 Dynamo。它没有专用的元数据 Quorum,但在对等节点中携带元数据并使用 Gossip[215] 协议传播。它支持需要主键的大表格。键由 Consistent Hashing[216] 进行分区和放置(placement),以避免节点加入/离开时的额外数据迁移。 Cassandra 采用 quorum write/read(写入 N 个副本,读取 N/2+1 个副本)来确保持久性和版本一致性。类似的 P2P 集群管理可以在 Service Fabric[217] 中找到,它托管微服务(microservice),为节点成员资格(node membership)的一致性做了很多工作。

  • [ElasticSearch][478] 起源于基于 Apache Lucene 的全文搜索引擎,非常受欢迎,然后演变为可横向扩展的,支持丰富搜索功能的数据库,可存储 JSON 文档、日志记录、时间序列、地理空间数据[218]。 ElasticSearch scaleout[219] 通过主从复制和哈希分片来管理数据。曾经 ElasticSearch 也因 ELK stack[220] 所熟知。

  • InfluxDB[221] 是一个流行的时间序列数据库。与 SQL 数据库相比,时间序列数据库可以利用固定的数据组织和查询模式,可以聚合(aggregate)指标维度(metrics dimensions)以应对高速数据流入,并重新采样以冷热分层(tiering)数据。另一个产品是 OpenTSDB[222],它基于 HBase 提供时间序列服务。时间序列数据库经常用于监控(例如 Prometheus[223])和物联网(例如 InfluxDB IoT[224])。

图数据库(Graph database)

  • Graphene[225] 代表图数据库的典型实现,基于半外部内存(semi-external memory, 即 DRAM + 磁盘)。为加速查询,它协同放置(co-locating)常常一起访问的边(edges)和顶点(vertices),管理小对象和细粒度 IO 。更早的工作可以追溯到 GraphLab[226]。其它同时代产品的还有 Neo4J[227],源于在数据库中保存对象图(Object-oriented); ArangoDB[228],支持 JSON文档图[229] 和多模(multi-mode); OrientDB[230] 也是一个 多模型数据库[231]。图数据库经常用于社交网络挖掘(social network mining)和迭代机器学习(iterative machine learning)。

  • Facebook TAO[78] 为社交图谱(social graph)的 OLTP 实现了精简的两级架构。持久性/容量层由 MySQL[232] 提供,它使用 RocksDB[194] 作为引擎。 QPS/缓存层由 Memcached 提供,有很多改进(例如 CacheLib[77])。为了一致性,TAO 支持跨分片的2PC,并防止 fracture read(不是 ACID,不是快照隔离(snapshot isolation))。查询为获取关联数据而优化。

  • FaRM A1[233]。 Bing 用于知识图谱(knowledge graph)的通用图数据库,全部数据在内存中。顶点/边被组织在链接结构对象中,通过指针地址访问,并通过 FaRM 实现乐观并发控制 (OCC) 事务和 MVCC(multi-version concurrency control)读取。其它同期产品包括 AWS Neptune[234]; CosmosDB[235],它从 DocumentDB[173] 发展而来,是一个全球分布式(globally distributed)的强一致性的多模型数据库,使用 Bw-tree with “Blind Incremental Update” 而不是 LSM-tree 来吸收更新。

  • ByteGraph[236] 在 RocksDB (TerarkDB[237]) 上构建图数据库,支持具有广泛兼容性的 Gremlin API[238]。加权的一致性哈希环将顶点和相邻边分片到同一个节点。 RocksDB 用 KV 轻松表示顶点和边,支持内存/磁盘分层和单节点事务。 Large edge list 由 edge-tree(B-tree)实现,并进一步支持二级索引。 ByteGraph 还支持地理复制(geo replication,最终一致性),分布式事务 (2PC) ,和基于成本的查询优化器(cost-based query optimizer)。

数据湖(Datalake)

  • Apache Hudi[239] 在 HDFS、Kafka、Spark、Hive 上构建数据湖。与数据仓库相比,它允许通过 CopyOnWrite 或 MergeOnRead 更新数据。其它 datalake contemporaries[240] 包括 Delta Lake[241],它为 Spark 带来了 ACID; Apache Iceberg[242],它支持高性能的查询。 Datalakes 一般强调跨系统的互操作性(interoperability)。结合数据湖和数据仓库,你会得到 Lakehouse[243] 模式。

  • F1 Query[244] 连接多个数据源,如 Spanner、BigTable、CSV、ColumnIO、Capacitor、ETL,以创建联邦查询引擎。之前的 F1[245] 建立在 Spanner 上并服务于 Google AdWords。 F1 Query 支持包括 join 的交互式 SQL 查询(interactive SQL queries),批量查询,通过 UDF 服务器支持自定义的 UDF。查询作为 DAG 并行执行,其中 “dynamic range repartitioning” 减轻了数据倾斜(data skew)带来的影响。 F1 Query 在查询优化器中使用启发式规则(heuristic rules)。此外,F1 Lightning[135] 通过复制额外的列式副本来增加对 HTAP 的支持,并通过跟踪时间戳水印(timestamp watermarks)来提供快照一致性(snapshot consistency)。

流处理(Stream processing)

持久内存(Persistent memory)

  • NOVA[254] 在持久内存 (PMEM) 上构建高并发的文件系统,有很多可借鉴的设计模式。 NOVA 通过 DRAM 基数树(radix tree)建立索引,并通过每个 inode 的日志、每个CPU核的 free-list 来提高并发性。 Nova 在(定制的)DAX-mmap 上使用日志、COW、clwb 指令构建原子文件操作。 ART 和哈希表[255] 也是 PMEM 存储的常用索引。

  • Level Hashing[256]。尽管 NOVA 使用基于树的 PMEM 数据结构(文件系统 inode 树),但另一种方法尝试在 PMEM 上使用哈希表数据结构。后者有利于 O(1) 查找。 Level Hashing 不使用日志。调整大小是通过两级旋转完成的。崩溃一致性(crash consistency)通过小心操作标志位来保证。但是,基于哈希的 PMEM 数据结构不支持范围查询(range query)。

  • Orion[257] 直接通过 RDMA 向客户端开放内存访问,进一步加速 PMEM 文件系统,类似的前作有 Octopus[258] 。远端 PMEM 成为一个存储池,本地 PMEM 通过 DAX[259] 访问。此外,这个 PMEM guide[260] 对编程很有帮助。

  • SplitFS[261]。相比 Orion,SplitFS 将数据路径放在用户空间,将元数据放在内核并通过 Ext4-DAX 操作。数据路径借由绕过内核来加速,而内核仍然管理影响一致性和隔离的关键操作。在这类系统中,Kuco[262] 引入了 Ulib、collaborative indexing、和two-level locking,以将更多细粒度操作卸载到用户空间。 ZoFS[263] 使用 MMU 将文件系统与不同用户隔离开来,而每个用户都可以在用户空间(受 MPK 保护)中操作元数据/数据。

云原生(Cloud native)

  • Snowflake[43] 是公有云上原生的 OLAP 数据库。内存缓存、查询处理、存储被解耦(disaggregated),重用公有云服务(例如 AWS S3[264]),并且可以独立扩展和计费。租户利用 VM(虚拟机)隔离,并将经典的资源利用不足问题卸载(offload)给公有云。为了避免每次都读取 AWS S3,Snowflake 添加了一个基于临时存储(ephemeral storage)的缓存层。节点可以预热(pre-warmed)以获得弹性(elastic)。 Snowflake IPO[265] 非常成功。

  • Service Mesh[266] 是一个容器化(containerized)的微服务(microservice)基础设施(infrastructure),其中 Sidecar 代理(例如 Envoy[267])只需很少的代码变更,即可为应用程序添加流量路由(traffic routing)、服务注册(Service Registry)、负载平衡(Load Balancing)、断路器(Circuit Breaker)、健康检查、加密等功能。之前的 Spring Cloud[268]可以经过一些修改,迁移到 K8S 和 Service Mesh 环境。

  • Dominant Resource Fairness[269] 是一种典型的 Cloud Resource Scheduling[270] 算法,用于 YARN[271]。 DRF规范化(normalize)多维资源分配以保证支配资源(dominant resource)。此外,2DFQ[272] 根据请求大小来将它们分配给线程,以确保公平性(fairness); Quasar[273] 通过机器学习在小型集群上采样工作负载的画像(profile),然后到整个集群运行; Container/CGroup[274] 为每个用户任务执行配额(quota)/ 权重(weight),这个模式也被 K8S scheduling[275]使用; Ceph QoS[276] 采用 dmClock[277],它使用加权的预留资源的标签(reservation tags)。另外,Leaky bucket[278] 是经典的节流算法; Heracles[279] 为延迟敏感的作业与批处理隔离资源。一般来说,云引入了 Multitenancy[107] 来描述一个由多个用户(租户)共享的系统,每个用户都分配了一组虚拟化、隔离、访问控制(access control)、优先级(priority)/ 配额的策略(policy)。对于成本估算,典型的方法是平滑窗口中的请求计数和大小,或者等待队列; Cost Modeling[280] 在数据库查询优化器中提供了更多的 comprehensive cost modeling methods[281]; 示例可以在论文 Access Path Selection[72] 和 Optimal Column Layout[71] 中找到。

  • Facebook 中使用的 Akkio[282] 跨地理区域数据中心(geo-regional datacenters)来迁移 u-shards,以保持访问局部性(access locality)。 U-shards(大小在 MBs 级别)代表了由 App 端知识决定的活跃访问的小数据集,远小于 Shards(大小在 GBs 级别),因此迁移成本更低。 Taiji[283] 是另一个 Facebook 的系统,它基于 SocialHash[284] 将用户负载均衡到数据中心,即好友群组可能会访问相似的内容。

二级索引(Secondary index)

  • Helios[159] 构建全球规模的二级索引。更新被吸收到共享日志中,作为最终数据来源,然后以最终一致性异步地构建索引。 索引是自底向上逐级合并日志来构建的,并存储在兼容HDFS的数据湖中。 第三方查询引擎可以利用索引来排除不需要访问的块(pruning)。 Hyperspace[285] 是数据湖上的另一个索引系统,用Spark任务建立; 它将细粒度的索引状态、元数据、数据、日志作为普通文件(with a spec)发布在数据湖上,以实现良好的互操作性。

  • SLIK[286] 为 RAMCloud[287] 构建全局二级索引。它对 B+树索引进行分区,后者表示为底层键值存储中的对象。 SLIK 通过在放宽(relaxed)索引一致性要求的情况下满足常见用例,来避免进行分布式事务的成本。

  • HBase 二级索引[288] 比较全局索引和本地索引,在 LSM-tree survey[289] 中有提到。全局索引只需要一次搜索,但在更新时会产生很高的一致性成本。本地索引 co-locates 于每个数据分区的节点,更新只需在本地维持一致性,但搜索需要查询所有分区。

内容分发网络(CDN)

  • Facebook Owl[290] 运行一个去中心(decentralized)的点对点(P2P)数据层(如 BitTorrent),同时维护一个集中(centralized)的控制面(control plane);每个区域运行有 Tracker,通过分片扩展。 P2P 架构能有效地横向扩展,在高流量增长下节约服务器资源。内容分发是逐块进行的,每个块沿着动态组成的临时(ephemeral)的分发树(distribution tree)传播。除了用于选择 Peer 和缓存的预设(preset)策略外,Owl使用仿真框架和 Random-restart Hill Climbing 算法来搜索最佳策略配置。CDN也可以看作是一种特殊的分布式缓存。

存储组件细分

为了绘制分布式存储系统的架构设计空间,本文将其分为三个不同的维度。它们映射到架构的静态/运行时视图和非功能目标(Non-functioanl Goals)。常见的组件可以从参考架构章节等来源提取。它们可能重叠,而本文力求将其简明清晰地分开。

按存储领域划分

  • 缓存
  • 文件系统
    • 分布式文件系统
  • 对象/块存储
  • 数据去重
  • 归档存储
  • OLTP/OLAP数据库
    • 共享日志
  • 内存数据库
    • 多核
  • NoSQL数据库
  • 图形数据库
  • 数据湖
  • 流处理
  • 持久内存
  • 云原生
    • 云调度
    • 地理迁移
  • 二级索引
  • 查询处理

按静态组件划分

  • 元数据节点
  • 数据节点
  • 索引
  • 日志(Logging & journaling)
  • 事务控制
  • 分配器(Allocator)
  • 数据布局
  • 数据压缩
  • 数据去重
  • 缓存层
  • 冷/热分层
  • 客户端
  • 存储介质
  • 网络和消息
  • 备份和灾难恢复
  • 升级/部署和重启
  • 监控和警报
  • 配置管理

按运行时工作流程划分

  • 读取路径
  • 写入路径 - 追加/覆写
  • 负载均衡
  • 数据复制/修复/迁移
  • GC/Compaction
  • 数据Scrubbing
  • 故障恢复
  • 节点成员和故障检测
  • 后台任务
  • 时钟同步
  • 资源调度和配额/节流
  • 过载控制
  • 卸载(Offloading)

按系统属性划分

  • 流量模式,查询模型
  • 数据分区和放置
  • 一致性
  • 事务和ACID
  • 横向扩展(Scaleout)
  • 纵向扩展(Scale-up)
  • 高可用性
  • 数据耐久性(Durability)
  • 数据完整性(Integrity)
  • 读/写放大
  • 空间放大
  • 并发性和并行性(Concurrency & Parallelism)
  • 吞吐量和延时
  • 跨越地理区域
  • 操作简便
  • 互操作性

分布式存储系统概述

技术设计空间-细分

后续章节讲述各组件的设计空间。它们以上面列出的”参考架构”为基础,涵盖”存储组件细分”的各个领域。与细分不同的是,技术和设计模式通常交织多个组件,需要协同设计。架构设计模式 也在下文讲述,其映射到 技术 以实现 系统属性。点连成线,扩展为连续的 设计空间,从而启发更多方案选择。

元数据

元数据的关键问题是元数据体积、如何横向扩展、如何存储、以及一致性。 元数据体积与数据分区(Partitioning)和放置(Placement)密切相关。本质上,元数据体积取决于存储对象的 跟踪粒度(Tracking Granularity) 和 自由度(Degree of Freedom)。它们是设计空间的关键维度。

元数据章节

元数据体积(Metadata size)

本质上,元数据体积取决于存储对象的 跟踪粒度(tracking granularity) 和 自由度(degree of freedom)。它们是设计空间的关键维度

  • 跟踪粒度 。 较小的分区通常会产生更好的平衡,但会消耗更多内存。 这同样适用于任务调度,想象将随机球扔进纸箱; 越小/越多的球,箱内球数越平衡。 不同的冷热分层可以使用不同的跟踪粒度,例如,缓存中使用块,存储使用文件,例如 Akkio u-shards 。

  • 自由度 。 对象需要内存来跟踪放置位置的根本原因是,对象可以自由地放置在任何位置。 限制可能的放置位置通常会减少内存消耗,例如哈希对象 ID 以映射到放置位置。 然而,这使得放置不灵活并且会产生迁移成本。

总结通常的技术和设计模式,有的用最少的元数据,有的用更多的元数据以进行细粒度控制

  • 基于哈希的放置 (Hash-based placement),零元数据的极端。 典型的例子是 Ceph CRUSH,或一致性哈希(consistent hashing)。 Ceph PG 的位置由确定性算法(deterministic algorithm)计算得出,该算法没有自由度,因此不需要元数据。 优点是元数据内存成本很小。 缺点是是增加/删除节点时的数据迁移,放置按容量平衡而不是热度,当集群接近满时几乎无法放置。

  • 跟踪完全放置 (Tracking full placement),完整元数据的极端。 一个对象能够放置在任何节点上,并且该位置在内存中被跟踪。 优点是很容易实现复杂的迁移,以及容量、热度的细粒度平衡。 缺点是元数据体积大,但有一些方法可以减少或卸载(offloading)。

  • VNode ,混合方法,用两层结构,在对象侧限制自由度。 一个对象首先被确定性地映射到一个 VNode,然后 VNode 可被放置在任何节点上。 VNode 增加了跟踪粒度,因此需要跟踪的元数据更少,但仍然享有放置自由。 例如数据库的分区可自由放置,行通过主键哈希决定所属的分区。 例如 Ceph PG 和 Dynamo 的 VNode,通过哈希将较小的对象组合成较大的组(尽管PG、VNode仍然使用哈希放置)。 例如Azure 存储的 Extent,它组合来自表格层的小块。

  • 将索引分区 (Partitioned Index)。 混合方法,限制可放置的空间的大小,用于 Kangaroo KLog 索引。 不是允许将条目(entry)放置在哈希表的任何槽(slot)中,而是将哈希表分成多个分区,对象只能放置在确定的分区中。 因此索引的空间减少,索引/指针所需内存减少。 另一种限制放置空间的方法是 Copyset[291]。

  • 叠加 (Overlay),混合方法,将自由放置层叠加在基于哈希的放置层上。 现有对象保留旧的基于哈希的放置。 新对象在元数据中被跟踪,并使用不同放置算法。 添加节点不会强制迁移现有对象。 MAPX[292] 是一个例子。

  • 减少对象链接 (Reduce object linkage)。 元数据体积的另一个来源是用于查找对象的映射链接,例如 16 字节的 UUID。 当对象很小并且组件被分解到系统中的不同层或节点时,它尤其会增长。 一个减少此元数据体积的方法是,将子对象 piggyback 到其父对象中以省去查找 ID 。

元数据横向扩展(Metadata scaleout)

处理横向扩展的主流方法是分区(partitioning,也称分片 sharding),但也有更简单的方法。

  • 分区 (Partitioning)。 元数据按主键范围切割,由不同的 Paxos Quorum 管理,例如 Tectonic 。 对象也可以按主键哈希分区。 这种方法解决了可扩展性问题,增加实现复杂性,并且会带来一致性方面的挑战。

  • 解耦 (Decoupuling)。 并非所有元数据都必须存储在中央存储中,不太重要的部分可以解耦到其它存储,以不同方式横向扩展,例如 Tectonic 。 这种方法增加了复杂性,并会产生消息传递成本,尤其是对于解耦前的密集的内存扫描。

  • 下推 (Pushdown)。 元数据可以分为两个层级。 第一层仍然在中央存储中。 第二层按需查找,下推到更多的数据节点,或者下推到 SSD 。 一个典型的例子是处理 “Lots of small files”(LOSF): 小文件被压缩成一个大文件,大文件本地存储了自己的索引; 例如 HDFS[293] 只管理大文件,而文件内索引则按需加载。

  • 层层代理 (Levels of delegation)。 与 Pushdown 类似,示例是 Big Table 。将集群范围的 B+-tree 视为元数据, 元数据本质上是一种查找数据的索引,如果是树状结构,可以逐层分解,自然地 scaleout 低级层次到整个集群,而顶层则专门保存在一致性的 Paxos Quorum 中 。

元数据存储

在哪里托管元数据,例如专用集群、分布在数据节点、动态即时生成等等。

  • Paxos Quorum 是流行的方法,例如 Ceph、FaRM、TiDB、CockroachDB 。 它们使用专用的 Paxos(或变体 variant)集群来托管元数据,或由 Paxos(变体)支持的 Etcd、ZooKeeper。

  • 点对点 (Peer-to-peer)。 源自 Dynamo 系统,不使用专用的元数据集群,而是将信息分布在整个集群中。 它使用 Gossip 协议来达到最终一致性。 此外,Dynamo 没有太多要跟踪的元数据,因为它使用一致性哈希放置。

  • 主/从 (Primary/Secondary)。 HDFS 使用单个 Namenode 来托管所有元数据并处理事务。 相比 Paxos 集群,这样更简单。 为实现 HA,HDFS 增加了一个(或多个)从备份节点。

  • 上帝节点 (God node)。 你可以看到分布式数据库从一个(或一个 Paxos Quorum 的) “timestamp oracle” 节点或 “sequencer” 节点获取事务时间戳。 例如 TiDB 的 PD,FoundationDB,CORFU。 Sequencer 节点是无状态的,可以通过重启快速恢复,并使用 Epoch 来分离新/旧。

元数据卸载(Metadata offloading)

元数据可以由别处代为管理,以避免自己处理横向扩展、一致性和持久性的麻烦。

  • Consistent Core 。 App 由微服务框架提供的 ZooKeeper、Etcd 管理元数据。 通过这种方式,问题被卸载到别处。 这种方法很受欢迎。

  • 内存中的数据库 (In-memory DB)。 存储集群的可用内存数据库管理元数据。 例如 HopsFS 或 Hekaton。 数据库管理元数据分区、一致性、横向扩展以及将冷数据分层到 SSD 。 在单个数据节点级别,Ceph BlueStore 将元数据卸载到 RocksDB,并复用其事务功能。

  • 冷热分层 (Cold tiering)。 冷的元数据可以卸载(offloading)到 SSD。 何时卸载什么数据需要仔细管理,以避免减慢定时维护的扫描循环,尤其是在多重节点故障(correlated node failures)和紧要数据修复时。 也可以将冷数据内存压缩,但这会消耗 CPU。

元数据一致性(Metadata consistency)

关于一致性,不同的领域有自己的术语,例如 DB、Storage、Filesystem,这有时会带来混淆:

  • 数据库 领域通常使用诸如强一致性(strong consistency)、外部一致性(external consistency)、可串行化(serializability)、隔离级别(isolation levels)、快照一致性(snapshot consistency)等术语。 参见 分布式事务[294] 。

  • 存储 领域和 分布式系统 可能使用线性化(linearizability)、顺序一致性(sequential consistency,见上文)等术语。 而弱一致性有,最终一致性(eventual consistency)、因果一致性(causal consistency)。 (良好实现的)最终一致性(eventual consistency)保证更新在给定的时间窗口内完成传播,不会翻悔,并且单向传播。 因果一致性经常用于客户端的消息传递,例如其需要查看自己的修改结果。

  • 文件系统 领域用 “journaling” 表示元数据的日志,用 “logging” 表示数据的日志。 它们涉及写入原子性(write atomicity)、操作原子性(operation atomicity)、崩溃一致性(crash consistency)等。 写入原子性的例子是,如果一个写同时更改数据和 inode,则它们应该全部成功或全部不成功。 操作原子性的例子有 rmdirrename ,这些操作不应该向用户暴露进行到一半的状态。 崩溃一致性意味着在节点崩溃后,文件系统应该恢复到正确的状态,例如没有执行一半却可见的 rmdirrename ,例如 PMEM 上没有损坏的链表。

  • 虚拟机 (VM) 和 备份 系统使用一致性快照(consistent snapshot)等术语。 数据建库可以使用计算 VM、缓存 VM、存储 VM 。 当 Hypervisor 拍摄一致性快照时,这要求所有 VM 都在一致的时间点拍摄快照。 反例是,计算 VM 认为更新已提交,但存储 VM 的快照时间较早,并表示没有此提交。

  • Paxos 算法使用一致性读取(consistent read)或 Quorum 读取(quorum read)等术语。 问题来自于一半的投票者可能正滞后投票(lag votes),或者一半的副本可能正滞后执行(lag execution),因此客户端可能从副本读取过时(stale)的状态。 为了克服这个问题,客户端只能从 Paxos leader 读取(不能分配负载,并且 leader 可能已经发生故障转移),或者使用包含超过一半非 leader 副本的 quorum read,或者切换到因果一致性。

元数据一致性和数据一致性涉及共同的技术,元数据需要与数据保持一致地更新。 Epoch[295] 和 fencing token[296] 是常用的技术,它们使过时的元数据/数据过期(例如崩溃重启后),以及排除过时 leader。 我将大部分篇幅留给数据一致性章节。 一般来说,元数据需要强一致性,或者更弱但版本化(versioned)。

  • 单节点 ,强一致性。 将所有元数据放在一个节点上是老办法,但实现起来非常简单。 HA 可以通过一个备节点来实现,或者简单地依靠更快的重启。 现代 CPU 确保每个核的顺序一致性,跨核可以通过锁来实现线性化。

  • Paxos ,强一致性,在 Quorum 中。 依赖 Paxos Quorum 是实现元数据强一致性的主流方法,例如 Ceph、HBase 。 一个流行的变体是 Raft[297] ,它起源于 RAMCloud[287],但更加知名。

  • 因果一致性 ,弱一致性,依赖传播。 当强一致性代价高昂时,通常出于性能考虑,元数据可以切换到较弱的一致性。 最常用的是因果一致性,它利用传播中的顺序。 它可以通过在消息中添加版本号(简化 vector clocks[298])来实现。

  • 快照一致性 ,弱一致性,依赖版本控制。 与约束传播的因果一致性类似,快照一致性约束版本,后者看到的所有组件状态都处于一致的时间点。 通常两者都需要版本号或时间戳。 一般来说,“弱”一致性是模糊的,而版本号提供了进行测量和控制的方法。

  • Gossip 。 跨节点传播元数据的一种常见方法是 Gossip,即在节点之间的通信中 piggyback 元数据,例如 Ceph。 该方法也常用于节点心跳检测、成员资格(node membership)。 最终一致性 可以通过版本跟踪来实现。 节点通常还需要联络 Consistent Core 以定期刷新可能过时的元数据。

一致性

一致性是贯穿分布式存储系统设计的核心之一,技术种类繁多,涉及大多数组件。 本文选择 规模(Scale) 作为一级分类来说明一致性的设计空间:从单节点级别、数据中心级别、到地理区域级别。 一般来说,要考虑的关键设计空间维度如下。有关更多信息,请参见分布式事务[294]。

一致性章节

  • 同步点(Point of sync)。 当一条数据被更改时,必须有一个时间点,在该时间点之后更改对用户可见,称之为同步点。 它必须是原子的,即在不可见与可见之间没有半状态(half states)。 它必须信守承诺,即一旦跨过同步点,就无法反悔。 它必须达成共识(consensus),即系统中的组件在同步点上达成一致,根据其传播分为强一致性(strong consistency)和最终一致性(eventual consistency)。 对于实现,同步点通常依赖于 原子磁盘扇区写入[299](atomic disk sector write,例如日志提交),原子内存指针切换[300](atomic memory pointer switch,例如 B+-树),或由另一个(另一组)节点作为一致性核心(Consistent Core,例如领导者节点)。

  • 确保顺序(Ensure ordering)。 系统必须就先发生或后发生的事情达成一致。 这对于 Append-only 或基于 WAL 的系统来说是自然的,类似的是每个操作都可以通过上锁的数据结构来序列化(serialize)的系统。 当系统涉及多个节点,或者日志有多个并行段(parallel segment)时,确保顺序会变得棘手。 版本控制(versioning 或时间戳 timestamp)被引入,其中全序(Total Ordering)对应可序列化(Serializable),偏序(Partial Ordering)对应 Vector Clock,不相交(disjoint)的读/写版本时间戳对应快照隔离(Snapshot Isolation,可序列化事务级别需要相同的读写时间戳)。 事务系统决定的时间戳顺序可能与现实世界不同,这个问题对应外部一致性(External Consistency)。 处理排序冲突的方法各不相同,“新来者等待”对应普通的上锁(locking)或悲观并发控制(Pessimistic Concurrency Control),“新来者重试”对应 OCC (Optimistic Concurrent Control,乐观并发控制),“抢占锁”(preempting a lock)对应抢占式并发控制(preemptive concurrency control)或 wound-wait[301]。 并发机制的实现上,通常 CPU/内存级别使用 locks/latches,磁盘级别使用 flush 或者 direct write。

  • 分别讨论 ACID。 在事务 ACID 中,通常 ACI 和一致性是统一的,但是 D 持久性(durability)有可能是分开的。 大多数存储系统选择一起实现它们,主要是因为磁盘上的排序(ordering)是通过 flush 或 direct write 实现的,它们与持久性耦合。 我们可以在下文中看到更多打破这一模式并提高性能的技术(例如 Soft Update、Journal Checksum)。

单节点级别的一致性(Single node level consistency)

CPU/内存的级别 上,基础是单个 CPU 内核确保顺序一致性(尽管编译器和 CPU 重新排序指令)。 多核编程涉及指令原子性(例如 Intel x64 arch 保证 64 位读/写是原子的[300])、内存操作排序(例如 load/store 语义)、内存更改的可见性(visibility,例如 volatile 关键字、缓存失效 cache invalidation); 它们可以在 C++ 内存模型[302] 下进行总结。 CPU 提供细粒度的指令,例如上锁/CAS(例如 lock、xchg、cmpxchg)、内存屏障(memory fencing,例如 lfence、sfence、mfence)、缓存 flush(例如 CLFLUSH、CLWB)。 进一步,它们用于构建 编程中的锁[303]、无锁算法[70] 和 PMEM 提交协议[260](如 O_DIRECT 刷到磁盘,CLFLUSH 刷缓存到内存/PMEM)。 更多技术可见于数据库中的 B+ 树上锁技术[304] 和 Linux 内核同步[305]。 它们不是本文架构设计的主要主题。

谈到存储,更多的关注给了 内存/磁盘级别崩溃恢复 (crash recovery,即系统完整性 system integrity)。 预写日志 (WAL) 是一致性(以及 ACID 中的写入原子性和持久性)的主流解决方案,随着 Append-only 存储系统(例如 LSM-tree)的采用趋势,它变得更加广泛。 WAL(redo/undo log)也是实现 数据库事务[306] 的必要组件,但是有更多的方法来保持一致性。

  • Write-ahead logging (预写日志,WAL),一致性由顺序记录的日志和提交条目(commit entry)来保证。 元数据/数据更改通过 journaling/logging 持久保存到磁盘; 其中 journaling/logging 的提交条目是更改提交可见的同步点,它们同步 flush 到磁盘。 日志是自然的完全有序的,也不排除使用版本控制/时间戳。 数据库进一步使用 redo 日志和 undo 日志(ARIES[306]),其中 redo 日志是通常意义的日志,而 undo 日志为支持 “No Force, Steal” 引入,即一个页面(page)可以提前 flush 到磁盘,即使所属的(大)事务尚未提交。

  • Shadow Paging,由 COW (Copy-on-write,写时复制)和原子指针切换实现一致性,示例是 BtrFS。 更新通过 COW 添加到新 B+ 树页面(pages)。 提交时,同步点在于原子地切换父节点的指针到新页面。 同样的模式在内存和磁盘中都使用,其中 CPU/内存通过锁控制排序。 该技术有利于内置(built-in)对快照的支持,通过 COW 提高并行性(parallelism),并且没有序列化日志提交时的瓶颈。 然而,叶节点的变化会导致父节点的变化,并向上传播到树根,这是昂贵的(除非使用页面映射表,Page Mapping Table)。

  • Soft Update,一致性通过跟踪内存中的排序实现,但并不确保持久性,例子是 FFS[307]。 Inode 跟踪更新的依赖关系,系统确保(enforce)它们的执行顺序。 实际写入磁盘可以延后,异步发生,并提高并行性。 用户需要等待更新被持久化后的通知。 Soft Update 本身并不能保证必要的元数据/数据更改在崩溃(crash)时是持久的(durable),需要谨慎地实现,以确保崩溃一致性(Crash Consistency)。

  • Transactional Checksumming,一致性通过跟踪磁盘上的排序,但并不确保持久性。 系统开始并行写入块 A/B,但期望块 A 仅在块 B 之后提交。 块 A 携带 B 的校验码(checksum); 如果在中间发生崩溃,在磁盘上留下 A 而没有 B ,校验码可以告诉块 A 无效。 该技术打破了顺序记录日志的瓶颈,但是在故障恢复期间需要付出更多代价以确定同步点。 有关更多信息,请参见 乐观崩溃一致性[308](Optimistic Crash Consistency)。

元数据/数据组件 之间的一致性也需要维护(接元数据章节 TODO)。 典型的存储系统将新更改的可见性从磁盘数据传播到索引,然后传播到用户。 这里的索引是一种元数据,它指示如何查找数据,例如 inode 树。 从系统内部,传播通常是 最终一致性 (eventual consistency)的,例如分配磁盘空间,写入数据,然后过一段时间提交日志。 通过接口(隐藏系统内部结构)和通知(异步),这些操作对用户变成“原子的”。 当元数据和数据被分离到不同的节点组时,同样的设计模式适用。

数据中心级别的一致性(Datacenter level consistency)

在单节点级别的一致性之后,我们来到分布式多节点级别。 从强到弱,现代分布式数据库通常在可序列化(Serializable)或快照隔离(Snapshot Isolation)级别实现事务 ACID 。 存储系统通常为数据复制(Data Replication)提供强一致性,NoSQL、缓存、跨系统交互通常采用较弱的一致性模型,来降低复杂性和性能开销。

  • 分布式事务 (Distributed Transaction)。 有关更多信息,请参阅 分布式事务 TODO[294] 文章。 例子有 Spanner、Percolator、CockroachDB、TiDB。 这些实现在同步点、如何确保(enforce)排序(ordering)和锁冲突处理方面有所不同。 此外,数据库的全局二级索引,为了与用户写维持强一致性,也实现了分布式事务。

分布式事务策略

  • Raft 数据复制。 例子有 CockroachDB、TiDB。 就像在 Paxos quorum 中运行元数据一样,数据分区(data partition)通过 Raft 协议(一种 Paxos 变体 variant)进行复制。 这确保了强一致性,并重用了 Paxos 上的优化,例如 乱序提交[309] (Out-of-order Commit)。 Megastore[310] 中有 Paxos 复制的更全面的优化。

  • 3-way Replication。 例子有 Ceph,以及类似的 Azure 存储中使用的 Chain Replication[311]。 它比 Raft 更简单,也更早出现。 经典的实现是通过 Consistent Core(例如元数据集群)选出一个 Leader 节点来驱动具有强一致性的 Follower 节点。 可以通过流水线(Pipelining)优化吞吐量。

  • Quorum Read/Write。 例子有 Dynamo、Cassandra。 总共有 N 个副本,在 > N/2 个副本上进行读取或写入操作,来保证与最新版本的副本(replica)相交。 该实现也增加了更多复杂性,例如处理读放大(或简单地返回缓存的版本)、版本跟踪、以及节点写入失败。

  • 日志即数据库(Log is Database)。 与 WAL 简化单节点一的致性类似,分布式系统可以构建在共享日志(Shared Logging)服务之上,例如 FoundationDB、Helios Indexing。 这个想法可以扩展到在提供强一致性并充当单个节点的任何共享存储服务之上构建系统,例如 一个分布式文件系统,一个页面存储(Page Store)。 示例包括 AWS Aurora Multi-master、Azure 存储。 这个想法还扩展到以同步或最终一致的方式传播更改,这自然适用于数据库 WAL。 例如 Helios Indexing、MySQL BinLog Replication[312]。

以上技术建立了强一致性,对于较弱的一致性:

  • 最终一致性 (Eventual Consistency)。 通常,如果系统不对一致性做任何事情,并让更新传播,那就是最终一致性。 更好的实现提供版本控制来测量传播,并保证完全传播的最终时间。

  • 因果一致性 (Causal Consistency)。 与 元数据章节 TODO 相同。 它与最终一致性兼容,客户端必须看到它已经看到的东西。 对于实现,客户端维护它希望服务器返回的低水位版本号(low watermark)。

  • 定制的一致性级别。 例子是 RAMP-TAO,它检查本地结果集是否满足“读取原子性”(Read Atomicity),并从 RefillLibrary 中获取缺失的版本。 通常,可以通过使用数据跟踪版本、即时(on the fly)检查一致性约束、以及缓存查询结果,来实现更多定制的一致性级别。

  • 补偿事务 (Compensation Transaction)。 请参阅这篇 补偿事务[313] 文章。 它联合多个系统来构建(pseudo)ACID 事务。 每个系统内部都支持 ACID 事务和幂等(idempotent)操作。 客户端驱动事务,以单向最终一致性方式在多个系统中传播更新,支持至少一次(At Least Once)语义和明确的完成时间。 如果一个系统在过程中失败,破坏了原子性,客户端通过在每个系统上以相反的顺序重放“补偿事务”来回滚。 隐藏所有的复杂性,它为客户端提供了一个看似 ACID 的事务接口。 该技术常用于构建大型互联网公司的预订服务(booking service)。 此外,可以添加一个“预留”(reservation)步骤来使系统不易出错,这使它更像 2PC(除了其他客户端可以读取中间状态)。

地理区域级别的一致性(Geo-regional level consistency)

当涉及到跨区域多数据中心级别时,一致性的技术与单数据中心级别类似。 但由于延迟开销,更大的规模使得强一致性或同步复制变得困难。 大多数实现是最终一致性的,灾备恢复领域定义了度量概念:

  • RTO(Recovery Time Object,恢复时间目标)。 在第一个区域发生灾难后,系统和应用程序需要多长时间在第二个区域恢复。 如果系统启动、缓存预热、DNS 切换需要时间,RTO 可能会很长。

  • RPO(Recovery Point Object,恢复点目标)。 因为跨地域复制是异步的,所以从复制的数据到最新的数据是有延迟的,RPO 定义延迟窗口。 它反映在第二个区域中恢复后将丢失多少最近的数据。

除了与单数据中心级别的一致性重复的那些之外,常用技术如下。 相比之下,更多的优化针对不稳定的链路和 WAN(Wide-area network,广域网)中的低带宽。

  • 地理复制(Geo-replication)。 数据库通常支持用于跨区域备份的异步复制(最终一致性),通常通过复制日志,例如 MySQL BinLog 复制,和 Redis replication[314] 主从指令流(command stream)。 异步地理复制不排除同步复制一小块关键元数据,也不排除客户端查询主区域(primary region)以确定最新版本。

  • 增量差异(Incremental Diff)。 Ceph 提供 RBD diff[315] 导出快照增量,可用于以半自动方式进行地理复制。

  • 日志即数据库。 上面已经总结了大部分。 使用日志以最终一致性的方式复制更新,例如 Helios Indexing、MySQL BinLog Replication。

以上是最终一致性复制。 对于强一致性的地理复制,通常采用(并优化)Paxos 复制,而序列化事务的时钟同步成为一个更大的问题。

  • Megastore PaxosGoogle Megastore[310] 使用优化的 Paxos 协议跨 WAN 同步复制。 与主/从复制相比,附近或利用率较低的数据中心的任何 Paxos 副本都可以领导事务来平衡负载。 写入只需 > N/2 个副本的 Ack,这减少了跨数据中心的延迟。 通过使用协调者(coordinator)跟踪哪个副本具有最新版本,本地数据中心优先使用本地读取。 写入使用 Leader Paxos[297],其中倾向选择附近副本作为领导者。 当参与者(participant)太少时,引入见证者副本(Witness Replica)来组成 Quorum;见证者副本参与投票和复制日志,但不会执行日志来服务数据库数据。 引入了只读副本,它不投票,但重播(replay)日志以服务数据库数据,提供快照读取。 基于实现,Paxos 跨数据中心本质上是复制日志,类似 “日志即数据库”。

  • Spanner & TrueTime。 与 Megastore 一样,Google Spanner[316] 将副本存储在不同的地理区域,并采用 Paxos 复制。 分布式事务由 2PC 实现,其 Liveness 由参与者的 Paxos 副本的高可靠性保证。 特殊部分是 TrueTime[317],其用于跨数据中心同步时钟,从而通过 Commit Wait 实现外部一致性(Distributed Transactions[294])。 TrueTime 依赖于定制硬件,即 GPS 接收器和原子钟,作为每个数据中心的主时间主节点,保证 全球时钟漂移小于7毫秒[122]。

  • CockroachDB & Hybrid-Logical Clock (HLC)。 与 Spanner 一样,CockroachDB[318] 采用 Paxos (Raft) 跨区域数据复制,以及 2PC 进行分布式事务。 读取优先选择附近的副本,而写入可以首先选择同一区域中的附近副本,并异步复制到其它副本。 与 Spanner TrueTime 不同,CockroachDB 使用 HLC[121] 作为跨数据中心的时钟。 HLC 在其逻辑组件(logical component)上提供因果关系跟踪,并在其物理组件(physical component)上提供单调递增的 Epoch,并使用 NTP 作为纯软件时钟同步协议。

写入路径

分布式存储系统中的下一个重要组件是写入路径,通过它可以描述系统的主要工作方式。Append-only 或 Update In-place 从根本上划分了系统的风格和下一层所需的技术。写入路径几乎触及系统中的所有组件,例如元数据、索引、数据组织、日志记录、复制,以及许多系统属性(System Properties),例如一致性,耐久性(Durability),读写放大(Amplification)。而读路径可以看作是写路径的缩影,外加为优化性能而引入的缓存。

写入路径章节

追加 vs 就地更新(Append-only vs Update In-place)

系统的第一个关键维度是 Append-only(追加) 与 Update In-place(就地更新)。 传统的单节点文件系统通常就地更新磁盘数据(BtrFS 除外)。 后来 LSM-tree 的快速采用,导致了 Append-only 系统(也称为日志结构系统,Log-structured systems)的主导地位。 不仅 HDD 受益于顺序写入,SSD 由于内部的 FTL 和 GC,也受益于 Append-only(例如 RocksDB)。 更多地,PMEM 文件系统,例如 NOVA 采用 Append-only 和 Per-inode Logging;内存系统(In-memory systems),例如 Bw-tree ,采用 Append-only 增量页面(Delta pages)。

  • Update In-place。 例如 EXT4、Ceph。 如果要更新一条数据,则将其覆写(Overwrite)在磁盘的同一地址,而不是写入新地址。 与 Append-only 相比,地址跟踪更简单,不需要额外的内存元数据来记录新地址,并且不需要额外的昂贵的 GC 来回收旧数据。 缺点是:1)底层硬盘(HDD)不喜欢随机写入;2) 对于固定块大小的系统,存储压缩结果(大小可变)很棘手;3)双写问题(Double write),覆写需要事务以实现崩溃保护(Crash Consistency),因而新数据需要在日志中额外写入一次。

    • 基于内容的寻址(Content-based addressing)。 例子是 XtremIO。 每条数据都有一个固定的磁盘存储位置(在磁盘块级别,按数据哈希放置)。 当数据块位置由数据的内容哈希确定时,可以自动去重。 由于数据块位置的自由度为零,这样的系统需要很少的元数据来跟踪数据位置,并且不能直接通过 Append-only 实现。

    • 组关联缓存(Set-associative cache)。 示例是 Kangaroo 和 Flashcache[319] 。 整个 SSD 用来映射一个大的 HDD 空间,就像 CPU 缓存映射内存一样。 HDD 数据块可以存储在 SSD 上,从一个小集合的块中选择。 该集合由哈希确定,通过线性探测(Linear probing)寻找其中的块。 同样,通过限制数据位置的自由度,需要极少的内存元数据。

    • 数据库分页(Database paging)。 一种更简洁的 Update In-place 方法是将地址空间划分为页面,并使用页面作为传输的原子单位。 这里的“页”就像是存储“块”。 但是,系统还需要事务日志来保证崩溃一致性(Crash Consistency)。 更多地,即使只更新几个字节,也得切换(switch)整个页面,即写放大(Write amplification)。 页面可能有内部碎片(Internal Fragmentation),无法利用边缘的(marginal)字节,导致空间放大(Space amplification)。 如果页面(Page)不需要大小相等,该技术可变成“块”(Chunk)或“微分区”(Micro-partition)。

  • Append-only。 例子有 LSM-tree 或 RocksDB,日志即数据库,以及 Azure 存储。 系统不支持修改磁盘上已写入的数据,因此更新需要追加(append)到新的地方,类似日志。 此类系统的主要缺点是: 1) 需要不断的 GC(或 Compaction)来回收旧数据,这会占用甚至 50% 的系统带宽。 2) 数据位置的自由度高,系统要么需要庞大的内存元数据以进行跟踪,要么在扫描时过时数据时会导致读放大。 好处是:1)一切都简化了,因为写入的数据是不可变的(Immutable);2)写入是顺序的,受 HDD 喜爱;3)事务和崩溃一致性是自然支持的,因为数据即日志。 多年来,Append-only 被证明是成功的。

    • 顺序结构与否(Sequential structure or not)。 例子是 BtrFS。 并非所有的 Append-only 系统都使用顺序的日志记录(Sequential logging)。 在 BtrFS 中,新数据写入时 Copy-on-write 到新页面(COW),然后原子地链接到 B+ 树。 此外,像并行多段日志(Parallel Multi-segment Logging)这样的优化,也打破了默认的顺序日志记录。

    • 在线或离线清理(Cleanup inline or offline)。 Append-only 系统需要清理过时数据; 应该在写入路径上进行还是离线完成? GC/Compaction 选择离线。 Apache Hudi 的 Copy-on-write 选择在线于写入路径上。 此外,清理甚至可以延迟到第一次用户读取,即 Apache Hudi 的 Merge-on-read。

    • 增量数据(Delta data)。 Append-only 的想法可以扩展到 PMEM(例如 NOVA)或内存(例如 Bw-tree)上的索引。 追加增量数据的方式,有利于高并发,简化锁的处理,并避免像 COW 那样的读写放大。 从另一个角度来看,不可变数据可以通过 COW 或追加增量实现,但 COW 在写入路径上强制 Compaction。

    • 日志即数据库(Log is database)。 我们之前已经提到过它。 数据库分页的设计倾向随机写入,与之相比,在组件间传输日志倾向顺序写入。 如果只修改部分页面,同步整个页面(syncing pages)导致写放大,但可以吸收对同一地址的重复修改; 而由日志携带的增量,比完整页面小,但重复修改可以增长为长链,因此需要 Compaction 。 虽然日志可以很容易地用作数据库状态的一致性的事实(a consistent truth for database state),但重播(replay)最新数据会产生计算成本,并且需要仔细的版本控制以利用缓存的页面。

  • 混合方案(Hybrid approach)。 例子是 Ceph BlueStore,其中大写入用 Append-only,不与现存数据重叠的小写入用 Update In-place,小的覆写(Overwrite)合并到 RocksDB WAL (Write-ahead logging,预写日志)。 这种方法是为了克服 Ceph 的双写问题而发明的,它实质上将过去的 Update In-place 转变为 Append-only。

从更高的层面思考, Append-only 与 Update In-place 背后的驱动因素是是否延迟 维护磁盘上的数据组织(Maintaining on-disk data organization),以在线还是离线方式进行,或者说使用写入优化的数据格式(Write-optimized Data Format)vs 读取优化的数据格式(Read-optimized Data Format)。

  • 写入路径 是高效的,如果不需要维护磁盘上的 数据组织(Data Organization)(参见 数据组织章节)。 写入会因批处理(batching)、顺序性(sequential)而受益;这是 Append-only 带来的好处,除了用于 GC/Compaction 的额外带宽。 此外,写入受益于更少的需要协同更新(co-update)的组件(in sync,同步地),例如更少的索引、缓存、更少的碎片化的写入位置。

  • 读取路径(Read path) 是高效的,如果数据有索引,或者存储位置可以从键(Key)中计算得出,或者排序良好以有利于全扫描(Full Scan)。 数据应该更少碎片化(less fragmented),保留局部性(locality),并且具有更少的过时条目(stale entries)。 尽管 Append-only 生成碎片化的增量,但 GC/Compaction 可以将它们重写为读取优化的数据格式。 尽管 Update In-place 节省了 GC/Compaction 的带宽,但实现读取优化的数据格式可能仍需要额外的重写。

    • 数据索引(Data index) 通常被高效的读取路径所需要。 Update In-place 通过限制数据位置的自由度来减小索引大小,但不适用于二级索引(Secondary Index)。它也避免了减小跟踪粒度,即不像 Append-only 将小更新重定向到新页面。 这也意味着连锁发生的索引更新更少。
  • 磁盘上的数据组织(On-disk data organization)。 最好的读取优化的数据格式几乎总是需要完全重写才能生成,这解释了为什么 Append-only 是有利的,特别是考虑到列压缩(columnar compression,即 OLAP)。 新近的数据,可以通过冷热分层(Tiering,或类似于 LSM-tree 中的 “Level” )分离。 它仍可能受益于 Update In-place,以减少 GC/Compaction 或索引的扰动(尽管实际上大多数系统仍使用 Append-only)。

协同更新邻接组件(Co-updating neighbor Components)

除了磁盘数据外,写入路径还涉及更多需要一同更新的组件,例如元数据、索引、检查点(Checkpoint)、日志记录、缓存。

  • 元数据、索引(Metadata, index)。 这里主要关注的是从磁盘数据更改到最终用户的可见性的传播。 这在之前的 一致性章节 中提到过。

  • 检查点、日志记录(Checkpoint, logging)。 新的更改首先由 WAL 原子地实现持久化,一种典型的技术是分离键/值 (WiscKey)。 然后可以将已持久化的更改传播到索引和元数据,以便对用户可见。 日志记录是一种写入优化的数据格式,但读取需要结构化的数据。 “结构化的数据”要么定期从内存刷新到磁盘,即检查点,要么通过传输数据库页面实现。 碎片化、有重叠部分的检查点,需要 GC/Compaction 来重写为更读取优化的数据格式(例如 LSM-tree),并回收已删除的存储空间。

  • 缓存(Cache) 更新是异步的,通常从写路径,离线进行; 除非写入想要无效化(Invalidate)过时数据,或立即加载新数据。

除了在本地写入外,数据复制 也在混合在写入路径中。 它实现了持久性(Durability)和许多其它目的

  • 持久性(Durability),例如 Raft 复制,3-way Replication,Quorum Write,参见 一致性章节 。 为持久性而使用的数据复制通常是同步的,具有强一致性。

  • 灾难恢复(Disaster-recovery),例如备份(Backup), 地理复制(geo-replication),参见 一致性章节 。 它们可以异步进行,并设定 RPO 。

  • 局部性(Locality),例如将数据移动到用户本地区域的地理复制,例如 Akkio u-shards。由 CDN 充当静态内容的缓存,作为跨 WAN 提供商间的桥梁。

  • 数据布局(Data layout)。 例如 TiFlash 和 F1 Lightning 。 数据库将主要数据副本维护为行格式(row format)以服务于 OLTP,它复制一个额外的列布局(Columnar format)副本供 OLAP 使用。 可以使用 Raft 协议或细粒度版本跟踪来保持副本之间的一致性。

  • 冷热分层(Tiering)。 热点数据可以复制到缓存。 冷数据可以卸载(Offload)到慢速的 HDD 或归档存储(Archival Storage)中。 层之间的数据格式也可以不同,以优化访问延迟、存储效率或压缩。

  • 数据平衡(Data balance)。 通常,数据可以重新平衡以填充更空的新节点,从关联故障域(Correlated failures)的位置分散开,或平衡节点上的冷热访问。

  • 日志即数据库(Log is database)。 不同于复制数据或页面,而是将带有增量的日志作为数据的真实来源(source of truth),进行复制和传播。 参见 一致性章节

  • 分离写入路径和读取路径(Separating write path and read path)。 例子是 AnalyticDB、MySQL 主/从复制。 该设计源自数据库社区,使用一台服务器作为写入主服务器,并复制到多个副本以横向扩展(Scaleout)读取能力。 它利用该模式:社交网络以相对恒定的速率生成内容(写入),但用户浏览量(读取)可能会激增(burst)。

涉及数据的离线后台作业(offline background jobs)也可以按目的划分。 它们通常会重写数据副本,这是 写放大 的主要来源,但有必要通过生成更优化的数据布局来减少 读放大

  • 持久性(Durability)。 通常涉及数据修复(data repair)的过程,当节点或磁盘出现故障时被考虑。 这些后台作业需要较短的检测发现时间(detection time)和高优先级的带宽。 可以通过 引入更多节点 提供源数据,来提高数据修复的效率,例如 Ceph 修复让整个集群参与,Copyset 修复集群的分区参与,以及主/从复制让少数从节点参与。

  • 存储效率(Storage efficiency)。 数据压缩可以在写入路径之外运行,以避免增加用户可见的延迟。 擦除编码(Erasure Coding)可以进一步减少所需的存储空间。 GC 定期运行以回收已删除的存储空间。

  • 数据布局(Data layout)。 例如 RocksDB 运行离线 Compaction,来删除过时数据,整理重叠的 SST 文件,以提高读取效率。 例如 AnalyticDB 将新的写入缓冲在增量文件中,然后将它们合并到基线数据并构建全量索引。 类似的增量合并模式也可以在 Datalakes 中找到,例如 Apache Hudi 。 对于数据复制,目标副本可以放在另一个区域甚至另一个云服务中,同时计算也可以 卸载到云

  • 数据完整性(Data integrity)。 存储系统通常采用离线数据擦洗(Data Scrubbing)来检测静默数据损坏(Silent Data Corruption)。 端到端 CRC(End-to-end CRC)可以与数据一起存储。 此外,可以检查不同层次的不变约束(Invariants),例如索引与数据,映射关联(mapping constraints)。

写入不同的存储介质(Write to different storage media)

写入数据流经或最终持久化在一种存储介质中:内存、PMEM、SSD、HDD、或归档磁带(archival taps)。 数据结构和所使用的技术(techniques)因存储介质的特性(characteristics)和工作负载(workload)访问模式(access pattern)而异。 我们将在 数据索引章节数据组织章节 中看到更多信息。

  • 内存层(Memory tier) 在随机访问方面做得很好,与其它存储层相比延迟最低。 主要关注的是提高并发性、缓存效率、以及在内存排列(pack)更多数据。 典型的数据结构可以是普通指针链接(例如 FaRM),跳表(Skip List,例如 RocksDB),或有利于并发的 Bw-tree。 相比红黑树,B+-tree[320] 更大的节点有利于装进缓存行(Cache Line)。 还有哈希表用于快速查找(例如 Memcached)。 可以启用内存压缩(Memory compression)和磁盘交换(disk swap,例如 TMO[321])。

  • 持久内存层(PMEM tier) 比 DRAM 慢 2~3 倍(PMEM empirical guide[322]),并且不喜欢小的随机写入。 主要关注点是提高并发性,补偿较慢的 CPU ,并保障崩溃一致性,同时避免昂贵的缓存 flush 指令。 RDMA 和 Kernel Bypassing 是常见的技术。 基于树的 Append-only 数据结构,例如 NOVA 中的 per inode 日志记录,仍然是有利的。 另一种方法使用哈希表数据结构,例如 Level Hashing 。

  • SSD 层。 除了少数系统 Update In-place 外,大多数系统都转向 Append-only,例如 RocksDB 和使用 RocksDB 作为引擎的 TiDB / Cockroach / MySQL,使用 LSM-tree(类似)引擎的 HBase / ClickHouse,或构建在共享日志记录之上的 FoundationDB / Azure 存储。 换句话说,SST 文件和共享日志是 SSD 上常见的数据结构。 OLAP 数据库也青睐于批量追加和重写为压缩的列式布局。 一些数据库选择为每一列建立索引,而另一些则完全依赖于全扫描(Full Scan)。

  • HDD 层。 由于两者都青睐于 Append-only,因此 HDD 或 SSD 上的数据结构相似,大多数系统可以在两者上互换运行。 不同之处在于 SSD 需要更多的 CPU ,和为每个设备(SSD die、plane)分配的并行度。

  • 存档磁带层(Archival tapes tier)。 Append-only 也是受欢迎的写入方式,例如 Data Domain,因此与 HDD 或 SSD 没有太大区别。 数据通常以顺序结构进行去重(Dedup)和追加,并依靠索引进行查找。 去重指纹(fingerprint)可以与保留局部性(Locality)的数据一起存储。 更高的压缩级别和更长的纠删码常被使用。

  • 计算层(Computation tier)。 上述层按数据大小排序。 而计算层的特殊之处在于,在某些情况下没有数据需要存储,它们可以通过计算重新得出。 换句话说,在计算中“存储”数据。

在不同的存储介质之间分层(Tiering between different storage media)

通常,存储介质层是根据数据的价格、规模和性能目标来选择的。 每一层都有自己的优化技术。 跨层的数据移动还需要高效的冷热检测/预测(detection/prediction)算法,这些算法通常是 LRU 变体,但更关注于减小因大数据规模而带来的跟踪元数据体积:

  • 指数平滑(Exponential Smoothing)。 这是标准的学术方法,它用一个权重来平均现在和历史的热度,更早的历史被指数级地遗忘。 该方法没有提到如何有效地实现它。 热度可以通过时间窗口中的数据访问次数和字节数来衡量。

  • LRU(Least-recent Used,最近最少使用)。 与指数平滑一样,LRU 是大多数冷热分层算法的典型,但没有指定如何实现。

  • 每个对象的比特(Bits per object)。 例子是 Kangaroo RRIParoo 算法。冷热度由每个对象的比特跟踪。 当访问对象时,或需要全局逐出时(例如时钟到期、缓存已满),可以翻转一个比特位。 如果所有比特位都匹配,则可以逐出该对象。

  • 列表中的对象(Objects in list)。 例子是链表实现的 LRU,或 Linux 内核的 内存页面交换[323] 。 冷热由列表中的对象位置跟踪。 对象在访问时被提升到头部,在变冷时被降级到尾部,并最终被逐出列表。

  • 上次访问和过期(Last accessed and expire)。 通常在 App 操作旁路缓存时看到。 简单地说,数据库中最新访问的项目被放入缓存中。 如果缓存已满,则最旧的项目将被逐出。 缓存项也会因超时而过期。

  • 离线分类(Offline classification)。 例如 Hekaton Siberia,Google G-SWAP[324]。 当冷热跟踪元数据过大时,系统可以将流量记录(可能被采样,sampled)转储到磁盘,并采用离线的定期分类作业或 机器学习 对冷热数据进行分类。

  • 用户标记(User tagging)。 为最终用户开放接口,以明确标记一条数据是热是冷。 方法简单,而用户总是更了解自己的数据。

写入和读取路径合并(Write & read paths coalescing)

虽然写/写和读/读合并是常见的技术,但写/读和读/写有有趣的方式来组合,并重用彼此的中间结果。

  • 写入合并(Writes coalescing)。 小写入可以组合成一个大写入到磁盘。 系统可以使用定时蓄水池(timeout plug)来积累足够多的小写入来合并,或者是扫描并重排序写入队列。 可以组合相邻地址中的小写入以形成大的顺序写入。 可以合并(cancel out)对同一地址的重复写入,仅需将最后一次写存入磁盘。 可使用更快的 Stageing 内存、闪存(flash)、或 PMEM ,来吸收小写入。

  • 读取合并(Read coalescing)。 与写入一样,可以组合小读取以利于顺序磁盘访问,或者通过缓存来避免重复磁盘读取。 读取查询通常需要扫描比用户请求更多的物理数据,这意味着可以批处理(batch)多个查询,并共享一次磁盘扫描。

  • 读作为写的路径 (Read as a path for write)。 当读取查询扫描数据以查找某些内容时,概念上它正在构建索引。 读取可以利用该扫描,将索引的“零件”推送到写入路径,而写入路径负责真正地构建索引。 例如 REMIX LSM-tree[325] 利用范围查询为 SST 文件构建索引。 写入路径还负责将新接收的数据重写为读取优化的数据格式,它可以重用读取查询刚刚扫描并加载到内存中的内容。 当加载操作昂贵、且涉及远程网络时,这个技术更有用。

  • 写作为读的路径(Write as a path for read)。 新写入的数据通常更有可能被读取。 写入可以将它们留在内存或 Staging 区中,直接填充(populate)缓存,并将它们组织在读取优化的数据结构中,让接下来的读取受益。 例如典型的 LSM 树中的内存表(Memtable)。

卸载(Offloading)

在线或离线于写入路径,FPGA 和 ASIC 通常用于从 CPU 卸载计算,例如压缩/加密和多租户(multi-tenant)的云虚拟网络(virtual network)处理。 卸载能够减轻不断增长的 IO 硬件吞吐量对 CPU 的压力,而下推(Pushdown)则缩短了数据传输的路径。

  • FPGA 的特色是支持重新配置(Reconfiguration),有利于灵活性和快速实验。 ASIC 是专用电路,很难更改,但制造完毕后,它们的效率要比 FPGA 高得多。 FPGA 有成功的用例,例如 Project Catapult[326] ,SmartNICs[327] 也很受欢迎。

  • 压缩/加密 是典型的卸载用例,因为逻辑是固定的,很少有异常处理,并且计算是面向数据管道(data pipeline)的。 网络处理类似。 此外,如今的高速 RDMA 对 CPU 的要求更高,云虚拟网络涉及更多层次的的重定向。

  • 最近的 IPU[328](Infrastructure Processing Unit,基础设施处理单元)在 DPU[329] 之后被提出,将常见的数据中心基础设施功能卸载到 CPU 以外的处理芯片。

  • Smart SSD[330] 将计算芯片添加到 SSD。 查询过滤或 GC/Compaction 可以下推到 SSD 内部,而不需要跨 PCIe 的更长的数据传输路径。

数据组织

传统意义上的”数据组织”是指数据库中的物理列/行的数据布局。本文选择从一个更广泛的角度来看待数据组织,它是按目的划分的。从本质上讲,当查询层想要实现高性能时,它承载了最多的数据库技术;而耐久性/横向扩展层与之正交,可以卸载(Offload)到共享存储系统中;而性能层通常由缓存来解决。本节把重点放在数据组织的查询层,并在其它章节讨论性能层/横向扩展层。

数据组织章节

  • 耐久性层(Durability tier)。在一个存储系统中组织数据的基本需求是使其持久。复制(Replication)是常见技术,以存储空间为代价;同时也很脆弱,因为数据损坏(corruption)可以被一同复制。复制也与性能层耦合,用额外的复本来平衡读取。纠删码(Erasure Coding,EC)减少了存储空间,提高了耐久性,代价是数据的重建(Reconstruction)。 一致性 是伴随数据复制的一个常见问题。 端到端CRC(End-to-end CRC)和 定期擦洗(Scrubbing) 是必要的,以防止在写入路径、数据转换或静默中发生数据损坏。 备份地理复制(Geo-replication) 是灾备的标准配置,而 时间旅行(Time travel) 可通过追回早期版本的数据,来恢复人工错误。

  • 查询层(Query tier)。磁盘数据需要支持读取和更新。常见的访问模式是存储系统中谈到的 顺序/随机读追加(append)、更新(或读-修改-写)(updates, read-modify-write),以及数据库中谈到的 点/范围查询(point/range query)、扫描(scan)、插入(insert)、更新(或查询后更新)(updates, update-after-query)。 传统上,磁盘数据同时服务于耐久性层和查询层,这在写路径上产生了成本,以保持读优化的格式。将读取路径和写入路径分开会有帮助,或者将读取路径完全转移到 性能层,例如内存数据库。 查询层可以为 OLTPOLAP数据湖(Datalake) 进一步定制化(specialized),它们共享主要技术,但在查询模式、一致性、数据规模和结构化数据方面有所不同。

  • 性能层(Performance tier)。通常它们是额外的数据副本以平衡读取:SSD 层用于缓存(或者也是耐久性的一部分);PMEM 暂存区(staging area)以吸收和顺序化重复的随机写入;普通内存缓存;或者将所有计算转移到内存数据库。当用作缓存时,SSD 或内存可以针对小块而不是来自磁盘的整块,见 数据缓存章节。在内存中组织的数据更依附于索引,与磁盘上的不同,见 数据索引章节

  • 横向扩展层(Scaleout tier)。为了应对不断增长的数据量和高吞吐量的目标,数据被 分区复制,并通过 放置(Placement)工作,从更多的机器上提供服务。针对异构(heterogeneous)的作业和规模的 资源调度负载平衡,和 迁移 因需求而变。见 数据分区章节一致性 始终是一个问题。在单节点上,它很容易依赖于 CPU 的缓存一致性(Cache Coherence),但向上扩展(Scale-up)时就会受到 CPU 功率/热量和太多核心之间的缓存一致性成本的限制。在分布式系统中,分布式事务的一致性仍然会产生很高的网络成本,除非引入应用程序级别的协同(agreement)来放松约束。

耐久性层(Durability tier)

我们在 一致性章节 中讨论了复制问题。我们将在 数据完整性章节 中看到更多关于 CRC 和擦洗的内容。下面我们简要地扩展一下纠删码的设计空间。

  • 存储开销(Storage overhead)。纠删码的主要目标是以相当的耐久性来存储数据;但与普通复制相比,存储空间更少。

  • 耐久性。如果一组磁盘坏了,数据必须是可恢复的。如果一组节点离线,数据必须对用户的读取可用(有重建)。

  • 性能。与普通复制相比,当部分数据离线时,读取(重建)纠删码数据会产生巨大的成本,尤其是尾部延迟(Tail latency)。由于存储副本较少,用于服务的总聚合带宽是有限的。

纠删码的模式种类非常丰富,特别是与集群布局和用户流量模式相结合。简而言之,主要的模式来自于以下几类:

  • Reed-Solomon Codes。标准的教科书模式(Schema),每个数据符号(Data symbol)都对称地参与到每个校验符号(Parity symbol)中。该代码是 MDS 的,意味着能够在固定的存储开销下恢复最多的丢失模式。

  • LRC 编码。为了减少重建读取或数据修复所需要的带宽,部分校验符号选择包含更少的数据符号。换句话说,该模式以可恢复的损失模式为代价,提高了性能。

  • Regenerating 编码。另一种减少数据重建中所需带宽的方法。MSR (Minimum Storage Regenerating) 编码达到了低带宽的边界(lower bound),而对存储开销没有惩罚。Regenerating 编码的构造通常比较复杂,需要更多的计算。

查询层的数据布局(Data layout for query tier)

在高层次上,我们首先关注数据布局的 目标 。理想情况下,我们希望每个目标都能达到最优,但这是不可能的,目标之间的权衡构成了设计空间(Design space):

  • 读放大(Read amplification)。为了返回所需的值,在IO数量和大小上需要多少额外的数据读取?定位数据从索引查询开始。如果没有细粒度的索引,读取需要扫描整个块。如果块中有重叠的范围,就会涉及更多包含陈旧数据的块。理想情况下,如果任何数据都可以被准确定位,甚至不需要扫描。读取放大有可能 通过批量查询来摊薄(amortized by batching queries),但要付出延迟的代价。

  • 写放大(Write amplification)。要写一个数据,需要多少数量和大小的额外IO?如果空槽太小,数组中的就地更新(In-place update)会引发连锁数据移动。仅追加(Append-only)的系统会触发后台写来进行 GC/Compaction。后台作业也会进行重写。写入放大有可能被 推到离线进行,离开用户写入路径,以空间放大为代价。删除可以被看作是一种特殊类型的写。注意数据移动造成的写放大也伴随额外的读。

  • 空间放大(Space amplification)。与纯碎的用户数据相比,查询层额外花费了多少的存储空间?这包括未回收的陈旧(或删除)的数据,为插入预先分配的空槽,页/块内部的碎片,分配无法利用的外部碎片(External fragmentation)。空间放大可以通过更频繁的 GC/Compaction 来减少,但要付出读/写放大的代价。存储空间的目标对云存储的COGS至关重要,后者常以容量为卖点。

  • 顺序化读取(Sequentialize reads)。硬盘受益于连续读取。我们希望下一次的读取能击中之前的预取,多个读取能在一个更大的批次中进行,范围查询能映射到磁盘上的连续扫描。我们希望 数据局部性 在访问模式中得到维持。

  • 顺序化写入(Sequentialize writes)。硬盘/SSD受益于顺序写入。仅追加系统对所有的写操作进行顺序化处理。就地更新系统更难,但通过预先分配的空槽或文件系统的 Extents。

  • 压缩。压缩对于查询层的存储效率至关重要。它还可以通过在读/写中传输更少的数据来减少放大效应。压缩需要与加密一起工作,其中 CBC(chained block cipher)可以随机化相同的块。将相似的数据打包在一起,可以使压缩更有效率(如列式布局)。通过直接查询和传递压缩后的块,可以减少传输开销(延迟物化[331])。通过 SIMD 矢量化和 JIT 编译[332] ,查询变得更加高效。

  • 索引查询(Index lookup)。一个理想的数据布局应该便于索引查找,以服务于读取或寻找写入位置。索引结构和遍历可以嵌入到数据单元中,或者将数据聚集进索引。考虑到有限的索引大小和颗粒度,数据块可以有第二层级的 Min-max Sketching,Zone map,或 Bloomfilters。数据也可以被压缩,支持范围查询,并不需要另外的索引,见 Succinct 数据结构[333]。

接下来,我们定义 数据单元(Data unit),例如,使用多大的 Block、Chunk、文件。我们需要考虑属性(properties)在哪个级别的数据单元上执行(enforce),索引在哪个粒度上发生,数据放置与迁移单元大小,等等。下面从小到大列出数据单元:

  • 单个键-值(KV),或者只有值,如果键可以派生,例如列式布局中的增量行ID。这是最小的数据单位。通常在这一层为每个键值建立索引是非常昂贵的。

  • 行组(Row group)。一个文件可以有多个行组。对于索引来说,它可能仍然太小,但本身带有 Min-max Sketching 和聚合统计。例子是 Parquet[334],或 AnalyticDB 行-列布局。一个行组包含了一组行的所有列,而在行组内部数据被组织成列式。

  • Chunk。我使用”Chunk”来通用地表示最小的可进行索引的数据单元。Chunk的一个例子是RocksDB中的 “SST文件”,读取时首先定位一个 chunk(在这里考虑一个”简陋”的索引),然后对其进行全扫描(可以利用每个行组的 Sketching 进行优化)。另一个例子是B+树索引中的”页”(这种系统通常没有行组),我们需要考虑页内部的记录布局。下一个例子是文件系统中的”Block”,它是由Inode树索引的;或者是”Extent”,分配器分配了一个比需求更大的空间,以追加未来的写入。

  • 分区(Partition)。服务器选择服务的最小的单元。它是数据开始参与分布式系统的地方,也是放置、复制和迁移的单元。

  • 用于分类的数据单元。存储系统需要决定一个数据单元作为 跟踪和分类 的级别。分类(Classification)是存储系统中常见的问题,用于高效的GC/Compaction、温度分层和各种后台工作。机器学习 可以,但使用不多,主要是由于元数据的大小和大量跟踪单元的计算成本。考虑到愿意支付的跟踪成本,分类单元既可以比分区大,也可以比分区小很多。

    • (Generation)。即LSM树或 RocksDB 中的”Level”。它标志着数据已经经历了多少轮 GC/Compaction。它 分类 数据在未来不会被删除/覆盖的可能性。LSM树将更多的属性与代结合在一起,例如分块大小、排序、Compaction 策略。这是一个设计选择,但不是必须的。

    • 温度分层(Temperature tiering)。一个带有统计数据的标签,用于 分类 数据在未来被访问的可能性有多大,基于一定的流量。将冷数据卸载(offload)到更便宜的存储介质的有效方法对存储空间的效率至关重要,而对突然的用户读取的快速响应却需要传输(非对称的)数据单元。在冷/热数据之间分离 GC/Compaction 策略也是有益的。

    • 工作负载流(Workload streams)。存储系统为混合的用户工作负载提供服务。这里的”流”是指将数据操作从单一工作负载中分离出来(例如,单一App,单一内容,单一用户)。这个概念来自 NVMe协议[335],一个例子是 FStream[336]。实际上,”流”将类似的数据组合在一起,以产生更好的压缩、重删,在 GC/Compaction 和温度方面共享相近的生命周期。

在下一个层次,我们抽象出数据布局的 属性。它们制约着数据单元内部/之间的物理数据组织,写要花费维护,读要受益于加速。下面列出了从小到大数据单元的属性。它们将属性映射到高水平的目标。应用属性和它们之间的权衡构成了技术的设计空间(Design Space)。

  • 在键值级别,常见的技术是 将键和值分开(WiscKey)。大多数 Compaction 发生在键上,因此节省了对值的重写放大(这是大的)。另一种技术是 将键的公共前缀去重,这样可以节省存储空间。例如 HBase 中的 “列族”, trie树,和 Masstree。

  • 在行组层面,一个值得注意的属性是数据是以 列格式还是行格式 存储。OLTP数据库倾向于行格式,数据被组织成行,行堆积在一个页面中。OLAP数据库更倾向于列格式,数据被组织成列,来自一列的值连续存储在行组中,然后再到下一列。

    • 列存格式(Columnar format)。由于列中的数据相似,压缩效率更高,从而减少了存储空间,扫描时读取的数据也更少。普通的OLTP工作负载很难在初次写入时生成列格式,因此需要为批处理和重写支付写入放大。然而,查询一列然后在一行中查找另一列,会产生额外的IO和非连续的读取,因为列被存储在不同的位置。常见的列式格式例子有Parquet, Apache ORC[337] 等。

    • 行格式(Row format)。扫描会包含不必要的列,也就是读的放大。与列式格式相比,压缩的效率较低,而且还需要花费读取传输。但是更新/插入可以直接以行为单元操作。查询一行中的所有列只需要花费一次读取。

继续数据布局的 属性。在 Chunk级别 涵盖了许多属性,比如数据是否被 排序(或部分排序),块之间的 重叠跨Chunk链接,允许 就地更新。它们进一步与块内或块间耦合。大部分LSM树的压缩优化都是在谈论这个层次。

  • 排序的,Chunk内。例如 RocksDB 的 SST 文件,或者列式格式的列值,它存储的记录是经过排序的。排序有利于查找记录,允许在范围查询中连续读取,便于建立外部索引或将索引嵌入文件中。然而,由于用户以任何顺序写入,排序后的数据不能从一开始就获得,除非在内存中进行缓冲,或者支付重写的写入放大费用。此外,排序后的数据可以实现更有效的压缩算法,例如,Run-length Encoding(RLE)。

    • 就地更新。同时保持大块内部的排序属性和就地更新是很难的。放弃内部排序,排序属性可以被 放松到块间级别 ,这样读放大仍是可控制的,并且仍然可以建立块级别的索引。为了吸收插入,一个块可以支付存储空间来预先分配空槽,或者支付额外的写操作来将记录转移到其它地方。

    • 索引排序顺序 vs 数据排序顺序。数据库的记录可以看起来是按索引排序的(例如按顺序遍历B+树),但在磁盘上是随机排列的。虽然范围查询通过利用索引排序顺序来节省查找时间,但磁盘上的扫描仍然受到随机读取的影响。为了使磁盘上的数据按排序顺序排列,可以为重写支付放大费用。或者,让索引叶层有更大的块来进行内部的顺序读取,然后跳到下一个块。然而,二级索引很难实现二级键上的数据排序顺序,而这可以通过 Z-Order[338] 来弥补,代价是读取放大。

  • 排序的,跨Chunk。例子是RocksDB中的 “tiering” vs “leveling”[339]。”Leveing”要求各块是 不重叠的,即一个 Sorted Run,或者各块有一个 Total sort order。它使读受益,因快速定位只有一个需要扫描的块。然而,保持大块间的排序属性需要在Compaction 中更急切地支付写入。在”Tiering”中,块可以有 重叠 的键范围。打破”排序”属性可以放松写操作,但读可能需要扫描多个块。

    • 重叠。块可以有重叠的键值范围吗?另一种说法是块之间的排序属性是否被强制执行。

    • 部分排序,Chunk内。例子是 PebblesDB 中的 “Guards”。一个 Guard 包含多个可以重叠的块,但是跨 Guard 是没有重叠的。它在读/写放大之间创造了一个可调整的平衡。

    • 如何向Chunk分配键 在维护块间的排序/重叠属性时很重要。通过将键划分为不重叠的范围(或哈希)并分配给不同的块,它可以确保块之间不重叠。你可以看到 数据分区 不仅是为了扩大规模,也是一种 分离冲突空间 的方法,简化了算法处理。此外,它还 分离了寻址空间,减少了元数据的大小,正如你在 元数据章节 中看到的那样。

    • 固定的/可变大小的块。例如,SST 文件的块是可变大小的,数据库页是固定大小的,而存储系统可以使用固定大小的块或可变大小的块。固定大小的块通常出现在传统的文件系统中,就地更新,然而大小的变化是由分配器来处理的(这可能是很棘手的)。内部碎片(Internal fragmentation)会浪费块内的空间。 可变大小的区块有利于仅追加系统和压缩,因为压缩会产生不可预测的块大小。索引元数据必须更大,因为没有固定大小的跟踪单元。 在这两者之间的平衡中:1)系统可以约束 最小的写入大小,例如 4KB,所以即使是可变大小的块,索引元数据的大小也会减少。2) 通过一个大的”Extent”来分配,而不是单个的块,这样在这个Extent内可以追加固定大小的块,并减少外部碎片。

    • 通过索引补偿。拥有索引可以缓解块的维护。如果块是指B+树的索引页,它需要同时维护非重叠和固定大小的块属性。这是通过 键到块的分配 来完成的,由索引本身来保护。在有重叠块的情况下,不需要扫描所有匹配的块,全局树/哈希索引可以告知某个键是否存在于某个块中,就像 Bloomfilters 那样(这是常用的)。总而言之,索引可以补偿读放大

  • 跨Chunk链接。例子是LSM树中的 Forwarding pointers,L-1级的块可以嵌入指向L级块的指针。当一个读取扫描了 L-1 级但没有找到匹配的记录时,它可以跟随转发指针到 L 级,这就省去了扫描的重新开始。例如,REMIX LSM树[325]。本质上,该方法是在块间层 嵌入一个索引。请注意,我们也提到了 在块内部的索引,或者一个单独的 外部索引。从概念上讲,索引是利用数据之间的 联系 来建立的,当块在LSM树级别上有重叠的键范围时,这就会发生。

  • 数据局部性(Data locality)。一起被访问的数据应该在物理上接近,以便读取时能够获取所有的数据。这可以发生在节点/分区级别,以节省跨网络带宽;在同一 Chunk/Block 内作为一个单元被缓存,或者在邻近记录中排列,以利于预取(Prefetch)。一个例子是图数据库,其中边和顶点常按遍历顺序逐一访问。

    • Data clustering & data skipping[340],就像它们的名字所说。数据聚类意味着经常一起访问的数据应该被物理地打包在一起,所以它们可以在一次顺序扫描中被预取或返回。当试图打包不同的数据库表字段时,这就变得很棘手。一个例子是 Z-Order。数据跳过(skip)与数据聚类相反,它试图在磁盘扫描期间跳过尽可能多的不必要的数据。它利用内嵌的 Sketching 过滤器或索引,避免聚集太多无关的数据。

在分区和分类(Classification)级别的数据单元层面上的其他数据布局 属性

  • 分区级。它映射到 Chunk 级别以服务单个查询。还有一些其它的”大尺度”属性:

    • 复制放置 影响到在分布式系统层面上的查询服务方式,但在 数据分区章节 中讨论更为恰当。Colocation 将一起使用的数据放在同一个节点上,以利于预取和节省网络成本。

    • 互操作性(Interoperability)。数据胡,例如 Delta Lake,对其内部数据、元数据和事务日志都使用开放的格式(Parquet, Apache ORC, JSON, CSV)。这允许任何其它应用程序进行互操作,并允许在云端其它地方启动一个新的服务器来恢复处理。

  • 分类级。它映射到单个或一组类似的块作为跟踪单元。分组既可以是物理上的,将块定位在一起,也可以是逻辑上的,用元数据跟踪类似的块。

我们可以通过探索两个极端,即写优化布局和读优化布局来总结数据布局 属性。我们可以调整属性来观察这两者之间的过渡。

  • 写优化的布局(Write-optimized layout)。新的更新/插入的数据被依次追加到日志的末尾,没有任何特殊处理。写入路径的成本最低。

  • 读优化的布局(Read-optimized layout)。块被以键值为单位进行完全的索引。块的内部是排序的,不重叠,并且足够大以避免范围查询引发的碎片化IO。如果没有太多的跨列查询,则采用列式布局。经常一起访问的字段被打包在一起。

  • 从写优化的布局过渡到读优化的布局。应用各种属性,我们可以观察到三种趋势:1)引入排序顺序,2)减小跟踪粒度,3)将类似的数据分组。

    • 引入排序顺序。一个查询需要利用排序顺序来更快地定位数据,并跳过不相关的数据。磁盘上的访问也从顺序读取中获益。排序也有利于压缩,如 RLE。块内部排序通常由重写完成。块之间的排序可以从松到紧,通过 Guard 或索引实现。

    • 减少跟踪粒度。其好处在于索引和 Skipping。通过更小的粒度定位和过滤掉更多不相关的数据,查询可以节省更多的读取开销。元数据的开销是一种权衡,低级别的索引部分/Sketching/统计信息可以嵌入到块中,而不是保留在内存中。块可以切得更小,大小更平衡,并在行组级别嵌入多样性。

    • 将相似的数据组合在一起。例如,将键和值分开,将单列的值分组的列格式,将数据按生命周期分组的代或LSM树的 Level,将冷/热数据分组的温度分层,将单个工作负载的类似数据分组的工作负载流。这样的分类,无论是基于类型规则、统计还是机器学习,都能有效地在任何地方发挥作用,例如压缩、扫描、GC/Compaction、生命周期相关的数据移动。

更多关于优化布局的内容:

  • 空间优化的布局(Space-optimized layout)。空间放大对云存储COGS很重要,但关注相对较少。写入优化的布局会因为未回收的陈旧数据而损害空间效率。读取优化的布局会损害空间效率,如果它在页、块或预先分配的空槽中保持内部碎片。高效的压缩也是需要的。空间优化布局可以是一个具有紧密记录排列的列式布局,这似乎可以和读优化布局一起实现。如果我们接受重写,我们也可以通过写优化布局吸收新写入的数据。

  • 平衡优化的布局(Balanced-optimized layout)。考虑到 GC/Compaction 的成本,我们很难同时实现写/读优化。一个平衡的布局是值得的,而只有在针对目标应用负载定制后才是 优化 的。这本质上是一个 机器学习 的问题,其中 Optimal Column Layout 探索了就地更新的策略(binary linear optimization)。

垃圾回收(GC)/ Compaction

GC/Compaction 在仅追加系统或LSM树系统中很常见,而且相当耗费带宽。就地更新系统也可以使用 Compaction 来生成读优化的布局,如果一些新的值被暂时写在别处(Out-of-place writes),则需要GC。我选择混合使用 GC/Compaction 的名词,因为两者都可以回收陈旧/删除的数据。在LSM树中,Compaction 可以代替 GC,而如果索引/Bloomfilter/Versioning告诉你哪个键是陈旧的,GC 也可以不用 Compaction。

以下是高效的 GC/Compaction 的典型设计目标,它们与数据布局的目标相对应:

  • 要足够及时,以减少用户读取时产生的 读取放大

  • 足够及时,以减少 空间放大

  • 减少对 写放大 的支付,无论是在写路径执行还是在后台离线作业。

  • 尽可能安排 连续的读连续的写

  • 花费合理的 CPU/内存/磁盘 资源。减少与用户流量的竞争或增加延迟。

GC/Compaction的设计空间由一系列的”旋钮”(knobs)组成,选择何时和如何运行:

  • 大小的粒度(Size granularity)。为 GC/Compaction 选择的数据单元有多大?它可以是一个单独的块,一组块,或者LSM树中一个 Sorted Run 或 Level 的所有块。一个块可以被配置成小块或大块。从本质上讲,在更大范围内强制执行排序顺序意味着相应的更大的Compaction 粒度,这有利于读取,但维护成本更高。大颗粒度的跟踪元数据的成本较低,但会对不必要的数据产生更多的重写。

  • 选择候选者(Selecting candidate)。哪一个块要和其他的块一起 GC/Compaction。如果 GC/Compaction 运行能移除最陈旧的值,或者有更多可能重叠数据的块,则效率更高。一个新的块也可以被推迟处理,以积累更多的陈旧值。适当的索引和统计跟踪可以在这里花费。选择最佳的候选者可以优化 GC/Compaction。

  • 何时触发。什么时候开始运行 GC/Compaction?可以是存储空间被填满,某些LSM树级别达到最大尺寸,一个块积累了足够多的老旧数据,周期性定时器被触发,最近的读/写成本达到报警或因待处理工作而停滞(Stalled),或者用户流量足够低。它们的目标是主动维护系统属性,同时尽量减少对用户活动的影响。

  • 在哪里运行。传统上,GC/Compaction 需要在本地节点运行,以节省网络/磁盘传输。然而,在共享存储中,其他节点可以同时加载块,以横向扩展计算能力。他们也可以有额外的复制副本来平衡读取,或者用智能SSD来在硬件中计算。此外,GC/Compaction 可以将数据存储在云存储中(如 AWS S3),分解存储组件(Disaggregated 如 Snowflake),并将计算卸载到云端(如 Remote Compaction)。

基于分类的数据单元(前面提到的),如代、温度、工作负载流,可以应用不同的 GC/Compaction 策略(上述)。

  • 。即LSM树或 RocksDB 中的 0、1、2、…N 级。每个级别通常被配置为不同的最大尺寸、大块尺寸、GC/Compaction 频率。他们也可以使用不同的分层和分级策略。一个例子是 Dostoevsky 的 Lazy Leveling。粗略地说,较低的层级会产生更多的写放大,因为他们更频繁地压缩,而较高的层级会产生更多的读放大,因为要扫描大块。

  • 温度分层。延迟热数据,把它们保存在内存中,然后在一次运行中积累更多的陈旧值来进行 GC/Compaction,这是有益的。冷数据应该和热数据分开,以避免污染和被迫完全重写。GC/Compaction 可以更不频繁地在冷数据上运行,因为它们的活动较少。一个例子是 TRIAD[341]。

  • 工作负载流。它将具有相似温度的冷/热水平的数据分组。具有相似生命周期的相关数据更有可能被一起删除/更新,所以在一次 GC/Compaction 运行中被回收。一个例子是 Google Drive 中的文件,一个文件作为一个整体被删除,但是把不同文件的块混在一个块中会导致生命周期的碎片化。

HyPer Scalable MVCC GC paper[342] 也给出了另一种类似的 GC 设计分类(针对数据库 MVCC 版本):跟踪级别,触发频率,版本存储,识别过时版本,删除垃圾版本。

HyPer paper GC categorization

压缩(Compression)

列存格式以压缩的方式组织数据,压缩算法还允许直接读取记录而不需要解压。下面的 压缩算法选择分类法[343] 不仅反映了数据中的共同属性,也反映了压缩后的数据组织方式,以便有效地进行查询。

在列存格式中使用什么压缩模式

上述内容被归类为 “列存压缩”。有更多的压缩算法系列可用于存储系统。它们可以归类为以下几种

  • 基于块的压缩(Block based compression)。经典的日常使用的压缩算法,LZ77[344] 是大多数的核心:ZIP, LZ4, LZ* 家族, GZIP, DEFLAT, 7-ZIP, RAR, Zstd。LZ77 做了 dedup,其中文本 Token 互相指向,构成了一个虚拟的字典。LZ77 通常与 反熵阶段[345] 一起使用,以进一步缩短比特表示(bit representation),见”反熵压缩”。

  • 显式地基于字典的压缩(Explicit dictionary based compression)。数据库压缩字符串值可以专门使用像 FSST string symbol table[346] 这样的算法,其核心是一个查找字典。Zstd 也提供了一个 Zstd dictionary mode[347],在这里可以提供一个预先训练好的字典来压缩小文件。

  • Succinct 数据结构(Succinct data structures)。我们之前提到过它。此外,LZ-End[348] 是一个在研究中被认可的有趣算法。它略微修改了 LZ77,以支持随机访问而不需要块解压,但却需要额外的地址跳转的查找。

  • 列式压缩(Columnar compression)。列式数据库使用它来压缩列。这些算法家族在上面的分类图中显示。这种压缩假定列数据共享相似性。它们通常支持索引点查找,扫描,查询过滤,不需要解压(Late Materialization)。

  • 反熵压缩(Anti-entropy compression)。与LZ77系列一起使用,这些算法根据条目的频率来选择比特表示,因此总的比特长度更短。例如,Zstd 使用 LZ77 + FiniteStateEntropy[349]。

数据索引

数据索引通常驻留在内存中(即DRAM),并向前链接到数据。虽然驻留在PMEM中是可能的,但到目前为止,它仍然比DRAM慢。数据索引源于标准的教科书式的数据结构,在工业应用中演变成更复杂的结构,并在分布式系统中扩大规模。它们为读取查询提供服务,指出写入的位置,并承担成本以保持数据的一致性。

数据索引章节

数据索引属性

我们可以总结一下数据索引中的常见属性。它们组成了设计空间和各种可用的技术

  • 结构。一个数据索引通常有一个基础结构,即树、哈希表、列表。

  • 排序顺序。例子是树与哈希表。基于树的索引通常保持数据之间的排序,从而实现范围查询。哈希表因O(1)的查询时间为人所知,但是全局排序会丢失。虽然基于树的索引很难同时保持二级键(Secondary key)的排序,而哈希表可以通过额外维护一个树状索引来跟踪排序。换句话说,将多个索引结合 在一起是合并读取属性的一种方式,但在更新上有代价。

  • 点查询。所有的数据索引都支持点查询,通常时间成本从O(1)到O(log(n))不等。本质上,与内存大小有一个取舍:1)如果整个键空间可以放在内存中,我们只需要一个巨大的数组来映射任何键到它的值。2) 哈希表折叠了映射空间,用哈希作为映射内核,因此需要更小的内存大小。新的空间必须足够稀疏,因为映射中的平衡程度不可预测(除非完美散列[350])。3) 来到树上,键是由相互连接来索引的,而不是由地址映射来索引的,因此需要更小的内存大小。

  • 范围查询。保留排序顺序的数据索引可以支持范围查询,通常是树。否则它需要一个全扫描,除非应用Guard/Segmentation来保留部分排序,跳表可以被看作是一个例子。另一种理解范围查询的方法是,一个数据索引必须支持查找一个键的邻居,即使这个键本身并不存在。

  • 更新/插入/删除的成本。索引本质上是对数据组织方式的约束,这意味着必须在写入路径上花费成本来维护这种约束结构。链接结构更容易插入,而紧凑的数组如果没有空槽,就必须移动数据。此外,还有一个额外的成本可以花在写路径中/外,以:1)重新平衡数据结构,以减少尾部延迟(例如,红黑树旋转)。2)处理寻址冲突(如哈希表)。3) 空间扩展或缩小(例如,当哈希表满时,扩展2倍的数组大小,或同样地缩小)。4) 垃圾收集/GC (例如,基于Epoch的内存回收) 5) 紧凑的deltas (例如,Bw-tree页面deltas)。

  • 只读。一些数据索引,例如SuRF[181],不支持更新,除非是完全重写或非常昂贵的操作。这样的索引可以被打包在连续的数组中,高度压缩以利于内存的大小;并通过精致的Interleave来加速(范围)查询。

  • 顺序读取。当跳转到查找键时,当接下来访问邻居键时,以及扫描范围查询时,内存访问是否更有顺序性?这影响了CPU的缓存效率,其中基于树的索引通常比哈希表做得更好。另一个维度是磁盘上的顺序读取,如果索引有磁盘上的组件。

  • 顺序写入。在磁盘上写数据是否遵循顺序访问?一个典型的例子是B+树与Be-树。B-tree在中间节点缓冲小的写入,按顺序冲到磁盘上。甚至可以用仅追加的日志来缓冲DRAM索引的更新,以便按顺序批量Flush它们。LSM-tree可以被看作是另一种类型的索引,以实现对磁盘的仅追加的顺序写入。

  • 缓存亲和性(Cache affinity)。当数据索引被访问时,对CPU缓存的效率如何?常见的衡量标准是缓存缺失、IPC(每周期指令)、分支预测缺失、流水线停滞、内存等待和内存写入(与CPU寄存器中)。典型的技术包括。1)在数据结构中嵌入指针,而不是使用另外的节点结构。2)紧凑数据结构,使之与高速缓存线保持一致。3) 避免False Sharing[351]。4) 利用连续的数据结构。

  • 索引内存大小。数据索引需要多少内存,或者通常是数据存储的元数据大小。基于树的索引受到跨节点指针大小和节点内碎片的影响。然而,哈希表需要留下空槽以避免冲突。一个例子是ART树,当占用较少的时候,它可以定制较小的节点大小。其他技术有 1) Pointer swizzling,将数据紧凑排列到指针的尾部位。2) 根据最大记录数,将指针替换为较短的比特ID。3)数据分区以减少地址空间,从而减少指针的大小。更有效的方法是解耦和扩展,见元数据章节

  • 并发性。例如B+树中的Lock Coupling[352],NOVA中的每个节点日志,以及Bw树中的页deltas。常见的技术有。1) 更有效的共享/意向/独占锁协议,更小的锁颗粒度和持续时间。2)数据分区,因此多个锁可以并行工作。3) 无锁数据结构,但需要仔细设计,以应对高竞争情况。4) 对称并行多副本,即把空间分割成不交织但相同的处理流程,例如,每个磁盘一个线程甚至不需要锁,例如针对不同文件的请求。

  • 压缩。键可以去重常见的前缀以节省内存大小,例如Trie树或 Masstree。少子的节点可以合并为一个(路径压缩)。持有较少成员的节点可以削减其容器大小(如ART树)。大的B+树节点也可以压缩其内容。冷页面甚至可以采用内存压缩或卸载到磁盘。Succinct数据结构可以压缩数据,并为搜索和范围查询提供同样的支持,而不需要一个分离的索引。

  • 模糊性(Fuzziness)。数据索引可能会返回错误的结果,例如Bloomfilter, SuRF。允许不准确的结果使得新的高内存效率的索引家族得以产生。它们也可以被看作是Sketch Structures[353],例如Min-max Sketching,DB数据块中常用的Zone Map。

  • 数据聚类(Data clustering)。就像索引可以作为前向指针嵌入到数据块中一样,数据也可以被聚集到索引中[354]。这意味着数据读取在遍历了索引后少了一次读取,并且数据与索引的物理顺序一致。”聚集索引”是一个数据库术语。

  • 磁盘组件。有两个层面 1) 索引的工作部分是否完全驻留在内存中?2) 节点重启后,索引是如何从磁盘恢复到内存的。

    • 磁盘工作部件。一个普通的哈希表,跳表,ART树只驻留在内存中。然而,B+-tree有较低级别的页面驻留在磁盘上,并根据需要加载到内存。Bw-tree也可以将页的deltas冲到磁盘上,并用链接指针来跟踪。一般来说,磁盘的传输单位是页。 但是,这个问题可以用另一种方式来思考:索引的哪些工作部分驻留在CPU缓存中,哪些在内存中?因为缓存硬件隐藏了大部分的复杂性,所以讨论更关注于CPU缓存效率和多核并发。

    • 磁盘恢复。一个Naive的方法是将索引上的每一个操作都以仅追加的方式记录到磁盘上。然而,在重启时重放(数天)完整的日志是太慢了。第二种方法是保留短期的日志,并定期将检查点刷入磁盘。这就是LSM-tree所做的。这种方法也出现在B+树和数据库中,页面按需同步到磁盘(也被称为检查点,但不需要一次完全刷盘),恢复遵循更微妙的ARIES协议。

流行的数据索引

有相当多的封装良好的数据索引在工业界广泛使用。下面简要地列出它们。它们是参考架构和技术的来源。数据索引中的优化很重要。

  • 哈希表。普通的哈希表在DRAM索引、PMEM和数据库哈希索引中还很有用。当地址冲突时,哈希表会有所不同,如何选择下一个地址,以及如何增加冲突的键。冲突键的第二层容器也值得优化。哈希表可以同时使用一种、两种或几种不同的哈希算法,并针对它们进行App级的特化。平滑的容量扩展和收缩是另一个优化点。数据分区有助于减少冲突空间和缩短地址指针(例如Kangaroo)。

    • 已知的哈希表有 Cuckoo hashing[355],它在两个哈希表之间跳动,HotRing[356] 将热键切换到前面,Consistent Hashing[357] 和 Ceph CRUSH map,Level Hashing[256]

    • 特别是,如果一个服务器离线,Consistent Hashing 会遭受负载不平衡,它的键被分配给环上紧邻的服务器。如果每个服务器在一致性哈希环上有多个点,这个问题就可以得到缓解。

  • 跳表[358](Skiplist) 首先是一个保留了数据排序顺序的列表结构。为了加快查找速度,它增加了几层新的列表,每层都通过跳过更多的键来增加稀疏度。本质上,它就像一棵树,但节点是横向连接的。他们的数据索引被用于Redis和RocksDB,以其简单性和高并发时的性能而闻名。以另一种方式,排列在连续内存中的列表也可以用于索引少量的数据,其中排序顺序被保留,并且使用二分搜索。一个例子是排列在B+树节点中的值数组。

  • Radix树[359] 被用于Linux内核的内存管理[360],以及NFS和NOVA的节点索引。Radix树是一个将路径压缩到最多的Trie树,因此每个节点的孩子数量都映射到下一级数据的多样性(即radix)。它也可以被看作是一个碎片化的数组,大的连续数组被分成几个小段,这些小段被树中下一级的另一个小数组所索引。

  • 红黑树[361] 是C++有序map的标准实现。它是一个二分搜索树,具有高效旋转的自平衡性,但又不会太严格而损害更新性能(而不是AVL树[362])。

  • B+-tree。普通的数据库索引,但仍被证明在存储、PMEM、缓存中广泛使用。B+-tree 使自身平衡,使用紧凑的大节点来限制树的高度,这映射到磁盘的读取,并通过遍历链接保留排序顺序。B+-tree完全基于页工作,这简化了DB中的磁盘数据传输和内存管理。B+-tree共享各种访问效率、存储空间和锁定并发的优化。

    • Steal, no force。这些术语来自ARIES[365]协议,用于数据库事务的恢复(在阿莱克西斯ARIES[306]文章中得到很好的解释)。他们在内存和磁盘之间进行DB页面同步。”Steal”允许DB将未提交的事务页冲刷到磁盘上,从而引入了Undo日志的需要。”No force”允许DB不把已提交的事务的页面冲刷到磁盘上,因此需要从故障恢复中获得Redo日志。”Steal, no force”不仅提高了DB在大型事务上的性能,而且使DB缓冲区管理与事务和索引解耦的组件。

    • B+树锁技术[366] 将DB概念 “Latch”(用于数据结构)与 “Lock”(用于事务)分开。它引入了广泛使用的技术 “Lock coupling”。B+树的并发设计空间包括:1)SIX锁,它引入了意图锁,可以等待正在进行的写操作完成;2)Lock coupling,通过锁走过父/子节点,限制上锁范围;3)Blink/OLFIT树,支持基于版本的OCC无锁读和有锁写;4)Bw树,是无锁和delta页的仅追加的。(在数据库内核月报B+tree[304]中有很好的解释)。

  • 流行的数据索引用于内存数据库和PMEM。它们本质上源于B+树,并且经常出现在论文和行业产品中。由于它们已经在参考架构章节中有所涉及,这里做一个简单的介绍。

    • ART tree 被用于HyPer内存数据库中。它是由 Radix 树,像Trie树一样去重键前缀,来构建的,并通过适应几个不同的记录数的节点大小而使空间高效。节点本质上是一个固定长度的数组。叶子节点可以内联存储值。路径压缩是对具有单个子节点的节点进行的。ART树支持范围查询。

    • Masstree 在B+树上集体应用了许多优化技术。Trie树被用来去重共同的键前缀。接下来要访问的节点在重叠的流水线中从DRAM预取到CPU缓存。操作可以在树节点上并发进行,而读是基于版本的OCC无锁的,写是持有一个锁。更多细粒度的优化可以在Masstree论文中找到。

    • Bw-tree 是一个无锁的B+树变体,用于Hekaton和DocumentDB。它仅附加页面delta,然后需要Compaction和基于Epoch的内存回收。它采用了一个页面映射表来避免递归传播COW页面更新到根部。页面映射表也使许多需要切换页面指针的原子操作成为可能。Bw-tree的页面delta可以被增量地刷到磁盘,这使得它可以与只附加的LSM-tree相媲美,后者对写入进行缓冲和顺序化。

    • Be-tree 是B+-tree的变体,与LSM-tree相比,减少了随机写入。与B+-tree相比,小的写入在节点中被缓冲,并在满的时候被刷新到较低的级别。通过这种方式,小的写入是分批进行的,而磁盘写入大部分是顺序进行的。与LSM-tree相比,Be-tree在数据组织上仍然保持了B+-tree结构,以支持最佳的读取性能。

  • 其他类型的索引。以下是一些在特殊使用情况下很有用的索引。

    • 位图索引[367] 在数据库中使用。与B+树相比,当一个列的cardinality(不同值的数量)较低时,它就变得适用。它与列式布局中的Bit-vector压缩[368]的工作方式相同。

    • 倒置索引[369](Inverted Index) 在全文搜索引擎中使用,例如Lucene, ElasticSearch,通过词来查找文档。词的权重可以通过TF-IDF[370]得分来评估。页面或文档的权重可以通过PageRank算法[371]进行评估,这一点从Google那里得知,而PageRank是页面链接矩阵的特征向量[372]。倒置索引在数据库中被广泛采用,因为更多开始支持全文搜索。

分布式存储中的数据索引

我们在这里讨论一些关于数据索引的次要话题

  • 数据索引如何在分布式系统中横向扩展? 通常情况下,基于树的索引可以在一致性核心中托管其上层,并在集群中自然扩展下层。哈希表和基于列表的索引可以通过对范围的数据分区来实现扩展。

  • 如何在数据更新时保持索引的一致性? 我们已经在一致性章节讨论过这个问题。本质上,它需要一个分布式事务来交织数据和索引。如果索引是分区的,并且完全与同一节点上的数据共存,一个本地事务就足够了。索引也可以以最终一致的方式接收数据更新,而版本可以引导用户了解传播的进度和快照的隔离。第三种选择是为旧数据建立完整的索引,而新的增量delta数据在没有索引或廉价索引的情况下运行。通常情况下,索引可以作为另一个普通的数据库表来实现,以重复使用数据结构和事务。

  • 如何建立二级索引? 我们在参考架构中提到了一些二级索引,它们趋于最终的一致性。一个典型的数据库可以通过支付数据更新的事务成本来支持二级索引。在分布式存储上,有两类二级索引

    • 全局二级索引 在二级键的全局空间上建立索引。它需要一个分布式事务来一致性地更新。然而,如果把它当作一个普通的数据库表,可以容易地重复使用代码。

    • 本地二级索引 在每个数据节点上都建立了一个本地索引。每个索引只覆盖本地空间,而不同的数据节点可以有重叠的二级键,但不被索引知道。该索引只需要一个本地事务就可以持续更新本地数据。然而,查找一个二级键需要查询所有的数据节点。考虑到也有数据库选择按行的哈希分区,运行并行查询可能不是那么糟糕。如果一个节点的Bloomfilter告诉它的键不存在,它可以跳过查询。

Succinct数据结构

Succinct代表了一个具有有趣的 自我索引 属性的数据压缩算法系列,见下文。我为它添加了一个专门的章节。它们非常符合DNA索引和搜索的用例。它们也可以用于内存索引,以及在支持数据库查询的同时压缩内存数据。

  • 压缩后的大小接近于 熵的极限Succinct wiki[373])。也就是说,压缩率接近于经典的基于块的压缩。

  • 支持点/范围查询,特别是文本搜索,在压缩的数据上 就地 进行,没有分离的索引,但性能接近于使用索引,比全扫描快得多。支持文本搜索对于DNA测序是很方便的。

    • 也就是说,Succinct可以用来替代内存中的索引,尤其是二级索引。此外,Succinct还可以压缩数据。
  • 在Succinct数据结构中的查询通常涉及其内部数据结构中的几个 地址跳转(如 Compressed Suffix Array)。这对于内存中的索引/压缩是可以的,但对于磁盘上的数据压缩可能就不那么方便了。此外,顺序读取的吞吐量/读取大块的数据,可能也是一个问题。

  • 与经典的基于块的压缩相比,Succinct数据结构通常是 建立速度较慢。一旦建立,它通常 很难修改。虽然支持各种查询,但Succinct数据结构在顺序扫描时可能 很慢

    • 在面向列的数据库中,常见的 列压缩 算法(如RLE)是Succinct数据结构的有力替代。列式压缩算法也支持直接执行DB查询。他们也更容易和更快地进行修改。它们在数据库中得到了更广泛的采用。

有几个最常用的 Succinct 数据结构

  • FM-index[374]是一个流行的和通用的Succinct数据结构。它是基于Burrows-Wheeler Transform[375](BWT)。它的工作方式与CSA接近。

  • Compressed Suffix Array[376] (CSA)是从一个不同的领域建立的。但它最终会收敛到一个非常类似于FM-index和BWT的数据结构。本质上,它跟踪输入字符串的后缀并对其进行排序。从每个后缀中提取尾部字符和前部字符,并足以重建原始输入字符串。提取的字符是经过排序的,因此可以有效地进行压缩。文本搜索是基于对这些字符的匹配。当点查找需要地址偏移时,CSA需要存储它们,但使用采样来减少存储开销。

  • Succinct Trie[377] 是一个以比特编码的Trie树。Rank & Select 原语被用来遍历树的父/子部分。这些原语可以被优化,以便更快地执行。Succinct Trie 通常被用作压缩的索引。

有几个值得注意的Succinct数据结构的采用

  • TerakaDB/ToplingDB[378]中的压缩索引。ToplingDB使用Succinct Trie (CO-Index)来为RocksDB的键进行索引,而磁盘上的数据则由PA-ZIP进行压缩。PA-ZIP支持对压缩数据的随机访问,而不需要对整个数据块进行解压。PA-ZIP不使用Succinct数据结构。

  • Spark RDD[379] 增加了一个基于 Succinct 的实现。它是压缩的,并支持文本搜索和文本出现次数。它发布了GitHub AMPLab/Succinct[380]和一篇SuccinctStore论文[376]。

  • GitHub simongog/sdsl-lite[381] 是一个著名的Succinct数据结构的开源实现。该实现是高效的,主要用于研究。

  • DNA测序。在一个巨大的压缩DNA数据库中搜索一个子序列是很方便的,而且正好与Succinct的工作相匹配。请看基因组压缩[382]的一篇实例论文。LZ-End[383]也是一个著名的算法。

数据缓存

数据缓存解决了数据组织中的性能层问题。它利用了数据热度的倾斜性(Skewness)和时间访问的局部性(Locality),将昂贵的小容量存储介质与快速访问进行权衡。互联网服务通常大量利用缓存(如 Redis)来托管大多数用户数据。这里首先通过对数据缓存的不同属性(Properties)进行分类来绘制数据缓存的设计空间(Design Space)。

数据缓存章节

  • 存储介质(Storage media)。占主导地位的缓存设备是 DRAM,例如 Redis、Memcached,置于缓慢的磁盘访问上层。后来,SSD 被整合到缓存设备中,以利用其更大的容量、缓存的热重启、以及比 HDD 更快的速度。PMEM 新近出现,主要用于写入暂存(Write Staging),卸载冷内存,或者作为文件系统和数据库的快速持久内存。在云原生场景中,例如 Snowflake,本地 VM 中的 Ephemeral HDD 被用来缓存从远端 S3 获取的计算结果。

  • 耐用性语义(Durability semantics)。通常情况下,缓存是在其它地方保存的数据的 副本,失去缓存对耐久性没有影响。然而,缓存分层(Cache Tiering,例如在Ceph中)要求缓存是持久的,例如3个副本中的1个副本被迁移到缓存中,而把另外2个副本留在慢速存储中(例如 HDD,应用纠删码)。写入暂存 也需要耐久性,同时它被用来吸收最近的写入,进行数据去重和顺序化,并为后续的读取缓存最近的数据。内存缓冲区(Buffer)在编程中常见,数据在处理前需要从磁盘加载到内存,它是易失的(Volatile)。它在流处理中也被用来缓冲和合成中间结果(例如在 Redis 服务器中),其中的耐久性可以通过 RDD[248] 来加强。

  • 缓存的内容和颗粒度(What to cache and the granularity),从小单元到大单元。存储/数据库通常缓存 (block) 和 。Memcached 缓存了键值对,而 Redis 缓存了 数据结构,如列表、集合、映射。分层(Tiering)系统可以移动更大的 (chunk) 或 文件。数据库也可以在语义上进行更多的缓存,即 表中的行(table rows),它比原始块包含更高密度的用户数据;查询结果物化视图,它们缓存数据以及计算工作。查询优化器的结果 也可以被缓存,其中参数化查询是常见应用。内存数据库 可以看作是对整个数据库层面的缓存。

  • 在哪里托管缓存。例如,使用一个分离的系统,卸载到另一个服务器节点,在另一个进程中运行,或者嵌入到本地 App 中。

内存缓存

在内存中缓存数据,本质上是如何用 DRAM 索引来管理数据。我们在 数据索引章节 中提到了这一点。典型的数据结构是哈希表和树。此外,还可以采用内存压缩和冷卸载来扩大容量。有一些设计属性需要考虑,我们在下面总结。它们也应用于 SSD 缓存。

  • 缓存分区和复制。通过哈希分区来扩大缓存的规模是很常见的。但是,如果客户端不得不分割请求,该方法就会被 IOPS 所限制。例如,一个大的请求必须分成两个小的请求,因为查询的键被哈希映射到两个服务器。复制在此时变得有效,它可以分散一小块非常热的数据的负载,它也被用来节省跨区域的查询。负载平衡 可以通过分区/复制来完成,而热/冷的再平衡迁移通常没有必要,因为缓存是易失的。

  • 缓存预热。一个新重启的缓存节点需要运行一段时间来填充热数据。冷重启会影响严重依赖缓存的系统的性能。对于热重启,缓存进程可以在退出前将数据卸载到磁盘,从其它缓存节点回填,或者让共享进程在重启时暂时保留其内存。

  • 条目逐出(Item eviction)。这些方法与存储的温度分层相似,在 写入路径章节 已经提到。此外,缓存可以被设计成在新的条目完全到来之前永远不会被逐出。级联过期或失效 应该被避免,一大群的缓存条目被逐出,容易迅速推高查询存失效率(Cache Miss)并影响延迟。

  • 传播更新和失效(Propagating updates and invalidation)。更新和失效对于保持缓存与底层持久化存储的一致性是必要的。然而,对于 N 个缓存节点和 M 个应用节点,N*M 的连接数是不明智的。可以引入一个中介模块或一个中央消息队列,并合并消息。缓存也可以订阅数据库的变更日志来更新自己(共享日志系统)。

管理缓存和持久化存储之间的一致性有下面几种方法。Facebook 的 Memcached/TAO 论文有丰富的讨论。

  • 对于 读取一致性,一个典型的方法是 Cache Aside。应用程序首先从数据库中读取,然后将条目放入缓存。当更新数据库时,应用程序负责使缓存中的条目失效。从缓存中读取一小段时间的陈旧数据是可以容忍的。跨区域的缓存一致性可以通过主从复制,以及维护更新和失效的顺序来实现。面对跨区域的滞后,用户可以通过因果一致性要求看到其最新的更新,这可以通过缓存项的版本跟踪来实现。

  • 对于 写入一致性,同样的典型方法就是上面的 Cache Aside,或者称之为 Write Through[384]。缓存也可以完全隐藏后台的持久性存储,它将接受所有的写,并保证持久性(例如,缓存作为写暂存,或代理)。当一个缓存写回持久化存储时,需要考虑 维护写的顺序(Write Ordering)。一个反例是,日志提交(Journal commit)比日志数据更早被刷盘。

  • 对于 多键一致性(Multi-key consistency),这个问题分解为原子写和原子读。两者都可以通过用缓存条目标记版本来条目加强,检测不一致性并应用缓解措施。与持久性存储的一个关键区别是,缓存可以 先是不一致,然后检测和修复,而持久性存储必须保证数据的一致性。

SSD 缓存

SSD 缓存也使用 DRAM 作为第一级缓存,并将冷数据卸载到 SSD。DRAM 的索引通常是哈希表或 B+ 树。新的挑战来自于管理更大容量的 SSD 带来的更大的索引尺寸、处理 SSD 的重写和垃圾回收、管理 SSD 上的条目逐出,以及管理 SSD 的磨损问题。它们有下面一些设计属性。

  • SSD 缓存结构。有下面几种方法。SSD 缓存与 CPU 和 DRAM 之间的硬件缓存有相似之处,同时也与存储有共同的属性。

    • Set-associate 缓存,例如 Flashcache 和 Kangaroo 中的 KSet。Set-associative 缓存限制了条目在缓存行中的自由度,因此需要很少的内存来承载索引(与哈希表同等级)。

    • 仅追加存储(Append-only storage),例如 Kangaroo 中的 BCache 和 KLog。缓存条目被依次追加到磁盘上,并被组织在一个更大的桶中作为垃圾回收的单位。

    • 键值存储,例如,使用 RocksDB 来管理 SSD 数据。然而,RocksDB 并不是被设计用来作为缓存的,磁盘的点查没有索引,删除的空间在经过多层 Compaction 后释放的速度太慢。缓存与持久化存储的第二个关键区别是,删除更频繁

  • 管理索引的大小。一个普通的方法是设置 较大的页面大小,而缓存条目可以分为 小对象和大对象,例如 Kangaroo。大对象的数量较少,因此可以使用完整的 DRAM 索引。小对象将大部分的 SSD 容量分配给 Set-associative 缓存,它产生的索引内存很少。它叠加了一个更有效的 仅追加存储 来利用批处理,其使用了 有限的 SSD 容量,因而 DRAM 索引大小很小。进一步的 元数据大小减少 技术,如 “索引分区” 可以被应用。Bloomfilter 是另一种提高内存效率的技术,用来判断一个条目是否存在于磁盘上。

  • SSD 垃圾回收Set-associative 缓存 有巨大的写入放大。一个缓存行被设置为 与闪存页对齐。覆写一个缓存条目需要重写整个缓存行(即闪存页)。仅追加存储 通常遵循常见的垃圾回收技术。桶组成了资源节流单元,高垃圾量的桶可以被优先选中。条目逐出 与我们之前提到的相同,内存大小需要紧凑。闪存的缓存行可以将逐出和插入合并为一次重写,也就是说,不会在没有插入的情况下进行逐出

  • SSD 磨损(Wear out)。当被用作缓存时,SSD 本身就会遭受更多 严重损耗。这是与持久性存储的第三个关键区别。这是因为缓存的容量比底层的持久性存储要小得多,但缓存必须通过大多数新的写操作,而且由于周期性的数据冷热转换,还需要流动更多。缓解措施可以是防止冷数据流经缓存,并通过使用足够的缓存容量来承载热/冷周期,以避免搅动。

元数据缓存

这一节主要讲的是数据的缓存,但我们也简单地提到了元数据的缓存。

  • 元数据通常在 一致的核心(或分区的、Dissaggregated 的节点)中完全在内存中托管。客户端可以直接询问一致性核心,而不是需要另一个缓存服务。此外,元数据的大小通常比数据小得多。

  • 元数据的传播通常利用 Piggybacked 请求、Gossip 协议和对一致性核心的直接刷新请求。客户端通常在本地内存中缓存它所需要的东西,有一个过期或版本检查策略。

  • 数据的 二级索引 可以被看作是元数据的一种类型。在每个实现中,它们通常被视为普通的数据或表,与前面几节中提到的缓存设施共享。作为索引,它们可以在内存中设置更高的优先级。

数据分区和放置

数据分区是分布式系统中 横向扩展(Scaleout)的基本范式。它有更多的设计特性,很多类似于数据组织章节,你可以发现跨节点的数据分区和如何决定数据分块有共通之处。分片(Sharding)大多时候是数据分区(Partitioning)的同义词。

数据分区章节

  • 横向扩展。数据分区将数据空间映射为分区,这样每个分区可以在不同的节点上提供服务,以扩大系统容量。该系统是 动态的,即一个单独的分区会在大小或热度上增长或缩小,这就引入了 拆分或合并分区 的需求。

  • 访问局部性。一起访问的数据应该放在一个分区里。例如,一个分区包括连续的数据范围,并保留了 排序顺序,以利于范围查询。例如:在一个 事务 中经常被分组在一起的不同表,被共同放置在一个分区中。例如:一个分区包括 经常一起访问 的不同对象或表列。例如:一个单一的对象可以被 分解成不同的组件,每个组件根据访问模式被不同地分区。访问模式是 动态的,这意味着分区或位置需要随时间变化。寻找最佳的分区,既可以贪婪地基于最近的指标,也可以通过 机器学习 对历史行为进行优化。

  • 单元的粒度。对于细粒度的调度来说,分区可以很小,并且仍然可以通过在一个节点上 co-locating 多个分区来保持局部性。随着数据量的增长,需要使用更多的元数据。现有的分区颗粒度也可以通过采用合并/拆分来 适应 未来的增长/缩减。然而,基于哈希的分区需要仔细设计以避免过度的数据迁移。

  • 容量的平衡。如何确保每个节点获得相似的数据容量?要么通过均衡数据分区来实现,要么依靠平衡数据放置。分区大小的增长/缩小进一步引入了管理合并/拆分和迁移的需求。

  • 冷热的平衡。如何确保每个节点收到类似的IOPS/吞吐量?热度是除容量之外的第二个需要平衡的维度。平衡要么嵌入到数据分区层面,要么依赖于数据放置。需要自适应的数据迁移来处理未来的流量模式变化。

  • Shuffle。与现有分区相比,计算可能需要一个不同的分区键。这种情况经常发生在 MapReduce/Spark 管道中,数据需要通过不同的分区键进行聚合,以及在数据库的连接操作中不使用主键。通常情况下,解决方案是 Reshuffle,通过新的键来发送数据,或者有时可以将一个小表完全复制到每个目的地。

数据放置是下一步,决定在哪个节点放置分区。通常,数据分区和放置是结合在一起的,以解决上述设计特性。数据放置有更多的设计特性。

  • 数据迁移。迁移的第一个来源是数据平衡,这来自于容量的不对称增长,热度的变化,访问局部性的变化。另一个来源是节点的加入或退出,即空节点需要填充,故障节点需要将数据放在其它地方。基于哈希的放置通常需要仔细设计以避免过度的数据迁移。这个话题与 负载平衡 密切相关,而 资源调度 则更侧重于放置有多个维度约束的作业,如 CPU、内存、IO、延迟。

  • 元数据大小。允许对象放置的完全自由度,以及使用细粒度的跟踪单元,有助于数据平衡和减少迁移。然而,这两者都需要花费更多的元数据大小。元数据本身也可以被分割和扩展,见 元数据章节

  • 故障域。共同相关的数据,例如 3-副本 或 EC symbols,需要避免放在同一个故障域中。故障域分层由磁盘、节点、TOR、数据中心 Row、T2 交换机、和区域 DNS 组成。升级部署增加了另一层故障域。

常用技术

键值结构的常见数据分区方法是基于哈希和范围的分区。对于文件系统的节点树和图的顶点/边,数据分区的灵活性更高。数据分区和放置技术与 元数据章节 密切相关。

  • Range。经常出现在数据库中,以支持范围查询,例如 CockroachDB, HBase。一个表通过连续的行键范围进行水平划分。范围通常通过分割/合并来动态管理。一个表还可以通过经常一起访问的列进行垂直分区。

  • VNode。键被哈希映射到称为”VNodes”的桶中。VNodes 是进一步放置的输入。与直接放置每个键相比,VNode 降低了跟踪的粒度,并平衡了冷热。系统中的 VNode 数量通常是预先配置的,很难改变。我们之前提到过 VNode。

    • 哈希分区 数据库,例如 YugabyteDB,可以支持哈希分区。每个分区就像一个 VNode。行通过行键哈希映射分配给它们。分布式 Memcached 也可以通过哈希分区来扩展。虽然哈希会自动平衡各节点的热度,但由于一个范围的查询涉及到所有节点,IOPS 会大大增加。
  • 文件系统的 Inode 树。像范围与哈希一样,树也可以通过子结构与哈希的随机性进行分区。

    • 基于 子树 的。例如,CephFS 的特点是”动态子树分区”,整个子树可以根据热度被迁移到不同的 MDS 节点。基于子树的分区保留了访问的局部性,但易受热度偏斜的影响。当访问深层 FS 路径时,每个中间节点都要进行元数据的获取,而 Subtree 分区有助于将所有元数据放置在一个节点上。

    • 基于 Hash 的。例如,HopsFS 通过父节点 ID 对节点进行分区,以使 dir 命令的操作本地化。哈希有利于负载平衡,但破坏了访问的局部性。

    • 分解成不同的组件。例如:InfiniFS[385]。节点元数据被解耦为访问属性和内容属性。每一个都有不同的访问局部性,因此每一个都被不同地分区。该方法增强了基于哈希的分区下的局部性。

  • 图分区 具有挑战性,因为图组件之间的互连是不规则的。此外,图上的计算通常很难被局部化到分区,例如深度学习需要参数服务器。

    • 哈希/范围分区。例如,FaRM A1 应用哈希分区来支持随机性。如:Facebook TAO 由 MySQL 支持,并分配了一个 shard_id 用于分区。由于总是一起访问,相邻的边被打包到它们的顶点。

    • Clique[482] 标识了一组内部通信密集但外部通信稀疏的顶点。Facebook Taiji 通过 Social Hashing[386] 对数据进行分区,即按朋友群、地理域、组织单位等进行分区。昂贵的分区可以通过机器学习离线计算。

    • 复制。例如:Facebook TAO。一些分区可能经常被发生在其它分区的计算所需要。如果跨区域,流量就会很昂贵。这样的分区可以被复制到所有的消费者节点,以利于访问局部性。

有关数据放置的技术与数据分区有类似的分类。

  • 元数据跟踪。使用一致性核心来跟踪每个分区的位置,这需要花费元数据的大小,分区的放置有充分的自由度。可以探索各种算法,在容量/热度上进行细粒度的安排。节点加入/退出时不需要过多的迁移。例如 HDFS/HBase,Tectonic。

  • 一致性哈希。哈希方法节省了元数据的大小。直观地说,一个分区可以将它的位置哈希映射到一个节点上,但是一个节点的加入/退出会搅乱所有现有的位置,从而导致过度的数据迁移。一致性哈希的引入稳定了搅动,即只触及邻近的 VNodes。例子有 Cassandra、Dynamo。我们之前提到过一致性哈希。

    • CRUSH。Ceph 发明了 CRUSH 算法,这是一个基于哈希的放置算法。它产生随机但确定的放置,并限制在节点成员变化期间的过度迁移。与一致的哈希算法相比,CRUSH 支持以树状组织的分层故障域,以及不同权重的设备。
  • 基于内容的寻址。放置是由数据块内容的哈希值决定的,因此,去重是自动的。一个例子是 XtremeIO。我们之前提到过它。

数据完整性

数据的完整性是至关重要的。一个存储系统可以很慢,功能少,不可扩展,但它不应该丢失数据。有几种影响数据完整性的故障模式。

数据完整性章节

  • 耐用性损失(Durability loss)。足够多的磁盘坏了,以至于一块数据无法恢复。相比之下,可用性损失(Availability loss) 是指一个服务节点宕机,但数据仍然可以从磁盘离线恢复。在硬件层面,整个磁盘故障(Failure)通常映射到电源单元或磁盘封装,而损坏通(Corruption)常映射到单个扇区故障。RAIDShield[387] 指出,快速增长的重新分配扇区数是预测磁盘故障的好方法。

  • 读取时的磁盘错误(Disk error on reads)。磁盘读取可以产生瞬时的或持久的读取错误。它可能会也可能不会映射到潜在的坏扇区。这个比率可以用比特错误率,或 UBER[388] 来衡量。

  • 无声的磁盘损坏(Silent disk corruption)。一个磁盘扇区可能在不知不觉中被损坏,磁盘硬件可能在第一次读取时才发现它。或者磁盘的读取是成功的,但软件层面的 CRC 验证发现了不匹配。

  • 内存损坏(Memory corruption)。内存位会不时地损坏,并产生不正确的计算结果,这包括 ECC 内存。更糟糕的是,一个损坏的指针,可能会不可预知地篡改大范围的内存。

  • 意外的数据删除 Bug(Unexpected data deletion bugs)。一个高写入的存储系统需要及时回收被删除的空间,但一个编程 Bug 可能意外地删除有效数据。这种情况在小心翼翼的部署下可能不常发生,但一旦发生,受影响的数据会比普通的磁盘故障多得多。

  • 不正确的元数据 Bug(Incorrect metadata bugs)。元数据需要随着数据的变化而经常更新。一个编程 Bug 很容易错误地更新元数据,从而失去了对数据位置或状态的跟踪。处理版本不兼容的升级时,更容易出错。

  • 通过复制传播的 Bug(Bugs propagated through replication)。由于 Bug 也被复制了,全套复制被损坏的情况并不罕见。复制对于保护硬件损坏是有效的,但对于软件 Bug 却没有那么大的帮助。

常用的提高数据的完整性的技术:

  • 基于复制的。复制数据或应用EC。也复制元数据,以防一个副本被破坏。进行定期的备份,包括备份到另一个地理位置,以及备份到一个离线系统,以防止错误的传播。

  • CRC 被普遍用于验证一块数据是否符合验证,其成本是在有限域上计算多项式。与密码级哈希相比,CRC 能可逆的地恢复错误的比特。CRC 算法满足线性函数(CRC wiki[389]),可用于优化。一个32位的 CRC 能够检测任何2位的错误,长度<=31的Burst错误,任何双位的错误,或任何奇数的错误(CRC lecture[390])。

方法论(Methodologies)

这些技术的使用应该配合深思熟虑的方法论,考虑针对随机硬件故障的可靠性和针对人为错误的可靠性。

  • CRC 应该是端对端(End-to-end)。用户客户端产生 CRC,CRC 被保存在系统的最后一层。数据在返回给用户之前要经过 CRC 的验证。在处理过程中计算的 CRC 不太可靠,因为输入的数据可能已经被破坏了。更普遍的原则是,端到端验证是必要的

  • 任何数据转换都需要验证(Data transform)。复制,EC,缓冲区拷贝,压缩,网络发送,格式改变,从磁盘存储/加载,等等。任何数据转换都应该在之前/之后比较 CRC,以防中间发生任何内存损坏。更普遍的原则是,每个增量步骤都需要验证(Incremental step)。

  • 保存元数据两次。元数据非常关键,它可以在一致性核心中保存一次,并在数据节点上保留另一个副本。这两个副本以不同的工作流程进行更新。如果元数据在一致性核心中发生损坏,它们仍然可以从数据节点中恢复。更普遍的原则是,异质验证(Heterogeneous),关键数据或计算应该由两个不同的工作流来持久化或验证,这样,一方的损坏可以从另一方恢复。

  • 数据排序需要验证(Data ordering)。分布式系统可以以不一致的顺序接收数据包。当数据被添加时,它们的整体顺序应该被验证,以确保中间没有发生变化。

  • 定期扫描磁盘(Scrubbing)。这在分布式存储中很常见,例如 Ceph,磁盘需要定期刷新以防止无声的损坏。为了按期完成刷新,它需要足够的吞吐量和期限调度。

  • 验证下推(Pushdown)。一个存储系统可以由多个层次来组织。验证计算可以被推到底层,以缩短数据传输路径。它之所以适用,是因为验证逻辑通常是固定的,很少有异常处理,而且是面向数据管道的。它们也可以被卸载到硬件加速器芯片或智能硬件上。

  • 混沌工程(Chaos Engineering)。定期在系统中注入故障和破坏,测试系统的错误检测和恢复能力。定期演练数据恢复的工程操作。风险较大的活动应更频繁地进行。

高可用性(High Availability)

这一章节也附带讨论高可用性(Availability)和数据耐久性(Durability),大部分内容之前已经涉及。完整性的目的是确保正确的数据始终可用。可用性问题通常是短暂的,在服务器恢复后就会消失。但完整性和耐久性失效意味着数据在无限的未来失去可用性。

  • 复制(Replication)。数据/元数据 HA 的基本技术是持久化多个副本。一个副本可以恢复另一个,3个副本中的2个可以投票排除一个错误的副本。同步复制只有在所有副本完成更新后才会 Ack 客户端,而 地理复制 或备份可以在 RPO 的承诺下使用。

  • 主-主(Active-active)。计算/服务 HA 的基本技术是运行多个服务实例并允许故障转移。主-备 在备用机上节省计算资源,但在备用机启动时有 RTO 延迟。Paxos 是普遍使用的主-主算法,多数人的 Quorum 对脑裂进行仲裁。主-主可以扩展到多数据中心或多区域,进行 Paxos/sync 或 async 复制。

    • 单元结构(Cell architecture) 将数据分区,并将依赖的服务封装成单元。每个单元只指定一个活跃的主数据中心,而所有数据中心都运行活跃的单元。因此,所有的数据中心都是主-主的,没有备用的数据中心。数据可以跨数据中心进行同步/不同步复制或不复制。数据中心故障转移需要谨慎,以避免过载存活的数据中心。

    • 多区服务(Multi-zone services)。AWS AZ[391] 和 Azure redundancy[392] 将地理区域的灾难故障域划分为可用区。一个服务可以跨越多个区域,一个数据中心的灾难不会影响可用性。区域是主动-主动的。

  • 两地三个中心 在银行中普遍使用。一个城市部署了两个同步复制的数据中心,第二个城市部署了第三个非同步复制的数据中心,用于灾难恢复。

HA 依赖于对故障的稳健检测,其中主要问题是 Observational Difference[393] 引起的灰色故障。例子是 App 死亡但心跳线程仍在工作,网络链路性能下降只在高百分位发生,不一致地报告健康状态,间歇性故障。克服此类问题的常见技术源于 元数据一致性

  • 在心跳和应用程序的进度之间 同步锁定(Synchronized locksteps),例如使用请求执行数作为心跳,或使用自动到期的 Fencing Token / Lease。

  • Gossip 协议,多个对等体可以参与观察故障,一个请求可以向多个对等体确认。

  • Quorum 决议,重要的事件,如节点故障或节点成员的改变,应该有一个一致性的 Quorum 来做出最终决定。

耐久性(Durability)

耐久性通常与 HA 共享类似的技术,只是更强调磁盘故障/损坏和完整性验证,之前已经介绍过了。可靠性建模(Reliability Modeling)是常用的,其中 指数分布[394] 满足了大多数需求。

资源调度

云上的多维资源调度是一个大话题,见 参考架构章节 中提到的 DRF/2DFQ 等。在这一节中,我们将介绍一个典型的存储系统中的系统属性。

资源调度章节

  • 优先级。一个用户/后台作业/请求应该被首先或延迟处理,用最大或最小的资源。优先级也反映为不同用户作业的权重。通常,关键的系统流量如数据修复 > 用户延迟敏感工作负载 > 用户批量工作负载 > 后台系统工作。

  • 节流(Throttling)。一个用户/后台作业/请求所使用的资源不应超过其 配额(Quota)。节流也意味着隔离影响从一个用户到另一个用户的传播,其中共享资源如 CPU、网络、IO带宽、延迟很容易成为通道。典型的节流算法是基于令牌的 Leaky bucket,或者是对请求数/大小的简单队列限制。

  • 弹性(Elastic) 有多种含义。1) 一个服务可以及时扩展到更多的资源,以应对不断增长的负载。2) 一个后台作业可以借用未使用的资源进行快速处理,甚至暂时超过其配额。3)如果一个高优先级的工作突然需要更多的资源,一个低优先级的工作可以及时收缩自己。弹性涉及快速启动或增长的资源,用机器学习预测使用情况,即时执行配额,并探测增长,这有时类似于网络协议中的 拥塞控制

    • 资源利用率(Resource Utilization) 最终应得到改善,而不影响对延迟敏感的工作负载。这也有利于 能源效率(Power Efficiency),这是数据中心的主要运营成本。CPU 可以调低频率,空闲的节点可以关闭。
  • 公平性(Fairness),在上锁或资源分配中经常提到。用户作业应该有类似的机会获得资源,与它们的优先级/权重成正比,而不是被偏袒或饿死。

    • 反饥饿(Anti-starvation)是硬币的另一面。低优先级的后台工作不应该被延迟太多,例如 GC/Compaction 来释放容量。它类似于时间管理中重要但不紧急的象限。它需要检测饥饿的作业并应用缓解措施。

    • 优先级倒置(Priority Inverstion) 是另一个问题。高优先级的工作可能在等待另一个低优先级的工作所持有的资源,例如一个锁。应该追踪依赖关系以提高优先级,或者抢先杀死(Preemptive kill)并重试。

    • 抢断(Preempting)。它定义了较高优先级的作业是否应该停止/暂停较低的作业以占用其资源的策略。除了作业调度,抢断也出现在事务调度和死锁解决中,抢断是指较年轻的工作是否应该抢断较老的工作,或者反之亦然,这是不同的。抢断一个长期存在的事务的成本可能很高。OCC 也可以看作是先赢的工作抢断慢的工作,在这种情况下,频繁重试的成本很高。

设计维度

在设计资源调度时,有几个设计维度需要考虑:

  • 作业粒度(Job granularity)。小工作通常有利于资源调度的平衡。随机地把球扔进篮里:球越小越多,每个蓝的最终球数越平衡。该方法被广泛用于多核处理,即异步多级流水线(Multi-stage Pipeline)。虽然小的作业粒度是有益的,但它要花费元数据,增加IOPS,而且磁盘仍然倾向于批处理。

  • 过载控制(Overload control)。系统过载,然后出现级联故障的情况并不少见,例如同步的大量缓存过期,重试次数跨层放大,节点故障修复/重试而带来更多的节点瘫痪,CPU/内存/网络耗尽而传播过载,崩溃故障转移后再次崩溃,等等。控制旋钮,平滑降级,断路器是必要的。

  • 成本建模(Cost modeling)。读/写大小是存储系统中常见的实用的成本建模。它们共同组成了队列计数和队列大小。最全面的成本建模作为参考可以在数据库 查询优化器[395] 中找到。预测的 IO 成本可以和最后期限结合起来,以提前取消那些不能在时间或资源限制内完成的请求。

性能

虽然系统快速运行是性能的最典型含义,但性能对应到更多的 系统属性:延迟和吞吐量、可预测的性能、可扩展性、资源效率等等。利用并发和并行是提高性能的关键技术之一。可预测、可持续的性能更加重要,也带来更多挑战。性能的另一端指向资源效率,管理调度和成本。硬件发展是性能提升的一大因素。

性能章节

  • 延迟(Latency) 和 吞吐量(Throughput)。延迟衡量一个请求被服务和响应的速度;它对小的请求更为重要。吞吐量衡量给定大小的数据被处理和响应的速度;它对单个大的请求,或一批大小的请求更重要。注意排队中已存在的请求会对延迟产生负面影响,因为它将队列延迟加入到服务延迟中。但它们通常有利于吞吐量,如果系统没有过载,并利用请求的批处理和并行性。队列深度 (Queue Depth, QD),或 Outstanding/Active/On-going 的请求数,可以用来衡量这种行为。

    • 尾部延时(Tail Latency)。请求延迟是一个概率分布,通常 P25/P50/P99 变化很大,特别是在云存储中,其服务多客户提供的混合工作负载,具有难以预测的突发模式(Burst patterns),而且规模很大。P99 很重要,因为它仍然对应到许多客户。P25 通常对应缓存命中,而 P99 通常指向请求执行中的坏情况。减少尾部延迟的典型技术包括发送额外的请求,用监测滞后的节点并主动重试,以及 The power of two random choices[396]。

    • 排队理论[397](Queuing Theory)。系统被抽象为通过队列顺序/并行连接的组件。虽然随机(Stochastic)数学可以用于建模,但用生产样本进行模拟通常更实用。尽管排队理论指出,在资源利用率为 100% 的情况下,服务延迟可以增长到无限大,但其假设是完全随机的请求输入(Ingestion)。在一个良好的调度系统中,请求在选定的时间而不是随机地到达,仍然有可能以低延迟实现高资源利用率(对于高优先级工作)。排队理论也被用于 容量规划(Capacity Planning),队列布局可以指出数据流的瓶颈,同时它也有助于调试和故障排除,以定位哪个点注入了过量的延迟。排队理论还指导着 配置调优(Configuration tunning),只有当每个组件的队列大小和容量都合适时,整个系统的性能才能达到最大。

    • [IPC](Instruction Per Cycle,每周期指令数)。虽然延迟/吞吐量对衡量 IO 系统很有用,但扩展到 CPU-缓存-内存 领域,或内存处理系统,对应的概念是什么?典型的衡量标准是 IPC、Cache MissMemory Stall,它来自 CPU 的统计信息。一个精心设计的程序通过减少错误预测的分支跳转,有效利用 CPU 缓存、流水线和预取内存来提高 IPC;以及减少由于并发控制算法导致的 Cache Miss、缓存行锁定、进程锁定等待。

  • 可预测的性能[214](Predicable Performance)。对延迟/吞吐量的更高要求是,它们在不同请求之间、不同时间点、以及不同规模之间,应该是一致的。一个典型的反例是,由于后台垃圾回收的运行,SSD 的性能时好时坏,这里经常使用”确定的延迟(Deterministic Latency)”这个术语。另一个反例是 SSD 的性能在 Over-provisioned 空间用完后开始下降,这时通常会使用”可持续的性能(Sustainable Performance)”这个术语。人们还希望云存储能够提供从请求到请求的一致延迟,即缩短 P50 和 P99 之间的差距;并确保在应用程序/虚拟机运行数天并被迁移时有稳定的性能。

    • 影响可预测性能的因素。像 GC/Compaction 这样的后台维护工作很容易阻塞用户的请求,例如在队列的头部有一个大的读/写请求。工作负载的热点有变化,而负载平衡和迁移可能没有及时启动。客户的请求速率、容量可能会快速增长,出现突发情况,而自动扩展的反应速度不够快,切换也不流畅,而迁移本身也会消耗资源。一个虚拟机可以和嘈杂的邻居(Noisy Neighbor)一起运行,为了提高资源效率,Co-locating 是必要的,但是配额/节流(Quota/Throttling)并不完美。缓存会失误(Cache Miss),而冷重启或流量搅动(Churn)会导致级联故障(Cascaded Failures)。在缓存命中/错过之间切换,或者任何类似的情况,都是 Bi-modality[214] 行为,这是造成性能差异的一个根本原因。例如数据库可能在后台存储模式的变化。自适应执行(Adaptive Execution)根据流量模式切换所使用的策略、数据结构和索引,效率更高,但会造成性能的非平滑跳跃。网络也会有突发流量、拥堵和 Incast 问题。总之,实现可预测的性能仍然是云存储的挑战之一。

    • 服务级协议 (SLA) / 服务级目标 (SLO)。云存储为客户提供 SLA,即关于性能和可用性/耐久性的资金保证,而 SLO 则提供更严格的测量数字。对客户来说,提供可预测的性能甚至比简单地说我们的速度块更重要。其它可能比速度快更重要的有,提供丰富的功能集、值得信赖的客户服务(Customer Service)、有用的故障排除和可视化,以及极致的数据安全和保障。

    • 平滑降级(Graceful Degradation)。当过载时、或某些组件离线(如 Auth 服务)、或新功能禁用/回滚时,系统应该有一个平滑的路径来降级服务水平。应该避免的是级联故障,重试风暴,或缺少用于恢复的操作开关。典型的技术包括用断路器节流、取消不能满足未来最后期限的请求、避免在每个级别放大重试,等等。

    • 定额/节流/准入控制/截止时间(Quota/Throttling/Admission Control/Deadline)。这些词的含义是重叠的。客户账户或分配的对象都有配额,这些配额进一步用于工作调度。节流是多租户云存储的普遍需求,它可以强制执行配额使用的资源,保护系统过载,并避免因嘈杂的邻居而影响延迟。软配额通常被允许在客户对象之间或不同客户之间共享,以暂时吸收突发流量。长期或周期性的流量变化可以通过机器学习来预测,主动按需扩大/缩小。节流可以通过增量反馈控制回路来拨动,并通过运行时微观实验(Runtime Micro Experiments)来测量干扰(NyxCache论文[398])。

    • 冷启动(Cold Restart) 是一个典型的问题,如果一个缓存节点重启,它不能很好地服务于请求,直到再次填满。这很容易在批量升级时引入搅局,使系统过载,杀死更多节点,带来级联故障。AWS Redshift 引入了 Warmpools[150] 来提供预热的缓存节点。

  • 可扩展性(Scalability)。处理规模问题的基本方法是 分而治之(Dvide and Conquer)。随着现代硬件的快速发展,在 Scale-up(向上扩展)方面的高效也是必要的,例如,用 Manycore CPU 进行高效并发,用 NUMA 处理大内存,用 RDMA 网络、PMEM、NVM SSD 快速响应请求。Scaleout(水平扩展)是典型的云存储解决方案,具有无限的规模(理论上),但分布式一致性和通信的每一步都要收取额外的成本费用。

    • 分区(Partitioning) 和 复制(Replication)。分区可以提升整个数据空间的性能,而复制可以提升特定数据单元的性能。它们可以在不同的、非对称的粒度上工作。缓存也可以被看作是复制的一种情况,它利用更昂贵的硬件来利用空间/时间局部性提高性能。

    • 数据分层(Data Tiering)。缓存将数据复制到更快的存储硬件上,而数据分层则将数据迁移到它们上。另一个提高性能的基本方法是 在更好的硬件上运行它。近年来,硬件行业的发展,如内存、网络、固态硬盘、磁盘密度等,甚至比软件还要快,因此,购买新一代的硬件甚至比人工优化软件的成本和上市时间要好。

  • 资源效率(Resource Efficiency)。通常情况下,更好的性能需要编程高效的代码。在不同的系统层,如 CPU-缓存、内存计算、网络,以及不同的存储介质,如 HDD、SSD、PMEM、DRAM,技术都有所不同。提高性能的下一个基本方法是 做更少的事情。一个典型的例子是,如果关闭所有的日志记录,系统就会运行得更快,而一个功能较少的新系统通常运行得更快。资源效率的下一个关键部分是负载平衡。不是资源太少,而是在交换和公平分配时出现问题,导致饥饿。

    • 负载平衡(Load Balancing)。负载平衡的第一跳是高效的 任务调度(Job Scheduling) 和 放置(Placement) 于服务器上,在保证 SLA 下,将资源利用率、公平性、任务 co-locating 达到最优。客户工作负载的增长速度、突发流量、热点被监控,配合 Scaleout分区分裂/合并。集群对过度/低度利用的节点进行监控,不时地进行任务 迁移。配额/节流/权限控制__ 是下一个部分,以保护 SLA,确保可预测的性能,并作为迁移的触发条件。节点 故障检测 是介于两者之间的基础设施能力,其中 灰色故障(Gray Failure)可以注入间歇性的延迟或报告不一致的健康状态,这需要健壮性的处理。

    • COGS 。总的来说,IOPS、存储空间和查询 TPS 的成本,应该被测量和控制,以了解端到端的资源效率。这也是项目/产品管理的一部分,以辅助决策一个投资是否值得其成本。与数据中心采购/运营、电信租赁、研发等方面的总体支出相比,COGS 本质上是可销售的收入。容量规划 也是 COGS 的一部分,有关如何选择 SKU 和购买多少,通常需要提前几个月到几年。

    • 内核绕过 (Kernel Bypassing)。英特尔 DPDK[399] 借由 RDMA 需要更快的 CPU 处理而流行,而 Linux 内核的网络堆栈相对较慢,所以它们被绕过了。RDMA 也可以被看作是对服务器 CPU 的一种绕过。这种方法随后在英特尔的 SPDK[400] 中被采用,内核绕过使 PMEM 和 NVM SSD 的 CPU 处理速度更快。DPU 进一步绕过了主机 CPU 来接管普通的存储基础设施。Ceph 还开发了 BlueStore,它在底层实现了定制的 BlueFS[401],与原生的 Linux 文件系统相比,绕过了许多功能。内核绕过是另一个 做更少的事情 的例子:缩短调用路径,减少跳跃节点,直接访问,直接返回。

  • 硬件加速/卸载(Hardware Acceleration/Offloading)。虽然 CPU 是通用的,但在特定用途的芯片上花费同样(或更少)的钱可以在低能耗的情况下产生更高的计算吞吐量。此外,CPU 本身也越来越难赶上现代 IO 设备(如 PMEM、RDMA 网络和深度学习/机器学习)所要求的快速增长的处理速度。当计算更加标准化时,卸载更容易,例如网络数据包处理、压缩/加密;而磁盘IO通常更加复杂,并与可变的数据格式和异常处理交错进行。

    • 基于 ASIC 的压缩/加密卡很常见。AWS Nitro / Microsoft Catapult 是成功的商业案例,ASIC/FPGA 提升了虚拟云网络的性能,以及压缩/加密等。

    • SmartNIC 在网卡中建立了虚拟化、RDMA、处理器卸载。CPU 的工作可以被卸载到网卡层面,具有更短的往返路径。而 智能SSD(或 Computational SSD Drives[402])在 SSD 层面建立查询处理,绕过 PCIe 进行早期过滤数据。

    • GPU/TPU 引领机器学习加速器发展,专门用需要消耗大量算力 FLOPS 的深度学习训练。IPU/DPU 试图将数据中心的基础设施整合到更高效的 COGS 芯片中。更先进的 GPU 互连,如 NVLink[403] 被开发出来,组成了一个 HPC 集群。

    • HPC 是另一个领域,高端硬件,通常带有定制的加速器和多核,被用于科学处理。这些加速器通常随后变得成熟,并进入商品服务器的市场,如 RDMA。

  • 调试与故障排除(Debugging & Troubleshooting)。性能不仅是关于现在,也是关于开发提高性能的速度(Velocity)。只有在有指标的情况下,才会有洞察力来进行改进。设计良好的监控系统包括实时时间序列指标、标准可交换的日志、以及用于保留和复杂查询的数据仓库。OpenTelemetry[404],与 Google Dapper[405] 类似,是一个典型的微服务跟踪框架,可用于调试性能问题。

    • 一个典型的分析 包括 自上而下分解 组件的调用层次(或 队列布局[406]),并定位哪个组件注入了延迟。然后将问题请求与最近的系统变化、某些 SKU 标签、源单位产生的流量模式等相关联。在进入服务器层面后,进一步缩小范围,将其分支到磁盘 IO、网络 IO 或 CPU/缓存 的低效率。在每一个分支点,都应该有 支持工具 进行调查和可视化。最后,分析应该给出估计的影响数字,与观察结果 相匹配验证假设

    • 思想实验 从一个 自下而上 的方法开始。假设延迟是由某一类型的请求在一个特定的百分位水平上注入,来自于某底部组件。系统是否有足够的指标和故障排除工具来发现它?然后再从上往下看,影响延迟的主要来源是什么?性能故障排除不应该是一个困难的问题。相反,它应该是一个系统化的方法,发现我们能发现的,以及我们需要但缺失的指标和工具,然后一步一步地增强基础设施。

    • 线速,差距分析 。另一种分析性能的方法是,首先找出底层存储设备或网络设备的原始硬件速度(线速,Line Speed),然后分析从线速到存储系统实际性能的差距是由什么造成的。这提供了一个系统的方法来逐层剖析性能,并保证在投入更多设备资源后能最大利用。不管怎么说,优化应该 从瓶颈开始,以指标提供的洞察为支撑。

并发和并行(Concurrency & Parallelism)

并发性和并行性是提高性能的关键技术。我们在这里主要关注多核的单节点的优化,而分布式扩展系统则放在后面的章节。一般来说,并行意味着在同一时间发生(需要硬件支持),而并发意味着一起发生,但不一定在同一时间(通过交错调度)。

并行的基本能力来自于 硬件并行。例如,CPU 的缓存芯片可以被设计成同时查找所有的缓存线,而软件则必须借助于 CPU 多核支持的各种多线程技术。最好的性能来自于利用所有的并行单元,并使协调/同步开销最小。

  • 这里列出了可以利用的典型的 硬件并行性。首先,最常见的是 CPU Socket->CPU 内核->CPU 超线程。其次,NUMA 和 DRAM banks[407]。固态硬盘内置的 Plane level 的并行性[408](Chip -> Die -> Plane -> Block -> Page -> Cell)。PMEM 可能有类似于 SSD 的内部并行性。

  • SIMD,矢量执行 是常见的数据库技术,可以利用每个指令的数据并行性。列扫描被视为操作(位)向量。此外,代码生成和 JIT 被用来产生更多的 CPU 高效执行计划。AWS Redshift 进一步查找外部缓存中最近的编译结果。

  • ASIC, FPGA, TPU, GPU 专门的硬件可以进一步提高目标工作负载的并行性和效率。对于 FPGA 来说,芯片面积有多大,可以编程的计算单元就有多少,从而有多少可以并行的工作。更多的芯片可以通过高带宽链路(如 NVLink)互连起来,组成一个 HPC 集群。

负载平衡 对于在并行使用的多个硬件单元之间实现最大效率至关重要,这就像一个横向扩展的分布式系统。

  • 任务切割成较小的单元更容易平衡,就像把较小的球扔进垃圾箱一样。这解释了为什么存储引擎可以从 多阶段流水线(Multi-staged Pipeline) 中受益。它类似于分布式系统中较小的分区大小。除了分割任务,流水线 还重叠了任务的执行,以提高基础资源的利用率。预取(Prefetching) 和 预测执行[409](Speculative Execution) 进一步将未来与现在重叠。

  • Work Stealing 是另一种常见的技术。闲置的线程从繁忙的线程中夺取工作,调度任务的成本会自动分摊到更多的闲置线程中。它类似于分布式系统中的工作 迁移

减少通信 是最问题的关键。锁定(Locking)和同步(Synchronization)是并发性和并行性的首要话题,它们被用来协调 通信。但最好的系统被设计成不需要通信,因而是最简化的。这同样适用于分布式的 Scaleout 系统,或者多核的单节点 Scale-up 系统。

  • 同态多副本 (Symmetric parallel copies)。数据和任务被分割成多个副本,每个副本以完全相同的方式处理,副本间不需要任何互动。例如,在云存储系统中处理来自不同客户的请求。例如:Ceph OSD,每个线程都专门掌控一个磁盘。例如,在一个网络交换机中,一个核心将任务安排给其它所有的核心做数据包处理。

  • 通信密度。Locking/Latching,读取另一个线程的本地数据,以及访问共享内存/缓存地址,它们都是通信。绘制每个 CPU 核心的通信连接图,这种通信的频率如何?连接的扇出(Fan-out)是多少?联网的密度是多少?一个好的算法应该减少这三种情况。

    • 无锁算法(Lock/Latch Free Algorithms)在多核条件下通常有很高的通信成本,正如 HyPer Scalable MVCC GC 论文[342] 所指出的。通信点通常是一个 CAS 操作,它从根本上锁定了 CPU 的缓存线。所有的核心在锁上竞争,形成了一个 N对N 的通信图,它经常被触发,有很高的扇出,因此有很高的网络密度。

    • Flat Combining,也是上述 HyPer 论文中使用的 Thread Local 技术。每个线程只在其线程本地数据上工作,一个领导者线程会协调其它线程的工作。这将通信量减少到 1-N 的扇出,从而降低了网络密度。

    • Epoch-based Reclamation 进一步降低了通信频率。只有当 Epoch 过后,每个线程远离了其共享资源,领导者线程才会开始资源的清理工作。类似的想法也适用于像 Sloppy Counters、Delayed batched async updates 等技术。

  • 减少资源竞争,在没有必要的时候。一个典型的例子是 虚假共享(False Sharing),CPU 核心在缓存线上竞争,但其依赖不是应用程序所需要的,而是由编译器打包内存对象引入的。

    • 分区和减小锁的粒度。一个典型的技术是对哈希表进行分区,每个锁只对应一个分片。这就分割了通信网络以减少连接密度。另外,典型的编程课程会讲授细粒度的锁。这减少了通信连接的持续时间,与减少频率类似,也可能减少连接的扇出。

    • B+树 Lock Coupling 将树的父/子节点分步加锁,锁定跨度有限,就像一只螃蟹。与锁定整个子树相比,它也减少了锁定范围,从而减少了竞争资源,这是另一个细粒度锁的例子。以相同的顺序获取锁是相关问题,通过预先建立具有固定规则的协调步骤,可以避免死锁。

    • 写时复制(Copy-on-write,COW),不可变(Immutable)的数据对象,影子分页(Shaodw Paging),和 Delta 更新 是相关的技术。更新不是在原始数据上工作,而是在副本上工作,或者只写 Delta。通过这种方式,更新者避免了在原始数据上的竞争。此外,不可变性(Immutability) 可以极大地简化系统设计,但同时也给后续的垃圾回收带来了压力。

    • 通过调度实现并发性(Concurrency by scheduling)。这个例子是 NetApp Waffinity[191],对不相干的文件和地址分区的访问可以安全地并行化。NetApp 使用顶层调度器来确保竞争访问不会被纳入调度,而不是使用低级别的锁进行编程。

这里也要提到并发和并行的 工程方面。我将协程(coroutine)放在这一分类。

  • 协程,线程,和进程。理论上,它们应该能够达到相同的性能或并行水平,只是协程允许绕过操作协同内核,而线程比进程更轻量级地共享资源/内存。然而,好的编程 API 确实很重要,协程通过 Async/await 迅速推广。线程被留给开发者对并发性和并行性进行更基础的控制,其中线程执行池可以被精心设计,而进程在资源/故障隔离方面更好。

  • 同步和异步。从理论上讲,它们应该能够达到相同的性能或并行性水平。但是异步编程更容易将 CPU 时间与 IO 时间重叠以提高效率(例如 Epoll),也更容易将长函数切割成小任务以利于负载平衡。从根本上说,异步可以通过忙轮询(同步)或定期检查来实现的。当使用高性能的 IO 设备如 NVM 和 RDMA 时,忙轮询(Busy Polling)有时会更有效。

  • 锁和抢占(Preempting)。一个简单的锁让先到者获胜,并阻塞后来者。但它可以通过不同的方式实现,让先来者/后来者获胜,或者是阻塞/非阻塞,以及 OCC 重试。这样的技术可以用来优化数据库事务,特别是混合了短时的 OLTP 事务和长时的 OLAP 事务。

  • 测试正确性 对于复杂的并发程序,对于云存储来说,并不容易,但很重要。C# Coyote[410] 通过搜索庞大的执行排序空间来寻找潜在的错误。FoundationDB 也配备了由 Flow 构建的确定性模拟测试。此外,TLA+[411] 被用来对状态机进行建模,以验证 Liveness 和 Invariants。

CPU-缓存和内存处理(CPU-cache and In-memory)

性能优化可以分成几个方面:

  • CPU、缓存,和内存。它们通常与 Scale-up 主题重叠,着重优化单个节点,即在堆叠了更多的 CPU 核和大内存后,如何有效地利用它们。

  • IO 和网络。它们通常与 Scaleout 主题重叠,即一个分布式系统与许多节点互连。另外,磁盘 IO 和网络传统上比 CPU、缓存和内存平面要慢。我们将在 网络章节 中详细介绍。

对于优化的 CPU、缓存,和内存 平面,有以下几个方面:

  • 并发与并行,正如我们在上一节中所介绍的。

  • 内存墙(Memory Wall)。今天的 CPU 比 DRAM 快得多(1ns vs 100ns,见 Interactive latency[412]),它依赖于 Cache 作为中间桥梁。效率可以通过 IPCMemory StallCache Miss 计数器来衡量。一个好的算法需要:1)利用局部性性进行缓存 2)用缓存预取进行流水线处理 3)避免在同一缓存线上竞争 4)避免额外的内存写入,同时将大部分操作留在 CPU 寄存器和缓存中。

  • 分支预测错误(Branch mis-predict)对 CPU 预测执行来说代价很高。一个高效的数据处理程序应该避免过多的非确定性的 if-分支。这些原则对 GPU 来说更加重要,因为 GPU 的控制单元很少,大部分芯片面积都用于同步数据操作。

  • 做更少的事情 总是会使程序更快。数据库查询的 代码生成JIT 可以看作是一个例子,高度定制的代码为每个特定查询的 SQL 而自动生成编译,以提高 CPU 的效率。尽管这些代码对于人类程序员来说是不友好的,或者有太多的组合需要手工操作。

规模化存储(Scaleout storage)

在这一节中,我们将优化性能的重点放在横向扩展的分布式存储上。前面的章节已经涵盖了大部分的主题,比如 负载平衡尾部延迟流水线 等等。更多以前的章节讨论了 分区缓存索引顺序化 IO 等。大部分的性能提升来自于 Scaleout 本身,以及对单节点性能的仔细优化。此处我们列出上述未覆盖的项目:

  • 压缩 是一个看似独立的话题,但可以显著提高性能,因为在 IO 设备上传输的数据更少。我们在之前的章节中已经谈了很多。

网络

网络通常与存储设计是正交的,它更多地与数据中心的建设和硬件设备相联系。网络在多个方面影响存储系统,与其它部分相比,网络有几个关键的区别。基本层面是网络架构。最基本的层次是网络架构。它定义了数据中心的网络基础设施是如何构建的,它制约着基线性能和可扩展性。下一个层次是负载平衡器,它是请求进入数据中心的大门。传输层的一大话题是如何在避免拥塞的同时扩大传输吞吐量。进一步,网络栈可以被定制和优化。

网络章节

  • 性能面。网络会影响到节点之间的通信延迟。网络链接,考虑到超额订阅(Oversubscription)和路径路由,影响数据传输的最大吞吐量。数据中心的设计影响到一个存储集群现在和将来可以增长的总容量。网络的稳定性会影响到应用需要处理瞬时节点故障和信息传递损失所需的扰动。

  • 所有成本(Cost of Ownership)。网络设备的耗电量每月都要花费资金,而且通过冷却会翻倍(耗电组件比较[413])。考虑到流量工程和CPU/网络 瓶颈,资源利用率会影响资金的使用效率。

  • 升级管理。网络速度正在迅速增长。具有不同带宽的旧/新交换机需要有效地协同工作。升级需要在不中断实时服务的情况下进行,并以最小的流量降级。建设一个新的数据中心可能需要几年时间。

与存储系统的其它部分相比,网络有几个关键的区别:

  • 非持久性。整个存储栈是围绕着持久性建立的,但网络不需要担心这个问题。无状态 的特性减轻了升级和设计的复杂性,并允许将更多的注意力转移到标准化的协议和高性能的交换。

  • 高可靠架构。今天的数据中心网络通常是基于Clos网络架构[414]。TOR、叶子(Leaf)和脊柱(Spine)交换机以全网状(Full-mesh)连接,当一个设备发生故障时自然支持高可靠。交换机(通常是合并的路由器功能)用OSPF[415]等协议动态地更新路由。流量以高可靠的方式选择路径,使用多路径协议,如ECMP[416]、WCMP[417]。

  • 标准化的逻辑。与涉及自定义数据格式和异常处理的磁盘IO相比,网络功能更加标准化和无状态。事实上,网络会向跨厂商的协议和公布的规范靠拢。功能经常被卸载到较低级别的组件上,如SmartNIC、FPGA/ASIC、RDMA。

  • 快速增长。这些年网络速度迅速增长,从10Gbps,40Gbps,到100Gbps,甚至200Gbps。尽管网卡在处理小数据包时工作得很好,但CPU核心很难跟上。考虑到100Gbps和1KB的数据包大小,一个核心只有80ns来处理每个数据包。

网络架构

最基本的层次是网络架构。它定义了数据中心的网络基础设施是如何构建的,它制约着基线性能和可扩展性。

  • Clos网络 是常用的网络架构。与它的祖先,巨大的 “单交换机条(Single switch bar)”相比,Clos是通过连接小型交换机来构建的。其优点是允许通过增加单个交换机进行扩展,并能容忍单个交换机离线。它由多层组成,例如,T0(TOR)、T1(叶子层)、T2(脊柱层)。每层是一组交换机。邻居层是以全网状(Full-mesh)的双方连接(bipartite connected)。现实世界的部署可以有定制。

    • 超额订阅(Oversubscription)。为了节省成本,较高的层级可能有较稀疏的链接和比低层级低的聚合带宽。它假定 局部性(Locality),密集的消耗局限在低层。类似的模式可以在数据库中看到,将查询过滤推到智能固态硬盘上,MapReduce将工人作业放在数据节点上,以及ECWide[418]通过利用机架内修复来减少T1流量。

    • 子域(Sub-domains)。在Google Jupiter Rising[419]中,不是用全网连接T1和T2,而是将T1切割成不相干的域,称为”聚合块(Aggregation block)”。分离域可以减少链接密度。”聚合块”也是Google Orion SDN[420]中用于故障隔离的控制器域。Dragonfly topology[421]显示了类似的想法,称为 “子网络”/”组”。

    • 聚合块的内部是”两层交换机的双方连接(bipartite connected”。它在通过T2进行路由之前聚合流量。它就像一个小型的Clos网络,或者一个具有Radix度(许多端口)的 虚拟交换机。类似的想法也显示在Dragonfly+ topology[422]中。

    • Sidelinks。在标准的Clos中,一个交换机不能直接连接到同一层的另一个交换机。它必须通过一个高的层级。相反,Google B4之后[423](WAN网络)引入了Sidelink,在同一层(同一数据中心内)增加连接。它利用了不对称性,即Sidelink比跨层(跨数据中心广域网)的链接更便宜。如果每个可能的Sidelink都被添加,Clos网络就会退化为对称的全网络连接(像Dragonfly+中的组对组连接)。

    • 光电路交换机[424] (Optical Circuit Switch,OCS)。Google Jupiter Evolving[425]用OCS取代了脊柱层。除了更低的延迟,OCS还与数据速率无关(data rate agnostic)。它只是用微小的机动镜(tiny motored mirrors)反射彩色的光。当旧的电交换机被升级为更快的交换机时,OCS不需要改变以支持更高的带宽。OCS的内部需要对虚拟光路进行重新编程,这是由谷歌Orion SDN完成的。OCS采用双向光链路,通过光循环器(Optical Circulator)和WDM收发器,从电线路转换过来。

  • 路由(Routing)。今天,数据中心交换机通常与路由器功能合并。标准的路由协议根据邻居的广告(Advertisements)动态更新,例如OSPF(基于链路状态),RIP[415](基于距离矢量)。然而,数据中心网络架构大多是静态的。谷歌Jupiter网络反而依赖于Orion SDN,一个 集中控制平面(Centralized Control Plane),定期刷新路由,并通过OpenFlow[426]协议向下推送到交换机。

    • SDN[39]实现了 流量工程[427](Traffic Engineering),它实时收集指标,通过WCMP多路径在异构交换机上分割流量,并对暂时的流量激增和设备故障做出反应。
  • 控制平面。传统上,网络管理员用单个终端配置每个交换机。现在的趋势是转向集中式控制平面,通常与SDN相结合。集中式控制器收集和汇总指标,在一个地方显示给网络管理员。配置和路由由全局视图决定,并向下推到终端交换机(数据平面)。OpenFlow是控制面和数据面通信的主导协议。

    • 除了Google Orion,还有其它的 SDN控制器,如OpenDaylight[428],Openstack Neutron[429]。SDN可以做一些有趣的事情,如虚拟分布式路由器,虚拟机专用网络。
  • 数据平面。除了商用交换机,Open vSwitch[430](OVS)是最知名的SDN数据平面软件之一,可以安装在商品交换机(Commodity Switch)上(如Linux)。它们遵循OpenFlow协议,与SDN控制器协同工作。

    • 尽管典型的数据中心网络数据平面遵循Clos,CDN可以使用 P2P架构。例如,在Facebook Owl中,邻居节点交换数据副本,而它们仍然保持一个集中的控制平面。

    • 控制平面网络[431] 在Google Orion SDN中被命名,但普遍适用。为了保证可靠性,控制平面网络和数据平面网络通常是分开的,例如,交换机上的管理端口和普通端口。当一个错误的配置中断了数据平面网络时,我们仍然可以使用控制平面网络进行调试,修复,并发送远程命令。

  • 跨数据中心。以上主要是针对一个数据中心内的网络。全球的数据中心可以通过运行在广域网上的隧道协议(如VPN)进行互连。广域网由自治系统(如互联网ISP、公司网络)组成,其中BGP[415]成为典型的路由协议(稳定、限制更新频率、丰富的路径政策)。

    • Google B4 Experience[432] 遵循与Jupiter一致的方法。它使用谷歌Orion作为SDN控制器并应用流量工程。它在Jupiter网络中增加了一个 “聚合块”,但用于面向外部的流量,称为 “FBR超级节点”。谷歌数据中心网络[433]描述,面向内部的流量(大型数据库查询和响应)比面向外部的流量(客户网络请求和响应)要繁重得多。

负载平衡器(Load Balancers)

网络的下一个层次是负载平衡器。它是请求进入数据中心的大门。它将流量分配给适当的节点。它还与少数实用功能合并。它不是分布式存储系统的必要部分,但通常作为前端。

  • 全局负载平衡 是第一站。一个客户的请求应该被送到同一地理区域内的附近的数据中心。负载平衡通常由DNS解析完成。同一网站的域名被翻译成不同的IP地址。每个IP地址都映射到附近的数据中心,以便路由到。

  • 数据中心的负载平衡器。在数据中心门口的负载平衡器。有许多类型,按硬件或软件,单节点或分布式来分类。

    • 商用负载平衡器,例如F5 BIG-IP[434],通常是部署在数据中心门口的一个硬件盒子(有一个备份节点)。负载平衡器将一个VIP(虚拟IP)暴露给外部客户,并将运行在物理IP上的一组内部服务器隐藏起来。

    • 另外,分布式负载均衡器 可以用软件来构建,例如Google Maglev[435],UCloud Vortex[436]。这是一组可以水平扩展的服务器。它们可以通过Paxos(或简单地在数据库中)维护共享状态。它们通过ECMP接收来自外部路由器的数据包。它们通过一致的散列将请求分配给内部服务器。

    • 还有一组负载均衡器来自 网络应用 领域。典型的例子是 NGINX[437], HAProxy[438], LVS[439](Linux虚拟服务器)。它们通过相互监视的备份节点和Keepalived[440]实现高可靠。

  • 负载均衡器的工作层次。负载均衡器的分类可以按其使用OSI模型[441]中的哪一层级的信息来分发请求。

    • 第二层(通过MAC地址)负载均衡器是很少见。

    • 第三层(通过IP地址)负载均衡器通过IP地址进行路由。典型的例子是将针对VIP的请求路由到一组物理IP上。路由器也可以被看作是第三层的负载平衡,例如用多路径、ECMP和BGP分割流量,选择收费较低的自治系统。

    • 第四层(通过TCP/UDP)负载均衡器考虑到端口,这允许例如将80/443端口映射到http/https池,以及NATs[442]。

    • 第七层(通过应用内容)负载均衡器利用应用层面的消息内容来调度请求,例如,URL cookies来实现粘性会话[443]。利用第七层更复杂,更慢,但可以很强大,例如将防火墙整合到负载均衡器中(下面有更多例子)。大多数情况下,负载均衡器都朝向七层覆盖发展。

  • API网关[444] 是一个由微服务(Microservice)引入的词。负载均衡器通常在数据中心或服务集群的门口。还有很多功能可以集成到它上面。类似防火墙,高级功能需要负载均衡器覆盖到第七层。

    • 路由器 和负载均衡器可以合并到一个(硬件)盒子里。就像交换机和路由器的功能可以合并一样。负载平衡器也可以运行 BGP,将大响应包路由到合适的自治系统。

    • 心跳健康检查 可以由负载均衡器完成。它需要知道哪个内部服务器是坏的,避免向它发送更多的数据包。

    • HTTPS。负载均衡器可以作为边界来处理加密/解密。外部客户通过HTTPS连接到负载均衡器,而集群内部则在HTTP上工作(可信环境)。VPN 可以以类似的方式提供服务。甚至用户的 认证(Authentication)也可以被整合到负载均衡器中。

    • 负载均衡器可以与 防火墙 合并。作为所有流量的关键路径(Critical Path),它对应用信息进行解包以过滤恶意内容。同样地,负载均衡器可以被扩展以保护免受 DDOS攻击

    • URL分发(Dispatching)。负载平衡器可以与API网关合并。它能理解网络应用程序的URL,并将它们分配到所需的服务器池中。每个URL模式可以映射到一个不同的微服务,负载均衡器作为服务路由器工作。

    • 负载平衡器可以与 断路器(Circuit breaker)合并。它跟踪实时的API或用户流量使用情况,执行节流(throttling),如果服务过载,则降级服务,以防止级联故障。

  • 直接服务器返回[445](Direct server return,DSR)是一种通常用于负载均衡器的技术。内部服务器直接向外部客户发送响应数据包,绕过负载均衡器。它节省了负载均衡器的带宽,特别是当响应比客户请求大得多时,例如视频流。

拥堵控制(Congestion Control)

来到传输层,一个很大的话题是如何在避免拥堵的同时保持最大的传输吞吐量。有几个影响数据中心网络性能的关键因素:

  • 交换机缓冲区的堆积(Switch buffer buildup)。交换机缓冲区中排队的消息越多,延迟就越高。当缓冲区溢出时,交换机会丢弃数据包并标记拥堵(ECN[446])。这就是拥堵发生的时候。由于重新发送,被丢弃的数据包会经历又一轮的延迟。重新发送又使网络的负荷加重。

    • 下一层的问题是,TCP正在 猜测交换机缓冲区的使用情况。TCP根据本地看到的拥塞信号来增加/缩减发送速率。然而,与最佳状态相比,这些步骤可能过于激进或过于缓慢。TCP看到的情况与真实的交换机缓冲区的使用情况有延迟。交换机缓冲区也是由许多其它服务器共享的。因此,TCP的猜测可能是不准确的,导致周期性的拥堵和利用率不足。
  • Incast 是多对一的流模式。它在数据中心通信中很常见,当聚合查询结果、MapReduce或纠删码重建读取时。除了使目标交换机过载外,它还会迅速溢出交换机的缓冲区,导致拥堵。交换机开始丢弃数据包,这却导致源端重发,逐级增加负载。

  • 流间干扰(Flow interference)。交换机缓冲区是在端口之间共享的。一个流引起的拥堵会影响其它流。交换机可以运行流控制协议来缓解拥堵(例如PFC[447]),然而它可能会影响不相关的流,只因为它们共享同一个交换机。一个有小消息的流也可能被另一个有大消息的流影响,即队列头部阻塞[448]。

有几个已知的TCP拥塞控制协议。它们对默认的TCP栈进行了定制,以提高流性能、公平性、网络利用率,并容忍突发事件。

  • DCQCN[449] 针对部署在RoCEv2[450]上的RDMA的拥堵控制。它基于每个流的拥塞控制(QCN[451])而不是PFC(基于每个端口)。它将拥塞控制分为CP算法(交换机端,拥塞点)、RP算法(发送端,反应点)和NP算法(接收端,通知点)。

    • 当缓冲队列超过极限时,交换机(CP)标记ECN(即拥塞触发)。接收方(NP)将ECN分批发送,然后将CNP[450](RoCEv2定义的拥堵通知)发回给发送方(RP)。发送方保持对ECN标记数据包部分的估计,在本文中称为 “α”。拥堵时(即发件人看到CNP),发件人每轮减少α的发送率(几乎是指数级的)。

    • 发送方的恢复由FastRecovery、AdditiveIncrease和HyperIncrease完成。FastRecovery执行固定数量的回合。在每一轮中,当前速率被设置为(目标速率+当前速率)/2(指数式缩短差距)。AdditiveIncrease用同样的公式设置当前速率,但另外每轮增加目标速率,例如5Mbps。HyperIncrease使用与AdditiveIncrease相同的公式,但将5Mbps改为例如50Mbps。

  • DCTCP[452]。DCQCN是基于DCTCP和QCN的。DCTCP的目标是普通网络而不是RDMA。正如在DCQCN中提到的,DCTCP引入了:1)让交换机标记ECN,接收方将其回传给发送方,2)估计 “α “以降低发送速率。

    • 与DCQCN不同,DCTCP并没有改变TCP默认拥塞控制中的Slow start[453]行为。慢速重启每轮将飞行中的数据包(拥塞窗口)加倍(比DCQCN的恢复速度慢得多),直到检测到数据包丢失(糟糕,拥塞已经发生,交换缓冲区溢出)。
  • BBR[454]。与针对数据中心网络的DCQCN/DCTCP不同,BBR针对广域网。默认的TCP将数据包丢失视为拥堵。这个假设在数据中心网络中是可以的,但在广域网中就不行了,因为在广域网中丢包是很常见的。此外,BBR试图减少交换机缓冲区的使用,因为高使用率(即高队列长度)会增加延迟。作为解决方案,BBR忽略了数据包丢失。它逐渐增加飞行中的数据包,以探测最佳带宽和延迟。通过接近发送率来避免拥堵,在那里带宽是最大的,交换机缓冲区的使用是零。BBR已成功部署在谷歌B4和YouTube。

网络栈(Networking Stack)

下一个层次是网络栈,即运行网络的软件,它可以被优化以达到更好的性能。典型的技术是:

  • TCP vs UDP。TCP是一个基于连接的协议。维护连接需要花费主机内存。TCP处理数据包的重新发送,以确保交付。TCP实现了拥塞控制,以便与交换机设置适当的速度,并实现了滑动窗口,以便与接收器设置适当的速度。UDP没有这些功能。因此,UDP是快速、轻量级的,适用于容忍数据包丢失的情况,例如视频流。UDP也是建立自定义协议的基础。

  • 内核绕过(Kernel Bypassing)。网络是快速的,但是内核、系统调用和上下文切换却拖累了它。典型的技术是DPDK,它在用户空间处理网络。客户端可以选择 poll而不是callback,因为通知间隔时间太短,就像自旋锁与阻塞锁的对比一样,后者会放弃CPU时间片。另一个例子是 RDMA,DMA绕过CPU来操作主机DRAM。

  • 卸载(Offloading) 网络处理被下推到较低的层次,例如是到网卡加速硬件,这将在后面介绍。今天,CPU的速度比高速网络慢得多,它需要额外的芯片来帮助。

网络栈也延伸到了定制的硬件和加速芯片。由于CPU比网络慢,而且大多数网络功能是标准化的,它们适合被卸载。

  • RDMA 依靠专门的网卡而不是CPU来访问主机DRAM。它通常运行在支持RoCEv2的普通以太网上(需要RDMA NIC),或者运行在带有全新硬件栈的InfiniBand[455]上。有更多的RDMA设计指南[456]。

    • 此外,一台主机可以使用RDMA直接访问另一台主机的 PMEM。这使得构建更快的存储系统成为可能,例如 Orion/Octopus。
  • FPGA 可以部署在网卡附近,以加速常见的网络功能,如隧道(虚拟化所需,如VxLAN[457],GRE[458]),加密,压缩,QoS,ACL(访问控制列表)。当代码变得稳定时,它们可以被烧成 ASIC 芯片,性能更强、更省电,但难以改变。

    • AWS Nitro 是一个成功的例子,它使用ASIC卡来卸载云网络(Nitro卡[459])。Microsoft Catapult 是另一个例子。
  • SmartNIC 将计算能力嵌入到NIC中。它可以集成上述FPGA子弹中提到的功能(和芯片)。它可以支持SR-IOV[460],为虚拟机虚拟化网卡。智能网卡甚至可以集成vSwitch,从构建虚拟机专用网络中卸载CPU。有一篇更详细的Azure SmartNIC[461]论文。

    • SmartNIC与 SDN 密切相关。商品交换机通常运行Linux和Open vSwitch,可由SDN控制器编程。这些交换机需要加速硬件来与商用交换机的定制芯片竞争(可编程性较差)。SmartNIC来填补这一空白。

应用层(Application Layer)

最后一个关于网络的层次是应用层。它涉及到应用程序如何有效和可靠地使用网络栈。

  • 消息传递方式。服务器可以通过直接发送消息,或通过PRC调用进行通信。这些请求可以是同步的,也可以是带有回调的异步的。服务器也可以通过消息队列,例如 RabbitMQ[462] ,通过主题和订阅来交换消息。消息队列可以以Exactly-once语义运行,例如Kafka事务性。服务器也可以用Gossip以P2P的方式分享信息,并有上限的收敛时间。它通常用于元数据的传播,例如Ceph,它捎带着更新和健康检查。

    • 连接管理。一个实际的需要是减少TCP连接数。假设有N个服务器,要管理所有N*N个连接是很昂贵的。除了池化和 keep-alive,一个解决方案是引入一个中介,例如 mcrouter[76],将连接数减少到 2*N。
  • 序列化(Serialization)。生活在内存中的应用程序对象需要在信息传递前被序列化,然后在接收方取消序列化。序列化是CPU密集型的。尽管 压缩 可以节省传输带宽,但它要花费更多的CPU,与今天的网络相比,它的速度越来越慢。一般来说,序列化协议需要紧凑的比特表示(如varint编码[463]),快速的编码,并允许格式改变和向后兼容。

    • 典型的 序列化协议protobuf[464], bond[465], Thrift[466], FlatBuffers[467]。Parquet, Apache ORC是用于磁盘存储的列式格式。Apache Arrow[468] 是一种用于内存处理的列式格式。与Parquet相比,Apache Avro[469]是基于行的格式。最后,JSON更慢,更大,但因 人类可读性 受到欢迎。

    • 客户端协议重新设计[470]。自定义的客户端协议在返回大的响应时可以很有效率,例如自适应地选择行格式与列布局,在CPU成本过高时启用压缩,截断不必要的数据和填充,用预取来并行化迭代器。自定义序列化协议可以利用应用层知识。大的字符串可以被特别处理,例如,自定义压缩、字典、前缀去重。

  • 不稳定的网络。在应用程序看来,网络是不稳定的。一个典型的问题是 成员资格检测(Membership Detection),例如其中一个节点临时地上线或下线。太积极地将其标记为故障会导致不必要的扰动(例如数据修复)。太慢的标记它的故障会影响服务的可靠性。更多的,脑裂(Brain Split),观察差异(Observational Difference),或灰色故障(Grey Failures)都可能发生,不同的节点组不能就它们看到的东西达成一致。典型的解决方案是通过一致性核心(Consistent Core)进行决策,例如Service Fabric、Google Orion SDN(”Fail Static”)。

    • 由于网络不稳定,消息完整性(Messaging Integrity)是另一层保护。事实上,一个分布式存储系统无法假设网络是可靠的。数据包丢失、消息重排和重放都可能发生,以不可预测的方式。应用层通常会实现自己的CRC、幂等(Idempotent)操作和代际失效(epoch invalidation)。

更多主题

存储组件细分章节相比,有几个主题之前未被涉及:

  • 分配器(Allocator)。它指的是单节点文件系统的磁盘空间分配器。在产线级文件系统中,有成熟的、现成的解决方案。分布式存储通常通过建立在本地节点文件系统之上,直接利用它们。另一方面,在多节点的情况下,”分配器”变成我们之前提到的 数据放置(Data Placement)。

    • 分配器可以和 文件系统压缩 复杂化,例如Btrfs compression COW[471],Ceph BlueStore compression[472]。首先,空间分配单位(即Extent,一个大的顺序块)和更新单位(块)是不对齐的。第二,被更新的范围和压缩边界可能是不对齐的(部分写入 Partial Write 问题)。第三,压缩后的块的大小是不对齐的(额外的索引开销),被覆写的块可能不能放进它原来的存储槽位(追加或异地写入)。文件系统分配器和索引需要针对这些问题一起工作,通过Extent级的GC/压缩,减少内部/外部的碎片,并减少读/写的放大。总的来说,仅追加的文件系统使压缩更容易实现。
  • 升级/部署。在一个大规模的分布式存储系统上进行安全和增量的升级,并进行原子回滚,这可能是很复杂的,而且涉及很多工程实践。微软的SDP就是一个例子。但它们有些离题,所以我在这篇文章中没有涉及它们。

  • 配置管理CMDB[473]是一个有趣的话题,例如,你需要一个数据库来管理大规模云中的许多裸金属节点。然而,它们不属于本文主题,所以我在这篇文章中没有涉及它们。

  • 操作便利性。这是一个有趣的话题,设计一个系统,使日常操作便利(smooth)、安全,并避免人为错误。它涉及监控、安全配置/部署程序、节流和降级,以及与Devops系统的互操作性。然而,它们不属于本文主题,所以我在这篇文章中没有涉及它们。

  • 存储中的机器学习。我涵盖了这些主题,但没有深入探讨。总的来说,它可以被分为

    • 分类问题(Classification)。给定一个数据单元,如何预测它的未来流量、生命周期、热/冷度?它可以为每个细粒度的场景应用最佳技术。相似类型的数据可以被分组,以使用压缩或分层。例如G-SWAP,缓存替换,Siberia系统。

    • 优化问题(Optimization)。给定几个数据布局策略,如何将它们与预测的流量模式进行最佳匹配?给出请求和作业,如何使它们最适合于一个资源池(即调度)?例如最优列布局(Optimal Column Layout),数据库优化器,云资源调度。

    • 自动调优(Auto tuning)。给出一个大的配置参数空间和具体的用户场景,如何搜索出最好的一个?可以用模型分析或仿真运行的方式进行网格搜索。例如Dostoevsky, G-SWAP, AWS Redshift ATO[474], Azure SQL Database auto tuning[475], OtterTune[476] 。

    • 校准(Calibration)。对目标工作负载进行微观实验(Micro experiments)或受控模拟(Controlled simulation),以找出其特点和对其它工作负载的干扰。这些发现被用于更好的QoS控制和调度。例如NyxCache, Quasar系统。

    • 反馈控制回路(Feedback control loop)。这是找到最佳操作点的经典方法。它足够简单。不断增加负载,直到系统监视器报告警告。例子有TMO、Heracles、请求节流、TCP探测最佳发送率。

结语

这篇文章(几乎成了一本书)由两部分组成:软件 架构方法论 和存储 技术设计空间。在第一部分中,本文介绍了软件架构的目的,如何从组织视角上看待,开展它的过程,以及关键的方法论和原则。软件架构是用户场景到工作软件的桥,它处理用户功能和系统属性的复杂性,它在技术设计空间中引导最佳设计,它推动部门间合作并确保交付质量。软件架构是与复杂性的斗争,它构建适应人类思维的模型,力求简单性,自然趋向于人类语言,后者是经过历史考验的现实模型。它成为 结构的艺术,感知组织链条的影响,客户市场的动力,系统属性和技术间的张力,将信息流转变为软件建设的飞轮。

在第二部分,本文介绍了分布式存储系统的技术设计空间。本文首先列出不同存储领域的 参考架构,然后细分了每个存储组件的系统属性和设计空间。存储组件细分章节列出软件架构中需要考虑的领域、组件和系统属性。流行的技术刻录成语言,变为设计模式。 架构设计模式 经常与多个组件交织,并在系统属性间权衡。离散的技术加入到 连续的设计空间 中,地形绘图浮现。分解、搜寻和重组需要的东西,以抵达解决问题的最佳点。存储行业正在快速变化,更强大的硬件,不断增长的规模,新的业务场景,以及对可靠性和成本的持续关注,它们推动技术设计空间不断演变。新机遇出现了。

引用

[1] 忠实地反射世界 : https://www.zhihu.com/question/346067016

[2] COGS : https://en.wikipedia.org/wiki/Cost_of_goods_sold

[3] GXSC’s answer : https://www.zhihu.com/question/24614033/answer/497338972

[4] Score matrix : https://www.productplan.com/learn/prioritization-matrix-example/

[5] Ele.me Payment System : https://mp.weixin.qq.com/s/mtPQLSONUCWOC2HDPRwXNQ

[6] Simple is beauty, Architecture 3 principles : https://xie.infoq.cn/article/5e899856e29017c1079b3be86

[7] MVP : https://en.wikipedia.org/wiki/Minimum_viable_product

[8] Chrome Apps : https://developer.chrome.com/docs/apps/first_app/

[9] Minecraft : http://gametyrant.com/news/5-best-modding-tools-for-minecraft

[10] Opensource Envoy : https://mattklein123.dev/2021/09/14/5-years-envoy-oss/

[11] 道延架构 : https://mp.weixin.qq.com/s?__biz=Mzg5Mjc3MjIyMA==&mid=2247544273&idx=1&sn=d7b9cec80cd593d28b42f5b6179af8be

[12] KISS : https://en.wikipedia.org/wiki/KISS_principle

[13] Conway’s Law : https://en.wikipedia.org/wiki/Conway%27s_law

[14] D Score : https://book.douban.com/subject/26915970/

[15] Measuring software complexity : https://thevaluable.dev/complexity-metrics-software/

[16] Domain-drive Design - Chapter 1 : https://mp.weixin.qq.com/s?__biz=MzA4NTkwODkyMQ==&mid=2651257296&idx=1&sn=7273271d15bc7e2e41da58a155c6e4ab&chksm=84229506b3551c10f20437b06e0e2fb75c1cb0642d5571ea0b30f534a9000b7bb4f2946a393c

[17] 4+1 View : https://zhuanlan.zhihu.com/p/112531852

[18] Design Patterns : https://en.wikipedia.org/wiki/Software_design_pattern#Creational_patterns

[19] Coding Styles : https://google.github.io/styleguide/cppguide.html

[20] Code Refactoring : https://m.douban.com/book/subject/1229923/

[21] Code Complete : https://book.douban.com/subject/1477390/

[22] UML diagrams : https://en.wikipedia.org/wiki/Unified_Modeling_Language

[23] PSP : https://www.geeksforgeeks.org/personal-software-process-psp/

[24] Combining CMMI/PSP : https://www.isixsigma.com/tools-templates/combining-cmmia-psp-tsp-and-six-sigma-software/

[25] CMMI : https://en.wikipedia.org/wiki/Capability_Maturity_Model_Integration

[26] Domain-Driven Design : https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215

[27] IDDD flu vaccine : https://learning.oreilly.com/library/view/implementing-domain-driven-design/9780133039900/ch01lev2sec5.html#ch01lev2sec5

[28] Drive DDD design by language : https://learning.oreilly.com/library/view/implementing-domain-driven-design/9780133039900/ch02lev1sec4.html#ch02lev1sec4

[29] Core domains : https://cloud.tencent.com/developer/article/1709312

[30] Enterprise architecture : https://dev.to/dhruvesh_patel/software-architecture-five-common-design-principles-2il0

[31] 四横三纵 architecture : https://mp.weixin.qq.com/s?__biz=MzI4OTc4MzI5OA==&mid=2247544948&idx=6&sn=e89031d33a1b7f753095164b022ae80d

[32] Alibaba 四横三纵 : https://posts.careerengine.us/p/5f0db6acb5fef84f7de7203d

[33] Kenneth Lee’s blogs : https://gitee.com/Kenneth-Lee-2012/MySummary/tree/master/%E8%BD%AF%E4%BB%B6%E6%9E%84%E6%9E%B6%E8%AE%BE%E8%AE%A1

[34] Kenneth Lee’s articles : https://www.zhihu.com/column/kls-software-arch-world

[35] On Designing and Deploying Internet-Scale Services : https://www.usenix.org/legacy/event/lisa07/tech/full_papers/hamilton/hamilton_html/

[36] EBay’s 三高 design P1 : https://mp.weixin.qq.com/s/bnhXGD7UhwTxL8fpddzAuw

[37] EBay’s 三高 design P2 : https://mp.weixin.qq.com/s/Xyvfx9mLKqquulnrhFi42Q

[38] AWS’s 如何软件开发 : https://mp.weixin.qq.com/s?__biz=MzI4OTc4MzI5OA==&mid=2247520243&idx=1&sn=dfce28433ff14ef188055dc5daf67bd7

[39] SDN : https://en.wikipedia.org/wiki/Software-defined_networking

[40] Netflix microservice architecture : https://medium.com/swlh/a-design-analysis-of-cloud-based-microservices-architecture-at-netflix-98836b2da45f

[41] Kappa architecture : https://towardsdatascience.com/a-brief-introduction-to-two-data-processing-architectures-lambda-and-kappa-for-big-data-4f35c28005bb

[42] Online, nearline, offline : https://netflixtechblog.com/system-architectures-for-personalization-and-recommendation-e081aa94b5d8

[43] Snowflake : https://www.usenix.org/conference/nsdi20/presentation/vuppalapati

[44] Snowflake architecture : https://medium.com/codex/why-snowflake-data-cloud-over-lakehouse-architecture-647b27ecf59e

[45] DDD onion architecture : https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/

[46] React-Redux : https://medium.com/mofed/react-redux-architecture-overview-7b3e52004b6e

[47] Growth mindset : https://www.youtube.com/watch?v=M1CHPnZfFmU

[48] CoolShell design principles : https://coolshell.cn/articles/4535.html

[49] Spring DOI : https://www.baeldung.com/spring-dependency-injection

[50] AspectJ AOP : https://docs.spring.io/spring-framework/docs/4.3.15.RELEASE/spring-framework-reference/html/aop.html

[51] Microsoft SDP : https://azure.microsoft.com/en-us/blog/advancing-safe-deployment-practices/

[52] CAP : https://www.educative.io/blog/what-is-cap-theorem

[53] Patterns of Distributed Systems : https://martinfowler.com/articles/patterns-of-distributed-systems/

[54] Consistent Core : https://martinfowler.com/articles/patterns-of-distributed-systems/consistent-core.html

[55] Replicated Log : https://martinfowler.com/articles/patterns-of-distributed-systems/replicated-log.html

[56] Cloud Design Patterns : https://docs.microsoft.com/en-us/azure/architecture/patterns/

[57] Designing Data-Intensive Applications : https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/

[58] CMU 15-721 : https://15721.courses.cs.cmu.edu/spring2020/schedule.html

[59] On Designing and Deploying Internet-Scale Services : https://www.usenix.org/legacy/event/lisa07/tech/full_papers/hamilton/hamilton_html/index.html

[60] SteveY’s comments : https://coolshell.cn/articles/5701.html

[61] TiDB : https://www.vldb.org/pvldb/vol13/p3072-huang.pdf

[62] Greenplum : https://arxiv.org/pdf/2103.11080.pdf

[63] LSM-based Storage Techniques: A Survey : https://arxiv.org/abs/1812.07527

[64] Zhihu : https://zhuanlan.zhihu.com/p/351241814

[65] Scaling Replicated State Machines with Compartmentalization : https://arxiv.org/abs/2012.15762

[66] An Empirical Evaluation of In-Memory Multi-Version Concurrency Control : http://www.vldb.org/pvldb/vol10/p781-Wu.pdf

[67] In-Memory Big Data Management and Processing : https://www.comp.nus.edu.sg/~ooibc/TKDE-2015-inmemory.pdf

[68] Constructing and Analyzing the LSM Compaction Design Space : http://vldb.org/pvldb/vol14/p2216-sarkar.pdf

[69] Dostoevsky: Better Space-Time Trade-Offs for LSM-Tree : https://www.youtube.com/watch?v=fmXgXripmh0

[70] Latch-free Synchronization in Database Systems, RCU, Lock-free algorithms : http://www.jmfaleiro.com/pubs/latch-free-cidr2017.pdf

[71] Optimal Column Layout for Hybrid Workloads, Optimal Column Layout : https://stratos.seas.harvard.edu/files/stratos/files/caspervldb2020.pdf

[72] Access Path Selection in Main-Memory Optimized Data Systems, Access Path Selection : https://www.eecs.harvard.edu/~kester/files/accesspathselection.pdf

[73] Redis : https://redis.io/

[74] Tendis : https://cloud.tencent.com/developer/article/1815554

[75] Kangaroo cache : https://www.pdl.cmu.edu/PDL-FTP/NVM/McAllister-SOSP21.pdf

[76] Scaling Memcached, Mcrouter : https://www.usenix.org/conference/nsdi13/technical-sessions/presentation/nishtala

[77] CacheLib : https://www.usenix.org/conference/osdi20/presentation/berg

[78] RAMP-TAO cache consistency, Facebook TAO : https://www.vldb.org/pvldb/vol14/p3014-cheng.pdf

[79] BCache : https://bcache.evilpiepirate.org/BcacheGuide/

[80] Ceph : https://segmentfault.com/a/1190000038448569

[81] BtrFS : https://dominoweb.draco.res.ibm.com/reports/rj10501.pdf

[82] XFS : http://www.scs.stanford.edu/nyu/03sp/sched/sgixfs.pdf

[83] EXT4 : https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout

[84] CephFS : https://docs.ceph.com/en/pacific/cephfs/index.html

[85] Dynamic subtree partitioning : https://ceph.io/assets/pdfs/weil-mds-sc04.pdf

[86] Mantle load balancing : https://engineering.ucsc.edu/sites/default/files/technical-reports/UCSC-SOE-15-10.pdf

[87] MDS journaling : https://docs.ceph.com/en/pacific/cephfs/mds-journaling/

[88] Locks : https://docs.ceph.com/en/pacific/cephfs/mdcache/#distributed-locks-in-an-mds-cluster

[89] HopsFS : https://www.usenix.org/conference/fast17/technical-sessions/presentation/niazi

[90] HDFS : https://storageconference.us/2010/Papers/MSST/Shvachko.pdf

[91] Google Filesystem : https://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf

[92] Big Table : https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf

[93] Chubby : https://static.googleusercontent.com/media/research.google.com/en//archive/chubby-osdi06.pdf

[94] HBase : https://hbase.apache.org/

[95] Hive : https://hive.apache.org/

[96] Spark : https://databricks.com/blog/2014/01/21/spark-and-hadoop.html

[97] Hudi : https://hudi.apache.org/docs/comparison/

[98] Isilon : http://doc.isilon.com/onefs/hdfs/02-ifs-c-hdfs-conceptual-topics.htm

[99] Ceph : https://www.ssrc.ucsc.edu/pub/weil-osdi06.html

[100] Ubuntu Openstack storage survey : https://ubuntu.com/blog/openstack-storage

[101] BlueStore, Double-write problem : https://mp.weixin.qq.com/s/dT4mr5iKnQi9-NEvGhI7Pg

[102] Ceph 10 year lessons : https://www.pdl.cmu.edu/PDL-FTP/Storage/ceph-exp-sosp19.pdf

[103] Wisckey : https://www.usenix.org/system/files/conference/fast16/fast16-papers-lu.pdf

[104] Azure Storage : https://azure.microsoft.com/en-us/blog/sosp-paper-windows-azure-storage-a-highly-available-cloud-storage-service-with-strong-consistency/

[105] AWS S3 : https://stackoverflow.com/questions/564223/amazon-s3-architecture

[106] Nutanix : https://www.nutanix.com/hyperconverged-infrastructure

[107] Tectonic, Multitenancy : https://www.usenix.org/conference/fast21/presentation/pan

[108] Copyset Placement : http://www.stanford.edu/~skatti/pubs/usenix13-copysets.pdf

[109] TiDB : https://docs.pingcap.com/tidb/dev/tidb-architecture

[110] XtremIO : https://www.youtube.com/watch?v=lIIwbd5J7bE

[111] SolidFire : https://www.youtube.com/watch?v=AeaGCeJfNBg

[112] Pure Storage : https://www.purestorage.com/products.html

[113] Data Domain : https://www.usenix.org/legacy/events/fast08/tech/full_papers/zhu/zhu.pdf

[114] Rolling hash : https://www.gluster.org/deduplication-part-1-rabin-karp-for-variable-chunking/

[115] Ceph dedup : https://ceph.io/assets/pdfs/ICDCS_2018_mwoh.pdf

[116] Pelican : https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-balakrishnan.pdf

[117] Flamingo : https://www.usenix.org/node/194437

[118] AWS Glacier : https://aws.amazon.com/s3/storage-classes/glacier/

[119] Pergamum : https://www.usenix.org/legacy/event/fast08/tech/full_papers/storer/storer_html/

[120] Tape Library : https://www.snia.org/sites/default/orig/DSI2015/presentations/ColdStorage/OasamuShimizu_Tape_storage_for_cold_data_archive.pdf

[121] CockroachDB, HLC : https://dl.acm.org/doi/pdf/10.1145/3318464.3386134

[122] Google Spanner, Less than 7 ms clock drifts globally : https://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf

[123] Hybrid-Logical Clock : https://www.cockroachlabs.com/docs/stable/architecture/transaction-layer.html

[124] Stores in RocksDB, CockroachDB on RocksDB : https://www.cockroachlabs.com/blog/cockroachdb-on-rocksd/

[125] Raft : https://www.cockroachlabs.com/docs/stable/architecture/replication-layer.html#raft

[126] Write Pipelining : https://www.cockroachlabs.com/blog/transaction-pipelining/

[127] Parallel Commit : https://www.cockroachlabs.com/blog/parallel-commits/

[128] YugabyteDB : https://blog.yugabyte.com/ysql-architecture-implementing-distributed-postgresql-in-yugabyte-db/

[129] YugabyteDB challenges CockroachDB : https://blog.yugabyte.com/yugabytedb-vs-cockroachdb-bringing-truth-to-performance-benchmark-claims-part-2/

[130] Zhihu YugabyteDB/CockroachDB debate : https://www.zhihu.com/question/449949351

[131] CockroachDB rebuts YugabyteDB : https://www.cockroachlabs.com/blog/unpacking-competitive-benchmarks/

[132] Percolator : https://github.com/pingcap/tla-plus/blob/master/Percolator/Percolator.tla

[133] TiFlash : https://docs.pingcap.com/zh/tidb/dev/tiflash-overview

[134] Greenplum’s related works, GreenPlum : https://arxiv.org/pdf/2103.11080

[135] F1 Lightning : http://www.vldb.org/pvldb/vol13/p3313-yang.pdf

[136] OceanBase : https://zhuanlan.zhihu.com/p/93721603

[137] Hybrid row-column data layout : https://dbdb.io/db/oceanbase

[138] X-Engine : https://www.cs.utah.edu/~lifeifei/papers/sigmod-xengine.pdf

[139] PolarDB : https://www.usenix.org/conference/fast20/presentation/cao-wei

[140] SeaStar : https://www.scylladb.com/2016/03/18/generalist-engineer-cassandra-performance/

[141] Smart SSD paper : https://cacm.acm.org/magazines/2019/6/237002-programmable-solid-state-storage-in-future-cloud-datacenters/fulltext

[142] PolarDB Serverless : http://www.cs.utah.edu/~lifeifei/papers/polardbserverless-sigmod21.pdf

[143] AnalyticDB : http://www.vldb.org/pvldb/vol12/p2059-zhan.pdf

[144] Pangu : https://www.alibabacloud.com/blog/pangu%E2%80%94the-highperformance-distributed-file-system-by-alibaba-cloud_594059

[145] Fuxi : http://www.vldb.org/pvldb/vol7/p1393-zhang.pdf

[146] YARN : https://www.cnblogs.com/liangzilx/p/14837562.html

[147] Lambda architecture : https://www.cnblogs.com/listenfwind/p/13221236.html

[148] ClickHouse : https://clickhouse.com/docs/en/development/architecture/

[149] MergeTree : https://developer.aliyun.com/article/762092

[150] AWS Redshift, Warmpools : https://assets.amazon.science/93/e0/a347021a4c6fbbccd5a056580d00/sigmod22-redshift-reinvented.pdf

[151] AWS Nitro : https://aws.amazon.com/ec2/nitro/

[152] Serial Safe Net : https://arxiv.org/pdf/1605.04292.pdf

[153] Log is database 1 : https://zhuanlan.zhihu.com/p/33603518

[154] Log is database 2 : https://zhuanlan.zhihu.com/p/338582762

[155] Log is database 3 : https://zhuanlan.zhihu.com/p/151086982

[156] AWS Aurora Multi-master : https://www.allthingsdistributed.com/2019/03/Amazon-Aurora-design-cloud-native-relational-database.html

[157] CORFU : https://blog.acolyer.org/2017/05/02/corfu-a-distributed-shared-log/

[158] Delos : https://www.usenix.org/system/files/osdi20-balakrishnan.pdf

[159] Helios Indexing, Helios : http://www.vldb.org/pvldb/vol13/p3231-potharaju.pdf

[160] FoundationDB : https://www.foundationdb.org/files/fdb-paper.pdf

[161] HyderDB : http://www.cs.cornell.edu/~blding/pub/hyder_sigmod_2015.pdf

[162] HyPer : https://hyper-db.de/

[163] Vectorized query execution : https://www.vldb.org/pvldb/vol11/p2209-kersten.pdf

[164] LLVM : https://stackoverflow.com/questions/2354725/what-exactly-is-llvm

[165] Morsel-driven execution scheduling : https://db.in.tum.de/~leis/papers/morsels.pdf

[166] SAP HANA : http://sites.computer.org/debull/A12mar/hana.pdf

[167] MemSQL : https://www.singlestore.com/blog/revolution/

[168] GemFire used by 12306.cn : https://blog.csdn.net/u014756827/article/details/102610104

[169] Hekaton : https://www.microsoft.com/en-us/research/publication/hekaton-sql-servers-memory-optimized-oltp-engine/

[170] Bw-Tree : https://www.cs.cmu.edu/~huanche1/publications/open_bwtree.pdf

[171] Page Mapping Table, Bw-tree : https://www.microsoft.com/en-us/research/publication/the-bw-tree-a-b-tree-for-new-hardware/

[172] LLAMA : https://db.disi.unitn.eu//pages/VLDBProgram/pdf/research/p853-levandoski.pdf

[173] DocumentDB : https://www.vldb.org/pvldb/vol8/p1668-shukla.pdf

[174] Project Siberia : http://www.vldb.org/pvldb/vol6/p1714-kossmann.pdf

[175] Offline : https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/p1016-eldawy.pdf

[176] ART tree : https://db.in.tum.de/~leis/papers/ART.pdf

[177] Masstree : https://pdos.csail.mit.edu/papers/masstree:eurosys12.pdf

[178] Be-tree : https://www.usenix.org/conference/fast15/technical-sessions/presentation/jannen

[179] VMWare copy files : https://www.usenix.org/conference/fast20/presentation/zhan

[180] BloomFilter : http://oserror.com/backend/bloomfilter/

[181] SuRF : https://db.cs.cmu.edu/papers/2018/mod601-zhangA-hm.pdf

[182] FaRM : https://www.microsoft.com/en-us/research/project/farm/

[183] A1 : https://arxiv.org/abs/2004.05712

[184] DCQCN : https://blog.csdn.net/hithj_cainiao/article/details/117292144

[185] Silo : https://wzheng.github.io/silo.pdf

[186] Manycore : https://taesoo.kim/pubs/2016/min:fxmark.pdf

[187] Linux Kernel manycore, Sloppy Counter : https://pdos.csail.mit.edu/papers/linux:osdi10.pdf

[188] Filesystems manycore : https://taesoo.kim/pubs/2016/min:fxmark-slides.pdf

[189] Epoch-based Reclamation : https://aturon.github.io/blog/2015/08/27/epoch/#epoch-based-reclamation

[190] Flat Combining : https://www.cs.bgu.ac.il/~hendlerd/papers/flat-combining.pdf

[191] NetApp Waffinity : https://www.usenix.org/conference/osdi16/technical-sessions/presentation/curtis-maury

[192] RocksDB : http://rocksdb.org/

[193] KV backend in many systems : https://en.wikipedia.org/wiki/RocksDB

[194] MySQL MyRocks, RocksDB : https://vldb.org/pvldb/vol13/p3217-matsunobu.pdf

[195] TiDB on RocksDB : https://docs.pingcap.com/tidb/dev/rocksdb-overview/

[196] BlueStore and RocksDB : http://www.yangguanjun.com/2018/10/25/ceph-bluestore-rocksdb-analyse/

[197] RocksDB FAQ : http://rocksdb.org/docs/support/faq.html

[198] Universal Compaction : https://github.com/facebook/rocksdb/wiki/Universal-Compaction

[199] Remote Compaction : https://zhuanlan.zhihu.com/p/419766888

[200] PebblesDB : https://www.cs.utexas.edu/~vijay/papers/pebblesdb-sosp17-slides.pdf

[201] SST “Guards” : https://vigourtyy-zhg.blog.csdn.net/article/details/109005795

[202] MongoDB : https://engineering.mongodb.com/papers

[203] IPO : https://www.cnbc.com/2017/10/19/mongodb-mdb-ipo-stock-price-on-first-trading-day.html

[204] HBase : https://segmentfault.com/a/1190000019959411

[205] ZooKeeper : https://mikechen.cc/4657.html

[206] Replicated State Machine : https://www.youtube.com/watch?v=TWp6H7mb09A

[207] Percolator : https://research.google/pubs/pub36726/

[208] ByteSQL : https://mp.weixin.qq.com/s/DvUBnWBqb0XGnicKUb-iqg

[209] Bytable : https://mp.weixin.qq.com/s/oV5F_K2mmE_kK77uEZSjLg

[210] Lindorm : https://zhuanlan.zhihu.com/p/407175099

[211] Cassandra : https://www.cs.cornell.edu/projects/ladis2009/papers/lakshman-ladis2009.pdf

[212] Dynamo : http://docs.huihoo.com/amazon/Dynamo-Amazon-Highly-Available-Key-Value-Store.pdf

[213] DynamoDB : https://www.allthingsdistributed.com/2012/01/amazon-dynamodb.html

[214] Paper, Predictable performance, Bi-modality : https://www.usenix.org/conference/atc22/presentation/elhemali

[215] Gossip : http://kaiyuan.me/2015/07/08/Gossip/

[216] Consistent Hashing : https://www.toptal.com/big-data/consistent-hashing

[217] Service Fabric : https://dl.acm.org/doi/pdf/10.1145/3190508.3190546

[218] Geospatial data : https://www.baeldung.com/elasticsearch-geo-spatial

[219] ElasticSearch scaleout : https://www.cnblogs.com/sgh1023/p/15691061.html

[220] ELK stack : https://www.elastic.co/what-is/elk-stack

[221] InfluxDB : https://www.influxdata.com/_resources/techpapers-new/

[222] OpenTSDB : https://zhuanlan.zhihu.com/p/111511463

[223] Prometheus : https://logz.io/blog/prometheus-influxdb/

[224] InfluxDB IoT : https://www.influxdata.com/blog/how-influxdb-iot-data/

[225] Graphene : https://www.usenix.org/conference/fast17/technical-sessions/presentation/liu

[226] GraphLab : https://arxiv.org/ftp/arxiv/papers/1408/1408.2041.pdf

[227] Neo4J : https://neo4j.com/

[228] ArangoDB : https://www.arangodb.com/

[229] JSON document graph : https://www.g2.com/categories/graph-databases

[230] OrientDB : http://www.enotes.vip/index.php/tz_enotes/Article/showArticleReader.html?art_id=513

[231] Multi-model database : https://db-engines.com/en/system/ArangoDB%3BNeo4j%3BOrientDB

[232] MySQL : https://www.usenix.org/system/files/conference/atc13/atc13-bronson.pdf

[233] FaRM A1 : https://ashamis.github.io/files/A1-A-Distributed-In-Memory-Graph-Database.pdf

[234] AWS Neptune : https://aws.amazon.com/neptune/

[235] CosmosDB : https://azure.microsoft.com/en-us/blog/a-technical-overview-of-azure-cosmos-db/

[236] ByteGraph : https://www.vldb.org/pvldb/vol15/p3306-li.pdf

[237] TerarkDB : https://www.zhihu.com/question/46787984

[238] Gremlin API : https://tinkerpop.apache.org/gremlin.html

[239] Apache Hudi : https://zhuanlan.zhihu.com/p/450041140

[240] Datalake contemporaries : https://www.slideshare.net/databricks/a-thorough-comparison-of-delta-lake-iceberg-and-hudi

[241] Delta Lake : https://databricks.com/wp-content/uploads/2020/08/p975-armbrust.pdf

[242] Apache Iceberg : https://www.dremio.com/resources/guides/apache-iceberg-an-architectural-look-under-the-covers/

[243] Lakehouse : https://databricks.com/blog/2020/01/30/what-is-a-data-lakehouse.html

[244] F1 Query : http://www.vldb.org/pvldb/vol11/p1835-samwel.pdf

[245] F1 : https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/41344.pdf

[246] Kafka Transactional : https://assets.confluent.io/m/2aaa060edb367628/original/20210504-WP-Consistency_and_Completeness_Rethinking_Optimized_Distributed_Stream_Processing_in_Apache_Kafka-pdf.pdf

[247] Kappa architecture : https://blog.twitter.com/engineering/en_us/topics/infrastructure/2021/processing-billions-of-events-in-real-time-at-twitter-

[248] Spark, RDD : https://spark.apache.org/docs/latest/rdd-programming-guide.html

[249] Spark stream processing : https://spark.apache.org/docs/latest/streaming-programming-guide.html

[250] Stream processing contemporaries : https://medium.com/@chandanbaranwal/spark-streaming-vs-flink-vs-storm-vs-kafka-streams-vs-samza-choose-your-stream-processing-91ea3f04675b

[251] Flink : https://flink.apache.org/

[252] Checkpointed 2PC exactly-once : https://www.infoq.com/news/2021/11/exactly-once-uber-flink-kafka/

[253] Ack by XOR of path nodes : https://hps.vi4io.org/_media/teaching/wintersemester_2017_2018/bd1718-11-streams.pdf#20

[254] NOVA : https://www.usenix.org/conference/fast16/technical-sessions/presentation/xu

[255] ART and hashtable : https://bigdata.uni-saarland.de/publications/ARCD15.pdf

[256] Level Hashing : https://www.usenix.org/conference/osdi18/presentation/zuo

[257] Orion : https://www.usenix.org/system/files/fast19-yang.pdf

[258] Octopus : https://www.usenix.org/conference/atc17/technical-sessions/presentation/lu

[259] DAX : https://blog.csdn.net/maokelong95/article/details/107195192

[260] PMEM guide, PMEM commit protocols : https://www.usenix.org/system/files/login/articles/login_summer17_07_rudoff.pdf

[261] SplitFS : https://arxiv.org/abs/1909.10123

[262] Kuco : https://www.usenix.org/conference/fast21/presentation/chen-youmin

[263] ZoFS : https://ipads.se.sjtu.edu.cn/_media/publications/dongsosp19-rev.pdf

[264] AWS S3 : https://docs.snowflake.com/en/user-guide/data-load-s3.html

[265] Snowflake went IPO : https://edition.cnn.com/2020/09/16/investing/snowflake-ipo/index.html

[266] Service Mesh : https://istio.io/latest/about/service-mesh/

[267] Envoy : https://istio.io/latest/docs/ops/deployment/architecture/

[268] Spring Cloud : https://xie.infoq.cn/article/2baee95d42ed7f8dd83cec170

[269] Dominant Resource Fairness : https://cs.stanford.edu/~matei/papers/2011/nsdi_drf.pdf

[270] Cloud Resource Scheduling : https://www.researchgate.net/publication/293329163_A_Survey_on_Resource_Scheduling_in_Cloud_Computing_Issues_and_Challenges

[271] YARN : https://mp.weixin.qq.com/s/9A0z0S9IthG6j8pZe6gCnw

[272] 2DFQ : https://cs.brown.edu/~jcmace/papers/mace162dfq.pdf

[273] Quasar : http://csl.stanford.edu/~christos/publications/2014.quasar.asplos.pdf

[274] CGroup : https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_monitoring_and_updating_the_kernel/using-cgroups-v2-to-control-distribution-of-cpu-time-for-applications_managing-monitoring-and-updating-the-kernel

[275] K8S scheduling : https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/

[276] Ceph QoS : https://docs.ceph.com/en/latest/rados/configuration/mclock-config-ref/

[277] MClock : https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Gulati.pdf

[278] Leaky bucket : https://blog.51cto.com/leyew/860302

[279] Heracles : https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43792.pdf

[280] Cost Modeling : https://github.com/pingcap/tidb/blob/master/planner/core/task.go#L260

[281] Comprehensive cost modeling methods : https://15721.courses.cs.cmu.edu/spring2020/schedule.html#apr-15-2020

[282] Akkio : https://www.usenix.org/conference/osdi18/presentation/annamalai

[283] Taiji : https://research.facebook.com/publications/taiji-managing-global-user-traffic-for-large-scale-internet-services-at-the-edge/

[284] SocialHash : https://blog.acolyer.org/2016/05/25/socialhash-an-assignment-framework-for-optimizing-distributed-systems-operations-on-social-networks/

[285] Hyperspace : https://www.microsoft.com/en-us/research/publication/hyperspace-the-indexing-subsystem-of-azure-synapse/

[286] SLIK : https://www.usenix.org/system/files/conference/atc16/atc16_paper-kejriwal.pdf

[287] RAMCloud : https://ramcloud.atlassian.net/wiki/spaces/RAM/pages/6848671/RAMCloud+Papers

[288] HBase Secondary Index : http://ceur-ws.org/Vol-1810/DOLAP_paper_10.pdf

[289] LSM-tree survey : https://arxiv.org/pdf/1812.07527.pdf

[290] Facebook Owl : https://www.facebook.com/atscaleevents/videos/2897218060568137/?t=739

[291] Copyset : https://www.usenix.org/conference/atc13/technical-sessions/presentation/cidon

[292] MAPX : https://www.usenix.org/conference/fast20/presentation/wang-li

[293] HDFS : https://vanducng.dev/2020/12/05/Compact-multiple-small-files-on-HDFS/

[294] Distributed Transactions : https://mp.weixin.qq.com/s/HLIiUc6ZwGgVjZu7VJEzWQ

[295] Epoch : https://wongxingjun.github.io/2015/05/18/Paxos%E7%AE%97%E6%B3%95%E7%9A%84%E4%B8%80%E7%A7%8D%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3/

[296] Fencing token : https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

[297] Raft, Leader Paxos : https://raft.github.io/

[298] Vector clocks : https://newbiettn.github.io/2014/05/03/lamport-clock-vs-vector-clock/

[299] Atomic disk sector write : https://www.sqlite.org/atomiccommit.html

[300] Atomic memory pointer switch, 64-bit reads/writes are atomic : https://stackoverflow.com/questions/78277/how-to-guarantee-64-bit-writes-are-atomic

[301] Wound-wait : https://cloud.google.com/spanner/docs/whitepapers/life-of-reads-and-writes

[302] C++ memory model : https://www.youtube.com/watch?v=A_vAG6LIHwQ

[303] Programming locks : https://compas.cs.stonybrook.edu/~nhonarmand/courses/fa17/cse306/slides/11-locks.pdf

[304] B+-tree locking techniques, 数据库内核月报 B+tree : http://mysql.taobao.org/monthly/2018/09/01/

[305] Linux Kernel synchronization : https://mirrors.edge.kernel.org/pub/linux/kernel/people/christoph/gelato/gelato2005-paper.pdf

[306] Database transactions, ARIES, 阿莱克西斯 ARIES : https://zhuanlan.zhihu.com/p/143173278

[307] FFS : https://www.ece.cmu.edu/~ganger/papers/usenix2000.pdf

[308] Optimistic Crash Consistency : https://research.cs.wisc.edu/adsl/Publications/optfs-sosp13.pdf

[309] Out-of-order commit : https://www.zhihu.com/question/278984902

[310] Megastore, Google Megastore : http://cidrdb.org/cidr2011/Papers/CIDR11_Paper32.pdf

[311] Chain Replication : https://sigops.org/s/conferences/sosp/2011/current/2011-Cascais/printable/11-calder.pdf

[312] MySQL BinLog Replication : https://hevodata.com/learn/mysql-binlog-based-replication/

[313] Compensation Transaction : https://developer.jboss.org/docs/DOC-48610

[314] Redis replication : https://redis.io/docs/manual/replication/

[315] RBD diff : https://ceph.io/en/news/blog/2013/incremental-snapshots-with-rbd/

[316] Google Spanner : https://cloud.google.com/spanner/docs/replication

[317] TrueTime : https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45855.pdf

[318] CockroachDB : https://www.cockroachlabs.com/blog/geo-partitioning-one/

[319] Flashcache : https://github.com/facebookarchive/flashcache/blob/master/doc/flashcache-doc.txt

[320] B+-tree : https://www.zhihu.com/question/516912481/answer/2403713321

[321] TMO : https://www.cs.cmu.edu/~dskarlat/publications/tmo_asplos22.pdf

[322] PMEM empirical guide : https://www.usenix.org/conference/fast20/presentation/yang

[323] Memory page swap : https://github.com/torvalds/linux/blob/master/mm/workingset.c

[324] Google G-SWAP : https://research.google/pubs/pub48551/

[325] REMIX LSM-tree : https://zhuanlan.zhihu.com/p/357024916

[326] Project Catapult : https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/Catapult_ISCA_2014.pdf

[327] SmartNICs : https://zhuanlan.zhihu.com/p/393393682

[328] IPU : https://www.forbes.com/sites/karlfreund/2021/08/02/nvidia-dpu–intel-ipu-game-changers-or-just-smart-nics/

[329] DPU : https://en.wikipedia.org/wiki/Data_processing_unit

[330] Smart SSD : https://www.youtube.com/watch?v=_8gEmK1L4EY

[331] Late materialization : https://web.stanford.edu/class/cs245/win2020/readings/c-store-compression.pdf

[332] SIMD vectorization and JIT compile : https://15721.courses.cs.cmu.edu/spring2020/papers/16-vectorization2/p2209-kersten.pdf

[333] Succinct data structures : https://www2.eecs.berkeley.edu/Pubs/TechRpts/2019/EECS-2019-141.pdf

[334] Parquet : https://github.com/apache/parquet-format

[335] NVMe protocol : https://www.seagate.com/files/www-content/product-content/ssd-fam/nvme-ssd/nytro-xf1440-ssd/_shared/docs/an-introduction-to-nvme-tp690-1-1605us.pdf

[336] FStream : https://www.usenix.org/conference/fast18/presentation/rho

[337] Apache ORC : https://medium.com/data-engineer-things/demystify-hadoop-data-formats-avro-orc-and-parquet-e428709cf3bb

[338] Z-Order : https://zhuanlan.zhihu.com/p/491256487

[339] “tiering” vs “leveling” : https://zhuanlan.zhihu.com/p/112574579

[340] Data clustering & data skipping : https://zhuanlan.zhihu.com/p/354334895

[341] TRIAD : https://github.com/epfl-labos/TRIAD

[342] Scalable MVCC GC paper : http://www.vldb.org/pvldb/vol13/p128-bottcher.pdf

[343] Compression algorithm selection taxonomy : https://www.cs.umd.edu/~abadi/talks/Column_Store_Tutorial_VLDB09.pdf

[344] LZ77 : https://www.youtube.com/watch?v=jVcTrBjI-eE

[345] Anti-entropy stage : https://stackoverflow.com/questions/55547113/why-to-combine-huffman-and-lz77

[346] FSST string symbol table : https://www.vldb.org/pvldb/vol13/p2649-boncz.pdf

[347] Zstd dictionary mode : https://ayende.com/blog/189954-A/random-access-compression-and-zstd

[348] LZ-End : https://users.dcc.uchile.cl/~gnavarro/ps/dcc10.1.pdf

[349] FiniteStateEntropy : https://github.com/Cyan4973/FiniteStateEntropy

[350] Perfect hashing : https://en.wikipedia.org/wiki/Perfect_hash_function

[351] False sharing : https://trishagee.com/2011/07/22/dissecting_the_disruptor_why_its_so_fast_part_two__magic_cache_line_padding/

[352] Lock coupling : https://15721.courses.cs.cmu.edu/spring2016/papers/a16-graefe.pdf

[353] Sketch structures : https://dsf.berkeley.edu/cs286/papers/synopses-fntdb2012.pdf

[354] Clustered into index : https://docs.microsoft.com/en-us/sql/relational-databases/indexes/clustered-and-nonclustered-indexes-described?view=sql-server-ver15

[355] Cuckoo hashing : https://en.wikipedia.org/wiki/Cuckoo_hashing

[356] HotRing : https://www.usenix.org/system/files/fast20-chen_jiqiang.pdf

[357] Consistent Hashing : https://medium.com/system-design-blog/consistent-hashing-b9134c8a9062

[358] Skiplist : https://en.wikipedia.org/wiki/Skip_list

[359] Radix tree : https://en.wikipedia.org/wiki/Radix_tree

[360] Memory management : https://lwn.net/Articles/175432/

[361] Red-back tree : https://en.wikipedia.org/wiki/Red%E2%80%93black_tree

[362] AVL tree : https://stackoverflow.com/questions/5288320/why-is-stdmap-implemented-as-a-red-black-tree

[363] Rust implements ordered map with B-tree : https://www.zhihu.com/question/516912481

[364] B+-tree has another Rust discussion : https://github.com/rust-lang/rust/issues/27090

[365] ARIES : https://cs.stanford.edu/people/chrismre/cs345/rl/aries.pdf

[366] B+-tree locking techniques : https://15721.courses.cs.cmu.edu/spring2017/papers/06-latching/a16-graefe.pdf

[367] Bitmap index : https://www.oracle.com/technical-resources/articles/sharma-indexes.html

[368] Bit-vector compression : https://www.cs.umd.edu/~abadi/talks/Column_Store_Tutorial_VLDB09.pdf#page=52

[369] Inverted index : https://codingexplained.com/coding/elasticsearch/understanding-the-inverted-index-in-elasticsearch

[370] TF-IDF : https://en.wikipedia.org/wiki/Tf%E2%80%93idf

[371] PageRank algorithm : https://en.wikipedia.org/wiki/PageRank

[372] Eigenvector : https://blog.csdn.net/sdgihshdv/article/details/77340966

[373] Succinct wiki : https://en.wikipedia.org/wiki/Succinct_data_structure

[374] FM-index : https://www.youtube.com/watch?v=kvVGj5V65io

[375] Burrows-Wheeler Transform : https://www.youtube.com/watch?v=4n7NPk5lwbI

[376] Compressed Suffix Array, SuccinctStore paper : https://www.usenix.org/conference/nsdi15/technical-sessions/presentation/agarwal

[377] Succinct Trie : https://www.cs.cmu.edu/~huanche1/publications/surf_paper.pdf

[378] TerakaDB/ToplingDB : https://www.zhihu.com/question/46787984/answer/103639893

[379] Spark RDD : https://databricks.com/blog/2015/11/10/succinct-spark-from-amplab-queries-on-compressed-rdds.html

[380] GitHub AMPLab/Succinct : https://github.com/amplab/succinct

[381] GitHub simongog/sdsl-lite : https://github.com/simongog/sdsl-lite

[382] Genomes compression : https://academic.oup.com/bioinformatics/article/27/21/2979/217176?view=extract

[383] LZ-End : https://drops.dagstuhl.de/opus/volltexte/2017/7847/pdf/LIPIcs-ESA-2017-53.pdf

[384] Write through : https://www.zhihu.com/question/319817091

[385] InfiniFS : https://zhuanlan.zhihu.com/p/492210459

[386] Social Hashing : https://blog.acolyer.org/2019/11/15/facebook-taiji/

[387] RAIDShield : https://www.usenix.org/system/files/conference/fast15/fast15-paper-ma.pdf

[388] UBER : https://www.jedec.org/standards-documents/dictionary/terms/uncorrectable-bit-error-rate-uber

[389] CRC wiki : https://en.wikipedia.org/wiki/Cyclic_redundancy_check

[390] CRC lecture : https://www.cs.princeton.edu/courses/archive/spring18/cos463/lectures/L08-error-control.pdf

[391] AWS AZ : https://cloud.netapp.com/blog/aws-availability-using-single-or-multiple-availability-zones

[392] Azure redundancy : https://docs.microsoft.com/en-us/azure/storage/common/storage-redundancy

[393] Observational Difference : https://www.microsoft.com/en-us/research/wp-content/uploads/2017/06/paper-1.pdf

[394] Exponential distribution : http://web.stanford.edu/~lutian/coursepdf/unit1.pdf

[395] Query optimizers : https://mp.weixin.qq.com/s?__biz=MzI5Mjk3NDUyNA==&mid=2247483895&idx=1&sn=05b687a465f5e705dbebfdccaf478f4b

[396] The power of two random choices : https://brooker.co.za/blog/2012/01/17/two-random.html

[397] Queuing theory : https://lrita.github.io/images/posts/math/%E6%8E%92%E9%98%9F%E8%AE%BA%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8%E6%B5%85%E6%9E%90.pdf

[398] NyxCache paper : https://www.usenix.org/conference/fast22/presentation/wu

[399] DPDK : https://www.dpdk.org/

[400] SPDK : https://spdk.io/

[401] BlueFS : https://zhuanlan.zhihu.com/p/46362124

[402] Computational SSD drives : https://www.usenix.org/system/files/fast22-qiao.pdf

[403] NVLink : https://ieeexplore.ieee.org/document/7924274

[404] OpenTelemetry : https://opentelemetry.io/docs/concepts/

[405] Google Dapper : https://research.google/pubs/pub36356/

[406] Queuing layout : https://zhuanlan.zhihu.com/p/22124514

[407] DRAM banks : https://zhuanlan.zhihu.com/p/539717599

[408] Parallelism at Plane level : https://blog.51cto.com/alanwu/1544227

[409] Speculative execution : https://www.usenix.org/conference/fast22/presentation/lv

[410] Coyote : https://github.com/microsoft/coyote

[411] TLA+ : https://lamport.azurewebsites.net/tla/book.html

[412] Interactive latency : https://colin-scott.github.io/personal_website/research/interactive_latency.html

[413] Power component comparison : https://www.researchgate.net/figure/Power-consumption-of-CPU-memory-network-and-disk-for-various-computing-processes-19_fig1_327203171

[414] Clos network architecture : https://www.youtube.com/watch?v=XrnATy3AvpA

[415] OSPF, RIP, BGP : https://www.youtube.com/watch?v=KjNYEzEBRD8

[416] ECMP : https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing

[417] WCMP : https://www.researchgate.net/publication/266657103_WCMP_Weighted_cost_multipathing_for_improved_fairness_in_data_centers

[418] ECWide : https://www.usenix.org/conference/fast21/presentation/hu

[419] Google Jupiter Rising : https://conferences.sigcomm.org/sigcomm/2015/pdf/papers/p183.pdf

[420] Google Orion SDN : https://www.usenix.org/conference/nsdi21/presentation/ferguson

[421] Dragonfly topology : https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/34926.pdf

[422] Dragonfly+ topology : https://www.researchgate.net/profile/Eitan-Zahavi/publication/313341364_Dragonfly_Low_Cost_Topology_for_Scaling_Datacenters/links/5a30c4baaca27271ec8a1201/Dragonfly-Low-Cost-Topology-for-Scaling-Datacenters.pdf

[423] Google B4 After : https://dl.acm.org/doi/pdf/10.1145/3230543.3230545

[424] Optical Circuit Switch : https://arxiv.org/abs/2208.10041

[425] Google Jupiter Evolving : https://research.google/pubs/pub51587/

[426] OpenFlow : https://medium.com/@fiberoptics/openvswitch-and-openflow-what-are-they-whats-their-relationship-d0ccd39b9a5c

[427] Traffic Engineering : https://www.youtube.com/watch?v=Hfl-i56hZUg&loop=0

[428] OpenDaylight : https://www.fiber-optic-transceiver-module.com/openstack-vs-opendaylight-vs-openflow-vs-openvswitch-whatre-their-relations.html

[429] Openstack Neutron : https://docs.openstack.org/neutron/latest/

[430] Open vSwitch : https://en.wikipedia.org/wiki/Open_vSwitch

[431] Control plane network : https://www.usenix.org/system/files/nsdi21-ferguson.pdf

[432] Google B4 Experience : https://dl.acm.org/doi/pdf/10.1145/2486001.2486019

[433] Google datacenter network : https://www.youtube.com/watch?v=kythGOICErQ&t=1244s

[434] F5 BIG-IP : https://www.f5.com/glossary/load-balancer

[435] Google Maglev : https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/eisenbud

[436] UCloud Vortex : https://www.ucloud.cn/yun/34767.html

[437] NGINX : https://www.nginx.com/products/nginx/high-availability/

[438] HAProxy : https://www.digitalocean.com/community/tutorials/how-to-set-up-highly-available-haproxy-servers-with-keepalived-and-reserved-ips-on-ubuntu-14-04

[439] LVS : https://en.wikipedia.org/wiki/Linux_Virtual_Server

[440] Keepalived : http://www.keepalived.org/

[441] OSI model : https://www.imperva.com/learn/application-security/osi-model/

[442] NATs : https://en.wikipedia.org/wiki/Network_address_translation#Type_of_NAT_and_NAT_traversal,_role_of_port_preservation_for_TCP

[443] Sticky session : https://www.imperva.com/learn/availability/sticky-session-persistence-and-cookies/

[444] API gateway : https://microservices.io/patterns/apigateway.html

[445] Direct server return : https://docs.bluecatnetworks.com/r/DNS-Edge-Deployment-Guide/How-DSR-load-balancing-works

[446] ECN : https://en.wikipedia.org/wiki/Explicit_Congestion_Notification

[447] PFC : https://en.wikipedia.org/wiki/Ethernet_flow_control

[448] Head-of-line blocking : https://en.wikipedia.org/wiki/Head-of-line_blocking

[449] DCQCN : https://conferences.sigcomm.org/sigcomm/2015/pdf/papers/p523.pdf

[450] RoCEv2, CNP : https://en.wikipedia.org/wiki/RDMA_over_Converged_Ethernet

[451] QCN : https://1.ieee802.org/dcb/802-1qau/

[452] DCTCP : https://www.microsoft.com/en-us/research/wp-content/uploads/2017/01/dctcp-sigcomm2010.pdf

[453] Slow start : https://en.wikipedia.org/wiki/TCP_congestion_control

[454] BBR : https://www.zhihu.com/question/53559433

[455] InfiniBand : https://en.wikipedia.org/wiki/InfiniBand

[456] RDMA design guidelines : https://www.usenix.org/system/files/conference/atc16/atc16_paper-kalia.pdf

[457] VxLAN : https://en.wikipedia.org/wiki/Virtual_Extensible_LAN

[458] GRE : https://en.wikipedia.org/wiki/Generic_Routing_Encapsulation

[459] Nitro card : https://docs.aws.amazon.com/whitepapers/latest/security-design-of-aws-nitro-system/the-components-of-the-nitro-system.html

[460] SR-IOV : https://learn.microsoft.com/en-us/windows-hardware/drivers/network/overview-of-single-root-i-o-virtualization–sr-iov-

[461] Azure SmartNIC : https://www.microsoft.com/en-us/research/project/azure-smartnic/

[462] RabbitMQ : https://www.rabbitmq.com/

[463] Varint encoding : https://developers.google.com/protocol-buffers/docs/encoding

[464] Protobuf : https://en.wikipedia.org/wiki/Protocol_Buffers

[465] Bond : http://microsoft.github.io/bond/manual/bond_cs.html

[466] Thrift : https://stackoverflow.com/questions/69316/biggest-differences-of-thrift-vs-protocol-buffers

[467] FlatBuffers : https://stackoverflow.com/questions/25356551/whats-the-difference-between-protocol-buffers-and-flatbuffers

[468] Apache Arrow : https://stackoverflow.com/questions/56472727/difference-between-apache-parquet-and-arrow

[469] Apache Avro : https://www.clairvoyant.ai/blog/big-data-file-formats

[470] Client Protocol Redesign : https://15721.courses.cs.cmu.edu/spring2020/papers/11-networking/p1022-muehleisen.pdf

[471] Btrfs compression COW : https://superuser.com/questions/858219/btrfs-filesystem-compression-and-copy-on-write

[472] Ceph BlueStore compression : https://www.spinics.net/lists/ceph-devel/msg28846.html

[473] CMDB : https://en.wikipedia.org/wiki/Configuration_management_database

[474] AWS Redshift ATO : https://aws.amazon.com/blogs/big-data/automate-your-amazon-redshift-performance-tuning-with-automatic-table-optimization/

[475] Azure SQL Database auto tuning : https://learn.microsoft.com/en-us/azure/azure-sql/database/automatic-tuning-overview?view=azuresql

[476] OtterTune : https://ottertune.com/features/

[477] Why ClickHouse is fast : https://clickhouse.tech/docs/en/faq/general/why-clickhouse-is-so-fast/

[478] ElasticSearch : https://en.wikipedia.org/wiki/Elasticsearch

[479] Premature Optimization : https://stackify.com/premature-optimization-evil/

[480] Case Interview Examples : https://www.craftingcases.com/case-interview-examples/

[481] MECE: https://strategyu.co/wtf-is-mece-mutually-exclusive-collectively-exhaustive/

[482] Clique : https://en.wikipedia.org/wiki/Clique_(graph_theory)



Create an Issue or comment below