Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

异步的优势 #4

Open
taoso opened this issue Sep 25, 2019 · 61 comments
Open

异步的优势 #4

taoso opened this issue Sep 25, 2019 · 61 comments
Labels

Comments

@taoso
Copy link

taoso commented Sep 25, 2019

go 使用协程+阻塞的模式来处理并发问题。这样的模式虽然对运行时要求很高,但对程序员却非常友好。这样的代码码也非常容易维护。

异步模式最大的问题就是回调嵌套,项目大了根本没法维护。我就是不想用回调方式写业务代码才转 go 的。

你认为这类 go 语言的异步框架有什么优势,要解决什么问题?

@Allenxuxu
Copy link
Owner

@lvht
回调方式确实不利于写复杂的业务逻辑代码。

我开发 gev 的初衷也并不是为了来写业务逻辑,这样的异步框架更适合用来构建更为底层的基础设施,比如反向代理程序、消息队列等。

个人认为,这类 go 异步框架,使用更少的内存,速度更多,适用于一些有特殊需求的场景。

https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/
https://colobu.com/2019/02/23/1m-go-tcp-connection/

@taoso
Copy link
Author

taoso commented Sep 26, 2019

这样的异步框架更适合用来构建更为底层的基础设施,比如反向代理程序、消息队列等。

这一类场景使用 go 并无优势呀。c/c++/rust 都是更好的选择。

@Allenxuxu
Copy link
Owner

开发效率高,代码维护性好,rust我不了解,但开发效率,可维护性远比c/c++好。

@taoso
Copy link
Author

taoso commented Sep 26, 2019

go 本来就是牺牲性能换取可维护性的。对于你提到的场景,都是足够简单,用 c/cxx 开发是没有问题。典型的是 Nginx 和 Envoy。我不赞成一种语言打天下,本来设计哲学就不一样。

@Allenxuxu
Copy link
Owner

go 相对于c/c++性能稍差,但是可维护性高。我尝试在特殊场景平衡两者,而且我并不认为回调会大幅降低可维护性。
gev 足够简洁,并不会带来多少维护负担。

@zhaoke0513
Copy link

这种网络库稳定之后,应该不需要怎么维护吧
你更关心的应该是上层的业务逻辑

@yangjuncode
Copy link

异步对于1M或者以上的长连接是很有必要的

@MrChang0
Copy link

这个issue可以开着做长期讨论

@Allenxuxu
Copy link
Owner

这个issue可以开着做长期讨论

可以,听舒畅的👀

@Allenxuxu Allenxuxu reopened this Oct 24, 2019
@Allenxuxu Allenxuxu pinned this issue Oct 24, 2019
@MrChang0
Copy link

我觉得网络库很大的作用在于把业务和底层的处理隔离开来,在远古时期使用c/c++没有网络库的情况下还需要手动的listen,bind巴拉巴拉等等反智的行为,并且有太多的tcp细节需要处理。即使是到了go的时代,如果一个server还是从listen,accept开始写,这种行为依然是反智的。重复的底层劳动没有意义,把这些操作封装起来反复使用并且及时维护,这就是网络库存在的价值。

@shaoyuan1943
Copy link

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

@taoso
Copy link
Author

taoso commented Dec 9, 2019

如果一个server还是从listen,accept开始写,这种行为依然是反智的。

@MrChang0 这种反智代码能占不了多少行吧

@yiippee
Copy link

yiippee commented Dec 9, 2019

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

@taoso
Copy link
Author

taoso commented Dec 9, 2019

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

Go 语言就是用来解决反智的异步回调问题的。本项目又用 go 重新撸了一个异步回调框架……

@shaoyuan1943
Copy link

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

我不是很能理解作者的意图。goroutine的benchmark我们在1.5版本的时候就做过,那个时候goroutine所占用的内存就已经相当相当少了,使用者几乎不用做任何优化,其实从官方说法上就能知道:goroutine几乎不会有内存瓶颈。

@taoso
Copy link
Author

taoso commented Dec 9, 2019

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

我不是很能理解作者的意图。goroutine的benchmark我们在1.5版本的时候就做过,那个时候goroutine所占用的内存就已经相当相当少了,使用者几乎不用做任何优化,其实从官方说法上就能知道:goroutine几乎不会有内存瓶颈。

作者的意图很明显——撸轮子

@Allenxuxu
Copy link
Owner

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

