Node.js:浅析高并发与分布式集群
Node特性:高并发
在解释node为什么能够做到高并发之前,不妨先了解一下node的其他几个特性:
单线程
我们先来明确一个概念,即:node是单线程
的,这一点与JavaScript在浏览器中的特性相同,并且在node中JavaScript主线程与其他线程(例如I/O线程)是无法共享状态的。
单线程的好处就是:
- 无需像多线程那样去关注线程之间的状态同步问题
- 没有线程切换所带来的开销
- 没有死锁存在
当然单线程也有许多坏处:
- 无法充分利用多核CPU
- 大量计算占用CPU会导致应用阻塞(即不适用CPU密集型)
- 错误会引起整个应用的退出
不过在今天看来,这些坏处都已经不再是问题或者得到了适当的解决:
(1) 创建进程 or 细分实例
关于第一个问题,最直白解决方案就是使用
child_process核心模块
或者cluster
:child_process 和 net 组合应用。我们可以通过在一台多核服务器上创建多个进程(通常使用fork
操作)来充分利用每个核心,不过要处理好进程间通信问题。另一个方案是,我们可以将物理机器划分为多台单核的虚拟机,并通过pm2等工具,管理多台虚拟机形成一个集群架构,高效运行所需服务,至于每台机器间的通信(状态同步)我这里先按下不表,在下文的
Node分布式架构
中再做详细说明。
(2) 时间片轮转
关于第二点,我跟小伙伴讨论过后认为可以通过时间片轮转方式,在单线程上模拟多线程,适当减少应用阻塞的感觉(虽然这种方法不会真的像多线程那样节约时间)
(3) 负载均衡、坏点监控/隔离
至于第三点,我跟小伙伴们也讨论过,认为主要的痛点就在于node不同于JAVA,它所实现的逻辑是以异步为主的。
这就导致了node无法像JAVA一样
方便地
使用 try/catch 来来捕获并绕过错误,因为无法确定异步任务会何时传回异常。而在单线程环境下,绕不过错误就意味着导致应用退出
,重启恢复的间隙会导致服务中断,这是我们不愿意看到的。当然,在服务器资源丰富的当下,我们可以通过 pm2 或 nginx 这些工具,动态的判断服务状态。在服务出错时隔离坏点服务器,将请求转发到正常服务器上,并重启坏点服务器以继续提供服务。这也是
Node分布式架构
的一部分。
异步I/O
你可能会问,既然node是单线程的,事件全部在一个线程上处理,那不是应该效率很低、与高并发相悖吗?
恰恰相反,node的性能很高。原因之一就是node具有异步I/O
特性,每当有I/O请求发生时,node会提供给该请求一个I/O线程。然后node就不管这个I/O的操作过程了,而是继续执行主线程上的事件,只需要在该请求返回回调时在处理即可。也就是node省去了许多等待请求的时间。
这也是node支持高并发的重要原因之一
实际上不光是I/O操作,node的绝大多数操作都是以这种异步的方式进行的。它就像是一个组织者,无需事必躬亲,只需要告诉成员们如何正确的进行操作并接受反馈、处理关键步骤,就能使得整个团队高效运行。
事务驱动
你可能又要问了,node怎么知道请求返回了回调,又应该何时去处理这些回调呢?
答案就是node的另一特性:事务驱动
,即主线程通过event loop事件循环触发的方式来运行程序
这是node支持高并发的另一重要原因
图解node环境下的Event loop:
┌───────────────────────┐
┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ I/O callbacks │<————— 执行几乎所有的回调,除了 close callbacks 以及 timers 调度的回调和 setImmediate() 调度的回调
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ idle, prepare │<————— 内部调用,可忽略
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │ - (retrieve new I/O events; node will block here when appropriate)
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ | | |
| | └───────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| ┌──────────┴────────────┐
│ │ check │<————— setImmediate() 的回调将会在这个阶段执行
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
└──┤ close callbacks │<————— socket.on('close', ...)└───────────────────────┘
poll阶段:
当进入到poll阶段,并且没有timers被调用的时候,会发生下面的情况:
(1)如果poll队列不为空:
- Event Loop 将同步的执行poll queue里的callback(新的I/O事件),直到queue为空或者执行的callback到达上线。
(2)如果poll队列为空:
- 如果脚本调用了setImmediate(), Event Loop将会结束poll阶段并且进入到check阶段执行setImmediate()的回调。
- 如果脚本没有setImmediate()调用,Event Loop将会等待回调(新的I/O事件)被添加到队列中,然后立即执行它们。
当进入到poll阶段,并且调用了timers的话,会发生下面的情况:
- 一旦poll queue是空的话,Event Loop会检查是否timers, 如果有1个或多个timers时间已经到达,Event Loop将会回到timer阶段并执行那些timer的callback(即进入到下一次tick)。
优先级:
根据上面的图,我们不难得出:
Next Tick Queue > MicroTask Queue
那么setTimeout、setInterval和setImmediate谁快呢?
答案是:不确定
单单从执行图上看,如果两者都是在mian module里定义的,那么:setTimeout、setInterval > setImmediate
但是有两个条件制约了这一结论:
- event loop初始化需要一定时间
- setTimeout有最小毫秒数(一般认为最少1ms)
所以当 event loop准备时间 > setTimeout毫秒数时,进入timers检查时已有setTimeout的任务,故timeout
先输出。反之则immediate
先输出。
如果是在poll阶段定义的setTimeout和setImmediate,那么immediate
先于timeout
输出。原因是在poll阶段,会先进入check阶段再进入timers阶段。例如:
const fs = require('fs');fs.readFile('./test.txt', 'utf8', (err, data) => {setTimeout( () => {console.log('setTimeout');}, 0);setImmediate( () => {console.log('setImmediate');})
})/**** console:* > setImmediate* > setTimeout***/
多说一句:
由于timer需要从红黑树中取出定时器来判断时间是否到了,时间复杂度为O(lg(n)),故如果想立即异步执行一个事件,最好不要用 setTimeout(func, 0)。而是使用 process.nextTick() 来完成。
分布式Node架构
我了解到的Node集群架构主要分为以下几个模块:
Nginx(负载均衡、调度) -> Node集群 -> Redis(同步状态)
按我的理解整理了一副图:
当然,这应该是比较理想状态下的架构方式。因为虽然 Redis 的读/写相当快,但这是因为其将数据存储在内存池里,在内存上进行相关操作。
这对于服务器的内存负荷是相当高的,所以通常我们还是会在架构中加入 Mysql,如下图:
先解释一下这幅图:
当用户数据到来时,将数据先写入 Mysql,Node 需要数据时再去 Redis 读取,若没有找到再去 Mysql 里面查询想要的数据,并写入 Redis,下次使用时就可以直接去 Redis 里面查询了。
加入 Mysql 相较于只在 Redis 里读/写的好处有:
(1)避免了短期内无用的数据写入 Redis,占用内存,减轻 Redis 负担
(2)在后期需要对数据进行特定查询、分析的时候(比如分析运营活动用户涨幅),SQL关系查询能提供很大的帮助
当然在应对短时间大流量写入的时候,我们也可以直接将数据写入 Redis,以达到快速存储数据、增加服务器应对流量能力的目的,等流量下去了再单独将数据写入 Mysql。
简单介绍完了大体的架构组成,接下来我们来细看每个部分的细节:
流量接入层
流量接入层所做的就是对所有接受的流量进行处理,提供了以下服务:
- 流量缓冲
- 分流和转发
超时检测
- 与用户建立连接超时
- 读取用户body超时
- 连接后端超时
- 读后端响应头超时
- 写响应超时
- 与用户长连接超时
集群健康检查/隔离坏点服务器
- 隔离坏点服务器并尝试修复/重启,直到该服务器恢复正常
失败重试机制
- 在请求转发到某集群某机器上,返回失败后,将该请求转发到该集群的别的机器,或者跨集群的机器上进行重试
连接池/会话保持机制
- 对于延迟敏感用户使用连接池机制,减少建立连接的时间
- 安全防护
- 数据分析
当转发到各个产品线后就到了负载层工作的时候了:将请求根据情况转发到各地机房
当然,这个平台并不止转发这一个功能,你可以把它理解为一个大型的私有云系统,提供以下服务:
- 文件上传/服务线上部署
- 线上配置修改
- 设置定时任务
- 线上系统监控/日志打印服务
- 线上实例管理
- 镜像中心
- 等等...
Node集群层
这一层主要的工作是:
(1)编写可靠的 Node 代码,为需求提供后端服务
(2)编写高性能查询语句,与 Redis、Mysql 交互,提高查询效率
(3)通过 Redis 同步集群里各个 Node 服务的状态
(4)通过硬件管理平台,管理/监控物理机器的状态、管理IP地址等
(当然这部分我只是粗浅地列列条目,还是需要时间来积累、深入理解)
数据库层
这一层主要的工作是:
(1)创建 Mysql 并设计相关页、表;建立必要的索引、外键,提升查询便利性
(2)部署 redis 并向 Node 层提供相应接口
总结
虽然 Node 的单线程特性给其提供的服务带来了许多问题,但只要我们积极面对这些问题,用合理的方法(如使用 child_process 等模块或构建分布式集群)去解决他们,发挥 Node 的各种优势,就可以享受到它所带来的好处!
待更新:
- Redis相关特性
- sql查询性能指标 & 优化策略
- Node内存监控 & 内存泄露排查/处理
Node.js:浅析高并发与分布式集群相关推荐
- mysql 高并发 集群架构_一种高并发的GPU集群架构及其负载均衡方法技术
[技术实现步骤摘要] 一种高并发的GPU集群架构及其负载均衡方法 本专利技术属于GPU集群架构及其负载均衡方法 ,特别是涉及一种高并发的GPU集群架构及其负载均衡方法. 技术介绍 GPU因其高性能的并 ...
- Redis practise(二)使用Docker部署Redis高可用,分布式集群
思路 鉴于之间学习过的Docker一些基础知识,这次准备部署一个简单的分布式,高可用的redis集群,如下的拓扑 tuopu.png 下面介绍下,对于这张拓扑图而言,需要了解的一些基础概念. Redi ...
- ajax 高并发请求,理解node.js处理高并发请求原理
很少分享技术文章,写的不好的地方请大家多多指教,本文是自己对于node.js的一些见解,如有纰漏请在评论区交流. 高并发策略 通常高并发的解决方案就是提供多线程模型,服务器为每个客户端请求分配一个线程 ...
- 深入浅出百亿请求高可用Redis分布式集群
摘要:作为noSql中的kv数据库的王者,redis以其高性能,低时延,丰富的数据结构备受开发者青睐,但是由于redis在水平伸缩性上受限,如何做到能够水平扩容,同时对业务无侵入性是很多使用redis ...
- HA高可用完全分布式集群 HDFS搭建
HDFS HA 需要 设置静态IP.安装 jdk.设置ssh无密码登录.安装zookeeper.安装hadoop. 在Hadoop2.X之前,Namenode是HDFS集群中可能发生单点故障的节点,每 ...
- wcf高并发 mysql_使用nginx搭建高可用,高并发的wcf集群
很多情况下基于wcf的复杂均衡都首选zookeeper,这样可以拥有更好的控制粒度,但zk对C# 不大友好,实现起来相对来说比较麻烦,实际情况下,如果 你的负载机制粒度很粗糙的话,优先使用nginx就 ...
- 视频系统 流媒体 rtsp hls h264 h265 aac 高并发 低延时 系统 设计 录像 视频合成 转发 点播 快进 快退 单步播放 分布式集群
系统改名为:,升级包改使用jpg图像封装,从2.124版本开始,1.*的升级包停止使用 系统工具 下载地址(2019-04-19) :https://pan.baidu.com/ ...
- java唯一订单号_java web在高并发和分布式下实现订单号生成唯一的解决方案
方案一: 如果没有并发,订单号只在一个线程内产生,那么由于程序是顺序执行的,不同订单的生成时间戳正常不同,因此用时间戳+随机数(或自增数)就可以区分各个订单.如果存在并发,且订单号是由一个进程中的多个 ...
- redis详解(四)-- 高可用分布式集群
一,高可用 高可用(High Availability),是当一台服务器停止服务后,对于业务及用户毫无影响. 停止服务的原因可能由于网卡.路由器.机房.CPU负载过高.内存溢出.自然灾害等不可预期的原 ...
最新文章
- Hyperface笔记
- Android Material Design之在RecyclerView中嵌套CardView实现
- linux smb 所有者,python-3.x - 在Linux上使用python从smb共享中获取文件的所有者。 - SO中文参考 - www.soinside.com...
- 怎么样解决小交换机引起的路由环路故障?
- 前端学习(1811):前端调试之css装饰cursor练习
- spring学习(40):注入数组类型
- 2020年边缘计算最新前沿报告:如何与核心云、5G、AI协同?如何打造新业态和部署运营?...
- 【英语学习】【Level 07】U03 Amazing wonders L6 My homeland, my pride
- echart data放入数组_线性表(数组、链表、队列、栈)详细总结
- 无盘服务器集群,镜像(无盘柜)-单活集群解决方案
- cp命令强制覆盖方式实现
- imu传感器工作原理_各种传感器工作原理汇总
- 从0开始写sql脚本到执行sql脚本。
- Java精品项目源码第111期小蜜蜂扩音器网上商城系统
- C# RSA2048 公钥加密,私钥解密
- 首屈一指的全球招聘与薪资支付平台Deel收购Zeitgold,以增强薪资和人工智能服务
- 解读:大数据分析及其数据来源
- iOS开发学习笔记-C语言学习(一)
- Eclipse护眼背景色
- 未来5年的人才需求达到2000万,掌握这门技能,月入上万不是梦