golang 核心开发者 Dmitry Vyukov(1.1 调度器作者) 关于性能剖析

让我们假设你有一golang 程序,想改善其性能。有几种工具可以帮我们完成这个任务。这些工具可以帮我们识别程序中的热点(cpu,io,memory), 热点即是那些需要我们集中精力于其上,能显著改善改善性能的地方。然而,另外一种结果也是可能的,工具帮我们识别出程序里的多种性能缺陷。比如,每次查询数据库,你都准备sql 语句,然而,你可以在程序启动时,只准备一次。另一个例子,一个O(n^2)的算法莫名其妙的溜进,某些存在O(n) 算法的地方。为了识别出这些情况,你需要合理检查程序剖析所看到的结果。比如第一个例子中,有显著的时间花费在sql 语句准备阶段,这就是一个危险的信号。

了解多种关于性能的边界因素也是重要的。比如,你的程序使用100Mbps网络链路通信,它已经使用了链路90Mbps以上的带宽,那你的程序就没有太多性能改善空间。对于磁盘io,内存消耗,计算性任务也存在类似的情况。这些情况,铭记在心,接着我们可以查看可用的工具。

注意:工具之间会相互干扰,比如,精准的内存剖析会偏差cpu剖析,goroutine阻塞剖析影响调度器的追踪等,隔离地使用工具能够获得更精确的信息。以下阐述基于golang 1.3。

cpu 剖析器

go运行时内置cpu profiler,它可以显示哪些函数消耗了多少的百分比cpu时间,有三种方式可以访问它:

1.最简单的是go test 命令-cpuprofile标志,例如,以下命令:

$ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http

剖析给定的benchmark,把cpu profile 信息写入‘cprof‘ 文件

接着:

$ go tool pprof --text http.test cprof

打印最热点的函数列表,有几种可用输出格式,最有用的几个:--text,--web,--list.运行 ‘go tool pprof‘ 获取完整列表

2.net/http/pprof 包,这个方案对于网络服务应用十分理想,你仅需导入net/http/pprof,就可使用下面命令profile:

go tool pprof --text mybin http://myserver:6060:/debug/pprof/profile

3.手动profile 采集,需要引入runtime/pprof,在main函数添加下述代码:

if *flagCpuprofile != "" {
   f, err := os.Create(*flagCpuprofile)
   if err != nil {
      log.Fatal(err)
   }
   pprof.StartCPUProfile(f)
   defer pprof.StopCPUProfile()
}

剖析信息会写入指定的文件,与第一个选项同样方式可视化它。这里有一个使用--web 选项可视化的例子:

你可以使用--list=funcname 选项查阅单个函数,列如以下profile 显示时间append 函数时间花费:

.      .   93: func (bp *buffer) WriteRune(r rune) error {
.      .   94:     if r < utf8.RuneSelf {
5      5   95:         *bp = append(*bp, byte(r))
.      .   96:         return nil
.      .   97:     }
.      .   98: 
.      .   99:     b := *bp
.      .  100:     n := len(b)
.      .  101:     for n+utf8.UTFMax > cap(b) {
.      .  102:         b = append(b, 0)
.      .  103:     }
.      .  104:     w := utf8.EncodeRune(b[n:n+utf8.UTFMax], r)
.      .  105:     *bp = b[:n+w]
.      .  106:     return nil
.      .  107: }

当剖析器不能解开调用栈时,使用三个特殊条目使用:GC,System,ExternalCode。GC 表示花费在垃圾回收的时间;System表示花费在goroutine 调度器,栈管理及其他辅助运行时代码的时间;ExternalCode,表示花费在调用本地动态库时间。这里有一些关于如何解释profile结果的提示:

如果你看到大量时间花费在runtime.mallocgc 函数,程序可能做了过多的小内存分配。profile能告诉你这些分配来自哪里。

如果大量时间花费在channel,sync.Mutex,其他的同步原语,或者系统组件,程序可能遭受资源争用。考虑以下从新组织代码结构,消除最常访问的共享资源。常用的技术包括分片/分区, 局部缓冲/聚集, 写时拷贝等。

如果大量时间花费在syscall.Read/Write, 程序可能产生了太多小数据量读写操作,可考虑使用bufio 包裹os.File or net.Conn。

如果大量时间花费在GC 组件,要么是程序分配了太多的临时对象,要么是堆内存太小,导致垃圾收集操作运行频繁。

注意:在当前的darwin 平台,cpu profiler 不能正确工作  darwin 不能正常工作;window 平台需要安装cygwin,perl,graphviz,用来生成svg/web profile;在linux平台,你也可使用perf system profiler,它不能解开go stack,但可剖析解开cgo/swig 代码和内核代码。

memory profiler

内存剖析器显示哪些函数分配堆内存,你可以采集它,使用’go test  --memprofile‘, 或者net/http/ppro经由  http://myserver:6060:/debug/pprof/heap 或者调用  runtime/pprof.WriteHeapProfile

你可以仅仅可视化profile 收集过程中的活跃分配(传递 --inuse_space 标志,默认),或者自程序启动以来的所有分配(--alloc_space )。

你可以显示分配了多少字节或者多少个对象(--inuse/alloc_space or --inuse/alloc_objects )。多次剖析过程中,profiler 趋向于采样更大的对象。了解大对象影响内存消耗和gc 时间,大量小分配影响执行速度也在某种程度影响gc时间。对象可以是持久或临时的生命周期。如果在程序启动时,你有几个大的持久对象分配,它们很可能被采集到。这些对象影响内存消耗和gc时间,但不影响正常的执行速度。另一方面,如果你有大量短生命周期对象,那么profile过程中,它们几乎不能被呈现。但它们对执行速度有显著影响,因为它们被分配释放很频繁。

一般情况是,如果你要减少内存消耗,考虑使用--inuse_space 选项的profile;如果是想要改善执行速度,使用--alloc_objects 选项profile。有几个选项可以用来控制报告的粒度,--function函数级别(默认),--lines,--files,--adrresses,行级,文件级,指令地址级。

优化通常是应用特定的,以下是一些常用建议:

1.合并对象进入更大的对象。例如,使用bytes.Buffer替代*bytes.Buffer,作为结构体成员,这个能减少内存分配次数,减轻gc压力。

2.那些脱离它们声明作用域的局部变量,被提升的堆内存分配。编译器通常不能判定几个这样变量有相同的生命周期,所以要单独分配它们。可以把下面代码:

for k, v := range m {
   k, v := k, v   // copy for capturing by the goroutine
   go func() {
       // use k and v
   }()
}

替换为:

for k, v := range m {
   x := struct{ k, v string }{k, v}   // copy for capturing by the goroutine
   go func() {
       // use x.k and x.v
   }()
}

使用一次分配代替两次,但对代码可读性有消极影响,所以适度使用。

3.一个合并分配的特例,如果你了解使用中slice典型大小,slice的底层数组可以使用预分配。

type X struct {
    buf      []byte
    bufArray [16]byte // Buf usually does not grow beyond 16 bytes.
}

func MakeX() *X {
    x := &X{}
    // Preinitialize buf with the backing array.
    x.buf = x.bufArray[:0]
    return x
}

4.使用占用空间小的数据类型, 比如,使用 int8 替代 int。

5.那些不包含任何指针的对象(注意:string,slice,map,chan 包含隐式指针),不会被垃圾收集器扫描。比如 1Gb byte slice 不会影响gc time,从活跃使用的对象中移除指针,能对gc time 产生正面影响。一些可能方式:使用索引替代指针,分割对象成两部分,其中一部分没有任何指针。

6.使用freelist 重用临时对象,减少分配次数。标准库包含的sync.Pool 类型允许在gc 之间多次重用同一个对象。不过要意识到,就像任何手动内存管理模式一样,不正确地使用sync.Pool 可以导致 use-after-free bug。

Blocking  Profile 

goroutine 阻塞 profiler展示goroutine 阻塞等待同步原语(包括定时器channel),出现在代码中哪些地方。你可以使用‘go test --blockprofile‘,  net/http/pprof 经由 http://myserver:6060:/debug/pprof/block,或者调用    runtime/pprof.Lookup("block").WriteTo

阻塞剖析器默认没有被开启,’go test --blockprofile‘ 会为你自动开启,但是使用net/http/pprof, runtime/pprof,需要你手动开启。调用runtime.SetBlockProfileRate,开启阻塞剖析器,SetBlockProfileRate 控制blocking profile 中阻塞时间的报告粒度。

如果一个函数包含多个阻塞操作,了解到那个操作导致阻塞,就变得不太明晰,如此可以使用--lines 标志辨别。

注意并不是所有的阻塞都是不好的。当一个goroutine阻塞,底层工作线程会切换到另一个goroutine。这样以来,协作的go环境的阻塞与非协作系统互斥器上的阻塞,就有着显著差异。(c++, java 线程库里,阻塞会导致线程空闲,和昂贵的线程上下文切换)

