查看原文
其他

前Oracle架构师:如何实现分布式平台的内核设计

2016-05-17 董健 InfoQ
本文是第一期InfoQ「大咖说」视频直播内容整理:前贝尔实验室、Oracle架构师董健基于多年工作经验,站在内核实现的角度去看如何设计一个分布式平台。微信后台回复关键词「内核」获取视频回放链接。文末下期分享预告!

引言

我们今天的分享的内容都是从具体的产品实现中抽象出来的。这些设计方法在操作系统、中间件、数据库、分布式平台和大数据平台中都在使用,大部分其实就是我们在具体产品中使用的设计。

托尔斯泰曾经说过一句名言:“优秀的架构都是相似的,垃圾的架构各有各的垃圾之处”。分布式平台作为容器产品,需要解决的问题都是类似的,每一个问题的解决方案其实就那么几种,每个方案都有其优缺点,然后做取舍做选择。

通过这场分享,希望帮助大家去了解你们正在使用的平台的定位,解决了哪些问题,都是如何解决的,哪些问题有潜在的坑不能踩。没有解决的问题的延伸解决方案都是什么,和其他平台如何融合等等。


图1:分享提纲

分布式架构为弹性应用解困

任何有点规模的应用,都无法回避分布式架构。分布式架构的存在就是为了解决大规模的应用。大规模的应用不好走,小规模应用无缝过渡到大型规模应用更不好做,从大型规模应用无缝降级为小规模应用则更难做。这就是我们经常说的弹性应用。

弹性应用架构的特点就是能够利用弹性的计算资源来解决问题,分布式必然成为架构中最核心的因素。我们一起看一下分布式如何在这种架构中贯穿和应用。

1、弹性应用面临的困境


图2

仔细总结一下,我们会发现我们平常遇到的企业应用和互联网应用大致可以分为三类。

  1. 经典的OLTP应用,也可以理解成RPC应用,简单来说就是要求算1+1=2,但是它的难点是10万,100万个人同时请求这种计算。它的特点就是简单任务的高并发执行。它的后台通常都是关系型数据库支撑,数据库绝对是永远的痛,只能靠提升数据库性能,或者通过应用隔离,分库分表来扩容。

  2. 会话型应用,就是不同的独立请求之间是关联的,系统是有状态的。首先它需要有丰富的通讯模式,简单的RPC是不满足的,同时多个独立的请求之间可以在一个会话内实现数据共享和交互。这种应用的数据形式和数据存储形式、灵活性和性能都很重要。

  3. 包括并行计算、分布式计算、大数据,它们通常是比较复杂的任务,需要利用大量的计算资源共同完成,需要有复杂任务切分机制,同时数据量也比较大,数据形式复杂,数据存储和处理能力依然是性能瓶颈。

我们会发现,这三种经典应用的核心问题都是:计算能力不够、存储空间不够、磁盘访问效率低、持久化数据能力不够。最终只有一个方案:分布式。

2、困境中的答案


图3

那么到底什么是分布式?分布式是一个比较广义的命题。简单的来讲,分布式就是利用软件,通过scale out的方式解脱单一计算节点上的无法从硬件上无限突破的两大性能瓶颈,CPU和磁盘。解决CPU的问题就是分布式计算,解决磁盘的问题就是分布式存储。

先看分布式计算。分布式计算的目的就是提升参与计算的节点个数以提高计算能力。用浅显的话说,只要是同一类(记住,是同一类,不是同一件)的任务由超过一个CPU(准确是CPU的核心)完成都算是分布式,记住我用的是CPU,不是机器。计算节点并不一定是一台独立的计算机,它可以是线程、进程、机器、集群等,分布式应用也不一定必须是多机应用,多进程、多线程一样是分布式。

我们知道单一硬件资源计算能力的增长早已经停止了,多核和多机架构其实没有本质区别。通过分布式计算,其实相当于通过软件实现所谓的“摩尔定律”的回归,而且,分布式解决的不单是多核,还可以解决多CPU,多机的配合,继续提高系统的处理能力。

分布式存储则是在维持单位存储容量的管理成本不变的前提下,提升参与存储的节点数以提升存储容量和存储访问能力。

分布式不是一个新技术,是一种应用框架,是一种设计模式。因此,针对分布式的设计模式,衍生出很多技术来支撑,比如路由、负载均衡、任务调度、并行计算、资源竞争、线程间通讯、进程间通讯、网络通讯,等等。同时,衍生出新的软件设计需求,RASP。RASP是分布式系统中一个非常重要的要素,我们后面会反复提及。

介绍了什么是分布式,那么把分布式的设计模式融入到应用中后的应用架构应该是什么样的呢?。毕竟任何一个系统的核心是应用业务逻辑。

3、理想的分布式应用架构长什么样?


图4

这里我们看一下站在业务角度思考问题时,理想的分布式应用架构长什么样最合理。

本质上讲,分布式架构就是把应用逻辑改成分布式执行的方式。

不同的应用类型的处理方式又是不一样的。所以,一个非常直观的设计方式就是先将应用还原成一个工作流,就是每个软件系统的流程图。工作流中的每个步骤就是一个功能模块,每个步骤就是分布式的基础,它可以对应原来的一个函数或方法,针对每个步骤进行独立的分布式,再保持每个步骤的正常连接。

我们可以针对不同的应用类型选用不同的分布式设计方法和目标。比如图一,这是一个单任务串行的应用,它的步骤之间没有依赖关系,它的分布式方法就是单任务并行。于是多个步骤被更多计算节点同时执行,从而降低单个请求或者任务的完成时间。

对于应用,它的步骤之间是有依赖关系的,它的分布式方法就是由多任务串行变成多任务并行,通过流水线的形式实现多个任务的同一个步骤可以并发执行,在保持响应时间的前提下提高系统的吞吐量。

另外,从图3可以看出,应用的每一个步骤可能是一个函数,也可能是一个子系统,而这个子系统内部又包含了很多分布式执行的步骤。因此,基于步骤分布式的粒度可以非常灵活。通过粒度选择可以调整应用的吞吐量,从而在性能和管理成本中间选择一个平衡点。

中国有句成语叫做“分而治之”,很好的描述了分布式的目标,“分”是将任务拆分成多个步骤串行或者并行执行,“治”是多个步骤之间的连接和配合。从上述的例子我们可以看出,具体被“分”的内容和“治”的方法是业务逻辑,是应用行为。因此理想的分布式应用设计方法论应该站在应用角度去“分”和“治”,“分”和“治”的行为应该是站在原有应用逻辑的设计角度上去实现分布式。这样任何应用都可以用这种模式去轻松实现分布式,而且最贴合应用的原始设计。

那么,为了支撑这样的理想的分布式应用设计方法,底层的分布式平台应该做什么呢?

4、理想的分布式平台该做什么?


图5

一个完整的分布式平台会包含开发态(分布式框架)和运行态(分布式平台)两部分,共同帮助将应用“分而治之”,分”和“治”的内容是应用本身,一定是应用决定如何“分”和“治”。

开发态首先是提供编程范式和API支撑应用的“分”和“治”。

关于分,框架采用和原有应用设计类似的拆分方法来拆分出可以分布式执行的粒度,比如应用靠函数/对象来实现模块化,还应该保持这种拆分粒度,任何粒度的应用步骤都可以被封装成分布式的单元。

关于治,框架保持原有步骤之间的连接,但提供了多种连接形式,比如原来函数肯定是同步调用,现在可以改成同步或者异步调用。

作为框架,它的首要目标是尽量不改变原有编程模式,引入最少的新知识和技术。同时,它必须是更低层面的语义抽象,必须非常简单和轻量,这样才能适应更多的应用类型,让“分”透明,让“治”简单灵活。

现在有很多分布式框架,以分布式为导向,再让应用去适应,这其实是不正确的。

再看运行态,它是提供“分”和“治”的运行容器支撑,对应用是完全透明的,能够让应用运行时无缝地在多线程、多进程、多机环境下透明并行/并发执行。

当然,提供RASP,也是平台提供的核心功能。

施乐的首席科学家,普适计算之父,Mark Weiser,曾经说过, “最高深的技术是那些令人无法察觉的技术,这些技术不停地把他们自己编织进日常生活,直到你无从发现为止”。同理,最好用的分布式计算框架/平台也应该尽量淡化自己的存在,在应用中尽量无缝的融入自己,而不是改变应用原有的设计。

分布式平台核心要素的设计剖析

介绍完了什么是分布式系统、分布式系统能解决什么问题、理想的分布式平台的定位和设计原则以后,我们来一起看一下如何设计一个完善的分布式平台,剖析一下分布式系统的核心要素有哪些,而这些设计要素又有哪些设计选择。

分布式架构设计的点非常多,我们在设计的过程中在每个点上都有非常多的考虑和创新,我会结合我们的分布式平台的设计实现,尽可能把关键点都囊括到,并针对一些核心点的具体设计方案展开详细讨论。

这个剖析也从开发态和运行态分别展开介绍。

1、开发态(框架)的核心要素


图6

先看开发态。作为开发态的分布式框架,它辅助的是应用开发过程,它的核心是如何用最小的成本设计灵活、高效的分布式应用。

它包含的范畴也比较广。这里我列出了最核心的八点。

首先是支持的编程范式,比如RPC、对话、EDA、Map-Reduce、批处理、并行计算。它直接决定该框架能够支持的业务类型。

下一个是通讯协议,它决定调用者和被调用者之间的信息交换形式。分布式框架支持的通讯协议在任何两个节点间必须是统一的,比如客户端与服务器、服务实例间、线程间、进程间、机器间、集群间。通讯协议对应的是编程范式,应用开发应该只和编程范式打交道,最后被平台转换成通讯协议,这样,运行时可以动态调整通讯协议而不重新开发应用。比如对话的编程范式可以被配置成HTTP chunking,而应用对HTTP chunking一无所知。

和通讯协议密切相关的是数据协议。它决定调用者和被调用者之间的业务数据交换形式。它主要解决的就是离散态的内存序列化的问题,同样适用于业务数据持久化。一般来说,解决序列化无非三种办法:IDL、数据类型自描述、应用自主控制(IDL的升级版)。数据协议对应的是数据类型,和通讯协议一样,数据协议也应该对应用透明,应用开发完全不知道数据协议的存在。

配合透明的通讯协议和数据协议,可以轻松把应用中的一个服务发布成一个REST服务,通讯协议使用HTTP,数据协议使用JSON,而这同一个服务同时还可以被内部子系统调用,使用高速的二进制协议。一个服务能够透明支持各种通讯协议和数据协议,这是“服务化”架构中最基础的一个功能。

通过这样的支持,通讯协议和数据协议才可以变成可插拔、可扩展的模块,并对应用完全透明,应用开发只需要关注跟业务逻辑相关的事情。很多的分布式框架或者RPC框架中都没有实现的这么灵活。

会话的支持指的是多个独立请求之间关联形成一个虚拟连接,实现它们之间的数据共享。它主要是为了支持复杂的会话类应用。

多语言互操作主要是借助透明的通讯协议和数据协议,实现进程内、进程间、节点间的多语言互操作。

透明跨操作系统这里主要针对的是不需要解释器或者虚拟机的编程语言在OS的API层面提供跨OS支撑,隐藏OS和CPU体系结构的差异性,这也是一个分布式框架必须要做到的。

作为分布式开发框架,必然需要提供各种灵活的辅助工具包。通常包括丰富的容器数据类型支持,同时一个重要功能就是内存管理和数据共享。我们在分布式框架中为包括C/C++语言开发者提供了一套包括内存垃圾回收的灵活的内存管理框架,将内存的生命周期和可见范围结合,一方面简化开发人员对内存的使用,同时让内存在第一时间自动回收,并保证内存在不该被访问的范围内不可见。

服务实例间通信也应该包含在分布式开发框架的工具包中。通常服务实例应该是完全独立的,但是服务实例间通信对于有依赖关系的任务配合非常重要。

这里,我们针对分布式开发框架中最重要的部分,便捷的编程模型,做一个重点介绍。

2、便捷的编程模型


图7

讨论编程模型,其实就是讨论编程框架对于应用的入侵问题。自从开发web应用开始,程序的主循环逻辑就由框架管理了,程序员已经习惯了各种框架,很少有应用开发人员自己写程序的入口了。分布式应用的主逻辑肯定是被平台控制的,编程模型基本都是在开发各种callback。所以本质上分布式平台对于应用肯定是入侵的。

比如左图是一个典型的分布式应用开发的一个简单场景,里面包含着应用和框架的分工和配合。灰色是框架的模块,蓝绿色是应用的代码。框架有初始化模块,然后应用初始化,框架的主逻辑在接受到一个请求后,会调用应用的服务逻辑,应用的服务逻辑中会通过编程范式请求另一个服务,这时候利用框架提供的路由和负载均衡功能定位目标服务,通过数据协议和通讯协议完成调用获取结果,最后当前应用服务生成返回结果,完成一个服务的调用。

所以,讨论编程模型的本质就是讨论callback的开发方法,多个callback之间的交互方法,一个callback的多个instance之间的交互方法。

一个编程模型的优劣直接决定了开发人员的工作量和出错几率,同时也会影响运行时的灵活程度,编程模型也会直接影响RASP,比如分布式粒度的选择直接决定了未来高可用的级别。

理想的编程模型,应该尽可能少的引入新的语义,尽量让开发人员用单机单用户单线程的思路来开发分布式应用,让分布式对开发者透明,将一切分布式的语义都封装在原始程序开发之外,将框架尽可能隐藏。

同时,对于跟运行相关的内容,尽可能不要在编程阶段引入,比如服务运行在多线程、多进程还是多机通过配置的形式来驱动。

一个应用中必然包含多个子系统,里面用到的编程范式也不相同。如果能够通过一套编程模型来同时支持多种业务,则可以实现子系统间的资源协调和配合,提高可用性和可伸缩性,降低交互成本。这个时候编程模型需要尽可能用同一种原语解决中间件、通讯交换机、Hadoop/Spark解决的多种问题。

3、典型的分布式编程模型


图8

这里,我们介绍几种典型的分布式编程模型。

首先是请求级分布式应用。一般的web应用都是这种模型。它的最小分布式单位是请求,通常通过线程、进程、机器作为计算节点承载并发请求。这种编程模型的应用和框架之间是有条件的隔离的,HTTP session和HTTP协议相关的处理都是典型的入侵,除此外应用基本不改变设计方法。

它的容器通常有两种。

