原文地址: https://www.envoyproxy.io/docs/envoy/v1.28.0/intro/intro

Envoy 官网介绍文档的中文翻译(请求的生命周期)

以下我们将描述一个请求通过 Envoy 代理时的生命周期中的事件。 我们首先描述 Envoy 如何适应请求路径,然后描述从下游到达 Envoy 代理后发生的内部事件。 我们将跟踪请求,直到相应的上游分发和响应路径。

术语

Envoy的代码库和文档中用到了以下术语:

  • 集群(Cluster):一组端点,代表一个逻辑服务。Envoy会将请求转发给这些端点。
  • 下游(Downstream):与Envoy建立连接的实体。这可以是一个本地应用程序(在sidecar模型中)或网络节点。 在非sidecar模型中,它是一个远程客户端。
  • 端点(Endpoints):实现逻辑服务的网络节点。这些节点被组合成集群。在集群中的端点是Envoy代理的上游。
  • 过滤器(Filter):处理请求的某个方面的模块,位于连接或请求处理管道中。 它可以被视为由Unix管道连接的小工具(过滤器)的集合。
  • 过滤器链(Filter chain):一系列过滤器,它们按顺序执行。
  • 监听器(Listeners):负责绑定到IP/端口、接受新的TCP连接(或UDP数据报)并协调请求处理下游方面的Envoy模块。
  • 上游:当转发服务请求时,Envoy连接到的端点(网络节点)。 这可以是一个本地应用程序(在sidecar模型中)或网络节点。 在非sidecar模型中,它对应于远程后端。

网络拓扑

请求如何在包括Envoy在内的网络组件中流动,取决于网络的拓扑结构。Envoy可以用于各种网络拓扑。 下面我们将重点关注Envoy的内部操作,但在此部分中,我们将简要介绍Envoy如何与网络的其他部分相关联。

Envoy最初是一个服务网格边车代理,将负载均衡、路由、可观察性、安全性和发现服务从应用程序中分离出来。 在服务网格模型中,请求通过Envoy作为网关流经网络。请求通过入口或出口监听器到达Envoy:

  • 入口监听器从服务网格中的其他节点接收请求,并将它们转发给本地应用程序。本地应用程序的响应通过Envoy返回到下游节点。

  • 出口监听器接收来自本地应用程序的请求,并将它们转发到网络中的其他节点。 这些接收节点通常也会运行Envoy,并使用其入口监听器接收请求。

Envoy 可用于服务网格之外的各种配置。例如,它还可以充当内部负载均衡器:

或者作为网络边缘的入口/出口代理:

在实践中,经常使用这些的混合,其中 Envoy 在服务网格、边缘和内部负载均衡器中发挥作用。 一个请求路径可能会遍历多个 Envoy。

Envoy 可以配置为多层拓扑,以实现可扩展性和可靠性,请求首先通过边缘 Envoy,然后再通过第二个 Envoy 层:

在上述所有情况下,请求将从下游通过 TCP、UDP 或 Unix 域套接字到达特定 Envoy。 Envoy 将通过 TCP、UDP 或 Unix 域套接字向上游转发请求。下面我们重点关注单个 Envoy 代理。

配置

Envoy是一个非常容易扩展的平台。这导致了可能的请求路径组合的爆炸性增长,具体取决于以下因素:

  • L3/L4协议,例如TCP、UDP、Unix域套接字。
  • L7协议,例如HTTP/1、HTTP/2、HTTP/3、gRPC、Thrift、Dubbo、Kafka、Redis和各种数据库。
  • 传输套接字,例如明文、TLS、ALTS。
  • 连接路由,例如PROXY协议、原始目的地、动态转发。
  • 身份验证和授权。
  • 断路器和异常检测的配置和激活状态。
  • 许多其他用于网络、HTTP、监听器、访问日志记录、健康检查、跟踪和统计扩展的配置。

一次专注于一个配置很有帮助,因此此示例涵盖了以下内容:

  • 下游和上游都通过TCP连接使用TLS的HTTP/2请求。
  • HTTP连接管理器作为唯一的网络过滤器。
  • 假设的CustomFilter和路由器过滤器作为HTTP过滤器链。
  • 文件系统访问日志记录。
  • Statsd接收器。
  • 具有静态端点的单个集群。

为了简单起见,我们假设使用静态引导配置文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
static_resources:
  listeners:
  # There is a single listener bound to port 443.
  - name: listener_https
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 443
    # A single listener filter exists for TLS inspector.
    listener_filters:
    - name: "envoy.filters.listener.tls_inspector"
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
    # On the listener, there is a single filter chain that matches SNI for acme.com.
    filter_chains:
    - filter_chain_match:
        # This will match the SNI extracted by the TLS Inspector filter.
        server_names: ["acme.com"]
      # Downstream TLS configuration.
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            tls_certificates:
            - certificate_chain: {filename: "certs/servercert.pem"}
              private_key: {filename: "certs/serverkey.pem"}
      filters:
      # The HTTP connection manager is the only network filter.
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          use_remote_address: true
          http2_protocol_options:
            max_concurrent_streams: 100
          # File system based access logging.
          access_log:
          - name: envoy.access_loggers.file
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
              path: "/var/log/envoy/access.log"
          # The route table, mapping /foo to some_service.
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["acme.com"]
              routes:
              - match:
                  path: "/foo"
                route:
                  cluster: some_service
          # CustomFilter and the HTTP router filter are the HTTP filter chain.
          http_filters:
          # - name: some.customer.filter
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
  - name: some_service
    # Upstream TLS configuration.
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
    load_assignment:
      cluster_name: some_service
      # Static endpoint assignment.
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 10.1.2.10
                port_value: 10002
        - endpoint:
            address:
              socket_address:
                address: 10.1.2.11
                port_value: 10002
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options:
            max_concurrent_streams: 100
  - name: some_statsd_sink
  # The rest of the configuration for statsd sink cluster.
# statsd sink.
stats_sinks:
- name: envoy.stat_sinks.statsd
  typed_config:
    "@type": type.googleapis.com/envoy.config.metrics.v3.StatsdSink
    tcp_cluster_name: some_statsd_sink

高级架构

Envoy中的请求处理路径有两个主要部分:

  • 监听器子系统, 负责处理下游请求处理。它还负责管理下游请求生命周期以及客户端的响应路径。下游HTTP/2编解码器位于此处。

  • 集群子系统, 负责选择和配置到端点的上游连接。这是集群和端点健康状况、负载均衡和连接池知识的所在之处。上游HTTP/2编解码器位于此处。

这两个子系统通过HTTP路由器过滤器进行桥接,将HTTP请求从下游转发到上游。

我们使用监听器子系统集群子系统 这两个术语来指代由顶级ListenerManager和ClusterManager类创建的模块和实例类组。 我们将在下面讨论许多组件,这些组件在请求之前和请求过程中由这些管理系统实例化, 例如监听器、过滤器链、编解码器、连接池和负载均衡数据结构。

Envoy具有基于事件的多线程模型。 主线程负责服务器生命周期、配置处理、统计信息等,而一些工作线程处理请求。 所有线程都围绕事件循环(libevent)运行,对于给定的下游TCP连接(包括其上的所有复用流),在其生命周期中,将由一个工作线程精确处理。 每个工作线程维护自己的TCP连接池到上游端点。 UDP处理使用SO_REUSEPORT,使内核始终将源/目标IP:端口元组哈希到相同的工作线程。 对于给定的工作线程,UDP过滤器状态是共享的,过滤器负责提供所需的会话语义。 这与我们下面讨论的面向连接的TCP过滤器不同,其中过滤器状态存在于每个连接和HTTP过滤器的每个请求上。

