导语

本文介绍了公司“云化服务”的大背景下,将一个Go服务迁移至公司的基于K8s+docker的容器云平台,使用火焰图进行性能排查和优化方面的实践。欢迎在留言区进行阅读探讨。

背景

在公司“云化服务”的大背景下,将一个Go服务迁移至公司的基于K8s+docker的容器云平台。在迁移过程中发现服务在Docker容器内的CPU使用率异常的问题。针对此问题,进行了一些排查和优化的实践。本文将重现排查过程以及优化方案,希望能为读者提供一些参考。

问题现象

在将服务由物理机迁移至容器云计算平台过程中,发现CPU使用率远超预期。该服务是一个Go编写的消息推送服务,其业务特点是:短时间内会推送大量消息,因此该服务的负载曲线会是一个类似方波的图形。在迁移前预估业务高峰期的CPU使用率为20%,但是实际的CPU使用率远超预期,到达了70%,是预估值的3倍,如下图所示:灰度迁移中容器内CPU使用率异常图使用指令 pidstat -w 观察到线程切换次数也比较高,达到了千次/秒。

排查过程

1. 采样在确认容器节点和物理机节点的请求量负载基本一致后,开始对容器节点进行性能排查。Linux平台有很多性能分析工具像perf、systemtap等。Go的工具集非常丰富,相比于其他Linux工具,可以更加简便深入地进行分析调试。这里直接使用go tool pprof对服务进行profiling采样分析。

a) 首先在代码中开启pprof,对于具备http server的服务来说,仅需增加一行代码 import _ "net/http/pprof"

b) 访问 http://ip:port/debug/pprof/ 可以在浏览器中查看pprof采样得到的数据,使用命令行进行采样则更加方便 go tool pprof http://ip:port/debug/pprof/profile?seconds=30

命令行内查看pprof

c) 在pprof中输入指令web,即可生成一个函数调用链的CPU耗时分析,但是通常对于一个线上逻辑较为复杂的服务来说,此图并不直观,以火焰图的方式查看效果更佳

d) 在Go 1.10之后,官方的pprof工具直接支持了火焰图展示。目前由uber开源的第三方工具go-torch更为常用,这里使用了 go-torch -b ./pprof.demo.cpu.pb.gz -f demo.svg生成了火焰图如下所示:

CPU异常时的火焰图2. 分析火焰图的颜色不代表实际意义,纵轴代表代码函数调用栈,横轴代表CPU占用百分比,横轴的不同部分代码块是按照字母顺序排序。具体对火焰图进行分析:

a) 总体来看 runtime 相关代码占据了60%左右的CPU,实际的业务代码占据了40%左右的CPU

b) 由于所使用的消息队列的客户端是使用Go调用C通过 cgo机制实现的,而cgo是较为缓慢和消耗CPU的,因此这里的 runtime._ExternalCode 会占用较多的CPU,是符合正常逻辑的

c) runtime中占用CPU比例较大的是:runtime.gcBgMarkWorker, runtime.schedule, runtime.findrunnable,正常情况下不应占据如此多的CPU。

翻看Go的相关源码发现:在Go的运行模型GMP中,每个P会运行一个gcBgMarkWorker用于垃圾回收。Go SDK源码于是合理地进行假设:是否由于P的数量不正确导致GC过多,从而CPU使用率过高?Go程序在运行时,会使用查询到的CPU的数量作为默认的P的数量,简单地用一个Go脚本验证一下:

func main() {    cpu := runtime.NumCPU()    procs := runtime.GOMAXPROCS(0)    fmt.Println("cpu num:", cpu, " GOMAXPROCS:", procs)}// output -> cpu num:32 GOMAXPROCS:32

该脚本运行结果表明:在程序运行时读取到的CPU的数量是宿主机的CPU数量,而不是容器设置的CPU核心数量。3. 验证通过环境变量GOMAXPROCS可以设置Go运行时P的数量,设置环境变量GOMAXPROCS=8,这个值是容器分配的CPU核心数。灰度一台效果如下:灰度后CPU使用率对比对比未设置环境变量的节点,CPU峰值从69%下降到19%,效果非常明显。全量上线之后,CPU使用率保持了与预期值一致。

解决方案

由于Go程序本身的特性,在运行时会默认读取系统的CPU核心数作为最大的并行执行线程数。而在容器内,读取到的是宿主机的CPU核心数。在容器被分配的CPU核心数远小于宿主机的CPU核心数的情况下,就会发生CPU使用率异常升高的情况。出现问题的这个服务,其业务特点就是周期性的峰值QPS极高,所以会较为明显地观察出CPU使用率异常的现象。通过配置环境变量 GOMAXPROCS,指定最大的并行执行线程数,可以解决CPU使用率异常的问题。由于业务逻辑的不同,达到最佳性能的GOMAXPROCS也不同。《The Way to Go》曾给出过一个经验公式:GOMAXPROCS=CPU数量-1。在容器中,通常设置成申请的核心数即可。

原理分析

1. G-M-P模型Go程序的运行是使用协程的方式,在Go中协程被称之为goroutine。在操作系统看来,所有程序都是以多线程(暂不细分LWP和线程的区别)的方式运行,而线程切换(context switch)对性能的影响还是比较高的。一次线程切换一般需要1000~2000ns,而一次goroutine协程切换一般需要200ns。Go为了提高并发能力,代码中任务的执行,由运行在内核态的操作系统对线程的控制,转移为运行在用户态的Go scheduler对协程的控制,协程的调度在Go runtime中进行。Go程序与OS关系图实际的内核线程与goroutine之间的数量关系为M:N,在M个内核线程上会有N个goroutine。Go的运行模型为GMP模型:

a) G:代表goroutine

b) M:代表实际的内核线程

c) P:代表虚拟的processer

协程调度的核心思想是:

a) 重用线程,一个线程上会多次执行协程任务

b) 限制同时运行(不包含阻塞)的线程数为GOMAXPROCS,默认情况下就是 CPU 的核心数目

c) 线程私有的 runqueues,并且可以从其他线程 stealing goroutine 来运行,线程阻塞后,可以将 runqueues 传递给其他线程

在最初的设计中是没有P这个单元的,在增加了P这个逻辑processer后,并发能力大大增强。2. 协程调度原理在GMP模型下,协程调度原理如下:

a) 在程序启动时,默认地会启动CPU最大核心数量的P,同时为每个P分配一个实际的内核线程M。

b) 使用协程G来执行任务代码,包括GC等runtime代码。

c) 每一个协程G在创建之后,都会通过P来找到一个实际的内核线程M,由该M来执行G中的代码。

d) P通过队列来接收G,队列分为本地队列LocalQueue和全局队列GlobalQueue。

e) 如果某个P的本地队列无可执行的G,那么就会去全局队列里面去抽取一部分的G来执行。如果全局队列为空,那么就会随机选择其他的P,将被选中的P的本地队列中的G抽取一部分来执行。这个机制称之为work stealing。

f) 为了避免某个G占用了非常多的资源,有一个后台任务sysmon,用来检测G的运行时间。当G的运行时长超过10ms后,会被强制剥夺运行权限,将其放入全局队列之中。

g) 如果内核线程M由于发生系统调用、网络调用等产生了阻塞,为了最大地提高效率,P会暂时将M解绑,创建一个新的内核线程M或者找到其他可运行的线程,与其绑定,继续执行该P上面的待执行的G。

h) 如果某个G因为触发了GC或者atomic,mutex,channel等阻塞操作,为了避免阻塞,该G同样会被调度走,等待其处于可运行的状态后再被调度执行。

运行调度模型3.  原因探究在火焰图中runtime.gcBgMarkWorker, runtime.schedule, runtime.findrunnable 占用CPU较多:

a) 首先是GC占用过长的问题,源码中的注释写道,每个P都拥有一个GC后台协程。

如果宿主机的核心数是32,容器分配的核心数是8,那么程序运行时就会有32个P。因此就会有32个GC后台协程,那么runtime.gcBgMarkWorker就会占据很多CPU。

Go SDK源码

b) 前面提到Go的调度原理,P寻找可执行的G的顺序为:1.先检查P的本地队列 2.如果没找到,则去全局队列寻找 3.如果还没有找到,则去其他的P的本地队列里面去抽取一部分来执行。调度器的runtime.findrunnable函数就是执行的此流程。那么在宿主机核心数的数量远多于实际分配的核心数的情况下,就会有很多空闲的P需要执行runtime.findrunnable的流程,而且会出现work stealing这种现象。

c) 了解到容器主要使用CGroup的cpu.cfs_period_us和cpu.cfs_quota_us等参数来对进程使用的CPU资源进行限制的。在不进行绑核等配置的操作下,CPU资源的分配是按照使用情况而不是实际的核心来分配的。例如宿主机的核心数是32,容器分配的核心数是8,那么最多可以使用总计800%的CPU资源,这些资源会被平均分配到32个核心,每个核心占用25%的CPU。那么当达到配置的限额后,就会触发Linux的调度策略,导致线程切换增多。

问题扩展 

1. JDK对于不能正确识别容器内分配的核心数的现象,JDK同样有此问题。JDK在JDK 8u191之后开始支持使用容器分配的CPU核心数。https://www.oracle.com/technetwork/java/javase/8u191-relnotes-5032181.html.2. automaxprocs对于Go服务来说,可以通过环境变量GOMAXPROCS来设置合理的CPU核心数量。另外,Uber开源了一个自动调整GOMAXPROCS的库:https://github.com/uber-go/automaxprocs.

总结

本文对迁移服务至容器过程中,出现的CPU使用率过高的问题进行了排查和优化。通过设置环境变量GOMAXPROCS的方式,限制了GMP模型中P的数量的方式,解决了CPU使用率过高的问题。然后对Go程序的GMP模型和调度原理进行了探究,并分析了本文中CPU使用率过高问题的原因。最后,对容器内CPU数量不能正确识别这个较为普遍的问题,展示了Java和Go两种解决方案。参考文献1. [Analysis of the Go runtime scheduler] http://www.cs.columbia.edu/~aho/cs6998/reports/12-12-11_DeshpandeSponslerWeiss_GO.pdf2. [Go runtime scheduler]https://speakerdeck.com/retervision/go-runtime-scheduler?slide=143. [深度解密Go语言之scheduler]https://qcrao.com/2019/09/02/dive-into-go-scheduler/4. [CPU considerations for Java applications running in Docker and Kubernetes]https://medium.com/@christopher.batey/cpu-considerations-for-java-applications-running-in-docker-and-kubernetes-7925865235b75. [容器中某Go服务GC停顿经常超过100ms排查]https://mp.weixin.qq.com/s/Lk1EbiT7WprVOyX_dXYMyg作者简介王立明 /  云平台部-平台应用部后台开发工程师,主要负责消息推送服务以及框架组件的开发。

