Hello,Bucky 代码分析
上一章我们已经成功的把一个完整的bucky应用在云端运行起来了,这一章我们会对这个简单的例子的关键部分进行一些讲解,介绍bucky框架的一些最重要的基础概念。
目录结构
使用bucky命令行工具创建的解决方案(solution)目录的典型结构如下:
├── .bucky
│ └── ...
├── dist
│ ├── bucky
│ │ ├── ...
│ │ ...
│ ├── h5
│ └── wx
├── knowledges.json
├── solution.json
├── src
│ └── account
│ ├── account.js
│ ├── config.json
│ └── onload.js
└── test
└── test_account.js
下面对上面的文件和目录逐一说明:
./solution.json解决方案的配置文件,bucky工具的正确运行需要从这个文件里读取必要的信息。我们鼓励您用bucky工具的配置命令来修改这个文件(其格式也很易读,你可以用任何文本编辑器来编辑)。./knowledges.jsonknowledge是bucky的一个重要的基础概念,你可以把他想象成“系统的全局配置”。bucky工具创建的该文件里已经包含了系统正常运行所依赖的关键配置,你可以在此基础上添加自己的配置。./src/HelloBucky的默认项目(project)目录。一个解决方案(solution)下可以有多个项目(project)方便多人协作开发。 默认情况下bucky的各种build命令都是操作solution里的所有project,如果你有一个特别大的项目,通过划分project可以提高你的开发构建目录。./src/account/一个典型的bucky package目录,包含应用代码。构建bucky应用的主要工作就是编写各种package.后面的会详细的介绍package。./test/用来放各种单元测试代码的目录。我们提倡为每一个package都编写独立的单元测试。./.bucky/隐藏目录:包含用户配置。这个目录不需要同步到任何代码管理系统。./dist/编译输出目录:考虑到javascrpit有多个运行环境要兼容,开发者编写的逻辑代码可能要经过一些编译处理。这个目录包含的是build以后的文件,在运行/调试的时候系统都是加载这里面的文件。由于这个目录的文件都是生成的,所以不应该提交到任何代码管理工具 。修改任何文件后,一定要经过编译的过程(bucky compile)才会生效。我们还在持续提高调试器友好度,有些时候你在调试器里下断点,需要把断点打到这个目录里的文件上。
如果您已经使用过javascript社区的一些脚手架,会对这个结构有“是曾相识”的感觉。未来随着bucky工具的改进,我们会进一步提升整个工具链与社区常见脚手架工具的互操作能力,让大家能自由选择自己书舒服的工具链。在早期测试版由于时间的关系,我们计划先做好non-depends的开发体验。
模块化与应用逻辑
bucky框架设计的一个重要使用体验就是“简洁直观”,在开发应用的过程中,我们希望工程师的主要关注点在业务逻辑上,而不是如何构建环境并处理各种细节。我们回顾一下我们要实现的需求,就是实现两个接口:
- signup会在系统里创建一个账号,如果用户名已经被使用过就会返回失败。
- signin会进行一次账号的登陆验证,如果用户名和密码输入正确就会成功。
在动手实现需求之前,我们要考虑的问题只有两个
- 模块化怎么设计
- 数据保存在什么地方
模块化是软件开发里最朴素的思想,包含了“分层”、“隔离”、“复用”的多种目的,我们这里就不详细介绍如何进行模块化了(大型系统进行正确的模块划分一直是系统架构领域的关键问题)。毫无疑问,这么简单的需求,只需要一个模块就能实现了。我们给这个模块起名叫"account"。
物理上,我们将会创建一个packageID为account的XAR package(简称package). 这个package的包含如下文件:
├── src
│ └── account
│ ├── account.js
│ ├── config.json
│ └── onload.js
其中:
config.jsonpackage的配置文件onload.jspackage的入口脚本,会在package被加载后自动运行一次account.js代码文件,又叫bucky module(简称module),一个package中可以包含多个module.
config.json
XARPackage的config里包含了两部分信息,“package是什么”,以及“package里的代码运行起来依赖什么”。bucky框架会在将要加载一个包时根据这些信息,分配一个合适的Runtime。
{
//package id,一个app里package id不能重复
"packageID": "account",
//package 友好版本
"version": "1.0.0.0",
//package build号,通过package id来加载包一般会加载当前版本,也可以通过指定build号来加载特定版本
"build": 1,
//package 希望在什么样的js环境里运行。
"runtimeType": "pc_server.bucky",
// 描述信息
"meta": {
"desc": ""
},
//依赖的package,在加载当前package之前,depends里的package要处于已加载状态。
"depends": [],
//该XARPackage下导出的模块文件名和对应的相对路径
"modules": {
"account": "account.js"
},
//package需要使用的“driver”,这里我们要使用mysql
"drivers": ["bx.mysql.client"],
//package 依赖的knowledges,在加载成功之前系统会自动完成这些knowledge的同步
//依赖的knowledges必须在解决方案的knowledges.json里有配置过。
"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
}
]
}
需要重点关注的字段有:
runtimeType:格式是 $deviceType.$jsvmType,pc_server.bucky 的意思是在“x86架构的服务器上的bucky容器jsvm里运行”,如果package可以任何runtime里运行(比如一些工具函数组成的package),可以配置成*。参考API References->core.RuntimeType。drivers: package中会使用的“驱动”资源,参考API References->core.DeviceDriver。depends: 依赖的packages。modules: package中定义的模块,包含模块名和模块相对路径。knowledges: package中会使用的knowledge,这些knowledges必须是在解决方案的knowledges.json里配置过。
使用bucky add package命令创建包的时候,会通过交互命令行询问这个package需要依赖的资源。
使用资源
任何代码运行都需要计算资源。传统的编程模型通过启动命令先创建了用于代码执行的进程,然后再通过进程获得当前设备上的各种资源(CPU,内存,磁盘,各种IO设备)。进程也作为单机操作系统里最重要的基础概念成为了一个常识。任何系统架构落到实现层面,都可以用“在哪些机器的哪些进程里运行哪些代码”,来详细说明。
我们认为这个常识并不适合分布式系统(通过这个常识进行分布式系统的架构,需要关注的细节太多了),bucky创建了一种新的分布式系统资源申请与使用的机制:开发直接编写逻辑代码,在代码里直接申请并使用资源,在代码运行的时候,由云内核把代码dispatch到合适的设备上去运行。这就做到了计算拓扑无关性,分布式系统里的各种资源也能更灵活的被使用,调优。
分布式系统中最基础的计算资源是CPU和内存,通过加载package的操作触发,由某个合适的Device提供,最终代码运行在一个合适的Runtime中来使用CPU和内存资源。
分布式系统中的状态持久化需求由Block标准资源实现,但直接使用Block资源的目的是用于开发分布式文件系统等中间件,目前还处于测试验证阶段。当前版本我们把一些常见的中件间以非标准资源的方式提供给应用开发者。系统允许通过驱动来访问这些非标准资源。在这个Demo里,我们使用mysql DB资源来保存账号信息。当应用申请一个mysql资源的时候,系统会根据应用对mysql资源的配置,自动的创建DB,返回给业务代码访问。
配置mysql资源
bucky要求任何mysql DB资源都必须先配置才能使用。对mysql资源的配置是全局配置,因为分布式系统的所有资源,都可能被所有的XARPackage访问。全局配置按照我们的设计,写在knowledges.json中。内容如下
"global.mysql.instances": {
"type": 0,
"object": {
"db_account": {
"schema": "account_db_schema"
}
}
},
"global.mysql.schemas": {
"type": 0,
"object": {
"account_db_schema": {
"onCreate": [
"CREATE TABLE IF NOT EXISTS `users` (`username` VARCHAR( 100 ) NOT NULL ,`password` VARCHAR( 100 ) NOT NULL, `userinfo` VARCHAR( 256 ), UNIQUE KEY (username)) ENGINE=InnoDB DEFAULT CHARSET=utf8;"
]
}
}
},
"global.mysql.configs": {
"type": 0,
"object": {}
}
上面这段配置的含义是:系统里定义了一个id为db_account的mysql db instance,这个DB在首次初始化的时候,会根据account_db_schema的定义,执行一条创建表的sql语句。
应用逻辑
有了上面我们看看signup接口的代码
const DB_INSTANCE_ID = "db_account";
var ErrorCode = bucky.ErrorCode;
function signup(username,password,userinfo,onComplete) {
let thisRuntime = getCurrentRuntime();
let mysqlDriver = thisRuntime.getDriver('bx.mysql.client');
if(userinfo == null) {
userinfo = {};
}
mysqlDriver.getMySQLInstance(DB_INSTANCE_ID, thisRuntime, true, function(ret, mysqlInstance) {
let conn = mysqlInstance.createConnection();
conn.connect((err) => {
if (err) {
BX_WARN("connect to db failed",DB_INSTANCE_ID);
onComplete(ErrorCode.RESULT_DB_OPEN_FAILED);
} else {
let sql = "INSERT INTO users SET username=?, password=?,userinfo=?";
conn.query(sql, [username, password,JSON.stringify(userinfo)], (err, results) => {
conn.end();
if (err == null) {
if (results.affectedRows === 0) {
if (results.code === "ER_DUP_ENTRY") {
onComplete(ErrorCode.RESULT_ALREADY_EXIST);
} else {
onComplete(ErrorCode.RESULT_EXCEPTION);
}
} else if (results.affectedRows === 1) {
onComplete(ErrorCode.RESULT_OK);
}
} else {
BX_WARN("signup failed,username:" + username,err);
onComplete(ErrorCode.RESULT_EXCEPTION);
}
});
}
});
});
}
这段代码重点演示了如何从分布式系统中获取一个mysql DB资源,并使用它。使用mysql DB资源来实现注册逻辑的过程非常单纯,如果您有一定mysql经验相信非常容易看懂,就不详细介绍了。这段代码由于使用了mysql资源,所以在运行的时候,系统会把这段代码放到一个能访问mysql资源的服务器上去运行,自动的实现了微服务化。
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命令行文档。
[TIP: 系统不存在魔法]
系统不存在魔法,是我们多年坚持的理念,通过阅读Proxy的代码,工程师可以分析调试应用系统。而且系统也允许高级开发者根据需要,手工编写自己的Proxy逻辑。
完整的执行流程
- 加载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。
- 如果加载的是
总结
下一章我们会介绍一个复杂一点的聊天室的Demo, 进一步了解bucky的一些概念和提供的基础分布式系统资源。