构建Node.js应用的10个重要的方面
目标
应用程序需要有个明确目标,能帮干活能解决问题。这一点同样可以用于你决定去做的每件事上。目标明确根基夯实,应用程序才会越做越好。明确的目标会帮你闯出出一条如何正确解决问题的道路、也让你在遇到阻碍陷入困境无法前行的时候依然坚定执着着望向远方。。。
架构
架构会考虑到源代码的设计,文件排列,程序库/模块使用,他们交织在一起编织出了应用程序。架构形式会随着应用程序的开发不断发生变化,它可能是一个web应用服务器,像express这样的处理静态资源(比如图片),也可能是一个scheduler/worker pipeline,排队以及处理队列中的项。
无论何种目的,都有一些通用的原则应该遵守。
- 模块化。尽量保证你的代码遵守DRY原则(Don't Repeat Yourself(不要重复自己))。如果你发现你需要在许多不同的地方使用一段相似的代码,通常应该将他们放在一个单独的文件(或模块)的一个函数中,从而形成通用帮助函数集模块。这个模块可以在其他依赖它的地方使用require()函数来加载。这样做的目的不仅仅是防止多次重写相似的功能,并且当你升级功能时,你只需要修改同一个函数即可。
- 遵守Node规范,尽量保证第三方node模块放在node_modules文件夹下。同时也应该保证node_modules在你的.gitignore里,这样你就不会提交不相干的依赖文件。
- 分离关系。前端相关的内容(静态CSS,javascript,HTML,模板文件,图片,资源文件)应该与后端应用逻辑(路由,服务器,中间件)分离。同样的,应该将部署脚本,配置文件,数据关系文件和测试文件分开放置。
部署
你应用到生产环境的方法很大程度上依赖于你堆栈的性质。以下是我们尝试过的一些方法:
- 手动通过SSH传到服务器和拷贝git仓库。优点:全手动控制,零部署工具设置。缺点:大量服务器时难以实施。所有的事情都需要手动设置,因此你不会得到任何好处,像upstart / initrc supervisation或者没有日志的运行。
- Capistrano.优点:对于团队开发者来说是标准的流程,只需简单的运行:cap deploy。缺点:难以配置,需要Ruby依赖。
- Chef 脚本. 优点:通过脚本安装程序。缺点:每次想部署时,需要重启服务器。Chef最常被用于服务器的安装/配置,而不是应用程序部署。
- Deliver. 当我们厌倦了其他的一些操作时,我们可以使用GoSquared出品的这个部署工具。
它的灵感来自于Heroku’s的git推送系统的基础部署。你只需要为应用程序配置一个系统用户(那些你需要做的——我们都帮你自动做好了),建立一个基本的分发配置,在工程里运行分发的命令行。它使用git通过SSH把应用程序推送到你的服务器,可以使用 foreman或者equivalent来监听应用程序的启动,复位和恢复。
这并不是一个详尽的部署方案的列表,你可能需要发挥一点创意来构想一个最适合您自个需求的解决方案。无论你采用何种策略,将部署配置文件纳入您的应用程序的源代码版本控制并将部署流程记录在您的README文件中都是一个好主意。
配置
几乎每个应用程序都有一些常量和设置需要能被方便地更改。常见的有主机名,端口号,超时时间,模块选项和错误。在一个地方保存这些值将非常有利,可存在一个文件或多个文件中,如果这些值足够多。这样做可以使他们能更快地被修改,而不必花时间在梳理代码以跟踪查找到它们。
我之前只是转储配置的设置到一个输出配置属性为对象的文件。这在一个非常特殊的环境下运行良好,比如在生产环境的时候,但随着时间的推移,这开始成为维护的一个瓶颈,变得没有条理性,并且在应用改变时产生了多个文件作为一个基础设施。
我们可以通过环境配置获得想要的结果,这个想法是指你可以在应用程序运行时修改基于环境的配置值。这个方式很简单,输出一个叫做$NODE_ENV 的shell环境变量包含一个你将要运行程序的环境模式标识。你的应用程序将定制配置的设置,当它启动时使用你定义好的环境。
环境方面的配置为你提供了贯穿整个应用程序生命周期的更多的灵活性,你应该可以在本地开发和运行你的应用程序,而不需要网络连接(你想要在火车上能够进行?事实上写这篇文章时我正好在火车上),这需要为本地服务指定主机地址和端口号。然后,你可能会希望在部署到服务器之前能够进行测试。这些都需要不同的配置。
我们一般使用 node-config,一个为工作精确设计的模块。所有你需要做的是在config/default.js中定义配置的值,接下来为你不同的环境建一个文件,包含继承了default.js的默认指令。你设置$NODE_ENV作为环境变量的名字,这个模块会覆盖defaults.js中定义的[$NODE_ENV]属性。导入到你的应用程序合并配置对象,你只需使用require()就可以。
日志,度量和监测
你想给自己足够的应用的不正常行为的证据,以至于你就可以在尽可能很少的时间内使‘b0rked’变为”所有错误已经修正“。一个最好的办法是依赖日志。通常的前提是,如果得到了错误,记录它。你必须遵从node的错误处理规则,在Callback的第一个章节已经预设了当一个错误发生时的错误信息:
makeRyanDahlProud(function(err, result){
if(err){
console.log(err);
}
});
不管怎样,如何应对错误完全在于你。你或许想记录错误,然后继续。或者你想终止执行这个回调。无论如何,你应该在未来应用这些错误,而且记录日志是最简单的实现方式。
尽管记录错误日志是一个很好的方法,但是也会导致大量的消息被发送到logs日志或者终端。TJ Holowaychuck开发了一个叫做 debug的模块,允许为日志消息创建命名空间,通过这途径,我们就可以过滤掉不必要的消息,而通过命名空间抓取我们需要的消息。TJ的 repertoire还有大量其他模块。
指标
应用指标为我们提供了有用的信息,以便查看应用在做什么操作和这些操作的时间间隔。它给我们提供一个重要的途径去检测异常的事件,瓶颈还有扩充收缩计划的参考。我将这些归结到一个小的模块,叫做abacus,它可以帮助我们维护计数器的集合,并通过statsd把它们以可视化绘图形式描绘到graphite。实践证明,保证应用在给定的参数下运行时非常方便。
监测
监测并非是必需的,但是监测是一个很好的途径,让我们了解运行应用的服务器的资源使用率。另外的早期警告系统,同样非常重要,可以让我们避免一些低级问题而导致的应用崩溃。没有比服务器硬盘空间不足而导致的应用崩溃,或者CPU使用超过负载而崩溃,而更让人窘迫。
现在有大量的监测工具或者服务:开源的有Ganglia, Monit, Sensu ,还有SaaS服务ServerDensity, NodeTime and NewRelic 。
容错
在部署的时候也需要考虑的,就是你还需要考虑如果你的应用崩溃后,将会发生什么。应用最好在系统监测程序下受控,例如Ubuntu的upstart。配置upstar十分琐碎,但可以在应用崩溃时,处理开始,结束,和重启,所以还是值得的。Foreman有一个导出功能,可以为你的foreman-backed应用生成upstar配置。
即使应用可以在崩溃后重启,但是这样做的含义是什么?这样会让服务在某些时候中止么?会丢失数据么?会留下一些为完成的任务么?一串问题你需要在设计时考虑,冗余是一个很好的解决方案。例如,你需要在多台服务器之间传输数据,可以考虑增加一个反向代理(负载均衡,例如 HAProxy 或者web服务器,例如 nginx),从负载中移除应用中的不完善实例,直到健康检查达到合格分数为止。
效率和可伸缩性
也许在开始阶段我们很少考虑,但随着你的应用的成长,事务压力会不断增大,你也许需要考虑让应用变得更高效,甚至是可伸缩性。这里存在的风险是过早的优化。你不需要担心让你的应用拥有超强的可伸缩性或者拥有超级性能,因为在真正面对这么高的负载的之前,你为什么为此而苦恼呢?你还是把最宝贵的时间放在核心组件的构建上,或者"最小的可行性",就如,刚开始启动,至少先到达需要扩展的阶段。
当你的应用在单节点和单实例的情况已不能满足需求的时候,你有几个选择,所有的权衡,就像在雷区中摆布一样。首先要做的是,找出瓶颈。为什么应用会那么慢?是不是CPU占用率已达到顶峰值?是不是硬盘I/O不够大或者是否足够的连贯?
有时候最直接的答案是把应用迁移到拥有足够资源的服务器上。如果这对于你来说,是可行的,那么任务就比在多节点上重新架构你的应用要简单的多,但是如果应用增长速度很快,那这个可不是最好的解决方案。但是这个方法可能会消耗你大量的时间去构建。
在多个服务器上横向扩展应用,是一个棘手的方案,并且引入大量的错误,但是长期来说却带来生命力和令人着迷的技术挑战。
文档和组内协作
一个应用如果没有文档,就像一个包装盒没有说明书一样。你可以找出需要做什么,但这样做是笨拙的,浪费时间和不精确的。最好你能提供清晰的文档描述,简要说明你的代码。这样做不单单是帮助其他人很快的上手和快速的开发,而且还能帮助你在六个月后重新拾起这个应用,修复臭虫。
我并不主张像写小说或者教程一样去描述你的代码。很多代码可以自解释,只要代码足够的简洁。相反,你的文档需要描述的是,代码未能解释的灰色地带。注释可以描述代码的功能性,还有描述设计的理念,平衡,依赖,陷阱,边缘例子和其他需要考虑的东西。随着应用的成长,代码的描述文档也会增长,以反映应用变得越来越稳定的过程。在应用还处于初级阶段的时候,没必要太投入去编写文档。因为文档在初期会经常改变,当你花了很多时间在它身上的时候,可能会很快又被废除掉。
每个应用程序都应该包括一个README[MD],其中包含所有需要知道是应用程序正常工作的细节。通常这包括:
- 应用程序其目的的简短说明
- 安装说明
- 启动说明
- 测试说明
- 如何部署
- 其他需要知道的点
我们需要一个模块提前代码里的注释并生成干净,有吸引力的文档。我们会使用一个叫 docker 的模块在我们的app中。
测试
以前我压根没有认识到测试有多么重要,也没有被(QA)烦过。可能是因为我(太牛了)从来没有遇到这样悲催的情况:多年以后,之前开发的应用开始出问题了,你无法保证哪部分还正常工作,哪部分就快瘫了。可是现在,经历过这些狗血的事情后。我知道,我们需要测试,我们需要伟大的QA!!!
严格的测试可以造就好的应用。你需要考虑横向地把应用切分为组件,而让组件可以单独的测试。这超越了基本的单元测试的迂腐,包含更多信息的组件和集成测试,可以让你找到一个方法,以保证你的应用可以很好的集成在一起工作。
最好的方式是在你开发应用的时候编写测试用例(一旦你有足够的信心,你测试的功能不再有变化),那样随着应用的开发进程,测试也可以得到足够的保证。这样可以保证在开发新的功能的时候不会打断现有的功能。测试同样给其他开发者提供一个很好的例子,理解应用各个组件如何工作和结果是什么。
现在有大量的测试框架可供选择,不同的技术( BDD, TDD). 我们尝试过 vows 和 tap,但是我目前更喜欢用 mocha和should.js,因为我觉得使用它们,比使用纯javascript,更能平衡架构和工具,同时允许使用同样的代码库,脚本文件,而且还能在你的测试用例中启动服务器。
依赖
Node拥有一个强大的模块系统和被称为npm的包管理工具,npm能帮助你无缝的集成各种模块到你的应用中。npm给你提供了一个非常丰富的开源模块的目录 索引,你可以找到你想要的。
一旦你知道你的应用需要什么组件(例如和redis数据库沟通的是什么客户端),明智的选择是首先检查现有的配置索引中的用户级节点模块。Node.js是黑客们中最喜欢的实验工具,所以这里有很多模块可供你尝试。自然,这些组件的质量都不一样,所以你需要了解哪些是好的模块,哪些却不是(名单:README, 测试,例子,作者),但是基本上可以提供给你所有你需要的。