HLS 协议详解

HLS 概述

HLS 全称是 HTTP Live Streaming, 是一个由 Apple 公司实现的基于 HTTP 的媒体流传输协议. 他跟 DASH 协议的原理非常类似. 通过将整条流切割成一个小的可以通过 HTTP 下载的媒体文件, 然后提供一个配套的媒体列表文件, 提供给客户端, 让客户端顺序地拉取这些媒体文件播放, 来实现看上去是在播放一条流的效果.

由于传输层协议只需要标准的 HTTP 协议, HLS 可以方便的透过防火墙或者代理服务器, 而且可以很方便的利用 CDN 进行分发加速, 并且客户端实现起来也很方便.

HLS 目前广泛地应用于点播和直播领域.

在 HTML5 页面上使用 HLS 非常简单:

直接:

<video src="example.m3u8" controls></video>

或者:

<video controls>
    <source src="example.m3u8"></source>
</video>

下面, 我将会概括性地介绍 HLS 协议的方方面面(暂时不包括 AES 加密部分的内容), 配合 HLS 的 RFC 食用效果更佳.

HLS 协议详解

hls_arch

上面是 HLS 整体架构图, 可以看出, 总共有三个部分: Server, CDN, Client.

其实, HLS 协议的主要内容是关于 M3U8 这个文本协议的, 其实生成与解析都非常简单. 为了更加直接地说明这一点, 我下面举两个简单的例子:

简单的 Media Playlist:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:2680

#EXTINF:7.975,
https://priv.example.com/fileSequence2680.ts
#EXTINF:7.941,
https://priv.example.com/fileSequence2681.ts
#EXTINF:7.975,
https://priv.example.com/fileSequence2682.ts

包含多种比特率的 Master Playlist:

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000
http://example.com/low.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000
http://example.com/mid.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000
http://example.com/hi.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=65000,CODECS="mp4a.40.5"
http://example.com/audio-only.m3u8
  • HLS 通过 URI(RFC3986) 指向的一个 Playlist 来表示一个媒体流.
  • 一个 Playlist 可以是一个 Media Playlist 或者 Master Playlist, 使用 UTF-8 编码的文本文件, 包含一些 URI 跟描述性的 tags.
  • 一个 Media Playlist 包含一个 Media Segments 列表,当顺序播放时, 能播放整个完整的流.
  • 要想播放这个 Playlist, 客户端需要首先下载他, 然后播放里面的每一个 Media Segment.
  • 更加复杂的情况是, Playlist 是一个 Master Playlist, 包含一个 Variant Stream 集合, 通常每个 Variant Stream 里面是同一个流的多个不同版本(如: 分辨率, 码率不同).

HLS Media Segments

  • 每一个 Media Segment 通过一个 URI 指定, 可能包含一个 byte range.
  • 每一个 Media Segment 的 duration 通过 EXTINF tag 指定.
  • 每一个 Media Segment 有一个唯一的整数 Media Segment Number.
  • 有些媒体格式需要一个 format-specific sequence 来初始化一个 parser, 在 Media Segment 被 parse 之前. 这个字段叫做 Media Initialization Section, 通过 EXT-X-MAP tag 来指定.

支持的 Media Segment 格式

MPEG-2 Transport Streams
  • 即最常见的 TS 文件.
  • RFC: ISO_13818.
  • Media Initialization Section: PAT(Program Association Table) 跟 PMT(Program Map Table).
  • 每个 TS segment 必须值含一个 MPEG-2 Program.
  • 每一个 TS segment 包含一个 PAT 和 PMT, 最好在 segment 的开始处, 或者通过一个 EXT-X-MAP tag 来指定.
Fragmented MPEG-4
  • 即常提到的 fMP4.
  • RFC: ISOBMFF.
  • Media Initialization Section: ftyp box(包含一个高于 ios6 的 brand), ftyp box 必须紧跟在 moov box 之后. moov box 必须包含一个 trak box(对于每个 fMP4 segment 里面的 traf box, 包含匹配的 track_ID). 每个 trak box 应该包含一个 sample table, 但是他的 sample count 必须为 0. mvhd box 跟 tkhd 的 duration 必须为 0. mvex box 必须跟在上一个 trak box 后面.
  • 不像普通的 MP4 文件包含一个 moov box(包含 sample tables) 和一个 mdat box(包含对应的 samples), 一个 fMP4 包含一个 moof box (包含 sample table 的子集), 和一个 mdat box(包含对应的 samples).
  • 在每一个 fMP4 segment 里面, 每一个 traf box 必须包含一个 tfdt box, fMP4 segment 必须使用 movie-fragment relative addressing. fMP4 segments 绝对不能使用外部的 data references.
  • 每一个 fMP4 segment 必须有一个 EXT-X-MAP tag.
Packed Audio
  • 一个 Packed Audio Segment 包含编码的 audio samples 和 ID3 tags. 简单的打包到一起, 包含最小的 framing, 并且没有 per-sample timestamp.
  • 支持的 Packed Audio: AAC with ADTS framing [ISO_13818_7], MP3 [ISO_13818_3], AC-3 [AC_3], Enhanced AC-3 [AC_3].
  • 一个 Packed Audio Segment 没有 Media Initialization Section.
  • 每一个 Packed Audio Segment 必须在他的第一个 sample 指定 timestamp 通过一个 ID3 PRIV tag.
  • ID3 PRIV owner identifier 必须是 com.apple.streaming.transportStreamTimestamp.
  • ID3 payload 必须是一个 33-bit MPEG-2 Program Elementary Stream timestamp 的大端 eight-octet number, 高 31 为设置为 0.
WebVTT
  • 一个 WebVTT Segment 是一个 WebVTT 文件的一个 section, WebVTT Segment 包含 subtitles.
  • Media Initialization Section: WebVTT header.
  • 每一个 WebVTT Segment 必须有以一个 WebVTT header 开始, 或者有一个 EXT-X-MAP tag 来指定.
  • 每一个 WebVTT header 应该有一个 X-TIMESTAMP-MAP 来保证音视频同步.

HLS Playlists

  • Playlist 文件的格式是起源于 M3U, 并且继承两个 tag: EXTM3UEXTINF
  • 下面的 tags 通过 BNF-style 语法来指定.
  • 一个 Playlist 文件必须通过 URI(.m3u8 或 m3u) 或者 HTTP Content-Type 来识别(application/vnd.apple.mpegurl 或 audio/mpegurl).
  • 换行符可以用 \n 或者 \r\n.
  • # 开头的是 tag 或者注释, 以 #EXT 开头的是 tag, 其余的为注释, 在解析时应该忽略.
  • Playlist 里面的 URI 可以用绝对地址或者相对地址, 如果使用相对地址, 那么是相对于 Playlist 文件的地址.

Attribute Lists

  • 有的 tags 的值是 Attribute Lists.
  • 一个 Attribute List 是一个用逗号分隔的 attribute/value 对列表.
  • 格式为: AttributeName=AttributeValue.

Basic Tags

Basic Tags 可以用在 Media Playlist 和 Master Playlist 里面.

  • EXTM3U: 必须在文件的第一行, 标识是一个 Extended M3U Playlist 文件.
  • EXT-X-VERSION: 表示 Playlist 兼容的版本.

Media Segment Tags

每一个 Media Segment 通过一系列的 Media Segment tags 跟一个 URI 来指定. 有的 Media Segment tags 只应用与下一个 segment, 有的则是应用所有下面的 segments. 一个 Media Segment tag 只能出现在 Media Playlist 里面.

  • EXTINF: 用于指定 Media Segment 的 duration
  • EXT-X-BYTERANGE: 用于指定 URI 的 sub-range
  • EXT-X-DISCONTINUITY: 表示不连续.
  • EXT-X-KEY: 表示 Media Segment 已加密, 该值用于解密.
  • EXT-X-MAP: 用于指定 Media Initialization Section.
  • EXT-X-PROGRAM-DATE-TIME: 和 Media Segment 的第一个 sample 一起来确定时间戳.
  • EXT-X-DATERANGE: 将一个时间范围和一组属性键值对结合到一起.

Media Playlist Tags

Media Playlist tags 描述 Media Playlist 的全局参数. 同样地, Media Playlist tags 只能出现在 Media Playlist 里面.

  • EXT-X-TARGETDURATION: 用于指定最大的 Media Segment duration.
  • EXT-X-MEDIA-SEQUENCE: 用于指定第一个 Media Segment 的 Media Sequence Number.
  • EXT-X-DISCONTINUITY-SEQUENCE: 用于不同 Variant Stream 之间同步.
  • EXT-X-ENDLIST: 表示结束.
  • EXT-X-PLAYLIST-TYPE: 可选, 指定整个 Playlist 的类型.
  • EXT-X-I-FRAMES-ONLY: 表示每个 Media Segment 描述一个单一的 I-frame.

Master Playlist Tags

Master Playlist tags 定义 Variant Streams, Renditions 和 其他显示的全局参数. Master Playlist tags 只能出现在 Master Playlist 中.

  • EXT-X-MEDIA: 用于关联同一个内容的多个 Media Playlist 的多种 renditions.
  • EXT-X-STREAM-INF: 用于指定一个 Variant Stream.
  • EXT-X-I-FRAME-STREAM-INF: 用于指定一个 Media Playlist 包含媒体的 I-frames.
  • EXT-X-SESSION-DATA: 存放一些 session 数据.
  • EXT-X-SESSION-KEY: 用于解密.

Media or Master Playlist Tags

这里的 tags 可以出现在 Media Playlist 或者 Master Playlist 中. 但是如果同时出现在同一个 Master Playlist 和 Media Playlist 中时, 必须为相同值.

  • EXT-X-INDEPENDENT-SEGMENTS: 表示每个 Media Segment 可以独立解码.
  • EXT-X-START: 标识一个优选的点来播放这个 Playlist.

服务器端与客户端逻辑

以下流程仅供参考, 其实不同的播放器客户端以及服务器端的拉取规则都有很多细节差异.

服务器端逻辑

  1. 将媒体源切片成 Media Segment, 应该优先从可以高效解码的时间点来进行切片(如: I-frame).
  2. 为每一个 Media Segment 生成 URI.
  3. Server 需要支持 “gzip” 方式压缩文本内容.
  4. 创建一个 Media Playlist 索引文件, EXT-X-VERSION 不要高于他需要的版本, 来提供更好的兼容性.
  5. Server 不能随便修改 Media Playlist, 除了 Append 文本到文件末尾, 按顺序移除 Media Segment URIs, 增长 EXT-X-MEDIA-SEQUENCEEXT-X-DISCONTINUITY-SEQUENCE, 添加 EXT-X-ENDLIST 到文件尾.
  6. 在最后添加 EXT-X-ENDLIST tag, 来减少 Client reload Playlist 的次数.
  7. 注意点播与直播服务器不同的地方是, 直播的 m3u8 文件会不断更新, 而点播的 m3u8 文件是不会变的, 只需要客户端在开始时请求一次即可.

客户端逻辑

  1. 客户端通过 URI 获取 Playlist. 如果是 Master Playlist, 客户端可以选择一个 Variant Stream 来播放.
  2. 客户端检查 EXT-X-VERSION 版本是否满足.
  3. 客户端应该忽略不可识别的 tags, 忽略不可识别的属性键值对.
  4. 加载 Media Playlist file.
  5. 播放 Media Playlist file.
  6. 重加载 Media Playlist file.
  7. 决定下一次要加载的 Media Segment.

HLS 的优势

  • 客户端支持简单, 只需要支持 HTTP 请求即可, HTTP 协议无状态, 只需要按顺序下载媒体片段即可.
  • 使用 HTTP 协议网络兼容性好, HTTP 数据包也可以方便地通过防火墙或者代理服务器, CDN 支持良好.
  • Apple 的全系列产品支持, 由于 HLS 是苹果提出的, 所以在 Apple 的全系列产品包括 iphone, ipad, safari 都不需要安装任何插件就可以原生支持播放 HLS, 现在, Android 也加入了对 HLS 的支持.
  • 自带多码率自适应, Apple 在提出 HLS 时, 就已经考虑了码流自适应的问题.

HLS 的劣势

  • 相比 RTMP 这类长连接协议, 延时较高, 难以用到互动直播场景.
  • 对于点播服务来说, 由于 TS 切片通常较小, 海量碎片在文件分发, 一致性缓存, 存储等方面都有较大挑战.

改进的 HLS 技术

由于客户端每次请求 TS 或 M3U8 有可能都是一个新的连接请求, 所以, 我们无法有效的标识客户端, 一旦出现问题, 基本无法有效的定位问题, 所以, 一般工业级的服务器都会对传统的 HLS 做一些改进.

这里主要介绍网宿的 Variant HLS 与又拍云的 HLS+.

网宿的 Variant HLS

首先, 我们可以下载一条网宿的 M3U8 文件:

wget http://bililive.kksmg.com/hls/stvd6edb9a6_45b34047833af658bf4945a8/playlist.m3u8

然后, 打开下载得到的 playlist 文件:

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=781000
http://bililive.kksmg.com/hls/stvd6edb9a6_45b34047833af658bf4945a8/playlist.m3u8?wsSession=0105cb4e8fe63bccab511a4a-149017212774715&wsIPSercert=b80d38c068c9e3634a7ebb2f2bbf9b89&wsMonitor=-1

可以看出这是一个 Master Playlist, 里面嵌套了一层 M3U8, 同时可以看出网宿采用 wsSession 来标识一条播放连接.

又拍云的 HLS+

Variant HLS

首先, 我们可以下载一条又拍云的 M3U8 文件:

wget http://uplive.b0.upaiyun.com/live/loading.m3u8

然后, 打开下载得到的 playlist 文件:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:YES
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:1
#EXTINF:0.998, no desc
http://183.158.35.12:8080/uplive.b0.upaiyun.com/live/loading-0.ts?shp_uuid=e4989f34fcab282e21ef1fd2980284cb&shp_ts=1490172420851&shp_cid=17906&shp_pid=3370578&shp_sip0=127.0.0.1&shp_sip1=183.158.35.12&domain=uplive.b0.upaiyun.com&shp_seqno=0

可以看出又拍云的 HLS+ 也支持这种 Variant HLS 方式来标识一条 HLS 连接, 可以看出, 又拍云使用 uuid 来表示一条 HLS 连接.

HTTP 302

首先, 以 HTTP 302 方式来请求播放地址.

