文档章节

谁偷了1/3个CPU - 诡异Go性能问题追根问底

n
 nilei
发布于 01/23 00:10
字数 1636
阅读 463
收藏 8

看到过不少文章介绍自己CPU占用恶高甚至接近100%,其实那到反而清楚无遗漏了,无非哪个busy loop卡住了。这里为大家描述一个近期遇到的Go程序在空闲时候依旧在top命令里总报告30%左右CPU占用的问题,这样的性能问题更隐蔽更难琢磨。

问题发生在我自己做的高性能多组Raft库Dragonboat里,这是一个Apache2开源的Go实现的多组Raft库,它的主打就是吞吐性能吊打竟品几十倍。因为性能是核心卖点,因此每个函数的CPU耗费都了如指掌,直到有一天突然发现系统空载的时候占用一个CPU核的30%的负载,如下图:

服务器程序空闲的时候,top中看到的cpu负载通常应该是个位数低位。深入分析以后发现是一个较深的Go调度实现的问题。

观察

看到上述top的结果,strace -c 看了一下,很多futex。多启动几个这样的空闲进程,抓个火焰图看,它是这样的:

必须得假设您对Go近期版本(如1.8-1.11)的调度有一定基本了解,了解M、P、G三者的意义和作用。如暂时不了解,可参考本文或者该文的中文翻译

火焰图中可以观察到几点:

  • 让当前M去sleep的操作挺重的,它由tickWorkerMain试图去读一个channel引发
  • runtime.futex的数据和上面提到的strace所报告的情况吻合

查看代码,Dragonboat库tickWorkerMain中有一个1khz的ticker,等于每秒读ticker.C这个channel 1000次。当时第一感觉是有些疑惑,因为常识告诉我,一个简单的非严格1khz的低频ticker,在3Ghz左右的服务器处理器上,cpu占用应该在1-2%这样极低的占用才合理。只是希望程序某个函数被一秒调用1000次,怎么就占用掉近30%的cpu?

先不管成因,孤立出这个问题点来看看。为了实现“程序某个函数被一秒调用1000次”这点,在Go中用ticker可以这么做:

package main

import (
    "time"
)

func main() {
    ticker := time.NewTicker(time.Millisecond)
    defer ticker.Stop()
    for range ticker.C {
    }
}

结果上述程序top报告25%的%CPU值。是Go的ticker实现的问题吗?换sleep循环看看:

package main

import (
    "time"
)

func main() {
    for {
        time.Sleep(time.Millisecond)
    }
}

运行上述time.Sleep程序,top报告15%的%CPU。这和同样一个sleep循环的C++在1%的%CPU有天壤之别。于是开始倾向于是Go的调度器的锅。

分析

再回到上述火焰图,park_m()后一连串操作很显眼。我们已经知道M表示Machine,通常认为是一个OS Thread,park_m()后续stopm()顾名思义就是这个把当前M给停用掉,告诉系统这个M暂时不用。

一切似乎开始明朗了。每次tickWorkerMain开始等下一个tick的时候,也就是去读ticker.C这个channel的时候都因为时间没到channel为空,当前goroutine会被park掉,这个操作很轻,只需要更改一个标志。而此时因为系统空闲,并没有别的goroutine可供调度,Go的scheduler就必须让这个M去sleep,而这个操作是较重的且有锁,最终futex的syscall被调用。更具体的,这还和Go的后台timer实现、system monitor实现有关(注意火焰图中runtime.sysmon),这里不展开。人人都会告诉你协程调度是很轻的操作,这当然没错。但他们都没有告诉你更重要的一点:协程调度反复高频出现没有goroutine可供调度的代价在Go的当前实现里是显著的。

必须指出,本问题是因为系统空闲没有goroutine可以调度造成的。显然的,系统繁忙的时候,即CPU资源真正体现价值时,上述30%的%CPU的overhead并不存在,因为大概率下会有goroutine可供调度,无需去做让M去sleep这个很重的操作。

而这一问题的影响是具体客观的:

  • 用户会反复提问为何系统空闲时占30%的%CPU,空闲时进程在top里始终排顶部不是个好事
  • 在一个慢速的用电池的ARM核上有这东西就麻烦了

cgo解决

我们也已经知道,C/C++做同样的事代价很轻。如果从不改业务逻辑出发,首先想到的就是不让M去sleep,不发生无goroutine可调度的情况。比如,可以用一个OS线程通过cgo独立于scheduler地去产生这个1kHz的tick,每秒从C代码去调用1000次所需的Go函数。这个思路很容易实现,无非是从Go代码里调用一个C函数,这个C函数每秒1000次的从sleep中醒来去调用Go里的1khz的tick的处理函数,具体就不贴具体代码了。

用这个思路修改了Dragonboat的代码一跑,空闲时的cpu负载大幅降低:

 

结果

上述workaround已经让这一问题对自己软件的影响极大降低了。去golang-nuts吐槽一下,再报golang的issue tracker,根本的问题还是Go Scheduler的实现。用户用1kHz的ticker不应该是这样大费周章,标准库、runtime上直接提供更高效的实现才是真正解决方案。

 

Dragonboat的开发中,这样的performance regression几乎每周都发生。从一秒十万次吞吐到一秒一千万吞吐的进化,是算法协议不断理解的深入,也是对Go runtime习性的不断熟悉的一个过程。后面陆续会风向大量这样的性能优化实践知识,均以目前互联网后台最热门Golang为语言,素材均为任何应用均会涉及的通用场景。作为最好的教材,欢迎大家试用Dragonboat,也请大家点Star支持它的持续开发。

 

© 著作权归作者所有

共有 人打赏支持
n
粉丝 30
博文 6
码字总数 11560
作品 1
私信 提问
加载中

评论(2)

n
nilei
这个问题在golang的issue里报了,无数用户在不同系统上确认了。已经没有任何疑问了。
mickelfeng
mickelfeng
windows上测试没出现占用cpu的情况,在linux下出现

1006 % time seconds usecs/call calls errors syscall
1007 ------ ----------- ----------- --------- --------- ----------------
1008 99.99 0.140587 10 14501 2969 futex
1009 0.01 0.000020 20 1 1 restart_syscall
1010 ------ ----------- ----------- --------- --------- ----------------
记一次服务器被挖矿经历与解决办法

记一次服务器被挖矿经历与解决办法 在最近的某一天里面,中午的一个小息过后,突然手机的邮件和公众号监控zabbix的告警多了起来。我拿起手机一看原来是某台服务器上的CPU跑满了,就开始登上去...

legehappy
2018/07/31
0
0
关于Linux系统指令 top 之 %wa 占用高,用`iostat`探个究竟

最近测试一项目,性能非常不理想。老版本逻辑和功能都简单时,性能是相当的好!接口点击率是万级的。谁知修改后上不了百。 架设Jboss服务器,业务逻辑用Java处理,核心模块使用C++处理,使用...

云栖希望。
2017/12/10
0
0
IE6下的几大灵异事件(欢迎补充)

虽说IE6各种诡异各种让人不爽,但面对那些坚持使用IE6的顽固分子,问题还得解决。 收集了几个经常会碰到的IE6下特别诡异的现象及解决办法,欢迎大家补充! 1. z-index无效 设置其父的z-index...

曾沙
2012/12/18
836
13
性能压测诡异的Requests/second 响应刺尖问题

最近一段时间都在忙着转java项目最后的冲刺,前期的coding翻代码、debug、fixbug都逐渐收尾,进入上线前的性能压测。 虽然不是大促前的性能压测要求,但是为了安全起见,需要摸个底心里有个数...

王清培
2017/09/23
0
0
性能压测诡异的Requests/second 响应刺尖问题

最近一段时间都在忙着转java项目最后的冲刺,前期的coding翻代码、debug、fixbug都逐渐收尾,进入上线前的性能压测。 虽然不是大促前的性能压测要求,但是为了安全起见,需要摸个底心里有个数...

王清培
2017/09/23
0
0

没有更多内容

加载失败,请刷新页面

加载更多

html5代码书写规范

DOCTYPE 页面文档类型统一使用HTML5 DOCTYPE. <!DOCTYPE html> Meta字符集设置 声明方法遵循HTML5的规范, Meta文件使用 "UTF-8" 浏览器显示编码指定. <meta charset="utf-8"> 手机端页面添......

niuhongxia
29分钟前
3
0
怎么修改 phpstorm 中注释的开始位置

PHPStorm 版本:v2018.3 如下图设置:

whoru
37分钟前
2
0
Android Arcface人脸识别sdk使用工具类

public class FaceUtil{ private static final String TAG = FaceUtil.class.getSimpleName(); private static FaceUtil faceInstance = null; public FaceDB mFaceDB; pri......

是哇兴哥棒棒哒
45分钟前
2
0
JFreeChart中文API和树形详解

-------------------------------- JfreeChart 中文API -------------------------------- 要想绘制出漂亮的图表,就必须了解图表的构成部分,将图表进行分解成N个部分。 然后再对每一个部分...

喜欢搬砖的农民工
47分钟前
2
0
Android ViewPager

1.PagerAdapter { public int getCount() { return list.size(); } public Object instantiateItem(ViewGroup container, int postion) { container.addView(iv); return iv; } public void ......

Coding缘
49分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部