Egg.js 多机平滑重启实践

鱼头的Web海洋

共 6370字,需浏览 13分钟

 · 2024-03-29

作者:图南

原文地址:https://zhuanlan.zhihu.com/p/84632879

前提

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

背景

我在公司处在一个侧重 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 新的进程,这样逐个重启过去的,可以理解为串行。84e4fe3406820ced7778e9058f650ed8.webp801f7673c7e127b70c90f0c13dadd47c.webp

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

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

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

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

新的方向:SLB 的利用

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

通常我们的服务是部署在多台 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.running === 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;9a9b0bec9e70f43160e05471bfedd2ac.webpf01d3976e5c3460c15ac377df6105ca1.webp

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

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

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

结尾:

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

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

如果你也对 eggjs,对 nodejs,对 js 有兴趣,有想探讨的问题,可以联系我的邮箱 tunan@gaoding.com。


浏览 11
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报