使用 go-deadlock 库来定位 Go 协程信道中的 deadlock
最近,我解决了一个反复出现的问题,其原因几周来一直不清楚。我的团队会“做某事” ^ 1^,然后问题就消失了,只剩下几天到一周的时间后来。但是,经过几个小时的调试,它才完全有意义。我只是在错误的地方寻找问题。我想我应该分享一下。
遇到得问题是这样的。每隔一周左右,我们就会从客户端收到一个错误报告,说明我们的Web应用程序加载时间很长,似乎根本没有加载,或者操作很慢。它似乎一次只发生在一个客户身上,我们都能够看到它发生时的行为。但是,通常在重新启动后勤服务或清理一些数据后将其清除。
但是,这次,我们的快速修复无效。应用程序未恢复。这是怎么回事?
正在等待轮到您
可以说,在我们为该应用程序提供的一项后勤服务中,每个组都有自己的Room
。在将消息广播到会议室之前,我们已锁定成员
列表,以避免任何数据争用或可能的崩溃。像这样:
func (r *Room) Broadcast(msg string) {
r.membersMx.RLock()
defer r.membersMx.RUnlock()
for _, m := range r.members {
if err := s.Send(msg); err != nil { // ❶
log.Printf("Broadcast: %v: %v", r.instance, err)
}
}
}
请注意,我们等待❶直到每个成员收到消息,然后再继续下一个成员。稍后,这将成为问题。
另一个线索
测试人员还注意到,他们可以在重新启动服务后进入会议室,并且一切似乎都可以正常工作。但是,一旦他们离开并回来,该应用程序就会停止正常运行。原来,他们被挂在此功能上了,该功能向房间添加了一个新成员:
func (r *Room) Add(s sockjs.Session) {
r.membersMx.Lock() // ❶
r.members = append(r.members, s)
r.membersMx.Unlock()
}
我们无法获得锁 aa ,因为我们的Broadcast
函数仍在使用它来发送消息。
发现问题
初步调查表明,支持服务中的某些问题已被挂断,但是我们如何找出问题所在?
幸运的是,在跟踪实时互斥使用的工具go-deadlock的帮助下,我们可以看到这种情况正在发生。该工具会报告goroutine何时可以访问互斥锁30秒钟或更长时间^ 2^。该API反映了标准的Go库,从而使其成为一个便捷的插入检查器。结果指向Add
函数,等待Broadcast
函数释放其锁定。
突然之间,客户端报告变得完全有意义了(特别是当我们发现他们正在处理网络迟滞问题时)。
- 遭受高延迟的成员与其他成员一起加入会议室(
Add
)。 - 一旦他们提取了更新(
Broadcast
),所有成员便开始注意到更新缓慢。 - 成员重新加载应用程序,希望它可以解决问题,然后尝试重新加入(
Add
)。 - 但是,它们不能执行,因为他们正在等待(
Broadcast
)完成,因为高延迟成员已经放慢了它。
解决方案
由于我们需要锁定Broadcast
中的锁以使我们的成员
列表不发生变化,因此解决方案是在从锁中获得所需的内容后并行执行所有发送:
func (r *Room) Broadcast(msg string) {
r.membersMx.RLock()
defer r.membersMx.RUnlock()
for _, m := range r.members {
go func(s sockjs.Session) {
if err := s.Send(msg); err != nil {
log.Printf("Broadcast: %v: %v", r.instance, err)
}
}(m)
}
}
这有一些优点:
- 没有成员需要等待另一个来获得广播消息。
- 成员无需等待即可加入会议室。
- 由于goroutine很便宜,并且套接字已经建立(通过WebSocket)。这样的多个异步调用应该不是问题
正如in the discussion,此解决方案无法保证消息会按顺序传递,也可能无法传递确定适合您的应用程序.
学到的经验
导致应用程序失败的这种特殊服务已经投入生产数月之久,没有出现任何此类已报告的问题,这导致错误的假设,即该服务每天处理数十万条消息,因此运行良好。但是,这不行。在适当的情况下,它暴露出一个明显的问题。
我现在打算问问我将来使用互斥锁或类似对象时的自己:当慢速I / O涉及由互斥锁保护的数据时,是否会导致不良行为?
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: