您的位置:首页 > 其它

Uber无模式数据存储

2016-03-17 16:16 141 查看

Uber无模式数据存储

设计无模式。Uber工程师使用MySQL定制数据库,允许我们从2014向后扩展。这是无模式三部分系列的第一部分。

在项目Mezzanine中,我们描述了如和从单一Postgres实例迁移Uber核心到无模式、容错以及高可用的数据库。这篇文章进一步描述其结构以及扩展已经存在Uber基础设施的无模式的作用,以及怎么走过来的。

新数据库竞赛

早在2014年,由于蓬勃发展的行程增长,我们用尽了数据库空间。每个新城市和行程里程碑将我们推向悬崖,那时,我们意识到Uber的基础设施将会年底无法工作:我们无法在Postgres上存储足够的行程数据。我们的任务是为Uber实现下一代数据库技术,这个任务花了多个月的时间,涉及到来自世界各地的工程师。

但是首先,为什么当丰富的商业和开源软件已经存在,还需要搭建一个可扩展的数据存储?对对新的形成数据存储我们有五个关键需求:

我们新的结果通过增加更多服务需要能够线性增加的能力,Postgres的一个特性是缺乏设置。增加服务应能增加可用的磁盘存储,以及减少系统响应时间。

我们需要写的可用性。我们之前实现了一个简单带有Redis的缓冲机制,因此如果些到Postgres失败,我们能够待会重试,因为在此期间行程被存储到Redis。但是在Redis,形成不能从Postgres中读取,我们失去了功能,例如计费。闹心的,是最后我们没有丢失行程。随着时间的推移,Uber增长,因此基于Redis的结果不能扩展。无模式提供了一个相似的机制作为Redis,但是在read-your-write语句上偏爱写的可用性。

我们需要一种通知下游依赖的方式。在当前的系统,我们同时处理许多行程组件(如,billing,分析等等)。这是一个容易出错的处理:如果任何一步出错,我们不得不再重试一遍,尽管一些组件成功运行。这不可扩展,所以我们希望打破这些步骤为单独的步骤,由数据改变启动。我还有一个异步行程事件系统,但是他是基于Kafka 0.7的。我们不能无损的运行它,因此我们引进一个新的、类似的但是可以无损运行的系统。

我们需要第二索引。正如我们从Postgres迁移时,新的存储系统必须支持Postgres索引,这意味着第二索引以相同方式搜索行程。

我们需要系统操作的信任,因为他包括关键任务的行程数据。如果我们在凌晨3点时分页,数据存储没有响应查询,记录业务。我们能够有操作知识快速修复它?

鉴于上述情况,我们分析了一些替代常用系统,如Cassandra,Riak和MongoDB等等的优点和潜在限制。为了说明,下表显示了不用系统选项能力的不同组合:



虽然中三个系统都能够通过线上添加新节点线性扩展,只有一组系统能够在故障期间接受写入。这些方案都没有内置的方式通知下游依赖的改变,因此我们需要在应用水平实现这些。他们都有索引,但是如果你需要索引多个不同的值,查询非常缓慢,因为他们使用scatter-gather的模式查询所有节点。最后一些我们有单集群使用经验的系统,并没有提供面向用户的网络traffic,并且没有与我们服务链接的各种业务问题。

最后,我们面对使用系统操作信任的问题,因为他包含关键任务的行程数据。替代的解决方案或许能够在理论上可靠运行,但我们是否有操作经验能够直接执行他们最大能力,极大的影响了我们的决定,我最后决定在Uber使用的情况下开发适合我们的解决方案。这不仅依赖于我们使用的技术,而且依赖在团队中使用的经验。

我们应该注意,因为我们调查选项超过两年多,没有发现合适行程存储的使用情况,我们在我们基础设施的其他领域成功地采用了Cassandra和Riak,并且在生产用使用以服务数以百万规模的用户。

我们相信无模式

由于上述选项在我们给出的时间框架下没有满足我们的需求,我们决定构建我们自己的操作简便的系统,同时吸取别人的教训应用scaling。设计是Friendfeed启发的,和重点业务方面由Pinterest启发。

我们最终建立一个key-value存储,在无模式的方式下(因此得名),允许你保存任何JSON数据不需要严格的模式验证。具有由MySQL共享的append-only模式,并带有缓冲写入以支持MySQL主节点出错,和一个当数据改变时通知发布订阅的功能。最后,无模式支持数据全局索引。下面我们讨论数据模型和一些关键特征的概述,包括Uber行程的分解,为了后续文章保留更深的例子。

无模式数据模型

无模式是一个只追加的稀疏的三维持久的hash map,和google的Bigtable非常相似。无模式中,最小的数据实体被称为一个cell,并且是不可变的;一旦写入,不能覆盖写和删除。cell是一个JSON由row key,column name,和一个名为ref key的引用key组成的blob,其中colum name是一个字符串,引用key是一个整数。

你可以认为row key是关系型数据库中的主键,column name是一个column。然而,在无模式中,没有预定义或强制模式,并且row不需要共享column name;实时上,column name完全由应用程序定义的。ref key用于确定给定row key 和列的cell版本。因此如果一个cell需要更新,你可以编写一个更大的ref key写入一个新的cell(最新的cell是有最高ref key的那个)。ref key也用于列表条目,但是通常用于版本。应用决定使用那个schema。

应用程序通常对相关数据中相同的列分组,然后没列种所有cell有大致相同的应用侧的schema。这种分组是一中捆绑改变数据的好方法,而且在数据库侧它允许应用迅速改变schema不需要停机时间。下面的例子讲更多的阐述这个问题。

例:无模式行程数据存储

在我们深入讨论我们如果在无模式中模型化一个行程之前,让我们看看Uber行程的分解。行程数据是不同时间点产生的,从搭乘,离去到付费,而这些不同部分的信息,由于人们反馈行程或者后台程序执行,是异步到达的。下图是一个简化的Uber行程:



一个行程是由合作伙伴,用户搭载,以及一个从开始到结束的时间戳。这些信息构成几本的行程,由此我们可以计算行程的花费(票价),这是用户的收到的账单。在行程结束之后,我们可能需要调整票价,要么信用卡或扣款。我们或许对这段行程添加评价,从用户或者车主(上图型号所示)得到反馈。或者,如果第一张过期或者拒绝的情况,我们可能需要尝试多张信用卡支付。Uber的行程流程是一个数据驱动的过程。随着数据变的可用或者被添加,那么确定一系列行程执行流程。这方面的信息,例如用户或车主的评分(上面注释的部分),在行程结束后几天收到。

那么,如何映射上述行程模型为无模式?

行程数据模型

使用斜体表示UUID,大写表示column name,下表显示一个行程存储的一个简化版本的数据模型。我们有两个行程{ trip_uuid1 ,trip_uuid2 }和四个列(BASE, STATUS, NOTES, and FARE ADJUSTMENT)。一个cell代表一个带有数字和一个JSON blob的格子(缩写为{…})。格子的叠加表示版本(即,不同的ref key)。



trip_uuid1有三个cell:一个在BASE列,两个在STATUS列,FARE ADJUSTMENT列为空。trip_uuid2有两个cell在BASE列,一个在NOTES列,同样的FARE ADJUSTMENT列为空。对于无模式,列是不同的,因此列的语法有应用定义的,在这种情况下是Mezzanine服务的。

在Mezzanine中,BASE列的cell包括基本行程信息,例如车主的UUID以及行程的时间。STATUS列包含行程当前的支付状态,我们为行程的每一次支付插入一个cell。(如果信用卡没有足够资金或者透支了,尝试可能失败)。如果有任何由车主或者Uber DOps (Driver Operations employee)离开关联的评价,NOTES列包含一个cell。最后如果票价被调整了,FARE ADJUSTMENT列包含cell。

使用切分后的列以避免数据竞争,并且最小化需要写入更新的数据数量。BASE列当一个行程完成是写入,因此通常只写入一次。STATUS列在BASE列的数据已经写入后,当尝试支付行程的行为发生,若支付失败可能发生多次时写入。NOTES列在BASE写入后的某个时间点可能被写入多次,但是它和STATUS列写入是完全分开的。相似的,如果行程费用改变,例如由于低效的路线,FARE ADJUSTMENTS只写入一次。

