V8的Hash冲突问题retweet

上个月,在28th Chaos Communication Congress(混乱通信大会?)上有两人发布了一个比较大的属于语言级别的安全问题,Hash Collision Dos攻击。受影响的语言包括:

Java, 所有版本
JRuby <= 1.6.5
PHP <= 5.3.8, <= 5.4.0RC3
Python, 所有版本
Rubinius, 所有版本
Ruby <= 1.8.7-p356
Apache Geronimo, 所有版本
Apache Tomcat <= 5.5.34, <= 6.0.34, <= 7.0.22
Oracle Glassfish <= 3.1.1
Jetty, 所有版本
Plone, 所有版本
Rack <= 1.3.5, <= 1.2.4, <= 1.1.2
V8 JavaScript Engine, 所有版本

视频:Effective Denial of Service attacks against web application platforms 关于修复这个问题的更新,请看 http://www.ocert.org/advisories/ocert-2011-003.html

我们知道Hash表是通过Hash算法散列到一个数组里面。如果Hash算法足够散列的话,我们取一个数据时只要通过这个Hash算法很快地找到相对应地值。

hash('a' )-> 1
hash('b') -> 2
hash('c') -> 3
....

但是我们如果知道这个Hash的算法,并制造一个包含N个值的key,让其Hash出来的值都是一样的话,于是就出现了一个单向链表,那么O(1)的搜索算法复杂度就成了O(n),而插入N个数据的算法复杂度就成了O(n^2)

hash('a' )-> hash('b' )-> hash('c' )-> 1

比如PHP 5.3地Hash算法是这样的:

  1. long zend_inline_hash_func(char *arKey, int nKeyLength)
  2. {
  3.         long $h = 5381;
  4.         char *arEnd = arKey + nKeyLength;
  5.  
  6.         while (arKey < arEnd) {
  7.                 $h += ($h << 5);
  8.                 $h += (long) *arKey++;
  9.         }
  10.         return $h;
  11. }

那么 hash(‘Ez’) = pow(33,1) * 69 + pow(32,0) * 122 = pow(33,1) * 70 + pow(33,0) * 89 = 2399 = hash(‘FY’),于是Hash冲突就产生了。如果构建一个key稍微大一点的表单,比如2000个key,然后POST到服务器。不停地提交这个表单,一台普通机器就能把Server搞垮。

我们来说说V8中Hash,如果Object的key是String时采用下面的Hash算法:

  1. uint32_t v8_hash(char *str, uint32_t length){
  2.   uint32_t hash = 0, i=0;
  3.  
  4.   for(;i<length;i++){
  5.     hash += str[i];
  6.     hash += (hash << 10);
  7.     hash ^= (hash >> 6);
  8.   }
  9.   return hash;
  10. }

可通过一定的算法把这个Hash算法逆反而生成N个值的Hash(key)后都一样的值,使其成为一个单向链表。使用V8引擎的浏览器,比如Chrome的用户点击这个链接就可看到小实例,600个key在使用冲突的key和不冲突的key而花费的时间上的对比。

V8现在做为Node.js的Javascript的引擎用于Server端,不可避免地遭遇到这个问题。所幸地是最新版本地0.6.7已经更新了此问题,但是对于用整形来做key的Hash表还是能产生冲突的,整形的Hash是另外一个算法。感谢淘宝的数据团队 @玄了个澄 写了一个解决整形Hash算法的patch

如何使用NPM来管理你的依赖retweet

本文已经首发于InfoQ中文站 ,版权所有,原文地址为http://www.infoq.com/cn/articles/msh-using-npm-manage-node.js-dependence,如需转载,请务必附带本声明,谢谢。

InfoQ中文站是一个面向中高端技术人员的在线独立社区,为语言&开发、架构&设计、过程&实践、运维&基础架构、企业架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、免费迷你书下载如《架构师 》等。

======================华丽的分割线========================

npm 是 Node.js 的模块依赖管理工具。作为开发者使用的工具,主要解决开发 Node.js 时会遇到的问题。如同 RubyGems 对于 Ruby 开发者和 Maven 对于 Java 开发者的重要性,npm 对与 Node.js 的开发者和社区的重要性不言而喻。本文档包括五点:package.jsonnpm 的配置npm install 命令npm link 命令其它 npm 命令

package.json

