记一次 Istio 调优 Part 2 —— 饥饿的线程与 SO_REUSEPORT

Gravity-Glue-6-Credit-Michael-Grab-900x580.jpg

图片来自:https://getboulder.com/boulder-artist-rocks-the-world/

话说,在很长一段时间,程序员依赖了摩尔定律。而在它到头之前,程序员找到了另一个救命稻草:并行/并发/最终一致。而到了今天,不是 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/envoy-threading-model-a8d44b922310)。连接绑定在线程上,连接上的所有请求均由绑定的线程处理。这种绑定是在连接建立时确定的,并且不会改变,直到连接关闭。所以,忙的线程很大可能绑定的连接数相对大。

🤔 为何要绑定连接到线程?
在 Envoy 内部,连接是有状态数据的,特别是对于 HTTP 的连接。为减少线程间共享数据的锁争用,同时也为提高 CPU cache 的命中率,Envoy 采用了这种绑定的设计。

观察 envoy worker 连接分布

Envoy 提供了大量的监控统计(https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats)。首先,用 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 上,早有怨言:

同时,也给出了解决方案: SO_REUSEPORT

解决之道

什么是 SO_REUSEPORT

一个比较原始和权威的介绍:https://lwn.net/Articles/542629/

so_reuseport.jpeg

图片来自:https://tech.flipkart.com/linux-tcp-so-reuseport-usage-and-implementation-6bfbf642885a

简单来说,就是多个 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/2018-09-25-istio-traffic-management-impl-intro/#virtual-listener

回到本文的重点,只关心实际上监听 socket 的 Listener,即 virtual-Listener。目标是让其使用 SO_REUSEPORT,以让新连接较平均分配到线程。

在 Envoy v1.18 中,有一个 Listener 参数: reuse_port:

https://www.envoyproxy.io/docs/envoy/v1.18.3/api-v3/config/listener/v3/listener.proto

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/docs/envoy/latest/api-v3/config/listener/v3/listener.proto

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 工程师了。因为几乎所有需求,均有组件可拿来主义,解决一个简单的问题大概只需要:

  1. 清楚找到问题的 keyword
  2. search keyword,凭经验过滤自己认为重要的信息
  3. 浏览相关的 Blog/Issue/文档/Source code
  4. 思考过滤信息
  5. 应用和实验
  6. Goto 1
  7. 如以上步骤均不行,提 Github Issue。 当然,自己 fix 做 contributor 就完美了。

我不知道,这是件好事,还是个坏事。search/stackoverflow/github 让人觉得搜到就是学到,最后知识就变成了碎片化的机械记忆,缺少了体系的、经自己深度消化和考证过的认知,更不用谈思考与创新了。

关于续集

下一 Part,我打算看看 NUMA 硬件架构下 ,如何用 CPU 绑定, 内存绑定, HugePages,优化 Istio/Envoy。当然,也是基于 Kubernetes 的 Topology ManagementCPU / MemoryManager。到现在为止,暂时效果不大,也不太顺利。网上有大量的用 eBPF 优化 Envoy 协议栈成本的信息,但我觉得技术上,还不太成熟,也没看到理想的成本效果。

参考

Istio:
https://zhaohuabing.com/post/2018-09-25-istio-traffic-management-impl-intro/#virtual-listener

SO_REUSEPROT:
https://lwn.net/Articles/542629/
https://tech.flipkart.com/linux-tcp-so-reuseport-usage-and-implementation-6bfbf642885a
https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/
https://domsch.com/linux/lpc2010/Scaling_techniques_for_servers_with_high_connection%20rates.pdf
https://blog.cloudflare.com/perfect-locality-and-three-epic-systemtap-scripts/

https://lwn.net/Articles/853637/

原文:https://blog.mygraphql.com/zh/posts/cloud/istio/istio-tunning/istio-thread-balance/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容