一次神奇的 Nodejs Debug 经历

中秋之后,服务器上老是出现 cpu 爆满的情况,难道服务器也闹情绪想休假。当然这种情况是很紧急的,所以马上就着手排查。

还没找出问题,又有同学报测试机上也有这种情况,但是应用进程不同。想到有可能这两件事是有关联的,所以先从测试机开始排查。

问题一, setTimeout 的 bug

测试机上的问题很好排查,每次进程重启后立刻飙升到 100%,通过万能的 log 大法,很快就定位到问题。应用中使用了 axon 模块作为消息中间件,在连接不成功时会用 setTimeout 尝试重连,间隔由最初的 100ms 开始每次乘以 1.5,这样在三次之后间隔就变成了 337.5ms,nodejs 有一个 bug,Timer 函数中出现小数时会导致死循环,我们用的是 0.10.30 版本,这个 bug 依然可以重现。所以解决办法也很简单了,直接 round 一下就可以了。以后在使用 Timer 类函数时也需要注意不要出现小数哦。

问题二,正则表达式的效率问题

由于 axon 在生产环境中也有使用,我们就将所有的版本都进行了升级,以为万事大吉。结果却不遂人愿,生产环境中的几个进程在启动后,时不时会出现负载 100% 的情况,这种可能性分析起来就很复杂了。我们依次尝试了 node-inspector,node-heapdump 等工具,都没有找到原因(实话说,以前用这些 debug 内存问题时也是一无所获,nodejs 的 debug 工具实在鸡肋)。也没有重现线上的问题。

一个有趣的现象是,我发现每次进程重启之后,飙升到 100% 的时间不定,但是每次都是瞬间从 0 到 100%。这种现象说明导致问题的原因可能不是程序内部产生,而是由外力产生,这个外力是什么呢,其实就是请求。再一次借助万能 log 大法,express 的 log 只能记录有响应的请求,如果一个请求在中途陷入了死循环,那么就得不到 express 的 log,所以我们自己写了一个中间件,在 express 的最前部引入,只打印出请求的链接。

接下来问题就很明显了(其实也破费一番周折),每当出现某一请求时,程序就陷入了死循环,屡试不爽。一步步排查下来,发现问题出现在 xss 模块中,其中的一个正则表达式,在匹配某些字符串时出现了问题:

  • 表达式:/<!--(.|\s)*?-->/gm
  • 测试字符串:<!--

当表达式未找到匹配内容时,效率指数级的下降,特别是在多个重复空格和子表达式时。这个原因解释起来就比较复杂了。大神的一篇文章很好的解释了这个问题,有时间我要翻译一下,一定又有很多收获。

解决的方法很简单,在这个 Pull Request中可以找到。看来以后使用正则表达式的时候得过留个心眼了。

Limbo: 简单访问远程数据库

简聊一下 轻松协作

对于 nodejs 生态来说,使用 mongoose 作为 Model 模块是再好不过的一件事,其一大特点就是简洁优雅的 Schema 定义,提供了每个键值的类型验证,数据验证,索引声明,虚拟键,并自带实例化方法的扩展,大大节省了开发的成本。但是在考虑开放数据的时候,一切就显得不那么美好了。

在打造简聊这款应用的过程中,我们就实实在在的遇到了这样的问题。由于需要使用 Teambition 的用户和团队数据,并且当简聊更新了用户数据之后,在 Teambition 中能实时的将这些更新推送到用户那里。按照惯例,我们最初使用的是 restful 接口。

第一阶段,使用 restful 接口

restful 接口的应用面最广,但是仍然存在很多不足,比如接口在参数和结构上限制较多,在考虑修改接口 api 的时候,往往会顾虑客户端的兼容性,而一旦客户端程序有新的需求,则需等待接口的更新。另一个麻烦的地方是需要做签名校验,对于内部的应用来说,我们完全可以通过防火墙来控制特定 ip 对端口的访问,签名在此处就显得有点多余。

第二阶段,单独拆封 Schema

然后我们想到了将 Schema 拆封成一个单独的仓库,nodejs 有良好的模块管理,在不同的应用中,我们只需要将这些模块引入进来,既做到同步更新,又做到 DRY。相对于 restful 接口的缺点就是,对于数据的调用入口过多,而且应用之间互相是不知情的。例如在简聊中有更新用户数据,在 Teambition 中就无法得知,并推送给其他客户端。

第三阶段,远程过程调用(rpc)

这个阶段和 restful 接口其实类似,我们在 Teambition 的后端进程中将一些接口方法暴露出来,这样我们的客户端程序就能通过简单的 rpc 方式调用这些接口。例如我们导出了 user.update 方法,在客户端代码中使用 rpc.call('user.update', params, callback) 即可调用相应的过程。这样的调用行为与使用本地代码无异,可能是目前能找到的最简单直接的方式了。

第四阶段,rpc 与 mongoose 的结合

事情可以变得更简单,由于目的主要是为了操作数据库,所以我们开发了一个模块 limbo,将 mongoose model 中所有方法暴露出来,以命名空间来划分,实现了在客户端与服务端程序一致的使用体验。

例如我们在服务端程序中使用 limbo 连接 mongodb,只需要做如下声明:(以下的代码都以 coffeescript 作为示例)

1
2
3
4
5
6
7
8
9
10
11
limbo = require 'limbo'

# 定义 Schema
UserSchame = (Schema) ->
  # 这里的 Schema 即 mongoose.Schema
  new Schema
    name: String
    email: String

# use 方法用作区分不同数据库连接的命名空间,一般参数选择数据库名就行
db = limbo.use('test').connect('mongodb://localhost:27017/test').load 'User', UserSchema

使用方式就与 mongoose 一致了

1
2
3
4
user = db.user
# user 是一个 limbo 中用于封装 model 的一个对象,你可以直接使用 user.model 来直接调用 mongoose model
user.findOne _id: 'xxxx'
user.create name: 'xxx', email: 'yyy'

下面是 limbo 中最激动人心的地方,你可以导出一个 collection 中的所有方法到 rpc server 中,只需要通过一个简单的声明

1
limbo.use('test').bind(7001).enableRpc()

下面我们就要提到如何在客户端程序中调用这些方法

1
2
3
4
5
6
7
8
9
10
11
# 在客户端也需要初始化一个 limbo 命名空间,需要与服务端一致,链接改为服务端的域名和端口号
db = limbo.use('test').connect('tcp://localhost:7001')

# 下面有两种方式来使用 rpc
# 1. 使用 call 方法
db.call 'user.findOne', _id: 'xxxx', ->
# 2. 使用方法链
db.user.findOne _id: 'xxxx', ->
# 第二种方式存在一个延迟,必须要在 limbo 与服务端程序握手成功之后才可以使用,
# 否则会抛出一个对象不存在的异常,不过在一般的应用中,
# 初始化所需的时间都会长于这个链接所需时间,所以延迟可以忽略不计了

可以看出,上面的第二种方式与服务端在本地使用 mongoose 的方式一模一样,这种黑魔法式的调用方式应该是广大码农喜闻乐见的。

limbo 另一个值得称道的功能是可以在服务端程序监听这些远程调用的事件,这得益于 nodejs 的 event 对象,limbo 本身就继承于 EventEmitter 对象,所以我们在每次远程调用后会触发一个事件给服务端程序,而在服务端只需要简单的监听这个事件即可

1
limbo.on 'test.user.findOne', (user) -> ...

正是这种 rpc 加事件反馈的机制,让简聊Teambition 可以实现简单实时的数据交换。我们将 limbo 托管在 github 上开源,是深知它还存在很多可以改进的地方,所以不免庸俗的说一句,欢迎 issue 和 pr~

最后,欢迎访问我们的新产品简聊,一款基于话题的轻量级协作应用。

给 Github Page 设置域名

Github 免费提供了很棒的静态站托管服务 github pages,并且为每人准备了一个二级域名 username.github.io。

但是对于喜欢个性又爱折腾的码农来说,使用别人的域名,是万万不能忍受滴,所以 github 支持了绑定个人域名。

英语不错的可直接传送官方文档 Tips for configuring a CNAME record with your DNS provider,如我这样记性不好的,就看下面的流程:

首先,你得有个自己的域名,比如这个 jingxin.me,然后我的目标是绑定到 sailxjx.github.io,并支持所有二级域名的跳转,如从 sailxjx.github.io/blog 会自动转到 jingxin.me/blog。

然后,我们要创建 CNAME 记录,这一步要在个人域名托管的 dns 服务上操作,添加一条 CNAME 记录,指向 sailxjx.github.io,这样,就实现了通过 jingxin.me 访问 github page 内容的目的。通过 dig 命令可以查看是否生效,这个时候如果你用 ping 或者 nslookup 会看到两个域名的 ip 是一样的。

1
2
3
4
5
$ dig jingxin.me +nostats +nocomments +nocmd
;jingxin.me.                  IN  A
jingxin.me.             346   IN  CNAME sailxjx.github.io.
sailxjx.github.io.      3346  IN  CNAME github.map.fastly.net.
github.map.fastly.net.  46    IN  A 103.245.222.133

最后,当然是告诉 github 需要自动从 sailxjx.github.io 跳转到 jingxin.me,是需要在相关的 repository 中添加一个 CNAME 文件,里面保留一行域名记录(不包含 http 等 schema 部分),比如这里,push 之后就可以在项目的设置中看到这样的提示

pages-section.png

下面就是短暂的等待了,快的话立即就生效了。

多余的话

除了使用 CNAME,github 还提供了另一种做法 Apex domains,由于官方并不推荐,所以这里也就不介绍了。

回调 vs 协程

原文地址:Callbacks vs Coroutines

最近 Google V8 引擎的一个补丁提供了 ES6 生成器,一篇叫做“用 Javascript 生成器来解决回调问题的研究”的文章引发了很大的争议。虽然生成器到目前为止仍然需要 --harmony--harmony-generators 选项才能激活,但是它已经值得你跃跃欲试!在这篇文章中我想要阐述的是自己对于协程的体验,并且说明为什么我认为它们是一种好方法。

回调和生成器

在认识回调和生成器之间的不同之前,我们先来看看生成器在 Nodejs 或浏览器这种由回调主宰的环境中是怎样发挥作用的。

首先生成器是回调的一种扩展,有些类型的回调就是用来”模拟”生成器的。这些”futures”,”thunks”,或”promises” —- 无论你怎么称呼,都是用来延迟执行一小段逻辑的,就好比你 yield 了一个变量然后由生成器来处理其他的部分。

一旦这些变量 yield 给了调用方,这个调用方等待回调然后重新回到生成器。见仁见智,生成器的原理和回调其实是一样的,然而下面我们会说到使用它的一些好处。

假如你还是不太清楚该怎么使用生成器,这里有一个简单的例子实现了由生成器来控制流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var fs = require('fs');
function thread(fn) {
  var gen = fn();
  function next(err, res) {
    var ret = gen.next(res);
    if (ret.done) return;
    ret.value(next);
  }

  next();
}
thread(function *(){
  var a = yield read('app.js');
  var b = yield read('package.json');
  console.log(a);
  console.log(b);
});
function read(path) {
  return function(done){
    fs.readFile(path, 'utf8', done);
  }
}

为什么协程会使代码更健壮

对于传统的浏览器或 Nodejs 环境,协程在自己的堆栈上运行每个”纤程”。这些纤程的实现各不相同,但是它们只需要一个很小的栈空间就能初始化(大约4kb),然后随需求增长。

为什么这样棒极了?错误处理!假如你使用过 Nodejs, 你就会知道错误处理不是那么简单。有些时候你会得到多个包含未知边际效应的回调,或者完全忘了回调这回事并且没有正确的处理和汇报异常。也许你忘了监听一个”error”事件,这样的话它就变成了一个未捕获的异常而让整个进程挂掉。

有些人喜欢使用进程,而且这样也挺好,但是作为一个在早期就使用 Nodejs 的人来说,在我看来这种流程有很多地方值得改进。Nodejs 在很多方面都很出色,但是这个就是它的阿喀琉斯之踵。

我们用一个简单的例子来看看由回调来读写同一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
function read(path, fn) {
  fs.readFile(path, 'utf8', fn);
}
function write(path, str, fn) {
  fs.writeFile(path, str, fn);
}
function readAndWrite(fn) {
  read('Readme.md', function(err, str){
    if (err) return fn(err);
    str = str.replace('Something', 'Else');
    write('Readme.md', str, fn);
  });
}

你可能会想这看起来也没那么糟糕,那是因为你整天看到这样的代码!好吧这是错误的:)为什么?应为大多数 node 核心方法,和多数第三方库都没有 try/catch 他们的回调。

下面的代码会抛出一个未捕获异常而且没有任何方法能捕获它。就算内核检测到这个异常并且告诉调用方这可能是一个错误点,大多数回调都有未知的行为。

1
2
3
4
5
6
7
8
function readAndWrite(fn) {
  read('Readme.md', function(err, str){
    throw new Error('oh no, reference error etc');
    if (err) return fn(err);
    str = str.replace('Something', 'Else');
    write('Readme.md', str, fn);
  });
}

所以生成器是怎么来优化这一点的?下面的代码片段用生成器和 Co 库来实现了相同的逻辑。你可能会想”这只是一些愚蠢的语法糖而已” - 但是你错了。只要我们将生成器传给 Co() 方法,所有委派给调用方的 yields,特别是强健的错误处理都会由 Co 来委派。

1
2
3
4
5
co(function *(){
  var str = yield read('Readme.md')
  str = str.replace('Something', 'Else')
  yield write('Readme.md', str)
})

就像下面这样,Co 这样的库会将异常”抛”回给他们原本的流程,这意味着你可以用 try/catch 来捕获异常,或者任其自流由最后 Co 的回调来处理这些错误。

1
2
3
4
5
6
7
8
9
co(function *(){
  try {
    var str = yield read('Readme.md')
  } catch (err) {
    // whatever
  }
  str = str.replace('Something', 'Else')
  yield write('Readme.md', str)
})

在编写 Co 的时候貌似只有它实现了健壮的错误处理,但是假如你看一下 Co 的源代码你会注意到所有的 try/catch 代码块。假如你用生成器你需要将 try/catch 添加到每个你用过的库中,来保证代码的健壮性。这就是为什么在今天看来,用 Nodejs 编写健壮性代码是一件不可能完成的任务。

生成器对于协程

生成器有时会被当成”半协程”,一个不完善,仅对调用方有效的协程。这让使用生成器比协程的目的更明确,好比 yield 能被当成”线程”。

协程要更加灵活一些,看起来就像是普通代码块,而不需要 yield:

1
2
3
4
var str = read('Readme.md')
str = str.replace('Something', 'Else')
write('Readme.md', str)
console.log('all done!')

有些人认为完整的协程是”危险的”,因为它不清楚哪个方法有没有延迟执行线程。个人来说我认为这种争论很可笑,大部分延迟执行的方法都很明显,比方说从文件或套接字中读写,http 请求,睡眠等等延迟执行不会让任何人感到惊讶。

假如有些不友善的方法,那么你就 “fork” 它们来强迫这些任务变成异步的,就像你在 Go 中做的一样。

在我看来生成器可能比协程更危险(当然比回调好得多)—-仅仅是忘记一个 yield 表达式就可能让你费解或在它执行下面的代码时导致未知的行为结果。半协程和协程两者各自有优缺点,但是我很高兴现在至少已经有了其一。

让我们来看看你用生成器可以怎样实现新的构造方法。

用协程实现简单的异步流程

你已经看到一个简单读/写表达式看起来比回调更优雅,我们来看看更多的内容。

假设所有操作默认按顺序执行简化了模型,有些人声称生成器或协程使状态变得复杂化,这事不正确的。用回调处理状态也是一样的。全局变量依然是全局变量,局部变量依然是局部变量,而闭包依然是闭包。

我们用例子来说明这个流程,假设你需要请求一个 web 页面,解析其中的链接,然后同步请求所有的链接并输出他们的 Content-types。

这里是一个使用传统回调的例子,没有使用第三方流程控制库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function showTypes(fn) {
 get('http://cloudup.com', function(err, res){
   if (err) return fn(err);
   var done;
   var urls = links(res.text);
   var pending = urls.length;
   var results = new Array(pending);
   urls.forEach(function(url, i){
     get(url, function(err, res){
       if (done) return;
       if (err) return done = true, fn(err);
       results[i] = res.header['content-type'];
       pending || fn(null, results);
     });
   });
 });
}

showTypes(function(err, types){
  if (err) throw err;
  console.log(types);
});

这么简单的一个任务被回调搞得毫无可读性。再加上错误处理,重复回调的预防,存储结果和他们本身的一些回调,你会完全搞不懂这个方法是用来干嘛的。假如你需要使代码更健壮,还需要在最后的方法处加上 try/catch 代码块。

现在下面有一个由生成器实现的相同的 showTypes() 方法。你会看到结果和用回调实现的方法是一样的,在这里例子中 Co 处理了所有我们在上面需要手工处理的错误和结果集的组装。被 urls.maps(get) 方法 yield 的数组被平行执行,但是结果集然后是保持不变的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
function header(field) {
  return function(res){
    return res.headers[field]
  }
}
function showTypes(fn) {
  co(function *(){
    var res = yield get('http://cloudup.com')
    var responses = yield links(res.text).map(get)
    return responses.map(header('content-type'))
  })(fn)
}

我并不是建议所有的 Npm 模块使用生成器并且强制依赖 Co,我仍然建议使用相反的方法 —- 但是在应用层面我强烈推荐它。

我希望这能说明协程在编写无阻塞的程序时是一个强有力的工具。

软件与配置

前段时间不间断的开发新项目,实际是想得多,写得少,最后落实到代码中,大概也就200多k文件。现在回头看一下,觉得还挺满意的,因为每个新项目会找出些不一样的地方,不求做到最好,但求标新立异,否则岂不是失去了开发的意义,也辜负了“程序猿”这个需要些许创造性的工作。

每次新项目达到一个阶段,能回顾一下,总是极好的。这次细想一下,有什么值得总结的地方,就又回到了“软件配置”这个极庸俗的话题上来了。

在我看来,任何软件都少不了配置,但是在之前的文章中我也提到过,配置是软件的大敌。任何软件配置越复杂,学习成本也就越高,这是一种反人类的趋势,所以这种软件,要么被更新,更简单的软件取代,要么就根本无人问津,消失在软件的海洋中。

当然,配置也可以被广泛的定义,形式是多种多样的,有些还是群众们喜闻乐见的,不能一棒子打死了。下面谨罗列一下我心目中的配置文件类型,和它们的适用范围。

第一阶段,单纯的key-value

这应该是最常见的一种配置,也最接近配置文件的本意。所谓人各有所好,软件在部署的过程中会遭遇不同的平台,也会遇到不同的适用情况和环境,这个时候就需要依靠配置文件来告诉软件应该怎样正确的运行。操作系统中的环境变量就是一种最常见的配置,PATH定义了用户需要的可执行文件的搜索范围,SHELL决定了和人交互的shell版本。在web应用中,数据库连接,域名,api签名秘钥等等都应当以配置的形式出现,否则就是给懒惰的开发者增加麻烦。

为什么说是“懒惰的开发者”,应为这些配置其实都可以通过在源代码中通过if-else条件来解决,要是碰到个勤快的开发(也许也是愚蠢的开发),就会把签名秘钥写在代码中,通过if-else来判断哪种环境应该调用哪种签名生成规则。但是在编程界,“懒惰是一种美德”,前面的做法非但徒增开发的难度,也不够灵活,用逼格更高的话来说,就是“不够优雅”。

所以需要用到key-value类型的配置。通常情况下,这种配置文件的出现形式会是一个json文件或是一个xml文件。假如这种配置文件不需要做到跨语言调用的话,跟进一步的做法是直接使用软件可用的脚本语言,通常会于源代码的语言保持一致。像我们写nodejs应用,使用js文件来做配置,就会比用json来的更加灵活。

第二阶段,预定义方法

下面提到的配置类型都有别于狭义的配置文件。比如说某些预先定义好的方法。我们可以在软件中预先定义好一些方法,这些方法我们不知道会不会用到,也不知道什么时候会用到,唯一明确的一点就是,我们知道这些方法能接受哪些参数,并且会得到哪些输出。我们在定义这些方法时,假如能确保他们的参数形式保持一致,那就更好了。这样对于习惯拿来主义的用户或我们自己,就更加的便利了。

具体的例子,比方说rails中的路由就是个这样的例子(从routes.rb文件的位置也可以看出它就是个配置文件),作者给我们定义好了一系列的方法,getpost,最有意思的就是resource。我们知道这些方法需要什么样的参数,能得到什么样的效果,假如我们不用这些配置,那么写上完整的路由控制代码,也能达到一样的目的。但是不需要,因为我们“很懒惰”。

第三阶段,钩子

如果你对于软件的使用者有足够的信心或信任,那么完全可以将一些接口留给用户来实现,软件中的钩子就是一个很好的例子,这在很多full-stack的框架中是很常见的,另一个场景是git的hook,在每个命令的前后,git都预留了钩子由用户来实现,这样它的可玩性就高了很多,到了github上,网站把很多常用的钩子打包成一个个模块,用户只需要给需要的模块打上勾就行了,这也可以称为配置的一种形式。

第四阶段,可编程性

这已经脱离了配置的范畴了,一些软件提供强大的api,用户通过这些api来拓展软件,甚至集成到软件本身。比如nginx使用lua来作为对单纯配置的拓展,实现了任何你所需要的功能。而github有一个很有意思的项目,叫做hubot,是一个智能机器人项目,以消息的输入输出作为基本要素,用户可以加上任意的adapter,为hubot增加应答规则。其核心非常简单,但是众人拾柴火焰高,也许日久天长,这种基于规则库的低级智能,真的能发展成影响到每个人生活方方面面的智能管家。

第五阶段,无配置文件

从智能的话题延伸出去,既然软件本身需要具有智慧,那么为什么不能领悟它自己的需求,而要由人来给它写好配置?最起码,软件要能记忆曾经执行的过的操作,记录过的配置。

以两个monitor软件为例,’supervisor’是一款老牌的进程监控软件,由python写成,’pm2’是后起之秀,由nodejs写成,但是他们都不局限于监控python或nodejs进程,而可以做到全平台任意进程的监控。

假如初次使用这两款软件,更多的人会习惯于’supervisor’的操作方式,先写好一个配置文件,也许叫做supervisor.conf,定义好有哪些程序需要运行,执行文件的路径,环境变量,等等。然后我们敲下’supervisord start’来让这些进程运行起来,一切看起都很完美。

然后我们开始使用pm2,一开始会惊奇于它没有要求任何配置文件,你找到需要执行的文件,敲下pm2 start app.js,程序就开始执行,然后我们就可以通过start|restart|reload|kill等一系列命令操作这个进程。这个过程没有任何要求编写配置文件的步骤。

这怎么可能?我一开始的反应是这样,然后开始在各个目录中翻找所谓的’默认配置’,发现一无所获,后来我阅读了pm2的源代码,发现其实所有的配置,都在第一次启动进程的时候被忠实的记录下来。事情本该如此,既然第一次我已经告诉了软件需要的所有参数和变量,为什么有那么多的软件,还需要进行人为的干预来决定需要的参数,这不是一个优秀软件应该具备的素质。

