Listener#
Listener
顾名思义,就是被动监听和接受连接的的组件。那么是不是每个 Listener 都会 listen socket ? 我们带着问题往下看。
开始学习 Listener 前,先回顾一下前面章节的 Istio 下 Envoy 配置举例 中的例子。
备注
这里下载 Envoy 的配置 yaml envoy@istio-conf-eg-inbound.envoy_conf.yaml
.
Listener 举例#
上面的例子中,读者可以看到很多 Istio 配置的 Listener 的身影:
Inbound:
端口:15006
名字:virtualInbound
职责:主要的 Inbound Listener
端口:15090
端口:15000
…
Outbound:
Bind socket 的 Listener
端口:15001
名字:virtualOutbound
职责:主要的 Outbound Listener。转发 iptable 劫持的流量到下面的 Listener
不 Bind socket 的 Listener
名字:0.0.0.0_8080
职责:所有监听 8080 端口的 upstream cluster 流量,都会经由这个 Listener 出去。
配置
bind_to_port: false
可见,Istio 给 Listener 的名字,取得有点不太好理解。实际监听 TCP 端口的,叫 virtualInbound
/virtualOutbound
,不监听 TCP 端口的,反而没有 virtual
这个前缀。
Listener 内部组件#
Listener 由 Listener filters
、Network Filter Chains
组成。
Listener Filter
和 Network Filter
两个概念比较容易混淆。简单说一下:
Listener Filter
: 在连接建立之初,收集连接上的首几个信息,为选择Network Filter Chain
做数据准备。可以是收集 TCP 基本数据, 如 src IP/port,dst IP/port, 也可以收集 iptables 转发前的原 dst IP/port 。
可以是 TLS 握手数据,SNI / APLN。
Network Filter
:TCP/TLS 握手后,进行更上层协议的处理,如 TCP Proxy / HTTP Proxy
Listener filters#
如,上面的 图:Istio里的 Envoy Inbound 配置举例 中,可以看到几个 Listener filters:
envoy.filters.listener.original_dst
envoy.filters.listener.tls_inspector
envoy.filters.listener.http_inspector
图中已经陈述了相关的功能。
Network Filter Chains#
如,上面的 图:Istio里的 Envoy Inbound 配置举例 中,可以看到几个 Network Filter Chains,它们的名字是可以重复的。而其中每个都有自己的 filter_chain_match
,Envoy 使用这个匹配条件,将连接匹配到不同的 Network Filter Chain
。
每个 Network Filter Chain
由顺序化的 Network Filter
组成。 Network Filter
将在后面的章节介绍。
Listener 相关的组件和启动顺序#
不好意思,直接由上面的 High Level 说明,一下拉回地面,讲源码了。不过我尽量不贴源码出来吓唬人,而是先从类功能、结构、职责说说。 Envoy 只有两种类型的 Listener 实现。TCP 和 UDP 的。这里我只看 TCP 了。 图中信息量不少,不用怕,我慢慢道来。
首先介绍一下核心类:
TcpListenerImpl
- Listener 的核心主类。负责 listen socket 和 listen socket 事件处理。每个 Worker Thread 会为配置中的每个 Listener 创建自己的专属
TcpListenerImpl
实例。如:我们配置了两个 Listener, L1 和 L2。有两个 work thread: W0 和 W1 。那么会有 4 个
TcpListenerImpl
实例。
TcpListenerImpl
类中有bool bind_to_port
属性,可以推测真有不 bind/listen socket 的TcpListenerImpl
了。
TcpListenSocket
- 负责 Listner 的实际 socket 操作,包括bind
和listen
WorkerImpl - Worker 线程的主入口类
DispatcherImpl - 主事件循环和队列类
ListenerManagerImpl
创建 和 bind
TcpListenSocket
。按配置参数创建
WorkerImpl
触发创建
TcpListenerImpl
或者你和我一样,刚看 Envoy 的代码时,总会混淆名字相近的类。如: TcpListenerImpl
、 TcpListenSocket
。
细心的同学如果看了图例,就知道图中黑、红连线代表不同类型的线程的。说说图中主流程:
main 线程
间接调用 ListenerManagerImplbind socket 绑定到 ip 和 port
启动新的 worker 线程
加入异步 task:
add Listener task
(每个 Worker + Listener 执行一次) 到 worker 的任务队列中。worker 线程取出任务队列,执行
add Listener task
worker 线程 异步 listen socket,和注册事件处理器
细心的同学会发现问题:
为何要在主线程中 bind socket ?
可以在进程启动早期就发现 socket 监听端口冲突等常见问题。详细解释在 Envoy 的源码文档中 https://github.com/envoyproxy/envoy/blob/main/source/docs/listener.md 。
两个 worker 线程可以 listen 同一个 socket?
在旧版本默认不使用
reuse_port
socket opts 情况下,是使用 duplicate socket/file descriptor 的方法为每个 work thread 复制一个文件描述符。新版本默认使用
reuse_port
socket opts ,就可以每个线程独立 bind 相同 port 了。 好处见我的文章:记一次 Istio 调优 Part 2 —— 饥饿的线程与 SO_REUSEPORT
代码级的启动顺序#
Listener 相关的组件和启动顺序 - 核心流程图说明以下几步( reuse_port=false
情况下):
进程 main 间接调用 ListenerManagerImpl,间接建立 socket,假设文件描述符为 fd-root
bind socket 绑定到 ip 和 port
启动新的 worker 线程
加入异步 task:
add Listener task
(每个 Worker + Listener 执行一次) 到 worker 的任务队列中。worker 线程取出任务队列,执行
add Listener task
worker 线程 duplicate 文件描述符 fd-root 为 fd-envoy
worker 线程 异步 listen socket,和注册事件处理器
求证过程#
如果有兴趣研究 Listener 的实现细节,建议看看我 Blog 的文章: