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将很多独立的功能性组件封装到独立的 Filter 中,在配置中可以按照需要的顺序来配置这些标准化组件,实现丰富的流量控制策略。深入理解 Filter Chain 的设置有助于理解 Envoy 的整个模型,充分利用 Envoy 提供的原生能力。
例如 MySQL Filter 与 TCP Filter 组合,可以监控和优化 MySQL 的连接,提供 MySQL 的accesslog 支持,也可以为 MySQL 访问增加统一的 metadata 支持,如图 1.1。
图1.1 MySQL Proxy 的拓扑
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 的配置接口定义来看 Filter 的定义来进一步可理解它的 Filter Chain(本文只看static resource的定义,因为动态配置发现与静态配置除了管理方式不同之外,接口本身是一样的)。
图1.3 静态配置的 Protobuf 定义
特别提一下HTTP Filter,它们实际上是由 envoy.http_connection_manager 来顺序执行的,这里简单介绍注册 HTTP Filter 和最后调用这些Filter的过程。
首先,在 Envoy 程序启动阶段,配置工厂对象被注册到一个全局 Map 中,这样就可以在就可以通过配置来控制 Envoy 的行为了。REGISTER_FACTORY 是一个宏定义,也可以直接调用 Registry::RegisterFactory() 在程序初始化阶段注册 Filter 的配置工厂对象。
图1.4 注册 Filter 配置工厂对象
配置工厂对象注册完成后,就可以根据一个name,比如 envoy.router 得到配置工厂对象,然后初始化对应的Filter实例,详细的注册过程和实现后文介绍Envoy定制开发的小节有详细介绍,此处不赘述。
注册完配置工厂对象,接下来就需要注册 Filter 本身的工厂对象,如 envoy.http_connection_manager 实现的 callback 接口可以把 HTTP Filter 的工厂对象注册到它的注册表中,如图1.5。
图1.5 注册 HTTP Filter 的 callback 接口
这个接口实际上就将 HTTP Filter 动态添加到 envoy.http_connection_manager 的注册表中,在处理请求的过程中,这些 Filter 就会被按照配置中的顺序依次执行,完成对请求的预处理。
以上简单介绍了 Filter Chain 的基本原理,这是 Envoy 中不太好理解的一个地方,因为它可以随着编译时选择的具体 Filter 改变,在官方文档中很难体现出具体细节。极端的灵活性带来了极端的复杂度,而这样的复杂度的结果就是使用者很难充分利用它提供的能力,因此熟悉 Filter 的源码就是相关从业人员的必修课之一。
本节仅对几个常用的 HTTP Filter 做源码层面的介绍,只关注怎么在 Envoy 里面加入新的 Filter 以及如何使用新的 Filter,不涉及 Network Filter 以及比较复杂的HTTP Filter, 如 envoy.http_connection_manager 和 envoy.router 等。
Envoy 提供基于 gRPC 的限流扩展,可以基于自定义的限流服务来实现针对 HTTP 上下文的限流,先看 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.rate_limit Filter 中主要实现了 StreamFilter 的两个方法 decodeHeaders 和 encodeHeaders,除此之外其他的方法没有逻辑,直接进入下一个 Filter。
在decodeHeaders中实现了主要的限流逻辑,需要自定义一个扩展的gRPC限流接口,来决策是否拒绝请求,它的输入就是当前请求的HTTP Header,如图2.1.2。
图2.1.2 decodeHeaders: envoy.rate_limit 中限流的实现
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的支持
接着 encodeHeaders 在响应中增加了一组header,用以在调用限流结果结束后,结果写入HeaderMap 中,来表征限流的结果,如图2.1.4。
图2.1.4 envoy.rate_limit 中修改响应 header
大多数解决特定工程问题的 Filter 都主要关注 Request Header 中的信息,不过 Envoy 中decodeHeader 中传入的 Header 是同时支持 HTTP/1.x 和 HTTP/2 的。
还有一个值得注意的小细节就是 envoy.rate_limit 中为限流设置了 20ms 的默认超时,现实中这个时间需要根据外接限流服务的情况来设置。
envoy.fault 为下游的服务提供了 Fault Injection 的能力,模拟给定的请求失败和异常延迟,用以定位噪音下的复杂系统中难以定位的异常。
还是先看它的注册过程,先注册了名为 envoy.fault 的配置工厂对象,这样配置文件里面就可以使用名为 envoy.fault 的 HTTP Filter 了,然后在配置工厂里面调用 addStreamFilter 将配置工厂注册到Envoy的注册表中,然后就可以使用它来控制 HTTP 流量了,如图2.2.1。
图2.2.1 注册 envoy.fault 的配置工厂到注册表中
在 envoy.fault 中也支持 per_filter_config,与前文介绍的envoy.rate_limit是类似的,不赘述。envoy.fault 在decodeHeaders中根据配置中定义的规则来判断是否应该直接返回噪声或是增加随机延迟,如图2.2.2。
图2.2.2 envoy.fault 随机噪声的实现
envoy.fault 也是一个中规中矩的 HTTP Filter,实现了 decodeHeaders,但工程上尤其是微服务中,主动加入噪声无论是对检测系统稳定性还是分析一些很难定位的问题都很有用,这在Istio 被抽象成了 VirtualService 中的 Fault,而且 Istio 还将 Envoy 配置中原本独立管理的配置和 Route 直接关联在了一起,这也能降低整体配置的复杂度。
本节实现了一个简单的 HTTP Filter istio.evangelist,并用它来实现对 HTTP 流量的额外控制(打印日志)。为了简单起见,这里基于 Istio Proxy 来实现这个扩展,可以充分利用已有的 Bazel rules。
先来定义 istio.evangelist 配置接口,Envoy 在编译中会根据这里定义的 Protobuf 接口来生成相应的接口代码,如图3.1.1。
图3.1.1 基于 Protobuf 定义 istio.evangelist 的配置
如果需要对配置做静态检查,可以为接口定义清晰的约束,这是因为 Envoy 生成 gRPC 接口文件的时候中引入了 PGV ,利用Protobuf 中的 Custom Options 来自动生成接口的静态检查代码。
这里为 istio.evangelist 的配置定义约束要求传入的 msg 必须是长度8-25的字符(具体支持的校验规则参考PGV的官方文档),如图3.1.2。
图3.1.2 为 istio.evangelist 的配置增加静态检查
充分的静态检查能为程序逻辑提供一个输入的变化范围的基准线,降低整体逻辑的复杂度,Envoy 能提供如此复杂的功能,而又不显得臃肿和难以理解,大概是得益于它这套优雅的配置管理框架。
Envoy 在编译阶段选择所需的 Filter,在程序初始化阶段将 Filter 的配置工厂对象注册到 Envoy 的注册表中,完成这一步之后就可以在配置里面使用 istio.evangelist 了,如图 3.2.1。
图3.2.1 注册 istio.evangelist 的配置管理数据结构
值得一提的是,Envoy中定义了RegisterFactory来注册所有的扩展组件,无论是Network Filter, HTTP Filter还是很多其他扩展都是在启动时注册到Envoy中,然后可以根据一个唯一的Name来从 Factory 中初始化对应组件的对象。
在Envoy中其他的组件也是这样注册的,Envoy中使用了RegisterFactory的一个宏定义,如tcp_proxy,如图3.2.2。
图3.2.2 Envoy中RegisterFactory的宏定义REGISTER_FACTORY
这个地方其实就是把class注册到一个map中,注册的时候用到的key其实就是自定义的factory中的 name() 方法返回,而value则为对应factory的对象,如图3.2.3。
图3.2.3 注册Filter工厂对象
自定义的Factory中定义了三个方法,实际上这实现了 Envoy::Extensions::HttpFilters::Common::FactoryBase 中定义的两个抽象方法:
这里还定义了一个 name() 方法,返回这个filter的注册的名字为 envoy.evangelist ,如图3.2.3。
图3.2.3 定义并注册Evangelist Filter的工厂对象
由于这里定义的 envoy.evangelist 是一个HTTP Filter,所以这里返回的是 Http::FilterFactoryCb,如果是Network Filter,就需要返回对应的 Network::FilterFactoryCb,如图3.2.4。
图3.2.4 Filter 对应的接口 HTTP/Network Filter
实现了Factory之后就定义了在启动Envoy的时候,将自定义的Filter加入Envoy的注册表中,这样无论是动态配置还是静态配置就都可以用到相应的Filter了。接下来实现 Evangelist Filter 的处理逻辑,如图3.2.5。
图3.2.5 Evangelist Filter的处理逻辑
Envoy中为HTTP Filter定义了三种接口,如图3.2.6:
在注册这三种接口的时候需要分别调用不通的callback方法,将Filter注册到三个独立的注册表中,供 envoy.http_connection_manager 来根据配置一次执行。
图3.2.6 HTTP Filter的三种接口
最后,在本文中自定义的 Evangelist Filter 处理HTTP请求过程中打印几条日志,如图3.2.7。
图 3.2.7 在Evangelist Filter 处理请求过程中打印日志
增加了自定义的 Evangelist Filter 之后,编译新的 Istio Proxy,可见这时候在HTTP Filter已经可以使用了,如图3.2.8。
图3.2.8 Evangelist Filter已经注册到定义后的Envoy中
启动一个Envoy服务,将 www.example.com 配置成它的upstream,如图3.2.9。
图3.2.9 配置中增加 Evangelist Filter
本地访问可以看到日志中已经打印出来 Evangelist Filter 中对应方法的日志,如图3.2.10。
图3.2.10 打印 Evangelist Filter 处理请求过程日志
至此,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相关的自动化运维和开发。
领取专属 10元无门槛券
私享最新 技术干货