返回 登录
0

Schemaless架构(二):Uber基于MySQL的Trip数据库

阅读7646

Uber的Schemaless数据库是从2014年10月开始启用的,这是一个基于MySQL的数据库,本文就来探究一下它的架构。本文是系列文章的第二部分;第一部分是关于Schemaless的设计

《Mezzanine项目——Uber的超级大迁移》一文中,我们描述了如何将Uber的核心trip数据从一个单独的Postgres实例迁移到Schemaless这个可扩展与高可用的数据库中。然后对Schemaless进行了简单介绍,包括其发展决策过程、整体数据模型,并介绍了Schemaless的trigger与索引等功能。 本文将概述Schemaless的架构。

Schemaless简介

回顾一下,Schemaless是一个可扩展的容错数据库,其数据的基本单位被称为单元(cell),它是不可变的,一旦写入,便无法被覆盖(在特殊情况下,我们可以删除旧记录);单元可以被行键(row key)、列名(column name)和引用键(ref key)引用;单元内容通过编写引用键更高的新版来执行更新,但行键和列名保持不变。Schemaless不对其中存储的数据执行任何操作(故而命名schemaless)。从Schemaless的观点来看,它只负责存储JSON对象。Schemaless有着独特的模式,它支持最终在单元字段保持一致的高效二级索引。

架构

Schemaless有两种节点:工作节点和存储节点,可以放在同一个物理/虚拟主机上,也可以放在分离的主机上。工作节点接收客户端请求,将其分发到存储节点中,再将结果聚合起来。存储节点存放数据的方式使得在同一个存储节点上进行单个或多个检索速度很快。我们将这两种节点类型分开,分别进行扩展。Schemaless的基本结构如下:

图片描述

工作节点

Schemaless的客户端与工作节点通过HTTP端点通讯。它们向存储节点发出路由请求,并将从存储节点获得的结果进行聚合(在需要时),同时处理后台任务。对于进展缓慢或出现故障的工作节点,客户端数据库将尝试连接到其他主机并重试请求。对Schemaless的写入请求是幂等的,因此每次请求重试都是安全的(这个性能真的很棒)。客户端数据库利用了这个功能。

存储节点

我们将数据集划分成固定数量的分片(一般配置为4096),然后将其映射到存储节点上。根据单元的行键,将单元与分片一一对应。复制每个分片到存储节点的可配置数量。总体来讲,这些存储节点构成存储集群,每个存储集群包含一个主(master)、两个辅(minion)。Minion(也称为副本)会分布到多个数据中心中,提供数据冗余,以防灾难性的数据中心宕机。

读取和写入请求

一旦Schemaless用作读取,比如读取单元或查询索引时,工作节点能够从集群的任意存储节点中读取数据。每次请求是从master还是minion的存储节点中读取是可配置的;默认是读取master存储节点的数据,也就是说确保客户端能够看到写入请求的结果。写入请求(请求插入单元)必须要在单元集群的master上执行。一旦master数据更新,存储节点将更新异步复制到集群的minion上。

故障处理

分布式数据存储系统有趣的一点在于它们处理故障的方式,比如在存储节点未能响应请求时(无论master还是minion)。Schemaless在设计时,旨在将存储节点无法响应读取与写入请求的失败影响降到最低。

读取请求

Master和minion的设置意味着:只要集群中有一个节点可用,就能满足读取请求。如果master可用,Schemaless就总能在检索时返回最新的数据。如果master不可用,一些数据可能还未传到minion,因此Schemaless可能返回过期的数据。然而在生产环境中,复制的延迟通常是次秒级的,因此minion的数据往往是最新的。工作节点在与存储节点的连接中使用断路器模式,以检测存储节点是否出现问题。用这种办法,在出现故障时将读取任务转移到另一节点上。

写入请求

一个minion宕机不会影响写入;相应操作可以转到master上去。不过如果master宕机,Schemaless仍会接收写入请求,但会将这些请求存入另一个master(随机选择)的磁盘。这与DynamoCassandra系统中的暗示移交(hinted handoff)十分类似。向另一个master写入意味着在master恢复或者minion升级为master前,随后的读取请求都无法读取这些新的写入请求。事实上,在异步复制中Schemaless总是通过将写入转到另一个master的方式来处理故障;我们将这种技术称为缓存写入(buffered writes,下面会详细描述)。

