呱呱视频社区(guagua.cn)于2006年上线,是国内较早涉足视频直播业务的平台之一,以“即时多人视频互动”为产品核心,经过多年积累,如今注册用户逾8500万,同时在线用户逾100万。
在呱呱视频社区高速发展过程中,我们遇到并解决了非常多的技术难题,下面就与大家分享曾经面临的一些“坑”,剖析我们的解决之道。
众所周知,视频社区与IM、网络游戏行业不同,承载海量用户需要很大的网络带宽成本,以及众多的服务器资源。
我们的祖国幅员辽阔,城市按GDP规模、居民人均收入、500强企业落户数量、机场吞吐量等硬指标等,分为若干个级别。一线城市的IDC机房稳定,服务质量好,但价格不菲,而二线、三线城市,IDC的各方面比一线城市有明显差距,但胜在价格实惠。
由于互联网公司的低成本运营模式,尤其是初创公司为降低成本,一般都大量使用了二线、三线城市的IDC机房。使用网络、设备质量差一些的IDC机房,且需要提供高质量的服务。在服务器硬件上,各公司也是尽量选择物美价廉的设备,交给程序员们物尽其用。
首先介绍较为简单的服务器层面软硬件问题。服务器操作系统运行中可能崩溃,硬盘、CPU、内存等各种硬件随时都可能发生故障导致死机,且之后再重新启动也无法成功;同时由于各种原因(主要是成本考虑),该淘汰的设备还在超期服役等。其次复杂的网络质量方面,UDP丢包,有网络设备瑕疵导致大量丢包情况,也有偶发的少量丢包等;还有TCP已建立连接异常断开,新建立连接缓慢;甚至还会遇到网络STN攻击、流量洪水攻击等等。
为克服以上困难,我们做了很多努力,核心思想是负载均衡、故障转移。即同类服务承载要尽量均衡,从而避免某个点压力巨大,导致崩溃问题经常发生;当某个点发生故障,服务器端需要尽快进行切换和转移。此处比较难的是区分故障类型:瞬间问题马上就可恢复的,以及无法恢复而必须尽快转移的。考虑到服务器端进行切换,也需要成本或面临各种风险,故要慎重。
下面以视频服务器的负载均衡和故障转移的应用举例,向大家介绍下实现的细节和注意事项。
如图1所示,所有的业务服务器都需要定时(比如1分钟)向资源分配服务器汇报心跳包,包括当前的负载情况,如网络流量、支撑的用户数等各种参数。资源服务器会按业务服务器类型建立各类型服务器的资源池,每个资源池定时分析其当前各业务服务器的负载情况,按负载情况及预定级别形成不同的分组。预定级别可以分成3-5个级别,例如负载轻、中、重、超出、不可用等,级别不易过多也不易过少。
媒体接入服务器会定时向资源分配服务器查询当前媒体数据分发服务器的可用列表,得到后会把该列表缓存在本地;当需要新建分发数据通道时,则找到可用的分发数据服务器进行连接和使用。资源服务器会实时监测媒体数据分发服务器的连接状态,若某服务器宕机,则会把该服务器标记为不可用,并通知使用它的各服务器;各服务器收到该通知后会主动进行切换,从而实现故障转移功能。
需要注意的有几点:
在解决负载均衡和故障转移后,避免出现单点服务,比如查询某个视频房间的入口位置,只能到唯一的服务器进程上查询,如果该进程挂了,则后面的所有用户都无法登录房间,这种情况一定要避免。避免的方法有很多,主要是数据不能为某个进程独占,而需要同类服务共享;数据共享的难题是进行实时的更新和同步,在更新时要考虑多进程之间的锁定,在同步时要考虑跨机房保证安全等。
Redis是一个高性能的key-value数据库,我们也使用它共享数据,它支持主从同步,很方便地实现了跨机房数据共享。当系统提供的服务规模小,投入的成本低时,单点服务情况较为常见,而一旦系统承载量巨大,务必需要解决单点问题。
中国的互联网基础设施由几个大运营商和众多中小运营商组成。大运营商控制骨干网络,故服务质量好价格也要贵一些;而中小运营商由于价格实惠,也有众多拥趸。为保证不同网络的用户都达到较好的视频直播体验,呱呱花了非常大的技术力量做异构网络融合。
异构网络的融合有一定困难,各互联网公司无论大小都要面对,如果选择使用的机房分布过碎,成本会很高,数据一致性很难保证;如果分布得不够,大量用户体验会变差很多。在呱呱视频产品的整个运营过程当中,这是一个很大的技术难题。
在考虑成本的情况下,我们少量使用了双线或BGP机房,由BGP机房承载核心媒体数据的交换,从而实现低成本异构网络的完美跨越。针对该解决方法,这里简单描述下,为跨越异构网络,我们在BGP机房需要建立逻辑层的媒体数据传输通道。如果当前视频的规模需求比较大,需要跨越异构网络,此时会向后端服务申请一个媒体数据传输通道,由该通道实现不同异构网络之间的互通互联。
下面讲述呱呱视频社区在媒体数据传输过程中遇到的问题和解决思路。
作为视频产品运营平台,提升用户使用视频的体验是我们一贯的追求。发扬UDP协议的速度优势,且通过程序员们尽心竭力的思考和努力,着力避免UDP协议的缺点,如易丢包、偶发抖动、乱序等问题是我们的头等大事。
为解决UDP包的丢失问题,我们考虑过多种方法,重传、冗余发送、动态路由等,权衡实现复杂度、实施成本等,最终我们选择了重传。
在重传情况下,为尽量降低网络延迟,我们在每个层级都执行丢包检查,从而避免在最终接收方做检查再补包的延迟巨大问题。即每个层级的接收方,都为媒体数据传输建立相应的缓冲队列,然后使用定时任务扫描该队列,一旦发现丢包,即向上一级申请补包,在缓冲时间范围内都要进行补包。需要注意的是,缓冲队列以时间,而不是UDP包的个数为单位,就是为了补包计算时间长度和控制方便。
比如缓冲队列的时间为10秒,则队头队尾的最大时间间隔范围内,一旦发现丢包,则立即向上一级申请补包。补包的时间长度取决于往返时延(RTT,Round-Trip Time),RTT(往返时延)在计算机网络中是一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。
使用每个级别独立补包的好处,还可以避免出现网络查询风暴的情况,即不会导致主播或某个服务器崩溃,举例如下。
A主播发给服务器S01,S01服务器发给10台服务器S20-S29,而S20-S29服务器每台都负责分发给1000个正在收看的用户,此时共用1万名用户在观看A主播的视频。
当A主播网络瞬间抖动时某个包未发送成功,服务器S01未能收到;如果使用最终丢包检查和补包方式,1万名用户都会申请补包,而S21-S29的服务器都会把该请求透传给S01服务器,故S01瞬间就要处理1万次请求,也许它还能承受,毕竟是服务器嘛。如果S01再把这1万次请求丢给A主播,想象下,A主播的机器瞬间就会崩溃了,即使不崩溃,后续的直播估计也被打断了。如果网络抖动时,A主播仅面对S01服务器的若干次补包申请,则A主播的机器承载应该没有问题,可以应付自如。
对于UDP补包的细节,还有一点需要注意,即每次补包的时间差不必为固定值;如果S20发现某个包未到,则向S01服务器申请补包,第一次可以在发现丢包后100ms发起申请,如还未收到,第二次则可以在上次发起后150ms再次发起;再次发起的时间间隔可以类推,200ms、250ms、300ms等,直至最大400ms。
为什么不选择固定值,比如200ms,而动态调整且逐渐增大呢,这样做的好处是什么呢?第一次申请补包为100ms,时间长度比较小,目的就是希望上一级瞬间就能把丢失的UDP数据包补回来;而后逐渐增加补包时间间隔,目的是为了减轻上一级的压力,为什么S20服务器一直未收到该丢包呢,很可能是S01服务器上也没有这个包,此时无论S20服务器的申请有多少,该申请都会被S01服务器丢弃,故再多的申请也都没有意义了。当然补包申请时间间隔也必须有个最大值,而不能无限增加,否则及时性就太差,而没有补包的意义了。
针对UDP包出现的乱序和抖动情况,可以通过接收方缓冲区的处理算法来进行优化,首先在接收方客户端创建抖动缓冲器(Jitterbuffer),这是一个共享的数据区域,在该区域中,每隔一段均匀的间隔,媒体数据包会被收集并发到相应的语音、视频处理器。
UDP包到达时间的变化称作抖动,由于网络拥塞、路由变更而产生。抖动缓冲器位于接收端,它有意地延迟到达的数据包,如此一来,终端用户就会感受到一个清晰的,没有什么声音失真和视频连续的媒体网络连接。
我们一般根据接收端用户的网络情况,不断调整抖动缓冲器的时间长度,如果网络变差,那我们就尽快增加缓冲时间,从而保证音频视频的连续性;当网络通讯状态变好后,而不必着急缩短缓冲时间,以避免网络再次变差产生摆动效应,导致用户的音频视频播放由于网络稍有变动就不断调整。
当增加缓冲时间时,观看用户会卡一下,而调整之后则不会出现,比没有数据播放而反复出现的卡现象,用户体验会好很多。如果缩短缓冲时间,则需要丢弃一些未能播放的音视频数据,故也会给用户带来些许影响。当然缓冲时间范围也要有个最小、最大的范围,具体该范围可以根据产品的业态不断调整和完善。呱呱视频就通过以上算法的实现,获得了很不错的用户体验。
基于各方面情况,包括上面讲述的互联网网络质量差,偶发的各种软件、硬件故障实际情况,在程序员们的百倍付出后,仍然不能保证服务质量达到100%稳定可靠。
为提高服务质量,需要提供一定的冗余能力,建议各位天才工程师在冗余能力范围以内做事,而不要超出范围考虑或实现。比如国内骨干网络异常,此时可能需要网络维护和研发工程师一起手工进行干预,而不要指望通过软件自动适应,毕竟这种异常情况很少发生,投入过多的冗余成本是否合算,个人理解需要视自己产品的运营目标确定。
互联网行业的有损服务,瞬间卡顿,个别的延迟和缓慢,在视频直播行业个人理解是可以接受的,当然我们也希望在面对海量用户时尽可能保证服务顺畅。
为什么说有损服务可以接受呢,原因是造成瞬间卡顿的原因多种多样,有时是因为服务器硬件、软件造成,有时是因为接收方用户的网络情况、系统资源消耗过高等,有时甚至是有视频直播主播网络瞬间卡顿。
针对不同情况,保证服务100%可靠异常艰难,但我们是不是就什么都不需要做了呢?当然不是。
为了不断提高用户体验,我们不断收集用户信息和状态,从海量数据中进行分析,哪些服务和体验问题是个例用户,哪些是广泛存在的。针对广泛存在的用户体验问题,要及时进行跟踪和处理;针对用户个例,也要做适当分析,防止其成为广泛问题。
定义是个例情况还是广泛问题,有时是比较困难的,较为简单的方法是按照出现的频率、规模进行分类整理,从大到小进行排序和处理。同时还要注意数据的实时分析和事后分析相结合,由于服务器的运算能力是宝贵资源,且我们需要及时处理出现的偶发大规模问题,故实时运算是需要优先保障运营稳定的,即牺牲小我成就大我。
而事后分析,我们可以拿出专用资源,或者在业务低峰期进行专门的分析和运算,用于评估整体情况,这种分析一般安排在凌晨(比如2点)开始,大家都已进入梦乡,而服务器资料闲着也是浪费。等到早晨上班时,如果分析系统运行正常,一份昨天的运行报告也许已经发送到你的邮箱了。
此时,作为运维工程师和专门处理的研发工程师,要盯紧该运行报告,查漏补缺,在故障大规模发生前进行处理,避免发生问题导致挨骂;万一挨骂了也要不气馁、不言败,再接再厉,分析出是运行报告不够完善,还是由于关注细节不足,导致未能在问题萌芽阶段发现它。
举个真实例子,说明实时统计的作用和问题。
原模型是这样的,后端服务器S30-S50是20台核心业务处理服务器,它们会每间隔5分钟把当前负载情况写入Redis,而后端监控程序,也是以5分钟的频率到Redis读取数据,并呈现出来。一旦监控程序发现某个服务器问题,可以使用短信、电子邮件、播放音乐等方式提醒;当时还安排了值班的运维工程师,后半夜万一睡着,就指望播放的音乐叫醒他进行手工处理了。
这一切设计得非常美好,可问题还是发生了,当时监控程序是每读取一次显示一行记录到窗口上。当天夜里Redis崩溃了,监控程序没有对该情况进行保护,在未能读出数据时,没有给出错误提示,而持续刷新的数据就停到那里了;监控程序内部只是在不断重连,尽力读取数据了。而运维工程师由于经验不足,从没遇到监控程序不再刷新的情况,且已习惯监控程序的正常运行很久,未能仔细观察,以致故障几个小时之后才被发现。
对这个例子进行分析,可以发现运维工程师和监控程序都有责任,运维工程师未能及时发现停止刷新问题,监控程序的开发者未能全面考虑和兼容各种异常,只检测核心服务器的负载,而没有考虑到Redis崩溃情况。即监控程序的稳定运行,不能假设在Redis正常的基础上。
这次问题给我们的教训是,任何监控系统都要充分考虑到各环节异常情况,即使该环境非常稳定可靠,望大家能引以为戒,不要再犯类似错误。
关于:中科研拓
深圳市中科研拓科技有限公司专注提供软件外包、app开发、智能硬件开发、O2O电商平台、手机应用程序、大数据系统、物联网项目等开发外包服务,十年研发经验,上百成功案例,中科院软件外包合作企业。通过IT技术实现创造客户和社会的价值,致力于为用户提供很好的软件解决方案。联系电话400-0316-532,邮箱sales@zhongkerd.com,网址www.zhongkerd.com