内存占用过高

2017-02-18

就从这个issue说起吧。它描述的问题是这样子的,事务在提交之前,会把所有操作的数据都在本地组织好,然后一口气提交。这部分数据显然是需要占用内存的。在发往kv层之前,通讯需要加一次编码,将数据marshal成protobuf格式,这又是一份内存,相当于内存的开销再乘2。

作为一个优化,充分利用了Go语言在并发上的优势,当然是由许许多多的goroutine在并发地写的,网络IO自然不会那么快,所以大部分goroutine阻塞在发送数据上面排队。于是它们引用的内存是暂时无法被GC掉的,也就是说这些内存都是实际要占用一段时间的。

如果遇到事务冲突被abort掉之类的,就更悲剧了。尤其是大事务,白白占资源那么久,最后还没成功。当然这块的代码也是比较麻烦的,涉及到失败后,goroutine的各种资源释放的问题,也是比较容易出现leak的地方。死掉还好,最怕session搞了一个超大的事务完不成,却还活着的结束不掉,又不释放资源,那整个系统都死了,这种case线也出现过。扯远了,继续说内存占用。

记录结构的表示问题,一张表或者一个数据库在文件里面存下来是1G,加载进内存后是不是1G呢?显然不是。到内存里面表示的结构要复杂得多,将这些数据组织起来,花掉的内存比原始数据大许多,多好几倍也不稀奇。只是大部分时候是不需要把大量数据load进来的,但做join或者一些复杂的查询,全表扫或者delete,就不定了。

就说说order by。如果我想select一些数据并order by,数据是分布在多个结点的,当然可以多goroutine并行的读取,但是要求返回有序,就需要全部缓存在内存里重排序。如果可以流式地处理,可能占不到多少内存的,一旦加了order by,可能占的内存就翻了许多倍。

再说GC对内存占用的影响。Go语言的GC机制可以把它的内存占用看成三个层次:

  1. heap_in_use是程序实际需要用到的内存的量,这部分是被引用到的,即使GC之后也无法释放。我们真正需要关注的是这项。
  2. GC管理的内存,GC管理的内存达到上次实际使用内存量的两倍,会触发GC操作,比如当前heap_in_use是512M,那么当内存涨到1G的时候触发GC,经过回收之后的内存也许是300M,那么再次触发的时机就是内存使用量涨到600M的时候。
  3. 内存分配器管理的部分。向操作系统按页级别地申请大块内存,再给GC的分配使用。当GC回收之后,只是归还到分配池里面,还不会立刻归还给操作系统。

经过这个GC机制的影响,平时程序占用的内存是实际需要的内存2倍以上。如果刚执行完一个很耗资源的操作,由top看到的内存跟实际相差许多倍,这也是正常的,分配器还没把内存归还给系统而已。

做存储肯定都知道写放大的概念,其实内存里面也同样类似。需要操作的数据看起来不多,而实际占用的内存却是好多倍。这个数据具体是多少呢?

根据这边的一些观察,实际上处理的数据就90M~100M,而程序的内存占用达到了900M~1G !!! 还真别惊讶,就按指数倍翻了几次而已。

说了很多对内存占用放大的因素,其实我可以确切地知道哪些地方占内存,为什么占内存,但是说到解决,却也只能两手一摊。

memory