首页 体育 教育 财经 社会 娱乐 军事 国内 科技 互联网 房产 国际 女人 汽车 游戏

容器中某 Go 服务 GC 停顿经常超过 100ms 排查

2019-12-28

有搭档反应说, 最近开端试用公司的k8s, 布置在docker里的go进程有问题, 接口耗时很长, 并且还有超时. 逻辑很简略, 仅仅调用了kv存储, kv存储一般呼应时刻 5ms, 并且量很少, 小于40qps, 该容器分配了0.5个核的配额, 日常运转CPU缺乏0.1个核.

我找了个容器, 踢掉拜访流量. 用ab 50并发结构些恳求看看. 网络来回延时60ms, 可是均匀处理耗时200多ms, 99%耗时到了679ms.

用ab处理时, 看了下CPU和内存信息, 都没啥问题. docker分配的是0.5个核. 这儿也没有用到那么多.

看了下监控, GC STW超越10ms, 50-100ms的都许多, 还有不少超越100ms的. Go不是宣称在1.8后GC中止基本是小于1ms的吗?

看看该进程的runtime信息, 发现内存很少,gc-pause很大,GOMAXPROCS为76,是机器的核数。

export GODEBUG=gctrace=1, 重启进程看看. 能够看出gc中止确实很严重.

在一台有go sdk的服务器上对服务跑一下trace, 再把trace文件下载到本地看看

下图可见有一个GC的wall time为172ms,而本次gc的两个stw阶段,sweep termination和mark termination都在80多ms的姿态, 简直占满了整个GC时刻, 这当然很不科学.

这个服务是运转在容器里的, 容器和母机同享一个内核, 容器里的进程看到的CPU核数也是母机CPU核数, 关于Go运用来说, 会默许设置P的个数为CPU核数. 咱们早年面的图也能够看到, GOMAXPROCS为76, 每个运用中的P都有一个m与其绑定, 所以线程数也不少, 上图中的为171.但是分配给该容器的CPU配额其实不多, 仅为0.5个核, 而线程数又不少.

猜想: 关于linux的cfs来说, 当时容器内一切的线程都在一个调度组内. 为了确保功率, 关于每个被运转的task, 除非由于堵塞等原因自动切换, 那么至少确保其运转/proc/sys/kernel/sched min granularity_ns的时刻, 能够看到为4ms.

容器中Go进程没有正确的设置GOMAXPROCS的个数, 导致可运转的线程过多, 或许呈现调度推迟的问题. 正好呈现进入gc主张stw的线程把其他线程中止后, 其被调度器切换出去, 好久没有调度该线程, 实质上造成了stw时刻变得很长.

处理的办法, 由于可运转的P太多, 导致占用了主张stw的线程的虚拟运转时刻, 且CPU配额也不多. 那么咱们需求做的是使得P与CPU配额进行匹配. 咱们能够挑选:

增加容器的CPU配额.

容器层让容器内的进程看到CPU核数为配额数

依据配额设置正确的GOMAXPROCS

第1个办法: 没太大作用, 把配额从0.5变成1, 没实质的差异.

第2点办法: 由于我对k8s不是很熟, 待我调研后再来弥补.

第3个办法: 设置GOMAXPROCS最简略的办法便是发动脚本增加环境变量

GOMAXPROCS=2 ./svr_bin 这种是有用的, 但也有缺乏, 假如布置配额大一点的容器, 那么脚本无法跟着变.

uber有一个库, go.uber.org/automaxprocs, 容器中go进程发动时, 会正确设置GOMAXPROCS. 修改了代码模板. 咱们在go.mod中引证该库

并在main.go中import

运用automaxprocs库, 会有如下日志:

关于虚拟机或许实体机

8核的情况下: 2019/11/07 17:29:47 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined

关于设置了超越1核quota的容器

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

设置完后, 再用ab恳求看看,网络往复时刻为60ms, 99%恳求在200ms以下了, 之前是600ms. 平等CPU耗费下, qps从差不多提升了一倍.

由于分配的是0.5核, GOMAXPROC识别为1. gc-pause也很低了, 几十us的姿态. 一起也能够看到线程数从170多降到了11.

以下为并发50, 总共处理8000个恳求的perf stat成果比照. 默许CPU核数76个P, 上下文切换13万屡次, pidstat检查system cpu耗费9%个核. 而依照quota数设置P的数量后, 上下文切换仅为2万屡次, cpu耗费3%个核.

这个库怎么依据quota设置GOMAXPROCS呢, 代码有点绕, 看完后, 其实原理不杂乱. docker运用cgroup来约束容器CPU运用, 运用该容器装备的cpu.cfs quota us/cpu.cfs period us即可取得CPU配额. 所以关键是找到容器的这两个值.

cat /proc/self/mountinfo

cpuacct,cpu在/sys/fs/cgroup/cpu,cpuacct这个目录下.

cat /proc/self/cgroup

该容器的cpuacct,cpu详细在/kubepods/burstable/pod62f81b5d-xxxx/xxxx92521d65bff8子目录下

两者相除得到0.5, 小于1的话,GOMAXPROCS设置为1,大于1则设置为核算出来的数。

automaxprocs库中中心函数如下所示, 其间cg为解析出来的cgroup的一切装备途径. 别离读取cpu.cfs _quota _us和cpu.cfs _period _us, 然后核算. 

谷歌搜了下, 也有人提过这个问题

runtime: long GC STW pauses #19378 https://github.com/golang/go/issues/19378

容器中进程看到的核数为母机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核数便是分配的配额的话, 也能够处理这个问题. 这方面我就不太了解了.

热门文章

随机推荐

推荐文章