文章目录
- OceanBase架构解析(二)
- OceanBase介绍
- 背景分析
- 设计思路
- 系统架构
- 客户端
- RootServer
- MergeServer
- ChunkServer
- UpdateServer
- 定期合并&数据分发
- 单点性能
- 数据正确性
OceanBase架构解析(二)
OceanBase介绍
OceanBase是阿里集团研发的可扩展的关系数据库,实现了数千亿条记录、数百TB数据上的跨行跨表事务,截止到2012年8月,支持了收藏夹、直通车报表、天猫评价等OLTP和OLAP在线业务,线上数据量已经超过一千亿条。
从模块划分的角度看,OceanBase可以划分为四个模块:主控服务器RootServer、更新服务器UpdateServer、基线数据服务器ChunkServer以及合并服务器MergeServer。OceanBase系统内部按照时间线将数据划分为基线数据和增量数据,基线数据是只读的,所有的修改更新到增量数据中,系统内部通过合并操作定期将增量数据融合到基线数据中。
背景分析
淘宝收藏夹是淘宝线上应用之一,淘宝用户在其中保存自己感兴趣的宝贝(即商品,此外用户也可以收藏感兴趣的店铺)以便下次快速访问、对比和购买等,用户可以展示和编辑(添加/删除)自己的收藏。淘宝收藏夹数据库包含了收藏info表(一条一条的收藏信息)和收藏item表(被收藏的宝贝和店铺)等:
●收藏info表保存收藏信息条目,数百亿条。
●收藏item表保存收藏的宝贝和店铺的详细信息,数十亿条。
●热门宝贝可能被多达数十万买家收藏。
●每个用户可以收藏千个宝贝。
●宝贝的价格、收藏人气等信息随时变化。
如果用户选择按宝贝价格排序后展示,那么数据库需要从收藏item表中读取收藏的宝贝的价格等最新信息,然后进行排序处理。如果用户的收藏条目比较多(例如4000条),那么查询对应的item的时间会较长:假设如果平均每条item查询时间是5ms,则4000条的查询时间可能达到20s,如果真如此,则用户体验会很差。
如果把收藏的宝贝的详细信息实时冗余到收藏info表,则上述查询收藏item表的操作就不再需要了。但是,由于许多热门商品可能有几千到几十万人收藏,这些热门商品的价格等信息的变动可能导致收藏info表的大量修改,并压垮数据库。
设计思路
OceanBase的目标是支持数百TB的数据量以及数十万TPS、数百万QPS的访问量,无论是数据量还是访问量,即使采用非常昂贵的小型机甚至是大型机,单台关系数据库系统都无法承受。
一种常见的做法是根据业务特点对数据库进行水平拆分,通常的做法是根据某个业务字段(通常取用户编号,user_id)哈希后取模,根据取模的结果将数据分布到不同的数据库服务器上,客户端请求通过数据库中间层路由到不同的分区。这种方式目前还存在一定的弊端,如下所示:
●数据和负载增加后添加机器的操作比较复杂,往往需要人工介入;
●有些范围查询需要访问几乎所有的分区,例如,按照user_id分区,查询收藏了一个商品的所有用户需要访问所有的分区;
●目前广泛使用的关系数据库存储引擎都是针对机械硬盘的特点设计的,不能够完全发挥新硬件(SSD)的能力。
另外一种做法是参考分布式表格系统的做法,例如Google Bigtable系统,将大表划分为几万、几十万甚至几百万个子表,子表之间按照主键有序,如果某台服务器发生故障,它上面服务的数据能够在很短的时间内自动迁移到集群中所有的其他服务器。这种方式解决了可扩展性的问题,少量突发的服务器故障或者增加服务器对使用者基本是透明的,能够轻松应对促销或者热点事件等突发流量增长。另外,由于子表是按照主键有序分布的,很好地解决了范围查询的问题。
万事有其利必有一弊,分布式表格系统虽然解决了可扩展性问题,但往往无法支持事务,例如Bigtable只支持单行事务,针对同一个user_id下的多条记录的操作都无法保证原子性。而OceanBase希望能够支持跨行跨表事务,这样使用起来会比较方便。
最直接的做法是在Bigtable开源实现(如HBase或者Hypertable)的基础上引入两阶段提交(Two-phase Commit)协议支持分布式事务,这种思路在Google的Percolator系统中得到了体现。然而,Percolator系统中事务的平均响应时间达到2~5秒,只能应用在类似网页建库这样的半线上业务中。另外,Bigtable的开源实现也不够成熟,单台服务器能够支持的数据量有限,单个请求的最大响应时间很难得到保证,机器故障等异常处理机制也有很多比较严重的问题。总体上看,这种做法的工作量和难度超出了项目组的承受能力,因此,我们需要根据业务特点做一些定制。
通过分析,我们发现,虽然在线业务的数据量十分庞大,例如几十亿条、上百亿条甚至更多记录,但最近一段时间(例如一天)的修改量往往并不多,通常不超过几千万条到几亿条,因此,OceanBase决定采用单台更新服务器来记录最近一段时间的修改增量,而以前的数据保持不变,以前的数据称为基线数据。基线数据以类似分布式文件系统的方式存储于多台基线数据服务器中,每次查询都需要把基线数据和增量数据融合后返回给客户端。这样,写事务都集中在单台更新服务器上,避免了复杂的分布式事务,高效地实现了跨行跨表事务;另外,更新服务器上的修改增量能够定期分发到多台基线数据服务器中,避免成为瓶颈,实现了良好的扩展性。
当然,单台更新服务器的处理能力总是有一定的限制。因此,更新服务器的硬件配置相对较好,如内存较大、网卡及CPU较好;另外,最近一段时间的更新操作往往总是能够存放在内存中,在软件层面也针对这种场景做了大量的优化。
系统架构
OceanBase由如下几个部分组成:
●客户端:用户使用OceanBase的方式和MySQL数据库完全相同,支持JDBC、 C客户端访问,等等。基于MySQL数据库开发的应用程序、工具能够直接迁移到OceanBase。
●RootServer:管理集群中的所有服务器,子表(tablet)数据分布以及副本管理。 RootServer一般为一主一备,主备之间数据强同步。
●UpdateServer:存储OceanBase系统的增量更新数据。UpdateServer一般为一主一备,主备之间可以配置不同的同步模式。部署时,UpdateServer进程和RootServer进程往往共用物理服务器。
●ChunkServer:存储OceanBase系统的基线数据。基线数据一般存储两份或者三份,可配置。
●MergeServer:接收并解析用户的SQL请求,经过词法分析、语法分析、查询优化等一系列操作后转发给相应的ChunkServer或者UpdateServer。如果请求的数据分布在多台ChunkServer上,MergeServer还需要对多台ChunkServer返回的结果进行合并。客户端和MergeServer之间采用原生的MySQL通信协议,MySQL客户端可以直接访问MergeServer。
OceanBase支持部署多个机房,每个机房部署一个包含RootServer、MergeServer、ChunkServer以及UpdateServer的完整OceanBase集群,每个集群由各自的RootServer负责数据划分、负载均衡、集群服务器管理等操作,集群之间数据同步通过主集群的主UpdateServer往备集群同步增量更新操作日志实现。客户端配置了多个集群的RootServer地址列表,使用者可以设置每个集群的流量分配比例,客户端根据这个比例将读写操作发往不同的集群。
多副本:OceanBase一般部署为三个Zone,每个Zone由多个节点/服务器(OBServer)组成;
全对等节点:每个节点均有自己的SQL引擎和存储引擎,各自管理不同的数据分区,完全对等;
无共享: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地址列表。
如果OceanBase部署多个集群,客户端还需要处理多个集群的流量分配问题。使用者可以设置多个集群之间的流量分配比例,客户端获取到流量分配比例后,按照这个比例将请求发送到不同的集群。
RootServer
RootServer的功能主要包括:集群管理、数据分布以及副本管理。
RootServer管理集群中的所有MergeServer、ChunkServer以及UpdateServer。每个集群内部同一时刻只允许一个UpdateServer提供写服务,这个UpdateServer成为主UpdateServer。这种方式通过牺牲一定的可用性获取了强一致性。RootServer通过租约(Lease)机制选择唯一的主UpdateServer,当原先的主UpdateServer发生故障后,RootServer能够在原先的租约失效后选择一台新的UpdateServer作为主UpdateServer。另外,RootServer与MergeServer&ChunkServer之间保持心跳(heartbeat),从而能够感知到在线和已经下线的MergeServer&ChunkServer机器列表。
OceanBase内部使用主键对表格中的数据进行排序和存储,主键由若干列组成并且具有唯一性。在OceanBase内部,基线数据按照主键排序并且划分为数据量大致相等的数据范围,称为子表(tablet)。每个子表的默认大小是256MB(可配置)。OceanBase的数据分布方式与Bigtable一样采用顺序分布,不同的是,OceanBase没有采用根表(RootTable)+元数据表(MetaTable)两级索引结构,而是采用根表一级索引结构。
主键值在[1,100]之间的表格被划分为四个子表:1~25,26~50,51~80以及81~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支持两种缓存模式,块缓存(Block Cache)以及行缓存(Row Cache)。块缓存以块为单位缓存最近读取的数据,行缓存以行为单位缓存最近读取的数据。
MergeServer将每个子表的读取请求发送到子表所在的ChunkServer,ChunkServer首先读取SSTable中包含的基线数据,接着请求UpdateServer获取相应的增量更新数据,并将基线数据与增量更新融合后得到最终结果。
由于每次读取都需要从UpdateServer中获取最新的增量更新,为了保证读取性能,需要限制UpdateServer中增量更新的数据量,最好能够全部存放在内存中。OceanBase内部会定期触发合并或者数据分发操作,在这个过程中,ChunkServer将从UpdateServer获取一段时间之前的更新操作。通常情况下,OceanBase集群会在每天的服务低峰期(凌晨1:00开始,可配置)执行一次合并操作。这个合并操作往往也称为每日合并。
UpdateServer
UpdateServer是集群中唯一能够接受写入的模块,每个集群中只有一个主Update-Server。UpdateServer中的更新操作首先写入到内存表,当内存表的数据量超过一定值时,可以生成快照文件并转储到SSD中。快照文件的组织方式与ChunkServer中的SSTable类似,因此,这些快照文件也称为SSTable。另外,由于数据行的某些列被更新,某些列没被更新,SSTable中存储的数据行是稀疏的,称为稀疏型SSTable。
为了保证可靠性,主UpdateServer更新内存表之前需要首先写操作日志,并同步到备UpdateServer。当主UpdateServer发生故障时,RootServer上维护的租约将失效,此时,RootServer将从备UpdateServer列表中选择一台最新的备UpdateServer切换为主UpdateServer继续提供写服务。UpdateServer宕机重启后需要首先加载转储的快照文件(SSTable文件),接着回放快照点之后的操作日志。
由于集群中只有一台主UpdateServer提供写服务,因此,OceanBase很容易地实现了跨行跨表事务,而不需要采用传统的两阶段提交协议。当然,这样也带来了一系列的问题。由于整个集群所有的读写操作都必须经过UpdateServer,UpdateServer的性能至关重要。OceanBase集群通过定期合并和数据分发这两种机制将UpdateServer一段时间之前的增量更新源源不断地分散到ChunkServer,而UpdateServer只需要服务最新一小段时间新增的数据,这些数据往往可以全部存放在内存中。另外,系统实现时也需要对UpdateServer的内存操作、网络框架、磁盘操作做大量的优化。
定期合并&数据分发
定期合并和数据分发都是将UpdateServer中的增量更新分发到ChunkServer中的手段,二者的整体流程比较类似:
1)UpdateServer冻结当前的活跃内存表(Active MemTable),生成冻结内存表,并开启新的活跃内存表,后续的更新操作都写入新的活跃内存表。
2)UpdateServer通知RootServer数据版本发生了变化,之后RootServer通过心跳消息通知ChunkServer。
3)每台ChunkServer启动定期合并或者数据分发操作,从UpdateServer获取每个子表对应的增量更新数据。
定期合并与数据分发两者之间的不同点在于,数据分发过程中ChunkServer只是将UpdateServer中冻结内存表中的增量更新数据缓存到本地,而定期合并过程中ChunkServer需要将本地SSTable中的基线数据与冻结内存表的增量更新数据执行一次多路归并,融合后生成新的基线数据并存放到新的SSTable中。定期合并对系统服务能力影响很大,往往安排在每天服务低峰期执行(例如凌晨1点开始),而数据分发可以不受限制。
单点性能
OceanBase架构的优势在于既支持跨行跨表事务,又支持存储服务器线性扩展。当然,这个架构也有一个明显的缺陷:UpdateServer单点,这个问题限制了OceanBase集群的整体读写性能。下面从内存容量、网络、磁盘等几个方面分析UpdateServer的读写性能。其实大部分数据库每天的修改次数相当有限,只有少数修改比较频繁的数据库才有每天几亿次的修改次数。另外,数据库平均每次修改涉及的数据量很少,很多时候只有几十个字节到几百个字节。假设数据库每天更新1亿次,平均每次需要消耗100字节,每天插入1000万次,平均每次需要消耗1000字节,那么,一天的修改量为:1亿×100+1000万×1000=20GB,如果内存数据结构膨胀2倍,占用内存只有40GB。而当前主流的服务器都可以配置96GB内存,一些高档的服务器甚至可以配置192GB、384GB乃至更多内存。
从上面的分析可以看出,UpdateServer的内存容量一般不会成为瓶颈。然而,服务器的内存毕竟有限,实际应用中仍然可能出现修改量超出内存的情况。例如,淘宝双11网购节数据库修改量暴涨,某些特殊应用每天的修改次数特别多或者每次修改的数据量特别大,DBA数据订正时一次性写入大量数据。为此,UpdateServer设计实现了几种方式解决内存容量问题,UpdateServer的内存表达到一定大小时,可自动或者手工冻结并转储到SSD中,另外,OceanBase支持通过定期合并或者数据分发的方式将UpdateServer的数据分散到集群中所有的ChunkServer机器中,这样不仅避免了UpdateServer单机数据容量问题,还能够使得读取操作往往只需要访问UpdateServer内存中的数据,避免访问SSD磁盘,提高了读取性能。
从网络角度看,假设每秒的读取次数为20万次,每次需要从UpdateServer中获取100字节,那么,读取操作占用的UpdateServer出口带宽为:20万×100=20MB,远远没有达到千兆网卡带宽上限。另外,UpdateServer还可以配置多块千兆网卡或者万兆网卡,例如,OceanBase线上集群一般给UpdateServer配置4块千兆网卡。当然,如果软件层面没有做好,硬件特性将得不到充分发挥。针对UpdateServer全内存、收发的网络包一般比较小的特点,开发团队对UpdateServer的网络框架做了专门的优化,大大提高了每秒收发网络包的个数,使得网络不会成为瓶颈。
从磁盘的角度看,数据库事务需要首先将操作日志写入磁盘。如果每次写入都需要将数据刷入磁盘,而一块SAS磁盘每秒支持的IOPS很难超过300,磁盘将很快成为瓶颈。为了解决这个问题,UpdateServer在硬件上会配置一块带有缓存模块的RAID卡,UpdateServer写操作日志只需要写入到RAID卡的缓存模块即可,延时可以控制在1毫秒之内。RAID卡带电池,如果UpdateServer发生故障,比如机器突然停电,RAID卡能够确保将缓存中的数据刷入磁盘,不会出现丢数据的情况。另外,UpdateServer还实现了写事务的成组提交机制,将多个用户写操作凑成一批一次性提交,进一步减少磁盘IO次数。
磁盘随机IO是存储系统性能的决定因素,传统的SAS盘能够提供的IOPS不超过300。关系数据库一般采用高速缓存(Buffer Cache)[注释]的方式缓解这个问题,读取操作将磁盘中的页面缓存到高速缓存中,并通过LRU或者类似的方式淘汰不经常访问的页面;同样,写入操作也是将数据写入到高速缓存中,由高速缓存按照一定的策略将内存中页面的内容刷入磁盘。这种方式面临一些问题,例如,Cache冷启动问题,即数据库刚启动时性能很差,需要将读取流量逐步切入。另外,这种方式不适合写入特别多的场景。
最近几年,SSD磁盘取得了很大的进展,它不仅提供了非常好的随机读取性能,功耗也非常低,大有取代传统机械磁盘之势。一块普通的SSD磁盘可以提供35000 IOPS甚至更高,并提供300MB/s或以上的读出带宽。然而,SSD盘的随机写性能并不理想。这是因为,尽管SSD的读和写以页(page,例如4KB,8KB等)为单位,但SSD写入前需要首先擦除已有内容,而擦除以块(block)为单位,一个块由若干个连续的页组成,大小通常在512KB~2MB。假如写入的页有内容,即使只写入一个字节,SSD也需要擦除整个512KB~2MB大小的块,然后再写入整个页的内容,这就是SSD的写入放大效应。虽然SSD硬件厂商都针对这个问题做了一些优化,但整体上看,随机写入不能发挥SSD的优势。
OceanBase设计之初就认为SSD为大势所趋,整个系统设计时完全摒弃了随机写,除了操作日志总是顺序追加写入到普通SAS盘上,剩下的写请求都是对响应时间要求不是很高的批量顺序写,SSD盘可以轻松应对,而大量查询请求的随机读,则发挥了SSD良好的随机读的特性。摒弃随机写,采用批量的顺序写,也使得固态盘的使用寿命不再成为问题,主流SSD盘使用MLC SSD芯片,而MLC号称可以擦写1万次(SLC可以擦写10万次,但因成本高而较少使用),即使按最保守的2500次擦写次数计算,而且每天全部擦写一遍,其使用寿命为2500/365=6.8年。
数据正确性
数据丢失或者数据错误对于存储系统来说是一种灾难。OceanBase设计为强一致性系统,设计方案上保证不丢数据。然而,TCP协议传输、磁盘读写都可能出现数据错误,程序Bug则更为常见。为了防止各种因素导致的数据损毁,OceanBase采取了以下数据校验措施:
●数据存储校验。每个存储记录(通常是几KB到几十KB)同时保存64位CRC校验码,数据被访问时,重新计算和比对校验码。
●数据传输校验。每个传输记录同时传输64位CRC校验码,数据被接收后,重新计算和比对校验码。
●数据镜像校验。UpdateServer在机群内有主UpdateServer和备UpdateServer,集群间有主集群和备集群,这些UpdateServer的内存表(MemTable)必须保持一致。为此,UpdateServer为MemTable生成一个校验码,MemTable每次更新时,校验码同步更新并记录在对应的操作日志中。备UpdateServer收到操作日志并重放到MemTable时,也同步更新MemTable校验码并与接收到的校验码对照。UpdateServer重新启动后重放日志恢复MemTable时也同步更新MemTable校验码并与保存在每条操作日志中的校验码对照。
●数据副本校验。定期合并时,新的子表由各个ChunkServer独立地融合旧的子表中的SSTable与冻结的MemTable而生成,如果发生任何异常或者错误(比如程序bug),同一子表的多个副本可能不一致,则这种不一致可能随着定期合并而逐步累积或扩散且很难被发现,即使被察觉,也可能因为需要追溯较长时间而难以定位到源头。为了防止这种情况出现,ChunkServer在定期合并生成新的子表时,也同时为每个子表生成一个校验码,并随新子表汇报给RootServer,以便RootServer核对同一子表不同副本的校验码。