序言
分布式系统理论出现在上个世纪70年代,但其广泛应用确是最近十多年的事情,其中一个原因就是人类活动创造出的数据量远远超出了单个计算机的存储和处理能力。另一个原因是分布式环境下的编程十分困难(异步调用,自己保存和恢复调用前后的上下文,处理更多的异常)。
前言
海量数据处理系统。分布式存储系统由数量众多的、低成本和高性价比的普通PC服务器通过网络连接而成。化纵向扩展为横向扩展,在软件层面实现自动容错,保证数据的一致性,自动负载均衡,使得系统的处理能力得到线性的扩展。
分布式存储是基础,云存储和大数据是构建在分布式存储之上的应用。如果没有分布式存储,便谈不上对大数据进行分析。分布式存储技术是互联网后端架构的“九阳神功”,掌握了这项技能,以后理解其他技术的本质会变得非常容易。
云计算、大数据以及互联网公司的各种应用,其后台基础设施的主要目标都是构建低成本、高性能、可扩展、易用的分布式存储系统。
相比传统的分布式系统,互联网公司的分布式系统具有两个特点:一个特点是规模大,另一个特点是成本低。不同的需求造就了不同的设计方案。本章介绍大规模分布式系统的定义与分类。
分布式存储系统是大量普通PC服务器通过Internet互联,对外作为一个整体提供存储服务。具备可扩展、低成本、高性能、易用等特性。
分布式存储系统的挑战主要在于数据、状态信息的持久化,要求在自动迁移、自动容错、并发读写的过程中保证数据的一致性。分布式存储涉及的技术主要来自两个领域:分布式系统以及数据库
分布式存储系统挑战大,研发周期长,涉及的知识面广。一般来说,工程师如果过能够深入理解分布式存储系统,理解其他互联网后台架构不会再有任何困难。
分布式存储面临的数据需求比较复杂,大致分为三类
1.分布式文件系统
分布式文件系统用于存储Blob(Binary Large Object,二进制大对象)对象,此外分布式文件系统也常作为分布式表格系统以及分布式数据库的底层存储。
总体上看,分布式文件系统存储三种类型的数据:Blob对象、定长块以及大文件。分布式文件系统内部按照数据块来组织数据,将这些数据块分散到存储集群,处理数据复制、一致性、负载均衡、容错等分布式系统难题,并将用户对Blob对象、定长块以及大文件的操作映射为对底层数据块的操作。
2.分布式键值系统
分布式键值系统用于存储关系简单的半结构化数据。从数据结构的角度看,分布式键值系统与传统的哈希表比较类似,不同的是,分布式键值系统支持将数据分布到集群中的多个存储节点。
3.分布式表格系统
分布式表格系统用于存储关系较为复杂的半结构化数据。
4.分布式数据库
分布式数据库一般是从单机关系数据库扩展而来,用于存储结构化数据。
单机存储引擎就是哈希表、B树等数据结构在机械磁盘、SSD等持久化介质上的实现。单机存储系统是单机存储引擎的一种封装,对外提供文件、键值、表格或者关系模型。
哈希存储引擎是哈希表的持久化实现,B树存储引擎是B树的持久化实现,而LSM树(Log Structure Merge Tree)存储引擎采用批量转储技术来避免磁盘随机写入。
计算机的硬件体系架构保持相对稳定。架构设计很重要的一点就是合理选择并且能够最大限度地发挥底层硬件的价值。
为了提高计算机的性能,出现多核处理器,架构也逐渐从SMP(对称多处理结构)过渡到NUMA(Non-Uniform Memory Access,非一致存储访问)架构。
存储系统的性能瓶颈一般在于IO。
传统的数据中心网络拓扑,分为三层,接入层、汇聚层和核心层。Google在2008年的时候将网络改造为扁平化拓扑结构,即三级CLOS网络。
存储系统的性能瓶颈主要在于磁盘随机读写。设计存储引擎的时候会针对磁盘的特性做很多的处理,不如将随机写操作转化为顺序写,通过缓存减少磁盘随机读操作。
存储系统的性能主要包括两个维度:吞吐量以及访问延时,设计系统时要求能够在保证访问延时的基础上,通过最低的成本实现尽可能高的吞吐量。磁盘和SSD的访问时延差别很大,但带宽差别不大。
存储引擎是存储系统的发动机,直接决定了存储系统能够提供的性能和功能。
Bitcask是一个基于哈希表结构的键值存储系统,仅支持追加操作。
1.数据结构
Bitcask在内存中存储了主键和value的索引信息,磁盘文件中存储了主键和value的实际内容。
2.定期合并
将所有老数据文件中的数据扫描一遍合并生成新的数据文件,这里的合并其实就是对同一个key的多个操作以只保留最新一个的原则机型删除,每次合并后,新生成的数据文件就不再有冗余数据了。
3.快速恢复
索引文件就是将内存中的哈希索引表转储到磁盘生成的结果文件。
1.数据结构
叶子节点保存每行的完整数据,非叶子结点保存索引信息。B+树的根节点是常驻内存的。修改操作首先需要记录提交日志,接着修改内存中的B+树。
2.缓冲区管理
缓冲区管理器的关键在于替换策略,即选择将哪些页面淘汰出缓冲池
将对数据的修改增量保持在内存中,达到指定的大小限制后将这些修改操作写入磁盘,读取时需要合并磁盘中的历史数据和内存中最近的修改操作。
1.存储结构
LevelDB存储引擎主要包括:内存中的MemTable和不可变MemTable(Immutable MemTable,也称为Frozen MemTable,即冻结MemTable,即冻结MemTable)以及磁盘上的几种主要文件:当前(Current)文件、清单(Manifest)文件、操作日志(Commit Log,也称为提交日志)文件以及SSTable文件。
2.合并
LevelDB写入操作很简单,但是读取操作比较复杂。LevelDB的Compaction操作分为两种:minor compaction和major compaction。
如果说存储引擎相当于存储系统的发动机,那么,数据模型就是存储系统的外壳。数据模型大概分为文件、关系以及随着NoSQL技术流行起来的键值模型。
文件系统以目录树的形式组织文件。POSIX(Portable Operating System Interface)是应用程序访问文件系统的API标准,定义了文件系统存储接口及操作集。POSIX标准不仅定义了文件操作接口,而且还定义了读写操作语义。POSIX标准适合单机文件系统,在分布式文件系统中,出于性能考虑,一般不会完全遵守这个标准。
Select查询语句计算过程大致如下(不考虑查询优化):
大量的NoSQL系统采用了键值模型(Key-Value模型),每行记录由主键和值两个部分组成。Key-Value模型过于简单,支持的应用场景有限,NoSQL系统中使用比较广泛的模型是表格模型(没有一个统一的标准)。
NoSQL系统带来了很多新的理念,比如良好的可扩展性,弱化数据库的设计范式,弱化一致性要求,在一定程度上解决了海量数据和高并发的问题。NoSQL只是对SQL特性的一种取舍和升华,使得SQL更加适应海量数据的应用场景,二者的优势将不断融合。
关系数据库在海量数据场景中,面临着事务,联表,性能等问题。然而NoSQL系统也面临如下问题,缺少统一标准,使用以及运维复杂。
总而言之,不必纠结SQL和NoSQL的区别,而是借鉴二者各自不同的优势,着重理解关系数据库的原理以及NoSQL系统的高可扩展性。
事物的概念和特性。为了性能的考虑,定义多种合理级别。并发控制通过锁机制来实现。互联网业务中读事务的比例往往远远高于写事务,为了提高读事务性能,可以采用写时复制(Copy-On-Write,COW)或者多版本并发控制(Multi-Version Concurrency Control,MVCC)技术来避免写事务阻塞读事务。
事务是数据库操作的基本单位,它具有原子性、一致性、隔离性和持久性这四个基本属性。
隔离级别与读写异常的关系。
数据库系统以及其他的分布式存储系统一般采用操作日志技术来实现故障恢复。
通过操作日志顺序记录每个数据库操作并在内存中执行这些操作,内存中的数据定期刷新到磁盘,实现将随机写请求转化为顺序写请求。
REDO日志的约束规则:在修改内存中的元素X之前,要确保与这一修改相关的操作日志必须先刷入磁盘中。
数据压缩分为有损压缩与无损压缩两种。压缩算法的核心是找出重复数据,列式存储技术通过把相同列的数据组织在一起,不仅减少了大数据分析需要查询的数据量,还大大地提高了数据的压缩比。
压缩是一个专门的研究课题,需要根据数据的特点选择或者自己开发和是的算法。压缩的本质就是找数据的重复或者规律,用尽量少的字节表示。
存储系统在选择压缩算法时需要考虑压缩比和效率,压缩比和读写数据两者之间需要一个很好的权衡点。Google Bigtable系统中通过牺牲一定的压缩比换取算法执行速度的大幅提升,从而获得更好的折中。
传统的行式数据库将一个个完整的数据行存储再数据页中。列式数据库是将同一个数据列的各个值存放在一起。很多列式数据库还支持列组(column group,Bigtable系统中称为locality group),即将多个经常一起访问的数据列的各个值存放在一起。列组是一种行列混合存储模式,这种模式能够同时满足OLTP和OLAP的查询需求。
由于同一个数据列的数据重复度很高,因此,列式数据库压缩时有很大的优势。
介绍分布式系统相关的基础概念和性能估算方法。接着,介绍分布式系统的基础理论知识,包括数据分布、复制、一致性、容错等。最后介绍常见的分布式协议。
大规模分布式存储系统的一个核心问题在于自动容错。然而,服务器节点是不可靠的,网络也是不可靠的,本节介绍系统运行过程中可能会遇到的各种异常。
可以这么认为,副本是分布式存储系统容错技术的唯一手段。由于多个副本的存在,如何保证副本之间的一致性是整个分布式系统的理论核心。
可以从两个角度理解一致性:第一个角度是用户,或者说是客户端,即客户端读写操作是否符合某种特性;第二个角度是存储系统,即存储系统的多个副本之间是否一致,更新的顺序是否相同。
评价分布式存储系统的一些常用的指标
性能分析是作为后续性能优化的依据。系统中的资源(CPU、内存、磁盘、网络)是有限的,性能分析就是需要找出可能出现的资源瓶颈。
数据分布的方式主要有两种,一种是哈希分布,另一种方法是顺序分布。分布式存储系统的一个基本要求就是透明性,包括数据分布透明性,数据迁移透明性,数据复制透明性,故障处理透明性。
哈希取模的方法很常见,其方法是根据数据的某一种特征计算哈希值,并将哈希值与集群中的服务器建立映射关系,从而将不同哈希值的数据分布到不同的服务器上。
传统的哈希分布算法还有一个问题:当服务器上线或者下线时,N值发生变化,数据映射完全被打乱,几乎所有的数据都需要重新分布,这将带来大量的数据迁移。解决方法有增加元数据服务器和一致性哈希(Distrid Hash Table,DHT)算法(在很大程度上避免了数据迁移)
哈希散列破坏了数据的有序性,只支持随机读取操作,不能够支持顺序扫描。顺序分布与B+树数据结构比较类似,每个子表相当于叶子节点,随着数据的插入和删除,某些子表可能变得很大,某些变得很小,数据分布不均匀。
分布式存储系统的每个集群中一般有一个总控节点,其他节点为工作节点,由总控节点根据全局负载信息进行整体调度。
分布式存储系统通过复制协议将数据同步到多个存储节点,并确保多个副本之间的数据一致性。
同一份数据的多个副本中往往由一个副本为主副本(Primary),其他副本为备副本(Backup),由主副本将数据复制到备份副本。复制协议分为两种,强同步复制以及异步复制,二者的区别在于用户的写请求是否需要同步到备副本才可以返回成功。
一致性和可用性时矛盾的。
强同步复制和异步复制都是将主副本的数据以某种形式发送到其他副本,这种复制协议称为基于主副本的复制协议(Primary-based protocol)。
主备副本之间的复制一般通过操作日志来实现。操作日志的原理很简单:为了利用好磁盘的顺序读写特性,将客户端的写操作先顺序写入磁盘中,然后应用到内存中,由于内存是随机读写设备,可以很容易通过各种数据结构,比如B+树将数据有效地组织起来。
CAP理论:一致性(Consistency),可用性(Availability)以及分区可容忍性(Tolerance of network Partition)三者不能同时满足。
工程理解CAP
分布式存储系统要求能够自动容错,也就是说,分区可容忍性总是需要满足的,因此,一致性和写操作的可用性不能同时满足。
首先,分布式存储系统需要能够检测到机器故障,在分布式系统中,故障检测往往通过租约(Lease)协议实现。接着,需要能够将服务复制或者迁移到集群中的其他正常服务的存储节点。
单机故障和磁盘故障发生概率最高,几乎每天都有多起事故,系统设计首先需要对单台服务器故障进行容错处理。
心跳是一种很自然的想法。存在的问题是“机器B是否应该被认为故障且停止服务”达成一致,通过租约(Lease)机制进行故障检测。租约机制就是带有超时时间的一种授权。
当总控机检测到工作机发生故障时,需要将服务迁移到其他工作机节点。常见的分布式存储系统分为两种结构:单层机构和双层结构。
可扩展性不能简单地通过系统是否为P2P架构或者是否能够将数据分布到多个存储节点来衡量,而应该综合考虑节点故障后的恢复时间,扩容的自动化程度,扩容的灵活性等。
分布式存储系统种往往有一个总控节点用于维护数据分布信息,执行工作机管理,数据定位,故障检测和恢复,负载均衡等全局调度工作。一般情况,总控节点不会成为瓶颈。
数据库可扩展性实现的手段包括:通过主从复制提高系统的读取能力,通过垂直拆分和水平拆分将数据分布到多个存储节点,通过主从复制将系统扩展到多个数据中心。
传统的数据库架构在可扩展性上面临如下问题
传统数据库扩容与大规模存储系统的可扩展性有何区别呢?
将存储节点分为若干组,某个组内的节点服务完全相同的数据,其中有一个节点为主节点,其他节点为备节点。由于同一个组内的节点服务相同的数据,这样的系统称为同构系统。而异构系统是为了实现线性可扩展性,将数据划分为很多大小接近的分片,每个分片的多个副本可以分布到集群种的任何一个存储节点。如果某个节点放生故障,原有的服务将由整个集群而不是某几个固定的存储节点来恢复,由于整个集群都参与到节点1的故障恢复过程,故障恢复时间很短,而且集群规模越大,优势就会越明显。
两阶段提交协议用于保证跨多个节点操作的原子性,也就是说,跨多个节点的操作要么在所有节点上全部执行成功,要么全部失败。Paxos协议用于确保多个节点对某个投票(例如哪个节点为主节点)达成一致。
两阶段提交协议经常用来实现分布式事务,在此过程中包括协调者和事务参与者两类节点。正常执行过程分为请求阶段和提交阶段。
可能会面临两种故障:事务参与者发生故障和协调者发生故障。总而言之,两阶段提交协议是阻塞协议,执行过程中需要锁住其他更新,且不能容错,大多数分布式存储系统都采用敬而远之的做法,放弃对分布式事务的支持。
Paxos协议用于解决多个节点之间的一致性问题。主节点发生故障,系统选举新的主节点,存在网络分区的时候,有可能存在多个备节点提议(Proposer,提议者)自己成为主节点。Paxos协议正是用来实现这个需求。
常见的做法是将2PC和Paxos协议集合起来,通过2PC保证多个数据分片上的操作的原子性,通过Paxos协议实现同一个数据分片的多个副本之间的一致性。另外,通过Paxos协议解决2PC协议中协调者宕机问题。当2PC协议中的协调者出现故障时,通过Paxos协议选举出新的协调者继续提供服务。
跨机房问题主要包含两个方面:数据同步以及服务切换。
跨机房部署三个方案
分布式文件系统的主要功能有两个:一个是存储文档、图像、视频之类的Blob类型数据;另外一个是作为分布式表格系统的持久化层。
GFS是Google分布式存储的基石。Google文件系统(GFS)是构建在廉价服务器之上的大型分布式系统。
GFS系统的节点可分为三种角色:GFS Master(主控服务器)、GFS ChunkServer(CS,数据块服务器)以及GFS客户端
主控服务器会定期与CS通过心跳的方式交换信息。需要注意的是,GFS种的客户端不缓存文件数据,只缓存主控服务器种获取的元数据,这是由GFS的应用特点决定的。GFS最主要的应用有两个:MapReduce与Bigtable。
Linux文件系统删除64MB大文件消耗的时间太久且没有必要,因此,删除chunk时可以只将对应的chunk文件移动到每个磁盘的回收站,以后新建chunk的时候可以重用。
ChunkServer是一个磁盘和网络IO密集型应用,为了最大限度地发挥机器性能,需要能够做到将磁盘和网络操作异步化,但这会增加代码实现的难度。
从GFS的架构设计可以看出,GFS是一个具有良好可扩展性并能够在软件层面自动处理各种异常情况的系统。
Google的成功经验也表明了一点:单Master的设计是可行的。单Master的设计不仅简化了系统,而且能够较好地实现一致性,后面我们将要看到的绝大多数分布式存储系统都和GFS一样依赖单总控节点。
文档、图片、视频一般称为Blob数据,存储Blob数据的文件系统也相应地称为Blob存储系统。Blob文件系统的特点是数据写入后基本都是只读,很少出现更新操作。
TFS架构设计时需要考虑两个问题:Metadata信息存储和减少图片读取的IO次数
TFS设计时采用的思路是:多个逻辑图片文件共享一个物理文件
TFS架构上借鉴了GFS,但与GFS又有很大的不同。首先,TFS内部不维护文件目录树,每个小文件使用一个64位的编号表示;其次,TFS是一个读多写少的应用,相比GFS,TFS的写流程可以做得更加简单有效。
图片应用中有几个问题,第一个问题是图片去重,第二个问题是图片更新与删除。去重使用的是在外部维护一套文件级别的去重系统(Dedup),采用MD5或者SHA1等Hash算法为图片文件计算指纹(FingerPrint)。去重是一个键值存储系统,淘宝内部使用Tair来进行图片去重。
图片在TFS中的位置是通过
Facebook相册后端早期采用基于NAS的存储,通过NFS挂载NAS中的照片文件来提供服务。
HayStack系统主要包括三个部分:目录(Directory)、存储(Store)以及缓存(Cache)。
相比TFS,Haystack的一大特色就是磁盘空间回收。
CDN通过将网络内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。这样既可以减轻源服务器的负担,也可以减少整个网络中的流量分布不均的情况,进而改善整个网络性能。
CDN采用两级Cache:L1-Cache以及L2-Cache。图片服务器是一个运行着Nginx的Web服务器,它还会在本地缓存图片,只有当本地缓存也不命中时才会请求后端的TFS集群,图片服务器集群和TFS集群部署在同一个数据中心内。
每个CDN节点内部通过LVS+Haproxy的方式进行负载均衡。其中,LVS是四层负载均衡软件,性能好。Haproxy是七层负载均衡软件,能够支持更加灵活的负载均衡策略。数据通过一致性哈希的方式分布到不同的Squid服务器,使得增加/删除服务器只需要移动1/n(n为Squid服务器总数)的对象。
相比分布式存储系统,分布式缓存系统的实现要容易很多。这是因为缓存系统不需要考虑数据持久化,如果缓存服务器出现故障,只需要简单地将它从集群中剔除即可。
由于Blob存储系统读访问量大,更新和删除很少,特别适合通过CDN技术分发到离用户最近的节点。CDN也是一种缓存,需要考虑与源服务器之间的一致性。
分布式键值模型可以看成是分布式表格模型的一种特例。
Dynamo以很简单的键值方式存储数据,不支持复杂的查询。Dynamo中存储的是数据值的原始形式,不解析数据的具体内容。
Dynamo系统采用一致性哈希算法将数据分不到多个存储节点中。一致性哈希的优点在于节点加入/删除时只会影响到在哈希环中相邻的节点,而对其他节点没影响。
考虑到节点的异构性,不同节点的处理能力差别可能很大,Dynamo使用了改进的一致性哈希算法:每个物理节点根据其性能的差异分配多个token,每个token对应一个“虚拟节点”。
Gossip协议用于P2P系统中自治的节点协调对整个集群的认识,比如集群的节点状态、负载情况。由于有种子节点的存在,新节点加入可以做得比较简单。
机器宕机开始被认定为永久失效的时间不会太长,积累的写操作也不会太多,可以利用Merkle树对机器的数据文件进行快速同步。
NWR是Dynamo中的一个亮点,其中N标识复制的备份数,R指成功读操作的最少节点数,W指成功写操作的最少节点数。可以对NWR取不同的值来满足不同的需求。
通过在Dynamo这样的P2P集群中,引入向量时钟(Vector Clock)的技术手段来尝试解决冲突。(类似于多版本控制机制)最常见的解决冲突方法有两种:一种是通过客户端逻辑来解决,另一种常见的策略是“last write wins”,即选择时间戳最新的副本,然而,这个策略依赖集群内节点之间的时钟同步算法,不能完全保证准确性。
Dynamo把异常分为两种类型:临时性的异常和永久性异常。
Dynamo的容错机制:
Dynamo的负载均衡取决于如何给每台机器分配虚拟节点号,即token。
写的过程平平无奇;读取数据的时候,当各个副本的数据一致时,会直接返回,否则,需要根据冲突处理规则合并多个副本的读取结果。
Dynamo的存储节点包含三个组件:请求协调、成员和故障检测、存储引擎。
Dynamo设计支持可插拔的存储引擎;请求协调组件采用基于事件驱动的设计;读操作成功返回客户端以后对应的状态机不会立即被销毁,而是等待一小段时间,这段时间内可能还有一些节点会返回过期的数据,协调者将更新这些节点的数据到最新版本,这个过程称为读取修复。
Dynamo采用务中心节点的P2P设计,增加了系统可扩展性,但同时带来了一致性问题,影响上层应用。主流的分布式系统一般都带有中心节点,这样能够简化设计,而且中心节点只维护少量元数据,一般不会成为性能瓶颈。
Tair是淘宝开发的一个分布式键/值存储引擎。Tair分为持久化和非持久化两种使用方式:非持久化的Tair可以看成是一个分布式缓存,持久化的Tair将数据存放于磁盘上。
Tair作为一个分布式系统,是由一个中心控制节点和若干个服务节点组成。
Amazon Dynamo采用P2P架构,而在Tair中引入了中心节点Config Server。这种方式很容易处理数据的一致性,不再需要向量时钟、数据回传、Merkle树、冲突处理等复杂的P2P技术。另外,中心节点的负载很低。笔者认为,分布式键值系统的整体架构应该参考Tair,而不是dynamo。
分布式表格系统对外提供表格模型,每个表格由很多行组成,通过主键唯一标识,每一行包含很多列。整个表格再系统中全局有序,顺序分布。
Bigtable是Google开发的基于GFS和Chubby的分布式表格系统,用于存储海量结构化和半结构化数据。某一行的某一列构成一个单元(Cell),每个单元包含很多列(Column)。
整体上看,Bigtable是一个分布式多维映射表。Bigtable将多个列组织成列族(column family),这样,列名由两个部分组成:(column family,qualifier)。列族是Bigtable中访问控制的基本单元,也就是说,访问权限的设置是再列族这一级别上进行的。
Google的很多服务,比如Web检索和用户的个性化设置,都需要保存不同时间的数据,这些不同的数据版本必须通过时间戳来区分。
Bigtable构建在GFS之上,为文件系统增加一层分布式索引层。Bigtable主要由三个部分组成:客户端程序库(Client)、一个主控服务器(Master)和多个子表服务器(Tablet Server)。
Chubby是一个分布式锁服务,底层的核心算法为Paxos。Paxos算法的实现过程需要一个“多数派”就某个值达成一致,进而才能得到一个分布式一致性状态。客户端、主控服务器以及子表服务器执行过程中都需要依赖Chubby服务,如果Chubby发生故障,Bigtable系统整体不可用。
Bigtable中的数据在系统中切分为大小100~200MB的子表,所有的数据按照行主键全局排序。Bigtable中包含两级元数据,元数据表及根表。为了减少访问开销,客户端使用了缓存(cache)和预取(prefetch)技术。
Bigtable系统保证强一致性,同一个时刻同一个子表只能被一台Tablet Server服务,这是通过Chubby的互斥锁机制保证的。
Bigtable的底层存储系统为GFS。GFS本质上是一个弱一致性系统,其一致性模型只保证“同一个记录至少成功写入一次”,但是可能存在重复记录,而且可能存在一些补零(padding)记录。
Bigtable写入GFS的数据分为两种:
Bigtable本质上构建在GFS之上的一层分布式索引,通过它解决了GFS遗留的一致性问题,大大简化了用户使用。
Bigtable中Master对Tablet Server的监控是通过Chubby完成的,Tablet Server在初始化时都会从Chubby中获取一个独占锁。通过这种方式所有的Tablet Server基本信息被保存在Chubby中一个称为服务器目录(Server Directory)的特殊目录之中。
每个子表持久化的数据包含两个部分:操作日志以及SStable。
Bigtable Master启动时需要从Chubby中获取一个独占锁,如果Master发生故障,Master的独占锁将过期,管理程序会自动指定一个新的Master服务器,它从Chubby成功获取独占锁后可以继续提供服务。
子表是Bigtable负载均衡的基本单位。子表迁移分为两步:第一步请求原有的Table Server卸载子表;第二步选择一台负载较低的Tablet Server加载子表。子表迁移前原有的Tablet Server会对其执行Minor Compaction操作,将内存中的更新操作以SSTable文件的形式转储到GFS中,因此,负载均衡带来的子表迁移在新的Tablet Server上不需要回放操作日志。
子表迁移的过程中有短暂的时间需要暂停服务,为了尽可能减少暂停服务的时间,Bigtable内部采用两次Minor Compaction的策略。具体操作如下:
顺序分布于哈希分布的区别在于哈希分布往往是静态的,而顺序分布式动态的,需要通过分裂与合并操作动态调整。
Bigtable每个子表的数据分为内存中的MemTable和GFS中的多个SStable,由于Bigtable中同一个子表只被一台Tablet Server服务,进行分类时比较简单。
合并操作由Master发起,相比分裂操作要更加复杂。
Bigtable采用Merge-dump存储引擎。
插入、删除、更新、增加(Add)等操作在Merge-dump引擎中都看成一回事,除了最早生成的SSTable外,SSTable中记录的只是操作,而不是最终的结果,需要等到读取(随机或者顺序)时才合并得到最终结果。
Bigtable中包含三种Compaction策略:Minor Compaction、Merging Compaction和Major Compaction。
数据在SSTable中按照主键有序存储,每个SSTable由若干个大小相近的数据块(Block)组成,每个数据块包含若干行。Tablet Server的缓存包括两种:块缓存(Block Cache)和行缓存(Row Cache)。另外,Bigtable还支持布隆过滤器(Bloom Filter),如果读取的数据行在SSTable中不存在,可以通过布隆过滤器发现,从而避免一次读取GFS文件操作。
Compaction后生成新的SSTable,原有的SSTable称为垃圾需要被回收掉。这里需要注意,由于Tablet Server执行Compaction操作生成一个全新的SSTable与修改元数据这两个操作不是原子的,垃圾回收需要避免删除刚刚生成但还没有记录到元数据中的SSTbale文件。一种比较简单的做法是垃圾回收只删除至少一段时间,比如1小时没有被使用的SSTable文件。
GFS+Bigtable两层架构以一种很优雅的方式兼顾系统的强一致性和可用性。
Bigtable架构也面临一些问题:单副本服务,SSD使用,架构的复杂性导致Bug定位很难。
Google Bigtable架构把可扩展性基本做到了极致,Megastore则是在Bigtable系统之上提供友好的数据库功能支持,增强易用性。Megastore是介于传统的关系型数据库和NoSQL之间的存储技术。
可以根据用户将数据拆分为不同的子集分布到不同的机器上。Google进一步从互联网应用特性中抽取实体组(Entity Group)概念,从而实现可扩展性和数据库语义之间的一种权衡,同时获得NoSQL和RDBMS的优点。存在实体组根表和实体组子表的概念,根实体除了存放用户数据,还需要存放Megastore事务及复制操作所需的元数据,包括操作日志。
Megastore系统由三个部分组成:
Megastore的功能主要分为三个部分:映射Megastore数据模型到Bigtable,事务及并发控制,跨机房数据复制及读写优化。Megastore首先解析用户通过客户端传入的SQL请求,接着根据用户定义的Megastore数据模型将SQL请求转化为对底层Bigtable的操作。
总体上看,数据拆分成不同的实体组,每个实体组内的操作日志采用基于Paxos的方式同步到多个机房,保证强一致性。实体组之间通过分布式队列的方式保证最终一致性或者两阶段提交协议的方式实现分布式事务。
对于多个集群之间的操作日志同步,Megastore系统采用的是基于Paxos的复制协议机制。
Megastore最新读取流程如下
分布式存储系统有两个目标:一个是可扩展性,最终目标是线性可扩展;另外一个是功能,最终目标是支持全功能SQL。Megastore是一个介于传统的关系型数据库和分布式NoSQL系统之间的存储系统,融合了SQL和NoSQL两者的优势。
Megastore的主要创新点:
Windows Azure Storage(WAS)时微软开发的云存储系统,包括三种数据存储服务:Windows Azure Blob、Windows Azure Table。Windows Azure Queue。三种数据存储服务共享一套底层架构。
WAS部署在不同地域的多个数据中心,依赖底层的Windows Azure结构控制器(Fabric Controller)管理硬件资源。结构控制器的功能包括节点管理,网络配置,健康检查,服务启动,关闭,部署和升级。另外,WAS还通过请求结构控制器获取网络拓扑信息,集群物理部署以及存储节点硬件配置信息。
WAS主要分为两个部分:定位服务(Location Service,LS)和存储区(Storage Stamp)
另外,WAS包含两种复制方式:存储区内复制(Intra-Stamp Replication)和跨存储区复制(Inter-Stamp Replication)
文件流层提供内部接口供服务分区层使用。文件流层中的文件称为流,每个流包含一系列的extent。每个extent由一连串的block组成。block是数据读写的最小单位,每个block最大不超过4MB。WAS中的block与GFS中的记录(record)概念是一致的。
extent是文件流层数据复制,负载均衡的基本单位,每个存储区默认对每个extent保留三个副本,每个extent的默认大小为1GB。WAS中的extent与GFS中的chunk概念是一致的。
stream用于文件流层对外接口,每个stream在层级命名空间中有一个名字。WAS中的stream与GFS中的file概念是一致的。
分区层构建在文件流层之上,用于提供Table、Blob、Queue等数据服务。分区层的一个重要特性是提供一致性并保证事务操作顺序。
分区层内部支持一种称为对象表(Object Table,OT)的数据架构,每个OT是一张最大可达若干PB的大表。
WAS整体架构借鉴GFS+Bigtable并有所创新。
关系型数据库设计之初并没有预见到IT行业发展如此之快,总是假设系统运行在单机这一封闭系统上。
有很多思路可以实现关系数据库的可扩展性。例如,在应用层划分数据,将不同的数据分片划分到不同的关系数据库上,如MySQL Sharding;或者在关系数据内部支持数据自动分片,如Microsoft SQL Azure;或者干脆从存储引擎开始重写一个全新的分布式数据库,如Google Spanner以及Alibaba OceanBase。
为了扩展关系数据库,最简单也是最为常见的做法就是应用层按照规则将数据拆分为多个分片,分布到多个数据库节点,并引入一个中间层来对应用屏蔽后端的数据库拆分细节。
以MySQL Sharding架构为例,分为几个部分
MySQL Sharding集群一般按照用户id进行哈希分区,这里面存在两个问题:
引入数据库中间层将后端分库分表对应用透明化在大公司互联网公司内部很常见。面临一些问题:数据库复制、扩容问题、动态数据迁移问题
Microsoft SQL Azure是微软的云关系型数据库,后端存储又称为云SQL Server(Cloud SQL Server)。他构建在SQL Server之上,通过分布式技术提升传统关系型数据库的可扩展性和容错能力。
云SQL Server分为四个主要部分:
云SQL Server采用“Quorum Commit”的复制协议,用户数据存储三个副本,至少写成功两个副本才可以返回客户端成功。某些备副本可能出现故障,恢复后将往主副本发送本地已经提交的最后一个事务的提交顺序号。主副本与备副本之间传送逻辑操作日志,而不是对磁盘物理页的redo&undo日志。
如果数据节点发生了故障,需要启动宕机恢复过程。由于云SQL Server采用“Quorum Commit”复制协议,如果每个分区有三个副本,至少保证两个副本写入成功,主副本出现故障后选择最新的备副本可以保证不丢数据。
全局分区管理器控制重新配置任务的优先级,否则,用户的服务会受到影响。
全局分区管理器也采用“Quorum Commit”实现高可用性。它包含七个副本,同一时刻只有一个副本为主,分区相关的元数据操作至少需要在四个副本上成功。如果全局分区管理器主副本出现故障,分布式基础部件将负责从其他副本中选择一个最新的副本作为新的主副本。
负载均衡相关的操作包含三种:副本迁移以及主备副本切换。新的服务器节点加入时,系统内的分区会逐步地迁移到新节点,这里需要注意的是,为了避免过多的分区同时迁入新节点,全局分区管理器需要控制迁移的频率,否则系统整体性能可能会下降。另外,如果主副本所在服务器负载过高,可以选择负载较低的备副本替换为主副本提供读写服务。这个过程称为主备副本切换,不涉及数据拷贝。
云存储系统中多个用户的操作相互干扰,因此需要限制每个SQL Azure逻辑实例使用的系统资源:
Microsoft SQL Azure将传统的关系型数据库SQL Server搬到云环境中,比较符合用户过去的使用习惯。云SQL Server与单机SQL Server的区别:不支持的操作、观念转变
相比Azure Table Storage,SQL Azure在扩展上有一些劣势,例如,单个SQL Azure实例大小限制。Azure Table Storage单个用户表格的数据可以分布到多个存储节点,数据总量几乎没有限制;而单个SQL Azure实例最大限制为50GB,如果用户的数据量大于最大值,需要用户在应用层对数据库进行水平或者垂直拆分,使用起来比较麻烦。
Google Spanner是Google的全球级分布式数据库(Globally-Distributed Database)。Spanner的扩展性达到了全球级,可以扩展到数百个数据中心,数百万台机器,上万亿行记录。更为重要的是,除了夸张的可扩展性之外,它还能通过同步复制和多版本控制来满足外部一致性,支持跨数据中心事务。
Spanner的表是层次化的,最底层的表是目录表(Directory table),其他表创建时,可以用INTERLEAVE IN PARENT来表示层次关系。实际存储时,Spanner会将同一个目录的数据存放到一起,只要目录不太大,同一个目录的每个副本都会分配到同一台机器。因此,针对同一个目录的读写事务大部分情况下都不会涉及跨机操作。
Spanner构建在Google下一代分布式文件系统Colossus之上。由于Spanner是全球性的,因此它有两个其他分布式存储系统没有的概念:Universe和Zones。
Spanner系统包含如下组件:
每个数据中心运行着一套Colossus,每个机器有100~1000个子表,每个子表会在多个数据中心部署多个副本。
通过Paxos协议,实现了跨数据中心的多个副本之间的一致性。
锁表实现单个Paxos组内的单机事务,事务管理器实现跨多个Paxos组的分布式事务。为了实现分布式事务,需要实现两阶段提交协议。有一个Paxos组的主副本会成为两阶段提交协议中的协调者,其他Paxos组的主副本为参与者。
为了实现并发控制,数据库需要给每个事务分配全局唯一的事务id。然而,在分布式系统中,很难生成全局唯一id。一种方式是采用Google Percolator(Google Caffeine的底层存储系统)中的做法,即专门部署一套Oracle数据库用于生成全局唯一id。虽然Oracle逻辑上是一个单点,但是实现的功能单一,因而能够做得很高效。Spanner选择了另外一种做法,即全球时钟同步机制TrueTime。
Spanner使用TrueTime来控制并发,实现外部一致性,支持以下几种事务:
目录是Spanner中对数据分区、复制和迁移的基本单位,用户可以指定一个目录有多少副本,分别存放在哪些机房中,例如将用户的目录存放在这个用户所在地区附近的几个机房中。
一个Paxos组包含多个目录,目录可以在Paxos组之间移动。Spanner移动一个目录一般出于以下几种考虑:
实现时,首先将目录的实际数据移动到指定位置,然后再用一个院子操作更新元数据,从而完成整个移动过程。
Google的分布式存储系统一步步地从Bigtable到Megastore,再到Spanner,这也印证了分布式技术和传统关系数据库技术融合的必然性,即底层通过分布式技术实现可扩展性,上层通过关系数据库的模型和接口将系统的功能暴露给用户。
OceanBase系统的最终目标:可扩展的关系数据库。
从模块划分的角度看,OceanBase可以划分为四个模块:主控服务器RootServer、更新服务器UpdateServer、基线数据服务器ChunkServer以及合并服务器MergeServer。OceanBase系统内部按照时间线将数据划分为基线数据和增量数据,基线数据是只读的,所有的修改更新到增量数据中,系统内部通过合并操作定期将增量数据融合到基线数据中。
阿里巴巴需要研发适合互联网规模的分布式数据库,这个数据库不仅能解决收藏夹面临的业务挑战,还要能做到可扩展、低成本、易用,并能够应用到更多的业务场景。
根据业务特点对数据库进行水平拆分,这种方式目前还存在一定的弊端
另一种做法是参考分布式表格系统的做法,例如Google Bigtable系统,将大表划分为几万、几十万甚至几百万个子表,子表之间按照主键有序,如果某台服务器发生故障,它上面服务的数据能够在很短的时间内自动迁移到集群中所有的其他服务器。
Bigtable只支持单行事务,针对同一个user_id下的多条记录的操作都无法保证原子性。而OceanBase希望能够支持跨行跨表事务,这样使用起来会比较方便。
OceanBase决定采用单台更新服务器来记录最近一段时间的修改增量,而以前的数据保持不变,以前的数据称为基线数据。基线数据以类似分布式文件系统的方式存储于多台基线数据服务器中,每次查询都需要把基线数据和增量数据融合后返回给客户端。这样,写事务都集中在单台服务器上,避免了复杂的分布式事务,高校地实现了跨行跨表事务;另外,更新服务器上的修改增量能够定期分发到多台基线数据服务器中,避免成为瓶颈,实现了良好的扩展性。
OceanBase客户端与MergeServer通信,目前主要支持如下几种客户端:
Java/C客户端访问OceanBase的流程大致如下:
RootServer的功能主要包括:集群管理、数据分布以及副本管理。RootServer管理集群中的所有MergeServer、ChunkServer以及UpdateServer。OceanBase内部使用主键对表格中的数据进行排序和存储,主键由若干列组成并且具有唯一性。
MergeServer的功能主要包括:协议解析、SQL解析、请求转发、结果合并、多表操作等。
OceanBase客户端与MergeServer之间的协议为MySQL协议。MergeServer缓存了子表分布信息,根据请求涉及的子表将请求转发给该子表所在的ChunkServer。如果是写操作,还会转发给UpdateServer。MergeServer支持并发请求多台ChunkServer,即将多个请求发送给多台ChunkServer,再一次性等待所有请求的应答。
MergeServer本身是没有状态的,因此,MergeServer宕机不会对使用者产生影响,客户端会自动将发生故障的MergeServer屏蔽掉。
ChunkServer的功能包括:存储多个子表,提供读取服务,执行定期合并以及数据分发。
MergeServer将每个子表的读取请求发送到子表所在的ChunkServer,ChunkServer首先读取SSTable中包含的基线数据,接着请求UpdateServer获取相应的增量更新数据,并将基线数据与增量更新融合后得到最终结果。
UpdateServer是集群唯一能够接受写入的模块,每个集群中只有一个主UpdateServer。UpdateServer中的更新操作首先写入到内存表,当内存表的数据量超过一定值时,可以生成快照文件并转储到SSD中。为了保证可靠性,主UpdateServer更新内存表之前需要首先写操作日志,并同步到备UpdateServer。另外,系统实现时也需要对UpdateServer的内存操作、网络框架、磁盘操作做大量的优化。
定期合并和数据分发都是将UpdateServer中的增量更新分发到ChunkServer中的手段。
定期合并于数据分发两者之间的不同点在于,数据分发过程中ChunkServer只是将UpdateServer中冻结内存表中的增量更新数据缓存到本地,而定期合并过程中ChunkServer需要将本地SSTable中的基线数据与冻结内存表的增量更新数据执行一次多路归并,融合后生成新的基线数据并存放到新的SSTable中。
虽然定期合并过程中给个ChunkServer的各个子表合并时间和完成时间可能都不相同,但并不影响读取服务。如果子表没有合并完成,那么使用旧子表,并且读取UpdateServer中的冻结内存表以及新的活跃内存表;否则,使用新子表,只读取新的活跃内存表,
查询结果 = 旧子表 + 冻结内存表 + 新的活跃内存表
= 新子表 + 新的活跃内存表
Eric Brewer教授的CAP理论指出,在满足分区可容忍性的前提下,一致性和可用性不可兼得。
虽然目前大量的互联网项目选择了弱一致性,但我们认为是底层存储系统,比如MySQL数据库,在大数据量和高并发需求压力之下的无奈选择。强一致性将大大简化数据库的管理,应用程序也会因此而简化。因此,OceanBase选择支持强一致性和跨行跨表事务。
另外,OceanBase所有写事务最终都落到UpdateServer,而UpdateServer逻辑上是一个单点,支持跨行跨表事务,实现上借鉴了传统关系数据库的做法。
OceanBase数据分为基线数据和增量数据两个部分,基线数据分布在多台ChunkServer上,增量数据全部存放在一台UpdateServer上。
不考虑数据复制,基线数据的数据结构如下:
增量数据的数据结构如下:
分布式系统需要处理各种故障,例如,软件故障、服务器故障、网络故障、数据中心故障、地震、火灾等。与其他分布式存储系统一样,OceanBase通过冗余的方式保障了高可靠性和高可用性。
在OceanBase系统中,用户的读写请求,即读写事务,都发给MergeServer。MergeServer解析这些读写事务的内容,例如词法和语法分析、schema检查等。对于只读事务,由MergeServer发给相应的ChunkServer分别执行后再合并每个ChunkServer的执行结果;对于读写事务,由MergeServer进行预处理后,发送给UpdateServer执行。
OceanBase架构的优势在于即支持跨行跨表事务,又支持存储服务器线性扩展。当然,这个架构也有一个明显的缺陷:UpdateServer单点,这个问题限制了OceanBase集群的整体读写性能。
磁盘随机IO是存储系统性能的决定因素,传统的SAS盘能够提供的IOPS不超过300。最近几年,SSD磁盘取得了很大的进展,它不仅提供了非常好的随机读取性能,功耗也非常低,大有取代传统机械磁盘之势。
然而,SSD盘的随机写性能并不理想。这是因为,尽管SSD的读和写以页(page,例如4KB,8KB等)为单位,但SSD写入前需要首先擦除已有内容,而擦除以块(block)为单位,一个块由若干个连续的页组成,大小通常在512KB2MB。加入写入的页有内容,即使值写入一个字节,SSD也是需要擦除整个512KB2MB大小的块,然后再写入整个页的内容,这就是SSD的写入放大效应。
数据丢失或者数据错误对于存储系统来说是一种灾难。
OceanBase采取了以下数据校验措施:
OceanBase对外提供的是与关系数据库一样的SQL操作接口,而内部却实现成一个线性可扩展的分布式系统。系统从逻辑实现上可以分为两个层次:分布式存储引擎层以及数据库功能层
从另外一个角度看,OceanBase融合了分布式存储系统和关系数据库这两种技术。通过分布式存储技术将基线数据分布到多台ChunkServer,实现数据复制、负载均衡、服务器故障检测与自动容错,等等;UpdateServer相当于一个高性能的内存数据库,底层采用关系数据库技术实现。
分布式存储引擎层负责处理分布式系统中的各种问题,例如数据分布、负载均衡、容错、一致性协议等。数据库功能层构建在分布式存储引擎层之上。分布式存储引擎层包括三个模块:RootServer、UpdateServer以及ChunkServer。
OceanBase包含一个公共模块,包含其他模块共用的网络框架、内存池、任务队列、锁、基础数据结构等。
OceanBase源代码中有一个公共模块,包含其他模块需要的公共类,例如公共数据结构、内存管理、锁、任务队列、RPC框架、压缩/解压缩等。
内存管理是C++高性能服务器的核心问题。在分布式存储系统开发初期,这个时期内存管理的首要问题并不是高效,而是可控性,并防止内存碎片。
OceanBase系统有一个全局的定长内存池,这个内存池维护了由64KB大小的定长内存块组成的空闲联表,其工作原理如下:
OceanBase的全局内存池实现简单,但内存使用率比较低,即使申请几个字节的内存,也需要占用大小为64KB的内存块。因此,全局内存池不适合管理小块内存,每个需要申请内存的模块,比如UpdateServer中的MemTable,Chunkserver中的缓存等,都只能从全局内存池中申请大块内存,每个模块内部再实现专用的内存池。每个线程处理读写请求时需要使用临时内存,为了提高效率,每个线程会缓存若干大小分别为64KB和2MB的内存块,每个线程总是首先尝试从线程局部缓存中申请内存,如果申请不到,再从全局内存池中申请。
OBIAllocator是内存管理器的接口,包含alloc和free两个方法。ObMalloc和ObTCMalloc是两个实现了ObIAllocator接口的全局内存池,不同点在于,ObMalloc不支持线程缓存,ObTCMalloc支持线程缓存。ObTCMalloc首先尝试从线程局部的空闲链表申请内存块,如果申请不到,再通过ObMalloc的alloc方法申请。释放内存时,如果没有超出线程缓存的内存块个数限制,则将内存块还给线程局部的空闲链表;否则,通过ObMalloc的free方法释放。另外,允许通过set_mod_id函数设置申请者所在的模块编号,便于统计每个模块的内存使用情况。
群居内存池的意义如下:
总而言之,OceanBase的内存管理没有采用高深的技术,也没有做到通用或者最优,但是很好地满足了服务器程序开发的两个最主要的需求:可控性以及没有内存碎片。
每个索引节点满了以后将分裂为两个节点,并触发对该索引节点的父亲节点的修改操作。分裂操作将增加插入线程冲突的概率,如果Data1和Data2的祖父节点,从而产生冲突。
另外,为了提高读写并发能力,B树实现时采用了写时复制(Copy-on-write)技术,修改每个索引节点时首先将该节点拷贝出来,接着在拷贝出来的节点上执行修改操作,最后再原子地修改其父亲节点的指针使其指向拷贝出现的节点。这种实现方式的好处在于修改操作不影响读取,读取操作永远不会被阻塞。
这里的B树不支持更新(Update)以及删除操作,这是由OceanBase MVCC存储引擎的实现机制决定的。对于更新操作,MVCC存储引擎会在行的末尾追加一个单元记录更新的内容,而不会影响索引结构;对于删除操作,MVCC存储引擎内部实现为标记删除,即在行的末尾追加一个单元记录行的删除时间,而不会物理删除某行数据。
为了实现并发控制,OceanBase需要对一行记录加共享锁或者互斥锁。
在生产者/消费者模型中,往往有一个任务队列,生产者将任务加入到任务队列,消费者从任务队列中取出任务进行处理。OceanBase还实现了LightyQueue用于解决全局任务队列锁冲突问题。
OceanBase服务端接收客户端发送的网络包(ObPacket),并交给handlePacket处理函数进行处理。默认情况下,handlePacket会将网络包加入到全局任务队列中。接着,工作线程会从全局任务队列中不断获取网络包,并调用do_request进行处理,处理完成后应答客户端。可以通过set_thread_count函数来设置工作线程以及网络线程的个数。
客户端发包分为两种情况:异步请求(post_request)以及同步请求(send_request)。异步请求时,客户端将请求包加入到网络发送队列后立即返回,不等待应答。同步请求时,客户端将请求包加入到网络发送队列后开始阻塞等待,直到网络线程接收到服务端的应答包后才唤醒客户端,从而执行后续处理逻辑。
ObCompressor定义了压缩与解压缩的通用接口,具体的压缩库实现了这些接口。压缩库以动态库(.so)的形式存在,每个工作线程第一次调用compress或者decompress方法时将加载相应的动态库,这样便实现了压缩库的插件化。目前,支持的压缩库包括LZO以及Snappy。
RootServer是OceanBase集群对外的窗口,客户端通过RootServer获取集群中其他模块的信息。RootServer实现的功能包括:
RootServer的中心数据结构为一张存储了子表数据分布的有序表格,称为RootTable。每个子表存储的信息包括:子表主键范围、子表各个副本所在ChunkServer的编号、子表各个副本的数据行数、占用的磁盘空间、CRC校验值以及基线数据版本。
RootServer是一个读多写少的数据结构,除了ChunkServer汇报、RootServer发起子表复制、迁移以及合并等操作需要修改RootTable外,其他操作都只需要从RootTable中读取某个子表所在的ChunkServer。
ChunkServer汇报的子表信息可能和RootTable中记录的不同,比如发生了子表分裂。此时,RootServer需要根据汇报的tablet信息更新RootTable。
RootServer中还有一个管理所有ChunkServer信息的数组,称为ChunkServer-Manager。数组中的每个元素代表一台ChunkServer,存储的信息包括:机器状态(已下线、正在服务、正在汇报、汇报完成,等等)、启动后注册时间、上次心跳时间、磁盘相关信息、负载均衡相关信息。
RootServer中有两种操作都可能触发子表迁移:子表复制(rereplication)以及负载均衡(rebalance)。
每台ChunkServer记录了子表迁移相关信息,包括:ChunkServer上子表的个数以及所有子表的大小总和,正在迁入的子表个数、正在迁出的子表个数以及子表迁移任务列表。
子表复制以及负载均衡生成的子表迁移任务并不会立即执行,而是会加入到迁移源的迁移任务列表中,RootServer还有一个后台线程会扫描所有的ChunkServer,接着执行每台ChunkServer的迁移任务列表中保存的迁移任务。子表迁移时限制了每台ChunkServer同时进行的最大迁入和迁出任务数,从而防止一台新的ChunkServer刚上线时,迁入大量子表而负载过高。
子表分裂由ChunkServer在定期合并过程中执行,由于每个子表包含多个版本,且分布在多台ChunkServer上,如何确保多个副本之间的分裂点保持一致成为问题的关键。OceanBase采用了一种比较直接的做法:每台ChunkServer使用相同的分裂规则。由于每个子表的不同副本之间的基线数据完全一致,且定期合并过程中冻结的增量数据也完全相同,只要分裂规则一致,分裂后的子表主键范围也保证相同。
每个子表包含多个副本,只要某一个副本合并成功,OceanBase就认为子表合并成功,其他合并失败的子表将通过垃圾回收机制删除掉。
为了确保一致性,RootServer需要确保每个集群中只有一台UpdateServer提供写服务,这个UpdateServer称为主UpdateServer。RootServer通过租约(Lease)机制实现UpdateServer选主。
每个集群一般部署一主一备两台RootServer,主备之间数据强同步,即所有的操作都需要首先同步到备机,接着修改主机,最后才能返回操作成功。
RootServer主备之间需要同步的数据包括:RootTable中记录的子表分布信息、ChunkServerManager中记录的ChunkServer机器变化信息以及UpdateServer机器信息。子表复制、负载均衡、合并、分裂以及ChunkServer/UpdateServer上下线等操作都会引起RootServer内部数据变化,这些变化都将以操作日志的形式同步到备RootServer。备RootServer实现回放这些操作日志,从而与主RootServer保持同步。
UpdateServer用于存储增量数据,他是一个单机存储系统,由如下几个部分组成:
UpdateServer是OceanBase性能瓶颈点,核心是高效,实现时对锁(例如、无锁数据结构)、索引结构、内存占用、任务处理模型以及主备同步都需要做专门的优化。
UpdateServer存储引擎与Bigtable存储引擎看起来很相似,不同点在于:
UpdateServer存储引擎包含几个部分:操作日志、MemTable以及SSTable。更新操作首先记录到操作日志中,接着更新内存中活跃的MemTable(Active MemTable),活跃的MemTable到达一定大小后将被冻结,称为Frozen MemTable,同时创建新的Active MemTable。Frozen MemTable将以SSTable文件的形式转储到SSD磁盘中。
任务模型包括网络框架、任务队列、工作线程。
正常情况下,备UpdateServer的日志回放线程会从全局日志缓冲区中读取操作日志,在内存中回放并同时将操作日志刷到备机的日志文件中。如果发生异常,比如备UpdateServer刚启动或者主备之间网络刚恢复,全局日志缓冲区中没有日志或者日志不连续,此时,备UpdateServer需要主动请求主UpdateServer拉取操作日志。主UpdateServer首先查找日志缓冲区,如果缓冲区没有数据,还需要读取磁盘日志文件,并将操作日志回复备UpdateServer。
ChunkServer用于存储基线数据,它由如下基本部分组成:
每台ChunkServer服务着几千到几万个子表的基线数据,每个子表由若干个SSTable组成(一般为1个)。
每台ChunkServer服务于多个子表,子表的个数一般在10000~100000之间。ChunkServer内部通过ObMultiVersionTabletImage来存储每个子表的索引信息,包括数据行数(row_count),数据量(occupy_size),校验和(check_sum),包含的SSTable列表,所在磁盘编号(disk_no)等。
ChunkServer维护了多个版本的子表数据,每日合并后升级子表的版本号,如果子表发生分裂,每日合并后将一个子表变成多个子表。
SSTable中的数据按主键排序后存放在连续的数据块(Block)中,Block之间也有序。接着,存放数据块索引(Block Index),由每个Block最后一行的主键(End Key)组成,用于数据查询中的Block定位。接着,存放布隆过滤器(Bloom Filter)和表格的Schema信息。最后,存放固定大小的Trailer以及Trailer的偏移位置。
SSTable分为两种格式:稀疏格式以及稠密格式。ChunkServer中的SSTable为稠密格式,而UpdateServer中的SSTable为稀疏格式,且存储了多张表格的数据。另外,SSTable支持压缩功能,压缩以Block为单位。每个Block写入磁盘之前调用压缩算法执行压缩,读取时需要解压缩。
OceanBase读取的数据可能来源于MemTable,也可能来源于SSTable,或者是合并多个MemTable和多个SSTable生成的结果。无论底层数据来源如何变化,上层的读取接口总是ObIterator。
ChunkServer中包含三种缓存:块缓存(Block Cache)、行缓存(Row Cache)以及块索引缓存(Block Index Cache)。一般来说,块索引不会太大,ChunkServer中所有SSTable的块索引都是常驻内存的。不同缓存的底层采用相同的实现方式。
OceanBase没有使用操作系统本身的页面缓存(page cache)机制,而是自己实现缓存。相应地,IO也采用Direct IO实现,并且支持磁盘IO与CPU计算并行化。
ChunkServer采用Linux的Libaio实现异步IO,并通过双缓冲机制实现磁盘预读与CPU处理并行化。
双缓冲区广泛用于生产者/消费者模型,ChunkServer中使用了双缓冲区异步预读的技术,生产者为磁盘,消费者为CPU,磁盘中生产的原始数据需要给CPU计算消费掉。为了做到不冲突,给每个缓存区分配一把互斥锁(简称La和Lb)。生产者或者消费者如果要操作某个缓冲区,必须先拥有对应的互斥锁。
缓冲区包括如下几种状态:
RootServer将UpdateServer上的版本变化信息通知ChunkServer后,ChunkServer将执行定期合并或者数据分发。
如果UpdateServer执行了大版本冻结,ChunkServer将执行定期合并。
如果UpdateServer执行了小版本冻结,ChunkServer将执行数据分发。与定期合并不同的是,数据分发只是将UpdateServer冻结的数据缓存到ChunkServer,并不会生成新的SSTable文件。因此,数据分发对ChunkServer造成的压力不大。
定期合并期间系统的压力较大,需要控制定期合并的速度,避免影响正常服务。定期合并限速的措施如下:
OceanBase团队持续不断地性能优化以及旁路导入功能的开发,单点的架构经受住了考验。但是OceanBase系统设计时已经留好了“后门”,以后可以通过对系统打补丁的方式支持UpdateServer线性扩展。
OceanBase UpdateServer相当于一个内存数据库。
对于定期导入大批数据,对导入性能要求很高。为此,OceanBase专门开发了旁路导入功能,直接将数据导入到ChunkServer中(即ChunkServer旁路导入)。
OceanBase的数据按照全局有序排列,因此,旁路导入的第一步就是使用Hadoop MapReduce这样的工具将所有的数据排好序,并且划分为一个个有序的范围,每个范围对应一个SSTable文件。接着,再将SSTable文件并行拷贝到集群中所有的ChunkServer中。最后,通知RootServer要求每个ChunkServer并行加载这些SSTable。每个SSTable文件对应ChunkServer的一个子表,ChunkServer加载完本地的SSTable文件后会向RootServer汇报,RootServer接着将汇报的子表信息更新到RootTable中。
OceanBase可以借鉴关系数据库中的分区表的概念,将数据划分为多个分区,允许不同的分区被不同的UpdateServer服务。
数据库功能层构建在分布式存储引擎层之上,实现完整的关系数据库功能。关系数据库系统中优化器是最为复杂的,这个问题困扰了关系数据库几十年。
用户可以通过兼容MySQL协议的客户端、JDBC/ODBC等方式请求发送给某一台MergeServer,MergeServer的MySQL协议模块将解析出其中的SQL语句,并交给MS—SQL模块进行词法分析(采用GUN Flex实现)、语法分析(采用GUN Bison实现)、预处理、并生成逻辑执行计划和物理执行计划。
只读事务(SELECT语句),经过词法分析、语法分析,预处理后,转化为逻辑查询计划和物理查询计划。逻辑查询计划的改进以及物理查询计划的选择,即查询优化器,是关系数据库最难的部分。
所有的物理运算符构成一个树,每个物理运算的输出结果都可以认为是一个临时的二维表,树中孩子节点的输出总是作为它的父亲节点的输入。
SQL最终执行时,只需要迭代root_op(即limit_op)也能够把需要的数据依次迭代出来。limit_op发现前一批数据迭代完成则驱动下层的project_op获取下一批数据,project_op发现前一批数据迭代完成则驱动下层的sort_op获取下一批数据。以此类推,直到最底层的table_scan_op不断地从原始表t1中读取数据。
单表相关的物理运算符包括:
GroupBy、Distinct物理操作符可以通过基于排序的算法实现,也可以通过基于哈希的算法实现,分别对应HashGroupBy和MergeGroupBy,以及HashDistinct和MergeDistinct。
多表相关的物理操作符主要是Join。最为常见的Join类型包括两种:内连接(Inner Join)和左外连接(Left Outer Join),而且基本都是等值连接。两张表实现等值连接方式主要分为两类:基于排序的算法(MergeJoin)以及基于哈希的算法(HashJoin)。
子查询分为两种:关联子查询和非关联子查询,其中比较常用的是使用IN子句的非关联子查询。IN子查询转化为常量表达式后,MergeServer执行SQL计算时,可以将IN后面的常量列表发送给ChunkServer,ChunkServer只返回category_id在常量列表中的商品记录,而不是将所有的记录返回给MergeServer过滤,从而减少二者之间传输的数据量。
多表操作由MergeServer执行,对于单表操作,OceanBase涉及的基本原则是尽量支持SQL计算本地化,保持数据节点与计算节点一致,也就是说,只要ChunkServer能够实现的操作,原则上都应该有它来完成。
当然,如果能够确定请求的数据全部属于同一个子表,那么,所有的物理运算符都可以由ChunkServer执行,MergeServer只需要将ChunkServer计算得到的结果转发给客户端。
写事务,包括更新(UPDATE)、插入(INSERT)、删除(DELETE)、替换(REPLACE,插入或者更新,如果行不存在则插入新行;否则,更新已有行),由MergeServer解析后生成物理执行计划,这个物理执行计划最终将发给UpdateServer执行。写事务可能需要读取基线数据,用于判断更新或者插入的数据行是否存在,判断某个条件是否满足,等等,这些基线数据也会由MergeServer传给UpdateServer。
大部分写事务都是针对单行的操作,如果单行事务不带其他条件:
OceanBase的MemTable包含两个部分:索引结构及行操作链。其中,索引结构存储行头信息,采用内存B树实现;行操作链表中存储了不同版本的修改操作,从而支持多版本控制。
MemTable行操作链表包含两个部分:已提交部分和未提交部分。
每个写事务会根据提交时的系统事件生成一个事务版本,读事务只会读取在它之前提交的写事务的修改操作。
OLAP业务的特点是SQL每次执行涉及的数据量很大,需要一次性分析几百万行甚至几千万行的数据。另外,SQL执行时往往只读取每行的部分列而不是整行数据。
MergeServer将大请求拆分为多个子请求,同时发往每个子请求所在的ChunkServer并发执行,每个ChunkServer执行子请求并将部分结果返回给MergeServer。MergeServer合并ChunkServer返回的部分结果并将最终结果返回给客户端。
列式存储主要的目的有两个:1)大部分OLAP查询只需要读取部分列而不是全部列数据,列式存储可以避免读取无用数据;2)将同一列的数据在物理上存放在一起,能够极大地提高数据压缩率。
列组(Column Group)
OceanBase通过列组支持行列混合存储,每个列组存储多个经常一起访问的列。OceanBase SSTable首先按照列组存储,每个列组内部再按行存储。分为几种情况:
OceanBase还允许一个列属于多个列,通过冗余存储这些列,能够提高访问性能。
大表左连接需求来源于淘宝收藏夹业务。关系数据库多表连接操作和冗余都是不能够被接受的。
这个问题本质上是一个大表左连接(Left Join)的问题,连接列为item_id,即右表(商品表)的主键。对于这个问题,OceanBase的做法是在collect_info的基线数据中冗余collect_item信息,修改增量中将collect_info和collect_item两张表格分开存储。商品价格、人气变化只需要记录在UpdateServer的修改增量中。
OceanBase的实现方式得益于每天业务低峰期进行的每日合并操作。每日合并时,ChunkServer会将UpdateServer上collect_info和collect_item表格中的修改增量融合到collect_info表格的基线数据中,生成新的基线数据。因此,collect_info和collect_item的数据量不至于太大,从而能够存放到单机机器的内存中提供高效查询服务。
很多业务只需要存储一段时间,比如三个月或者半年的数据,更早之前的数据可以被丢弃或者转移到历史库从而节省存储成本。
OceanBase线上每个表格都包含创建时间(gmt_create)和修改时间(gmt_modified)列。使用者可以设置自动过期规则,比如只保留创建时间或修改时间不晚于某个时间点的数据行,读取操作会根据规则过滤这些失效的数据行,每日合并时这些数据行会被物理删除。
OceanBase系统一直在不断演化,需要在代码不断变化的过程中保持系统的稳定性。
一个新版本需要经过开发 =》单元测试 & 快速测试 =》RD(开发工程师)压力测试 =》系统提测 =》QA(测试工程师)接口、功能、容灾、压力测试 =》兼容性测试 =》Benchmark测试才能最终发布,其中,RD压力测试和兼容性测试是可选的。发布的新版本还需要经过业务压力测试或者线上流量回放才能上线试运行,试运行一段时间后没有发现问题才能最终上线。
系统Bug暴露越早修复代价越低,开发工程师是产生Bug的源头,开发阶段主要通过编码规范、代码审核(Code Review)、单元测试保证代码质量。
RD提测新版本后,进入QA测试阶段。
OceanBase不是设计出来的,而是在使用过程中不断进化出来的。
OceanBase内部实现了系统表机制,用于存储监控以及运维相关的信息。内部系统表包含的内容如下:
虽然OceanBase同时支持OLTP以及OLAP应用,但是OceanBase具有一定的适用场景。如果应用总数据量小于200TB,每天更新的数据量小于1TB,且读写压力较大,单台关系数据库无法支撑,那么,适合采用OceanBase。对于这种应用,OceanBase具有如下优势:
当然,OceanBase并不是万能的。例如,OceanBase不适合存储图片、视频等非结构化数据,也不适合存储业务原始日志。这些信息更适合存储在专门的分布式文件系统,比如Taobao File System、HDFS中。
收藏夹属于典型的OLTP业务,主要功能如下:
天猫评价也属于典型的OLTP应用,主要功能如下:
和传统数据库方案相比,OceanBase的优势主要体现在两个方面:
直通车报表是典型的OLAP报表,包含如下几个方面:
分布式存储系统从整体架构的角度看大同小异,实现起来却困难重重。
通用分布式存储系统不是设计出来的,而是随着应用需求不断发展起来的。他来源于具体业务,又具有一定的通用性,能够解决一大类问题。通用分布式存储平台的优势在于规模效应,等到平台的规模超过某个平衡点时,成本优势将会显现。
稳定性和性能并不是分布式存储系统的全部,一个好的系统还必须具备较好的可用性和可运维性。
云存储是云计算的存储部分,云计算后端架构的难点集中在云存储。
云存储目前为止并没有一个明确的定义。云存储是通过网络将大量普通存储设备构成的存储资源池中的存储和数据服务以及统一的接口按需提供给授权用户。
可以看出,云存储技术的核心在于分布式存储。云存储是传统存储技术在大数据时代自然演进的结果,相比传统存储,云存储具有如下优势:
Amazon推出的针对企业的S3简单存储服务(Amazon Simple Storage Service),它是Amazon云计算平台(Amazon Web Service,AWS)的一种对象存储服务,用于存储照片、图片、视频、音乐等个人文件。
云存储包括两个部分:云端+终端。
作为云计算的存储部分,云存储的核心优势与云计算相同。主要包括两个方面:最大程度地节省成本以及加快创新速度。
综上所述,由于云存储更低的硬件成本和网络成本,更低管理成本和电力成本,以及更高的资源利用率,Google,Amazon,Microsoft等互联网巨头能够通过从数据中心开始构建整套公有云存储解决方案达到节省30倍以上成本的目的。
云存储是云计算的存储部分,理解云存储架构的前提是理解云平台整体架构。云计算按照服务类型大致可以分为三类:基础设施即服务(IaaS)、平台即服务(PaaS)以及软件即服务(SaaS)。
Amazon Web Services(AWS)是Amazon构建的一个云计算平台的总称,它提供了一系列云服务。通过这些服务,用户能够访问和使用Amazon的存储和计算基础设施。AWS平台分为如下几个部分:
AWS平台引入了区域(Zone)的概念。区域分为两种:地理区域(Region Zone)和可用区域(Availability Zone),其中地理区域是按照实际的地理位置划分的,而可用区域一般是按照数据中心划分的。
Google云平台(Google App Engine,GAE)是一种PaaS服务,使得外部开发者可以通过Google期望的方式使用它的基础设施服务,目前支持Python和Java两种语言。
GAE云平台主要包含如下几个部分:
另外,作为PaaS服务,GAE还提供了如下两种工具:
GAE的核心组件为应用服务器以及存储区。
Windows Azure 平台包含如下几个部分:
从托管Web应用程序的角度看,云平台主要包括云存储以及应用运行平台。
云平台的核心组件包括:云存储组件和应用运行平台组件。
云存储平台还包含一些公共服务,这些基础服务由云存储组件及运行平台组件所共用。
云存储技术体系结构分为四层:硬件层、单机存储层、分布式存储层、存储访问层。
用户的应用程序可能会托管在应用运行平台中,应用场景大致分为三类:
在技术层面,云存储安全包括如下几个方面:
总而言之,安全是云存储的核心问题,但不必对此过分担心,前途是光明的,道路是曲折的,随着专业的云存储服务提供商对系统不断的优化,云存储比现有的IT模式反而会更加安全。
MapReduce并不是大数据的全部。虽然MapReduce解决了海量数据离线分析问题,但是,随着应用对数据的实时性要求越来越高,流式计算系统和实时分析系统得到越来越广泛的应用。
简而言之,从各种各样类型的数据,包括非结构化数据、半结构化数据以及结构化数据中,快速获取有价值信息的能力,就是大数据技术。
大数据的特点可以用四个V来描述:
互联网技术归根结底就是云计算和大数据技术,云计算提供海量数据的存储和计算能力,并最大程度地降低分布式处理的成本,大数据技术进一步从海量数据中抽取数据的价值,从而诞生Google搜索引擎、Amazon商品推荐系统这样的杀手级应用,形成一条大数据采集、处理、反馈的数据处理闭环。
MapReduce使得普通程序员可以在不了解分布式底层细节的前提下开发分布式程序。使用者只需编写两个称为Map和Reduce的函数即可,MapReduce框架会自动处理数据划分、多机并行执行、任务之间的协调,并且能够处理某个任务执行失败或者机器出现故障的情况。
MapReduce框架包含三种角色:主控进程(Master)用于执行任务划分、调度、任务之间的协调等;Map工作进程(Map Worker,简称Map进程)以及Reduce工作进程(Reduce Worker,简称Reduce进程)分别用于执行Map任务和Reduce任务。
MapReduce框架实现时主要做了两点优化:
Google Tenzing是一个构建在MapReduce之上的SQL执行引擎,支持SQL查询且能够扩展到成千上万台机器,极大地方便了数据分析人员。
Microsoft Dryad是微软研究院创建的研究项目,主要用来提供一个分布式并行计算平台。Dryad的主控进程(Job Manager)负责将整个工作分割成多个任务,并分发给多个节点执行。每个节点执行完任务通知主控进程,接着,主控进程会通知后续节点获取前一个结点的输出结果。等到后续节点的输出数据全部准备好后,才可以继续执行后续任务。
Dryad与MapReduce具有的共有特性就是,只有任务完成之后才会输出传递给接收任务。如果某个任务失败,其结果将不会传递给它在工作流中的任何后续任务。因此,主控进程可以在其他计算节点上重启该任务,同时不用担心会将结果重复传递给以前传递过的任务。
相比多个MapReduce作业串联模型,Dryad模型的优势在于不需要将每个MapReduce作业输出的临时结果存放在分布式文件系统中。如果先存储前一个MapReduce作业的结果,然后再启动新的MapReduce作业,那么,这种开销很难避免。
Google Pregel用于图模型迭代计算。Pregel采用了BSP(Bulk Sychronous Parallel)模型。每个“超步”分为三个步骤:每个节点首先执行本地计算,接着将本地计算的结果发送给图中相邻的节点,最后执行一次栅栏同步,等待所有节点的前两步操作结束。Pregel模型会在每个超步做一次数迭代计算,当某次迭代生成的结果没有比上一次更好,说明结果已经收敛,可以终止迭代。
Pregel通过检查点(checkpoint)的方式进行容错处理。它在每执行完一个超步之后会记录整个计算的现场,即记录检查点情况。检查点中记录了这一轮迭代中每个任务的全部状态信息,一旦后续某个计算节点失效,Pregel将从最近的检查点重启整个超步。尽管上述的容错策略会重做很多并未失效的任务,但是实现简单。考虑到服务器故障的概率不高,这种方法在大多数时候还是令人满意的。
流式计算(Stream Processing)解决在线聚合(Online Aggregation)、在线过滤(Online Filter)等问题,流式计算同时具有存储系统和计算系统的特点,经常应用在一些类似反作弊、交易异常监控等场景。流式计算的操作算子和时间相关,处理最近一段时间窗口内的数据。
流式计算强调的是数据流的实时性。MapReduce系统主要解决的是对静态数据的批量处理,当MapReduce作业启动时,已经准备好了输入数据,比如保存在分布式文件系统上。而流式计算系统在启动时,输入数据一般并没有完全到位,而是经由外部数据流源源不断地流入。另外,流式计算并不像批处理系统那样,重视数据处理的总吞吐量,而是更加重视对数据处理的延迟。
典型钩子函数包括:
Yahoo S4最初是Yahoo为了提高搜索广告有效点击率而开发的一个流式处理系统。S4的主要设计目标是提供一种简单的编程接口来处理数据流,使得用户可以定制流式计算的操作算子。
S4中每个处理节点称为一个处理节点(Processing Node,PN),其主要工作是监听事件,当事件到达调用合适的处理元(Processing Elements,PE)处理事件。如果PE有输出,则还需调用通信层接口进行事件的分发和输出。
通信层提供集群路由(Routing)、负载均衡(Load Balancing)、故障恢复管理(Failover Management)、逻辑节点到物理节点的映射(存放在Zookeeper上)。当检测到节点故障时,会切换到备用节点,并自动更新映射关系。通信层隐藏的映射使得PN发送消息时只需要关心逻辑点而不用关心物理节点。
Twitter Storm是目前广泛使用的流式计算系统,它创造性地引入了一种记录级容错的方法。
Storm中有一个系统级组件,叫做acker。这个acker的任务就是追踪从spout中流出来的每一个message绑定的若干tuple的处理路径。
实时分为两种情况:如果查询模式单一,那么,可以通过MapReduce预处理后将最终结果导入到在线系统提供实时查询;如果查询模式复杂,例如设计到多个列热议组合查询,那么,只能通过实时分析系统解决。实时分析系统融合了并行数据库和云计算这两类技术,能够从海量数据中快速分析出汇总结果。
并行数据库往往采用MPP(Massively Parallel Processing,大规模并行处理)架构。MPP架构是一种不共享的结构,每个节点可以运行自己的操作系统、数据库等。每个节点内的CPU不能访问另一个节点的内存,节点之间的信息交互是通过节点互联网络实现的。
将数据分布到多个节点,每个节点扫描本地数据,并由Merge操作符执行结果汇总。
常见的数据分布算法有两种:
Greenplum是EMC公司研发的一款采用MPP架构的OLAP产品,底层基于开源的PostgreSQL数据库。
Vertica是Michael Stonebraker的学术研究项目C-Store的商业版本,并最终被惠普公司收购。Vetica在架构上与OceanBase由相似之处。
Google Dremel是Google的实时分析系统,可以扩展到上千台及其规模,处理PB级别的数据。