上周有幸和淘宝前端团队的七念老师作了一些NodeJS方面上的交流(实际状况实际上是他电话面试了我╮(╯-╰)╭),咱们主要聊到了我参与维护的一个线上NodeJS服务,关于它的现状和当下的不足。他向我提出的一些问题带给了我很大启发,尽管回答的不是很好。问题大意是,对于你意识到的这些不足,你将尝试怎样去改进它们?甚至,若是给你一个机会来从新设计这个系统服务,你将如何作?相比如今有什么的改进?javascript
为何说这些问题对我产生了启发,是由于这些问题是我未曾考虑过的。或者说考虑过,但没有这么严肃的考虑过。这里的“严肃”指的是具体到线上,细节,容灾容错等方面。而在电话以后我从新尝试回答这些问题的过程当中又收获了很多新的知识。html
这篇文章与以往的文章不一样,并非阐述某一个问题的最佳解决方案,也不会落实到具体的代码上。而是分享在探寻答案过程当中收获的心得、留下的困惑还有一点我的的经验。至于这些可否拿来回答最初的那些问题我没有十足的把握,也许能,但确定不是最佳答案。由于后端架构实在一个颇有深度的话题,也是一个极其成熟的技术方向。即便有了理论方面的积累,面对变幻无穷的业务需求不免仍是灵活的对方案进行改进,而不管是理论仍是实践经验都是我欠缺的。前端
这段话原本应该是写在结尾,感受顺嘴也就挂在了开头。java
最后,本文的部份内容和图片参考自图书Node.js design patterns的第七章内容Scalability and Architectural Patterns。其实书中该章中的大部份内容也并不是原创,可是它作了很好的汇总和迁移,具体我会在以后说明。因此若有雷同,不是巧合。node
一个怎样的后端服务才能算得上优秀?或者放低身段说合格?再把这个问题翻译翻译,优秀或者合格的标准是什么?程序员
假设如今须要你用NodeJS搭建一个http服务,我猜想你会借助express框架用不到10行的代码完成这项工做。不能说这么作是错的,但这样简易的程序是脆弱的:一旦部署上线以后,可能瞬间就被大量涌入的请求击垮,更不要提各类潜在的漏洞危险。退一步说,即便线上程序通过了这一关考验,若是你要更新程序怎么办?不得不让用户中断访问一段时间?面试
在我看来,后端服务务必要知足两条特性:算法
固然还有一些其余特性也很重要,好比程序要健壮,接口设计要友好,程序修改起来要灵活等等。但容错性和拓展性才是正常运行的基本保障,至少保证了你的服务是可用的,永远是可用的。而不管实现服务的代码如何优雅,它都是为业务服务的,一旦用户没法访问你的服务了,再优美的代码也无济于事。因此接下来的问题就是,咱们后端程序的架构如何的设计以保证知足这两条特性呢?数据库
首先咱们说说拓展性(Scalability)。express
按照书中的说法,拓展性划分为三类,以下图所示:
而一般实际的拓展过程当中多维度是同时进行的,例如增添了新的功能也就意味着有跟多的流量进入,也就是意味着须要增长新的服务实例。
咱们先谈第一类X轴拓展,增长服务的实例。增长服务实例也分为两类,横向拓展(horizontal scaling)和纵向拓展(vertical scaling),横向表示利用更多的机器,纵向表示在同一台机器上挖掘它的潜力。但其实横向和纵向二者解决问题的思路的差别并不大。
从小到大,先说纵向拓展。
咱们都知道NodeJS程序是以单进程形式运行,32位机器上最多也只有1GB内存的实用权限(在64位机器上最大的内存权限扩大到1.7GB)。而目前绝大部分线上服务器的CPU都是多核而且至少16GB起,如此以来Node程序便没法充分发挥机器的潜力。同时NodeJS本身也意识到了这一点,因此它容许程序建立多个子进程用于运行多个实例。
具体技术细节涉及到Cluster模块,详情能够查看NodeJS相关文档: https://nodejs.org/api/cluster.html
下图就是对以上所说多进程模式原理的图解:
简单来讲,首先咱们有一个主进程master,但master主进程并不实际的处理业务逻辑,但除了业务逻辑之外事情它都作:它是manager,负责启动子进程,管理子进程(若是子进程挂了要及时重启),它也扮演router,也就是对该程序的访问请求首先到达主进程,再由主进程分配请求给子进程worker。而子进程才负责处理业务逻辑。
在这个机制下有两条细节须要咱们定夺如何处理。
如何把外界的请求平均的分配给不一样的worker处理?这里的平均不是指数量上的平均(由于单条请求处理的工做量可能不一样),而是既不能让某个子进程太闲,也不能让某个子进程太忙,保证它们始终处于工做的状态便可。这也是咱们常说的负载均衡(load-balancing)。 默认状况下Cluster模块采用的是round robin负载均衡算法,说白了就是依次按顺序把请求派给列表上的子进程,派到结尾以后又重头开始。
这个算法只能保证每一个子进程收到的请求个数是平均的,和随机算法相似。但若是某个子进程遇到问题,处理变得迟缓了,然后续的请求又源源不断的分配过来,那么这个子进程的压力就大了,这就略显不公了。除此以外咱们还要考虑到超时,重作等机制的创建。因此主进程master做为路由时不只仅是转发请求,还要能智能的分配请求。
另外一个问题是状态共享问题,假如某个用户第一次访问该服务时是分配给了线程A上的实例A处理,而且用户在这个实例上进行了登录,而没有过几秒钟以后当用户第二次访问时分配给了线程B上的实例B处理,若是此时用户在A上的登录状态没有共享给其余实例的话,那么用户不得不从新登录一次,这样的用户体验是没法接受的。以下图所示
这个问题的解决办法是把状态进行共享:
也能够新增一个模块用于记录用户第一次访问的实例,并在以后当用户访问服务时始终访问该实例
主进程-子进程的模式思路不只适用于纵向拓展,还适用于横向拓展。当单台机器已经没法知足你需求的时候,你能够把单实例子进程的概念拓展为单台机器:咱们将在多台机器上部署多个进行实例,用户的访问请求也并不是直接到达它们,而是先到达前方的代理机器,它也是负责负载均衡的机器,负责将请求转发给部署了应用实例的机器。这样的模式咱们也一般称为反向代理模式:
咱们仍然能对这个模式持续改进,例如动态的启动或者关闭机器上的实例用于节省资源,甚至想办法移除负载平衡这一环节用于提升通信的效率。在这里就不延伸开了去了,具体能够参考Node.js design patterns这本书中的内容。
最后在这里要说一件很重要的事情。上面说的负载平衡也好,反向代理也好,都不是新的技术。相反,都是很是很是成熟,有着至关多经验积累的技术。然而为何咱们接触起来却感受如此的新鲜和陌生?我想缘由大概是NodeJS程序员大可能是由前端工程师转化而来,而你们此前都只专一于前端代码而不多接触后端知识。然而若是你从入行开始就是一个Java程序员或者运维工程师,相信你对这一切早就耳熟能详而且手到擒来。
几年前看到过一篇文章,(很惋惜如今找不到了,若是有哪位同窗知道篇文章的麻烦告知一下谢谢),记录的是一位技术人员针对网站访问量增大而作的一系列技术改进。文章的后半部分我记不得了,可是前半部分遇到的问题和改进的思路和咱们是如出一辙的:请求骤增,增长实例机器和解决session共享问题。我想说的是,虽然NodeJS是新技术,可是咱们解决问题的思路和方案能够来自传统软件行业,而且它们在这方面比咱们有经验的多。因此咱们在学习NodeJS,在寻找一些问题的解决方案时,不要局限于NodeJS自己,而是应该开阔眼界,跨语言包容的去汲取知识。
你也许会问新增功能有什么难点?每一个程序员的平常就是不断的进行功能迭代。但在这里咱们但愿解决一个问题,就是既然咱们没法保证功能不会出错,那咱们有没有办法保证当一个功能出错以后不会影响整个程序的正常运行?这也是咱们所说的容错性。
道理都懂,咱们都明白程序须要容错,因此try/catch是从编码上解决这个问题。但问题是try/catch不是万能的,万无一失的程序也是不存在的,因此咱们要换个思路解决这个问题,咱们容许程序出错,可是要及时把错误隔离,而且再也不影响程序的运行。这个就要从架构上解决这个问题。例如使用微服务(Microservices)架构。
在介绍微服务架构以前,咱们要了解其它架构为何无法知足咱们的要求。例如咱们经常使用的单体(monolithic)架构。单体架构这个词你可能不熟悉,但几乎咱们天天都在和它打交道,大部分的后端服务都归属于单体架构,对它的解释我翻译Martin Fowler的描述:
企业级应用一般分为三个部分:用户界面(包含运行在用户浏览器上的html页面和javascript脚本),数据库(一般是包含许多表的关系数据库),和服务端应用。服务端应用将会处理http请求,执行业务逻辑,从数据库中取得数据,生成html视图返回给浏览器。这样的服务端应用就被称为单体(monolith)——单个具备逻辑性的执行过程。任何针对系统的修改都会致使从新构建和部署一个新版本的服务端应用。
(注:以上这段描述摘自Martin Fowler的文章Microservices,我认为这是对微架构描述最全面的文章,若是想对这一小节作更深刻的了解能够把这篇文章细读。 这也是我读到的Martin Fowler所写的文章中最通俗的文章。我的认为Martin Fowler的文章读起来比较晦涩,John Resig紧随其后)
单体架构是一种很天然的搭建应用的方式,它符合咱们对业务处理流程的认知。但单体应用也存在问题:任何一处,不管大小的修改都会致使整个应用被从新构建和从新部署。随着应用规模和复杂性的不断增大,参与维护的人数增多,每一轮迭代修改的模块增多,对上线来讲是极大的考验,对于内部单个模块的拓展也是极为不利的。例如当图片压缩请求剧增时,须要新增图片压缩模块的实例,但实际上不得不扩展整个单体应用的实例。
微服务架构解决的就是这一系列问题。顾名思义,微服务架构下软件是由多个独立的服务组成。这些服务相互独立互不干预。以拆分上面所说的单体应用为例,咱们能够把处理HTTP请求的模块和负责数据库读写的模块分离出来成为独立的服务,这两个模块从功能上看是没有任何交集。这样的好处就是,咱们能够独立的部署,拓展,修改这些服务。例如应用须要添加新的接口时,咱们只须要修改处理HTTP请求的服务,只公开这部分代码给修改者,只上线这部分服务,拓展时也只须要新添这部分服务的实例。
微服务和咱们一般编写的模块(以文件为单位,以命名空间为单位)相比更加独立,更像是一个五脏俱全的“小应用”,若是你读完了我以前推荐的Martin Fowler关于微服务的文章的话,你会对这点更深有感触:微服务除了在运维上独立之外,它还能够拥有独立的数据库,还应该配备独立的团队维护。它甚至能够容许使用其余的语言进行开发,只要对外接口正常便可。
固然微服务也存在不足,例如如何将诸多的微服务在大型架构中组织起来,如何提升不一样服务之间的通讯效率都是须要在实际工做中解决的问题。
微服务说到底仍是解耦思想的实践。从这个意义上来讲,React下的Flux架构某种意义上也属于微服务。若是你了解Flux的起源的话,Flux架构其实来源于后端的CQRS,即Command Query Responsibility Segregation,命令与查询职责分离,也就是将数据的读操做和写操做分离开。这么设计的理由有不少,举例说一点:在许多业务场景中,数据的读和写的次数是不平衡,可能上千次的读操做才对应一次写操做,好比机票余票信息的查询和更新。因此把读和写操做分开可以有针对性的分别优化它们。例如提升程序的scalability,scalability意味着咱们可以在部署程序时,给读操做和写操做部署不一样数量的线上实例来知足实际的需求。
若是你也有Unity编程经验的话会对解耦更有感触,在Unity中咱们已经不能称之为解耦,而是自治,这是Unity的设计模式。举个例子,屏幕上少则可能有十几个游戏元素,例如玩家、敌人还有子弹。你必须为它们编写“死亡”的规则,“诞生”的规则,交互的规则。由于你根本没法预料玩家在什么时候何种位置发射出子弹,也没法预料子弹什么时候在什么位置碰撞上什么状态敌人。因此你只能让它们在规则下自由发挥。这和微服务有殊途同归之妙:独立,隔离,自治。
实话实说,这篇文章里没有干货,全都是舶来品。但舶来品不是一个贬义词,它是咱们学习知识和解决问题的第一手材料。我仍是想重申一遍,在后端领域来讲Node.js是一个新人,咱们应该学习前辈的经验。借用许多年前奔驰广告的一句话:经典是对经典的继承,经典是对经典的背叛。只有站在前人的肩膀上,咱们才有可能创新,看的更远。