容器中某Go服务GC停顿经常超过100ms排查
GC停顿经常超过100ms现象有同事反馈说,最近开始试用公司的Kubernetes,部署在Docker里的Go进程有问题,接口耗时很长,而且还有超时。逻辑很简单,只是调用了KV存储,KV存储一般响应时间<5ms,而且量很少,小于40QPS,该容器分配了0.5个核的配额,日常运行CPU不足0.1个核。复现我找了个容器,踢掉访问流量,用ab 50并发构造些请求看看。网络来回延时60ms,但是平均
gc 111 @97.209s 1%: 82+7.6+0.036 ms clock, 6297+0.66/6.0/0+2.7 ms cpu, 9->12->6 MB, 11 MB goal, 76 P
gc 112 @97.798s 1%: 0.040+93+0.14 ms clock, 3.0+0.55/7.1/0+10 ms cpu, 10->11->5 MB, 12 MB goal, 76 P
gc 113 @99.294s 1%: 0.041+298+100 ms clock, 3.1+0.34/181/0+7605 ms cpu, 10->13->6 MB, 11 MB goal, 76 P
gc 114 @100.892s 1%: 99+200+99 ms clock, 7597+0/5.6/0+7553 ms cpu, 11->13->6 MB, 13 MB goal, 76 P
curl -o trace.out 'http://ip:port/debug/pprof/trace?seconds=20'
sz ./trace.out
newstack->gopreempt_m->goschedImpl->schedule->
if sched.gcwaiting != 0 -> gcstopm -> stopm->notesleep(&_g_.m.park)
增加容器的CPU配额
容器层让容器内的进程看到CPU核数为配额数
根据配额设置正确的GOMAXPROCS
go.uber.org/automaxprocs v1.2.0
import (
_ "go.uber.org/automaxprocs"
)
对于虚拟机或者实体机,8核的情况下:2019/11/07 17:29:47 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined
对于设置了超过1核qusnow boyota的容器,2019/11/08 19:30:50 maxprocs: Updating GOMAXPROCS=8: determined from CPU quota
对于设置小于1核quota的容器,2019/11/08 19:19:30 maxprocs: Updating GOMAXPROCS=1: using minimum allowed GOMAXPROCS
如果Docker中没有设置quota,2019/11/07 19:38:34 maxprocs: Leaving GOMAXPROCS=79: CPU quota undefined,此时建议在启动脚本中显式设置GOMAXPROCS。
gc 97 @54.102s 1%: 0.017+3.3+0.003 ms clock, 0.017+0.51/0.80/0.75+0.003 ms cpu, 9->9->4 MB, 10 MB goal, 1 P
gc 98 @54.294s 1%: 0.020+5.9+0.003 ms clock, 0.020+0.51/1.6/0+0.003 ms cpu, 8->9->4 MB, 9 MB goal, 1 P
gc 99 @54.406s 1%: 0.011+4.4+0.003 ms clock, 0.011+0.62/1.2/0.17+0.003 ms cpu, 9->9->4 MB, 10 MB goal, 1 P
gc 100 @54.597s 1%: 0.009+5.6+0.002 ms clock, 0.009+0.69/1.4/0+0.002 ms cpu, 9->9->5 MB, 10 MB goal, 1 P
gc 101 @54.715s 1%: 0.026+2.7+0.004 ms clock, 0.026+0.42/0.35/1.4+0.004 ms cpu, 9->9->4 MB, 10 MB goal, 1 P
....
1070 1060 0:17 / /sys/fs/cgroup ro,nosuid,nodev,noexec - tmpfs tmpfs ro,mode=755
1074 1070 0:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
1075 1070 0:22 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
1076 1070 0:23 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
1077 1070 0:24 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,hugetlb
1078 1070 0:25 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct,cpu
1079 1070 0:26 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
1081 1070 0:27 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
....
10:net_cls:/kubepods/burstable/pod62f81b5d-xxxx/xxxx92521d65bff8
9:cpuset:/kubepods/burstable/pod62f81b5d-xxxx/xxxx92521d65bff8
8:cpuacct,cpu:/kubepods/burstable/pod62f81b5d-xxxx/xxxx92521d65bff8
7:hugetlb:/kubepods/burstable/pod62f81b5d-5ce0-xxxx/xxxx92521d65bff8
6:blkio:/kubepods/burstable/pod62f81b5d-5ce0-xxxx/xxxx92521d65bff8
5:devices:/kubepods/burstable/pod62f81b5d-5ce0-xxxx/xxxx92521d65bff8
4:memory:/kubepods/burstable/pod62f81b5d-5ce0-xxxx/xxxx92521d65bff8
....
cat /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod62f81b5d-5ce0-xxxx/xxxx92521d65bff8/cpu.cfs_quota_us
50000
cat /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod62f81b5d-5ce0-xxxx/xxxx92521d65bff8/cpu.cfs_period_us
100000
容器中进程看到的核数为母机CPU核数,一般这个值比较大>32,导致Go进程把P设置成较大的数,开启了很多P及线程。
一般容器的quota都不大,0.5-4,Linux调度器以该容器为一个组,里面的线程的调度是公平,且每个可运行的线程会保证一定的运行时间,因为线程多,配额小,虽然请求量很小,但上下文切换多,也可能导致发起stw的线程的调度延迟,引起stw时间升到100ms的级别,极大的影响了请求。
通过使用automaxprocs库,可根据分配给容器的cpu quota,正确设置GOMAXPROCS以及P的数量,减少线程数,使得GC停顿稳定在<1ms了,且同等CPU消耗情况下,QPS可增大一倍,平均响应时间由200ms减少到100ms,线程上下文切换减少为原来的1/6。
同时还简单分析了该库的原理,找到容器的cgroup目录,计算cpuacct,CPU下cpu.cfs_quota_us/cpu.cfs_period_us,即为分配的CPU核数。
当然如果容器中进程看到CPU核数就是分配的配额的话,也可以解决这个问题。这方面我就不太了解了。
不仅仅是Go在容器会因为资源没能完整屏蔽有问题,像Java的函数式编程paralle stream底层的fork-join框架会根据CPU数来启动对应的线程,JVM没有设置heap的话,那heap会根据识别的内存大小来自动设置堆大小,Python也会根据CPU数进行一些设置。
在JavaSE8u131+和JDK9支持了对容器资源限制的自动感知能力,在JDK 8u191和JDK 10之后,社区对JVM在容器中运行做了进一步的优化和增强,JVM可以自动感知Docker容器内部的CPU和内存资源限制,Java进程可用CPU核数由cpu sets、cpu shares和cpu quotas等参数计算而来。
LXCFS可以在容器内挂载/proc目录,可以解决一部分容器可见性问题。
对于Go来说,在进程启动时osinit方法中使用sched_getaffinity获取CPU个数,如果用户设置GOMAXPROCS环境变量,那么就使用该环境变量来此设置P的个数,否则使用CPU个数设置P,对于Go的话,解决这个问题,最简单的方式就是容器启动前Kubernetes根据分配的cpu quota来设置GOMAXPROCS环境变量,这样也不需要用户去关心了。
B站毛老师建议关注一下容器cgroup的CPU,cpuacct下的cpu.stat,里面有容器调度被限制的次数和时间(毛老师说他们已经在SRE上实践了比较久,业务和运维结合比较紧密,也组成团队一起做了很多优化)。
更多推荐
所有评论(0)