Istio 的流量核心是 Envoy。而 Envoy 的核心是事件架构。明白了事件架构,深入研究 Envoy 就不难了。本文尝试分析 Envoy 的事件与 NetFilter 的设计架构。
💂 关于封面:
Rowan Atkinson(阿特金森), 《Mr Bean 憨豆先生》的演员。大部分人只以为他只是个用身体语言去演出不那么高级趣味的肥皂剧的演员,那么直是可惜了。现实是:
憨豆先生,电气工程师? 阿特金森是一位知名学者,在纽卡斯尔大学获得了电气工程学士学位,在牛津大学女王学院获得了硕士学位。最重要的是,牛津大学在 2006 年授予他荣誉院士称号,这是一项相当严格的成就。 尽管他最初开始表演是为了克服童年的口吃,但他在舞台上找到了归宿。他甚至决定放弃攻读电气工程博士学位,将时间投入到演艺事业中。 自2012年臭名昭著的憨豆先生退休以来,阿特金森只复活了该角色一次,但这位演员在娱乐界的工作还远未结束。
📚 摘录说明:
本文摘自一本我在写作中的开源书《Istio & Envoy 内幕》 中 事件驱动与线程模型 与 Network Filter。如果说你看到的转载图片不清,可回到原书。
引
Cloud Native 好像开始有点退潮了,Service Mesh 好像也难免。之前吹过的牛,到现在,能实现的已经成熟了,不好实现的,已经没人相信了。一句话:当故事太接近现实时,故事就不再吸引人了。
作为一个已经没资格吃青春饭,更顽固不化地认为 AI 参数调整不是程序员功夫的中年老程。好像已经没什么能提起研究兴趣了。但老程心灵也需要在时代的风雨中找到点心灵的安顿。回想这几十年来 IT 技术演进过程,什么东西是核心,什么东西是变化最少的?或者这些东西就是我辈老程的最后机会了。毕竟,比快餐技术,老程毫无优势。
Service Mesh 如果不潮了,Istio 不潮了,那么剩下什么? 什么才是核心价值? Envoy 。不掌握 Envoy ,就不要说精通 Istio 了。而且,我相信, Envoy 作为一个成熟的 L7 代理,它的价值不只有 Service Mesh 。它起码是一个扩展性、可维护性、可观察性都更好的 Nginx 。
而 Envoy 的核心是事件与 NetFilter 的设计架构。了解这个架构,一则对 Envoy 的原理与性能有更深入的了解,二则可以学习一个 L7 代理架构设计如何兼顾到性能、可维护性、扩展性。
Network Filter
Network Filter Chains
在前面章節的 {ref}图:Istio里的 Envoy Inbound 配置举例
中,可以看出,一个 Listener 可以包含多个 Network Filter Chain
。而其中每个 Chain 都有自己的 filter_chain_match
,用于配置新建立的 Inbound Connection
选定 Network Filter Chain
的策略。
每个 Network Filter Chain
都有自己的名字。需要注意的是,Network Filter Chain
的名字是允许重复的。
每个 Network Filter Chain
又由顺序化的 Network Filter
组成。
Network Filter
Envoy 对为保证扩展性,采用多层插件化的设计模式。其中,Network Filter
就是 L2 / L3 (IP/TCP) 层的组件。如,上面的 {ref}图:Istio里的 Envoy Inbound 配置举例
中,顺序地有:
- istio.metadata_exchange
- envoy.filters.network.http_connection_manager
两个 Network Filter。其中,主要逻辑当然在 http_connection_manager
了。
Network Filter 框架设计概念
我在学习 Envoy 的 Network Filter 框架设计时,发现它和我想像中的 Filter 设计非常不同。甚至有点违反我的直觉。见下图:
图:Model of Network Filter Framework
以下仅以 ReadFilter 说说:
My intuition Ideal model 我直觉中的模型
是:
- Filter 框架层有
Upstream
这个概念 - 一个 Filter 的输出数据和事件,会是下一个 Filter 的输入数据和事件。因为这叫 Chain,应该和 Linux 的
cat myfile | grep abc | grep def
类似。 - Filter 之间逻辑上的 Buffer 应该是隔离的。
而 Realistic model(现实的模型)
中
- 框架层面,没有
Upstream
这个概念。Filter 实现自行实现/不实现 Upstream,包括连接建立和数据读写,事件通知。所以,框架层面,更没有 Cluster / Connection Pool 等等概念了。 - 见下面一项
- Filter 之间共享了 Buffer,前面的 Filter 对 Buffer 的读操作,如果沒进行
drained 排干
,后面的 Filter 将会重复读取数据。前面的 Filter 也可以在 Buffer 中插入新数据。 而这个有状态的 Buffer,会传递到后面的 Filter 。
Network Filter 对象关系
写到这里,是时候看看代码了。不过,不是直接看,先看看 C++ 类图吧。
图:Network Filter 对象关系
可见,大家日常生活中,WriteFilter 并不常用 :) 。
Network Filter 框架设计细说
在代码实现层, Network Filter 框架下,抽象对象间的协作关系如下:
图:网络过滤器框架抽象协作
下面,以经典的 TCP Proxy Fitler 为例,说明一下。
图:Network Filter Framework - TCP 代理过滤器示例
Network Filter - ReadFilter 协作
图:Network Filter - ReadFilter 协作
ReadFilter 协作比较复杂,也是 Network Filter Framework 的核心逻辑。所以要细说。
如前所言, Framework 本身没的直接提供 Upstream / Upstream Connection Pool / Cluster / Route 这些抽象对象和相关事件。而这里,我们暂且把这些称为:外部对象与事件
。Filter 实现需要自己去创建或获取这些 外部对象
,也需要自己去监听这些 外部事件
。外部事件
可能包括:
- Upstream 域名解释完成
- Upstream Connection Pool 连接可用
- Upstream socket read ready
- Upstream write buffer full
- …
Network Filter - WriteFilter 协作
图:Network Filter - WriteFilter 协作
由于 WriteFilter
在 Envoy 中使用场景有限,只有 MySQLFilter / PostgresFilter / KafkaBrokerFilter 和 Istio 的 MetadataExchangeFilter 。所以这里就不展开说明了。
扩展阅读
如果有兴趣研究 Listener 的实现细节,建议看看我 Blog 的文章:
- 逆向工程与云原生现场分析 Part2 —— eBPF 跟踪 Istio/Envoy 之启动、监听与线程负载均衡
- 逆向工程与云原生现场分析 Part3 —— eBPF 跟踪 Istio/Envoy 事件驱动模型、连接建立、TLS 握手与 filter_chain 选择
- Taming a Network Filter
事件驱动与线程模型
不出意外,Envoy 使用了 libevent 这个 C 事件 library, libevent 使用了 Linux Kernel 的 epoll 事件驱动 API。
说明一下图中的流程:
- Envoy worker 线程挂起在
epoll_wait()
方法中,在内核中注册等待 epoll 关注的 socket 发生事件。线程被移出 kernel 的 runnable queue。线程睡眠。 - 内核收到 TCP 网络包,触发事件
- 操作系统把 Envoy worker 线程被移入 kernel 的 runnable queue。Envoy worker 线程被唤醒,变成 runnable。操作系统发现可用 cpu 资源,把 runnable 的 envoy worker 线程调度上 cpu。(注意,runnable 和 调度上 cpu 不是一次完成的)
- Envoy 分析事件列表,按事件列表的 fd 调度到不同的
FileEventImpl
类的回调函数(实现见:FileEventImpl::assignEvents
) FileEventImpl
类的回调函数调用实际的业务回调函数- 执行 Envoy 的实际代理行为
- 完事后,回到步骤 1 。
HTTP 反向代理的总流程
整体看,Socket 事件驱动的 HTTP 反向代理总流程如下:
图中看出,有4种事件驱动了整个流程。后面几节会逐个分析。
Downstream TCP 连接建立
现在看看,事件驱动和连接的建立的过程和关系:
- Envoy worker 线程挂起在
epoll_wait()
方法中。线程被移出 kernel 的 runnable queue。线程睡眠。 - client 建立连接,server 内核完成3次握手,触发 listen socket 事件。
- 操作系统把 Envoy worker 线程被移入 kernel 的 runnable queue。Envoy worker 线程被唤醒,变成 runnable。操作系统发现可用 cpu 资源,把 runnable 的 envoy worker 线程调度上 cpu。(注意,runnable 和 调度上 cpu 不是一次完成的)
- Envoy 分析事件列表,按事件列表的 fd 调度到不同的 FileEventImpl 类的回调函数(实现见:
FileEventImpl::assignEvents
) - FileEventImpl 类的回调函数调用实际的业务回调函数,进行 syscall
accept
,完成 socket 连接。得到新 socket 的 FD:$new_socket_fd
。 - 业务回调函数把 调用
epoll_ctl
把$new_socket_fd
加到 epoll 监听中。 - 回到步骤 1 。
事件处理抽象框架
上面主要在 kernel syscall 层面上介绍事件处理的底层过程。下面介绍在 Envoy 代码层面,如何抽象和封装事件。
Envoy 使用了 libevent 这个 C 编写的事件 library。还在其上作了 C++ OOP 方面的封装。
如何快速在一个重度(甚至过度)使用 OOP 封装和 OOP Design Pattern 的项目中读懂核心流程逻辑,而不是在源码海洋中无方向地漂流? 答案是:找到主线。 对于 Envoy 的事件处理,主线当然是 libevent
的 event_base
,event
。如果你对 libevent 还不了解,可以看看本书的 libevent 核心思想
一节。
event
封装到ImplBase
对象中。event_base
包含在LibeventScheduler
<-DispatcherImpl
<-WorkerImpl
<-ThreadImplPosix
下
然后,不同类型的 event
,又封装到不同的 ImplBase
子类中:
- TimerImpl
- SchedulableCallbackImpl
- FileEventImpl
其它信息上图已经比较详细,不再多言了。
扩展阅读
如果有兴趣研究实现细节,建议看看我 Blog 的文章:
- 逆向工程与云原生现场分析 Part3 —— eBPF 跟踪 Istio/Envoy 事件驱动模型、连接建立、TLS 握手与 filter_chain 选择
- 逆向工程与云原生现场分析 Part4 —— eBPF 跟踪 Istio/Envoy 之 upstream/downstream 事件驱动协作下的 HTTP 反向代理流程
与 Envoy 作者 Matt Klein 的: Envoy threading model
libevent 核心思想
libevent
的有两个重要的概念: event_base
、event
。
event
event 这个词意义大广大,libevent 中的 event
对象,到底是什么意思? 理解 OOP 方法编写的某个对象的功能或定位,有一个窍门,就是看对象的属性名和方法名。从上图看了, event
是指在某个 fd(file descriptor 文件描述符/句柄) 上可能会发生的信号,如 Read Ready 、 Write Ready 等等。 注意,这里是可能会发生的事件,包括:未来可能发生、正在发生、曾经发生过的事件。
应用可能对 event
进行很多操作,包括监听或订阅事件,以在事件真正发生后,可以 callback 到应用代码。
event_base
可以认为是 event
的集合。多数事件驱动型的应用实现,每个工作线程拥有自己的 event_base
。并执行自己的 event_base
的事件循环,包括 Envoy 。
http://www.wangafu.net/~nickm/libevent-book/Ref2_eventbase.html
- event_base -
event_base
structures. Eachevent_base
structure holds a set of events and can poll to determine which events are active.
If an event_base is set up to use locking, it is safe to access it between multiple threads. Its loop can only be run in a single thread, however. If you want to have multiple threads polling for IO, you need to have an event_base
for each thread.
Each event_base has a “method
”, or a backend that it uses to determine which events are ready. The recognized methods
are:
- select
- poll
- epoll
- kqueue
- devpoll
- evport
- win32
[Libevent状态转换图 from https://developer.aliyun.com/article/659277#fromHistory]
结语
本文的副标题是:机关算尽太聪明 。因为我从行业开始流行 《Design Pattern》开始,就觉得程序界的炫技之风开始星星之火。看到了太多为用 Design Pattern 而Design Pattern 的代码设计。而很多时候,过度的或不恰当的 OOP / Design Pattern ,让代码的维护者或读者,更难直观地理解代码。如果这是他们的目标,的确也是实现了。Envoy 中有吗?我不知道。