网易智慧企业 Node.js 实践二: 平滑发布和前端代码

2020-12-30 16:07:46 浏览数 (1)

健康检查

在第一讲中,我们提到通过网关把流量转发到 Node 应用,那网关是如何确定 Node 应用的可用性呢?

如果 Node 应用在发布的过程中也把流量转发过来,就会导致请求失败,所以我们的网关会对 Node 应用进行健康检查,要首先确定 Node 应用是健康的,也就是可以对外服务的。

具体来说就是网关每隔30秒会调一下 Node 应用健康检查的 HTTP 接口,如果接口返回的 code 是200,那就表示 Node 应用是可用的,用户的请求在下次检查之前都会转发过来,如果返回其他 code,表示应用不可用,请求就不会转发过来。过30秒再重复这个过程。

这个方案实现起来非常简单,只要在 Node 添加个能正常请求的 HTTP 接口即可,比如我们用的接口 `/health/check`它的controller 内就 `this.ctx.body = 'OK'`就可以了。

如果 Node 应用正常启动,可以接受用户请求,那么这个接口返回 code 就会是200,如果这个接口不能正常访问,返回的code不是200,那么也意味着整个应用是不能访问的。

但上面这个方案就没有问题了吗?问题当然是存在的,比如我们在发布时候,首先要让 Node 应用下线,如果恰好 Node 应用刚被健康检查通过后就下线了,那么就会导致后面30秒内转发到 Node 应用的流量访问失败,所以我们有了升级方案-平滑发布。

平滑发布

平滑发布需要跟发布系统进行配合,也就是我们在发布应用的时候发布系统会自动调用 Node 应用的下线接口,发布完成之后会调用 Node 应用的上线接口,这样就可以通过一个全局变量控制应用的状态,而这个状态是和应用的真实状态没有关系的。

调用下线接口后,应用状态置为下线,然后等待一段时间才真正让应用下线,所以如果这时有流量进来,应用依然可以正常服务。

逻辑很简单,但是实现的时候要考虑到 Egg.js 的多进程模型,Egg.js 一般根据服务器的 CPU 核数来定启动相应数量的 Worker 进程,这样就可以完美利用多核资源。

每个进程里都跑的是同一份源代码,这些进程同时监听一个端口,所以当发布系统调用下线接口时,只有其中一个进程会收到请求,如果只是把收到请求的这个进程的全局变量置为下线的话,其它的进程在收到健康检查的时候依然返回的是在线状态,这样就不对了,所以要使用进程间通信,告诉所有进程下线。

基于这些分析我们实现了 Egg.js 插件 `pp-ndp` ,另外由于 Egg.js 插件中不允许有路由,所以我们通过中间件的形式实现,主要代码如下:

代码语言:javascript复制
```
const { request } = ctx;
const { path, hostname } = request;
if (path === online) {
    app.messenger.sendToApp(ONLINE, '');
    ctx.body = 'NDP: Nodejs Is Online';
} else if (path === offline) {
    app.messenger.sendToApp(OFFLINE, '');
    ctx.body = 'NDP: Nodejs Is Offline';
} else if (path === check) {
  ctx.body = 'NDP: Nodejs Start Success';
} else if (path === status) {
  if (app[ISONLINE]) {
    ctx.body = 'NDP: Nodejs Is Online';
  } else {
    ctx.status = 500;
  }
} else {
  await next();
}
```

当然这个方案的前提是有多台 Node 服务机器,并按分组进行发布。如果只有一台机器就没必要这么麻烦了,因为发布一定会导致停服。

插件 `pp-ndp` 为了满足不同业务需求,online、offline、check、status 这四个 URL 是支持自定义配置的。

这个方案不仅解决了我们平滑发布的问题,让发布不再那么恐怖,而且还可以利用这个方案让应用上线后能够更好的服务,比如:可以在应用获取配置之后再把应用置为上线状态,或者可以在应用成功注册或连接某服务之后再把应用置为上线状态。让应用保证最健康的状态对外服务。

代码上 CDN 和 代码发现

看到 CDN 可能会奇怪, Node 应用为什么要 CDN?

其实是因为我们为了方便使用同构渲染,而把前端代码和 Node 代码放在了一个应用里面,虽然这样解决了服务端渲染代码访问问题,但是客户端代码还是走 CDN 比较合理。

关于 webpack 使用 CDN, 网上有很多文章可以参考,我主要介绍下如何发现前端代码,包括代码上 CDN 和模版中插入前端代码 URL。

主要是使用 `webpack-manifest-plugin` 这个 webpack 插件,它会生成一个文件,比如我们用的 `manifest.json`,里面包括前端代码资源名称和对应路径,类似:

代码语言:javascript复制
```
{
  "vendor.js": "/static/f5e0281b/js/vendor.chunk.js",
  "vendor.js.map": "/static/f5e0281b/js/vendor.chunk.js.map",
  "Page.css": "/static/f2065164/css/Page.chunk.css",
  "Page.js": "/static/f2065164/js/Page.chunk.js",
  "Page.js.map": "/static/f2065164/js/Page.chunk.js.map",
}
```

只需要把这个文件内列的文件上传到 CDN 即可,不需自己手动去打包目录一个一个找。在上传 CDN 的时候给每个文件保持同样路径。使用我们实现的工具 `pp-cdn` 在发布过程中的代码编译完成之后进行上传。在 Node 模版中引用代码时,使用我们开发的 Egg.js 插件 `pp-just`,使用方式:

代码语言:javascript复制
```html
<script src='{{ctx.just.use("Page.js")}}'></script>
```

插件内部也是读取 `manifest.json` 文件,输出加上 CDN 域名之后的 URL,比如上面的代码就转变为:

代码语言:javascript复制
```html
<script src='https://qiyukf.nosdn.127.net/huke/static/f2065164/js/Page.chunk.js'></script>
```

其实之所以这么做,是为了利用前端代码多版本带来的好处,我们是使用文件 hash 作为文件路径的一部分作为多版本控制的,这样每次发布后编译后会把新生成的文件路径写入 `manifest.json`,然后通过上面讲的方式就可以获取到最新版本的代码。

当然目前 Node 和 前端代码在一起是不合理的,可能会导致不必要的发布,后续应该会完全分离,但是使用`manifest.json`也可以作为我们后续代码分离后的方案之一。

总结

技术方案的选择一般要结合团队已有的技术方案和业务需求,本文介绍的平滑发布方案在业务前期,确实解决了我们的发布问题,让发布变得更安全。

但是随着业务发展,我们需要灰度环境,来更好的确保应用的健康状态并提前发现应用中的问题。

另外我们还需要知道我们的应用的运行状态,所以在下一讲内容中我们会分享关于灰度发布和应用监控相关的内容。

- END -

0 人点赞