npm命令运行时会读取当前目录的 package.json 文件和解释这个文件,这个文件基于 Packages/1.1 规范。在这个文件里你可以定义你的应用名称( name )、应用描述( description )、关键字( keywords )、版本号( version )、应用的配置项( config )、主页( homepage )、作者( author )、资源仓库地址( repositor )、bug的提交地址( bugs ),授权方式( licenses )、目录( directories )、应用入口文件( main )、命令行文件( bin )、应用依赖模块( dependencies )、开发环境依赖模块( devDependencies )、运行引擎( engines )和脚本( scripts )等。

对于开发者而言,开发和发布模块都依赖于他对这个文件 package.json 所包含的意义的正确理解。我们下面用一个本文共用的例子来说明:

{
  1.   "name": "test",
  2.   "version": "0.1.0",
  3.   "description": "A testing package",
  4.   "author": "A messed author &lt;messed@example.com&gt;",
  5.   "dependencies": {
  6.     "express": "1.x.x",
  7.     "ejs": "0.4.2",
  8.     "redis": "&gt;= 0.6.7"
  9.   },
  10.   "devDependencies" : {
  11.     "vows": "0.5.x"
  12.   },
  13.   "main":"index",
  14.   "bin" : { "test" : "./bin/test.js"},
  15.   "scripts" : {
  16.     "start" : "node server.js",
  17.     "test" : "vows test/*.js",
  18.     "preinstall" : "./configure",
  19.     "install" : "make &amp;&amp; make install"
  20.   },
  21.   "engines": {
  22.     "node": "0.4.x"
  23.   }
  24. }

这个例子里我们定义了应用的入口文件( main )为 index ,当其他应用引用了我们的模块 require('test') 时,这个 main 的值 index.js 文件被调用。脚本( scripts )使用hash 表定义了几个不同的命令。script.start 里的定义的 node server.js 会在 npm start 时被调用,同样的 npm test 调用时对应的 scripts.test 里定义的命令被调用。在有些 native 模块需要编译的话,我们可以定义预编译和编译的命令。本例中还定义了应用依赖模块( dependencies )和开发环境依赖模块( devDependencies )。应用依赖模块会在安装时安装到当前模块的 node_modules 目录下。开发环境依赖模块主要时在开发环境中用到的依赖模块,用命令 npm 的命令 install 或 link 加上参数 —dev 安装到当前模块的 node_modules 目录下。

大家也注意到 package.json 里的版本号有些是 >= 0.6.7 有些是 1.x.x,这有什么区别?npm 使用于语义化的版本识别来进行版本管理。并不是所有的模块都会提供向后兼容性,有时候某些模块因为某些原因导致不向后兼容。所以我们需要定义一些规则来保证模块能够在某些特定的版本中可用,并且保证能用最新的版本,因为那些版本总是修改了一些 bug 或提升了性能等。我们来看一下版本定义的字段:

0.4.2
  • 主版本( 0 )
  • 副版本( 4 )
  • 补丁版本( 2 )

在上面 package.json 的定义里我们确信模块在所有的 Nodejs 0.4及以上和0.5以下版本里都能运行。依赖模块 redis 在所有大于或等于0.6.7的版本上都能运行,依赖模块 ejs 只能确保运行在0.4.2版本里,依赖模块 express 确保能够兼容大于或等于1.0.0并且小于2.0.0。

npm 的配置

npm 拥有很多默认配置。你可以使用这些默认的配置,也可以修改这些默认的配置,甚至可以在环境变量或命令行下修改这些配置。配置的权重是如下顺序定义的:

  1. 命令行,使用—为前缀的参数。比如 —foo bar,设定变量 foo 的值为” bar “。—foo 后不带值的参数,设定 foo 的值为 true 。
  2. 环境变量,所有 npm_config_ 为前缀的环境变量。比如 npm_config_foo = bar ,设定变量 foo 为 “ bar “。
  3. 用户定义。所有的变量存储在 $HOME/.npmrc 文件里的变量。
  4. 全局。所有 $PREFIX/etc/npmrc 文件里的变量。$PREFIX 变量可通过 npm prefix -g 获取,一般默认是 /usr/local。
  5. 内置的配置。通过安装时运行 ./configure 所定义的变量。可通过命令curl http://npmjs.org/install.sh | env npm_config_foo=bar 设置。

使用配置能给我们带来很大的灵活性。比如我们使用 npm install 时,对默认的资源库地址 https://registry.npmjs.org/ 不是很满意,我们可以使用下面的命令来更改资源库地址。

npm –registry “an other registry” install express
  1.  
  2. #或者下面的命令
  3.  
  4. env npm_config_registry="an other registry" npm install express

或是对 npm 默认的 vi 编辑器不满意,直接命令 npm set editor mate 。npm 的配置可通过命令 npm config ls 获取。这个命令是获取修改后的配置,要获取包括默认配置的全部配置加上 -l 参数。值得注意的是,开发者通过 npm config set registry "an other registry" 的方式修改 registry 这个属性值,一定要明白这个修改这个值所带来的负面效应。一旦设置了 registry 这个值,当你要 publish 一个模块,会把模块发布到修改后的资源库里,而不是原始默认的资源库。其他的资源库是原始默认的资源库的一个复制品,定时从默认的资源库取资源。一般来说,没有把其新家的模块同步到默认的资源库的能力。这样会导致发生你的模块在修改后的资源库里能够找到,而在其它的资源库里找不到的事情。

npm install命令

安装模块只需要 npm install express connect 命令给我们带来了很大的方便。安装模块的路径分两种:

  • 全局路径,也就是带上参数 -g 的安装模式。这个命令会把模块安装在 $PREFIX/lib/node_modules 下,可通过命令 npm root -g 查看全局模块的安装目录。 package.json 里定义的bin会安装到 $PREFIX/bin 目录下,如果模块带有 man page 会安装到 $PREFIX/share/man 目录下。
  • 本地路径,不带 -g 参数的。从当前目录一直查找到根目录/下有没有 node_modules 目录,有模块安装到这个目录下的 node_modules 目录里,如果没有找到则把模块安装到当前目录 node_modules 目录下。package.json 定义的 bin 会安装到 node_modules/.bin 目录下,man page 则不会安装。

我们需要选择什么样的安装方式呢?全局模式可以让你不用担心找不到模块,如果不需要还是尽量避免全局模式。

  • 如果我们只是 require('pkg') 一个模块,我们不需要使用全局模式。
  • 如果我们需要在命令行中调用,我们需要使用全局模式。因为这个安装把 package.json里 bin 下的定义安装到 $PATH 目录下。

有些模块我们既需要在命令行中调用又想 require('pkg') ,比如 Coffee-script 。那么我们可以使用全局模式安装,然后使用下一段要讲的命令 npm link 把它链接到本地的 node_modules 目录下。

不要担心 package.json 里 script 中定义的命令不会因为不是全局安装而不能运行。比如在例子里定义的 devDependencies 的 vows 。在调用 npm test 时 npm 会把 node_modules/.bin 目录放到环境变量 $PATH 的最前面。

对开发者而言,这算是最有价值的命令。假设我们开发了一个模块叫 test ,然后我们在 test-example 里引用这个模块 ,每次 test 模块的变动我们都需要反映到 test-example 模块里。不要担心,有了 npm link 命令一切变的非常容易。

首先我们需要把 test 链接到全局模式下:

cd ~/work/node/test # 进入test模块目录
  1. npm link            # 创建链接到$PREFIX/lib/node_modules

那么 test 的模块将被链接到 $PREFIX/lib/node_modules 下,就像我的机器上 $PREFIX 指到 /usr/local ,那么 /usr/local/lib/node_modules/test 将会链接到 ~/work/node/test 下。执行脚本 bin/test.js 被链接到 /usr/local/bin/test 上。

接下来我们需要把 test 引用到 test-example 项目中来:

cd ~/work/node/test-example # 进入test-example模块目录
  1. npm link test               # 把全局模式的模块链接到本地

npm link test 命令会去 $PREFIX/lib/node_modules 目录下查找名叫 test 的模块,找到这个模块后把 $PREFIX/lib/node_modules/test 的目录链接到 ~/work/node/test-example/node_modules/test 这个目录上来。

现在任何 test 模块上的改动都会直接映射到 test-example 上来。再比如假设我们开发很多应用,每个应用都用到 Coffee-script :

npm install coffee-script -g # 全局模式下安装coffee-script
  1. cd ~/work/node/test             # 进入开发目录
  2. npm link coffee-script          # 把全局模式的coffee-script模块链接到本地的node_modules下
  3. cd ../test-example              # 进入另外的一个开发目录
  4. npm link coffee-script          # 把全局模式的coffee-script模块链接到本地
  5. npm update coffee-script -g     # 更新全局模式的coffee-script,所有link过去的项目同时更新了。

就像你看到,npm link 对于开发时一个模块被多个模块引用时非常有用。windows 的用户会想,我这儿没有 UNIX 下的 link 工具怎么办?别担心只要你的 Node.js 支持 fs.symlink 就可用到这个特性。

其它 npm 命令

npm 命令里还有很多有用的命令。npm explore . -- git pull origin master ,更新当前的 git 资源库。npm edit . ,编辑当前模块的所有依赖模块。npm docs coffee-script ,打开 coffee-script 模块的文档。npm outdated coffee-script ,查看 coffee-script 是否有新版本。npm submodule . ,你可以要求你的依赖模块是从 git 资源库安装的,而不是从 registry 安装。因为作者的 git 资源库总是最新的版本,registry 上的是模块作者发布上去的稳定版本。甚至你可以用 npm 来编程。

var npm = require('npm');
  1. npm.load({}, function (err) {
  2.   if (err) return commandFailed(err);
  3.   npm.on("log", function (message) { if(arg) console.log(message) })
  4.   var requirements = JSON.parse(fs.readFileSync('config/requirements.json'));
  5.   npm.commands.install(requirements, function (err, data) {
  6.     if (err) return commandFailed(err);
  7.   });
  8. });

做为 Node.js 的开发者工具,npm 已经为我们想到很多的应用场景。这也是 Node.js 社区一致推荐它为开发者模块依赖管理工具。

漫谈jsonretweet

什么是 json 呢?json 是一种轻量级的数据交换格式,首先被老道( Douglas Crockford )在2001年定义。2002年建立了一个网站 http://www.json.org/ 提倡这种方便易用的数据格式。2006后提交到 IETF 里变成事实的标准 RFC 4627

json格式

在 json.org 和 RFC 4627 里都定义了一种 json 数据格式,json 包含两种数据结构: object 和 array 。

一个 object 就像一个无序的 hash 数据,被包裹在 { } 符号里。每一个数据都是一对键和值,用 , 分隔开。每一个键和值用 : 分割开。

object

一个 array 是一个有序的数组,被包裹在 [ ] 符号里。每一个值被 , 分隔开。

array

json 里的数据可以为numbertruefalsenull 、被双引号包裹的 stringobjectarray

value

也就是说在 json 里,下面的数据格式是错误的。json 里的键和 Javascript 的对象标识符是不同的概念,必须用双引号包裹包裹。在 Javascript 对象里,对象标识符可以不用引号,用单引号或双引号。string 可以用单引号或双引号来表现。而在 json 的 概念里的 string,其表现形式为必须用双引号包裹。

    {
        'json':"another lightweight data-interchange format"
    }

ECMAScript 5 定义中没有严格的按照 RFC 4627 的标准来,而是做了补充。json 可以包含任何一种 json 数据。true12345678902.011E3null"josn" 都可以是 json 的有效数据被 JSON.parse 接受。在 php 里的 json 实现也是对 RFC 4627 的标准做了补充 var_dump(json_decode('true', true)) 。老道在接受这个这个事实的变化之后,对 json2.js 增加了可以接受任何一种 json 数据的处理,但是没有看到 json.org 和 RFC 4627 对这种变化做出修改。

ECMAScript 里对 json 的序列化和发序列化的定义

在 ECMAScript 5 里对 json 的序列化和反序列化通过 JSON.stringifyJSON.parse 来实现。

反序列化JSON.parse

JSON.parse 接受一个必选参数和一个可选可选参数。

JSON.parse(text [,reviver ]) //函数定义
  1.     var value = '{"name":"shihua","gender":"male"}', index = 0;
  2.     var obj = JSON.parse(value);
  3.     var obj1 = JSON.parse(value, function(k,v){
  4.         index ++;
  5.         if(k === 'gender'){
  6.             return;
  7.         }
  8.         return v;
  9.     });
  10.     obj1.name // shihua
  11.     obj1.gender //undefined
  12.     index //3

不要惊讶 index //3,你没看错,是调用了3次函数。这是 ECMAScript 5 里定义的标准,也是我觉得比较丑陋的定义。从人类的理解来说,大家都觉得应该是调用2次是正常的,但是这个定义确实是3次调用。抽象类Walk首先接受这样的两个参数:{"":value}"" 。如果你要清理键值为 "" 的值,那么你要付出惨重的代价,整个函数处理后的结果是 undefinded

var obj = JSON.parse('{“name”:”shihua”,”gender”:”male”,”":”I am an empty string”}', function(k,v){
  1.         if(k === ''){
  2.             return;
  3.         }
  4.         return v;
  5.     });
  6.     obj // undefinded

这个抽象类Walk定义让大神 John Resig 也在这上面翻了跟斗,见 ECMAScript 5 Strict Mode, JSON, and More。这篇文章被 Retweet 了3936次和被各路大神品鉴了无数次,到现在还没有人指出这个错误。这篇文章里的一处错误在 JSON.stringify( obj, translate ) 的例子,应该在if ( key === "name" ) {return value + " Resig";} 后再加 if ( key === "" ) {return value} 。如果觉得John Resig 大神一定没错,而是标准变了。因为 ECMAScript 5 是2009年12月发布的,而这篇文章是2009年5月份写的。请点击这个链接查看2009年5月份的标准。

序列化函数JSON.stringify

JSON.stringify 接受一个必选参数和两个可选可选参数。

JSON.stringify(value [, replace , [, space]])//函数定义
  1.     var value = {name:"shihua",gender:"male"}, index = 0;
  2.  
  3.     var s = JSON.stringify(value);
  4.     s  // '{"name":"shihua","gender":"male"}'
  5.  
  6.     var s1 = JSON.stringify(value, ['gender']);
  7.     s1  // '{"name":"shihua"}'
  8.  
  9.     var s2 = JSON.stringify(value, function(k,v){
  10.         index ++;
  11.         if(k === 'gender'){
  12.             return;
  13.         }
  14.         return v
  15.     });
  16.     s2  // '{"name":"shihua"}'
  17.     index // 3
  18.     var s3 = JSON.stringify(value, null "\t");
  19.     s3
  20.     /*
  21.     '{
  22.         "name": "shihua",
  23.         "gender": "male"
  24.     }'
  25.     */

就如同例子给出的 JSON.stringify 的 replace 参数可接受数组和函数。当replace为函数时,大神再一次在例子上被坑倒了。

总结

json被广泛用于前后端的数据交互。理解json的数据格式、序列化和反序列化有助于我们能够更好的把控数据。最后请相信任何大神都会有打盹的时候,这就是人生的真理。

P.S.: MS 在 IE8 上的 JSON.parser 上有 bug 。详见:http://stackoverflow.com/questions/1288962/ie8-native-json-parse-bug-causes-stack-overflow

部署node.js的应用retweet

先吐槽一下:-) 。这个博客基本上归于月经帖了,主要是我碰上了“火星恐惧症”,症状表现为:担心要写的东西太老土太平常太小白或是已经被人看过写过,怕被别人说一点都没有体现在档的领导下应有的先进生产力的。国庆这些天在家做了反省,如果老是担心这种事的发生,就没办法在博客上交流了。什么别人的眼光,那都是浮云。

==========================华丽的分割线============================

最近Node.js很火,让很多的前端看到了可以直接从前端写到后端的希望。但是每次部署一个Node.js的应用却让前端苦恼不已。每次登陆服务器,用自己不熟悉的方式从版本控制仓库中拖下源代码,kill掉应用的进程,重起一个应用的守护进程。如果能够自动化的部署一个Node.js应用,而不需要去接触这些前端不太熟悉的Unix系统命令和管理。对于大家来说就是提升了生产力。Capistrano是一个强大的自动化部署工具,所以我们选用他来做自动化部署。并且我已经把做好了的一个部署脚本Nodebot提交到github上去,供大家参考。

首先我们需要一个环境,分为两个环境:部署客户端环境和服务器环境。

部署客户端系统需求:

你可以用任何系统了,系统只要具备下面的4个软件就可。

  1. 安装ruby。去http://www.ruby-lang.org/en/downloads/下载适合你系统的ruby版本。
  2. 安装rubygem。去http://rubyforge.org/projects/rubygems/下载最新版的RubyGems,解压后运行以下命令安装。 sudo ruby setup.rb
  3. 安装Capistrano和Capistrano-etx。运行以下的命令安装。sudo gem install capistrano capistrano-ext

这样你就拥有了在机器部署node.js应用的软件。然后你需要能够不使用密码直接登陆到服务器上。运行下面的命令:

# Create a local ssh key
  1. ssh-keygen
  2. #Copy key to server
  3. cat ~/.ssh/id_rsa.pub | ssh user@domain.com "cat >> .ssh/authorized_keys"

这样你就可以不需要密码直接登陆到服务器。

服务器端系统需求:

当然要Unix一类的系统了,Nodebot需要具备upstart的系统(一个基于事件的守护进程管理系统)。虽然Node.js能跑在window上,但是那只适合开发环境,不适合生产环境。还要有下面的软件,你也可以下载Nodebot并在部署客户端运行命令cap  nodebot:setup 来自动化安装。

  1. 安装scm软件。比如git,svn或hg,取决于你的node.js应用源代码管理软件。
  2. 安装Node.js。没什么好说的,地球人都知道这是什么。
  3. 安装npm。Node.js的一个包管理软件
  4. 安装node-jake。一个javascript的代码构建工具,有点像make和rake。用来安装应用的依赖。
  5. 具备upstart的系统,比如Ubantu。来做应用的守护进程。参看 http://upstart.ubuntu.com/

部署的登陆用户需要sudo权限,并且不需要提示输入密码。参考命令:

#Add a sudoer
  1. sudo useradd -m foo
  2. #edit sudoer privilege
  3. sudo visudo

出现编辑器时,插入下面一行,foo就是你新建的用户:

foo ALL=(ALL) NOPASSWD: ALL

然后在deploy.rb文件(下面会提到)里配置foo为部署用户。

部署客户端脚本

环境有了,那就要开工了。我们希望在服务器上的Node.js应用运行几个不同的环境:测试环境,开发环境,产品环境等。运行capify .命令会在当前的目录下产生一个文件和一个目录。在目录里有个deploy.rb,这是我们需要修改的文件。由于我们需要多个环境,所以我们引入了Capistrano-etx。

set :stages, %w[staging production]
  1. set :default_stage, 'staging'
  2. require 'capistrano/ext/multistage'

我们设定了两个环境:staging和production,默认是staging环境。我们再在config目录下建一个deploy目录,里面放的两个.rb文件对应不同的环境配置.最后部署环境的目录下是这样的一个结构:

.
├── Capfile
└── config
    ├── deploy
    │   ├── production.rb
    │   └── staging.rb
    ├── deploy.rb
    └── node.rb

deploy.rb文件看起来像这样。你需要设定host, repository,user和admin_user :

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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
##
# Capistrano tasks for Ubantu.
#
# Author: Shihua Ma http://f2eskills.com/
 
set :stages, %w[staging production]
set :default_stage, 'staging'
require 'capistrano/ext/multistage'
#application name
set :application, "example"
#start server script
set :node_file, "app.js"
#deploy host
set :host, "hostname"
#user name,must be a sudoer without prompting for password
set :user, "username"
set :admin_runner, user
 
 
set :repository, "git@git@github.com:mashihua/Nodebot.git"
set :scm, :git
set :deploy_via, :remote_cache
role :app, host
set :use_sudo, true
 
namespace :deploy do
desc "Start node server"
task :start, :roles => :app, :except => { :no_release => true } do
run "sudo start #{application}_#{node_env}"
end
desc "Stop node server"
task :stop, :roles => :app, :except => { :no_release => true } do
run "sudo stop #{application}_#{node_env}"
end
desc "Restart node server"
task :restart, :roles => :app, :except => { :no_release => true } do
run "sudo restart #{application}_#{node_env} || sudo start #{application}_#{node_env}"
end
 
desc "Check required packages and install if packages are not installed"
task :check_packages, roles => :app do
run "cd #{release_path} && jake depends"
end
 
task :create_deploy_to_with_sudo, :roles => :app do
run "sudo mkdir -p #{deploy_to}"
run "sudo chown #{admin_runner}:#{admin_runner} #{deploy_to}"
end
 
desc "Update submodules"
task :update_submodules, :roles => :app do
run "cd #{release_path}; git submodule init && git submodule update"
end
task :write_upstart_script, :roles => :app do
upstart_script = <<-UPSTART
description "#{application}"
 
start on startup
stop on shutdown
 
script
# We found $HOME is needed. Without it, we ran into problems
export HOME="/home/#{admin_runner}"
export NODE_ENV="#{node_env}"
 
cd #{current_path}
exec sudo -u #{admin_runner} sh -c "NODE_PATH=#{node_path} /usr/local/bin/node #{current_path}/#{node_file} #{application_port} >> #{shared_path}/log/#{node_env}.log 2>&1"
end script
respawn
UPSTART
 
put upstart_script, "/tmp/#{application}_upstart.conf"
run "sudo mv /tmp/#{application}_upstart.conf /etc/init/#{application}_#{node_env}.conf"
end
 
end
 
before 'deploy:setup', 'deploy:create_deploy_to_with_sudo'
after 'deploy:setup', 'deploy:write_upstart_script'
after "deploy:finalize_update", "deploy:update_submodules", "deploy:check_packages"
view raw deploy.rb This Gist brought to you by GitHub.

staging.rb环境文件看起来像这样。设定了应用的环境,git的branch,应用的监听端口和部署目录:
1 2 3 4 5 6 7
set :node_env, "staging"
#git repos branch
set :branch, "master"
#listing port
set :application_port, "1603"
#deploy path
set :deploy_to, "/srv/www/apps/#{application}/#{node_env}"
view raw staging.rb This Gist brought to you by GitHub.

production.rb环境文件看起来像这样:
1 2 3 4 5 6 7
set :node_env, "production"
#git repos branch
set :branch, "production"
#listing port
set :application_port, "1604"
#deploy path
set :deploy_to, "/srv/www/apps/#{application}/#{node_env}"
view raw production.rb This Gist brought to you by GitHub.

主要命令:

  • cap -T 查看所有的task
  • cap deploy:setup   设置staging环境,比如创建部署的目录等。staging是默认环境,命令等同于cap staging deploy:setup。调用production环境的命令cap production deploy:setup,第一个参数是环境名,第二个参数是任务名。
  • cap nodebot:setup  安装系统一些软件和配置守护进程。上面已经介绍过。
  • cap production deploy   部署 production环境的Node.js应用。包括从仓库取最新的代码,链接最新的代码到一个目录。重起应用的Server。
  • cap deploy:stop, cap deploy:start和cap deploy:restart   停止,启动和重起staging环境的Server。

简单的Node.js应用:

我们用一个简单的Node.js应用来说明实际的效果,应用的目录结构就像这样:

.
├── Jakefile.js
├── app.js
├── config
│   └── requirements.json
└── log

app.js 就是应用的启动脚本

1 2 3 4 5 6 7 8 9 10 11 12 13 14
var express = require('express');
 
var app = express.createServer();
 
app.get('/', function(req, res){
//the log will out put to log/{node_env}.log
console.log("Method:" + req.method);
//send text to agent
res.send('Hello World. NODE_ENV=' + process.env.NODE_ENV);
});
 
 
//listening on application_port where set by capistrano
app.listen(process.argv[2] || 3000);
view raw app.js This Gist brought to you by GitHub.

Jakefile.js jake构建工具调用的脚本,在本例子中用来安装express
1 2 3 4 5 6 7 8 9 10 11 12 13 14
var fs = require('fs');
 
desc('Check and install required packages');
task('depends', [], function (arg) {
var npm = require('npm');
npm.load({}, function (err) {
if (err) return commandFailed(err);
npm.on("log", function (message) { if(arg) console.log(message) })
var requirements = JSON.parse(fs.readFileSync('config/requirements.json'));
npm.commands.install(requirements, function (err, data) {
if (err) return commandFailed(err);
});
});
}, true);
view raw Jakefile.js This Gist brought to you by GitHub.

config/requirements.json 应用依赖的定义
1
[ "express@2.4.7"]

log 应用日志的输出

实际部署的效果: staging环境production环境

结论:

使用Nodebot你可以轻松的部署你的Node.js应用。把你的焦点放到实际的需求中,而不用关心部署环境的建立和部署应用的麻烦,只许在部署客户端轻松的输入简单的命令。

解析SQL语句retweet

以前对郑凯说过要解百姓网公开笔试题:查询条件的子集判断,一直拖到现在。上周正好和同事吴孟春聊到taobao的一个项目myfox。他说解析SQL是一件很难的事,于是便激起了那颗驿动的心,要做做如何解析SQL语言。在开始之前,我们来看看什么是LISP。LISP是一种语言,定义了7个基础的操作运算符和两个表达式,操作符:quote,atom,eq,cons,cond,car,cdr。表达式包含:原子(atom)和表(list)。就这么简单的一个语言却有强大的功能。在开始下面的例子之前,我们来看看它的怪异的表达式:

  1.  (+  (+ 1 2) (- 3 1))))

