异样类型与解决办法

Node.js 中的异样依据产生形式分为同步异样与异步异样,后者又进一步分为 Thunk 异样与 Promise 异样,共 3 类异样:

  • 同步异样 就是同步执行过程中抛出的异样,比方 throw new Error();
  • Thunk 异样 是指产生在异步回调中的异样,比方 fs.readFile 读不存在的文件,以回调第一个参数返回。
  • Promise 异样 是指 reject 引起的或 async 办法中抛出的异样,能够通过 Promise 的 catch 办法捕捉。

在本文的 Node.js 版本 v12.8.2 中,未解决的同步异样会间接引起过程异样敞开,未解决的 Thunk 异样会被忽视但如果在回调抛出就会引起过程异样敞开,未解决的 Promise 异样会引起过程正告事件但不会导致过程异样敞开。

在一个 7 x 24 小时运行的企业级 Web 服务器集群中,通常须要多层措施保障高可用性,针对程序异样至多在以下 3 层做好解决:

  • 代码级别异样解决:应用编程语句及运行时机制对产生的异样进行解决。
  • 过程级别异样解决:依据过程状态与重启策略对异样过程进行治理。
  • 节点级别异样解决:通过负载平衡和容器编排等运维伎俩将拜访调离异样的节点。

本章将基于上一章已实现的工程 host1-tech/nodejs-server-examples - 03-middleware 联合上述 3 方面的思考对代码进行调整。

加上异样解决机制

当初先写入用于注入异样的接口以提供高级的混沌工程入口:

// src/controllers/chaos.jsconst { Router } = require('express');const ASYNC_MS = 800;class ChaosController {  async init() {    const router = Router();    router.get('/sync-error-handle', this.getSyncErrorHandle);    router.get('/sync-error-throw', this.getSyncErrorThrow);    router.get('/thunk-error-handle', this.getThunkErrorHandle);    router.get('/thunk-error-throw', this.getThunkErrorThrow);    router.get('/promise-error-handle', this.getPromiseErrorHandle);    router.get('/promise-error-throw', this.getPromiseErrorThrow);    return router;  }  getSyncErrorHandle = (req, res, next) => {    next(new Error('Chaos test - sync error handle'));  };  getSyncErrorThrow = () => {    throw new Error('Chaos test - sync error throw');  };  getThunkErrorHandle = (req, res, next) => {    setTimeout(() => {      next(new Error('Chaos test - thunk error handle'));    }, ASYNC_MS);  };  getThunkErrorThrow = () => {    setTimeout(() => {      throw new Error('Chaos test - thunk error throw');    }, ASYNC_MS);  };  getPromiseErrorHandle = async (req, res, next) => {    await new Promise((r) => setTimeout(r, ASYNC_MS));    next(new Error('Chaos test - promise error handle'));  };  getPromiseErrorThrow = async (req, res, next) => {    await new Promise((r) => setTimeout(r, ASYNC_MS));    throw new Error('Chaos test - promise error throw');  };}module.exports = async () => {  const c = new ChaosController();  return await c.init();};
// src/controllers/index.jsconst { Router } = require('express');const shopController = require('./shop');+const chaosController = require('./chaos');module.exports = async function initControllers() {  const router = Router();  router.use('/api/shop', await shopController());+  router.use('/api/chaos', await chaosController());  return router;};

Express 提供了默认的异样解决兜底逻辑,会将主动捕捉的异样并交给 finalhandler 解决(间接输入异样信息)。Express 能够主动捕捉同步异样并通过 next 回调捕捉异步异样,然而无奈捕捉在异步办法中间接抛出的异样。因而拜访上述接口会呈现以下成果:

URL成果
http://localhost:9000/api/chaos/sync-error-handle异样被捕捉并解决
http://localhost:9000/api/chaos/sync-error-throw异样被捕捉并解决
http://localhost:9000/api/chaos/thunk-error-handle异样被捕捉并解决
http://localhost:9000/api/chaos/thunk-error-throw引起过程异样敞开
http://localhost:9000/api/chaos/promise-error-handle异样被捕捉并解决
http://localhost:9000/api/chaos/promise-error-throw引起过程正告事件

须要留神 promise-error-throw 注入的异样并没有被捕捉也没有引起过程异样敞开,这会让程序进入非常含糊的状态,给整个 Web 服务埋下高度的不确定性,有必要对此类异样增强解决:

$ mkdir src/utils             # 新建 src/utils 目录寄存帮忙工具$ tree -L 2 -I node_modules   # 展现除了 node_modules 之外的目录内容构造.├── Dockerfile├── package.json├── public│   ├── glue.js│   ├── index.css│   ├── index.html│   └── index.js├── src│   ├── controllers│   ├── middlewares│   ├── moulds│   ├── server.js│   ├── services│   └── utils└── yarn.lock
// src/utils/cc.jsmodule.exports = function callbackCatch(callback) {  return async (req, res, next) => {    try {      await callback(req, res, next);    } catch (e) {      next(e);    }  };};
// src/server.js// ...async function bootstrap() {  // ...}+// 监听未捕捉的 Promise 异样,+// 间接退出过程+process.on('unhandledRejection', (err) => {+  console.error(err);+  process.exit(1);+});+bootstrap();
// src/controllers/chaos.jsconst { Router } = require('express');+const cc = require('../utils/cc');const ASYNC_MS = 800;class ChaosController {  async init() {    const router = Router();    router.get('/sync-error-handle', this.getSyncErrorHandle);    router.get('/sync-error-throw', this.getSyncErrorThrow);    router.get('/thunk-error-handle', this.getThunkErrorHandle);    router.get('/thunk-error-throw', this.getThunkErrorThrow);    router.get('/promise-error-handle', this.getPromiseErrorHandle);    router.get('/promise-error-throw', this.getPromiseErrorThrow);+    router.get(+      '/promise-error-throw-with-catch',+      this.getPromiseErrorThrowWithCatch+    );    return router;  }  // ...  getPromiseErrorThrow = async (req, res, next) => {    await new Promise((r) => setTimeout(r, ASYNC_MS));    throw new Error('Chaos test - promise error throw');  };++  getPromiseErrorThrowWithCatch = cc(async (req, res, next) => {+    await new Promise((r) => setTimeout(r, ASYNC_MS));+    throw new Error('Chaos test - promise error throw with catch');+  });}module.exports = async () => {  const c = new ChaosController();  return await c.init();};

再关上异样注入接口看一下成果:

URL成果
http://localhost:9000/api/chaos/promise-error-throw引起过程异样敞开
http://localhost:9000/api/chaos/promise-error-throw-with-catch异样被捕捉并解决

当初程序的状态变得十分可控了,接下来构建镜像并联合重启策略启动容器:

$ # 构建容器镜像,命名为 04-exception,标签为 1.0.0$ docker build -t 04-exception:1.0.0 .# ...Successfully tagged 04-exception:1.0.0$ # 以镜像 04-exception:1.0.0 运行容器,命名为 04-exception,重启策略为无条件重启$ docker run -p 9090:9000 -d --restart always --name 04-exception 04-exception:1.0.0 

拜访 http://localhost:9090 的各个 chaos 接口即可看到当服务过程异样敞开后会主动重启并以冀望的状态继续运行上来。

衰弱状态检测

服务过程在重启时会有短暂一段时间的不可用,在理论生产环境会应用负载平衡将拜访散发到多个利用节点进步可用性。须要提供衰弱状态检测来帮忙负载平衡判断流量去向。因为以后的异样解决机制会放弃程序的正当状态,因而只有提供一个可拜访的接口就可能代表衰弱状态:

// src/controllers/health.jsconst { Router } = require('express');class HealthController {  async init() {    const router = Router();    router.get('/', this.get);    return router;  }  get = (req, res) => {    res.send({});  };}module.exports = async () => {  const c = new HealthController();  return await c.init();};
// src/controllers/index.jsconst { Router } = require('express');const shopController = require('./shop');const chaosController = require('./chaos');+const healthController = require('./health');module.exports = async function initControllers() {  const router = Router();  router.use('/api/shop', await shopController());  router.use('/api/chaos', await chaosController());+  router.use('/api/health', await healthController());  return router;};

在后续生产环境部署时依据 /api/health 的状态码配置负载平衡检测利用节点衰弱状态即可。

补充更多异样解决

接下来用异样页面重定向替换 Express 默认异样兜底逻辑,并为店铺治理相干接口也加上 Promise 异样捕捉:

<!-- public/500.html --><html>  <head>    <meta charset="utf-8" />  </head>  <body>    <h1>零碎忙碌,请您稍后再试</h1>    <a href="/">返回首页</a>  </body></html>
// src/server.js// ...async function bootstrap() {  server.use(express.static(publicDir));  server.use('/moulds', express.static(mouldsDir));  server.use(await initMiddlewares());  server.use(await initControllers());+  server.use(errorHandler);  await promisify(server.listen.bind(server, port))();  console.log(`> Started on port ${port}`);}// ...+function errorHandler(err, req, res, next) {+  if (res.headersSent) {+    // 如果是在返回响应后果时产生了异样,+    // 那么交给 express 内置的 finalhandler 敞开链接+    return next(err);+  }++  // 打印异样+  console.error(err);+  // 重定向到异样指引页面+  res.redirect('/500.html');+}+bootstrap();
// src/controllers/shop.jsconst { Router } = require('express');const bodyParser = require('body-parser');const shopService = require('../services/shop');const { createShopFormSchema } = require('../moulds/ShopForm');+const cc = require('../utils/cc');class ShopController {  shopService;  async init() {    this.shopService = await shopService();    const router = Router();    router.get('/', this.getAll);    router.get('/:shopId', this.getOne);    router.put('/:shopId', this.put);    router.delete('/:shopId', this.delete);    router.post('/', bodyParser.urlencoded({ extended: false }), this.post);    return router;  }-  getAll = async (req, res) => {+  getAll = cc(async (req, res) => {    // ...-  }+  });-  getOne = async (req, res) => {+  getOne = cc(async (req, res) => {    // ...-  };+  });-  put = async (req, res) => {+  put = cc(async (req, res) => {    // ...-  };+  });-  delete = async (req, res) => {+  delete = cc(async (req, res) => {    // ...-  };+  });-  post = async (req, res) => {+  post = cc(async (req, res) => {    // ...-  };+  });}module.exports = async () => {  const c = new ShopController();  return await c.init();};

这样一来,残缺的异样解决就做好了。

本章源码

host1-tech/nodejs-server-examples - 04-exception

更多浏览

从零搭建 Node.js 企业级 Web 服务器(零):动态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异样解决