写点什么

放弃 Python,Uber 用 Go 重写 Schemaless 数据库的分片层

Anders Johnsen

  • 2018-11-19
  • 本文字数:3086 字

    阅读完需:约 10 分钟

放弃 Python,Uber 用 Go 重写 Schemaless 数据库的分片层

摘要: 2014 年,Uber 构建了可扩展的容错数据库 Schemaless,但随着业务的增长,原实现方式对资源消耗更多,同时请求延迟也在增加,为了保持 Schemaless 的性能,Uber 在不影响生产服务的情况下用 Go 重写了 Schemaless 数据库的分片层,完成了将产品系统从旧实现迁移到新实现的 Frontless 项目。



2014 年,Uber 工程构建了可扩展的容错数据库Schemaless,为公司的快速发展提供了便利。我们仅在 2016 年就部署了 40 多个 Schemaless 实例和数千个存储节点。


随着业务的增长,我们的资源消耗和延迟也在增长;为了保持 Schemaless 的性能,我们需要一个能够很好的支持大规模应用的解决方案。在明确了假如将现有 Schemaless“集群”的 Python 工作节点用Go(一种支持轻量级并发特性的语言)重写的话,我们的数据库可以获得显著的性能提升后,我们在不影响正常生产的情况下,完成了将产品系统从旧实现迁移到新实现的任务。这一任务被称为 Frontless 项目,它证明了我们可以在不影响生产服务的情况下重写大型数据库的前端。


在本文中,我们会讨论如何将 Schemaless 分片层从 Python 迁移到 Go,这一改变可以使我们用更少的资源来处理更多的流量,从而改善用户对我们服务的体验。

Schemaless 的背景

作为Mezzanine项目,Schemaless 于 2014 年 10 月首次推出,当初计划将 Uber 的核心 trip 数据库从一个独立的Postgres实例迁移到一个高可用的数据库中。


包含核心 trip 数据的 Mezzanine 数据库被构建为第一个 Schemaless 实例。从那时算起,目前已经部署了 40 多个 Schemaless 实例用于众多客户端服务。(关于我们内部数据库的完整历史演进过程,请参阅我们的三篇系列文章,Schemaless 的设计架构triggers概述)。


在 2016 年中,有数千个工作节点在 Schemaless 实例中运行,每个工作节点都消耗大量的资源。工作节点最初是使用 Python 和由NGINX交付的uWSGI应用程序服务器进程中的一个Flask微框架构建的,每个 uWSGI 进程一次处理一个请求。


该模型简单易行,易于建立,但不能有效地满足我们的需求。为了处理额外的同步请求,我们必须增加更多的 uWSGI 进程,每个进程都作为一个需要额外开销的新的 Linux 进程,因而这从根本上限制了并发线程的数量。在 Go 中,goroutines 被用来构建并发程序。goroutine 采用轻量级设计,是由 Go 的运行时系统管理的线程。


为了研究重写 Schemaless 分片层的优化增益,我们创建了一个实验性的工作节点,该节点实现了一个使用频率较高、资源消耗也比较高的端点。重写的结果显示,延迟减少了 85%,资源消耗减少的甚至更多。



图 1:该图描述了 Frontless 形式实现的端点中值请求延迟情况


在进行了这个实验之后,我们明确了重写将使 Schemaless 通过释放 CPU 和内存来支持其所有实例中的依赖服务和工作节点。有了这些知识基础,我们启动了这个 Frontless 项目,用 Go 重写整个 Schemaless 分片层。

Frontless 架构设计

为了成功地重写 Uber技术堆栈的这个重要部分,我们需要确保我们的重新实现 100%与现有的工作节点兼容。我们做了一个关键的决定,以验证新实现与原始代码的关系,这意味着每个对新 Go 工作节点的请求都要得到跟之前对 Python 工作节点请求相同的结果。


我们估计一个完整的重写会花费我们六个月的时间。在此期间,在 Uber 的生产系统中实现的新功能和 bug 修复将在 Schemaless 的情况下进行,所以我们的迁移有了一个动态的目标。我们选择了迭代开发形式,这样我们就可以一次性在一个端点上不断的从遗留的 Python 代码库中迁移出功能,并同时在新的 Go 代码库中验证。


最初,Frontless 工作节点只是在现有的 uWSGI Schemaless 工作节点前面的一个代理,所有请求都通过该节点。迭代将从重新实现一个端点开始,然后在生产中进行验证;当不再有错误出现后,新的实现才会正式上线。


从部署的角度来看,Frontless 和 uWSGI Schemaless 的工作是一起构建和部署的,这使得在所有实例中都可以实现统一的 Frontless,并同时支持所有生产场景的验证。



图 2:在我们的迁移过程中,一个名为 worker 节点的服务,其中 Frontless 和 Schemaless 在同一个容器中运行。Frontless 随后收到请求,并决定是否应该将其转发给 Schemaless,或者由 Frontless 处理。最后,Schemaless 或 Frontless 从存储节点获取结果,并将其返回给服务。

读取端点:对比验证

我们首先聚焦在用 Go 重新实现读取端点上。在我们最初的实现中,Schemaless 实例上读取端点处理平均占用 90%的流量,并且它也是最消耗资源的。


当一个端点用 Frontless 实现后,将会启动验证进程,检测与 Python 实现的差异性。Frontless 和 Schemaless 执行请求操作时便会触发验证并对比响应结果。



图 3:当一个服务发送请求到 Frontless 时,它会将请求转发给 Schemaless,该请求将通过查询存储节点生成响应。然后,由 Schemaless 做出的响应将返回到 Frontless,并将其转发给服务。Frontless 还将通过查询存储节点来创建响应。这两种响应是由 Frontless 和 Schemaless 构建的,如果出现任何差异,结果将作为 bug 报告发送给 Schemaless 开发团队。


使用此方法验证,将使发送到存储工作节点的请求数量增加一倍;为了使请求数量增加后工作正常,我们添加了配置标志来激活每个端点的验证,并调整请求验证的百分比阈值。这样便可以在几秒内启动或禁用对指定端点任意部分的验证功能。

写入端点:自动集成测试

Schemaless 的写入请求只能一次性成功,所以为了验证这些我们不能再使用以前的策略了。然而,由于与读取端点相比,在 Schemaless 中写入端点要简单得多,因此我们决定通过自动化集成测试来测试它们。


我们建立起了集成测试环境,这样 Schemaless Python 和 Frontless Go 就可以运行相同的测试场景了。测试是自动化的,可以在本地执行,也可以在几分钟内通过持续的集成来执行,这可以加快开发周期。


为了规模化测试我们的实现,我们设置了一个 Schemaless 测试实例,其中流量测试模拟了生产流量。在这个测试实例中,我们将 Schemaless 的 Python 流量写入实现迁移到了 Frontless 上,并运行验证来确保写入的正确性。


最后,一旦所有实现都满足生产环境时,我们就可以通过运行时配置将 Schemaless 的 Python 实现的流量写入功能缓慢地迁移到 Frontless 上,这样便可以在几秒钟内将部分流量写入工作移动到新的实现中。

Frontless 的成果

到 2016 年 12 月为止,所有的 Mezzanine 数据库都是由 Frontless 处理的。如图 4 所示,所有请求的中值延迟降低了 85%,p99 请求延迟降低了 70%:



图 4:上图展示了由 Python(Schemaless 的工作语言,用红色表示)和 Go(Frontless 的工作语言,用蓝色表示)实现时数据库请求处理的时间。


随着我们 Go 的实现,Schemaless 的 CPU 使用率下降了 85%以上。这种效率的增加让我们减少了在所有 Schemaless 实例中使用工作节点的数量,这些节点也是基于与以前相同的 QPS,这从而提高了节点利用率。



图 5:上面的图展示了在我们的数据库中由 Python(Schemaless 工作语言,红色的)和 Go(Frontless 的工作语言,蓝色的)处理的一个稳定的请求流中的 CPU 使用情况。

Frontless 的未来

Frontless 项目表明,我们有可能在零停机的情况下,用一种全新的语言重写一个关键系统。通过重新实现服务而不改变 Schemaless 的现有客户端,我们能够在几天内而不是数周或几个月内实现、验证和启用端点。重点是,验证过程(新的端点实现与现有生产中的实现进行比较)给了我们信心,因为 Frontless 和 Schemaless 可以得到相同的结果。


然而,最重要的是,我们在生产中重写关键系统的能力证明了 Uber 迭代开发过程的可伸缩性。


原文链接:https://eng.uber.com/schemaless-rewrite/


2018-11-19 09:002258

评论 1 条评论

发布
用户头像
难道我大python没有未来了吗?
2018-11-21 17:33
回复
没有更多了
发现更多内容

言简意赅,聊聊RPC的123事

一叶而不知秋

RPC

央行:取消境外机构投资者额度限制,推动金融市场进一步开放

kimmking

一种消息消费失败的处理方案

redis kafka

Flutter 中SharedPreferences 的初始化?

三爻

flutter android

游戏夜读 | 有哪几种游戏玩家?

game1night

源码分析shared_ptr实现

泰伦卢

c c++ C#

IT人的职业提升只靠技术精进行的通吗?

MavenTalker

程序员 程序人生 职业规划 技术人

开源一个自研的基于Netty的高性能网络通信框架

Geek_373e87

TCP dubbo Netty

快速开发那点事儿 | 选择最合适的轮子,量产跑车

Java_若依框架教程

敏捷开发 JAVA开发 RouYI 快速开发

没有业务规则的技术实现

wd

吐槽

算法篇之数组右移

月夜

算法 数组右移

地址收录

gogogo

关于架构的几件小事:架构是什么

北风

架构 企业架构

讨论:应不应该用存储过程?

kimmking

Flink 作为现代数据仓库的统一引擎:Hive 集成生产就绪!

Apache Flink

大数据 flink 流计算 实时计算

自助设备系列——自助设备的春天

孙苏勇

产品

MyBatis 3 解析Mapper映射配置

杨家昌

mybatis 配置 Mapper XMLMapperBuilder

为什么很多技术都觉得前端很简单?

极客时间

大前端

spring boot 集成 elasticsearch 7.x (一)

wd

elasticsearch high level client

new[]和delete[]为何要配对使用?

泰伦卢

c c++ C#

MyBatis 3 解析mybatis-config.xml配置

杨家昌

深度解析 mybatis 初始化 XMLConfigBuilder Configuration

MyBatis 3 XML配置

杨家昌

mybatis mybatis-config.xml XML配置 配置

hive 建表语句迁移

飞哥

小米手机广告在日本「炸」了,背后的教训是什么?

葛仲君

产品 本地化 小米 日本 文化

中年危机,我们如何面对?

石云升

基于大疆无人机SDK二次开发

sydMobile

android 无人机

【预告】千亿数据的潘多拉魔盒:从分库分表到分布式数据库

kimmking

分布式数据库 分库分表 分布式系统 中间件

专题:快速开发那点事儿 | 轮子怎么选

Java_若依框架教程

Java 敏捷开发 Ruoyi 快速开发

这样理解Java中的函数式编程就对了

jerry

java8 函数编程

第一篇InfoQ的博客

程序员小岑

写作 体验

iTerm2使用小技巧-密码管理器

小菜与老鸟

iTerm

放弃 Python,Uber 用 Go 重写 Schemaless 数据库的分片层_软件工程_InfoQ精选文章