我不是很能理解作者的意图。goroutine的benchmark我们在1.5版本的时候就做过,那个时候goroutine所占用的内存就已经相当相当少了,使用者几乎不用做任何优化,其实从官方说法上就能知道:goroutine几乎不会有内存瓶颈。

https://colobu.com/2019/02/23/1m-go-tcp-connection/

@ghost
Copy link

ghost commented Apr 14, 2020

协成多了cpu在调度上花费很多,目前正在做一个长连接接入服务,一条连接有两个协成分别进行读写,没请求的话单机能抗400w连接,但是实际场景是一条连接维持20s左右,一条连接平均发1次请求,结果一台机子只能抗住30w连接,qps也才1w多,pprof跑了下,调度占了快一半了。我准备换成这个框架看下效果

@Allenxuxu
Copy link
Owner

协成多了cpu在调度上花费很多,目前正在做一个长连接接入服务,一条连接有两个协成分别进行读写,没请求的话单机能抗400w连接,但是实际场景是一条连接维持20s左右,一条连接平均发1次请求,结果一台机子只能抗住30w连接,qps也才1w多,pprof跑了下,调度占了快一半了。我准备换成这个框架看下效果

😄对框架有疑惑的地方,欢迎提出👏

@ghost
Copy link

ghost commented Apr 14, 2020

websocket升级的时候需要把http header保存下来的,里面的数据后面conn处理请求要用到的,现在看起来没法做到,需要修改框架支持,

@ghost
Copy link

ghost commented Apr 14, 2020

connection应该有个唯一ID,可以提供在这条连接上保存和读取自定义数据的能力,否则需要全局维护连接和数据的对应关系,目前像客户端版本号,平台这些数据还是很需要的,统计用,push的时候也能做到根据自定义条件push
OnMessage 返回发送数据和直接 conn的 Send发送 性能,表现有啥区别吗,conn Send会阻塞吗?有的时候OnMessage并不需要返回数据,这个返回值去掉由用户控制发送时机是否更好

@ghost
Copy link

ghost commented Apr 14, 2020

用websocekt,调用conn send发送消息失败,看了下是因为websocket消息的封装没在protocol的pack里面做,是在on message里面做的,这么做可能是因为websocket的发送是有messageType和data两部分,这个可以通过修改 protocal pack 和 unpack函数的实现来做吧

@Allenxuxu
Copy link
Owner

用websocekt,调用conn send发送消息失败,看了下是因为websocket消息的封装没在protocol的pack里面做,是在on message里面做的,这么做可能是因为websocket的发送是有messageType和data两部分,这个可以通过修改 protocal pack 和 unpack函数的实现来做吧

websocket 相关的,我们在另一个 issue 下讨论,这个 issue 保留讨论异步相关的。
➡️ #33

@ghost
Copy link

ghost commented Apr 14, 2020

服务替换成这个后,同时并发连接数由30w提高的41w,qps 1.5w提高到2w,提升挺大的.缺点就是目前不够完善,需要二次开发才能使用,目前线上服务流量挺大的,也不敢用于生产环境直接测试.

@g302ge
Copy link

g302ge commented Dec 8, 2020

其实最大的好处就是,goroutine 虽然lightweight 但是架不住多,如果成千上万没有问题,但是一百万呢?所以说原生Epoll主要是为了省内存,但是头条的实践已经告诉我们,这个东西毕竟还是goroutine 驱动的,所以会有可能变成串行的

@lesismal
Copy link

lesismal commented Jul 7, 2021

如果看了这些例子,楼主还是坚持认为go由于为了同步优势牺牲了异步姿势导致海量并发的性能损失而无法处理该类业务场景,或者异步库必须得异步,那我不知道该怎么解释了,我只能建议,talk is cheap,还是上代码来对比吧,书上或者日常积累到的观点有很多正确的沉淀,但那并不是全部,尽信书不如无书,在处理具体问题的时候,还是要从实际出发
最近我也确实挺困扰的,因为对于大部分一起讨论的人,并没有自己做过异步网络库和复杂异步协议的实现,所以在聊到这些问题的时候,很参与者其实是欠缺这部分知识的,所以造成很多人get不到问题所在
上面提到的几个知名库里都有异步来优化海量并发方案以及遇到的问题的讨论,有兴趣的话,各位可以也去看看

@lesismal
Copy link

lesismal commented Jul 7, 2021

