
大家好!我是 Sergey Kamardin,是 Mail.Ru 的一名工程师。
本文主要介绍如何使用 Go 开发高负载的 WebSocket 服务。
如果你熟悉 WebSockets,但对 Go 了解不多,仍希望你对这篇文章的想法和性能优化方面感兴趣。
1. 简介
为了定义本文的讨论范围,有必要说明我们为什么需要这个服务。
Mail.Ru 有很多有状态系统。用户的电子邮件存储就是其中之一。我们有几种方法可以跟踪该系统的状态变化以及系统事件,主要是通过定期系统轮询或者状态变化时的系统通知来实现。
两种方式各有利弊。但是对于邮件而言,用户收到新邮件的速度越快越好。
邮件轮询大约每秒 50,000 个 HTTP 查询,其中 60% 返回 304 状态,这意味着邮箱中没有任何更改。
因此,为了减少服务器的负载并加快向用户发送邮件的速度,我们决定通过用发布 - 订阅服务(也称为消息总线,消息代理或事件管道)的模式来造一个轮子。一端接收有关状态更改的通知,另一端订阅此类通知。
之前的架构:

现在的架构:

第一个方案是之前的架构。浏览器定期轮询 API 并查询存储(邮箱服务)是否有更改。
第二种方案是现在的架构。浏览器与通知 API 建立了 WebSocket 连接,通知 API 是总线服务的消费者。一旦接收到新邮件后,Storage 会将有关它的通知发送到总线(1),总线将其发送给订阅者(2)。 API 通过连接发送这个收到的通知,将其发送到用户的浏览器(3)。
所以现在我们将讨论这个 API 或者这个 WebSocket 服务。展望一下未来,我们的服务将来可能会有 300 万个在线连接。
2. 常用的方式
我们来看看如何在没有任何优化的情况下使用 Go 实现服务器的某些部分。
在我们继续使用 net/http 之前,来谈谈如何发送和接收数据。这个数据位于 WebSocket 协议上(例如 JSON 对象),我们在下文中将其称为包。
我们先来实现 Channel 结构体,该结构体将包含在 WebSocket 连接上发送和接收数据包的逻辑。
2.1 Channel 结构体
// WebSocket Channel 的实现
// Packet 结构体表示应用程序级数据
type Packet struct {
...
}
// Channel 装饰用户连接
type Channel struct {
conn net.Conn // WebSocket 连接
send chan Packet // 传出的 packets 队列
}
func NewChannel(conn net.Conn) *Channel {
c := &Channel{
conn: conn,
send: make(chan Packet, N),
}
go c.reader()
go c.writer()
return c
}









