写点什么

鏖战双十一 - 阿里直播平台面临的技术挑战

2016 年 1 月 27 日

前言:一直以来双十一都是以交易为重心,今年当然也是如此,但是这并不妨碍万能的淘宝将双十一打造的让用户更欢乐、体验更丰富、玩法更多样、内容更有趣,因此,今年也诞生了以直播为特色的游戏双十一会场,也就是本文所要着笔重点介绍的,即阿里直播平台在双十一所面临的复杂技术挑战以及技术选型的台前幕后。

大型直播的技术挑战体现在大流量、高并发场景下,视频流的处理、分发,播放质量保障,视频可用性监控,超大直播间实时弹幕及聊天互动,高性能消息通道,内容控制 (包括视频内容自动鉴黄及消息文本过滤),以及系统可用性、稳定性保障等等方面。本文将针对其中的一些技术细节,抽丝剥茧,希望通过些许文字的分析和介绍,能让大家有所启发。

视频直播

对于直播平台来说,为了保障各种网络环境下能够流畅的观看视频,需要将高清的输入流转换出多路不同清晰度的视频流,以支持不同网络条件下视频清晰度的切换,而由于不同的端所支持的协议及封装格式并不完全相同,比如无线端的 HTML5 页面可以很好的支持 HLS 协议,但是对于 RTMP 这类协议基本无能为力,而 PC 端为了降低延时,需要采用 RTMP 这一类流媒体协议。因此,为了支持多终端 (PC、Andriod、IOS、HTML5) 观看,需要对输入流进行编码及封装格式的转换。转码完成之后,还需要对视频流进行分发,毕竟源站的负载能力有限,节点数有限,离大部分用户的物理距离远,对视频这一类十分占用带宽资源的场景来说,为了提高播放质量减少卡顿,需要尽量减少到用户的传输链路。因此,通常的做法就是将视频流进行切片存储到分布式文件系统上,分发到 CDN,或者是直接通过 CDN 进行流的二级转发,因为 CDN 离用户最近,这样才能保证直播内容对于用户的低延时,以及用户的最短路径访问。客户端对延时的要求,以及采用何种协议,决定了视频是否需要分片,分片的目的在于,通过 HTTP 协议,用户不需要下载整个视频,只需要下载几个分片,就可以播放,实际上直播与录播的技术是相通的,区别在于直播的流无法预测终结时间,而录播的视频流终止时间是已知的。对延时要求没那么高的场景来说,客户端可以采用 HLS 协议,毕竟 IOS、Andriod、HTML5 等无线端应用能够很好的兼容和支持,且常用的静态资源 CDN 可以不做相应改造,就能支持,但是 HLS 协议的一大天生缺陷就是延时较高,视频内容在切片、分发、客户端下载的过程中耗费了很长时间。对于时效性要求非常高的场景,就需要采用 RTMP、RTSP 一类的实时流媒体协议,来降低延时,并且,为了降低源站的压力,需要 CDN 边缘节点来做流的转发,那么,CDN 就必须得支持相应的流媒体协议,也就是通常所说的流媒体 CDN。

由于直播流由主播上传,如何控制违法违规内容特别是黄色内容,成了十分棘手的问题。令人欣慰的是,随着技术的发展,算法对于黄色图像的识别准确率已经很高,基本达到可以在生产环境应用的程度,因此我们也尝试在视频流分发之前,对视频帧进行提取,并且将图像交给算法进行识别,当超过预设的阈值时,可进行预警或者关停直播间。经过一段时间的实践,取得了一定的效果,降低了人力成本,但不可避免的是图像识别算法时间复杂度高,吞吐率较一般算法低。

