OceanBase——双十一海量交易背后的复杂技术

前言:

其实这篇博客我早就想完成它了,计划着在十一月初就搞定的,可是双十一活动太多,前几天一直在淘宝和天猫看要买的东西。这篇博客也是断断续续的写着,到今天才算是把它整理出来了。这篇博文主要介绍了OceanBase的系统框架,以及框架内各个成员的功能。主要是学习它的框架,以及分析问题、解决问题的方法。以下内容需要掌握基本的分布式系统相关知识,最好是阅读过分布式文件系统或者分布式数据库等相关书籍。以下内容源自于《大规模分布式存储系统》。这本书对我的帮助很大,首先它的作者是阿里巴巴的高级技术专家。其次是这本书的内容吸引了我。双十一是一个巨大的挑战,在这样海量交易的背后隐藏着的复杂技术,难道你不感兴趣吗?如果对分布式相关知识比较感兴趣的话,我建议可以先看看其基本概念,这类书籍有很多,如果是想系统的学习这方面的知识,我推荐《分布式系统概念与设计》。在了解相关知识后,也可以看看Google比较有名的论文,例如GFS,BigTable和MapReduce等,可以在我的专栏分类——分布式/大数据中找到这些论文的中文翻译。本文主要介绍的是分布式存储系统,那么就让我们先来了解下分布式存储系统的分类。

 

分布式存储系统的数据可以分为以下三类:

(1)结构化数据一般存储在关系数据库中,可以用二维关系表结构来表示。结构化数据的模式和内容是分开的,数据模式需要预先定义。 国内开源的分布式数据库——阿里巴巴的OceanBase

(2)非结构化数据:包括所有格式的办公文档、文本、图片、音频和视频信息等。这类数据以对象的形式组织,对象之间没有关联,这样的数据一般称为Blob(Binary Large Object)数据。分布式文件系统用于存储Blob数据,典型的系统有Fackbook Haystack、GFS以及HDFS。

(3)半结构化数据:介于非结构化数据和结构化数据之间,HTML文档就属于半结构化数据。它的模式结构和内容混在一起,没有明显的区分,也不需要预先定义数据的模式结构。典型的系统是Google Bigtable

 

背景简介

OceanBase是阿里集团研发的可扩展的关系数据库,实现了数千亿条记录、数百TB数据上的跨行跨表事务,截止到2012年8月,支持了收藏夹、直通车报表、天猫评价等OLTP和OLAP在线业务,线上数据量已经超过了一千亿条。

  • OLTP(Online Transaction Processing): 联机事务处理,针对小数据进行增删改。如银行转账。
  • OLAP(Online Analytic Processing) :联机分析处理,针对大数据的分布式处理,通常为select(分析)。

从模块划分角度看,OceanBase可以划分为四个模块:主控服务器、更新服务器、基线服务器和合并服务器。OceanBase系统内部按照时间线将数据划分为基线数据和增量数据,基线数据是只读的,所有的修改更新到增量数据中,系统内部通过合并操作定期将增量数据融合到基线数据中。

淘宝是一个迅速发展的网站。它的数据规模及其访问量对关系数据库提出了很大挑战:数百亿条的记录、数十TB的数据、数万TPS、数十万QPS让传统的关系数据库不堪重负,单纯的硬件升级已经无法使问题得到解决,分库分表也并不总是奏效的。下面有一个实际例子。注意:以上数据量时2012年左右的规模。

淘宝收藏夹是淘宝线上应用之一,淘宝用户在其中保存自己感兴趣的商品,以便下次快速访问、对比和购买等,用户可以展示和编辑自己的收藏。淘宝收藏夹数据库包含了收藏info表(一条一条的收藏信息)和收藏item表(被收藏的宝贝和店铺)等:

  • 收藏info表保存收藏信息条目,数百亿条。
  • 收藏item表保存收藏的商品和店铺的详细信息,数十亿条。
  • 热门商品可能被多达数十万买家收藏。
  • 每个用户可以收藏千个商品。
  • 商品的价格、收藏人气等信息随时变化。

 

如果用户选择按商品价格排序后展示,那么数据库需要从收藏item表中读取收藏的商品的价格等最新信息,然后进行排序处理。如果用户的收藏条目比较多,那么查询对应的item的时间会较长:假设如果每条item查询时间是5ms,则4000条的查询时间可能达到20s,如果真的是这样,用户体验会很差。

若把收藏夹的商品的详细信息实时冗余到收藏info表,则上述查询收藏item表的操作就不再需要了。但是,由于许多热门商品可能有几千到几十万人收藏,那些热门商品的价格等信息的变动可能导致收藏info表的大量修改,并压垮数据库。为此,阿里巴巴需要研发适合互联网规模的分布式数据库,这个数据库不仅要能解决收藏夹面临的挑战,还要能做到可扩展、低成本、易用,并能够应用到更多的业务场景。

 

设计思路

OceanBase的目标是支持数百TB的数据量以及数十万TPS、数百万QPS的访问量,无论是数据量还是访问量,即使采用非常昂贵的小型机甚至是大型机,单台关系数据库系统都无法承受。

一种常见的做法是根据业务特点对数据库进行水平拆分,通常的做法是根据某个业务字段(通常取用户编号,user_id)哈希后取模,根据取模的结果将数据分布到不同的数据库服务器上,客户端请求通过数据库中间层路由到不同的分区。这种方式目前还存在一定的弊端:

(1)数据和负载增加后添加机器的操作比较复杂,往往需要人工介入;

(2)有些范围查询需要访问几乎所有的分区,例如,按照user_id分区,查询收藏了一个商品的所有用户可能需要访问所有的分区;

(3)目前广泛使用的关系数据库存储引擎都是针对机械硬盘的特点设计的,不能够完全发挥新硬件(SSD)的能力。

关于(2),需要详细的说明一下。若使用user_id哈希后取模,根据取模的结果将数据分布到不同的数据库服务器上。这样用户在查看收藏夹里的商品时,可以请求相应数据库服务器,从而获取到数据。但是,一旦有个商品的价格发生了变动,那么收藏了该商品的用户的数据也要发生改变。试想一个商品价格的改动,需要修改所有收藏该商品的用户的收藏夹信息,这个操作可能是非常简单的,但是考虑到淘宝商品种类之多,用户量之大,该问题就变的异常费时且困难。而且到了双十一这样的特殊时刻,这样的操作复杂程度也会随之上升。

另外一种做法是参考分布式表格系统的做法,例如Google Bigtable系统,将大表划分为几万、几十万甚至几百万个子表,子表间按照主键有序,如果某台服务器发生故障,它上面服务的数据能够在很短的时间内自动迁移到集群中的其他服务器。这种方式解决了可扩展性的问题,少量突发的服务器故障或者增加服务器对使用者基本是透明的,能够轻松应对促销或者热点事件等突发流量增长。另外,由于子表是按照主键有序分布的,很好地解决了范围查询的问题。

分布式表格系统虽然解决了可扩展性问题,但往往无法支持事务,例如Bigtable只支持单行事务,针对同一个user_id下的多条记录的操作都无法保证原子性。而OceanBase希望能够支持跨行跨表事务,这样使用起来会比较方便。

最直接的做法是在Bigtable开源实现(如HBase或者Hypertable)的基础上引入两阶段提交协议支持分布式事务,这种思路在Google的Percolator系统中得到了体现。然而,Percolator系统中事务的平均响应时间达到2~5秒,只能应用在类似网页建库这样的半线上业务中。另外,Bigtable的开源实现也不够成熟,单台服务器能够支持的数据量有限,单个请求的最大响应时间很难得到保证,机器故障等异常处理机制也有很多比较严重的问题。总体上看,这种做法的工作量和难度超出了项目的承受能力。

通过分析,开发人员发现,虽然在线业务的数据量十分庞大,例如几十亿条、上百亿条甚至更多记录,但最近一段时间(例如一天)的修改量往往并不多,通常不超过几千万条到几亿条,因此,OceanBase决定采用单台更新服务器来记录最近一段时间的修改增量,而以前的数据保持不变,以前的数据称为基线数据。基线数据以类似分布式文件系统的方式存储于多台基线数据服务器中,每次查询都需要把基线数据和增量数据融合后返回给客户端。这样,写事务都集中在单台更新服务器上,避免了复杂的分布式事务,高效地实现了跨行跨表事务;另外,更新服务器上的修改增量能够定期分发到多台基线数据服务器中,避免成为瓶颈,实现了良好的扩展性。

 

整体架构图

OceanBase由以下几个部分组成:

(1)客户端:用户使用OceanBase的方式和MySQL数据库完全相同,支持JDBC、C客户端访问,等等。基于MySQL数据库开发的应用程序、工具能够直接迁移到OceanBase

(2)RootServer:管理集群中的所有服务器,子表数据分布以及副本管理。RootServer一般为一主一备,主备之间数据强同步。

(3)UpdateServer:存储OceanBase系统的增量更新数据。UpdateServer一般为一主一备,主备之间可以配置不同的同步模式。部署时,UpdateServer进程和RootServer进程往往共用物理服务器。

(4)ChunkServer:存储OceanBase系统的基线数据。基线数据一般存储两份或者三份,可配置。

(5)MergeServer:接收并解析用户的SQL请求,经过词法分析、语法分析、查询优化等一系列操作后转发给相应的ChunkServer或者UpdateServer。如果请求的数据分布在多台ChunkServer上,MergeServer还需要对多台ChunkServer返回的结果进行合并。客户端和MergeServer之间采用原生的MySQL通信协议,MySQL客户端可以直接访问MergeServer。

OceanBase支持部署多个机房,每个机房部署一个包含RootServer、MergeServer、ChunkServer以及UpdateServer的完整OceanBase集群,每个集群由各自的RootServer负责数据划分、负载均衡、集群服务器管理等操作,集群之间数据同步通过主集群的主UpdateServer往备集群同步增量更新操作日志实现。客户端配置了多个集群的RootServer地址列表,使用者可以设置每个集群的流量分配比例,客户端根据这个比例将读写操作发往不同的集群。

 

客户端

OceanBase客户端与MergeServer通信,目前主要支持以下几种客户端:

MySQL客户端:MergeServer兼容MySQL协议,MySQL客户端及相关工具(如Java数据库访问方式JDBC)只需要将服务器的地址设置为任意一台MergeServer的地址,就可以直接使用。

Java客户端:OceanBase内部部署了多台MergeServer,Java客户端提供对MySQL标准JDBC Driver的封装,并提供流量分配、负载均衡、MergeServer异常处理等功能。简单来讲,Java客户端首先按照一定的策略定位到某台MergeServer,接着调用MySQL JDBC Driver往这台MergeServer发送读写请求。Java客户端实现符合JDBC标准,能够支持Spring、iBatis等Java编程框架。

C客户端:OceanBase C客户端的功能和Java客户端类似。它首先按照一定的策略定位到某台MergeServer,接着调用MySQL标准C客户端往这台MergeServer发送读写请求。C客户端的接口和MySQL标准C客户端接口完全相同,因此,能够通过LD_PRELOAD的方式将应用程序依赖的MySQL标准C客户端替换为OceanBase C客户端,而无需修改应用程序的代码。

OceanBase集群有多台MergeServer,这些MergeServer的服务器地址存储在OceanBase服务器端的系统表(与Oracle的系统表类似,存储OceanBase系统的元数据)内。OceanBase Java/C客户端首先请求服务器端获取MergeServer地址列表,接着按照一定的策略将读写请求发送给某台MergeServer,并负责对出现故障的MergeServer进行容错处理。

Java/C客户端访问OceanBase的流程大致如下:

(1)请求RootServer获取集群中MergeServer的地址列表。

(2)按照一定的策略选择某台MergeServer发送读写请求。客户端与MergeServer之间的通信协议兼容原生的MySQL协议,因此,只需要调用MySQL JDBC Driver或者MySQL C客户端这样的标准库即可。客户端支持的策略主要有两种:随机以及一致性哈希。一致性哈希的目的是将相同的SQL请求发送到同一台MergeServer,方便MergeServer对查询结果进行缓存。

(3)如果请求MergeServer失败,则从MergeServer列表中重新选择一台MergeServer重试;如果请求某台MergeServer失败超过一定的次数,将这台MergeServer加入黑名单并从MergeServer列表中删除。另外,客户端会定期请求RootServer更新MergeServer地址列表。

 

RootServer

RootServer的功能主要包括:集群管理、数据分布以及副本管理。

RootServer管理集群中的所有MergeServer、ChunkServer以及UpdateServer。每个集群内部同一时刻只允许一个UpdateServer提供写服务,这个UpdateServer称为主UpdateServer。这种方式通过牺牲一定的可用性获取了强一致性。RootServer通过租约机制选择唯一的主UpdateServer,当原先的主UpdateServer发生故障后,RootServer能够在原先的租约失效后选择一台新的UpdateServer作为主UpdateServer。另外,RootServer与MergeServer & ChunkServer之间保持心跳,从而能够感知到在线和已经下线的MergeServer & ChunkServer机器列表。

OceanBase内部使用主键对表格中的数据进行排序和存储,主键由若干列组成并且具有唯一性。在OceanBase内部,基线数据按照主键排序并且划分为数据量大致相等的数据范围,称为子表。每个子表的默认大小是256MB(可配置)。OceanBase的数据分布方式与Bigtable一样采用顺序分布,不同的是,OceanBase没有采用根表(RootTable)+元数据表(MetaTable)两级索引结构,而是采用根表一级索引结构。

如图所示,主键在[1,100]之间的表格被划分为四个子表。RootServer中的根表记录了每个子表所在的ChunkServer位置信息,每个子表包含多个副本(一般为三个副本,可配置),分布在多台ChunkServer中。当其中某台ChunkServer发生故障时,RootServer能够检测到,并且触发对这台ChunkServer上的子表增加副本的操作;另外,RootServer也会定期执行负载均衡,选择某些子表从负载较高的机器迁移到负载较低的机器上。

