收藏:IOCP , kqueue , epoll ... 有多重要?---云风
IOCP , kqueue , epoll ... 有多重要?
http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html设 计 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 接受已知的数据包,然后处理它们。
这样做,逻辑服务器对网络层的代码量的需求也大大减少了,可以更专心的构建逻辑。
云风 提交于 April 19, 2006 11:09 AM | 固定链接
TrackBack
如果你想引用这篇文章,请复制下面的链接发送引用通告(GBK)
http://blog.codingnow.com/mt/mt-tb.cgi/115
Comments
其 实用什么无关紧要,主要是能实现功能。 IOCP/epoll/kqueue的出现有他们的理由,设计一个可扩展的服务器模型,这些基本上是最好的网络模型。不过,按照你的意见,可能你们比较赶 时间,所以,你所说的这个形势的服务器构架,完全可以实现。不过相对来说,在逻辑服务器的互动和合作需求越来越大之后,简单的阻塞recv或者小规模的 select可能满足不了需要的。
Posted by: Detective | February 8, 2007 02:04 PM
逻辑服务器收到数据后的逻辑处理使用单线程么?会不会太影响效率。
Posted by: Anonymous | October 9, 2006 11:01 AM
select无疑是比iocp或epoll更耗资源.在大量连接的情况下效率很低.如果只是担心跨平台问题,可以找封装好的异步网络库,比如ACE中的proactor或者ASIO.在有些情况下服务器的逻辑用异步架构来描述更加直接明了
Posted by: 卓强 | May 20, 2006 08:26 PM
其实用哪种方式处理 socket 并不重要, 网络层并不会是瓶颈,再怎么优化都是小头,不会带来大的提高
Posted by: KxjIron | April 26, 2006 11:26 AM
传奇3就是这样的 逻辑服务器不直接面对用户 标准的配置中 4个地图(逻辑)服务器 8个gate 由8个gate负责分配连接 实际上包的一些前期处理也是gate做的 比如关键词过滤 非法包过滤等 这样的架构是很合理的 不过这个话题似乎和select还是epoll没啥关系 我们可以把连接看成gate 是单独放一个进程还是和逻辑进程放一起本质是没区别的 用select做这个门显然窄了点 你说的关于调试方面的确实是不错的主意
Posted by: code | April 25, 2006 01:44 PM
Posted by: Brink of Wind | April 25, 2006 12:47 PM
Posted by: 卢立祎 | April 24, 2006 10:36 AM
传奇类的服务器端早就是这么设计的,单服务器的网络设计落后很久了
Posted by: sunway | April 24, 2006 08:52 AM
连接服务器因为写起来简单,所以可以用 kq 这些,也可以用 libevent . 甚至不用都不会造成性能瓶颈。用了也不会增加设计的复杂度。
select 之所以低效,不是说用这种模型的程序低效。(select 是可以同时等多个 socket)而是指,无论什么 os 都不可能把 select 实现的高效,这是由 select 的定义决定的,kernel 在实现 select 时必须自己做轮询,才知道你到底想知道哪个 socket 的情况。这种轮询会无谓的吃掉 cpu 。
Posted by: Cloud | April 21, 2006 10:25 PM
我只是好奇你的连接服务器用什么模型,恐怕还得用 OS 相关的 IOCP epoll 吧,把代码工程组织好应该是可以跨 OS 的吧。这种模型我也用过,不过连接服务器、逻辑服务器分别在不同的机器上,为了传输效率总是在服务器间将数据“积攒”到一定数量再一起传输。
Posted by: eric119 | April 21, 2006 09:55 PM
其 实轮询未必就是低效的,这是要根据具体应用来分析的,像云风的这种设计中每隔100ms才对用户命令进行一次处理,那么你完全可以每隔100ms去 socket上查询一下是否有数据到达,如果同时有1000个连接,那么每秒只需要10000个socket的查询,而且select可以查询一组 socket,这个查询的开销基本上是可以忽略的。
Posted by: analyst | April 21, 2006 09:23 PM
重现的关键是记录输入,输入有网络输入,键盘/鼠标/游戏杆输入(server 没有),时钟输入,随机数输入(通常可以避免)
这里最容易忽略的是时钟输入,但是这个是保证时序的关键。
一般的方法是,给没有线程定一个时间脉冲,比如 10Hz 比如 100Hz 或者 1Hz 都可以。每一段时间执行完,都必须等待下一个时间脉冲信号才能继续。定时器当然也得自己写了,用这个时钟脉冲去引发。
单线程这样就够了,多线程则在出入锁的时候记录次序,相对麻烦一些,但是可以实现的。
to mouse, 我们现在也在用 libevent 。 libevent 不支持 iocp . 完成端口的模型和 kq 这些不一样,所以无法支持。
定义一种接口完成耦合不如定义一种通讯协议松散。在可以满足性能要求的时候,我认为分开进程处理要好一些。
Posted by: Cloud | April 21, 2006 01:12 PM
Posted by: mouse | April 21, 2006 10:01 AM
云风说的这种结构现在应该比较普遍了.而且是比较原始的一个模型.现在很多正真的应用,应该比这还复杂得多.比如说无边界地图世界,一个逻辑服务器不足以处理时.那多逻辑服务器集群问题了. 傻傻地问一句,如果在服务器上用python脚本来做嵌入的逻辑处理可行吗?是不是都以单线程来执行python的虚拟机?
Posted by: tony | April 21, 2006 09:13 AM
hello, all: 重现不好做, 大虾们指点
1.逻辑服务器除了消息驱动外,还有定时的时钟驱动,这个如何重现?
2.逻辑服务器如果做成多线程提供服务,重现又不知如何解决的?
Posted by: KxjIron | April 20, 2006 11:22 PM
有点新的想法,如果连接服务器和逻辑服务器采用以下的结构,就可以几乎做到重现了:
连接服务器 --+-- 逻辑服务器 | +-- 数据记录服务器
数据记录服务器也是一对一连接,阻塞式读取。当需要重现的时候,把数据流的方向改一下就可以了。
Posted by: Felix | April 20, 2006 06:21 PM
我赞同逻辑层显然不应该跟这些API耦合。设计逻辑层时也不应该需要考虑前面是epoll还是select。如果一个使用select的程序后来要改成epoll很麻烦,那么恐怕是设计有些问题了。
不过封装/隔离这些OS API的方法却有多种。
有些人会用现成的封装,比如ACE中的Reactor、Proactor;也有些人会自己封装。共同点是(以Proactor为例)内部用IOCP还是epoll,还是用select模拟,改起来很容易,不会影响逻辑层。
往往封装后是一些class或者一个framework,和逻辑层是API耦合。
而你的设计,是把这些封装成一个连接服务器。和逻辑服务器之间由私有网络协议耦合。这样做也挺好的,同样达到了隔离OS API的目的。不过或许也会增加一些overhead。当然这个overhead是否真的存在或者是否重要,就具体情况具体分析了 :)
Posted by: Wesley | April 20, 2006 06:02 PM
连接服务器为了性能自然可以用 kq 这些了。但是这些代码量很小,也很容易被重构。所以我说(一开始就)考虑epoll, kq 这些重要性不大。
我写这篇 blog 的想法是,基于特定的 api 去做网游的底层是没有必要的。比如我读过一个很糟糕的网游的代码,是基于 iocp 的。必须跑在 windows 上,当试图移植的时候无从下手。
延时的问题我前几天写过 blog 阐述过我的观点。在服务器组内部的延迟远小于外部通讯延迟的时候,这种多余的延迟是可以被游戏本身的设计弥补的。
Posted by: Cloud | April 20, 2006 02:08 PM
这只是把问题从逻辑服务器分离出来,转移到连接服务器中了。那么连接服务器该怎么写呢?要不要用iocp,epoll,kqueue?
另外这连接服务器和逻辑服务器其实未必是不同机器,也可以是同一台机器上的两个不同进程。那么你这个结构其实就和通常的网络服务器程序非常类似了。不同在于,通常的程序一般逻辑层会有多个线程在排队从传输层拿数据包,而你的设计似乎逻辑层是单线程的。
传输层(或者说连接管理层)是一定要和逻辑层分离的,这点我完全赞同你的做法。至于是分到2个不同的机器还是2个进程还是同一个进程中的不同线程,以及这2层之间的队列的语义(推/拉)如何确定,却是需要斟酌的。
Posted by: Wesley | April 20, 2006 01:16 PM
大哥,我昨晚看了你的这篇文章,回去想了一下,只有一个疑点,那就是延时的问题,就是说数据链路上多了一个处理的环节,势必会引起网络通信的延时,如果没处理好,那么只能增加后面逻辑服务器的设计强度了,还有你说的客户端的预测时间什么的设计强度了
Posted by: kunkun | April 20, 2006 10:46 AM
有几点小意见。
- 其实你说的架构:
连接服务器 ---+--- 逻辑服务器
在很多系统里都有类似的实现。不过大多数是有更多的逻辑服务器|组成cluster作负载平衡用途的。这里用epoll而不用select正是为了 消除你说的select造成的轮询的overhead。即使是在连接服务器,还是有这样的overhead的。如果kernel要轮询的fd真的有30k 个,那就不是一般的慢了。
-
因为是1对1的连接,所以逻辑服务器的某个线程可以阻塞式读取数据。正如你所说可以使服务器接受到的包有严格的时序性。这个很好。:)
-
进一步来说,接受数据的线程可以完全不需要作重组数据的任务。只需要把原始数据记录下来,通知做数据重组的线程开始工作就可以了。
Posted by: Felix | April 20, 2006 12:15 AM
当然,我这里指的多线程的使用,是为了优化多处理器的使用,而不对应逻辑上的多个任务
Posted by: Atry | April 19, 2006 02:57 PM
其实也不尽然,若是把多线程仅仅用在局部小尺度的优化,那大概还是可以重现
Posted by: Atry | April 19, 2006 02:56 PM
逻辑服务器是不可能可以完全重现的,除非逻辑服务器铁定是单线程跑。 如果要考虑多线程的优化,即使是有严格时序化的socket信息,也在逻辑上必须要有一个线程池这样的东西将任务丢给他去做,而这个线程池的多个线程是并行处理的,也就无法重现。
Posted by: Atry | April 19, 2006 02:55 PM
另外,单线程的阻塞接受会不会无法利用多处理器?
Posted by: Atry | April 19, 2006 02:49 PM
我同意你的看法,完全同意
这 篇文章里云风似乎更关注调试的便利性. 在逻辑服务器采用多线程的时候, 是否可以考虑让前端的请求带一个唯一的标识,然后让这个唯一标识过来的请求按照到达连接服务的时序跟逻辑服务器的某个处理线程附加一种线程的亲缘性,即所 有这个唯一标识过来的请求都由特定的线程来处理,这样可以确保不同标识的连续请求可以被不同的线程并发处理,但是同一标识的请求只能被特定的线程处理,这 样同样可以达到单线程处理的调试目的.
Posted by: youngs | August 8, 2007 11:09 PM