直播整个链路是比较长的,包含有接流、转码、切片、分发、客户端下载、播放等众多环节,链路上任何一个节点有问题,都可能导致视频不能播放。因此,关键节点的监控就十分重要,除此之外,还需要对整个链路的可用性进行监控,比如,针对 HLS 协议来说,可通过监控相应的 m3u8 索引列表有没有更新,来判断视频直播流是否中断。当然,如需判断视频流中的帧有没有花屏、有没有黑屏,就更复杂了,况且,监控节点所访问的 CDN 节点与用户所访问的 CDN 节点可能并不在同一地域。当前中国的网络环境,特别是跨网段的网络访问,对于流媒体应用来说,存在较大的不可控因素,客户端网络接入环境对视频的播放有决定性影响。因此,收集终端用户的播放数据质量数据进行反馈,及时进行视频清晰度切换,显得特别重要。这些数据包括客户端的地域分布、播放卡顿信息、视频分片加载时间等等,根据这些信息反馈,可以较为全面的评估 CDN 节点部署是否合理,是否需要新增 CDN 节点,视频的转码参数对于不同机型的兼容性等等,及时进行调整以改善用户体验。

(点击放大图像)

图1 视频直播架构

消息/ 弹幕

WEB IM 应用及弹幕近年来有越来越火的趋势,是营销与气氛活跃的一种非常重要的手段。对于同时在线人数庞大的实时聊天互动、实时直播弹幕这一类场景来说,在保障消息实时性的前提条件下,将会面临非常高的并发压力。举个例子来说,假设一个活跃的直播间有 10w 人同时在线,正在直播一场热门的游戏赛事,假设每秒钟每个人说一句话,将会产生 10w 条消息,也就是 10w/s 的消息上行 QPS,而每条消息又需要广播给房间里面的每一个人,也就是说消息下行将成 10w 倍的放大,达到惊人的 10w*10w=100 亿 /s 的消息下行 QPS,而这仅仅是一场直播的 QPS,类似的直播可能有多场正在同时进行,对于消息通道来说,无疑将是一个巨大的挑战。因此,在系统设计的时候,首先要考虑的问题,就是如何降低消息通道的压力。

(点击放大图像)

图2 消息的投递与消费

用户将信息投递到消息系统之后,系统首先对消息进行一系列的过滤,包括反垃圾、敏感关键词、黑名单等等,对于信息的过滤后面会详细介绍,此处暂且不表。为了避免系统被瞬间出现的峰值压垮,可先将消息投递到消息队列,削峰填谷,在流量的高峰期积压消息,给系统留一定裕度,降低因限流丢消息对业务产生的影响。而后端始终以固定的频率处理消息,通过异步机制保障峰值时刻系统的稳定,这是一个典型的生产者—消费者模型。对于消息的消费端,则可采用多线程模型以固定的频率从消息队列中消费消息,遍历对应房间所对应的在线人员列表,将消息通过不同的消息通道投递出去。多线程增加了系统的吞吐能力,特别是对需要将消息一次性投递给几万上十万用户这样的场景,可以异步使用大集群并行处理,提高系统的吞吐能力。异步使后端的消息投递可不受前端消息上行峰值流量的干扰,提高系统稳定性。

除了采用消息队列异步处理之外,当房间人数太多,或者消息下行压力太大的情况下,还需要进一步降低消息下行通道的压力,这就需要采用分桶策略。所谓的分桶策,实际上就是限制消息的传播范围,假设10w 人在同一个房间聊天,每人说一句可能瞬间就会排满整个屏幕,消息在这种情况下基本没有可读性。为提高信息的可读性,同时也降低下行压力,可将每10000 人(具体每个桶的容量可以根据实际需求来调整,这里只举例) 放一个桶,用户发送的消息,只在一个桶或者部分桶可见,用户按照桶的维度接收消息,这样一方面前端用户接收到的消息量会少很多(跟用户所处的桶的活跃度相关),并且一条消息也不用发送给所有用户,只发送给一个或者部分桶,以降低消息下行压力。

分桶的策略有很多,最简单粗暴的方式就是根据用户随机。首先根据房间的活跃程度,预估房间该分多少个桶,然后将用户通过hash 函数映射到各个桶,随机策略的好处是实现非常简单,可以将用户较为均衡的分配到每个桶,但是会有很多弊端。首先,准确的预估房间的活跃用户数本身就比较困难,基本靠蒙,这将导致单个桶的用户数量过大或者偏小,太大就会增加消息链路的压力,而偏小则降低用户积极性,后期调整分桶数也会很麻烦,需要将全部用户进行重hash。另外从用户体验的角度来考虑,在直播过程中,在线用户数正常情况下会经历一个逐渐上升达到峰值然后逐渐下降的过程,由于分桶的缘故,在上升的过程中,每个桶的人是比较少的,这必然会影响到弹幕的活跃度,也可能因此导致用户流失,而在下降的过程中,逐渐会有用户退出直播,又会导致各个桶不均匀的情况出现:

图3 随机hash 分桶

另一种方案是按需分桶,固定每个桶的大小,当现有的桶都满了之后,再开辟新的分桶,以控制每个桶的人数,使其不至于太多也不至于太少,这样就解决了之前可能出现的每个桶人数过少的问题,但是,有个问题将比之前的随机分桶更为明显,老的桶中不断有用户离开,人将逐渐减少,新开辟的桶将越来越多,如不进行清理的话,最后的结果仍然是分桶不均衡,并且会产生很多空桶,因此,就需要在算法和数据结构上进行调整:

图4 按需分桶策略

通过一个排序list,每次将新增用户添加到人数最少分桶,这样可以让新用户加入最空闲的桶,以保持均衡,当桶满的时候,就不再添加新的用户,但是,当老用户离开的速度大大高于新用户进来的速度时,桶还是不均匀的,这时,就需要定期对分桶进行整理,以合并人数少的桶或者回收空桶,而合并的过程中,新用户又会不断的加入进来,并且,还需要保证消息发送时能读到正确的用户列表,在分布式高并发场景下,为了保证效率,有时候加锁并不是那么容易,这就有可能出现脏写与脏读,桶的整理算法将会非常复杂,有点类似于JVM 中的内存回收算法。

与大数据量高并发场景下的分库分表策略类似,实际上分桶策略也是一种取舍权衡与妥协,虽解决了原有下行通道压力过大的问题,也引入了新问题。首先,分桶改变了原本普通用户对于消息的可见性,一条消息只对于部分桶的用户可见,而非所有桶的用户,这样不同的桶内的用户看到的消息可能是不同的,另一个问题是,以上的分桶策略有可能导致“热门桶”和“冷门桶”效应出现,即可能将很多“吐槽达人”分配到同一个桶,导致该桶的氛围十分活跃,而其他不那么活跃的用户分配到一起,以致于出现“冰火两重天”的局面,从而影响产品体验。当然,对于部分特殊的消息,如系统公告内容,或者是部分特殊角色(房间管理员、贵宾、授课的老师等等) 所发送的消息,这一类消息需要广播给所有用户,这种情况下就需要系统对消息类型做区分,特殊的消息类型另作处理。

对于消息投递任务来说,需要知道消息将以什么方式被投递给谁,这样就需要动态地维护一个房间的人员列表,用户上线/ 下线及时通知系统,以便将用户添加到房间人员列表或者从房间人员列表中移除。用户上线十分简单,只需要在进入房间的时候通知系统即可,但对于下线用户的处理则有点折腾,为什么这么说呢?用户退出直播间的方式可能有多种,关闭浏览器tab、关闭窗口、关闭电源、按下home 键将进程切换到后台等等,有的操作可能可以获取到事件回调,但也有很多种情况是无法获取事件通知的,这样就会导致人员列表只增不减,房间的人越来越多,消息投递量也随之增加,白白的浪费了资源。为解决这一问题,就需采用心跳

心跳指的是客户端每隔一段时间向服务端汇报在线状态,以维持服务端的在线人员列表,当同时在线人数达到一个很大的量级(如百万级) 的时候,每秒心跳的QPS 也会变得非常之高,如何保障心跳的高效率、高吞吐就成了岑待解决的问题。首先是通信协议的选择,是HTTP 协议,还是WebSocket,还是TCP 协议或者其他。HTTP 协议的好处在于兼容性及跨终端,所有浏览器、Andriod、IOS 的WebView,都能很好支持和兼容,在目前移动重于PC 的大环境下,显得尤为重要,但是HTTP 协议劣势也是显而易见的,作为应用层协议,单次通信所要携带的附加信息太多,效率低。WebSocket 在移动端的场景下比较合适,但是运用在PC 端,需解决众多老版本浏览器的兼容性问题,socket.io 的出现则大大简化了这一原本非常复杂的问题。TCP 协议在传统的客户端应用上使用较多,但是对于WEB 应用来说,存在天然障碍。使用WebSocket 和TCP 协议的好处显而易见的,通信效率会比HTTP 协议高很多,并且这两种协议支持双工通信。

另一个问题是后端存储的选择,该使用怎样的存储结构来存储在线人员列表这样的数据结构,以支撑这么高的并发读写。经过优化并且使用SSD 的关系型数据库相较以往性能已经有了很大的提升,但是对于频繁变化的大量在线人员列表来说,持久化存储实际上是没太大意义的。因此,读写性能更高的内存存储,如memcache,redis,可能是一种更现实的选择。memcache 只能支持简单的KV 对象存储,数据读写需要进行频繁的序列化和反序列化,但吞吐更高,而redis 的好处在于丰富的数据类型,如Lists、Hashs、Sets,省去了序列化和反序列化操作,并且支持更高效的分页遍历及count 操作:

(点击放大图像)

图5 基于redis Sets 构建的分桶存储结构

消息通道

HTTP 协议请求 / 响应式特性决定了它擅长处理浏览型业务,而对于需要与服务端进行频繁交互的即时通讯场景来说,则会显得十分蹩脚。在直播进行的过程中,用户可以对主播进行吐槽、评论,用户与用户之间也可以进行频繁的交流,需要有像弹幕、WEB IM 这样的工具来支持,这种场景下,消息的实时性尤为重要。

要实现这类场景,最简单最粗暴的方式莫过于不断地轮询应用服务器,采用拉的方式读取弹幕以及用户的聊天内容,消息的实时程度取决于拉的频率,拉的过快,可能服务器无法承受,拉的频率过低,则消息的实时性跟不上。轮询的方式太过于粗暴,需要频繁的请求服务器,成本较高,并且用户更新信息的频率实时变化,很难选择比较合理的轮询时间,因为不同时间段用户发送消息的频率是有很大差异的,对于拉取的信息,客户端需要进行筛选和去重。因此,对于 WEB 端的即时交互应用,需要采用其他解决方案,如 comit 服务端推送,或者通过 websocket 来实现类似的场景。

comet[1] 又被称作为反向 ajax(Reverse AJAX ),分为两种实现方式,一种是长轮询 (long-polling) 方式,一种是流 (streaming) 的形式。在长轮询的方式下,客户端与服务端保持 HTTP 连接,服务端会阻塞,直到服务端有信息传递或者是 HTTP 请求超时,客户端如果收到响应,则会重新建立连接,另一种方式是流的形式,服务器推送数据给客户端,但是连接并不关闭,而是始终保持,直到连接超时,超时后客户端重新建立连接,并关闭之前的连接。通过这两种方式,便可借用 HTTP 协议,来实现服务端与客户端的即时通讯。

而 WebSocket[2] 是 IETF 所提出的一种新的协议,旨在实现浏览器与服务器之间的全双工 (full-duplex) 通信,而传统的 HTTP 协议仅能够实现单向的通信,即客户端发起的到服务端的请求,虽然 comet 在一定程度上可以模拟双向通信,但是通信的效率较低,且依赖特定的服务器实现。

(点击放大图像)

图6 WebSocket 协议[3]

为何说comet 的通信效率会低于WebSocket 呢,因为不管是comet 的长轮询机制还是流机制,都需要在请求超时后发送请求到服务端,并且,长轮询在服务端响应消息之后,需要重新建立连接,这样又带来了额外的开销。

图7 长轮询与websocket 消息发送对比

我们知道HTTP 协议的Request Header 中附带了很多信息,但是这中间包含的很多信息有些场景其实并不是必须的,这样就浪费了大量的带宽及网络传输时间。而WebSocket 协议使得浏览器与服务器只需要一次握手动作,便形成了稳定的通信通道,两者之间就可以通过frame 进行数据传递,而消息传递所发送的Header 是也是很小的,这样就会带来效率的提升。

图8 websocket 与comet 性能对比

