« 亲近自然 | 返回首页 | 读书这件事 »

IOCP , kqueue , epoll ... 有多重要?

设计 mmo 服务器,我听过许多老生常谈,说起处理大量连接时, select 是多么低效。我们应该换用 iocp (windows), kqueue(freebsd), 或是 epoll(linux) 。的确,处理大量的连接的读写,select 是够低效的。因为 kernel 每次都要对 select 传入的一组 socket 号做轮询,那次在上海,以陈榕的说法讲,这叫鬼子进村策略。一遍遍的询问“鬼子进村了吗?”,“鬼子进村了吗?”... 大量的 cpu 时间都耗了进去。(更过分的是在 windows 上,还有个万恶的 64 限制。)

使用 kqueue 这些,变成了派一些个人去站岗,鬼子来了就可以拿到通知,效率自然高了许多。不过最近我在反思,真的需要以这些为基础搭建服务器吗?

刚形成的一个思路是这样的:

我们把处理外部连接和处理游戏逻辑分摊到两个服务器上处理,为了后文容易表述,暂时不太严谨的把前者称为连接服务器,后者叫做逻辑服务器。

连接服务器做的事情可以非常简单,只是把多个连接上的数据汇集到一起。假设同时连接总数不超过 65536 个,我们只需要把每个连接上的数据包加上一个两字节的数据头就可以表识出来。这个连接服务器再通过单个连接和逻辑服务器通讯就够了。

那么连接服务器尽可以用最高效的方式处理数据,它的逻辑却很简单,代码量非常的小。而逻辑服务器只有一个外部连接,无论用什么方式处理都不会慢了。

进一步,我们可以把这个方法扩展开。假定我们逻辑以 10Hz 的频率处理逻辑。我们就让连接服务器以 10Hz 的脉冲把汇总的数据周期性的发送过去,先发一个长度信息再发数据包。即使一个脉冲没有外部数据,也严格保证至少发一个 0 的长度信息。额外的,连接服务器还需要控制每个脉冲的数据总流量,不至于一次发送数据超过逻辑服务器处理的能力。

那么,逻辑服务器甚至可以用阻塞方式调用 recv 收取这些数据,连 select 也省了。至于数据真的是否会被接收方阻塞,就由连接服务器的逻辑保证了。

说到阻塞接收,我跟一个同事讨论的时候,他严重担心这个的可靠性,不希望因为意外把逻辑服务器挂在一个 system call 上。他列举了许多可能发生的意外情况,不过我个人是不太担心的,原因不想在这里多解释。当然我这样设计,主要不是为了节省一个 select 的调用,而是希望方便调试。(当然,如果事实证明这样不可行,修改方案也很容易)

因为阻塞接收可以保证逻辑服务器的严格时序性,当我们把两个服务器中的通讯记录下来,以后可以用这些数据完全重现游戏逻辑的过程,无论怎么调试运行,都可以保证逻辑服务器的行为是可以完全重现的。即,每 0.1s 接受已知的数据包,然后处理它们。

这样做,逻辑服务器对网络层的代码量的需求也大大减少了,可以更专心的构建逻辑。

Comments

gateway也需要啊

相当于 web 服务器前面加了一层 nginx 反向代理?

真是不容易,被我翻出来,还是认真的学习了一下

挖个坟....
我们现在的游戏古剑OL的后端架构跟云风大大说的一样,甚至逻辑服务器之间通信也要经过这个所谓的“连接服务器”(现在应该都叫gateway了),不过从客户端到服务端,各个组件之间通信都是事件驱动异步的(主程封装了iocp/epoll),其实我觉得后端组件之间绝逼是在一个高速网域内,其间的延时相对于复杂的异步逻辑的代价来讲真的可以忽略不计。
而且现在很多都是分布式架构,单纯依赖单一节点的高性能没太大的意义,不如将节点的代码逻辑变得简单一点,专注于提供系统业务的分布能力和健壮性。

省电+1

这概念有点像DEVICE_POLLING,只有系统大负载的时候效率会比较高,要是业务不多,还是省点电吧。

经过这么长时间,我终于能大致看懂云大在说什么了。。。 = =

一个链接应该是足够用了,但比较担心阻塞链接的系统容灾问题?

云风大哥。逻辑服务器设计成阻塞的明显会降低网络吞吐量吧?尤其是交互式(一问一答)这种方式。

风云大哥。逻辑服务器设计成阻塞的明显会降低网络吞吐量吧?尤其是交互式(一问一答)这种方式。

哈哈哈,老文章了,还这么火。我公司也是样这架构的服务器,用libevent写的连接服务器,客户端和逻辑服务端的网络代码是同一套代码,都是用select实现的,现在发现select没必要了,直接阻塞也行的。反正是逻辑服务器单进程的。

不知深浅一句。

我理解,是不是建立一个事件池?这样,在连接服务器和逻辑服务器中间其实是不介意用什么通信方式的,目标只有两个:正确传递事件和处理结果,保证效率。

如此这样,连接服务器要等着处理结果?

说到阻塞接收,我跟一个同事讨论的时候,他严重担心这个的可靠性,不希望因为意外把逻辑服务器挂在一个 system call 上。他列举了许多可能发生的意外情况


-----------
我对他列出的意外情况很关心

大神的文章都是很多人赞同的.不同的声音,就要被当作异类枪决.

好吧,我不是不同意大神的说法.我只是有点疑惑.我在游戏公司也有7年了.自己带的游戏也有5款.我们公司用的却是select,而看过许多国外的游戏服务端,都是iocp,由于商业机密问题,我不便透露我公司是什么.

如果真的没必要用iocp...那那些国外的游戏公司为啥要用iocp,总有个其他原因吧...当然,已知的可以防部分dos攻击是知道的了.

连接服务器?
这真是云风在2006年写的文章么?
2006年已经有很多网络游戏设计都是有连接服务器了。网易照理说也应该有才对。

不过这个连接服务器一般叫做GateWay服务器。作用不仅仅是转发了,同时也起到隔绝网络的作用。

很多年前看的这篇,最后记录信息调试那句话相当有帮助,我在我们的游戏服务端也实现了录像和重放的功能,调试诡异错误的时候非常方便,重放一下就可以了

多线程select如何?
32个线程,每个管1024个连接。
结构也简单。
如果大多数连接活跃的话,
不见得比epoll效率低。
准备做个实验。
就不知道linux kernal能撑住30000并发链接不。

不能注册吗?

进来学习一下

怎么这么多留言?
最近在做一个贪吃蛇的游戏, 然后为了处理逻辑的问题, 去看了zeromq, 然后考虑了一下架构. 现在上网查资料翻到了这篇, 发现和我构思的一样, 看来这条路还是走得通的. 那我可以实际开工测试了.

单进程的连接服务器是不是会有较大延迟呢,比如一次来了5000个读事件,等你循环5000次read()系统调用后,时间可能已经过去很久了吧? 能不能用多个线程/进程,每个只处理较少的连接比如2000个?这样在多核处理器上是不是更有优势

网络速度的限制往往是通过应答加滑动窗口来限制,固定的限制是没有实际价值。

所谓模型的不同是为了减小不同的网速产生的io时延的额外管理开销. 或者说是为了os为了优化io/thread(process)/cpu/mem综合性能做的不同改进而已,也使用与不同的场景,所谓的改进也是牺牲系统和应用的复杂性来换取高性能. 你的这个想法加复杂性延伸到了不太可控的程度.... 应用程序(逻辑服务器)处理请求会变得异常复杂,比如如何判断每个请求的结尾标志/如何做流数据类型的处理...因为内容大小的伸缩完全不可控...

这个方法不错,改天试试看

设计网络服务器其实跟设计操作系统类似, 就是尽量让资源等待不占用CPU, 让CPU去干重要的活, 没活或者没数据的就空闲不管它, 说白了就是业务逻辑处理时间不能跟网络资源等待串行, 有了这个原则, 去设计东西就可以轻松很多, 有兴趣的可以看看我开发的通信库 http://sbase.googlecode.com
基本是脱离逻辑部分的代码封装。

:( 我怎么看着像cgi啊

>>libevent 不支持 iocp . 完成端口的模型和 kq 这些不一样,所以无法支持。

其实有办法把 iocp 集成到 libevent 中去的。

http://monkeymail.org/archives/libevent-users/2008-June/001269.html

风云说的,我觉得是比较偏重于调试和模块化划分,有点大不了重改的想法。如果是我我肯定先保证通讯模块的稳定性和性能,然后再往下构架。。。毕竟是一劳永逸的事情。
关于服务器开发,必须要做到网络与逻辑分开。逻辑处理线程池可以根据各自需求整合到网络引擎中,或者整合到逻辑中。
我一般写服务器都是跨平台的,window下面用iocp,linux下用epoll,或者poll+多线程。
线程池部分直接整合到网络引擎中。
逻辑部分采用插件模式,即插即用,关于逻辑中的共享数据我将单独保存。

以上模型我就写了一次,用了3年,当然随着网络技术不断发展我会不断支持各自新的网络技术。
关于ACE或者各种开源引擎,我觉得好比是一件漂亮的衣服,但是要用的话很难控制甚至达不到开发者的初衷,发生问题的时候你只能等待新版本的更新了,这方面我以前可是吃尽了苦头。

小弟愚见!祝愿风云游戏作品越来越好,也希望有一天我写的游戏能够尽快稳定。

这篇文章里云风似乎更关注调试的便利性. 在逻辑服务器采用多线程的时候, 是否可以考虑让前端的请求带一个唯一的标识,然后让这个唯一标识过来的请求按照到达连接服务的时序跟逻辑服务器的某个处理线程附加一种线程的亲缘性,即所有这个唯一标识过来的请求都由特定的线程来处理,这样可以确保不同标识的连续请求可以被不同的线程并发处理,但是同一标识的请求只能被特定的线程处理,这样同样可以达到单线程处理的调试目的.

其实用什么无关紧要,主要是能实现功能。
IOCP/epoll/kqueue的出现有他们的理由,设计一个可扩展的服务器模型,这些基本上是最好的网络模型。不过,按照你的意见,可能你们比较赶时间,所以,你所说的这个形势的服务器构架,完全可以实现。不过相对来说,在逻辑服务器的互动和合作需求越来越大之后,简单的阻塞recv或者小规模的select可能满足不了需要的。

逻辑服务器收到数据后的逻辑处理使用单线程么?会不会太影响效率。

select无疑是比iocp或epoll更耗资源.在大量连接的情况下效率很低.如果只是担心跨平台问题,可以找封装好的异步网络库,比如ACE中的proactor或者ASIO.在有些情况下服务器的逻辑用异步架构来描述更加直接明了

其实用哪种方式处理 socket 并不重要, 网络层并不会是瓶颈,再怎么优化都是小头,不会带来大的提高

传奇3就是这样的 逻辑服务器不直接面对用户 标准的配置中 4个地图(逻辑)服务器 8个gate 由8个gate负责分配连接 实际上包的一些前期处理也是gate做的 比如关键词过滤 非法包过滤等
这样的架构是很合理的
不过这个话题似乎和select还是epoll没啥关系 我们可以把连接看成gate 是单独放一个进程还是和逻辑进程放一起本质是没区别的 用select做这个门显然窄了点
你说的关于调试方面的确实是不错的主意

http://www.oracle.com.cn/viewthread.php?tid=80153

FYI

借宝地一用

http://luliyi1024.googlepages.com/

传奇类的服务器端早就是这么设计的,单服务器的网络设计落后很久了

连接服务器因为写起来简单,所以可以用 kq 这些,也可以用 libevent . 甚至不用都不会造成性能瓶颈。用了也不会增加设计的复杂度。

select 之所以低效,不是说用这种模型的程序低效。(select 是可以同时等多个 socket)而是指,无论什么 os 都不可能把 select 实现的高效,这是由 select 的定义决定的,kernel 在实现 select 时必须自己做轮询,才知道你到底想知道哪个 socket 的情况。这种轮询会无谓的吃掉 cpu 。

我只是好奇你的连接服务器用什么模型,恐怕还得用 OS 相关的 IOCP epoll 吧,把代码工程组织好应该是可以跨 OS 的吧。这种模型我也用过,不过连接服务器、逻辑服务器分别在不同的机器上,为了传输效率总是在服务器间将数据“积攒”到一定数量再一起传输。

其实轮询未必就是低效的,这是要根据具体应用来分析的,像云风的这种设计中每隔100ms才对用户命令进行一次处理,那么你完全可以每隔100ms去socket上查询一下是否有数据到达,如果同时有1000个连接,那么每秒只需要10000个socket的查询,而且select可以查询一组socket,这个查询的开销基本上是可以忽略的。

重现的关键是记录输入,输入有网络输入,键盘/鼠标/游戏杆输入(server 没有),时钟输入,随机数输入(通常可以避免)

这里最容易忽略的是时钟输入,但是这个是保证时序的关键。

一般的方法是,给没有线程定一个时间脉冲,比如 10Hz 比如 100Hz 或者 1Hz 都可以。每一段时间执行完,都必须等待下一个时间脉冲信号才能继续。定时器当然也得自己写了,用这个时钟脉冲去引发。

单线程这样就够了,多线程则在出入锁的时候记录次序,相对麻烦一些,但是可以实现的。

to mouse, 我们现在也在用 libevent 。 libevent 不支持 iocp . 完成端口的模型和 kq 这些不一样,所以无法支持。

定义一种接口完成耦合不如定义一种通讯协议松散。在可以满足性能要求的时候,我认为分开进程处理要好一些。

不防仿照libevent做个中间层,可以方便地使用不同的底层机制。

参见
http://monkey.org/~provos/libevent/

还有一些性能分析的数据。

云风说的这种结构现在应该比较普遍了.而且是比较原始的一个模型.现在很多正真的应用,应该比这还复杂得多.比如说无边界地图世界,一个逻辑服务器不足以处理时.那多逻辑服务器集群问题了.
傻傻地问一句,如果在服务器上用python脚本来做嵌入的逻辑处理可行吗?是不是都以单线程来执行python的虚拟机?

hello, all:
重现不好做, 大虾们指点

1.逻辑服务器除了消息驱动外,还有定时的时钟驱动,这个如何重现?

2.逻辑服务器如果做成多线程提供服务,重现又不知如何解决的?

有点新的想法,如果连接服务器和逻辑服务器采用以下的结构,就可以几乎做到重现了:

连接服务器 --+-- 逻辑服务器
      |
      +-- 数据记录服务器

数据记录服务器也是一对一连接,阻塞式读取。当需要重现的时候,把数据流的方向改一下就可以了。

我赞同逻辑层显然不应该跟这些API耦合。设计逻辑层时也不应该需要考虑前面是epoll还是select。如果一个使用select的程序后来要改成epoll很麻烦,那么恐怕是设计有些问题了。

不过封装/隔离这些OS API的方法却有多种。

有些人会用现成的封装,比如ACE中的Reactor、Proactor;也有些人会自己封装。共同点是(以Proactor为例)内部用IOCP还是epoll,还是用select模拟,改起来很容易,不会影响逻辑层。

往往封装后是一些class或者一个framework,和逻辑层是API耦合。

而你的设计,是把这些封装成一个连接服务器。和逻辑服务器之间由私有网络协议耦合。这样做也挺好的,同样达到了隔离OS API的目的。不过或许也会增加一些overhead。当然这个overhead是否真的存在或者是否重要,就具体情况具体分析了 :)

连接服务器为了性能自然可以用 kq 这些了。但是这些代码量很小,也很容易被重构。所以我说(一开始就)考虑epoll, kq 这些重要性不大。

我写这篇 blog 的想法是,基于特定的 api 去做网游的底层是没有必要的。比如我读过一个很糟糕的网游的代码,是基于 iocp 的。必须跑在 windows 上,当试图移植的时候无从下手。

延时的问题我前几天写过 blog 阐述过我的观点。在服务器组内部的延迟远小于外部通讯延迟的时候,这种多余的延迟是可以被游戏本身的设计弥补的。

这只是把问题从逻辑服务器分离出来,转移到连接服务器中了。那么连接服务器该怎么写呢?要不要用iocp,epoll,kqueue?

另外这连接服务器和逻辑服务器其实未必是不同机器,也可以是同一台机器上的两个不同进程。那么你这个结构其实就和通常的网络服务器程序非常类似了。不同在于,通常的程序一般逻辑层会有多个线程在排队从传输层拿数据包,而你的设计似乎逻辑层是单线程的。

传输层(或者说连接管理层)是一定要和逻辑层分离的,这点我完全赞同你的做法。至于是分到2个不同的机器还是2个进程还是同一个进程中的不同线程,以及这2层之间的队列的语义(推/拉)如何确定,却是需要斟酌的。

大哥,我昨晚看了你的这篇文章,回去想了一下,只有一个疑点,那就是延时的问题,就是说数据链路上多了一个处理的环节,势必会引起网络通信的延时,如果没处理好,那么只能增加后面逻辑服务器的设计强度了,还有你说的客户端的预测时间什么的设计强度了

有几点小意见。

1. 其实你说的架构:

连接服务器 ---+--- 逻辑服务器

在很多系统里都有类似的实现。不过大多数是有更多的逻辑服务器|组成cluster作负载平衡用途的。这里用epoll而不用select正是为了消除你说的select造成的轮询的overhead。即使是在连接服务器,还是有这样的overhead的。如果kernel要轮询的fd真的有30k个,那就不是一般的慢了。

2. 因为是1对1的连接,所以逻辑服务器的某个线程可以阻塞式读取数据。正如你所说可以使服务器接受到的包有严格的时序性。这个很好。:)

3. 进一步来说,接受数据的线程可以完全不需要作重组数据的任务。只需要把原始数据记录下来,通知做数据重组的线程开始工作就可以了。

当然,我这里指的多线程的使用,是为了优化多处理器的使用,而不对应逻辑上的多个任务

其实也不尽然,若是把多线程仅仅用在局部小尺度的优化,那大概还是可以重现

逻辑服务器是不可能可以完全重现的,除非逻辑服务器铁定是单线程跑。
如果要考虑多线程的优化,即使是有严格时序化的socket信息,也在逻辑上必须要有一个线程池这样的东西将任务丢给他去做,而这个线程池的多个线程是并行处理的,也就无法重现。

另外,单线程的阻塞接受会不会无法利用多处理器?

我同意你的看法,完全同意

Post a comment

非这个主题相关的留言请到:留言本