前言:node
本文主要讨论数据分片的三个问题:(1)如何作数据分片,即如何将数据映射到节点;(2)数据分片的特征值,即按照数据中的哪个属性(字段)来分片;(3)数据分片的元数据的管理,如何保证元数据服务器的高性能、高可用,若是是一组服务器,如何保证强一致性。python
正文算法
在前文中,提出了分布式系统(尤为是分布式存储系统)须要解决的两个最主要的问题,即数据分片和数据冗余,下面这个图片(来源)形象生动的解释了其概念和区别:sql
其中数据即A、B属于数据分片,原始数据被拆分红两个正交子集分布在两个节点上。而数据集C属于数据冗余,同一份完整的数据在两个节点都有存储。固然,在实际的分布式系统中,数据分片和数据冗余通常都是共存的。mongodb
本文主要讨论数据分片的三个问题:数据库
所谓分布式系统,就是利用多个独立的计算机来解决单个节点(计算机)没法处理的存储、计算问题,这是很是典型的分而治之的思想。每一个节点只负责原问题(即整个系统须要完成的任务)的一个子集,那么原问题如何拆分到多个节点?在分布式存储系统中,任务的拆分即数据分片。编程
何为数据分片(segment,fragment, shard, partition),就是按照必定的规则,将数据集划分红相互独立、正交的数据子集,而后将数据子集分布到不一样的节点上。注意,这里提到,数据分片须要按照必定的规则,不一样的分布式应用有不一样的规则,但都遵循一样的原则:按照最主要、最频繁使用的访问方式来分片。缓存
三种数据分片方式性能优化
首先介绍三种分片方式:hash方式,一致性hash(consistent hash),按照数据范围(range based)。对于任何方式,都须要思考如下几个问题:服务器
为了后面分析不一样的数据分片方式,假设有三个物理节点,编号为N0, N1, N2;有如下几条记录:
hash方式:
哈希表(散列表)是最为常见的数据结构,根据记录(或者对象)的关键值将记录映射到表中的一个槽(slot),便于快速访问。绝大多数编程语言都有对hash表的支持,如python中的dict, C++中的map,Java中的Hashtable, Lua中的table等等。在哈希表中,最为简单的散列函数是 mod N(N为表的大小)。即首先将关键值计算出hash值(这里是一个整型),经过对N取余,余数即在表中的位置。
数据分片的hash方式也是这个思想,即按照数据的某一特征(key)来计算哈希值,并将哈希值与系统中的节点创建映射关系,从而将哈希值不一样的数据分布到不一样的节点上。
咱们选择id做为数据分片的key,那么各个节点负责的数据以下:
由此能够看到,按照hash方式作数据分片,映射关系很是简单;须要管理的元数据也很是之少,只须要记录节点的数目以及hash方式就好了。
但hash方式的缺点也很是明显:当加入或者删除一个节点的时候,大量的数据须要移动。好比在这里增长一个节点N3,所以hash方式变为了mod 4,数据的迁移以下:
在这种方式下,是不知足单调性(Monotonicity)的:若是已经有一些内容经过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应可以保证原有已分配的内容能够被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其余缓冲区。
在工程中,为了减小迁移的数据量,节点的数目能够成倍增加,这样几率上来说至多有50%的数据迁移。
hash方式还有一个缺点,即很难解决数据不均衡的问题。有两种状况:原始数据的特征值分布不均匀,致使大量的数据集中到一个物理节点上;第二,对于可修改的记录数据,单条记录的数据变大。在这两种状况下,都会致使节点之间的负载不均衡,并且在hash方式下很难解决。
一致性hash
一致性hash是将数据按照特征值映射到一个首尾相接的hash环上,同时也将节点(按照IP地址或者机器名hash)映射到这个环上。对于数据,从数据在环上的位置开始,顺时针找到的第一个节点即为数据的存储节点。这里仍然以上述的数据为例,假设id的范围为[0, 1000],N0, N1, N2在环上的位置分别是100, 400, 800,那么hash环示意图与数据的分布以下:
能够看到相比于上述的hash方式,一致性hash方式须要维护的元数据额外包含了节点在环上的位置,但这个数据量也是很是小的。
一致性hash在增长或者删除节点的时候,受到影响的数据是比较有限的,好比这里增长一个节点N3,其在环上的位置为600,所以,原来N2负责的范围段(400, 800]如今由N3(400, 600] N2(600, 800]负责,所以只须要将记录R7(id:533) 从N2,迁移到N3:
不难发现一致性hash方式在增删的时候只会影响到hash环上响应的节点,不会发生大规模的数据迁移。
可是,一致性hash方式在增长节点的时候,只能分摊一个已存在节点的压力;一样,在其中一个节点挂掉的时候,该节点的压力也会被所有转移到下一个节点。咱们但愿的是“一方有难,八方支援”,所以须要在增删节点的时候,已存在的全部节点都能参与响应,达到新的均衡状态。
所以,在实际工程中,通常会引入虚拟节点(virtual node)的概念。即不是将物理节点映射在hash换上,而是将虚拟节点映射到hash环上。虚拟节点的数目远大于物理节点,所以一个物理节点须要负责多个虚拟节点的真实存储。操做数据的时候,先经过hash环找到对应的虚拟节点,再经过虚拟节点与物理节点的映射关系找到对应的物理节点。
引入虚拟节点后的一致性hash须要维护的元数据也会增长:第一,虚拟节点在hash环上的问题,且虚拟节点的数目又比较多;第二,虚拟节点与物理节点的映射关系。但带来的好处是明显的,当一个物理节点失效是,hash环上多个虚拟节点失效,对应的压力也就会发散到多个其他的虚拟节点,事实上也就是多个其他的物理节点。在增长物理节点的时候一样如此。
工程中,Dynamo、Cassandra都使用了一致性hash算法,且在比较高的版本中都使用了虚拟节点的概念。在这些系统中,须要考虑综合考虑数据分布方式和数据副本,当引入数据副本以后,一致性hash方式也须要作相应的调整, 能够参加cassandra的相关文档。
range based
简单来讲,就是按照关键值划分红不一样的区间,每一个物理节点负责一个或者多个区间。其实这种方式跟一致性hash有点像,能够理解为物理节点在hash环上的位置是动态变化的。
仍是以上面的数据举例,三个节点的数据区间分别是N0(0, 200], N1(200, 500], N2(500, 1000]。那么数据分布以下:
注意,区间的大小不是固定的,每一个数据区间的数据量与区间的大小也是没有关系的。好比说,一部分数据很是集中,那么区间大小应该是比较小的,即以数据量的大小为片断标准。在实际工程中,一个节点每每负责多个区间,每一个区间成为一个块(chunk、block),每一个块有一个阈值,当达到这个阈值以后就会分裂成两个块。这样作的目的在于当有节点加入的时候,能够快速达到均衡的目的。
不知道读者有没有发现,若是一个节点负责的数据只有一个区间,range based与没有虚拟节点概念的一致性hash很相似;若是一个节点负责多个区间,range based与有虚拟节点概念的一致性hash很相似。
range based的元数据管理相对复杂一些,须要记录每一个节点的数据区间范围,特别单个节点对于多个区间的状况。并且,在数据可修改的状况下,若是块进行分裂,那么元数据中的区间信息也须要同步修改。
range based这种数据分片方式应用很是普遍,好比MongoDB, PostgreSQL, HDFS
小结:
在这里对三种分片方式(应该是四种,有没有virtual node的一致性hash算两种)进行简单总结,主要是针对提出的几个问题:
上面的数据动态均衡,值得是上述问题的第4点,即若是某节点数据量变大,可否以及如何将部分数据迁移到其余负载较小的节点
分片特征值的选择
上面的三种方式都提到了对数据的分片是基于关键值、特征值的。这个特征值在不一样的系统中有不一样的叫法,好比MongoDB中的sharding key, Oracle中的Partition Key,无论怎么样,这个特征值的选择都是很是很是重要的。
那么。怎么选择这个特征值呢?《Distributed systems for fun and profit》给出了言简意赅的标准:
大概翻译为:基于最经常使用的访问模式。访问时包括对数据的增删改查的。好比上面的列子,咱们选择“id”做为分片的依据,那么就是默认对的数据增删改查都是经过“id”字段来进行的。
若是在应用中,大量的数据操做都是经过这个特征值进行,那么数据分片就能提供两个额外的好处:
若是大量操做并无使用到特征值,那么就很麻烦了。好比在本文的例子中,若是用name去查询,而元数据记录的是如何根据按照id映射数据位置,那就尴尬了,须要到多有分片都去查一下,而后再作一个聚合!
另一个问题,若是以单个字段为特征值(如id),那么无论按照什么分布方式,在多条数据拥有相同的特征值(如id)的状况下,这些数据必定都会分布到同一个节点上。在这种状况下有两个问题,一是不能达到节点间数据的均衡,二是若是数据超过了单个节点的存储能力怎么办?关键在于,即便按照分布式系统解决问题的常规办法 -- 增长节点 --也是于事无补的。
在这个时候,单个字段作特征值就不行了,可能得再增长一个字段做为“联合特征值”,相似数据库中的联合索引。好比,数据是用户的操做日志,可使用id和时间戳一块儿做为hash函数的输入,而后算出特征值;但在这种状况下,若是还想以id为查询关键字来查询,那就得遍历全部节点了。
因此说没有最优的设计,只有最符合应用需求的设计。
下面以MongoDB中的sharding key为例,解释特征值选择的重要性以及对数据操做的影响。若是有数据库操做基础,即便没有使用过MongoDB,阅读下面的内容应该也没有问题。
以MongoDB sharding key为例
关于MongoDB Sharded cluster,以前也写过一篇文章《经过一步步建立sharded cluster来认识mongodb》,作了简单介绍。在个人工做场景中,除了联合查询(join)和事务,MongoDB的使用和Mysql仍是比较类似的,特别是基本的CRUD操做、数据库索引。MongoDb中,每个分片成为一个shard,分片的特征值成为sharding key,每一个数据称之为一个document。选择适合的字段做为shardingkey很是重要,why?
前面也提到,若是使用非sharding key去访问数据,那么元数据服务器(或者元数据缓存服务器,后面会讲解这一部分)是无法知道对应的数据在哪个shard上,那么该访问就得发送到全部的shard,获得全部shard的结果以后再作聚合,在mongoDB中,由mongos(缓存有元数据信息)作数据聚合。对于数据读取(R: read or retrieve),经过同一个字段获取到多个数据,是没有问题的,只是效率比较低而已。对于数据更新,若是只能更新一个数据,那么在哪个shard上更新呢,彷佛都不对,这个时候,MongoDB是拒绝的。对应到MongoDB(MongoDD3.0)的命令包括但不限于:
findandmodify:这个命令只能更新一个document,所以查询部分必须包含sharding key
update:这个命令有一个参数multi,默认是false,即只能更新一个document,此时查询部分必须包含sharding key
remove:有一个参数JustOne,若是为True,只能删除一个document,也必须使用sharidng key
另外,熟悉sql的同窗都知道,在数据中索引中有unique index(惟一索引),即保证这个字段的值在table中是惟一的。mongoDB中,也能够创建unique index,可是在sharded cluster环境下,只能对sharding key建立unique index,道理也很简单,若是unique index不是sharidng key,那么插入的时候就得去全部shard上查看,并且还得加锁。
接下来,讨论分片到shard上的数据不均的问题,若是一段时间内shardkey过于集中(好比按时间增加),那么数据只往一个shard写入,致使没法平衡集群压力。
MongoDB中提供了"range partition"和"hash partition",这个跟上面提到的分片方式 hash方式, ranged based不是一回事儿,而是指对sharding key处理。MongoDB必定是ranged base分片方式,docuemnt中如是说:
那么什么是"range partition"和"hash partition",官网的一张图很好说明了两者的区别:
上图左是range partition,右是hash partition。range partition就是使用字段自己做为分片的边界,好比上图的x;而hash partition会将字段从新hash到一个更大、更离散的值域区间。
hash partition的最大好处在于保证数据在各个节点上均匀分布(这里的均匀指的是在写入的时候就均匀,而不是经过MongoDB的balancing功能)。好比MongoDB中默认的_id是objectid,objectid是一个12个字节的BSON类型,前4个字节是机器的时间戳,那么若是在同一时间大量建立以ObjectId为_id的数据 会分配到同一个shard上,此时若将_id设置为hash index 和 hash sharding key,就不会有这个问题。
固然,hash partition相比range partition也有一个很大的缺点,就是范围查询的时候效率低!所以到底选用hash partition仍是range partition还得根据应用场景来具体讨论。
最后得知道,sharding key一但选定,就没法修改(Immutable)。若是应用必需要修改sharidng key,那么只能将数据导出,新建数据库并建立新的sharding key,最后导入数据。
元数据服务器
在上面讨论的三种数据分片分式中,或多或少都会记录一些元数据:数据与节点的映射关系、节点状态等等。咱们称记录元数据的服务器为元数据服务器(metaserver),不一样的系统叫法不同,好比master、configserver、namenode等。
元数据服务器就像人类的大脑,一只手不能用了还没忍受,大脑不工做整我的就瘫痪了。所以,元数据服务器的高性能、高可用,要达到这两个目标,元数据服务器就得高可扩展 -- 以此应对元数据的增加。
元数据的高可用要求元数据服务器不能成为故障单点(single point of failure),所以须要元数据服务器有多个备份,而且可以在故障的时候迅速切换。
有多个备份,那么问题就来了,怎么保证多个备份的数据一致性?
多个副本的一致性、可用性是CAP理论讨论的范畴,这里简单介绍两种方案。第一种是主从同步,首先选出主服务器,只有主服务器提供对外服务,主服务器将元数据的变革信息以日志的方式持久化到共享存储(例如nfs),而后从服务器从共享存储读取日志并应用,达到与主服务器一致的状态,若是主服务器被检测到故障(好比经过心跳),那么会从新选出新的主服务器。第二种方式,经过分布式一致性协议来达到多个副本件的一致,好比大名鼎鼎的Paxos协议,以及工程中使用较多的Paxos的特化版本 -- Raft协议,协议能够实现全部备份都可以提供对外服务,而且保证强一致性。
HDFS元数据
HDFS中,元数据服务器被称之为namenode,在hdfs1.0以前,namenode仍是单点,一旦namenode挂掉,整个系统就没法工做。在hdfs2.0,解决了namenode的单点问题。
上图中NN即NameNode, DN即DataNode(即实际存储数据的节点)。从图中能够看到, 两台 NameNode 造成互备,一台处于 Active 状态,为主 NameNode,另一台处于 Standby 状态,为备 NameNode,只有主 NameNode 才能对外提供读写服务。
Active NN与standby NN之间的数据同步经过共享存储实现,共享存储系统保证了Namenode的高可用。为了保证元数据的强一致性,在进行准备切换的时候,新的Active NN必需要在确认元数据彻底同步以后才能继续对外提供服务。
另外,Namenode的状态监控以及准备切换都是Zookeeper集群负责,在网络分割(network partition)的状况下,有可能zookeeper认为原来的Active NN挂掉了,选举出新的ActiveNN,但实际上原来的Active NN还在继续提供服务。这就致使了“双主“或者脑裂(brain-split)现象。为了解决这个问题,提出了fencing机制,也就是想办法把旧的 Active NameNode 隔离起来,使它不能正常对外提供服务。具体参见这篇文章。
MongoDB元数据
MongoDB中,元数据服务器被称为config server。在MongoDB3.2中,已经再也不建议使用三个镜像(Mirrored)MongoDB实例做为config server,而是推荐使用复制集(replica set)做为config server,此举的目的是加强config server的一致性,并且config sever中mongod的数目也能从3个达到replica set的上线(50个节点),从而提升了可靠性。
在MongoDB3.0及以前的版本中,元数据的读写按照下面的方式进行:
MongoDB的官方文档并无详细解释这一过程,不过在stackexchange上,有人指出这个过程是两阶段提交。
MongoDB3.2及以后的版本,使用了replica set config server,在《CAP理论与MongoDB一致性、可用性的一些思考》文章中,详细介绍了replica set的write concern、read concern和read references,这三个选项会影响到复制集的一致性、可靠性与读取性能。在config server中,使用了WriteConcern:Majority;ReadConcern:Majority;ReadReferences:nearest。
元数据的缓存:
即便元数据服务器能够由一组物理机器组成,也保证了副本集之间的一致性问题。可是若是每次对数据的请求都通过元数据服务器的话,元数据服务器的压力也是很是大的。不少应用场景,元数据的变化并非很频繁,所以能够在访问节点上作缓存,这样应用能够直接利用缓存数据进行数据读写,减轻元数据服务器压力。
在这个环境下,缓存的元数据必须与元数据服务器上的元数据一致,缓存的元数据必须是准确的,未过期的。相反的例子是DNS之类的缓存,即便使用了过时的DNS缓存也不会有太大的问题。
怎么达到缓存的强一致性呢?比较容易想到的办法是当metadata变化的时候当即通知全部的缓存服务器(mongos),但问题是通讯有延时,不可靠。
解决不一致的问题,一个比较常见的思路是版本号,好比网络通讯,通讯协议可能会发生变化,通讯双方为了达成一致,那么可使用版本号。在缓存一致性的问题上,也可使用版本号,基本思路是请求的时候带上缓存的版本号,路由到具体节点以后比较实际数据的版本号,若是版本号不一致,那么表示缓存信息过旧,此时须要从元数据服务器从新拉取元数据并缓存。在MongoDB中,mongos缓存上就是使用的这种办法。
另一种解决办法,就是大名鼎鼎的lease机制 -- “An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency”,lease机制在分布式系统中使用很是普遍,不只仅用于分布式缓存,在不少须要达成某种约定的地方都大显身手,在《分布式系统原理介绍》中,对lease机制有较为详细的描述,下面对lease机制进行简单介绍。
Lease机制:
既然,Lease机制提出的时候是为了解决分布式存储系统中缓存一致性的问题,那么首先来看看Lease机制是怎么保证缓存的强一致性的。注意,为了方便后文描述,在本小节中,咱们称元数据服务器为服务器,缓存服务器为客户端。
要点:
在Lease论文的标题中,提到了“Fault-Tolerant”,那么lease是怎么作到容错的呢。关键在于,只要服务器一旦发出数据和lease,不关心客户端是否收到数据,只要等待lease过时,就能够修改元数据;另外,lease的有效期经过过时时间(一个时间戳)来标识,所以即便从服务器到客户端的消息延时到达、或者重复发送都是没有关系的。
不难发现,容错的前提是服务器与客户端的时间要一致。若是服务器的时间比客户端的时间慢,那么客户端收到lease以后很快就过时了,lease机制就发挥不了做用;若是服务器的时间比客户端的时间快,那么就比较危险,由于客户端会在服务器已经开始更新元数据的时候继续使用缓存,工程中,一般将服务器的过时时间设置得比客户端的略大,来解决这个问题。为了保持时间的一致,最好的办法是使用NTP(Network Time Protocol)来保证时钟同步。
Lease机制的本质是颁发者授予的在某一有效期内的承诺,承诺的范围是很是普遍的:好比上面提到的cache;好比作权限控制,例如当须要作并发控制时,同一时刻只给某一个节点颁发lease,只有持有lease的节点才能够修改数据;好比身份验证,例如在primary-secondary架构中,给节点颁发lease,只有持有lease的节点才具备primary身份;好比节点的状态监测,例如在primary-secondary架构中监测primary是否正常,这个后文再详细介绍。
工程中,lease机制也有大量的应用:GFS中使用Lease肯定Chuck的Primary副本, Lease由Master节点颁发给primary副本,持有Lease的副本成为primary副本。chubby经过paxos协议实现去中心化的选择primary节点,而后Secondary节点向primary节点发送lease,该lease的含义是:“承诺在lease时间内,不选举其余节点成为primary节点”。chubby中,primary节点也会向每一个client节点颁发lease。该lease的含义是用来判断client的死活状态,一个client节点只有只有合法的lease,才能与chubby中的primary进行读写操做。
总结
本文主要介绍分布式系统中的分片相关问题,包括三种分布方式:hash、一致性hash、range based,以及各自的优缺点。分片都是按照必定的特征值来进行,特征值应该从应用的使用场景来选取,并结合MongoDB展现了特征值(mongodb中的sharding key)对数据操做的影响。分片信息(即元数据)须要专门的服务器存储,元数据服务器是分布式存储系统的核心,所以须要提到其可用性和可靠性,为了减轻元数据服务器的压力,分布式系统中,会在其余节点缓存元数据,缓存的元数据由带来了一致性的挑战,由此引入了Lease机制。
其实不少技术不是几句话就能说清楚的,因此干脆找朋友录制了一些视频,不少问题其实答案很简单,可是背后的思考和逻辑不简单,要作到知其然还要知其因此然。在此给你们推荐一个Java架构方面的交流学习群:698581634,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系,主要针对Java开发人员提高本身,突破瓶颈,相信你来学习,会有提高和收获。在这个群里会有你须要的内容 朋友们请抓紧时间加入进来吧。