Factual API:Node的绝配应用

大家都听说过Node了。快、可扩展、并发性,这些都是它的特点。在Factual公司,让我们感到自豪的是,往往可以找到正确的工具来完成工作, 从jquery到hadoop,postgresql到mongodb。更幸运的是,我们有种工程师文化,鼓励我们实验一些新的用法。但是,每种投入产品 环境的技术,都必须经过严格的检验和衡量。我们考察的方面包括敏捷性、性能、稳定性和成本。
关于Node,有很多开发者被误导,以为用上了node,你就马上拥有了并发性和性能。事实是,你得配合各种技术,才能解决一个非常特别的问题。那 么,Node到底能解决什么问题?它是怎么解决的?必须付上什么代价?下面介绍的就是我们的一些简要的经验,以及我们是怎么使用Node的。
我们产品的第一个迭代是一个可以互动的网格(grid),用户可以上载、更新、重组我们的数据,并把数据表(dataset)展现为各种有用的表现 形式(visualization)。那是一个复杂的web app,我们用了很多JavaScript。那时我做了一个Ruby库,给JavaScript加上了一点基本的面向对象编程的特性(现在,这个库叫做 Mochiscript)。
从那以后,我们的业务重心偏向提供更有质量的数据,并通过API快速传递这些数据。这个业务重心的转移带来了一系列新的需求。我们现在可以支持的相 应速度是:200毫秒一个请求,包括用户认证,权限检查,实时统计和查询处理。我们必须用一个敏捷的语言实现它,因为需求会不断地增加变化。
当时由于我们都擅长Ruby,我们的第一个原型是用轻量级的Sinatra实现的。那个版本的表现的还不错,可以支持同时120个连接,最小相应时间20毫秒。但是从我们所需要支持的流量来看,这个方案的扩展性还不够好。
然后我们考虑了Java和Clojure(这两种语言都正应用在Factual的其他项目中),但是最后我们决定试试Node,因为敏捷的需要,也因为我们对JavaScript的熟悉。
首先我们把Mochiscript从Ruby导到Node,看看Google的V8引擎有多快,然后,就决定写一个原型,让它来跟Ruby版本的原 型竞争上岗。差距是明显的!Node版本的性能是,可以支持同时400个连接,最小相应时间10毫秒。在接下来的几个月里,原型变成了产品,Node的高 性能和Mochiscript组织代码的优越性帮了我们大忙。我们又做了一些优化,现在的相应时间已经可以达到最小5毫秒。
在尽情歌颂Node之前,先罗列一下我们付上的代价:

  1. 这个框架还不够成熟(在他们的早期http库中,我们发现了一个socket泄露的bug)
  2. 像意大利面一样杂乱的代码:到处都是callbacks。这个问题可以通过一些办法来解决,比如:一个好的设计,遵循一些编程规范,等等。但即便如此,还是不轻松。
  3. 有时候调试程序会让人抓狂(大部分是因为上面两个原因)

为什么Node适合我们的原因如下:

  1. 我们以前的性能瓶颈在IO
  2. 我们每个请求需要用到的CPU很少
  3. 我们可以用事件驱动编程,来帮我们大量使用缓存

前面两点理由并不新鲜,而这两点的结合正是Node的强项。这两点完美地适用于我们的情况。而第三点,是我们想着重介绍的。
对于很多工程师习惯的方式来说,用Node写程序需要有点儿思路转换。那些Callbacks会失控的。然而,Javascript提供的闭包带来了新方法。对我们很有用的一个方法,可以用来减少多余的数据库访问。
考虑一下下面这个例子──从数据库中获得用户数据:

var connect = require('connect'); var users = require('./lib/users'); var app = connect.
createServer(); app.get('/users/:userId', function (req, res) { users.get(req.params.
userId, function (err, user) { res.end("Welcome " + user.name); }); }); app.get('/stats',
 function (req, res) { res.end(JSON.stringify(counts)); });

现在让我们加入一个缓存层:

var connect = require('connect'); var users = require('./lib/users'); var app = connect.
createServer(); var userCache = {}; function getUser(id, cb) { if (id in userCache)
{ cb(userCache[id]); } else { users.get(id, cb); } } app.get('/users/:userId', function
 (req, res) { getUser(req.params.userId, function (user) { res.end("Welcome " + user.
name); }); });

既然我们用到了缓存,就得考虑怎么清除它。Redis的pubsub功能绝对适用这种用法,我们用它来实时更新缓存:

var connect = require('connect'); var users = require('./lib/users'); var app = connect.
createServer(); var redis = require('redis').createClient(); var userCache = {};
function getUser(id, cb) { if (id in userCache) { cb(userCache[id]); } else
{ users.get(id, cb); } } redis.subscribe('update-user', function (id) { delete userCache[id] });
 app.get('/users/:userId', function (req, res) { getUser(req.params.userId, function (user)
{ res.end("Welcome " + user.name); }); });

再来加点儿好玩的东西──访问统计:

var connect = require('connect'); var users = require('./lib/users'); var app = connect.
createServer(); var userCache = {}; var counts = {}; function getUser(id, cb) { if
 (id in userCache) { cb(userCache[id]); } else { users.get(id, cb); } } app.get('/users/:
userId', function (req, res) { getUser(req.params.userId, function (user) { if (req.url in counts)
 { counts[req.url]++; } else { counts[req.url] = 1; } res.end("Welcome " + user.name); }); });

这样的模式是Node或者JavaScript这样事件驱动模型的一个副产品,还真是给了我们不少灵活度,我们可以方便地加入缓存、实时统计、还有 实时配置等等。根据我们的经验,用redis或者memcache来做缓存也是很不错的,但是我们的目标是榨干Node服务器的最后一滴性能,所以我们要 限制任何多余的操作(甚至尽量不去parse json)。
我们理解Node不是所有问题的解决方案。但在我们的应用里,Node是一个绝配。我们大概从一年前开始适用Node,这一年来Node获得了长足 的长进,而我们也从Joyent和开源社区获得了很多帮助。我们开始在其他各种内部项目中适用Node,渐渐地这成为一种通用解决方案而不再小众。我鼓励 各位开发者来探索Node,看看它是否适合你的问题。至少,它会让你思考IO,以及你要花多少时间等待IO。

原文链接:http://blog.factual.com/factuals-api-a-good-fit-for-node
(作者:Jeff Su 译者:Forrest Cao)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Grow your business fast with

Suku