工作线程很少共享状态,并以非常简单的并行方式运行。这种线程模型使得能够扩展到非常高的核心数CPU。

请求流

概述

使用上述配置示例简要概述请求和响应的生命周期:

  1. 由工作线程上的Envoy监听器接受来自下游的TCP连接。
  2. 创建并运行监听器过滤器链。它可以提供SNI和其他TLS前的信息。 一旦完成,监听器将匹配一个网络过滤器链。 每个监听器可以有多个过滤器链,它们根据目标IP CIDR范围、SNI、ALPN、源端口等的组合进行匹配。 在这种情况下,传输套接字(TLS传输套接字)与该过滤器链关联。
  3. 在网络读取时,TLS传输套接字解密从TCP连接读取的数据到解密的数据流以供进一步处理。
  4. 创建并运行网络过滤器链。对于HTTP来说,最重要的过滤器是HTTP连接管理器,它是链中的最后一个网络过滤器。
  5. HTTP连接管理器中的HTTP/2编解码器将来自TLS连接的解密数据流拆分为多个独立流。每个流处理单个请求和响应。
  6. 对于每个HTTP流,创建并运行下游HTTP过滤器链。 请求首先通过CustomFilter,该过滤器可能读取和修改请求。 最重要的HTTP过滤器是位于HTTP过滤器链末端的路由器过滤器。 当对路由器过滤器调用decodeHeaders时,选择路由并选择一个集群。 该集群中的请求头将转发到上游端点。 路由器过滤器从集群管理器中获取与匹配集群相关的HTTP连接池进行此操作。
  7. 执行集群特定的负载均衡以找到端点。 检查集群的断路器以确定是否允许新的流。 如果端点的连接池为空或容量不足,则创建与该端点的新连接。
  8. 对于每个流,创建并运行上游HTTP过滤器链。 默认情况下,这仅包括发送数据到适当编解码器的CodecFilter, 但如果集群配置了上游过滤器链,则将为每个流创建和运行该过滤器链, 包括为重试和阴影请求创建和运行单独的过滤器链。
  9. 上游端点连接的HTTP/2编解码器通过单个TCP连接将请求流的任何其他流与该上游进行多路复用和帧化。
  10. 上游端点连接的TLS传输套接字加密这些字节,并将它们写入上游连接的TCP套接字。
  11. 请求(包括头部、可选正文和尾部)被代理到上游,响应被代理到下游。 响应通过HTTP过滤器的顺序与请求相反,从编解码器过滤器开始,遍历任何上游过滤器, 然后通过路由器过滤器并经过CustomFilter,最后发送到下游。
  12. 当响应完成时,流将被销毁。后请求处理将更新统计信息、写入访问日志并完成跟踪跨度。

我们将在以下部分详细阐述这些步骤。

1. 监听器 TCP Accept

监听器管理器负责接收表示监听器的配置,并实例化一些绑定到各自IP/端口的Listener实例。监听器可以处于以下三种状态之一:

  • 预热(Warming):监听器正在等待配置依赖项(例如路由配置、动态秘钥)。监听器尚未准备好接受TCP连接。

  • 活动(Active):监听器绑定到其IP/端口并接受TCP连接。

  • 排空(Draining):监听器不再接受新的TCP连接,但其现有的TCP连接在一段时间内仍被允许继续。

每个工作线程为其配置的每个监听器维护自己的Listener实例。 每个监听器可以通过 SO_REUSEPORT 或共享单个绑定到此端口的套接字来绑定到相同的端口。 当新的TCP连接到达时,内核决定哪个工作线程将接受该连接,并且该工作线程的Listener的Server::ConnectionHandlerImpl::ActiveTcpListener::onAccept()回调将被调用。

2. 监听器过滤器链和网络过滤器链匹配

工作线程的Listener然后创建并运行监听器过滤器链。过滤器链是通过应用每个过滤器的过滤器工厂创建的。工厂知道过滤器的配置,并为每个连接或流创建过滤器的新实例。

