服务器编程之路:进无止境(上)

首先不好意思,盗用了福特汽车的广告语,呵呵。


今天想在这里探讨一下高性能服务器(server)编程的一些通用技术(或者说是思想)。编程技术发展至今,高性能服务器编程领域仍然是C语言的菜。而C语言在服务器编程中的技术,也不断在实践中提高,正暗含我们的题目。


有基础的初学者写的第一个基于TCP的服务器程序,想必大概是这样的:


while (1)

{

         listen();                      // TCP套接字监听

         fd = accept();          // 接受远端连接

         create_thread(fd);        // 把这个连接扔到一个新开的线程里进行业务处理

}


现在我们知道,这样的服务器性能是有问题的。每接受一个连接,就要开一个线程进行处理,假设这个服务器有1000个并发,就需要同时有1000条线程。而OS调度进程或线程是有代价的,调度1000个线程,OS是很累的,平白无故耗了很多CPU在线程切换上,非常不划算。


虽然性能上不理想,但上面这第一版服务器程序结构上还是合理的。一个线程服务一个连接,线程相当于一个沙盒,里面怎么处理,都不会影响其他连接。所以对于性能要求不高的场景,用这种逻辑的程序简单方便。实际中,apachehttp服务就是按照这种逻辑实现的。


但码农们永远不会满足于这点性能的,这也不符合高性能服务器的要求。现在的服务器往往要求上万的并发度(意味着同时有上万条连接),这就不能靠无限扩张线程数来实现了。所以问题转化为:如何使用有限的线程数,服务无限的连接数?


主流的,并早已被大家接受的方案是这样的:

1.        使用IO复用机制(如select/epoll),把所有已连接的套接字监听起来。

2.        当某个套接字发生网络事件(比如有数据到来),回调给使用者。

3.        回调所在的线程,是从一个有限大小的线程池中取出来的。

这即所谓的“事件模型”。所有处理不是顺序的,而是由事件触发,在回调中处理。


开源代码中,比较有名的库libevent,干的就是这个事情。在我们公司内,主要用在设备的netframework,软件线libdsl里面的DEngine组件,互联网团队使用的litepi库中的IOEngine,都是这个思路。


看起来很美是不是?至少性能上解决了“使用有限的线程数,服务无限的连接数”这个命题。但实际上,这个命题隐含了两个预设条件,如果使用者不能很好地理解这种模式的运行原理,就免不了要踩雷。


预设条件1:根据抽屉原理,一旦连接数大于线程数,必然存在一条线程服务多个连接的情况。因此,连接不再有一个干净的运行沙盒。如果在处理一个连接时造成阻塞,就会阻塞这个线程,从而把该线程上的其他连接也阻塞了。


预设条件2:事件模型下,事件有可能被不同的线程回调。而正常情况下,一个连接的多个事件必然共享这个连接的数据,因此这些数据就会存在多线程竞争问题。使用者必须意识到同一个连接的处理,实际是在多线程中的,这往往与直觉相悖。


关于上面两个预设条件,可以多说两句:


关于条件1,直观的打个比方:好比打地鼠游戏,冒出一个地鼠你就得打。但万一你把一只地鼠打坏了(卡在洞口缩不进去了),那这整台游戏机就坏了。。。


关于条件2,如果能够将同一个连接的所有事件都固定在一个线程中回调,就能避免多线程竞争的问题了。这种模式称为“非对称”处理,相对的,前一种称为“对称”处理。非对称处理的缺陷在于无法均衡使用各条线程。但是对于大并发(并发上万)的场景,数量一大,就能达到统计平均,这种缺陷也就不存在了。所以这也是一个取舍的问题,netframeworkDEngine是对称处理,IOEngine就是非对称处理,它大大简化了使用者的编程。


好了,我们给出第二版的服务器程序,采用了“事件模型”,它是不是完美了呢?性能上,也许是;但在使用上,绝对不是。最根本的一个问题,它违反了人类做事的直觉。因为一般人,就算是水平很高的码农,干事情也喜欢一件事情顺序干到底(愚蠢的人类啊)。正常人盖房子肯定是:我要搬砖,我要砌墙,我要粉刷,完工!但是事件模型却要求:来来来,你去搬砖;来来来,你去砌墙;来来来,你去粉刷;好了,房子盖好了。人无法控制程序,反而被程序驱使,算不算悲哀呢?


至此,我们抛出第二个命题:有没有满足命题一(使用有限线程服务无限连接)的同时,又能让我们顺序地、同步地写程序?


答案当然是:有的!


(未完待续)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章