Node.js 中的異常根據發生方式分爲同步異常與異步異常,後者又進一步分爲 Thunk 異常與 Promise 異常,共 3 類異常:css
throw new Error();
。fs.readFile
讀不存在的文件,以回調第一個參數返回。reject
引發的或 async
方法中拋出的異常,能夠經過 Promise 的 catch
方法捕獲。在本文的 Node.js 版本 v12.8.2
中,未處理的同步異常會直接引發進程異常關閉,未處理的 Thunk 異常會被無視但若是在回調拋出就會引發進程異常關閉,未處理的 Promise 異常會引發進程警告事件但不會致使進程異常關閉。html
在一個 7 x 24 小時運行的企業級 Web 服務器集羣中,一般須要多層措施保障高可用性,針對程序異常至少在如下 3 層作好處理:node
本章將基於上一章已完成的工程 host1-tech/nodejs-server-examples - 03-middleware 結合上述 3 方面的思考對代碼進行調整。git
如今先寫入用於注入異常的接口以提供初級的混沌工程入口:github
// src/controllers/chaos.js const { 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.js const { 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
回調捕獲異步異常,可是沒法捕獲在異步方法中直接拋出的異常。所以訪問上述接口會出現如下效果:docker
須要注意 promise-error-throw 注入的異常並無被捕獲也沒有引發進程異常關閉,這會讓程序進入十分模糊的狀態,給整個 Web 服務埋下高度的不肯定性,有必要對此類異常增強處理:express
$ 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.js module.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.js const { 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 | 異常被捕獲並處理 |
如今程序的狀態變得很是可控了,接下來構建鏡像並結合重啓策略啓動容器:json
$ # 構建容器鏡像,命名爲 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 接口便可看到當服務進程異常關閉後會自動重啓並以指望的狀態持續運行下去。bootstrap
服務進程在重啓時會有短暫一段時間的不可用,在實際生產環境會使用負載均衡將訪問分發到多個應用節點提升可用性。須要提供健康狀態檢測來幫助負載均衡判斷流量去向。因爲當前的異常處理機制會保持程序的合理狀態,所以只要提供一個可訪問的接口就可以表明健康狀態:
// src/controllers/health.js const { 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.js const { 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.js const { 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 服務器(四):異常處理