某年某月某一天,我需要搞一个server来向众多的client分发数据。于是,首先,我来研究下这个server面对的要求。
1.首先,我希望能够应付尽可能多的client,这样可以为老板省钱;
2.其次,这些个client都是老不死的(tcp长连接),我希望我的server能很好的应付他们;
3.再次,这些老不死的还非常烦人,jjww个不停(数据交换大且频繁);
ok,我们得出暂时的结论,此server要求应对尽可能多的长连接,和大数据量交换,同时要求尽可能的降低cpu负载和内存使用率,简而言之,我把老板对我的期望加诸到此server身上:既要马儿跑的快又要马儿不吃草。
由上分析可见,首先,我们的server需要一个快速有效的I/O处理模型来应付众多client的折磨。那么我们有什么选择呢?
1.克隆
没错,既然一个server无暇应对如此来势汹汹的client,我们决定把自己复制n份,将client们分而化之。
正如我们在古老(经典)的网络编程教程所见,server的每次accept都伴随着一次fork(pthread_create)操作,使得这个fork出来的复制品来对付一个client。以一对一,公平合理。
等一下,光这样够吗?如果来了1000个,10000个client我们的server会怎么样呢?此时,众多的process(thread)用于处理client的请求,而cpu忙于在众多的process(thread)间切换,调度,处理锁,自顾应接不暇,实在没有功夫搭理client的请求了。
由此可见,克隆对付少数client时可以,对付成千上万的client就只能捉襟见肘了。
2.异步
为了避免众多进程导致的问题,异步模型开始引入。经典的如select,poll,最新的如我们的主角epoll。
那么,他们是怎么干活的呢?
首先,异步模型中只有一个进程,可是他能对付众多的client吗?我们从cpu的处理说起。
在单核的cpu中,所有的指令其实都是顺序执行,这就是说,一条指令在执行时,后续的指令必须等待前者的完成(部分完成,流水线可有效缩短等待时间),因此,我们前面所是的多进(线)程并不能真正的同时执行,那他们是怎么被处理的呢?
很简单,管理这些进(线)程的os使用了障眼法,它使用某种调度算法,给每个进(线)程一小段cpu时间工作。话说回来,就是当A进(线)程工作了一段时间后,会被os强制切换出去,换B进(线)程工作一段时间,如此A和B轮流坐庄,看起来似乎是并行工作了。
当我们拆穿这个把戏之后,因为os对众多线程时的差劲表现,我们可以自己玩了。就是说,我们通过使用单进(线)程模拟这个os对多进(线)程处理的过程来避免多进(线)程带来的副作用。
当然,这个模拟还是需要内核的配合的。
拿select举例,这个过程如下:
1.众多client的连接对应于不同socket上;
2.select向内核询问有没有他感兴趣的sockt可以操作;
3.若没有则等待或返回;
4.若有,挨个对准备就绪的socket进行读写操作
这样,就相当于每个client被分到相应的时间片得到了处理。所以,尽管整个工作是串行完成的,但是就我们看来,我们的server同时应付了众多的client。
3.克隆 + 异步
尽管异步相当完美的完成了我们交给他的任务,但是在负载巨大的情况下,我们还可以让克隆和异步一起工作。
一种做法是,一个server复制若干份,然后他们独立地进行异步处理(lighthttpd)。这也是目前非常实用高效的做法。