Global-Counter-in-Programming

今天去颐和园玩来着,看到官方开的溜冰场,虽然50一位,但也值了。非常开心。 — 我

前些日子给QiMen添加新功能,需要统计实时状态,状态中有个请求数量,所以需要一个计数器。

如果是一般的计数器还好,写一个全局变量一个一个加就可以了,但是由于是高并发的服务,很多线程都回去加加减减,所以全局变量的方法是不可取的。

那既然有高并发,我给这个变量加个锁好了。也不可以,因为是高并发,可以假设每秒有10k请求,那就需要Lock Unlock 10k次,显然是对资源的浪费,很没有必要。 所以加锁的方案也不可行。

既然这样,那不如加个队列吧,一头写,另一头读。这个方案貌似可以,但计数器的实现就变成了“队列”+“计数器”,感觉工程量有点大。所以还是pass了。

emmm,今天吃饭等号的时候,跟阿杰讨论了下,他说他们的方案有一种是依靠外部redis单线程的优势,用redis的counter来加加减减。好像能够解决问题,毕竟redis还是靠得住的,然而,网络IO貌似有点高,而且引入了新的redis组件,如果排除掉网络IO的影响,如果有10个QiMen,每个QiMen每秒10k的消息,那就是每秒100k的操作,不知道redis能不能抗住。(也不确定redis cluster搞不搞的定这个counter)可能是大家的业务场景不一样,所以杰师傅考虑的“统计数据外置”在我的情境下是没必要的,因为我只需要一个总的结果,每隔一段时间反馈给我就好。但大多数情况下,统计数据外置还是政治正确的,这样可以解耦数据,使服务无状态。

然后在golang example中其实就有一个counter,用的是atomic的原子特性。(话说这里有一个插曲,golang的playground中,给每个程序限制了一个线程运行,因为每次跑goroutine都会得到正确的结果,不论加不加锁,也不论加不加atomic,猜测playground在运行时添加了 GOMAXPROCS=1

var counter uint64 = 0
atomic.AddUint64(&counter, 1)

但这个counter也不能满足我的要求,因为我其实是不知道有多少个counter的,比如请求来源有10个(以IP划分),那我就有10个counter,所以在事先不知道有多少个counter的情况下,这种方案也“貌似”被pass了。(其实在prometheus中,exporter所用的counter内部实现也是atomic)

第一反应是用map:

counter := make(map[string]uint64, 0)

if _, ok := counter[source]; !ok {
  counter[source] = 0
}

c := counter[source]
atomic.AddUint64(&c, 1) // error here

然而,map里面不让取地址,因为是哈希表,里面的东西会经常变,没有一个准确的地址。(可以搜一下大佬们对此的讨论,如果感兴趣的话)

但是。凡事总有个但是。imcom哥提供了这样一种方案。

counter := make(map[string]*uint64, 0)

最终的counter还是哈希表,但内容是一个指针,这样atomic就可以对其操作。如果想读取值的话,就atomic.LoadUint64一下。还可以在上层封装一个,在一个struct中既创建一个普通的map,又创建一个指针的map,增删全在指针map上进行,读取呢,则先Load到普通的map,然后就可以进行之后的操作了,比如映射json。

type Counter struct{
  IP		map[string]uint64  `json:"ip"`
  IPCounter map[string]*uint64 `json:"-"`
}

问题解决。

对于atomic的扩展阅读:sync/atomic - 原子操作

comments powered by Disqus