健康检查
前文提到我们通过网关把流量转发到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 插件中不允许有路由,所以我们通过中间件的形式实现,主要代码如下:
```
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`,里面包括前端代码资源名称和对应路径,类似:
```
{
"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`,使用方式:
```html
<script src='{{ctx.just.use("Page.js")}}'></script>
```
插件内部也是读取`manifest.json` 文件,输出加上 CDN 域名之后的 URL,比如上面的代码就转变为:
```html
<script src='https://qiyukf.nosdn.127.net/huke/static/f2065164/js/Page.chunk.js'></script>
```
其实之所以这么做,是为了利用前端代码多版本带来的好处,我们是使用文件hash 作为文件路径的一部分作为多版本控制的,这样每次发布后编译后会把新生成的文件路径写入 `manifest.json`,然后通过上面讲的方式就可以获取到最新版本的代码。
当然目前Node 和 前端代码在一起是不合理的,可能会导致不必要的发布,后续应该会完全分离,但是使用`manifest.json`也可以作为我们后续代码分离后的方案之一。
总结
技术方案的选择一般要结合团队已有的技术方案和业务需求,本文介绍的平滑发布方案在我们业务前期,确实解决了我们的发布问题,让发布变得更安全。
但是随着业务发展我们需要灰度环境,来更好的确保应用的健康状态,提前发现应用中的问题。另外我们还需要知道我们的应用的运行状态,所以在下一讲内容中,我们会分享关于灰度发布和应用监控相关的内容。