就技术论技术,说话可能不懂礼貌,如有冒犯,再请见谅!
也是以前在些老论坛上养成的习惯,那时候还没这么多语言、框架,做网络层、做框架的人也多,聊问题也都是针对技术本身针锋相对互相“撕”得不亦乐乎,大家对于有趣的技术问题根本不care面子,越是容易撕得激烈,越是能把问题辩得透彻,到最后有结论或者共识,大家反而关系更融洽。军队也好团队也罢,高效的组织或者社区,都是需要热烈甚至激烈的讨论争论,客客气气探讨,所有人都持有共同高度的技术话语权,然后小白更看不懂对错、只知道看遣词造句的优雅然后跟着附庸点赞,然后反而可能让错误的观点获得更多的同意。
所以抱歉归抱歉,请见谅也是真心的,无恶意也是真的,所以我也不太会改
有的人可能会觉得我这种属于口出狂言,没办法,如果这样说的人有一天技术又精进了,或许也就get到我的苦楚了

@taoso
Copy link
Author

taoso commented Jul 7, 2021

@lesismal

所以说,你可能没仔细分析我说的“业务逻辑层同步”'

你说的应该就是 reactor 模式。本质上是把网络IO/协议编解码跟业务逻辑处理分开。两部分都可以用并发的方式实现,但本质都是池子。如果业务逻辑用同步的方式,还是有阻塞的问题。以前是用线程池实现,现在用协程实现,本质都是池子。业务处理的吞吐量一定小于网络处理的吞吐量,主要矛盾在业务处理这一端。另一端用不用异步根本没有太大区别。

如果真是要处理你所谓的海量并发,那一定是协议处理跟业务处理分开。有单独的长连接集群只负责处理网络IO和协议解析,最终把业务数据转发给后台业务逻辑集群来处理。但这么一来,长连接相关的服务就变得相对简单(因为只有IO和编解码),就不一定非得用 go 来实现了。Nodejs,甚至 c/c++ 都可以胜任。

@lesismal
Copy link

lesismal commented Jul 7, 2021

Nodejs,甚至 c/c++ 都可以胜任

c/c++做这些还是要回调,nodejs有了Promise、async/await虽然看上去舒服了一点,但是仍然也是设计回调的时序问题,比如一个Promise:

function callback() {
    ...
}
Promise(callback)
print("log before callback")

写法上看上去是先调用callback了?但实际上是先执行的是print,callback应该是在nodejs/libuv的下一次event loop中执行
这时候看上去的同步实际要考虑异步的执行的问题,写起来比go的同步要考虑的多的多
所以c/c++、nodejs们并不能在 这些地方比go做得好

如果业务逻辑用同步的方式,还是有阻塞的问题“

所以说,还请再看下我昨晚上的内容:

有人可能会说,标准库每个连接都是一个协程,每个连接上的请求的处理都不会被阻塞,异步库+协程池的方案中协程池数量少有可能被阻塞——这也是理解错误的。比如标准库1w连接数,1w个协程,业务需要数据层操作,虽然每个协程都会被调度,但是他们使用的公共的数据层连接池不可能有1w个这么大,比如sql,可能size几十几百的连接池,1w个连接中有5000个消息需要请求sql,那这5000个仍然要在数据库连接池的层面排队等待

造成阻塞的,是那些真正的公共资源比如数据库、缓存、rpc service,这些涉及io的基础设施。而对于golang,不管是标准库一连接一协程,还是异步网络库+协程池(size可以自定制),业务协程数量都可以合理高于基础设施的可并发量,比如sql连接池100,连接数100k,业务协程池5000,这是足够用的

c/c++,nodejs,同样也是会在公共资源这里受限,在受限的公共资源基础之上,更加高效地利用软硬件资源的角度,golang异步库+业务池是最平衡的一种方案。
当然,也如我前面提到的,并发量不那么大的时候,异步库性能没有优势,因为连接数不大所以能节省的内存也就那么点

@lesismal
Copy link

lesismal commented Jul 7, 2021

前面聊的基本都是基于web方面的,再举个例子,比如游戏,即使用golang做游戏,也不是用全同步的,因为游戏的交互复杂,并不全是“请求-响应”这种交互,还有很多主动推送。
比如广播:

for _, c := allConnections {
    c.Write(...)
}

如果是直接对每个连接这样Write,其中某些连接可能网络状况不好、拥塞导致tcp窗口满了之类的,c.Write阻塞了,阻塞1s,则这个广播,其他的连接数都被阻塞,这是一次广播阻塞一轮,而且实际生产环境有的连接可能不只阻塞1s,我遇到过阻塞15s然后因为Deadline而close的。

SetDeadline也解决不了广播的问题

for _, c := allConnections {
    // 这里即使加上deadline也不能解决,因为对于高实时性的游戏业务,即使设置100ms的deadline也是不合理的
    c.SetWriteDeadline(...)
    c.Write(...)
}

所以,游戏服务如果使用标准库的方案,通常每个连接需要开两个协程,一个读,另一个select {<-chan default} 接收message然后再发送,这样才能确保广播不被阻塞
多种业务场景看下来,其实golang标准库也不能通用针对所有场景避免异步,只是web领域http这种简单协议适用罢了

@lesismal
Copy link

lesismal commented Jul 7, 2021

多种业务场景看下来,其实golang标准库也不能通用针对所有场景避免异步,只是web领域http这种简单协议适用罢了

所以,楼主来聊异步的优势的观点其实是有点搞反了:因为看到了gev这些异步库,觉得异步反倒不如标准库方案
但问题的根本,是源于golang标准库方案的缺陷导致不能满足更多业务场景的需要,所以才有人来搞异步框架,从老外的evio开始,只是网络库层面的,gobwas+netpoll这些也都在尝试websocket层面的支持但他们没能解决线头阻塞的问题
“异步的优势”是应该被用来解释为什么我们搞异步框架,而不是用来反向说明不应该用golang搞异步框架

@lesismal
Copy link

lesismal commented Jul 7, 2021

再补充一些。
C/C++性能更好的场景,比如基础设施,比如nginx,虽然好,但是用它来定制更多功能还是很麻烦。如果用C/C++写,开发难度大效率低、招人难维护代码也难。nodejs这种就更不用提了,不适合用于高性能基础设施的场景,脚本弱类型callback hell更不行。
有人可能会说nginx lua的方案、性能不错而且能热更,但是lua也是有硬伤的,毕竟脚本,虽然在js v8之前lua 的jit在所有脚本里一骑绝尘,但是只支持5.1,单lua stack可用内存有限、好像大概2G左右,而且单lua stack不能并发,别以为nginx多进程多个worker就能充分利用cpu核心数了,这跟lua在这里的局限是两个层次,多进程的多核虽然并行了,但是单个nginx worker的逻辑线程上的这个lua stack中如果有阻塞的业务逻辑,它还是要阻塞的,那就意味着这个worker逻辑线程也阻塞了,所以lua里还是要callback,或者就阻塞着并且意味着单个worker内的逻辑单核无法并发,请注意这里的多worker并行和单worker逻辑线程并发是不一样的、是两个层次的性能问题。而如果换成golang,不存在这些槽点和短板。

@lesismal
Copy link

lesismal commented Jul 7, 2021

再补充一些。
C/C++性能更好的场景,比如基础设施,比如nginx,虽然好,但是用它来定制更多功能还是很麻烦。如果用C/C++写,开发难度大效率低、招人难维护代码也难。nodejs这种就更不用提了,不适合用于高性能基础设施的场景,脚本弱类型callback hell更不行。
有人可能会说nginx lua的方案、性能不错而且能热更,但是lua也是有硬伤的,毕竟脚本,虽然在js v8之前lua 的jit在所有脚本里一骑绝尘,但是只支持5.1,单lua stack可用内存有限、好像大概2G左右,而且单lua stack不能并发,别以为nginx多进程多个worker就能充分利用cpu核心数了,这跟lua在这里的局限是两个层次,多进程的多核虽然并行了,但是单个nginx worker的逻辑线程上的这个lua stack中如果有阻塞的业务逻辑,它还是要阻塞的,那就意味着这个worker逻辑线程也阻塞了,所以lua里还是要callback,或者就阻塞着并且意味着单个worker内的逻辑单核无法并发,请注意这里的多worker并行和单worker逻辑线程并发是不一样的、是两个层次的性能问题。而如果换成golang,不存在这些槽点和短板。

所以你看阿里有搞openresty,但是呢?他们现在中间件团队又大量用go在搞。c/c++ bind lua我也搞了很多,直到遇到了golang,再也没回头过,有了golang,其他语言不值得。
以前1000k确实是golang的短板,但是我们这些人,又撸出来了这么多框架,至少我自己的tls/http1x/websocket的支持都比较完善了,够用了,golang 1000k也不再是短板了。
golang,值得

@lesismal
Copy link

lesismal commented Jul 7, 2021

