http://1234n.com/?post/yzsrwa

最近一段时间对《仙侠道》的服务端进行了一系列针对GC的调优,这里跟各位分享一下调优的经验。

游戏第一次上线的时候,大部分精力都投入在做cpuprof和memprof找性能瓶颈和内存泄漏上,没有关注过Go的GC运行情况。

有一次cpuprof里的scanblock调用所占的比例让我注意到Go的GC所带来的性能消耗,记得那份cpuprof里,scanblock调用占到49%。也就是说有一半的CPU时间浪费在了GC上。

于是我开始研究如何进行优化,过程中免不了要分析数据,经过一番搜索,我好到了GOGCTRACE这个环境变量。

用法类似这样:

GOGCTRACE=1 ./my_go_program 2> log_file

通过这个环境变量可以让Go程序在每次GC时都输出信息,信息是输出到标准错误的,所以需要用 2> 把输出重定向到文件里。

输出的内容像这样:

gc16(8): 34+6+5 ms, 367 -> 365 MB 817253 -> 782045 (18216892-17434847) objects, 64(2182) handoff, 72(22022) steal, 553/244/51 yields

其中gc16表示第16次进行GC,后面的(8)表示由8个线程执行,这个线程数对应GOMAXPROCS环境变量,34+6+5 ms分别代表一系列GC动作消耗的时间,这三个时间加起来45ms,就是这个程序在这次GC过程中暂停的时间。

后面接着的是内存、对象数量等,在GC前后的变化,其中最关键的是对象数量,这边可以看到GC后还有782045个对象存在。

我在实际游戏服和内网开发测试服都开启了GOGCTRACE,发现GC暂停时间相差甚大,当时(还未做第一次优化前)外网GC暂停达到400多ms,而内网才20ms。

显然跟内存中数据多少有关系,于是我推测跟内存中对象数量关系最大,原因很简单,假设我是GC开发者,不可能让一个对象占用100M内存跟一万个对象占用100M内存同样消耗性能,显然那一个占用100M内存的对象,当我发现它不需要回收的话,我就不需要做什么事情了,而那一万个对象,我需要逐个检查是否还有被引用,所以内存大小不是关键,对象数量才是关键。

于是我按这个推测进行了第一次性能优化,我把存储游戏内存数据的链表结构改为slice,当初设计成链表是因为数据有插入和删除,slice可以扩容但是要收缩就比较麻烦了,于是想到了链表,链表要删除单个节点的时候,只需要把节点从链表上断开,不需要复制数据,效率高于数组结构。这里直观的表示一下两种数据结构的区别:

    type MyData1 struct {
next *MyData
Id int
Name string
} var mydata1 *MyData1 type MyData2 struct {
Id int
Name string
} var mydata2 []MyData2

上面示例代码的mydata1用的是链表结构,每个节点都有一个指向下一个节点的指针,想像下存储1万个对象到mydata1,是不是需要创建1万个MyData1类型的对象。

示例中的mydata2用的是slice结构,一个slice就是一个对象,其中的元素都是这一块内存中的值,而不是对象,需要注意 []MyData2 和 []*MyData2 是不一样的,如果换用第二种写法,那么每个元素一样都是一个对象,因为这时候slice存的不是值而是指向对象的指针,而这些指针每一个都分别指到一个对象。

我做了一组不同数据结构跟对象数量关系的实验,可以直观的感受区别:github链接

经过这番改造,对象数量少了一个数量级,具体对少对象我已经记不得了,但是可以自己估计一下,一个mydata1这样的内存表,假设平均20条记录,假设有50个这样的表,就是1000个对象,换成mydata2这样的内存表,就只要50个对象。

当然这样一换,内存占用肯定就上去了,但是实际观测下来,内存占用在可接收范围,甚至还是远小于之前我用erlang开发的游戏,而GC扫描时间从300多ms降到几十ms,降了一个数量级。

本来优化到此我就打算告一段落了,但是随着游戏的持续运行,数据的持续增加,我发现slice自身占用的对象数量也还是值得动动脑筋消除掉的,线上GC暂停时间最高的服务器,达到了100ms,如果再涨上去,一样还是可能达到200ms设置300ms。

所以又继续懂了一些脑筋,比如把玩家数据压缩起来,等需要用的时候再解开来用,尝试过json序列化等等,目的都是把多个对象归并成一个。

但是这些方案都是牺牲数据访问的效率为代价的,需要访问数据时就要反序列化展开数据。

其实在第一次优化时,我大部分时间花在尝试cgo上面,而不是尝试slice上,我第一个思路是用cgo申请内存,伪造成go的对象,这些对象就不受Go的GC管理里,也就不会对GC有负担。但是尝试下来,总是遇到各种指针异常,我可以确信不是我的指针运算问题,但是为什么自己申请的内存会影响到Go的执行,我一直弄不明白,时间不等人,不可能一直研究下去,所以我才想了slice的这个方案,不是最优解但至少暂时解决问题。

