在Go中event-service缺少WebSocket读取处理程序的解决方案

167 阅读5分钟

这是一个关于在go微服务中寻找错误的故事,它花费了相当长的时间,最后归结为一个相当小的细节(像往常一样)。在经历了这样的问题后,人们总是觉得有点傻,因为发现问题后似乎很明显,但这种感觉通常是被误导的,因为任何曾经写过生产级软件的人可能都能证明,这些事情就是发生了。

首先,我将描述服务和问题,然后是一些不成功的寻找问题的尝试,最后是简单的解决方案。

让我们开始吧! :)

服务

问题中的服务,我们在这里称之为event-service ,基本上是一个事件路由服务,它让客户订阅任意的事件,这些事件被发送到客户的下游。

如果一个事件发生了(例如:somethingCreated ),这个事件就会被放在一个rabbitmq 实例上,并从那里向这个事件服务的所有实例扩散。如果一个实例有一个订阅了传入事件的客户端,该事件就会通过WebSocket发送到该客户端(当然,客户端必须在之前启动该连接)。

WebSocket通信通常是单向的,所以事件只向下推送到客户端,而不是从客户端向上推送到事件服务。

问题所在

在推出该服务后,我们开始注意到,运行该服务的实例正在缓慢但持续地积累内存。甚至到了这样的地步:一个实例的内存用完了(尽管服务是有内存限制的)并崩溃了--幸运的是,我们的基础设施有足够的弹性,终端用户没有受到影响。

然而,这种行为是非常奇怪和令人担忧的--在实例上运行的进程似乎都没有占用正在消失的内存,但它仍然不断上升,上升,直到实例最终崩溃。我们尝试了几种不同的配置,以排除运行在实例上的某个服务组合对我们的问题负责,但没有成功。

我们在实验中还注意到,杀死开放的WebSocket连接(例如,通过重新启动负载均衡器或服务本身),使内存也被回收,所以我们认为这可能与清理连接有关,但同样,我们无法发现任何问题。

最令人困惑的是,内存似乎无处可去--我们尝试的任何unix命令都没有显示丢失的内存,甚至AWS支持也不能帮助我们找到内存(但公平地说,他们的背景要少得多)。

令人沮丧的是,我们也无法在我们的测试系统上重现这个问题,即使是用负载测试把测试系统炸到地面上,我们也无法复制同样的行为。

总之,在尝试了很多不同的工具、命令和向别人寻求帮助之后,我们实际上进入了完全的试验和错误模式,这就是我偶然发现问题的地方--缺少WebSocket读取处理程序。

简单的解决方案

基本上,我们关于WebSocket的设置是Gorilla WebSockets文档中相当标准的东西。在验证和注册用户时,我们打开一个WebSocket连接。

然而,由于从语义上讲,该连接只用于向客户推送事件,而不会从客户那里返回,因此我们只在连接上定义了一个writeHandler ,但没有定义readHandler ,这是一个致命的错误。

我们没有考虑到的是,客户端可以向上发送消息,而且也可以发送WILL,因为他们会发送一个常规的ping ,以确保连接仍然在工作。在我们的负载测试中,我们没有想到要整合这个ping 消息,这就是为什么我们不能重现这个问题。这也解释了为什么内存没有直接显示在event-service 进程中,也没有显示在任何其他docker进程中。

事实证明,来自客户端的持续ping填满了连接缓冲区,这在netstat (不幸的是,我们之前没有注意到这个统计)上确实是可见的,但在任何内存统计上都没有。

所以最后,增加了一个read-handler ,它简单地丢弃了这些信息,解决了这个问题。

func reader(c *Client) {
    for {
        _, _, err := c.conn.ReadMessage()
            if err != nil {
                log.WithField("error", err).Debug("Read error happened - stop reading from connection")
                    close(c.readFailure)
                    break
            }
    }
}

在这个例子的代码中,c.readFailure 是一个等待被通知读取失败的通道,它被一个goroutine订阅,然后关闭整个连接和所有连接到它的正在运行的goroutine/通道,所以我们不会泄漏资源。

这个微妙的变化解决了整个难题,虽然事后看来相当明显,但如果你来自一个更高级的语言,如C#、Java、JavaScript等,你可能会(像我们一样)期望有一个标准的读取处理程序,这正好避免了这种情况的发生。

然而,gorilla WebSockets对此有一定的记录,而Go的水平较低,这显然是我们没有正确阅读文档和深入测试这种情况的错误。

总结

像这样的bug一旦发现就显得很傻,但特别是在目前网络堆栈的复杂性下,这些事情必然会发生。

然而,有了这样的经历,这种bug将永远出现在我的视野中,我相信分享这样的故事可以减少其他人浪费他们的时间去寻找类似的问题。

无论如何,我希望这对你来说是有趣的/有帮助的,它可能为你在未来节省一些时间和头疼的问题!:)

资源