软件开发架构师

Envoy 部分源码分析与定制开发-InfoQ

运维 46 2019-11-08 13:10

Envoy 是目前社区中构建 Service Mesh 时的首选反向代理。作为 CNCF 旗下“毕业”的项目,社区给予了 Envoy 充分的肯定,丰富而实用的功能甚至可以采用 Envoy 轻松替换或对接一些传统的 Load Balacer,增强对服务整个调用过程的可见性和控制能力。

在 Istio 和 AWS AppMesh 以及一些自研的 Service Mesh 中,Envoy 都扮演了关键角色。无论 Istio 抑或是 AppMesh,它们所能提供的能力,都受限于 Envoy 本身,如果说 Istio 或 AppMesh 是一道菜,那 Envoy 就是这道菜的食材,是整个 Service Mesh 的灵魂。

Envoy 提供了丰富的协议和连接处理能力,还有各种各样的 Filter 可供选择,复杂的配置也可以支持各种使用场景,此外它还提供了一整套 Filter 框架,定制 Filter 加入对请求的处理过程就可以增加各种各样的功能。

遗憾的是,目前社区对 Envoy 本身的定制开发讨论比较少,大多关注在 Envoy 的 Control Plane 上,Envoy 官方文档也缺乏相应的指导,使得对 Envoy 本身的定制和扩展显得困难重重。本文通过介绍 Envoy 中 Filter Chain 来提供一种向 Envoy 中添加自定义功能的方法,希望对从事相关领域的工程人员有所帮助和启发。

文中如不特别注明,涉及的源码和对应的版本为:

  • Envoy: e5c31e3058cd0480f03feb2855ab1bb93b30e2ce

  • Istio Proxy: 1.1.16 (481d00ab40aee6ae74341e8f522d931bdd8e6951)

Filter Chain

Envoy 将很多独立的功能性组件封装到独立的 Filter 中,在配置中可以按照需要的顺序来配置这些标准化组件,实现丰富的流量控制策略。深入理解 Filter Chain 的设置有助于理解 Envoy 的整个模型,充分利用 Envoy 提供的原生能力。

例如 MySQL Filter 与 TCP Filter 组合,可以监控和优化 MySQL 的连接,提供 MySQL 的 accesslog 支持,也可以为 MySQL 访问增加统一的 metadata 支持,如图 1.1。

图 1.1 MySQL Proxy 的拓扑

Envoy 部分源码分析与定制开发-InfoQ-1

Envoy 提供了一系列针对 TCP 协议的 Network Filter,其中的 envoy.http_connection_manager 支持额外的 HTTP Filter Chain, 提供了 HTTP 协议(HTTP/1.1, HTTP/2, gRPC)的流量感知和控制能力, Filter Chain 结构如图 1.2。

图 1.2 envoy.http_connection_manager 和 HTTP Filter 之间的关系

Envoy 部分源码分析与定制开发-InfoQ-2

下面从 Envoy 的配置接口定义来看 Filter 的定义来进一步可理解它的 Filter Chain(本文只看 static resource 的定义,因为动态配置发现与静态配置除了管理方式不同之外,接口本身是一样的)。

图 1.3 静态配置的 Protobuf 定义

Envoy 部分源码分析与定制开发-InfoQ-3

Envoy 部分源码分析与定制开发-InfoQ-4

Envoy 部分源码分析与定制开发-InfoQ-5

Envoy 部分源码分析与定制开发-InfoQ-6

特别提一下 HTTP Filter,它们实际上是由 envoy.http_connection_manager 来顺序执行的,这里简单介绍注册 HTTP Filter 和最后调用这些 Filter 的过程。

首先,在 Envoy 程序启动阶段,配置工厂对象被注册到一个全局 Map 中,这样就可以在就可以通过配置来控制 Envoy 的行为了。REGISTER_FACTORY 是一个宏定义,也可以直接调用 Registry::RegisterFactory() 在程序初始化阶段注册 Filter 的配置工厂对象。

图 1.4 注册 Filter 配置工厂对象

Envoy 部分源码分析与定制开发-InfoQ-7

配置工厂对象注册完成后,就可以根据一个 name,比如 envoy.router 得到配置工厂对象,然后初始化对应的 Filter 实例,详细的注册过程和实现后文介绍 Envoy 定制开发的小节有详细介绍,此处不赘述。

注册完配置工厂对象,接下来就需要注册 Filter 本身的工厂对象,如 envoy.http_connection_manager 实现的 callback 接口可以把 HTTP Filter 的工厂对象注册到它的注册表中,如图 1.5。

