Go初体验

最近在一个项目中使用了Go语言,当然不可避免的遇到了一些问题,记录这些问题的解决方法,也算是Go初体验的一部分了。

项目中的需求很简单,从RabbitMQ读数据,然后写入Redis当中,当然这个量比较大,似乎用goroutine是一个好的解决办法。

所以刚开始的逻辑是,从MQ读取数据,然后对每个数据都用一个goroutine处理。goroutine从Redis Pool里面取Redis connection,然后写数据到Redis。Redis go library redigo1,库中提供Redis Pool的API,可以很方便的管理Redis connection。

 
func HandlePackage(data){
    redisConn := getConnFromPool()
    wirteRedis(data)
}

func main(){
    for package from MQ {
        go HandlePackage(package)
    }
}

问题1: no such host

结果一跑,就出现很多 “no such host” 错误,google了一下,发现是glibc的bug2。当并发很高的时候,glibc库中查找DNS的代码就会出错。根据bug描述,要么升级glibc到2.20(但是2.20版本还没有发布),要么重新编译Go的net库。于是重新编译了Go的net库后,新问题又出现了。

问题2: redis connection timeout

这次是跑着跑着,Redis很多connection都timeout了,于是仔细看了一下redigo的Pool文档,发现默认Pool是没有大小限制的,如果Pool里面没有connection,会一直创建。难怪会timeout,应该是MQ数据太多,起了太多的goroutine,所以就会创建很多Redis client,Redis Server顶不住了。

想着限制一下Pool的大小,但是看了redigo的issue3,发现如果限制大小,当Pool没有资源时,Get会直接返回error。

于是根据redigo的wiki,采用了youtube开源的vitess的一个Pool4,这个pool限制大小后,当资源耗尽时,再请求会阻塞,直到Pool里面有可用资源才返回。

问题3: high memory usage

这次修改后,跑着跑着,发现进程的内存一直在增长,用top命令看,发现内存的 Res 一直飙升到1.8g,采用go自带的profile5工具,发现sys和Stack sys都非常高,相反heap相应的项占用都非常少,看来不是内存泄漏的问题。

从profile上看,有时候goroutine会飙升到100k,估计内存居高不下跟goroutine有关。查阅了一下,发现在Go1.3中,存在一个bug6。goroutine消耗的资源,并不会释放,相反会存放在pool中,等待下次新建goroutine时使用。于是进程的内存只会增加,不会减少。

分析了一下原因,有可能是网络不好,或者Redis在做bgsave或者其它操作,导致写Redis变慢,这样数据的生产(从MQ拿数据)比数据消费(写数据到Redis)快的多,而Redis connection又限制数量,导致goroutine阻塞在从pool取Reids connection上面,于是会创建越来越多的goroutine,结果就是导致内存一直飙升,却不会释放。

后来在golang-nuts邮件列表上问了问,有个老外建议用channel做信号量,来控制goroutine的数量,这样在高峰或者特殊情况,就不会导致goroutine无限制增长。

c := make(chan bool, 50) // concurrency = 50
for <whatever> {
  c <- true // blocks if the channel is full
  go func() {
    defer func() { <-c }  // make room for another goroutine
    // do whatever the goroutine should do
  }
}

修改代码后,发现goroutine限制数量后,内存基本控制在200多M以内,不会有占用大内存的现象出现了。

当然现在还是Go的初级使用者,这种消费者比生产者慢的需求,用Go的buffered channel是否合适,有没有更合适的解决方法,还待进一步研究。

Reference