我最近在修改一个在团队内部用了一段时间的部署工具sneaky。一直以来它都工作的很好,唯一的烦恼是在发布一个新项目前需要编写一段配置文件,填上发布的目标地址,端口,必要的时候再加上一些钩子。近期的一次更新已经将原本需要的5到6个配置项缩减到最少1个。下一步,当然是干掉配置文件。我们告诉它,把软件部署到某某服务器,然后到了下次,我们需要干同样的事时,软件已经比我们先知道它要做的事。这才是软件的未来。

金钱与开源(part2)

接上篇金钱与开源(part1)

虚拟小费

有一些项目设立了“小费”机制来展现你(对某人)的赏识。我比较熟悉的两个是TipTheWebGitTip。我不确定TipTheWeb是否还有人维护。GitTip比较新一些,而且现在看起来更流行。

两者都建立在道德的基础上,TipTheWeb的Eric Ferraiuolo是个非常和善和高尚的人,他们的目标就和字面上看起来一样,为了使互联网变得更好。GitTip的Chad Whitacre看起来同样也是为互联网做一些好事。

给某人一些小费看起来就像是行善。这在你不知道什么是合适的礼物情况下,一种展现对某人爱心和赞誉的方式。能让人在精神上和物质上得到满足。

话虽如此,我仍旧怀疑这种方式能提高项目的创建量。难道程序员仅靠这些捐款就能付得起房租?我肯定是不行的。

金钱肯定是有来源的,所以我不认为完全靠这种方式能支持一个开源项目进行下去。某种程度上,GitTip上的一些人有“真实的工作”,并且决定将一部分钱用来做捐助。在GitTip上给小费,然后希望人们对你负责,这看起来有点尴尬。小费是果而非因,也就是说,“我喜欢你所以才给你小费”,而不是,“我希望你为我做点事情,所以才给你小费”。

我不反对捐助。事实上,我觉得它很重要,这是我们这些有经济自由的人的一种道德需要。然而,我做过计算,尝试找出一种能让我的捐助利益最大化的方式,每个月给某个开发者5美元并不能让我伤筋动骨。这看起来更像是一个游戏,对另一个人表示尊敬和赞扬,并附带一点点金钱奖励。

这与花大价钱雇佣开发者来创造软件不一样。但是缺少了一种交易关系,这里能得到的收益是很少的。做营销的方式林林总总,我不觉得这种方式能有很高的商业价值。为什么不去雇佣这些开发者,然后得到更高的回报呢?

给小费的方式既有趣,又让人感觉愉悦。但是我很怀疑它是否真的能改变世界。这并不能保证你过上无忧无虑的生活,而且我感觉假如你真的靠小费来生活,那么它可能会改变你(编写代码)的初衷。

这让我想到了另一种有前途但是同样存在问题的方式。

奖金

无论何时我们都得对于钱的问题万分小心。很多研究发现事物的动机有外因和内因之分。假如我为了五美元去做一些事情,我可能享受不到免费做这件事的乐趣。就像Merlin Mann说的:“世界上有两种价值:免费,和成本。”

当然,“我将花X美元来请你帮我写Y功能”在软件界是一种通行的方式。大部分情况下,就我经验而言,它并不能取得很好的效果。任何人都不可能给出一个等价的条件。

一旦延期很久,双方都不会满意。通常这个时候甲方和乙方就开始扯皮,事情就杯具了。

BountySource是这个领域的后起之秀,它有一些有趣的特征。“Backers”既可以是个人也可以是公司。利用融资的形式来支付小费的想法很有吸引力,这样你就不必为某个个体负责了。而且,集成Github Issues的想法也很聪明。

但是我仍然十分怀疑BountySource能改造开源软件现有的生态环境。一个显而易见的缺陷就是奖励实在是微乎其微。而且,现在钱被提到了前面,热情就被商业所取代了。

举个例子,我刚刚获知在这个实现SemVer 2.0规范Issue中有一笔87美元的赏金。

事实是,我已经和其他人一起着手写这样一份规范(在当时还很模糊)。一旦完成,我能遵守规范的唯一方式就是重写整个node-semver。这总共需要两周邹游的时间,包括将改动提交到npm上来让它起作用。

所以,虽然87美元看起来很诱人,但是我不能仅凭87美元来过两个礼拜。就算它有870美元,8700美元甚至87000美元,提高奖金也不是最好的办法。它尽最后的努力来使一个社区满足你的需求。假如社区根本不关心这事,那么他们也不会关心你或你的需求。

奖金机制在这里存在的问题就是,软件有时候并不是一个有着明确边界的产品。增加一个功能看上去更像收养一只小猫,而非投递一件包裹。假如你无论如何都想收养这只小猫,但是有些人想要花钱来让你收养另一只,就算能行,最后也会事与愿违。强扭的瓜不甜,最后可能导致的是一个糟糕的软件。

奖金可能在一个项目负责人希望激励成员找出bug或添加功能的时候有用。但是,项目负责人不可能给出组有的钱来请人开发(假如这样的话,还不如直接雇佣他们了)。假如一个项目负责人简单的声明,“我觉得x功能很有用,欢迎大家来实现它。”这已经起到了激励作用,奖金就完全没有必要了。最理想的情况,就像Mikeal Rogers出钱请人来找bug,这只是一种让人写代码的营销手段。

奖金只有在一个有明确目标的项目中才会起作用,就像查找安全漏洞。漏洞奖励计划发现给予奖金会比直接雇佣一个安全专家来的更行之有效。找出一些其他的有明确胜利目标的场景也是很有意思的。

然而,上面的只是一些例外,而不是常态。公司使用开源软件的时候通常并不会急着要某一个功能,或者在早期就发现一堆的bug。他们会与一些程序要保持联络来让他们的需求得以实现。这并不是明确的要求给程序加某个改动,而是在他们需要的时候,可以保证让程序加上这些特征。就像让某些人随时待命一样。

