乐趣区

关于istio:分析-Java-应用在-Istio-下的-warm-up

剖析 Java 利用在 Istio 下的 warm up

故事的开始

在很久很久前的,有个测试找到一个开发,说 k8s 下 HPA(主动伸缩) 新启动的 pod 的 api latency 特地高:

须要留神,上图是在 Java 服务提供者本身察看到的 latency,即观察点是服务的 server 端或说 upstream。有同学会问,这个重要吗?api latency 就是 api latency 了,还要管是什么观察点的察看后果?

因为已知是 server 端的察看后果,那么就间接考察 server 端了。这次运气比拟好,间接被告知 server 端 cpu 应用高。再看看 pod 配置:

        resources:
          limits:
            cpu: "2"
            memory: 4Gi
          requests:
            cpu: "2"
            memory: 4Gi

考察

直觉通知我,是 CPU 限流 (CPU Throttled) 了(这个直觉是两年前花了一个月在坑中爬出来后造成的,Grafana Dashboard 也是本人老手造的)。可怜言中:

同时,留神到一个景象是,线程数在 pod 启动时,直线拉升:

好了,问题如同很简略,就看什么工作用了那么多 cpu,有什么能够优化。

谁的锅

性能优化有个根本实践:

如果你不能拆解 不可控的简单问题 可控的简略问题,那么这个优化只能看运气。

用 ps 拆解过程到线程是一个思路。看看 pod 启动期间的状况:

export TZ='Asia/Shanghai' ;\
rm /tmp/top-thread-$POD_NAME.log ;\
while true ; do \
  k exec $POD_NAME -- bash -c 'ps --sort=-%cpu  -L -o tid,state,%cpu,cputimes,cputime,nice,pri,comm,maj_flt,psr,policy,wchan -p `pgrep --newest java` | (read -r; printf"%s\n""$REPLY"; sort -k3rn -k4rn) | head -20 '| awk'{print strftime("%Y-%m-%d %H:%M:%S"), $0; fflush();}' | tee -a /tmp/top-thread-$POD_NAME.log ;\
  sleep 5s ;\
done


2023-07-06 21:43:02     TID S %CPU     TIME     TIME  NI PRI COMMAND          MAJFL PSR POL WCHAN
2023-07-06 21:43:02  414573 R 39.2       50 00:00:50   0  19 C2 CompilerThre      1  26 TS  -
2023-07-06 21:43:02  414494 S 17.8       23 00:00:23   0  19 java                 9  36 TS  futex_wait_queue
2023-07-06 21:43:02  414574 R 14.8       19 00:00:19   0  19 C1 CompilerThre      3   2 TS  -
2023-07-06 21:43:02  415310 R  2.4        2 00:00:02   0  19 s0-io-0              0  63 TS  -
2023-07-06 21:43:02  415339 R  2.0        2 00:00:02   0  19 s0-io-3              0  65 TS  -
2023-07-06 21:43:02  415311 R  1.9        2 00:00:02   0  19 s0-io-1              0  63 TS  -
2023-07-06 21:43:02  415098 R  1.8        2 00:00:02   0  19 Thread-17            0  51 TS  -
2023-07-06 21:43:02  415808 S  1.7        1 00:00:01   0  19 qtp2063581529-8      0  15 TS  futex_wait_queue
2023-07-06 21:43:02  415901 S  1.7        1 00:00:01   0  19 qtp2063581529-1      0  72 TS  futex_wait_queue
2023-07-06 21:43:02  415893 S  1.6        1 00:00:01   0  19 qtp2063581529-1      0  10 TS  do_sys_poll
2023-07-06 21:43:02  414495 S  1.4        1 00:00:01   0  19 GC Thread#0          0  42 TS  futex_wait_queue
2023-07-06 21:43:02  414498 S  1.4        1 00:00:01   0  19 GC Thread#1          0   5 TS  futex_wait_queue
2023-07-06 21:43:02  415882 S  1.3        1 00:00:01   0  19 qtp2063581529-1      0  39 TS  do_sys_poll
2023-07-06 21:43:02  415768 S  1.2        1 00:00:01   0  19 qtp2063581529-7      0  60 TS  futex_wait_queue
2023-07-06 21:43:02  415109 R  1.1        1 00:00:01   0  19 Thread-18            1  53 TS  -
2023-07-06 21:43:02  415892 S  1.1        1 00:00:01   0  19 qtp2063581529-1      0  68 TS  futex_wait_queue
2023-07-06 21:43:02  415952 S  1.1        0 00:00:00   0  19 qtp2063581529-1      0  15 TS  futex_wait_queue
2023-07-06 21:43:02  416066 S  1.1        0 00:00:00   0  19 qtp2063581529-2      0  78 TS  futex_wait_queue
2023-07-06 21:43:02  415867 S  1.0        0 00:00:00   0  19 qtp2063581529-9      0   0 TS  futex_wait_queue
...

其中 qtp2063581529-... 就是负责解决 api 申请的线程。那么 C2 CompilerThre 是什么?当然就是 Compiler threads 了。地球人都晓得,java 是 Just-In-Time (JIT) compiler 的,就是边跑边优化编译。JIT 这块我最近整顿过一些笔记,见:《Mark’s DevOps 雜碎》– Java Compile。

环境

Istio 1.17:

  • 曾经启用:warmupDurationSecs warmupDurationSecs: 3m

Open JDK 17

Tuning(调优) – Take 1

Compiler Tuning

本着谁的锅谁背的准则。让 Compiler 迟一点优化编译,防止在启动时与其它初始化工作争用无限的、容器限度的 2 cpus。成熟的 Open JDK 17 当然曾经提供相干参数了。让咱们看看:

具体参考:《Mark’s DevOps 雜碎》– Java Compiler Tuning

首先咱们晓得,默认状况下,JVM 是在利用运行过程中,依据代码的应用状况(可临时简略视为代码被执行的次数),分期分级去优化编译的。每一级,都有每一级的准入门槛。

source: https://www.baeldung.com/jvm-tiered-compilation

Tier level(编译级别) Meaning
0 The code has been interpreted
1 Simple C1 compilation
2 Limited C1 compilation
3 Full C1 compilation
4 C2 compilation

JVM 提供了一堆参数能够调整几个编译档次的门槛,其默认值为:

bash-4.4$ java -XX:+PrintFlagsFinal cp . Main  | grep -i Threshold
     intx CompileThreshold                         = 10000                                  {pd product} {default}
   double CompileThresholdScaling                  = 1.000000                                  {product} {default}
     intx Tier3BackEdgeThreshold                   = 60000                                     {product} {default}
     intx Tier3CompileThreshold                    = 2000                                      {product} {default}
     intx Tier3InvocationThreshold                 = 200                                       {product} {default}
     intx Tier3MinInvocationThreshold              = 100                                       {product} {default}
     intx Tier4BackEdgeThreshold                   = 40000                                     {product} {default}
     intx Tier4CompileThreshold                    = 15000                                     {product} {default}
     intx Tier4InvocationThreshold                 = 5000                                      {product} {default}
     intx Tier4MinInvocationThreshold              = 600                                       {product} {default}

咱们的指标是推延优化,所以就是加高门槛了。一个个参数调整有点麻烦,于是,Java 设计了一个总的门槛高度系数:-XX:CompileThresholdScaling

试着加大这个系数:

bash-4.4$ java -XX:+PrintFlagsFinal -XX:CompileThresholdScaling=1.5 -cp . Main | grep -i Threshold                                                                
     intx CompileThreshold                         = 15000                                  {pd product} {ergonomic}
   double CompileThresholdScaling                  = 1.500000                                  {product} {command line}
     intx Tier3BackEdgeThreshold                   = 90000                                     {product} {ergonomic}
     intx Tier3CompileThreshold                    = 3000                                      {product} {ergonomic}
     intx Tier3InvocationThreshold                 = 300                                       {product} {ergonomic}
     intx Tier3MinInvocationThreshold              = 150                                       {product} {ergonomic}
     intx Tier4BackEdgeThreshold                   = 60000                                     {product} {ergonomic}
     intx Tier4CompileThreshold                    = 22500                                     {product} {ergonomic}
     intx Tier4InvocationThreshold                 = 7500                                      {product} {ergonomic}
     intx Tier4MinInvocationThreshold              = 900                                       {product} 

Compiler threads 优先级

下面晓得,编译由专门的 Compiler Thread 负责,编译线程的运行自身就和利用 api 解决线程争用原本就缓和的 CPU 资源。所以,还有一个思路是升高编译线程的调度优先级。Java 提供了一个参数 -XX:CompilerThreadPriority 能够去调整 Compiler Thread 优先级。须要留神的是,对于非 root 用户。-XX:CompilerThreadPriority=<n> 须要配合 -XX:ThreadPriorityPolicy=1 应用,方可失效。

$ java -XX:CompilerThreadPriority=10 -XX:ThreadPriorityPolicy=1 -cp . Main

OpenJDK 64-Bit Server VM warning: -XX:ThreadPriorityPolicy=1 may require system level permission, e.g., being the root user. If the necessary permission is not possessed, changes to priority will be silently ignored.

$ ps --sort=-%cpu  -L -o tid,state,%cpu,cputimes,cputime,nice,pri,comm,maj_flt,psr,policy,wchan -p `pgrep --newest java` | (read -r; printf "%s\n" "$REPLY"; sort -k3rn -k4rn) | head -20 '| awk'{print strftime("%Y-%m-%d %H:%M:%S"), $0; fflush();}

2023-07-06 21:43:02     TID S %CPU     TIME     TIME  NI PRI COMMAND          MAJFL PSR POL WCHAN
2023-07-06 21:43:02  414573 R 39.2       50 00:00:50   10 29 C2 CompilerThre      1  26 TS  -
2023-07-06 21:43:02  414494 S 17.8       23 00:00:23   0  19 java                 9  36 TS  futex_wait_queue
2023-07-06 21:43:02  414574 R 14.8       19 00:00:19   10 29 C1 CompilerThre      3   2 TS  -
2023-07-06 21:43:02  415310 R  2.4        2 00:00:02   0  19 s0-io-0              0  63 TS  -
2023-07-06 21:43:02  415339 R  2.0        2 00:00:02   0  19 s0-io-3              0  65 TS  -
2023-07-06 21:43:02  415311 R  1.9        2 00:00:02   0  19 s0-io-1              0  63 TS  -
2023-07-06 21:43:02  415098 R  1.8        2 00:00:02   0  19 Thread-17            0  51 TS  -
2023-07-06 21:43:02  415808 S  1.7        1 00:00:01   0  19 qtp2063581529-8      0  15 TS  futex_wait_queue

Linux 的线程优先级参数叫 Nice(NI),就是谦让系数的意思的。其实是数字越小,越优先。

具体参考:《Mark’s DevOps 雜碎》– Java Compiler Tuning

Tuning – Take 1 后果

在 java 命令参数加上:

java -XX:CompilerThreadPriority=15 -XX:ThreadPriorityPolicy=1 -XX:CompileThresholdScaling=20

后,看看后果。

可见,调整参数当前,有相当的改善,尽管还是比拟难看。这时如果查看编译线程用的 cpu,会发现确实显著降落了:

$ ps --sort=-%cpu  -L -o tid,state,%cpu,cputimes,cputime,nice,pri,comm,maj_flt,psr,policy,wchan -p `pgrep --newest java` | (read -r; printf "%s\n" "$REPLY"; sort -k3rn -k4rn) | head -20 '| awk'{print strftime("%Y-%m-%d %H:%M:%S"), $0; fflush();}

