转载请注明:
原始地址: https://www.jianshu.com/p/9f2fb27062cb
原作者:wonder
1、写在前面
owt-server使用node.js开发,涉及node.js c++混合开发。
owt-server的目录结构如下:
root@ubuntu:/home/wonder/OWT/owt-server-master# ls
build cert doc docker LICENSE node_modules README.md scripts source test third_party
各种环境安装脚本在 scripts/ 下,参考README.md进行编译、安装、运行、测试即可
2、owt-server简要介绍
owt-server是集群式的媒体服务。每种功能模块可以是集群(cluster)的一个工作站(worker),多个worker由中心管理者(manager)管理,管理者有主(master)/备(slave)/候选者(candidate)之分。有些模块可以复用同一个worker。
worker、manager之间通过消息队列进行 任务传递/rpc调用。owt-server使用了node.js中的amqp库模块连接宿主机中运行的rabbitmq,以此作为消息队列的底层实现。
3、clusterManager集群管理者模块概述
查看目录 source/cluster_manager/
root@ubuntu:/home/wonder/OWT/owt-server-master# ls source/cluster_manager/
clusterManager.js cluster_manager.toml dist.json index.js log4js_configuration.json matcher.js package.json scheduler.js strategy.js
其中index.js,为该模块的启动入口。**index.js **代码前段是引用必要的库,重点库有:
1)amqp_client (库实现位于source/common/amqp_client.js):主要用是owt-server的 rpc 封装(利用amqp实现RPC角色的封装定义,如rpcClient、rpcServer等),后文会介绍
2)clusterManager(位于source/cluster_manager/clusterManager.js):主要定义了集群管理中心之管理者(manager)、候选者(candidate)自荐竞选、主(master)/备(slave)、同步、保活等方法。
3.1 clusterManager模块详细分解
clusterManager.js定义了主要的4内部函数变量(var ClusterManager、var runAsSlave、var runAsMaster和var runAsCandidate)和一个导出函数变量(exports.run)
下面就是 clusterManager.js,源码没有注释,笔者在走读源码后根据自己的理解添加了注释。涉及到对专业术语有疑问的,如rpc、主\备\候选者(master/slave/candidate)、以及 js 语法,请自行百度。
推荐两篇介绍消息队列的网址,个人觉得很不错:
【推荐看这篇,全面且有拓展】:[https://www.jianshu.com/p/a4d92d0d7e19](https://www.jianshu.com/p/a4d92d0d7e19)
rabbitmq 对于AMQP的介绍:[https://www.rabbitmq.com/tutorials/amqp-concepts.html#message-acknowledge]
var ClusterManager = function (clusterName, selfId, spec) { //集群管理者定义**
//省略一些定义
......
// var ClusterManager 定义了集群管理中心(manager)的内部函数变量和返回值:**
// 以下为内部函数变量,函数体均省略,请查看源码**
var createScheduler = function (purpose); /*创建某一类任务的调度器(Scheduler,记录\管理\执行)*/
var checkAlive = function (); /*检查该manager 所管理的工作站 (worker)的存活情况*/
var workerJoin = function (purpose, worker, info); /*执行某一类任务的worker加入该manager */
var workerQuit = function (worker); /*一个 worker 从该manager 退出该*/
var keepAlive = function (worker, on_result); /*一个 worker 向该 manager 申请保活*/
var reportState = function (worker, state); /* 该 manager 报告某 worker 的状态*/
var reportLoad = function (worker, load); /*该 manager 报告某 woker 的负载*/
var pickUpTasks = function (worker, tasks); /*令某 worker 执行某些任务*/
var layDownTask = function (worker, task); /*令某 worker 放弃执行某任务*/
var schedule = function (purpose, task, preference, reserveTime, on_ok, on_error); /*对某类型(purpose) 的任务 (task) 按照指定配置 (preference,reserveTime) 分配worker*/
var unschedule = function (worker, task); /*撤销某 worker 下分配的任务*/
var getWorkerAttr = function (worker, on_ok, on_error); /*获取某 worker 的属性*/
var getWorkers = function (purpose, on_ok); /*获取某类 workers*/
var getTasks = function (worker, on_ok); /*获取某 worker 的任务*/
var getScheduled = function (purpose, task, on_ok, on_error); /*获取某类型任务的 worker*/
// 以下为返回值:
var that = {name: clusterName, id: selfId}; /*ClusterManager(...)的返回值that*/
that.getRuntimeData = function (on_data); /*收集该 manager 管理的每类Scheduler、每个 worker 、每个 task*/
that.registerDataUpdate = function (on_updated_data); /*向该 manager 注册消息同步实例*/
that.setRuntimeData = function (data); /*向该 manager 配置 data 中记录的 Scheduler、worker和 task*/
that.setUpdatedData = function (data); /*向该 manager 更新信息,data.type∈{"worker_join "," worker_quit"," worker_state"," worker_load","worker_pickup "," worker_laydown"," scheduled"," unscheduled" },data具体数据结构,请查看源码*/
that.serve = function (monitoringTgt); /*启用 manager 服务,并注册管理目标*/
that.rpcAPI = { /*rpc接口函数,以下函数体均省略,请查看源码。它们与内部函数变量是对应的*/
join: function (purpose, worker, info, callback) { ...},
quit: function (worker) { ...},
keepAlive: function (worker, callback) { ...},
reportState: function (worker, state) { ...},
reportLoad: function (worker, load) { ...},
pickUpTasks: function (worker, tasks) { ...},
layDownTask: function (worker, task) { ...},
schedule: function (purpose, task, preference, reserveTime, callback) { ...},
unschedule: function (worker, task) { ...},
getWorkerAttr: function (worker, callback) { ...},
getWorkers: function (purpose, callback) { ...},
getTasks: function (worker, callback) { ...},
getScheduled: function (purpose, task, callback) { ...}
}
}
var runAsSlave= function(topicChannel, manager) { //集群管理者作为 “备份”(salve) 的身份运行**
//省略一些定义
......
**//以下为内部函数变量,函数体均省略,请查看源码**
var requestRuntimeData = function (); /*向首要集群管理者 (master) 请求运行期间的数据,数据内容参阅上文var ClusterManager 返回值中的 getRuntimeData 函数*/
var onTopicMessage = function(message); /*接收到主题消息时的处理函数,message.type∈{ “runtimeData”,“updateData”,“declareMaster” },分别对应着 “收到运行期间的数据”,“收到数据更新”,“收到 master 的角色申明 ” */
var superviseMaster = function (); /*监督 master 的定时任务(30ms检查一次),若当前master失联(matster心跳超时大于2次),则该 salve 将进入候选者身份(candidate) 的状态*/
**//以下为调用 runAsSlave 将执行的函数体**
topicChannel.subscribe( //在指定的主题信道下(基于消息队列) 订阅两种主题消息
['clusterManager.slave.#', 'clusterManager.*.' + manager.id] , //两种主题的关键id
onTopicMessage , //主题消息处理函数
function () { //订阅成功后,执行的函数体
requestRuntimeData(); //向 master 请求运行期间的数据
superviseMaster(); //监督 master
}
)
**}**
**var runAsMaster = function(topicChannel, manager) {**
//省略一些定义
......
topicChannel.bus.asRpcServer( //启用远程调用服务
manager.name, //master 名称
manager.rpcAPI, //master的rpc接口
function(rpcSvr) { //rpc服务启用成功后执行的函数体
topicChannel.bus.asMonitoringTarget(function(monitoringTgt) { //启用worker监管服务,主要用于在worker远程调用登出master时的消息回传,可类比为消息确认ACK
manager.serve(monitoringTgt); //启用 master 集群管理服务
setInterval( //设置定时器
function () { // 向消息队列的三种主题 发送 “declareMaster ”消息
topicChannel.publish( //主题 'clusterManager.slave' 消息
'clusterManager.slave',
{type: 'declareMaster', data: {id: manager.id, life_time: life_time}}
);
topicChannel.publish( //主题 'clusterManager.candidate' 消息
'clusterManager.candidate',
{type: 'declareMaster', data: {id: manager.id, life_time: life_time}}
);
topicChannel.publish( //主题 'clusterManager.master' 消息
'clusterManager.master',
{type: 'declareMaster', data: {id: manager.id, life_time: life_time}}
);
},
20 //时间间隔20ms
);
var onTopicMessage = function (message); //消息处理函数
topicChannel.subscribe( //订阅主题消息
['clusterManager.master.#', 'clusterManager.*.' + manager.id],
onTopicMessage, //消息处理函数
function () { //订阅成功执行的函数体
manager.registerDataUpdate( //注册通知slave的具体方法
topicChannel.publish( //通过消息队列发送主题为 'clusterManager.slave' 的消息
'clusterManager.slave',
{type: 'updateData', data: data}
)
);
}
);
},
function(reason) { process.exit();}; //asMonitoringTarget 失败
},
function(reason) {process.exit(); }; //as RPC server 失败
}
}
}
var runAsCandidate = function(topicChannel, manager) {
//省略一些定义
......
var electMaster = function () { ...} //该候选者决定自身身份:是 master 还是 slave
var selfRecommend = function () { ...} //该候选者自荐,每30ms向消息队列发送'clusterManager.candidate' 主题消息“selfRecommend”
var onTopicMessage = function (message) { ...} //消息处理函数。初始化后定时160ms决定自身身份;收到“selfRecommend”,若消息中id大于自身id,放弃晋升master;收到“declareMaster”,停止自荐,清除定时,成为slave身份
topicChannel.subscribe( //订阅 “clusterManager.candidate.#”主题消息
['clusterManager.candidate.#'],
onTopicMessage, //消息处理函数
function () {
selfRecommend(); //订阅成功,该参与者开始自荐
}
);
}
exports.run= function (topicChannel, clusterName, id, spec) {
//该js库的导出函数
var manager = new ClusterManager(clusterName, id, spec); //生成一个集群管理者(manager)实例
runAsCandidate(topicChannel, manager); //该manager立即作为候选者(candiate)运行
}
思考:主/备方式的好处,在于:一定程度减少了中心式集群管理的风险,即中心管理者宕机造成集群失效的风险。其缺点也是存在的,即调度集中于主管理者,主管理者仅于备管理者进行同步,在调度请求非常频繁时,主管理者性能会成为瓶颈,这也是中心式网络应用的通病。
阅读clusterManager.js文件的总结:
a) 通过源码走读,可以明确管理者(manager)、主(master)/备(slave)/候选者(candidate)的分工以及竞选方式。
b) 这部分仅是对集群管理者(cluster_manager)的定义,对于集群工作站(worker)的定义还没有概念。目前仅知道,master暴露了一下rpc接口供调用。
c) 这部分对rpc的调用是比较高层次的,确实在owt-server的代码中,amqp_client.js文件对node.js中的amqp进行了封装,以amqp为基础实现了底层的消息收发、通知机制。
d) 这部分提到了Scheduler,它是作为某种类型的任务的管理器,供clusterManager.js使用的。它内部实现了task的记录、worker的记录、超时管理、task与worker的关联、task和worker的调度分配细节。对于记录、关联、超时管理等功能,下文不做详细描述,因为相关的接口基本与clusterManager.js 文件中 var ClusterManager 提供的接口一致。
因此,下文将仅对scheduler.js中的任务调度部分做详细分解。
( 消息队列重要文件amqp_client.js将在下一篇《owt-server 的集群管理、集群工作站、消息队列(二)》进行分解; 集群工作站(worker)将在《owt-server 的集群管理、集群工作站、消息队列(三)》结合具体应用类型进行分解)
3.2 scheduler模块---任务调度部分详细分解
话不多说,上干货。
首先,放两个scheduler 模块---任务调度部分需要使用的模块。
1) strategy.js
调度策略模块,描述了不同的调度准则:最近使用、最常使用、最少使用、roundRobin(轮询)、随机选取
这里贴两个(最常使用、roundRobin)进行说明
var mostUsed = function () {
this.allocate = function (workers, candidates, on_ok, on_error) { //获取该策略选中的某个 work 在 workers 中的标号,candidates中存放标号
var most = 0, found = undefined;
for (var i in candidates) { //在提前筛选出的候选candidates中搜索,(提前筛选好处是缩小策略算法运算的空间范围)
var id = candidates[i];
if (workers[id].load >= most) { //检查id所对应work的负载,选取最大负载的work的标号
most = workers[id].load;
found = id;
}
}
on_ok(found); //回调选中的标号
};};
var roundRobin = function () {
var latest_used = 65536 * 65536;
this.allocate = function (workers, candidates, on_ok, on_error) {
var i = candidates.indexOf(latest_used); //初始返回-1
if (i === -1) {
latest_used = candidates[0]; //初始选第一个候选
} else {
latest_used = (i === candidates.length - 1) ? candidates[0] : candidates[i + 1]; //选择下一个candidates[xxx]中存放的标号
}
on_ok(latest_used); //回调选中的标号
};};
2) matcher.js
条件匹配模块,描述了不同类型work的匹配准则。owt-server提供了多种类型的服务:portal、webrtc、video、audio、analytics、conference、recording、streaming。其中有些服务需要有独特的任务task和工作站worker的匹配准则。
举两个栗子(上干货):
var webrtcMatcher = function () {
this.match = function (preference, workers, candidates) { //参数1是配置喜好
var result = [],
found_sweet = false; //找到甜心?!?!?!?!!!,源码作者有点意思的(奸笑~)
for (var i in candidates) {
var id = candidates[i];
var capacity = workers[id].info.capacity; //每个worker在向master登记时,都会把自身能力带上
if (is_isp_applicable(capacity.isps, preference.isp)) { //这个isp是什么作用还不清楚,直译是“运营商”?懂的朋友可以交流一下
if (is_region_suited(capacity.regions, preference.region)) { //这个region也不太明确,根据字面直觉上和域控相关
if (!found_sweet) {
found_sweet = true;
result = [id];
} else {
result.push(id);
}
} else { //不在region里,并且没有找到甜心,强行指定甜心吗?有点迷
if (!found_sweet) {
result.push(id);
}
}
}
}
return result;
};};
var videoMatcher = function () {
this.match = function (preference, workers, candidates) {
if (!preference || !preference.video)
return candidates;
var formatContain = function (listA, listB) { //函数,统计B在A中的数量
var count = 0;
listB.forEach((fmtB) => {
if (listA.indexOf(fmtB) > -1)
count++;
});
return (count === listB.length);
};
var result = candidates.filter(function(cid) { //筛选结果
var capacity = workers[cid].info.capacity;
var encodeOk = false;
var decodeOk = false;
if (capacity.video) {
encodeOk = formatContain(capacity.video.encode, preference.video.encode); //判断偏好的视频编码器是否在worker的能力中
decodeOk = formatContain(capacity.video.decode, preference.video.decode); //判断偏好的视频解码器是否在worker的能力中
}
if (!encodeOk) { //编码不匹配
log.warn('No available workers for encoding:', JSON.stringify(preference.video.encode));
}
if (!decodeOk) { //解码不匹配
log.warn('No available workers for decoding:', JSON.stringify(preference.video.decode));
}
return (encodeOk && decodeOk); //编解码都匹配才行嘛!
});
return result;
};};
终于,
3)scheduler.js
代码不多,就是淦~
exports.Scheduler = function(spec) {
/*State <- [0 | 1 | 2]*/ //官方注释最为致命,这种魔数是看代码最大的障碍之一,尤其是表示状态的魔数
/*{WorkerId: {state: State, load: Number, info: info, tasks:[Task]}*/ // Scheduler 中worker表中的属性,以及tasks表属性
var workers = {};
/*{Task: {reserve_timer: TimerId,
reserve_time: Number,
worker: WorkerId} }*/
var tasks = {};
var matcher = Matcher.create(spec.purpose), //根据指定的应用类型名称创建对应matcher
strategy = Strategy.create(spec.strategy), //根据指定的策略创建对应matcher
schedule_reserve_time = spec.scheduleReserveTime;
that.schedule = function (task, preference, reserveTime, on_ok, on_error) { //参数1是需要调度的任务编号,参数2是该任务的偏好配置
if (tasks[task]) { //该任务编号在处理记录中
var newReserveTime = reserveTime && tasks[task].reserve_time < reserveTime ? reserveTime : tasks[task].reserve_time, //更新保留时长
worker = tasks[task].worker; //正在处理该 task 的 worker
if (workers[worker]) { // worker还在记录中
if (isTaskInExecution(task, worker)) { //该任务正在执行
tasks[task].reserve_time = newReserveTime; //更新任务保留时长
} else { //任务没在执行
reserveWorkerForTask(task, worker, newReserveTime); //向worker申请该task执行时长
}
return on_ok(worker, workers[worker].info); //回调指定的 worker 和 它的信息,并返回
} else { //如果 worker 没了
repealTask(task); //清理该任务记录,准备重新分派
}
}
var candidates = [];
for (var worker in workers) {
if (isWorkerAvailable(workers[worker])) { //衡量worker负载和状态
candidates.push(worker); //加入候选
}
}
if (candidates.length < 1) {
return on_error('No worker available, all in full load.');
}
candidates = matcher.match(preference, workers, candidates); //matcher它来了,基于任务偏好、matcher类型筛选候选者
if (candidates.length < 1) {
return on_error('No worker matches the preference.');
} else {
strategy.allocate(workers, candidates, function (worker) { //strategy它来了,基于strategy 策略选择合适的 worker
reserveWorkerForTask(task, worker, (reserveTime && reserveTime > 0 ? reserveTime : schedule_reserve_time)); //为该task分配记录
on_ok(worker, workers[worker].info); //回调
}, on_error);
}
};
}
总结:scheduler 使用了策略方式,结合strategy 和matcher 模块,将不同配置对应的策略与策略的执行进行解耦,方便扩展,是良好设计模式的体现。
3.3 index.js --- cluster_manager 模块入口
在节3中,提到了该模块下的index.js是入口程序,下面简单介绍一下,以形成模块到进程(程序)的概念。
首先是引用的库
var amqper = require('./amqp_client')); //amqp封装模块
var logger = require('./logger').logger; //日志
var log = logger.getLogger('Main');
var ClusterManager = require('./clusterManager'); //cluster_manager模块
var toml = require('toml'); //配置文件模块
var fs = require('fs'); //文件系统模块
其次,配置文件读取、配置设置
var config;
try {
config = toml.parse(fs.readFileSync('./cluster_manager.toml')); //可以读一下配置文件,更加清晰
} catch (e) {
log.error('Parsing config error on line ' + e.line + ', column ' + e.column + ': ' + e.message);
process.exit(1);
}
config.manager = config.manager || {}; //manager配置
config.manager.name = config.manager.name || 'owt-cluster'; //manager名字
config.manager.initial_time = config.manager.initial_time || 10 * 1000; //启动时间
config.manager.check_alive_interval = config.manager.check_alive_interval || 1000; //manager 检查 worker 失联的时间间隔
config.manager.check_alive_count = config.manager.check_alive_count || 10; //manager 剔除失联 worker 前的最大检查次数
config.manager.schedule_reserve_time = config.manager.schedule_reserve_time || 60 * 1000; //调度默认保留时间(仅当调度请求没有该字段时)
config.strategy = config.strategy || {}; //调度策略, 以下为各种应用类型的默认调度策略
config.strategy.general = config.strategy.general || 'round-robin';
config.strategy.portal = config.strategy.portal || 'last-used';
config.strategy.conference = config.strategy.conference || 'last-used';
config.strategy.webrtc = config.strategy.webrtc || 'last-used';
config.strategy.sip = config.strategy.sip || 'round-robin';
config.strategy.streaming = config.strategy.streaming || 'round-robin';
config.strategy.recording = config.strategy.recording || 'randomly-pick';
config.strategy.audio = config.strategy.audio || 'most-used';
config.strategy.video = config.strategy.video || 'least-used';
config.strategy.analytics = config.strategy.analytics || 'least-used';
config.rabbit = config.rabbit || {}; //rabbitmq配置
config.rabbit.host = config.rabbit.host || 'localhost'; //rabbitmq地址
config.rabbit.port = config.rabbit.port || 5672; //rabbitmq端口
最后,
function startup () {
var enableService = function () {
var id = Math.floor(Math.random() * 1000000000); //生成随机id
var spec = {initialTime: config.manager.initial_time,
checkAlivePeriod: config.manager.check_alive_interval,
checkAliveCount: config.manager.check_alive_count,
scheduleKeepTime: config.manager.schedule_reserve_time,
strategy: config.strategy
};
amqper.asTopicParticipant(config.manager.name + '.management', function(channel) { //利用amqp封装库加入主题,得到句柄channel
log.info('Cluster manager up! id:', id);
ClusterManager.run(channel, config.manager.name, id, spec); //使用配置、随机、句柄,启动ClusterManager
}, function(reason) {
log.error('Cluster manager initializing failed, reason:', reason);
process.exit();
});
};
amqper.connect(config.rabbit, function () { //amqp封装库连接rabbitmq消息队列
enableService(); //启动上述服务
}, function(reason) {
log.error('Cluster manager connect to rabbitMQ server failed, reason:', reason);
process.exit();
});
}
startup(); //启动
... //省略其他系统信号设置