mongoose-cli

一直以来mongoose学习都是比较麻烦的,mongoose-cli试图简化学习和测试mongoose部分,并通过app开发流程反思整个过程中业务逻辑部分如何抽象,以期简化开发与分层实现

app开发流程

如图

这里首先从交互图开始,需求统一为交互图。

  • 开发拿到交互图,首先要根据交互【拆分功能点】
  • 根据功能点形成【api文档】
  • 根据功能点和已有model进行【建模】
  • 根据模型,【模拟数据】,并校验模型直到可行
  • 根据功能点+模型,编写api接口

那么,我们看看这个流程里什么是最核心的东西?

  • 功能点
  • 模型

这2点其实是整个app里最核心的部分,即业务部分,我们如果把握住此处的设计,输出【api文档】 + 模型,即可拆分工作任务,WBS

业务建模

  • 避免过度设计,够用就好
  • 如果时间允许就给以后多留点扩展

nodejs + mongodb(mongoose)

根据上面的流程,结合MEAN架构,需要交付

  • api文档
  • mongoose模型

如何简化api文档?

能够根据api文档生成routes和controller部分代码,并且可逆

留空model和service即可。

如何简化model操作?

  • scaffold 脚手架,可以快速完成模型相关crud操作,界面也可以。
  • moa-console 控制台,在命令行即可测试模型方法等
  • mongoose-cli 随时随地,测试model,融合bluebird等promise库,让业务处理更简单
  • 可以把model直接打包发布到npm (TODO)

模型固化成node module的意义

  • 复用,多系统共享model
  • 可以通过xxx@1.0类似的版本,在npm里进行版本限定
  • 耦合低
  • 测试容易
  • 新人培训容易

mongoose-cli

上面是对于业务建模的思考,那么我们如何快速的进行建模,又能不和现有代码耦合呢?

之前说过,业务逻辑,基本就是model + 流程控制,能否直接都集成到一起?

mongoose-cli主要解决的就是这个问题

mongoose best practice

  • mongoose + mongoosedao
  • bluebird

Install

1
[sudo] npm install -g mongoose-cli

Usage

第一步:使用mongoose命令来初始化测试目录结构

1
2
3
4
5
6
➜  d  mongoose
➜ d cd mongoose-console
➜ mongoose-console ls
LICENSE README.md app config db.js example.js index.js node_modules package.json

➜ mongoose-console mc

第二步: 执行mc命令,在moa-console中测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
➜  mongoose-console  mc
提醒:debug状态连接数据库:
mongodb://127.0.0.1:27017/mongoose-console-test

[2015-08-06 20:59:47.378] [INFO] [default] - undefined

[2015-08-06 20:59:47.379] [INFO] [default] - Welcome to the Moa console.
[2015-08-06 20:59:47.380] [INFO] [default] - undefined

Available Entity:
- Bson
- Index
Moa> [mongoose log] Successfully connected to: NaN
mongoose open success

undefined
Moa> .list
Available Entity:
- Bson
- Index
Moa> Bson.find({},function(err,doc){console.log(doc)})
Moa> [ { _id: 55c35575b92da9b4fbeb3b26,
user_name: 'alfred sang',
__v: 0,
created_at: Thu Aug 06 2015 20:39:17 GMT+0800 (CST) },
{ _id: 55c356f4d1b21737ffefb2d4,
user_name: 'alfred sang',
__v: 0,
created_at: Thu Aug 06 2015 20:45:40 GMT+0800 (CST) },
{ _id: 55c356fb12e6f243ffb2c4dd,
user_name: 'alfred sang',
__v: 0,
created_at: Thu Aug 06 2015 20:45:47 GMT+0800 (CST) },
{ _id: 55c35a3fa6474371030783a3,
user_name: 'alfred sang',
__v: 0,
created_at: Thu Aug 06 2015 20:59:43 GMT+0800 (CST) } ]

(^C again to quit)
Moa>

example

1
2
3
4
5
6
7
8
9
10
➜  mongoose-console  node example.js 
提醒:debug状态连接数据库:
mongodb://127.0.0.1:27017/mongoose-console-test
[mongoose log] Successfully connected to: NaN
mongoose open success
{ __v: 0,
user_name: 'alfred sang',
_id: 55c35a3fa6474371030783a3,
created_at: Thu Aug 06 2015 20:59:43 GMT+0800 (CST) }
^C%

全文完

欢迎关注我的公众号【node全栈】

良苦用心几人懂

良苦用心几人懂?

别太拿自己当回事儿

创业,如果放不下架子,放不下面子,你没有机会享受高风险的回报,即使有,你也不配

常常是自己的所谓“为了项目好”,最后一意孤行,闹得所有人都开心,已有的计划无法继续,所有人都失去信心,如果项目黄了,你要承担多少责任?

最怕的是“一条鱼腥了一锅汤”

举个例子,处理jshint配置能用多久呢?如果半天都搞不定,我真心不认为这样的人需要去口口声声的讲,“代码要规范”,连基本功都不过关,谈规范,只是让别人坑而已

大师们尚且虚怀若谷,又何况我们呢?水瓶要么空要么满才不会逛荡,以一个空杯的心态面对世界,才能更好的适应社会。

如果心智成熟,和你的老大好好沟通,降低开发风险,又能对项目有利,你的领导会高看你眼,放心把更重要的工作交给你。

知道什么该学?

大晚上12点,马上要切换测试环境了,我说“在群里说一声,没睡的大伙测一下”

那货说:“大家都睡了,群里沟通成本太高”。。。

可你知道,这背后的真正用意是什么?你只要发了这个信息,老大们都会看到,都会记着你在辛苦的工作,至于结果,有人起来帮忙测试最好,如果没有,至少对自己也是好事儿

有的时候我特别想笑,你以为学东西就是学技术么?

我们再举个例子,别人教你东西,你愿意学或者不愿意学都要听,为啥?如果你觉得他得东西有价值你可以多听点,以后他还会给你分享

如果你直接拒绝,他以后也不会给你分享了。即使你特别不想听,找个借口委婉点也好

有的时候我可以给你讲12分,可你不愿意听,对不起,我讲6分就好了,为啥让自己那么累呢?关键你他妈还不给我钱。。。

学东西不要太表面,技巧性的东西在这个时代已经没有难度了,反正有各种办法查到,只是积累多少的问题的,最难的是他们背后的思考,那是实践后总结出来的真知

我折腾过很多东西,很累,但我习惯了如何快速了解新东西,并能用正确的方式使用他们,这是我的本事,那你呢?即使不会,我可以放下姿态跟人学,那你呢?

你当我是谁?

有的时候我也在想,你当我是谁?领导?还是大哥?

无论哪种都好,至少有那么一些尊重

我并不想美化这个词,但我得承认,尊重是人和人友好的基础

你尊重我,我教你,带你,给你做各种好的打算,我心甘情愿,即使再我最艰难的时候,我都是自己贴钱请大家

如果你因为情绪不好,找我聊,或者大骂我一顿,我都可以接受,如果我做的不好,你也可以找我聊,或者大骂我一顿。

即使我做的再不好,你身为我的小弟,是否要给我留够面子呢?

当着那么多人,与我争执,你是我小弟么?你当我是谁?

你知道谁是真的对你好么?

天天对你笑,恭维你的人,给你涨工资,夸你的人?

还是用心品吧,这世界没有免费的午餐,不会有无端的馅饼落我头上,唯一能表明也是“我还算一个有用的人”,而已

关于推卸责任

我不喜欢这个人代码,我也不喜欢那个人代码,那么你有想过为什么嘛?

我再退一步讲,你觉得别人会喜欢你的代码么?

如果你真的牛逼,你有代码洁癖,把所有的代码都整理的井井有条,制定规范,所有人都觉得好(不一定全用),那么你真的很牛逼,必须提职加薪,想做管理做管理,想当专家当专家,因为这种人是真的人才