.net runtime占用cpu_Go服务在容器内CPU使用率异常问题排查手记相关推荐

  1. docker网络问题解决办法“大全”:宿主机访问不了docker容器中web服务,容器内访问不了外网的问题的解决办法...

    ubuntu kylin 15 安装docker后 ,启动包含web应用的容器,在宿主机上死活访问不了web服务.后来发现进入容器后,安装不了软件,访问不了外网.因此网上查原因,有大神说让重建dock ...

  2. android如何获取进程占用的内存大小,Android获取cpu使用率,剩余内存和硬盘容量

    1.内存信息 在proc/meminfo下有详细的内存使用情况,我这里获取的内存信息就是从这个文件里获取的.获取到详细的内存信息后根据我自己的需求,从bufferdreader中单独抽取出来了剩余的内 ...

  3. samba服务器占用端口,SMB 服务器上的 CPU 使用率过高问题

    SMB 服务器上的 CPU 使用率过高问题 12/25/2019 本文内容 本文介绍了如何排查 SMB 服务器上的 CPU 使用率高的问题. 由于存储性能问题导致的 CPU 使用率过高 存储性能问题可 ...

  4. 关闭Windows Update更新,解决服务主机本地系统CPU使用率高问题

    禁用 Windows 10 更新 1.找到任务管理器中的" 服务主机:本地系统"->"Windows Update",右键,进入"服务" ...

  5. mysql数据库占用太多的CPU_mysql数据库CPU使用率过高解决方案

    压测时,经常会出现mysql数据库CPU使用率过高,下面介绍一下简单的问题定位方法: 1.确定是否有慢sql语句: 1)登录数据库服务器,连接数据库:执行命令:mysql -uroot -p 密码 2 ...

  6. 容器CPU使用率过高,导致宿主机load average飙升

    早上醒来已经收到多条服务器告警信息,具体是这样的,如下图:Processor load (15 min average per core) ;服务器CPU load 过高,接下来是处理过程,记录一下. ...

  7. dubbo内置哪几种服务容器_dubbo启动服务之容器(Container)

    讲解dubbo启动服务的时候先来了解下java的spi机制 后期文章会首发于本专栏,欢迎关注 JAVA高级进阶​zhuanlan.zhihu.com 一:SPI 简介 SPI 全称为 (Service ...

  8. docker网络问题解决办法“大全”:关于宿主机访问不了docker容器中web服务,或者容器内访问不了外网的问题的解决办法

    docker网络问题解决办法"大全":关于宿主机访问不了docker容器中web服务,或者容器内访问不了外网的问题的解决办法 参考文章: (1)docker网络问题解决办法&quo ...

  9. 基于事件驱动架构构建微服务第10部分:在docker容器内运行单元测试

    原文链接:https://logcorner.com/building-microservices-through-event-driven-architecture-part11-run-unit- ...

最新文章

  1. python精要(72)-函数参数列表副本
  2. Kali 2020版 Linux操作系统解决系统语言问题(英文--中文)
  3. 电机控制系统php,电机控制系统的未来发展变化趋势
  4. python工厂模式 简书_工厂模式
  5. IDEA中Git的更新、提交、还原方法
  6. 计算机网络课堂笔记3.29
  7. Eurek自我保护机制
  8. 大数据之编程语言:Scala视频教程-陈超-专题视频课程
  9. 雪亮工程整体解决方案
  10. 小程序推广引导下载app的解决办法
  11. 软件观念革命:交互设计精髓_最全交互设计书单
  12. 开发小程序的最大优势
  13. 三四线城市咖啡店的光荣与梦想
  14. Scikit-learn 秘籍 第五章 模型后处理
  15. 怎么用js代码画一棵树,附带下载链接
  16. 沟通书籍排行榜前十名 提高沟通能力的十大书籍推荐
  17. 看好699指纹手机暴露任泉的商业野心
  18. 幼儿园不同空间翻新设计注意事项
  19. Mobicomm 2019
  20. 许久不动笔,,再来点人生感悟吧

热门文章

  1. IntelliJ IDEA 从入门到上瘾教程,2019图文版!
  2. 为什么main方法是public static void?
  3. 3D-CNN各层计算
  4. 数据结构拾遗(3) --红黑树的设计与实现(下)
  5. Python学习day02_数字类型 与 布尔类型 短路逻辑和运算符优先级
  6. hadoop--Reduce Join
  7. SqlServer过滤字段中的中文
  8. JAVA面试题:你怎么设计一个消息队列?
  9. Android2.2快速入门
  10. Spring Resource接口获取资源