图片来自:https://getboulder.com/boulde…
引
话说,在很长一段时间,程序员依赖了摩尔定律。而在它到头之前,程序员找到了另一个救命稻草:并行 / 并发 / 最终统一。而到了明天,不是 Cloud Native / Micro Service 都不好意思打招呼了。多线程,更是 by default 的了。而在计算机性能工程界,也有一个词: Mechanical Sympathy
,直译就是 机器同情心
。而要“同情”的前提是,得理解。生存中,很多人理解和谋求work life balance
。但你的线程,是否 balance
你要不要同情一下?一条累到要过载线程,看到其它伙伴在吃下午茶,又是什么一种同情呢?如何能力让多线程达到最大吞吐?
开始
我的项目始终很关注服务响应工夫。而 Istio 的引入显著加大了服务提早,如何尽量减少提早始终是性能调优的重点。
测试环境
Istio: v10.0 / Envoy v1.18
Linux Kernel: 5.3
调用拓扑:
(Client Pod) --> (Server Pod)
其中 Client Pod
构造:
Cient(40 并发连贯) --> Envoy(默认 2 worker thread)
其中 Server Pod
构造:
Envoy(默认 2 worker thread) --> Server
Client/Serve 均为 Fortio(一个 Istio 性能测试工具)。协定应用 HTTP/1.1 keepalive。
问题
压测时,发现 TPS 压不下来,Client/Server/envoy 的整体 CPU 利用率不高。
首先,我关注的是 sidecar 上是不是有瓶颈。
Envoy Worker 负载不均
察看 envoy worker 线程利用率
因为 Envoy 是 CPU 敏感型利用。同时,外围架构是事件驱动、非阻塞线程组。所以察看线程的状况通常能够发现重要线索:
$ top -p `pgrep envoy` -H -b
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
41 istio-p+ 20 0 0.274t 221108 43012 R 35.81 0.228 49:33.37 wrk:worker_0
42 istio-p+ 20 0 0.274t 221108 43012 R 60.47 0.228 174:48.28 wrk:worker_1
18 istio-p+ 20 0 0.274t 221108 43012 S 0.332 0.228 2:22.48 envoy
依据 Envoy 线程模型(https://blog.envoyproxy.io/en…)。连贯绑定在线程上,连贯上的所有申请均由绑定的线程解决。这种绑定是在连贯建设时确定的,并且不会扭转,直到连贯敞开。所以,忙的线程很大可能绑定的连接数绝对大。
🤔 为何要绑定连贯到线程?
在 Envoy 外部,连贯是有状态数据的,特地是对于 HTTP 的连贯。为缩小线程间共享数据的锁争用,同时也为进步 CPU cache 的命中率,Envoy 采纳了这种绑定的设计。
察看 envoy worker 连贯散布
Envoy 提供了大量的监控统计(https://www.envoyproxy.io/doc…)。首先,用 Istio 的办法关上它:
apiVersion: v1
kind: Pod
metadata:
name: fortio-sb
annotations:
sidecar.istio.io/inject: "true"
proxy.istio.io/config: |-
proxyStatsMatcher:
inclusionRegexps:
- ".*_cx_.*"
...
视察 envoy stats :
$ kubectl exec -c istio-proxy $POD -- curl -s http://localhost:15000/stats | grep '_cx_active'
listener.0.0.0.0_8080.worker_0.downstream_cx_active: 8
listener.0.0.0.0_8080.worker_1.downstream_cx_active: 32
可见,连贯的调配相当不均。其实,Envoy 在 Github 上,早有牢骚:
- Investigate worker connection accept balance (https://github.com/envoyproxy…)
- Allow REUSEPORT for listener sockets https://github.com/envoyproxy…
同时,也给出了解决方案:SO_REUSEPORT
。
解决之道
什么是 SO_REUSEPORT
一个比拟原始和权威的介绍:https://lwn.net/Articles/542629/
图片来自:https://tech.flipkart.com/lin…
简略来说,就是多个 server socket 监听雷同的端口。每个 server socket 对应一个监听线程。内核 TCP 栈接管到客户端建设连贯申请 (SYN) 时,按 TCP 4 元组 (srcIP,srcPort,destIP,destPort) hash 算法,抉择一个监听线程,唤醒之。新连贯绑定到被唤醒的线程。 所以绝对于非 SO_REUSEPORT
,连贯更为均匀地散布到线程中(hash 算法不是相对均匀)。
Envoy Listner SO_REUSEPORT 配置
Envoy 把监听和接管连贯的组件命名为 Listener。作为 sidecar 的 envoy 有两种 Listener:
-
virtual-Listener,名字带 ’virtual’,但,这才是实际上监听 socket 的 Listener。🤣
- virtual-outbound-Listener:出站流量。监听 15001 端口。由 sidecar 所在的 POD 的利用收回的对外申请,均被 iptable redirect 到这个 listener,再由 envoy 转发。
- virtual-inbound-Listener:入站流量。监听 15006 端口。接管由其它 POD 发过来的流量。
- non-virtual-outbound-Listener,每个 k8s service 的端口号均对应一个名字为 0.0.0.0_$PORT 的
non-virtual-outbound-Listener
。这种 Listener 不监听端口。
详见:https://zhaohuabing.com/post/…
回到本文的重点,只关怀 实际上监听 socket 的 Listener,即 virtual-Listener
。指标是让其应用 SO_REUSEPORT
,以让新连贯较平均分配到线程。
在 Envoy v1.18 中,有一个 Listener 参数:reuse_port
:
https://www.envoyproxy.io/doc…
reuse_port (bool) When this flag is set to true, listeners set the SO_REUSEPORT socket option and create one socket for each worker thread. This makes inbound connections distribute among worker threads roughly evenly in cases where there are a high number of connections. When this flag is set to false, all worker threads share one socket. Before Linux v4.19-rc1, new TCP connections may be rejected during hot restart (see 3rd paragraph in‘soreuseport’commit message). This issue was fixed by tcp: Avoid TCP syncookie rejected by SO_REUSEPORT socket.
在我应用的 Envoy v1.18 中默认为敞开。而在最新版本中(写本文时未公布的 v1.20.0)这个开关有了变动,默认为关上:
https://www.envoyproxy.io/doc…
reuse_port (bool) Deprecated. Use enable_reuse_port instead. enable_reuse_port (BoolValue) When this flag is set to true, listeners set the SO_REUSEPORT socket option and create one socket for each worker thread. This makes inbound connections distribute among worker threads roughly evenly in cases where there are a high number of connections. When this flag is set to false, all worker threads share one socket. This field defaults to true. On Linux, reuse_port is respected for both TCP and UDP listeners. It also works correctly with hot restart.
✨ 题外话:如果你须要相对平均分配连贯,能够试试 Listener 的配置
connection_balance_config:exact_balance
,我没试过,不过因为有锁,对高频新连贯应该有肯定的性能损耗。
好,剩下的问题是如何关上 reuse_port
了。上面,以 virtualOutbound
为例:
kubectl apply -f - <<"EOF"
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: my_reuse_port_envoyfilter
spec:
workloadSelector:
labels:
my.app: my.app
configPatches:
- applyTo: LISTENER
match:
context: SIDECAR_OUTBOUND
listener:
portNumber: 15001
name: "virtualOutbound"
patch:
operation: MERGE
value:
reuse_port: true
EOF
是的,须要重启 POD。
我始终感觉 Cloud Native 一个最大问题是,你批改了一个配置,很难晓得是否真正利用了。面向指标状态配置的设计准则当然很好,但事实是可视察性跟不上。所以,还是 double check 吧:
kubectl exec -c istio-proxy $POD -- curl 'http://localhost:15000/config_dump?include_eds' | grep -C 50 reuse_port
很侥幸,失效了 (事实是,因环境问题,我为这个失效折腾了一天🤦):
{
"name": "virtualOutbound",
"active_state": {
"version_info": "2021-08-31T22:00:22Z/52",
"listener": {
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "virtualOutbound",
"address": {
"socket_address": {
"address": "0.0.0.0",
"port_value": 15001
}
},
"reuse_port": true
如果你和我一样,是个强迫症患者,那么还是看看有几个 listen 的 socket 吧:
$ sudo ss -lpn | grep envoy | grep 15001
tcp LISTEN 0 128 0.0.0.0:15001 0.0.0.0:* users:(("envoy",pid=36530,fd=409),("envoy",pid=36530,fd=363),("envoy",pid=36530,fd=155))
tcp LISTEN 0 129 0.0.0.0:15001 0.0.0.0:* users:(("envoy",pid=36530,fd=410),("envoy",pid=36530,fd=364),("envoy",pid=36530,fd=156))
是的,两个 socket 在监听同一个端口。Linux 再次突破咱们的模式化思维,再次证实它是个怪兽企鹅。
调优后果
丑妇还需见家翁,咱们看看后果吧。
线程的负载比拟均匀了:
$ top -p `pgrep envoy` -H -b
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
41 istio-p+ 20 0 0.274t 221108 43012 R 65.81 0.228 50:33.37 wrk:worker_0
42 istio-p+ 20 0 0.274t 221108 43012 R 60.43 0.228 184:48.28 wrk:worker_1
18 istio-p+ 20 0 0.274t 221108 43012 S 0.332 0.228 2:22.48 envoy
连贯比拟均匀地调配到两个线程了:
$ kubectl exec -c istio-proxy $POD -- curl -s http://localhost:15000/stats | grep '_cx_active'
listener.0.0.0.0_8080.worker_0.downstream_cx_active: 23
listener.0.0.0.0_8080.worker_1.downstream_cx_active: 17
服务的 TPS 也有肯定进步。
领会
我不太喜爱写总结,我感觉领会可能更有意义。Open Source / Cloud Native 倒退到明天,我感觉本人离写程序编码越来越远,更像一个 search
/stackoverflow
/github
/yaml
工程师了。因为简直所有需要,均有组件可拿来主义,解决一个简略的问题大略只须要:
- 分明找到问题的 keyword
- search keyword,凭教训过滤本人认为重要的信息
- 浏览相干的 Blog/Issue/ 文档 /Source code
- 思考过滤信息
- 利用和试验
- Goto 1
- 如以上步骤均不行,提 Github Issue。当然,本人 fix 做 contributor 就完满了。
我不晓得,这是件坏事,还是个好事。search
/stackoverflow
/github
让人感觉搜到就是学到,最初常识就变成了碎片化的机械记忆,短少了体系的、经本人深度消化和考据过的认知,更不必谈思考与翻新了。
对于续集
下一 Part,我打算看看 NUMA 硬件架构下,如何用 CPU 绑定,内存绑定,HugePages,优化 Istio/Envoy。当然,也是基于 Kubernetes 的 Topology Management
和 CPU / MemoryManager
。到当初为止,临时成果不大,也不太顺利。网上有大量的用 eBPF 优化 Envoy 协定栈老本的信息,但我感觉技术上,还不太成熟,也没看到现实的老本成果。
参考
Istio:
https://zhaohuabing.com/post/…
SO_REUSEPROT:
https://lwn.net/Articles/542629/
https://tech.flipkart.com/lin…
https://www.nginx.com/blog/so…
https://domsch.com/linux/lpc2…
https://blog.cloudflare.com/p…
https://lwn.net/Articles/853637/