为什么我们要在Uber的微服务架构中利用多租户

0
为什么我们要在Uber的微服务架构中利用多租户

优步服务的表现依赖于我们快速稳定地推出新功能的能力我们的平台,而不管相应的服务位于我们的技术堆栈中的哪个位置。我们平台强大的基础在于它microservice-based架构,这是一种常用的结构风格,其中应用程序由互操作服务组成。

微服务体系结构可以支持稳定的部署和模块化,以可扩展性而闻名。由于Uber的不同工程团队致力于互操作服务,因此确保我们的堆栈既能安全地推出新的更改,又能以模块化的方式可靠地重用架构的某些部分非常重要。总的来说,这些功能允许较高的开发速度和快速的发布周转时间,使我们能够在满足服务水平协议(sla)的同时,灵活地根据独立的时间表进行构建。

促进这种稳定性和模块化的最有效方法之一是允许多个系统共存于一个微服务架构中,通常被称为多租户.使用租户,可能是测试金丝雀的版本影子系统,甚至服务层或产品线,使我们能够保证代码隔离,并根据流量租用做出路由决策。流动数据(例如,消息队列中的请求或消息)和静止数据(例如,存储或持久缓存)的租约允许隔离和公平性保证,以及基于租约的路由机会。多租户帮助我们在一个简单的微服务堆栈上实现各种功能,包括改进的集成测试框架、影子流量路由、记录和重放流量、用于实验的实时流量的密封重放、容量规划、真实的性能预测,甚至同时运行多条产品线。

综上所述,多租户的这些好处促进了更灵活、可伸缩的微服务架构,从而提高了工作效率并改善了应用程序性能,使工程师和平台用户都受益。

Microservice景观

微服务架构允许团队独立于其他服务,为他们的服务推出新功能和错误修复,从而提高开发速度。例如,假设一个团队拥有四个服务(统称为系统1),这些服务具有一致的sla,并定期与具有自己sla的多个其他服务进行交互。

在下面的图1中,我们演示了团队的四个微服务(服务A、B、C和D)是如何交互的。在这个图中,服务A接收来自系统2的请求。系统1通过连接到服务B来处理请求,而服务B又连接到服务C和服务D。

系统示意图
图1。在系统1中,服务A通过连接到服务B来处理来自系统2的请求,而服务B又与服务C和服务D交互来完成请求。

在这个例子中,如果我们对服务B进行了更改,我们必须确保它仍然与服务a、C和d进行良好的互操作。在微服务架构中,我们需要执行这些集成测试场景来测试服务与系统中其他服务的交互。一般来说,有两种基本的方法来进行微服务架构的集成测试:并行测试和生产测试。

并行测试

第一种方法,并行测试,需要创建一个登台环境,看起来和感觉上都像生产堆栈,但只用于处理测试流量。如下图2所示,这个堆栈总是打开并运行生产代码,尽管它与生产堆栈隔离,并且通常以较小的规模运行:

系统和测试堆栈的图表
图2。并行测试要求工程师创建一个登台环境来处理测试流量,并确定生产堆栈是否满足sla。

在并行测试期间,实现对生产服务的更改的工程团队首先将带有新代码的服务部署到测试堆栈。这种方法允许开发人员在不影响生产的情况下稳定地测试任何服务,从而更容易在发布前识别和控制bug和其他问题。

并行测试要求测试流量永远不会泄漏到生产堆栈,这可以通过将两个堆栈物理隔离到单独的网络中,并确保测试工具只在测试堆栈中操作来实现。

虽然并行测试是一种非常有效的集成测试方法,但它也会带来一些挑战,这些挑战会影响微服务架构的成功:

  • 额外硬件成本:必须为测试提供整个堆栈,以及所有的数据存储、消息队列和其他基础设施组件,这意味着额外的硬件和维护成本。
  • 同步问题:测试堆栈只有在与相应的生产堆栈相同时才有用。随着两个堆栈彼此偏离,测试堆栈镜像生产堆栈变得越来越困难,并且基础设施组件有额外的负担来保持堆栈同步。
  • 不可靠的测试:当团队将他们的实验性和可能存在bug的代码部署到测试堆栈时,这些服务可能无法正确操作,导致测试经常失败。例如,假设拥有服务A的团队触发了一个并行测试,以查看他们的新代码是否有效,但测试失败是因为服务b中的一个错误。因为我们在一个与我们的产品构建完全不同的构建中进行测试,因此很难诊断错误在哪里;此外,在测试通过整个流程之前,我们不知道对服务A所做的更改是否安全,这意味着在拥有服务B的团队将干净的代码部署回测试堆栈之前,我们会被阻止。通过使用路由框架将流量路由到另一个被测服务实例化的沙箱环境,可以缓解这种特殊的缺点。
  • 容量测试不准确:为了评估整个堆栈或子堆栈的容量,我们需要在测试堆栈上推入测试负载。如果我们想测试特定的容量,我们必须增加测试堆栈的容量,然后才能将增量负载应用到测试堆栈上,这意味着目标容量超过当前生产负载的增量。这个增量负载可能无法使测试堆栈饱和,因此不清楚我们应该向生产堆栈添加多少容量才能达到目标容量。

生产测试

在微服务架构中进行集成测试的另一种方法是使当前的生产堆栈成为多租户,并允许测试和生产流量通过它。下面的图3显示了这样一个例子:

将流量路由到测试组件的系统图
图3。使用多租户生产堆栈,我们可以在生产服务的同时测试新的或更新的微服务。

这种雄心勃勃的方法意味着我们必须确保堆栈中的每个服务都能够处理生产请求和测试请求。

在这种方法中,由于我们正在测试服务B,测试构建是在一个隔离的沙箱区域中实例化的,该区域允许访问生产服务C和d。我们将测试流量路由到服务B’,即测试构建,而生产流量则像往常一样流经生产实例。服务B的服务只测试流量,不测试生产流量。同样,确保生产流量不以任何方式受到测试流量的影响也是至关重要的。

尽管这是一个简化的视图,但它有助于解释多租户如何帮助解决集成测试。在生产测试中出现了两个基本需求,它们也构成了多租户体系结构的基础:

  • 交通路由:能够根据流经堆栈的流量类型来路由流量。
  • 隔离:能够在测试和生产之间可靠地隔离资源,从而在业务关键型微服务中不会产生副作用。

这里的隔离需求特别广泛,因为我们希望隔离所有可能的静态数据,包括配置、日志、指标、存储(私有或公共)和消息队列。这种隔离需求不仅适用于被测试的服务,而且适用于整个堆栈。

多租户为集成测试之外的其他用例铺平了道路,例如分期部署和在堆栈中重放流量。

金丝雀的部署

当开发人员对他们的服务进行更改时,即使该更改经过了良好的审查和测试,我们也可能不希望一次性将更改部署到服务的所有运行实例中。这是为了确保在所做的更改出现问题或错误时,整个用户群不会受到攻击。这个想法是先将更改滚出到一个较小的实例集,称为金丝雀。然后,我们用反馈循环监视金丝雀,并逐渐广泛地推出代码更改。

canary可以被视为多租户体系结构中的另一个租户,其中canary是一个请求的属性,可用于做出路由决策。使用金丝雀,资源在部署期间是隔离的。在任何给定的时间,服务都可能部署了一个金丝雀,所有金丝雀流量都将路由到该金丝雀。根据请求本身的属性(如用户类型、产品类型和用户位置),可以更接近体系结构的边缘来决定是否将请求作为金丝雀进行采样。

捕获/重放和影子流量

在服务于实际生产交通的同时,能够看到服务票价的变化是获得正在进行的更改的安全性的强烈信号的有效手段。在密封安全的环境中重播以前捕获的实时流量或重播实时生产流量的影子副本是多租户的另一个用例。

将流量路由到测试组件的系统图
图4。创建用于测试服务的影子流量涉及将生产流量的副本从生产堆栈路由到安全的测试区域。

在这种情况下,我们对被测试实例发出的任何出站调用的响应存根。捕获和重放生产流量可以被视为集成测试的一个子类别,因为这些用例属于测试和实验领域。

重放流量在技术上是测试流量,可以是测试租户的一部分,允许与其他租户隔离。对于重放流量,我们可以灵活地分配单独的租赁,以进一步与其他测试流量隔离。

因此,多租户体系结构的一个重要特性是它能够保护和隔离多个业务关键产品线或不同层的用户基础。

Tenancy-oriented架构

在面向租户的微服务体系结构中,租户被视为第一类对象.租赁的概念与运行中的数据和静止的数据都有关系。构建多租户微服务架构涉及到将上下文附加到传入请求并在整个请求生命周期中传播该上下文,这使得用户能够基于该上下文路由请求。

租赁上下文

由于微服务架构是一组运行在相互连接的网络上的不同服务,因此我们需要能够将租约上下文附加到执行序列上。当请求进入边缘网关时,我们可以将该上下文附加到请求上,它告诉我们请求的租赁权。我们希望此上下文在请求的整个生命周期内与请求保持一致,并将其传播到在同一业务逻辑上下文中生成的任何新请求,从而为请求序列保留租约。

下面是一个简单的租赁上下文格式和一些示例:

{" request-tenancy ": /< tenant -id>/< tenant -tags>…}

例子:

" request-tenancy ": " product-foo/production "
" request-tenancy ": " product-bar/production/canary "
" request-tenancy ": " product-bar/production/health probe "
" request-tenancy ": " product-foo/testing/TID1234 "
" request-tenancy ": " product-bar/testing/shadow/SID5678 "

上下文传播

一般来说,当调用链中的任何服务接收到请求时,我们都希望其租赁上下文可用,因为服务可以利用租赁上下文作为其业务逻辑的一部分。但是,服务需要进一步传播上下文,因为它会发出更多的请求,作为处理同一传入请求的一部分。

大多数服务可能不需要查看租赁上下文,但是有些服务可能会评估请求上下文以绕过某些业务逻辑。例如,验证用户电话号码的审计服务可能希望绕过测试流量检查,因为测试请求中涉及的用户是测试用户。此外,当通过与银行网关通信以为用户转移资金的事务处理服务运行测试流量时,我们可能希望去掉银行网关,或者与银行的登台网关(如果有一个可用于测试)通信以防止任何真正的资金转移。租赁上下文传播可以使用开源工具来实现,例如OpenTracing而且Jaeger支持以语言和传输无关的方式进行分布式上下文传播。

租赁上下文还应该传播到其他正在运行的数据对象,例如对象中的消息Apache卡夫卡消息队列。新版本的Kafka支持添加报头,开源跟踪工具可用于为消息添加上下文。

我们可能还希望将租赁上下文传播到静态数据,包括服务用于存储持久数据的所有数据存储系统,如MySQL、Apache Cassandra和AWS。像Redis和Memcached这样的分布式缓存也可以归类为静态数据。架构所利用的所有存储系统和缓存都需要以合理的粒度支持存储上下文和数据,以允许基于租户上下文检索和存储数据。在较高的级别上,静态数据组件的唯一要求是能够基于租赁隔离数据和流量。

具体如何隔离数据以及如何将租户上下文与数据一起存储是特定于存储系统的实现细节。

Tenancy-based路由

一旦我们能够用租赁标记请求,我们就可以根据其租赁路由请求。这种路由对于生产、记录/重放和影子流量中的测试至关重要。此外,canary部署需要将金丝雀请求路由到运行在隔离金丝雀环境中的特定服务实例。

在确定无缝工作且没有开销的路由解决方案时,考虑部署和服务技术堆栈是很重要的。在选择车队范围的路由解决方案时,可能需要考虑编写服务所用的语言以及它们用于相互通信的传输和编码。开源服务网格工具特使Istio也非常适合提供基于租户的路由,这种路由不依赖于所使用的服务语言和传输或编码。

一般来说,基于租户的路由可以在服务的出口或入口实现。在出口,服务发现层可以帮助确定根据请求的租赁与哪个服务实例进行通信。或者,可以在入口处做出路由决策,然后将请求重新路由到正确的实例,如下图5所示:

系统和两个测试组件的示意图
图5。我们可以在服务的入口应用租赁路由,在上面的示例中,将测试流量从生产服务Ap发送到它的测试实例A1。

在图5所示的示例中,如果请求租约为,则可以使用sidecar将请求转发到测试实例测验.sidecar可以是一个进程,充当进入服务的所有流量的代理,并且与服务位于同一位置。流量首先由服务的sidecar接收,在那里我们可以检查请求的租赁上下文,并根据上下文做出路由决策。

根据要处理的用例,我们需要在租赁上下文中添加元数据。例如,对于生产中的测试,如果服务正在测试,我们希望将测试流量重定向到服务的测试实例。我们可以在允许这种行为的上下文中添加额外的信息,如下所示:


" request-tenancy ": /< tenant -id>/< tenant -tags>…
" services_under_test ": [
" foo ": {
" redirect ": <测试实例Id>,
},
...

当我们做出路由决定时,我们可以检查是否request-tenancy测验如果请求接收者是services_under_test.如果满足这些条件,则将请求路由到<测试实例Id>

数据隔离

我们希望构建一个架构,其中每个基础设施组件都能理解租赁,并能够基于租赁路由隔离流量,允许在我们的平台内更好地控制运行不同的微服务,如度量和日志。微服务架构中使用的典型基础架构组件有日志记录、度量、存储、消息队列、缓存和配置。基于租户隔离数据需要单独处理基础设施组件。例如,我们可能希望开始将租赁上下文作为服务生成的所有日志和指标的一部分。这有助于开发人员根据租约进行过滤,这可能有助于避免错误警报或防止启发式或训练数据变得倾斜。

类似地,在考虑存储服务时,需要考虑底层存储体系结构,以便有效地在租户之间创建隔离。一些存储架构倾向于多租户。有两种高级方法,一种是在数据旁边显式地嵌入租赁权的概念,并将不同租赁权的数据放在一起,另一种是根据租赁权显式地分离出数据,如下面的图6所示:

多租户系统示意图
图6。使用租赁将数据和消息队列测试流量路由到被测组件,可以隔离此测试流量,使其不会干扰生产系统。

后一种方法提供了更好的隔离保证,而前一种方法通常需要较少的操作开销。对于像Kafka这样的消息队列系统,我们既可以透明地为租期推出一个新主题,也可以为该租期奉献一个单独的Kafka集群。

为了实现数据隔离,需要将上下文传播到基础结构组件。确保服务在数据隔离方面的开销最小化是很重要的。理想情况下,我们希望服务不显式地处理租赁。此外,理想情况下,我们希望将隔离逻辑放置在所有数据流经的中心阻塞点。网关就是这样一个可以实现隔离逻辑的瓶颈,也是我们首选的方法。客户端库可以是实现基于租户的隔离的另一种选择,尽管编码语言的多样性使得在所有特定于语言的客户端库之间保持逻辑同步有点困难。

与配置隔离类似,我们希望服务的配置数据是特定于租赁者的,从而确保一个租赁者的配置更改不会影响其他租赁者。

前进

基于微服务的架构仍在不断发展,并成为开发人员和组织的敏捷性的辅助工具。精心规划的多租户体系结构可以提高开发人员的工作效率,并支持不断发展的业务线。

Uber的多租户实现已经获得了各种好处,例如使代码和配置的自动推出变得安全,这反过来又提高了开发人员的速度。多租户架构的隔离保证使Uber能够重新利用相同的微服务堆栈用于各种目的,包括测试流量。

有兴趣建设基础设施,每天在世界各地的城市支持1400万次骑行吗?考虑加入我们的团队吧!

一幢公寓楼的特征图是由PexelsPixabay

评论