整体架构介绍
Bucky的定位是下一代分布式操作系统,目标是:
- 让分布式应用系统的开发更简单,开发调试和接近单机
- 不需要处理系统运维(Serverless)
- 根据系统负载自动分配资源,降低系统运行的成本
- 磨平开发应用系统与核心系统的技能障碍
- 使用一种方法开发全平台的优秀UI
- 扩展应用能覆盖的拓扑边界
基本概念介绍
Application
在Bucky的概念里,一个分布式应用系统被称作一个Application.用旧的概念来看,Application包括了 前端的App(iOS,Android,Web)以及后台的各种服务。目前Bucky只涵盖了后端逻辑的开发,未来会在现有的基础上延生对前端的支持。
一个Application的生命周期的长度超越了传统的进程,实际上其生命周期的表达只是一个状态,用来表示系统是否在线。即使系统里没有任何活动的代码,只要系统处于在线状态,那么Application就处于“活”的状态。
大部分的代码在运行的时候都需要知道当前所在的Application,单从开发的角度来说,需要的只是一个用来初始化的appid,以及做某些高权限操作需要的token。
XARPackage
XARPackage是在Bucky中代码的物理组织形式。从属于Application的逻辑代码,都应该放在一个合适的XARPackage中。这反映了程序开发活动中最朴素的模块化的概念。
XARPackage里包含的js文件以module的概念被XARPackage持有。但加载XARPackage除了会默认运行一次onload.js外,并不会加载任何内部的模块。这样可以通过按需加载的方法,提升性能。
XARPackage的config里包含了两部分信息,“package是什么”,以及“package里的代码运行起来依赖什么”。在将要加载一个包时,系统会根据这些信息,分配一个合适的Runtime。
一个XARPackage的典型目录结构如下:
└── account
├── account.js
├── config.json
└── onload.js
其中account是该XARPackage的名字,而下面包含三个典型文件:
config.json是XARPackage的配置文件,定义了该XARPackage的元数据信息、包含的module、依赖的驱动、依赖的其他XARPackage、依赖的knowledges等。onload.js是XARPackage被一个Runtime加载时会运行的初始化代码,默认什么都不做。account.js是该XARPackage的一个module,一个module必须在config.json/modules项里配置。
我们可以看一个config.json的典型配置来初步感受下:
{
// 元数据信息
"packageID": "account",
"build": 1,
"runtimeType": "pc_server.bucky",
"meta": {
"desc": ""
},
// 依赖的其他XARPackage配置
"depends": [],
// 该XARPackage包含module配置
"modules": {
"account": "account.js"
},
// 依赖的驱动(Driver)配置
"drivers": ["bx.mysql.client"],
// 依赖的knowledges配置,参考下文`Knowledge`介绍。
"knowledges": [
...
]
}
module
加载XARPackage成功后,就可以从包中加载代码文件了。包里的代码文件我们称作module。应用开发的日常工作,就是把代码填进各个不同的module里。
一个典型的module的代码如下,基本上和nodejs的module写法一致:
"use strict";
function md5(str,onComplete) {
console.log("md5:" + str);
onComplete(str);
}
function foo(str) {
console.log("foo:" + str);
}
module.exports = {};
module.exports.md5 = md5;
在这个module中只导出了一个接口函数md5,而foo是内部函数,只能在module内部使用。Bucky框架会经常做本地包/远程包的透明调度,所以要求被导出的函数的最后一个参数必须是完成函数,也就是说,导出的函数默认是异步的。
[Remark]: 发布/加载 一个处于开发状态的XARPackage只有被发布到Repository服务器上后才能按正常流程加载。所以在开始正式测试前,请不要忘记运行发布代码。bucky支持
全本地加载模式,能使用常见的nodejs调试器在本地快速的调试后端代码逻辑,极大的提升开发效率。
Runtime
Runtime在Bucky的设计中是一个非常重要的概念,是代码运行的容器,一个容器我们称做一个Runtime Instance。其中RuntimeInstance、Applicaton、XARPackage之间的关系如下:
- 一个Runtime Instance 只属于一个Application。
- 一个处于运行状态的Application,可能会有多个属于该Application的RuntimeInstance。
- 一个RuntimeInstance可以加载同Application的不同XARPackage。
传统的开发模型要求先准备好容器,再加载正确的代码,而Bucky框架的一个核心理念是为代码找到合适的运行容器。我们希望让开发者专注于开发业务逻辑,上传代码到小应用云后,由小应用云的调度器在合适的时机创建合适的Runtime来运行开发者编写的代码 。这样就能让逻辑代码尽可能的与分布式系统的计算拓扑的细节解耦,同时提供近似单机开发一般的分布式系统开发体验。
应用在前端创建的Runtime通常都是client runtime,或则被称作匿名Runtime。在不同的js运行时下初始化client runtime的方法就是各种initCurrentRuntime()。应用开发可以使用全局函数getCurrentRuntime()方法来得到一个当前Runtime Instance。
Runtime从实现上看很像一个虚拟机,由自己独立的沙盒环境(包括Cache,Storage),并且系统提供了接口 pause/resume 一个指定的 Runtime,方便bucky内核在不同的物理设备之间迁移。
Driver
Bucky通过Driver来使用非bucky框架内的功能。一个XARPackage内的代码如需使用某些driver,必须在其配置文件(config.json)中声明,这样bucky的调度器才有机会把该XARPackage调度到正确的Runtime上运行。
加载Driver的代码如下:
let mysqlDriver = getCurrentRuntime().getDriver("bx.mysql.client")
如果当前设备有安装"bx.mysql.client"驱动,那么就能加载成功。目前框架只支持少量的驱动,列表如下:
- bx.mysql.client
- bx.mongodb.client
- bx.redis.client (未开放)
加载XARPackage的流程简介
Runtime最核心的功能就是加载并运行XARPackage,分析这个流程能更有效的理解Bucky的核心概念。
在一个Runtime环境下加载一个XARPackage并调用其模块接口的代码如下:
getCurrentRuntime().loadXARPackage("account",function(thePackage) {
let [errorCode, usermodule] = thePackage.getModule("userinfo");
usermodule.login("uname","pwd", function (result, errorCode) {
console.log("login result is " + result);
});
});
这段代码的实现流程简介如下:
- 加载XARPackage:
- 通过
Repository服务,下载account这个XARPackage的最新版本。 - 读取
account的配置文件config.json,判断当前Runtime是否能直接加载account这个包,如果不能直接加载则:- 加载其代理包
account proxy(proxy包通过框架提供的工具生成)。 - 通过
Repository服务,下载account proxy这个XARPackage的最新版本。
- 加载其代理包
- 通过
- 加载module:
- 读取配置文件config.json,加载依赖项(依赖的包,knowledge)。
- 运行package的onload.js,XARPackage加载成功。
- 开始加载模块,通过config.json里的module表查到模块对应的实现文件。
- 调用module的导出接口:
- 如果加载的是
account包,那么直接加载模块的实现文件,调用并返回结果。 - 如果加载的是
account proxy包,那么login调用实际上会发与小应用云的调度器通信,调度器会在云端创建一个合适的Runtime加载真正的account包,然后再进行PRC。
- 如果加载的是
加载XARPackage再加载其中的一个module也可以直接通过loadModule接口直接调用:
getCurrentRuntime().loadModule("account:userinfo",(errorCode,usermodule)=>{
usermodule.login("uname","pwd", function (result, errorCode) {
console.log("login result is " + result);
});
});
[Remark]: 同步加载 vs 异步加载 我们在设计上原本希望loadModule是同步的,但会导致在浏览器里无法正确实现。如果使用
all in one file的方法进行打包,又会在某些场景下产生额外的开销,所以目前实现的loadModule是异步的。
Proxy与RPC
当Runtime试图加载一个XARPackage时,系统的加载策略如下:
- 判断该原始XARPackage是否适合加载在当前Runtime。
- 如果不适合,bucky的调度器(Scheduler)会尝试寻找一个已经加载了该XARPackage的Runtime(或则创建一个)来加载原始XARPackage。
- 而在当前Runtime里加载的则是一个Proxy XARPackage。
其中,Proxy XARPackage实现了原始 XARPackage中的所有模块和接口,并通过RPC完成对原始包的代理调用:
function signup(...rpc_args) {
const onComplete = rpc_args[rpc_args.length - 1];
bucky.BX_CHECK(typeof onComplete === 'function');
rpc_args.pop();
const thisRuntime = bucky.getCurrentRuntime();
thisRuntime.rpcCall(targetPackageInfo, 'account:account::signup', rpc_args, onComplete);
}
简单说,bucky的调度器(Scheduler)会为不能直接本地运行的目标XARPackage选择一个合适的Runtime加载,然后再发起一个从当前Runtime到目标Runtime的RPC Call。
每一个XARPackage都应该有一个Proxy Package。我们已经在SDK中提供了工具来生成这些Proxy Package。发布代码的时候会自动把包和包的代理都发布到**包仓库(Repository)**上。这个过程都在构建的过程中完成,详细参考工具链->bucky命令行文档。
[Remark]:
系统不存在魔法系统不存在魔法是我们多年坚持的理念,通过阅读Proxy的代码,工程师可以分析调试应用系统。而且系统也允许高级开发者根据需要,手工编写自己的Proxy逻辑。
Knowledge
Knowledge是应用开发过程中需要经常打交道的一个核心概念,也是Bucky的关键设计之一。可以把Knowledge简单的理解成全局配置。当应用开发只读取Knowledge的时候,Knowledge系统也就退化成了一个全局配置系统。
配置knowledge
Knowledge采用key-value设计,配置Knowledge分为两个部分:
- 在应用程序的konwledges.json里配置Knowledge的初始化,定义该应用程序要使用的Knowledge名字、类型、初始值。
- 在应用程序XARPackage的config.json里,配置该XARPackage依赖了哪些Knowledge。
在bucky的应用程序里,一个典型的konwledges.json配置如下,使用bucky命令行工具初始化应用程序时会自动生成默认的konwledges.json文件,参考HelloBucky!
{
"global.events": {
"type": 0,
"object": {}
},
"global.appinfo": {
"type": 0,
"object": {}
},
"global.runtimes": {
"type": 0,
"object": {}
},
"global.loadrules": {
"type": 0,
"object": {}
},
"global.storages": {
"type": 0,
"object": {}
},
"global.mysql.instances": {
"type": 0,
"object": {}
},
"global.mysql.schemas": {
"type": 0,
"object": {}
},
"global.mysql.configs": {
"type": 0,
"object": {}
}
}
该配置文件中配置的是系统预定义的以"global."开头的基础knowledge,因为Bucky的一些基础机制依赖这些knowledge(这也说明Knowledge是框架最底层的概念之一)。
每个XARPackage都应该在自己config.json配置依赖的konwleges,例如典型config.json里配置konwledges如下:
{
...
// 依赖的knowledges配置,参考下文`Knowledge`介绍。
"knowledges": [
{
"key": "global.events",
"type": 0
},
{
"key": "global.runtimes",
"type": 0
},
{
"key": "global.storages",
"type": 0
},
{
"key": "global.mysql.instances",
"type": 0
},
{
"key": "global.mysql.schemas",
"type": 0
},
{
"key": "global.mysql.configs",
"type": 0
}
]
}
[tip]: 配置依赖项 需要注意的是,config.json里只需要配置kowledges的
key、type即可,不必配置对应的值,因为此处是配置依赖的konwledges,而不是配置其初始化。
使用Knowledge
Knowledge采用key-value设计,一个典型的使用Knowledge的代码如下:
let km = getCurrentRuntime().getKnowledgeManager();
km.dependKnowledge("myconfig",NodeInfo.TYPE_OBJECT);
km.ready(function(){
let myconfig = km.getKnowledge("myconfig").objectRead();
let back_color = myconfig["back_color"];
});
可以看到,使用Knowledge需要等待一个异步更新,以便得到系统的最新配置。框架为了简化这个过程,可以在XARPackage的config.json中配置依赖的Knowledge,这样在完成XARPackage加载后,运行应用代码前,框架就已经在当前Runtime里同步好了需要的Knowledge。则在XARPackage的module代码中可以直接获取:
let km = getCurrentRuntime().getKnowledgeManager();
let myconfig = km.getKnowledge("myconfig").objectRead();
let back_color = myconfig["back_color"];
Storage
分布式系统最重要的一个工作就是保存/查询数据。传统上,应用系统架构选型最总要的一件事情就是选择存储引擎。Bucky认为全局状态是被调度器管理的一种核心资源,我们通过调度器分配/迁移这些资源,来实现系统的自动扩容和容错。Bucky通过驱动(Driver)的方式支持传统存储引擎,并正在开发全新的基于Block的分布式存储引擎。传统的存储引擎为旧世界的代码提供便利,而新的分布式存储引擎将是我们解锁核心系统开发难度的利器。
目前Bucky正在支持和即将支持的存储引擎有:
- mysql存储引擎,通过Driver形式提供,包含一些必要的knowledges配置。
- mongo存储引擎,通过Driver形式提供,包含一些必要的knowledges配置。
- redis存储引擎(未开放)。
- 全新的分布式存储引擎,敬请期待!
使用Mysql/mongo等存储的细节不在此处展开,可以通过阅读demo和后续文档了解。
Event
Global Event
Global Event是bukcy框架提供的一个非常有用的组件。实现了一个逻辑上非常常用的功能:
“在系统中定义一个全局事件,然后在任何一段代码中都可以Attach这个事件,在任何一段代码中都可以Fire这个事件。”
在传统的后台开发中,我们常常使用消息中间件来达到类似功能。从过去的经验来看,我们鼓励应用尽量使用端到端的无状态事件系统(即系统允许丢失事件),当然bucky也允许用户开发自己的有状态事件系统。
Global Event的使用也很简单:
let em = getCurrentRuntime().getGlobalEventManager();
let eventCategory = 'myEventCatagory';
let eventID = 'myMessage';
// create an event catagory
em.create(eventCategory, (result) => {
// attach an eventID on the event catagory
console.log(`${eventCategory} is created:${result}`);
em.attach(eventCategory, eventID, (msg) => {
console.log(`${eventCategory}/${eventID} is fired:`, msg);
};
// Fire the eventID ont the event catagory
BaseLib.setOnceTimer(function() {
let msg = JSON.stringify({
cmd:'echo',
body:{
text:'Real-world programming, however, requires care, expertise, and wisdom.',
from:'SICP'
}
});
em.activeEvent(eventCategory, eventID, msg);
},1500);
});
从上述代码中我们可以看出来:
- Global Event的操作接口都是异步的。
- Global Event的由event catagory/eventID两层结构构成,方便用户组织事件的名字空间。
- Global Event的参数是一个字符串,我们一般鼓励在里面填写一个stringfiy后的JSON.
- Global Event是应用程序全局的,所以具有分布式系统在一致性方面的特性:attach成功之后存在与事件控制器失去链接的可能,而系统并不会保证在失去连接的这段时间内产生的事件不会丢失。
System Event
传统的分布式系统通常会在一些主机上通过系统的Cron服务来跑一些定时任务。这类任务从逻辑上和事件很接近, 不同之处在于事件并不由某端用户代码出发,而是通过给系统一个控制命令后,由系统触发。我们把这一类事件 称作System Event。Bucky目前支持的系统事件有两种: 系统定时器(System Timer)和系统任务(System Task)。
参考:系统事件
CallChain
在传统单机程序里,操作系统使用进程/线程模型来区分程序的调用链,在这样的模型里,函数的调用栈(CallStack)无疑是一个十分重要的概念,单线程的程序通过CallStack可以快速跟踪/分析程序的问题。在多线程模式下,CallStack依然发挥着重要的作用。然而,无论是单线程还是多线程模型下,CallStack在应对异步开发模式的时候,其对业务代码逻辑上的调用链条的跟踪调试能力已经捉襟见拙。
例如,在node环境下,虽然单个js文件是在单线程模型下执行,但是大部分代码都是异步链式代码(callback/async-await),此时CallStack能提供的只是一个函数在同步模式下的调用链条,而对于该函数是从哪个地方异步发起的毫无感知。在分布式系统下,业务逻辑的代码的调用范围是整个分布式系统,仅仅依靠CallStack使得问题的诊断难道大幅上升。
[Principle]: Abstraction 软件工程里有许多重要的哲学,其中一个重要的原则是:“任何一个问题,都可以通过增加一个抽象层解决”。事实上,在整个分布式系统的构建里,对资源建立怎样的抽象模型处于核心的地位,有时候,这需要洞见的能力。
Bucky对分布式系统里的调用链问题经过深入分析并设计了CallChain这个核心概念,并将CallChain作为系统底层实现的重要基石:
- 提高开发者/内核开发者诊断问题的效率,内置一种有更基础格式的日志。
- 为分布式系统的调度器提供更丰富的信息。
- 作为分布式系统调试器的基础。
CallChain是对“逻辑上一件事情处理流程的记录”,其中有两个核心问题:
- “逻辑上一件事情”的定义,什么时候开始,什么时候结束。
- 流程中的哪些步骤需要记录?所有的函数调用都要记录?RPC Call 需要记录?还是只有System Call需要记录。
Bucky系统里,任何一个函数都处于某个CallChain下,可以简单理解为每个函数都有一个CallChainID。然后,用一句话描述CallChain的核心能力:
“通过一个CallChain对应的ID,把从客户端发起的调用,在整个分布式系统里执行的逻辑调用链条都串起来。”
从而,我们至少获得了两个十分可观的效益:
- 在日志系统里,通过CallChain的ID,可以把分布式系统里的一次完整逻辑调用链条上的所有日志完整的串起来。
- 在调试系统里,通过CallChain的信息,可以对分布式系统里的一次完整逻辑调用链条做断点调试。
那么,我们只要从3个角度去理解CallChain的生命周期就可以使用CallChain:
- CallChain的产生,例如一个 UI/键盘交互事件,SystemTimer,用户自己创建的新CallChain。
- CallChain的继承,一个函数默认会继承父函数的CallChain。
- CallChain的分裂,Bucky系统或者用户从父CallChain创建的子CallChain。
例如,上一节在knowledges.json里配置SystemTimer时,可以为其配置一个独立的CallChain:
"cc":{
"enable":true,
"appid":"example-app-id",
"ccid":"180cafb0-8043-4662-a27d-cab110ecec0f", // CallChain的ID
"frameid":2
}
事实上,SystemTimer的配置里一般不需要手工设置cc,bucky系统会自动完成这个工作。实际上,CallChain很少会单独使用,通常情况下CallChain都是和日志系统绑定在一起使用的,参考下一节介绍的Bucky LOG系统。
LOG
在分布式系统里,如何打日志是开发严肃项目时必须要考虑的事情。bucky框架提供了内置的日志系统。
BX_TRACE(...);
BX_DEBUG(...);
BX_INFO(...);
BX_WARN(...);
BX_ERROR(...);
BX_CHECK(...);
BX_FATAL(...);
一条典型的blog日志输出如下:
[error],[2017-12-14 16:20:12.264],<@f4346679-a53b-49d1-89f1-a0539ab1dc39@1@2>,rpc call complete with error, pkgid=account, func=account:account::signup, statusCode=502, errcode=13, result= null, node_core.js:10952
其中,下面这个部分是blog内部自动添加的日志头,分别是日志级别、时间、ccid:
[error],[2017-12-14 16:20:12.264],<@f4346679-a53b-49d1-89f1-a0539ab1dc39@1@2>,
可见blog的输出格式如下:
[level],[date],<ccid>,log
默认情况下blog会使用当前函数所在的CallChain的ccid,如果要切换当前blog的ccid,可以使用blog的withcc函数替换:
blog.withcc(cc);
如何创建一个新的CallChain,请参考CallChain的API文档。
高级功能
Bucky除了支持应用系统开发,还计划支持核心系统开发(我们希望能使用bukcy构建出RDBMS),为了达到这个目的,我们还提供了一些分布式系统开发所必须的高级基础设施,包括:
- 统一ID生成
- 全局锁
- Logic Time Vector
- Binlog 写入