【Go 夜读】第 65 期 Go 网络编程:Go 原生同步网络模型解析 vs Multi-Reactors 异步网络模型

yangwen13 · · 1283 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

>文章来自于:https://reading.developerlearning.cn/reading/65-2019-10-31-go-net/ 分享者:潘建锋@亚马逊 ## 观看视频 https://youtu.be/4QurJJHuxaQ ## Go 夜读第 65 期 Go 原生网络模型 vs 异步 Reactor 模型 本期 Go 夜读是由 Go 夜读 SIG 核心小组邀请到潘建锋给大家分享 Go 原生网络模型 vs 异步 Reactor 模型,以下是本次分享的部分内容和 QA 。 >潘建锋,曾任职腾讯、现亚马逊在职。Go 语言业余爱好者,开源库 [gnet](https://github.com/panjf2000/gnet) 和 [ants](https://github.com/panjf2000/ants) 作者。 ## 引子 我们都知道 Golang 基于 goroutine 构建了一个简洁而优秀的原生网络模型,让开发者能够用同步的模式去编写异步的逻辑:goroutine-per-connection 模式,极大地降低了开发者编写网络应用时的心智负担,而且借助于 Go Scheduler 对 goroutines 的高效调度,这个原生网络模型足以应对绝大部分的应用场景。 然而,在工程性上能做到如此高的普适和兼容,给开发者提供如此简单易用的接口,其背后必然是基于非常复杂的封装,做了很多取舍,放弃了一些『极致』的概念和设计。事实上 Golang 的 netpoll 底层就是基于 epoll/kqueue/iocp 这些系统调用来做封装的,最终暴露出 goroutine-per-connection 这样的网络编程模式给开发者。 在绝大部分应用场景下,我推荐大家还是遵循 Golang 的 best practices,以这种模式来构建自己的网络应用,然而,在某些极度需要提高性能、节省资源以及技术栈必须是原生 Go (不考虑 C/C++ 写中间层供 Go 调用)的场景下,我们可以考虑自己构建 Reactor 网络模型。那么,Reactor 模型相对原生模型有哪些优势和弊端呢?我开发了的一个基于事件驱动机制的实验性质的异步网络框架:gnet,其在性能和资源占用上都远超 Go 原生 net 包(少数特定的应用场景),通过解析这个框架和 Go 原生网络模型,我们来一一分析~~ 预备知识:epoll、非阻塞IO、IO多路复用, [Linux IO模式及 select、poll、epoll详解](https://segmentfault.com/a/1190000003063859) ### 分享 Slides - https://taohuawu.club/static_res/html/webslides/gnet/gnet.html ## Q&A 总结 ### Q1: 为什么 gnet 会比 Go 原生的 net 包更快? 答: Multi-Reactors 模型相较于 Go 原生模型在以下场景具有性能优势: 1. 高频创建新连接:我们从源码里可以知道 Go 模式下所有事件都是在一个 epoll 实例来管理的,接收新连接和 IO 读写;而在 Reactors 模式下,accept 新连接和 IO 读写分离,它们在各自独立的 goroutines 里用自己的 epoll 实例来管理网络事件。 2. 海量网络连接:Go net 处理网络请求的模式是 goroutine per connection,甚至是 multiple goroutines per connection,而 gnet 一般使用与机器 CPU 核心数相同的 goroutines 来处理网络请求,所以在海量网络连接的场景下 gnet 更节省系统资源,进而提高性能。 3. 时间窗口内连接总数大而活跃连接数少:这种场景下,Go 原生网络模型因为 goroutine per connection 模式,依然需要维持大量的 goroutines 去等待 IO 事件(保持 1:1 的关系),Go scheduler 对大量 idle goroutines 的调度势必会损耗系统整体性能;而 gnet 模式下需要维护的仅仅是与 CPU 核心数相同的 goroutines,而且得益于 Reactors 模型和 epoll/kqueue,可以确保每个 goroutine 在大多数时间里都是在处理活跃连接。 4. 短连接场景:gnet 内部维护了一个内存池,在短连接这种场景下,可以大量复用内存,进一步节省资源和提高性能。 ### Q2: Go netpoll 源码里的 waitRead 方法到底是起什么作用? 答: 看源码: ```go // func (fd *FD) Read(p []byte) (int, error) for { n, err := syscall.Read(fd.Sysfd, p) if err != nil { n = 0 if err == syscall.EAGAIN && fd.pd.pollable() { if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } // On MacOS we can see EINTR here if the user // pressed ^Z. See issue #22838. if runtime.GOOS == "darwin" && err == syscall.EINTR { continue } } ... // func netpollblock(pd *pollDesc, mode int32, waitio bool) bool // need to recheck error states after setting gpp to WAIT // this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl // do the opposite: store to closing/rd/wd, membarrier, load of rg/wg if waitio || netpollcheckerr(pd, mode) == 0 { gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5) } ``` 通过分析 conn.Read(),我们知道这个方法是同步的,但从源码我们可以看出,Go 使用的是非阻塞 IO,所以调用 syscall.Read 的时候并不会阻塞,所以实际上它是通过 waitRead 这个方法来实现阻塞的:netFD 的 Read 操作在系统调用Read后,当遇到 syscall.EAGAIN 时,waitRead 里面的 netpollblock 会调用 gopark 将当前读这个网络描述符的 goroutine 给 park 住,直到这个网络描述符上的读事件再次发生为止,唤醒 goroutine,waitRead 调用返回,回到外层的 for 循环继续执行。conn.Write 方法和 Read 的实现原理是一样的,都是在发生syscall.EAGAIN 错误的时候将当前 goroutine 给 park 住直到 socket 再次可写为止。 ### Q3: Go 的网络模型有『惊群效应』吗? 答:没有。 我们看下源码里是怎么初始化 listener 的 epoll 示例的: ```go var serverInit sync.Once func (pd *pollDesc) init(fd *FD) error { serverInit.Do(runtime_pollServerInit) ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd)) if errno != 0 { if ctx != 0 { runtime_pollUnblock(ctx) runtime_pollClose(ctx) } return syscall.Errno(errno) } pd.runtimeCtx = ctx return nil } ``` 这里用了 sync.Once 来确保初始化一次 epoll 实例,这就表示一个 listener 只持有一个 epoll 实例来管理网络连接,既然只有一个 epoll 实例,当然就不存在『惊群效应』了。 ### Q4: Multiple Reactors + Goroutine-Pool Model 这个模式,把阻塞的任务放入Goroutine-Pool,但是如果 Response 依赖于阻塞任务返回的结果(比如依赖于一个http请求结果),这种情况Goroutine-Pool 是不是意义不大了? gnet 提供了异步写的 API: AsyncWrite,一般都是在 goroutine pool 里处理完阻塞逻辑之后直接调用这个方法把 response 写回 socket,总之,原则就是不能阻塞 eventloop goroutine,也就是 gnet.React 方法。 ### Q5: > 1. 潘少说go-net原生的网络模型相当于单reactor的模型,每一个连接一个goroutine来处理,由go的调度器实现高并发,这样应该也能利用上多核CPU的吧?为什么性能比multi-reactors的方式差这么多? > 2. multi-reactors的主reactor万一挂了,怎么办?(类似单点故障问题) > 3. multi-reactors + goroutine pool的模式下,subreactor负责输入输出,goroutine pool负责计算,若某个任务的数据量比较大,从subreactor到goroutine pool或从goroutine pool到subreactor的数据传输成本会不会很大? 答: 问题 1 参见上面第一个我回答的问题;问题 2 说的 Reactor 单点问题的确是存在的,因为 gnet 使用的是主从 Reactors 模式,main reactor 只有一个,所以的确存在这个潜在的问题,解决办法也有:使用多 acceptors,利用 SO_REUSEPORT 参数让内核帮你做 load balancing 避免惊群;至于问题 3 :并不存在数据传输成本,从当前 eventloop goroutine 也就是 gnet.React 方法里一般是用 closure 闭包的方式提交任务到 goroutine pool 的,是引用方式。 ### Q6: goroutine pool如何把数据送回到subreactor? 当你在独立的 goroutine 里完成你的阻塞逻辑之后得到了 response 数据,直接调用 [AsyncWrite](https://github.com/panjf2000/gnet/blob/master/connection.go#L162): ```go func (c *conn) AsyncWrite(buf []byte) { if encodedBuf, err := c.loop.svr.codec.Encode(buf); err == nil { _ = c.loop.poller.Trigger(func() error { if c.opened { c.write(encodedBuf) } return nil }) } } ``` 通过 closure 的方式,写一个唤醒事件到 epoll,同时传一个 func() error 到任务队列,在 sub reactor 的那个 goroutine 里执行这个函数,把数据写回 client。 ### Q7: 在等待传回的这段时间,subreactor是不是还是得阻塞着,无法处理其他请求 不会阻塞啊,此时 React 方法已经返回了,你的阻塞逻辑是提交到 goroutine pool 里处理,处理完直接调用 AsyncWrite 异步写回去了,方式就是我上面说的,写一个唤醒事件到 epoll,在 eventloop goroutine 里执行,所以不会有同步问题。 ## 回看视频 - https://www.bilibili.com/video/av74598921 - https://youtu.be/4QurJJHuxaQ ## 参考资料 - [A Million WebSockets and Go](https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/) - [Going Infinite, handling 1M websockets connections in Go](https://speakerdeck.com/eranyanay/going-infinite-handling-1m-websockets-connections-in-go) - [百万 Go TCP 连接的思考: epoll方式减少资源占用](https://colobu.com/2019/02/23/1m-go-tcp-connection/) - [gnet: 一个轻量级且高性能的 Golang 网络库](https://taohuawu.club/go-event-loop-networking-library-gnet) ---

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1283 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传