-
Notifications
You must be signed in to change notification settings - Fork 160
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
accept sleep and error handling #337
Comments
Hi, 欢迎来交流! 最初我也没加sleep,是抄标准库的: |
我刚翻了一下代码,标准库中Accept处理 Temporary 采用sleep的方式,应该是处理EMFILE/ENFILE 这个错误,这个错误的意思就是进程/系统句柄耗尽了,这时候它不sleep,也干不了啥 |
哦不好意思,刚才没细看、没看到这句 “标准库中使用了这不是理由啊” 这个issue里也有: |
而且吧,插件它给我标红提示,强迫症就给改了,你要不提我都把这给忘了,刚才搜了下,还有漏网之鱼:
至于50毫秒,这是accept,accept如此高频的场景并不多,而且如果已经accept error了,就算sleep 50ms也没关系,线上不至于很多节点同时循环多次accept error吧? |
kqueue这个没改,可能是因为我平时windows开发,插件没报所以没注意到,当时也没全局搜,所以只改了windows和linux,而且macos下也主要是调试所以无所谓。 我当时没深究这个问题,今天你追问,又涨姿势了。 所以要考虑下Timeout()要不要改回Temporary(),我先看看改回去插件还报不报。 |
正常accept 是不会出现 timeout这种语义的错误的 |
改了一版,现在插件没报了 |
这样的写法,其实有点儿不伦不类,既然实现了poller,完全可以做到完全非阻塞化(标准库accept/connect底层也这样实现的), |
这个提交改掉的: 当时是正在支持udp,改动的地方特别多,插件报红就按提示给改了,没去深究这块,过后都给忘记了,不严谨 |
你不信按照 techempower 的标准,写一个最简单的http server,对比测试一下吞吐性能 |
单就nbio poller管理的conn而言,除了sendfile那块可能会有阻塞,其他用法是非阻塞的。而且sendfile那块通常http单个请求,也不怕单个请求稍微阻塞一下导致这个连接的其他阻塞,nbio目前只支持http1.x,单个连接上的请求本来就是串行处理。协程池size合理就行,如果整个系统几千几万个协程都阻塞到sendfile上,那换标准库问题也可能照旧,只要不是所有逻辑协程都阻塞到sendfile或者其他应用业务的IO或者什么逻辑上,其他连接的请求照常处理。而且搞nbio本来就是为了限制协程数量避免爆资源
跟传统的框架比,确实,主要对比两类吧,一类是对比c/cpp,一类是对比标准库。 如果只考虑IO,很容易做到无锁。但IO后读到的消息如何处理,conn在不同业务系统中可能面对被并发调用如何让让用户简单易用、不增加过多心智负担? 另外nbio.Conn的锁我没拆成读、写两个锁,而是只有一个共同的锁,主要几个考虑:
所以我觉得没你想得那么简单,或者,如果你觉得哪里可以更好地实现,咱聊具体的实现方案,可能会更容易解释清楚。 |
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就都排队等着,这也是不行的。 另外,从操作系统提供的接口上看,应该是只有windows iocp的syscall支持proactor,*nix都是reactor的syscall。但是从框架层看,nbio不只是提供reactor,用户如果想省事就用OnData,否则可以用OnRead自行实现读取,所以proactor/reactor相当于都支持 还有你在知乎上提到的 goev,我去看过了,看了一会,但你觉得它用起来如何?跟nbio的example对比下,用户看到愿意用哪种? |
再继续说锁太多的问题 对于单个连接,高频并发操作它的情况其实并不多、竞争并不大,这时候锁其实主要是原子操作,而且官方这个fast path还可以inline,所以成本并不那么高: 反倒是一些无锁的实现,框架层倒是无锁了,也能吹牛自己无锁以及性能了,但是用户真正写业务的时候还是要做很多封装、八成也是要额外加锁。所以你看标准库提供的Fd类的比如Conn、File,基本都是带锁的,内核里这些也是有锁的。 |
无锁的还有个典型,gorilla/websocket,虽然不是poller框架本身,但情况是类似的。它提供的websocket.Conn是不支持并发读写的,所以呢,很多人用它,都要自己另行封装:一个读协程只处理读,一个写协程+select chan既能保证串行又能避免阻塞,典型实现比如 melody。 这就是框架层自己无锁,但实际用户使用的时候,还是要额外封装锁或者chan或者其他的机制来保障并发操作单个conn的一致性。除非你的系统简单到爆,无锁才有意义,但nbio提供的是通用框架,如果是内部业务、对性能要求更高,那我当然可以另行定制。 |
最初看xtaci用RawConn,也觉得还是syscall好些,所以也是自己syscall来搞但并不方便: 比如现在支持的标准库的conn的转换,用户自己使用标准库很容易上手,你让很多go用户再去写syscall处理这些都会容易劝退,这还没算上tls。 而且多数服务,端口号也不至于太多,每个端口一个协程甚至reuseport多个协程,这点协程资源也不算啥开销,而且通常是吞吐量瓶颈、accept不至于太瓶颈、比syscall也慢不到哪里去,真被ddos那也是防护那些外层需要考虑的、实际服务里自己的accept解决不了这事情。 所以现在这种方式下 |
再说一下无锁,无锁性能就一定最高吗,这里有一些对比: 这个测试结果和其他一些poller框架作者的测试结果可能不太一样,比如他们测试可能基本都高于标准库,但我测到的数据有些参数下标准库更高、有些是poller框架更高。 而且,这些测试还是先抛开无锁框架提供给业务开发者后、业务开发者可能额外封装锁之类带来的损耗。所以如果放到实际业务中,无锁框架的表现可能会比nbio更差一些,因为nbio本身已经有锁、不需要再去额外封装保障单个conn并发读写关闭的一致性相关的东西了 |
锁,其实仅仅解决了互斥的问题 网络框架 有个最重要的问题,是 tcp 要顺序化写入,要知道每次write,有可能成功一半,所以为什么一般的框架都是poller一个单独的线程,fd跟poller绑定,如果fd 跟poller绑定了,那就不会互斥了 |
不能这样看待TechEmpower 测试,平台定了标准,符合要求即可。plaintext测试的就是基础框架的性能,与协议无关(只是http 工具比较成熟罢了 ab wrk),先确保裸框架性能ok,再在此基础上堆应用 |
你做了个错误的假设,“不需要再去额外封装保障单个conn并发读写” 有了事件驱动,怎么还会有并发读写呢?这正是框架应该要解决的问题 |
nbio是直接写,没写完的挂载到这个conn上缓存起来、epoll 加写事件,然后等待可写再写入;在这些缓存的数据没发送完成之前,再次写入就直接加入到缓存里去、不会直接syscall write
nbio conn是fd跟poller绑定的,你这只是考虑单纯IO的操作不需要互斥,比如你的逻辑操作也都在IO协程里,因为绑定了,所以这个fd的所有操作都在这个poller协程里。 但我前面讲过了,单个请求阻塞,这个poller上的所有fd就都要等待这个请求处理完才能继续。gobwas/ws就存在这个问题。 逻辑协程是单独的,业务类型多种多样,比如你如果做IM、游戏,很多用户之间交互,还有广播,这些都可能是不同模块的协程触发的,这时候就是并发操作单个conn,你不加锁,就可能A模块sysclal写100字节但只成功了50,还没来得及把这50字节存起来等待可写、B模块也写了100字节也只成功了50,然后写入TCP的数据都混乱了,怎么保障你说的tcp写入顺序问题呢? 那如果框架层只支持无锁,业务层要怎么处理?一种是比如我上面提到的封装gorilla那种用协程+chan,但是常驻协程和chan的资源和性能都不划算,另一种更常见的就是用锁。 做框架不能只考虑自己框架内部处理IO的这点事,也得考虑别人业务层后续怎么方便。 |
你还是没get到我说的,不是框架层自己非要并发写,而是业务层人员并发调用你的Conn.Write/Close啊兄弟。如果你的用无锁的方式实现Conn.Write,我能想到的方式就是把要写的数据push到无锁队列然后异步去写,但是这样的确定我上面也给你讲了,队列可能同时存在很多buffer,资源压力是一,还有异步写及时性是二 通常的框架实现,写的时候是直接syscall(如果之前有尚未发送完的、直接push到后面、不syscall),写失败才缓存起来等待可写,对于绝大多数场景,tcp缓冲区没满,或者说你的发送频率不至于老把发送缓冲区干满了或者实在是网络拥塞,多数时候这样直接写是成功的,一是避免了异步buffer队列的资源压力,二是更及时。 |
又想起来个三,就是写的地方的buffer拷贝问题,有的地方写的buffer是别的模块人家自己一段复用的,写给Conn后,这个buffer可能会改变。 而如果是异步写,每次写都涉及这个问题 |
TechEmpower 我看过要求,就是gnet那样的(会有人工审核),排名靠前的那是简单处理,缓存date,但有的框架性能就是上不去,我自己也提交了一份测试代码 说句实话啊,你这个肯定拿不了第一名,哈哈 |
这个issue包含的内容太多了,有点儿乱了。。^_^ |
那你觉得这种性能对比有意义吗?
如果是像我楼上说的那样,根本就不是使用nbio/nbhttp,而是就读写、固定buffer。如果gnet能拿第一,那拜托你先看看这里的性能压测再说,不要以为nbio有锁、性能不行: |
另外,nbio的 Used By 列表里有 gnet-io/gnet-benchmarks: 但是我到它仓库里去看,并没有nbio,应该是以前把nbio加进去过,后来有删掉了,我比较笨、猜不到是什么原因 😂 |
话不能这样说啊,你仔细看下我回复你的内容,都是讲了原因的啊,可不是不给你解释原因就说无妨啊。 |
哈哈,你可别抄西抄的啊,觉得标准库这样就有道理,抄。觉得字节是大厂,这样写,有道理,抄。你要明白其原理啊 |
持有 和 索引,不是一回事哦,仔细想一下。 |
巧了,我也是做了好些年游戏,你这种小buffer处理读的,倒是有一些场景,比如短链接服务,类似于echo,游戏本身难度也不大。而且很多游戏,其实客户端发的包size不大,而且单个连接交互频率不那么高,所以你4k可能也足够用 但请先想明白了,你可没把框架限定在这种client小包非高频场景(高频的FPS/MOBA/MMORPG包频高但包小比如位置信息之类的,所以小buffer也不至于瓶颈,主要看协议设计) 你如果考虑通用场景,RPC、吞吐高的场景,这么搞就不合适了 |
不是让要取消 数组保存,是让你索引的时候不需要它,而是直接从event_data里边提取出来,这样在处理事件的时候就不需要connsunix数组了,只是在add/del事件的时候维护一下就可以了 |
咱别这么说话,你要么把原因说出来,别这么猜猜猜的。我当初试过把conn挂载到event上每次从event里取出来,然后就遇到内存错乱的问题了,我没多测,猜测就是原来的被gc了。 另外,我问你上一楼那个,首先我没搞懂你这话指的是什么,所以你先描述清楚点,问题都没讲清楚,让我怎么猜 |
那你觉得它 空转的意义是什么吗? 起到了什么作用 |
哦哦是这个意思啊,不行,不能这么搞,还是gc的问题 比如你epoll event loop里刚好这个fd有事件,但你刚好其他的协程里close了、已经从数组里删掉它了,调度的原因,你没办法保证你从event里取到的那个void*转换成conn后它还在其他地方持有、它可能已经被gc了,所以你直接用它仍然可能内存错乱 所以你看为啥getConn后还要判断一下 c !=nil 啊?就是为了避免它已经被删掉并gc了,取数组是为了明确它当下的可用性。
没你想得那么简单的兄弟。nbio的一些地方看上去也是直观的代码,但并不那么简单直观的 |
你慢慢看,有啥觉得可以优化的尽管提。 |
这就说回之前你框架内锁的问题了,其实本不应该出现竞争的,你如果在此基础之上只会越来越复杂,越来越不可控 框架,框架,是要框个规则出来的 |
咱别嘴炮,你先按你认为的正确方式把 goev 写完善,然后咱们再聊这个。到时候我也来研究下你的方案哪里优秀、确实优秀的我也来学习和使用 |
嗯呐 |
你们都是做哪种类型的游戏啊?兄弟是主程或者以上了没? |
我是技术合伙人,主程,好多年前了 mmoarpg |
那可以,等你goev完善些了喊我,我平时闲了也来看看 |
刚才没空没反汇编试,单个分支编译器应该是把它退化成 if 了,这会才反汇编看了下例子: // test.go
package test
func If(fd int) int {
if fd == 8 {
return fd * 8
} else {
return fd * fd
}
}
func Switch(fd int) int {
switch fd {
case 8:
return fd * 8
default:
return fd * fd
}
} go build -o test.o test.go
go tool objdump -S test.o > test.asm 得到的汇编是一样的 TEXT command-line-arguments.If(SB) gofile..C:/Users/Administrator/Desktop/test/test.go
if fd == 8 {
0xb6b 4883f808 CMPQ $0x8, AX
0xb6f 7506 JNE 0xb77
return fd * 8
0xb71 b840000000 MOVL $0x40, AX
0xb76 c3 RET
return fd * fd
0xb77 480fafc0 IMULQ AX, AX
0xb7b c3 RET
TEXT command-line-arguments.Switch(SB) gofile..C:/Users/Administrator/Desktop/test/test.go
case 8:
0xb7c 4883f808 CMPQ $0x8, AX
0xb80 7506 JNE 0xb88
return fd * 8
0xb82 b840000000 MOVL $0x40, AX
0xb87 c3 RET
return fd * fd
0xb88 480fafc0 IMULQ AX, AX
0xb8c c3 RET 所以这块也没必要改 |
ohhhhh.. the point is is coding style! |
这些也不太符合我审美啊: 尤其 nio.Append 这种接口,像我上面说的那样,用 Write 或者 Writev 明明可以做到同样效果并且定义更符合系统设计
我这不是没随便乱用嘛,压测主动调度的代码没用我就删掉啊 |
Write 或者 Writev 你是指 syscall.Write ? |
之前只顾着回复你的疑问了,刚又看到你回复我的,这个,你直接 这种每个 fd 封装一个 Conn/Fd ,由 Conn/Fd 自己实现 Read/Write 这些要好于你这种包提供读写 fd 方法优雅的多吧,而且可以给用户提供便利的接口时候屏蔽内部实现、不用让用户操心那么多底层细节,nbio就是这样设计的。 |
nbio提供这种接口,但不是直接提供 syscall 去操作 fd,如果做个框架出来都还是提供用户 syscall、fd 的话相当于把这些底层细节都交给用户去处理,那人家用你框架干嘛呀?人家直自己加个 epoll 也没多少代码量啊然后自己 syscall 不行嘛? |
我是面向对象的哦,Http 就是跟链接绑定的,一个链接一个对象
|
我问你的Write是不是syscall.Write的意思是 你是指直接系统调用write到缓冲区,还是封装一个方法Write到[]byte内存中,最后flush |
这我前面说过了呀,先直接syscall.Write(fd), 没写完的才放到Conn自己的buffer上,你翻下前面的回复,最好直接看下代码吧: |
你这个 netfd.Append 可一点都不面向对象啊。这种读写别人都是对象自己的方法,你这都是外部包取处理 raw fd 。。。 |
就先讨论这么多吧 |
accept error 统一Temporary了,sleep 时长没太大必要修改: #338 先close了,感谢反馈指正! |
抱歉,我无意评论了一篇文章,没有恶意啊,咱只讨论技术
先说第一个问题:
The text was updated successfully, but these errors were encountered: