Allenxuxu / gev

🚀Gev is a lightweight, fast non-blocking TCP network library / websocket server based on Reactor mode. Support custom protocols to quickly and easily build high-performance servers.
MIT License
1.73k stars 194 forks source link

异步的优势 #4

Open taoso opened 5 years ago

taoso commented 5 years ago

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

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

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

Allenxuxu commented 5 years ago

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

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

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

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

taoso commented 5 years ago

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

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

Allenxuxu commented 5 years ago

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

taoso commented 5 years ago

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

Allenxuxu commented 5 years ago

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

zhaoke0513 commented 5 years ago

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

yangjuncode commented 5 years ago

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

MrChang0 commented 5 years ago

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

Allenxuxu commented 5 years ago

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

可以,听舒畅的👀

MrChang0 commented 5 years ago

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

shaoyuan1943 commented 4 years ago

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

taoso commented 4 years ago

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

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

yiippee commented 4 years ago

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

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

taoso commented 4 years ago

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

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

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

shaoyuan1943 commented 4 years ago

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

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

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

taoso commented 4 years ago

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

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

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

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

Allenxuxu commented 4 years ago

我有个疑问,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 commented 4 years ago

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

Allenxuxu commented 4 years ago

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

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

ghost commented 4 years ago

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

ghost commented 4 years ago

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

ghost commented 4 years ago

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

Allenxuxu commented 4 years ago

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

websocket 相关的,我们在另一个 issue 下讨论,这个 issue 保留讨论异步相关的。 ➡️ https://github.com/Allenxuxu/gev/issues/33

ghost commented 4 years ago

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

g302ge commented 3 years ago

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

taoso commented 3 years ago

网络层只处理数据包的解析,解析好了丢到chan里给业务层的协程池消费者,消费者那块可以是同步的。 可以每个connection按自己的hash丢到指定的协程处理,这样可以保证单个连接消息处理的时序性 业务层按照不同的功能模块做自己功能模块的协程池,不同的模块之间只处理生产消费的消息投递就简单了

@lesismal 问题是网络层解包时间跟整个请求的处理时间相比几乎可以忽略不计。如果选用异步的方式解包,假设速度很快,但后续的同步处理过程依然很慢,那请求依然会阻塞住。php 圈的 swoole 也采用这种方式。把网络协议部分跟业务逻辑部分拆开基本没什么收益(除非你的业务逻辑很少)。

Nginx 用异步为什么会快呢?因为它是 proxy,收到请求后立刻转发给 upstream,自己本身没有可以阻塞整个过程的复杂逻辑。说白了就是处理过程非常简单。所以,nginx 用纯异步逻辑实现也没什么问题。

如果你的业务功能非常简单,但是需要处理高并发场景,那使用异步方式一点问题都没有。典型的场景就是推送服务。但这一类服务不一定非得用 go 去实现。据我所知,用 nodejs 就很溜。纯异步回调,根本没有协程调度开销。如果硬核一点,基于 libevent 或者 libuv 用 c 语言撸一把也是没有问题的。

我开这个 issue 主要是想表达「不同的语言有不同的范式」。这一条我也是从《七周七语言》那边学来的。那 go 语言的范式是什么呢?我认为是并发编程(但不一定就是高并发编程)。

最早我们是用多进程实现并发。但进程间通信(共享或者同步)就不那么容易,而且资源占用和调度成本都非常的高。后期人们引入了线程,可以共享部分数据,减少资源占用,但调度成本依然很高。等到了 go,人们使用协程来代替线程,再一次降低调度成本。整个过程一直在变,但不变的是什么呢?不变的是同步的编程风格。同步编程就是根据业务需要线性地处理各种业务逻辑。这种方式最贴近人脑的思考方式,最容易开发,最容易理解,最容易调试。

异步编程只能说是一种权宜之计,是一种无奈的选择。什么 callback/promise/async/await,抛个异常连函数调用栈都找不到。要不是「走头无路」,谁会去死磕异步编程。

总之,异步编程费脑子,同步编程费资源。但成年人的世界「全都要」。Go就是为了满足广大成年人的需求而设计的。一方面用协程减少资源消耗,另一方面将异步封装起来,让程序员可以用同步的方式编写代码。因为 Go 语言在「费脑子」和「费资源」之间取得了一种平衡,所以它跟异步编程相比就有点费资源了。

说这么多是为了想表达,go 的协程在设计的时候是为了解决异步编程的可维护性问题,付出的代价是消耗更多的资源(相对而言,比线程和进程方案要很很多)。这就是 go 语言的范式。我们不能一边享受着协程的便利,一边喷协程比不上异步编程的效率。

楼主 @Allenxuxu 搞 gev 也不是没有意义,只要适应的场景,就可以发挥作用。我们不应该陷入意识形态问题的。但所谓「树业有专攻」,语言也是一样。作为程序员,我们应该多掌握几种不同编程范式的语言,根据不同的使用场景选择最合适的才会事半功倍。

MrChang0 commented 3 years ago

芜湖,我也推荐一个很值得学习的开源项目skynet,加一个GettingStarted

lesismal commented 3 years ago

网络层只处理数据包的解析,解析好了丢到chan里给业务层的协程池消费者,消费者那块可以是同步的。 可以每个connection按自己的hash丢到指定的协程处理,这样可以保证单个连接消息处理的时序性 业务层按照不同的功能模块做自己功能模块的协程池,不同的模块之间只处理生产消费的消息投递就简单了

@lesismal 问题是网络层解包时间跟整个请求的处理时间相比几乎可以忽略不计。如果选用异步的方式解包,假设速度很快,但后续的同步处理过程依然很慢,那请求依然会阻塞住。php 圈的 swoole 也采用这种方式。把网络协议部分跟业务逻辑部分拆开基本没什么收益(除非你的业务逻辑很少)。

Nginx 用异步为什么会快呢?因为它是 proxy,收到请求后立刻转发给 upstream,自己本身没有可以阻塞整个过程的复杂逻辑。说白了就是处理过程非常简单。所以,nginx 用纯异步逻辑实现也没什么问题。

如果你的业务功能非常简单,但是需要处理高并发场景,那使用异步方式一点问题都没有。典型的场景就是推送服务。但这一类服务不一定非得用 go 去实现。据我所知,用 nodejs 就很溜。纯异步回调,根本没有协程调度开销。如果硬核一点,基于 libevent 或者 libuv 用 c 语言撸一把也是没有问题的。

我开这个 issue 主要是想表达「不同的语言有不同的范式」。这一条我也是从《七周七语言》那边学来的。那 go 语言的范式是什么呢?我认为是并发编程(但不一定就是高并发编程)。

最早我们是用多进程实现并发。但进程间通信(共享或者同步)就不那么容易,而且资源占用和调度成本都非常的高。后期人们引入了线程,可以共享部分数据,减少资源占用,但调度成本依然很高。等到了 go,人们使用协程来代替线程,再一次降低调度成本。整个过程一直在变,但不变的是什么呢?不变的是同步的编程风格。同步编程就是根据业务需要线性地处理各种业务逻辑。这种方式最贴近人脑的思考方式,最容易开发,最容易理解,最容易调试。

异步编程只能说是一种权宜之计,是一种无奈的选择。什么 callback/promise/async/await,抛个异常连函数调用栈都找不到。要不是「走头无路」,谁会去死磕异步编程。

总之,异步编程费脑子,同步编程费资源。但成年人的世界「全都要」。Go就是为了满足广大成年人的需求而设计的。一方面用协程减少资源消耗,另一方面将异步封装起来,让程序员可以用同步的方式编写代码。因为 Go 语言在「费脑子」和「费资源」之间取得了一种平衡,所以它跟异步编程相比就有点费资源了。

说这么多是为了想表达,go 的协程在设计的时候是为了解决异步编程的可维护性问题,付出的代价是消耗更多的资源(相对而言,比线程和进程方案要很很多)。这就是 go 语言的范式。我们不能一边享受着协程的便利,一边喷协程比不上异步编程的效率。

楼主 @Allenxuxu 搞 gev 也不是没有意义,只要适应的场景,就可以发挥作用。我们不应该陷入意识形态问题的。但所谓「树业有专攻」,语言也是一样。作为程序员,我们应该多掌握几种不同编程范式的语言,根据不同的使用场景选择最合适的才会事半功倍。

这个帖子我之前回复过很多,但是后来提到自己的项目,觉得不礼貌,所以都删了,顺便给 @Allenxuxu 抱歉,请见谅。 今天又浏览到这里,忍不住想再说一次: 楼主对golang、异步网络层和阻塞、甚至对架构的理解虽然比较不错了,但还没get到更深入的点。 前面有提到如果是异步,c++之类的更好这是不对的。c++那些没有协程,所以处理业务还是要异步,但是go有协程,网络库这一层可以异步,业务层仍然可以同步。 golang标准库的方式,每个连接一个协程,海量连接比如100k、1000k的场景下,协程本身的消耗太巨大了,调度成本也高,很不划算。现在各种云,go号称云语言,但是真的搞那么多云基础设施的话,go标准库方案的内存就要浪费非常多,往小了说那是成本问题,往大了说那是环境问题。 举个例子为什么go异步网络库同样可以写同步逻辑: 网络层异步,解析到一个完整的message,丢给协程池去处理,协程池数量合理,协程池内的业务逻辑仍然是同步的、跟标准库方案没什么差别。有人可能会说,标准库每个连接都是一个协程,每个连接上的请求的处理都不会被阻塞,异步库+协程池的方案中协程池数量少有可能被阻塞——这也是理解错误的。比如标准库1w连接数,1w个协程,业务需要数据层操作,虽然每个协程都会被调度,但是他们使用的公共的数据层连接池不可能有1w个这么大,比如sql,可能size几十几百的连接池,1w个连接中有5000个消息需要请求sql,那这5000个仍然要在数据库连接池的层面排队等待。但是异步库的方案,可能业务协程池是1000,poller协程只有8/16,可以节省出非常多的协程数量、内存,同配置的机器可以承载更大的业务量级。而且1w/10k这种都是十几年前的老问题了,现在谁还谈10k,都是100k、1000k,你想想如果是云厂商基础设施,某些业务几百甚至数千、万台服务器,会省多少?而且关键是不仅省了,业务层还是像标准库那样写同步逻辑呀!——不信你可以去看我的仓库 并且,不只是 @Allenxuxu ,老外的圈子里,一样很多人在寻找异步库的解决方案来应对100k、1000k的问题 https://github.com/eranyanay/1m-go-websockets https://github.comgobwas/ws https://github.com/gorilla/websocket

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

二楼对golang异步库的自我否定让人非常失望。。。:joy:

我知道在别人项目里提到自己项目不礼貌,但没有恶意,只是想多交流、多回馈社区。我的go异步库已经支持了tls/http1.x/websocket这些,近几个月有看到上面那些知名的库竟然不知道他们方案的缺陷会导致服务响应慢甚至不可用、容易被慢连接攻击,去给他们提了issue或者在issue中解释,甚至很多人不理解到底是什么问题,我也是郁闷了

另外, @lvht 别说什么七周七并发,那基本是把语言或者框架都拉高到了接口表现形式的层面来讨论差别,而高并发高性能的根本、从来都是系统资源的高效合理利用,对于现代操作系统,高效意味着不管应用层是什么形式、底层离不开poller异步,再往上是语言或者框架的呈现能力。golang的协程是对传统异步语言或者手动档协程语言的呈现能力的扩展,扩展并不意味着golang就扔掉了传统语言异步的能力,而是,golang可以把各种姿势的能力相结合、组合起来使用——就如我上面解释到的“异步网络库-同步逻辑层”那样。

