请求的生命周期
下面我们描述了请求通过 Envoy 代理时的生命周期事件。我们首先描述 Envoy 如何适应请求路径,然后描述请求到达 Envoy 代理后发生的内部事件。我们跟踪请求直到相应的上游调度和响应路径。
术语
Envoy 在其代码库和文档中使用以下术语
集群:具有 Envoy 将请求转发到的端点集的逻辑服务。
下游:连接到 Envoy 的实体。这可能是一个本地应用程序(在 sidecar 模型中)或一个网络节点。在非 sidecar 模型中,这是一个远程客户端。
端点:实现逻辑服务的网络节点。它们被分组到集群中。集群中的端点是 Envoy 代理的上游。
过滤器:连接或请求处理管道中的模块,提供请求处理的某些方面。Unix 中的类比是使用 Unix 管道(过滤器链)组合小型实用程序(过滤器)。
过滤器链:一系列过滤器。
监听器:负责绑定到 IP/端口、接受新的 TCP 连接(或 UDP 数据报)并协调请求处理的下游方面的 Envoy 模块。
上游:Envoy 在转发服务请求时连接到的端点(网络节点)。这可能是一个本地应用程序(在 sidecar 模型中)或一个网络节点。在非 sidecar 模型中,这对应于远程后端。
网络拓扑
请求如何通过网络中的组件(包括 Envoy)流动取决于网络的拓扑结构。Envoy 可以用于各种网络拓扑结构。我们将在下面重点介绍 Envoy 的内部操作,但简要地,我们将在本节中讨论 Envoy 与网络其他部分的关系。
Envoy 起源于 服务网格 sidecar 代理,将负载均衡、路由、可观察性、安全性以及发现服务从应用程序中分离出来。在服务网格模型中,请求通过 Envoy 作为网络的网关流动。请求通过入口或出口监听器到达 Envoy
入口监听器接收来自服务网格中其他节点的请求,并将它们转发到本地应用程序。来自本地应用程序的响应通过 Envoy 流回到下游。
出口监听器接收来自本地应用程序的请求,并将它们转发到网络中的其他节点。这些接收节点通常也会运行 Envoy,并通过它们的入口监听器接受请求。
Envoy 用于服务网格之外的各种配置。例如,它也可以充当内部负载均衡器
或作为网络边缘的入口/出口代理
实际上,通常使用这些配置的混合,其中 Envoy 在服务网格、边缘以及作为内部负载均衡器中发挥作用。请求路径可能会遍历多个 Envoy。
Envoy 可以在多层拓扑结构中配置,以实现可扩展性和可靠性,请求首先通过边缘 Envoy,然后再通过第二层 Envoy
在以上所有情况下,请求将通过 TCP、UDP 或 Unix 域套接字从下游到达特定 Envoy。Envoy 将通过 TCP、UDP 或 Unix 域套接字向上游转发请求。我们将在下面重点介绍单个 Envoy 代理。
配置
Envoy 是一个非常可扩展的平台。这导致了可能的请求路径的组合爆炸,具体取决于
L3/4 协议,例如 TCP、UDP、Unix 域套接字。
L7 协议,例如 HTTP/1、HTTP/2、HTTP/3、gRPC、Thrift、Dubbo、Kafka、Redis 以及各种数据库。
传输套接字,例如纯文本、TLS、ALTS。
连接路由,例如 PROXY 协议、原始目标、动态转发。
身份验证和授权。
断路器和异常值检测配置以及激活状态。
网络、HTTP、监听器、访问日志记录、健康检查、跟踪和统计扩展的许多其他配置。
一次专注于一个很有帮助,因此本示例涵盖以下内容
一个 HTTP/2 请求,通过 TLS 在 TCP 连接上进行下游和上游。
HTTP 连接管理器 作为唯一的 网络过滤器。
一个包含静态端点的 集群。
为了简单起见,我们假设一个静态引导配置文
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 过滤器形成对比,在 TCP 过滤器中,过滤器状态存在于每个连接上,并且在 HTTP 过滤器的案例中,存在于每个请求的基础上。
工作线程很少共享状态,并以一种微不足道的并行方式运行。这种线程模型能够扩展到具有非常高核心数量的 CPU。
请求流程
概述
使用上面的示例配置,请求和响应生命周期的简要概述
创建并运行 监听器过滤器 链。它可以提供 SNI 和其他预 TLS 信息。完成后,监听器将匹配一个网络过滤器链。每个监听器可能有多个过滤器链,这些过滤器链根据目标 IP CIDR 范围、SNI、ALPN、源端口等的组合匹配。一个传输套接字(在本例中为 TLS 传输套接字)与该过滤器链相关联。
在网络读取时,TLS 传输套接字将从 TCP 连接读取的数据解密到一个解密数据流中,以便进一步处理。
创建并运行 网络过滤器 链。HTTP 最重要的过滤器是 HTTP 连接管理器,它是链中的最后一个网络过滤器。
HTTP 连接管理器 中的 HTTP/2 编解码器将 TLS 连接的解密数据流去帧和解复用到多个独立流中。每个流处理一个请求和响应。
对于每个 HTTP 流,创建一个并运行 下游 HTTP 过滤器 链。请求首先通过 CustomFilter,CustomFilter 可能会读取和修改请求。最重要的 HTTP 过滤器是路由器过滤器,它位于 HTTP 过滤器链的末尾。当在路由器过滤器上调用
decodeHeaders
时,将选择路由并选择一个集群。流上的请求头将转发到该集群中的上游端点。 路由器 过滤器从集群管理器获取与匹配的集群相关的 HTTP 连接池 以执行此操作。为了找到一个端点,会执行特定于集群的 负载均衡。会检查集群的断路器以确定是否允许新的流。如果端点的连接池为空或没有容量,将创建一个与端点的新的连接。
对于每个流,都会创建一个 上游 HTTP 过滤器 链并运行。默认情况下,这仅包括 CodecFilter,将数据发送到相应的编解码器,但是如果集群配置了上游 HTTP 过滤器链,则该过滤器链将在每个流上创建并运行,这包括为重试和影子请求创建和运行单独的过滤器链。
上游端点连接的 HTTP/2 编解码器将请求的流与通过单个 TCP 连接到该上游的任何其他流多路复用并帧化。
上游端点连接的 TLS 传输套接字会加密这些字节并将它们写入上游连接的 TCP 套接字。
包含报头和可选主体和尾部的请求将被代理到上游,响应将被代理到下游。响应将按与请求相反的顺序通过 HTTP 过滤器,从编解码器过滤器开始,遍历任何上游 HTTP 过滤器,然后通过路由器过滤器并通过 CustomFilter,最后发送到下游。
如果启用了独立的半关闭,则在请求和响应都完成(在两个方向都观察到 HTTP/2 流的 END_STREAM)并且响应具有成功(2xx)状态代码后,将销毁流。否则,即使请求尚未完成,响应完成时也会销毁流。请求后处理将更新统计信息、写入访问日志并完成跟踪跨度。
我们在下面的部分中详细说明了这些步骤。
1. 监听器 TCP 接收
ListenerManager 负责获取代表 监听器 的配置,并实例化绑定到其各自 IP/端口的多个 Listener 实例。监听器可能处于以下三种状态之一
预热:监听器正在等待配置依赖项(例如路由配置、动态密钥)。监听器尚未准备好接收 TCP 连接。
活动:监听器绑定到其 IP/端口并接收 TCP 连接。
排空:监听器不再接受新的 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
的情况下,这是
virtual FilterStatus onAccept(ListenerFilterCallbacks& cb) PURE;
onAccept()
允许过滤器在 TCP 接收处理期间运行。回调返回的 FilterStatus
控制监听器过滤器链将如何继续。监听器过滤器可以暂停过滤器链,然后在稍后恢复,例如,响应对另一个服务的 RPC。
从监听器过滤器和连接属性中提取的信息将用于匹配过滤器链,从而提供将用于处理连接的网络过滤器链和传输套接字。
3. TLS 传输套接字解密
Envoy 通过 TransportSocket 扩展接口提供可插拔的传输套接字。传输套接字遵循 TCP 连接的生命周期事件,并读写网络缓冲区。传输套接字必须实现的一些关键方法是
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()
,当数据即将写入连接(由于某个响应)时被调用。Filter 同时实现 ReadFilter 和 WriteFilter。
关键过滤器方法的签名是
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
允许过滤器暂停过滤器链的执行。例如,如果需要查询速率限制服务,则速率限制网络过滤器将从 onData()
返回 Network::FilterStatus::StopIteration
,并在查询完成后调用 continueReading()
。
处理 HTTP 的监听器的最后一个网络过滤器是 HTTP 连接管理器 (HCM)。它负责创建 HTTP/2 编解码器和管理 HTTP 过滤器链。在本例中,这是唯一的网络过滤器。一个使用多个网络过滤器的网络过滤器链示例如下所示
在响应路径上,网络过滤器链按与请求路径相反的顺序执行。
5. HTTP/2 编解码器解码
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 过滤器接口有三种类型
StreamDecoderFilter,带有用于请求处理的回调。
StreamEncoderFilter,带有用于响应处理的回调。
StreamFilter 同时实现
StreamDecoderFilter
和StreamEncoderFilter
。
查看解码器过滤器接口
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()
函数。如果返回的 FilterHeadersStatus
为 Continue
,HCM 就会将头部(可能已被 CustomFilter 修改)传递给路由器过滤器。
解码器和编解码器过滤器在请求路径上执行。编码器和编解码器过滤器在响应路径上执行,按 反向顺序 执行。考虑以下示例过滤器链
请求路径将如下所示
而响应路径将如下所示
当在 路由器 过滤器上调用 decodeHeaders()
函数时,路由选择将完成,并选择一个集群。HCM 在 HTTP 过滤器链执行开始时从其 RouteConfiguration
中选择一个路由。这被称为 _缓存的路由_。过滤器可能会修改头部,并导致选择新的路由,方法是请求 HCM 清除路由缓存并重新评估路由选择。过滤器也可以通过 setRoute
回调直接设置此缓存的路由选择。当调用路由器过滤器时,路由将最终确定。所选路由的配置将指向一个上游集群名称。然后,路由器过滤器向 ClusterManager
请求该集群的 HTTP 连接池。这涉及负载均衡和连接池,将在下一节中讨论。
生成的 HTTP 连接池用于在路由器中构建 UpstreamRequest
对象,该对象封装了上游 HTTP 请求的 HTTP 编码和解码回调方法。一旦在上游连接池中的连接上分配了流,请求头部就会通过调用 UpstreamRequest::encodeHeaders
函数转发到上游端点。
路由器过滤器负责从 HTTP 连接池分配的流上的所有上游请求生命周期管理方面。它还负责请求超时、重试和亲缘性。
路由器过滤器还负责创建和运行 上游 HTTP 过滤器 链。默认情况下,上游 HTTP 过滤器将在头部到达路由器过滤器后立即开始运行,但是 C++ 过滤器可以在上游连接建立之前暂停,如果它们需要检查上游流或连接。上游 HTTP 过滤器链默认情况下通过集群配置进行配置,因此例如,阴影请求可以为主集群和阴影集群分别具有独立的上游 HTTP 过滤器链。同样,由于上游 HTTP 过滤器链位于路由器过滤器之上,因此它会在每次重试尝试时运行,允许对每个重试进行头部操作,包括有关上游流和连接的信息。与下游 HTTP 过滤器不同,上游 HTTP 过滤器不能更改路由。
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 过滤器中调用解码器/编码器请求生命周期事件的各种回调,例如,当响应尾部被转发或请求主体被流式传输时。同样,读/写网络过滤器也会在请求期间数据双向流动时调用其各自的回调。
异常值检测 端点的状态会在请求进行时进行修改。
对于 HTTP/2 和 HTTP/3 协议,代理完成并销毁流的点由独立的半关闭选项决定。如果启用了独立的半关闭,则在请求和响应都完成后,即达到各自的流结束时,流将被销毁,方法是在双向接收尾部或带有流结束的头部/主体后,并且响应具有成功(2xx)状态代码。这在 FilterManager::checkAndCloseStreamIfFullyClosed()
中处理。
对于 HTTP/1 协议,或者如果禁用了独立的半关闭,则在响应完成并达到其流结束时,即当接收到尾部或带有流结束的响应头部/主体时,流将被销毁,即使请求尚未完成。这在 Router::Filter::onUpstreamComplete()
中处理。
请求可能提前终止。这可能是由于(但不限于)
请求超时。
上游端点流重置。
HTTP 过滤器流重置。
断路。
上游资源不可用,例如路由缺少集群。
没有健康端点。
DoS 防护。
HTTP 协议违规。
来自 HCM 或 HTTP 过滤器的本地回复。例如,速率限制 HTTP 过滤器返回 429 响应。
如果发生上述任何情况,Envoy 可能会发送内部生成的响应(如果上游响应头部尚未发送),或者会重置流(如果响应头部已转发到下游)。Envoy 调试常见问题解答 提供了有关解释这些提前流终止的更多信息。
11. 请求后处理
请求完成后,流将被销毁。还会发生以下操作