简单的搭建一个高并发低时延系统
首先声明一点:这里的“高并发”是相对的,相对于硬件而言,而不是绝对的高并发。后者需要分布式来实现,这里不做讨论。本文关注的是单机的高并发。
最近在做一个语音通信系统,要求在线用户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 !