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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 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

目录
相关文章
|
16天前
|
API 持续交付 开发者
后端开发中的微服务架构实践与挑战
在数字化时代,后端服务的构建和管理变得日益复杂。本文将深入探讨微服务架构在后端开发中的应用,分析其在提高系统可扩展性、灵活性和可维护性方面的优势,同时讨论实施微服务时面临的挑战,如服务拆分、数据一致性和部署复杂性等。通过实际案例分析,本文旨在为开发者提供微服务架构的实用见解和解决策略。
|
17天前
|
弹性计算 Kubernetes Cloud Native
云原生架构下的微服务设计原则与实践####
本文深入探讨了在云原生环境中,微服务架构的设计原则、关键技术及实践案例。通过剖析传统单体架构面临的挑战,引出微服务作为解决方案的优势,并详细阐述了微服务设计的几大核心原则:单一职责、独立部署、弹性伸缩和服务自治。文章还介绍了容器化技术、Kubernetes等云原生工具如何助力微服务的高效实施,并通过一个实际项目案例,展示了从服务拆分到持续集成/持续部署(CI/CD)流程的完整实现路径,为读者提供了宝贵的实践经验和启发。 ####
|
5天前
|
Cloud Native 安全 API
云原生架构下的微服务治理策略与实践####
—透过云原生的棱镜,探索微服务架构下的挑战与应对之道 本文旨在探讨云原生环境下,微服务架构所面临的关键挑战及有效的治理策略。随着云计算技术的深入发展,越来越多的企业选择采用云原生架构来构建和部署其应用程序,以期获得更高的灵活性、可扩展性和效率。然而,微服务架构的复杂性也带来了服务发现、负载均衡、故障恢复等一系列治理难题。本文将深入分析这些问题,并提出一套基于云原生技术栈的微服务治理框架,包括服务网格的应用、API网关的集成、以及动态配置管理等关键方面,旨在为企业实现高效、稳定的微服务架构提供参考路径。 ####
26 5
|
9天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
24天前
|
Kubernetes 负载均衡 Docker
构建高效后端服务:微服务架构的探索与实践
【10月更文挑战第20天】 在数字化时代,后端服务的构建对于任何在线业务的成功至关重要。本文将深入探讨微服务架构的概念、优势以及如何在实际项目中有效实施。我们将从微服务的基本理念出发,逐步解析其在提高系统可维护性、扩展性和敏捷性方面的作用。通过实际案例分析,揭示微服务架构在不同场景下的应用策略和最佳实践。无论你是后端开发新手还是经验丰富的工程师,本文都将为你提供宝贵的见解和实用的指导。
|
7天前
|
负载均衡 监控 Cloud Native
云原生架构下的微服务治理策略与实践####
在数字化转型浪潮中,企业纷纷拥抱云计算,而云原生架构作为其核心技术支撑,正引领着一场深刻的技术变革。本文聚焦于云原生环境下微服务架构的治理策略与实践,探讨如何通过精细化的服务管理、动态的流量调度、高效的故障恢复机制以及持续的监控优化,构建弹性、可靠且易于维护的分布式系统。我们将深入剖析微服务治理的核心要素,结合具体案例,揭示其在提升系统稳定性、扩展性和敏捷性方面的关键作用,为读者提供一套切实可行的云原生微服务治理指南。 ####
|
8天前
|
Kubernetes Cloud Native Docker
云原生技术探索:容器化与微服务的实践之道
【10月更文挑战第36天】在云计算的浪潮中,云原生技术以其高效、灵活和可靠的特性成为企业数字化转型的重要推手。本文将深入探讨云原生的两大核心概念——容器化与微服务架构,并通过实际代码示例,揭示如何通过Docker和Kubernetes实现服务的快速部署和管理。我们将从基础概念入手,逐步引导读者理解并实践云原生技术,最终掌握如何构建和维护一个高效、可扩展的云原生应用。
|
10天前
|
监控 API 持续交付
后端开发中的微服务架构实践与挑战####
本文深入探讨了微服务架构在后端开发中的应用,分析了其优势、面临的挑战以及最佳实践策略。不同于传统的单体应用,微服务通过细粒度的服务划分促进了系统的可维护性、可扩展性和敏捷性。文章首先概述了微服务的核心概念及其与传统架构的区别,随后详细阐述了构建微服务时需考虑的关键技术要素,如服务发现、API网关、容器化部署及持续集成/持续部署(CI/CD)流程。此外,还讨论了微服务实施过程中常见的问题,如服务间通信复杂度增加、数据一致性保障等,并提供了相应的解决方案和优化建议。总之,本文旨在为开发者提供一份关于如何在现代后端系统中有效采用和优化微服务架构的实用指南。 ####
|
12天前
|
消息中间件 设计模式 运维
后端开发中的微服务架构实践与挑战####
本文深入探讨了微服务架构在现代后端开发中的应用,通过实际案例分析,揭示了其在提升系统灵活性、可扩展性及促进技术创新方面的显著优势。同时,文章也未回避微服务实施过程中面临的挑战,如服务间通信复杂性、数据一致性保障及部署运维难度增加等问题,并基于实践经验提出了一系列应对策略,为开发者在构建高效、稳定的微服务平台时提供有价值的参考。 ####
|
12天前
|
消息中间件 监控 数据管理
后端开发中的微服务架构实践与挑战####
【10月更文挑战第29天】 在当今快速发展的软件开发领域,微服务架构已成为构建高效、可扩展和易于维护应用程序的首选方案。本文探讨了微服务架构的核心概念、实施策略以及面临的主要挑战,旨在为开发者提供一份实用的指南,帮助他们在项目中成功应用微服务架构。通过具体案例分析,我们将深入了解如何克服服务划分、数据管理、通信机制等关键问题,以实现系统的高可用性和高性能。 --- ###
36 2

相关产品

  • 服务网格