图 1.5 注册 HTTP Filter 的 callback 接口

Envoy 部分源码分析与定制开发-InfoQ-8

这个接口实际上就将 HTTP Filter 动态添加到 envoy.http_connection_manager 的注册表中,在处理请求的过程中,这些 Filter 就会被按照配置中的顺序依次执行,完成对请求的预处理。

以上简单介绍了 Filter Chain 的基本原理,这是 Envoy 中不太好理解的一个地方,因为它可以随着编译时选择的具体 Filter 改变,在官方文档中很难体现出具体细节。极端的灵活性带来了极端的复杂度,而这样的复杂度的结果就是使用者很难充分利用它提供的能力,因此熟悉 Filter 的源码就是相关从业人员的必修课之一。

典型 Filter

本节仅对几个常用的 HTTP Filter 做源码层面的介绍,只关注怎么在 Envoy 里面加入新的 Filter 以及如何使用新的 Filter,不涉及 Network Filter 以及比较复杂的 HTTP Filter, 如 envoy.http_connection_manager 和 envoy.router 等。

envoy.rate_limit

Envoy 提供基于 gRPC 的限流扩展,可以基于自定义的限流服务来实现针对 HTTP 上下文的限流,先看 Filter 注册过程,注册一个 Filter 分为两部分:

  • 注册配置工厂对象,这样就可以在配置中使用这个 Filter 了

  • 注册 Filter 本身,这样就可以使用该 Filter 来影响流量处理行为了

首先调用 REGISTER_FACTORY 这个宏定义,将它的配置接口注册到 Envoy 的注册表中,这样在配置中就可以用 envoy.rate_limit 来对流量限流了,在配置工厂中调用 addStreamFilter 将 envoy.rate_limit 的 Filter 工厂对象注册到 envoy.http_connection_manager 的 Filter 注册表中,这样就可以用它来实现针对 HTTP 协议的流量控制了,如图 2.1.1。

图 2.1.1 注册 envoy.rate_limit 的 配置工厂对象

Envoy 部分源码分析与定制开发-InfoQ-9

envoy.rate_limit Filter 中主要实现了 StreamFilter 的两个方法 decodeHeaders 和 encodeHeaders,除此之外其他的方法没有逻辑,直接进入下一个 Filter。

在 decodeHeaders 中实现了主要的限流逻辑,需要自定义一个扩展的 gRPC 限流接口,来决策是否拒绝请求,它的输入就是当前请求的 HTTP Header,如图 2.1.2。

图 2.1.2 decodeHeaders: envoy.rate_limit 中限流的实现

Envoy 部分源码分析与定制开发-InfoQ-10

envoy.rate_limit 在获取配置的时候还检查了 per_filter_config,这是 Envoy 为 HTTP Filter 提供的一种扩展接口,在 envoy.rate_limit 中一旦配置了 per_filter_config 就会覆盖默认的 Filter 配置,如图 2.1.3。

图 2.1.3 envoy.rate_limit 对 per_filter_config 的支持

Envoy 部分源码分析与定制开发-InfoQ-11

接着 encodeHeaders 在响应中增加了一组 header,用以在调用限流结果结束后,结果写入 HeaderMap 中,来表征限流的结果,如图 2.1.4。

图 2.1.4 envoy.rate_limit 中修改响应 header

Envoy 部分源码分析与定制开发-InfoQ-12

大多数解决特定工程问题的 Filter 都主要关注 Request Header 中的信息,不过 Envoy 中 decodeHeader 中传入的 Header 是同时支持 HTTP/1.x 和 HTTP/2 的。

还有一个值得注意的小细节就是 envoy.rate_limit 中为限流设置了 20ms 的默认超时,现实中这个时间需要根据外接限流服务的情况来设置。

envoy.fault

envoy.fault 为下游的服务提供了 Fault Injection 的能力,模拟给定的请求失败和异常延迟,用以定位噪音下的复杂系统中难以定位的异常。

还是先看它的注册过程,先注册了名为 envoy.fault 的配置工厂对象,这样配置文件里面就可以使用名为 envoy.fault 的 HTTP Filter 了,然后在配置工厂里面调用 addStreamFilter 将配置工厂注册到 Envoy 的注册表中,然后就可以使用它来控制 HTTP 流量了,如图 2.2.1。

图 2.2.1 注册 envoy.fault 的配置工厂到注册表中

Envoy 部分源码分析与定制开发-InfoQ-13

在 envoy.fault 中也支持 per_filter_config,与前文介绍的 envoy.rate_limit 是类似的,不赘述。envoy.fault 在 decodeHeaders 中根据配置中定义的规则来判断是否应该直接返回噪声或是增加随机延迟,如图 2.2.2。

