Lyft 微服务研发效能提升实践 | 3. 利用覆盖机制在预发环境中扩展服务网格

本文涉及的产品
云原生网关 MSE Higress,422元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: Lyft 微服务研发效能提升实践 | 3. 利用覆盖机制在预发环境中扩展服务网格

怎样才能提高研发效率?是依赖于各自独立的本地开发测试环境,还是依赖完整的端到端测试?Lyft 的这一系列文章介绍了其开发环境的历史和发展,帮助我们思考如何打造一套适合大规模微服务的高效研发环境。本系列共 4 篇文章,这是第 3 篇。原文:Scaling productivity on microservices at Lyft (Part 3): Extending our Envoy mesh with staging overrides[1]


image.png


本系列介绍的是 Lyft 在面对越来越多的开发人员和服务时,如何高效扩展开发实践,本文是第三篇。



在之前的文章中,我们描述了为快速迭代本地服务而设计的笔记本电脑开发工作流程。在这篇文章中,将详细介绍安全且隔离的端到端(E2E)测试解决方案:预生产共享环境。在深入研究实现细节之前,我们将简要回顾促使我们构建这一系统的问题。


以前的集成环境


在本系列的第 1 部分中,我们介绍了在之前用于多服务端到端测试的工具Onebox。Onebox 的用户需要租用大型 AWS EC2 虚拟机来启动 100 多个服务,以验证修改是否能够跨服务边界工作。这种解决方案为每个开发人员提供了一个沙盒,运行自己版本的 Lyft,控制每个服务的版本、数据库内容和运行时配置。


image.png

每个开发人员都运行并管理自己独立的 Onebox


不幸的是,随着 Lyft 的工程师和服务数量的增加,Onebox 遇到了规模问题(详情见第一篇文章),我们需要找到可持续的替代方案来执行端到端测试。


我们将共享的预发环境视为一种可行的替代品。预发环境与生产环境的相似性让我们充满信心,但我们需要添加缺失的部分,从而提供一个安全的开发环境:隔离。


预发环境(Staging Environment)


预发环境运行与生产环境相同的技术栈,但使用了弹性资源、模拟用户数据以及人造 web 流量生成器。预发环境是 Lyft 一级环境,如果环境变得不稳定,SLO[2]受到影响,随时待命的工程师和开发人员就会提升 SEV[3]。尽管预发环境的可用性和真正的流量增加了端到端的可信度,但如果我们鼓励广泛使用预发环境,就可能会出现一些问题:


  1. 预发环境是完全共享的环境,就像生产环境一样,如果有人将一个故障实例部署到预发集群,就会影响到其他(可能是传递性的)依赖该服务的人。
  2. 交付新代码的方式是将 PR 合并到主线,从而触发一个新的部署流水线。为了测试实验性变更如何在端到端环境中工作,需要承受大量的过程负担:编写测试、代码审查、合并,并通过 CI/CD 进行进展。
  3. 这个繁重的过程可能会导致用户使用“逃生口”:将 PR 分支直接部署到预发环境。当未处理的提交在预发环境下运行时,将进一步放大降低环境稳定性的缺陷问题。


我们的目标是克服这些挑战,使预发环境更适合手工验证端到端工作流。我们想让用户在准备阶段测试他们的代码,而不是被过程所困。如果他们的修订出现问题的话,最小化变更的影响半径。为了实现这一点,我们创建了staging override


Staging Overrides(预发覆盖)


Staging override 是一组用于在预发环境中安全快速的验证用户变更的工具。我们从根本上改变了隔离模型的方法:在共享环境中隔离请求,而不是提供完全隔离的环境。其核心是,我们允许用户重写通过预发环境的请求,并有条件的执行实验代码。大致的工作流程如下:


  1. 在预发环境上创建一个不向服务发现注册的新部署,就是我们所说的卸载部署(offloaded deployment),并且保证向这个服务发出请求的其他用户不会被路由到这个(可能被破坏的)实例。
  2. 基础架构应该知道如何解释在请求头中嵌入的覆盖信息,从而确保覆盖元数据(override metadata)在整个调用图(request call graph)中传播。
  3. 修改每个服务的路由规则,从而可以利用请求头中提供的覆盖信息,根据覆盖元数据指定的规则,路由到对应的卸载部署(offloaded deployment)。


示例场景


假设一个用户想要在端到端场景中测试新版本的onboarding服务。之前基于 Onebox,用户可以启动 Lyft 堆栈的整个副本,并修改相应服务,以验证是否如预期般工作。


如今在预发环境中,用户可以共享环境,但可以替换已卸载的实例,这些实例不会影响到正常的预发流量。


image.png

典型用户向预发环境发出的请求不会通过任何被实时卸载的实例


通过给请求附加特定的头("request baggage"),用户可以选择将请求路由到新实例:

image.png

头部元数据允许用户在每个请求的基础上修改调用流


在本文的其余部分,我们将深入探讨如何构建这些组件来提供集成调试体验。


卸载部署(Offloaded Deployments)


image.png

Lyft 使用 Envoy 作为服务网络代理,处理众多服务之间的通信


在 Lyft,每项服务的每个实例都被部署在一个 Envoy[4] sidecar 旁边,作为该服务的唯一出入口。通过确保所有网络流量都通过 Envoy,我们为开发人员提供了一个简化的流量视图,该视图以一种与语言无关的方式提供了服务抽象、可观察性和可扩展性。


服务通过向其 Envoy sidecar 发送请求来调用上游[5]服务,Envoy 将请求转发到上游的健康实例。我们通过控制平面更新 Envoy 的配置,控制平面基于 Kubernetes 事件通过 xDS API[6]进行更新。


避免服务发现


如果我们希望创建一个不会从网格中正常获取服务流量的实例,我们需要指示控制平面将其排除在服务发现之外。为了实现这一点,我们在 Kubernetes pod 标签中嵌入额外的信息,表示该 pod 已被卸载:


...
app=foo
environment=staging
offloaded-deploy=true
...


然后我们可以修改控制平面来过滤这些实例,确保它们在准备阶段不接收标准流量。


当用户准备在预发环境(本地迭代后)创建卸载部署时,首先必须在 Github 中创建一个 pull request。我们的持续集成将自动启动部署所需的容器镜像构建。然后用户可以利用 Github 机器人显式的卸载部署他们的服务到预发环境:


image.png

我们的 Github 机器人可以从 PR 简单的创建一个卸载部署


通过这一方式,用户可以为某个服务创建独立的部署,与普通的临时部署共享完全相同的环境:与标准数据库的交互,出口调用到其他服务,并且可以被标准的指标/日志/分析系统所观测。对于那些只想ssh到实例并测试脚本或运行调试器而不担心影响预发环境其余部分的开发人员来说,这被证明非常有用。然而,当开发人员可以在手机上打开 Lyft 应用,并确保请求在一个卸载部署中得到 PR 代码的服务时,卸载部署才真正具有威力。


覆盖报头和上下文传播(Override Headers and Context Propagation)


要将请求路由到已卸载的部署,需要在请求中嵌入元数据,以便通知基础设施何时修改调用流。元数据包含想要覆盖的服务的路由规则,以及应该将流量引导到哪些卸载的部署上去。我们决定将这些元数据携带在请求头中,从而对服务和服务所有者保持透明。


不过,我们需要确保头信息可以通过使用不同语言编写的服务在网格中传播。我们已经使用 OpenTracing 报头(x-ot-span-context)将跟踪信息从一个请求传播到下一个请求。OpenTracing 有一个叫做“baggage[7]”的概念,这是一个嵌在跨服务边界的报头中的持久化键/值结构。将元数据编码到 baggage 中,通过请求和跟踪库将其从一个请求传播到下一个请求,使我们能够进行快速的处理。


