gf框架之grpool – 高性能的goroutine池

Go语言中的goroutine虽然相对于系统线程来说比较轻量级,但是在高并发量下的goroutine频繁创建和销毁对于性能损耗以及GC来说压力也不小。充分将goroutine复用,减少goroutine的创建/销毁的性能损耗,这便是grpool对goroutine进行池化封装的目的。例如,针对于100W个执行任务,使用goroutine的话需要不停创建并销毁100W个goroutine,而使用grpool也许底层只需要几千个goroutine便能充分复用地执行完成所有任务。经测试,在高并发下grpool的性能比原生的goroutine高出几倍到数百倍!并且随之也极大地降低了内存使用率。

性能测试报告:http://johng.cn/grpool-performance/ Continue reading “gf框架之grpool – 高性能的goroutine池”

grpool协程池(goroutine池)性能测试

Go语言中的goroutine虽然相对于系统线程来说比较轻量级,但是在高并发量下的goroutine频繁创建和销毁对于性能损耗以及GC来说压力也不小。充分将goroutine复用,减少goroutine的创建/销毁的性能损耗,这便是grpool对goroutine进行池化封装的目的。例如,针对于100W个执行任务,使用goroutine的话需要不停创建并销毁100W个goroutine,而使用grpool也许底层只需要几千个goroutine便能充分复用地执行完成所有任务。经测试,在高并发下grpool的性能比原生的goroutine高出几倍到数百倍!并且随之也极大地降低了内存使用率。 Continue reading “grpool协程池(goroutine池)性能测试”

Go学习整理笔记

最近整理一下Go学习中的一些要点,包括Go性能优化及底层源码要点,作此笔记,巩固基础。

一、并发

1、Go语言的goroutine类似于线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力;

2、通常情况下,用多进程来实现分布式负载均衡,减轻单进程垃圾回收压力;用多线程(LWP)抢夺更多的处理器资源;用协程来提高处理器时间片利用率;

3、相比较系统默认MB级别的线程栈,goroutine自定义栈初始仅需2KB,所以才能创建成千上万的并发任务。自定义栈采用按需分配策略,在需要时进行扩容,最大能到GB规模;

4、Go在运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该量默认与处理器核数相等,可用runtime.GOMAXPROCS函数或者环境变量修改;

5、通道:

         1)、通道(channel)相当于一个并发安全的队列;

         2)、goroutine leak是指goroutine处于发送或者接受阻塞状态,但一直未被唤醒,垃圾回收器并不收集此类资源,导致它们会在队列里长期休眠,形成资源泄露;

6、同步:

         1)、通道并不是用来取代锁的,它们有各自不同的使用场景。通道倾向于解决逻辑层次的并发处理架构,而锁则用来保护局部范围的数据安全;

         2)、将Mutex作为匿名字段时,相关方法必须实现为pointer-receiver模式,否则会因为复制导致锁机制失效;

         3)、应将Mutex锁粒度控制在最小范围内,及早释放;

7、建议:

         1)、对性能要求较高时,应避免使用defer Unlock;

         2)、读写并发时,用RWMutex性能会更好一些;

         3)、对单个数据读写保护,可尝试用原子操作;

         4)、执行严格测试,尽可能打开数据竞争检查;

Continue reading “Go学习整理笔记”

GO性能优化小结

1 内存优化

1.1 小对象合并成结构体一次分配,减少内存分配次数

做过C/C++的同学可能知道,小对象在堆上频繁地申请释放,会造成内存碎片(有的叫空洞),导致分配大的对象时无法申请到连续的内存空间,一般建议是采用内存池。Go runtime底层也采用内存池,但每个span大小为4k,同时维护一个cache。cache有一个0到n的list数组,list数组的每个单元挂载的是一个链表,链表的每个节点就是一块可用的内存,同一链表中的所有节点内存块都是大小相等的;但是不同链表的内存大小是不等的,也就是说list数组的一个单元存储的是一类固定大小的内存块,不同单元里存储的内存块大小是不等的。这就说明cache缓存的是不同类大小的内存对象,当然想申请的内存大小最接近于哪类缓存内存块时,就分配哪类内存块。当cache不够再向spanalloc中分配。

建议:小对象合并成结构体一次分配,示意如下:

替换为:

Continue reading “GO性能优化小结”

Go测试之性能监控与代码覆盖率

1、测试运行记录

在硬件环境方面,主要考察计算机的负载状况,比如CPU使用率内存使用率磁盘使用情况等。
在软件系统方面,主要包括内存分配并发处理数量死锁等情况

(1).收集资源使用情况

与收集资源使用情况有关的标记

标记名称 标记描述
-cpuprofile cpu.out 记录CPU使用情况,并写到指定的文件中,直到测试退出。
cpu.out作为指定文件的文件名可以被其他任何名称代替
-memprofile mem.out 记录内存使用情况,并在所有测试通过后将内存使用概要写到指定的文件中。
mem.out作为指定文件的文件名可以被其他任何名称代替
-memprofilerate n 此标记控制着记录内存分配操作的行为,这些记录将会被写到内存使用概要文件中。
N代表着分析器的取样间隔,单位为字节。每当有n个字节的内存被分配时,分析器就会取样一次

Continue reading “Go测试之性能监控与代码覆盖率”

Go性能优化:接口性能

接口的用途无需多言。但这并不意味着可在任何场合使用接口,要知道通过接口调用和普通调用存在很大差别。首先,相比静态绑定,动态绑定性能要差很多;其次,运行期需额外开销,比如接口会复制对象,哪怕仅是个指针,也会在堆上增加一个需 GC 处理的目标。




Continue reading “Go性能优化:接口性能”

Go性能优化:map使用注意事项

内置 map 类型是必须的。首先,该类型使用频率很高;其次,可借助 runtime 实现深层次优化(比如说字符串转换,以及 GC 扫描等)。可尽管如此,也不意味着万事大吉,依旧有很多需特别注意的地方。

1、预设容量

map 会按需扩张,但须付出数据拷贝和重新哈希成本。如有可能,应尽可能预设足够容量空间,避免此类行为发生。

从结果看,预设容量的 map 显然性能更好,更极大减少了堆内存分配次数。

 

Continue reading “Go性能优化:map使用注意事项”

Go性能优化:string与[ ]byte转换

字符串(string)作为一种不可变类型,在与字节数组(slice, [ ]byte)转换时需付出 “沉重” 代价,根本原因是对底层字节数组的复制。这种代价会在以万为单位的高并发压力下迅速放大,所以对它的优化常变成 “必须” 行为。

首先,须了解 string 和 [ ]byte 数据结构,并确认默认方式的复制行为。

动态演示: https://asciinema.org/a/6up6gvgqo0v9zkjpusvyucg8g

Continue reading “Go性能优化:string与[ ]byte转换”

Go性能监控/分析工具:go tool pprof

我们可以使用go tool pprof命令来交互式的访问概要文件的内容。命令将会分析指定的概要文件,并会根据我们的要求为我们提供高可读性的输出信息。

在Go语言中,我们可以通过标准库的代码包runtimeruntime/pprof中的程序来生成三种包含实时性数据的概要文件,分别是CPU概要文件、内存概要文件和程序阻塞概要文件。下面我们先来分别介绍用于生成这三种概要文件的API的用法。

CPU概要文件

在介绍CPU概要文件的生成方法之前,我们先来简单了解一下CPU主频。CPU的主频,即CPU内核工作的时钟频率(CPU Clock Speed)。CPU的主频的基本单位是赫兹(Hz),但更多的是以兆赫兹(MHz)或吉赫兹(GHz)为单位。时钟频率的倒数即为时钟周期。时钟周期的基本单位为秒(s),但更多的是以毫秒(ms)、微妙(us)或纳秒(ns)为单位。在一个时钟周期内,CPU执行一条运算指令。也就是说,在1000 Hz的CPU主频下,每1毫秒可以执行一条CPU运算指令。在1 MHz的CPU主频下,每1微妙可以执行一条CPU运算指令。而在1 GHz的CPU主频下,每1纳秒可以执行一条CPU运算指令。

在默认情况下,Go语言的运行时系统会以100 Hz的的频率对CPU使用情况进行取样。也就是说每秒取样100次,即每10毫秒会取样一次。为什么使用这个频率呢?因为100 Hz既足够产生有用的数据,又不至于让系统产生停顿。并且100这个数上也很容易做换算,比如把总取样计数换算为每秒的取样数。实际上,这里所说的对CPU使用情况的取样就是对当前的Goroutine的堆栈上的程序计数器的取样。由此,我们就可以从样本记录中分析出哪些代码是计算时间最长或者说最耗CPU资源的部分了。我们可以通过以下代码启动对CPU使用情况的记录。

在函数startCPUProfile中,我们首先创建了一个用于存放CPU使用情况记录的文件。这个文件就是CPU概要文件,其绝对路径由*cpuProfile的值表示。然后,我们把这个文件的实例作为参数传入到函数pprof.StartCPUProfile中。如果此函数没有返回错误,就说明记录操作已经开始。需要注意的是,只有CPU概要文件的绝对路径有效时此函数才会开启记录操作。

如果我们想要在某一时刻停止CPU使用情况记录操作,就需要调用下面这个函数:

在这个函数中,并没有代码用于CPU概要文件写入操作。实际上,在启动CPU使用情况记录操作之后,运行时系统就会以每秒100次的频率将取样数据写入到CPU概要文件中。pprof.StopCPUProfile函数通过把CPU使用情况取样的频率设置为0来停止取样操作。并且,只有当所有CPU使用情况记录都被写入到CPU概要文件之后,pprof.StopCPUProfile函数才会退出。从而保证了CPU概要文件的完整性。 Continue reading “Go性能监控/分析工具:go tool pprof”

Go语言中需要注意结构体方法副本传参与指针传参的区别

我们来看个例子:

执行后结果如下:

可以看到Test1中打印出b结构体的地址在变化,而Test2中没有变化,这说明每一次Test1的调用,都是传入的结构体b的一个副本(拷贝),当在Test1中对内部变量的任何改动,都将会失效(因为下一次访问的时候传入的是b结构体新的副本)。而Test2方法作为指针传参时,每一次传入的都是b结构体的指针,指向的是同一个结构体,因此地址没有变化,且对内部变量做改动时,都是改动的b结构体内容。

在Go语言中的这个差别可能是对OOP设计的一个坑,在Go语言中要想实现OOP的设计,在进行方法封装时,都采用Test2的写法。 Continue reading “Go语言中需要注意结构体方法副本传参与指针传参的区别”