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. boost::pool与内存池技术

      建议看这个链接的内容:http://cpp.winxgui.com/cn:mempool-example-boost-pool Pool分配是一种分配内存方法,用于快速分配同样大小的内存块,    ...

  2. mybatis的缓存机制

    一级缓存: MyBatis的一级缓存指的是在一个Session域内,session为关闭的时候执行的查询会根据SQL为key被缓存(跟mysql缓存一样,修改任何参数的值都会导致缓存失效) packa ...

  3. vb的LINQ实现

    vb实现LINQ非常简单的例子: Dim numbers() As Integer = {1, 2, 3, 4, 5, 6, 7} Dim allNumbers = From number In nu ...

  4. Android-应用的本地化及知识拓展之配置修饰符

    步骤很简单,只需要两步: 1.创建带有目标语言的配置修饰符的资源子目录 2.将可选资源放入该目录下,android系统会自动处理后续工作 在这里我们需要讲解一下配置修饰符. 中文的配置修饰符:-zh, ...

  5. c - 每位数字尾部加空格

    /* input:一个4位整数. output:每位整数后紧跟一个空格的字符串. */ char * insert(char *s) { int len = strlen(s); * len + ); ...

  6. Oracle 分区表中索引失效

    当对分区表进行 一些操作时,会造成索引失效. 当有truncate/drop/exchange 操作分区  时全局索引 会失效. exchange 的临时表没有索引,或者有索引,没有用includin ...

  7. C语言转义字符相关知识

    在C语言里所有的ASCII码都可以用“\”加数字(一般是8进制数字)来表示.而C中定义了一些字母或数字前加"\"来表示常见的那些不能显示的ASCII字符,如\0,\t,\n等,就称 ...

  8. ref参数的用途

    ref参数 能够将一个变量带入方法进行改变,改变完成后再将改变完成后的变量带出方法 ref参数要求在方法外必须为值赋值,而方法内可以不赋值 static void Main(string[] arr) ...

  9. css技巧之如何实现ul li边框重合

    提到边框重合,我们不妨打开淘宝首页浏览主体分类内容板块瞧瞧---亲,你看到了,正是这个,边框重合.其实我们不难发现,这个效果并不难,只是我们没有真正的动手做过而已,所以不知道怎么做,那么下面就是一个很 ...

  10. 转载:mysql-Auto_increment值修改

    转载网址:http://libo93122.blog.163.com/blog/static/1221893820125282158745/ | 2012-03-13 11:19:10 | 2012- ...