[Node.js] 06 - Multi-thread and process module
课前阅读:关于Node.js后端架构的一点后知后觉
书推荐:<Node.js design patterns>
衍生问题: 微服务的必要性,Flux架构
容错性和拓展性
一、立体拓展
假设现在需要你用NodeJS搭建一个http服务,我猜测你会借助express框架用不到10行的代码完成这项工作。
但容错性和拓展性才是正常运行的基本保障,至少保证了你的服务是可用的,永远是可用的。
-
- X轴方向:纯粹的对服务实例进行拓展,例如为了响应更多的请求
- y轴方向:为服务添加新的功能,功能性拓展
- z轴方向:按照业务数据对服务进行拓展(这里没搞懂,不知道这么说是否准确)
增加服务实例也分为两类,横向拓展(horizontal scaling)和纵向拓展(vertical scaling),
-
- 横向,表示利用更多的机器, // ----> (B)
- 纵向,表示在同一台机器上挖掘它的潜力。// ----> (A)
二、服务实例拓展
- (A) 纵向拓展
单进程
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上的登陆状态没有共享给其他实例的话,那么用户不得不重新登陆一次,这样的用户体验是无法接受的。
- (B) 横向拓展
主进程-子进程的模式思路不仅适用于纵向拓展,还适用于横向拓展。
当单台机器已经无法满足你需求的时候,你可以把单实例子进程的概念拓展为单台机器:我们将在多台机器上部署多个进行实例,用户的访问请求也并非直接到达它们,而是先到达前方的代理机器,它也是负责负载均衡的机器,负责将请求转发给部署了应用实例的机器。
这样的模式我们也通常称为反向代理模式。
三、功能拓展
既然我们无法保证功能不会出错,那我们有没有办法保证当一个功能出错之后不会影响整个程序的正常运行?这也是我们所说的容错性。
道理都懂,我们都明白程序需要容错,所以try/catch是从编码上解决这个问题。
我们允许程序出错,但是要及时把错误隔离,并且不再影响程序的运行。这个就要从架构上解决这个问题。例如使用微服务(Microservices)架构。
- 为什么要用微服务
-
- 单体(monolithic)架构介绍
在介绍微服务架构之前,我们要了解其它架构为什么没法满足我们的要求。例如我们常用的单体(monolithic)架构。单体架构这个词你可能不熟悉,但几乎我们每天都在和它打交道,大部分的后端服务都归属于单体架构,对它的解释我翻译Martin Fowler的描述:
企业级应用通常分为三个部分:
- 用户界面(包含运行在用户浏览器上的html页面和javascript脚本),
- 数据库(通常是包含许多表的关系数据库),
- 服务端应用。
服务端应用将会处理http请求,执行业务逻辑,从数据库中取得数据,生成html视图返回给浏览器。
这样的服务端应用就被称为单体(monolith)——单个具有逻辑性的执行过程。任何针对系统的修改都会导致重新构建和部署一个新版本的服务端应用。
(注:以上这段描述摘自Martin Fowler的文章Microservices,我认为这是对微架构描述最全面的文章,如果想对这一小节做更深入的了解可以把这篇文章细读。 这也是我读到的Martin Fowler所写的文章中最通俗的文章。个人认为Martin Fowler的文章读起来比较晦涩,John Resig紧随其后)
-
- 单体(monolithic)架构de问题
单体架构是一种很自然的搭建应用的方式,它符合我们对业务处理流程的认知。但单体应用也存在问题:
任何一处,无论大小的修改都会导致整个应用被重新构建和重新部署。随着应用规模和复杂性的不断增大,参与维护的人数增多,每一轮迭代修改的模块增多,对上线来说是极大的考验,对于内部单个模块的拓展也是极为不利的。例如当图片压缩请求剧增时,需要新增图片压缩模块的实例,但实际上不得不扩展整个单体应用的实例。
-
- 微服务是方案
微服务架构解决的就是这一系列问题。顾名思义,微服务架构下软件是由多个独立的服务组成。这些服务相互独立互不干预。以拆分上面所说的单体应用为例,我们可以把处理HTTP请求的模块和负责数据库读写的模块分离出来成为独立的服务,这两个模块从功能上看是没有任何交集。这样的好处就是,我们可以独立的部署,拓展,修改这些服务。例如应用需要添加新的接口时,我们只需要修改处理HTTP请求的服务,只公开这部分代码给修改者,只上线这部分服务,拓展时也只需要新添这部分服务的实例。
微服务和我们通常编写的模块(以文件为单位,以命名空间为单位)相比更加独立,更像是一个五脏俱全的“小应用”,如果你读完了我之前推荐的Martin Fowler关于微服务的文章的话,你会对这点更深有感触:微服务除了在运维上独立以外,它还可以拥有独立的数据库,还应该配备独立的团队维护。它甚至可以允许使用其他的语言进行开发,只要对外接口正常即可。
-
- 微服务de不足
当然微服务也存在不足,例如
- 如何将诸多的微服务在大型架构中组织起来,
- 如何提高不同服务之间的通信效率都是需要在实际工作中解决的问题。
微服务说到底还是解耦思想的实践。从这个意义上来说,React下的Flux架构某种意义上也属于微服务。
如果你了解Flux的起源的话,Flux架构 其实来源于后端的CQRS,即Command Query Responsibility Segregation,命令与查询职责分离,也就是将数据的读操作和写操作分离开。
这么设计的理由有很多,举例说一点:在许多业务场景中,数据的读和写的次数是不平衡,可能上千次的读操作才对应一次写操作,比如机票余票信息的查询和更新。所以把读和写操作分开能够有针对性的分别优化它们。例如提高程序的scalability,scalability意味着我们能够在部署程序时,给读操作和写操作部署不同数量的线上实例来满足实际的需求。
- 类比unity设计模式 - 自治
如果你也有Unity编程经验的话会对解耦更有感触,在Unity中我们已经不能称之为解耦,而是自治,这是Unity的设计模式。
举个例子,屏幕上少则可能有十几个游戏元素,例如玩家、敌人还有子弹。你必须为它们编写“死亡”的规则,“诞生”的规则,交互的规则。因为你根本无法预料玩家在何时何种位置发射出子弹,也无法预料子弹何时在什么位置碰撞上什么状态敌人。
所以你只能让它们在规则下自由发挥。这和微服务有异曲同工之妙:独立,隔离,自治。
四、Node.js 与 Netty Server
- 良心对比一
Jeff: netty可能有更好的封装,来解决例如“状态共享”这样的难题。
protobuf比Json更好一点:http://www.infoq.com/cn/articles/json-is-5-times-faster-than-protobuf
- 良心对比二
From: https://my.oschina.net/lifeofpi/blog/120210
以下代码体现了nodejs的简洁性。
var http = require('http');
http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).listen(1337, "127.0.0.1");
console.log('Server running at http://127.0.0.1:1337/');
多进程/多线程实践
一、JS 角度的多线程
Goto: [JS] ECMAScript 6 - Async : compare with c#
- Javascript异步编程的4种方法 - Ruan
(1) 回调函数
(2) 事件监听:func.trigger --> func.on
(3) 发布订阅:jQuery.publish --> jQuery.subscribe
(4) Promises对象
二、Promise 对象
有三种状态,也就支持了.then这样的链式写法。
Ref: 浅谈ES6的Promise对象【看上去讲得更好】
- 使用 Bluebird 在生产环境
原生promise可能不稳定,为了防止不可预知的bug,在生产项目中最好还是不要使用原生的或者自己编写的Promise(目前为止并不是所有浏览器都能很好的兼容ES6),而是使用已经较为成熟的有大量小伙伴使用的第三方Promise库,下面就为小伙伴推荐一个—— Bluebird
- catch 比 then() 的 rejected 回调好些
promise.then( () => { console.log('this is success callback') } ).catch( (err) => { console.log(err) } )
/* 需要根据链接进一步学习 */
三、Generator 函数
状态机、遍历器 ----> yield & next 函数
四、Async 函数
ES2017 标准引入了 async 函数,是个Generator 函数的语法糖。
五、node.js角度的多线程
Ref: Node.js的process模块
Ref: Node.js 多进程
Ref: nodejs 多核处理模块cluster【基于以上两篇的技术,竟然有了负载均衡】
cluster是一个nodejs内置的模块,用于nodejs多核处理。cluster模块,可以帮助我们简化多进程并行化程序的开发难度,轻松构建一个用于负载均衡的集群。
/* implement */