Golang的GPM 模型在网络编程中存在的问题
By admin
- One minute read - 159 words现状
目前在网络编程中,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.gFree
和 sched.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 占用无法恢复
参考资料
- 当一个goroutine 运行结束后会发生什么
- 创建一个goroutine都经历了什么?
- Go语言并发调度器和网络模型解析
- Go netpoller 原生网络模型之源码全面揭秘
- Goroutine 并发调度模型深度解析之手撸一个高性能 goroutine 池