RootServer采用一主一备的结构,主备之间数据强同步,并且通过Linux HA(http://www.linux-ha.org)软件实现高可用性。主备RootServer之间共享VIP,当主RootServer发生故障后,VIP能够自动漂移到备RootServer所在的机器,备RootServer检测到以后切换为主RootServer提供服务。

 

MergeServer

MergeServer的功能主要包括:协议解析、SQL解析、请求转发、结果合并、多表操作等。

OceanBase客户端与MergeServer之间的协议为MySQL协议。MergeServer首先解析MySQL协议,从中提取出用户发送的SQL语句,接着进行词法分析和语法分析,生成SQL语句的逻辑查询计划和物理查询计划,最后根据物理查询计划调用OceanBase内部的各种操作符。

MergeServer缓存了子表分布信息,根据请求涉及的子表将请求转发给该子表所在的ChunkServer。如果是写操作,还会转发给UpdateServer。某些请求需要跨多个子表,此时MergeServer会将请求拆分后发送给多台ChunkServer,并合并这些ChunkServer返回的结果。如果请求涉及多个表格,MergeServer需要首先从ChunkServer获取每个表格的数据,接着再执行多表关联或者嵌套查询等操作。

MergeServer支持并发请求多台ChunkServer,即将多个请求发给多台ChunkServer,再一次性等待所有请求的应答。另外,在SQL执行过程中,如果某个子表所在的ChunkServer出现故障,MergeServer会将请求转发给该子表的其他副本所在的ChunkServer。这样,ChunkServer故障是不会影响用户查询的。

MergeServer本身是没有状态的,因此,MergeServer宕机不会对使用者产生影响,客户端对自动将发生故障的MergeServer屏蔽掉。

 

ChunkServer

ChunkServer的功能包括:存储多个子表,根据读取服务,执行定期合并以及数据分发。

OceanBase将大表划分为大小约为256MB的子表,每个子表由一个或者多个SSTable组成(一般为一个),每个SSTable有多个块(Block,大小为4KB~64KB之间,可配置)组成,数据在SSTable中按照主键有序存储。查找某一行数据时,需要首先定位这一行所属的子表,接着在相应的SSTable中执行二分查找。SSTable支持两种缓存模式,块缓存以及行缓存。块缓存以块为单位缓存最近读取的数据,行缓存以行为单位缓存最近读取的数据。

MergeServer将每个子表的读取请求发送到子表所在的ChunkServer,ChunkServer首先读取SSTable中包含的基线数据,接着请求UpdateServer获取相应的增量更新数据,并将基线数据与增量更新融合后得到最终结果。

由于每次读取都需要从UpdateServer中获取最新的增量更新,为了保证读取性能,需要限制UpdateServer中增量更新的数据量,最好能够全部存放在内存中。OceanBase内部会定期触发合并或者数据分发操作,在这个过程中,ChunkServer将从UpdateServer获取一段时间之前的更新操作。通常情况下,OceanBase集群会在每天的服务低峰期(凌晨1:00开始)执行一次合并操作。

 

UpdateServer

UpdateServer是集群中唯一能够接受写入的模块,每个集群中只有一个主UpdateServer。UpdateServer中的更新操作首先写入到内存表,当内存表的数据量超过一定值时,可以生成快照文件并转储到SSD中。快照文件的组织方式与ChunkServer中的SSTable类似,因此,这些快照文件也称为SSTable。另外,由于数据行的某些列被更新,某些列没被更新,SSTable中 存储的数据行是稀疏的,称为稀疏型SSTable。

为了保证可靠性,主UpdateServer更新内存表之前需要首先写操作日志,并同步到备UpdateServer。当主UpdateServer发生故障时,RootServer上维护的租约将失效,此时,RootServer将从备UpdateServer列表中选择一台最新的备UpdateServer切换为主UpdateServer继续提供写服务。UpdateServer宕机重启后需要首先加载转储的快照文件(SSTable文件),接着回放快照点之后的操作日志。

 

定期合并&数据分发

定期合并和数据分发都是将UpdateServer中的增量更新分发到ChunkServer中的手段,二者的整体流程比较类似:

(1)UpdateServer冻结当前的活跃内存表,生成冻结内存表,并开启新的活跃内存表,后续的更新操作都写入新的活跃内存表。

(2)UpdateServer通知RootServer数据版本发生了变化,之后RootServer通过心跳消息通知ChunkServer。

(3)每台ChunkServer启动定期合并或者数据分发操作,从UpdateServer获取每个子表对应的增量更新数据。

定期合并与数据分发两者之间的不同点在于,数据分发过程中ChunkServer只是将UpdateServer中冻结内存表中的增量更新数据缓存到本地,而定期合并过程中ChunkServer需要将本地SSTable中的基线数据与冻结内存表的增量更新数据执行一次多路归并,融合后生成新的基线数据并存到新的SSTable中。定期合并对系统服务能力影响很大,往往安排在每天服务低峰期执行,而数据分发可以不受限制。

如图,活跃的内存表冻结后生成冻结内存表,后续的写操作进入新的活跃内存表。定期合并过程中ChunkServer需要读取UpdateServer中冻结内存表的数据、融合后生成新的子表,即:新子表 = 旧子表 + 冻结内存表

虽然定期合并过程中各个ChunkServer的各个子表合并时间和完成时间可能都不相同,但并不影响读取服务。如果子表没有合并完成,那么使用旧子表,并且读取UpdateServer中的冻结内存表以及新的活跃内存表;否则,使用新子表,只读取新的活跃内存表,即:查询结果 = 旧子表 + 冻结内存表 + 新的活跃内存表 = 新子表 + 新的活跃内存表。

 

一致性选择

Eric Brewer教授的CAP理论指出,在满足分区可容忍性的前提下,一致性和可用性不可兼得。

虽然目前大量的互联网项目选择了弱一致性,但这是底层存储系统,比如MySQL数据库,在大数据量和高并发需求压力之下的无奈选择。弱一致性给应用带来了很多麻烦,比如数据不一致时需要人工订正数据。如果存储系统既能够满足大数据量和高并发的需求,又能够提供强一致性,且硬件成本相差不大,用户毫不犹豫地选择它。强一致性将大大简化数据库的管理,应用程序也会因此而简化。因此,OceanBase选择支持强一致性和跨行跨表事务。

OceanBase UpdateServer为主备高可用架构,修改操作流程如下:

(1)将修改操作的操作日志(redo日志)发送到备机;

(2)将修改操作的操作日志写入主机硬盘;

(3)将操作日志应用到主机的内存表中;

(4)返回客户端写入成功。

OceanBase要求将操作日志同步到主备的情况下才能够返回客户端写入成功,即使主机出现故障,备机自动切换为主机,也能够保证新的主机拥有以前所有的修改操作,严格保证数据不丢失。另外,为了提高可用性,OceanBase还增加了一种机制,如果主机往备机同步操作日志失败,比如备机故障或者主备之间网络故障,主机可以将备机从同步列表中剔除,本地更新成功后就返回客户端写入成功。主机将备机剔除前需要通知RootServer,后续如果主机故障,RootServer能够避免将不同步的备机切换为主机。

OceanBase的高可用机制保证主机、备机以及主备之间网络三者之中的任何一个出现故障都不会对用户产生影响,然而,如果三者之中的两个同时出现故障,系统可用性将受到影响,但仍然保证数据不丢失。如果应用对可用性要求特别高,可以增加备机数量,从而容忍多台机器同时出现故障的情况。

OceanBase主备同步也允许配置为异步模式,支持最终一致性。这种模式一般用来支持异地容灾。例如,用户请求通过杭州主站的机房提供服务,主站的UpdateServer内部有一个同步线程不停地将用户更新操作发送到青岛机房。如果杭州机房整体出现不可恢复的故障,比如地震,还能够通过青岛机房恢复数据并继续提供服务。另外,OceanBase所有写事务最终都落到了UpdateServer,而UpdateServer逻辑上是一个单点,支持跨行跨表事务,实现上借鉴了传统关系数据库的做法。

 

数据结构

OceanBase数据分为基线数据和增量数据两个部分,基线数据分布在多台ChunkServer上,增量数据全部存放在一台UpdateServer上。如图,系统中有5个子表,每个子表有3个副本,所有的子表分布到4台ChunkServer上。RootServer中维护了每个子表所在的ChunkServer的位置信息,UpdateServer存储了这5个子表的增量更新。

不考虑数据复制,基线数据的数据结构如下:

  • 每个表格按照主键组成一颗分布式B+树,主键由若干列组成;
  • 每个叶子节点包含表格一个前开后闭的主键范围(rk1,rk2]内的数据;
  • 每个叶子节点称为一个子表(tablet),包含一个或者多个SSTable;
  • 每个SSTable内部按主键范围有序划分为多个块并内建块索引;
  • 每个块的大小通常在4~64KB之间并内建块内的行索引;
  • 数据压缩以块为单位,压缩算法由用户指定并可随时变更;
  • 叶子节点可能合并或者分裂;
  • 所有叶子节点基本上是均匀的,随机分布在多台ChunkServer机器上;
  • 通常情况下每个叶子节点有2~3个副本;
  • 叶子节点是负载平衡和任务调度的基本单元;
  • 支持布隆过滤器的过滤。

 

增量数据的数据结构如下:

  • 增量数据按照时间从旧到新划分为多个版本;
  • 最新版本的数据为一颗内存中的B+树,称为活跃MemTable;
  • 用户的修改操作写入活跃MemTable,到达一定大小后,原有的活跃MemTable将被冻结,并开启新的活跃MemTable接受修改操作;
  • 冻结的MemTable将以SSTable的形式转储到SSD中持久化;
  • 每个SSTable内部按主键范围有序划分为多个块并内建块索引,每个块的大小通常为4~8KB并内建块内行索引,一般不压缩;
  • UpdateServer支持主备,增量数据通常为2个副本,每个副本支持RAID1存储。

 

 

 

发布了77 篇原创文章 · 获赞 178 · 访问量 22万+

猜你喜欢

转载自blog.csdn.net/qq_38289815/article/details/103090095