Egg.js 多机平滑重启实践

前提:

首先要声明的是,我们的应用都是在阿里云上多机部署的。当然这里不是安利文,而是给有相同问题的朋友一个实践的参考。

背景:

我在公司处在一个侧重 js 技术方向的团队,后端项目也较多基于 node.js 开发。项目几经更迭也经历了 koa1 --> koa2 --> egg.js 的框架变更。

在早期项目依赖 koa 的时候,部署方案就是依赖 gitlab-ci + pm2 的方式做自动化部署和进程管理。pm2 可以管理进程的启动和监控,也可以在进程意外终止的时候重新拉起新的进程保证项目持续运作。但缺点也很明显,首先 pm2 本身会需要资源,这个资源与项目进程的负载是成线性关系的,也就是当我们的并发量大的时候,进程需要更多的资源来处理请求,而 pm2 作为资源的分配者,也需要更多的资源来管理请求和进程的资源调度。甚至出现 god deamon 进程占用了1个多g内存😖。这本身来讲是额外的资源消耗,并不是我们所希望的。另一方面我们也遇到了在高并发的情况下,pm2 并不能实现 100% 的平滑重启。每当有新的代码被部署的时候,还是会出现一定的请求失败的情况。这与 pm2 本身有关,相关的问题不是单一的,这里不一一展开🙍‍♂️。

切换到 egg.js 之后,请求的调度任务由项目本身的 master 进程来管理,可以尽可能让项目最大化的利用硬件资源。而且少了 pm2 作为媒介,不用去理会 pm2 造成的影响,可以更加关注项目本身的问题。

但是 egg.js 提供的启动方案只有简单的 start 和 stop。也就是当我要更新项目的时候,一定要关闭所有进程然后再启动项目。这样会造成服务的短暂不可用的情况,显然不是我们希望看见的。

所以,我们通过各种尝试来完善 egg.js 的重启问题😁。

尝试:编写热重启脚本

在简单了解 pm2 的重启原理后,我们知道,pm2 先 fork 出一个新的进程,然后通过 ipc 通知一个进程关闭,当进程关闭后,pm2 再 fork 新的进程,这样逐个重启过去的,可以理解为串行。

通过 pm2 的这个方案,结合 egg-scripts 的源码,我们修改出了一个可以逐个启动进程的启动脚本 egg-cluster-script 😄

起初在请求量低的时候,这个方案看似是可行的(因为错误少,没发现)。但是当我们把服务对接给公司其他业务方后,请求量激增,这个方案的问题就暴露出来了😓:

  1. 每当我们 kill 一个进程的时候,在这个进程在真正退出之前依然会被 master 分配请求,这些请求并不能被消化,所以在重启的时候永远有一个不能处理请求的进程被分配了请求,造成大量的请求错误
  2. 当有新的 schedule 脚本上线的时候,无法添加到 master 进程中进行调度,你最后还是不得不重启整个项目

如果要深入到请求调度上的问题,这个改动的成本就相对较高了。最终,我们放弃了这个方案。转而寻求通过外部手段的方式来达到平滑重启的目的😖。

新的方向:SLB 的利用

首先我们前提中提到我司的服务都是部署在阿里云上的,基本的部署情况差不多如图:

通常我们的服务是部署在多台 ecs 上的,每台 ecs 上部署多个进程的应用。通过 SLB 做负载均衡,把请求根据权重适当的分配给每个 ecs🤔。

在 SLB 中,定时的健康检查判断每个 ecs 上的服务是不是可用的,当不健康的检查超出了给定的阈值,SLB 就会将 ecs 摘除,不会再将请求分发给这个 ecs,直到这台 ecs 的健康检查恢复正常。

通过这个健康检查的原理,当 ecs 被摘除的时候,我们就可以任意去摆布这台 ecs 上的进程了。

有了思路后,接下来就是指定实现的方案🧾:

  1. 给 app 添加健康状态的属性,例如:app.running = true,当 process 接收到特定的信号量的时候,会改变健康状态。不挂在 app 上也行,只要能保证全局找得到这个唯一的状态值;
// app.js
module.exports = class AppBook {
    /**
     *
     * @param {Egg.Application} app
     */
    constructor(app) {
        this.app = app;
        app.running = true;

        process.on('SIGINT', () => {
            app.running = false;
        });
    }
}

2. 项目提供一个健康检查的接口 /devops/health ,通常情况下我们采取 head 请求直接返回 状态码,当 app.running = true 的时候返回 204,否则返回 500;

const { Controller } = require('egg');

module.exports = class DevopsController extends Controller {
  healthCheck() {
    const { ctx } = this;

    if(this.app.runnint === true) {
      ctx.body = null;
    }
    else {
      ctx.status = 500;
      ctx.body = '';
    }
  }
} 

3. 编写信号发送脚本改变 app 的健康状态;

// scripts/health-down.js
// 这里的 findNodeProcess,appWorkerPath,titleTemplate 都可以从 egg-script 中找到
async function run () {
    const processList = await findNodeProcess(item => {
        const cmd = item.cmd;
        const title = 'your-app-name'
        return cmd.includes(appWorkerPath) && cmd.includes(util.format(titleTemplate, title));
    });

    for(const pro of processList) {
        const pid = pro.pid;
        process.kill(pid,'SIGINT');
    }

    // 健康状态修改之后暂定 5s 让 slb 摘除 ecs 后再进行进程处理
    await new Promise(resolve => setTimeout(resolve, 5000));
}

run();

4. 给 package.json 添加 scripts: "health:down": ''node scripts/health-down.js", 我们是使用 pm2-depoly 执行的部署。所以在 ecosystem.config.js 中,应用的 deploy 做修改;

// ecosystem.config.js
module.exports = {
    deploy: {
        production: {
            user: 'your-deploy-user',
            host: [...'your-ecs-hosts'],
            ref: 'deploy-ref',
            repo: 'project-repo',
            ssh_options: ['StrictHostKeyChecking=no'],
            path: 'deploy-path-on-ecs',
            'pre-deploy': 'git fetch && npm run health:down',
            'post-deploy': 'npm install --production --no-save && npm stop && npm start'
        }
    }
};

5. 设置健康检查策略,让 slb 可以动态摘除/添加 ecs;

我这里健康检查的频率设置的相对频繁,可以根据自己的需要修改这里的配置。大体的意思就是只要 2 次检查不通过就会把 ecs 摘除,之后只要连续两次检查正常就会把 ecs 重新添加回来。

之后就是结合自己的 ci 来自动部署了。通过这个方式,我们的项目可以在任何时候实现项目的平滑重启,经验证即使在高峰时段也没有出现异常。

当前已经应用的项目是日访问量在 2亿😺 左右的服务,正在逐步推广到其他服务中去。这个实践也并非针对 eggjs 项目,应该是具有相对通用性的方案,可以在任意语言和框架中采用😁。

结尾:

一定有人问为什么一开始不直接采用 SLB 的方案而要绕这么大个弯子。其实原因挺多的

  1. 首先,我一开始是真的没想到这个方案😳
  2. 作为一个一线代码搬运工,什么事都希望能通过编码来解决,这是一种执着💊
  3. eggjs 在项目中也是第一次实践,多动手能更为理解其整个生态😂

转自 https://zhuanlan.zhihu.com/p/84632879

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值