构造和附加 Baggage


实际的 HTTP 报头是一个 base64 编码的 trace protobuf[8]。我们创建了自己的 protobuf,命名为Overrides,注入到跟踪的 baggage 中,如下代码演示:


syntax = "proto3";
/* container for override metadata */
message Overrides {
  // maps cluster_name -> ip_port
  map<string, string> routing_overrides = 1;
}

我们可以将样本数据结构嵌入到跟踪 baggage 中


from base64 import standard_b64decode, standard_b64encode
from flask import Flask, request
from lightstep.lightstep_carrier_pb2 import BinaryCarrier
import overrides_pb2
def header_from_overrides(overrides: overrides_pb2.Overrides) -> bytes:
    """
    Attach the `overrides` to the trace's baggage and return the new `x-ot-span-context` header
    """
    # decode the trace from the current request context
    header = request.headers.get('x-ot-span-context', '')
    trace_proto = BinaryCarrier()
    trace_proto.ParseFromString(standard_b64decode(header))
    # b64encode the provided custom `overrides` and place in the baggage
    b64_overrides = standard_b64encode(overrides.SerializeToString())
    trace_proto.basic_ctx.baggage_items['overrides'] = b64_overrides
    # re-encode the modified trace for use as an outgoing HTTP header
    return standard_b64encode(trace_proto.SerializeToString())
# create a sample `Overrides` proto that overrides routing for `users` service
overrides_proto = overrides_pb2.Overrides()
overrides_proto.routing_overrides["users"] = "10.0.0.42:80"
with Flask(__name__).test_request_context('/add-baggage'):
    new_header_with_baggage = header_from_overrides(overrides_proto)
    print({"x-ot-span-context": new_header_with_baggage})
    # {'x-ot-span-context': b'Ei8iLQoJb3ZlcnJpZGVzEiBDaFVLQlhWelpYSnpFZ3d4TUM0d0xqQXVOREk2T0RBPQ=='}

如何提取当前的 trace 并覆盖


为了从开发人员那里抽象出这种数据序列化,我们为现有的代理应用程序添加了创建头的工具(请阅读更多关于代理的信息)。开发人员将客户端指向代理,从而可以用用户定义的 Typescript 代码拦截请求/响应数据。我们创建了一个助手函数setEnvoyOverride(service: string, sha: string),它将通过sha查找 IP 地址,创建Override protobuf,编码头部,并确保它被附加到通过代理的每个请求上。


上下文传播(Context Propagation)


上下文传播在任何一个分布式跟踪系统中都很重要。我们需要元数据在请求的整个生命周期内都可用,以确保许多深层调用的服务能够访问用户指定的覆盖。我们希望确保每个服务都能将元数据正确转发到请求流中后续的服务中——即使服务本身并不关心其内容。


image.png

调用图中的每个服务都必须传播元数据以实现完整的跟踪覆盖


Lyft 的基础设施用我们最常用的语言(Python、Go、Typescript)维护标准的请求库,为开发人员处理上下文传播。如果服务所有者使用这些库调用另一个服务,上下文传播对用户就是透明的。


不幸的是,在这个项目推出期间,我们发现上下文传播并不像我们希望的那样普遍。最初经常有用户来找我们,说他们的请求没有被覆盖,罪魁祸首通常是 trace 丢失。我们投入了大量资金,以确保上下文传播能够跨各种语言特性(例如 Python gevent/greenlets)、多种请求协议(HTTP/gRPC)以及各种异步作业/队列(SQS[9])工作。我们还添加了可观察性和工具来诊断涉及 trace 丢失的问题,例如标识没有添加头部的服务出口的指示板。


扩展 Envoy


既然我们已经在请求中传播了重写元数据,就需要修改网络层来读取元数据并重定向到想要的卸载实例。

因为我们所有服务都是通过 Envoy sidecar 发出请求的,所以可以在这些代理中嵌入一些中间件来读取覆盖并适当修改路由规则。我们利用 Envoy 的 HTTP 过滤系统[10]处理请求,因此在 HTTP 过滤器中实现了两个步骤:读取请求头的覆盖信息,并修改路由规则,从而将路由重定向到已卸载的部署。


利用 Envoy HTTP 过滤器跟踪


我们决定创建一个解码过滤器[10],允许我们在请求被发送到上游集群之前解析并对覆盖做出反应。HTTP 过滤系统提供了简单的 API,可以获取当前的目的路由以及正在处理的请求的所有报头。虽然是用 C++实现的,但下面的伪代码反映了基本要点:


def routing_overrides_filter(route, headers):
    routing_overrides = headers.trace().baggage()['overrides'] # {'users': '10.0.0.42:80'}
    next_cluster = route.cluster() # 'users'
    # modify the route if there's an override for the cluster we are going to
    if next_cluster in routing_overrides:
        # the user provided the ip/port of their offloaded deploy in the header baggage
        offloaded_instance_ip_port = routing_overrides[next_cluster] # '10.0.0.42:80'
        # redirect the request to the ORIGINAL_DST cluster with the new ip/port header
        headers.set('x-envoy-original-dst-host', ip_port)
        route.set_cluster('original_dst_cluster')


过滤器使用 Envoy 的跟踪实用程序提取 baggage 中包含的覆盖。虽然过滤器总是可以访问像traceIdisSampled这样的跟踪信息,但我们首先必须修改 Envoy,从而可以提取 baggage 中的信息[11]。合并了这个更改后,过滤器就可以使用新的 API 来提取底层 trace 中的 baggage:routing_overrides = headers.trace().baggage()['overrides']


最初目的集群(Original Destination Cluster)


假设覆盖应用于当前目标集群,则必须将请求重定向到已卸载的部署。我们使用 Envoy 的原始目的地12(ORIGINAL_DST)将请求发送到一个由 baggage 提供的覆盖。


对于我们配置的ORIGINAL_DST集群,最终目的地是由一个特殊的x-envoy-original-dst-host[13]报头决定的,它包含一个 ip/port,如10.0.42:80,HTTP 过滤器可以改变这个报头来重定向请求。


例如,如果请求最初是为user集群准备的,但是用户重写了 ip/port,我们将把x-envoy-original-dst-host更改为所提供的 ip/port。


x-envoy-original-dst-host被修改后,过滤器需要将请求发送到ORIGINAL_DST集群,以确保发送到新的目的地。这一需求促使我们对 Envoy 做出了第二个变更:支持路由可变性[14]。合并此变更后,过滤器就可以改变目标集群:route.set_cluster('original_dst_cluster')


结果


通过卸载部署、传播 baggage 和 Envoy 过滤器,我们现在已经展示了预发覆盖🎉的所有主要组件。


这个工作流程极大改进了端到端测试的开销。我们现在每个月都有 100 个独立的服务部署,与以前的 Onebox 解决方案相比,预发覆盖有以下优点:


  • 环境配置: Onebox 要求用户启动数百个容器并运行定制的种子脚本,需要开发人员花上至少 1 个小时准备环境。通过预发覆盖,用户可以在 10 分钟内通过端到端环境部署某个变更。
  • 低成本基础设施: Onebox 运行的是完全独立于预发/生产环境的技术栈,所以底层基础设施组件(例如网络、可观察性)通常是单独实现的。通过将端到端测试转移到预发环境,由于环境改进为集中维护,降低了基础设施支持的成本。
  • 低成本功能验证: 由于 Onebox 和产品之间的差异,即使在 Onebox 端到端测试之后,用户也经常(合理的)怀疑代码的正确性。预发与生产环境在数据和流量模式方面更为接近,使用户更有信心相信如果变更在预发环境中就绪,也就意味着在生产环境中就绪。