对于我们的TLS监听器配置,监听器过滤器链由TLS检查器过滤器(envoy.filters.listener.tls_inspector)组成。此过滤器检查初始TLS握手并提取服务器名称(SNI)。然后,SNI可用于过滤器链匹配。尽管TLS检查器在监听器过滤器链配置中显式出现,但Envoy还能够在需要SNI(或ALPN)时自动插入此过滤器。

TLS检查器过滤器实现了 ListenerFilter接口。 所有过滤器接口,无论是监听器还是网络/HTTP,都要求过滤器为特定的连接或流事件实现回调。对于 ListenerFilter,这是:

1
virtual FilterStatus onAccept(ListenerFilterCallbacks& cb) PURE;

onAccept() 允许过滤器在 TCP accept 处理期间运行。回调返回的FilterStatus控制监听器过滤器链将如何继续。监听器过滤器可以暂停过滤器链,然后稍后恢复,例如响应于对另一个服务的RPC。

从监听器过滤器和连接属性中提取的信息随后用于匹配过滤器链,提供将用于处理连接的网络过滤器链和传输套接字。

  1. TLS传输套接字解密

Envoy通过TransportSocket扩展接口提供可插拔的传输套接字。 传输套接字遵循TCP连接的生命周期事件,并读取/写入网络缓冲区。传输套接字必须实现的一些关键方法如下:

1
2
3
4
virtual void onConnected() PURE;
virtual IoResult doRead(Buffer::Instance& buffer) PURE;
virtual IoResult doWrite(Buffer::Instance& buffer, bool end_stream) PURE;
virtual void closeSocket(Network::ConnectionEvent event) PURE;

当TCP连接上有数据可用时,Network::ConnectionImpl::onReadReady()``通过SslSocket::doRead()调用TLS传输套接字。 传输套接字然后在TCP连接上执行TLS握手。 当握手完成后,`SslSocket::doRead()向负责管理网络过滤器链的`Network::FilterManagerImpl``实例提供解密的字节流。

重要的是要注意,没有任何操作,无论是TLS握手还是过滤器管道的暂停,都是真正的阻塞操作。 由于Envoy是基于事件的,任何需要额外数据才能完成处理的情况都会导致早期事件完成,并将CPU让给其他事件。 当网络提供更多可读数据时,一个读取事件将触发TLS握手的恢复。

4. 网络过滤器链处理

与监听器过滤器链一样,Envoy通过 Network::FilterManagerImpl 实例化一系列网络过滤器,从它们的过滤器工厂中。 每个新连接都有一个新的实例。 网络过滤器,就像传输套接字一样,遵循TCP生命周期事件,并在传输套接字可用数据时被调用。

网络过滤器以管道的形式组成,与每个连接一个的传输套接字不同。网络过滤器有三种类型:

  • ReadFilter 实现 onData(),当从连接中可用数据时调用(由于某些请求)。

  • WriteFilter 实现 onWrite(),当数据即将写入连接时调用(由于某些响应)。

  • 实现ReadFilter和WriteFilter的Filter

关键过滤器方法的方法签名如下:

1
2
3
virtual FilterStatus onNewConnection() PURE;
virtual FilterStatus onData(Buffer::Instance& data, bool end_stream) PURE;
virtual FilterStatus onWrite(Buffer::Instance& data, bool end_stream) PURE;

与监听器过滤器一样,FilterStatus允许过滤器暂停过滤器链的执行。例如,如果需要查询限速服务,限速网络过滤器将返回Network::FilterStatus::StopIterationonData(),并在查询完成后调用continueReading()

处理HTTP的监听器的最后一个网络过滤器是HTTP连接管理器(HCM)。它负责创建HTTP/2编解码器和管理HTTP过滤器链。在我们的示例中,这是唯一的网络过滤器。使用多个网络过滤器的示例网络过滤器链如下所示:

在响应路径上,网络过滤器链按与请求路径相反的顺序执行。

5. HTTP/2 codec 解码

Envoy中的HTTP/2编解码器基于nghttp2。它由HCM使用来自TCP连接的明文字节(经过网络过滤器链转换后)调用。编解码器将字节流解码为一系列HTTP/2帧,并将连接解复用为多个独立的HTTP流。流复用是HTTP/2的关键功能,与HTTP/1相比提供了显著的性能优势。每个HTTP流处理一个请求和响应。

编解码器还负责处理HTTP/2设置帧以及流和连接级别的流量控制

编解码器负责抽象HTTP连接的细节,向HTTP连接管理器和HTTP过滤器链呈现标准的连接视图,该连接分为多个流,每个流具有请求/响应头/正文/尾部。无论协议是HTTP/1、HTTP/2还是HTTP/3,这都是正确的。

6. HTTP过滤器链处理

对于每个HTTP流,HCM实例化一个下游HTTP过滤器链, 遵循上述监听器和网络过滤器链的模式。

有三种HTTP过滤器接口:

查看解码器过滤器接口:

1
2
3
virtual FilterHeadersStatus decodeHeaders(RequestHeaderMap& headers, bool end_stream) PURE;
virtual FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) PURE;
virtual FilterTrailersStatus decodeTrailers(RequestTrailerMap& trailers) PURE;

与在连接缓冲区和事件上操作不同,HTTP过滤器遵循HTTP请求的生命周期,例如,decodeHeaders()方法接受HTTP头部作为参数,而不是字节缓冲区。返回的 FilterStatus 与网络和监听器过滤器一样,提供了管理过滤器链控制流的能力。

当HTTP/2编解码器提供HTTP请求的头部时,这些头部首先传递给 CustomFilter 中的 decodeHeaders() 方法。如果返回的 FilterHeadersStatusContinue,HCM然后将头部(可能由CustomFilter修改)传递给路由器过滤器。

解码器和编解码器过滤器在请求路径上执行。编码器和编解码器过滤器在响应路径上执行,方向相反。考虑以下示例过滤器链:

请求路径看起来是:

响应路径看起来是:

当在路由过滤器上调用decodeHeaders()时,路由选择将最终确定并选择一个集群。在HTTP过滤器链执行开始时,HCM从其RouteConfiguration中选择一条路由。这被称为缓存的路由。过滤器可以修改头部并要求HCM清除路由缓存并重新评估路由选择。过滤器还可以通过setRoute回调直接设置此缓存的路由选择。当路由器过滤器被调用时,路由被确定。所选路由的配置将指向上游集群名称。路由器过滤器然后要求ClusterManager为集群创建一个HTTP连接池。这涉及到下一部分中讨论的负载均衡和连接池。

由此产生的HTTP连接池用于在路由器中构建一个UpstreamRequest对象,该对象封装了上游HTTP请求的HTTP编码和解码回调方法。一旦在HTTP连接池中的连接上分配了一个流,请求头部将通过调用UpstreamRequest::encodeHeaders转发到上游端点。

路由器过滤器负责从HTTP连接池中分配的流上的上游请求生命周期管理的所有方面。它还负责请求超时、重试和亲和性。

路由器过滤器还负责创建和运行上游HTTP过滤器链 <arch_overview_http_filters>。 默认情况下,上游过滤器将在路由器过滤器收到头部后立即开始运行,但是C++过滤器可以暂停,直到需要检查上游流或连接时才建立上游连接。 默认情况下,上游过滤器链通过集群配置进行配置,因此例如,被遮蔽的请求可以为主集群和遮蔽的集群设置单独的上游过滤器链。 另外,由于上游过滤器链位于路由器过滤器上游,因此每个重试尝试都会运行一次, 允许每个重试进行头部操作并包含有关上游流和连接的信息。 与下游过滤器不同,上游过滤器不能更改路由。

7. 负载均衡

每个集群都有一个负载均衡器, 当新请求到达时,它会选择一个端点。 Envoy支持各种负载均衡算法,例如加权轮询、Maglev、最少负载、随机。 负载均衡器通过组合静态引导配置、DNS、动态xDS(CDS和EDS发现服务)以及主动/被动健康检查来获取其有效分配。 有关Envoy中负载均衡如何工作的更多详细信息,请参阅负载均衡文档

一旦选择了端点,将从该端点的连接池, 查找用于转发请求的连接。 如果没有到主机的连接,或者所有连接都已达到其最大并发流限制, 则将建立新的连接并将其放置在连接池中,除非该集群的最大连接数的断路器已跳闸。 如果配置了连接的最大生命周期流限制并且已达到,则将在池中分配新的连接,并将受影响的HTTP/2连接排空。 还会检查其他断路器,例如到集群的最大并发请求。请参阅 断路器连接池 以获取更多详细信息。

8. HTTP/2编解码器编码

选定的连接的HTTP/2编解码器通过单个TCP连接将请求流与任何其他流向相同上游的流进行复用。这与HTTP/2编解码器解码相反。

与下游HTTP/2编解码器一样,上游编解码器负责将Envoy的标准HTTP抽象,即通过单个连接进行多流复用,具有请求/响应头/正文/尾部,并将其映射到HTTP/2的具体细节,生成一系列HTTP/2帧。

9. TLS传输套接字加密

上游端点连接的TLS传输套接字对来自HTTP/2编解码器的字节进行加密,并将它们写入TCP套接字以进行上游连接。与 TLS传输套接字解密 一样,在我们的示例中,集群配置了提供TLS传输安全的传输套接字。上游和下游传输套接字扩展具有相同的接口。

10. 响应路径和HTTP生命周期

由头部和可选的正文和尾部组成的请求被代理到上游,响应被代理到下游。 响应按照与请求相反的顺序通过HTTP和网络过滤器。

在解码器/编码器请求生命周期事件的各种回调中,HTTP过滤器将被调用,例如在转发响应尾部或流式传输请求正文时。 同样,在请求期间,读写网络过滤器也将有其各自的回调被调用,因为数据在两个方向上继续流动。

随着请求的进展,端点的异常检测状态也会得到修订。

当上游响应到达其流的末尾时,即收到带有设置了end-stream的响应头部/正文或尾部时,请求完成。 这由outer::Filter::onUpstreamComplete()处理。

请求可能会提前终止。这可能是由于以下原因(但不限于):

  • 请求超时。
  • 上游端点流重置。
  • HTTP过滤器流重置。
  • 断路器触发。
  • 上游资源不可用,例如缺少一个路由的集群。
  • 没有健康端点。
  • DoS保护。
  • HTTP协议违规。
  • 来自HCM或HTTP过滤器的本地回复。例如,速率限制HTTP过滤器返回429响应。

如果发生其中任何一种情况,Envoy可能会发送一个内部生成的响应(如果上游响应头尚未发送), 或者如果响应头已经转发到下游,则会重置流。 有关解释这些早期流终止的更多信息,请参阅Envoy调试常见问题解答

11. 请求后处理

一旦请求完成,流将被销毁。以下操作也会发生:

  • 更新请求后的统计数据(例如计时、活动请求、升级、健康检查)。 然而,一些统计数据在请求处理期间会更早地更新。 此时,统计数据不会写入统计数据接收器, 而是由主线程定期批量写入。在我们的示例中,这是一个statsd接收器。

  • 访问日志被写入访问日志接收器。 在我们的示例中,这是一个文件访问日志。

  • 跟踪跨度被最终确定。如果我们的示例请求被跟踪,一个描述请求持续时间和详细信息的跟踪跨度将在HCM处理请求头时由HCM创建,然后在请求后处理期间由HCM最终确定。