当松散2周,一个sprint没有完成,开发要求2天修补bug,我其实想问,你真的好意思要么?这2天修补bug的时间,会导致整个开发计划延后2天,下一个冲刺无法正常完成。。。

形成惯性,以后的计划都无法正常完成。。。。。

我郁闷的不是这2天,而是羞耻心和责任心的缺失

羞耻心和责任心才是进步的动力

如果你真的在这2周很努力的去做了,我觉得这个延期可以理解,但是后面要想办法补上

因为你觉得自己没做好,才会努力去学习,改进,这才是成长的动力

另外你真的补上了,会让别人对你很信任

人和人相处到最后实际上就是尊重和信任,无它

人有脸,树有皮啊,谁能想到我写这段文字的时候,内心是多么的凄凉,可事实真的是真的,有人的人是真的真的没有

别逼着我做决定

我很想:“把工资一结,然后告诉那人,你不配拿我这份尊重!”

忍忍吧,至少现在我还不能这样做,但是早晚有一天,出来混是要还的。

抱怨

这世界上最蠢的事儿就是抱怨,不解决任何事情,又让自己很郁闷,我先反省一下自己的蠢

tomorrow is another day

全文完

欢迎关注我的公众号【node全栈】

从npm tips到express插件机制设计

大部分时间,我们只用到npm的install,init,publish等功能,但它设计的非常好,有很多是我们不了解的

How npm handles the “scripts” field

全局命令

用nodejs来写cli工具是非常爽的,我干了不少这样的事儿

  • kp = kill by port
  • je = json editor
  • mh = start mongo here

核心就是在package.json里配置

"preferGlobal": "true",
"bin": {
  "mh": "index.js"
},

即可

它的原理很简单,就是把这些命令,丢到环境变量里,等于

mh = node /npm_install_path/index.js

如果我没猜错的话是软连接实现

ln -s /bin/mh /npm_install_path/index.js

为什么会知道它的原理呢?因为每次写cli都要发布到npmjs,然后安装,然后测试是否正确,太麻烦,如果使用测试,路径等也比较麻烦

后来发现

npm link

会把开发代码直接在本地完成上面的事儿,爽死了

link之后,会有提示

/Users/sang/.nvm/v0.10.38/bin/nmm -> /Users/sang/.nvm/v0.10.38/lib/node_modules/nmm/index.js
/Users/sang/.nvm/v0.10.38/lib/node_modules/nmm -> /Users/sang/workspace/moa/nmm

如何确认它是软连接呢?

➜  nmm git:(master) ls -alt /Users/sang/.nvm/v0.10.38/bin/nmm
lrwxr-xr-x  1 sang  staff  32 Jul  7 15:38 /Users/sang/.nvm/v0.10.38/bin/nmm -> ../lib/node_modules/nmm/index.js

常见的start,test

一般我喜欢重写start和test命令,比如

"scripts": {
  "start": "nodemon ./bin/www",
  "test": "mocha -u bdd"
},

通过npm start使用nodemon来启动express服务。

通过npm test来跑mocha测试。

无论从语义还是便利性上,都是不错的。

more see https://docs.npmjs.com/cli/start

npm run

但是,npm支持命令就那么多,可能不够用,比如我要测试代码覆盖率

"scripts": {
  "start": "npm publish .",
  "test": "./node_modules/.bin/gulp",
  "mocha": "./node_modules/.bin/mocha -u bdd",
  "cov":"./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
},

很明显没有npm cov命令的,那么怎么办呢?不要急,可以通过npm run-script来搞定

上面的scripts定义,可以这样执行

npm run cov

对于自定义脚本,这样就可以解决这个问题,它的实现原理很简单,但却非常实用。

pre-commit

有的时候我们有这样的需求,在提交代码之前,做一下测试,如果

npm test && git push

这样就太麻烦了,程序员还是应该更懒一点

有没有更简单的办法呢?pre-commit

npm install --save-dev pre-commit

用法是在package.json里增加pre-commit字段,它一个数组

{
  "name": "437464d0899504fb6b7b",
  "version": "0.0.0",
  "description": "ERROR: No README.md file found!",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: I SHOULD FAIL LOLOLOLOLOL \" && exit 1",
    "foo": "echo \"fooo\" && exit 0",
    "bar": "echo \"bar\" && exit 0"
  },
  "pre-commit": [
    "foo",
    "bar",
    "test"
  ]
}

像上面的定义是在 git push之前按顺序执行foo,bar和test,也就是相当于

npm run foo
npm run bar
npm test
git push

install

我们最常用的npm install是把node模块里文件下载安装到node_modules里面,这个很好理解,那么如果我想要自定义安装呢?

以我们上面讲的https://github.com/observing/pre-commit,它是需要先安装pre-commit脚本,这个时候该怎么办呢?

实际上我们可以在scripts自定义install命令的

"install": "node install.js",

npm install pre-commit的时候,它会下载代码,然后他会执行install脚本里的内容。也就是说在install.js里,它可以把想做的事儿做了,脚本也好,编译c扩展也好,都非常简单

再论install

我们一般写模块的时候,首先都是npm init的,然后加大量代码,比如你要加test,你可能还有examples,甚至放大量doc,这些东西,难道让装你这个npm的人都下载么?

想想就是件恐怖的事儿

npm的解决方案和git的方案一下,git是创建.gitignore,npm也照做

touch .npmignore

然后在里面放上想过滤的,不想用户安装时候下载的就好了

比较讨厌的是https://github.com/github/gitignore竟然没有

循环引用

循环引用在ios开发非常常见,即互相引用,导致无法引用计数归零,就没法清理内存,再扯就远了

看npm里,比如a模块依赖b模块,

{
  "name": "A"
  "version": "0.1.2",
  "dependencies": {
    "B": "0.1.2"
  }
}

安装完后

├── node_modules
│   └── B
├── package.json
└── README.md

如果a和b都依赖c呢?

安装后

├── node_modules
│   ├── B
│   │   ├── node_modules
│   │   └── package.json
│   └── C   
├── package.json
└── README.md

这样b能引用c,c就不用安装了

这个问题是node_modules/B/package.json里

{
  "name": "B"
  "version": "0.1.2",
  "dependencies": {
    "C": "0.0.1"
  },
  "scripts": {
    "postinstall": "node ./node_modules/C make"
  }
}

在安装b之后,不会执行c的安装了,主要是路径变量,做法很简单,判断路径即可

// node_modules/B/runMe.js
var deps = ['C'], index = 0;
(function doWeHaveAllDeps() {
  if(index === deps.length) {
    var C = require('C');
    C.make();
    return;
  } else if(isModuleExists(deps[index])) {
    index += 1;
    doWeHaveAllDeps();
  } else {
    setTimeout(doWeHaveAllDeps, 500);
  }
})();

function isModuleExists( name ) {
  try { return !!require.resolve(name); }
  catch(e) { return false }
}

如果想试试,参考http://krasimirtsonev.com/blog/article/Fun-playing-with-npm-dependencies-and-postinstall-script

这个问题并不常见,比较少,但是postinstall确实让人脑洞打开的一个东西

postinstall

如果各位熟悉mongoose的hook,一定会知道pre和post是啥意思,一般来说pre是previos之前的意思,post是之后的意思。

那么postinstall从字面上解,即安装之后要执行的回调。

看一下文档

https://docs.npmjs.com/misc/scripts

它确确实实是安装后的回调,这意味着我们可以借助npm做的更多

