lesismal / nbio

Pure Go 1000k+ connections solution, support tls/http1.x/websocket and basically compatible with net/http, with high-performance and low memory cost, non-blocking, event-driven, easy-to-use.
MIT License
2.15k stars 151 forks source link

accept sleep and error handling #337

Closed shaovie closed 1 year ago

shaovie commented 1 year ago

抱歉,我无意评论了一篇文章,没有恶意啊,咱只讨论技术 先说第一个问题:

  1. Accept中为啥Sleep() ? (标准库中使用了这不是理由啊)
lesismal commented 1 year ago

Hi, 欢迎来交流!

最初我也没加sleep,是抄标准库的: https://github.com/golang/go/blob/master/src/net/http/server.go#L3071

shaovie commented 1 year ago

不是说人家有咱就抄嘛, 而且你也抄错了嘛,你判断的是 ne.Timeout() 标准库是, Temporary(),这两个是完全不一样的含义的, 什么情况下会timeout呢?系统调用accept 可没这个errno, 有可能你这个代码永远不会触发,

标准库判断 Temporary,(就是我说的EAGAIN 这个错误),是因为它是个阻塞式的accept(当然,内部还是用了poller),而且它有continue渐进式的尝试,不是一口气就sleep 50毫秒,这样太影响吞吐量了, 即便它内部有poller,还是有可能返回syscall.EAGAIN,下图就是最底层的accept部分

image

标准库这样处理,因为它是一个server的代码,并不是一个框架代码,它应该只是起保保险作用,我并不觉得这是合理的处理方案,(标准库提供的serve 可不会号称高性能)

shaovie commented 1 year ago

我刚翻了一下代码,标准库中Accept处理 Temporary 采用sleep的方式,应该是处理EMFILE/ENFILE 这个错误,这个错误的意思就是进程/系统句柄耗尽了,这时候它不sleep,也干不了啥

https://go.dev/src/syscall/syscall_unix.go#L129 看这里

lesismal commented 1 year ago

哦不好意思,刚才没细看、没看到这句 “标准库中使用了这不是理由啊”

翻了下源码: image

image

image

这个issue里也有: https://github.com/golang/go/issues/6163

lesismal commented 1 year ago

而且你也抄错了嘛,你判断的是 ne.Timeout()

最初是用的Temporary(),但是: image

而且吧,插件它给我标红提示,强迫症就给改了,你要不提我都把这给忘了,刚才搜了下,还有漏网之鱼: https://github.com/lesismal/nbio/blob/master/poller_kqueue.go#L202

而且它有continue渐进式的尝试,不是一口气就sleep 50毫秒,这样太影响吞吐量了

至于50毫秒,这是accept,accept如此高频的场景并不多,而且如果已经accept error了,就算sleep 50ms也没关系,线上不至于很多节点同时循环多次accept error吧?

lesismal commented 1 year ago

kqueue这个没改,可能是因为我平时windows开发,插件没报所以没注意到,当时也没全局搜,所以只改了windows和linux,而且macos下也主要是调试所以无所谓。

我当时没深究这个问题,今天你追问,又涨姿势了。

所以要考虑下Timeout()要不要改回Temporary(),我先看看改回去插件还报不报。

lesismal commented 1 year ago

另外,用户自己Accept得到conn再加到nbio里来管理也行: https://github.com/lesismal/nbio-examples/blob/master/netstd/server/server.go https://github.com/lesismal/nbio-examples/blob/master/netstd/client/client.go

shaovie commented 1 year ago

kqueue这个没改,可能是因为我平时windows开发,插件没报所以没注意到,当时也没全局搜,所以只改了windows和linux,而且macos下也主要是调试所以无所谓。

我当时没深究这个问题,今天你追问,又涨姿势了。

所以要考虑下Timeout()要不要改回Temporary(),我先看看改回去插件还报不报。

正常accept 是不会出现 timeout这种语义的错误的 只有poller中才会返回timeout,你追一下代码 runtime/netpoll.go:469

lesismal commented 1 year ago

改了一版,现在插件没报了 https://github.com/lesismal/nbio/tree/accept_err

shaovie commented 1 year ago

另外,用户自己Accept得到conn再加到nbio里来管理也行: https://github.com/lesismal/nbio-examples/blob/master/netstd/server/server.go https://github.com/lesismal/nbio-examples/blob/master/netstd/client/client.go

这样的写法,其实有点儿不伦不类,既然实现了poller,完全可以做到完全非阻塞化(标准库accept/connect底层也这样实现的), nbio,里边有太多的锁了,这其实是不应该的

lesismal commented 1 year ago

只有poller中才会返回timeout,你追一下代码 runtime/netpoll.go:469

这个提交改掉的: https://github.com/lesismal/nbio/commit/7eba463da3a64ed95244882db936d51c99aff630#diff-1d08c05b5af7bbda17aa3b31bab702ce07165d6a9fa9fe2489e9168161c2ea84

当时是正在支持udp,改动的地方特别多,插件报红就按提示给改了,没去深究这块,过后都给忘记了,不严谨

shaovie commented 1 year ago

你不信按照 techempower 的标准,写一个最简单的http server,对比测试一下吞吐性能

lesismal commented 1 year ago

既然实现了poller,完全可以做到完全非阻塞化(标准库accept/connect底层也这样实现的)

单就nbio poller管理的conn而言,除了sendfile那块可能会有阻塞,其他用法是非阻塞的。而且sendfile那块通常http单个请求,也不怕单个请求稍微阻塞一下导致这个连接的其他阻塞,nbio目前只支持http1.x,单个连接上的请求本来就是串行处理。协程池size合理就行,如果整个系统几千几万个协程都阻塞到sendfile上,那换标准库问题也可能照旧,只要不是所有逻辑协程都阻塞到sendfile或者其他应用业务的IO或者什么逻辑上,其他连接的请求照常处理。而且搞nbio本来就是为了限制协程数量避免爆资源

这样的写法,其实有点儿不伦不类, nbio,里边有太多的锁了,这其实是不应该的

跟传统的框架比,确实,主要对比两类吧,一类是对比c/cpp,一类是对比标准库。 c/cpp指令快,很多框架逻辑单线程,IO那些异步线程池去搞,http这种服务还可以多进程处理、让每个进程内代码保持简单,但go不一样,go的指令没那么快,如果逻辑单协程,那只能去跟py那些比性能了。要想性能不拉跨,go逻辑多协程是必需品。这就涉及很多跨协程对单个conn的并发操作了。nbio提供的不是设计成只支持http1.x这种无状态串行处理的业务,游戏、IM各种业务,这些服务的不同功能、模块都可能会有对单个conn的并发操作,如果不用锁,而是提供像其他库那样的WriteAsync之类的方法,不符合net.Conn、写起来难受,即使把conn.Write本身就实现异步也不可取,因为异步去处理的过程中buffer的生命周期以及异步化过程中可能有很多这种buffer堆积带来资源压力。 至于对比标准库的,因为本身是为了解决标准库海量连接场景下的问题,所以本身也没有可比性,这里就不说了

如果只考虑IO,很容易做到无锁。但IO后读到的消息如何处理,conn在不同业务系统中可能面对被并发调用如何让让用户简单易用、不增加过多心智负担?

另外nbio.Conn的锁我没拆成读、写两个锁,而是只有一个共同的锁,主要几个考虑:

  1. 主要是非阻塞的,syscall也不会耗时很久
  2. 单个连接上不至于有太高频并发读写,所以实际触发竞争不多
  3. 双工关闭我觉得没什么必要,因为一旦出错,即使双工分开处理也并不能保证4层协议数据可达,不如简化更省心智

所以我觉得没你想得那么简单,或者,如果你觉得哪里可以更好地实现,咱聊具体的实现方案,可能会更容易解释清楚。

lesismal commented 1 year ago

你不信按照 techempower 的标准,写一个最简单的http server,对比测试一下吞吐性能

TechEmpower 是个挺逗逼的测试啊,比如你看 gnet 的readme,在TechEmpower里排名那么牛逼,但这公平吗?你去看下它的测试代码,不是完整的http解析器,回包是类似固定的buffer,都不是真正实现了http功能的代码,去跟别人完整功能的http服务器比性能然后拿到第一,有意义吗?

而且,很多poller框架在io协程里,比如就是在epoll event loop的协程里,直接处理http请求直接回写给对端,这在压测场景还算ok,因为简单的回写不会太耗费cpu,但实际业务中的http handler可能有数据库、下游rpc/http请求,这些可都是慢操作,如果在epoll event loop里去处理http handler,一个请求处理得慢,这个epoll上面管理的其他所有fd就都排队等着,这也是不行的。 epoll的几种模式里,ET+ONESHOT,可以等事件来了为这个fd单起一个协程去处理读、解析、处理请求,这样不至于在epoll event loop协程阻塞、不影响其他fd,但另一个问题来了,ONESHOT每次读完要重新syscall去添加事件,这也影响吞吐。 所以nbio采用的默认方案是LT,就在epoll event loop里处理读和解析,解析到的请求再丢给逻辑协程池去处理。 但是nbio支持多种epoll模式:LT, ET, ET+ONESHOT,用户一行配置就可以切换不同的模式,或者自己去定制实现读取解析和处理的方式,都可以

另外,从操作系统提供的接口上看,应该是只有windows iocp的syscall支持proactor,*nix都是reactor的syscall。但是从框架层看,nbio不只是提供reactor,用户如果想省事就用OnData,否则可以用OnRead自行实现读取,所以proactor/reactor相当于都支持

还有你在知乎上提到的 goev,我去看过了,看了一会,但你觉得它用起来如何?跟nbio的example对比下,用户看到愿意用哪种?

lesismal commented 1 year ago

再继续说锁太多的问题

对于单个连接,高频并发操作它的情况其实并不多、竞争并不大,这时候锁其实主要是原子操作,而且官方这个fast path还可以inline,所以成本并不那么高: https://github.com/golang/go/blob/master/src/sync/mutex.go#L82 https://github.com/golang/go/blob/master/src/sync/mutex.go#L218

反倒是一些无锁的实现,框架层倒是无锁了,也能吹牛自己无锁以及性能了,但是用户真正写业务的时候还是要做很多封装、八成也是要额外加锁。所以你看标准库提供的Fd类的比如Conn、File,基本都是带锁的,内核里这些也是有锁的。 为啥? 因为无锁只适合简单的场景,比如上面提到的只考虑IO部分,或者很多人鼓吹无所队列的那种只是入队出队的场景。 但是只考虑IO正如我前面所说,抛开了逻辑处理的考量,相当于框架层自己图省事、把包袱甩给了业务层开发者,框架层自己能拿来吹牛逼了、但是整个系统并没有减轻负担、反倒是业务层人员增加了负担; 而无所队列,如果只是简单队列那可以用,如果是队列与其他一些功能耦合的,无锁并不能锁过程、不能保证超过原子本身的多步过程的一致性,比如Goroutine Pool 那个issue里我昨天刚好有说到,用cond+queue实现类似chan的功能的场景。

lesismal commented 1 year ago

无锁的还有个典型,gorilla/websocket,虽然不是poller框架本身,但情况是类似的。它提供的websocket.Conn是不支持并发读写的,所以呢,很多人用它,都要自己另行封装:一个读协程只处理读,一个写协程+select chan既能保证串行又能避免阻塞,典型实现比如 melody。

这就是框架层自己无锁,但实际用户使用的时候,还是要额外封装锁或者chan或者其他的机制来保障并发操作单个conn的一致性。除非你的系统简单到爆,无锁才有意义,但nbio提供的是通用框架,如果是内部业务、对性能要求更高,那我当然可以另行定制。

lesismal commented 1 year ago

这样的写法,其实有点儿不伦不类,既然实现了poller,完全可以做到完全非阻塞化(标准库accept/connect底层也这样实现的)

最初看xtaci用RawConn,也觉得还是syscall好些,所以也是自己syscall来搞但并不方便: https://github.com/lesismal/nbio/commit/314d37a1d1eea51a4e7cf864823c2ba90befa41d#diff-1d08c05b5af7bbda17aa3b31bab702ce07165d6a9fa9fe2489e9168161c2ea84

比如现在支持的标准库的conn的转换,用户自己使用标准库很容易上手,你让很多go用户再去写syscall处理这些都会容易劝退,这还没算上tls。

而且多数服务,端口号也不至于太多,每个端口一个协程甚至reuseport多个协程,这点协程资源也不算啥开销,而且通常是吞吐量瓶颈、accept不至于太瓶颈、比syscall也慢不到哪里去,真被ddos那也是防护那些外层需要考虑的、实际服务里自己的accept解决不了这事情。

所以现在这种方式下

lesismal commented 1 year ago

再说一下无锁,无锁性能就一定最高吗,这里有一些对比: https://github.com/lesismal/go-net-benchmark/issues/1

这个测试结果和其他一些poller框架作者的测试结果可能不太一样,比如他们测试可能基本都高于标准库,但我测到的数据有些参数下标准库更高、有些是poller框架更高。 这可能是环境差异,所以我从来不在自己benchmark repo里标榜自己第一,而是每次鼓励用户直接跑代码自己测,代码在那,自己跑的眼见为实,不随便相信一些框架官方提供的数据,自己是运动员又当裁判员,这样不太好

