首先解释什么是 Envoy。Envoy 社区的定义是:Envoy 是一个开源的边缘与服务代理,专为云原生应用而设计。此处只需要抓住最核心的词--代理。
代理,本质属性就是做两件事:第一,代替客户端向服务端发送请求,第二,代理服务端向客户端提供服务。前者隐藏客户端,后者隐藏服务端。在开端代理软件领域,比较著名有 HA Proxy 以及 Nginx,而 Envoy 则是该领域的的一个后起之秀。作为为云原生而设计的高性能网络代理,它具备以下的优点:
Envoy 具备很多优点,但它核心的概念却很少,只有四个。基本上,Envoy 中绝大部分的模块和功能都是围绕着这四个概念展开的:
以上的四个概念或者说资源类型,构成了 Envoy 架构最核心的骨架。此外,Envoy 将请求的来源方向称之为下游(Downstream),而请求转发的去向称之为上游(Upstream)。
总结来说:Downstream 请求自 Listener 进入 Envoy,流经 Filter 被解析、修改、记录然后根据 Route 选择 Cluster 将其发送给 Upstream 服务。
了解以上的核心概念,对于 Envoy 的使用者和学习者来说都非常有用,因为 Envoy 的功能模块和配置使用都是和这些核心概念紧密相关的。
在了解了 Envoy 的背景和最核心的概念之后,接下来介绍 Envoy 最核心的三个特性。分别是基于 xDS 协议的动态配置,基于核心概念 Filter 的可扩展性,以及可观察性。
xDS 协议是 Envoy 带来的最大的改变之一。Envoy 使用 Protobuf 来定义几乎所有的配置,然后通过 xDS 协议实现配置项的动态化加载和生效。
xDS 全称是 x Discovery Service,x 表示某种资源。在 Envoy 当中,最为核心资源自然是 Listener、Cluster、Route 和 Filter。除了 Filter 之外,其他三种资源都有一个对应的 DS 与之关联,分别是 LDS、CDS、RDS:
前面最核心的四种资源之中,Filter 比较特殊,它的配置一般来说都嵌入在 LDS/CDS/RDS 之内。比如 Listener 中除了监听端口之外,最关键的配置内容就是在该端口下接收的数据需要执行哪些 Filter 链以及相关 Filter本身的配置项。这些最终都是通过 LDS 来下发。而 Cluster 中也有对应的 Filter 链,可以通过 CDS 更新和下发。而 RDS 则提供了路由粒度的 Filter 配置能力。
此外,还有一类关键 DS:EDS。EDS 是对 CDS 的补充。很多时候,服务一旦创建,就不会经常变动。但是服务的实例可能会经常变动,比如 Deploy 滚动更新之类的。EDS,就是用于在 CDS 不变的前提下,动态更新每个 Cluster 后面可用的实例。
大部分情况下,如果要做 Envoy 相关的工作,LDS/CDS/RDS/EDS 四种 DS 协议是必须了解的。
目前 Envoy 支持 gRPC 服务、Restful 接口以及磁盘文件三种不同类型的 xDS 数据源。以最经典的gRPC 为例,Envoy 会定义了一个流式的 gPRC Service,xDS 服务提供方只需要实现对应的 gRPC Service。然后在需要配置更新时,向 gRPC 流推送配置数据即可。Envoy 侧会接受配置数据,然后加载更新。
Restful xDS 就是由 Envoy 开放一个 Post 接口由配置的提供方去调用;磁盘文件 xDS 则是由 Envoy 去 Watch 指定文件的变化并在文件更新时更新配置。这两种 xDS 配置方法在实践当中都很少使用。
本质上 xDS 协议并不复杂,真正困难的在于如何把各种资源抽象出来并通过一种通用的协议来封装和传输、如何管理数据的版本以及保证更新过程流量的平稳。这些都涉及到一些实现的细节,就不再赘述了。
下图是一个相对实际的例子。当使用 Isito Pilot 作为 xDS Server 时,如何利用 xDS 来动态更新 Envoy 配置。一般情况下,是用户修改了 K8s 集群中的一些 CRD 资源亦或者注册中心有配置更新才会触发配置更新;之后 Pilot watch 到相关变化变更将相关变化抽象成各种 Envoy 中对应的资源,如 Listener、Cluster,然后通过各个 xDS 将对应资源推送到 Envoy。
可扩展性是 Envoy 最关键的特性之一。Envoy 提供了一个 Filter 机制,让开发者可以完全无侵入的在各个层级定义和扩展 Envoy 功能。
前文介绍过,下游的数据从 Listener 流入到 Envoy。到了 Envoy 之后,Listener 就会把数据交给 Filter 来处理。但是 Filter 也是很多种的。
总结来说就是:Listener Filter 处理连接、Network Filter 处理二进制数据、L7 Filter 处理解析后结构化数据。
其中,L7 Filter 都是作为某个 Network Filter 的子 Filter 存在。这也很好理解,L7 处理解析后结构化数据,总要有一个 Network Filter 来解析二进制数据然后把解析后数据传递给它。
下图是一个相对完整的 Envoy 插件链执行流程图。原本应该还有所谓的 encoder/decoder 层级的,它们才是真正负责协议解析的组件。但是在目前实现当中,encoder/decoder 一般都是嵌在 Network Filter 中作为某个 Network Filter 的一部分,所以这里干脆简化掉了。
Envoy 可扩展性方面还有最后一个问题:功能更新和升级带来的运维成本。Envoy 是使用 C++ 来实现的。每实现一个新的功能性的 Filter,无论是复杂的 L4 Network Filter,还是相对简单的 L7 HTTP Filter,都需要重新构建整个 Envoy。相比于配置的动态化,Envoy 功能上似乎没那么动态化。
为了解决这个问题,Envoy 社区提出了基于 WASM 的 Filter 扩展机制。WASM 是一种前端的技术,最初设计是用于加速 JS 脚本以及将 C++ 等语言带到 WEB 上。Envoy 内置了 WASM 虚拟机,开发者可以将自己的功能扩展使用如 C++、Go 、AS 等各种语言开发,然后编译成 WASM 字节码文件。之后,Envoy 动态的加载文件就可以实现功能的增强。
此外,Envoy 社区也提供了 Lua Filter,可以让 Envoy 通过动态的下发一个 Lua 脚本来实现功能扩展。举例来说,用户可以使用 Lua 来编写一段 Lua 逻辑,只要实现 envoy_on_request 和 envoy_on_response 两个函数,然后将 Lua 脚本通过 xDS 动态的下发给 Envoy,Envoy 在处理请求的过程中,就会执行对应的 Lua 脚本代码。
目前 WASM 还没有完全落地,但是 Gloo、Istio 社区都在力推,前景美好。而 Lua 相对来说,比较成熟,但是社区提供的功能较弱一些。所以轻舟团队也做了一些优化工作,后文当中也会讲到。
接下来需要介绍的 Envoy 关键特性是可观察性。可观察性其实是一个很大的主题,完全可以做一个单独的文章分享。但是这里就是主要介绍一下可观察性的概念以及 Envoy 的一些优势。希望有机会在其他文章中在对 Envoy 可观察性做更详细的介绍。
可观察性是指在软件程序运行过程中,获取软件程序内部状态或者发生的一个能力。如果程序执行起来之后,开发者和运维人员就对内部状态一无所知,那很多问题就根本无法定位。
按照侧重面的不同,Envoy 的可观察性主要依靠三个部分构成。分别是日志、指标、以及分布式追踪。
xDS 协议、可扩展性、可观察性,这是我个人认为的在 Envoy 之中,最为核心的三个特性。它们横跨多个模块,基本上,可以说,这三者就是使得 Envoy 如此独特的基石。
最后分享从 Envoy 数据面的视角看 Envoy 如何在网易轻舟落地。通过这一部分的介绍,大家可以更好地理解 Envoy 的各个特性是如何在实践之中发挥效用的。
最简单的用法是把 Envoy 当作一个纯粹的七层网络代理,没有任何其他的依赖,只有单独的一个 Envoy,就像使用单体的 Nginx 一样。可以使用静态配置,或者基于磁盘文件作为数据源的 xDS 协议。
对于广大的个人开发者以及个人网站维护者而言,如果需要一个七层代理,不妨尝试一下 Envoy,立刻就能拥有一个高性能的网络代理,而且具备丰富的观察手段。同时对于希望深入学习 Envoy 的开发者来说,这种方法也是上手 Envoy 最快的方式。
此外,也可以将 Envoy 应用于 API 网关之中。使用 Envoy 作为核心数据面,结合控制台、日志分析系统、指标监控报警系统、APM 链路跟踪系统、外部注册中心等等来构建一个功能完善、可观察、易扩展的 API 网关。也可以将 Envoy 作为 Service Mesh 服务网格中数据面,接管微服务集群东西向流量,构建全新的微服务架构。
注:有兴趣的同学也可以了解一下 Envoy Mobile 项目,那是另一个更加有趣的场景。
接下来要着重介绍的,就是Envoy 在网关和网格两种场景下的落地和实践。
Envoy 在网易轻舟实践中的整体架构如下。当然,图中简化了很多细节,只留下了核心的一些模块。
Envoy 会作为 API 网关以及服务网格数据面,承接整个微服务集群中东西向和南北向的流量,实现微服务集群的全流量接管。
在 Envoy 之上,就是所谓的控制面。前面也说了,Envoy 使用 xDS 协议来实现配置的动态化加载,这些配置本身就是各种各样的流量治理策略,比如如何路由请求、是否限流、是否熔断、如何健康检查等等,它们依赖一个 xDS 服务提供方来向 Envoy 推送。控制面主要的工作就是作为 Envoy 配置提供方来负责整个集群中的 Envoy 配置,并以此实现集群中流量的治理和控制。
在网易轻舟,控制面是基于开源的 Istio Pilot 做的增强和扩展。Istio 应该是目前开源社区发展最好的服务网格软件。轻舟服务网格控制面使用 Isito 是一个自然而然的选择。而网关控制面,则是直接复用的网格控制面中的核心组件 Pilot。这样,在轻舟 API 网关以及轻舟服务网格之中,控制面和数据面的组件都是完全共享的,实现了基础设施技术栈的统一。不用单独维护工程和代码,功能也可以通用,只是部署模式和配置分发略有差异。
而在 Istio Pilot 之上,则是轻舟单独抽象的一层 API Plane。它是对 Pilot 以及集群基础设施如 K8s API 的一个封装和聚合,屏蔽下层的细节。
在 API Plane 之上,就是轻舟控制台,用户操作和管理集群的入口。通过对接 API Plane,也可以实现自定义的控制台。
此外,Prometheus 组件会负责自动发现并采集集群中各个组件(尤其是 Envoy)的指标监控数据。轻舟控制台会把一些关键指标数据进行可视化展示。
APM 系统会实现全链路的跟踪和集群服务拓扑分析。轻舟原本是使用私有协议,目前将要切换到 SkyWalking,当然,如果业务方或者客户有需求,ZipKin 之类的也可以支持。
最后,Envoy 日志会被采集到 ELK 日志分析系统之中,轻舟以此来实现各种审计功能。
基本上,轻舟 API 网关和轻舟服务网格大体架构就是如此。接下来,将分别详细介绍一下轻舟 API 网关以及轻舟服务网格。
API 网关是整个微服务集群的流量入口,主要用于解决微服务架构中服务暴露的问题。相比于数据面纯粹的代理,API 网关更强调流量的治理。
在 API 网关之中,个人认为,从一个数据面开发的角度看,最重要的是五个点,分别是:性能、可观察性、治理能力、稳定性以及控制台。
比上文概览图详细一些的轻舟 API 网关架构简图如下所示。在控制面:控制台作为操作的入口,通过 API Plane 操作 K8s 或者 Pilot。Pilot 发现服务和配置并抽象成 xDS 协议约定的数据推动给 Envoy。在数据面:Envoy 接收来自控制面的配置数据并利用可扩展的各种 Filter 完成流量的治理。这里不过多展开,接下来分别介绍轻舟 API 网关对于前述五个要点的实践。
性能
在性能方面,Envoy 本身就是保障,在都使用最简配置的情况下,它的性能与 Nginx 相差仿佛,比 HA Proxy 要稍差一些。根据实际测试结果,一个 8C8G 的轻舟 API 网关,在容器网络下,可以达到 8w+ QPS,而物理网络下,可以达到 10w+ QPS。
并发量 | Fails | TPS | MRT(ms) |
---|---|---|---|
50 | 0 | 66089.44 | 0.71 |
100 | 0 | 75219.25 | 1.27 |
300 | 0 | 81199.04 | 3.59 |
500 | 0 | 82626.56 | 5.92 |
在 Envoy API 网关性能保障方面,有三个小的 Tips 分享一下:
可观察性
在可观察性方面,轻舟 API 网关构建了完整的监控体系。由前文可知,Envoy 本身就提供了非常丰富的指标监控数据,以及灵活的日志系统,所以重点是如何把这些 Envoy 本身提供的能力利用起来的问题。
首先是指标监控,前面也提到过,轻舟使用的是 Prometheus 数据库。指标监控就是一个个计数器,而把这些计数器在时间轴上排列出来时,就可以形成线和图,用于观察整个网关的流量趋势。对于一些核心的数据,我们会对接到控制台。同时,也提供了 Grafana 来把 Prometheus 中的数据做可视化。此外,Prometheus 还负责提供根据指标数据监控告警的能力。比如说,一段时间内,4xx/5xx 的请求数暴涨,这个时候就立刻会把相关告警推送给对应的负责人。这些告警能力,也是通过控制台来对外暴露,让业务方或者客户配置。
其次是日志,Envoy JSON 日志存在性能问题,普通的文本日志解析起来又差一些。所以这里有一个取巧的办法,使用文字字符串拼接成 JSON 格式字符串。同时,使用日志提供的 Log Filter 对包含特殊字符串的字段做转义,这样就可以保证性能的同时,输出标准的 JSON 字符串了。
而分布式跟踪,Envoy 原生支持了 Zipkin、OpenTracing、LightStep 等多种分布式跟踪系统,可以直接对接。而我们自己开发了 SkyWalking 的 Tracing 支持,还在持续的优化之中,而且也准备贡献给社区。
治理能力
前文之中花了不小的篇幅来介绍 Envoy 的可扩展性,实际上都是为了介绍 API 网关的治理能力做铺垫。得益于 Envoy 强大的 L4/L7 Filter 扩展机制,开发者可以很轻松地在不侵入 Envoy 源码的前提下,扩展出各种流量治理能力。Envoy 提供了全局/本地限流、黑白名单、服务/路由熔断、动静态降级、流量染色等等流量治理功能,基本已经覆盖了 API 网关的绝大部分需求了。
而考虑到各个方业务的特殊性,也考虑到 Envoy C++ 扩展的开发门槛,以及运维部署的成本,所以轻舟网关也做了一些其他方面的工作。首先是 WASM 扩展,它肯定是社区的发展方向,而且兼具性能、安全、动态分发、多语言支持多种优点。目前轻舟正在内部预研中,用它开发一些简单插件,评估性能和稳定性。
另一个就是利用 Lua 脚本扩展。在这方面,轻舟主要做了两个方面的动作:
稳定性
网关稳定性除了代码质量之类的基础保障之外,最重要的就是依靠流程和规范去保证。但是这一方面的经验往往不太通用,不同的公司、组织可能完全不一样。所以,这里也简单的列出了一些比较大而化之的小 Tips:
控制台
控制台主要负责两方面的工作:第一,将指标、监控、APM 数据聚合并做可视化展示,帮助开发人员快速定位问题;辅助团队了解流量动态;第二,简化各个功能模块使用、通过封装对外暴露友好、易操作的交互页面。尽可能减少直接 CRD/配置文件操作,降低风险。
前面提到的四项能力都是 API 网关的基础能力。但最终网关好不好用,一般都取决于控制台。这其实就需要产品去思考如何设计API 网关的问题了。个人暂时还没有突破到这个领域,没有什么特别的经验分享,只是要说明,对 API 网关来说,一个对用户和开发者友好的控制台很重要。
以上是关于轻舟 API 网关的一些分享。相对来说,API 网关是一个很成熟的技术,有很多优秀的实践,经典的比如 Kong,新兴的比如 Gloo、Ambassador,如果大家有兴趣,都可以多多了解一下。
接下来再介绍一下 Envoy 在轻舟服务网格中的应用。相比 API 网关,服务网格是一个相对新颖的服务架构。最初微服务架构提出是为了解决单体应用复杂度太高,伸缩性差等问题。但是微服务架构本身又带来了新的挑战:1)服务的拆分使得需要付出额外的成本进行服务发现与服务管理管理、较为复杂的服务调用链路导致故障以及问题定位困难等;2)服务暴露问题。
后者可以使用 API 网关来解决。而前者,一般通过引入一些 RPC 框架来应对。但是 RPC 也有问题,SDK 和语言绑定,作为基础设施的 RPC 框架与业务本身绑定;SDK 提供方需要为不同的语言、不同的服务组件提供不同的 SDK 并进行维护;SDK 功能更新,业务也必须更新甚至要做修改和适配;跨 RPC 协议交互困难。
所以业界又提出了一个全新的架构:为每一个业务进程启动一个 Sidecar 进程,它来代理业务进程的所有进出流量,并且将服务发现、流量治理、监控、安全等一系列必须但是又业务本身无关的功能剥离出来下沉到 Sidecar 中。
下图是一个很简单直观的 Sidecar 工作原理图。当业务进程作为服务提供方时,会由 Envoy 作为反向代理,流量先到 Envoy 再到业务本身。当业务进程作为客户端希望调用其他服务时,Envoy Sidecar 会作为正向代理,流量也要经过 Envoy 流转。一系列的流量的治理功能就可以从 SDK 剥离到独立的 Sidecar 进程中。
至于流量拦截的过程,一般通过修改 IP Tables 实现,对业务可以做到无感知。当然,也可以做到由业务进程直接向 Sidecar 某个特定端口发送请求的方式来让 Envoy 拦截流量,这样做可以减少 IP Tables 的开销,但是 Sidecar 就没有办法做到透明。
在服务网格实践当中,诸如性能、七层治理能力、可观察等问题和 Envoy 网关实践中都类似,毕竟服务网格本质上也只是无数代理的聚合,很多特性都是通用的。所以这里主要说的是另外两方面的问题:
多协议治理
在多协议治理能力方面,再次需要提到 Envoy 提供的丰富的可扩展性,它的 L4/L7 Filter 扩展机制。前面在网关部分,主要利用的其实是 Envoy 的 L7 扩展能力,大部分的 Filter 开发都是针对 HTTP 协议,在 Envoy 提供的 HCM 基座之上扩展 HTTP Filter。
而 L4 Filter 则允许开发者扩展新的基座,实现多协议治理能力的增强。而且新开发的 L4 Filter 之上,同样可以构建对应协议的 L7 Filter。
现在社区已经提供了大量的 L4 Filter。几乎涵盖的大部分的常用的协议,诸如 Dubbo、Thrift、Redis、RocketMQ等等。但是相比于 HTTP 协议的 HCM,它们的能力还比较孱弱,所以具体实践之中,必须对它们做增强。目前,轻舟主要针对 Thrift、Dubbo 和 Redis 做了优化和扩展,包括可观察性增强、动态 RDS 支持、七层治理能力等等。
理论上,任何新的基于 TCP/UDP 的协议,都可以通过开发新的 L4 Filter 接入到服务网格中来。
平滑接入
针对第二个网格平滑接入的问题,自然需要控制面的大量投入,实现诸如 Envoy Sidecar 版本管理、动态流量劫持等能力。但是本文主要针对数据面 Envoy 进行分享。所以此处要介绍的是在服务网关平滑接入过程之中发挥重要作用的一项关键特性:热重启。
Envoy 提供了热重启机制保证在需要重新载入 Envoy 或者需要更新 Envoy 二进制过程中,Envoy 始终处于可用状态。新旧两个 Envoy 进程通过 UNIX Socket 进行通信和数据交换。同时,部分的文件通过共享内存在新旧 Envoy 进程之中共享。
具体来说,当新的 Envoy 进程启动之后,新 Envoy 进程会载入配置(通过 BootStrap 以及 xDS 等)。在载入 Listener 过程之中,会通过 UNIX Socket 从父进程中获取对应 Listener 的套接字描述符(实际上就只是一个整型数)。如果不存在,则创建新的套接字。
当子进程配置加载完成并启动工作线程事件循环之后。子进程向父进程发起请求,由父进程优雅的清理监听套接字上的剩余连接以及正在进行中的请求。此外,子进程在工作线程就绪之后还会尝试从父进程处获取父进程当前的指标监控数据(Counter 和 Gauge)并合并到当前的指标监控数据中来。如此可以保证新旧两个进程监控指标数据的一致性。
Envoy 热重启机制使得 Envoy 在重新载入的过程当中,始终可以正常的对外提供服务,让作为基础设施的 Envoy 可以做到业务无感知的热升级和热替换,为业务平滑接入服务网格,保障服务网格整体的稳定性提供了基础。
要做到真正的平滑接入,单单数据面热重启自然是不够的,Envoy Sidecar 版本管理和分发,动态流量劫持,配置智能加载等等控制面能力也是必不可少。限于篇幅与主题,此处不再展开,有机会再分享。
本文整体介绍了 Envoy 以及构筑在 Envoy 之上的网关网格技术。服务网格、API 网关本身都是非常宏大的主题,涉及知识面很广,其中的某一个小点都有很多可以挖掘的地方。限于作者目前的知识和理解,本文难免存在不够全面或错谬之处,敬请读者指正。
作者简介
王佰平,网易数帆轻舟事业部研发工程师,负责轻舟 Envoy 网关与轻舟Service Mesh 数据面开发、功能增强、性能优化等工作。对于 Envoy 数据面开发、增强、落地具有较为丰富的经验。