2019 年 3 月 23 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·北京站,京东云技术专家罗玉杰在活动上做了《 OpenResty 在直播场景中的应用 》的分享。

OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使用者的交流与学习,推动 OpenResty 开源项目的发展。活动已经在深圳、北京两地举办,未来将陆续在武汉、上海、杭州、成都等城市巡回举办。

罗玉杰,京东云技术专家,10 余年 CDN、流媒体行业从业经验,热衷于开源软件的开发与研究,对 OpenResty、Nginx 模块开发有较深入的研究,熟悉 CDN 架构和主流流媒体协议。

以下是分享全文:

大家下午好,我是来自京东云的罗玉杰,今天给大家分享的主题是 《OpenResty 在直播场景中的应用》。

项目需求

京东云前期的服务是基于 Nginx 二次开发的,之后因为要对接上云的需求,于是新做了两个服务,一个是对接云存储的上传服务,另一个是偏业务层的直播时移回看服务。项目的需求是做视频数据上云,主要是视频的相关数据对接云存储,需求的开发周期很紧,基本上是以周为单位。

我们之前的服务用 C 、C++ 开发,但 C 和 C++ 的开发周期很长。我们发现这个项目基于 OpenResty 开发是非常适合的,可以极大地缩短开发周期,同时提高运行效率,并且 OpenResty 对运维非常友好,能提供很多的配置项,让运维根据线上动态修改一些配置,甚至运维都可以看懂代码的主流程。

项目体系结构

image.png

△ 体系结构图

上图是一个直播服务的主流体系结构,先是主播基于 RTMP 协议推到 CDN 边缘,接着到视频源站接入层,然后把 RTMP 流推送到切片上传服务器,上面有两个服务:

一个是切片服务,把流式的视频流进行切片存储到本地,生成 TS 视频文件和 M3U8 文本文件,每形成一个小切片都会通知上传服务,后者将这些 TS 文件和 M3U8 文件基于 AWS S3 协议上传到云存储服务,我们的云存储兼容 AWS S3 协议。

在此基础上,我们用 OpenResty 做了一个直播时移回看服务,用户基于 HLS 协议看视频,请求参数里带上时间段信息,比如几天之前或者几个小时之前的信息,此服务从云存储上下载 M3U8 信息进行裁剪,再返回给用户,用户就可以看到视频了。HLS 协议的应用面、支持面很广,各大厂商、终端支持得都非常好,而且对 HTTP 和 CDN 原有的技术栈、体系非常友好,可以充分地利用原来的一些积累。有的播放是基于 RTMP,HDL(HTTP + FLV)协议的,需要播放器的支持。

项目功能

1、基于 s3 PUT 协议将 TS 文件上传至云存储。

2、S3 multi 分片上传大文件,支持断点续传。这个服务重度依赖于 Redis,用 Redis 实现任务队列、存储任务元数据、点播 M3U8。

3、基于 Redis 实现任务队列的同时做了 Nginx worker 的负载调度。在此基础上做了对于后端服务的保护,连接和请求量控制,防止被短时间内特别大的突发流量把后端的云服务直接打垮。实现任务队列之后,对后端的链接数是固定的,而且请求处理看的是后端服务的能力,简单地说,它处理得多快就请求得多快。

4、为了保证云和服务的高可靠性,我们做了失败重试和异常处理、降低策略。其中,任务失败是不可避免的,现在也遇到了大量的任务失败,包括链接失败、后端服务异常等,需要把失败的任务进行重试,降级。把它在失败队列里面,进行一些指数退避。还有一些降级策略,我这个服务依赖于后面的 Redis 服务,和后端的云存储服务,如果它们失败之后,我们需要做一些功能的降级,保证我们的服务高可用。在后端 Redis 服务恢复的时候再把数据同步过去,保证数据不会丢失。

5、还有就是生成直播、点播 M38,为后续的服务提供一些基础数据。如直播时移回看服务。

AWS S3 协议

AWS S3 比较复杂的就是鉴权,主要用它的两个协议,一个是 PUT,一个是 MULTI PART。

image.png

△ 鉴权

AWS S3 的鉴权和 Nginx 中的 Secure Link 模块比较相似,将请求相关信息用私钥做一个散列,这个散列的内容会放到 http 头 authorization 里面,服务端收到请求后,会有同样的方式和同样的私钥来计算这个内容,计算出的内容是相同的就会通过,不相同的话会认为是一个非法请求。

image.png

△PUT 协议

image.png

△ MULTI PART 协议

它主要分三步骤,第一步是创建任务,创建任务之后会返回一个 ID 当做任务的 Session ID,用 POST 和 REST 规范实现的协议。初始化任务之后,可以传各种分片了,然后还是用 PUT 传小片,加上 Session ID,每一片都是这样。

image.png

△ Complete 消息

上传任务成功之后,会发一个 Complete 消息,然后文件就认为是成功了,成功之后就会合并成一个新的文件,对外生成一个可用的大文件。

HLS 协议

HLS 协议,全称是 HTTP LIVE STREAMING 协议,是由苹果推出的,可读性很强。里面的每一个片都是一个 HTTP 请求,整个文本协议就是一个索引。

image.png

△ HLS:HTTP Live Streaming 协议

上图是每一个视频段的时长,这个是 8 秒是视频的最大长度。直播的应用中会有一个 Sequence 从零开始递增的,如果有一个新片,就会把旧片去掉,把新的加上去,并增加 sequence。