先看一下npm还提供了那些回调

  • prepublish: Run BEFORE the package is published. (Also run on local npm install without any arguments.)
  • publish, postpublish: Run AFTER the package is published.
  • preinstall: Run BEFORE the package is installed
  • install, postinstall: Run AFTER the package is installed.
  • preuninstall, uninstall: Run BEFORE the package is uninstalled.
  • postuninstall: Run AFTER the package is uninstalled.
  • preversion, version: Run BEFORE bump the package version.
  • postversion: Run AFTER bump the package version.
  • pretest, test, posttest: Run by the npm test command.
  • prestop, stop, poststop: Run by the npm stop command.
  • prestart, start, poststart: Run by the npm start command.
  • prerestart, restart, postrestart: Run by the npm restart command. Note: npm restart will run the stop and start scripts if no restart script is provided.

擦,太牛逼了,这货考虑的真的太全了,那么下面我们就看看如何利用npm的回调干坏事吧

express插件机制设计

大家都知道express基于connect,有middleware中间件的概念,它本身遵循小而美的设计哲学,导致它非常精简

从express@generator来看,它就只能做点小打小闹的东西,如果要设计一个复杂的大系统,就免不了和代码结构,模块,组件等战斗

从我的角度讲,这些东西都可以理解成是业务插件,比如对于一个框架来说,用户管理就应该像ruby里的devise一样,以一个gem的形式存在,如果代码里引用,调用就好了。

gem + rails plugin机制可以做,那么express + npm也是可以的,但是我们缺少的plugin机制,本文先不讲plugin机制,先说利用npm的回调实现它的可能性

比如在一个boilerplate项目里,我们安装插件

npm install --save moa-plugin-user

安装完成之后,我们需要对项目里的文件或配置也好做一个插件登记,这些东西是否可以放到postinstall里呢?

剩下的就都是nodejs代码了,大家写就好了。

如何学习

https://docs.npmjs.com/

文档虽好,可是不好理解啊,而且有的时候用到了才会看

对于开发而言,代码在手,天下我有,尤其nodejs的模块都是完全开放得,您看不看它都在你的项目目录里,一丝不挂。

编码之外,看看node_modules目录,打开package.json看看,如果发现有不懂的就去查一下文档,这样效果是最好的。

看模块可以挑一些比较好,开源贡献比较多的模块

从别人的代码里学到东西,这应该是最强的学习能力,是长远的,与各位共勉。

全文完

欢迎关注我的公众号【node全栈】

gulp结构化

gulp结构化是一个很大的问题,如果一直在Gulpfile.js上增加,大到一定程度上问题就来了

  • 可读性差
  • 莫名其妙的bug
  • 测试难

有没有比好的实践呢?

pixi

https://github.com/GoodBoyDigital/pixi.js

这个一个非常出名的 HTML 5 2D rendering engine。做游戏和一些微信超炫应用是比较好的一个技术选型。

它自己吹的是“it’s fast. Really fast”。

对于一个开发来说,一定要扒出点好东西才算合格。

它的gulpfile.js

var gulp        = require('gulp'),
    requireDir  = require('require-dir');

// Specify game project paths for tasks.
global.paths = {
    src: './src',
    out: './bin',

    get scripts() { return this.src + '/**/*.js'; },
    get jsEntry() { return this.src + '/index'; }
};

// Require all tasks in gulp/tasks, including subfolders
requireDir('./gulp/tasks', { recurse: true });

// default task
gulp.task('default', ['jshint', 'build']);

代码量很小,而且jshint和build根本没看到,它是怎么加载进来的呢?

requireDir  = require('require-dir');

是根据目录加载的node模块,和我常用的require-directory是一样的功能。

requireDir('./gulp/tasks', { recurse: true });

这就很明显了,看一下gulp目录

➜  gulp git:(master) tree .
.
├── tasks
│   ├── build.js
│   ├── clean.js
│   ├── dev.js
│   ├── jsdoc.js
│   ├── jshint.js
│   ├── scripts.js
│   └── watch.js
└── util
    ├── bundle.js
    ├── handleErrors.js
    ├── jsdoc.conf.json
    └── karma.conf.js

2 directories, 11 files

可以说这是一个比较好的一个gulp实践

mount-tasks

我根据上面pixi的做法,使用require-directory改了一个版本,没几行代码,主要是实现了指定目录,把里面的js加载成gulp 可用的 task。

Install

npm install --save mount-tasks

Usages

在Gulpfile.js里

var gulp        = require('gulp');

// Require all tasks in vendor/tasks, including subfolders
require('mount-tasks')(__dirname + '/tasks')

// default task
gulp.task('default', ['clean', 'build']);

在tasks目录,我们放2个task,结构如下

➜  mount-tasks git:(master) tree tasks
tasks
├── build.js
└── clean.js

0 directories, 2 files

此时,执行gulp,就可以出发clean和build任务了。

我们简单看一下任务是如何定义的,是否足够简单

clean.js里代码(build.js和这个类似)

var gulp    = require('gulp');

gulp.task('clean', function () {
  console.log('clean');
});

是不是足够简单呢?

如果你对gulp不太了解,可以看看这篇文档

https://github.com/streakq/js-tools-best-practice/blob/master/doc/Gulp.md

全文完

欢迎关注我的公众号【node全栈】

mongodb分页优化

mongodb分页很简单,本文主要讲分页可能遇到的问题,以及优化方案

从传统web到移动端api,我们都面临一样的问题,比如ajax get有大小显示等,都会强迫你不得不分页

比如我的项目使用ratchet做h5框架,它的push.js里就是ajax get加载其他页面,页面太大就会报错。

分页说明

以典型的列表api来说:下拉刷新是获取最新信息,然后上拉加载下一页

常见api要写的2个接口

  • get_latest(model,count)
  • get_with_page(number,size)

get_latest一般是取最新的数据,比如我们常见的下拉刷新,一般都是这样的接口的。由于2次下拉之间,可能非常长的时间间隔,所以取到的数据会把当前列表的数据冲掉。

通常做法

  • 如果n(比如n=30s)分钟内有连续请求,提示最近已更新,没必要再刷,或者直接返回当前数据
  • 如果取到新数据,将当前列表的数据冲掉,保证数据一致性

如果判断我到最后一页了

常见的办法是取出总数,除以pagesize,然后判断当前页是否和总页数-1

n = all_count - 1

量少的时候,毫无感觉,如果量大了,你去查一下count(*)是啥后果呢?

所以比较好的做法是按照id去查,前端根据每次返回的数据条数,如果条数等于pagesize,你就可以取下一页数据,相反,如果取到的数据小于pagesize,你就知道没有那么多数据可以取了,即到了尾页。此时只要disable获取下一页的按钮即可。

使用 skip() 和 limit() 实现

//Page 1
db.users.find().limit (10)
//Page 2
db.users.find().skip(10).limit(10)
//Page 3
db.users.find().skip(20).limit(10)
........

抽象一下就是:检索第n页的代码应该是这样的

db.users.find().skip(pagesize*(n-1)).limit(pagesize)

当然,这是假定在你在2次查询之间没有任何数据插入或删除操作,你的系统能么?

当然大部分oltp系统无法确定不更新,所以skip只是个玩具,没太大用

而且skip+limit只适合小量数据,数据一多就卡死,哪怕你再怎么加索引,优化,它的缺陷都那么明显。

如果你要处理大量数据集,你需要考虑别的方案的。

使用 find() 和 limit() 实现

之前用skip()方法没办法更好的处理大规模数据,所以我们得找一个skip的替代方案。

为此我们想平衡查询,就考虑根据文档里有的时间戳或者id

在这个例子中,我们会通过‘_id’来处理(用时间戳也一样,看你设计的时候有没有类似created_at这样的字段)。

‘_id’是mongodb ObjectID类型的,ObjectID 使用12 字节的存储空间,每个字节两位十六进制数字,是一个24 位的字符串,包括timestamp, machined, processid, counter 等。下面会有一节单独讲它是怎么构成的,为啥它是唯一的。

使用_id实现分页的大致思路如下

  1. 在当前页内查出最后1条记录的_id,记为last_id
  2. 把记下来的last_id,作为查询条件,查出大于last_id的记录作为下一页的内容

