背景

open-falcon的graph模块占用内存太多, 即发起了graph的内存优化,在上线过程中发生了曲线值异常的问题

过程

1
2
3
4
5
6
7
8
func (this *SafeLinkedList) PushFront(item *cmodel.GraphItem) {
-       v := smodel.GraphItemPoint{}.New(item)
+       v := smodel.NewPointObjFromPool(item)
+       defer smodel.PutPointObjBack2Pool(v)

...

    this.L.PushFront(v)

上述代码中, PushFront是每个点到来都需要操作的函数, 即每上报一个点, smodel.GraphItemPoint{}.New(item) 都需要执行一次
因为这里是一个临时对象,所以尝试使用对象池来优化(虽然后面证明优化无效,但整个过程值得分享),以期望减少内存的分配及申请对象的次数,进而减少gc

注意上面的this.L.PushFront(v),这里是一个关键点

另外为对象池增加了helper函数,方便进行对象的Get和Put操作,如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
+var pointObjPool = sync.Pool{New: func() interface{} { return &GraphItemPoint{} }}
+
+func NewPointObjFromPool(item *cmodel.GraphItem) *GraphItemPoint {
+       ret := pointObjPool.Get().(*GraphItemPoint)
+       if item == nil {
+               return nil
+       }
+
+       ret.Timestamp = item.Timestamp
+       ret.Value = item.Value
+
+       return ret
+}
+
+func PutPointObjBack2Pool(p *GraphItemPoint) {
+       pointObjPool.Put(p)
+}
+

在这种场景下,使用sync.Pool对象池Get一个新的对象后,使用this.L.PushFront将对象的地址(因为返回的是一个指针),放在了内存cache中。

当执行查询操作时,在内存cache中获取到的是一个地址,将这个地址解引用获取内容时,就会出现问题。

因为这个对象地址是从对象池申请的,该地址在执行Put操作后,对于对象池而言就没什么用了。
而对象池的再次Get操作,很有可能分配出相同的地址。而在实际业务中,这个地址是有意义的,所以造成了从地址解引用实际内容时出现了问题。

整个过程有些类似C里面野指针的概念。

解决

如果从对象池获取值而非指针的话,那么这个问题就解了,如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func (this *SafeLinkedList) PushFront(item *cmodel.GraphItem) {
    if item == nil {
        return
    }
    v := smodel.NewPointObjFromPool()


...

    v.Timestamp = item.Timestamp
    v.Value = item.Value


...

    this.L.PushFront(&v)
    smodel.PutPointObjBack2Pool(v)

关于对象池的更改,如下

1
2
3
4
5
6
func NewPointObjFromPool() GraphItemPoint {
    return pointObjPool.Get().(GraphItemPoint)
}
func PutPointObjBack2Pool(p GraphItemPoint) {
    pointObjPool.Put(p)
}

-EOF-