说了这么多,刚才review了下gev的websocket,是异步的stream parser,没有gobwas+netpoll那种可能导致服务慢、服务不可用的缺陷,挺好,已star

@lesismal
Copy link

lesismal commented Jul 7, 2021

七周七并发这种书,适合用来了解大概,但太浅了,真的搞,就还是CSAPP APUE UNP linux内核那些老书,“啃老”啃明白点,就不会被那些除了独孤九剑的各大派花架子剑谱忽悠了

@lesismal
Copy link

lesismal commented Jul 7, 2021

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

我不是很能理解作者的意图。goroutine的benchmark我们在1.5版本的时候就做过,那个时候goroutine所占用的内存就已经相当相当少了,使用者几乎不用做任何优化,其实从官方说法上就能知道:goroutine几乎不会有内存瓶颈。

https://colobu.com/2019/02/23/1m-go-tcp-connection/

1m-go-tcp-connection 没有实现streaming-parser,gobwas/ws+netpoll 那个也是,慢连接随便就能把他们的服务搞死,写个简单代理中转数据的时候完整包拆分中间加点延时就能随便复现。我最近几个月给他们解释了好几次了但get到的人很少,作者们竟然还曾提出用SetDeadline之类的来解决,搞得我自己都郁闷了

@rfyiamcool
Copy link
Contributor

rfyiamcool commented Jul 13, 2021

@lvht rawepoll 还是很有必要的. 你不认同rawepoll, 很大原因是你没遇到高并发和大量长连接的场景。 社区中为啥uber, mosn和字节跳动都有使用 rawepoll 框架? 都是为了解决性能问题。

@taoso
Copy link
Author

taoso commented Jul 13, 2021

@lvht rawepoll 还是很有必要的. 你不认同rawepoll, 很大原因是你没遇到高并发和大量长连接的场景。 社区中为啥uber, mosn和字节跳动都有使用 rawepoll 框架? 都是为了解决性能问题。

@rfyiamcool 没有不认同 epoll。我只是认为如果 rawepoll 是强需求,那可能 go 就不是最好的选择。

@lesismal
Copy link

@lvht rawepoll 还是很有必要的. 你不认同rawepoll, 很大原因是你没遇到高并发和大量长连接的场景。 社区中为啥uber, mosn和字节跳动都有使用 rawepoll 框架? 都是为了解决性能问题。

mosn那个rawpoll好像还没覆盖全业务,可能还是缺少很多异步parser吧,他们使用的easygo性能比较一般,而且使用起来也是比较麻烦:
https://github.com/lesismal/go_network_benchmark/issues/1

@lesismal
Copy link

@lvht rawepoll 还是很有必要的. 你不认同rawepoll, 很大原因是你没遇到高并发和大量长连接的场景。 社区中为啥uber, mosn和字节跳动都有使用 rawepoll 框架? 都是为了解决性能问题。

@rfyiamcool 没有不认同 epoll。我只是认为如果 rawepoll 是强需求,那可能 go 就不是最好的选择。

应该可以干翻java netty、nodejs那些了,go的方案能达到性能和开发效率各方面都很平衡,极致性能是比c/c++/rust要差一些,但已经非常好了

@lesismal
Copy link

lesismal commented Jul 13, 2021

异步库性能并不是一定强于标准库,影响二者性能方面比较多,比如:

  1. 标准库方案与异步库方案连接数量阈值
  2. 异步解析的复杂度
  3. 如果io和业务协程池分开又涉及跨协程传递的额外调度亲和性降低
  4. 不同业务类型(cpu消耗和io消耗)对于整体调度的成本考量
  5. 如果异步库使用内存池,则又涉及跨协程内存传递与生命周期管理,还有跨协议栈/层数据传递与生命周期管理,很复杂
  6. 还有异步库由于不同协议栈/层与业务层之间的跨协程,导致小对象(比如Conn)难于精确释放,比如poller层面收到需要close的信号时如果释放小对象并归还到Pool,应用层此时可能仍然持有该对象,如果应用层释放前该对象又被其他地方取出,则对象脏了会出问题。并且框架层难于要求业务层去保障这个释放时机,如果要求应用层按照一定的方式使用,既难又又业务不友好,所以这些基础小对象类型比较难用Pool优化
    fasthttp那种仍然是一个连接对应一个协程,它不管是小对象还是内存buffer,用池优化都相对容易,异步框架Pool优化的难度大很多

但总归来讲,对于海量并发,至少内存的节约是很客观的,协程数量节约到一个阈值后调度成本的节约也能让异步库性能优势更好

