美文网首页程序员互联网科技代码改变世界
[译]Uber是如何使用MySQL设计可扩展性数据存储的?(一)

[译]Uber是如何使用MySQL设计可扩展性数据存储的?(一)

作者: 微笑0619 | 来源:发表于2016-03-24 14:40 被阅读308次

    原文:DESIGNING SCHEMALESS, UBER ENGINEERING’S SCALABLE DATASTORE USING MYSQL

    作者:JAKOB HOLDGAARD THOM

    译者:杰微刊兼职译者缪晨

    Schemaless的形成:根据Uber工程师的习惯使用MySQL设计的数据存储,使我们可以 从2014 扩容到更高。这是Schemaless三部曲中的第一部。

    在 Mezzanine项目 中我们描述了我们是如何将Uber的核心行程数据从单个的Postgres节点迁移到Schemaless,这是我们开发的一个容错性很高、可用的数据存储。本文将进一步讲述它的架构、它在Uber基础结构中的角色以及他是如何成为该角色的。

    我们对新数据库的迫切需求

    2014年初,由于出行业务的迅猛增长,数据库空间即将耗尽。每次入住新的城市以及行程的里程碑都会把我们推向危险的境地,直到我们发现到年末时Uber的基础架构将无法继续发挥效用:Postgre并不能存储如此多的行程数据。我们的任务是实现Uber的下一代数据库技术,一个耗时数月甚至几乎整年的任务,大量的来自于我们世界各地研究所的工程师参与进来

    但是首先,在商业与开源选择如此之多的当下,为什么要自己构建一个可扩展的数据库。我们对我们新的行程数据存储有5点关键的需求:

    1、我们新的解决方案需要可以通过增加服务器线性扩容,这是原有的Postgre所缺乏的。添加服务器应该能在增加硬盘存储的同时减少系统的响应时间。

    2、我们需要写入的能力。我们之前通过Redis实现了一套简单的缓冲机制,因此如果Postgre写入失败,我们可以稍后重试,因为行程已经在中间层存入了Redis当中。但是当行程数据在Redis当中时,是不能从Postgre中读取的,然后一些功能就挂了,比如计费。很烦,不过至少我们没有丢失行程数据。随着时间流逝,Uber逐渐成长,我们基于Redis的解决方案不能扩容。Schemaless需要支持一种类似Redis的机制,但最好还是写完即时可读。

    3、 我们需要一种机制通知下游依赖。在现有系统当中,我们同时处理多个行程组件(比如计费,分析等)。这种处理方式很容易出错:如果任何一步失败了,我们就比如从头重试,即使一些组件处理已经成功了。这就不能扩容了,因此我们想把这些步骤打碎成独立的步骤,由数据变更发起。我们曾经确实有一个异步事件系统,但是它是基于 Kafka 0.7的。我们没法让它无损运行,因此我们希望新系统有一些类似的机制,但是可以无损运行。

    4、我们需要副索引。由于我们是从Postgre迁移的,那新的存储系统需要支持Postgre的索引,会按照习惯用副索引搜索行程数据。

    5、我们需要运维够信赖的可靠系统,因为其中包含了行程数据的关键任务。如果凌晨3点我们接到叫车请求,但是这时数据存储无法响应查询,导致业务宕机,我们是否有相关操作知识可以快速解决这个问题。

    鉴于以上种种,我们分析了几种常用的选择的优势和潜在的限制,比如Cassandra、Riak、MongoDB等。出于说明的目的,我们提供了如下图表,展示了不同系统选择下的不同功能组合:

    所有的三个系统都可以通过在线增加节点线性扩容,只有一对系统可以在宕机时收到写操作。所有的解决方案中都没有内置的方式将变化通知下游依赖,因此可能需要在应用层实现该功能。它们都索引功能,但是如果你想索引多个不同的值,查询会变慢,因为他们使用分散查询并聚合结果的方式查询了所有节点。最后对于其中的一些系统有过单集群的使用经验,但不提供面向用户的在线流量,而且在我们的服务连接的时候有各种各样的运维问题。

    最终我们的确定运维信赖为主要因素,因为它包含了行程数据的关键任务。 可供选择的解决方案理论上可能都是可靠的,但是我们是否有运维的知识来立即发挥其最大功效,基于此我们决定基于Uber的使用情况开发自己的解决方案。这不仅基于我们使用的技术,而且根据成员的经验。

    需要注意的是我们对这些系统的研究持续了两年,没有发现适合行程数据存储的,但是我们已经在其它领域成功接受了Cassandra与Riak作为我们的基础服务,而且在生产环境使用这些为数百万级的用户提供服务。

    在Schemaless中我们相信

    由于以上的所有选择在规定的时间内都不能完全满足自己的需求,我们决定构建我们自己的系统使运维尽量简单,也参考了其它厂扩容的经验。这个设计的灵感来自于Friendfeed,运维的方面则参考了Pinterest。

    最后我们决定构建一个键值存储,允许存储任何JSON数据而不需要严格的格式验证,一个非结构化的模式(命名的由来)。这是一个只扩展分片的MySQL, master节点都带有写缓冲在应对MySQL宕机,数据变更通知是一个订阅-发布的功能,我们称之为trigger。最后,Schemaless支持全局数据索引。下面我们将讨论一下数据模型概览以及一些关键特性,包括剖析Uber的一份行程数据,更深入的例子保留在接下来的文章中。

    Schemaless的数据模型

    Schemaless是一个只扩展的稀疏三维持久化哈希表,非常类似Google的 Bigtable。Schemaless中的最小数据被称作cell,不可更改;一次写入后不可被覆写或删除。Cell是一个JSON blob通过一个rowkey和一个columnname引用,还有一个referencekey叫做ref key。rowkey是一个UUID,column name是一个字符串,reference key是一个整型。

    你可以将row key看作是关系型数据库的主键,column name看作是关系型数据库的列。无论如何,在Schemaless中没有预定义或强制模式而且每行不需要共享column name;事实上,columnname完全由应用层定义。ref key用于给一个指定row key和列加版本。因此如果一个cell需要更新,你只需写一个新的cell附带一个更大的ref key (最新的cell是那个有最大的ref key的)。ref key也可以用作标记一个列表中的实体,但主要用作标记版本。具体哪种形式由应用本身决定。

    应用通常把相关的数据组织进同一列,然后每列的所有cell在应用侧的结构都大致相同。这种分组方式很好的把一起修改的数据很好的组织到了一起,这样应用程序就可以在数据库不停机的情况下迅速修改结构。下面的例子进行了更详细的叙述。

    实例:Schemaless中的行程数据存储

    在深入了解我们如何在Schemaless中对行程数据建模之前,让我们先剖析一下一个Uber的行程。行程数据在不同的时间点产生,从上车下车到付费,这许多信息伴随着用户在行程中的反馈以及后台进程处理异步到达。下图简要说明了一个Uber行程的不同阶段是何时发生的:

    这个图表展示了一个我们行程流的简化版。*标志的部分是可选的且可能发生多次。

    一个行程是由乘客发起,由司机结束,包含开始与结束的时间戳。这些信息构成了行程的基础,我们据此计算出该次行程的费用,由司机来收费。行程结束后,我们可能要调整跟收取或发放的费用。我们也可能给行程数据添加备注,从乘客或司机出发出反馈(上图中星号部分标出)。在第一张信用卡超期或禁用的情况下,我们不得不尝试用多张信用卡付款。Uber行程流是一个数据驱动的过程。随着数据变得有效或添加,特定的一组处理会在该行程上执行。这些信息中的一部分,比如乘客或司机的评级(上图中note部分),可能在行程结束后几天处理。

    好了,那我们如何把上述的行程模型映射到Schemaless?

    行程数据模型

    使用 斜体字 标注UUID,大写字母表示column name,下表展示了我们行程数据存储的简化版的数据模型。我们有两个行程(UUIDstrip_uuid1 和 trip_uuid2) 以及四列(BASE, STATUS, NOTES, and FARE ADJUSTMENT)。一个格子表示一个cell,带有一个数字以及一个JSON的 (以{…}缩写)。格子的覆盖代表不同版本 (也就是不同的ref keys)。

    trip_uuid1 有三个cell:一个在BASE列,两个在STATUS列,FARE ADJUSTMENT列没有内容。trip_uuid2的BASE列有两个格子,NOTES列有一个,同样的FARE ADJUSTMENTS列也没有内容。在Schemaless中,列没什么不同;每列的语义都由应用层定义,本例中是 Mezzanine.

    在Mezzanine中,BASE列的cell包含了行程的基础信息,比如司机的UUID和行程的时间。STATUS列包含行程现在的支付状态,每次我们尝试对行程支付的时候都会插入一个cell (由于信用卡额度不足或者逾期等问题尝试可能失败)。如果司机或者Uber的DOps(司机调度员)有行程相关的备注,会在NOTES列添加一个cell。最后的FARE ADJUSTMENT列的cell记录了行程价格的调整。

    我们如此划分列是为了避免数据冲突 而且最小化更新时需要写的数据量。BASE列在行程结束时写入,基本只会写一次。当行程开始尝试支付的时候开始尝试写STATUS列,此时BASE已经写好了,如果支付失败可能会写多次。相似的NOTES列在BASE列写过后的一些节点可能会写多次,但是与STATUS列的写操作完全独立。类似的FARE ADJUSTMENTS列只在行程费用变更时会写,例如路况不好等原因。

    Schemaless触发器

    Schemaless的一个重要特性是触发器,提供了在Schemaless实例变更时可获得通知的能力。由于cell是不可变的,以及新的版本是追加的,所以每个cell都代表了一个修改或者一个版本,这允许一个实例的值变更可以像日志一样查看变化。对于一个给定实例,可以监听这些变化以及触发基于这些变化的函数,非常像类似Kafka这种事件总线系统。Schemaless的触发器使Schemaless成为一个完美的真实来源的数据存储,因为除了随机访问数据,下游的依赖可以运用触发器功能来监听并触发任何应用侧特定的代码(与LinkedIn’s的 DataBus类似),进而实现数据创建与数据处理的解耦。

    在其它用例中,Uber在BASE列写入Mezzanine实例后,使用Schemaless的触发器来进行结账操作。针对上面的例子,当trip_uuid1的BASE列被写入后,我们的支付服务被BASE列的触发器触发,获取这个cell,然后尝试用信用卡支付该行程。无论成功与否,信用卡支付的结果都会回写如Mezzanine的STATUS列。通过这种方式实现了支付服务于行程创建的解耦,Schemaless扮演了一个异步事件总线的角色。

    索引的易用性

    最后,Schemaless支持在JSON blob中的字段上定义索引。当这些预定义的用于找到cell的字段与查询的参数相匹配时,就会用到索引。索引查询效率很高,因为索引查询只需要访问一个单一的分片来找到需要返回的cell的集合。事实上,查询还可以更深度的优化,因为Schemaless允许非标准化的cell数据直接加入索引中。索引中含有非标准化数据意味着索引查询在查询和取信息操作一起只需要查询一个分片。事实上,我们通常推荐Schemaless用户在可能需要的地方都把非标准数据加到索引当中,除非只需要直接用row key查询并取回cell。在某种意义上,这样一来我们用存储交换来了快速查询的性能。

    作为Mezzanine的一个例子,我们定义个了一个副索引来查询指定司机的所有行程。我们将行程的创建时间以及行程发生的城市非标准话加入到索引中。这样就可以查询一个司机在一个城市中一段时间中的所有行程。下面我们给出了driver_partner_index YAML 格式的定义,这是行程数据存储的一部分,定义在BASE列上 (这个例子用标准#符号添加了注释).

    table: driver_partner_index # Name of the index.

    datastore: trips    # Name of the associated datastore

    column_defs:

    – column_key: BASE # From which column to fetch from.

    fields: # The fields in the cell to denormalize

    – { field: driver_partner_uuid, type: UUID}

    – { field: city_uuid, type: UUID}

    – { field: trip_created_at, type: datetime}

    使用这个索引,通过筛选city_uuid或者trip_created_at,我们能够找出指定driver_partner_uuid的行程。在这个例子中我们只用到BASE列的中的字段,但是Schemaless支持从多个列中非标准化数据,相当于上面column_def列表中的多个实例。

    像上文提到的Schemaless高效的索引得益于基于分片字段将索引分片。因此一个索引的唯一需求是索引中有一个字段是分片字段(例如上例中最先给出的driver_partner_uuid)。该分片字段决定了索引实体应该在哪个分片写入或者读取。原因是我们在查询索引的时候需要提供分片字段。这意味着在查询时,我们只需要查询一个分片来获取索引实体。关于分片字段有一点需要注意的是要选一个分布好的字段。UUID最佳,其次是city ids,不要选状态字段(枚举值)。

    除分片字段外,Schemaless还支持相等、不等以及范围查询的过滤器,同时支持只查询索引字段的一个子集以及根据索引实体指向的row key获取特定列或所有列。现在分片字段必须是不可修改的,因此Schemaless只需跟一个分片交互,但是我们正在探寻如何在没有太大性能开销的情况下让他成为可变的。

    索引具备最终一致性,无论何时我们写入一个cell,我也更新这个索引实体,但是这不发生在同一个事务中。Cell与索引实体通常不在同一个分片上,因此如果我们想要提供一致的索引,就需要在写入操作中引入 2PC ,这会明显加大开销。通过最终一致性的索引,我们避免了这项开销,但是Schemaless的用户可能会看到过期的数据。多数情况下cell变化与相关索引变化之间的延迟能控制在20ms之内。

    总结

    我们给出了一个数据模型、触发器以及索引的概览,这些都是Schemaless的关键功能,我们行程存储引擎的主要组成部分。在后续的文章中,我们将看到一些Schemaless的其它特性来阐明在Uber的基础设施中,它是如何成为服务的好伙伴的:更多的架构,使用MySQL作为一个存储节点,以及我们如何使触发器在客户端成为可容错的。

    更多精彩内容:

    Part 2: 揭秘Schemaless的架构(敬请期待)

    Part 3: 如何使用Schemaless的触发器(敬请期待)

    相关文章

      网友评论

        本文标题:[译]Uber是如何使用MySQL设计可扩展性数据存储的?(一)

        本文链接:https://www.haomeiwen.com/subject/hcerlttx.html