而且,这些测试还是先抛开无锁框架提供给业务开发者后、业务开发者可能额外封装锁之类带来的损耗。所以如果放到实际业务中,无锁框架的表现可能会比nbio更差一些,因为nbio本身已经有锁、不需要再去额外封装保障单个conn并发读写关闭的一致性相关的东西了

shaovie commented 1 year ago

既然实现了poller,完全可以做到完全非阻塞化(标准库accept/connect底层也这样实现的)

单就nbio poller管理的conn而言,除了sendfile那块可能会有阻塞,其他用法是非阻塞的。而且sendfile那块通常http单个请求,也不怕单个请求稍微阻塞一下导致这个连接的其他阻塞,nbio目前只支持http1.x,单个连接上的请求本来就是串行处理。协程池size合理就行,如果整个系统几千几万个协程都阻塞到sendfile上,那换标准库问题也可能照旧,只要不是所有逻辑协程都阻塞到sendfile或者其他应用业务的IO或者什么逻辑上,其他连接的请求照常处理。而且搞nbio本来就是为了限制协程数量避免爆资源

这样的写法,其实有点儿不伦不类, nbio,里边有太多的锁了,这其实是不应该的

跟传统的框架比,确实,主要对比两类吧,一类是对比c/cpp,一类是对比标准库。 c/cpp指令快,很多框架逻辑单线程,IO那些异步线程池去搞,http这种服务还可以多进程处理、让每个进程内代码保持简单,但go不一样,go的指令没那么快,如果逻辑单协程,那只能去跟py那些比性能了。要想性能不拉跨,go逻辑多协程是必需品。这就涉及很多跨协程对单个conn的并发操作了。nbio提供的不是设计成只支持http1.x这种无状态串行处理的业务,游戏、IM各种业务,这些服务的不同功能、模块都可能会有对单个conn的并发操作,如果不用锁,而是提供像其他库那样的WriteAsync之类的方法,不符合net.Conn、写起来难受,即使把conn.Write本身就实现异步也不可取,因为异步去处理的过程中buffer的生命周期以及异步化过程中可能有很多这种buffer堆积带来资源压力。 至于对比标准库的,因为本身是为了解决标准库海量连接场景下的问题,所以本身也没有可比性,这里就不说了

如果只考虑IO,很容易做到无锁。但IO后读到的消息如何处理,conn在不同业务系统中可能面对被并发调用如何让让用户简单易用、不增加过多心智负担?

另外nbio.Conn的锁我没拆成读、写两个锁,而是只有一个共同的锁,主要几个考虑:

  1. 主要是非阻塞的,syscall也不会耗时很久
  2. 单个连接上不至于有太高频并发读写,所以实际触发竞争不多
  3. 双工关闭我觉得没什么必要,因为一旦出错,即使双工分开处理也并不能保证4层协议数据可达,不如简化更省心智

所以我觉得没你想得那么简单,或者,如果你觉得哪里可以更好地实现,咱聊具体的实现方案,可能会更容易解释清楚。

锁,其实仅仅解决了互斥的问题

网络框架 有个最重要的问题,是 tcp 要顺序化写入,要知道每次write,有可能成功一半,所以为什么一般的框架都是poller一个单独的线程,fd跟poller绑定,如果fd 跟poller绑定了,那就不会互斥了

shaovie commented 1 year ago

你不信按照 techempower 的标准,写一个最简单的http server,对比测试一下吞吐性能

TechEmpower 是个挺逗逼的测试啊,比如你看 gnet 的readme,在TechEmpower里排名那么牛逼,但这公平吗?你去看下它的测试代码,不是完整的http解析器,回包是类似固定的buffer,都不是真正实现了http功能的代码,去跟别人完整功能的http服务器比性能然后拿到第一,有意义吗?

而且,很多poller框架在io协程里,比如就是在epoll event loop的协程里,直接处理http请求直接回写给对端,这在压测场景还算ok,因为简单的回写不会太耗费cpu,但实际业务中的http handler可能有数据库、下游rpc/http请求,这些可都是慢操作,如果在epoll event loop里去处理http handler,一个请求处理得慢,这个epoll上面管理的其他所有fd就都排队等着,这也是不行的。 epoll的几种模式里,ET+ONESHOT,可以等事件来了为这个fd单起一个协程去处理读、解析、处理请求,这样不至于在epoll event loop协程阻塞、不影响其他fd,但另一个问题来了,ONESHOT每次读完要重新syscall去添加事件,这也影响吞吐。 所以nbio采用的默认方案是LT,就在epoll event loop里处理读和解析,解析到的请求再丢给逻辑协程池去处理。 但是nbio支持多种epoll模式:LT, ET, ET+ONESHOT,用户一行配置就可以切换不同的模式,或者自己去定制实现读取解析和处理的方式,都可以

另外,从操作系统提供的接口上看,应该是只有windows iocp的syscall支持proactor,*nix都是reactor的syscall。但是从框架层看,nbio不只是提供reactor,用户如果想省事就用OnData,否则可以用OnRead自行实现读取,所以proactor/reactor相当于都支持

还有你在知乎上提到的 goev,我去看过了,看了一会,但你觉得它用起来如何?跟nbio的example对比下,用户看到愿意用哪种?

不能这样看待TechEmpower 测试,平台定了标准,符合要求即可。plaintext测试的就是基础框架的性能,与协议无关(只是http 工具比较成熟罢了 ab wrk),先确保裸框架性能ok,再在此基础上堆应用

shaovie commented 1 year ago

你做了个错误的假设,“不需要再去额外封装保障单个conn并发读写” 有了事件驱动,怎么还会有并发读写呢?这正是框架应该要解决的问题

lesismal commented 1 year ago

网络框架 有个最重要的问题,是 tcp 要顺序化写入,要知道每次write,有可能成功一半,

nbio是直接写,没写完的挂载到这个conn上缓存起来、epoll 加写事件,然后等待可写再写入;在这些缓存的数据没发送完成之前,再次写入就直接加入到缓存里去、不会直接syscall write 这是框架要考虑的基本问题

所以为什么一般的框架都是poller一个单独的线程,fd跟poller绑定,如果fd 跟poller绑定了,那就不会互斥了

nbio conn是fd跟poller绑定的,你这只是考虑单纯IO的操作不需要互斥,比如你的逻辑操作也都在IO协程里,因为绑定了,所以这个fd的所有操作都在这个poller协程里。

但我前面讲过了,单个请求阻塞,这个poller上的所有fd就都要等待这个请求处理完才能继续。gobwas/ws就存在这个问题。 要想解决这种,就得IO和逻辑分离。

逻辑协程是单独的,业务类型多种多样,比如你如果做IM、游戏,很多用户之间交互,还有广播,这些都可能是不同模块的协程触发的,这时候就是并发操作单个conn,你不加锁,就可能A模块sysclal写100字节但只成功了50,还没来得及把这50字节存起来等待可写、B模块也写了100字节也只成功了50,然后写入TCP的数据都混乱了,怎么保障你说的tcp写入顺序问题呢?

那如果框架层只支持无锁,业务层要怎么处理?一种是比如我上面提到的封装gorilla那种用协程+chan,但是常驻协程和chan的资源和性能都不划算,另一种更常见的就是用锁。

做框架不能只考虑自己框架内部处理IO的这点事,也得考虑别人业务层后续怎么方便。

lesismal commented 1 year ago

不能这样看待TechEmpower 测试,平台定了标准,符合要求即可。plaintext测试的就是基础框架的性能,与协议无关(只是http 工具比较成熟罢了 ab wrk),先确保裸框架性能ok,再在此基础上堆应用

那你看看这个,人家*net作者自己都承认这种性能不合理

image

如果如他所说是TechEmpower作者同意,再入你所说这就算符合要求,那赶明儿我也写一个,我连*net那个http按行解析还是啥都不实现,我压测client就固定长度的http文本,接收端读到数据只统计长度、长度够一个就回写一个固定的http respongse buffer,基本可以把cpu计算忽略了,那我也能去拿个第一。 但是如果c/cpp/rust选手们也都这样搞,go社区可能还是卷不过

关键是,这种性能测试数据,它有意义?你提出nbio accept这个问题、刨根问底精神我非常赞赏并且受益!但面对这种相当于造假的行为,标准咋又降这么低了呢。。不带这样子的啊 :joy: image

lesismal commented 1 year ago

你做了个错误的假设,“不需要再去额外封装保障单个conn并发读写” 有了事件驱动,怎么还会有并发读写呢?这正是框架应该要解决的问题

你还是没get到我说的,不是框架层自己非要并发写,而是业务层人员并发调用你的Conn.Write/Close啊兄弟。如果你的用无锁的方式实现Conn.Write,我能想到的方式就是把要写的数据push到无锁队列然后异步去写,但是这样的确定我上面也给你讲了,队列可能同时存在很多buffer,资源压力是一,还有异步写及时性是二

通常的框架实现,写的时候是直接syscall(如果之前有尚未发送完的、直接push到后面、不syscall),写失败才缓存起来等待可写,对于绝大多数场景,tcp缓冲区没满,或者说你的发送频率不至于老把发送缓冲区干满了或者实在是网络拥塞,多数时候这样直接写是成功的,一是避免了异步buffer队列的资源压力,二是更及时。

lesismal commented 1 year ago

又想起来个三,就是写的地方的buffer拷贝问题,有的地方写的buffer是别的模块人家自己一段复用的,写给Conn后,这个buffer可能会改变。 如果是直接写,写成功了就不用考虑其他,写失败了,把这个buffer缓存到fd上时框架自己就要考虑拷贝,如果框架不拷贝,那业务开发者就要保证每次丢给Conn.Write的buffer后续也不会被覆盖导致脏内存,或者更复杂的引用计数之类的机制去让框架与业务开发者共同管理buffer生命周期、更复杂。但是这里,直接写成功的时候占多数场景,避免了这种拷贝。

而如果是异步写,每次写都涉及这个问题

shaovie commented 1 year ago

TechEmpower 我看过要求,就是gnet那样的(会有人工审核),排名靠前的那是简单处理,缓存date,但有的框架性能就是上不去,我自己也提交了一份测试代码

说句实话啊,你这个肯定拿不了第一名,哈哈

shaovie commented 1 year ago

这个issue包含的内容太多了,有点儿乱了。。^_^

lesismal commented 1 year ago

TechEmpower 我看过要求,就是gnet那样的(会有人工审核),排名靠前的那是简单处理,缓存date,但有的框架性能就是上不去,我自己也提交了一份测试代码

那你觉得这种性能对比有意义吗?

说句实话啊,你这个肯定拿不了第一名,哈哈

如果是像我楼上说的那样,根本就不是使用nbio/nbhttp,而是就读写、固定buffer。如果gnet能拿第一,那拜托你先看看这里的性能压测再说,不要以为nbio有锁、性能不行: https://github.com/lesismal/nbio/issues/337#issuecomment-1663768522

lesismal commented 1 year ago

单对比tcp echo性能,nbio和gnet差不多,有时候这个高有时候那个高,我自己环境里跑多轮,nbio高的时候会多一些,贴一个,或者你自己看那个issue里的。但我更建议的是你自己跑代码看实际数据。 image

lesismal commented 1 year ago

另外,nbio的 Used By 列表里有 gnet-io/gnet-benchmarks: https://github.com/lesismal/nbio/network/dependents?dependents_after=MjgxMjQyMDkyMDE

image

但是我到它仓库里去看,并没有nbio,应该是以前把nbio加进去过,后来有删掉了,我比较笨、猜不到是什么原因 :joy:

shaovie commented 1 year ago

337 (comment)

这都是21年的代码了吧,我看gnet上榜也是去年的事,gnet 我感觉它是东拼西凑出来的,对底层并不是特别了解

lesismal commented 1 year ago

哦,我知道为啥 gnet-benchmarks 比标准库数据高了,刚去看了下代码,gnet 自己默认用的 64k 读 buffer: https://github.com/panjf2000/gnet/blob/v2.0.0/gnet.go#L312 https://github.com/panjf2000/gnet/blob/v2.0.0/gnet.go#L365

但标准库用的 16k buffer: https://github.com/gnet-io/gnet-benchmarks/blob/v2/echo-net-server/main.go#L33

是否有其他差异我暂时没细看,比如SetNodelay之类的,压测场景影响挺大的

当然,我觉得这是作者自己疏忽了导致没把测试条件对齐

shaovie commented 1 year ago

TechEmpower 我看过要求,就是gnet那样的(会有人工审核),排名靠前的那是简单处理,缓存date,但有的框架性能就是上不去,我自己也提交了一份测试代码

那你觉得这种性能对比有意义吗?

说句实话啊,你这个肯定拿不了第一名,哈哈

如果是像我楼上说的那样,根本就不是使用nbio/nbhttp,而是就读写、固定buffer。如果gnet能拿第一,那拜托你先看看这里的性能压测再说,不要以为nbio有锁、性能不行: #337 (comment)

有意义哦,想拿第一,可没那么简单

shaovie commented 1 year ago

哦,我知道为啥 gnet-benchmarks 比标准库数据高了,刚去看了下代码,gnet 自己默认用的 64k 读 buffer: https://github.com/panjf2000/gnet/blob/v2.0.0/gnet.go#L312 https://github.com/panjf2000/gnet/blob/v2.0.0/gnet.go#L365

但标准库用的 16k buffer: https://github.com/gnet-io/gnet-benchmarks/blob/v2/echo-net-server/main.go#L33

是否有其他差异我暂时没细看,比如SetNodelay之类的,压测场景影响挺大的

当然,我觉得这是作者自己疏忽了导致没把测试条件对齐

你看,这就是你的不对了,buffer越小越好啊,不是越大越好,够用就行了,测试场景,只有几十个字节而已

lesismal commented 1 year ago

这都是21年的代码了吧,

压测代码你可以把各个框架更到最新,或者去 gnet-benchmarks 或者其他压测或者你自己压测的代码来测,所以我从来没标榜nbio性能第一,都是建议大家自己跑代码测试。但是请不要拿那种不成熟、只为了优化压测场景的框架来对比。

我看gnet上榜也是去年的事,gnet 我感觉它是东拼西凑出来的,对底层并不是特别了解

这你肯定记错了,我去说这个事情都是两年半以前的了:

image

shaovie commented 1 year ago

你可以写一个plaintext的测试,非常简单,你熟悉你的框架,也就十几行代码就能行, 在techempower 仓库里边跑一下同样的gnet,看一下结果

lesismal commented 1 year ago

你看,这就是你的不对了,buffer越小越好啊,不是越大越好,够用就行了,测试场景,只有几十个字节而已

这你就错了吧,poller上的buffer,一个poller协程只需要一个,即使大点也不耗费多少资源、而且size可配置的不是写死的。 对于压测场景,对方并发写会造成己方echo,双方的接收缓冲区数据量都比较大,你实际用于读取的buffer越小,则需要syscall的次数越多、性能吞吐也就越差,这个算是常识吧。。。

而且每个poller对应的那一个buffer,读到数据后丢给业务层处理,业务层自己需要copy再去copy就是了。

nbio倒是提供了扩展,允许用户每次读都自己传入buffer,但这样非常不划算,因为单次可能只有1字节也可能有很大量数据待读取,而应用层每次传入一个读buffer的话,太小效率低,太大则连接数多的场景资源开销太大。

你到底写没写过这种框架或者做没做过压测啊,我昨天看了下你知乎的回帖,应该至少也有10年左右了吧。。。

lesismal commented 1 year ago

你可以写一个plaintext的测试,非常简单,你熟悉你的框架,也就十几行代码就能行, 在techempower 仓库里边跑一下同样的gnet,看一下结果

对比吞吐只要简单echo就可以了,plaintext没必要。而且nbio本身支持完整的http1.x,如果为了获得高性能的排行,就去写个假的http解析,这种下限太低的事情我做不来。如果是用nbio自己完整功能的http,那肯定打不过人家,我去自取其辱、关键别人还是作弊,我脑子不至于这么秀逗。。。

lesismal commented 1 year ago

TechEmpower 我看过要求,就是gnet那样的(会有人工审核),排名靠前的那是简单处理,缓存date,但有的框架性能就是上不去,我自己也提交了一份测试代码

讲真,gnet获得了一份打败fasthttp甚至所有c/cpp/rust/javanetty的结果,有用吗?一众小白不懂、倒是跟着吹个我go牛逼天下第一开心得不得了。

至少人家fasthttp和其他go http框架可是实打实完整功能的框架,用相当于作弊的方式去跑个比别人高的数据,值得拿来炫耀?我要真去用这种类似的方法拿了个高排名数据,我都觉得无地自容。

而且,单就non-blocking mod下的nbio性能而言,在普通连接数的场景,响应性并不如标准库conn的方案,只有海量连接数的场景才有优势,因为海量连接数基于标准库conn的方案很可能OOM,至少GC压力大、STW可能更频繁 就是因为普通连接数场景下 nbio non-blocking mod 打不过基于标准库conn方案,所以支持了多种 IOMod,blocking conn的情况下,nbio还是比标准库http以及基于标准库http的框架要快一些的,多种IOMod的说明: https://github.com/lesismal/nbio/releases/tag/v1.3.5

但即使是blocking conn仍然打不过fasthttp系,具体原因我在那个聊天repo里应该也有聊过,不这里说了

shaovie commented 1 year ago

你可以写一个plaintext的测试,非常简单,你熟悉你的框架,也就十几行代码就能行, 在techempower 仓库里边跑一下同样的gnet,看一下结果

对比吞吐只要简单echo就可以了,plaintext没必要。而且nbio本身支持完整的http1.x,如果为了获得高性能的排行,就去写个假的http解析,这种下限太低的事情我做不来。如果是用nbio自己完整功能的http,那肯定打不过人家,我去自取其辱、关键别人还是作弊,我脑子不至于这么秀逗。。。

要保持测试工具也一致呢,wrk

你看,这就是你的不对了,buffer越小越好啊,不是越大越好,够用就行了,测试场景,只有几十个字节而已

这你就错了吧,poller上的buffer,一个poller协程只需要一个,即使大点也不耗费多少资源、而且size可配置的不是写死的。 对于压测场景,对方并发写会造成己方echo,双方的接收缓冲区数据量都比较大,你实际用于读取的buffer越小,则需要syscall的次数越多、性能吞吐也就越差,这个算是常识吧。。。

而且每个poller对应的那一个buffer,读到数据后丢给业务层处理,业务层自己需要copy再去copy就是了。

nbio倒是提供了扩展,允许用户每次读都自己传入buffer,但这样非常不划算,因为单次可能只有1字节也可能有很大量数据待读取,而应用层每次传入一个读buffer的话,太小效率低,太大则连接数多的场景资源开销太大。

你到底写没写过这种框架或者做没做过压测啊,我昨天看了下你知乎的回帖,应该至少也有10年左右了吧。。。

所以它不就是跟大小没关系么,(越小对cpu cache越友好)

shaovie commented 1 year ago

或者你可以这样想,是你解读的有问题,你不应该拿gnet跟fasthttp比,应该跟pico.v netty这类相比,都是stripped实现

shaovie commented 1 year ago

你到底写没写过这种框架或者做没做过压测啊,我昨天看了下你知乎的回帖,应该至少也有10年左右了吧。。。

实不相瞒,我写的goev,在相同环境测试结果已经完胜gnet了,我也提交给techempower了,只是我没有高配机器,没办法在高配环境下校准参数

lesismal commented 1 year ago

实不相瞒,我写的goev,在相同环境测试结果已经完胜gnet了,我也提交给techempower了,只是我没有高配机器,没办法在高配环境下校准参数

也是无锁对吧?那你想过应用起来用户的感受吗。。

shaovie commented 1 year ago

你先别抵触techempower,盖房子 肯定是先从底下开始,框架就像地基嘛,先保证最底层是优的,然后在此基础上加盖

shaovie commented 1 year ago

我刚才提无锁提到了,保证tcp的顺序性,其实你只是加把锁是无法解决包的顺序性的, 比如:你一次send 1024大小的包,但是只成功了 512个字节,另外512就得缓存起来,但是如果用户并发写入又发了20字节,这时候成功了,但是对方收到的消息就是错乱的,当然我不是说框架必须要解决这个问题

但是你加锁的处理方案 可能就错误引导了这个问题

shaovie commented 1 year ago

会让用户以为加了锁就完事了

shaovie commented 1 year ago

所以框架要想给用户提供便利,应该是提供一种顺序写入的能力,

lesismal commented 1 year ago

你先别抵触techempower,盖房子 肯定是先从底下开始,框架就像地基嘛,先保证最底层是优的,然后在此基础上加盖

你如果觉得techempower有意义,那我只能祝你旗开得胜了,我是不会去参加这个测试的,完全是误导人的测试,很多人都会默认以为都是完整http功能的测试,然而你们相对于fasthttp和标准库这些都是相当于作弊的,没有意义

lesismal commented 1 year ago

我刚才提无锁提到了,保证tcp的顺序性,其实你只是加把锁是无法解决包的顺序性的,

你可能把问题搞混了,要保障的是用户并发写时每个写的地方的这段数据不被打乱,多个并发的调用顺序是没法保证的

比如:你一次send 1024大小的包,但是只成功了 512个字节,另外512就得缓存起来,但是如果用户并发写入又发了20字节,这时候成功了,但是对方收到的消息就是错乱的,当然我不是说框架必须要解决这个问题

错乱的问题是必须解决的,并发调用的而场景、框架不加锁,用户就得自己处理串行化

但是你加锁的处理方案 可能就错误引导了这个问题

这只是你以为的误导,你看go标准库里各种锁,如果你觉得这样都是错,那go标准库也没必要存在了。就像你第一眼觉得accept有sleep就不对一样。虽然我之前没深入研究这块,但有sleep确实是合理的

shaovie commented 1 year ago

no no ,你总是引用标准库,这是不对的,标准库是全局的goroutine下运行的,所以它要考虑全局锁,给普通用户提供一个可以便利使用的方案, 但是既然,我们做了底层封装就不能这样对比了