这样来说,是不是很简单?

代码如下

//Page 1
db.users.find().limit(pageSize);
//Find the id of the last document in this page
last_id = ...

//Page 2
users = db.users.find({'_id'> last_id}). limit(10);
//Update the last id with the id of the last document in this page
last_id = ...

这只是示范代码,我们来看一下在Robomongo 0.8.4客户端里如何写

db.usermodels.find({'_id' :{ "$gt" :ObjectId("55940ae59c39572851075bfd")} }).limit(20).sort({_id:-1})

根据上面接口说明,我们仍然要实现2个接口

  • get_latest(model,count)
  • get_next_page_with_last_id(last_id, size)

为了让大家更好的了解根据‘_id’分页原理,我们有必要去了解ObjectID的组成。

关于 ObjectID组成

前面说了:‘_id’是mongodb ObjectID类型的,它由12位结构组成,包括timestamp, machined, processid, counter 等。

TimeStamp

前 4位是一个unix的时间戳,是一个int类别,我们将上面的例子中的objectid的前4位进行提取“4df2dcec”,然后再将他们安装十六进制 专为十进制:“1307761900”,这个数字就是一个时间戳,为了让效果更佳明显,我们将这个时间戳转换成我们习惯的时间格式

$ date -d ‘1970-01-01 UTC 1307761900 sec’ -u
2011年 06月 11日 星期六 03:11:40 UTC

前 4个字节其实隐藏了文档创建的时间,并且时间戳处在于字符的最前面,这就意味着ObjectId大致会按照插入进行排序,这对于某些方面起到很大作用,如 作为索引提高搜索效率等等。使用时间戳还有一个好处是,某些客户端驱动可以通过ObjectId解析出该记录是何时插入的,这也解答了我们平时快速连续创 建多个Objectid时,会发现前几位数字很少发现变化的现实,因为使用的是当前时间,很多用户担心要对服务器进行时间同步,其实这个时间戳的真实值并 不重要,只要其总不停增加就好。

Machine

接下来的三个字节,就是 2cdcd2 ,这三个字节是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突,这也就是在同一台机器生成的objectid中间的字符串都是一模一样的原因。

pid

上面的Machine是为了确保在不同机器产生的objectid不冲突,而pid就是为了在同一台机器不同的mongodb进程产生了objectid不冲突,接下来的0936两位就是产生objectid的进程标识符。

increment

前面的九个字节是保证了一秒内不同机器不同进程生成objectid不冲突,这后面的三个字节a8b817,是一个自动增加的计数器,用来确保在同一秒内产生的objectid也不会发现冲突,允许256的3次方等于16777216条记录的唯一性。

客户端生成

mongodb产生objectid还有一个更大的优势,就是mongodb可以通过自身的服务来产生objectid,也可以通过客户端的驱动程序来产生,如果你仔细看文档你会感叹,mongodb的设计无处不在的使

用空间换时间的思想,比较objectid是轻量级,但服务端产生也必须开销时间,所以能从服务器转移到客户端驱动程序完成的就尽量的转移,必须将事务扔给客户端来完成,减低服务端的开销,另还有一点原因就是扩展应用层比扩展数据库层要变量得多。

总结

mongodb的ObejctId生产思想在很多方面挺值得我们借鉴的,特别是在大型分布式的开发,如何构建轻量级的生产,如何将生产的负载进行转移,如何以空间换取时间提高生产的最大优化等等。

说这么多的目的就是告诉你:mongodb的_id为啥是唯一的,单机如何唯一,集群中如何唯一,理解了这个就可以了。

性能优化

索引

按照自己的业务需求即可,参见官方文档 http://docs.mongodb.org/manual/core/indexes/

关于explain

rdbms里的执行计划,如果你不了解,那么mongo的explain估计你也不太熟,简单说几句

explain是mongodb提供的一个命令,用来查看查询的过程,以便进行性能优化。

http://docs.mongodb.org/manual/reference/method/cursor.explain/

db.usermodels.find({'_id' :{ "$gt" :ObjectId("55940ae59c39572851075bfd")} }).explain()


/* 0 */
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "xbm-wechat-api.usermodels",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "_id" : {
                "$gt" : ObjectId("55940ae59c39572851075bfd")
            }
        },
        "winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "_id" : 1
                },
                "indexName" : "_id_",
                "isMultiKey" : false,
                "direction" : "forward",
                "indexBounds" : {
                    "_id" : [ 
                        "(ObjectId('55940ae59c39572851075bfd'), ObjectId('ffffffffffffffffffffffff')]"
                    ]
                }
            }
        },
        "rejectedPlans" : []
    },
    "executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 5,
        "executionTimeMillis" : 0,
        "totalKeysExamined" : 5,
        "totalDocsExamined" : 5,
        "executionStages" : {
            "stage" : "FETCH",
            "nReturned" : 5,
            "executionTimeMillisEstimate" : 0,
            "works" : 6,
            "advanced" : 5,
            "needTime" : 0,
            "needFetch" : 0,
            "saveState" : 0,
            "restoreState" : 0,
            "isEOF" : 1,
            "invalidates" : 0,
            "docsExamined" : 5,
            "alreadyHasObj" : 0,
            "inputStage" : {
                "stage" : "IXSCAN",
                "nReturned" : 5,
                "executionTimeMillisEstimate" : 0,
                "works" : 5,
                "advanced" : 5,
                "needTime" : 0,
                "needFetch" : 0,
                "saveState" : 0,
                "restoreState" : 0,
                "isEOF" : 1,
                "invalidates" : 0,
                "keyPattern" : {
                    "_id" : 1
                },
                "indexName" : "_id_",
                "isMultiKey" : false,
                "direction" : "forward",
                "indexBounds" : {
                    "_id" : [ 
                        "(ObjectId('55940ae59c39572851075bfd'), ObjectId('ffffffffffffffffffffffff')]"
                    ]
                },
                "keysExamined" : 5,
                "dupsTested" : 0,
                "dupsDropped" : 0,
                "seenInvalidated" : 0,
                "matchTested" : 0
            }
        },
        "allPlansExecution" : []
    },
    "serverInfo" : {
        "host" : "iZ251uvtr2b",
        "port" : 27017,
        "version" : "3.0.3",
        "gitVersion" : "b40106b36eecd1b4407eb1ad1af6bc60593c6105"
    }
}

字段说明:

queryPlanner.winningPlan.inputStage.stage列显示查询策略

  • IXSCAN表示使用Index 查询
  • COLLSCAN表示使用列查询,也就是一个一个对比过去

cursor中的索引名称移动到了queryPlanner.winningPlan.inputStage.indexName

3.0中使用executionStats.totalDocsExamined来显示总共需要检查的文档数,用以取而代之2.6里的nscanned,即扫描document的行数。

  • nReturned:返回的文档行数
  • needTime:耗时(毫秒)
  • indexBounds:所用的索引

Profiling

另外还有一个Profiling功能

db.setProfilingLevel(2, 20)

profile级别有三种:

  • 0:不开启
  • 1:记录慢命令,默认为大于100ms
  • 2:记录所有命令
  • 3、查询profiling记录

默认记录在system.profile中

db['system.profile'].find()

总结一下

  • explain在写代码阶段就可以做性能分析,开发阶段用
  • profile检测性能慢的语句,便于线上产品问题定位

无论哪种你定位出来问题,解决办法

  • 根据业务,调整schema结构
  • 优化索引

有了上面这些知识,相信大家能够自己去给分页语句测试性能了。

全文完

欢迎关注我的公众号【node全栈】

架构小而美的实践

架构小而美的实践

