Go 实现百万WebSocket连接的方法示例

2020-01-28 13:45:43丽君

我想让你注意的是 readerwriter goroutines。每个 goroutine 都需要内存栈,初始大小可能为 2 到 8 KB,具体 取决于操作系统 和 Go 版本。

关于上面提到的 300 万个线上连接,为此我们需要消耗 24 GB 的内存(假设单个 goroutine 消耗 4 KB 栈内存)用于所有的连接。并且这还没包括为 Channel 结构体分配的内存, ch.send 传出的数据包占用的内存以及其他内部字段的内存。

2.2 I/O goroutines

让我们来看看 reader 的实现:


// Channel's reading goroutine.
func (c *Channel) reader() {
  // 创建一个缓冲 read 来减少 read 的系统调用
  buf := bufio.NewReader(c.conn)

  for {
    pkt, _ := readPacket(buf)
    c.handle(pkt)
  }
}

这里我们使用了 bufio.Reader 来减少 read() 系统调用的次数,并尽可能多地读取 buf 中缓冲区大小所允许的数量。在这个无限循环中,我们等待新数据的到来。请先记住这句话: 等待新数据的到来 。我们稍后会回顾。

我们先不考虑传入的数据包的解析和处理,因为它对我们讨论的优化并不重要。但是, buf 值得我们关注:默认情况下,它是 4 KB,这意味着连接还需要 12 GB 的内存。 writer 也有类似的情况:


// Channel's writing goroutine.
func (c *Channel) writer() {
  // 创建一个缓冲 write 来减少 write 的系统调用
  buf := bufio.NewWriter(c.conn)

  for pkt := range c.send {
    _ := writePacket(buf, pkt)
    buf.Flush()
  }
}

我们通过 Channel 的 c.send 遍历将数据包传出 并将它们写入缓冲区。细心的读者可能猜到了,这是我们 300 万个连接的另外 12 GB 的内存消耗。

2.3 HTTP

已经实现了一个简单的 Channel ,现在我们需要使用 WebSocket 连接。由于仍然处于常用的方式的标题下,所以我们以常用的方式继续。

注意:如果你不知道 WebSocket 的运行原理,需要记住客户端会通过名为 Upgrade 的特殊 HTTP 机制转换到 WebSocket 协议。在成功处理 Upgrade 请求后,服务端和客户端将使用 TCP 连接来传输二进制的 WebSocket 帧。 这里 是连接的内部结构的说明。


// 常用的转换为 WebSocket 的方法
import (
  "net/http"
  "some/websocket"
)

http.HandleFunc("/v1/ws", func(w http.ResponseWriter, r *http.Request) {
  conn, _ := websocket.Upgrade(r, w)
  ch := NewChannel(conn)
  //...
})

需要注意的是, http.ResponseWriterbufio.Readerbufio.Writer (均为 4 KB 的缓冲区)分配了内存,用于对 *http.Request 的初始化和进一步的响应写入。

无论使用哪种 WebSocket 库,在 Upgrade 成功后, 服务端在调用