2023-07-06 22:43:02     TID S %CPU     TIME     TIME  NI PRI COMMAND          MAJFL PSR POL WCHAN
2023-07-06 22:43:02  414573 R 12.2       50 00:00:50   10 29 C2 CompilerThre      1  26 TS  -

Tuning – Take 2

各位应该还记得后面这图,线程数在 pod 启动时,直线拉升。

如果限度了容器只应用 2 cpus,但又给 java web server 的 thread pool 配置了比拟大的 max threads。

想像一下,200 个并发 api 申请过去了。java web server 开了 200 线程,他们之前并发抢夺可怜的 2 cpus。就像 2022 年每天产生的事,200 集体去排队验 Hésuān 😷(什么,你竟然遗记了?你幸福了)。不过这个比喻不太贴切,CPU 是分片调度线程的。但从 http 申请的角度看,大家都在等,CPU 也在忙,只是把每个 http 申请的工作,分成细小的工夫片。而后两头“随便”插入其它 http 申请的工夫片。总之,最初是每个 http 申请都不开心。最差的状况下,downstream client 曾经 timeout 了,CPU 还在做曾经无意义的工作。

不如引入准入和排队机制,这正是巨匠口中常吟诵的“Overload control(过载爱护)”。这就能够让曾经入场的 http 申请在 timeout 前实现。而场外的 http 申请,要么是 在内存(而非 CPU)上排队(即期待线程池呈现闲暇线程),要么就分流去其它 Hésuān 检测点(其它曾经实现 warm up 的老 POD)。

要实现这个思路,办法有很多,我先用最简略的 thread pool 线程池限度。缩小 thread pool 大小,由 200 调整为 20。

背压机制(Backpressure)

没有思考 Backpressure 的 Overload control 不是好的 Overload control。我这里就是临时未思考了。Backpressure 在这个场景中,就是指在 web server 过载时,client 能够通过一些办法感知到,并在负载平衡算法中,缩小这个有问题 web server 的流量权重。

Tuning – Take 2 后果

只看这个 Dashboard 的话,后果是优良的。但如之前所说,请留神,这个观察点是 upstream http server 端。你有看看 downstream(client,在 Istio 场景下,就是和 http server 同一 pod 的 envoy istio-proxy sidecar) 的感触吗?Backpressure 做到位了吗?这个留给下回分解吧。

Tuning – Take 3

切实想不到,竟然还有 take 3。话说,你应该也有相似的教训。老板或管理层总在说,你们这些基层员工,要有格局。那么,问题来了,什么是格局?

老板可能是这样想的:

做事有广阔的眼帘,不只看本人岗位的一亩三分地,全局看事件,以事件的后果效益去领导工作上的事,而不只是专才。

我作为基层码农的一份子,是这样想的:(留神,这样想不代表就这样认同,这里想法是事实,认同是立场,留神分分明事实和立场)

客户在意的其实是老本和收益。你用的技术高下,有没什么技术情怀没什么关系。

举个例子,1k TPS,按这个利用的实现设计须要 20 个 cpu。你能够这样分 pod:

  • 10 pods,每个 2 cpus
  • 5 pods,每个 4 cpus

10 pods 和 5 pods 有区别吗?还真有。

10 pods 意味着同一个 java class。须要执行 10 次 JIT warm up,就是 10 次 JIT warm up 用的 cpu times。而 5 pods 只须要大略一半。微妙的是,个别客户不会在意你的每 pod cpu 数,只看总资源。

还有另外一种技术控可能能够承受的解释是:古代 jvm 在设计时,曾经是为多 cpu 优化过的。2 CPU 下,会有各种后盾线程争用 cpu,最终减少业务线程 latency 的可能。如编译和 GC。

后果:

结语

技术或者有几个所谓的境界:

  1. 理解新名词,学习一些网上顺手可得的科谱,做一些小我的项目,用这些名词刷亮本人的名片
  2. 理解这货色的实现机理甚至源码
  3. 灵活运用和定制
  4. 晓得它是为何而来,将倒退到哪儿去
  5. 什么都不懂
退出移动版