以上都是我最近几个月实现和优化的时候遇到的问题,还有一些优化空间,还有其他一些影响的点

@lockp111
Copy link

@lvht rawepoll 还是很有必要的. 你不认同rawepoll, 很大原因是你没遇到高并发和大量长连接的场景。 社区中为啥uber, mosn和字节跳动都有使用 rawepoll 框架? 都是为了解决性能问题。

@rfyiamcool 没有不认同 epoll。我只是认为如果 rawepoll 是强需求,那可能 go 就不是最好的选择。

u1s1,是不是最好的选择应该由使用者决定,而不是由语言决定,使用者要经过多方面的考虑:团队能力、规模、业务变动、拓展维护......
任何语言的标准库也许不是性能最好的,但兼容性是最好的,适用于大部分场景,所以像1000k go这种就不在考虑范围,当遇到了只能自己解决。
我觉得异步库确实很有必要,因为在高负载情况下,gorouting调度会有问题,如果是用标准库的话,高并发场景下会出现长尾效应,而异步是可以改善这些情况的。
go的设计是让我们更方便的使用gorouting,但方便不是滥用,过多的gorouting在高性能场景下依然是不健康的,所以会看到很多大项目在高性能场景下的优化就是减少gorouting、内存复用、减少GC等等......不至于一有性能问题就换语言吧。

@wwhai
Copy link

wwhai commented Dec 5, 2021

评论精彩!

@woshihuo12
Copy link

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

一百万的goroutine会不会给go的调度带来问题?

@woshihuo12
Copy link

woshihuo12 commented Apr 2, 2022

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

我不是很能理解作者的意图。goroutine的benchmark我们在1.5版本的时候就做过,那个时候goroutine所占用的内存就已经相当相当少了,使用者几乎不用做任何优化,其实从官方说法上就能知道:goroutine几乎不会有内存瓶颈。

https://colobu.com/2019/02/23/1m-go-tcp-connection/

1m-go-tcp-connection 没有实现streaming-parser,gobwas/ws+netpoll 那个也是,慢连接随便就能把他们的服务搞死,写个简单代理中转数据的时候完整包拆分中间加点延时就能随便复现。我最近几个月给他们解释了好几次了但get到的人很少,作者们竟然还曾提出用SetDeadline之类的来解决,搞得我自己都郁闷了

感觉没必要郁闷,你去攻击他们服务器一次,该郁闷的就是他们了。

@lesismal
Copy link

lesismal commented Apr 2, 2022

我有个疑问,net包里的TCPConn本身就是基于epoll进行封装的,作者为何基于原始epoll-go接口来写gev?

net包基于epoll封装了自己的go网络模型,可以同步的方式达到异步的效果。gev是纯异步非阻塞的。在绝大部分场景没啥区别,且go网络模型更好用,但是如果是长连接且活跃的连接不多,异步网络模型只是hold住那些不活跃的连接,而go网络模型会分配goroutine,所以相对内存消耗会多一些,但真的多不了多少,简单算一个goroutine 2k,一百万才2G。。。 一百万连接的应用能有几个啊,哈哈哈

一百万的goroutine会不会给go的调度带来问题?

百万goroutine倒也是可以,有些厂是单进程80-100w连接/协程的,但需要很好的硬件配置,并且再高就更难了,交互频率大了不只是调度,内存、gc的压力大、stw会更明显

@gukz
Copy link

gukz commented May 1, 2022

感谢各位大佬们的精彩讨论!!

@szl-926929
Copy link

@lesismal 大佬,啥时候写点这方面的博客,系统介绍介绍

异步库性能并不是一定强于标准库,影响二者性能方面比较多,比如:

  1. 标准库方案与异步库方案连接数量阈值
  2. 异步解析的复杂度
  3. 如果io和业务协程池分开又涉及跨协程传递的额外调度亲和性降低
  4. 不同业务类型(cpu消耗和io消耗)对于整体调度的成本考量
  5. 如果异步库使用内存池,则又涉及跨协程内存传递与生命周期管理,还有跨协议栈/层数据传递与生命周期管理,很复杂
  6. 还有异步库由于不同协议栈/层与业务层之间的跨协程,导致小对象(比如Conn)难于精确释放,比如poller层面收到需要close的信号时如果释放小对象并归还到Pool,应用层此时可能仍然持有该对象,如果应用层释放前该对象又被其他地方取出,则对象脏了会出问题。并且框架层难于要求业务层去保障这个释放时机,如果要求应用层按照一定的方式使用,既难又又业务不友好,所以这些基础小对象类型比较难用Pool优化
    fasthttp那种仍然是一个连接对应一个协程,它不管是小对象还是内存buffer,用池优化都相对容易,异步框架Pool优化的难度大很多

但总归来讲,对于海量并发,至少内存的节约是很客观的,协程数量节约到一个阈值后调度成本的节约也能让异步库性能优势更好

以上都是我最近几个月实现和优化的时候遇到的问题,还有一些优化空间,还有其他一些影响的点

@lesismal
Copy link

lesismal commented Sep 8, 2022

@szl-926929
去年只做了TCP/TLS/HTTP1.x/Websocket,还想完善更多比如HTTP2.0、HTTP3.0/QUIC,怕现在先整理了内容后面又升级了。HTTP2.0比较渣,做api并不比Websocket强,做文件服务器4层线头阻塞问题还不如1.x。
最近有人提3.0/QUIC的需求,QUIC是基于UDP,我之前没考虑支持UDP,因为标准库的UDP已经足够方便,封装起来很简单,所以没必要在poller库里支持,而且我的库早期定位就是TCP相关,所以提供的接口比如OnOpen/OnClose也是针对TCP,对于UDP本身是不存在这两种接口的。但是QUIC确实挺好,而且目前社区里go实现的QUIC跟TCP也类似,海量并发场景都会消耗巨大数量的协程甚至比TCP消耗得更多。我倒是挺想以后有档期支持下QUIC的,为了与已有的能整合到一起,所以最近也在给自己的库增加UDP的支持,但是不像其他poller框架那样只是支持io、每次读到数据还需要用户自己去判断UDP 4层的映射关系,我默认支持了这种4层映射,实现方式也简单,就是对端发来第一个UDP包时OnOpen,之后每次OnData回调时传递给用户的Conn都是同一个,SetDeadline相关超时或业务层关闭之类的就OnClose,这样对使用者来说更有好些。先把UDP弄上,HTTP3.0/QUIC是个大活,暂时还没排期,以后有档期了撸一波

golang的异步库其实跟c/c++实现类似,但是协程、内存池的策略细节更多些,全写够出本书了,暂时没那么多精力,异步这块内容有兴趣的话欢迎先读读我源码吧

@kscooo
Copy link

kscooo commented Sep 8, 2022

@szl-926929 去年只做了TCP/TLS/HTTP1.x/Websocket,还想完善更多比如HTTP2.0、HTTP3.0/QUIC,怕现在先整理了内容后面又升级了。HTTP2.0比较渣,做api并不比Websocket强,做文件服务器4层线头阻塞问题还不如1.x。 最近有人提3.0/QUIC的需求,QUIC是基于UDP,我之前没考虑支持UDP,因为标准库的UDP已经足够方便,封装起来很简单,所以没必要在poller库里支持,而且我的库早期定位就是TCP相关,所以提供的接口比如OnOpen/OnClose也是针对TCP,对于UDP本身是不存在这两种接口的。但是QUIC确实挺好,而且目前社区里go实现的QUIC跟TCP也类似,海量并发场景都会消耗巨大数量的协程甚至比TCP消耗得更多。我倒是挺想以后有档期支持下QUIC的,为了与已有的能整合到一起,所以最近也在给自己的库增加UDP的支持,但是不像其他poller框架那样只是支持io、每次读到数据还需要用户自己去判断UDP 4层的映射关系,我默认支持了这种4层映射,实现方式也简单,就是对端发来第一个UDP包时OnOpen,之后每次OnData回调时传递给用户的Conn都是同一个,SetDeadline相关超时或业务层关闭之类的就OnClose,这样对使用者来说更有好些。先把UDP弄上,HTTP3.0/QUIC是个大活,暂时还没排期,以后有档期了撸一波

golang的异步库其实跟c/c++实现类似,但是协程、内存池的策略细节更多些,全写够出本书了,暂时没那么多精力,异步这块内容有兴趣的话欢迎先读读我源码吧

udp 今天刚过一个提案,不知道是否有帮助。大佬感觉可以出一个博客系列讲解下 golang 原生网络方案和你实现的 asyncio + callback(parse) + groutine pool 的架构上的不同,可以讲粗一点,细节可以一笔带过让感兴趣的啃源码。

@lesismal
Copy link

lesismal commented Sep 8, 2022

