【nodejs高可用】如何部署一个稳定的nodejs应用

393 阅读9分钟

概述

本章将学习到,如何部署一个生产级别的Nodejs应用,会讲述到,应用如何做负载均衡,怎么去做实现进程管理和优雅退出,怎么做好健康检查和灰度发布等。

这里看一张图,一个网站是如何做的越来越大的~

一个网站的做大,由早期的单节点,到现在的多个节点。

真的印证了那句话,没有什么不是加机器不能的解决的。

如果再不行,就在加一层解决。

所谓的LB(负载均衡),业内会使用Nginx做为负载均衡。

早期的阿帕奇服务器,其实也是类似的作用。

image.png

当然后面有了网关的概念,结构又变成了这样。

image.png

网关起到的作用是保障,流量到达服务器之后是可靠的,不会被恶意爬虫爬挂。

当然这里的网关(gateway)也可能是集群的结构。

这里的Nginx,静态资源这些,都可以做缓存,也就是配置动静分离,这里不过多赘述。

Nginx,一般也需要部署多台,也就是主-从的结构。

image.png

主从一般通过keep-alive的形式进行通信保活。

一旦主Nginx挂了以后,从Nginx顶上去。

这里前端/运维人员,就有反应时间,去修主Ng服务,对主Ng进行重写等操作。

Nginx是7层负载均衡器,建立TCP需要内存成本,Nginx的抗流量能力跟机器的CPU,I/O等配置有关系。

其中网关呢,做统一的鉴权能力和限流。

在比较庞大,且复杂的互联网公司,有可能架构又是这样的。

接入层和LVS都可以做负载均衡。

image.png

nodejs应用的负载均衡

到这里时应用级别的负载均衡原理,上面是LVS,Ng的负载均衡。

NodeJs内置负载均衡的能力

一个任务有单点的风险的时候,可以把他分位一组任务,避免单点的压力。

可以优化响应时间,避免某个任务分配不均匀,有过载的问题。

负载均衡是并发维度的问题,本质上是差不多的。

从网络层到应用层的链路,是服务调用的时候做的。

Nodejs-Rpc的工作原理 - 集群(Cluter):

Nodejs本身是单线程的,但是Nodejs做服务端应用的时候,通常会搞多个实例出来,处理客户端请求,以此提升系统的吞吐量,更好的利用CPU(多核),这样被称之为集群。

const cluster = require('cluster');
const http = require('http');
const os = require('os');
 
// 获取 CPU 核心数
const numCPUs = os.cpus().length;
 
if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);
 
  // 为每个 CPU 核心创建一个工作进程
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();  // 创建工作进程
  }
 
  // 监听工作进程退出
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 退出`);
  });
 
} else {
  // 工作进程处理请求
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`请求由工作进程 ${process.pid} 处理`);
  }).listen(8000, () => {
    console.log(`工作进程 ${process.pid} 已启动,监听端口 8000`);
  });
}

其中集群的工作原理:

正常情况下,监听同一个端口是要报错的。(port被占用。)

Master主进程,fork os.cpu().length 个子进程(Worker);

Worker进程:创建服务的实例,给Master发消息通信

Master监听接收到请求后,转发给子进程处理,转发方法有两种:

round-robin,循环分发防止任务过载(这个实际上是负载均衡的一个算法,保证下面的子进程平均的接受流量)

Master创建监听 socket,OS调度机制难以捉摸,会是分发不稳定(工作进程,直接接连接),官方推荐的也是这种方法。

Worker之间是不共享内存的,每个进程都有自己的内存空间。

大概是这么一个逻辑,可以通过环境变量设置nodeJs的转发策略。

使用NODE_CLUSTER_SCHED_POLICE去设置。

image.png

什么叫RPC(远程接口调用)

RPC只是一个概念,我们传统的网页开发,都是客户端->服务端,客户端和服务端进行通信,RPC是服务端与服务端进行通信,实际上http和webservice也可以做为一种服务端的RPC通信。

一般互联网公司,都采用TCP的形式,做RPC通信。

Nodejs的通信,大概包含了传输协议 + 序列化的两个部分。

其中RPC也支持跨语言,跨服务去调度。

大名鼎鼎的Dubbo,也支持用Nodejs去远程调用。

cn.dubbo.apache.org/zh-cn/blog/…

现在的互联网时代,更推荐使用Grpc,反正都是一种远程调用方式,大差不差的。

中间需要考虑的,超时时间,重试次数这种,需要结合业务场景进行考虑。

RPC的一个通用的过程大概是这样。

image.png

当然可能会有一些私有协议,或者是Grpc这一种的,具体按照RPC的规则去调用。

至于分布式的RPC调用更为复杂:

有服务提供者(Provider),服务注册中心(Register Center),服务调用方(Consumer)

image.png

关于Round Robin和优先连接的实现策略,可以参考下:

blog.csdn.net/2401_886772…

nodejs应用优雅退出

先提出个问题,我需要暂停服务的场景,或者服务有啥风险,需要怎么下线。

使用kill把进程杀掉?

或者直接ctrl + c停掉了应用吗。

这种做法太简单粗暴了,旧的请求还没结束呢,结果服务就直接退出了,造成客户端连接断开。

image.png

如果日活比较大的情况下,会造成用户不好的体验,会被用户提工单,投诉等问题。

第二种问题,重启的过程中,会对依赖的数据库或者缓存中间件等,造成污染。

接下来介绍下,啥叫做优雅退出:

其实可以理解成一种程序设计理念,程序遭到严重错误等,避免给用户碰到一个糟糕的体验,以比较平滑的反馈,给用户返回。

如何实现?

这里提出个最简单的实现策略:监听Exit信号,手动关闭tcp连接。

分三步走:监听退出->关闭http->退出nodejs进程

就这么简单,在优雅退出里,尽可能保持用户的行为。

const http = require('http');
const process = require('process');

const server = http.createServer((req, res) => {
    setTimeout(() => {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Hello World\n');
    }, 5000);
}).listen(9090, function (err) {
    console.log('listen <http://localhost:9090>');
    console.log('pid = ' + process.pid)
});

process.on('SIGTERM', () => {
    console.log('SIGTERM received. Exiting...');
    server.close(
        () => {
            process.exit(0);
        }
    );
});

在集群方式下的优雅退出,相对会比较复杂。

大概流程是,Worker退出后->需要refork->监听Master退出了->master退出前杀掉全部的Worker

cluster.on('exit',(worker,code,signal)=>{...//remove对掉,关闭http,和上面类,这里不再演示。})

灰度发布机制/金丝雀发布/蓝绿发布

这里涉及到了几个问题。

公司应用里,如果发布一个新版本?

项目迭代到部署上线是有风险,如果有很多机器在生产环节上,如何保证发布的功能是正常的?

如果有问题,怎么回滚,回滚方案是啥?

(1)金丝雀发布:

部署一个新的功能,保证整体服务器问题的情况下,一种比较平滑的发布方案。

金丝雀发布就是一种灰度发布的方案。

金丝雀发布的优缺点

image.png

生产环境上,通过LB分流量的,流量进来之后,走到灰度机器。

一旦出现问题。可以随时把灰度机器之间Remove掉,就不会有流量再进到这条线,也保障了其他正常业务功能的稳定运行。

优点:环境是隔离的,影响很小,有问题回滚容易

缺陷:发布过程复杂,发布速度缓慢,验证流程长。

实际会通过配置中心(一般是前端界面)动态编写配置,之后调用服务器机器上启动的一个服务(java or node)来修改本地的nginx配置,之后调用 nginx -s reload 重新加载配置,不需要应用启动。

(3)蓝绿发布:

顾名思义,线上会有一个蓝的服务,和绿的服务。

准备两套环境,其中一套环境,在线上已经运行,流量百分之百打在这个环境上。

验证新的版本的可用性,先打到绿服务上,等到稳定之后,再把版本上线到这个环境上,就是平滑升级了。

这种发布存在一个问题,就是成本,太奢侈了,需要准备两套机器和环境。

但是比较可靠,理论上升级业务无感知,比较平滑。

(4)金丝雀-滚动发布(推荐方式)

目前所有大厂,包括笔主公司,比较常用的一种方式,那就是滚动。

一般都有流水线的方式,由web后台可视化操作。

操作的原理就是,把版本V1的流量之间切到版本Vn之上。

由于机器比较多,可能会分批次去进行,比如第一批次,切10%,第二批次切30%。

image.png

实际上,K8s,本身支持滚动更新和回退,理论上业务无感知,比较平滑

具体可以参考这个方案:juejin.cn/post/730679…

也比较平滑,比较推荐这种方式。

健康检查方案

一般健康检查是配合上面的发布策略一起结合着使用的,健康了->发布成功,不健康->发布失败。

实际上就是类似于,ping-pong,心跳的那种,就是监测自己的应用是否还活着。

应用需要暴露一个健康检查接口

业务上的架构可能非常复杂,需要设计一套合理的健康检查规则。

可能是网状的,可能是依赖树的结构。

这是一个最简单的简单检查的模型:

image.png

当出现超时/连接断开的情况,需要及时反馈给业务层。

NodeJs的程序实现:

具体可以参考下面这个库,业内已经有了不错的解决方案:

github.com/godaddy/ter…

微服务下,这些场景应该更为复杂,具体要按照公司的业务场景/架构,来制定合适的方案。

PM2进程管理

以上的那些,LB方案,宕机自动重启,健康检查,部分APM能力(监控能力不是很全),在PM2上都是支持的。

PM2,在Nodejs的社区里还是很强大的,部署PM2也比较容易。

全局安装PM2:

npm install pm2 -g

常见的使用方式在这里就不再过多介绍,看文档就好了。

其次,更推荐去阅读下PM2的源代码。

代码质量还可以,Nodejs的api用了很多,都是用的自己的api来实现的功能,没有什么黑魔法,容易阅读。

github.com/Unitech/pm2

但是目前,阿里官方的Eggjs不推荐使用pm2来管理应用,可能是想推自己内部的云Nodejs的监控系统,具体就不知道为啥了,留着继续探究下。

实际上,本文的内容,大部分都可以用K8s的方案去处理,各有各的方案,需要按照架构,部署方案等。去做相对的调整,具体的实现思路和方案都是差不多的。