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 版本进行对照, 看是否能保证更好的数据一致性。

设计优先的 Restful API 服务开发与服务解耦实践

最近公司需要一个 API 服务提供给用户用来查询直播系统的一些流相关数据接口。正好我们公司应用组的同事 CatTail 分享了他们在这个季度将他们的 API 服务进行了重构, 使用了 swagger 这套 OpenAPI 标准工具。正好借此机会, 学来用用。

Swagger 框架的选择

应用组这边用的是 Node.js 开发的 API 服务, 我这边涉及到数据的上报与高并发处理, 使用 Go 开发。所以, 需要选择一个 Go 语言版的 Swagger API 框架

经过简单的调研比较后, 我选择了 go-swagger 这个框架。这个开源项目是由 VMware 赞助并维护的, 支持最新的 Swagger 2.0 标准。项目的文档还不算太详细, 有的地方需要自己来摸索下。

我选择这个框架基于如下几点考虑:

  • 支持标准的 http middleware, 方便集成 Alice 插件管理工具, 可以直接使用大量与 net/http 兼容的各种 http middleware。
  • 代码侵入性不大, 如果后期想改用其他框架, 迁移起来也不太麻烦。
  • 自带 validator 功能, API spec 设计好后, 无需在代码中自己去写繁琐的边界校验功能。
  • 可以通过代码生成 API 文档, 这样能 100% 的保证代码与文档保持一致。
  • 由于使用 Swagger 的标准, 方便使用兼容这一标准的一整套工具链, 保证 API 监控, spec render 等, 也能复用应用组那边的资源, 哈哈。
  • 项目也在发展中, 目前支持的 scheme 有 http 和 https, 后续会支持 ws 和 wss。我将来计划开发的 WebRTC 信令服务也可以考虑用这个框架来做。
  • go-swagger 提供很多基础的 helper function 库实现: https://github.com/go-openapi

这个框架不太好的地方, 或者对于 Go 这种强类型语言不太好的地方, 就是 Swagger 基于 spec 定义的 definitions 生成的 model 与 数据库的 model 不能使用一套 struct, 因为涉及到一些 struct tag 的添加没法添加, 区分开后, 就涉及到两种结构之间的数据拷贝的问题, 代码显得有点冗长。暂时我还没想到更好的办法。

直播 API 服务架构设计

首先是整理需求, 和抽象需求的模型, 我这边收到几个类似的业务需求有 禁播/踢流 服务, 触发/定时截图, 触发/定时录制, 流事件回调与查询。都涉及到配置变更和推流与断流事件触发相应的业务逻辑。所以, 我抽象出了如下服务架构。

origin_admin

这个架构同时解决了两个问题:

  • 与应用解耦, 在过去应用配置数据是通过 redis 数据结构与底层服务进行对接的, 当数据结构变更与扩展时, 都会发生各种各样繁琐的数据兼容与繁琐的流程。现在底层服务直接提供 Restful API 风格的 CRUD 操作, 底层服务自身来进行数据的持久化, 当需要存储结构调优和变更时, 就可以自己内部来调整, 而不用去动对外的接口。这在互联网环境, 需求与架构不断变化, 快速迭代开发的情况相匹配。
  • 由于 CRUD 接口都在我的服务内部, 配置变更触发事件, 这个逻辑变得更加容易实现和控制。

统一的异步消息处理机制与事件回调机制:

  • 配置变更 与 流事件触发, 可以用类似的异步逻辑来处理, 这里目前是通过 NSQ 消息队列, 来异步处理消费这些事件。
  • 推流断流事件与配置变更事件可以联动触发其他服务的业务逻辑。这里我设计了统一的事件回调机制, 可以回调到录制服务, 截图服务, 踢流服务甚至客户自己的服务等。

说说技术选型

  • swagger: OpenAPI 标准, 可以利用 swagger 一整套工具, 方便后续做 API 统计与监控。
  • mq: NSQ, 简单, 够用, 稳定。
  • db: mongodb, 文档存储, 支持大数据, 支持单个 field 的 CRUD 操作, 方便 scale, 够用的读写性能, 够用的查询功能。

通过 Swagger spec 来设计 API

这里截取一小部分来说明

