Envoy
整体架构
简介
Envoy是使用C++开发的高性能代理,用于调节服务网格中所有服务的入站和出站流量。在Istio中,Envoy被用于sidecar,和对应的应用服务部署在k8s的同一个pod中。
Envoy调节所有出入应用服务的流量。经过所有Envoy的流量行为都会调用Mixer,为Mixer提供一组描述请求和请求周围环境的Attribute。根据Envoyy的配置和Attribute ,Mixer会调用各种后台的基础设施资源。而这些Attribute 又可以在Mixer中用于决策使用何种决策,并发送给监控系统。
设计原则
Envoy最重要的设计原则是网络透明性,Envoy的设计者认为,网络对应用程序应该是透明的,应用程序只需要按照标准化的方式使用网络,不需要感知网络的具体细节来源,这样当发生应用程序或者网络故障时,很容易区分问题的边界,从而定位出问题的根源。
网络透明是Envoy架构层面总的指导原则,架构设计的各个维度都围绕这个知道原则展开,比如对Envoy流量的透明拦截、对通信安全的透明支持以及完善的链路治理支持等,Envoy架构设计和实现层面处处可见网络透明的影子,目的是把用户体验放在第一位,最大程度地提高业务使用Envoy时的易用性。
Envoy架构设计的另外一个重要指导原则是最终一致性。Envoy在配置处理、统计子系统等多个场景都会采用基于最终一致性的设计。通过最终一致性,可以简化Envoy的架构设计,通过牺牲一些不太重要的指标,可以换来架构易用性和性能上的巨大提升
最后一个设计原则是对性能的折中考虑,Envoy语言选型、架构层面的优秀设计已经可以保证Envoy有非常优异的性能表现。性能对于通用的代理服务来说无疑是非常重要的,Envoy重视性能的同时,不会执着于极限性能,不会为了性能考虑,牺牲架构上的简单和易用性设计,不会为了极限性能牺牲代码的可维护性。
整体架构
Envoy线程模型由负责控制流的主线程和负责数据流的工作线程组成,其中工作线程池的线程池个数和CPU核数相当即可。所有工作线程同时监听iptables透明拦截的Downstream客户端请求,监听器信息由监听发现服务LDS负责管理,监听到服务端请求后再当前线程中创建连接,后续该连接上的请求消息处理均在这个线程内完成,以减少请求处理过程中的线程切换开销。请求处理采用的是异步事件驱动架构,通过各自线程的时间调度器进行事件调度。
Envoy的扩展性由Filter插件机制负责,每个监听器下对应俩类插件:一类是监听器相关的过滤插件,用于对 监听行为进行扩展;另一类是网络过滤插件,负责建立请求连接,协议解析和路由工作。
当前Envoy实现最完整的是HTTP网络过滤插件,通过路由发现服务RDS管理HTTP协议路由,HTTP路由是个多层树状结构,对应的是每个集群以及相应的匹配条件,其中集群信息由集群发现CDS管理。Envoy集群由多种类型,其中服务发现模式的集群,集群节点信息由节点发现服务EDS管理。通过集群的负载均衡算法获取到相应的目标节点后,将Downstream请求信息转发给Upstream。
Envoy请求处理需要的LDS,RDS,CDS和EDS配置信息由控制平面通过XDS协议动态下发给Envoy。
名词
Iptables
Iptables是集成在Linux内核中包过滤防火墙系统。使用iptables可以添加、删除具体的过滤规则,iptables默认维护者4个表和5个链,所有的防火墙策略都被写入这些表与链中
四表指的是iptables的功能,默认的iptables规则有filter表(过滤规则表)、nat表(地址转换规则表)、mangle表(修改数据标记为规则表)、raw(跟踪数据表规则表)
- filter表:控制数据包是否允许进出及转发,可以控制的链路有INPUT、FORWARD和OUTPUT
- nat 表:控制数据包中地址转换,可以控制的链路有 PREROUTING、INPUT、OUTPUT 和 POSTROUTING。
- mangle:修改数据包中的原数据,可以控制的链路有 PREROUTING、INPUT、OUTPUT、FORWARD 和 POSTROUTING。
- raw:控制 nat 表中连接追踪机制的启用状况,可以控制的链路有 PREROUTING、OUTPUT。
五链是指内核中控制网络的NetFilter定义的4个规则链。每个规则表中包含多个数据链INPUT(入站数据过滤)、OUTPUT(出站数据过滤)、FORWARD(转发数据过滤)、PREROUTING(路由前过滤)和POSTROUTING(路由后过滤),防火墙规则需要写入到这些具体的数据链中。
可以看出,如果是外部主机发送数据包给防火墙本机,数据将会经过 PREROUTING 链与 INPUT 链;如果是防火墙本机发送数据包到外部主机,数据将会经过 OUTPUT 链与 POSTROUTING 链;如果防火墙作为路由负责转发数据,则数据将经过 PREROUTING 链、FORWARD 链以及 POSTROUTING 链。
xDS
xDS是一类发现服务的总和,包含LDS,RDS,CDS,EDS以及SDS
Envoy通过xDS API可以动态获取Listener(监听器),Route(路由), Cluster(集群), Endpoint(集群成员)以及Secret(证书)配置。
LDS
Listener发现服务。
Listener监听器控制Envoy启动端口监听(目前只支持TCP协议),并配置L3L4层过滤协议,当网络连接达到后,配置好的网络堆栈开始处理后续事件。
这种通用监听器体系结构用于执行大多数不同的代理任务(限流,客户端认证,HTTP代理,TCP代理等)
RDS
Route发现服务,用于HTTP连接管理过滤器动态取路由配置
路由配置包含HTTP头部修改(增加删除HTTP头部键值),virtual hosts(虚拟主机),以及virtual hosts定义的各个路由条目
CDS
Cluster发现服务,用于动态获取Cluster信息。
Envoy cluster管理管理着所有的上游cluster
鉴于上有cluster或者主机可用于任何代理转发服务,所以上游cluster一般从Listener或route中抽出来
EDS
Endpont发现服务
在envoy术语中,Cluster成员就叫Endpoint,对于每个Cluster,Envoy通过EDS API动态获取Endpoint
EDS作为首选的服务发现的原因有俩点:
- 与通过DNS解析的负载均衡进行路由相比,Envoy能明确的指导每个上游主机的信息,因而可以做出更加智能的负载均衡策略
- Endpoint配置包含负载权重、可用域等附加主机属性,这些属性可用于服务网格负载均衡,统计收集等过程中
SDS
Secret发现服务,用于运行时动态获取TLS证书。
若没有SDS特性,在K8s环境中,必须创建包含证书的Secret,代理启动前Secret必须挂在到sidecar容器中,如果怕证书过期,则需要重新部署。
使用SDS,集中式的SDS服务器将证书分发给所有Envoy实例,如果证书过期,服务器将会将心的证书分发,Envoy
接收到新的证书后不用重新部署。
术语
Host
:能够进行网络通信的实体(如服务器上的应用程序)。Downstream
:下游主机连接到Envoy
,发送请求并接收响应。Upstream
:上游主机接收来自Envoy
连接和请求并返回响应。Listener
:可以被下游客户端连接的命名网络(如端口、Unix
套接字)。一般是每台主机运行一个Envoy
,使用单进程运行,但是每个进程中可以启动任意数量的Listener
(监听器),每个监听器都独立配置一定数量的(L3/L4
)网络过滤器。Cluster
:Envoy
连接到的一组逻辑上相似的上游主机。Mesh
:以提供一致的网络拓扑的一组主机。Runtime Configuration
:与Envoy
一起部署的外置实时配置系统。Listener Filter
:Listener
使用Listener Filter
(监听器过滤器)来操作链接的元数据,它的作用是在不更改Envoy
的核心功能的情况下添加更多的集成功能。Http Route Table
:HTTP
的路由规则,例如请求的域名,Path
符合什么规则,转发给哪个Cluster
。
与nginx的区别
Envoy
对HTTP/2
的支持比Nginx
更好,支持包括upstream
和downstream
在内的双向通信,而Nginx
只支持downstream
的连接。- 高级负载均衡功能是免费的,
Nginx
的高级负载均衡功能则需要付费的Nginx Plus
支持。 Envoy
支持热更新,Nginx
配置更新之后需要Reload
。Envoy
更贴近Service Mesh
的使用习惯,Nginx
更贴近传统服务的使用习惯。
特性
高级负载均衡
分布式系统中不同模块间的负载均衡是一个复杂的问题。因为 Envoy
是一个自组织的代理,所以它能在一个地方实现高级负载均衡技术并使他们可被访问。当前 Envoy
支持自动重试、断路器、全局限速、阻隔请求、异常检测,将来还会支持按计划进行请求速率控制。
动态配置
Envoy
提供了可选的一系列的分层的动态配置 API
,使用这些 API
可以构建出复杂的集中式部署管理。
正向代理
虽然 Envoy
设计初衷是服务和服务之间通信系统,得益于其监视、管理、服务发现和负载均衡算法的实现,Enovy
包含了足够多的特性为绝大多数 Web
服务做正向代理。
除了这些之外还有对 HTTP/2
的支持,L3
、L4
、L7
代理,可以实现 TCP Proxy
、HTTP Proxy
等功能。
线程模型
Envoy
使用单进程多线程架构,其中一个扮演主线程的控制各种协调任务,而一些工作线程负责监听、过滤和转发。一旦某个链接被监听器 Listener
接受,那么这个链接将会剩余的生命周期绑定在这个 Woker
线程。这种架构会使得大部分工作工作在单线程的情况下,只有少量的工作会涉及到线程间通信,Envoy
代码是 100% 非阻塞的。
Listener 监听器
- 一个
Envoy
进程可以设置多个不同的Listener
,建议一台机器只使用一个Envoy
实例。 - 每一个
Listener
的网络层L3/L4
过滤器是独立配置的。并且一个Listener
是可以通过配置来完成多种任务的,比如:访问限制、TLS 客户端校验、HTTP 链接管理等。 Listener
也有自己的非网络层过滤器,它可以修改链接的Metadata
信息,通常用来影响接下来链接是如何被网络层过滤器处理的。- 无论网络层过滤器还是
Listener
过滤器都可以提前终止后续的过滤器链的执行。
HTTP连接管理器
Envoy
是完整支持HTTP/1.1
、Websockets
和HTTP/2
,不支持SPDY
。- 这层过滤器主要是将原始的传递数据转变成
HTTP
层级的信息和事件,如收到Headers
、收到Body
数据,同样它也可以做接入日志、Request ID
生成和追踪、Req/Res
头部修改工作、路由表管理、统计分析。 - 每一个
HTTP
链接管理器有一个相匹配的路由表,路由表可以静态指定,也可以动态地通过RDS API
来设置route-dynamic
。 - 其内部还有
HTTP
过滤器,可以支持在HTTP
层级。在无需关注使用什么协议 (HTTP/1.1
或HTTP/2
) 实现的情况下进行操作HTTP
内容,支持Encode
、Decode
、Encode/Decode
三种不同类型过滤器。
HTTP路由器
- 经常用在做边缘/反向代理和构建内部
Envoy Mesh
发挥巨大作用。 HTTP
路由器可以支持请求重试配置:最大重试次数和设置重试条件,比如某些5XX
错误和具有幂等性操作的4XX
错误。Envoy
自己使用HTTP/2
链接管理器实现了gRPC
协议,将原来官方的Google gRPC
内置的很多功能,比如重试、超时、Endpoint
发现、负载均衡、负载报告、健康检查等功能都实现了。将来除非特殊特性必须,都可以使用Envoy gRPC
来实现。
Cluster 管理器
Cluster
管理器暴露 API
给过滤器,并允许过滤器可以得到链接到上游集群的 L3/L4
链接或者维持一个抽象的 HTTP
连接池用来链接上游集群(上游主机支持 HTTP 1.1
还是 HTTP 2
都是被隐藏的)。过滤器决定是使用 L3/L4
链接还是 HTTP Stream
来链接上游集群。而对于集群管理器来说,它负责所有集群内主机的可用性、负载均衡、健康度、线程安全的上游链接数据,上游链接类型 TCP/UP
、UDS
,上游可接受的协议 HTTP 1.1/2
。
Cluster
管理器既可以静态配置,也可以使用 CDS-Cluster-Discovery-Service API
来动态配置。集群在正式使用之前有一个 “加热” Warming
的过程:先做服务发现必要的初始化,比如 DNS
记录更新、EDS
更新,然后进行健康检查,当进行完上述的过程,会进入Becoming available
状态,这个阶段 Envoy
不会把流量指向它们; 在更新集群时,也不会把正在处理流量的集群处理掉,而是用新的去替换老的那些还未进行任何流量的集群。
本地线程存储机制
Envoy中多个线程并发操作同一个数据的场景很多,为了减少多线程并发访问共享数据时的锁开销,提升系统的整体性能,Envoy引入了TLS(线程本地存储)的概念。通过TLS,主线程上运行的代码可以分配到进程范围内的TLS槽,TLS槽是一个允许O(1)访问的向量索引,主线程可以将任意数据放在TLS槽,然后通过事件的方式将数据变更发布到每个工作线程中。每个工作线程都会基于主线程的TLS槽数据,复制一份完全相同的线程本地数据,工作线程只需要访问本线程的线程本地数据,同时每个线程的事件调度器响应主线程发布的变更事件,触发线程本地数据的更新。
Envoy的TLS数据通过下述的thread_local_data_来进行管理,thread_local_data_是基于C++11 thread_local的线程本地数据,每个线程均拥有各自的线程本地数据,同时通过本地的事件调度器dispatcher_和主线程进行通信。
服务发现
xDS
与HAProxy以及Nginx等传统Proxy依赖静态配置文件来定义各种资源以及数据转发规则不同,Envoy几乎所有配置都可以通过订阅来获取,如监控制定下路径的文件、启动gRPC流或轮训Rest接口,对应发现服务以及各种各样的API称为xDS。Envoy与xDS之间通过Proto约定请求和响应的数据模型,不同类型资源,对应的数据类型也不同。
以Istio中Pilot为例,当Pilot发现新的服务或路由规则被创建(通过K8s急群众特定的CRD资源变化、或发现Consul服务注册和配置变化),Pilot会通过已经和Envoy之间建立好的gRPC流将相关的配置推送到Envoy。Envoy接收到相关配置并校验无误后,就会动态的更新运行配置,使用新的配置更新相关资源。
利用xDS协议,Envoy可以实现配置的完全动态化,配置实时更新而无需重启Envoy或者影响业务。此外,利用其L3/L4/L7 Filter机制,Envoy可以完全无侵入的扩展各种强大的功能。利用其内置的Tracing机制和Stats模块,可以很方便的实现对流量的跟踪以及监控,保证Envoy中流量的可观察性。无论是Envoy Filter或者其Stats,都包含大量的内容,此处不会详述。
Listener:Envoy工作的基础
简单理解,Listener是Envoy打开的一个监听端口,用于接收来自Downstream(客户端)连接。Envoy可以支持复数个Listener。多个Listener之间几乎所有的配置都是隔离的。Listener配置中核心包括监听地址、Filter链等。
Listener对应的配置/资源发现服务称之为LDS。LDS是Envoy正常工作的基础,没有LDS,Envoy就不能实现端口监听(如果启动配置也没有提供静态Listener的话),其他所有xDS服务也失去了作用。
Cluster:对上游服务的抽象
在Envoy中,每个Upstream上游服务都被抽象成一个Cluster。Cluster包含该服务的连接池、超时时间、endpoints地址、端口、类型(类型决定了Envoy获取该Cluster具体可以访问的endpoint方法)等等。
Cluster对应的配置/资源发现服务称之为CDS。一般情况下,CDS服务会将其发现的所有可访问服务全量推送给Envoy。与CDS紧密相关的另一种服务称之为EDS。CDS服务负责Cluster资源的推送。而当该Cluster类型为EDS时,说明该Cluster的所有endpoints需要由xDS服务下发,而不使用DNS等去解析。下发endpoints的服务就称之为EDS。
Router:上下游之间的桥梁
Listener可以接收来自下游的连接,Cluster可以将流量发送给具体的上游服务,而Router则决定Listener在接收到下游连接和数据之后,应该将数据交给哪一个Cluster处理。它定义了数据分发的规则。虽然说到Router大部分时候都可以默认理解为HTTP路由,但是Envoy支持多种协议,如Dubbo、Redis等,所以此处Router泛指所有用于桥接Listener和后端服务(不限定HTTP)的规则与资源集合。
Route对应的配置/资源发现服务称之为RDS。Router中最核心配置包含匹配规则和目标Cluster,此外,也可能包含重试、分流、限流等等。
Filter:强大源于可扩展
Filter,通俗的讲,就是插件。通过Filter机制,Envoy提供了极为强大的可扩展能力。在Envoy中,很多核心功能都使用Filter来实现。比如对于Http流量和服务的治理就是依赖HttpConnectionManager(Network Filter,负责协议解析)以及Router(负责流量分发)两个插件来实现。利用Filter机制,Envoy理论上可以实现任意协议的支持以及协议之间的转换,可以对请求流量进行全方位的修改和定制。强大的Filter机制带来的不仅仅是强大的可扩展性,同时还有优秀的可维护性。Filter机制让Envoy的使用者可以在不侵入社区源码的基础上对Envoy做各个方面的增强。
Filter本身并没有专门的xDS服务来发现配置。Filter所有配置都是嵌入在LDS、RDS以及CDS(Cluster Network Filter)中的。
xDS以及各个资源之间的关系如图2所示。
两种角色
南北流量:客户端 和服务端之间通信的流量
东西流量:服务端和服务端之间通信的流量
作为一个服务代理软件,Envoy并不限定自己的使用方法。它最长扮演的是俩种不同的角色,一种是作为集体流量入口网关(Getway),管理南北流量;一种是作为服务Sidecar,拦截并治理服务网格中的东西流量。
API网关负责集中管理集群或者网络对外暴露的接口,为集群外或者网格外客户端调用集群内或网格服务提供了统一的流量入口和治理方案。
Sidecar是当前微服务领域先进的实践。微服务框架解决了单体应用过于复杂,难以维护,语言绑定,不易扩展等问题,使得多种语言、多种框架的多个微服务构成一个整体,并通过API网关向外提供统一接口。但是微服务框架也带来了服务治理复杂、故障定位难、服务注册、服务发现等新挑战。
克服这些挑战,业界有如下三种技术方案。
- 第一种,服务自身感知服务发现、服务注册等流程,每个服务去实现相关逻辑保证服务之间协同工作正常。但是此类方案过于复杂,成本太高,每个服务都必须修改代码,难以维护。
- 第二种,通过在服务中集成服务发现、服务注册等相关功能的SDK,由SDK去完成服务治理的相关功能。SDK可以减少服务业务代码的复杂性,而且也避免了不同服务间的重复冗余的代码编写。但是SDK首先会带来语言绑定的问题,否者必须为每种语言实现对应的SDK;其次,SDK和服务绑定,所以当SDK升级时,也必须重新编译甚至适配SDK。
- 第三种,就是使用Sidecar拦截服务的进出口流量。Sidecar与服务从功能、到语言到框架都完全解耦,是两个完全独立的进程。Sidecar升级和服务更新互不影响。服务可以专注于业务功能,所有服务注册、服务发现、服务治理等能力都放置在Sidecar中实现。
作为Sidecar时,Envoy通过修改IP Table实现对服务的进出口流量的拦截,并进一步实现对进出口流量的管理。每个Sidecar通过xDS协议和控制面交互,获取集群中其他服务的相关信息以及各种服务治理相关(鉴权、分流、流量复制等等)的配置。服务本身只需要专注于业务逻辑,所有网络流量相关的工作都委托给Envoy Sidecar。
增量xDS
Envoy通过xDS协议与控制面实现配置数据的交换。当控制面检测中配置变化(比如通过Kubernetes Watch到新service或者其他的CRD资源更新),会向Envoy发送一个discoveryResponse来将更新后的配置下发到Envoy。之后,Envoy主线程在接受到数据之后,通过向各个工作线程中追加配置更新事件来完成配置的实际更新和生效。
但是,需要注意的是,控制面下发的discoveryResponse是一个全量的配置。换言之,哪怕是修改了一条路由中的一个小小请求头匹配,所有Listener、Cluster、Router都必须更新,Envoy会用接受到的新配置替换旧配置。使用现有更新方案虽然逻辑简单明了,但是在负载较多、配置量较大时,会造成大量的流量浪费和不必要的计算开销。
尤其是对于Sidecar模式下的Envoy,该问题会更加明显。网格中服务需要访问其他服务时,气流量会首先被Envoy Sidecar所拦截,之后由Sidecar将请求转发给对应的服务。由于Sidecar并不了解其代理的服务依赖网格中的哪些服务,所以它会记录网格中所有的信息。但是,事实上一个网格服务往往只会依赖网格中少量的服务。
因为上述问题,Envoy社区提出了delta xDS方案,实现增量的xDS配置更新。简单的说,在delta增量更新方案中,当配置发生变化时,只有发生变化的一项配置(配置的最小单位为一个完整的proto message)需要下发和更新。
基于delta增量更新方案,可以实现以下三种新的功能:
- Lazy loading:按照具体需要订阅相关资源。全量xDS中,每个Envoy Sidecar都会缓存大量的Cluster配置,但是实际部分Cluster从未被访问过,甚至将数据流量导向此类Cluster的相关路由配置都不存在,此类的Cluster配置只会浪费内存和降低Envoy效率。使用delta增量更新方案,可以在实际的配置被使用时再订阅该资源,从控制面获取相关配置(首次访问性能会受到一定影响)。
- 增量更新:当部分资源更新时,如某个Cluster配置发生变化,某条Router修改了参数,那么只有对应的Cluster或Router配置会被下发和更新。在负载较多、配置量较大时,该功能可以有效减少网络内因配置更新而引入的数据流量。
- 缓存擦除(或者说on demand loading):根据Envoy当前负载实际请求动态调整订阅资源类型,对于不再活跃的配置,取消订阅,从Envoy内存中擦除,直到相关配置再次被使用。通过该功能,可以有效限制Envoy配置所占用的数据量,在超大规模应用场景中,可以有效减少Envoy内存开销。