Envoy 进阶指南(下):深入探究Envoy服务和架构
接上篇:《Envoy 进阶指南(上):从入门到核心功能全掌握》
链接
文章目录
- 3.深入探究Envoy
- 3.1 Envoy服务发现机制
- 3.1.1文件订阅
- 3.1.2 gRPC 流式订阅
- 3.1.3 REST-JSON 轮询订阅
- 3.2监听器(Listener)
- 3.3.架构
- 3.3.1.请求流程
3.深入探究Envoy
3.1 Envoy服务发现机制
Envoy 通过查询文件或管理服务器来动态发现资源。这些发现服务及其相应的 API 被统称为 xDS。Envoy 通过订阅(subscription)方式来获取资源,如监控指定路径下的文件、启动 gRPC 流(streaming)或轮询 REST-JSON URL。后两种方式会发送 DiscoveryRequest 请求消息,发现的对应资源则包含在响应消息 DiscoveryResponse 中。下面,我们将具体讨论每种订阅类型。
3.1.1文件订阅
发现动态资源的最简单方式就是将其保存于文件,并将路径配置在 ConfigSource 中的 path 参数中。Envoy 使用 inotify(Mac OS X 上为 kqueue)来监控文件的变化,在文件被更新时,Envoy 读取保存的 DiscoveryResponse 数据进行解析,数据格式可以为二进制 protobuf、JSON、YAML 和协议文本等。
core.ConfigSource 配置格式如下:
{
"path": "...",
"api_config_source": "{...}",
"ads": "{...}"
}
文件订阅方式可提供统计数据和日志信息,但是缺少 ACK/NACK 更新的机制。如果更新的配置被拒绝,xDS API 则继续使用最后一个有效配置。
3.1.2 gRPC 流式订阅
单例资源类型发现
每个 xDS API 可以单独配置 ApiConfigSource,指向对应的上游管理服务器的集群地址。每个 xDS 资源类型会启动一个独立的双向 gRPC 流(每个 xDS 资源类型对应的管理服务器可能不同)。API 交付方式采用最终一致性。可以参考后续聚合服务发现(ADS) 章节来了解必要的显式控制序列。
译者注:core.ApiConfigSource 配置格式如下:
{
"api_type": "...",
"cluster_names": [],
"grpc_services": [],
"refresh_delay": "{...}",
"request_timeout": "{...}"
}
类型 URL
每个 xDS API 都与给定的资源类型一一对应。关系如下:
- LDS : envoy.api.v2.Listener
- RDS : envoy.api.v2.RouteConfiguration
- CDS : envoy.api.v2.Cluster
- EDS : envoy.api.v2.ClusterLoadAssignment
- SDS :envoy.api.v2.Auth.Secret
类型 URL 的概念如下所示,其采用 type.googleapis.com/ 的形式,例如 CDS 对应于 type.googleapis.com/envoy.api.v2.Cluster。在 Envoy 发起的发现请求和管理服务器返回的发现响应中,都包括了资源类型 URL。
ACK/NACK 和版本
每个 Envoy 流以发送一个 DiscoveryRequest 开始,包括了列表订阅的资源、订阅资源对应的类型 URL、节点标识符和空的 version_info。EDS 请求示例如下:
version_info:
node: { id: envoy }
resource_names:
- foo
- bar
type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
response_nonce:
管理服务器可立刻或等待资源就绪时发送 DiscoveryResponse 作为响应,示例如下:
version_info: X
resources:
- foo ClusterLoadAssignment proto encoding
- bar ClusterLoadAssignment proto encoding
type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
nonce: A
Envoy 在处理 DiscoveryResponse 响应后,将通过流发送一个新的请求,请求包含应用成功的最后一个版本号和管理服务器提供的 nonce。如果本次更新已成功应用,则 version_info 的值设置为 X,如下序列图所示:
ack 更新
在此序列图及后续章节中,将统一使用以下缩写格式:
- DiscoveryRequest :(V=version_info,R=resource_names,N=response_nonce,T=type_url)
- DiscoveryResponse : (V=version_info,R=resources,N=nonce,T=type_url)
在信息安全中,Nonce 是一个在加密通信只能使用一次的数字。在认证协议中,它往往是一个随机或伪随机数,以避免重放攻击。Nonce 也用于流密码以确保安全。如果需要使用相同的密钥加密一个以上的消息,就需要 Nonce 来确保不同的消息与该密钥加密的密钥流不同。(引用自维基百科)在本文中 nonce 是每次更新的数据包的唯一标识。
有了版本(version_info)这个概念,就可以为 Envoy 和管理服务器共享当前应用配置,以及提供了通过 ACK/NACK 来进行配置更新的机制。如果 Envoy 拒绝了配置更新 X,则回复 error_detail 及前一个版本号,在本例中为空的初始版本号,error_detail 包含了有关错误的更加详细的信息:
nack 更新
重新发送 DiscoveryRequest 后,API 更新可能会在新版本 Y 上成功应用:
每个流都有自己的版本概念,但不同的资源类型不能共享资源版本。在不使用 ADS 的情况下,每个资源类型可能具有不同的版本,因为 Envoy API 允许不同的 EDS/RDS 资源配置指向不同的 ConfigSources。
何时发送更新
管理服务器应该只向 Envoy 客户端发送上次 DiscoveryResponse 后更新过的资源。Envoy 则会根据接受或拒绝 DiscoveryResponse 的情况,立即回复包含 ACK/NACK 的 DiscoveryRequest 请求。如果管理服务器不等待更新完成,每次返回相同的资源结果集合,则会导致 Envoy 和管理服务器通讯效率大打折扣。
在同一个流中,新的 DiscoveryRequests 将取代此前具有相同资源类型的 DiscoveryRequest 请求。这意味着管理服务器只需要响应给定资源类型最新的 DiscoveryRequest 请求即可。
最终一致性考虑
由于 Envoy 的 xDS API 采用最终一致性,因此在更新期间可能导致流量被丢弃。例如,如果通过 CDS/EDS 仅获取到了集群 X,而且 RouteConfiguration 引用了集群 X;在 CDS/EDS 更新集群 Y 配置之前,如果将 RouteConfiguration 将引用的集群调整为 Y ,那么流量将被吸入黑洞而丢弃,直至集群 Y 被 Envoy 实例获取。
对某些应用程序,可接受临时的流量丢弃,客户端或其他 Envoy sidecar 的重试可以解决该问题,并不影响业务逻辑。那些对流量丢弃不能容忍的场景,可以通过以下方式避免流量丢失,CDS/EDS 更新同时携带 X 和 Y ,然后发送 RDS 更新从 X 切换到 Y ,此后发送丢弃 X 的 CDS/EDS 更新。
一般来说,为避免流量丢弃,更新的顺序应该遵循 make before break 模型,其中:
- CDS 首先更新 Cluster 数据(如果有变化)
- EDS 更新相应 Cluster 的 Endpoint 信息(如果有变化)
- LDS 更新 CDS/EDS 相应的 Listener
- RDS 最后更新新增 Listener 相关的 Route 配置
删除不再使用的 CDS cluster 和 EDS endpoints(不再被引用的 endpoint)
如果没有添加新的集群/路由/监听器,或者在更新期间暂时丢弃流量,则可以独立推送 xDS 更新。请注意,在 LDS 更新的情况下,监听器须在接收流量之前被预热,例如如其配置了依赖的路由,则需要先从 RDS 中获取。添加/删除/更新集群信息时,集群也需要进行预热。另一方面,如果管理平面确保路由更新时所引用的集群已经准备就绪,则路由可以不用预热。
3.1.3 REST-JSON 轮询订阅
单个 xDS API 可以通过 REST 端点进行同步(长)轮询。除了无持久流与管理服务器交互外,消息交互顺序与上述两个订阅方式相似。在任何时间点,只存在一个未完成的请求,因此响应消息中的 nonce 在 REST-JSON 中是可选的。DiscoveryRequest 和 DiscoveryResponse 的消息编码遵循 JSON 变换 proto3 规范。ADS 不支持 REST-JSON 轮询订阅。
当轮询周期设置为较小的值时,为了进行长轮询,这时要求避免发送 DiscoveryResponse,除非发生了对请求的资源的更改。
3.2监听器(Listener)
监听器(Listener)就是 Envoy 的监听地址,可以是端口或 Unix Socket。Envoy 在单个进程中支持任意数量的监听器。通常建议每台机器只运行一个 Envoy 实例,每个 Envoy 实例的监听器数量没有限制,这样可以简化操作,统计数据也只有一个来源,比较方便统计。目前 Envoy 支持监听 TCP 协议和 UDP 协议。
TCP
每个监听器都可以配置多个过滤器链(Filter Chains),监听器会根据 filter_chain_match 中的匹配条件将流量转交到对应的过滤器链,其中每一个过滤器链都由一个或多个网络过滤器(Network filters)组成。这些过滤器用于执行不同的代理任务,如速率限制,TLS 客户端认证,HTTP 连接管理,MongoDB 嗅探,原始 TCP 代理等。
除了过滤器链之外,还有一种过滤器叫监听器过滤器(Listener filters),它会在过滤器链之前执行,用于操纵连接的元数据。这样做的目的是,无需更改 Envoy 的核心代码就可以方便地集成更多功能。例如,当监听的地址协议是 UDP 时,就可以指定 UDP 监听器过滤器。
UDP
Envoy 的监听器也支持 UDP 协议,需要在监听器过滤器中指定一种 UDP 监听器过滤器(UDP listener filters)。目前有两种 UDP 监听器过滤器:UDP 代理(UDP proxy) 和 DNS 过滤器(DNSfilter)。UDP 监听器过滤器会被每个 worker 线程实例化,且全局生效。实际上,UDP 监听器(UDP Listener)配置了内核参数 SO_REUSEPORT,这样内核就会将 UDP 四元组相同的数据散列到同一个 worker 线程上。因此,UDP 监听器过滤器是允许面向会话(session)的。
监听器配置结构
监听器的配置结构如下:
{
"name": "...",
"address": "{...}",
"filter_chains": [],
"per_connection_buffer_limit_bytes": "{...}",
"metadata": "{...}",
"drain_type": "...",
"listener_filters": [],
"listener_filters_timeout": "{...}",
"continue_on_listener_filters_timeout": "...",
"transparent": "{...}",
"freebind": "{...}",
"socket_options": [],
"tcp_fast_open_queue_length": "{...}",
"traffic_direction": "...",
"udp_listener_config": "{...}",
"api_listener": "{...}",
"connection_balance_config": "{...}",
"reuse_port": "...",
"access_log": []
}
- name : 监听器名称。默认情况下,监听器名称的最大长度限制为 60 个字符。可以通过 --max-obj-name-len 命令行参数设置为所需的最大长度限制。
- address : 监听器的监听地址,支持网络 Socket 和 Unix Domain Socket(UDS) 两种类型。
- filter_chains : 过滤器链的配置。
- per_connection_buffer_limit_bytes : 监听器每个新连接读取和写入缓冲区大小的软限制。默认值是 1MB。
- listener_filters : 监听器过滤器在过滤器链之前执行,用于操纵连接的元数据。这样做的目的是,无需更改 Envoy 的核心代码就可以方便地集成更多功能。例如,当监听的地址协议是 UDP 时,就可以指定 UDP 监听器过滤器。
- listener_filters_timeout : 等待所有监听器过滤器完成操作的超时时间。一旦超时就会关闭 Socket,不会创建连接,除非将参数 continue_on_listener_filters_timeout 设为 true。默认超时时间是 15s,如果设为 0 则表示禁用超时功能。
- continue_on_listener_filters_timeout : 布尔值。用来决定监听器过滤器处理超时后是否创建连接,默认为 false。
- freebind : 布尔值。用来决定是否设置 Socket 的 IP_FREEBIND 选项。如果设置为 true,则允许监听器绑定到本地并不存在的 IP 地址上。默认不设置。
- socket_options : 额外的 Socket 选项。
- tcp_fast_open_queue_length : 控制 TCP 快速打开(TCP Fast Open,简称 TFO)。TFO 是对TCP 连接的一种简化握手手续的拓展,用于提高两端点间连接的打开速度。它通过握手开始时的 SYN 包中的 TFO cookie(一个 TCP 选项)来验证一个之前连接过的客户端。如果验证成功,它可以在三次握手最终的 ACK 包收到之前就开始发送数据,这样便跳过了一个绕路的行为,更在传输开始时就降低了延迟。该字段用来限制 TFO cookie 队列的长度,如果设为 0,则表示关闭 TFO。
- traffic_direction : 定义流量的预期流向。有三个选项:UNSPECIFIED、INBOUND 和 OUTBOUND,分别代表未定义、入站流量和出站流量,默认是 UNSPECIFIED。
- udp_listener_config : 如果 address 字段的类型是网络 Socket,且协议是 UDP,则使用该字段来指定 UDP 监听器。
- connection_balance_config : 监听器连接的负载均衡配置,目前只支持 TCP。
- reuse_port : 布尔值。用来决定是否设置 Socket 的 SO_REUSEPORT 选项。如果设置为 true,则会为每一个 worker 线程创建一个 Socket,在有大量连接的情况下,入站连接会均匀分布到各个 worker 线程中。如果设置为 false,所有的 worker 线程共享同一个 Socket。
- access_log : 日志相关的配置。
3.3.架构
Envoy 的架构如图所示:
Envoy 接收到请求后,会先走 FilterChain,通过各种 L3/L4/L7 Filter 对请求进行微处理,然后再路由到指定的集群,并通过负载均衡获取一个目标地址,最后再转发出去。
其中每一个环节可以静态配置,也可以动态服务发现,也就是所谓的 xDS。这里的 x 是一个代词,类似云计算里的 XaaS 可以指代 IaaS、PaaS、SaaS 等。
配置结构
Envoy 的整体配置结构如下:
{
"node": "{...}",
"static_resources": "{...}",
"dynamic_resources": "{...}",
"cluster_manager": "{...}",
"hds_config": "{...}",
"flags_path": "...",
"stats_sinks": [],
"stats_config": "{...}",
"stats_flush_interval": "{...}",
"watchdog": "{...}",
"tracing": "{...}",
"runtime": "{...}",
"layered_runtime": "{...}",
"admin": "{...}",
"overload_manager": "{...}",
"enable_dispatcher_stats": "...",
"header_prefix": "...",
"stats_server_version_override": "{...}",
"use_tcp_for_dns_lookups": "..."
}
- node : 节点标识,配置的是 Envoy 的标记信息,management server 利用它来标识不同的 Envoy 实例。参考 core.Node
- static_resources : 定义静态配置,是 Envoy 核心工作需要的资源,由 Listener、Cluster 和 Secret 三部分组成。参考 config.bootstrap.v2.Bootstrap.StaticResources
- dynamic_resources : 定义动态配置,通过 xDS 来获取配置。可以同时配置动态和静态。
- cluster_manager : 管理所有的上游集群。它封装了连接后端服务的操作,当 Filter 认为可以建立连接时,便调用 cluster_manager 的 API 来建立连接。cluster_manager 负责处理负载均衡、健康检查等细节。
- hds_config : 健康检查服务发现动态配置。
- stats_sinks : 状态输出插件。可以将状态数据输出到多种采集系统中。一般通过 Envoy 的管理接口 /stats/prometheus 就可以获取 Prometheus 格式的指标,这里的配置应该是为了支持其他的监控系统。
- stats_config : 状态指标配置。
- stats_flush_interval : 状态指标刷新时间。
- watchdog : 看门狗配置。Envoy 内置了一个看门狗系统,可以在 Envoy 没有响应时增加相应的计数器,并根据计数来决定是否关闭 Envoy 服务。
- tracing : 分布式追踪相关配置。
- runtime : 运行时状态配置(已弃用)。
- layered_runtime : 层级化的运行时状态配置。可以静态配置,也可以通过 RTDS 动态加载配置。
- admin : 管理接口。
- overload_manager : 过载过滤器。
- header_prefix : Header 字段前缀修改。例如,如果将该字段设为 X-Foo,那么 Header 中的 x-envoy-retry-on 将被会变成 x-foo-retry-on。
- use_tcp_for_dns_lookups : 强制使用 TCP 查询 DNS。可以在 Cluster 的配置中覆盖此配置。
过滤器
Envoy 进程中运行着一系列 Inbound/Outbound 监听器(Listener),Inbound 代理入站流量,Outbound 代理出站流量。Listener 的核心就是过滤器链(FilterChain),链中每个过滤器都能够控制流量的处理流程。过滤器链中的过滤器分为两个类别:
- 网络过滤器(Network Filters): 工作在 L3/L4,是 Envoy 网络连接处理的核心,处理的是原始字节,分为 Read、Write 和 Read/Write 三类。
- HTTP 过滤器(HTTP Filters): 工作在 L7,由特殊的网络过滤器 HTTP connection manager 管理,专门处理 HTTP1/HTTP2/gRPC 请求。它将原始字节转换成 HTTP 格式,从而可以对 HTTP 协议进行精确控制。
除了 HTTP connection manager 之外,还有一种特别的网络过滤器叫 Thrift Proxy。Thrift 是一套包含序列化功能和支持服务通信的 RPC 框架,详情参考维基百科。Thrift Proxy 管理了两个 Filter:Router 和 Rate Limit。
除了过滤器链之外,还有一种过滤器叫监听器过滤器(Listener Filters),它会在过滤器链之前执行,用于操纵连接的元数据。这样做的目的是,无需更改 Envoy 的核心代码就可以方便地集成更多功能。例如,当监听的地址协议是 UDP 时,就可以指定 UDP 监听器过滤器。
根据上面的分类,Envoy 过滤器的架构如下图所示:
3.3.1.请求流程
-
在工作线程上运行的 Envoy 监听器,接受来自下游的 TCP 连接。
图源:https://www.envoyproxy.io/docs/envoy/v1.22.6/intro/life_of_a_request -
监听过滤器链被创建并运行后。它可以提供 SNI 和 pre-TLS 信息。一旦完成后, 监听器将匹配网络过滤器链。每个监听器可能具有多个过滤器链,这些过滤器链是在目标 IP CIDR 范围、SNI、ALPN、源端口等的某种组合上匹配。传输套接字(在我们的例子中为 TLS 传输套接字)与此过滤器链相关联。
图源:https://www.envoyproxy.io/docs/envoy/v1.22.6/intro/life_of_a_request
-
在进行网络读取时,TLS 传输套接字将从 TCP 连接读取的数据进行解密,以进行进一步处理。
图源:https://www.envoyproxy.io/docs/envoy/v1.22.6/intro/life_of_a_request -
网络过滤器链已创建并运行。与监听过滤器一样,Envoy 将通过 Network::FilterManagerImpl 实例化其过滤器工厂中的一系列网络过滤器。
图源:https://www.envoyproxy.io/docs/envoy/v1.22.6/intro/life_of_a_request
HTTP 最重要的过滤器是 HTTP 连接管理器,它是链中的最后一个网络过滤器。它负责创建 HTTP/2 编解码器并管理 HTTP 筛选器链。在我们的示例中,这是唯一的网络过滤器。一个使用多个网络过滤器的网络过滤器链的示例如下:
图源:https://www.envoyproxy.io/docs/envoy/v1.22.6/intro/life_of_a_request
在响应路径上,以与请求路径相反的顺序执行网络筛选器链。
图源:https://www.envoyproxy.io/docs/envoy/v1.22.6/intro/life_of_a_request
-
HTTP 连接管理器中的 HTTP/2 编解码器,对来自 TLS 连接的解密数据流进行解帧和解复用,使其成为若干独立的数据流。每个流处理一个请求和响应。
-
对于每个 HTTP 请求流,都会创建并运行一个 HTTP 过滤器链。该请求首先通过可以读取和修改请求的 CustomFilter。路由过滤器是最重要的 HTTP 过滤器,它位于 HTTP 过滤器链的末尾。在路由过滤器上调用 decodeHeaders 时,将选择路由和集群。数据流上的请求头被转发到该集群中的上游端点。路由过滤器通过从集群管理器中匹配到的集群获取 HTTP 连接池,以执行操作。
图源:https://www.envoyproxy.io/docs/envoy/v1.22.6/intro/life_of_a_request -
执行集群特定的负载均衡以查找端点。通过检查集群的断路器,来确定是否允许新的数据流。如果端点的连接池为空或容量不足,则会创建到端点的新连接。
-
上游端点连接的 HTTP/2 编解码器,将请求流与通过单个 TCP 连接流,向上游的任何其他流,进行多路复用和帧化。
-
上游端点连接的 TLS 传输套接字,对这些字节进行加密,并将其写入上游连接的 TCP 套接字。
图源:https://www.envoyproxy.io/docs/envoy/v1.22.6/intro/life_of_a_request -
由请求头、可选的请求体和尾部组成的请求,在上游被代理,而响应在下游被代理。响应以与请求相反的顺序,通过 HTTP 过滤器,从路由器过滤器开始并通过自定义过滤器,然后再发送到下游。
-
当响应完成后,请求流将被销毁。请求后,处理程序将更新统计信息,写入访问日志并最终确定追踪 span。