一种是分层容器,包含运行容器和代码运行时两部分比如Apache+PHP的组合。这种架构通常都是开发态和运行态相对分离的架构选择,编程框架只和运行时打交道,可以无缝替换容器或者运行时。

另一种是统一容器,比如JEE的应用服务器,容器是一个整体,自身提供编程接口和范式。编程框架完全入侵应用,必须按照这个编程开发范式开发应用。

请求级分布式应用模型的请求的粒度很大,请求间隔离很差。单请求的等待过程中占用的资源无法释放导致新的请求无法进入,并发处理能力弱。存储/数据库性能瓶颈会导致前端的分布式努力付之东流。

通常配备负载均衡来解决单机处理容量低的问题。

这种编程模型仅适合OLTP类型的业务。

另外一个就是非常热门的Map-Reduce框架。

它是一个纯粹的并行编程的框架,以分布式为主体,完全的入侵应用。所有的应用都需要按照框架来重构,属于一个逆向思维的分布式架构。

大数据时代,数据量太多,传统数据库不能解决,数据类型变多,没有数据库可以存储,需要计算的工作太多,分布式程序不容易写,也没有合适的运行容器。Map-reduce是一个在合适的时间出现的针对特定问题的分布式应用解决方案,确实很伟大,解决这些问题很擅长,而且可以利用廉价的计算资源,并且开源,受欢迎程度可想而知。

但是它解决的是针对性的问题,在分布式处理方面的模式也是相对单一的,比较适合批处理形式的任务,绝不是分布式领域的银弹,这些软件的作者和开源社区都对它有清晰的定位,但是经常被夸大成了超级武器,在大数据时代无处不在,甚至开始在各种应用中无处不在。

4、服务型分布式计算框架——分布式遇上SOA


图9

这里再介绍一种我们在自己的分布式平台中使用的分布式编程模型,我们称之为“服务型分布式计算框架”。

服务和SOA是永恒的设计话题。在应用架构领域,服务的设计应该是一个始终贯彻的原则。由于后台业务系统的复杂性不断增加,现在“服务化”的论调又在被大幅提倡。“服务型分布式计算框架”其实就是将服务和分布式进行有机的结合。

回想一下前面我们介绍的理想的分布式应用架构应该长什么样。那个时候我们提出应该先将应用还原成一个工作流,将工作流中的每个步骤作为分布式的基础。对应这里的三张流程图,单任务并行、多任务并发、灵活的分布式粒度。我们的“服务型分布式计算框架”其实就是把这些步骤封装成服务,而它原来可能是函数、方法。

于是,这套编程框架的核心就是保留原业务系统的所有设计流程,将每个需要分布式的步骤解耦、封装,在不改变业务流程的基础上,将每个步骤提炼成一个独立的服务。而所谓的服务封装其实并没有做什么事情,还保留原有的函数或者方法的样子,只是它们在运行时可以变成分布式平台调度的运行单位。服务的连接方法也没有什么改变,依然是函数/方法调用,运行时的连接是通过配置变成多线程、多进程、多机、多集群。

异步决定着整个系统的性能,但是异步开发很繁琐。我们在框架中引入了异常简单的异步开发框架,并提供便捷的现场保存/恢复机制,从而实现所有函数/方法的异步调用。

这套编程框架的核心是业务导向,应用开发几乎感觉不到框架的存在,仅有的应用入侵就是全异步开发,但是它带来的回报相对于它的简单投入是异常值得的,而且这还不是强制的。

通过将每个需要分布式执行的步骤封装成服务,运行态和开发态实现完全隔离。开发态看到的是虚拟的计算资源,和面对单机单用户没有区别,具体的分布式策略完全在运行时通过配置决定,分布式变成了运维的事而不是开发的事。

借助这样的分布式应用架构,开发人员可以完全专注于业务开发,最大程度的保留最适合业务的应用架构,不用关心哪些服务需要对外发布,未来可以通过轻松的配置把一个内部服务发布成外部服务。基于服务的粒度可以让应用的架构非常灵活,运行效率非常高效。

重要的是,每个应用的分布式模型是不一样的,是依赖于业务流程的,于是每个应用都是一个彻底的个性化分布式应用。

借助这个平台,我们开发了实时流媒体平台,开发了分布式数据库,开发了大数据分析平台。

因此,我们称之为自上而下、自内而外的的全SOA架构。它根本不需要服务化,因为它天生就是服务化的。

刚才我们介绍了分布式框架的几个核心要素,重点介绍了编程模型。我们再来看看另一面,运行态的分布式平台。

5、运行态(平台)的核心要素


图10

作为运行态的分布式平台,它是分布式应用的运行支撑容器。

分布式应用一般都是服务端应用,我们可以通过一个服务端应用的一个简单运行流程来看一下分布式平台如何在各个环节发挥作用。

看这里的流程图,我们会看到一个服务器从启动开始到提供服务,在接受到一个请求后,开始处理请求,同时这个请求又调用另一个服务。于是作为调用者和被调用者的角色都会在这个流程图中体现。我们结合这个流程图来分别介绍每个环节涉及的核心要素。

首先我们看到的是动态服务发布,它指的是计算节点上无停机发布新的服务。它可以用于无停机升级,同时,在一些大规模计算过程中,有时候需要动态调度一些闲置计算节点临时参与某个任务,不需要提前部署,这时候也需要用到这个功能。有动态发布,当然也有动态禁用。

选中的计算节点接受到任务后,需要通过任务调度模块进行任务的处理,任务调度是整个分布式平台的核心,是弹性可伸缩的关键。

当该任务被调度器选中进行处理时,选中的计算节点需要完成运行时的服务绑定。前面讲到的动态服务发布是声明能力,动态绑定是真正的加载能力。

当这个服务作为调用者需要调用别的服务的时候,首先需要发现服务,这时候就会用到的第一个功能就是路由和负载均衡功能。

然后选择好提供服务的计算节点后,需要通过通讯协议和数据协议的运行时支持完成请求和响应数据的封装和传输。

在分布式平台的处理过程中,因为有多个节点同时参与计算,于是有很多数据需要在多个节点间共享和流动,因此数据一致性是分布式系统非常核心的一个要素,几乎贯穿在各个地方,渗透在所有环节中。

而我们反复提及的RASP则是分布式系统的命脉。每个点都需要完整的设计。

比如可靠性的一个核心功能就是系统的健康性监控和问题隔离。监控应该提供应用级监控和操作系统级监控两个级别。一旦发现应用故障,可靠性需要完成自动隔离和恢复以降低系统的抖动。

可用性是分布式系统区别于单机系统的一个重要特征。一旦实现高可用,不论再怎么强调“share nothing”,“无中心”,必然会出现中心节点,那么中心节点的可用性问题就更加复杂,随之而来的选举制度、数据同步、时间窗口都需要考虑。

可伸缩性包括两个方面。一个是水平可伸缩,实现在一个平台上无限拓展支撑的业务类型,这时候就需要分布式平台本身足够薄,普适性强,支持更多类型的应用,必须是“微内核”架构。另一个是垂直可伸缩,针对同一类业务实现计算资源的无停机扩展/压缩。可伸缩后,核心管理节点的瓶颈问题非常严峻,如何解决它的高吞吐量是需要重点考量的。

性能则是分布式的终极目标了。分布式平台本质上就是为性能和高可用服务的。

OA&M,Monitoring是作为一个分布式平台必备的管理和运维功能。

上面我们针对分布式平台的各个环节涉及的主要元素应该做的事情以及部分的实现方法做了个简单的介绍,我们选择其中三点再展开重点介绍,其它点以后有机会再和大家探讨。

6、任务分配:路由和负载均衡


图11

先看一下路由和负载均衡。

分布式系统的核心目标就是分配尽可能多的任务给尽可能多的计算节点同时完成。这个分配工作包含两部分:路由和负载均衡。

路由解决的是谁能干的问题。它包括静态路由、发现动态发布的服务、数据驱动路由、会话驱动路由、时间驱动路由、应用定制路由,负载驱动路由。

负载驱动路由就是负载均衡,它是路由的后半程,决定谁干最合适,只有很多人能干才有负载均衡。负载均衡可以是集群、机器、进程、线程、服务、连接级别的。

路由和负载均衡的算法并不复杂,复杂的是支撑这些算法的数据从哪里来,就是路由和负载数据。

围绕这些数据,会有两种管理角色在分布式平台中存在:数据协调者和任务分配者。

数据协调者负责维护路由和负载信息的共享。它的架构有几种。

一种是单中心节点,很多集群都采用这种方案。它的缺点很明显,单点故障,性能有瓶颈。

于是会衍生出一写一读,或者一写多读,单点故障变成多点故障。带来的问题就是数据同步

还有一种方案是多中心节点,多写多读。这种架构会产生若干冲突和时间窗口,对于路由这样的需要绝对精准的数据,基本无法采用。但是对于类似负载的数据,它更新形式都是替换,而且允许数据不完全一致,是可以采用的。

再有的方案是多数据分区中心节点,和数据库的数据库分片一样。但是会产生数据间依赖、死锁、分布式事务等问题,逻辑也更加复杂。

围绕路由和负载数据的另一个角色是任务分配者。它根据共享的路由和负载信息分配任务,大部分系统是把路由和负载均衡的分配放在一起的。

它同样可以有单中心节点,弊端也是一样的。

任务分配者的另一种设计是无中心自主分配,所有节点都是读节点。这种设计的好处是只要系统还剩下一个节点,业务交易都不会停止,客户端完成路由、负载均衡,更灵活高效,可用性更高。但是数据同步问题同样严峻,而且除了节点间有数据同步,节点内也有数据同步,更加复杂。

我们发现一旦为了提高性能和可用性,都不可避免的产生数据一致性的问题。这个是分布式系统中最头疼的问题之一。我们先通过几个具体的场景看一下都有哪些数据一致性问题。

7、典型的数据一致性问题


图12

这里分享四个时间窗口导致路由和负载数据不一致的场景。

第一个是节点注册。

一个新的节点想要加入一个分布式集群,需要通知其它节点它的加入。这个时候A和B节点几乎同时加入,它们首先去中心节点获取所有节点的信息,这时候它们都互相看不到对方,于是在通知自己的状态的时候都会漏掉对方,这个问题会一直无法被修复。

解决方案没有什么更好的选择:把节点注册这种消息的广播让中央节点完成实现序列化,

第二个是节点注册&数据更新。

如果A节点启动,向中心节点注册,在注册响应收到之前,A因为还未获取其他节点的基础信息,所以不会接受别的全局数据的更新,这时候A节点的加入信息被中心节点广播给了B。B在收到A加入的信息后如果向包括A的全部节点广播数据的变化,而A节点就会忽略该数据,导致B的数据更新对A不可见,后续的更新将全部为脏数据。

解决方案:所有全局数据的修改都需要通过中心节点完成广播实现序列化。

第三个场景也是节点注册&数据更新

它和第二个场景类似,区别就是B在收到A的注册之前如果向不包括A的全部节点广播数据的变化,A节点就会收不到该变化消息。

解决方案也和二是相同的。

第四个场景是数据叠加更新

中心节点在收到数据变化时,会向各个节点同步数据,为了提高每个节点上负责数据同步进程的高可用性,提升性能,每个节点可以选择最闲置的进程接受数据。由于网络原因,第一个更新晚于第二个更新到达接受数据的节点,会导致数据不一致。

解决方案大家也能想到了,每个节点负责接受数据同步也是一写多读,保证数据更新同步的序列化。

分析了这四个场景,我们再完整的梳理一下数据一致性需要考虑的问题和对应的设计选择。

8、数据的一致性


图13

分布式系统的特点就是通过数据在多个节点的共享实现数据的同步和计算任务的协调,只要有数据共享,数据的同步和一致性就无法避免。

数据一致性的核心原因就是多节点的交互延迟导致时间窗口的存在。它几乎是分布式平台设计的“噩梦”,为什么这么说呢?因为它就没有完美答案,但是在这个前提下我们还需要尽一切努力去寻找接近“完美”的答案。做一个平台,有的功能可以是根据产品定位舍弃的,比如有的数据库的定位就是数据不是时时一致的,但是时间窗口问题是不能舍弃的,你不能说某个时间窗口产生的数据不一致我不管了,所有的时间窗口问题都必须解决。

而且,更加痛苦的是,时间窗口的问题一旦处理不好,后果通常是不可修复的。

说说数据一致性的目标,首先是保证高可用,比如N个节点在N-1个节点出故障的前提下实现系统依然可用。同时对于出现的问题可识别,一旦出现数据不一致,可以隔离问题节点,可容忍,数据不一致造成的问题不能产生不可修复的错误,比如“脏”数据只会导致负载不均衡或者业务调用失败,但绝不能导致业务被错误地调用,可修复,故障解除后,能自动恢复,或者通过人工简单干预实现数据的修复。

而且数据的同步是为了辅助管理和业务的,它不应该造成系统拥堵,否则就本末倒置了。

那分布式平台中需要同步的数据都有什么呢?

首先是管理数据,比如路由、负载、等,这些必然通过平台同步。

还有就是业务数据,比如分布式数据库中的核心就是管理分布在多个节点上的业务数据。它可以通过平台同步,比如Hadoop、Spark这些的业务数据都是通过平台同步的,这时候就需要平台植入应用,也可以应用自己控制。

业务数据同步一样需要中央节点,也会产生可用性和性能问题,采用的方法也都是类似的。

那这些数据如何同步呢?

最简单安全的方法就是一写多读,否则会产生很多时间窗口,造成不可修复的错误。前面介绍路由和负载均衡的时候已经介绍了。

那么一写自然变成瓶颈,提高一写的性能就很关键。

怎么提高呢?开源节流。

首先是开源。最根本的办法就是数据分区,就好像数据库想要从根本上提高性能,只能通过数据分片。但是数据分片带来更多问题,数据间依赖、死锁、分布式事务、逻辑复杂。

另一个就是节流。让数据同步低频,增量,异步。

数据同步中的主节点因为是单点故障,高可用的设计必不可少,于是自然引出关于主节点的fail over和fail back应该自动还是手动的问题。

比如通过选举自动找出新的主节点,就会出现很多新的时间窗口。而且很容易形成多个自治区。比如主节点和备份主节点属于两个子网,两个子网之间发生网络故障。于是会形成多个主节点的局面,数据完全混乱。当然可以用更加复杂的选举算法来不断修复问题,让问题变得更加复杂。

这里没有标准答案,但是有哲学答案,简单最美和“舍得”。

同时,对于负载这种高频变化的数据如何同步也是个学问。同步的速度低于更新的速度导致数据无价值,单中心节点维护降低吞吐率,多机实时同步容易导致统计风暴。当然也有通过响应时间计算的,对应的问题是如何解决单向请求。

这些都是取舍和选择。

时间同步是一个非常烧脑的环节,感觉没有技术难度却完全没有思路。我们换一个方向,来看一下比较高精尖的模块,任务调度。

9、任务调度


图 14

和操作系统内核一样,任务调度是整个分布式平台的内核。分布式平台的内核有很多种设计,我们这里介绍的设计主要以服务型分布式计算编程框架为基础。

任务调度的基础是可以被调度的任务粒度。它是由编程模型决定的,它直接影响调度的灵活性和系统的性能。比如针对请求级分布式框架,它是请求,对于map-reduce,它是任务的一个步骤,对于服务型分布式计算框架来说,它是函数或者方法,准确说是服务。

而底层承载调度任务的计算资源可以是线程、进程、机器、集群。一个优秀的内核必须用统一的方法覆盖所有类型的计算资源,实现弹性可伸缩。

调度器是分布式平台的核心,是系统资源是否能够发挥到极致的基础。

服务型分布式计算平台的调度器设计采用的是类操作系统内核的设计,用的是“尽力运算”的调度原则,目的就是将CPU用到100%。我们经常说,“不能将CPU用到100%的分布式平台都是耍流氓”,注意这里我用的是“不能”,能不能用到和让不让用到完全是能力的区别。

我们都知道操作系统内核关于进程(线程)调度的原则是“尽力运算”,它会把进程分为运行、阻塞和挂起三种状态。进程利用CPU计算是运行态,一旦发现阻塞指令,比如I/O操作,将自动将该进程切换至阻塞态,同时将让出的CPU分配给其他进程,处理完以后第一时间重新被调度。通过时间片让每个进程都有机会被执行,一旦时间片到,就会被调度到挂起态,保证调度的平均性。

分布式平台的调度器也一样,它调度的是服务,它能够识别客户应用中的具体指令,并尽全力执行。对于长期执行计算的任务会在时间片到达后被调度出。任何服务,一旦遇到慢速服务调用,比如数据库访问,将自动切换给其他的任务,但同时又能在阻塞资源可用的时候第一时间被调度执行。

我们都知道操作系统是天然全异步的,操作系统不会因为磁盘或者网络繁忙死机,都是因为CPU和内存死机。单线程环境下全异步的方式可以大幅提高系统的吞吐量,在大多数情况下,“单线程是最好的线程模型”。基于服务的调度器让全异步设计更进一步,调度的粒度更小,我们可以实现函数步骤级别的全异步。

通过这样的调度器,我们可以把操作系统层面的资源消耗降至最低,CPU全部贡献给了业务。而且这种调度比操作系统成本还低,分布式平台的大部分操作都是在用户态完成的,比操作系统切换任务的速度更快。

通过服务的封装形式,被执行的任务被虚拟化。服务不和任何计算资源绑定,可在任何形式的计算资源上并行执行、连接。

通过服务虚拟化,所有的计算资源形成一个完整的计算缓冲池,从而实现了计算资源和计算能力的虚拟化。一旦计算资源虚拟化,我们将再也看不到物理的计算节点,只是看到统一的计算能力,按需使用即可,这是分布式计算的终极目标。

到这里,我们把分布式平台的几个核心要素都进行了比较完整的介绍。下面我们看一种比较特殊的分布式应用,分布式数据库。

分布式数据库

数据库是大部分应用的瓶颈所在,分布式数据库是解决数据库瓶颈的最有效方法。大数据时代的基础软件中分布式数据库(广义数据库)也是核心,让其价值更加明显,它的技术难度也被放大。

我们这里并不深入介绍分布式数据库,只是把分布式数据库这样一个关注度高、实现复杂的产品作为一种特殊的分布式应用来看待,从而帮助我们理解通用的分布式平台设计方法如何帮助设计分布式数据库,看看分布式数据库有哪些特殊之处,从而帮助我们理解不同的分布式应用的核心都是“相似”的。

当然我们这里探讨的的分布式数据库设计也确实是以一款真实的分布式数据库产品的的内核设计。

1、分布式数据库是一种分布式应用


图15

前面我们主要关注的是分布式计算,主要在看CPU,我们还记得分布式解决了两个问题,CPU和磁盘,磁盘就是分布式存储。

其实,分布式存储就是属于分布式计算的一种,是一个特定的分布式应用。站在服务型分布式计算的角度,分布式存储其实就是DaaS,它的核心业务逻辑是通过分布式的架构对数据进行访问。

数据库是一个广义的概念,它是一种存储和管理数据的有效形式和方法论。分布式数据库是分布式存储的一种体现,因此分布式数据库代表的就是分布式存储的方法论和支撑技术。

分布式数据库作为一种特定的分布式应用,它需要解决两个个性化问题。

先解决数据的分布,也就是数据分片。数据存储的效率较低、成本大,每个节点存储所有数据的拷贝不可能,数据频繁移动不可能,因此必须有数据分布策略。

当数据分片存储以后,所有访问数据的逻辑都需要考虑这个数据的分布策略,对应的访问数据的业务逻辑在调用时和普通分布式计算的核心区别就是在路由算法中要先使用数据依赖路由。

站在这个角度思考,相对于普通的分布式应用来说,分布式数据库的设计就并不复杂。当然,这是宏观的,具体到微观的设计,会有大量的问题。

这就是原理和产品的区别。

2、分布式数据库的分布式架构


图16

数据库也是一个普通的应用,我们站在服务型分布式计算框架的角度上来让这个应用分布式。相比一般的分布式应用它有两大不同,这里我们以其中的一个不同,数据依赖路由,为基础,看一下分布式数据库的设计原理。

还是基于服务型分布式计算框架的应用设计方法,先忘记分布式,专注在应用设计上。

先假想设计一个单用户单线程数据库,比如SQLite,然后让服务型分布式计算框架来帮助我们实现分布式。

看左边的流程图。写操作是其中一个主要的业务逻辑。先确定业务逻辑,画出工作流,然后逐个步骤实现:

  • 具体写到某个具体的存储引擎的操作被封装成一个标准的服务,就是存储引擎写服务;

  • 写逻辑首先需要通过路由算法选择调用哪个写服务,这个时候就用到了分布式数据库特殊的数据依赖路由算法,根据数据分布的策略,将要写的数据发送给对应的存储引擎的写服务;

  • 当同时有多个数据需要写的时候,传统单机应用就是串行执行,这里可以通过异步调用多个写服务的方法实现并行化;

  • 针对每个存储引擎的写服务,对于需要写副本的要求,也可以理解为多个需要串行的写操作,通过异步调用多个写服务实现并行化;

  • 在所有写操作调用完成之后,会调用索引更新操作,这个就属于不能并行的步骤,异步调用这些服务,它的调用就是通过流水线的机制提高吞吐量。

右边的流程图是读操作。设计方法是类似的:

  • 具体从某个具体的存储引擎读数据的操作被封装成一个标准的服务,就是存储引擎检索服务;

  • 这里面请求的解析、关系代数逻辑、数据的二次过滤、数据的二次整合的处理和普通的服务没有什么区别,不能并行,可以异步并行调用实现流水线形式的分布式执行;

  • 索引选择的过程其实就是通过数据依赖算法选择调用哪个存储引擎检索服务,根据数据库的分布策略和分布式索引,选择对应的检索服务,该服务会访问对应的存储引擎,这样就可以通过异步调用实现从多个节点上并行检索数据,然后当这些服务调用全部结束后才进入下一个步骤,按需进行数据的二次聚合、排序等操作,并返回结果。

这只是分布式数据库的两个经典场景的简单描述,而且也只是最基本的设计分析,分布式数据库还需要实现很多功能,但是设计思路都是类似的。

小结

做一个简单的总结。

今天我们介绍了什么是分布式系统、分布式系统能解决什么问题、什么样的分布式系统更加理想、常规的设计方法论。

同时关于如何设计一个完善的分布式系统中涉及的核心要素的设计方法展开了一些讨论,并针对一些最核心的要素的设计做了较深入的探讨,通过需求、方法论和具体的设计帮助大家了解一个分布式系统的内部实现。

最后关于一种特定的分布式应用,分布式数据库,做了简单介绍。

好, 这就是今天我演讲的所有内容。谢谢大家。

获取视频回放链接

请在InfoQ公众号后台回复关键词:「内核」

注:是在微信后台留言,不是在本文评论区。

老司机介绍

董健南开大学计算机科学硕士,软件、通信、互联网领域拥有近 20 年的丰富经验,深谙世界领先的核心平台技术,具备世界级系统的架构和设计经验。曾供职于贝尔实验室、bea、甲骨文,担任架构师、高级研发经理、产品经理等职位,带领团队开发过服务全球顶尖运营商的智能网系统世界排名第一的交易中间件 Tuxedo世界第一个消息中间件 MessageQ,WebLogic 等产品,这些产品曾服务于涵盖全球 500 强的超过 3000 个企业客户,并应用于它们的核心业务应用。

下期InfoQ大咖说直播预告

前阿里巴巴技术专家,

现蘑菇街架构师  七公

霸气开讲!

微信后台回复关键词「InfoQ」,获取直播链接

回复关键词「直播」,加入直播交流群



【0元参会】首届360开源大会

大会不仅邀请10多位360内部技术专家,还特邀Apache基金会副总裁、触控未来CEO、华为和Intel等多位开源专家共聚大会,畅聊开源!报名请戳阅读原文~


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存