上面表达式是我们在别的语言中看到的((1 + 2) + (3 – 1))表达式。可以看到lisp中运算符放在前面使程序调用更清晰,并且能够清晰地看到语法结构树。如果+号和-号是个函数,那么可以清晰地看到它们都接收两个参数。从上面地例子也能清晰地看到调用次序,首先是+号接收两个参数(+ 1 2)和(- 3 1),这两个参数是个函数,那么就先求它们的值后再用+号来求值,收缩后表现为(+ 3 2)。如果(+ 1 2)和(- 3 1)都是一个函数定义add和sub,那么上面的表达式可收缩为(+(add (2)) (sub (2)));

  1. ;addition operator
  2. (defun add (x)
  3.   (+ x 1)
  4. ;subtraction operator
  5. (defun sub (x)
  6.   (- x 1))

接下来我们先来看看一个SQL例子:

  1. SELECT * FROM keyword
  2. WHERE id > 2011-05-10 AND keyword != ''
  3. ORDER BY SUM(search_num) DESC
  4. LIMIT 0, 100

总的来说SQL是对集合的各种操作。SELECT是取什么样的集合,FROM是从哪儿取集合,WHERE是过滤一个集合,ORDER BY 是对一个集合进行排序,LIMIT是限制一个集合返回的结果集大小。可以用下面这种LISP语句来映射上面的SQL语句:

  1. (limit? '(0 100)
  2.   (order_by?  (sum? search_num)
  3.     (where? (from? keyword (select? *))
  4.       (and? (> id 1) (!= keyword '')))))

这就是SQL的语法结构树。为什么要解析成这样呢?首先是limit?,因为他定义了最后的结果集。它以两个集合为运算对象,返回第二个集合里的第0个到第100个的数据。order_by?定义了排序的运算,它以两个集合为运算对象,第一个是(sum? search_num) 第二个是一个列表 ,where?相当于定义了一个过滤器的函数,接受两个集合(from? keyword (select? *))和  (and? (> id 1) (!= keyword ”))), (from? keyword (select? *))返回一个数据库里的数据集合。 (and? (> id 1) (!= keyword ”))对这个集合进行过滤处理。大家看到希望了吗?解析成这样的语法结构树后,我们只要定义一些函数,解析SQL语法,把语法Token反应到相定义的函数里就能做很多事。比如定义函数> 接受两个参数,返回过滤后的集合。where是个高阶函数,它对SELECT出来的集合进行过滤。and?是与操作,返回两个集合里都存在的子集。绕了这么多,还是用前端同学都能理解的javascript表示:

  1. function where (LIST){
  2.   function and(LISTA,LISTB){
  3.     var val = [];
  4.     LISTA.forEach(function(v){
  5.       LISTB.forEach(function(x){
  6.         if(x === v){
  7.           val.push(x);
  8.         }
  9.       });
  10.     });
  11.     return val;
  12.   }
  13.   function greater(a,b){
  14.     return LIST.filter(function(v){
  15.         return v[a] > b;
  16.     });
  17.   }
  18.   function not_equal(a,b){
  19.     return LIST.filter(function(v){
  20.        return v[a] != b;
  21.      });
  22.   }
  23.   return and(greater('id',1),not_equal('keyword' ,''));
  24. }

调用where([{id:1,keyword:'1'},{id:10,keyword:'2'},{id:20,keyword:''}]),返回 { id=10, keyword=”2″}]。这就是(where? (from? keyword (select? *)) (and? (> id 1) (!= keyword ”))),在javasctipt里的代码(> id 1)变成了greater(‘id’,1)。 当然在实际的token映射到函数还需要对各种状态和函数参数进行处理,因为将输入的字符流转化成单词 (token) 流和将token映射到函数本身包含许多状态。

概念清晰了,我们就能处理百姓网的题目了。对于queryA,我们生成一个集合比如[{id:[1,100],date:[2000,5000]}],对于queryB我们生成一个集合比如[{id:[20,50]}]。如果queryB包含在queryA中,返回true。如果queryB是[{data:[5002,5080]}],返回false。再来看淘宝的myfox,它是用于数据分片了,如何组织查询。也即数据按日期分布在不同的机器上。比如A机器存储[1,1000]的集合,B机器存储[1001,2000]的集合,C机器存储[2001,3000]的集合。现在有个SQL语句SELECT * FROM db WHERE  date > 1500 AND date < 2500。把它转换成SELECT * FROM db WHERE  date > 1500,SELECT * FROM db WHERE  date < 2500到B和C机器上查询,然后再合并两个集合为一个大集合。只要正确的解析完SQL后,淘宝的对应每台机器的查询和百姓网的SQL集合判断就不是大问题。