❯ curl -v http://uplive.b0.upaiyun.com/live/loading.m3u8\?shp_identify\=302 -o playlist
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 183.158.35.59...
* TCP_NODELAY set
* Connected to uplive.b0.upaiyun.com (183.158.35.59) port 80 (#0)
> GET /live/loading.m3u8?shp_identify=302 HTTP/1.1
> Host: uplive.b0.upaiyun.com
> User-Agent: curl/7.51.0
> Accept: */*
>
< HTTP/1.1 302 Found
< Server: marco/0.26
< Date: Wed, 22 Mar 2017 08:54:11 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 259
< Connection: keep-alive
< Access-Control-Allow-Methods: GET
< Access-Control-Allow-Origin: *
< Location: http://183.158.35.19:8080/uplive.b0.upaiyun.com/live/loading.m3u8?shp_uuid=2862b1b817a74cf719b1cd8f554616cd&shp_ts=1490172851450&shp_cid=59553&shp_pid=1730488&shp_sip0=127.0.0.1&shp_sip1=183.158.35.19&domain=uplive.b0.upaiyun.com&shp_identify=302
<
{ [259 bytes data]
* Curl_http_done: called premature == 0
100   259  100   259    0     0   4813      0 --:--:-- --:--:-- --:--:--  4886
* Connection #0 to host uplive.b0.upaiyun.com left intact

打开 playlist 内容:

Redirect to http://183.158.35.19:8080/uplive.b0.upaiyun.com/live/loading.m3u8?shp_uuid=2862b1b817a74cf719b1cd8f554616cd&shp_ts=1490172851450&shp_cid=59553&shp_pid=1730488&shp_sip0=127.0.0.1&shp_sip1=183.158.35.19&domain=uplive.b0.upaiyun.com&shp_identify=302

在跳转之后的地址存放真正的 playlist, 同时, 也能够将 uuid 加入到了连接上.

总地来说, 不管通过哪种方式, 最终我们都能通过一个唯一的 id 来标识一条流, 这样在排查问题时就可以根据这个 id 来定位播放过程中的问题.

HLS 延时分析

HLS 理论延时 = 1 个切片的时长 + 0-1个 td (td 是 EXT-X-TARGETDURATION, 可简单理解为播放器取片的间隔时间) + 0-n 个启动切片(苹果官方建议是请求到 3 个片之后才开始播放) + 播放器最开始请求的片的网络延时(网络连接耗时)

为了追求低延时效果, 可以将切片切的更小, 取片间隔做的更小, 播放器未取到 3 个片就启动播放. 但是, 这些优化方式都会增加 HLS 不稳定和出现错误的风险.

Demo

Refs

2016 总结

今年是忙碌的一年, 发生了太多的事情. 完成了一些事情, 也有太多的事情没有完成. 貌似每年都是这样.

这里总结一下以及明年的规划.

2016 Done

  • 完成写框架的心愿.
  • 一个 TCP 之上 Redis 服务端框架: redface
  • 类 koajs 的轻量级 web 框架: light
  • 统一的网络层框架: RTSS/engine
  • 在公司内部用 swagger 写了一个 API 服务.
  • 玩转了 TCP, UDP, HTTP, WebSocket 的 client 跟 server 端的实现. 写了非常多的工具跟轮子.
  • 个人的工具库, 现在可以随手写个 Go 的后端服务: utilgo
  • 不再去深入落后的 RTMP 那套玩意了, 开启 WebRTC 之旅.
  • 线上分享: akshare
  • 在公司结识了一帮可以一起打拼的好兄弟, 没有各种 “人” 的问题.

2017 Tobe Done

  • WebRTC: 玩转 WebRTC. 深度参与一些开源项目, 认识一些牛人.
  • Quant: 爬虫以及 Quant, 达到熟练以及实战的水平.
  • 身体: 锻炼身体, 生活更加健康一些.
  • 开源: 写一个开源的实时通信的平台, 包括后端, 前端, 以及移动端.
  • blog: 养成定期写 blog 的习惯, ==, 少看一些动漫以及日剧.
  • 文档: 锻炼写出优秀文档的能力, 向阮一峰看齐.
  • 写书: 出一本流媒体方面的书.
  • 写书: <FreeRadius 新手入门> 这本书翻译完, 太多人催我了.

HTML5 直播协议之 WebSocket 和 MSE

当前为了满足比较火热的移动 Web 端直播需求, 一系列的 HTML5 直播技术迅速的发展了起来.

常见的可用于 HTML5 的直播技术有 HLS, WebSocket 与 WebRTC. 今天我要向大家介绍一下 WebSocket 与 MSE 相关的内容, 并在最后通过一个实际的例子, 来展示其具体的用法.

大纲

  • WebSocket 协议介绍.
  • WebSocket Client/Server API 介绍.
  • MSE 介绍.
  • fMP4 介绍.
  • Demo 展示.

WebSocket

通常的 Web 应用都是围绕着 HTTP 的请求/响应模型而构建的. 所有的 HTTP 通信都是通过客户端来控制的, 都是由客户端向服务器发出一个请求, 服务器接收和处理完毕后再返回结果给客户端, 客户端再将数据展现出来. 这种模式不能满足实时应用的需求, 于是出现了 SSE, Comet 等 “服务器推” 的长连接技术.

WebSocket 是直接基于 TCP 连接之上的通信协议, 可以在单个 TCP 连接上进行全双工的通信. WebSocket 在 2011 年被 IETF 定为标准 RFC 6455, 并被 RFC 7936 所补充规范, WebSocket API 被 W3C 定为标准.

WebSocket 是独立的创建在 TCP 上的协议, HTTP 协议中的那些概念都不复存在, 和 HTTP 的唯一关联是使用 HTTP 协议的 101 状态码进行协议切换, 使用的 TCP 端口是 80, 可以用于绕过大多数防火墙的限制.

websocket_protocol

WebSocket 握手

为了更方便地部署新协议,HTTP/1.1 引入了 Upgrade 机制, 它使得客户端和服务端之间可以借助已有的 HTTP 语法升级到其它协议. 这个机制在 RFC7230 的 6.7 Upgrade) 一节中有详细描述.

要发起 HTTP/1.1 协议升级,客户端必须在请求头部中指定这两个字段:

Connection: Upgrade
Upgrade: protocol-name[/protocol-version]

如果服务端同意升级, 那么需要这样响应:

HTTP/1.1 101 Switching Protocols
Connection: upgrade
Upgrade: protocol-name[/protocol-version]

[... data defined by new protocol ...]

可以看到, HTTP Upgrade 响应的状态码是 101, 并且响应正文可以使用新协议定义的数据格式.

WebSocket 握手就利用了这种 HTTP Upgrade 机制. 一旦握手完成,后续数据传输就直接在 TCP 上完成.

WebSocket JavaScript API

目前主流的浏览器提供了 WebSocket 的 API 接口, 可以发送消息(文本或者二进制)给服务器, 并且接收事件驱动的响应数据.

Step1 检查浏览器是否支持 WebSocket.

if(window.WebSocket) {
	// WebSocket代码
}

Step2 建立连接

var ws = new WebSocket('ws://localhost:8327');

Step3 注册回调函数以及收发数据

分别注册 WebSocket 对象的 onopen, onclose, onerror 以及 onmessage 回调函数.

通过 ws.send() 来进行发送数据, 这里不仅可以发送字符串, 也可以发送 Blob 或 ArrayBuffer 类型的数据.

如果接收的是二进制数据,需要将连接对象的格式设为 blob 或 arraybuffer.

ws.binaryType = 'arraybuffer';

WebSocket Golang API

服务器端 WebSocket 库我推荐使用 Google 自己的 golang.org/x/net/websocket, 可以非常方便的与 net/http 一起使用.

可以将 websocket 的 handler function 通过 websocket.Handler 转换成 http.Handler, 这样就可以跟 net/http 库一起使用了.

然后通过 websocket.Message.Receive 来接收数据, 通过 websocket.Message.Send 来发送数据.

具体代码可以看下面的 Demo 部分.

MSE

在介绍 MSE 之前, 我们先看看 HTML5 <audio><video> 有哪些限制.

HTML5

  • 不支持流.
  • 不支持 DRM 和加密.
  • 很难自定义控制, 以及保持跨浏览器的一致性.
  • 编解码和封装在不同浏览器支持不同.

MSE 是解决 HTML5 的流问题.

Media Source Extensions (MSE) 是一个主流浏览器支持的新的 Web API. MSE 是一个 W3C 标准, 允许 JavaScript 动态的构建 <video><audio> 的媒体流. 他定义了对象, 允许 JavaScript 传输媒体流片段到一个 HTMLMediaElement.

通过使用 MSE, 你可以动态地修改媒体流而不需要任何的插件. 这让前端 JavaScript 可以做更多的事情, 我们可以在 JavaScript 进行转封装, 处理, 甚至转码.

虽然 MSE 不能让流直接传输到 media tags 上, 但是 MSE 提供了构建跨浏览器播放器的核心技术, 让浏览器通过 JavaScript API 来推音视频到 media tags 上.

现在每个客户端平台都开始逐步开放流媒体相关的 API: Flash 平台有 Netstream, Android 平台有 Media Codec API, 而 Web 上对应的就是标准的 MSE. 由此可以看出, 未来的趋势是在客户端可以做越来越多的事情.

Browser Support

通过 caniuse 来检查是否浏览器支持情况.

mse-support

通过 MediaSource.isTypeSupported() 可以进一步地检查 codec MIME 类型是否支持.

比较常用的视频封装格式有 webm 和 fMP4.

WebM 和 WebP 是两个姊妹项目, 都是由 Google 赞助的. 由于 WebM 是基于 Matroska 的容器格式, 所以天生是流式的, 很适合用在流媒体领域里.

下面着重介绍一些 fMP4 格式.

我们都知道 MP4 是由一系列的 Boxes 组成的. 普通的 MP4 的是嵌套结构的, 客户端必须要从头加载一个 MP4 文件, 才能够完整播放, 不能从中间一段开始播放.

而 fMP4 由一系列的片段组成, 如果你的服务器支持 byte-range 请求, 那么, 这些片段可以独立的进行请求到客户端进行播放, 而不需要加载整个文件.

为了更加形象的说明这一点, 下面我介绍几个常用的分析 MP4 文件的工具.

  • gpac 原名 mp4box, 是一个媒体开发框架, 在其源码下有大量的媒体分析工具可以使用, testapps
  • mp4box.js 是 mp4box 的 Javascript 版本.
  • bento4 一个专门用于 MP4 的分析工具.
  • mp4parser 在线 MP4 文件分析工具.

fragment mp4 vs non-fragment mp4

下面一个 fragment mp4 文件通过 mp4parser 分析后的截图

fmp4

下面一个 non-fragment mp4 文件通过 mp4parser 分析后的截图

nfmp4

Apple 在今年的 WWDC 大会上宣布会在 iOS 10, tvOS, macOS 的 HLS 中支持 fMP4.

值得一提的是, fMP4, CMAF, ISOBMFF 其实都是类似的东西.

MSE JavaScript API

从高层次上看, MSE 提供了 * 一套 JavaScript API 来构建 media streams. * 一个拼接和缓存模型. * 识别一些 byte 流类型: * WebM * ISO Base Media File Format * MPEG-2 Transport Streams

MSE 内部结构

mse_arch

MSE 本身的设计是不依赖任务特定的编解码和容器格式的, 但是不同的浏览器支持程度是不一样的. 可以通过传递一个 MIME 类型的字符串到静态方法: MediaSource.isTypeSupported 来检查.

比如:

MediaSource.isTypeSupported('audio/mp3'); // false
MediaSource.isTypeSupported('video/mp4'); // true
MediaSource.isTypeSupported('video/mp4; codecs="avc1.4D4028, mp4a.40.2"'); // true

获取 Codec MIME string 的方法可以通过在线的 mp4info 或者使用命令行 mp4info test.mp4 | grep Codecs

可以得到类似如下结果:

❯ mp4info fmp4.mp4| grep Codec
    Codecs String: mp4a.40.2
    Codecs String: avc1.42E01E

当前, H.264 + AAC 的 MP4 容器在所有的浏览器都支持.

普通的 MP4 文件是不能和 MSE 一起使用的, 需要将 MP4 进行 fragment 化.

检查一个 MP4 是否已经 fragment 的方法

mp4dump test.mp4 | grep "\[m"

如果是 non-fragment 会显示类似信息.

❯ mp4dump nfmp4.mp4 | grep "\[m"
[mdat] size=8+50873
[moov] size=8+7804
  [mvhd] size=12+96
    [mdia] size=8+3335
      [mdhd] size=12+20
      [minf] size=8+3250
    [mdia] size=8+3975
      [mdhd] size=12+20
      [minf] size=8+3890
            [mp4a] size=8+82
    [meta] size=12+78

如果已经 fragment, 会显示如下类似信息.

❯ mp4dump fmp4.mp4 | grep "\[m" | head -n 30
[moov] size=8+1871
  [mvhd] size=12+96
    [mdia] size=8+312
      [mdhd] size=12+20
      [minf] size=8+219
            [mp4a] size=8+67
    [mdia] size=8+371
      [mdhd] size=12+20
      [minf] size=8+278
    [mdia] size=8+248
      [mdhd] size=12+20
      [minf] size=8+156
    [mdia] size=8+248
      [mdhd] size=12+20
      [minf] size=8+156
  [mvex] size=8+144
    [mehd] size=12+4
[moof] size=8+600
  [mfhd] size=12+4
[mdat] size=8+138679
[moof] size=8+536
  [mfhd] size=12+4
[mdat] size=8+24490
[moof] size=8+592
  [mfhd] size=12+4
[mdat] size=8+14444
[moof] size=8+312
  [mfhd] size=12+4
[mdat] size=8+1840
[moof] size=8+600

把一个 non-fragment MP4 转换成 fragment MP4.

可以使用 FFmpeg 的 -movflags 来转换

对于原始文件为非 MP4 文件

ffmpeg -i trailer_1080p.mov -c:v copy -c:a copy -movflags frag_keyframe+empty_moov bunny_fragmented.mp4

对于原始文件已经是 MP4 文件

ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov fragmented.mp4

或者使用 mp4fragment

mp4fragment input.mp4 output.mp4

demo

  • MSE Vod Demo 展示利用 MSE 和 WebSocket 实现一个点播服务.
  • MSE Live Demo 展示利用 MSE 和 WebSocket 实现一个直播服务.

MSE VOD Demo

MSE Live Demo

Refs

WebSocket

MSE

直播协议 HTTP-FLV 详解

传统的直播协议要么使用 Adobe 的基于 TCP 的 RTMP 协议, 要么使用 Apple 的基于 HTTP 的 HLS 协议.

今天我要向大家介绍另外一种结合了 RTMP 的低延时, 以及可以复用现有 HTTP 分发资源的流式协议 HTTP-FLV.

FLV

首先, 一定要先介绍一下 FLV 文件格式的细节.

FLV adobe 官方标准

FLV 文件格式标准是写在 F4V/FLV file format spec v10.1 的附录 E 里面的 FLV File Format.

单位说明

类型 说明
Unit data types
SI8 Signed 8-bit integer
SI16 Signed 16-bit integer
SI24 Signed 24-bit integer
SI32 Signed 32-bit integer
SI64 Signed 32-bit integer
UI8 Unsigned 8-bit integer
UI16 Unsigned 16-bit integer
UI24 Unsigned 24-bit integer
UI32 Unsigned 32-bit integer
UI64 Unsigned 64-bit integer
xxx[] Slice of type xxx
xxx[n] Array of type xxx
STRING Sequence of Unicode 8-bit characters (UTF-8), terminated with 0x00

FLV 文件头和文件体 (E.2, E.3)

从整个文件上看, FLV = FLV File Header + FLV File Body

字段 类型 说明
FLV File Header
Signature UI8[3] 签名, 总是 ‘FLV’ (0x464C56)
Version UI8 版本, 总是 0x01, 表示 FLV version 1
TypeFlagsReserved UB [5] 全 0
TypeFlagsAudio UB[1] 1 = 有音频
TypeFlagsReserved UB[1] 全 0
TypeFlagsVideo UB[1] 1 = 有视频
DataOffset UI32 整个文件头长度, 对于FLV v1, 总是 9
FLV File Body
PreviousTagSize0 UI32 总是 0
Tag1 FLVTAG 第一个 tag
PreviousTagSize1 UI32 前一个 tag 的大小, 包括他的 header, 即: 11 + 前一个 tag 的大小
Tag2 FLVTAG 第二个 tag
PreviousTagSizeN-1 UI32 前一个 tag 大小
TagN FLVTAG 最后一个 tag
PreviousTagSizeN UI32 最后一个 tag 大小, 包括他的 header

通常, FLV 的前 13 个字节(flv header + PreviousTagSize0)完全相同, 所以, 程序中会单独定义一个常量来指定.

FLV Tag (E.4)

字段 类型 说明
FLV Tag
Reserved UB[2] 保留给FMS, 应为 0
Filter UB[1] 0 = unencrypted tags, 1 = encrypted tags
TagType UB [5] 类型, 0x08 = audio, 0x09 = video, 0x12 = script data
DataSize UI24 message 长度, 从 StreamID 到 tag 结束(len(tag) - 11)
Timestamp UI24 相对于第一个 tag 的时间戳(unit: ms), 第一个 tag 总是 0
TimestampExtended UI8 Timestamp 的高 8 位. 扩展 Timestamp 为 SI32 类型
StreamID UI24 总是 0, 至此为 11 bytes
AudioTagHeader IF TagType == 0x08
VideoTagHeader IF TagType == 0x09
EncryptionHeader IF Filter == 1
FilterParams IF Filter == 1
Data AUDIODATA 或者 VIDEODATA 或者 SCRIPTDATA

Timestamp 和 TimestampExtended 组成了这个 TAG 包数据的 PTS 信息, PTS = Timestamp | TimestampExtended << 24.

AudioTag (E.4.2)

由于 AAC 编码的特殊性, 这里着重说明了 AAC 编码的 Tag 格式.

字段 类型 说明
Audio Tag
AudioTagHeader
SoundFormat UB[4] 音频编码格式. 2 = MP3, 10 = AAC, 11 = Speex
SoundRate UB[2] 采样率. 0 = 5.5 kHz, 1 = 11 kHz, 2 = 22 kHz, 3 = 44 kHz
SoundSize UB[1] 采样大小. 0 = 8-bit, 1 = 16-bit
SoundType UB[1] 音频声道数. 0 = Mono, 1 = Stereo
AACPacketType UI8 只有当 SoundFormat 为 10 时, 才有该字段. 0 = AAC sequence header, 1 = AAC raw
AACAUDIODATA
Data AudioSpecificConfig IF AACPacketType == 0, 包含着一些更加详细音频的信息
Data Raw AAC frame data in UI8 [n] IF AACPacketType == 1, audio payload, n = [AAC Raw data length] - ([has CRC] ? 9 : 7)

AudioTagHeader 的第一个字节, 也就是接跟着 StreamID 的 1 个字节包含了音频类型, 采样率等的基本信息.

AudioTagHeader 之后跟着的就是 AUDIODATA 部分了. 但是, 这里有个特例, 如果音频格式(SoundFormat)是 AAC, AudioTagHeader 中会多出 1 个字节的数据 AACPacketType, 这个字段来表示 AACAUDIODATA 的类型: 0 = AAC sequence header, 1 = AAC raw.

AudioSpecificConfig 结构描述非常复杂, 在标准文档中是用伪代码描述的, 这里先假定要编码的音频格式, 做一下简化.

音频编码为: AAC-LC, 音频采样率为 44100.

字段 类型 说明
AudioSpecificConfig
audioObjectType UB[5] 编码结构类型, AAC-LC 为 2
samplingFrequencyIndex UB[4] 音频采样率索引值, 44100 对应值 4
channelConfiguration UB[4] 音频输出声道, 2
GASpecificConfig
frameLengthFlag UB[1] 标志位, 用于表明 IMDCT 窗口长度, 0
dependsOnCoreCoder UB[1] 标志位, 表明是否依赖于 corecoder, 0
extensionFlag UB[1] 选择了 AAC-LC, 这里必须为 0

在 FLV 的文件中, 一般情况下 AAC sequence header 这种包只出现1次, 而且是第一个 audio tag, 为什么需要这种 tag, 因为在做 FLV demux 的时候, 如果是 AAC 的音频, 需要在每帧 AAC ES 流前边添加 7 个字节 ADST 头, ADST 是解码器通用的格式, 也就是说 AAC 的纯 ES 流要打包成 ADST 格式的 AAC 文件, 解码器才能正常播放. 就是在打包 ADST 的时候, 需要 samplingFrequencyIndex 这个信息, samplingFrequencyIndex 最准确的信息是在 AudioSpecificConfig 中, 这样, 你就完全可以把 FLV 文件中的音频信息及数据提取出来, 送给音频解码器正常播放了.

VideoTag (E.4.3)

由于 AVC(H.264) 编码的特殊性, 这里着重说明了 AVC(H.264) 编码的 Tag 格式.

字段 类型 说明
Video Tag
VideoTagHeader
FrameType UB[4] 1 = key frame, 2 = inter frame
CodecID UB[4] 7 = AVC
AVCPacketType UI8 IF CodecID == 7, 0 = AVC sequence header(AVCDecoderConfigurationRecord), 1 = One or more AVC NALUs (Full frames are required), 2 = AVC end of sequence
CompositionTime SI24 IF AVCPacketType == 1 Composition time offset ELSE 0

VideoTagHeader 的第一个字节, 也就是接跟着 StreamID 的 1 个字节包含着视频帧类型及视频 CodecID 等最基本信息.

VideoTagHeader 之后跟着的就是 VIDEODATA 部分了. 但是, 这里有个特例, 如果视频格式(CodecID)是 AVC, VideoTagHeader 会多出 4 个字节的信息.

AVCDecoderConfigurationRecord 包含着是 H.264 解码相关比较重要的 SPS 和 PPS 信息, 在给 AVC 解码器送数据流之前一定要把 SPS 和 PPS 信息送出,否则的话, 解码器不能正常解码. 而且在解码器 stop 之后再次 start 之前, 如 seek, 快进快退状态切换等, 都需要重新送一遍 SPS 和 PPS 的信息. AVCDecoderConfigurationRecord 在 FLV 文件中一般情况也只出现 1 次, 也就是第一个 video tag.

AVCDecoderConfigurationRecord 长度为 sizeof(UI8) * (11 + sps_size + pps_size)

字段 类型 说明
AVCDecoderConfigurationRecord
configurationVersion UI8 版本号, 1
AVCProfileIndication UI8 SPS[1]
profileCompatibility UI8 SPS[2]
AVCLevelIndication UI8 SPS[3]
reserved UB[6] 111111
lengthSizeMinusOne UB[2] NALUnitLength - 1, 一般为 3
reserved UB[3] 111
numberOfSequenceParameterSets UB[5] SPS 个数, 一般为 1
sequenceParameterSetNALUnits UI8[sps_size + 2] sps_size(16bits) + sps(UI8[sps_size])
numberOfPictureParameterSets UI8 PPS 个数, 一般为 1
pictureParameterSetNALUnits UI8[pps_size + 2] pps_size(16bits) + pps(UI8[pps_size])

SCRIPTDATA (E.4.4)

ScriptTagBody 内容用 AMF 编码

字段 类型 说明
SCRIPTDATA
ScriptTagBody
Name SCRIPTDATAVALUE Method or object name. SCRIPTDATAVALUE.Type = 2 (String)
Vale SCRIPTDATAVALUE AMF arguments or object properties.
SCRIPTDATAVALUE
Type UI8 ScriptDataValue 的类型
ScriptDataValue 各种类型 Script data 值

一个 SCRIPTDATAVALUE 记录包含一个有类型的 ActionScript 值.

onMetadata (E.5)

FLV metadata object 保存在 SCRIPTDATA 中, 叫 onMetaData. 不同的软件生成的 FLV 的 properties 不同.

字段 类型 说明
onMetaData
audiocodecid Number Audio codec ID used in the file
audiodatarate Number Audio bit rate in kilobits per second
audiodelay Number Delay introduced by the audio codec in seconds
audiosamplerate Number Frequency at which the audio stream is replayed
audiosamplesize Number Resolution of a single audio sample
canSeekToEnd Boolean Indicating the last video frame is a key frame
creationdate String Creation date and time
duration Number Total duration of the file in seconds
filesize Number Total size of the file in bytes
framerate Number Number of frames per second
height Number Height of the video in pixels
stereo Boolean Indicating stereo audio
videocodecid Number Video codec ID used in the file (see E.4.3.1 for available CodecID values)
videodatarate Number Video bit rate in kilobits per second
width Number Width of the video in pixels

keyframes 索引信息

官方的文档中并没有对 keyframes index 做描述, 但是, flv 的这种结构每个 tag 又不像 TS 有同步头, 如果没有 keyframes index 的话, seek 及快进快退的效果会非常差, 因为需要一个 tag 一个 tag 的顺序读取. 后来在做 flv 文件合成的时候, 发现网上有的 flv 文件将 keyframes 信息隐藏在 Script Tag 中. keyframes 几乎是一个非官方的标准, 也就是民间标准.

两个常用的操作 metadata 的工具是 flvtool2 和 FLVMDI, 都是把 keyframes 作为一个默认的元信息项目. 在 FLVMDI 的主页上有描述:

  keyframes: (Object) This object is added only if you specify the /k switch. 'keyframes' is known to FLVMDI and if /k switch is not specified, 'keyframes' object will be deleted.

  'keyframes' object has 2 arrays: 'filepositions' and 'times'. Both arrays have the same number of elements, which is equal to the number of key frames in the FLV. Values in times array are in 'seconds'. Each correspond to the timestamp of the n'th key frame. Values in filepositions array are in 'bytes'. Each correspond to the fileposition of the nth key frame video tag (which starts with byte tag type 9).

也就是说 keyframes 中包含着 2 个内容 ‘filepositions’ 和 ‘times’ 分别指的是关键帧的文件位置和关键帧的 PTS. 通过 keyframes 可以建立起自己的 Index, 然后在 seek 和快进快退的操作中, 快速有效地跳转到你想要找的关键帧位置进行处理.

FLV 分析工具

HTTP-FLV

HTTP-FLV, 即将音视频数据封装成 FLV, 然后通过 HTTP 协议传输给客户端.

这里首先要说一下, HLS 其实是一个 “文本协议”, 而并不是一个流媒体协议. 那么, 什么样的协议才能称之为流媒体协议呢?

流(stream): 数据在网络上按时间先后次序传输和播放的连续音/视频数据流. 之所以可以按照顺序传输和播放连续是因为在类似 RTMP, FLV 协议中, 每一个音视频数据都被封装成了包含时间戳信息头的数据包. 而当播放器拿到这些数据包解包的时候能够根据时间戳信息把这些音视频数据和之前到达的音视频数据连续起来播放. MP4, MKV 等等类似这种封装, 必须拿到完整的音视频文件才能播放, 因为里面的单个音视频数据块不带有时间戳信息, 播放器不能将这些没有时间戳信息数据块连续起来, 所以就不能实时的解码播放.

延迟分析

理论上(除去网络延迟外), FLV 可以做到仅仅一个音视频 tag 的延迟.

相比 RTMP 的优点:

  • 可以在一定程度上避免防火墙的干扰 (例如, 有的机房只允许 80 端口通过).
  • 可以很好的兼容 HTTP 302 跳转, 做到灵活调度.
  • 可以使用 HTTPS 做加密通道.
  • 很好的支持移动端(Android, IOS).

抓包分析

打开网宿的 HTTP-FLV 流:

http://175.25.168.16/pl3.live.panda.tv/live_panda/d4e0a83a7e0b0c6e4c5d03774169fa3e.flv?wshc_tag=0&wsts_tag=57e233b1&wsid_tag=6a27c14e&wsiphost=ipdbm

HTTP/1.1 200 OK
Expires: Wed, 21 Sep 2016 07:16:02 GMT
Cache-Control: no-cache
Content-Type: video/x-flv
Pragma: no-cache
Via: 1.1 yc16:3 (Cdn Cache Server V2.0)
Connection: close

发现响应头中出现 Connection: close 的字段, 表示网宿采用的是短连接, 则直接可以通过服务器关闭连接来确定消息的传输长度.

如果 HTTP Header 中有 Content-Length, 那么这个 Content-Length 既表示实体长度, 又表示传输长度. 而 HTTP-FLV 这种流, 服务器是不可能预先知道内容大小的, 这时就可以使用 Transfer-Encoding: chunked 模式来传输数据了.

如下的响应就是采用的Chunked的方式进行的传输的响应头:

HTTP/1.1 200 OK
Server: openresty
Date: Wed, 21 Sep 2016 07:38:01 GMT
Content-Type: video/x-flv
Transfer-Encoding: chunked
Connection: close
Expires: Wed, 21 Sep 2016 07:38:00 GMT
Cache-Control: no-cache

mqkv 一个通用的基于分布式消息队列的分布式存储

公司直播系统的源站集群需要一个中心存储服务来提供流的元数据信息的存储和查询功能。由于源站集群是部署在全国各地的多个机房, 多个节点上的。所以, 这里是一个典型的分布式存储的应用场景。这个服务的稳定性非常重要, 也直接影响到直播系统整个服务整体的可用性。

下面, 我来与大家分享交流一下, 我们源站集群共享存储方案经历了哪些变化, 最后, 介绍下我们的下一代共享存储方案 mqkv, 也欢迎熟悉分布式与存储的小伙伴提提建议。

由于, 我们公司的 CDN 系统是使用 Redis 的主从同步机制来进行配置元数据的同步和分发到全国各个 CDN 节点的, 我们对 Redis 的各种特性比较熟。所以, 这里, 我们最先考虑的也是 Redis 的方案。

应用场景

分布式存储也是一个很宽泛的概念, 可以选择的技术很多。首先, 要分析好我们的应用场景是怎样的, 哪些是我们 care 的, 哪些是我们不太 care 的。

推流源站会对流的元数据信息进行写操作, 而拉流源站会进行流的元数据的读操作。而直播是个典型的一对多提供服务的场景, 所以, 相应的, 我们的共享存储方案也是写少读多。同时, 我们可以允许少量的数据丢失, 我们更看重的是服务无SPOF, 稳定性与读写性能。

方案一: Redis Cluster

Redis 3.0 之后推出了自己的 Redis Cluster 集群方案, 所以, 在最开始, 我们还是优先去尝试官方的集群方案。但是, 我们发现 Redis Cluster 仍然不能实现跨机房容灾, 跨机房高可用的功能还是需要自己来实现。所以, 我们源站集群共享存储的最初版本是每台源站的读写都是去操作部署在一个 BGP 机房的 Redis Cluster。但这个方案会导致读写性能都不理想, 所以, 后面我们考虑了在程序中引入缓存, 来减少读压力。

方案二: Redis Cluster + TTL MemoryCache

为了优化读性能, 我们首先考虑在程序中加入缓存, 结合我们的业务场景, 我们开发引入了基于 groupcache 的超时缓存方案。我们比较专注最新的数据, 过期的数据没有意义, 反而会影响我们的业务逻辑, 所以, 元数据的每个 key 都可以配置一个 TTL 过期时间, 当时间到达时, 这个 key 就会 expire 掉。在一个 key expire 的同时有大量访问这个 key 的请求这个临界点时, groupcache 的内部有锁机制保障, 不会出现大量的回源请求, 给中心存储造成压力, 这个方案在我们的测试环境中, 测试结果跟我们预期基本一致。

方案三: Redis Master/Slave 读写分离

而其实我们上面的方案并没有上线, 就有人提出了 Redis 读写分离的方案。写到一个中心 Redis Master 节点, 然后每台源站只去读本机的 Redis Slave 节点, 通过 Redis 的主从同步机制来确保数据一致性。最开始没有用这套方案是因为 Redis 的主从同步机制与 Redis Cluster/Sentinel 有冲突, 不能共存。后来, 运维提供了通过 keepalived 来保证 redis 的高可用, 所以, 我们线上采用了这套方案, 通过运行实际效果来看, 比较理想。

方案四: etcd

我们也在调研一些其他的分布式 kv 存储的方案, 下面是 etcd 的 benchmark 结果。

etcd_test

etcd 并发量10, 100, 1000 分别测试 PUT, GET, DELETE 连续三个操作。结果平均响应时间也跟着上去了。这个结果我们是无法接受的, 所以, 这个方案没有再继续深入研究了。

方案五: mqkv

在对现有的一些分布式存储以及集群方案测试结果非常失望后, 我们开始考虑自研适用于我们这种业务场景的分布式存储方案。

我们预期要达到的效果:

  • 每台源站读写操作都在本地, 要有较好的读写性能。写操作可以异步化, 尽快返回。
  • 无中心节点, 无 SPOF。(写在一个中心的方案, 写的这个主 Redis 还是一个单节点, 一旦机房断网或者断电, 那么, 整个直播服务就不可用了)
  • 允许出现少量的写数据失败的情况。

基于以上几点我设计了 mqkv: * 支持跨机房部署, 避免的单点问题。 * 读写操作都在同一个源站的本机, 读写性能均达到最佳。 * mqkv 提供给应用的接口使用的协议是 Redis 协议, 兼容大量的 redis client driver。

mqkv 架构图

mqkv_arch

实现过程

首先, 我实现了一个 Go 语言版的 redis server 的 api 框架 RedFace。设计主要参考了 net/http 的接口, 由于, 目前的业务逻辑还比较简单, 所以, 太复杂的代码并不多。

比较巧的是, 在我实现了这个包的那个周末, 我看到了 hacker news 上有个跟我的项目功能非常类似的一个项目上了头条, 叫 redcon。不过从接口可以明显的看出, 我实现的版本接口更加简洁, 友好。具体地, 可以对比下 redcon 的 example 和 redface 的 example

不过, redcon 的 benchmark 性能确实比我实现的要好, 这里, 我暂时还没有找到具体的原因, 哈哈, 欢迎高手帮忙分析下。

接下来, 就是 mqkv 的实现了, 其实在架构与逻辑确定好了, 轮子也造好了之后, 写代码就变成很简单的事情了。简单的说, 我就是将应用的 write 操作都异步化, 通过分布式消息队列将消息发送出去, read 操作直接 proxy 本地的 kv 存储。其中利用了 nsq 的 PUB/SUB 模型, 所有 write 操作都 produce 到 mqkv_topic 这个 topic 下, 同时, 每个 mqkv 也作为消费者注册消费 topic 为 mqkv_topic, channel 为本机 hostname 的消息。这样, 就实现一写多读的消息分发模型了。每个 mqkv 在本地的 redis 进行全量的 kv 存储, 这里的 Redis 连接, 我也是用了 Redis 中间件 来兼容 normal redis/redis sentinel/redis cluster 各种集群与高可用 redis 方案。

这样, mqkv 就完成了, 是不是很简单。

benchmark

  • 当 mqkv 启动后, 其实, 对于应用来说, 他本身就化身成为了一个 normal redis, 所以, 可用 redis-benchmark 进行压测。
❯ redis-benchmark -p 6389 -t set,get -n 1000000 -q -P 512 -c 512

SET: 28659.04 requests per second

GET: 23171.21 requests per second

以上是在我的 macbook pro 上性能测试结果。

监控

  • 支持 pprof 性能监控: GET /debug/pprof/profile
  • 支持 stats channel 信息 api: GET /api/v1/consumer_stats 可以参看当前 mqkv 自己所连 channel 的消息消费情况。

其他的一些还需解决的问题

当然 mqkv 还存在很多不完美的地方。

  • nsq 部署依赖 dns, 需要将 机器的 hostname 与 ip 关系写到 /etc/hosts 里面。不知道有没有更简单的方法。
  • 机器扩容, 源站宕机一段时间后恢复, 元数据如果恢复, 如何保证数据一致性。
  • 也在考虑开发一个 kafka 版本进行对照, 看是否能保证更好的数据一致性。