架构演进

  1. 原始阶段,狂狂堆代码,后来有人骂娘了
  2. 于是开始分层,mvc,甚至dao,service,还有rails里比较比较好的Concerns等
  3. 然后模块化,按功能拆分
  4. 服务化,当功能太多的时候,大家开始探索SOA,把服务挂到ESB上以期一统江湖
  5. 提高资源利用率,虚拟化,云计算的特点之一
  6. 到目前后端基本已经很成熟了,于是开始前端分层,mvc,mvvm等…

我们来审视一下这个演进过程,从刚开始到一个复杂的系统,是每个团队都有经历,甚至复杂到一定程度,连开发都不是知道改怎样去改,太庞大,太复杂了。所以大伙又开始拆分,模块也好,服务也好,都是让它变小,可控,当然这样得益于云计算(后面会讲为什么)。

这里面最重要的变动是从模块化到多个应用并行。避免大而全,而是小而美,如果需要可以建ESB,然后一个门户就可以很好整合这些服务了。

小而美的架构是当下的主流

  • 拆分成多个应用,可以组装,也可以横向扩展
  • 选择简单的技术,将一件事儿做到极致,开发,测试,运维都非常简单,相对而言增加了运维的复杂程度,但有了docker,让我们轻松了许多
  • 避免单点以及其他运维故障,比如数据库锁升级致使瘫痪的解决方法是尽可能分库,降低锁风险,如果每个应用使用自己的库就非常简单了
  • 集成容易
  • 数据方面,分应用,分库,会导致数据分散,难处理。现在以后有比较成熟的技术,比如使用ETL或消息队列MQ延时同步到hadoop平台,数据仓库或者搜索引擎库,利用SQL、MR或者其他技术进行数据处理,如果是实时业务,也问题不大,共享hbase等都可以解决

我们为什么要选择这样的架构?

  • 招聘难

目前为止所有人的反馈都是招聘难,bat无耻的把握了大部分精英资源,中小公司又想以低成本招人,另外还有一点是现在的程序员素质良莠不齐,好像谁找不着工作,去培训3个月就能干了一样。

既然现实已经这样了,那么我们能做啥呢?

  1. 降低开发难度,【哲学:一次只做一件事儿】,会比较简单,也比较容易做好,而且集成、维护成本等都比较低
  2. 增加团队技术的多样性,【哲学:做一件事儿不只有一种方法】,不管是java也好,php也好,ror,node,python,go都好,只要稳定,易于上手的都可以考虑用。比如ruby的sinatra,比如scala的scalatra,比如node的express和koa,比如go的Martini,都非常简单,性能都不错的

我们的最终目标是快速交付,快速上线,以期在商业博弈中占点先手优势,而已。

  • 运维难

既然拆了这么多应用,实际上部署运维的难度是增大的,但是好处也是很明显

  1. 云服务越来越多,降低我们很多的运维成本,比如mysql,cdn,redis等都有现成可用的服务
  2. 利用docker,可以快速部署上万台服务器,甚至更多,另外docker还有一个好处是预置了某种开发环境,只要准备一次配置文件,以后只管创建镜像即可。比如我的scalatra和express运行环境不一样,我只要建2个配置,根据我的业务需求,哪个服务需要加到支持,我就多建一些镜像即可。如果是传统的服务器,想想就想去天台
  • 小步快跑难

技术部门很难的2点:

  • 开发速度
  • 执行速度

假定我们认可agile,无论什么样sprint迭代,我们都要快速交付

核心业务

其实对于多个系统而言,model是最业务核心

多模块

express的好处是可以把app当成子模块,路由可以挂载到当前app上

拆与合

拆的好处

  • 模块化和复用

言必称复用,就好像不谈oo和dp都不好意思说是开发一样。

  • 更多利好

比如

面向服务

语言的意义

这让我想到2个编程语言的哲学

  • There’s More Than One Way To Do It 做一件事儿不只有一种方法
  • Do one thing at a time, and do it well 一次只做一件事,并做到最好!

全文完

欢迎关注我的公众号【node全栈】

Nodejs负载均衡:haproxy,slb以及node-slb

我的线上环境是阿里云,既然阿里云有SLB,比自己运维一个要省事儿的多,事实上,自己做也真不一定做得比它好,本文试图以haproxy来解释一下slb的原理

讲解haproxy的目的是介绍负载算法,便于理解SLB,最后给出node-slb解决方案

目前比较流行的

目前,在线上环境中应用较多的负载均衡器硬件有F5 BIG-IP,软件有LVS,Nginx及HAProxy,高可用软件有Heartbeat. Keepalived

成熟的架构有

  • LVS+Keepalived
  • Nginx+Keepalived
  • HAProxy+keepalived
  • DRBD+Heartbeat

HAProxy

优点

  1. HAProxy是支持虚拟主机的,可以工作在4. 7层(支持多网段);
  2. 能够补充Nginx的一些缺点比如Session的保持,Cookie的引导等工作;
  3. 支持url检测后端的服务器;
  4. 它跟LVS一样,本身仅仅就只是一款负载均衡软件;单纯从效率上来讲HAProxy更会比Nginx有更出色的负载均衡速度,在并发处理上也是优于Nginx的;
  5. HAProxy可以对Mysql读进行负载均衡,对后端的MySQL节点进行检测和负载均衡,不过在后端的MySQL slaves数量超过10台时性能不如LVS;
  6. HAProxy的算法较多,达到8种;

官网 http://www.haproxy.org/ (自备梯子)

我觉得它是所有负载软件里最简单最好用的。配置文件比nginx还简单,而且还有监控页面。

下载最新版软件 http://www.haproxy.org/download/1.5/src/haproxy-1.5.12.tar.gz

解压

tar -zxvf haproxy-1.5.12.tar.gz

切换到目录

cd haproxy-1.5.12 

打开readme看一下,如何安装

make TARGET=linux26
sudo make install

创建一个配置文件

# Simple configuration for an HTTP proxy listening on port 80 on all
# interfaces and forwarding requests to a single backend "servers" with a
# single server "server1" listening on 127.0.0.1:8000
global
    daemon
    maxconn 256

defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend http-in
    bind *:80
    default_backend servers

backend servers
    server server1 127.0.0.1:8000 maxconn 32


# The same configuration defined with a single listen block. Shorter but
# less expressive, especially in HTTP mode.
global
    daemon
    maxconn 256

defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

listen http-in
    bind *:80
    server server1 127.0.0.1:8000 maxconn 32

启动

haproxy -f test.cfg

查看状态

记得在配置文件里加上

listen admin_stats
    bind 0.0.0.0:8888
    stats refresh 30s
    stats uri /stats
    stats realm Haproxy Manager
    stats auth admin:admin
    #stats hide-version

http://ip:8888/stats

负载均衡–调度算法

HAProxy的算法有如下8种:

  • roundrobin,表示简单的轮询,这个不多说,这个是 负载均衡 基本都具备的;
  • static-rr,表示根据权重,建议关注;
  • leastconn,表示最少连接者先处理,建议关注;
  • source,表示根据请求源IP,建议关注;
  • uri,表示根据请求的URI;
  • url_param,表示根据请求的URl参数’balance url_param’ requires an URL parameter name
  • hdr(name),表示根据HTTP请求头来锁定每一次HTTP请求;
  • rdp-cookie(name),表示根据据cookie(name)来锁定并哈希每一次TCP请求。

SLB是神马

负载均衡(Server Load Balancer,简称SLB)是对多台云服务器进行流量分发的负载均衡服务。SLB可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性

SLB是如何实现的

使用tengine实现的。

Tengine是由淘宝网发起的Web服务器项目。它在Nginx的基础上,针对大访问量网站的需求,添加了很多高级功能和特性。

see http://tengine.taobao.org/

SLB用法

创建slb

点击管理按钮,进入实例详情

没啥需要改的,我们直接看服务监听功能,看看如何配置slb

  • 配置端口
  • 转发规则
  • 带宽
  • 健康检查等

点击编辑按钮,此时可以看到具体配置页面

目前slb支持2种转发规则

  • 轮询
  • 最小连接数

轮询应该是和haproxy的roundrobin调度算法一样,表示简单的轮询

最小连接数SLB会自动判断 当前ECS 的established 来判断是否转发

配置完了slb server,下一步要设置具体slb把请求转发给哪台机器,这实际上才是最核心的的配置。

阿里云把这件事儿做的超级简单

假设我现在有一个ecs服务器为已填加

点击【未添加的服务器】,此时会列出未加入负载池的ecs服务器

选中一台服务器

点击批量添加

配置一下权重,如果机器性能一样就配置权重一样,性能越好,权重越大

可选值【0 – 100】

完成配置后,已添加服务器里就有了2台服务器

保证你的服务器都启动,比如2台服务器的80端口都正常即可

此时你需要做的是把你的域名解析到slb服务器的ip地址上

node-slb

an expressjs middleware for aliyun slb

缘起

http://bbs.aliyun.com/read/188736.html?page=1

2)请问健康检查发的什么请求? head 还是 get?
head请求。

如果express路由没有处理head请求的话,会触发其他路由,可能会出现请求重定向死循环

原理

var debug = require('debug')('slb');

module.exports = function (req, res, next) {
  if(req.method.toLowerCase() == 'head'){    
    debug('[ALIYUN.COM LOG]: SLB health checking....OK...');
    return res.sendStatus(200);
  }

  next();
};

原理非常简单:以中间件的形式,处理一下req.method为head的适合,终止此请求即可

安装

npm install --save node-slb

用法

var slb = require('node-slb');

var app = express();
app.user(slb);

测试

首先启动demo的服务

➜  node-slb git:(master) ✗ npm start

> node-slb@1.0.0 start /Users/sang/workspace/github/node-slb
> cd demo && npm install && npm start


> url@0.0.0 start /Users/sang/workspace/github/node-slb/demo
> node ./bin/www

执行test命令,测试请求

➜  node-slb git:(master) ✗ npm test

> node-slb@1.0.0 test /Users/sang/workspace/github/node-slb
> curl -i -X HEAD http://127.0.0.1:3000

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 2
ETag: W/"2-d736d92d"
Date: Mon, 29 Jun 2015 03:46:49 GMT
Connection: keep-alive

此时,观察服务器日志

➜  node-slb git:(master) ✗ npm start

> node-slb@1.0.0 start /Users/sang/workspace/github/node-slb
> cd demo && npm install && npm start


> url@0.0.0 start /Users/sang/workspace/github/node-slb/demo
> DEBUG=slb node ./bin/www

[ALIYUN.COM LOG]: SLB health checking....OK...

如果出现[ALIYUN.COM LOG]: SLB health checking....OK...说明正常。

如果想打印日志,可以DEBUG=slb,如果不想打印日志,默认即无。

总结

  • 首先介绍了haproxy和负载均衡算法
  • 介绍了阿里云slb用法
  • 给出node-slb,一个express中间件

全文完

欢迎关注我的公众号【node全栈】

Nodejs开源项目怎么样写测试、CI和代码测试覆盖率

wx 是一个不错的微信应用框架,接口和网站做的也不错,和wechat-api是类似的项目

群里有人问哪个好

朴灵说:“不写测试的项目都不是好项目”

确实wx目前还没有测试,对于一个开源项目来说,没有测试和代码覆盖率是不完善的,而且从技术选型来说,大多是不敢选的。

那么Nodejs开源项目里怎么样写测试、CI和代码测试覆盖率呢?

测试

目前主流的就bdd和tdd,自己查一下差异

推荐

  • mocha和tape

另外Jasmine也挺有名,angularjs用它,不过挺麻烦的,还有一个选择是qunit,最初是为jquery测试写的,在nodejs里用还是觉得怪怪的。

如果想简单可以tap,它和tape很像,下文会有详细说明

mocha

mocha是tj写的

https://github.com/mochajs/mocha

var assert = require("assert")
describe('truth', function(){
  it('should find the truth', function(){
    assert.equal(1, 1);
  })
})

断言风格,这里默认是assert,推荐使用chaijs这个模块,它提供3种风格

  • Should
  • Expect
  • Assert

rspec里推荐用expect,其实看个人习惯

比较典型一个mocha例子

var assert = require('chai').assert;
var expect = require('chai').expect;
require('chai').should();


describe('Test', function(){
    before(function() {
    // runs before all tests in this block

  })
  after(function(){
    // runs after all tests in this block
  })
  beforeEach(function(){
    // runs before each test in this block
  })
  afterEach(function(){
    // runs after each test in this block
  })

  describe('#test()', function(){
    it('should return ok when test finished', function(done){
      assert.equal('sang_test2', 'sang_test2');
      var foo = 'bar';
      expect(foo).to.equal('bar');
      done()
    })
  })
})

说明

  • 理解测试生命周期
  • 理解bdd测试写法

单元测试需要的各个模块说明

  • mocha(Mocha is a feature-rich JavaScript test framework running on node.js and the browser, making asynchronous testing simple and fun.)
  • chai(Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.)
  • sinon(Standalone test spies, stubs and mocks for JavaScript.)
  • zombie (页面事件模拟Zombie.js is a lightweight framework for testing client-side JavaScript code in a simulated environment. No browser required.)
  • supertest(接口测试 Super-agent driven library for testing node.js HTTP servers using a fluent API)

更多的看http://nodeonly.com/2014/11/24/mongoose-test.html

如果你想真正的玩敏捷,从用户故事开始,那么下面这2个库非常必要

啊,黄瓜。。。。

cucumber作为BDD(行为驱动测试)的自动化测试工具,可以很好的帮助进行功能测试。它将功能拆分为一个个的场景(可以理解为小功能点),每个场景内可以独立的做数据初始,然后再对初始的数据进行测试,检测是否达到预期的效果。

tape:像代码一样跑测试

tape是substack写的测试框架

https://github.com/substack/tape

var test = require('tape').test;
test('equivalence', function(t) {
    t.equal(1, 1, 'these two numbers are equal');
    t.end();
});

tape是非常简单的测试框架,核心价值观是”Tests are code”,所以你可以像代码一样跑测试,

比如

node test/test.js

写个脚本就无比简单了。当然如果你想加’test runner’ 库也有现成的。

The Test Anything Protocol

TAP全称是Test Anything Protocol

它是可靠性测试的一种(tried & true)实现

从1987就有了,有很多语言都实现了。

它说白点就是用贼简单的方式来格式化测试结果,比如

TAP version 13
# equivalence
ok 1 these two numbers are equal

1..1
# tests 1
# pass  1

# ok

比如node里的实现https://github.com/isaacs/node-tap

var tap = require('tap')

// you can test stuff just using the top level object.
// no suites or subtests required.

tap.equal(1, 1, 'check if numbers still work')
tap.notEqual(1, 2, '1 should not equal 2')

// also you can group things into sub-tests.
// Sub-tests will be run in sequential order always,
// so they're great for async things.

tap.test('first stuff', function (t) {
  t.ok(true, 'true is ok')
  t.similar({a: [1,2,3]}, {a: [1,2,3]})
  // call t.end() when you're done
  t.end()
})

一定要区分tap和tape,不要弄混了

科普一下什么是CI

科普一下,CI = Continuous integration 持续集成

Martin Fowler对持续集成是这样定义的:

持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快的开发内聚的软件。

它可以

  • 减少风险
  • 减少重复过程
  • 任何时间、任何地点生成可部署的软件
  • 增强项目的可见性
  • 建立团队对开发产品的信心

要素

1.统一的代码库
2.自动构建
3.自动测试
4.每个人每天都要向代码库主干提交代码
5.每次代码递交后都会在持续集成服务器上触发一次构建
6.保证快速构建
7.模拟生产环境的自动测试
8.每个人都可以很容易的获取最新可执行的应用程序
9.每个人都清楚正在发生的状况
10.自动化的部署