swagger: "2.0"
info:
  contact:
    email: [email protected]
    name: Akagi201
    url: http://akagi201.org
  description: UPYUN live streaming API service based on go-swagger
  title: UPYUN live streaming API service
  version: 0.1.0
# during dev, should point to your local machine
host: localhost:2201
# basePath will be prefixed to all paths
basePath: /api/v1
produces:
  - application/json
consumes:
  - application/json
schemes:
  - http
  - https
tags:
  - name: system
    description: 系统信息
  - name: stream
    description: 流信息
  - name: event
    description: 对内 事件 接口
  - name: config
    description: 对内 应用配置 接口
paths:
  /system/version:
    get:
      tags:
        - system
      summary: 获取版本信息
      operationId: getVersion
      responses:
        200:
          description: get versions
          schema:
            $ref: "#/definitions/version"
        default:
          description: error
          schema:
            $ref: "#/definitions/error_response"
definitions:
  version:
    type: object
    title: version_info
    required:
      - version
      - signature
    properties:
      version:
        type: string
        minLength: 1
      signature:
        type: string
        minLength: 1
  error_response:
    type: object
    title: error_response
    required:
      - code
      - data
    properties:
      code:
        type: integer
        format: int32
        description: 所有错误码统一定义
        example: 201
      data:
        type: string
        minLength: 1
        description: 详细错误信息
        example: Operator already exists

限制条件与文档说明, 例子都可以写到 spec 里面。完整的 spec 文档, 请参考 http://swagger.io/specification/

可以使用开源的 swagger-ui 来 render 这份文档, 当然如果你觉得他太丑了, 可以自己写个 render。

uplive_api

使用 go-swagger 框架

目录结构

❯ tree -L 3
.
├── README.md
├── client // client SDK
│   ├── config
│   │   ├── config_client.go
│   │   ├── del_stream_config_parameters.go
│   │   ├── del_stream_config_responses.go
│   │   ├── get_stream_config_parameters.go
│   │   ├── get_stream_config_responses.go
│   │   ├── set_stream_config_parameters.go
│   │   └── set_stream_config_responses.go
│   ├── event
│   │   ├── event_client.go
│   │   ├── get_stream_events_parameters.go
│   │   ├── get_stream_events_responses.go
│   │   ├── stream_event_collector_parameters.go
│   │   └── stream_event_collector_responses.go
│   ├── origin_admin_client.go
│   ├── stream
│   │   ├── get_stream_status_parameters.go
│   │   ├── get_stream_status_responses.go
│   │   └── stream_client.go
│   └── system
│       ├── get_version_parameters.go
│       ├── get_version_responses.go
│       └── system_client.go
├── cmd // 最终生成的二进制
│   └── origin-admin-server
│       └── main.go
├── models // 根据 spec definition 生成的 model
│   ├── error_response.go
│   ├── result_response.go
│   ├── stream_action.go
│   ├── stream_event.go
│   ├── stream_events.go
│   ├── stream_info.go
│   ├── stream_status.go
│   └── version.go
├── restapi // api 处理逻辑
│   ├── configure_origin_admin.go // 自定义 API 逻辑入口文件
│   ├── controller.go // 我添加的针对每个 API 的具体处理 Handler 逻辑实现, 后期可以拆分成目录和多个文件的结构
│   ├── doc.go
│   ├── embedded_spec.go
│   ├── operations
│   │   ├── config
│   │   ├── event
│   │   ├── origin_admin_api.go
│   │   ├── stream
│   │   └── system
│   └── server.go
├── store // 与 mgo 交互的 model 定义
│   └── mongo
│       ├── event.go
│       └── stream_config.go
└── swagger.yml // spec 定义的地方

总结与体会

  • 对于新人来说, 会有一定的学习成本, 包括开发理念的传递。不过 design first 这个理念是非常好的。
  • 整体来说使用这套框架还是比较顺利, 没有遇到什么无法解决的问题, 也希望 go-swagger 能够不断完善起来, 另外, 大家用的多了, 自然功能也就完善了, 所以, 在此, 还是向大家隆重推荐一下。

Reading WebRTCBook

webrtcbook-cover

