eggjs / egg

🥚 Born to build better enterprise frameworks and apps with Node.js & Koa
https://eggjs.org
MIT License
18.84k stars 1.81k forks source link

[RFC] egg readiness 改造 #3757

Open killagu opened 5 years ago

killagu commented 5 years ago

背景

readiness 定义

https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ liveness: The kubelet uses liveness probes to know when to restart a Container. For example, liveness probes could catch a deadlock, where an application is running, but unable to make progress. Restarting a Container in such a state can help to make the application more available despite bugs. readiness: The kubelet uses readiness probes to know when a Container is ready to start accepting traffic. A Pod is considered ready when all of its Containers are ready. One use of this signal is to control which Pods are used as backends for Services. When a Pod is not ready, it is removed from Service load balancers.

简单来说 liveness 代表应用是不是挂了,readiness 代表应用是不是可访问的。

启动

启动依赖

目前 egg 使用 ready 来代表插件是否启动完成,应用是否启动完成。目前在插件依赖关系复杂的情况下,应用启动时间很长。原因是不管什么插件,是否是弱依赖的,都需要准备完成。而事实上是部分插件在不 ready 的情况下应用也应该是可访问的。

举个例子:有个缓存插件,会阻塞应用启动。但是业务场景是无缓存的时候可以 fallback 到 db 去。

// cache-plugin
// app.js
module.exports = () => {
  app.beforeStart(async () => {
  await app.cache.ready();
  });
};

// cache.js
module.exports = app => {
  return class Cache extends Base {
  async _init() {
  await app.cacheConn.ready();
  }

  async fetchCache(key) {
  return app.cacheConn.get(key);
  }
  }
};

可以做如下修改,在弱依赖的时候,不阻塞应用的启动。

// config.default.js
module.exports = {
  const config = {
  cache: {
  dependency: false,
  },
  };
  return config;
};

// sdk-base.js
...
async ready() {
  if (this.options.dependency === false) {
  return true;
  }
  ...
}

get isReady() {
  ...
}
...

// cache.js
module.exports = app => {
  return class Cache extends Base {
  async _init() {
  await app.cacheConn.ready();
  }

  async fetchCache(key) {
  return app.cacheConn.isReady && app.cacheConn.get(key);
  }
  }
};

启动过程

目前的启动过程是,master 会去监听 cluster 的 listening 事件,在 worker 监听了正确的端口之后,就会认为启动是完成了。 按照 liveness 和 readiness,应该是 worker 在启动时候就要开始监听端口,liveness 是通了,然后会去访问 worker 的 readiness 接口判断是否 ready 了。 但是受限于 node 的多进程模型,可能 worker 进程的状态是不一致的,所以需要 master 进程对状态做聚合。

访问

Readiness probes runs on the container during its whole lifecycle.

目前的 ready 在应用启动完成之后就没有用了,但是 readiness 是在整个生命周期中都是会调用的。所以 ready 接口在插件、应用的状态发生改变之后,也需要做出变化,体现在 readiness 的返回结果上。

cc: @eggjs/core

qingdengyue commented 5 years ago

Liveness

我觉得可以不用跟生命周期结合。Kubernetes的含义上也是说明,它用来标记是否需要重启容器(POD Container)。亦即从kubernetes的角度来说,它只需要知道容器内的应用已经启动就可以了。毕竟在启动的过程中,无论在哪个阶段,这个值就应该认为是有效的。

Readiness

这个参数的业务含义大一点。我觉得插件需要提供两种机制:

1. 是否弱依赖

https://github.com/eggjs/egg-redis/pull/30 这里新增了一个参数: weakDependent ,值为:true,就不会必须ready才继续执行。

2. Health机制

可以提供loader,示例如下:

//app/health/redis.js
module.exports=(singleInstance)=>{
    //TODO: 判断插件是否处于health状态
   return true or false;
}

在egg启动的过程中,动态加载loader,使用类似WorkerManager的机制判断所有插件的health状态, 并默认提供一个 /health 接口用于外部进行健康状态检查。

这个思路是,是否health,其实egg本身是没办法知道的,只有插件方才知道如何判断。egg只需要定时获取插件提供的health状态即可。

killagu commented 5 years ago

@qingdengyue 感谢回复。我们意见是一致的。

qingdengyue commented 5 years ago

Liveness

可以在Master启动的时候,在一个目录比如 /home/存储一个文件egg.lock,这样Kubernetes的机制就可以通过文件是否存在来确定Liveness的状态是否为true。进程存活和Kubernetes来说,Kubernetes是个外部条件,一个外部条件如何能知道一个容器的应用是否Liveness。文件lock是个最方便的形式,URL方案因为依赖于整个egg启动成功,所以会跟Readiness雷同,所以此处使用URL形式不够完善。

Liveness 实现方案

1.在startCluster的实现中,增加写入lock文件操作,目录可以使用 baseDir,文件生成后路径为:baseDir/egg.lock 2.在 https://en.wikipedia.org/wiki/Unix_signal 信号的事件中,删除1中的lock文件

Readiness

首先,我的思路有个前提,就是假设egg和插件实现是两个独立的部分,以及假设egg和插件的实现不存在依赖关系,以及egg的启动成功与否不依赖插件的启动与否。

假设条件单独列如下: 1. 假设egg和插件实现是两个独立的部分 2. 假设egg和插件的实现不存在依赖关系 3. egg的启动成功与否不依赖插件的启动与否

有以上假设的原因是因为,对于所有不在egg中的其他部分,不应该会影响egg的启动过程,尤其是第三方的插件或者框架。

由于以上的假设,那么egg中就需要提供弱依赖和Health检查机制。 弱依赖:用于配置插件和框架是否可以中断egg的启动过程。 Health检查机制:用于确定插件和框架是否处于健康状态。

Readiness 实现方案

如下处理流程:

  1. 方案在egg中实现weakDependent和Health检测
  2. 插件或者框架使用weakDependent配置项和提供Health规范来实现健康检查机制。
  3. egg在loader加载的过程中加载插件实现的这两个内容
  4. egg didLoad 的处理中如果weakDependent为false,表示可以终止egg继续启动,这个在插件或者框架那边抛出异常WeakDependentException,egg检测到异常后,就调用egg的退出过程
  5. egg ready 触发后,启动startChecker处理Health检测,并输出日志。

    1. 是否弱依赖

    eggjs/egg-redis#30 这里新增了一个参数: weakDependent ,值为:true,就不会必须ready才继续执行。

    2. Health机制

    可以提供loader,示例如下:

    //app/health/redis.js
    module.exports=(singleInstance)=>{
        //TODO: 判断插件是否处于health状态
       return true or false;
    }

    在egg启动的过程中,动态加载loader,使用类似WorkerManager的机制判断所有插件的health状态, 并默认提供一个 /health 接口用于外部进行健康状态检查。

    这个思路是,是否health,其实egg本身是没办法知道的,只有插件方才知道如何判断。egg只需要定时获取插件提供的health状态即可。

popomore commented 5 years ago

之前写了一个插件,通过 API 的方式告诉 kubernetes 状态 https://github.com/eggjs/egg-healthy

  1. 这个 RFC 基于单进程做,如果是 kubernetes 要面向容器化的方案,容器即服务。
  2. 可以提供一个参数改变启动方式(fastStart),原来是 ready 后 listen,现在可以改成 listen 后等待 ready 在调用 healthy 接口。
  3. 生命周期的问题其实应该每个插件自己解决,如果这个插件的某段逻辑不依赖启动,那么可以不放到生命周期里面。插件依赖问题依然存在,所以生命周期各个环境不应该变化。

关于 liviness 其实是很业务化的问题,如何判断这个服务器是 down 了,还是通过 healthy 的 API 来做。

killagu commented 5 years ago

生命周期的问题其实应该每个插件自己解决,如果这个插件的某段逻辑不依赖启动,那么可以不放到生命周期里面。插件依赖问题依然存在,所以生命周期各个环境不应该变化。

对于不同的场景来说,依赖程度可能是不同的,是不是插件可以提供统一的配置?

关于 liviness 其实是很业务化的问题,如何判断这个服务器是 down 了,还是通过 healthy 的 API 来做。

现在 app 是否 ready 是感知所有的 readyCallback 完成状态。是不是可以更细化一些,app 可以去感知每个插件的状态,这样可以在 app 层面去计算出是否 ready 和是否有问题。

popomore commented 5 years ago

现在 app 是否 ready 是感知所有的 readyCallback 完成状态。是不是可以更细化一些,app 可以去感知每个插件的状态,这样可以在 app 层面去计算出是否 ready 和是否有问题。

这个回答的 liveness 有点不对,应该还是 readiness 的问题。既然是生命周期,肯定是和生命周期挂钩的,有些插件是在这个生命周期依赖上一个生命周期的数据,不然就改了实现了,这个回答也回了上面的问题。

killagu commented 5 years ago

关于 liviness 其实是很业务化的问题,如何判断这个服务器是 down 了,还是通过 healthy 的 API 来做。

现在 app 是否 ready 是感知所有的 readyCallback 完成状态。是不是可以更细化一些,app 可以去感知每个插件的状态,这样可以在 app 层面去计算出是否 ready 和是否有问题。

这个是我讲错了。我想讲的其实是 readiness。

偏业务化的应该是 readiness 才对吧?我理解 liveness 应该是进程存活着就是通过的。

killagu commented 5 years ago

既然是生命周期,肯定是和生命周期挂钩的,有些插件是在这个生命周期依赖上一个生命周期的数据,不然就改了实现了

插件提供弱依赖的配置是可以统一的,但是每个插件对弱依赖的处理是不同的。这里有点不太好处理