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

📅 2026/7/6 4:33:34
压榨机器,Hack,设计极限强度的网络应用
在《对话网友 - TCP一万连接系统设计》文后回复中短短的评论不足以说明问题于是单独撰文解释。对于一般的应用来说操作系统足以对付对于极限应用来说操作系统往往就成了我们的障碍这里的障碍有两个意义第一个意义是它出于某种考虑而禁止了许多可以提高性能的机制是不能也另一个意义是它限制了我们的思维是不为也。由于操作系统要考虑多种方面的应用因此在设计时做了很多防御性的措施而对于具体的应用来说这些措施往往不是最佳的必要时我们需要针对自己的应用进行定制就像Google修改Linux的文件系统一样为了实现极限的网络应用我们需要对操作系统进行Hack。对极限应用来说传统的网络编程模型根本就是不够看的。先列举几个应用场景场景1SmartBit是个很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) 编程模型放弃线程采用最原始的基于事件的离散处理模型。简单来说我们把每一个需要做的工作分解成一个个的事件然后放在队列中应用程序呢从队列中一个个取出事件并执行执行的过程中如果有其它后续操作可以生成新事件放入队列的尾部。如果我们需要某些事件优先执行简单的队列就不行了需要优先队列。更进一步我们如果需要引入时间模型需要安排某个事件在某个时间执行时间在前的事件优先执行怎么办呢以前普遍使用的是HeapHeap的插入的复杂度是O(logn)查找最大元素的复杂度是O(1)。Heap就是最优了吗不是还有比它更NB的数据结构——Calendar QueueCalendar 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 Sockethttp://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的。而在现实中每个网络协议在正式提交之前都会进行仿真研究而网络协议的仿真正是采用的这种模型——因为这种模型性能极高灵活度也最大。采用拿来主义的话很多应用层协议只需要改改就可以用了。