图 2.2.2 envoy.fault 随机噪声的实现

Envoy 部分源码分析与定制开发-InfoQ-14

envoy.fault 也是一个中规中矩的 HTTP Filter,实现了 decodeHeaders,但工程上尤其是微服务中,主动加入噪声无论是对检测系统稳定性还是分析一些很难定位的问题都很有用,这在 Istio 被抽象成了 VirtualService 中的 Fault,而且 Istio 还将 Envoy 配置中原本独立管理的配置和 Route 直接关联在了一起,这也能降低整体配置的复杂度。

自定义 Filter

本节实现了一个简单的 HTTP Filter istio.evangelist,并用它来实现对 HTTP 流量的额外控制(打印日志)。为了简单起见,这里基于 Istio Proxy 来实现这个扩展,可以充分利用已有的 Bazel rules。

自定义 Filter 配置

先来定义 istio.evangelist 配置接口,Envoy 在编译中会根据这里定义的 Protobuf 接口来生成相应的接口代码,如图 3.1.1。

图 3.1.1 基于 Protobuf 定义 istio.evangelist 的配置

Envoy 部分源码分析与定制开发-InfoQ-15

如果需要对配置做静态检查,可以为接口定义清晰的约束,这是因为 Envoy 生成 gRPC 接口文件的时候中引入了 PGV ,利用 Protobuf 中的 Custom Options 来自动生成接口的静态检查代码。

这里为 istio.evangelist 的配置定义约束要求传入的 msg 必须是长度 8-25 的字符(具体支持的校验规则参考 PGV 的官方文档),如图 3.1.2。

图 3.1.2 为 istio.evangelist 的配置增加静态检查

Envoy 部分源码分析与定制开发-InfoQ-16

充分的静态检查能为程序逻辑提供一个输入的变化范围的基准线,降低整体逻辑的复杂度,Envoy 能提供如此复杂的功能,而又不显得臃肿和难以理解,大概是得益于它这套优雅的配置管理框架。

Filter 实现和使用

Envoy 在编译阶段选择所需的 Filter,在程序初始化阶段将 Filter 的配置工厂对象注册到 Envoy 的注册表中,完成这一步之后就可以在配置里面使用 istio.evangelist 了,如图 3.2.1。

图 3.2.1 注册 istio.evangelist 的配置管理数据结构

Envoy 部分源码分析与定制开发-InfoQ-17

值得一提的是,Envoy 中定义了 RegisterFactory 来注册所有的扩展组件,无论是 Network Filter, HTTP Filter 还是很多其他扩展都是在启动时注册到 Envoy 中,然后可以根据一个唯一的 Name 来从 Factory 中初始化对应组件的对象。

在 Envoy 中其他的组件也是这样注册的,Envoy 中使用了 RegisterFactory 的一个宏定义,如 tcp_proxy,如图 3.2.2。

图 3.2.2 Envoy 中 RegisterFactory 的宏定义 REGISTER_FACTORY

Envoy 部分源码分析与定制开发-InfoQ-18

这个地方其实就是把 class 注册到一个 map 中,注册的时候用到的 key 其实就是自定义的 factory 中的 name() 方法返回,而 value 则为对应 factory 的对象,如图 3.2.3。

图 3.2.3 注册 Filter 工厂对象

Envoy 部分源码分析与定制开发-InfoQ-19

自定义的 Factory 中定义了三个方法,实际上这实现了 Envoy::Extensions::HttpFilters::Common::FactoryBase 中定义的两个抽象方法:

  • createFilterFactory:根据静态配置初始化对应 filter 的 factory

  • createFilterFactoryFromProto: 根据动态配置接口初始化对应 filter 的 factory

这里还定义了一个 name() 方法,返回这个 filter 的注册的名字为 envoy.evangelist ,如图 3.2.3。

图 3.2.3 定义并注册 Evangelist Filter 的工厂对象

Envoy 部分源码分析与定制开发-InfoQ-20

Envoy 部分源码分析与定制开发-InfoQ-21

由于这里定义的 envoy.evangelist 是一个 HTTP Filter,所以这里返回的是 Http::FilterFactoryCb,如果是 Network Filter,就需要返回对应的 Network::FilterFactoryCb,如图 3.2.4。

图 3.2.4 Filter 对应的接口 HTTP/Network Filter

Envoy 部分源码分析与定制开发-InfoQ-22

实现了 Factory 之后就定义了在启动 Envoy 的时候,将自定义的 Filter 加入 Envoy 的注册表中,这样无论是动态配置还是静态配置就都可以用到相应的 Filter 了。接下来实现 Evangelist Filter 的处理逻辑,如图 3.2.5。

图 3.2.5 Evangelist Filter 的处理逻辑

Envoy 部分源码分析与定制开发-InfoQ-23

Envoy 部分源码分析与定制开发-InfoQ-24

Envoy 中为 HTTP Filter 定义了三种接口,如图 3.2.6:

  • StreamDecoderFilter: 对请求做预处理,如认证、授权等

  • StreamEncoderFilter:对响应做二次处理

  • StreamFilter:对请求做预处理,并且对响应做二次处理

在注册这三种接口的时候需要分别调用不通的 callback 方法,将 Filter 注册到三个独立的注册表中,供 envoy.http_connection_manager 来根据配置一次执行。

图 3.2.6 HTTP Filter 的三种接口

Envoy 部分源码分析与定制开发-InfoQ-25

最后,在本文中自定义的 Evangelist Filter 处理 HTTP 请求过程中打印几条日志,如图 3.2.7。

图 3.2.7 在 Evangelist Filter 处理请求过程中打印日志

Envoy 部分源码分析与定制开发-InfoQ-26

增加了自定义的 Evangelist Filter 之后,编译新的 Istio Proxy,可见这时候在 HTTP Filter 已经可以使用了,如图 3.2.8。

图 3.2.8 Evangelist Filter 已经注册到定义后的 Envoy 中

Envoy 部分源码分析与定制开发-InfoQ-27

启动一个 Envoy 服务,将 www.example.com 配置成它的 upstream,如图 3.2.9。

图 3.2.9 配置中增加 Evangelist Filter

Envoy 部分源码分析与定制开发-InfoQ-28

本地访问可以看到日志中已经打印出来 Evangelist Filter 中对应方法的日志,如图 3.2.10。

图 3.2.10 打印 Evangelist Filter 处理请求过程日志

Envoy 部分源码分析与定制开发-InfoQ-29

至此,istio.evangelist 已经集成到了 Envoy 中,如果需要实现额外的扩展,就可以在 decodeHeaders 等接口方法中来实现相关的逻辑。在设计一个 Filter 的时候尽可能明确它的作用范围,如果只作用于 Request 的处理过程,那其实实现 StreamDecodeFilter 就够了;如果要作用于 Response 的处理过程,可以实现 StreamEncodeFilter,避免引入过多冗余代码。

总结

Envoy 大部分功能是以插件形式存在的,灵活的 Filter Chain 让它成为社区中 Service Mesh 的不二选择,不过这样灵活性带来的问题是复杂度和相比较高的学习成本。

Envoy 官方文档中介绍一些常用功能的时候缺少注意事项的介绍,比如在 HTTP 处理过程中的自动 retry 支持,什么时候触发 retry、以及触发 retry 的时候什么逻辑会被反复执行等。这都需要对相关的实现有源码层面的理解,在理解了 Filter Chain 的实现机制之后,无论是对设计过程中的技术选型还是运维过程中的排查问题都有很大的帮助。

此外,Envoy 的配置管理是一个很值得借鉴的点,它通过 Protobuf 来定义配置的数据模型,包括接口数据结构与静态检查,为支持多种配置管理方式打下了基础。Envoy 还为 Protobuf 实现了基于 Custom Options 的扩展,在生成代码的时候可以对内容进行静态检查,这在很多基于 Protobuf 通信 (如 gRPC) 的场景下可以很大程度降低接口的维护成本(目前社区中类似开源的方案不是很多,可以算独树一帜)。

此外,深入学习 Envoy 源码的基础之一是需要理解它基于 Bazel 的构建框架,只有充分理解构建的过程,在定制开发时才能有的放矢,如第三方库的版本都可以从 Bazel 脚本中寻找答案,这里不再赘述。

总之,Envoy 是一个非常灵活的框架,其配置数据结构也是随着编译时选择插件的列表产生变化,也使得文档中详细介绍每个细节带来了困难,社区出产生的 Istio 等项目带来的主要价值之一就是将 Envoy 中复杂多变的、没有具体业务含义的功能抽象为与实际工程问题相关的概念,降低了学习成本。

笔者管中窥豹,从 Filter 以及基于 Filter 的扩展开发入手简单的分析了 Envoy 中最灵活的部分,希望对相关从业人员带来一些启发。

作者介绍:

杨谕黔,FreeWheel 基础架构组 Lead Software Engineer, 主要关注微服务治理、容器和 Service Mesh 相关的自动化运维和开发。

文章评论