今天通过一个因为并发控制不当导致下游服务崩溃的案例,给大家分享一个关于并发控制的误区。
Go
语言原生支持并发,只要使用go
关键字就能把函数交给goroutine
来并发地执行一段程序,正是因为并发难度特别低,有不少人在掌握语法后就特别喜欢尝试进行并发编程,包括我也是,不过我向来保持着对编程的敬畏之心(就是胆儿小~)所以那会刚用Go写代码时对并发尝试地比较克制,生怕写出了线上BUG。
当时我们是有一个异步给不活跃的用户补发一些满减卡券并触达用户的任务,是由组里的一个从前端转过来的老哥写的,由于所有接口都是现成的他只要写个任务查出最近一周满足条件的用户,异步地去调用公司内部的卡券服务就行了,维护卡券服务的同事给我们的标准是并发请求的最大请求数不要超过500,结果当天上线后晚流量高峰的时候直接压挂卡券系统。
做代码排查的时候,发现老哥直接用的time.Sleep
做的并发控制。像下面这段代码一样:
func badConcurrency() {
batchSize := 500
for {
data, _ := queryDataWithSizeN(batchSize)
if len(data) == 0 {
break
}
for _, uid := range data {
go func(i int) {
doSomething(i)
}(uid)
}
// 休眠1s
time.Sleep(time.Second * 1)
}
}
每查 500 条数据后,交给500个goroutine
去执行发卡券的任务,然后休眠1秒再重复上面的过程。有人肯定要问了,诶~这么看每秒是不超过500个请求啊!
em ~ 从调用方的角度看确实超不过 500,但是却没有考虑下游服务也是分忙时和闲时的(因为是比较基础的服务,不光一个业务方调)。如果下游服务正好在忙时,在1s内没有处理完上一批发过来的 500 个请求,上游就又发过来 500 个请求,用不了多少时间系统就会达到过载状态。
那么有人会问,下游服务难道自己没有做限流或者队列暂存吗?当时老哥也是这么问的,理想情况确实应该有,但那是理想情况,现实情况就是这个内部服务自己没做限流,我相信这在大家的公司也是比较普遍的情况 。
那么又有人会问:“这个事故不该是记在维护卡卷服务的研发头上吗?” ,大哥们,如果真是这样记事故,各研发组之间的部门墙得多厚啊,只要领导不太偏袒一方当然是各打五十大板,都整改啦。
针对这种情况,如果是你来写这个调用方的程序,你认为有哪些的方法既能使用并发增加调用方发起请求的能力又能照顾到下游服务的忙时负载呢?欢迎大家积极留言,下篇文章我会汇总几个方案分享给大家。