在time.Ticker 上阻塞通常没什么问题。如果一个goroutine在一个ticker上阻塞10 s,阻塞剖析中也会看到10s阻塞。在sync.WaitGroup 上阻塞,大多也没什么问题。例如,一个任务花费10s,goroutine在waitgroup等待,记账10s。在sync.Cond 上阻塞是好是坏,取决于具体情况。消费者阻塞在channel 上,暗示生产者的慢速,或者缺少可以做的工作。生产者阻塞在channel 上,暗示消费者的慢速,通常这也不是个问题。阻塞于channel基于的信号量,显示有多少goroutine被卡在信号量上。阻塞于sync.Mutex, sync,RWMutex, 一般不太好。你可以使用--ignore  排除那些不感兴趣的阻塞事件。

goroutine的阻塞产生两种消极后果:

1. 程序不能与处理器线性比例伸缩。

2. 过多的goroutine阻塞与解阻塞,消耗太多cpu时间。

以下是一些提示帮助减少goroutine阻塞:

1. 在匹配生产者,消费者模型代码中,使用充足缓冲的 buffer channel,无缓冲channel实质上限制了程序的并行度。

2. 在有很多读取操作, 很少修改数据操作的场景,使用sync.RWMutex 代替 sync.Mutex。读者之间不会相互阻塞。

3. 某些场景甚至可以通过使用写时拷贝技术完全移除mutex。如果被保护的数据结构修改的 不太频繁,制造一份拷贝是可行的:

type Config struct {

    Routes   map[string]net.Addr
    Backends []net.Addr
}

var config unsafe.Pointer  // actual type is *Config

// Worker goroutines use this function to obtain the current config.
func CurrentConfig() *Config {
    return (*Config)(atomic.LoadPointer(&config))
}

// Background goroutine periodically creates a new Config object
// as sets it as current using this function.
func UpdateConfig(cfg *Config) {
    atomic.StorePointer(&config, unsafe.Pointer(cfg))
}

这个模式防止写者在更新操作时阻塞了读者的活动。

4.分区是另外一种常用的在易变数据结构上减少争用/阻塞的技术。下面是一个怎么分区一个hashmap 的示例:

type Partition struct {
    sync.RWMutex
    m map[string]string
}

const partCount = 64
var m [partCount]Partition

func Find(k string) string {
    idx := hash(k) % partCount
    part := &m[idx]
    part.RLock()
    v := part.m[k]
    part.RUnlock()
    return v
}

5. 局部缓冲和批量更新可以帮助减少不可分区的数据结构上争用。

const CacheSize = 16

type Cache struct {
    buf [CacheSize]int
    pos int
}

func Send(c chan [CacheSize]int, cache *Cache, value int) {
    cache.buf[cache.pos] = value
    cache.pos++
    if cache.pos == CacheSize {
        c <- cache.buf
        cache.pos = 0
    }
}

这个技术并不限于channel上,可以用在批量更新map, 批量分配内存。

6. 使用sync.Pool 的freelist ,而非基于channel,或mutex 保护的 freelist。sync.Pool 内部用了一些聪明技术减少阻塞。

Goroutine Profiler

goroutine 剖析器仅是让你看到进程所有活跃goroutine的当前堆栈,这个对于调试负载均衡及死锁问题,十分方便。

goroutine profile 仅对运行中的应用程序,显得合理,所以go test 命令做不到这点。你可以使用 net/http/pprof 经由 http://myserver:6060:/debug/pprof/goroutine 或者调用  runtime/pprof.Lookup("goroutine").WriteTo 。但是最有用的方法是在浏览器里键入 http://myserver:6060:/debug/pprof/goroutine?debug=2, 你会看到类似程序崩溃时的堆栈追踪。注意显示 “syscall" 状态的goroutine 消费os 线程,其他goroutine不会。”io wait“ 状态的goroutine 也不消费os 线程,它们停靠在非阻塞的网络轮询器上。

时间: 2024-12-13 22:26:44

golang 核心开发者 Dmitry Vyukov(1.1 调度器作者) 关于性能剖析的相关文章

Linux内核架构读书笔记 - 2.5.4 核心调度器

