<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                [TOC] # Node 出現 uncaughtException 之后的優雅退出方案 Node 的異步特性是它最大的魅力,但是在帶來便利的同時也帶來了不少麻煩和坑,錯誤捕獲就是一個。由于 Node 的異步特性,導致我們無法使用 try/catch 來捕獲回調函數中的異常,例如: ~~~ try { console.log('進入 try/catch'); require('fs').stat('SOME_FILE_DOES_NOT_EXIST', function readCallback(err, content) { if (err) { throw err; // 拋出異常 } }); } catch (e) { // 這里捕獲不到 readCallback 函數中拋出的異常 } finally { console.log('離開 try/catch'); } ~~~ 運行結果是: ~~~ 進入 try/catch 離開 try/catch test.js:7 throw err; // 拋出異常 ^ Error: ENOENT, stat 'SOME_FILE_DOES_NOT_EXIST' ~~~ 上面代碼中由于?`fs.stat`?去查詢一個不存在的文件的狀態,導致?`readCallback`?拋出了一個異常。由于?`fs.read`?的異步特性,`readCallback`?函數的調用發生在?`try/catch`?塊結束之后,所以該異常不會被 try/catch 捕獲。之后 Node 會觸發?`uncaughtException`?事件,如果這個事件依然沒有得到響應,整個進程(`process`)就會 crash。 程序員永遠無法保證代碼中不出現?`uncaughtException`,即便是自己代碼寫的足夠小心,也不能保證用的第三方模塊沒有 bug,例如: ~~~ var deserialize = require('deserialize'); // 假設 deserialize 是一個帶有 bug 的第三方模塊 // app 是一個 express 服務對象 app.get('/users', function (req, res) { mysql.query('SELECT * FROM user WHERE id=1', function (err, user) { var config = deserialize(user.config); // 假如這里觸發了 deserialize 的 bug res.send(config); }); }); ~~~ 如果不幸觸發了?`deserialize`?模塊的 bug,這里就會拋出一個異常,最終結果是整個服務 crash。 當這種情況發生在 Web 服務上時結果是災難性的。`uncaughtException`?錯誤會導致當前的所有的用戶連接都被中斷,甚至不能返回一個正常的 HTTP 錯誤碼,用戶只能等到瀏覽器超時才能看到一個`no data received`?錯誤。 這是一種非常野蠻粗暴的異常處理機制,任何線上服務都不應該因為?`uncaughtException`?導致服務器崩潰。一個友好的錯誤處理機制應該滿足三個條件: 1. 對于引發異常的用戶,返回 500 頁面 2. 其他用戶不受影響,可以正常訪問 3. 不影響整個進程的正常運行 很遺憾的是,保證?`uncaughtException`?不影響整個進程的健康運轉是不可能的。當 Node 拋出`uncaughtException`?異常時就會丟失當前環境的堆棧,導致 Node 不能正常進行內存回收。也就是說,每一次?`uncaughtException`?都有可能導致內存泄露。 既然如此,退而求其次,我們可以在滿足前兩個條件的情況下退出進程以便重啟服務。 ## 用 domain 來捕獲異步異常 普遍的思路是,如果可以通過某種方式來捕獲回調函數中的異常,那么就不會有`uncaughtException`?錯誤導致的崩潰。為了解決這個問題,Node 0.8 之后的版本新增了?`domain`?模塊,它可以用來捕獲回調函數中拋出的異常。 `domain`?主要的 API 有?`domain.run`?和?`error`?事件。簡單的說,通過?`domain.run`?執行的函數中引發的異常都可以通過?`domain`?的?`error`?事件捕獲,例如: ~~~ var domain = require('domain'); var d = domain.create(); d.run(function () { setTimeout(function () { throw new Error('async error'); // 拋出一個異步異常 }, 1000); }); d.on('error', function (err) { console.log('catch err:', err); // 這里可以捕獲異步異常 }); ~~~ 通過?`domain`?模塊,以及 JavaScript 的詞法作用域特性,可以很輕易的為引發異常的用戶返回 500 頁面。以 express 為例: ~~~ var app = express(); var server = require('http').createServer(app); var domain = require('domain'); app.use(function (req, res, next) { var reqDomain = domain.create(); reqDomain.on('error', function (err) { // 下面拋出的異常在這里被捕獲 res.send(500, err.stack); // 成功給用戶返回了 500 }); reqDomain.run(next); }); app.get('/', function () { setTimeout(function () { throw new Error('async exception'); // 拋出一個異步異常 }, 1000); }); ~~~ 上面的代碼將 domain 作為一個中間件來使用,保證之后 express 所有的中間件都在?`domain.run`函數內部執行。這些中間件內的異常都可以通過?`error`?事件來捕獲。 盡管借助于閉包,我們可以正常的給用戶返回 500 錯誤,但是?`domain`?捕獲到錯誤時依然會丟失堆棧信息,此時已經無法保證程序的健康運行,必須退出。Node http server 提供了?`close`?方法,該方法在調用時會停止 server 接收新的請求,但不會斷開當前已經建立的連接。 ~~~ reqDomain.on('error', function () { try { // 強制退出機制 var killTimer = setTimeout(function () { process.exit(1); }, 30000); killTimer.unref(); // 非常重要 // 自動退出機制,停止接收新鏈接,等待當前已建立連接的關閉 server.close(function () { // 此時所有連接均已關閉,此時 Node 會自動退出,不需要再調用 process.exit(1) 來結束進程 }); } catch(e) { console.log('err', e.stack); } }); ~~~ 這個例子來自 Node 的文檔。其中有幾個關鍵點: * Node 有個非常好的特性,所有連接都被釋放后進程會自動結束,所以不需要再?`server.close`方法的回調函數中退出進程 * 強制退出機制: 因為用戶連接有可能因為某些原因無法釋放,在這種情況下應該強制退出整個進程。 * `killTimer.unref()`: 如果不使用?`unref`?方法,那么即使 server 的所有連接都關閉,Node 也會保持運行直到?`killTimer`?的回調函數被調用。`unref`?可以創建一個"不保持程序運行"的計時器。 * 處理異常時要小心的把異常處理邏輯用 try/catch 包住,避免處理異常時拋出新的異常 通過?`domain`?似乎就已經解決了我們的需求: 給觸發異常的用戶一個 500,停止接收新請求,提供正常的服務給已經建立連接的用戶,直到所有請求都已結束,退出進程。但是,理想很豐滿,現實很骨感,`domain`?有個最大的問題,它[不能捕獲所有的異步異常](http://cnodejs.org/topic/516b64596d38277306407936)!。也就是說,即使用了?`domain`,程序依然有因為?`uncaughtException`?crash 的可能。 所幸的是我們可以監聽?`uncaughtException`?事件。 ## `uncaughtException`?事件 `uncaughtException`?是一個非常古老的事件。當 Node 發現一個未捕獲的異常時,會觸發這個事件。并且如果這個事件存在回調函數,Node 就不會強制結束進程。這個特性,可以用來彌補`domain`?的不足: ~~~ process.on('uncaughtException', function (err) { console.log(err); try { var killTimer = setTimeout(function () { process.exit(1); }, 30000); killTimer.unref(); server.close(); } catch (e) { console.log('error when exit', e.stack); } }); ~~~ `uncaughtException`?事件的缺點在于無法為拋出異常的用戶請求返回一個 500 錯誤,這是由于`uncaughtException`?丟失了當前環境的上下文,比如下面的例子就是它做不到的: ~~~ javascript app.get('/', function (req, res) { setTimeout(function () { throw new Error('async error'); // uncaughtException, 導致 req 的引用丟失 res.send(200); }, 1000); }); process.on('uncaughtException', function (err) { res.send(500); // 做不到,拿不到當前請求的 res 對象 }); ~~~ 最終出錯的用戶只能等待瀏覽器超時。 ## `domain`?+?`uncaughtException` 所以,我們可以結合兩種異常捕獲機制,用?`domain`?來捕獲大部分的異常,并且提供友好的 500 頁面以及優雅退出。對于剩下的異常,通過?`uncaughtException`?事件來避免服務器直接 crash。 ~~~ var app = express(); var server = require('http').create(app); var domain = require('domain'); // 使用 domain 來捕獲大部分異常 app.use(function (req, res, next) { var reqDomain = domain.create(); reqDomain.on('error', function () { try { var killTimer = setTimeout(function () { process.exit(1); }, 30000); killTimer.unref(); server.close(); res.send(500); } catch (e) { console.log('error when exit', e.stack); } }); reqDomain.run(next); }); // uncaughtException 避免程序崩潰 process.on('uncaughtException', function (err) { console.log(err); try { var killTimer = setTimeout(function () { process.exit(1); }, 30000); killTimer.unref(); server.close(); } catch (e) { console.log('error when exit', e.stack); } }); ~~~ ## 其他的一些問題 ### `express`?中異常的處理 使用?`express`?時記住一定不要在 controller 的異步回調中拋出異常,例如: ~~~ app.get('/', function (req, res, next) { // 總是接收 next 參數 mysql.query('SELECT * FROM users', function (err, results) { // 不要這樣做 if (err) throw err; // 應該將 err 傳遞給 errorHandler 處理 if (err) return next(err); }); }); app.use(function (err, req, res, next) { // 帶有四個參數的 middleware 專門用來處理異常 res.render(500, err.stack); }); ~~~ ### 和 cluster 一起使用 cluster 是 node 自帶的負載均衡模塊,使用 cluster 模塊可以方便的建立起一套 master/slave 服務。在使用 cluster 模塊時,需要注意不僅需要調用?`server.close()`?來關閉連接,同時還需要調用`cluster.worker.disconnect()`?通知 master 進程已停止服務: ~~~ var cluster = require('cluster'); process.on('uncaughtException', function (err) { console.log(err); try { var killTimer = setTimeout(function () { process.exit(1); }, 30000); killTimer.unref(); server.close(); if (cluster.worker) { cluster.worker.disconnect(); } } catch (e) { console.log('error when exit', e.stack); } }); ~~~ ### 不要通過?`uncaughtException`?來忽略錯誤 當?`uncaughtException`?事件有一個以上的?`listener`?時,會阻止 Node 結束進程。因此就有一個廣泛流傳的做法是監聽?`process`?的?`uncaughtException`?事件來阻止進程退出,這種做法有內存泄露的風險,所以千萬不要這么做: ~~~ javascript process.on('uncaughtException', function (err) { // 不要這么做 console.log(err); }); ~~~ ### pm2 對于?`uncaughtException`?的額外處理 如果你在用 pm2 0.7.1 之前的版本,那么要當心。pm2 有一個 bug,如果進程拋出了`uncaughtException`,無論代碼中是否捕獲了這個事件,進程都會被 pm2 殺死。0.7.2 之后的 pm2 解決了這個問題。 ### 要小心 worker.disconnect() 如果你在退出進程時希望可以發消息給監控服務器,并且還使用了 cluster,那么這個時候要特別小心,比如下面的代碼: ~~~ var udpLog = dgram.createSocket('udp4'); var cluster = require('cluster'); process.on('uncaughtException', function (err) { udpLog.send('process ' + process.pid + ' down', /* ... 一些發送 udp 消息的參數 ...*/); server.close(); cluster.worker.disconnect(); }); ~~~ 這份代碼就不能正常的將消息發送出去。因為?`udpLog.send`?是一個異步方法,真正發消息的操作發生在下一個事件循環中。而在真正的發送消息之前?`cluster.worker.disconnect()`?就已經執行了。`worker.disconnect()`?會在當前進程沒有任何鏈接之后,殺掉整個進程,這種情況有可能發生在發送 log 數據之前,導致 log 數據發不出去。 一個解決方法是在?`udpLog.send`?方法發送完數據后再調用?`worker.disconnect`: ~~~ var udpLog = dgram.createSocket('udp4'); var cluster = require('cluster'); process.on('uncaughtException', function (err) { udpLog.send('process ' + process.pid + ' down', /* ... 一些發送 udp 消息的參數 ...*/, function () { cluster.worker.disconnect(); }); server.close(); // 保證 worker.disconnect 不會拖太久.. setTimeout(function () { cluster.worker.disconnect(); }, 100).unref(); }); ~~~ ## 小結 說了這么多,結論是,目前為止(Node 0.10.25),依然沒有一個完美的方案來解決任意異常的優雅退出問題。用?`domain`?來捕獲大部分異常,并且通過?`uncaughtException`?避免程序 crash 是目前來說最理想的方案。回調異常的退出問題在遇到 cluster 以后會更加復雜,特別是對于連接關閉的處理要格外小心。 # 參考資料 [Node 出現 uncaughtException 之后的優雅退出方案](https://www.infoq.cn/article/quit-scheme-of-node-uncaughtexception-emergence/)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看