网站程序分析:少写一个var毁掉一个网站
恩,这是名副其实的杯具(很抱歉我用这个词,shitstorm)。长话短说,如今MelonCard已经被TechCrunch使用,然而所有事情都可能突然出现问题。过去几天里,我们对MelonCard进行了巨大的改进,使用NodeJS长轮询机制以及平滑的KnockoutJS动态jQuery Templates前端。在完成站点无缝升级,在达成“所有功能都是最新”目标的同时,成为外观更美、体验更好的产品。为了避免可能发生的不良影响,我们进行了手工测试和单元测试,并为Node结合使用了一整套Vows。完成所有的系统测试,朝着目标全速前进,对吗?事实并没有那么快。
译注:
长轮询(long polling):是Comet的一种实现方式,也是Facebook,Plurk实现动态更新内容的方法,具体原理是发送一个长时间等待的request,当服务器有资料response的时候立刻断掉,接着再发送一个新的request。
Vows:Node.js的异步行为开发框架
我们的系统用的是NodeJS,根据用户输入决定他的当前状态,例如用户输入“我正在等待更新这两条记录”,服务器(基于时间戳检查)会返回“您的记录已经是最新的”或者“记录xxx已更新为yyy。”(实际的过程会比这个更加复杂,我们使用了Redis共享变量和会话,并对Rails、mySQL、Redis和Node之间的接口进行安全检查)。(这个过程)看上去非常简单,但当事情不如预期的那样时,即使是简单的NodeJS 代码也会成为噩梦。今天噩梦发生了。
处理完今天的日常工作之后,我们遇到了一个正常的新用户注册高峰(一个小时内有50-100个新用户注册)。突然之间,所有事情都开始出现问题,没有一个页面可以正常工作。我们的邮箱开始被“你的产品挂掉了”类似的邮件塞满了。我抓起一杯咖啡准备战斗。
我的第一个反应是:NodeJS能够很好地处理负载,这是众所周知的。50或100个用户不可能让系统崩溃。在得到Ryan Dahl的帮助之后,我们知道这不是Node本身的问题(后来的结果也验证了这一点)。服务器开始返回异常结果,用户输入“我的记录是a、b和c”然而服务的回答是“你这个蠢货,删掉x、y和z这里的记录是a、b和c。”即使能够确定问题的范围并可以重复错误,也几乎不可能通过Node可怜的错误处理和调试功能解决。采用的方法就是下面的unix命令(是的,我对production进行查询)
NODE_ENV='production' node/privacy.js | grep "Returned results"
你能够想象对这些结果进行分类是件多么恐怖的事。结果是,所有的分段测试和单元测试都正常,我已经束手无策了。最为重要的是,我们的系统再一次执行了大量的(安全性)会话检查。例如,如果用户在(浏览器)不同的标签页之间进行登录和退出时,会有大量的“Unauthorized (未授权)”错误弹出(这让企图了解真正问题的我们更加糊涂)。当我列出错误的时候,看上去像这样:
Trace: at EventEmitter.<anonymous>(/—/node/privacy.js:118:11) at EventEmitter.emit (events.js:81:20)
错误出现的那一行(或唯一返回给我的Node)如下:
process.on('uncaughtException', function (err) { console.log(['Caught exception', err]); console.trace(); });
虽然程序还没有崩溃,但我还是没有找到线索。在这里最佳实践也没有办法捕捉那一行的错误(手动前端测试,单元测试,错误处理,等等)。的确,我应当做负载测试,但是即使(测试)到达了竞争条件同样不能让我感到安全。
4个小时以后(当我翻到第503页“ Temporarily Unavailable”,这时我的联合创始人对每一位失望或好奇的用户回复了致歉的邮件),我意识到问题出在服务器将我的请求输入(请求参数)错误地当成随机用户请求参数。确切的说,服务器的设计只会对你的请求返回你的相关信息,但是对你的请求产生了错误的理解。比如你说“我喜欢苹果和甜瓜”,但是服务告诉你“不要傻了,你喜欢的是芒果。”所以,虽然(从安全的角度来说)一切都是安全的,但是结果是错的。为什么我的ExpressJS服务器不能理解我的请求呢。我继续追踪发现了下面的代码:
app.all('/apps/:user_id/status', function(req, res, next) {
// …
initial = extractVariables(req.body);
});
看上去很糟糕是吧?这是个该死的策略。我不是个JavaScript专家,但请允许我尽我所能为你解释(或者也刻意看看这里)。在JavaScript里,你可以声明变量为函数(局部)变量或者全局变量(在变量声明/引用的作用域链表上为其增加复杂性,直到最后确定为全局变量)。当我没有使用var来声明一个全局变量initial时,它会遍历作用域链表直到全局范围,最后会创建一个全局变量initial。当下一个请求来到时,它会遍历同样的链表并重写该变量(另一个请求仍然希望使用这个变量)。每个请求都会重复这一过程。当服务器试图在接下来的处理中回复每个请求时,它会读到不断被截断的变量,并返回的结果不正常,甚至可以说是荒唐。因此我应当这么修改:
var initial = extractVariables(req.body);
这样会在我的匿名函数作用域内创建这个变量,因此接下来的请求就不会截断这个值。这是新手才会犯的错误,但是在我做的所有调试或测试中都完全不会带来竞争/并发问题。
现在我要对你说的是:你应该用过 CoffeeScript并且可能对TameJS很在行。你可能是正确的。我想理解第一次执行NodeJS调用了哪些函数,但这对于我的公司是一个不必要的损失。在其他情况下,它可能带来更严重的问题(假如我在会话中错误的使用了变量会如何?)。最重要的是,缺乏真正好的错误处理(在Rails,我们通过backtrace跟踪错误并把错误发送给开发团队)以及真正的调试(依靠的是grep和less命令)让我觉得我们离好的开发差距很远。或者也许我应当更加小心。
在4个小时宕机并令上百个用户失望而归之后,我找到了问题的症结并修复了产品。风雨之后,拨云见日。我们开始对受到影响的用户致歉,盘点这次事故的损失并继续工作。但是,缺少一个关键字带来这样的损失还是让我感到难以释怀。少写了一个var会让我成为坏人吗?
英文原文:How One Missing `var` Ruined our Launch by -Geoff
翻译地址:唐尤华,伯乐在线。