当前位置 : 主页 > 编程语言 > 其它开发 >

压榨机器,Hack,设计极限强度的网络应用

来源:互联网 收集:自由互联 发布时间:2022-05-25
在《对话网友 - TCP一万连接系统设计》文后回复中,短短的评论不足以说明问题,于是单独撰文解释。 对于一般的应用来说,操作系统足以对付,对于极限应用来说,操作系统往往就成

    在《对话网友 - TCP一万连接系统设计》文后回复中,短短的评论不足以说明问题,于是单独撰文解释。

    对于一般的应用来说,操作系统足以对付,对于极限应用来说,操作系统往往就成了我们的障碍,这里的障碍有两个意义,第一个意义是,它出于某种考虑,而禁止了许多可以提高性能的机制,是不能也,另一个意义是,它限制了我们的思维,是不为也。

    由于操作系统要考虑多种方面的应用,因此在设计时做了很多防御性的措施,而对于具体的应用来说,这些措施往往不是最佳的,必要时,我们需要针对自己的应用进行定制,就像Google修改Linux的文件系统一样,为了实现极限的网络应用,我们需要对操作系统进行Hack。对极限应用来说,传统的网络编程模型根本就是不够看的。

    先列举几个应用场景:

    场景1:SmartBit是个很NB的协议测试工具,但是它只能测试一些最基本的协议。如果要测试一些更广泛的协议或者自定义协议,只能自己编写测试工具了,但是自己编写的测试工具受操作系统限制,如果使用传统的编程模型,很难达到超高的性能。

    场景2:有一些应用,这些应用的逻辑比较固定,而同时对性能要求又极高,比如说,各种专门的服务器,时间服务器啊,DNS服务器啊,大型的仿真系统啊,交换机啊,路由器啊,IDS啊等等,当然,这些都可以通过硬件来解决,但如果能用通用机器通用的软件来解决不更好吗?

    对于这些应用,就需要抛弃传统的网络应用概念了,什么Socket啊,IOCP啊,全见鬼去吧——都不够看。当然,如果常规开发就可以解决,比如《对话网友 - TCP一万连接系统设计》文中所提及的场景,自然就不需要采用专门的手段了。下面说的是常规方法解决不了时,当IOCP也只够塞牙缝时,应该怎么做的问题。

===========================

    大型应用为了提高性能,往往会放弃关系数据库,回归传统的key-value型数据库,为了极限化网络程序的性能,我们需要放弃传统的编程模型,回归最传统最原始的编程模型。

    对于操作系统来说,影响网络应用性能主要有这些方面:

    (1) 进程:现在的OS都是多任务系统,而单任务系统的性能是最佳的。

    (2) 内存:packet的复制问题。数据从到网卡,到内核,再到应用程序,要复制好几次,这些是无谓的。

    (3) 系统调用:系统调用是非常耗费资源的,Socket访问啦,内存分配啊,获取机器时间啦……都是系统调用。

    (4) 编程模型:IOCP模型不是最佳的。而传统的基于线程的编程模式,在对付大并发量时,完全不够看。比如 … 要跑1000万个线程 … 这时只能使用proto thread(或erlang所谓的轻量级线程)。假设更高呢?一亿个线程?这就要对线程进行消解了,采用完全的离散处理机制,把线程彻底的消解掉。

===========================

    下面,就以上四点来说,看看我们可以做哪些工作,把系统压榨到富士康的程度。

    (1) 进程

    为了这些,单独搞个操作系统不划算,就在现有的操作系统基础上看看可以怎样解决。很显然,我们不需要其它的应用来干扰我们的网络应用。因此需要给这个应用以最高的优先级。不Hack操作系统的情况下,最高的优先级也就是实时进程了。

    (2) (3) 内存和系统调用

    为了避免内存的复制,减少系统调用,需要和传统的Socket说ByeBye。最好的方式是自己实现一个协议栈。当然,不必实现一个完全的协议栈代码,只需实现我们需要的那部分即可,实质上,实现UDP协议只需要几百行代码,实现TCP也只需要三四千行代码,同时,又有很多开源的实现可以参考,这个工作看起来困难,实际上并不是很困难。还有个问题是内存的分配。操作系统内存的分配是低效的,因此,需要使用对象池,将内存重复使用。

    (4) 编程模型

    放弃线程,采用最原始的基于事件的离散处理模型。简单来说,我们把每一个需要做的工作分解成一个个的事件,然后放在队列中,应用程序呢,从队列中一个个取出事件并执行,执行的过程中,如果有其它后续操作,可以生成新事件,放入队列的尾部。

    如果我们需要某些事件优先执行,简单的队列就不行了,需要优先队列。更进一步,我们如果需要引入时间模型,需要安排某个事件在某个时间执行,时间在前的事件优先执行?怎么办呢?以前普遍使用的是Heap,Heap的插入的复杂度是O(logn),查找最大元素的复杂度是O(1)。Heap就是最优了吗?不是!还有比它更NB的数据结构——Calendar Queue,Calendar Queue插入的复杂度是O(1),查找最大元素的复杂度也是O(1)。因此,我们需要一个基于Calendar Queue的事件调度器。关于Calendar Queue,可以参见《Calendar queues: A fast O(1) priority queue implementation for the simulation event set problem. Communications of the ACM, 31(10):1220-1227, October 1988》这篇文章。Calendar Queue Scheduler网上也有开源的实现,几百行代码而已。这样一来,我们就将程序的调度由操作系统调度变成我们自己调度了,且调度的复杂度为O(1)。

    基于事件的处理有一个最大的问题就是每一事件的处理时间不能过长,否则容易Block住整个执行流程。具体实现中,可以将长事件分解成一个个的小事件离散的执行,这对程序员的要求较高。

===========================

    下面就把上面几点整合起来,看一个完整的工程实现。

    首选,在OS选择上,Windows不能用,不容易Hack。选择Linux。其实应该选用实时Linux的,因为普通的Linux,它的实时进程的优先级是低于系统进程的,但鉴于对实时Linux不熟悉,就选用普通的Linux了。普通的Linux已经够用了。

    然后,如何收发Packet呢?既然已经弃用传统Socket了,那就必须自己写一套收发机制了。我尝试过两种机制:

    一种是基于信号的机制,当有数据包来时,操作系统可以向应用程序发出一个信号,应用程序收到信号后可以自己去取。测试感觉,在大数据量下,这种情况丢包比较多;

    另一种是基于PF_RING Socket(http://www.ntop.org/PF_RING.html)的机制。PF_RING是在操作系统内核中建立一个环状缓冲区,然后应用程序定时扫描这个环状缓冲区,即不用复制内存,又绕过了操作系统那些复杂的Socket机制,这样可以最大化性能,同时,最小化丢包率。

    从环状缓冲区接收到数据之后,就需要自己来解析。关于轻量级协议栈的代码网上有不少,改吧改吧就能用。核心的几个协议没多少行代码。解析成功之后,就将它包装成一个个事件,给每个事件安排个Handler,然后把事件挂在Calendar Queue里等候处理即可。

    整个这么一套下来,那性能是超级牛——整个处理过程被消解成O(1)了,也就是说,在达到硬件限制之前,系统参数比如响应时间、丢包率等只和吞吐量有关,和并发连接数量无关——这里根本没有并发的概念,只有packet的收发和调度处理,主要的过程都是O(1)复杂度的。实际测试中,如果应用层计算量不大,那么限制主要来于网卡。我当初采用的只是奔4的普通pc机,百兆网卡,100Mbps全部跑满了,cpu依然很蛋定。而如果应用层计算量大,主要限制就是在cpu了。在这种编程模型下实现了QQ协议的客户端和服务器端,在一台机器上跑2万个QQ的客户端,按真实的基于UDP的QQ协议进行登录,获取好友,发送信息,发送群信息。每个QQ使用的ip地址和端口不同(由于绕过了系统的Socket,当然可以使用不同的ip地址了)。在另一台机器上跑个模拟的QQ的服务器,接收客户端的信息,然后发给要发到的客户端的ip。两台机器都是奔4机,单线程在跑。CPU占满了,主要cpu计算集中在编解码,应用逻辑这一块。内存占用只有30M,丢包很低。这里的限制就主要是CPU了。进行Profile,真正用在网络和调度部分的cpu只占20%。

    这种编程模式是最难的异步编程模式。但通过合理封装,可以简化操作,封装的好的话,写起来程序也是很happy的。而在现实中,每个网络协议,在正式提交之前,都会进行仿真研究,而网络协议的仿真正是采用的这种模型——因为这种模型性能极高,灵活度也最大。采用拿来主义的话,很多应用层协议只需要改改就可以用了。

===========================

    当然,这说的是极限情况下的系统设计问题。在很多情况下,我们需要极限的设计,但由于思维可能被限制住了,而打不破思维定势,从而采用普通的设计,这就是不应该的了。不能动不动就是啥IOCP只能多少啥啥啥,IOCP只是常规的极限,不是Hack的极限。而事实是,一台稍微好点的机器,可以实现1000Mbps的处理速度,算下来是100000packet每秒。这种吞吐量下,有可能是100万会话(这里就不用连接这个词了),每个会话每10秒发一个packet,也可能是10个会话,每个会话每秒发10000packet。而现有的操作系统的API针对后者比较擅长,但实际上两者的吞吐量是一样的,略有不同的是100万会话可能占有更多的内存资源而已。

    有时,不需要极限的设计,自然是不必采用这种手段了。

版权所有,欢迎转载
网友评论