一个创业公司起步时极可能就两台机器,一台 Web 服务器,一台数据库服务器,在一个应用系统中集成了全部的功能模块。但随着业务的发展和流量的增加,单应用已不能知足业务需求,分布式成为必由之路。前端
背景算法
一个网站的技术架构早期不少是 LAMP(Linux + Apache + MySQL + PHP),随着业务扩展和流量增加,该架构下的系统很快达到瓶颈,即使尝试一些高端服务器(如:IOE),除了价格昂贵以外,也阻挡不了瓶颈的到来。分布式改形成为必由之路。
什么是分布式改造?数据库
所谓分布式改造,就是尽可能让系统无状态化,或者让有状态的信息封装在必定范围内,以避免限制应用的横向扩展。简单来讲,就是当一个应用的少数服务器宕机后,不影响总体业务的稳定性。
实现应用的分布式改造须要解决好哪些问题?后端
- 应用须要微服务化,即将大量粗粒度的应用逻辑拆小作服务化改造
- 必须创建分布式服务框架。必须具有分布式配置系统、分布式 RPC 框架、异步消息系统、分布式数据层、分布式文件系统、服务的发现、注册和管理。
- 必须解决状态一致性问题
分布式架构与传统单机架构的最大区别在于,分布式架构能够解决扩展问题:横向扩展和纵向扩展。缓存
什么是横向扩展?性能优化
横向扩展,主要解决应用架构上的容量问题。简单说,假如一台机器部署的 WEB 应用能够支持 1亿 PV 量,当我想支持 10亿 PV 量的请求时,能够支持横向扩展机器数量。
什么是纵向扩展?服务器
纵向扩展,主要解决业务的扩展问题。随着业务的扩展,业务的复杂程度也不断提升,架构上也要能根据功能的划分进行纵向层次的划分。好比,Web/API 层只作页面逻辑或展现数据的封装,服务层作业务逻辑的封装等。业务逻辑层还能够划分红更多的层次,以支持更细的业务的组合。
一个典型的分布式网站架构如图1.1所示:网络
它将用户的请求经过负载均衡随机分配给一台Web机器,Web机器再经过远程调用请求服务层。可是数据层通常都是有状态的,而数据要作到分布式化,就必须保证数据的一致性。要保证数据的一致性,通常都须要对最细粒度的数据作单写控制,所以要记录数据的状态、作好数据的访问控制等
一个有状态的分布式架构如图1.2所示:数据结构
分布式集群中通常都有一个Master负责管理集群中全部机器的状态和数据访问的规制等,为了保证高可用Master也有备份,Master一般会把访问的路由规则推给实际的请求发起端,这样Client就能够直接和实际要访问的节点通讯了,避免中间再通过一层代理
还有一种分布式架构是非Master-Slave模式而是Leader选举机制,即分布式集群中没有单独的Master角色,每一个节点功能都是同样的,可是在集群的初始化时会选取一个Leader 承担Master的功能。一旦该Leader失效,集群会从新选择一个Leader。
这种方式的好处是不用单独考虑Master的节点的可用性,可是也会增长集群维护的复杂度:架构
须要分布式中间件
从前面典型的分布式架构上能够看出,要搭建一个分布式应用系统必需要有支持分布式架构的框架。例如首先要有一个统一的负载均衡系统(LB/LVS)帮助平均分配外部请求的流量,将这些流量分配到后端的多台机器上,这类设备通常都是工做在第四层,只作链路选择而不作应用层解析;应用层的负载均衡能够经过HA来实现,例如能够根据请求的URL或者用户的Cookie 精准地调度流量。请求到达服务层,就须要解决服务之间的系统调用了。这时,须要在服务层构建一个典型的分布式系统,包括同步调度的分布式RPC框架、异步调度的分布式消息框架和解决静态配置信息的分布式配置框架。这三个分布式框架就像人体的骨骼和经络,把整个服务层链接起来。咱们会在后面详细介绍这三个典型的分布式框架(分布式框架的开源产品有不少,例如Dubbo、RocketMQ等)。
服务化和分布式化
咱们在网站升级中通常会接触到两个概念:
- 服务化改造;
- 分布式化改造。
它们是一回事吗?
服务化改造更可能是从业务架构的角度出发,目的是将业务作更细粒度的功能拆分,使业务逻辑更加清晰、边界更加清楚且易于维护;服务化的另外一个好处是收敛业务逻辑,经过接口标准化提供统一的访问方式。
分布式化更可能是从系统架构层面的角度出发,更可能是看请求的访问路径,即一个请求必须先访问什么再访问什么、一次访问要通过哪些步骤才能最终有结果等……所以,这是两个不一样层面的工做。
分布式配置框架能够说是其余分布式框架的基础,由于在分布式系统中要作到全部机器节点都彻底对等几乎是不可能的,必然有某些机器或者集群存在某些差别,但同时又要保证程序代码是一份,因此解决这些差别的惟一办法就是差别化配置——配置框架就承担着这些个性化的定制功能:它把差别性封装到配置框架的后台中,使集群中的每台机器节点的代码看起来都是一致的,只是某些配置数据有差别。
配置框架的原理很是简单,即向一个控制台服务端同步最新的一个K/V集合、JSON/XML或者任意一个文件,配置信息能够是在内存中的也能够是持久化的文件。
它的最大难点在于集群管理机器数量的能力和配置下发的延时率。
如图1.3所示,分布式配置框架通常有如下两种管理方式:
拉取模式
拉取模式就是Client集群主动向 ConfigServer机器询问配置信息是否有更新,若是有则拉取最新的信息更新本身。这种模式对配置集群来讲比较简单,不须要知道Client的状态也不须要管理它们,只需处理Client的请求就好了,是典型的C/s模式。缺点是ConfigServer 无法主动及时更新配置信息,必须等Client请求时再更新。通常Client都会设置一个定时更新周期,一般是几秒钟。
推送模式
这种模式是当ConfigServer感知到配置信息变化时主动把信息推送给每一个Client。它须要ConfigServer感知到每一个Client的存在,须要保持和它们之间的心跳,这使得ConfigServer的管理难度增长了,当Client的数据很大时,ConfigServer 有可能会成为瓶颈。
在实际的应用场景中,通常而言,若是对配置下发延时率比较敏感并且Client数据不是太大(千级别)时,推荐使用推送模式,像ZooKeeper就是这种框架(咱们在后面的小节会介绍几种分布式管理的场景);而当Client 数据在万级以上时推荐使用拉取模式。不过,无论运用哪一种模式咱们都要考虑如下两个问题:
分布式配置框架是最简单的管理Client机器及相应配置下发功能的框架,所以,它也很容易发展成带有这两种属性的其余的工具平台,如名字服务、开关系统等。
应用系统要作分布式改造,必须先要有分布式RPC框架,不然将事倍功半,为何呢?这就像盖楼同样,若是没有先搭好骨架的话,极可能就是给本身“埋坑”。要作好分布式RPC框架须要实现服务的注册、服务发现、服务调度和负载均衡、统一的SDK封装。当前Java环境中分布式RPC框架如Dubbo、HSF都是比较成熟的框架,可是其余语言像PHP、C++还很少。
一个一般意义上的RPC框架通常包含如图1.4所示的结构:
服务注册
服务要能被发现,必须先注册。服务的注册对Java程序来讲很是简单,只须要在应用启动时调用RPC框架直接向服务端注册就行,通常须要传递类名、方法名、参数类型以及版本号。因为语言上的一些限制,用PHP来作服务的注册和发现相比之下要困难不少。因为PHP不像Java那样很容易地支持长链接,因此在PHP文件启动时很难像Java程序那样在初始化函数里完成注册。对PHP来讲目前有两个办法:第一个办法是写一个FPM的扩展,在第一次FPM初始化时完成服务的注册。可是若是一个FPM部署了多个服务模块,那么如何区分也是一个难题;第二个办法是在PHP模块里手动配置一个要注册的服务名录列表,在PHP打包发布时进行注册。
服务注册后,服务发布者所在的机器须要和注册中心保持心跳以维持本身处于一直能够提供服务的状态,不然注册中心就会踢掉机器。对PHP来讲,维持RPC链接是个麻烦事,因此通常会在本机另外再起一个proxy agent或者直接发送HTTP请求,可是这样作比较消耗性能。
服务发现
服务发布方注册服务后,服务调用方就要可以发现服务。服务发现(如图1.5所示)最重要的一个目标就是要把服务和提供服务的对应机器解耦,而不是经过机器的IP寻找服务。
服务调用方只须要关心服务名,不用关心该服务由谁提供、在哪儿提供、是否可用……服务发现的组件会把这些信息封装好。
对Java语言来讲。服务发现就是把对应的服务提供者当前可以存活的机器列表推给服务调用者的机器的内存,真正发起调用时随机选取一个IP就能够发起RPC调用。对PHP来讲,如何把服务发布者的机器列表推给调用方也很麻烦,目前的解决方案更倾向于在本机的Agent中完成地址列表的更新,而后真正调用时再查询Agent更新的本地文件,并从本地文件中查找相应的服务。
服务调度和负载均衡
服务注册和发现完成后,就要处理服务的调度和负载均衡了(如图1.6所示)。服务调用有两个关键点:一是要摘除故障节点,二是负载均衡。
- 摘除故障节点。这对Java来讲比较容易处理:一旦有机器下线后,很容易更新地址列表。但对PHP来讲就只能按期从Agent中拉取最新的地址列表,作不到像Java那样实时。
- 负载均衡。负载均衡须要将调用方的请求平均分布到不一样的服务提供者的机器上,一个最简单的算法就是随机选取,作得复杂一点能够给每一个提供者IP设置一个权重,而后根据权重选取。
统一的 SDK 封装
服务框架须要提供一个统一的客户端和服务端的标准接口规范,这样能够减小业务开发的重复工做量,例如SDK会统一封装通讯协议、失败重试以及封装一些隐式参数传递(trace信息)。Java经过提供一个统一的jar包,封装了服务发布和调用的接口,业务层只要作些简单的配置就能方便地调用服务,例如Java通常都会配置一个Spring的Bean。
对其余语言来讲,运用IDL规范是个好选择。在thrift的基础上,修改code-gen,生成struct(class)的read和write,生成Client和server插件框架,并基于thrift的lib提供Binary Protocol、TCP Transport。
一个分布式应用系统中,除了RPC调用以外,还须要在应用之间传递一些消息数据,这时分布式消息中间件就成为必需品。消息中间件主要用于异步和一个Provider多个Consumer的场景中。消息可分为实时消息和延时消息。
实时消息
实时消息就是当消息发送后,接受者能实时消费的消息(如图1.7所示),不少开源的消息中间件如开源的RocketMQ,Apache Kafka等都是比较成熟的消息中间件。
异步解耦的好处体如今多个方面:能够分开调用者和被调用者的处理逻辑,下降系统耦合,解决处理语言之间的差别、数据结构之间的差别以及生产消息和消费者的速度差别(削峰填谷)。
经过中间的消息队列服务,能够作不少事情,可是要保证如下两点:
一个消息被多个订阅者消费是典型的一种应用场景(如图1.9所示),多消费端很是适合用在单一事情触发的场景中。例如当一个订单产生时,下游会对这个订单作不少额外的处理,而消息的生产者对这些消息的消费者根本不会关心,很是适合用在一个大型的异构系统中。
在此场景中咱们会遇到下面这些典型问题:
延时消息
除了实时消息外,延时消息用得也比较普遍。典型的例子好比一张电影票的订单产生后,用户在15分钟后仍然没有付款,那么系统会要求在15分钟后取消该订单,释放座位(如图1.10所示)。这种场景很是适合用延时消息队列来处理。
延时消息在技术实现上比实时消息队列要更难,由于它须要增长一个触发事件,而这个触发事情有时候不必定是时间触发事件,还有多是其余消息事件触发,这样致使它所承担的业务逻辑会更重,架构也会更复杂。
延时消息的核心是须要有一个延时事情的触发器,此外还必须解决消息的持久化存储问题,其余方面和实时消息队列差很少。整体来讲,全部的消息队列都必需要解决最终一致性、高性能和高可靠性问题。
几种常见的消息中间件
在 http://queues.io/ 上几乎列出了当前大部分开源的消息队列,每一个产品各有特色,适用于不一样应用场景,适合的才是最好的。
分布式数据层主要解决数据的分库分表、主备切换以及读写分离等问题,统一封装数据库的访问细节,如创建链接中的用户名和密码、链接数、数据类型的转换等信息。
分库分表
分布式数据层最重要的功能是对数据作分库分表处理(如图1.11所示),尤为对互联网公司来讲,数据量的不断增加要求切分数据是至关日常的任务。
当一条或者一批SQL提交给分布式数据层,并根据某些标识进行规制运算后,应该将这些SQL分发给规则被命中的机器去执行并返回结果。这里最重要的是数据分片的规制要对开发透明,即写SQL的同窗不用关心他要请求的数据到底在哪台机器上,当咱们改变数据分片规制时,只须要修改路由规则而无须修改SQL。因此很显然该分布式数据层须要解析用户的SQL,而且有可能会重写SQL(例如修改表名或者增长一些 HINT 等信息)
主备读写分离
数据库的读写分离是常见的操做,如图1.12所示。因为数据库资源很是宝贵,为了保证数据库的高可用通常都会设置一主多备的架构;为了充分利用数据库资源,都会进行读写分离,即写主库读从库。在同机房的场景下,数据库主从复制的延迟很是低,对应用层没有什么影响。
在原理上主从的读写分离比较简单,就是拆开用户的写请求和读请求,并分别路由到不一样的DataSource上。在这种场景下要注意读写一致性的问题。在某些场景如双11抢单时,用户下完单当即查询下单是否成功,若是查询从库延时会比较大,用户极可能看不到下单成功界面从而重复下单,要有保障机制避免此类问题发生。
有些应用须要读写文件数据时,若是只写本机的话那么就会和本机绑定,这样这个应用就成为有状态的应用,那么就很难方便地对这个应用进行迁移,水平扩展也变得困难。
不只是文件数据,一些缓存数据也存在相似问题。如今不少的分布式缓存系统Redis、Memcache等就是用于解决数据的分布式存储问题的。
当前开源的分布式文件系统不少,像开源的TFS、FastDFS、GFS等,它们主要解决的是数据的高可用性和高性能问题,下面咱们介绍一个分布式文件系统Seaweedfs的设计,它的设计比较巧妙,颇有启发性(如图1.13所示):
Seaweedfs 文件系统有3个很是重要的组成部分,分别是Master、VolumeServer和Volume:
假设一个Volume有30GB的容量,被分配一个惟一的32bit的VolumelD;每一个VolumeServer 维护多个Volume和每一个Volume剩余可写的存储空间,并上报给Master;Master维护整个集群中当前可写的Volume列表,一旦Volume写满就标识为只读。若是整个集群中没有可写的Volume,那么整个集群都将不可写,只能经过扩容增长新的VolumeServer。
Client 要上传一个文件首先须要向Master申请一个fid,Master会从当前可写的Volume列表中随机选择一个。和大多数分布式文件系统同样,这个fid就是表示文件在集群中的存储地址,最重要的是fid的前32bit,表明的是VolumeID,表示该文件具体存储在哪一个Volume中并返回此fid应该存储的VolumeServer的机器地址。
典型的一个上传文件请求如curl-F"file=@/tmp/test.jpg""192.168.0.1:9333/submit",返回{"fid":"3,01f83e45ff","fileName":"test.jpg","fileUrl":"192.168.0.1:8081/3,01f83e45ff",
"size":12315}。Client拿到fileUrl再将文件实际上传到192.168.0.1:8081的VolumeServer中。
文件的多副本也是在Master上管理的,巧妙的地方在于文件的多副本不是以单个用户的文件为单位而是以Volume为单位进行管理。例如上面的“3,01f83e45ff”这个fid,它是存在3的Volume中,那么这个3Volume会有一个副本,即在其余的VolumeServer上也有一个3的Volume,那么当test.jpg上传到192.168.0.1:8081时,它会查询Master这个Volume的副本在哪台机器上,而后由这台机器把文件copy到对应的机器上,再返回结果给用户。目前仍是采用强一致性来保证多副本的一致性,若是某个副本上传失败则返回用户失败的结果。
扩容比较简单,当集群中全部的Volume都写满时,再增长一些VolumeServer并增长若干Volume,Master会收集新增的VolumeServer中空闲的Volume并加入到可写的Volume列表中。
若是有机器挂掉的话,因为Volume是有备份Volume的,因此只要存在一份Volume,Master都会保证可以返给用户正确的请求。这里须要注意的是Volume的多备份管理并无主次的概念,每次Master都会在可用的多备份中随机选择一个返回。假设3这个Volume的副本分别在192.168.0.2:8081和192.168.0.3:8081上,那么若是192.168.0.3:8081机器“宕”掉,那么这个3对应的Volume就会被设置为只读,实际上192.168.0.3:8081上全部对应的Volume都会被设置为只读——只要某个Volume的副本数减小,都会禁止再写。
综上,这个文件系统的设计思路能够总结成如下两点:
到目前为止,该文件系统0.7的版本还不是太完善,表如今没有一个很完善的Client程序来处理Master到VolumeServer之间的跳转,通常须要本身写。可是它最吸引人的地方就是Volume(集装箱式的设计),这个设计比较简单和独立,尤为是管理比较方便。固然,大部分分布式系统的设计也都有类似之处。
解决好跨应用的链接和数据访问后,咱们的应用也要作好相应的改造,如应用分层的设计、接口服务化拆分等。
应用分层设计
应用分层设计颇有必要。例如最起码要把对数据库的访问统一抽象出来造成数据层,而不是直接在代码里写SQL——这会使重构应用和水平拆分数据库很是困难。
咱们一般从垂直方向划分应用,分红服务层、业务逻辑层和数据层,每一层尽可能作到解耦:上层依赖下层,而下层不要反向依赖上层。
应用分层最核心的目的是每一个层都会封装一些信息、完成一些特定的功能需求,层与层之间经过接口交互,并且交互的数据是清晰和固定的,作到隔离和交互。能够从如下两个方向判断分层是否合理。
- 若是我要增长一些新需求或者修改某些需求时,是否能清楚地知道要到哪一个层去完成,换句话说,这些分层的职责是否清晰
- 若是每一个层对个人接口不变,那么每一个层内部的修改是否会致使其余层也发生修改,即每一个层是否作到了收敛
分层设计中最怕的就是在接口中设计一些超级数据结构,如传递一个对象,而后把这个对象一直传递下去,并且每一个层均可能修改这个对象。这种作法致使两个问题:一是一旦该对象更改,全部层都要随之更改;二是没法知道该对象的数据在哪一个层被修改,在排查问题时会比较复杂。所以,在设计层接口时要尽可能使用原生数据类型如String、Integer和Long等。
微服务化
微服务化,是从水平划分的角度尽可能把服务分得更细,每一个业务只负责一个功能单元,这样能够把这些微服务组合成更大的功能模块。也就是有目的地拆小应用,造成单一职责从而提高系统可维护性、扩展性和开发效率。图1.14所示是基于Spring Boot构建的一个典型的微服务架构,它按照不一样功能将大的会员服务和商品服务拆成更小原子的服务,将重要稳定的服务独立出来,以避免常常更新的服务发布影响这些重要稳定的服务。
在大型分布式互联网系统中,Session问题是典型的分布式化过程当中会遇到的难题。由于Session数据必须在服务端的机器中共享,并要保证状态的一致性。该问题在《深刻分析Java Web技术内幕(修订版)》的第10章中有详细的介绍。
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它是一个为分布式应用提供一致性服务的软件,所提供的功能包括:配置维护、域名服务、分布式同步、组服务等。下面咱们介绍一下典型的分布式环境下遇到的一些典型问题的解决办法。
集群管理(Group Membership)
ZooKeeper 可以很容易地实现集群管理的功能,如图1.15所示。若是多台Server组成一个服务集群,那么必须有一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,就必须知会集群中的其余集群,并从新分配服务策略。一样,当集群的服务能力增长时,就会增长一台或多台Server,这些也必须让“总管”知道。
ZooKeeper不只可以维护当前集群中机器的服务状态,并且可以选出一个“总管”,让“总管”来管理集群——这就是ZooKeeper的另外一个功能Leader Election。它的实现方式是在ZooKeeper 上建立一个EPHEMERAL类型的目录节点,而后每一个Server在它们建立目录节点的父目录节点上调用getChildren(String path,Boolean watch)方法并设置watch为true。因为是EPHEMERAL目录节点,当建立它的Server死去时,这个目录节点也随之被删除,因此Children将会变化;这时getChildren上的Watch将会被调用,通知其余Server某台Server已死了。新增Server也是一样的原理。
那么,ZooKeeper如何实现Leader Election,也就是选出一个Master Server呢?
和前面的同样,每台Server建立一个EPHEMERAL目录节点,不一样的是它仍是一个SEQUENTIAL目录节点,因此它是个EPHEMERAL_SEQUENTIAL目录节点。之因此它是EPHEMERAL SEQUENTIAL目录节点,是由于咱们能够给每台Server编号—咱们能够选择当前最小编号的Server为Master,假如这个最小编号的Server死去,因为它是EPHEMERAL节点,死去的Server对应的节点也被删除,因此在当前的节点列表中又出现一个最小编号的节点,咱们就选择这个节点为当前Master。这样就实现了动态选择Master,避免传统上单Master容易出现的单点故障问题。
共享锁
在同一个进程中,共享锁很容易实现,可是在跨进程或者不一样Server的状况下就很差实现了。然而ZooKeeper能很容易地实现这个功能,它的实现方式也是经过得到锁的Server建立一个EPHEMERAL_SEQUENTIAL 目录节点,再经过调用getChildren方法,查询当前的目录节点列表中最小的目录节点是不是本身建立的目录节点,若是是本身建立的,那么它就得到了这个锁;若是不是,那么它就调用exists(String path,Boolean watch)方法,并监控ZooKeeper上目录节点列表的变化,直到使本身建立的节点是列表中最小编号的目录节点,从而得到锁。释放锁很简单,只要删除前面它本身所建立的目录节点便可,如图1.16所示。
用ZooKeeper 实现同步队列的实现思路以下:
咱们用图1.17的流程图来直观地展现该过程:
用ZooKeeper实现FIFO队列的思路以下:
在特定的目录下建立SEQUENTIAL类型的子目录/queue_i,这样就能保证全部成员加入队列时都是有编号的;出队列时经过getChildren()方法返回当前全部队列中的元素,再消费其中最小的一个,这样就能保证FIFO。
分布式消息通道普遍应用在不少公司,尤为是在移动App和服务端须要上传、推送大量的数据和消息时。好比打车App天天要上传大量的位置信息,服务端也有不少订单要及时推送给司机;此外,因为司机是在高速移动过程当中,因此网络链接的稳定性也不是很好——这类场景给消息通道的高可用设计带来很大的挑战。
如图1.18所示是一个典型的移动App的消息通道的设计架构图,这种设计比较适合上传数据量大,而且高速移动致使网络不太稳定的链路。
链路1是Client和整个服务端的长链接链路,通常采用私有协议的TCP请求。若是是第一次请求还会经过2作连接认证,认证经过后会把该Client和接入集群的某个服务器作个K/V对,并记录到路由表里一—这能够方便下发消息时找到该连接。
通过链路4,上行消息处理集群会将TCP请求转成普通的HTTP请求,再调用后端业务执行具体的业务逻辑,或者只是上传一个数据而已,不作任何响应。若是业务有数据须要下发,会通过链路6,把消息推送到消息下发处理集群,由它把消息推送给Client。
消息下发集群会查询连接路由表,肯定当前Client的连接在哪台机器上,再经过该服务器把消息推送下去。这里常见的问题是当前Client的网络不可达,致使消息没法推送。在这种状况下,消息下发处理集群会保持该消息,并定时尝试再推送;若是Client 从新创建链接,链接的服务器也会随之变化,那么消息下发集群会去查询连接路由表再从新链接新的K/V对。
链路9是为了处理Client端的一些同步请求而设计的。例如Client须要发送一个HTTP请求而且指望能返回结果,这时Client中的业务层可能直接请求HTTP,再通过Client中的网络模块转成私有TCP协议,在上行长链请求集群转成HTTP请求,调用后端业务并将HTTP的response转成消息发送到消息下发处理集群,异步下发给Client,到达Client 再转成业务的HTTP response。这种设计的主要考虑是当HTTP响应返回时,若是长链已经断掉,该响应就无法再推送回去。所以,这种上行同步请求而下行异步推送是一种更高可用的设计。
从总体架构上看,只有接入集群是有状态的,其余集群都是无状态的,这也保证了集群的扩展性。若是接入点在全国有多个点,而且这些点与服务端有专线网络服务,接入集群还能够作到就近接入。
当前的分布式集群管理中一般有两种设计思路:
两种思路各有优缺点:
Master 节点是固定集中式的,管理着全部其余节点,统一指挥、统一调度,全部信息的一致性都由它控制,不容易出错,是一种典型的集权式管理。
- 一旦Master挂了,整个集群就容易崩溃
- 因为它控制了全部的信息,因此也容易成为性能瓶颈
图示以下:
- 每一个节点的功能都是同样的,因此每一个节点都有能力成为 Master 节点
- 整个集群中全部机器的状态都保持一致
要达到整个集群中全部机器的状态都保持一致,须要节点之间充分的信息交换,这会致使:
- 机器之间交互控制的信息过多
- 集群越大信息越多,管理越复杂,在出现 bug 时不太容易排查
例如,对等集群管理模式中,最典型就是 Cassandra 的集群管理。Cassandra 利用了 Gossip 协议(谣言协议)达到集群中全部机器的状态都保持一致。
Gossip 协议(谣言协议)是指:一个节点状态发生变化很快被传播到机器中的全部节点,因而每一个节点发生相应的变动知识发散:路由器路由表维护所涉及的 RIP 动态路由协议原理。在RIP中,每一个路由器都周期地向其直通的邻居路由器发送本身彻底的路由表,而且也从本身直通的邻居路由器接收路由更新信息。由于每一个路由器都是从本身的邻居路由器了解路由信息,所以也将其称为“谣言”路由。
图示以下:
下面咱们以开源的Tair 集群管理为例着重介绍Maser/Slaver的一种管理模式(如图1.21所示),它的设计比较巧妙:
从集群的架构上能够看出一般有3个角色:Client、ConfigServer和DataNode,整个集群经过一个路由信息对照表来管理,以下面路由对照表所示:
Bucket | Node |
---|---|
0 | 192.168.0.1 |
1 | 192.168.0.2 |
2 | 192.168.0.1 |
3 | 192.168.0.2 |
4 | 192.168.0.1 |
5 | 192. 168.0.2 |
Bucket是DataNode上数据管理的基本单位,经过Bucket能够将用户的数据划分红若干个集合。上表中分红6个Bucket,那么全部用户的数据能够对6取模,这样每条数据都会存储在其中的一个Bucket中,而每一个Bucket也会对应一台DataNode。只要控制这张列表就能够控制用户数据的分布。
ConfigServer与DataNode保持心跳,并根据DataNode的状态生成对照表,Client主动向 ConfigServer 请求最新的对照表并缓存。ConfigServer最重要的责任就是维护对照表,但从实际的数据交互角度看,它并非强依赖——正常的数据请求不须要和ConfigServer 交互,即便ConfigServer 挂掉也不会当即影响整个集群的工做。缘由在于对照表在Client 或者DataNode上都有备份,所以ConfigServer不是传统意义上的Master 节点,也就不会成为集群的瓶颈。
下面介绍一下它们如何解决集群中的状态变动:扩容和容灾
扩容
假如要扩容一台机器192.168.0.3,那么整个集群的对照表须要从新分配,而从新分配对照表必然也会伴随着数据在DataNode之间的移动。数据的从新分配必须基于两个原则:尽量地保持现有的对照关系,均衡地分布到全部节点上。新的对照表以下表所示:
Bucket | Node |
---|---|
0 | 192.168.0.1 |
1 | 192.168.0.2 |
2 | 192.168.0.1 |
3 | 192.168.0.2 |
4 | 192.168.0.3 |
5 | 192.168.0.3 |
此时只需将4和5Bucket数据移动到新机器上。
容灾
容灾模式比扩容更复杂一些,除了上面两个原则之外,还须要考虑数据的备份状况。假如保存了3份数据,则对照表以下表所示:
Bucket | Node | Node | Node |
---|---|---|---|
0 | 192.168.0.1 | 192.168.0.2 | 192.168.0.3 |
1 | 192.168.0.2 | 192.168.0.3 | 192.168.0.1 |
2 | 192.168.0.1 | 192.168.0.2 | 192.168.0.3 |
3 | 192.168.0.2 | 192.168.0.1 | 192.168.0.3 |
4 | 192.168.0.3 | 192.168.0.1 | 192.168.0.2 |
5 | 192.168.0.3 | 192.168.0.1 | 192.168.0.2 |
第一列的Node做为主节点,若是主节点挂掉,那么第二列的备份节点就会升级为主节点;若是备份节点挂掉则不会受影响,而是再从新分配一个备份节点以保证数据的备份数。
当DataNode节点发生故障时,ConfigServer要从新生成对照表,并把新的对照表同步给全部的DataNode。
Client是如何获取最新对照表的呢?
每份对照表都有一个版本号,每次Client向DataNode请求数据时,DataNode都会把本身对照表的版本号返回给Client,若是Client发现本身的版本低,则会从ConfigServer拉取最新的对照表。
这种方式会产生一个问题:当对照表发生变动时,Client有可能会更新不及时致使请求失败。
为什么ConfigServer不把对照表主动推送给Client呢?
固然能够,但这会致使ConfigServer保持对每一个Client的心跳,加剧ConfigServer的负担,尤为当Client数量很是大的时候,容易给ConfigServer形成管理瓶颈。
所以,上面的设计实际上是取中考虑,即在Client的数量和DataNode发生故障的几率之间选择一个。
综上,集群管理中最大的困难就是当DataNode数量发生变化、涉及的数据发生迁移时,既要保证数据的一致性,又要保证高可用。
网站的分布式改造,核心是要解决如下问题:
最后咱们用图 1.22 、图 1.23 来总结单应用系统向分布式系统演进的过程:
单应用集群架构:
演进后典型的分布式集群架构:
说明
本文内容源自于许令波著的《大型网站技术架构演进与性能优化》一书的第一章。