若是你看过 2018 Node.js 的用户报告,你会发现 Node.js 的使用有了进一步的增加,同时也出现了一些新的趋势。html
能够看到愈来愈多的前端开发者们具有了全栈的能力,更多的核心应用开始基于 Node.js 开发,而其中,保障应用的稳定性是每个开发者的“头等大事”。前端
稳定性是什么?通常来讲,指的是应用持续提供可用服务的能力,一旦应用频繁不可用或出现故障没法及时恢复,对用户的使用体验都是巨大的伤害,甚至会形成不少更严重的后果。稳定性保障不只仅是开发阶段的事情,它应该是贯穿应用的开发、测试、上线、监控等,覆盖整个 DevOps 生命周期的事情。node
自己阿里云提供了丰富的产品和服务来支持整个 DevOps。nginx
包括 Code 代码托管、PTS 性能测试、SLS 日志服务、云效 等等。git
本文也将围绕整个 DevOps 生命周期,来介绍基于阿里云的 Node.js 稳定性保障的实践。程序员
稳定性的保障从应用开发阶段就已经开始了,这部分也是相关资料文章最多的,相信有追求的开发者都会关注而且已经应用和实践。github
应用运行过程当中不免会有异常发生,再大神的程序员也不敢保证本身写的代码不出问题。其实出现异常不可怕,可怕的是异常没有捕获,进而引发应用进程 crash,致使应用不可用。数据库
正常来讲,捕获异常有一下几种方式:后端
try/catchapi
try/catch 是捕获异常的经常使用方式,能够帮助咱们可控的捕获错误,可是 try/catch 没法捕获异步异常。
try { setTimeout(() => { throw new Error('error'); }, 0); } catch(err) { // can't catch it console.log(err); }
上面的异步异常使用 try/catch 是没法捕获的。捕获异步平常咱们可使用一下的方式。
异步异常
callback 异步回调
经过异步回调来处理异步错误多是目前最普遍的方案。
function demo(callback) { setTimeout(() => { callback(new Error('error'), null); }, 0); } demo((err, res) => { if (err) console.log(err); });
固然,callback 方式存在一直被人诟病的嵌套问题
promise
使用 promise 能够经过 reject 抛出错误,经过 catch 捕获错误 ``` new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('error')); }, 0); }) .catch(err => { console.log(err); }); ```
generator
使用 generator 可让咱们使用同步的代码写法来调用异步函数,能够直接 try/catch 来捕获异常
function* demo() { try { yield new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('error')); }, 0); }); } catch(err) { // can catch console.log(err); } } yield demo();
async/await
async/await 应该是目前最简单和优雅的异步解决方案了,写起来和同步代码同样直观,能够直接使用 try/catch 来捕获异常
const demo = async function() { try { await new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('error')); }, 0); }); } catch(err) { // can catch console.log(err); } };
uncaughtException
当异常抛出未被捕获时,会触发 uncaughtException 事件。只要监听了 uncaughtException 事件并设置了回调,Node 进程就不会异常退出。
process.on('uncaughtException', function(err) { console.error(err); });
可是这时异常的上下文会丢失(respond 对象),没法给用户友好的返回。并且因为uncaughtException 事件发生后,会丢失当前环境的堆栈,可能致使 Node 不能正常进行内存回收,从而致使内存泄露。所以,使用 uncaughtException 的正确作法通常是,当 uncaughtException 发生时,记录详细的日志,而后结束进程,经过日志和报警来及时的定位和排查问题。
为了弥补 try/catch、uncaughtException 的不足,Node 新增了一个 domain 模块,能够捕获异步异常而且不会丢失上下文。
听起来很完美,可是该模块目前是不稳定的(Stability: 0 - Deprecated)。同时其可能存在稳定性和内存泄露的问题,所以要谨慎使用。
通常来讲,咱们开发 Node 应用,只须要关注咱们应用逻辑异常的捕获便可,自己咱们使用的 Node 框架,好比:Egg、Midway 等都会在底层帮咱们进行处理,保证一些咱们不可控或者未预期的异常出现时,不会致使应用崩溃。
虽然框架帮咱们进行的兜底,可是依然须要咱们针对本身的应用逻辑进行异常处理,给用户友好的异常提示。通常出现异常时,咱们须要尽量保证:
若是你使用的是 Egg,你可使用 onerror 插件来作统一的处理。同时不建议将异常信息直接返回给用户,返回用户的应该是更语义化更友好的信息,而原始的错误堆栈和信息等,你能够经过日志进行记录,日志信息越详细越好,好比除了最基本的 name、message、stack 外,你还能够记录当前一些关键的参数以及当前调用链路的 traceId 等,这样的目的只有一个,就是能够快速定位到错误,以及错误发生的上下文。具体的链路监控下文会讲到。
在设计应用架构时,重要的一步就是区分强弱依赖。强弱依赖的定义应该视对业务的影响程度而定,并不能单纯的认为会致使系统挂掉的依赖才是强依赖。尽可能减小强依赖,由于强依赖意味着,一旦该强依赖出现问题,会导直接影响业务的进行。一个应用的依赖可能涉及到如下几个部分。
应用的开发基本离不开数据的读写,这也致使咱们的应用基本都是强依赖 DB 的,DB 一旦出现问题,那咱们的应用可能就不可用了,所以咱们能够经过 DB 上加一层缓存来增长一层保险,当数据更新的时候刷新对应的缓存,这样任何一层出现问题,都不会对应用带来灾难性后果。这里你须要额外注意数据同步的机制和一致性的保证,同时对于数据读取要设置合理的超时时间,好比读取缓存,若是 10ms 内没有响应就直接读取数据库,再有就是异常的处理,好比要保证读取缓存时出现异常不能影响 DB 的正常读取。
若是依赖了其余的中间件,也要考虑是否对某个中间件进行了强依赖,若是这个中间件故障了,会不会对咱们的应用形成严重故障。
咱们的应用或多或少都会依赖其余的二方或者三方系统,对咱们依赖的这些系统的稳定性,咱们尽可能要作到心中有数,尽可能不进行强依赖,若是出现异常,要作好详细的日志记录,快速定位出现问题的依赖方和出现问题的上下文,否则定位问题和复现问题可能就要花去你大部分时间了,同时提早作好处理方案,不要出现问题了就抓瞎了。固然若是咱们依赖其余系统提供的数据,那依然可使用缓存来加一层保障。
其中有可能你的应用面临突发流量时,须要对一些下游弱依赖进行降级,以保证当前系统以及下游的正常运行使用。须要明确的是依赖能够降级,可是功能不能降级,举个例子,实现一个商品收藏夹的页面功能,每一个商品上会有一个加购按钮,若是商品是否能够加购的查询依赖于二方系统,那你就须要考虑面临突发流量时对该依赖进行降级,错误的降级方式是直接不展现这个加购按钮,这种方式降级了依赖同时降级了功能。比较好的处理方式是,所有商品都展现加购按钮,当用户点击加购时,才去请求二方系统,检查是否能够加购。经过牺牲一点用户的体验来保证整个系统的稳定性。
咱们知道 JavaScript 单线程运行的,换句话说一个 Node.js 进程只能运行在一个 CPU 上,所以没法享受到多核运算的好处。Node.js 针对这个问题提供了 Cluster 模块,能够在服务器上同时启动多个进程,每一个进程里都跑的是同一份源代码,而且能够同时监听一个端口。固然做为一个对外服务的应用来讲,要考虑的东西还有不少,好比异常如何处理,进程间如何共享资源,进程间如何调度等等。若是你使用的是 Egg/Midway,这些问题框架已经帮你解决掉了。对于 Egg 来讲,你能够详细参考:多进程模型和进程间通信。这里再也不赘述。
单元/功能测试的重要性毋容置疑,为代码质量提供持续性的保障,同时能够加强你修改、发布代码的信心。单元测试用于测试最小功能单元,好比单个方法。而针对 Node 开发的 Web 应用,咱们能够直接针对接口进行功能测试,若是针对函数方法写单元测试的话,成本有点高,而接口的功能测试基本能够覆盖 Router、Controller、Model 整条链路了,覆盖不到的函数逻辑再对其单独编写单元测试用例,这样成本会小不少,并且达到的测试覆盖率并无折扣。
若是你使用了 Egg/Midway 等框架,框架自己对单元测试能力已经帮你进行了集成,你只须要按照约定编写用例并使用便可,能够参考 Egg 单元测试。
有了单元/功能测试之后,下一步就须要考虑持续集成了。阿里云提供了 CodePipline 以及云效 帮助你进行快速可靠的持续集成与交付
开发、测试、发布过程当中的流程规范也是保障稳定性的重要一环,能够有效避免一些人为的疏忽。好比应用写了测试用例,可是在用例没经过的状况下发布上线等等。所以配置一套自动化的流程规范十分有必要,阿里云的云效提供了完整的项目管理、持续集成的能力,在上面能够完成平常开发、测试、发布的流程。详细的操做能够参考其帮助文档。这里补充一些流程上的实践。
CodeReview 十分重要,它能够及时发现一些比较明显的代码、逻辑问题,同时能够保证多人合做的代码理解和维护。可是若是没有一个流程规范和卡口,CodeReview 是很难自发坚持下去的。
CodeReview 能够分为提交前(pre-commit)和提交后(post-commit)两种。自己就是字面意思,pre-commit 既必须经过 CodeReview 才能够提交代码,而 post-commit 既先提交代码,而后发起 CodeReview。相比起来,pre-commit 流程更加合理,由于 post-commit 不阻碍代码提交变动、发布的流程,既即便没有 reivew 经过,依然能够提交变动并发布。而 post-commit 相对于 pre-commit 来讲会更容易实施。
而对于 post-commit,若是其 review 的结果并不影响代码提交变动和发布,那如何作流程卡口呢?你可使用云效自定义流水线,经过人工卡点的方式来保证流程。
经过人工卡点,来增长流程卡口,后续云效也会上线 CodeReivew 功能,敬请期待。更多流水线的操做,你能够参考其帮助文档
若是你以为配置 pre-commit 过于麻烦,而 post-commit 流程上过于滞后的话,也能够采用依靠约定的折中方案,使用 Git 的 PR 功能。咱们不从部署分支上进行开发,而是基于部署分支继续检出开发分支,开发完成须要提交部署时,提交 PR,指定给须要 review 的同窗,经过后会将开发分支合并到部署分支。固然这种方式依赖流程规范的约定,没法进行强制的卡口。
前文讲过,咱们须要为应用实现单元/功能测试,那如何保证应用部署发布前必定经过了单元/功能测试呢?咱们能够在云效的流程中增长测试卡点,来保证咱们编写的测试用例经过后,当前部署分支才可进行发布,经过云效的自动化测试卡口保障持续交付质量。
首先咱们须要新建一个测试任务,在 [云效的测试服务]https://testing.rdc.aliyun.com/))中选择“单元测试”。
将建立的测试任务和流水线关联,做为持续集成交付的测试卡口。每次集成交付,都会运行测试任务,同时保证测试结果达到红线要求,不然流水线运行失败。
更多操做步骤能够参考帮助文档
应用在发布前以及上线后周期性的,都须要作性能测试,一方面让咱们对应用的吞吐内心有数,另外一方面保证长时间运行的稳定,毕竟有些问题多是运行不少次才可能出现的,好比 OOM 等。阿里云提供了方便的性能测试产品:PTS。
PTS 支持构建串行、并行的构建你的压测场景,而且支持并发和 TPS 模式来控制你的压测流量,最后,PTS 还提供了丰富的监控和压测报告,实时监控和报告中包括但不局限于各 API 的并发、TPS、响应时间和采样的日志,请求和响应时间还有不一样的细分数据,和阿里云生态内的云监控、ARMS监控无缝集成。
首先你须要对压测进行计划,须要明确场景,对流量进行预估,设定目标值,不然压测毫无心义,你彻底没法明确当前系统是否能够稳定的支撑你的业务场景。其次须要对各类系统预案进行摸高压测,明确各个预案下能支持的压力上限,以此来保证在合适的状况下能够执行对应的预案并能够达到预期效果。
详细的建立压测场景的步骤,能够参考 PTS 帮助文档。通常来讲,咱们能够建立两个场景,分别用来回归测试和容量评估,回归测试的场景,能够设置固定的并发数量,周期性的持续压测,来暴露一些长时间运行可能的潜在问题、而容量评估场景,须要设置自动增加的方式,用来寻找系统的压力上限。
对于容量评估的场景,咱们能够开启自动增加,按照固定比例进行压测量级的递增,并在每一个量级维持固定压测时长,以便观察业务系统运行状况。
同时 PTS 给咱们提供了更加方便的智能测试模式,帮咱们探测系统的最佳压力点、极限压力点和破坏压力点,帮助咱们评估系统容量。更详细的操做步骤,能够参考 PTS 容量评估
对于预估正常的并发量来讲,性能测试通常经过标准为:
更多可参考 PTS 测试指标。对于压力测试来讲,通常咱们把 CPU 压到 100% 或者内存压到 90% 左右,既可认为压到了极限,若是此时你发现其余指标可能都是正常的,那么说明你的应用可能还有很大的优化空间,能够有针对性的去检查并进一步优化。
咱们须要保证应用长时间持续性的稳定,而有些问题多是运行不少次才可能出现的,好比 OOM 等。而回归测试指的是周期性的持续压测,经过回归测试,来提早暴露出系统长时间运行中可能出现的潜在问题。
PTS 为咱们提供的方便的定时功能,能够指定测试任务的执行日期、执行时间、循环周期和通知方式等,从而实现定时压测。你能够参考 PTS 定时压测来配置本身的回归测试。
固然云效也给咱们提供了功能更为强大的回归测试平台,能够将线上真实流量复制并用于自动回归测试的平台。经过它,不只可以实现低成本的平常自动化回归,同时经过它提供扩展能力能够支持系统重构升级的自动回归。好比系统重构时,复制真实线上环境流量到被测试环境进行回归,至关于在不影响业务的状况下提早上线检测系统潜在的问题。同时还能够将录制的流量做为用例管理起来进行自动化回归。
你能够参考自动回归服务接入使用文档来配置功能强大的回归测试。
应用出现异常并不可怕,可怕的是出现问题之后而并不自知。没有哪一个系统能够保证线上不出现问题,重要的是及时发现问题并解决,不让问题持续恶化。所以线上的监控和报警十分重要。
通常来讲咱们须要进行三个方面的监控:业务可用性、业务指标衡量、业务错误追踪,而对应的方式为:健康检查、单点度量、错误日志和链路。
健康检查是用来定义一个应用当前的状态,它须要能频繁调用并快速返回,而健康检查包含着一系列的检查项,好比:
通常来讲,咱们能够经过 Pandora + 云监控 CloudMonitor 来帮助咱们进行健康检查。
首先 Pandora 是阿里内部开源出去的,提供一个通用的 Node.js 应用运行时模型和相关基础设施。提供一个标准的 Node.js 的 DevOps 流程。其提供了一些基础的检查,好比磁盘检查,端口检查等。同时咱们也能够自定义更多的检查项。
你能够参考 Pandora 健康检查来使用其提供的健康检查能力。
Pandora 配置好后,咱们能够经过云监控对暴露出来的检查服务进行监控。
你能够参考云监控的主机监控来配置你的监控能力。
阿里云提供了 Node.js 性能平台来帮助咱们对 Node.js 应用进行单点度量。Node.js 性能平台是面向中大型 Node.js 应用提供性能监控、安全提醒、故障排查、性能优化等服务的总体性解决方案。
Node.js 性能平台提供了丰富的度量指标,包括系统、进程的内存、CPU、QPS 等等。
同时,其还为咱们提供的故障排查的能力,好比热点函数分析、内存泄露分析等。你能够参考 Node 应用内存泄漏分析方法论与实战来学习使用 Node.js 性能平台发现、定位解决内存泄露问题。
通常来讲,咱们须要采集如下几类日志:
其中,trace 链路日志是很重要可是容易被忽略的日志,链路的重要性不言而喻,能够帮助咱们分析上下游依赖、进行节点分析和故障排查,尤为是依赖其余二方/三方系统时,trace 链路日志十分重要,可是也是须要花很是大的精力去作,业界的 newRelic,oneAPM 都有着很是明显的链路视图。
通常来讲咱们采用 Pandora + SLS 日志服务 + Node.js 性能平台 来进行日志收集。
其中 Pandora 经过拦截 httpServer 和 httpClient,在对咱们系统业务没有侵入性的同时帮助咱们收集 trace 链路日志,详细的配置,你能够参考 Pandora 链路追踪及监控。
Node.js 性能平台会帮助咱们收集 error 日志。
配合 SLS 日志服务,能够帮助咱们无死角的采集咱们须要的任何日志信息。SLS 详细的配置能够参考其帮助文档。
应用出现异常后,须要有及时的报警机制来提醒咱们,以便快速响应和处理。
通常来讲,须要的监控项及报警指标为:
日志监控
日志报警
机器指标
流量监控
其中 SLA 为服务等级,用百分比的服务可用性来来定义服务质量。
通常来讲咱们使用 云监控 CloudMonitor + SLS 日志服务 + Node.js 性能平台的报警配置便可。
其中云监控 CloudMonitor的报警主要针对上文提到的健康检查。你能够参考云监控报警服务来配置报警功能。
对于 SLS,咱们能够对错误数量进行报警,或者根据同比环比来进行报警。好比咱们能够新建两个快速查询,针对咱们应用 error 和 nginx error 日志。
这里的查询语句为 * | select count(*) as sum
。而后将快速查询另存为告警,根据须要配置告警规则,触发告警时,能够选择经过钉钉机器人进行通知。详细的配置,能够参考 SLS 官方文档设置告警。
对于服务器指标告警,好比 CPU、内存等。咱们能够利用 Node.js 性能平台 配置监控。
能够看到,上面配置的告警规则是:堆上线 80%、load1 和 load5 <= 三、cpu 上线 80%。这里须要编写监控项的表达式,能够参考如何进行监控项表达式的编写。
其实稳定性的保障还有不少工做和措施能够作,好比咱们的部署能够采起多集群、多 Region 的部署,这样能够保证当某个集群或者 Region 出现故障,不会形成更大范围的问题,保证故障范围可控。同时咱们还能够采起灰度发布的方式,在不断验证新上线功能的状况下,平滑的过渡发布上线,保证应用总体稳定性等等。
最后的最后,稳定性保障是应用整个生命周期内的事情,是每一个开发者的责任和义务。
本文做者:冬萌
本文为云栖社区原创内容,未经容许不得转载。