Go学习整理笔记

john · · 2030 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

最近整理一下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)、执行严格测试,尽可能打开数据竞争检查;

二、测试/监控/性能调优

Go语言除了高性能、跨平台、[模块化的设计思想、大道至简的设计原则]、语言级的并发支持等优点以外,还有一项比较突出的优点,那就是提供了丰富的工具链,为代码测试、性能监控及调优提供了很好的支持。

1、测试

    1)、可测试性也是代码质量的一个体现;

    2)、单元测试可通过测试结果为代码审查(code review)提供筛选依据,避免因烦琐导致代码审查沦为形式主义;

    3)、代码覆盖率可通过例如:go test -cover -covermode count -coverprofile cover.out 命令来实现,并且可以在浏览器上查看结果;

    4)、通过编写测试代码,使用命令:go test -bench . 进行基准测试,可以有针对性地测试出模块某部分的性能瓶颈;

2、监控

    1)、出现性能瓶颈的地方,往往都是可预知的,可以有针对性地写一些基准测试代码,使用相应的工具进行分析检测;

    2)、通过编写测试代码,使用命令(通过man go-testflag查看更多参数):

    go test -run NONE -bench . -memprofile mem.out -cpuprofile cpu.out -blockprofile block.out net/http

    来测试并监控性能指标(CPU&MEM&BLOCK),可以有针对性地测试出某些模块或者方法的性能瓶颈.

    注意,这里生成profile性能监控结果文件应当使用 go tool pprof 文件名 来打开查看;

    3)、使用Go自带的性能监控工具 – pprof(由Perl语言编写的):

        a、runtime/pprof :引入后调用在代码中使用相关runtime命令进行局部代码监控,并输出监控结果,通过go tool pprof命令进行查看;

        b、net/http/pprof:对runtime/pprof的HTTP封装, 通过 import _ “net/http/pprof” 引入即可;

    4)、GC监控:运行时加上环境变量GODEBUG gctrace=1

3、其他工具:

    go-torch       – 功能类似于pprof,但是生成更直观的性能火炬图

    goreporter     – 生成Go代码质量评估报告

    dingo-hunter   – 用于在Go程序中找出deadlocks的静态分析器

    flen           – 在Go程序包中获取函数长度信息

    go/ast         – Package ast声明了关于Go程序包用于表示语法树的类型

    gocyclo        – 在Go源代码中测算cyclomatic函数复杂性

    Go Meta Linter – 同时Go lint工具且工具的输出标准化

    go vet         – 检测Go源代码并报告可疑的构造

    ineffassign    – 在Go代码中检测无效赋值

    safesql        – Golang静态分析工具,防止SQL注入

4、其他一些设计/编码时的优化:

    http://johng.cn/go-optimize-brief/

    1)、内存优化:

        a)、小对象合并成结构体一次分配,减少内存分配次数;

        b)、缓存区内容一次分配足够大小空间,并适当复用;

        c)、slice和map采make创建时,预估大小指定容量;

        d)、长调用栈避免申请较多的临时对象;

        e)、避免频繁创建临时对象;

    2)、并发优化:

        a)、高并发的任务处理使用goroutine池;

        b)、避免高并发调用同步系统接口;

        c)、高并发时避免共享对象互斥;

    3)、其他优化:

        a)、避免使用CGO或者减少CGO调用次数;

        b)、减少[]byte与string之间转换,尽量采用[]byte来字符串处理;

        c)、字符串的拼接优先考虑bytes.Buffer;

三、内存分配

1、内置运行时的编程语言通常会抛弃传统的内存分配方式,改由自主管理。这样可以完成类似预分配、内存池等操作,以避免系统调用带来的性能问题。当然,有一个重要原因是为了更好地配合垃圾回收;

2、Go内存分配的基本策略:

         1)、每次从操作系统申请一大块内存(比如1MB),以减少系统调用;

         2)、将申请到的大块内存按照特定大小预先切分成小块,构成链表;

         3)、为对象分配内存时,只需从大小合适的链表提取一小块即可;

         4)、回收对象内存时,将该小块内存重新归还到原链表,以便复用;

         5)、如闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销;

3、内存分配器只管理内存块,并不关心对象状态。且它不会主动回收内存,垃圾回收器在完成清理操作后,触发内存分配器的回收操作;

4、内存分配器将其管理的内存块分为两种:

         1)、span:  由多个地址连续的页(page)组成的大块内存;

         2)、object:将span按照特定大小切分成多个小块,每个小块可存储一个对象;

5、内存分配器按照页数来区分不同大小的span。比如,以页数为单位将span存放到管理数组(数组长度固定60)中,需要时就以页数为索引进行查找;

6、内存分配器会尝试将多个微小对象组合到一个object块内,以节省内存;

7、内存分配器由三种组件组成:

         1)、cache:  每个运行期工作线程都会绑定一个cache,用于无锁object分配(请求内存块规格检索在此完成);

         2)、central:为所有cache提供切分好的后备span资源;

         3)、heap:   管理闲置span,需要时向操作系统申请新内存;

8、分配流程:

         1)、计算待分配对象对应的规格(size class);

         2)、从cache.alloc数组找到规格相同的span;

         3)、从span.freelist链表提取可用object;

         4)、如span.freelist为空,从central获取新span;

         5)、如central.nonempty为空,从heap.free/freelarge(32k作为界限)获取,并切分为object链表;

         6)、如heap没有大小合适的闲置的span,向操作系统申请新内存块;

9、释放流程:

         1)、将标记为可回收的object交还给所属span.freelist;

         2)、该span被放回central,可供任意cache重新获取使用;

         3)、如span已收回全部object,则将其交还给heap,以便重新切分复用;

         4)、定期扫描heap里长时间闲置的span,释放其占用的内存;

         5)、以上不包括大对象,它直接从heap分配和回收;

10、通常情况下,编译器有责任尽可能使用寄存器和栈来存储对象,这有助于提升性能,减少垃圾回收器的压力;

11、Go编译器支持逃逸分析(escape analysis),它会在编译期通过构建调用图来分析局部变量是否会被外部引用,从而决定是否可直接分配在栈/堆上;

12、内存回收:

         1)、之所以说“回收”而不是“释放”,是因为整个内存分配器的核心思想是内存复用;

         2)、基于效率考虑,回收操作自然不会直接盯着单个对象,而是以span为基本单位。通过比对bitmap里的扫描标记,逐步将object收归原span,最终上交central或heap复用;

         3)、无论是向操作系统申请内存,还是清理回收内存,只要往heap里放span,都会尝试合并左右相邻的闲置span,以构成更大的自由块;

13、内存释放:

         1)、在运行时入口函数main.main里,会专门启动一个监控任务sysmon,它每个一段时间(约5分钟)就会检查heap里的闲置内存块;

         2)、遍历free、freelarge里所有的span,如闲置时间超过阈值,则释放其关联的物理内存;

         3)、所谓物理内存释放,其实是调用madvise告知操作系统(*nix),某段内存暂不使用,建议内核收回对应物理内存;

四、垃圾回收

1、Go语言的垃圾回收策略为标记-清除(mark and sweep);

2、垃圾回收器是Go一直在改进最努力的部分,所有的变化都是为了缩短STW(Stop-The-World)时间,提高程序实时性;

3、从Go 1.5开始增加三色并发标记检测,此处的并发,是指垃圾回收和用户逻辑并发执行;

4、三色标记基本原理:

         白色:待回收对象

         灰色:处理中对象

         黑色:活跃的对象

         1)、起初所有对象都是白色(虽然是白色,但是未标记,不能直接回收);

         2)、扫描找出所有可达对象,标记为灰色,放入待处理队列(gcWork高性能缓存队列);

         3)、从队列提起灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色;

         4)、写屏障见识对象内存修改,重新标色或放回队列;

         5)、当完成全部扫描和标记工作后,剩余的不是白色就是黑色,分别代表待回收和活跃对象,清理操作只需将白色对象内存收回即可;

5、虽然标记是并发执行的,但STW执行垃圾回收时,仍然会暂停所有的用户逻辑线程(虽1.7版本已大幅优化GC性能,1.8甚至量坏情况下GC为100us,但暂停时间还是取决于临时对象的个数,临时对象数量越多,暂停时间可能越长,并消耗CPU);

五、并发调度

1、内置运行时,在进程和线程的基础上做更高层次的抽象是现代语言最流行的做法;

2、并发调度模型相关组件:

         1)、Processor(简称P),其作用类似于CPU核,用来控制可同时并发执行的任务数量;

         2)、Goroutine(简称G),进程内的一切都是在以G方式运行,包括运行时相关服务,以及main.main入口函数;

         3)、系统线程(简称M),它和P绑定,以调度循环方式不停执行G并发任务。M通过修改寄存器(CPU指令存储区),将执行栈指向G自带的栈内存,并在此空间内分配堆栈帧,执行任务函数;

3、尽管P/M构成执行组合体,但两者数量并非一一对应。通常情况下,P的数量相对恒定,默认与CPU核数相同,但也可能更多或者更少(runtime.GOMAXPROCS),而M则是由调度器按需创建的。

4、虽然可在运行期间用runtime.GOMAXPROCS函数修改P的数量,但需付出极大代价:stopTheWorld && startTheWorld;

5、系统监控线程(sysmon)对于内存分配、垃圾回收、并发调度非常重要,主要作用如下:

         1)、释放闲置超过5分钟的span物理内存块;

         2)、如果超过2分钟没有垃圾回收,则强制执行;

         3)、将长时间未处理的netpoll结果添加到任务队列;

         4)、向长时间运行的G任务发出抢占调度;

         5)、收回因syscall而长时间阻塞的P;

6、抢占调度只是在目标G上设置一个抢占标志,当改任务调用某个函数时,被编译器安插的指令就会检查这个标志,从而决定是否会暂停当前任务;

7、stopTheWorld:用户逻辑必须暂停在一个安全点上,否则会引发很多意外问题。因此,stopTheWorld同样是通过“通知”机制,向所有正在运行的G任务发出抢占调度,使其暂停;

8、defer延迟调用:延迟调用远不是一个CALL指令那么简单,会涉及很多内容。诸如对象分配、缓存,以及多次函数调用。在某些性能要求比较高的场合,应该避免使用defer;

 

 

 

 

 

 

 

 

 


有疑问加站长微信联系(非本文作者)

本文来自:johng

感谢作者:john

查看原文:Go学习整理笔记

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2030 次点击  ∙  1 赞  
加入收藏 微博
1 回复  |  直到 2020-02-13 12:17:04
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传