再次拜托各位对go持有类似错误观念的同学:协程是对业务层同步能力的扩展,不是对异步能力的阉割!

taoso commented 3 years ago

hi @lesismal ,不能认同你的一些观点,回复如下:

今天又浏览到这里,忍不住想再说一次: 楼主对golang、异步网络层和阻塞、甚至对架构的理解虽然比较不错了,但还没get到更深入的点。

建议就事论事,不要评价个人能力。不过这是题外话。

前面有提到如果是异步,c++之类的更好这是不对的。c++那些没有协程,所以处理业务还是要异步,但是go有协程,网络库这一层可以异步,业务层仍然可以同步。

我并不是说只要是异步,c++之类一定更好。我想表达的意思是如果没有协程,异步只能用回调的方式来实现。如果你只是基于 websocket 写个聊天服务器,用回调没有问题;如果只是想实现类似 nginx 那样的高性能 http 服务器,回调更是首选。但你的业务非常复杂,代码本身分了好几层,需要反复调用数据库、缓存、外部接口,那么基于回调的写法就会变成所谓的「回调地狱」。只间的一次调用出错,你连调用栈都找不到。这就是异步模式性能好,资源消耗低所需要付出的代价。

golang标准库的方式,每个连接一个协程,海量连接比如100k、1000k的场景下,协程本身的消耗太巨大了,调度成本也高,很不划算。现在各种云,go号称云语言,但是真的搞那么多云基础设施的话,go标准库方案的内存就要浪费非常多,往小了说那是成本问题,往大了说那是环境问题。

对于 100k 或者 1000k 的场景,协程消耗是巨大的,这一点我们没有分歧。

二楼对golang异步库的自我否定让人非常失望。。。😂

我不是否定 golang 的异步库,我是认为异步编程模式本身有其适应场景,并非银弹。

另外, @lvht 别说什么七周七并发,那基本是把语言或者框架都拉高到了接口表现形式的层面来讨论差别,而高并发高性能的根本、从来都是系统资源的高效合理利用,对于现代操作系统,高效意味着不管应用层是什么形式、底层离不开poller异步,再往上是语言或者框架的呈现能力。golang的协程是对传统异步语言或者手动档协程语言的呈现能力的扩展,扩展并不意味着golang就扔掉了传统语言异步的能力,而是,golang可以把各种姿势的能力相结合、组合起来使用——就如我上面解释到的“异步网络库-同步逻辑层”那样。

再次拜托各位对go持有类似错误观念的同学:协程是对业务层同步能力的扩展,不是对异步能力的阉割!

我在这里特别提到《七周七语言》是为了表达不同的语言有不同的范式。

我开这个 issue 主要是想表达「不同的语言有不同的范式」。这一条我也是从《七周七语言》那边学来的。那 go 语言的范式是什么呢?我认为是并发编程(但不一定就是高并发编程)。

以上是针对具体观点的回复。下面我再次总结一下自己的观点。

异步回调模式性能好、资源消耗少,但不于开发和维护复杂的业务系统;同步模式(不论是多进程、多线程)逻辑简单,但资源消耗大,难以处理高并发场景。而 go 语言选择了内置协程方式,其实是在异步和同步之间选了一个平衡点。所以 go 兼具异步和同步两种模式的优点,但也继承了两者的缺点。

内置协程,go 可以处理较大规模的并发,同时保留同步模式的简单易维护的特性。从某种程度上看简单和高并改是矛盾的,在设计的时候只能有所舍。

但是,如果我们只拿 go 的并发性能跟异步回调的性能去比的话,难免有失公平。因为 go 在设计上就选择了牺牲一定的性能来换取长期的可维护性。在硬件性价比如此高的今天,我认为这种策略是合理的。