顾问合同

另一种给开源软件筹款的方式是签订一份顾问合同,用户可以向专家咨询,而专家则会仔细的对待这些问题。这可能包括登陆到某个系统或者临时帮程序员做debug,或解释一些稀奇古怪的错误消息,或检查代码来找出为什么它跑起来会这么奇怪。

Joyent付给我钱就是应为他们需要我来帮忙调试Node和npm的一些问题。

我想这应该是公司给开源软件开发者付钱的最通行的一种做法,相比于雇佣一个专业领域的开发者,这种做法更经济。

公司经常会使用很多不同的开源软件。假如签订顾问成本的开销比雇佣人来专管所有这些软件的成本要小,那么公司就可以从中获得好处了,他们可以花更少的钱,得到更好的服务。

此外,至少在理想情况下,这种激励并不让人反感。在产品中找问题是不可避免的,虽然这并不是很有趣。假如我知道我涉及其中,那么我会尽量写出强健,简单而容易调试的软件。这对每一个人都有好处,即使对于那些不为我的软件花钱的人。

当然,你任然可以发现有些不尽如人意的地方。但是“假如你给我X美元每个月,那么我会在24小时内回你邮件,并且每个月用N小时来解决你的问题。”要比“假如你现在给我X美元,我会在三个月内拿出你要的Y功能(而且不会有错)。”容易理解的多。在我平日的生活中,我仍然能感觉到在做着喜欢的工作,而且能帮助到有困难的用户。金钱是很有吸引力的一个东西,但不是至高无上的。

然而,这种方式任然存在很多问题。

我们的开源软件变得越来越模块化,而且相互依赖,“开发者”会变得很难追踪。这可能很快就会导致相互指责。假如你和某一个依赖我的代码的开发者签了合同,他们可能就会为了修改我的bug而迁怒于我,而且他们通常与我的想法不同。这种“分包”管理的方式看起来并不是很行之有效。

其次,大部分开源软件开发者并不擅长做顾问。擅长Javascript,运维或C语言不表示你就做给出最好的客户支持,或者你该做出那些改动。结果就是,开发者被大公司牵着鼻子走。软件变得越来越大而臃肿。这并不是一件很可怕的事,反正有大公司为它买单。但是它却忽略了中小型公司做需要的效率问题。

我们的软件变得越来越模块化和互相依赖,而且大部分开发者并不擅长推销他们的服务,那么第三个问题产生了。服务提供者必须很清楚明白自己能提供那些服务。比方说,你能对Node提供支持,但是你肯定不能保证对npm中的所有模块提供支持。

在理想世界中,这些都是可以解决的问题。这里有一大堆的问题,而且有无数的社会和技术问题等着人来解决。一个用户怎么可能找到合适的人来解决这些问题?又如何为这些问题公平的买单?你怎么才能避免开发者们在合作问题上起争端呢?

分出合理的技术水平,合理的分配收入,给正确的开发者分配任务,这是一个综合性的复杂问题。假设这不是很难而且现在已经有人能解决这个问题,是不现实的。

未来

我希望开源软件在某一天能被看成是一个“吃香的”职业,尤其是当你并没有在公司里获得一个“真正的”职位时。假如更多的人能享受自由的生活方式,而不用为他们的财政问题担心,这就更好了。这也可能应发更多的有趣的争论,像是为一个项目兼职,或是为开源软件的工作做出一些改变。

在技术行业中,人们始终能找到一起合作或创业的机会,或者给公司打工。我有很多很好的工作经历,通常是在不完全封闭的公司。然而,我并不认为这是让我们作为社会的一员而做出最大贡献的最佳方法。

那些自由软件开发者所得比不上其所付出,这是很可悲的。而更可悲的是某些人对这些做出巨大奉献的开发者的苛求。他们做出了巨大的贡献,而大部分却入不敷出。

大部分人都能做这类工作,而且可能会更有效率。但是,为了满足经济需要,他们最后还是找了份工作,这看起来是一种低效的做法。假如开源软件开发者能获得足够的动力和激励,谁能想象我们的发展会有多迅速?

附录:没有提及的话题

  1. 为什么我做开源软件的时候更有创造力也更快乐?

  2. 为什么开源软件对技术行业有帮助?

  3. 对开放才算开源?“开放源代码” 对比 “开放式开发”

我不能对这些问题做出解答,应为这篇文章已经够长的了。我的目的不是在这里卖开源软件。请在今后的文章中关注这些话题。

参考

Money and Open Source

金钱与开源(part1)

注:本文作者是npm的作者Isaac Z. Schlueter,也是nodejs的主要作者之一

“我们需要找到更好的方法来为开源软件筹资”,这是我近几年反复思考的一个问题,下面我要说的或许是一种解决方案。

大部分开发者在传统的合作方式下都干得不错。激励方式也很简单:努力工作,改善产品,帮助团队完成产品,然后取得共赢。当一切正常时,这种方式很好。你会发现自己很有归属感,而且在不断的自我提高。

但这并不适用于所有人。出于愚蠢的安全性考虑,你可能需要牺牲一些灵活性和创造力。至少,它会限制你的软件的适用范围,因此它能提供的价值也仅止于此。

自从我开始全身心投入编写开源软件,我感觉非常愉快,而且更有创造力了。事实上,我不会接受一份要求我放弃编写开源软件的工作,我也很有幸能有足够的资本来避免做出这种荒唐的决定。当然,这也可能导致很坏的结果,但是我很幸运的用这些时间来写了npm,然后JoyentNode.js开始迅速发展起来,现在我在干的正是这些。

艺术家和浪客

可悲的是,很多有望成为开源软件界超级明星的人并没有我这么幸运。没有钱是万万不能的,而最实际的赚钱方式就是找一个工作。

很多雇主希望“让”雇员用“私人的”时间来处理自己的项目,或者至少能贡献给公司的项目(通着这些项目会带有一份公司的保密条款)。这并不是很明智,“自由(free)”时间并不“免费(free)”,将精力全花在工作上会让你失去应有的生活。

