前言如果你看过2018年Node.js用户报告,你会发现Node.js的使用进一步增加,同时也出现了一些新的趋势。越来越多的Node.js开发者开始使用容器并积极拥抱Serverless。越来越多的Node.js开始服务于企业。超过一半的Node.js应用程序使用远程服务。越来越多的前端开发人员开始使用它们。可以看到,越来越多的前端开发者具备了全栈能力,更多的核心应用开始基于Node.js进行开发。其中,保证应用程序的稳定性是每个开发者的“头等大事”。什么是稳定性?一般来说,它是指应用程序持续提供可用服务的能力。一旦应用频繁出现不可用或者未能及时恢复,将会对用户体验造成极大的伤害,甚至会造成很多更严重的后果。稳定性保障不仅仅是开发阶段的问题,应该贯穿应用开发、测试、上线、监控等各个环节,覆盖整个DevOps生命周期。阿里云本身提供了丰富的产品和服务来支撑整个DevOps。包括Code代码托管、PTS性能测试、SLS日志服务、云效应等。本文还将介绍阿里云基于Node.js的稳定性保障围绕整个DevOps生命周期的实践。应用开发稳定性的保障,从应用开发阶段就已经开始了。这部分也是最相关的信息和文章。相信有追求的开发者都会关注它,并且已经应用??和实践过。异常捕获和处理应用程序在运行过程中难免会出现异常。再伟大的程序员,也不能保证自己写的代码不会出问题。其实,有异常并不可怕。可怕的是没有捕捉到异常,会导致应用进程崩溃,导致应用不可用。通常情况下,捕获异常有几种方式:try/catchtry/catch是一种常见的异常捕获方式,可以帮助我们可控地捕获错误,但是try/catch无法捕获异步异常。try{setTimeout(()=>{thrownewError('error');},0);}catch(err){//抓不到console.log(err);}上面的异步异常使用了try/catch是不可捕获的。每天捕获异步我们可以使用以下方式。AsynchronousexceptioncallbackAsynchronouscallback通过异步回调处理异步错误可能是目前最广泛的解决方案。functiondemo(callback){setTimeout(()=>{callback(newError('error'),null);},0);}demo((err,res)=>{if(err)console.log(呃);});当然,回调方法有一个被诟病已久的嵌套问题。Promise可以通过reject抛出错误,通过catch捕获错误```newPromise((resolve,reject)=>{setTimeout(()=>{reject(newError('error'));},0);}).catch(err=>{console.log(err);});```generator使用generator可以让我们使用同步代码的写法调用异步函数,可以直接try/catch来捕获异常函数*demo(){try{yieldnewPromise((resolve,reject)=>{setTimeout(()=>{reject(newError('error'));},0);});}catch(err){//可以捕获console.log(err);}}yielddemo();async/awaitasync/await应该是迄今为止最简单优雅的异步解决方案写起来和同步代码一样直观,可以直接使用try/catch来捕获异常constdemo=asyncfunction(){try{awaitnewPromise((resolve,reject)=>{setTimeout(()=>{reject(newError('error'));},0);});}catch(err){//可以捕获console.log(err);}};未捕获tException当异常被抛出而没有被捕获时,会触发uncaughtException事件。只要监听到uncaughtException事件并设置回调,Node进程就不会异常退出。process.on('uncaughtException',function(err){console.error(err);});但此时异常的上下文会丢失(respond对象),无法友好返回。而且uncaughtException事件发生后,当前环境的栈会丢失,可能导致Node无法正常回收内存,造成内存泄漏。所以uncaughtException的正确使用方式一般是在uncaughtException发生时记录详细的日志,然后结束进程,通过日志和告警及时定位和排查问题。domain为了弥补try/catch和uncaughtException的不足,Node增加了一个domain模块,可以在不丢失上下文的情况下捕获异步异常。听起来很完美,但该模块目前不稳定(稳定性:0-已弃用)。同时可能存在稳定性和内存泄漏问题,谨慎使用。一般来说,我们在开发Node应用的时候,只需要关注我们应用逻辑异常的捕获即可。我们使用的Node框架,例如:Egg、Midway等,都会在底层帮我们处理,以保证在一些我们无法控制或者不期望的异常发生时,不会导致应用崩溃.虽然框架帮助我们覆盖了底线,但我们仍然需要为自己的应用逻辑处理异常,并给予用户友好的异常提示。一般情况下,当出现异常时,我们需要尽可能保证:对出现异常的用户进行友好提醒,不影响应用其他用户的正常使用和应用进程的正常运行。详细的异常日志记录和告警机制,方便快速定位和解决问题如果您使用的是Egg,可以使用onerror插件进行统一处理。同时不建议将异常信息直接返回给用户。返回给用户的信息应该更加语义化和友好。您可以通过日志记录原始错误堆栈和信息。日志信息越详细越好,比如除了最基本的name、message、stack之外,还可以记录一些当前的关键参数和当前调用链路的traceId。这只有一个目的,就是快速定位错误和错误发生的上下文。具体的链路监控下面会说到。强依赖和弱依赖在设计应用架构时,一个重要的步骤是区分强依赖和弱依赖。强弱依赖的定义应该看对业务的影响程度,不能简单的认为会导致系统挂掉的依赖就是强依赖。尽量减少强依赖,因为强依赖意味着强依赖一旦出现问题,会直接影响业务的进行。一个应用程序的依赖关系可能涉及以下几个部分。数据应用的开发基本上离不开数据的读写,这也导致我们的应用基本上都严重依赖于DB。一旦DB出现问题,我们的应用可能不可用,所以我们可以添加Layercache来增加一层保险。当数据更新时,刷新相应的缓存,这样任何一层出现问题都不会给应用带来灾难性的后果。这里需要格外注意数据同步的机制和一致性的保证。同时要设置合理的数据读取超时时间,比如读取缓存。如果10ms内没有响应,可以直接读取数据库,然后有异常处理。比如需要保证读取缓存时出现异常不能影响正常读取DB。如果中间件依赖其他中间件,还要考虑是否对某个中间件有强依赖。如果中间件出现故障,是否会导致我们的应用程序严重失败。二方/三方系统我们的应用或多或少会依赖于其他两方或三方系统。我们试图了解我们所依赖的这些系统的稳定性,并尽量不产生强依赖性。如果出现异常,做好详细的日志记录,快速定位依赖方和问题上下文,否则可能会花费你大部分时间定位问题和重现问题,同时准备解决方案提前避免问题刚抓瞎了。当然,如果我们依赖其他系统提供的数据,我们仍然可以使用缓存来增加一层保护。有可能当你的应用面临突发流量时,需要对下游的一些弱依赖进行降级,以保证当前系统和下游的正常运行和使用。需要说明的是,依赖是可以降级的,但是功能是不能降级的。比如实现一个商品收藏的页面功能,每个商品都会有一个购买按钮。如果产品可以购买,查询依赖二方系统,那么在遇到突发流量时需要考虑降级依赖。错误的降级方式是不直接显示购买按钮。这种方式降低了依赖性并同时降低了功能。更好的处理方法是为所有产品显示附加按钮。当用户点击添加按钮时,会请求二方系统检查是否可以添加。通过牺牲一点用户体验来保证整个系统的稳定性。多进程我们知道JavaScript在单线程上运行。也就是说,一个Node.js进程只能运行在一个CPU上,无法享受到多核计算带来的好处。Node.js提供了Cluster模块来解决这个问题,可以在服务器上同时启动多个进程。每个进程运行相同的源代码,可以同时监听一个端口。当然,作为一个对外服务的应用,需要考虑的事情还有很多,比如如何处理异常,进程之间如何共享资源,进程之间如何调度等等。如果你正在使用Egg/Midway,这些问题框架已经为你解决了。关于Egg,详细可以参考:多进程模型与进程间通信。这里我就不细说了。单元/功能测试单元/功能测试的重要性毋庸置疑,它为代码质量提供了持续的保证,同时可以增强你修改和发布代码的信心。单元测试用于测试最小的功能单元,例如单个方法。对于Node开发的Web应用,我们可以直接对接口进行功能测试。如果对函数方法写单元测试,成本有点高,接口上的功能测试基本可以覆盖Router、Controller、Model的整个链路。将单元测试用例单独写成小于功能逻辑,这样成本会小很多,达到的测试覆盖率也不会打折扣。如果你使用Egg/Midway这样的框架,框架本身已经为你集成了单元测试功能。您只需要按照约定编写和使用用例即可。可以参考Egg单元测试。持续集成有了单元/功能测试之后,下一步就要考虑持续集成了。阿里云提供CodePipline和CloudEfficiency,帮助您进行快速可靠的持续集成和交付流程规范开发、测试和发布。流程规范也是保证稳定性的重要一环,可以有效避免一些人为疏忽。例如,应用程序编写测试用例,但在测试用例失败时在线发布,等等。因此,配置一套自动化流程规范是非常有必要的。阿里云的云效提供完整的项目管理和持续集成能力,日常的开发、测试、发布流程都可以在上面完成。详细操作请参考其帮助文档。下面是一些流程实践。CodeReviewCodeReview很重要。可以及时发现一些明显的代码和逻辑问题,同时保证多人协作的代码理解和维护。但是没有流程规范和检查点,CodeReview很难自发地坚持下去。CodeReview可以分为pre-commit和post-commit。字面意思是pre-commit必须通过CodeReview才能提交代码,而post-commit是先提交代码再发起CodeReview。相比较而言,pre-commit的流程更加合理,因为post-commit不会阻碍提交变更和发布代码的过程,即使reivew没有通过,也可以提交和发布变更。并且post-commit比pre-commit更容易实现。至于post-commit,如果它的review结果不影响代码的提交和发布,processcheckpoint怎么办?您可以使用云效应自定义流水线,通过人工检查点确保流程。通过手动卡点,增加流程关卡,后续云效还将推出CodeReivew功能,敬请期待。更多流水线操作可以参考其帮助文档。如果你觉得配置pre-commit太麻烦,post-commit过程太滞后,也可以使用Git的PR功能作为依赖协议的折衷方案。我们不从部署分支开始开发,而是在部署分支的基础上继续检查开发分支。当开发完成,需要提交部署时,提交一个PR,分配给需要审核的同学。审核通过后,将开发分支合并到部署分支。当然,这种方法取决于工艺规范的约定,不能强行检查。添加测试卡点上面提到我们需要对应用进行单元/功能测试,那么如何保证应用在部署发布前必须通过单元/功能测试呢?我们可以在云效流程中添加测试点,确保我们编写的测试用例通过后才能发布当前部署分支,通过云效的自动化测试检查点来保证持续交付的质量。首先,我们需要新建一个测试任务,在【云效测试服务】https://testing.rdc.aliyun.com/)中选择“单元测试”)。将创建的测试任务与流水线关联起来,作为持续集成交付的测试端口。每次集成交付都会运行测试任务,同时保证测试结果满足红线要求,否则流水线运行失败。更多操作步骤请参考帮助文档。性能测试应用需要在发布前和上线后定期进行性能测试。一方面让我们知道应用的吞吐量,另一方面保证了长期运行的稳定性。毕竟有些问题可能是运行多次后才会出现的,比如OOM之类的。阿里云提供了一个方便的性能测试产品:PTS。PTS支持串行和并行构建您的压测场景,支持并发和TPS模式控制您的压测流量。最后,PTS还提供了丰富的监控和压测报告。实时监控和报表包括但不限于各个API的并发量、TPS、响应时间、采样日志等。还有针对请求和响应时间的不同细分数据,与阿里云生态中的云监控、ARMS监控无缝集成。创建压测场景,首先需要对压测进行规划。需要明确场景,预估流量,设定目标值。否则压测就没有意义,你也无从知晓当前系统能否稳定支撑你的业务场景。.其次,需要对各种系统场景进行高压测试,明确每一种场景下所能支撑的压力上限,以确保相应的场景能够在合适的情况下执行,达到预期的效果可以实现。创建压测场景的详细步骤请参考PTS帮助文档。一般来说,我们可以创建两个场景,分别用于回归测试和容量评估。对于回归测试场景,可以设置固定的并发数,通过周期性持续压测的方式,暴露一些可能长时间运行的潜在问题。在容量评估场景中,需要设置自动增长的方式来寻找系统的压力上限。压力配置针对容量评估的场景,我们可以开启自动增长,按照固定比例增加压测级别,并在每个级别保持固定的压测时长,以观察业务系统的运行情况。同时,PTS为我们提供了更加便捷的智能测试模式,帮助我们检测系统的最佳压力点、极限压力点和失效压力点,帮助我们评估系统容量。更详细的操作步骤可以参考PTS容量评估性能指标。预估正常并发,性能测试一般通过标准:超时率小于1/10,000错误率小于1/10,000CPU利用率小于75%Load平均每核CPU小于1,内存使用率低于80%。更多信息请参考PTS测试指标。对于压力测试,一般我们会把CPU压到100%,或者内存压到90%左右,可以认为是极限。如果此时你发现其他指标可能都正常,那么你的应用可能还有很大的优化空间,可以有针对性地检查一下,进一步优化。对于回归测试,我们需要长期保证应用的稳定性,而有些问题可能是运行多次后才会出现的,比如OOM。回归测试是指周期性的连续压力测试。通过回归测试,提前暴露系统在长期运行过程中可能出现的潜在问题。PTS提供便捷的定时功能,可以指定测试任务的执行日期、执行时间、周期和通知方式,从而实现定时压力测试。可以参考PTS定时压测配置自己的回归测试。当然,云霄也为我们提供了更强大的回归测试平台,可以复制真实的线上流量,用于自动回归测试。通过它,不仅可以实现低成本的日常自动回归,还可以提供扩展能力来支持系统重构和升级的自动回归。例如,在系统重构时,将线上真实环境流量复制到测试环境进行回归,相当于提前上线检测系统潜在问题,不影响业务。同时,记录的流量也可以作为自动回归的用例进行管理。可以参考自动回归服务接入文档配置强大的回归测试。监控告警应用出现异常并不可怕,可怕的是问题发生后你还不知道。任何系统都不能保证上网不会出问题。重要的是及时发现问题并解决问题,防止问题进一步恶化。因此,在线监控和报警非常重要。监控和日志一般来说,我们需要监控三个方面:业务可用性、业务指标度量、业务错误跟踪,对应的方式有:健康检查、单点度量、错误日志、链路。健康检查健康检查是用来定义应用程序当前的状态,需要经常调用并快速返回,而健康检查包含一系列的检查项,例如:一般来说,我们可以使用Pandora+云监控CloudMonitor来help我们做健康检查。首先,Pandora是阿里内部开源的,提供了通用的Node.js应用运行时模型和相关基础设施。提供标准的Node.jsDevOps流程。它提供了一些基本的检查,比如磁盘检查、端口检查等,同时我们也可以自定义更多的检查项。可以参考PandoraHealthCheck使用其提供的健康检查能力。Pandora配置完成后,我们可以通过云监控来监控暴露的检测服务。您可以参考云监控的主机监控来配置您的监控能力。单点测评阿里云提供了Node.js性能平台,帮助我们对Node.js应用进行单点测评。Node.js性能平台是为中大型Node.js应用提供性能监控、安全提醒、故障排除、性能优化等服务的整体解决方案。Node.js性能平台提供了丰富的指标,包括系统、进程内存、CPU、QPS等。同时也为我们提供了热函数分析、内存泄漏分析等排查能力,大家可以参考Node应用内存泄漏分析方法论和实战来学习如何使用Node.js性能平台发现、定位和解决内存泄漏问题。错误日志和链接一般来说,我们需要收集以下几类日志:trace:请求链接的监控日志。当出现错误时,可以根据traceId快读定位到导致问题的请求链接,还原上下文。特别是如果我们的应用依赖其他二方/三方系统,当链接比较长时,我们可以清楚的知道调用依赖系统时的入参和返回,快速读取和定位有问题的链接,减少麻烦争论和定位恢复问题的时间。错误:错误日志。包括应用程序本身和业务逻辑中的错误。metric:CPU、内存等机器指标nginx:如果你的应用使用了nginx,nginx错误日志的收集也很关键。nginx的错误日志可能是最容易被忽略的。我们经常看到这样的场景,应用是正常的,但是访问就挂了。经过半天的排查,开发者终于定位到了nginx抛出的错误。其中tracelink日志是非常重要但又容易被忽视的日志。链接的重要性不言而喻,可以帮助我们分析上下游依赖关系,节点分析和排查问题,尤其是依赖其他二方/三方系统的时候,tracelinklog就很重要了,但是它也需要付出很大的努力才能做到。业界的NewRelic和oneAPM有着非常明显的链接观点。一般来说,我们使用Pandora+SLS日志服务+Node.js性能平台进行日志采集。其中Pandora在不侵入我们系统业务的情况下,通过拦截httpServer和httpClient帮助我们收集trace链接日志。详细配置可以参考Pandora链路跟踪监控。Node.js性能平台会帮我们收集错误日志。配合SLS日志服务,可以无死角的帮助我们采集任何需要的日志信息。SLS的详细配置请参考其帮助文档。当告警应用出现异常时,需要有及时的告警机制提醒我们,以便我们能够快速响应和处理。监控项和告警指标一般来说,需要的监控项和告警指标有:日志监控Nginx错误日志应用程序错误日志Trace链接日志日志告警每分钟错误日志数>流量*SLA级别机器指标CPU>70%内存泄漏:@heap_used/@heap_limit>0.7Load>CPU核心流量监控每周同比监控:同比下降>SLA承诺流量预警,接近QPS峰值其中SLA为服务水平,服务质量定义为服务可用性的百分比。告警配置一般来说,我们可以使用云监控+SLS日志服务+Node.js性能平台的告警配置。其中,云监控的告警主要针对上述的健康检查。可以参考云监控报警服务配置报警功能。对于SLS,我们可以按错误数量报警,也可以按年报警。例如,我们可以为我们的应用程序错误和nginx错误日志创建两个快速查询。这里的查询语句是*|选择count(*)作为总和。然后将快捷查询保存为报警,根据需要配置报警规则,触发报警时可以选择通过钉钉机器人进行通知。详细配置可以参考SLS官方文档设置告警。对于服务器指标告警,如CPU、内存等,我们可以利用Node.js性能平台配置监控。可以看出,上面配置的告警规则是:heaponline80%,load1和load5<=3,cpuonline80%。这里需要写监控项的表达式,可以参考如何写监控项的表达式。最后,还有很多工作和措施可以做,以确保稳定。比如我们的部署可以采用多集群、多区域部署,这样可以保证当某个集群或者区域发生故障时,不会引发更大范围的问题。确保故障范围可控。同时,我们也可以采用灰度发布的方式。在不断验证新上线功能的情况下,平滑过渡发布上线,保证应用的整体稳定性等。最后,稳定性保障是关系到应用整个生命周期的事情,是每个开发者的责任和义务。本文作者:东萌阅读原文,为云栖社区原创内容,未经允许不得转载。