任务队列、均衡、流控

下面再介绍一下具体的功能实现,任务收到请求之后不是直接处理,而是异步处理的。先把请求分发到各个 Worker 的私有队列,分发算法是用的 crc32,因为 crc32 足够快、足够轻量,基于一个 key 视频流会有域名、app、stream,再加上 TS 的文件名称。这样分发可以很好地做一次负载均衡。基于这个任务队列,可以处理大量的突发请求,如果突然有了数倍的请求,可以把这些消息发到 Redis 里,由 Redis 存储这些请求。每个 Worker 会同步进行处理,把 TS 片上传,上传完之后再生成 M3U8 文件。我们现在对后端固定了连接数,一个 woker 一个链接,因为存储集群的连接数量是有限的,现在采取一个简单策略,后端能处理请求多快,就发送多快,处理完之后可以马上发送下一个。因任务队列是同步处理,是同步非阻塞的,不会发送超过后端的处理能力。

我们未来准备进行优化的方向就是把任务队列分成多个优先级,高优先级的先处理,低优先级的降级处理。比如我们线上遇到的一些视频流,它不太正常会大量的切小,比如正常视频 10 秒一片,而它 10 毫秒就一片,这样我们会把它的优先级降低,防止异常任务导致正常任务不能合理地处理。以后就是要实现可以动态调解链接数、请求速率和流量。如果后端的处理能力很强,可以动态增长一些链接数和请求速率,一旦遇到瓶颈后可以动态收缩。

image.png

任务队列

任务分发比较简单,主要就是上面的三行代码,每一个 Worker 拿到一个任务后,把任务分发给相应的 Worker ,它的算法是拿到总 Worker 数然后基于 crc32 和 key ,得到正确的 Worker ID,把它加到任务队列里。这样的做法好处是每个任务分发是非单点的,每一个 Worker 都在做分发,把请求的任务发到任务队列里,请求的元信息放入 Redis 里面,还有一个就是任务拉取消费的协程,拉取任务并执行。

失败重试、降级、高可靠

如果数据量大会有很多失败的任务,失败任务需要放入失败队列,进行指数退避重试。重试成功后再进行后续处理,比如添加进点播 m3u8、分片 complete。分片 complete 是如果原来有 100 个任务会同时执行,但是现在有 3 个失败了,我们可以判断一下它是不是最后一个,如果是最后一个的分片就要调一下 complete,然后完成这个分片,完成整个事务。

同时我们做了一个 Redis 失败时的方案,Redis 失败后需要把 Redis 的数据降级存到本地,一部分存到 share dict,另一部分用 LRU cache,TS 对应 m3u8 的索引信息会用 share dict 做缓存。LRU 主要是存一些 m3u8 的 key,存储哪些信息和流做了降级,Redis 恢复后会把这些信息同步到 Redis。因为存在于各个 worker 里面数据量会比较大,有些任务会重复执行,我们下一步工作就想基于 share dict,加一个按照指定值来排序的功能,这样就可以优先处理最近的任务,将历史任务推后处理。

我们还有一些 M3U8 的列表数据存储在 Redis,因为线上的第一版本是单实例的,存储空间比较有限,但是现在对接的流量越来越多,单实例内存空间不足,于是我们做了支持 Redis 集群的工作,实现 Reids 高可用,突破内存限制。

还有一个比较兜底的策略:定期磁盘巡检,重新处理失败任务。事务可能是在任何的时点失败的,但是只要我们能够重做整个任务,业务流程就是完整的。

遇到的问题和优化方案

第一版的时候是全局的单一任务队列,基于 resty lock 的锁取保护这个队列,每一个 woker 争用锁,获取任务,锁冲突比较严重,CPU 消耗也高,因为那个锁是轮询锁,优化后我们去掉了一个锁实现了无锁,每一个 worker一个任务队列, 每个 worker 基于 CRC_32 分发任务。

旧版一个 TS 更新一次 M3U8,一次生成一个哈希表,数量较多的情况下 CPU 开销比较大。我们进行了优化,做了一些定时触发的机制,进行定期更新,因为点播 M3U8 对时间是不敏感的,可以定期地更新,减少开销。当然直播的 还是实时生产的,因为要保证直播的实时性。

直播方面如果异常切片太多,用户也不能很好观看,会进行主动丢片,主要是基于 Redis 锁去实现;对于 Redis 内存消耗高的问题我们搭建了 Redis 集群。

直播时移回看服务

我们开发了一个直播时移回看服务,根据用户请求的时间去后台下载相应的 M3U8 的数据进行裁剪拼接返回给用户。这一块的 M3U8 信息不是很大,非常适合用 MLCACHE 保存,它是一个开源的两级缓存,worker 一级的和共享内存一级,因为共享内存缓存有锁冲突,MLCACHE 会把一些热点数据缓存到 worker 级别,这样是无锁的,使用后效果非常好,虽然文件不大,但是运行时间建连,网络IO耗时很大,经过缓存之后可以大大提高处理效率,节省时间。时移的时候每一个用户会也一个 Session 记录上次返回的 M3U8 位置,因为直播流会有中断,不是 24 小时都有流的,用户遇到了一个断洞,可以跳过看后面的视频,时移不需要等待,并且用户网络短暂异常时不会跳片。