我们选择用 go 肯定是因为 go 的设计符合我们的大多数业务场景。硬要拿 go 去处理 100k 或者 1000k 的场景不是不可以,只是有更好的语言可以用。就像我们完全可以用 go 实现一个 web 服务器(比如 candy),但不论怎么优化,性能上肯定比不过用 c 实现的 nginx(因为垃圾回收、只能通过栈传参等)。因为 go 本来就为了其他一些目标故意牺牲了一些性能。

另外,我也分析了异步模式的一些局限。异步不适合写复杂的业务系统。对于简单的长连接类的服务,我们又有很多其他语言可选,甚至连 nodejs 都能很好地处理一些高并发问题。如果对性能有极致要求,完全可以用 c 实现,自己控制内存分配和事件调度。但不论 http 协议多庞大,作了一个 web 服务器,nginx 的逻辑在本质上是简单的,所以用 c 实现所付出的维护性代价是值得的。

最后总结一下,在语言选择上没有银蛋,在系统设计上没有银蛋。不论设计什么系统,我们始终要考虑我们选择了什么和牺牲了什么,投入产出比怎样。如果我们手里只有锤子,那所有的问题都会变成钉子。

lesismal commented 3 years ago

建议就事论事,不要评价个人能力。不过这是题外话。

抱歉,我没有恶意,只是根据你的观点,觉得你没get到,而且看完上一楼的回复,还得再说一次:你可能还是没理解我上面说的“业务逻辑层同步“。如果觉得我这样讲是不尊重,那我只能再抱歉一次。 至于异步调用栈都找不到,这是底层库层面的,比如协议解析后message传递给业务协程池去处理,解析中你调用栈找不到了,但是并不影响你业务逻辑层上面的调试,协议解析层交给实现着就行了、绝大多数人写nodejs也并不需要去调试libuv。并且,如果真想调试协议解析层,断点提前、放到协议解析那块就好了。

但是,如果我们只拿 go 的并发性能跟异步回调的性能去比的话,难免有失公平。因为 go 在设计上就选择了牺牲一定的性能来换取长期的可维护性。在硬件性价比如此高的今天,我认为这种策略是合理的。

异步库的方案,对比标准库一连接一协程的方案,存在一个连接数的阈值,并不是异步库一定就性能优势,异步库的优势主要是降低内存占用、超大并发情况下的优势,单就10k,可能标准库的响应速度更快,因为数据的读取、解析没有断层、都是单协程内完成的、同一段buffer也可以复用很多次。单就内存优化来讲,比如内存池: fasthttp等项目开了个pool的好头,但是对于异步网络库高度定制内存管理等场景,不够通用。 标准库方案的那种一个连接一个协程,就单个连接而言,在内存管理、pool定制上比异步库更方便,buffer多是在单个协程内、处理完一个消息后才会进行下一轮ReadMessage,所以内存管理更可控,异步库内存生命周期管理起来比一个连接一个协程麻烦太多了。 简单定制的tcp、固定header 那种要省力得多,tls/http/websocket解析起来太麻烦了,跨协议栈不同层之间的数据传递和cache,尤其是慢连接,比如测试解析逻辑时一个字节一个字节发送,再加上可能跨协程的buffer传递,还有解析到一个协议包后之前整段buffer的分割之后还需要粘包和cache等问题,结合2^N的pool的方案在应对慢连接时大部分性能都消耗在内存的使用上了,反倒性能不好。所以最近把http/websocket的内存池改了,不在使用的2^N对齐的buffer,换成无固定长度buffer的方式了,不保证一对一的分配和释放,只是尽量在各个协议层复用pool中的buffer

我们选择用 go 肯定是因为 go 的设计符合我们的大多数业务场景。硬要拿 go 去处理 100k 或者 1000k 的场景不是不可以,只是有更好的语言可以用。

这么说吧,如果你只是拿来跟c比,这句话没问题。但问题是,标准库方案,1000k的表现,go不如java、nodejs,这是很不好的。而c/cpp的框架,业务开发实在是不够友好,单说性能go做不到c/cpp/rust,但是异步库支持完善后,至少能赶上、超过java、node,这是go作为云语言应该做到的事情,而不是只去对比性能极致、开发效率不友好的那几种语言

因为 go 本来就为了其他一些目标故意牺牲了一些性能。 另外,我也分析了异步模式的一些局限。异步不适合写复杂的业务系统。对于简单的长连接类的服务,我们又有很多其他语言可选,甚至连 nodejs 都能很好地处理一些高并发问题。如果对性能有极致要求,完全可以用 c 实现,自己控制内存分配和事件调度。但不论 http 协议多庞大,作了一个 web 服务器,nginx 的逻辑在本质上是简单的,所以用 c 实现所付出的维护性代价是值得的。

讲了这么多,至少给我的感觉是:你的观点仍然是go为了同步优势牺牲了异步姿势,所以不应该用go去做海量并发业务,用其他语言就行了。 所以说,你可能没仔细分析我说的“业务逻辑层同步” 上代码吧:


mux := &http.ServeMux{}
// 这里的handler,兼容标准库,handler内的业务逻辑,都是同步的,跟标准库方案的写法一样,并不需要改成异步去实现
mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(time.Now().Format("20060102 15:04:05")))
})

// 异步库的server svr := nbhttp.NewServer(nbhttp.Config{ Network: "tcp", Addrs: []string{"localhost:8888"}, }, mux, nil) // pool.Go)

err := svr.Start() if err != nil { fmt.Printf("nbio.Start failed: %v\n", err) return }

lesismal commented 3 years ago

上面例子中的http handler,是可以定制执行者的,你可以让它在poller协程上直接执行,比如nginx那种主要做cpu消耗网关、代理等类型的业务,这样减少跨协程数据传递的麻烦,也能方便你调试完整的调用栈,至于异步解析的复杂肯定是要比同步的实现复杂一些、遇到半包数据时调试解析部分的调用栈也会复杂些,但是多数人并不需要调试这个解析层的内容 如果不定制,例子中的http handler默认在业务协程池中执行,当然你也可以定制一个自己的协程池来执行handler函数,但业务代码仍然是同步逻辑、不需要改成异步

lesismal commented 3 years ago

再举个websocket的代码例子:

func onWebsocket(w http.ResponseWriter, r *http.Request) {
    upgrader := &websocket.Upgrader{EnableCompression: true}
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        panic(err)
    }
    wsConn := conn.(*websocket.Conn)

        // 这里的消息处理内部业务逻辑,仍然是同步的,当然对于websocket,你也可以把这里解析到的message丢给其他业务模块的协程去处理、回复或者不回复,或者主动推送
    wsConn.OnMessage(func(c *websocket.Conn, messageType websocket.MessageType, data []byte) {
        // echo
        c.WriteMessage(messageType, data)
    })

    wsConn.OnClose(func(c *websocket.Conn, err error) {
            fmt.Println("OnClose:", c.RemoteAddr().String(), err)
    })
}

我这里还有很多例子: https://github.com/lesismal/nbio/tree/master/examples

lesismal commented 3 years ago

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

lesismal commented 3 years ago

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

taoso commented 3 years ago

@lesismal

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

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

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

lesismal commented 3 years ago

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 commented 3 years ago

前面聊的基本都是基于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 commented 3 years ago

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

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

lesismal commented 3 years ago

再补充一些。 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 commented 3 years ago

再补充一些。 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 commented 3 years ago

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

lesismal commented 3 years ago

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

lesismal commented 3 years ago

我有个疑问,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 commented 3 years ago

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

taoso commented 3 years ago

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

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

lesismal commented 3 years ago

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

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

lesismal commented 3 years ago

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

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

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

lesismal commented 3 years ago

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

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

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

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

lockp111 commented 3 years ago

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

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

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

wwhai commented 2 years ago

评论精彩!