高效网游服务器实现探讨(一)
转载请注明出处:http://blog.csdn.net/phoenixsh
随着多核处理器的普及,如何充分利用多核并行工作就成为高性能程序设计的一个重点。本系列文章将围绕高性能网游服务器的实现,探讨这方面的技术。
网游服务器的特点是:
具有大量客户端连接(数百至数千个),每个客户端都以一定的速率不断发送和接收数据;
服务器端的数据流量通常在几个至几十个Mbps之间;
数据需要实时处理;
数据包具有时序关系,往往需要按照严格的先后顺序予以处理。
网游服务器实际上代表了一类典型的新兴流数据处理服务器。这里只是为了讨论方便而限定于网游服务器,但是所讨论的原理和技术应该是普适的。
同步多线程技术肯定是无法满足要求的。由于每个客户端都在持续和服务器交换数据,系统将无法有效管理太多的线程;即使使用线程池技术,所能服务的客户连接也是很有限的。至于数据处理的实时性和数据的时序都无法顾及。
异步技术有好几种方式,这里只讨论IOCP和轮询模式。IOCP是微软推动的技术。对非常大量的连接(数千至数万)很有效。但是由于使用了多线程,这些线程需要把所需读写的数据通过共享的FIFO与主线程解耦(否则无法保持时序)。这就造成频繁的线程切换,无法满足大数据量的实时处理要求。另外,由于网卡只有一块(就一个网络地址而言),多线程并不能增加读写的速率。在另外一些时序要求不那么严格的场合,这些线程可以各自独立完成所有的处理任务,只需要在线程内部保持数据的时序。这就是向同步多线程技术退化了。
轮询是常用的模式。程序员把需要处理的Socket连接注册到一个数据结构中,然后提交给系统检查它们的读写状态。系统返回可供操作的Socket连接列表供程序员逐个处理。如果有数据可读就读入并处理,如果可写则把相应的数据写出去。为了提高效率和程序结构的清晰起见,Socket服务器通常单独使用一个线程,并且通过FIFO数据结构和主线程解耦。
在单核处理器上,上面这种轮询的模式是没有问题的。但是在多核平台上,用于解耦的FIFO将会变成并发瓶颈。这是因为传统的实现技术必须对FIFO加锁。虽然网络线程和主线程分别跑在不同的核上,理论上可以物理同时地运行(如果分别操作不同的数据项),但是同步锁却强行迫使其中的一个线程必须等待另外一个线程退出临界段,即使另外一个核空闲着。
这时候就需要一种支持并发的数据结构,下面称之为ConcurrentFIFO。
public interface ConcurrentFIFO {
public Object remove();
public void put(Object o);
}
put方法把一个数据对象推进FIFO,而remove方法从FIFO删除并返回一个数据对象。通过精心设计,ConcurrentFIFO的实现是线程安全的,两个线程可以安全而同时地访问FIFO。这样在多核平台上就能达到极高的性能。
通用的ConcurrentFIFO是非常难于实现的。基本的技术是使用原子的CAS操作来实现。CAS即CompareAndSet。现代处理器基本上都能支持这一类指令。但是这种数据结构的实现的一个很大的障碍就是垃圾回收。在多线程并发运行的情况下,被原子替换下来的数据无法得知其是否是其它线程所需要的,也就无法决定是否回收这块内存。除非有垃圾回收器,否则ConcurrentFIFO是很难实现的。(鼓吹手工管理内存效率最高的朋友们请瞪大眼睛看清楚)
其实,即使是对于有垃圾回收和内建线程支持的Java语言,要想构造一个支持并发的数据结构,也是极端困难的。java.util.concurrent包是经过并发领域的专家(Doug Lea,同时也是早期lig++的主要作者,以及DLmalloc的作者。我后面讨论内存管理的时候还要提到他)精心编写,并且由java社区的许多专家仔细评审测试之后才发布的。
我在这里当然不打算自己实现一个通用的ConcurrentFIFO。Java语言的使用者是幸福的,因为这些东西标准库里面已经提供了。(C++版本的恐怕要等到C++0x出来了。听说还有很多人反对C++增加垃圾回收器,那样可够悬的。)但是考虑到网游服务器主要还是使用C/C++编写,我将尝试提供一个C或者C++版本的实现,只能供单线程读单线程写(Single Reader Single Writer),使用细粒度锁实现(而不是CAS技术)。虽然简陋了点,但是能够满足需要。不过我最近很忙,而写这种东西又非常费时间,所以我不能保证什么时候才有下文。今天太晚了,就先写到这里。发表于 @ 2007年03月07日 00:55:00|评论(loading...)|编辑