通过wireshark 抓包可以做一个简单的测试对比, 假设服务端每秒推送50 条消息给用户,每条消息的长度为10byte,对应不同的用户规模,采用websocket 和comet 两种不同的通信机制,所需要传递的字节数如图8 所示,可见,随着用户规模及消息量的提升,采用websocket 协议进行通信,将对通信效率带来数量级的提升,详细的测试过程可参见笔者博客。

引入WebSocket 协议,虽然原则上解决了浏览器与服务端实时通信的效率问题,相较comet 这种基于HTTP 协议的实现方式,能获得更好的性能,但同时也引入了一些新的问题。摆在首位的便是浏览器的兼容性问题,开发WEB 应用程序的一个最头痛的问题就是多版本浏览器的兼容,不同浏览器产商对于协议的实现有各自的理解,并且,市面上还充斥着大量低版本不支持HTML5 协议的浏览器,且这部分用户在中国还占有较大基数,因此在产品研发的过程中不得不予以考虑。得益于socket.io[4] 的开源,通过websocket、Adobe Flash Socket、long-polling、streaming、轮询等多种机制的自适应切换,帮我们解决了绝大部分浏览器(包括各种内核的webview) 的兼容性问题,统一了客户端以及服务端的编程方式。对于不同平台的消息推送,我们内部也衍生了一些成熟的技术平台,封装了包括消息推送,消息订阅,序列化与反序列化,连接管理,集群扩展等工作,限于篇幅,这里就不多说了。

文本内容过滤

双十一视频直播的另一个挑战在于,如何对弹幕这一类的UGC 内容进行过滤。根据以往经验,对淘宝来说,如果内容不经过滤直接透出,毫无疑问用户将看到满屏的广告,大量的广告信息将直接导致部分功能无法使用。因此,文本内容过滤,对于我们来说可以说是一门基本功。早期垃圾、广告内容,或者非法信息的识别,主要依赖于人工,但是当下WEB2.0 DT 时代,对于海量的信息进行人工筛查显然不太现实,社会在发展,技术也在进步,一系列新的文本筛查及过滤技术被运用到了工程领域。

敏感词匹配

通过长时间的累积,大点的网站一般都会有一份敏感词列表,如果用户提交的内容包含敏感词,就需要进行转义(转换成***),或者是拒绝发表。那如何判断文本中是否包含敏感词,以及如何进行敏感词过滤呢?如果敏感词只有很少的几个,那最简单的方式当然是通过关键词搜索算法或者是正则表达式进行匹配,但实际情况显然要复杂的多,需要过滤的关键词可能有成千上万个,而正则表达式的效率实际上是比较低的,尤其在高并发场景下,对系统的吞吐能力要求非常之高。因此,这种场景下就需要有更高效的算法。目前较为常见的是采用Trie 树算法或者是Trie 算法的变种,如空间和时间复杂度都较为均衡的双数组Trie 树算法来实现。Trie 树也称为字典树或者是单词查找树,它的好处在于可以利用字符串的公共前缀以减少查询时间,最大限度的减少字符串比较次数,降低关键词在内存中占用的存储空间。

图9 Trie 树结构

Trie 本质上是一个有限状态机,根据输入的数据进行状态转移。假设敏感词包含“美利坚”、“美丽”、“金币”、“金子”、“帝王”,而输入的文本是“我的同桌是一个美丽的姑娘”, 每读取一个字符作为输入,根据这个字符以及它的后续字符对 Trie 树进行查找,直到叶子节点,便可以找出文本中包含的违禁词“美丽”以及这个词在文本中对应的位置和频次。

当然,也可以通过构造多级的 Hash Table 来简化 Trie 树的实现,相同父节点的字放在同一个 Hash Table 中,以减少不必要的查询,对于输入的文本来说,只需要将字符一个个依次作为输入对 Hash Table 进行查找,如包含对应的 key,则继续,直到查询到最下层叶子节点。

图 10 多级 Hash Table 简化 Trie 树的实现

在系统变得越来越智能的同时,恶意的用户也变得越来越狡猾,他们可能会在文本当中夹杂一些干扰字符来绕过敏感词检查,如“美 * 利 * 坚”,这就需要我们对输入的文本做降噪处理,过滤掉其中的干扰字符,再进行匹配。

除了算法的实现之外,在集群环境下,还得综合考虑系统的吞吐以及机器内存的压力,由于敏感词过滤需要依赖庞大的敏感词词库,并且,还需将词库解析成 Trie 树或者多级的 Hash Table 置于内存当中,以便查找,这势必将占用大量的内存空间。因此,为了提升内存资源利用率,大部分情况我们会采用 RPC 模式部署敏感词过滤服务,但是,在高并发场景下,为了降低因引入敏感词过滤而带来的延时,提高系统的吞吐,又需要将敏感词数据推送到本地内存,通过读本地内存中的数据,降低因 RPC 带来的时延,当敏感词库有更新时,服务端需将相应的变更信息推送给其他应用。

分类算法

分类实际上就是按照某种标准来给对象贴标签,然后再根据标签进行区分,基于概率统计的贝叶斯分类算法 [5] 是最常见的分类算法,也是目前垃圾文本识别领域应用最广泛的算法。

使用贝叶斯分类算法进行二分类大致可分为这几个步骤:

  1. 收集大量的垃圾内容和非垃圾内容语料,建立训练的垃圾语料集和正常内容的语料集。
  2. 对语料文本进行分词,提取出独立的字符串,并且统计字符串在文本中出现的频次。
  3. 每个训练语料集对应一个 hash table,比如垃圾语料集放在 hashtable_bad 中,而非垃圾语料集放在 hashtable_good 中,而 hashtable 中存储通过分词提取出的字符串以及对应的词频。
  4. 计算 hashtable 所有的字符串出现的概率,即 P= 字符串的词频 / 字符串的总数。
  5. 综合 hashtable_good 与 hashtable_bad,推测当一串文本中包含某个字符串时,该文本为垃圾内容的概率, 对应的数学表达式如下: P(A|ki) = Pbad(ki) / [ Pgood(ki) +Pbad(ki) ],其中事件 A 表示文本为垃圾内容,k1,k2 ……kn 代表提取的关键词,而 P(A|ki)则表示在文本中出现关键词 ki 时,该文本为垃圾内容的概率,Pbad(ki)为 ti 在 hashtable_bad 中的值,而 Pgood(ki)为 ki 在 hashtable_good 中的值。
  6. 建立新的 hashtable_probability 存储字符串 ki 到 P(A|ki)的映射。

行文至此,贝叶斯分类的训练学习过程就完成了,接下来就可以根据 hashtable_probability 来计算文本为垃圾内容的可能性了。假设用户提交的文本内容经过分词得到 n 个关键词 k1,k2,k3……kn,hashtable_probability 中对应的值为 P1,P2……Pn ,P(A|k1,k2,k3……kn) 表示在用户提交的文本中同时出现关键字 k1,k2,k3……kn 时,该段内容为垃圾文本的概率, P(A|k1,k2,k3……kn) =P1*P2*……Pn 。当 P(A|k1,k2,k3……kn) 超过预定阈值时,可以判断该内容为垃圾内容,通过调整阀值,可以控制反垃圾系统对于内容过滤的严苛程度。

当然,以上只是简单的二分类情况,随着语料库的丰富,可以将垃圾语料进行细分,比如诈骗、广告、黄色、脏话、人身攻击等等,并且可以根据具体场景进行组合和分级使用,因为不同的使用场景对于文本中出现垃圾文本的容忍程度是不同的,要求越严苛的环境,势必误杀的概率也会增加,并且在某个场景下,一些语料可能属于垃圾广告内容,而在其他的场景下,可能又属于正常的内容。举例来说,如果在电商网站的评价中发现留有 QQ、微信、电话等联系方式,很有可能就属于广告内容,而对于社交应用来说,则属于正常内容。另外对于一些常见词,需要进行过滤,以减少对概率计算的干扰,词实际上也是有权重的,比如出现某些关键词,文档为垃圾内容的概率显著增加,可以适当提升此类词的权重。

贝叶斯算法的优势是随着语料库的不断丰富,可应对恶意用户层出不穷的“新把戏”。基于训练,算法模型可以发现已知类型的垃圾内容,但是对于新出现的类型,算法模型也无能为力,因此就需要外界的干预。要么通过人工标注,要么采用其他方式 (如文本重复度判断),来发现新的垃圾文本类型,丰富训练的语料库。随着语料库的丰富,识别的准确性也会越来越高,道高一尺魔高一丈,最终在与恶意用户的斗争过程中,系统的能力也将越来越强大。

黑名单用户

对于频繁发送恶意信息的用户,可以采用禁言一段时间或者是永久禁言的的方式进行应对, 这样就需要维护一份用户黑名单,当用户提交 UGC 内容的时候,先判断是否在黑名单中,如果已经加入黑名单,则拒绝发表。

黑名单的最简单实现方式莫过于采用 hash table,借用 redis 的 hash 这一类的数据结构进行内存缓存,并且定时进行持久化或是数据备份,防止宕机导致的数据丢失,这种方式实现简单,查询效率也比较高,可以满足大部分场景的使用,但是缺点也十分的明显,采用 hash table 的方式随着黑名单列表的增加,占用内存的空间越来越大,当业务需要对黑名单按一定场景进行区分和隔离的时候,这种情况尤为明显,并且,黑名单列表越大,hash 冲突也将越多,以致于查询的效率也会降低,。当对黑名单过滤要求不那么精确的时候,可以采用 Bloom Filter 6 的方案。

图 11 Bloom Filter 的基本原理 [7]

Bloom Filter 是一个 m 位的数组,初始状态下数组所有位被置 0,需要设置黑名单时,通过一系列不同的 hash 函数,每个 hash 函数将对应的输入元素映射到数组中的一位上,通过 hash 函数得到数组的索引,将数组对应位置 1,在查询的时候,通过相同的 hash 函数,找到对应的位,如果对应的位不都为 1 的话,则表示用户不在黑名单中,那么,当所有的位都为 1 的情况,就表示用户一定黑名单中么?也不一定,可能是其他几次用户输入刚好将本次所有位都置 1 了,如图 11 所示,当插入 x、y、z 这三个元素之后,再来查询 w,会发现 w 不在集合之中,而如果 w 经过三个 hash 函数计算所得索引处的位全是 1,那么 Bloom Filter 就会告诉你,w 在集合之中,实际上这里是误报,w 并不在集合之中,这就是所谓误报。从概率上来说,误报的几率很低,在系统可接受范围内,如需要 100% 精确判断,则 Bloom Filter 就不太合适。

基本的 Bloom Filter 是不支持删除操作的,Counting Bloom Filter 的出现解决了这个问题,它将标准 Bloom Filter 位数组的每一位扩展为一个小的计数器(Counter),在插入元素时给对应的 k(k 为哈希函数个数)个 Counter 的值分别加 1,删除元素时给对应的 k 个 Counter 的值分别减 1,Counting Bloom Filter 通过多占用几倍存储空间的代价,给 Bloom Filter 增加了删除操作,但是相比 hash table 而言,存储效率还是提升了很多。

图 12 Counting Bloom Filter 的基本原理 [8]

系统稳定性

对于每年的双十一来说,如何在大流量高并发场景下,保障系统稳定提供服务不宕机,已经是一个经久不衰的话题,也是每年对于技术人员的一次彻底的检阅。从大促开始之前的性能优化、依赖梳理、峰值评估,到系统压力测试,新机器采购上线,再到流控阀值的评估、降级开关配置,再到监控数据采集、容灾应急预案、全链路压测、演习,再到实时、离线数据分析等等,关于这些的介绍,相信网上相关文章都已汗牛充栋,此处就不再赘述。

总结

本篇主要介绍了阿里直播平台在双十一所面临的一些技术挑战,以及应对的方案和思考过程,从音视频直播架构,到弹幕和消息投递,再到消息通道的技术选型、文本过滤的实现机制,大型直播无论是音视频的可靠性保障、视频鉴黄,还是文本消息经过数万在线用户放大之后的消息下行,以及对于消息内容本身的过滤,都面临了很大的挑战,挑战不可怕,兵来将挡,水来土掩,迎难而上,才能有突破和创新。

