11

Egg.js 多机平滑重启实践

 3 years ago
source link: https://zhuanlan.zhihu.com/p/84632879
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 在项目中也是第一次实践,多动手能更为理解其整个生态

如果你也对 eggjs,对 nodejs,对 js 有兴趣,有想探讨的问题,可以联系我的邮箱 [email protected]

如果你对我们的工作感兴趣,想和我们一起攻克难关的话,也可以直接将简历发到我的邮箱。

我们有专职的 nodejs 研发工程师,有在厦门具有相当影响力的前端团队,相信喜欢 js 喜欢 nodejs 的你会喜欢我们的团队~

我们是:稿定(厦门)科技有限公司 - 平台技术部


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK