生产环境下的代码迁移:重写Uber无模式数据存储的分片层

生产环境下的代码迁移:重写Uber无模式数据存储的分片层

2014年,Uber Engineering成立无模式,我们的容错和可扩展的数据存储,以促进我们公司的快速发展。作为参考,仅在2016年,我们就部署了40多个Schemaless实例和数千个存储节点。

随着我们业务的增长,我们的资源利用率和延迟也在增长;为了保持schemalless的性能,我们需要一个能够大规模执行的解决方案。在确定我们的数据存储可以获得显著的性能提升,如果我们重写Schemaless的Python工作节点舰队(一种内置支持轻量级并发的语言),我们将产品系统从旧的实现迁移到新的实现- - - - - -这一切都还在生产中。这个过程被称为project Frontless,它证明了我们可以在不中断活动服务的情况下重写海量数据存储的前端。

在本文中,我们将讨论如何将无模式分片层从Python迁移到Go,这个过程使我们能够用更少的资源处理更多的流量,并改善我们的服务中的用户体验。

无模式的背景

Schemaless于2014年10月首次推出项目夹层该计划旨在将优步的核心出行数据库从单一数据库扩展到单个数据库Postgres实例到高可用性数据存储中。

包含核心行程数据的夹层数据存储被构建为第一个Schemaless实例。从那时起,Schemaless的40多个实例已经被部署到许多客户机服务中。(要了解我们内部数据存储的完整历史,请查看我们概述Schemaless的三部分系列。设计体系结构,触发器).

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

该模型易于建立和构建,但不能有效地扩展以满足我们的需求。为了处理额外的并发请求,我们必须启动更多的uWSGI进程,每个进程作为一个新的Linux进程,具有自己的开销,因此固有地限制了并发线程的数量。在围棋中,goroutine被用来构建并发程序。goroutine被设计成轻量级的,是由Go的运行时系统管理的线程。

为了研究在Go中重写schemalless分片层的优化效果,我们创建了一个实验性工作者节点,该节点实现了一个经常使用的高资源消耗端点。重写的结果显示,延迟减少了85%,资源消耗甚至减少了更多。

图1:该图描述了在Frontless中实现的端点的请求延迟中值。

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

设计无正面的架构

成功地改写了优步的技术堆栈,我们需要确保我们的重新实现与我们现有的工作节点100%兼容。我们做了一个关键的决定,根据原始代码验证新的实现,这意味着每个对Python worker的请求都会在新的Go worker中产生相同的结果。

我们估计一个完整的重写需要大约6个月的时间。在整个过程中,在Uber的生产系统中实现的新特性和bug修复会在Schemaless中落地,为我们的迁移创造了一个移动的目标。我们选择了一个迭代的开发过程,这样我们就可以不断地从遗留的Python代码库中验证和迁移特性到新的Go代码库中,每次一个端点。

最初,无前端工作节点只是现有uWSGI无模式工作节点前面的一个代理,所有请求都通过该工作节点传递。迭代将从重新实现一个端点开始,然后在生产中进行验证;当没有观察到错误时,新的实现将开始运行。

从部署的角度来看,Frontless和uWSGI Schemaless worker是一起构建和部署的,这使得Frontless统一地跨所有实例推出,并支持一次验证所有生产场景。

图2:在迁移过程中,一个称为工作者节点的服务中Frontless和Schemaless在一个容器中一起运行。然后Frontless接收请求,并决定它是应该转发给Schemaless还是由Frontless处理。最后,Schemaless或Frontless从存储节点获取结果并将其返回给服务。

读取端点:通过比较进行验证

我们首先着重于在Go中重新实现读端点。在我们最初的实现中,读端点平均处理Schemaless实例上90%的流量,并且消耗了最多的资源。

当在Frontless中实现一个端点时,将启动一个验证阶段,根据Python实现检查其正确性。通过让Frontless和Schemaless工作程序执行请求并比较响应,可以进行验证。

图3:当服务向Frontless发送请求时,它将把请求转发给Schemaless,后者将通过查询存储节点生成响应。然后,Schemaless所做的响应将返回给Frontless, Frontless将其转发给服务。Frontless还将通过查询存储节点创建响应。比较Frontless和Schemaless构建的两个响应,如果出现任何差异,结果将作为错误报告发送给Schemaless开发团队。

使用这种方法验证请求会使发送到存储工作节点的请求数量翻倍;为了使请求的增加变得可行,添加了配置标志来激活每个端点的验证,并调整要验证的请求的百分比阈值。这使得可以在几秒钟内为给定端点的任何部分在生产中启用和禁用验证。

编写端点:自动化集成测试

无模式写请求只能成功一次,所以为了验证这些请求,我们不能使用以前的策略。但是,由于Schemaless中的写端点比读端点要简单得多,所以我们决定通过自动集成测试来测试它们。

我们设置了集成测试,这样我们就可以对fronttless worker和Schemaless worker运行相同的测试场景。测试是自动化的,可以在本地执行,或者通过我们的持续集成在几分钟内执行,这加快了开发周期。

为了大规模地测试我们的实现,我们设置了一个schemalless测试实例,其中测试流量模拟生产流量。在这个测试实例中,我们将写流量从Schemaless的Python实现转移到Frontless,并运行验证以检查单元格是否被正确写入。

最后,一旦实现可以投入生产,我们就可以通过运行时配置将一定比例的写流量在几秒钟内移动到新的实现,从而慢慢地将写流量从Schemaless的Python实现迁移到Frontless worker。

无耻的结果

到2016年12月,mezz数据存储的所有读端点都由Frontless处理。如下面的图2所示,所有请求的中位数延迟降低了85%,p99请求延迟降低了70%:

图4:上面的图演示了我们的数据存储处理Python(红色的是Schemaless工作程序)和Go(蓝色的是Frontless工作程序)请求所需的时间。

在Go实现之后,schemalless CPU利用率下降了85%以上。这种效率的提高让我们减少了所有Schemaless实例中使用的工作节点的数量,基于与前面相同的QPS,这提高了节点利用率。

图5:上图展示了在数据存储中由Python (Schemaless worker,红色)和Go (Frontless worker,蓝色)处理的稳定请求流中的CPU使用情况。

没有前方的未来

Project Frontless表明,我们可以用一种全新的语言重写一个关键的系统,而且没有停机时间。通过重新实现服务而不更改Schemaless的任何现有客户机,我们能够在几天内实现、验证和启用端点,而不是几周或几个月。具体来说,验证过程(将新端点与生产环境中的现有实现进行比较)让我们相信fronttless和Schemaless工作程序将得到相同的结果。

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

如果您对构建具有全球影响的容错和可伸缩的数据存储感兴趣,可以考虑申请on的职位我们的团队

评论