无模式触发器

无模式一个关键的特征是触发器,具有注意到改变转换为无模式实例的能力。因为cell是不可变的,并且新版本是追加的,每个cell也代表一个变化或者一个版本,允许实例中的值被视为更改日志。对一个给定的实例,监听这些变化,并基于它们触发功能是可能的,和事件bus系统如Kafka非常相似。

无模式触发器使得无模式完善了实测数据存储,因为除了数据随机存储,下游依赖可以使用触发功能监控,并触发任何特定应用的代码(相似的系统是Linkedin的DataBus),进行去耦数据的创建和处理。

对于其他使用案例,当BASE列写入到MEzzanin实例,Uber使用无模式触发为行程生成账单。鉴于上面的例子,当trip_uuid1 BASE列写入,在BASE列触发支付服务选取这个cell并尝试通过控制信用卡支付这次行程。控制信用卡的结果,不论成功或失败,写回到Mezzanine的STATUS列。这种方式的支付服务减弱行程的创建,无模式作为一个异步事件Bus。



索引便于访问

最后,无模式支持在JSON blob中字段定义的索引。索引是一个通过这些预定义字段的查询,以发现匹配查询参数的cell。查询这些索引是效率的,因为索引查询只需要到单一碎片发现cell集合并返回。事实上,查询可以进一步优化,因为无模式允许cell数据使用非规范化到索引。具有非规范化数据的索引意味着一个对于查询和检索信息,索引查询只需要去顾及一个碎片。实际中,我们通常推荐无模式用户规范化数据,用户可能认为除了直接通过row key直接检索cell情况外,查询任何信息需要索引。某种意义上来讲,因此为快速查询查找换取储存。

对于一个Mezaamine的例子,我们有一个定义为运行我们发现给定车主行程的辅助索引。我们已经非规范化行程创建时间和行程开始的城市。这使得在给定时间范围内查询某个城市车主的所有行程称为可能。下面我们给出以YAML形式的driver_partner_index定义,这是行程存储和定义在BASE列(本例标注#的注释)的一部分。

使用这个索引,对于由city_uuid 和/或trip_created_at过滤发现给定driver_partner_uuid所有行程。在这个例子中,我们只使用BASE列的字段,但是无模式支持多列的规范化数据,等同于上面的column_def列表中的多实例。

正如前面提到的无模式具有高效的索引,基于分片字段由分片索引实现。因此对索引的唯一要求是索引的字段之一是被指定为分片字段(上例中,为driver_partner_uuid,因为它最先给定)。分片字段决定那个分片实例应该被写入或检索。原因是当我们查询索引时需要提供分片字段。这意味着在查下你期间,我们需要顾及检索索引实例的一个分区。关于分区字段有一点要注意的他应该有一个良好的分布。UUID是最好的,city ids是次优的,并且状态字段(枚举)不利于存储。

除了分片字段,无模式支持相等,不等和过滤范围查询,并且支持在索引中指选择字段子集以及对索引实例指向的row key检索特定或所有列。目前,分片字段必须是不可变的,因此无模式总是需要访问一个碎片。但是我们正探索如何使其可变并且不没有大量性能开销。

索引是最终一致的,每当我们写入cell同样也要更新索引实例,但是不会在同一事务发生。cell和索引实例通常不属于同样的分区。因此如果我们能够提供一致的索引,在写入时需要引入2PC,否则会产生显著开销。随着最终一致索引避免了开销,但无模式用户可能在索引中发现过期数据。大多数cell变化和响应索引改变之间的滞后远远低于20ms

总结

给出了数据模型,触发器和索引的概览,所有这些是定义无模式的关键特征,行程存储引擎的主要部件。在以后的文章中,我们将看看无模式的其他一些功能来说明它如何成为一个受欢迎的Uber基础设施的服务:更多的架构,使用MySQL作为存储节点,以及我们如果触发客户端侧的容错。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: