• 搭建一个高并发低时延系统


    首先声明一点:这里的“高并发”是相对的,相对于硬件而言,而不是绝对的高并发。后者需要分布式来实现,这里不做讨论。本文关注的是单机的高并发。

    最近在做一个语音通信系统,要求在线用户2W,并发1K路通话。硬件是两台服务器,酷睿多核,4G内存,千兆网卡(我用过的最好的硬件,负担这些应该问题不大)。

    系统的另一个指标是呼叫时延和语音时延。这是这个系统的关键。最终我们的系统拿到用户现场测试的时候,效果可能有点太好,对方测试不大相信。其实降低时延只要几个地方把握好了,应该问题不大的。这里总结一下。

     

    1、 整体结构:

    整体上采用控制与承载相分离的结构。控制部分负责流程的控制部分,包括流程的建立,处理,语音资源的管理等,是系统核心部分。承载部分主要负责语音处理,包括语音编解码,加解密,转发录音等。这样的好处是:1)降低系统的整体复杂度。2)提高系统的可扩展性。特别是如果用户数上去,这种结构更好扩展。

    这在通信中其实就是一个典型的软交换结构。两台服务器,一台负责控制,一台负责承载。控制和承载之间通过网络通信。

    控制程序是一个进程,可以管理多个承载程序。

    2、 流程:

    要降低时延,关键的一点是功能实现流程的设计。要减少不必要的环节和网元间的交互。数据能够一次通知就不要两次交互。必要的时候,为了时延,可以牺牲一点协议的标准性,使用私有协议完成(至少从目前看没有问题,这个系统是一个端对端封闭的系统。)。

    3、开发语言:

    控制层面使用的python来实现的。控制部分流程逻辑复杂,而python很擅长描述逻辑。本来有点担心python的运行效率,其实没有必要:整个系统的压力在承载,而不在控制部分,控制部分不会有太多的压力;另外,cpu够强悍,时延的瓶颈在I/O。况且,python也重用了我们之前用C实现的协议编解码库。

    承载部分用C来实现。

    4、 利用多核

    利用多进程来利用多核。在承载服务器上,并行跑了两个进程,每个分别处理500路通话。也许线程切换成本更低,但是编程复杂度高。对于线程,我只用最简单的模型。

    控制部分没有多进程,似乎利用不能这个服务器的多核,不过目前来看,还不需要,因为现在就能很好的满足需求。

    5、 网络通信

    承载服务器的压力中很大一部分来自于网络通信。按照我们的功能,1000路语音并发,意味着没20毫秒至少要处理1000个语音包(最恶劣的情况是2000个语音包,包括收发)。

    Libevent开源,号称轻量级高性能,而且应用也广泛,也许是个不错的选择。不过在我看来还是有些庞大,很多特性(跨平台,多种通信模型)我都用不上。

    更为关键的一点事,linux的epool接口足够简单,而且非常好用。接口中提供一个参数可以设置用户数据,这样我可以把一些数据包括函数指针放进去,从而很方便的构造一个事件驱动的网络模块。它能够保证代码足够简单。

    6、 文件读写

    整个系统涉及到文件读写的主要有两块:录音及日志。我们常用的文件操作接口都是阻塞式,进程(线程)会被挂起,等待读写完毕,然后在继续执行。我们都知道,对磁盘的操作要慢很多,所以这个地方是请求时延的一个瓶颈。

    异步IO可以解决这个问题。参考资料:http://www.ibm.com/developerworks/cn/linux/l-async/。不过网上看到有人说AIO接口有bug。时间不多,没有时间深入研究,还是保守的放弃了这个思路。新技术有风险,使用需谨慎。

    Libeio也应该是一个选择。参考资料:http://rdc.taobao.com/blog/cs/?p=1524,它是用线程池来模拟异步IO。问题是,我们的程序主要是写文件,而且一般不需要知道结果,在这种情况下使用libeio的必要性有多大?

    我们最终的方案是参考libeio,直接为承载进程申请了一个线程来负责写文件。主线程负责语音编解码及转发,完全非阻塞,以保证低延迟。涉及的文件写操作,通过接口发送给另外一个线程调用阻塞IO接口来实现。线程间接口很简单,一块要写的内容加一个路径名。

    7、 数据库操作

    我们的数据库使用的是mysql。和文件读写一样,数据库操作也是请求时延的一个瓶颈。在整个流程中,我们会多次的读写数据。我们的做法是:系统启动后,将运行时用到的数据全部读到内存,后面直接查看内存。好在数据不大,这个工作也简单。如果涉及到数据的新增修改删除。则另外一个线程完成相关操作,再通知主线程更新内存。

    最终的结果是,主线程是完全非阻塞的,涉及到阻塞的操作,全部移到另外一个线程中。两个线程不共享任何全局数据,只通过FIFO交互。

    这个地方redis也许是可以考虑的一种选择,它的数据保存在内存,读写效率也非常好。不过相对来说还是有点复杂,而且还是nosql,我们的开发人员并不熟悉。“最小惊讶原则”不但适用于程序接口,也适用于系统。

     

    经过所有的这些考虑和优化,可以基本达到目标,而且足够简单。

     

    如何榨干服务器:

    经过上面的一些优化,基本上可以满足用户的需求了。但我知道,还没有完全的利用服务器的能力(包括CPU,IO)。要进一步榨干服务器的能力,可以在承载服务器上将每个进程的处理能力扩大一倍,每个进程处理1000路。也可以考虑再多跑几个进程。

    控制服务器没有充分的利用多核,可以考虑在控制服务器也运行两个承载程序。

    这样下来,初步估计硬件不提高的情况下,注册用户数至少能够提高到6W,并发呼叫数目至少能够提高到3K。

    提高绝对容量和并发:

    业务特点不同,通信行业高并发的解决方案和互联网行业可能会有一些区别。可以想一下我们使用的电话系统,就是一个实例。它通过分布在全国各地的一个个用户归属的局端,再配合强大的路由能力,以及端到端之间非常标准的协议来解决。

    采用类似的方案也可以提高本系统的容量和并发,不过,目前系统的容量已经可以满足我们公司市场几年内的需求,没有进一步提升的必要,还是保持简单的好。

     

    总结:

    很多时候,使用开源软件都是一个非常不错的主意,可以避免“重复发明轮子”。而且,它还有一定的诱惑:它可以为你的履历加分。

    但是有的时候,你需要的可能并不是“轮子”,想想为一个滑板安装一个汽车轮是什么效果。

     

    什么样的方案才是好的方案?

    1、 满足现在的需求及未来50%的需求。当一个可预见的需求发生概率超过一半时,为它考虑可扩展时必要的。否则会过度设计而冲击简单性。

    2、 保持简单。

     

    最后,再次提一下KISS原则,keep it simple and stupid !

     
    分类: 软件设计
  • 相关阅读:
    PointToPointNetDevice doesn't support TapBridgeHelper
    NS3系列—10———NS3 NodeContainer
    NS3系列—9———NS3 IP首部校验和
    NS3系列—8———NS3编译运行
    【习题 7-6 UVA
    【Good Bye 2017 C】 New Year and Curling
    【Good Bye 2017 B】 New Year and Buggy Bot
    【Good Bye 2017 A】New Year and Counting Cards
    【Educational Codeforces Round 35 D】Inversion Counting
    【Educational Codeforces Round 35 C】Two Cakes
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/2531707.html
Copyright © 2020-2023  润新知