额外的工作


启动预发覆盖是一项涉及网络、部署、可观察性、安全性和开发工具的跨组织工作。以下是一些没有涉及到的额外工作流程:

  • 配置覆盖: 除了在 baggage 中指定路由覆盖外,我们还允许用户在每个请求的基础上修改配置变量。通过修改配置库,赋予 baggage 优先级,让用户在启用全局配置之前为请求设置特性标志。
  • 安全影响: 因为可以指定覆盖路由规则,所以必须锁定过滤器功能,以确保不良行为者不能被任意路由。
未来的工作


展望未来,我们可以通过预发覆盖做更多的事情,让用户重新创建想要验证的端到端场景:


  • 可共享的 baggage: 为用户提供一个集中管理的 baggage 存储,允许持久化一组独特的覆盖(服务foo是 X,服务bar是 Y,标签baz是 Z),通过与团队成员共享确切的场景而改善协作。
  • 覆盖用例: 让我们的基础设施了解其他覆盖,以便让用户控制请求的行为。例如,我们可以使用 Envoy 错误注入[15]将人造延迟注入到请求中,临时启用调试日志记录,或者重定向到不同的数据库。
  • 与本地开发集成: 我们可以允许重写请求,直接将请求重路由到用户的笔记本电脑,而不是要求用户在准备阶段启动他们的 PR 实例。


请继续关注我们系列中的下一篇文章,我们将展示如何在交付阶段使用自动化验收测试对生产部署进行验收!


References:

[1] Scaling productivity on microservices at Lyft (Part 3): Extending our Envoy mesh with staging overrides: https://eng.lyft.com/scaling-productivity-on-microservices-at-lyft-part-3-extending-our-envoy-mesh-with-staging-fdaafafca82f

[2] Service Level Objective: https://en.wikipedia.org/wiki/Service-level_objective

[3] Severity Levels: https://response.pagerduty.com/before/severity_levels/

[4] Envoy Proxy: https://www.envoyproxy.io/

[5] Terminology: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/intro/terminology#:~:text=Upstream%3A%20An%20upstream%20host%20receives%20connections%20and%20requests%20from%20Envoy%20and%20returns%20responses.

[6] xDS protocol: https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol

[7] Tags, logs and baggage: https://opentracing.io/docs/overview/tags-logs-baggage/

[8] lightstep_carrier.proto: https://github.com/lightstep/lightstep-tracer-protos/blob/13cdeec9bd4a0ba2cd7062fecde3a057071edcb8/src/lightstep/lightstep_carrier.proto

[9] Amazon Simple Queue Service: https://aws.amazon.com/sqs/

[10] HTTP filters: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/http/http_filters

[11] tracing: add baggage methods to Tracing::Span: https://github.com/envoyproxy/envoy/pull/12260

[12] Original destination: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/original_dst

[13] Original destination host request header: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/original_dst#original-destination-host-request-header

[14] http: support route mutability: https://github.com/envoyproxy/envoy/pull/15266

[15] Fault Injection: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/fault_filter

目录
相关文章
|
2月前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
87 1
|
5天前
|
搜索推荐 NoSQL Java
微服务架构设计与实践:用Spring Cloud实现抖音的推荐系统
本文基于Spring Cloud实现了一个简化的抖音推荐系统,涵盖用户行为管理、视频资源管理、个性化推荐和实时数据处理四大核心功能。通过Eureka进行服务注册与发现,使用Feign实现服务间调用,并借助Redis缓存用户画像,Kafka传递用户行为数据。文章详细介绍了项目搭建、服务创建及配置过程,包括用户服务、视频服务、推荐服务和数据处理服务的开发步骤。最后,通过业务测试验证了系统的功能,并引入Resilience4j实现服务降级,确保系统在部分服务故障时仍能正常运行。此示例旨在帮助读者理解微服务架构的设计思路与实践方法。
46 16
|
2天前
|
缓存 Rust 安全
ASM数据面代理扩展能力综述
本文介绍ASM数据面代理提供的各种扩展能力,方便您选择更合适的扩展方式满足业务需求。
|
2月前
|
弹性计算 持续交付 API
构建高效后端服务:微服务架构的深度解析与实践
在当今快速发展的软件行业中,构建高效、可扩展且易于维护的后端服务是每个技术团队的追求。本文将深入探讨微服务架构的核心概念、设计原则及其在实际项目中的应用,通过具体案例分析,展示如何利用微服务架构解决传统单体应用面临的挑战,提升系统的灵活性和响应速度。我们将从微服务的拆分策略、通信机制、服务发现、配置管理、以及持续集成/持续部署(CI/CD)等方面进行全面剖析,旨在为读者提供一套实用的微服务实施指南。
|
1月前
|
运维 监控 Java
后端开发中的微服务架构实践与挑战####
在数字化转型加速的今天,微服务架构凭借其高度的灵活性、可扩展性和可维护性,成为众多企业后端系统构建的首选方案。本文深入探讨了微服务架构的核心概念、实施步骤、关键技术考量以及面临的主要挑战,旨在为开发者提供一份实用的实践指南。通过案例分析,揭示微服务在实际项目中的应用效果,并针对常见问题提出解决策略,帮助读者更好地理解和应对微服务架构带来的复杂性与机遇。 ####
|
1月前
|
算法 NoSQL Java
微服务架构下的接口限流策略与实践#### 一、
本文旨在探讨微服务架构下,面对高并发请求时如何有效实施接口限流策略,以保障系统稳定性和服务质量。不同于传统的摘要概述,本文将从实际应用场景出发,深入剖析几种主流的限流算法(如令牌桶、漏桶及固定窗口计数器等),通过对比分析它们的优缺点,并结合具体案例,展示如何在Spring Cloud Gateway中集成自定义限流方案,实现动态限流规则调整,为读者提供一套可落地的实践指南。 #### 二、
68 3
|
1月前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
156 5
|
1月前
|
消息中间件 运维 安全
后端开发中的微服务架构实践与挑战####
在数字化转型的浪潮中,微服务架构凭借其高度的灵活性和可扩展性,成为众多企业重构后端系统的首选方案。本文将深入探讨微服务的核心概念、设计原则、关键技术选型及在实际项目实施过程中面临的挑战与解决方案,旨在为开发者提供一套实用的微服务架构落地指南。我们将从理论框架出发,逐步深入至技术细节,最终通过案例分析,揭示如何在复杂业务场景下有效应用微服务,提升系统的整体性能与稳定性。 ####
46 1
|
1月前
|
监控 安全 持续交付
构建高效微服务架构:策略与实践####
在数字化转型的浪潮中,微服务架构凭借其高度解耦、灵活扩展和易于维护的特点,成为现代企业应用开发的首选。本文深入探讨了构建高效微服务架构的关键策略与实战经验,从服务拆分的艺术到通信机制的选择,再到容器化部署与持续集成/持续部署(CI/CD)的实践,旨在为开发者提供一套全面的微服务设计与实现指南。通过具体案例分析,揭示如何避免常见陷阱,优化系统性能,确保系统的高可用性与可扩展性,助力企业在复杂多变的市场环境中保持竞争力。 ####
48 2
|
1月前
|
消息中间件 运维 API
后端开发中的微服务架构实践####
本文深入探讨了微服务架构在后端开发中的应用,从其定义、优势到实际案例分析,全面解析了如何有效实施微服务以提升系统的可维护性、扩展性和灵活性。不同于传统摘要的概述性质,本摘要旨在激发读者对微服务架构深度探索的兴趣,通过提出问题而非直接给出答案的方式,引导读者深入
47 1

相关产品

  • 服务网格