有些人则放弃了在公司工作的机会,成为了一个自由职业者,换句话说,一个挨着饿的艺术家。他们用最少的时间来接一些活,然后将剩余的时间和创造力花在开发开源软件上。但这是一种很窘迫的生活方式,特别是需要供孩子上学的时候,甚至连抚养孩子都有困难。

这就是我们和社会面临的困境。开源软件给软件工业带来了巨大的好处,也就意味着,给所有工业领域提供了巨大的帮助。假如专注于开源软件的都是一些幸运和热情的人,那么这里面就存在很多未经发掘的潜力股。

假如我们想继续从开源软件中得到好处,特别是想让这种好处最大化的话,我们就得找出一种为它买单的方式。除了能让开源软件开发者吃饱喝足以外,付款也能将他们的努力与现实世界联系在一起。

下面我会提到一些这个领域的开发现状。每一个都一些致命的缺点,而且我还能找到解决这些问题的办法。我不认为这会让我们觉得悲观,这些需要解决的问题反而让我们觉得正朝着正确的方向前进。

合同,训练,咨询……

许多开发者通过签订短期合同来为喜欢的项目工作。虽然这有时是一个不错的赚钱养家的方式。但是我并不认为它从根源上解决了开源软件筹资的问题。

这与成为一个传统雇员,然后用空余时间来处理开源项目没什么不同。这并不能让开发者直接通过他们的开源项目来盈利。就像做培训和别的一些活一样,这并没有真正的为开源软件筹资,这只不过是你通过开源软件找到了一份工作而已。

话虽如此,合同当然可以资助一个人其他的开源活动,同样提供临时援助也能帮助创业者步入正轨。

专职员工

一种为开源软件筹资的方式是让雇员在工作时间为开源项目做贡献。这正是Node核心项目的运作方式。Joyent,StrongLoop, Voxer, Mozilla, LearnBoost, 和Microsoft的雇员都会将他们的部分工作时间花在Node上面。

(注:我没有在这里囊括那些用私人时间开发项目的人。这当然也很重要,但是“动用你的私人时间来做项目”正是这里遇到的问题。)

这样的效果相当好,事实上带来的好处是,能让软件更符合他们大部分用户的需求。当公司付给你薪水的时候,你就会觉得有必要为自己的代码负责。假如一个团队完全不考虑用户需求的开发软件,这会显得相当的不靠谱。

任何能给开源软件生态系统带来益处的方法都值得鼓励。 但是这种方式也存在一些问题。

当一个社区项目由现实中的企业驱动时,往往企业会利用它的优势地位,来做出一些符合自己利益的改变。从长远来看,这会损害项目的可靠性和预期目标。Joyent的高级副总裁Bryan Cantrill对这些事情可能导致的严重后果有一个很精彩的演讲:企业反开源模式:大错特错

就算企业能考虑尽量避免上面的情况发生(这样做得很少),表面上(对企业)的优待条件也会给社区带来损害。开源社区的很多人认为企业的利益本质上就是邪恶的。这样说并不客观,但是考虑到这些企业的反开源模式,这样说也并不是毫无道理。

此外,这种解决方案的可能性微乎其微,作为企业雇员,开源项目完全符合企业利益的情况是微乎其微的。当然,大多数使用Node的公司可能不能让Nodejs核心开发者雇员做出一个像样的商业应用,嗯~,可能是运气的问题吧。

实在太长了,未完待续… :)

参考

Money and Open Source

软件极简主义

今天回家的路上,竟然刮起了大风,虽然骑行艰难,但是想想公交中的闷热,倒是觉得凉爽多了。

在路上的时间是无聊的,于是就喜欢胡思乱想,很多自以为很棒的点子其实都是在这种不经意间想到的,办公室里的久坐反而显得效率低下了。回味一下最近做的几个项目,高屋建瓴的想想当初的设计,突然很想写一些关于软件设计的文章。就着饮料和巧克力(来点酒么?),今天就来写写软件极简主义吧。

Google了一圈,也没找到对“软件极简主义”的定义,姑且当做是我的独创吧。一般认为“极简主义”是设计界的一种风潮,但是软件发展至今,好像也渐渐有了这样的趋势,甚至我认为这是未来的必然,我们经常听人说“flexible”这个词,字面上来看就是“灵活的”,但是具体到这个软件是否灵活,就不太好判断了。但是,简单的软件,一定是灵活的。

极简主义的的大敌

软件极简主义的三个大敌:配置文件,冗余的参数,和大量复杂的接口。

很多人热爱配置,迷恋配置,认为越多的配置项意味着软件越强大,适用范围越广,但这是九十年代的事了。实际我们仔细翻翻常用的软件,90%的配置都是多余,没有人明白他是做什么的,也没有人希望去改变他。比方很多软件的configure文件,常常能列出上百个配置项,但是我们真的需要这么多吗?不,我们需要默认的那些值就行了。何谓默认?因为软件的设计者觉得这些是最优化也最有可能被选择的配置,那么既然是最优配置,我们又有什么理由去改变他们?

再说说冗余的参数,linux中有一个非常强大的命令’tar’,从man文件看来他起码有二十来个参数,但是我真的需要这么多参数吗?其实我只要记住压缩是-c,解压是-x就可以了,那么何必为了1%的功能而去加上这99%的参数呢。

最后是复杂的接口,举个栗子,全文搜索引擎solr非常强大,能满足我们对于文档索引的各种需求。但是他使用起来可不简单,原因我想就是因为他那种sql式的查询接口,把一件很单纯的事情搞复杂了。我们来设想一下,需要找出包含某几个关键词的文章,必要的条件是什么?关键词,文档,没了。而文档是存储在服务器的,为什么我们提供了关键词之后,仍需加上各种条件,他才能告诉我们想要的答案呢?我想软件发展到一定的智能,他就应该像一部能说话的百科全书,提问,然后告诉我们答案即可。

凡事都要对比着看,所以我们找点软件来对比一下。

redis 与 sql

redis很灵巧,所有源代码加起来不满5M,但是他很强大,hash结构能取代我们80%对于sql的需求。他也有配置文件,但是选项很少,而且每一项都有详尽的注释,并且使用默认配置就可以应对大部分的情况。唯一值得诟病的就是他的接口种类繁多,但好在这些接口很有规律可循,你只需了解了redis的基础数据结构,那么跟着官网的文档就很容易搞懂所有接口的用途,而且大部分的接口都只接受3个以内的参数,这可好记多了。我刚接触redis的时候,只花了半个小时就能玩得起来,我想面对sql恐怕没人能这么轻松的掌握吧。

cake 与 grunt

cakegrunt是nodejs中两个管理任务的模块,后者的名声更大一些,前者甚至不能说是一个模块,他只是coffeescript中附带的一个小工具。我曾尝试使用grunt来做任务管理,但是当我看到grunt官网那长长的一段initConfig时,就望而却步了。就像是我希望在墙里敲个钉子,你却给了我一台破城锤。我只不过想要给每个任务起个名字方便我以后调用和查阅而已,所以cake的一行命令足矣。

zmq 与 rabbitmq

zmq是我见过的最具有极简主义风格的软件(组件)。一方面他要面对的任务非常繁杂,在异步通信中所有我们可能遇到的情况,他都为我们考虑到了,但是他又将底层的复杂问题掩盖起来,让我们看到一个光滑的表面,深藏功与名。同样来看看他的同行rabbitmq,关键词:中心服务,多线程,模式单一,最后一个特点,慢!而仅有1.7M的zmq,快是最直观的感觉,而分布式和扩展性则是锦上添花。有人说zmq就像乐高积木,每个人都能搭出他想要的形状,这话一点都不错。

不是结束的结语

软件的设计日新月异,将来肯定会接触到更多优秀的软件,也许哪天我想法变了,也许哪天遇到了更神奇的方案,可能我会补充在这里。

介绍 Coffeescript 中的列表推导式

列表推导式是一个很著名的语法结构,它的特点是能让代码更简短,优雅,而且易于阅读。捎带些函数式编程特点的语言都支持这种语法结构,例如lisp家族和python。coffeescript作为一门年轻的语言,自然而然的继承了这个特点。

我们先看看这种语法和普通循环的区别:

1
2
3
4
5
6
arr = [1, 2, 3]

for i, v of arr  # use for..of loop
    arr1[i] = v * 2

arr2 = (v * 2 for i, v of arr)  # use list comprehension

可以看到本来需要两行的代码变成了一行,这对于略有装逼犯情结的码农来说,心理上的满足感自然是无与伦比的。优点也是显而易见的,就是可读。在有些语言中,这种语法还会产生一个新的作用域,不会污染外界的变量,比方说ruby。

我们再来看看一些进阶用法,下面是带上if条件的列表推导式:

1
2
arr = [1, 2, 3]
arr1 = (v * 2 for i, v of arr) if arr?

一般的列表推导式返回的结果是一个一维数组,这在我们需要对某个object中的值做转换时会产生不便(key会丢失),这个时候我们可以采用一种变通的方法:

1
2
obj = {a: 'a', b: 'b'}
obj1[k] = v + v for k, v of obj1 if obj1?

没加两边的括号和加了括号是有区别的,像上面这种结构,可以理解为将for..of结构中的第二行搬到了等号左边,其中的临时变量k, v当然也是可以直接使用的,而且后面if条件是对整个循环生效的,而不是单独加在每个循环中的,比较好理解吧。

熟练掌握了列表推导式之后,编写代码的时候会更加得心应手,对于代码重构,想必也是极好的。

相关文档

一个例子验证 Do 在 Coffeescript 中干了什么

使用jslint的时候有可能会见到这样的提示

Don’t make functions within a loop

一直没有太在意这个警告,直到最近做项目的时候还真的碰到了因为这个问题产生的bug。

那么下面就用一个例子来看看在循环中定义方法会产生什么样的后果吧。

1
2
3
array = [1, 2, 3]
for num in array
  setTimeout (-> console.log num), 1

得到的结果是’3,3,3’,而不是预期的’1,2,3’,先不说为什么,我们来看看coffeescript给出的解决方案。

1
2
3
4
array = [1, 2, 3]
for num in array
    do (num) ->
        setTimeout (-> console.log num), 1

在这里不得不佩服Jeremy Ashkenas的无限创造力,短短一个do,就解决了这么让人纠结的问题。下面来看看编译成javascript之后的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function() {
  var array, num, _fn, _i, _len;

  array = [1, 2, 3];

  _fn = function(num) {
    return setTimeout((function() {
      return console.log(num);
    }), 1);
  };
  for (_i = 0, _len = array.length; _i < _len; _i++) {
    num = array[_i];
    _fn(num);
  }

}).call(this);

下面我们来解释一下为什么上面的代码会有问题,以及这个do为我们做了些啥。

关于javascript的作用域,我们可以看一下这篇文章的引用

JavaScript’s scopes are function-level, not block-level, and creating a closure just means that the enclosing scope gets added to the lexical environment of the enclosed function.

大意是说

JavaScript的作用域是方法级别,而非块级的。创造一个闭包可以将作用域限定在这个封闭的方法中

这里的for..in循环在其他语言中就是一个块级的作用域,但是Javascript并不买它的帐,于是最后在方法中调用的num就变成了整个作用域中最后的状态(3)。解决的办法就是在循环中创建闭包,让num当成参数传入闭包,那么它在方法作用域中就不会受外部的变化而改变(实际上完全可以当成一个新的变量,不信你传个object进去,在闭包中的任何修改,都不会对外部作用域的object产生影响的)。

coffeescript用do关键字为我们将这种操作最简化,所以,尝试一下吧。

参考文档