而这一次,因为使用了slice,原先的内存数据库的数据结构就变得很单一,而优化的目的也明显,减少slice的内存消耗。正好那阵子我在尝试将SpiderMonkey嵌入到Go,接触到了cgo操作slice的一些技巧,比如将C的数组映射成Go的slice,或者利用reflect.SliceHeader取得slice所指向的内存块地址,然后用cgo复制数据。

于是我就想到用C来申请slice所需内存块,然后自己构造SliceHeader的办法。

这里需要说明下SliceHader和slice之间的关系。

Go提供了一个很有用的数据结构slice,slice比起C时代的数值有很明显的优势,有边界判断、可以反复切割、没有牺牲运行效率,如何做到的呢?官方这片文章有很清楚的说明:点击查看

简单说来,Go的slice其实是一个三个字段的结构体,三个字段分别存放着slice的当前长度、内存块的大小和实际内存块的地址,每次len(slice)的时候是不需要循环计算长度的,只是到结构体里去一下长度,而重新切割的过程,只是重新构造一个指向同一个内存块或块中某一位置的过程,所以不会有内存拷贝和循环等消耗性能的操作。

这个三个字段的结构体,在Go的反射包里面使用SliceHeader类型表示,这让我们的程序有机会构造自己的SliceHeader。

cgo的wiki文档里有这样一段示例代码,演示如何把C的数组包装成Go的slice:

import "C"
import "unsafe"
...
var theCArray *TheCType := C.getTheArray()
length := C.getTheArrayLength()
var theGoSlice []TheCType
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&theGoSlice)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(unsafe.Pointer(&theCArray[0]))
// now theGoSlice is a normal Go slice backed by the C array

这边用到了unsafe.Pointer,通过Pointer类型,我们可以在Go的程序里实现指针运算,之前我有写过相关文章,这里就不重复介绍了:点击查看

于是我将内存数据库用到的slice类型全部换成自己用C伪造的slice,还好当初内存数据库用的是代码生成器,否则代码就要改死掉了 :)

全部替换完后,我拿外网同样数据对比,优化前的程序GC扫描时间100多ms,对象数量140万,优化后的程序GC扫描时间18ms,对象数量16万。

本来可以就这样打完收功了,但是生活总是充满戏剧性,内网测试的时候发现好友列表里面的名字全乱码了,肯定跟优化有关系,但为什么会乱码呢?

我的推测是go构造的字符串对象被C构造的对象引用,这样的引用导致go把字符串对象当成没人使用,于是就被回收利用了。

我只好把所有字符串字段也全部改为C伪造的对象,原理给伪造slice是一样的,不同的是字符串用StringHeader表示。

经过改造,字符串再也不会乱码了,不过需要很小心的释放内存。

优化过程中Go提供的pprof模块起到了很重要的作用,所有的优化都是以数据为依据的,如果不能看到数据就没有办法定位问题。

程序中可以用 pprof.Lookup("heap") 来获得堆信息,其中包含了对象数量和GC执行时间等有用的数据。

上次群里有人问 map[int]XXX 这样的数据结构是否会有GC问题,正好这个数据结构我之前也考虑过,也在上面的数据结构实验里体现了,map[int]XXX 和 map[int]XXX是一样的,一条数据就是一个对象,对GC是否有影响取决于对象的数量。

从上面的观测数值来看百来万的对象数量所造成的暂停应该还不足以影响程序,除非应用场景对实时性要求非常高。

但是对于游戏这样的常驻内存程序来说,对象的增长速度和对象数量上限也需要留意,比如刚开始对象数量只有几万,随着日子增长,玩家数据增多,对象数量达到百万千万,那时候可能就会有影响了。

之前第一次优化过后正好有人在知乎问Go的GC情况,我回了一帖,里面有比较详细的第一次优化的数据,大家可以参考一下:点击查看

Go程序GC优化经验分享的更多相关文章

  1. Unity MMORPG游戏优化经验分享

    https://mp.weixin.qq.com/s/thGF2WVUkIQYQDrz5DISxA 今天由Unity技术支持工程师高岩,根据实际的技术支持工作经验积累,分享如何对Unity MMORP ...

  2. Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理

    Lucene底层原理和优化经验分享(1)-Lucene简介和索引原理 2017年01月04日 08:52:12 阅读数:18366 基于Lucene检索引擎我们开发了自己的全文检索系统,承担起后台PB ...

  3. 项目优化经验分享(八)TeamLeader经验总结

    引言 通过前面的七篇博客.我把自己在项目优化过程的经验进行了分享,今天这篇博客,作为一个总结,就来讲讲作为一个TeamLeader,在项目管理中遇到的问题和解决经验! 正文 问题一:团队之间怎么沟通? ...

  4. Unity技术支持团队性能优化经验分享

    https://mp.weixin.qq.com/s?__biz=MzU5MjQ1NTEwOA==&mid=2247490321&idx=1&sn=f9f34407ee5c5d ...

  5. Unite Europe案例项目《影子战术》层级优化经验分享

    http://forum.china.unity3d.com/thread-25087-1-9.html 在Unite Europe 2017的Keynote主题演讲中,我们为大家分享了将主机游戏&l ...

  6. Go --- GC优化经验

    不想看长篇大论的,这里先给个结论,go的gc还不完善但也不算不靠谱,关键看怎么用,尽量不要创建大量对象,也尽量不要频繁创建对象,这个道理其实在所有带gc的编程语言也都通用. 想知道如何提前预防和解决问 ...

  7. Web前端性能优化经验分享

    最近一直有给新同学做前端方面的培训,也有去参与公司前端的招聘,所以把自己资料库里面很多高效且有用的知识做了些 规整分类,然后再分享一篇关于前端优化方面的总结.而且春节一过就又是招聘的高峰期了,在校的. ...

  8. 两年Java程序员面试经验分享,从简历制作到面试总结!

    前言 工作两年左右,实习一年左右,正式工作一年左右,其实挺尴尬的,高不成低不就.因此在面试许多公司,找到了目前最适合自己的公司之后.于是做一个关于面试的总结.希望能够给那些依旧在找工作的同学提供帮助. ...

  9. C#.NET 大型企业信息化系统 - 防黑客攻击 - SSO系统加固优化经验分享

    好久没写文章了,突然间也不知道写什么好了一样,好多人可能以为我死了,写个文章分享一下.证明一下自己还在,很好的活着吧,刷个存在感. 放弃了很多娱乐.休闲.旅游.写文章.看书.陪伴家人,静心默默的用了接 ...

随机推荐

  1. XtraBackup做mysql主从同步

    一.背景: 线上一个主库压力比较大,所以增加一个从库,但是不能重启或者停止主库的正常运行,不能锁库锁表影响业务的正常运行.所以这里想到了XtraBackup 二.XtraBackup介绍: Xtrab ...

  2. 知问前端--Ajax

    本节课主要是创建一个问题表,将提问数据通过 ajax 方式提交出去.然后对内容显示进行布局,实现内容部分隐藏和完整显示的功能. 一.Ajax 提问创建一个数据表:question,分别建立:id.ti ...

  3. abstract修饰符,具体类与抽象类的区别

    abstract修饰符 abstract可以修饰类,表示一个抽象类,注意:抽象类和具体类唯一的区别:类不能创建对象,而具体类是可以创建对象的 1.具体类有构造方法,抽象类也有构造方法 2.具体类可以有 ...

  4. DKNightVersion的基本使用(夜间模式)

    DKNightVersion下载地址: https://github.com/Draveness/DKNightVersion 基本原理就是利用一个单例对象来存储颜色, 然后通过runtime中的ob ...

  5. Canvas--2

    Canvas2(关键词:setLineDash .rect .strokeRect .clearRect .arc.sin .strokeText )   绘制其他样式: lineCap 结束端点的设 ...

  6. C++基于模板顺序表的实现(带排序)

    说明:代码是可以运行的,但是发表在博客上后复制到编译器里面报N多错误,找了半天原因是网页里面生成了一些空白字符,这些字符编译器无法识别. 因此使用了2种插入格式插入代码. 第二个带注释解释的代码不可复 ...

  7. PHP扩展开发(4) - 多类扩展

    由于函数和单类的扩展,网上一搜一大片,这里就不再叙述了. 这里特别感谢laruence(鸟哥)开源的yaf扩展,解决困扰我多时的多类问题,还在看他的代码学习中,这里是对多类写法学习的一个阶段总结.   ...

  8. java并发编程_建立概念

    在学习多线程编程时,相信大家会遇到好多概念类的东西,对于这些概念的不准确理解会导致后面越学越糊涂,现将学习过程中遇到的概念整理到这篇博客上,一来记录学习点滴,二来也加深理解,如果有理解不准确的地方,希 ...

  9. C程序设计语言练习题1-10

    练习1-10 编写一个将输入复制到输出的程序,并将起重的制表符替换为\t,把回退符替换成\b,把反斜杠替换为\\.这样可以将制表符和回退符以可见的方式显示出来. 代码如下: #include < ...

  10. 理解java的三大特性之多态(三)

    摘自:http://cmsblogs.com/?p=52 面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已 ...