不想看长篇大论的,这里先给个结论,go的gc还不完善但也不算不靠谱,关键看怎么用,尽量不要创建大量对象,也尽量不要频繁创建对象,这个道理其实在所有带gc的编程语言也都通用。
想知道如何提前预防和解决问题的,请耐心看下去。我们项目的服务端完全用Go语言开发的,游戏数据都放在内存中由go 管理。在上线测试后我对程序做了很多调优工作,最初是稳定性优先,所以先解决的是内存泄漏问题,主要靠memprof来定位问题,接着是进一步提高性能,主要靠cpuprof和自己做的一些统计信息来定位问题。调优性能的过程中我从cpuprof的结果发现发现gc的scanblock调用占用的cpu竟然有40%多,于是我开始搞各种对象重用和尽量避免不必要的对象创建,效果显著,CPU占用降到了10%多。但我还是挺不甘心的,想继续优化看看。网上找资料时看到GOGCTRACE这个环境变量可以开启gc调试信息的打印,于是我就在内网测试服开启了,每当go执行gc时就会打印一行信息,内容是gc执行时间和回收前后的对象数量变化。我惊奇的发现一次gc要20多毫秒,我们服务器请求处理时间平均才33微秒,差了一个量级别呢。于是我开始关心起gc执行时间这个数值,它到底是一个恒定值呢?还是更数据多少有关呢?我带着疑问在外网玩家测试的服务器也开启了gc追踪,结果更让我冒冷汗了,gc执行时间竟然达到300多毫秒。go的gc是固定每两分钟执行一次,每次执行都是暂停整个程序的,300多毫秒应该足以导致可感受到的响应延迟。所以缩短gc执行时间就变得非常必要。从哪里入手呢?首先,可以推断gc执行时间跟数据量是相关的,内网数据少外网数据多。其次,gc追踪信息把对象数量当成重点数据来输出,估计扫描是按对象扫描的,所以对象多扫描时间长,对象少扫描时间短。于是我便开始着手降低对象数量,一开始我尝试用cgo来解决问题,由c申请和释放内存,这部分c创建的对象就不会被gc扫描了。但是实践下来发现cgo会导致原有的内存数据操作出些诡异问题,例如一个对象明明初始化了,但还是读到非预期的数据。另外还会引起go运行时报申请内存死锁的错误,我反复读了go申请内存的代码,跟我直接用c的malloc完全都没关联,实在是很诡异。我只好暂时放弃cgo的方案,另外想了个法子。一个玩家有很多数据,如果把非活跃玩家的数据序列化成一个字节数组,就等于把多个对象压缩成了一个,这样就可以大量减少对象数量。我按这个思路用快速改了一版代码,放到外网实际测试,对象数量从几百万降至几十万,gc扫描时间降至二十几微秒。效果不错,但是要用玩家数据时要反序列化,这个消耗太大,还需要再想办法。于是我索性把内存数据都改为结构体和切片存放,之前用的是对象和单向链表,所以一条数据就会有一个对象对应,改为结构体和结构体切片,就等于把多个对象数据缩减下来。结果如预期的一样,内存多消耗了一些,但是对象数量少了一个量级。其实项目之初我就担心过这样的情况,那时候到处问人,对象多了会不会增加gc负担,导致gc时间过长,结果没得到答案。现在我填过这个坑了,可以确定的说,会。大家就不要再往这个坑跳了。如果go的gc聪明一点,把老对象和新对象区别处理,至少在我这个应用场景可以减少不必要的扫描,如果gc可以异步进行不暂停程序,我才不在乎那几百毫秒的执行时间呢。但是也不能完全怪go不完善,如果一开始我早点知道用GOGCTRACE来观测,就可以比较早点发现问题从而比较根本的解决问题。但是既然用了,项目也上了,没办法大改,只能见招拆招了。总结以下几点给打算用go开发项目或已经在用go开发项目的朋友:1、尽早的用memprof、cpuprof、GCTRACE来观察程序。2、关注请求处理时间,特别是开发新功能的时候,有助于发现设计上的问题。3、尽量避免频繁创建对象(&abc{}、new(abc{})、make()),在频繁调用的地方可以做对象重用。4、尽量不要用go管理大量对象,内存数据库可以完全用c实现好通过cgo来调用。手机回复打字好累,先写到这里,后面再来补充案例的数据。数据补充:图1,7月22日的一次cpuprof观测,采样3000多次调用,数据显示scanblock吃了43.3%的cpu。图2,7月23日,对修改后的程序做cpuprof,采样1万多次调用,数据显示cpu占用降至9.8%数据1,外网服务器的第一次gc trace结果,数据显示gc执行时间有400多ms,回收后对象数量1659922个:gc13(1): 308+92+1 ms , 156 -> 107 MB 3339834 -> 1659922 (12850245-11190323) objects, 0(0) handoff, 0(0) steal, 0/0/0 yields
gc14(6): 16+15+1 ms, 75 -> 37 MB 1409074 -> 126097 (10335326-10209229) objects, 45(1913) handoff, 34(4823) steal, 455/283/52 yields
// 玩家数据表的集合type tables struct { tableA *tableA tableB *tableB tableC *tableC // ...... 此处省略一大堆表}// 每个玩家只会有一条tableA记录type tableA struct { fieldA int fieldB string}// 每个玩家有多条tableB记录type tableB struct { xxoo int ooxx int next *tableB // 指向下一条记录}// 每个玩家只有一条tableC记录type tableC struct { id int value int64}
// 玩家数据表的集合type tables struct { tableA tableA tableB []tableB tableC tableC // ...... 此处省略一大堆表}// 每个玩家只会有一条tableA记录type tableA struct { _is_nil bool fieldA int fieldB string}// 每个玩家有多条tableB记录type tableB struct { _is_nil bool xxoo int ooxx int}// 每个玩家只有一条tableC记录type tableC struct { _is_nil bool id int value int64}