什么是核心调度器? 参考前面的博文http://www.cnblogs.com/songbingyu/p/3696414.html 1 周期性调度器 作用: 管理内核中与整个系统和各个进程的调度相关的统计量 负责当前调度类的周期性调度方法 kernel/sched.c 1 /* 2 * This function gets called by the timer code, with HZ frequency. 3 * We call it with interrupts disabled. 4

如何调试PHP的Core之获取基本信息 --------风雪之隅 PHP7核心开发者

http://www.laruence.com/2011/06/23/2057.html https://github.com/laruence PHP开发组成员, Zend兼职顾问, PHP7核心开发者, Yaf, Yar, Yac等项目作者

Linux核心调度器之周期性调度器scheduler_tick--Linux进程的管理与调度(十八)

日期 内核版本 架构 作者 GitHub CSDN 2016-6-29 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 我们前面提到linux有两种方法激活调度器:核心调度器和 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 因而内核提供了两个调度器主调度器,周期性调度器,分别实现如上工作, 两者合在一起就组成了核心调度器(core schedu

Linux进程核心调度器之主调度器schedule--Linux进程的管理与调度(十九)【转】

转自:http://blog.csdn.net/gatieme/article/details/51872594 日期 内核版本 架构 作者 GitHub CSDN 2016-06-30 Linux-4.6 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度 我们前面提到linux有两种方法激活调度器:核心调度器和 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 因而内

图解kubernetes调度器ScheduleAlgorithm核心实现学习框架设计

ScheduleAlgorithm是一个接口负责为pod选择一个合适的node节点,本节主要解析如何实现一个可扩展.可配置的通用算法框架来实现通用调度,如何进行算法的统一注册和构建,如何进行metadata和调度流程上下文数据的传递 1. 设计思考 1.1 调度设计 1.1.1 调度与抢占 当接收到pod需要被调度后,默认首先调用schedule来进行正常的业务调度尝试从当前集群中选择一个合适的node 如果调度失败则尝试抢占调度,根据优先级抢占低优先级的pod运行高优先级pod 1.1.2 调

【转】Go调度器原理浅析

goroutine是golang的一大特色,或者可以说是最大的特色吧(据我了解),这篇文章主要翻译自Morsing的[这篇博客](http://morsmachine.dk/go-scheduler),我读这篇文章的时候不只是赞叹调度器设计的精巧,而且被Unix内核设计思想的影响和辐射所震撼,感觉好多好东西都带着它的影子. 绪论(Introduction)---------------------Go 1.1最大的特色之一就是这个新的调度器,由Dmitry Vyukov贡献.新调度器让并行的Go

[翻译]Go语言调度器

Go语言调度器 译序 本文翻译 Daniel Morsing 的博文 The Go scheduler,个人觉得这篇文章把Go Routine和调度器的知识讲的浅显易懂,作为一篇介绍性的文章,很不错. 译文 介绍 Go 1.1版本最大的特性之一就是一个新的调度器,由Dmitry Vyukov贡献.这个新的调度器为并行Go程序带来了令人激动.无以后继的性能提升,我觉得我应该为之写点什么东西. 这篇博客的大部分内容都已经在这篇原始设计文档中描述过了,这是一份相当好理解的文章,但是略显技术性. 虽然该

Linux 调度器发展简述

引言 进程调度是操作系统的核心功能.调度器只是是调度过程中的一部分,进程调度是非常复杂的过程,需要多个系统协同工作完成.本文所关注的仅为调度器,它的主要工作是在所有 RUNNING 进程中选择最合适的一个.作为一个通用操作系统,Linux 调度器将进程分为三类: 交互式进程 此类进程有大量的人机交互,因此进程不断地处于睡眠状态,等待用户输入.典型的应用比如编辑器 vi.此类进程对系统响应时间要求比较高,否则用户会感觉系统反应迟缓. 批处理进程 此类进程不需要人机交互,在后台运行,需要占用大量的系

Go调度器介绍和容易忽视的问题

本文记录了本人对Golang调度器的理解和跟踪调度器的方法,特别是一个容易忽略的goroutine执行顺序问题,看了很多篇Golang调度器的文章都没提到这个点,分享出来一起学习,欢迎交流指正. 什么是调度器 为了方便刚接触操作系统和高级语言的同学,先用大白话介绍下什么是调度器. 调度,是将多个程序合理的安排到有限的CPU上来使得每个程序都能够得以执行,实现宏观的并发执行.比如我们的电脑CPU只有四核甚至双核,可是我们却可以在电脑上同时运行几十个程序,这就是操作系统调度器的功劳.但操作系统调度的