一年Node.js開發開發經驗總結

本文首發於公衆號:符合預期的CoyPan

寫在前面

不知不覺的,寫Node.js已經一年了。不一樣於最開始的demo、本地工具等,這一年裏,都是用Node.js寫的線上業務。從一開始的Node.js同構直出,到最近的Node接入層,也算是對Node開發入門了吧。目前,我一我的維護了大部分組內流傳下來的Node服務,包括內部系統和線上服務。新增的後臺服務,也是儘量地使用Node進行開發。本文是一下本身最近的一些小小的總結和思考。javascript

本文不會深刻講解Node.js自己的特性,架構等等。我也沒有寫過Node擴展或者庫什麼的,對Node.js的瞭解也並不夠深刻。

爲什麼用Node

對於我來講,對於團隊來講,適用Node的緣由其實很簡單:開發起來快。熟悉JS的前端同窗能夠很快上手,節省成本。選一個http server庫起一個server,選擇合適的中間件,匹配好請求路由,看狀況合理使用ORM庫連接數據庫、增刪改查便可。前端

Node的適用場景

Node.js 使用了一個事件驅動、非阻塞式 I/O 的模型,使其輕量又高效。這種模型使得Node.js 能夠避免了因爲須要等待輸入或者輸出(數據庫、文件系統、Web服務器...)響應而形成的 CPU 時間損失。因此,Node.js適合運用在高併發、I/O密集、少許業務邏輯的場景。java

對應到平時具體的業務上,若是是內部的系統,大部分僅僅就是須要對某個數據庫進行增刪改查,那麼Server端直接就是Node.js一把梭。算法

對於線上業務,若是流量不大,而且業務邏輯簡單的狀況下,Server端也能夠徹底使用Node.js。對於流量巨大,複雜度高的項目,通常用Node.js做爲接入層,後臺同窗負責實現服務。以下圖:數據庫

一樣是寫JS,Node.js開發和頁面開發有什麼區別

在瀏覽器端開發頁面,是和用戶打交道、重交互,瀏覽器還提供了各類Web Api供咱們使用。Node.js主要面向數據,收到請求後,返回具體的數據。這是二者在業務路徑上的區別。而真正的區別實際上是在於業務模型上(業務模型,這是我本身瞎想的一個詞)。直接用圖表示吧。瀏覽器

開發頁面時,每個用戶的瀏覽器上都有一份JS代碼。若是代碼在某種狀況下崩了,只會對當前用戶產生影響,並不會影響其餘用戶,用戶刷新一下便可恢復。而在Node.js中,在不開啓多進程的狀況下,全部用戶的請求,都會走進同一份JS代碼,而且只有一個線程在執行這份JS代碼。若是某個用戶的請求,致使發生錯誤,Node.js進程掛掉,server端直接就掛了。儘管可能有進程守護,掛掉的進程會被重啓,可是在用戶請求量大的狀況下,錯誤會被頻繁觸發,可能就會出現server端不停掛掉,不停重啓的狀況,對用戶體驗形成影響。緩存

以上,多是Node.js開發和前端JS開發最大的區別。服務器

Node.js開發時的注意事項

用戶在訪問Node.js服務時,若是某一個請求卡住了,服務遲遲不能返回結果,或者說邏輯出錯,致使服務掛掉,都會帶來大規模的體驗問題。server端的目標,就是要 快速、可靠 地返回數據。架構

緩存

因爲Node.js不擅長處理複雜邏輯(JavaScript自己執行效率較低),若是要用Node.js作接入層,應該避免複雜的邏輯。想要快速處理數據並返回,一個相當重要的點:使用緩存。併發

例如,使用Node作React同構直出,renderToString這個Api,能夠說是比較重的邏輯了。若是頁面的複雜度高,每次請求都完整執行renderToString,會長時間佔用線程來執行代碼,增長響應時間,下降服務的吞吐量。這個時候,緩存就十分重要了。

實現緩存的主要方式:內存緩存。可使用Map,WeakMap,WeakRef等實現。參考如下簡單的示例代碼:

const cache = new Map();