参考文献

  1. comet, https://software.intel.com/zh-cn/articles/comet-java-realtime-system-essay
  2. WebSocket, https://tools.ietf.org/html/rfc6455
  3. WebSocket 通信原理
  4. socket.io 项目地址: http://socket.io/
  5. 贝叶斯分类, https://en.wikipedia.org/wiki/Naive_Bayes_classifier
  6. Bloom Filter, http://my.oschina.net/kiwivip/blog/133498
  7. 图片来源, http://my.oschina.net/kiwivip/blog/133498
  8. 图片来源, http://my.oschina.net/kiwivip/blog/133498

作者简介

陈康贤,淘宝花名龙隆,淘宝技术部技术专家,著有《大型分布式网站架构设计与实践》一书,在分布式系统架构设计、高并发系统设计、系统稳定性保障等领域积累了较为丰富的实践经验,对新技术有浓厚的兴趣 ,交流或简历请发送至 longlong@taobao.com


感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2016 年 1 月 27 日 16:567776

评论

发布
暂无评论
发现更多内容

图解JavaScript——进阶篇(执行上下文、变量对象、作用域、作用域链、闭包、this、原型及原型链、事件循环等一把梭)

执鸢者

Java 前端 函数执行 事件循环

Apache Pulsar 在 BIGO 的性能调优实战(上)

Apache Pulsar

疫情按下快进键,电商抢占市场红利需可靠的助力

华为云开发者社区

高并发 电商 华为云 流量 云服务器

云上攻击太多怎么办?不妨试试这些工具

华为云开发者社区

黑客 云服务 数据安全 华为云 企业上云

常用SQL语句分享

Simon

MySQL sql

TypeScript 设计模式之发布-订阅模式

pingan8787

typescript 前端 设计模式

领导发表的报告,你真的读懂了吗?

罗小龙

工作经验

Docker从入门到放弃---基础篇

书旅

Docker 容器 容器化

ARTS打卡第一周(200511-200517)

老胡爱分享

ARTS 打卡计划 ARTS活动

力扣题 - 29 解析大佬题解

Geek_663541

Java算法

Docker入门与简单使用

Simon

Docker Linux

Docker 架构及工作原理

哈喽沃德先生

Docker 容器 微服务 容器技术 容器化

GrowingIO 数据采集 iOS SDK 测试实践

GrowingIO技术专栏

ios 数据采集 sdk

MySQL中几种常见的日志

Simon

MySQL 日志

CDN百科11 | 担心纸质病例被洪水淹没?ECS+CDN+OSS如何助力医疗上云

阿里云Edge Plus

CDN 存储 云服务器

糖果不需要意义

zhoo299

随笔杂谈 电影

学习源码的第八个月,我成了Spring的开源贡献者

程序员DMZ

spring 开源

LeetCode题解:11. 盛最多水的容器,while循环双指针,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

带着面试题学习红黑树操作原理,解析什么时候染色、怎么进行旋转、与2-3树有什么关联

小傅哥

Java 数据结构 小傅哥 红黑树 2-3树

MySQL容器化详细教程

Simon

MySQL Docker 容器化

不说再见

太以

疫情 毕业季 遗憾 大学

MySQL下的DB Link

Simon

MySQL

CSS属性整理

kidd

学习笔记2

Qx

学习

Flink任务执行-3

小知识点

大数据 flink

SecureBoost算法

soolaugust

学习 同态加密 secureboost

我与游戏相伴【自我访谈】

叶阳夏烟

系列 游戏 游戏观 访谈录

安全系列之——RSA的公钥私钥有多少人能分的清楚?RSA的签名验签与加密解密如何使用公私钥?

诸葛小猿

加密解密 非对称加密 rsa 签名验签

LeetCode152-乘积最大子数组-medium

书旅

LeetCode 动态规划

[High Performance TIDB] Leeson 01:TIDB整体架构---作业

LanLiang

高性能 #TiDB

不完满的选择,要不要放弃

zhoo299

随笔杂谈

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

鏖战双十一-阿里直播平台面临的技术挑战-InfoQ