Golang的GPM 模型在网络编程中存在的问题

目录

现状

目前在网络编程中,golang采用的是一种 goroutine-per-connection 的模式,即为每一个连接都分配一个goroutine,一个连接就是一个goroutine,多个连接之间没有关系。

package main

import (
	"fmt"
	"io/ioutil"
	"net"
	"time"
)

//模拟server端
func main() {
	tcpServer, _ := net.ResolveTCPAddr("tcp4", ":8080")
	listener, _ := net.ListenTCP("tcp", tcpServer)

	for {
		//当有新客户端请求时拿到与客户端的连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println(err)
			continue
		}

		// 处理逻辑 goroutine-per-connection
		go handle(conn)
	}
}

func handle(conn net.Conn) {
	defer conn.Close()

	//读取客户端传送的消息
	go func() {
		response, _ := ioutil.ReadAll(conn)
		fmt.Println(string(response))
	}()

	//向客户端发送消息
	time.Sleep(1 * time.Second)
	now := time.Now().String()
	conn.Write([]byte(now))
}

这种模式看起来非常的舒服,简单易懂,大大减少了开发者的心智负担,深受开发者的喜爱。

存在的问题

对于 goroutine-per-connection 这种模式,真的就不有一点问题的吗?

假如现在只有两个 P,正常情况下每秒可以处理 10000 个 goroutine 连接。突然有1秒并发量极其的大,此时有 100万 个请求发送过来(极少出现这种极端情况,但并不表示不存在),根据 goroutine-per-connection 模式服务端会为这些连接创建 100万 个 goroutine,每个goroutine表示一个请求连接。

在上节《创建一个goroutine都经历了什么?》我们介绍goroutine创建的时候,讲过当创建一个goroutine 的时候,会检查是否有空间的goroutine可以复用,只有在没有的情况下才会创建一个新的goroutine,否则就直接复用空闲的goroutine。如果这时减去复用的goroutine,至少也有 99万 个连接goroutine存在。

一秒过后,此时又会慢慢恢复到正常的处理水平,即只有 10000 个连接处于活跃状态。而根据《当一个goroutine 运行结束后会发生什么》可知,当一个goroutine结束后,并没有对 goroutine 进行直接销毁回收,而是将其放在了 gFree 列表里(这里的gFree包含 p.gFreesched.gFree 列表)以便后期创建goroutine时可以复用。那么在下次GC之前,系统里将会一直有 99万 个空闲的goroutine存在,每个goroutine会占用 2K 大小内存,存在着严重的资源浪费的情况。

当有GC发生时,只会释放掉sched.gFree.stack 列表中g的stack,然后再将这些g放入 sched.gFree.noStack 列表中,但这些 noStack 列表中的g仍然无法回收。。

虽然上面这种情况很少发生,但并不表示不存在。一旦发生这种情况,将无法解决,除非重启服务。

如何解决

针对上面讲的这种情况有没有相应解决方案呢?

最容易想到的办法,就是在连接端进行并发量限制,即我们平时说的限流,减少创建goroutine的数量,当达到一定数量的goroutine 的时候就拒绝请求,直到有可用goroutine复用。但这种方式又有些浪费服务器资源,因为服务器明明有这种处理能力,却为了一些特殊情况而限制了服务器资源的充分利用。这种方式有眯像捡芝麻丢西瓜的样子。

还有没有其它更好的办法了呢?假如我们打破上面提到的 goroutine-per-connection 模式,让每个 goroutine 负责多个连接的话,会不会彻底解决呢?其实就是实现 IO多路复用。这里推荐实现这种模式的一个网络框架库 https://github.com/panjf2000/gnet ,这个库正是打破目前这种模式的代表,目前已在国内多家大型互联网公司的生产环境中使用,对它的介绍见官方文档 https://gnet.host/docs/about/what-is-gnet/,作者是一位就职于腾讯的工程师。

另个还推荐一个此作者的另一个库 ants ,是一个实现 goroutine池库,有兴趣的也可以看看它的实现源码。

总结

那么是不是说上面提到的 goroutine-per-connection 模式就没有必要使用了呢?其实不是的,要知道我们上面提到的情况是很少见,一般是不会出现的,且这种模式理解起来简单易懂,每个开发者都很熟悉,所以平时开发中还是推荐这种模式的。

可以看一下曹大以前遇到的此类问题: 为什么 Go 模块在下游服务抖动恢复后,CPU 占用无法恢复

参考资料