也就是说,测试不通过不能部署,只有提交到服务器上,就可以自动跑测试,测试通过后,就可以部署到服务器上了(注意是”staging”, 而非”production”)。

一般最常的ci软件是jenkins

举个大家熟悉的例子iojs开发中的持续集成就是用的jenkins

https://jenkins-iojs.nodesource.com/

jenkins是自建环境下用的比较多,如果是开源项目,推荐travis-ci

https://travis-ci.org/

对开源项目做持续集成是免费的(非开源的好贵),所以在github集成的基本是最多的。

对nodejs支持的也非常好。

举2个例子

测试报告

近年随着tdd/bdd,开源项目,和敏捷开发的火热,程序员们不再满足说,我贡献了一个开源项目

要有高要求,我要加测试

要有更高要求,我要把每一个函数都测试到,让别人相信我的代码没有任何问题

上一小节讲的ci,实际上解决了反复测试的自动化问题。但是如何看我的程序里的每一个函数都测试了呢?

答案是测试覆盖率

在nodejs里,推荐istanbul

Istanbul - 官方介绍 a JS code coverage tool written in JS

它可以通过3种途径生成覆盖报告

  • cli
  • 代码
  • gulp插件

安装

$ npm install -g istanbul

执行

$ istanbul cover my-test-script.js -- my test args

它会生成./coverage目录,这里面就是测试报告

比如我的项目里

./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly
    #MongooseDao()
      ✓ should return ok when record create
      ✓ should return ok when record delete fixture-user
      ✓ should return ok when record deleteById
      ✓ should return ok when record removeById
      ✓ should return ok when record getById
      ✓ should return ok when record getAll
      ✓ should return ok when record all
      ✓ should return ok when record query


  8 passing (50ms)

=============================================================================
Writing coverage object [/Users/sang/workspace/moa/mongoosedao/coverage/coverage.json]
Writing coverage reports at [/Users/sang/workspace/moa/mongoosedao/coverage]
=============================================================================

=============================== Coverage summary ===============================
Statements   : 47.27% ( 26/55 )
Branches     : 8.33% ( 1/12 )
Functions    : 60% ( 9/15 )
Lines        : 47.27% ( 26/55 )
================================================================================

默认,它会生成coverage.json和Icov.info,如果你想生成html也可以的。

比如说,上面的结果47.27%是我测试覆盖的占比,即55个函数,我的测试里只覆盖了26个。

那么我需要有地方能够展示出来啊

实践

我们以mongoosedao项目为例,介绍一下如何集成测试,ci和测试覆盖率

最终效果如图

npm run

package.json里定义自定义执行脚本

"scripts": {
  "start": "npm publish .",
  "test": "./node_modules/.bin/gulp",
  "mocha": "./node_modules/.bin/mocha -u bdd",
  "cov":"./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
},

除了start和test外,都是自定义任务,其他都要加run命令

npm run mocha
npm run cov

更多见npm-run-test教程

gulp watch

var gulp = require('gulp');
var watch = require('gulp-watch');

var path = 'test/**/*.js';

gulp.task('watch', function() {
  gulp.watch(['test/**/*.js', 'lib/*.js'], ['mocha']);
});

var mocha = require('gulp-mocha');

gulp.task('mocha', function () {
    return gulp.src(path , {read: false})
        // gulp-mocha needs filepaths so you can't have any plugins before it 
        .pipe(mocha({reporter: 'spec'}));
});


gulp.task('default',['mocha', 'watch']);

这样就可以执行gulp的时候,当文件变动,会自动触发mocha测试,简化每次都输入npm test这样的操作。

当然你可以玩更多的gulp,如果不熟悉,参考

创建.travis.yml

项目根目录下,和package.json平级

language: node_js
repo_token: COVERALLS.IO_TOKEN
services: mongodb
node_js:
  - "0.12"
  - "0.11"
  - "0.10"
script: npm run mocha
after_script:
  npm run cov

说明

  • 如果依赖mongo等数据库,一定要写services
  • 把测试覆盖率放到执行测试之后,避免报402错误

在travis-ci.org上,github授权,添加repo都比较简单

添加之后,就可以看到,比如

https://travis-ci.org/moajs/mongoosedao

travis-ci实际上根据github的代码变动进行自动持续构建,但是有的时候它不一定更新,或者说,你需要手动选一下:

点击# 10 passed,这样就可以强制它手动集成了。

其他都很简单,注意替换COVERALLS.IO_TOKEN即可。

创建 .coveralls.yml

https://coveralls.io/是一个代码测试覆盖率的网站,

nodejs下面的代码测试覆盖率,原理是通过istanbul生成测试数据,上传到coveralls网站上,然后以badge的形式展示出来

比如

Coverage Status

具体实践和travis-ci类似,用github账号登陆,然后添加repo,然后在项目根目录下,和package.json平级,增加.coveralls.yml

service_name: travis-pro
repo_token: 99UNur6O7ksBqiwgg1NG1sSFhmu78A0t7

在上,第一次添加repo,显示的是“SET UP COVERALLS”,里面有token,需要放到.coveralls.yml里,

如果成功提交了,就可以看到数据了

在readme.md里增加badge

