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

2020-01-28 13:45:43丽君
responseWriter.Hijack() 之后都会收到 I/O 缓冲区和 TCP 连接。

提示:在某些情况下, go:linkname 可被用于通过调用 net/http.putBufio {Reader, Writer} 将缓冲区返回给 net/http 内的 sync.Pool

因此,我们还需要 24 GB 的内存用于 300 万个连接。

那么,现在为了一个什么功能都没有的应用程序,一共需要消耗 72 GB 的内存!

3. 优化

我们回顾一下在简介部分中谈到的内容,并记住用户连接的方式。在切换到 WebSocket 后,客户端会通过连接发送包含相关事件的数据包。然后(不考虑 ping/pong 等消息),客户端可能在整个连接的生命周期中不会发送任何其他内容。

连接的生命周期可能持续几秒到几天。

因此,大部分时间 Channel.reader()Channel.writer() 都在等待接收或发送数据。与它们一起等待的还有每个大小为 4 KB 的 I/O 缓冲区。

现在我们对哪些地方可以做优化应该比较清晰了。

3.1 Netpoll

Channel.reader() 通过给 bufio.Reader.Read() 内的 conn.Read() 加锁来 等待新数据的到来 (译者注:上文中的伏笔),一旦连接中有数据,Go runtime(译者注:runtime 包含 Go 运行时的系统交互的操作,这里保留原文)“唤醒” goroutine 并允许它读取下一个数据包。在此之后,goroutine 再次被锁定,同时等待新的数据。让我们看看 Go runtime 来理解 goroutine 为什么必须“被唤醒”。

如果我们查看 conn.Read() 的实现 ,将会在其中看到 net.netFD.Read() 调用 :


// Go 内部的非阻塞读.
// net/fd_unix.go

func (fd *netFD) Read(p []byte) (n int, err error) {
  //...
  for {
    n, err = syscall.Read(fd.sysfd, p)
    if err != nil {
      n = 0
      if err == syscall.EAGAIN {
        if err = fd.pd.waitRead(); err == nil {
          continue
        }
      }
    }
    //...
    break
  }
  //...
}

Go 在非阻塞模式下使用套接字。 EAGAIN 表示套接字中没有数据,并且读取空套接字时不会被锁定,操作系统将返回控制权给我们。(译者注:EAGAIN 表示目前没有可用数据,请稍后再试)

我们从连接文件描述符中看到一个 read() 系统调用函数。如果 read 返回 EAGAIN 错误 ,则 runtime 调用 pollDesc.waitRead() :


// Go 内部关于 netpoll 的使用
// net/fd_poll_runtime.go

func (pd *pollDesc) waitRead() error {
  return pd.wait('r')
}

func (pd *pollDesc) wait(mode int) error {
  res := runtime_pollWait(pd.runtimeCtx, mode)
  //...
}

如果 深入挖掘 ,我们将看到 netpoll 在 Linux 中是使用 epoll 实现的,而在 BSD 中是使用 kqueue 实现的。为什么不对连接使用相同的方法?我们可以分配一个 read 缓冲区并仅在真正需要时启动 read goroutine:当套接字中有可读的数据时。