聪明的你,用golang写后端服务,各种使用channel和goroutine,把java要用线程池干的事儿用携程都搞掂了。服务线下运行一切正常,压测,单元测,联调统统通过。你露出得意的微笑,一键发布到生产环境,欣喜的发现服务崩溃了。
为什么服务会崩溃呢?
channel死锁
死锁是golang里边最常见的一类问题,我们从java和c/c++转过来的gopher在编程时候会特别注意mutex,semaphore,atomic等等的使用,反复检查会不会造成死锁。但是我们有可能会忽略掉另一个死锁大元凶:channel。
channel简直就是用来死锁的。
unbuffered channel读和写都是blocking的,也就是说读一个没人写的channel和写一个没人读的channel都会造成死锁。死锁状态下的goroutine会永远等待这个channel operation返回。这么做的goroutine多了,直接就会造成内存泄漏,服务器崩溃。
buffered channel不会吗?
也会的。buffered channel写满了之后再写就会死给你看。空的channel没人的情况下去读也死给你看。
怎么避免呢?
select {
case <-ch:
// a read from ch has occurred
case <-time.After(1 * time.Second):
// moving on after 1 second
}
这在官方的 effective go里边就有介绍。
第三方调用
我们往往不能天真的认为我们调用的上游服务不会挂,我们也不能淳朴的认为我们的网络环境永远是可靠的。往往一个小波动就会使得我们好多goroutine临时的卡在第三方api调用上。比如正常状况下我们期待所有goroutine20ms都会完成。但是突然网络抖动,或者第三方服务稳定性出了一点小问题,我们的goroutine都要2秒钟才会完成。
可能有人会觉得,不就是慢了一点吗,有什么大关系。
同志们,关系非常大。本来goroutine 20ms就退出的情况下,我们可能同时也就是几千个goroutine。如果goroutine生命周期变长,我们就会瞬间攒下来100*几千个goroutine,如果不加以限制的话,我们的服务就会瞬间崩溃。
所以别人的问题,可能会导致我们自己服务的失败。
怎么办?
使用circuit breaker。
我们想这样保护我们的服务:
首先设定SLA (service level agreement)
- 平均响应时间
- 容错率
- max qps
我们需要monitor第三方服务的调用出错率。当出错率高于某个阈值(超时也是一种错误),我们需要暂时对服务调用这个操作直接报错,这样调用的goroutine就可以迅速的退出。
我们还需要不断的尝试,看看调用状况是不是变回良好可用的状态,如果是,我们就得恢复正常的调用机制。并且重新开始计算出错率。
这个pattern其实就是大家常说的熔断器(circuit breaker)
熔断器的一个golang实现:afex/hystrix-go
怎么观测我们的服务承受的压力?
普罗米修斯:Prometheus
数据狗:Modern monitoring & analytics
NewRelic: Digital Performance Monitoring and Management | New Relic
如何debug到底goroutine卡在什么地方?
pprof : https://golang.org/pkg/net/http/pprof/
这篇文章粗浅无比的谈了一下golang服务如何防止,检测,排查goroutine过多引起的在线故障。意在抛砖引玉。聪明的你,我们一起学习,一起深入讨论。
希望我们露出得意微笑部署golang服务的时候,可以更加自信,更加成竹在胸。
有疑问加站长微信联系(非本文作者)