5 月 29 号在美国亚马逊上买了 这本书. 邮件上预计是 7 月 5 日到. 结果今天( 6 月 13 日)就到了, 真是开心. BTW, 这算是我买过的最贵的书了. 购买地址

webrtcbook-order

书里面的图片都是彩印的. 不算太厚, 才 295 页.

Reading ng-book 2

哈哈, 最近在同时学两个东西, 都是 ngxxx 一个是 nginx, 另一个就是 angular.

同样地, 这是一篇无聊的文章, 只是给我个人用来记录进度的.(不记录, 坚持不下来啊)

学习动机

  • 目前工作一个项目用的 angular 1.x
  • data 驱动 view
  • 前端太 tmd 的乱, 框架一大堆, 看别人分析, 都希望出现一个统一的框架. 我希望是 angular.
  • 想摆脱手写html, 手写js, 手写 css 的低端模式, 学个框架, 以后可以装逼说自己会前端了.

文档地址

进度

2016-02-17

  • 当前的 angular 2 状态是 beta.
  • angular: https://angular.io/
  • typescript: http://www.typescriptlang.org/
  • 简单的看了下网上各路人马的评价, 在typescript跟es6中选择了typescript.
  • TypeScript is a superset of JavaScript ES6 that adds types.
  • ES5 == normal JavaScript
  • ES6 == ES2015
  • Angular 2 is written in TypeScript and generally that’s what everyone is using.
  • Angular 2 itself is a javascript file.
  • Shim. A shim is a library that brings a new API to an older environment, using only the means of that environment.
  • Polyfill. A polyfill is a piece of code (or plugin) that provides the technology that you, the developer, expect the browser to provide natively. Flattening the API landscape if you will.

2016-05-18

Angular 2 项目基础

  • package.json
  • tsconfig.json
  • tslint.json

Angular 2 依赖

  • ES6 Shim
  • Zones
  • Reflect Metadata
  • SystemJS

notes

  • CSS 使用 Semantic-UI.
  • reference(in .d.ts) 语句指定 typing 文件的路径.
  • import 语句是来自 ES6, 叫做 destructuring.
  • Component 是 Angular 1里的 directives 的新版本.
  • 一个基础的 Component 包含两个部分: Component annotation 和 component definition class.
  • 把 annotation 看做 metadata added to your code. (? 跟 python 的 docorator 类似吧)
  • selector: angular自己的selector mix, 类似 CSS selector, XPath, JQuery selector.
  • npm run tsc 编译.
  • npm run tsc:w 编译并监听.
  • tsc --watch 每次修改都编译.
  • 读到 Page 16

2016-05-19

  • npm run go 监听修改并serve, 注意 8080 端口不能被占用.
  • array: Angular1 的 ng-repeat 在 Angular2 中类似的指令是 NgFor.
  • #newtitle 语法叫做一个 resolve. 效果是让这个变量在这个 view 范围的的表达式有效.
  • 绑定 input 到 value, newtitle 是一个 object, 代表这个 input DOM 元素, 类型是 HTMLInputElement. 可以访问 newtitle.value.
  • 绑定 actions 到 events, 组件类的一个函数赋值给 (click) 属性.
  • 反引号: ES6 语法, 反引号的字符串将会展开模板变量.
  • 可以在 attribute values 里面使用模板.
  • 在 Angular 1, directives 全局 match, 在 Angular 2, 你需要显式指定你用哪个组件(因此, 哪个 selector).
  • 在 js 里, 默认传播 click 事件给所有的父组件.
  • 一个好的实践是当写 Angular 代码时, 尽量将你使用的数据结构与组件代码隔离.
  • 封装的原则: LoD, https://en.wikipedia.org/wiki/Law_of_Demeter
  • train-wreck: 小心 long-method chaining foo.bar.baz.bam
  • 读到 Page 42

2016-05-20

  • MVC guideline: Skinny Controller, Fat Model
  • 核心理念是: 将大多数我们的 domain logic 移动到我们的 models, 因此我们的 components 尽可能做最少的工作.
  • 数组的两种表示: 1. 普通: Article[]; 2. generics: Array
    .
  • Component 的 attribute: inputs
  • 创建 coimponent 的核心不仅仅是封装, 而且是为了重用.

总结编写 Angular 2 应用步骤

  1. 将你的应用分解成 components.
  2. 创建 view.
  3. 定义你的 model.
  4. 显示你的 model.
  5. 添加互动.

typescript

  • ES6 = ES5 + classes + modules
  • TypeScript = ES6 + types + annotations
  • transpiler / transcompiler : TypeScript -> ES5.
  • TypeScript to ES5 有一个单一的 transpiler, 由核心 TypeScript team 开发.
  • ES6 to ES5 有两个 transpiler: traceur (by google), babel (by js community).
  • REPL: ts-node, tsun
  • number: 在TS中, 所有的number都是浮点数.
  • any: any 是默认类型, 如果我们忽略给一个变量指定类型,
  • classes: 在 ES5 中, OO是通过 prototype-based objects 实现的.
  • 最佳实践, 关于补充js中没有class: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide
  • oo in js: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript
  • class 有 properties, methods, constructors.
  • A void value is also a valid any value.
  • constructor: 必须被命名为: constructor. 每个 class 只能有一个 constructor.
  • inheritance in ES5: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
  • inheritance: 使用 extends 关键字.
  • Utilities(语法糖): ES6 提供一些语法糖: 1. fat arrow function syntax; 2. template strings;
  • Fat Arrow Function (=>): 是一个缩写来写函数.
  • 在 ES5 中, 如果我们想要使用一个函数作为参数, 我们需要使用 function 关键字和 {}.
  • => 语法的一个重要特性是, 他和包围他的代码使用相同的 this. 这与你在 JS 中创建一个普通函数不同.
  • 通常当你在 JS 中写一个函数时, 这个函数被分配他自己的 this.
  • => 函数是一个很好的方法来清理你的 inline 函数. 他使在JS中使用高阶函数更简单.
  • Template Strings: 在 ES6 中, 新的 template strings 被引入. 两个好的特性: 1. Varaibles within strings. 2. Multi-line strings.
  • Variables in strings: 也叫 string interpolation. 要使用反引号, 不能使用单引号或者双引号.
  • Angualr 2应用由 Component 组成(a tree of components), 一种理解 Component 的方式是教会浏览器新 tag.
  • Angular 2 的 Component 和 Angular 1 的 directive 类似. 同时, Angular 2 也有 directive.
  • component 是 composable.
  • Angular 2 没有指定一个 model library.
  • 读到 Page 76.
TypeScript 比 ES5 多的特性
  • types
  • classes
  • annotations
  • imports
  • language utilities (e.g. desctructuring)

2016-05-26

  • componet decorator: 包含一个 selector, 一个 template.
  • component controller: 由一个 class 定义.
  • component selector: 两种写法. <inventory-app></inventory-app>
<div inventory-app></div>
  • 添加实例到组件里来显示子组件.
  • template binding: {{...}}, 里面不仅仅是一个变量, 是一个表达式.
  • Inputs 和 Outputs: [squareBrackets] 传入 inputs, (parenthesis) 处理 outputs.
  • 数据流入你的组件, 通过 input binding, 事件流出你的组件, 通过 output binding.
  • 可以把 input + output bindings 看做你的组件的 public API.
  • component inputs:
  • Observer pattern: https://en.wikipedia.org/wiki/Observer_pattern

Reading Nginx tutorial

这是一篇无聊的文章, 用于记录自己的阅读进度而已.

文档地址

进度

2016-02-16

2016-02-17

2016-02-19

  • ngx_http_proxy_module: http://nginx.org/en/docs/http/ngx_http_proxy_module.html
  • 不是所有的 Nginx 变量都拥有存放值的容器。拥有值容器的变量在 Nginx 核心中被称为“被索引的”(indexed);反之,则被称为“未索引的”(non-indexed)。
  • 读完 Nginx 变量漫谈(三)
  • 读完 Nginx 变量漫谈 (四)
  • Nginx 变量值容器的生命期是与当前请求相关联的。每个请求都有所有变量值容器的独立副本,只不过当前请求既可以是“主请求”,也可以是“子请求”。即便是父子请求之间,同名变量一般也不会相互干扰。
  • Module ngx_http_auth_request_module: http://nginx.org/en/docs/http/ngx_http_auth_request_module.html
  • Nginx 变量漫谈(五)

2016-02-22

*