[![Build Status](https://travis-ci.org/moajs/mongoosedao.png?branch=master)](https://travis-ci.org/moajs/mongoosedao)
[![Coverage Status](https://coveralls.io/repos/moajs/mongoosedao/badge.png)](https://coveralls.io/r/moajs/mongoosedao)

它就会显示如下

Build Status
Coverage Status

另外一种用Makefile的玩法实践

举例:https://github.com/node-webot/wechat-api/blob/master/Makefile

TESTS = test/*.js
REPORTER = spec
TIMEOUT = 20000
ISTANBUL = ./node_modules/.bin/istanbul
MOCHA = ./node_modules/mocha/bin/_mocha
COVERALLS = ./node_modules/coveralls/bin/coveralls.js

test:
    @NODE_ENV=test $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \
        $(MOCHA_OPTS) \
        $(TESTS)

test-cov:
    @$(ISTANBUL) cover --report html $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS)

test-coveralls:
    @$(ISTANBUL) cover --report lcovonly $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS)
    @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID)
    @cat ./coverage/lcov.info | $(COVERALLS) && rm -rf ./coverage

test-all: test test-coveralls

.PHONY: test

我个人更喜欢npm+gulp的写法,总是有一种make是c里古老的东东。。。

总结

本文讲了

  • nodejs里常用框架
    • mocha
    • tape
    • tap
    • 前沿技术:cucumber和vowsjs
  • 科普一下CI
  • 测试报告
    • istanbul
  • 实践
    • gulp + npm run
    • mocha
    • travis-ci
    • coveralls
  • 介绍了基于makefile的另一种玩法

全文完

欢迎关注我的公众号【node全栈】

mongodb运维之副本集实践

忽然发现芋头好鸡贼

正式环境,4台机器+一台定时任务的机器。
服务器是阿里云的ECS,
负载均衡用的是阿里云的SLB,
mysql用阿里云的RDS,
缓存用阿里云的OCS,
运维基本上是都不需要担心了,
现在的云服务已经非常完善了,
其实我们用阿里云的服务非常多,
大概有20多个类型的服务,
感谢阿里云。

而我的技术栈是nodejs + mongodb,而阿里云有k-v兼容redis协议的nosql,无mongodb,所以就要悲剧的自己运维mongodb了。

阿里的ots是非结构化存储,没有nodejs的sdk,就算有,不兼容mongodb,也没啥可玩的。

云服务

MongoDB存储服务的云平台(MongoHQ, MongoLabs 和 Mongo Machine)

国内的貌似只有 http://developer.baidu.com/wiki/index.php?title=docs/cplat/bae/mongodb

芋头推荐用pg,支持json格式存储

还有就是parse和leancloud这类面向api的。

京东和腾讯都有过,后来关闭了,不知何故

mongodb部署最佳实践

常识: replset + shard

replset是副本集,shard是分片

mongoDB的主从模式其实就是一个单副本的应用,没有很好的扩展性和容错性。而副本集具有多个副本保证了容错性,就算一个副本挂掉了还有很多副本存在,并且解决了上面第一个问题“主节点挂掉了,整个集群内会自动切换”。

比如游戏,开了某一个服,那么所有的数据都在一台服务器上,此时它要保证的是服务不挂就可以,不用考虑更多的并发上的压力,那么它首先是副本集。

如果有节点挂了,它会重新选举新的主节点

而更多的情况是,你要考虑并发,而且可能是千万,亿万并发,副本集是搞不定的。

于是shard就登场了。

分片并不是mongo独有的概念,很多数据库都有,mongodb里的分片是指通过mongos来当网关路由,分发请求到每个shard,然后每个shard会对应各自的副本集。

既然是分发请求,就会有一定的性能损耗,但好处是你能处理更多请求。所以按照场景选择

  • 性能最佳,当然是一个副本集,如果能满足需求,优先
  • 如果副本集不足及支撑并发,那么就选shard

准备3台阿里云主机

  • 10.51.83.118
  • 10.51.77.129
  • 10.44.204.241

先各自ping一下,保证网络通畅。

确定我的目标是1主,2从,奇数个

这篇文字讲了Bully算法以及为啥是奇数个

http://www.lanceyan.com/tech/mongodb_repset2.html

注意点

  • 服务器节点之前时间要同步
  • 开启防火墙的一定要允许通过
  • 开启selinux的也要进行设置
  • 建立双击互信模式最好不过

格式化阿里云的新增硬盘

http://www.cnblogs.com/dudu/archive/2012/12/07/aliyun-linux-fdisk.html

然后挂载到/data目录下

配置文件

~/config/r0.config

port=27000
fork=true
logpath=/data/replset/log/r0.log
dbpath=/data/replset/r0
logappend=true
replSet=rs
#keyFile=/data/replset/key/r0

~/config/r1.config

port=27001
fork=true
logpath=/data/replset/log/r1.log
dbpath=/data/replset/r1
logappend=true
replSet=rs
#keyFile=/data/replset/key/r1

~/config/r2.config

port=27002
fork=true
logpath=/data/replset/log/r2.log
dbpath=/data/replset/r2
logappend=true
replSet=rs
#keyFile=/data/replset/key/r2

启动

确保目录为空,杀死所有mongod进程

rm -rf /data/replset/

ps -ef|grep mongod | awk '{print $2}' | xargs kill -9
ps -ef|grep mongod

创建目录

mkdir -p /data/replset/r0
mkdir -p /data/replset/r1
mkdir -p /data/replset/r2
mkdir -p /data/replset/key
mkdir -p /data/replset/log

准备key文件

echo "replset1 key" > /data/replset/key/r0
echo "replset1 key" > /data/replset/key/r1
echo "replset1 key" > /data/replset/key/r2
chmod 600 /data/replset/key/r*  

注意第一次不能用keyFile

mongod -f ~/config/r0.config &
mongod -f ~/config/r1.config &
mongod -f ~/config/r2.config &

配置文件里是fork=true,所以启动需要点时间

初始化

> rs.initiate()  
{
    "info2" : "no configuration explicitly specified -- making one",
    "me" : "iZ25xk7uei1Z:27001",
    "ok" : 1
}

擦,超级慢。。。。

使用下面语句初始化

mongo --port 27000
rs.initiate({ _id:'rs',members:[{ _id:0, host:'10.51.77.129:27000' }]})

这个其实也很慢。。。。

待完成后,继续增加其他2个节点(一定要注意,在rs:PRIMARY即主节点上才能增加rs:SECONDARY和ARBITER。如果之前连的是其他端口,需要切换的。)

rs.add("10.51.77.129:27001")
rs.addArb("10.51.77.129:27002")

查看状态

rs.status();

如果想移除某一个节点

rs.remove("10.51.77.129:27001")
rs.remove("10.51.77.129:27000")
rs.remove("10.51.77.129:27002")

reconfig

如果想删除,重置用rs.reconfig(),这样做不一定会成功,有的时候无法切换到主节点,所以需要,删除/data/replset目录,然后重启所有mongo的进程。

rs.reconfig({ _id:'rs',members:[{ _id:1, host:'10.51.77.129:27000' }]})
rs.add("10.51.77.129:27000")
rs.addArb("10.51.77.129:27002")

db.oplog.rs

rs:PRIMARY> use local
switched to db local
rs:PRIMARY> show collections
me
oplog.rs
startup_log
system.indexes
system.replset
rs:PRIMARY> 
rs:PRIMARY> 
rs:PRIMARY> db.oplog.rs.find()
{ "ts" : Timestamp(1435495192, 1), "h" : NumberLong(0), "v" : 2, "op" : "n", "ns" : "", "o" : { "msg" : "initiating set" } }
{ "ts" : Timestamp(1435495306, 1), "h" : NumberLong(0), "v" : 2, "op" : "n", "ns" : "", "o" : { "msg" : "Reconfig set", "version" : 2 } }
{ "ts" : Timestamp(1435495323, 1), "h" : NumberLong(0), "v" : 2, "op" : "n", "ns" : "", "o" : { "msg" : "Reconfig set", "version" : 3 } }

在SECONDARY节点无法show dbs

主从启动之后,连接slave可以成功连上,但是在slave中执行 show dbs 的时候就报错了:

QUERY    Error: listDatabases failed:{ "note" : "from execCommand", "ok" : 0, "errmsg" : "not master" }

解决方法:

在报错的slave机器上执行 rs.slaveOk()方法即可。

解释一下具体slaveOk方法是什么意思?

Provides a shorthand for the following operation:

db.getMongo().setSlaveOk()
This allows the current connection to allow read operations to run on secondary members. See the readPref() method for more fine-grained control over read preference in the mongo shell.

see

内存问题

查看内存情况最常用的是free命令:

[deploy@iZ25xk7uei1Z config]$ free -m
             total       used       free     shared    buffers     cached
Mem:          7567       6821        745          8        129       6122
-/+ buffers/cache:        569       6997
Swap:            0          0          0

限制内存

所有连接消耗的内存加起来会相当惊人,推荐把Stack设置小一点,比如说1024:

ulimit -s 1024

通过调整内核参数drop_caches也可以释放缓存:

sysctl vm.drop_caches=1

有时候,出于某些原因,你可能想释放掉MongoDB占用的内存,不过前面说了,内存管理工作是由虚拟内存管理器控制的,幸好可以使用MongoDB内置的closeAllDatabases命令达到目的:

mongo> use admin
mongo> db.runCommand({closeAllDatabases:1})

平时可以通过mongo命令行来监控MongoDB的内存使用情况,如下所示:

mongo> db.serverStatus().mem:
{
 "resident" : 22346,
 "virtual" : 1938524,
 "mapped" : 962283
}

还可以通过mongostat命令来监控MongoDB的内存使用情况,如下所示:

shell> mongostat
mapped vsize res faults
 940g 1893g 21.9g 0

其中内存相关字段的含义是:

  • mapped:映射到内存的数据大小
  • visze:占用的虚拟内存大小
  • res:占用的物理内存大小

注:如果操作不能在内存中完成,结果faults列的数值不会是0,视大小可能有性能问题。
在上面的结果中,vsize是mapped的两倍,而mapped等于数据文件的大小,所以说vsize是数据文件的两倍,之所以会这样,是因为本例中,MongoDB开启了journal,需要在内存里多映射一次数据文件,如果关闭journal,则vsize和mapped大致相当。

see

更好的做法是使用docker,一劳永逸

全文完

欢迎关注我的公众号【node全栈】