@kscooo
我们这些poller框架在架构上与标准库的整体异同其实比较简单,主要就几个部分:

  1. 同步与异步:syscall层面,标准库与poller框架,都是异步的IO
  2. 阻塞与非阻塞:标准库的File与runtime结合、提供给应用层的是阻塞的IO接口,比如TCPConn.Read/Write,而poller框架提供的是非阻塞的接口
  3. 协议解析:标准库是同步读、顺序解析、自上而下,half-packet只要ReadFull等读全了就行,更容易;poller框架需要考虑half-packet缓存、多次解析,更麻烦
  4. 协程数量:标准库每个连接至少1vN协程,poller框架连接数与协程数量可以使MvN,M可以远大于N
  5. 内存优化:标准库就是每个连接的协程里对应需要多少就用多少,优化方式是go的一些基本姿势比如尽量复用buffer以及减少gc的一些策略;poller框架则类似c/c++了,但是也需要结合sync.Pool或者其他,会更麻烦一点,这个不同业务可以有不同的策略来优化

但是每个部分涉及到的细节真要展开说、讲清楚,其实也确实有点难,我不太擅长,所以好多次提笔写了些然后又停下了

其实如果有一定网络基础的兄弟直接读源码可能理解得更快些,如果有疑问随便开issue交流就好了

udp这个提案我也订阅下看看后续

@kscooo
Copy link

kscooo commented Sep 8, 2022

@lesismal 谢谢,我之前也研究过一阵社区的实现,大概的原理我应该还能了解,但是像 nbio、宇宙条做的一些优化暂时还没看,等有机会把这个坑填了看能不能写个文章系统介绍下。大佬可以开个 tg/slack 或者微信群方便交流,不过这方面可能基于 issues 这种文字讨论更不容易灌水。

@lesismal
Copy link

lesismal commented Sep 8, 2022

@kscooo
slack试试这个,欢迎来撩:https://join.slack.com/t/arpcnbio/shared_invite/zt-1esuz619h-SbKacrYHNdbkNmO9SV~X1A
免费版邀请链接之前好像是30天有效期现在15天了:joy:

宇宙条的优化方向不太一样,他们的netpoll我实测性能没有达到预期,不知道是不是我代码写得不正确:
lesismal/go-net-benchmark#1

而且他们netpoll在连接数多、高并发时的占用甚至比标准库高得多,而且对于单个连接,他们好像并不是为了解决海量并发的问题、而是为了尽量提高他们的kitex rpc框架的响应性能,而且交流过程中他们负责人提过kitex也不是针对连接数很多的场景。
整体而言,他们的netpoll并不适合更通用的业务场景比如面向公网(网络质量不稳定)的服务。
之前给他们提过issue也没什么下文我就没继续研究了,有兴趣可以到他们repo里搜下我相关的issue。

所以他们的netpoll其实和其他的poller框架没什么可比性,完全是为了解决不同问题的框架,为了提高而牺牲的东西都不一样。

可能是他们结合kitex做了深度的协议解析的内存复用之类的、针对他们的场景能够提高性能。我跑他们自己的测试时kitex的性能一般、kitex-mux数据不错,但是他们的测试代码也并不是完全的对齐,比如kitex-mux用的连接池size设置为2、和别人不一样,他们只压测少量连接数的响应数据、并没有压测更多连接数时的server响应数据,而实际场景中不可能每个server只处理2个连接。而且目前除了我自己的arpc,其他rpc框架基本都是handler返回即调用结束、所有handler都是异步、不支持简单cpu消耗的handler直接同步返回,这样会浪费一些协程切换和逃逸之类的响应性能,实测这种简单cpu消耗的handler用arpc同步处理也确实比他们数据好得多,arpc这块相关的刚好这两天有人问到:https://github.com/lesismal/arpc/issues/41#issuecomment-1236634004, 而且推送之类的都支持、比普通rpc框架支持的业务场景丰富得多。

@lxzan
Copy link

lxzan commented May 29, 2023

@lvht rawepoll 还是很有必要的. 你不认同rawepoll, 很大原因是你没遇到高并发和大量长连接的场景。 社区中为啥uber, mosn和字节跳动都有使用 rawepoll 框架? 都是为了解决性能问题。

@rfyiamcool 没有不认同 epoll。我只是认为如果 rawepoll 是强需求,那可能 go 就不是最好的选择。

对于没有 c++/rust 程序员的公司, 纯go方案就是最好的选择

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests