原文:THE ARCHITECTURE OF SCHEMALESS, UBER ENGINEERING’S TRIP DATASTORE USING MYSQL
译者:杰微刊兼职翻译缪晨
Uber底层如何与Schemaless协同工作,这个数据存储基于MySQL从2014年10月帮助Uber的工程师扩容。这篇是Schemaless三部曲中的第二部:第一部是Schemaless的设计。
在 Mezzanine项目: Uber伟大的迁移,我们讲述了我们是如何将Uber的核心行程数据从一个单一的Postgres实例迁移到Schemaless的,这是我们开发的可扩容、高可用的数据存储。然后我们给出了一个Schemaless的概览?—它的开发的决定过程,以及数据模型概览—而且介绍来一些特征比如Schemaless的触发器和索引。这篇文章覆盖了Schemaless的架构。
Schemaless概要
回顾一下,Schemaless是一个可扩展且能容错的数据存储。基础的数据实体称作cell,是不可变的,一次写入之后不可被覆写(在一些特殊的情况下,可以删除旧的记录) 。一个格子由一个row key,columnname,以及ref key定位。一个cell通过写入一个带有更大的ref key和相同的row key和column name的新版本来更新。Schemaless对内部存储的数据的结构没有强制要求,并以此得名。从Schemaless的观点看,他只存储JSON 对象。Schemaless独特的支持基于cell的字段构建的高效的具备最终一致性的副索引。
架构
Schemaless的节点分为两类:工作节点和存储节点,既可以在同一个物理机/虚拟机上,也可以分开。工作节点接受到客户端的请求,把请求分散到存储节点,并把结果聚合。存储节点存储数据的方式使单个存储节点上的单个或多个cell的查询非常快。我们把两种节点分开来让两部分各自独立的扩容。下图是Schemaless架构概览:
工作节点
Schemaless的客户端通过HTTP协议与工作节点交互。工作节点将请求路由到存储节点,并根据需要将存储节点返回的结果聚合,并处理一些后台工作。为了解决运行慢或宕机节点的问题,客户端的库文件会显示的尝试其它主机,以及重试失败的请求。写操作对Schemaless是幂等的,因此所有的请求都可以安全的重试 (这真是个不错的特性)。这个特性被客户端类库充分的利用了。
存储节点
我们将数据集合划分入一个固定数量的分片(通常设置为4096),然后映射到存储节点上。一个cell根据它的row key映到到一个分片。每个分片都会按配置的数量复制到多个存储节点上。这些存储节点共同组成一个存储集群,每个节点都有一个主节点与两个从节点。从节点(也称为副本)分布在多个数据中心为机房故障提供冗余。
读写请求
当Schemaless处理一个读请求时,比如读取一个cell或者请求一个索引,工作节点可以从集群内的任何存储节点读取数据,具体从主节点还是从节点读取数据是在一个的底层配置的,默认读取主节点,这意味着客户端可以看到它写请求的结果。写请求(插入cell的请求),只能操作这个cell所属集群的主节点。当更新完主节点的数据,存储节点会异步的将这个更新复制到集群的从节点。
错误处理
分布式数据存储一个有趣的方向是如何处理故障,比如请求返回失败(主节点或从节点)。Schemaless设计的目的就是最小化存储节点读写请求失败造成的影响。
读请求
主节点与从节点的设置意味着只要集群里有一个节点可用就可以提供服务。如果主节点可用,Schemaless总是可以通过查询返回最新的数据。如果主节点宕机了,有些数据可能还没有复制到从节点,因此Schemaless返回得可能不是最新的数据。在生产环境中,复制的延时基本都是亚秒级的,因此从节点的数据基本都是最新的。工作节点对存储节点连接的管理采用 断路器模式,当一个存储节点宕机时,会自动寻找新的节点。通过这种方式,读取任务在故障时转移到了另一个节点。
写请求
从节点宕机并不影响写操作,写请求发往主节点,但是如果主节点宕机,Schemaless同样接受写请求,但是他们会在其它(随机选择的)主节点上实例化。这与Dynamo 或 Cassandra的hinted handoff机制很像。写往其它的主节点意味着随后的读请求在主节点恢复或者从节点升级为主节点前读不到这些写入结果。事实上,Schemaless在处理异步故障的时候都是通过写其它主节点解决的,我们称之为技术缓冲写入(将会在下节中详述)。
使用单一节点负责写入会产生一些优点与缺点。一个优势是对于每个分片的写入操作构成一个 全序 ,这对Schemaless的触发器来说非常重要,我们的异步处理处理框架(这在Schemaless系列文章中的第一篇有提及),因为这样它可以从任何一个节点读取该分片的数据,并能保证处理的顺序。集群中所有节点的cell的写顺序都是一致的,因此在一些情景下Schemaless的分片可以看作一份分区cell的修改日志。
单主节点最突出的缺点是:如果集群中主节点宕机了,我们将数据缓冲写入别的地方,但是不可读。这种麻烦情况的优势在于:Schemaless可以告知客户端,master节点宕机了,因此客户端知道刚刚写入的cell不是马上可以读取的。
缓冲写入
由于Schemaless使用MySQL异步复制,如果一个主节点收到一个写请求,并将写请求实例化,但是在复制到其它从节点的时候宕机了(比如硬件故障)。未解决这个问题,我们使用一项称作缓冲写入的技术。缓冲写入通过将数据写入多个集群来最小化数据丢失的概率。如果一个主节点宕机了,数据对接下来的读请求是不可用,但是还是先被实例化下来。
通过缓冲写入,当一个工作节点收到一个写入请求,他将请求写入两个集群:一个副集群和一个主集群(按这个顺序)。仅当两个写入都成功了时才会告知客户端写入成功。请看下表:
主集群的主节点是接下来的读请求期望读取数据的地方。如果主机群主节点在异步MySQL复制将cell复制到主集群从节点之前宕机了了,副集群主节点暂时充当数据备份。
副集群主节点是随机选择的,写操作写入一个特殊的缓冲表。一个后台的进程监控主集群的从节点来查看何时cell出现,仅当那是从缓冲表删除该cell。存在副集群意味着数据至少写入了两台主机。附带一下,副集群主节点的数量是配置的。
缓冲写入利用幂等性,如果一个带有指定row key,column name和ref key的cell已经存在,这个写入会被拒绝。幂等性意味着如果缓冲的cell有不同的row key,columnname和ref key,当主集群主节点恢复时会写入主集群。从另一方面说,如果多个带有相同的row key,columnname以及ref key的写操作被缓冲,他们中的只有一个可以成功,当主集群恢复的时候,其它的会被拒绝。
使用MySQL作为存储后端
Schemaless的强大(与简单)来源于存储节点中使用了MySQL。Schemaless本身只是在MySQL之上加了一层薄的封装用于将请求路由到正确的数据库。通过使用MySQL InnoDB内置的索引和缓存,我们获得了cell及副索引查询的高性能。
每个Schemaless分片是一个独立的MySQL数据库,每个MySQL数据库服务器包括一系列MySQL数据库。每个数据库包含一个盛放cell的MySQL表(称作实体表)以及每个副索引各有一张表,另外还有一组辅助表。每个Schemaless的cell是实体表中的一行,并有如下MySQL表定义:
added_id 列是一个自增的整数列,而且是实体表的MySQL主键。使用added_id作为主键使MySQL在磁盘上线性写入cell。此外added_id为每个cell提供了一个唯一的指针,因此Schemaless的触发器可以有效地使用它按插入顺序提取数据。
而 row_key, column_name, 和ref_key 三列即是每个Schemalesscell的row key、columnname和ref key。为了通过这三列高效地查找cell,我们在这三列上定义了一个MySQL联合索引。因此我们可以高效根据给定row key和column name找到所有cell。
body列使用压缩过的MySQLblob格式存储了cell的JSON对象。我们试验了各种编码和压缩算法,最后出于速度和体积的考虑决定使用MessagePack 和ZLib (详细内容将会在后面的文章中讨论)。最后,created_at列用于存储cell插入的时间戳,因为Schemaless的触发器会查询一个给定时间节点之后的cell。
基于这些配置,我们使用客户端控制结构,而无需修改MySQL的中的表结构;而且可以高效的寻找cell。此外added_id列使插入线性写入到磁盘上,据此我们可以像分区日志一样高效的操作数据。
总结
Schemaless如今是Uber底层一大批服务的生产中的数据存储。我们很多服务高度依赖Schemaless高可用及可扩容的特性。
更多内容:
[译]Uber是如何使用MySQL设计可扩展性数据存储的?(一)
[译]Uber是如何使用MySQL设计数据存储的(三)