使用单独的节点来接收写入请求,优势和劣势都很多。一个优势在于:写入每个分片的请求可以整体进行排序。对于Schemaless trigger来说,这是很重要的性能,我们的异步处理框架(系列文的第一部分中提到过)可以从任意节点为分片读取数据,同时确保同样的处理顺序。在所有集群的所有节点上负责写入请求的单元都是一样的。因此在某种意义上,Schemaless的分片可以看作是分区单元的修改日志。

单master最突出的缺点在于,如果一个集群的master宕机,我们向别的master缓存写入命令,但这些新的写入内容是无法读取的。这种麻烦情况的优点在于:Schemaless可以在master宕机时通知客户端,因此客户端会知道新写入的单元不再是立即可读的了。

缓存写入

由于Schemaless使用MySQL异步复制,在master收到并留存写入请求,然后还没来得及将其复制到minion前,便出现了故障(比如硬盘驱动器故障),这个写入请求就会丢失。为了解决这个问题,我们使用了一种技术,名叫缓存写入。通过写入多个集群,将数据丢失的风险减到最低。如果一个master宕机,后续的读取任务无法迅速执行,但请求存续却不受影响。

通过缓存写入,当工作节点收到写入请求时,会将请求写入两个集群:次级集群和主集群(按次顺序)。只有两者都执行成功的情况下,系统才会通知客户端写入成功。见下图:

图片描述

在后续读取中,数据应当在主集群的master中。如果在异步MySQL复制将单元复制到主集群的minion前,主集群的master就宕机了,那么就将次级集群的master用作临时数据备份。

次级集群的master是随机选择的,转移的写入命令将进入特殊的缓存表格。后台job会监控主集群的minion,查看单元的出现时间;然后才会将相应单元从缓存表格中删除。设置次级集群代表着需要将所有数据至少要写入两个主机。此外,次级集群的数量也是可配置的。

缓存写入用到了幂等性;如果一个行键、列名和引用键相同的单元已经存在,写入就会被拒绝。幂等性意味着只要单元的行键、列名和引用键不同,就会在主集群的master恢复运作时写入原master。另一方面,如果缓存了多个行键、列名和引用键相同的写入请求,那么只有一个能够成功;在主集群恢复时,剩下的请求都会被拒绝。

将MySQL用作存储后端

Schemaless的强大(与简单)大多是因为我们在存储节点中使用了MySQL。Schemaless本身是一个在MySQL之上相对较薄的层面,负责将路由请求发送给正确的数据库。通过使用MySQL索引,并将build缓存到InnoDB中,单元和二级索引的查询速度很快。

每个Schemaless分片都是独立的MySQL数据库,而每个MySQL数据库服务器包含一系列MySQL数据库。每个数据库包含一个单元的MySQL表格(叫做单元表),而每个二级索引也有一个MySQL表格,另有一组辅助表格。每个Schemaless的单元就是单元表中的一行,定义如下:

图片描述

added_id列是一个自动递增的整数列,也是单元表的MySQL主键。将added_id作为主键,可以让MySQL在磁盘上线性写入单元。此外,将added_id作为每个单元的独特指针,Schemaless trigger可以按照插入的时间顺序来有效地提取单元。

而row_key、column_name和ref_key分别代表Schemaless单元的行键、列名和引用键。为了通过这三栏进行有效地查询,我们为这三列定义了一个复合MySQL索引。这样一来,我们就能根据指定的行键和列名有效地找出所有单元了。

内容列中包含每个单元的JSON对象,以压缩的MySQL blob(二进制大对象)表示。我们尝试了各种编码和压缩算法,最终由于压缩速度和大小选用了MessagePackZLib(在后面的文章中,我们会详细进行描述)。最后,created_at列是单元插入的时间戳,可供Schemaless trigger用来查找指定日期的单元。

通过这种设置,客户端可以控制模式,而无需修改MySQL的布局;查找单元更有效率。此外,added_id列使得写入命令以线性执行,因此我们能够将数据视作分区日志来访问,达到高效。

总结

如今的Schemaless是Uber基础架构大量服务的生产数据库。我们的很多服务都极其依赖这个高可用性和可扩展的Schemaless。

原文:THE ARCHITECTURE OF SCHEMALESS, UBER ENGINEERING’S TRIP DATASTORE USING MYSQL(译者/孙薇 责编/仲浩)

评论