面试拾遗:Epoll 之下的线程模型 / Envoy 快速之谜
多核之下的 Epoll
对于 Epoll
来说,我们为了利用多核能力,其实有两种办法
- 负载均衡
accept()
- 负载均衡
read()
负载均衡 accept()
对于高频建立短连接的,我们要快速处理 accept()
水平触发情况下一种无效的唤醒: Level triggered
- Kernel: 接收到请求
- Kernel: 唤醒通知两个线程
- Thread A: 完成 epoll_wait().
- Thread B: 完成 epoll_wait().
- Thread A: 执行 accept() 成功
- Thread B: 执行 accept() 失败,对于 B 的唤醒无效
边缘触发情况下一种无效的唤醒: Edge triggered
- Kernel: 接收到请求,唤醒A/B,这里假设是 A
- Thread A: 完成 epoll_wait().
- Thread A: 执行 accept() 成功
- Kernel: 接收到请求
- Kernel: 唤醒B来接收(A在处理)
- Thread A: 执行了
accept()
成功 - Thread B: 执行了
accept()
失败了,被无效唤醒
当上述的线程从 2个变成 1000 个,我们就可能唤醒很多线程而无效。对于 Linux 4.5
增加了 EPOLLEXCLUSIVE
标记,只会唤醒一个 epoll_wait()
状态的线程。
Netty
对于 Netty
来说, accept()
的工作是放在 Bossgroup
中执行的,因此,如果我们需要处理大量高频的请求,我们应该尽可能的将 bossGroup
多分配一些,对于 Netty
来说是无法禁止使用 childGroup
的。
1 | public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) { |
因此我们势必会涉及到 BossGroup
-> WorkerGroup
的开销。
负载均衡 read()
对于裸用 Epoll
来说,我们会出现以下2个问题,都会出现两个不同的进程处理同一个 Socket 的数据
LT 模式下
- Kernel: 接收到
2047
字节数据 - Kernel: 唤醒A处理
- Thread A: 完成
epoll_wait()
- Kernel: 接收到
2
字节 - Kernel: 唤醒 B 处理
- Thread A: 执行
read(2048)
获得2048
字节数据 - Thread B: 执行
read(2048)
获得1
字节数据
ET 模式下
- Kernel: 接收到
2048
字节数据 - Kernel: 唤醒A处理
- Thread A: 完成
epoll_wait()
- Thread A: 执行
read(2048)
获得2048
字节数据 - Kernel: 接收到
1
字节数据 - Kernel: 唤醒B处理
- Thread B: 完成
epoll_wait()
- Thread B: 执行
read(2048)
获得1
字节数据 - Thread A: 重试
read(2048)
, 什么都没获得
针对上述的情况, Linux
加入了 EPOLLONESHOT
只允许一个线程注册唯一的回调事件。
1 | void Eepoll::ResetOneShot(intepollfd,SOCKET fd,bool bOne){ |
Netty
这种一般服务于 大量
长时间连接的,需要及时处理接收到的请求,这种情况下需要单 Boss
而需要多个 Worker
Workgroup default value
Workgroup
的默认值是 CPU Core
的 2
倍,昨天也被问了这个事情,查了一下 Google
也没有找到适合的答案,我估计这是应该是一个实验数据,在大部分的情况下这个 2
应该可以发挥最大的作用。(有空测试一下)
Why Envoy So Fast
这方面的资料比较少,并且作者在阅读 Envoy
源码的时候的确也没发现什么特别之处,从设计上尝试回答一下这个问题
Envoy Thread Model
Envoy
的线程模式是对等设计,在HTTP转发模式下,因为每一个线程和下游的服务[另外一个Envoy
]连接的时候会创建一个 HTTP/2
的连接[基于 Host:Port
区分],这些连接被缓存在一个 连接池内
,此时可以利用 Http2
多路复用的能力节约很多时间。
Each worker thread maintains its own connection pools for each cluster, so if an Envoy has two threads and a cluster with both HTTP/1 and HTTP/2 support, there will be at least 4 connection pools.
除此之外,non-blocking
是最核心的部分。对于一个热更新系统,我们可以很自然的想到我们需要共享一部分的数据在不同的线程之间,常见的是使用 ReadWriterLock
,Envoy
用了一种特别的设计办法 Thread Local Storage (TLS) system
:
类似于 RCU
的机制,所有的 Worker
仅仅进行读取的操作。
Envoy Filter
Envoy Filter
是动态生成的,这和大部分的传统 Filter
不太一样,对于大多数的实现,比如 Spring WebMVC
的 Filter
对于所有的请求都是相同的,我们可以在函数的入口判断是否需要处理,或者是直接调用下一个 Filter
而 Envoy
的 Filter
构成的 FilterChain
是由配置生成的,因此比如我们只需要 2
步操作的话,我们在 生成的FilterChain
中也只有 2
个 Filter
,减少了很多的函数调用栈的成本。
负债均衡 Accept
对于 Envoy
来说, Run as Sidecar
针对同 Pod
的请求多半是高频的短连接 HTTP
,这种模式下,我们 负债均衡 Accept
是最佳的选择,因此 Envoy
在设计的时候,就是使用负债均衡 Accept
的设计。