router.get('/getContent', async (req, res) => {
  const id = req.query.id;
  
  // 命中緩存
  if(cache.get(id)) {
    return res.send(cache.get(id));
  }
  
  // 請求數據
  const rsp = await rpc.get(id);
     // 通過一頓複雜的操做,處理數據
  const content = process(rsp);
  // 設置緩存
  cache.set(id, content);
  
  return res.send(content);
});

使用緩存時,有一個很重要的問題是:內存緩存如何更新。一種最簡單的方法,開一個定時器,按期刪除緩存,下一次請求到來時,從新設置緩存便可。在上述代碼中,增長以下代碼:

setTimeout(function() {
  cache.clear();
}, 1000 * 60); // 1分鐘刪除一次緩存

若是server端徹底使用Node實現,須要用Node端直接鏈接數據庫,在數據時效性要求不過高、且流量不太大的狀況下,就可使用上述相似的模型,以下圖。這樣能夠下降數據庫的壓力且加快Node的響應速度。

另外,還須要注意內存緩存的大小。若是一直往緩存裏寫入新數據,那麼內存會愈來愈大,最終爆掉。能夠考慮使用LRU(Least Recently Used)算法來作緩存。開闢一塊內存專門做爲緩存區域。當緩存大小達到上限時,淘汰最久未使用的緩存。

內存緩存會隨着進程的重啓而所有失效。

當後臺業務比較複雜,接入層流量,數據量較大時,可使用以下的架構,使用獨立的內存緩存服務。Node接入層直接從緩存服務取數據,後臺服務直接更新緩存服務。

固然,上圖中的架構是最簡單的情形,現實中還須要考慮分佈式緩存、緩存一致性的問題。這又是另一個話題了。

錯誤處理

因爲Node.js語言的特性,Node服務是比較容易出錯的。而一旦出錯,形成的影響就是服務不可用。所以,對於錯誤的處理十分的重要。

處理錯誤,最經常使用的就是try catch 了。但是 try catch沒法捕獲異步錯誤。Node.js中,異步操做是十分常見的,異步操做主要是在回調函數中暴露錯誤。看一個例子:

const readFile = function(path) {
    return new Promise((resolve,reject) => {
        fs.readFile(path, (err, data) => {
            if(err) { 
                throw err; // catch沒法捕獲錯誤,這和Node的eventloop有關。
        // reject(err); // catch能夠捕獲
      }
      resolve(data);
        });
    });
}

router.get('/xxx', async function(req, res) {
  try {
    const res = await readFile('xxx');
    ...
  } catch (e){
    // 捕獲錯誤處理
    ...
    res.send(500);
  }
});

上面的代碼中,readFile 中 throw 出來的錯誤,是沒法被catch捕獲的。若是咱們把 throw err 換成 Promise.reject(err),catch中是能夠捕獲到錯誤的。

咱們能夠把異步操做都Promise化,而後統一使用 async 、try、catch 來處理錯誤

可是,總會有地方會被遺漏。這個時候,可使用process來捕獲全局錯誤,防止進程直接退出,致使後面的請求掛掉。示例代碼:

process.on('uncaughtException', (err) => {
  console.error(`${err.message}\n${err.stack}`);
});

process.on('unhandledRejection', (reason, p) => {
  console.error(`Unhandled Rejection at: Promise ${p} reason: `, reason);
});

關於Node.js中錯誤的捕獲,還可使用domain模塊。如今這個模塊已經不推薦使用了,我也沒有在項目中實踐過,這裏就不展開了。Node.js 近幾年推出的 async_hooks 模塊,也還處於實驗階段,不太建議線上環境直接使用。作好進程守護,開啓多進程,錯誤告警及時修復,養成良好的編碼規範,使用合適的框架,才能提升Node服務的效率及穩定性。

寫在後面

本文總結了Node.js開發一年多以來的實踐總結等。Node.js的開發與前端網頁的開發思路不一樣,着重點不同。我正式開發Node.js的時間也不算太長,一些點並無深刻的理解,本文僅僅是一些經驗之談。歡迎交流。

相關文章
相關標籤/搜索