Node.js 指南(域模塊剖析)

域模塊剖析

可用性問題

隱式行爲

開發人員能夠建立新域,而後只需運行domain.enter(),而後,它充當未來拋出者沒法觀察到的任何異常的萬能捕捉器,容許模塊做者攔截不一樣模塊中不相關代碼的異常,防止代碼的發起者知道本身的異常。node

如下是一個間接連接模塊如何影響另外一個模塊的示例:segmentfault

// module a.js
const b = require('./b');
const c = require('./c');


// module b.js
const d = require('domain').create();
d.on('error', () => { /* silence everything */ });
d.enter();


// module c.js
const dep = require('some-dep');
dep.method();  // Uh-oh! This method doesn't actually exist.

因爲模塊b進入域但從不退出,任何未捕獲的異常都將被吞噬,不讓模塊c知道它爲何沒有運行整個腳本,留下可能部分填充的module.exports。這樣作與監聽'uncaughtException'不一樣,由於後者明確意味着全局捕獲錯誤,另外一個問題是在任何'uncaughtException'處理程序以前處理域,並阻止它們運行。服務器

另外一個問題是,若是事件發射器上沒有設置'error'處理程序,域會自動路由錯誤,對此沒有可選的插入機制,而是自動跨整個異步鏈傳播。這看起來彷佛頗有用,可是一旦異步調用深度爲兩個或更多模塊,其中一個不包含錯誤處理程序,域的建立者將忽然捕獲意外異常,而且拋出者的異常將被做者忽視。less

如下是一個簡單的示例,說明缺乏'error'處理程序如何容許活動域攔截錯誤:dom

const domain = require('domain');
const net = require('net');
const d = domain.create();
d.on('error', (err) => console.error(err.message));

d.run(() => net.createServer((c) => {
  c.end();
  c.write('bye');
}).listen(8000));

即便經過d.remove(c)手動刪除鏈接也不會阻止鏈接的錯誤被自動攔截。異步

困擾錯誤路由和異常處理的失敗是錯誤被冒出的不一致,如下是嵌套域如何根據它們什麼時候發生以及不會使異常冒出的示例:socket

const domain = require('domain');
const net = require('net');
const d = domain.create();
d.on('error', () => console.error('d intercepted an error'));

d.run(() => {
  const server = net.createServer((c) => {
    const e = domain.create();  // No 'error' handler being set.
    e.run(() => {
      // This will not be caught by d's error handler.
      setImmediate(() => {
        throw new Error('thrown from setImmediate');
      });
      // Though this one will bubble to d's error handler.
      throw new Error('immediately thrown');
    });
  }).listen(8080);
});

能夠預期嵌套域始終保持嵌套,並始終將異常傳播到域堆棧中,或者異常永遠不會自動冒出,不幸的是,這兩種狀況都會發生,致使可能使人困惑的行爲甚至可能難以調試時序衝突。async

API差距

雖然基於使用EventEmitter的 API可使用bind(),而errback風格的回調可使用intercept(),可是隱式綁定到活動域的替代API必須在run()內部執行。這意味着若是模塊做者想要使用替代那些提到的機制來支持域,則他們必須本身手動實現域支持,而不是可以利用現有的隱式機制。函數

錯誤傳播

若是可能的話,跨嵌套域傳播錯誤並非直截了當的,現有文檔顯示了若是請求處理程序中存在錯誤,如何close() http服務器的簡單示例,它沒有解釋的是若是請求處理程序爲另外一個異步請求建立另外一個域實例,如何關閉服務器,使用如下做爲錯誤傳播失敗的簡單示例:post

const d1 = domain.create();
d1.foo = true;  // custom member to make more visible in console
d1.on('error', (er) => { /* handle error */ });

d1.run(() => setTimeout(() => {
  const d2 = domain.create();
  d2.bar = 43;
  d2.on('error', (er) => console.error(er.message, domain._stack));
  d2.run(() => {
    setTimeout(() => {
      setTimeout(() => {
        throw new Error('outer');
      });
      throw new Error('inner');
    });
  });
}));

即便在域實例用於本地存儲的狀況下,也能夠訪問資源,仍然沒法讓錯誤繼續從d2傳播回d1。快速檢查可能告訴咱們,簡單地從d2的域'error'處理程序拋出將容許d1而後捕獲異常並執行其本身的錯誤處理程序,雖然狀況並不是如此,檢查domain._stack後,你會看到堆棧只包含d2

這可能被認爲是API的失敗,但即便它確實以這種方式運行,仍然存在傳遞異​​步執行中的分支失敗的事實的問題,而且該分支中的全部進一步操做必須中止。在http請求處理程序的示例中,若是咱們觸發多個異步請求,而後每一個異步請求將write()的數據發送回客戶端,則嘗試將write()發送到關閉的句柄會產生更多錯誤,

異常資源清理

如下腳本包含在給定鏈接或其任何依賴項中發生異常的狀況下在小資源依賴關係樹中正確清理的更復雜示例,將腳本分解爲基本操做:

'use strict';

const domain = require('domain');
const EE = require('events');
const fs = require('fs');
const net = require('net');
const util = require('util');
const print = process._rawDebug;

const pipeList = [];
const FILENAME = '/tmp/tmp.tmp';
const PIPENAME = '/tmp/node-domain-example-';
const FILESIZE = 1024;
let uid = 0;

// Setting up temporary resources
const buf = Buffer.alloc(FILESIZE);
for (let i = 0; i < buf.length; i++)
  buf[i] = ((Math.random() * 1e3) % 78) + 48;  // Basic ASCII
fs.writeFileSync(FILENAME, buf);

function ConnectionResource(c) {
  EE.call(this);
  this._connection = c;
  this._alive = true;
  this._domain = domain.create();
  this._id = Math.random().toString(32).substr(2).substr(0, 8) + (++uid);

  this._domain.add(c);
  this._domain.on('error', () => {
    this._alive = false;
  });
}
util.inherits(ConnectionResource, EE);

ConnectionResource.prototype.end = function end(chunk) {
  this._alive = false;
  this._connection.end(chunk);
  this.emit('end');
};

ConnectionResource.prototype.isAlive = function isAlive() {
  return this._alive;
};

ConnectionResource.prototype.id = function id() {
  return this._id;
};

ConnectionResource.prototype.write = function write(chunk) {
  this.emit('data', chunk);
  return this._connection.write(chunk);
};

// Example begin
net.createServer((c) => {
  const cr = new ConnectionResource(c);

  const d1 = domain.create();
  fs.open(FILENAME, 'r', d1.intercept((fd) => {
    streamInParts(fd, cr, 0);
  }));

  pipeData(cr);

  c.on('close', () => cr.end());
}).listen(8080);

function streamInParts(fd, cr, pos) {
  const d2 = domain.create();
  const alive = true;
  d2.on('error', (er) => {
    print('d2 error:', er.message);
    cr.end();
  });
  fs.read(fd, Buffer.alloc(10), 0, 10, pos, d2.intercept((bRead, buf) => {
    if (!cr.isAlive()) {
      return fs.close(fd);
    }
    if (cr._connection.bytesWritten < FILESIZE) {
      // Documentation says callback is optional, but doesn't mention that if
      // the write fails an exception will be thrown.
      const goodtogo = cr.write(buf);
      if (goodtogo) {
        setTimeout(() => streamInParts(fd, cr, pos + bRead), 1000);
      } else {
        cr._connection.once('drain', () => streamInParts(fd, cr, pos + bRead));
      }
      return;
    }
    cr.end(buf);
    fs.close(fd);
  }));
}

function pipeData(cr) {
  const pname = PIPENAME + cr.id();
  const ps = net.createServer();
  const d3 = domain.create();
  const connectionList = [];
  d3.on('error', (er) => {
    print('d3 error:', er.message);
    cr.end();
  });
  d3.add(ps);
  ps.on('connection', (conn) => {
    connectionList.push(conn);
    conn.on('data', () => {});  // don't care about incoming data.
    conn.on('close', () => {
      connectionList.splice(connectionList.indexOf(conn), 1);
    });
  });
  cr.on('data', (chunk) => {
    for (let i = 0; i < connectionList.length; i++) {
      connectionList[i].write(chunk);
    }
  });
  cr.on('end', () => {
    for (let i = 0; i < connectionList.length; i++) {
      connectionList[i].end();
    }
    ps.close();
  });
  pipeList.push(pname);
  ps.listen(pname);
}

process.on('SIGINT', () => process.exit());
process.on('exit', () => {
  try {
    for (let i = 0; i < pipeList.length; i++) {
      fs.unlinkSync(pipeList[i]);
    }
    fs.unlinkSync(FILENAME);
  } catch (e) { }
});
  • 當新鏈接發生時,同時:

    • 在文件系統上打開一個文件
    • 打開管道到獨惟一的socket
  • 異步讀取文件的塊
  • 將塊寫入TCP鏈接和任何監聽sockets
  • 若是這些資源中的任何一個發生錯誤,請通知全部其餘附加資源,他們須要清理和關閉它們

正如咱們從這個例子中能夠看到的,當出現故障時,必須採起更多措施來正確清理資源,而不是經過域API嚴格完成,全部域提供的都是異常聚合機制。即便在域中傳播數據的潛在有用能力也容易被抵消,在本例中,經過將須要的資源做爲函數參數傳遞。

儘管存在乎外的異常,但應用領域的一個問題仍然是可以繼續執行(與文檔所述相反)的簡單性,這個例子證實了這個想法背後的謬論。

隨着應用程序自己的複雜性增長,嘗試對意外異常進行適當的資源清理會變得更加複雜,此示例僅具備3個基本資源,而且全部資源都具備明確的依賴路徑,若是應用程序使用共享資源或資源重用之類的東西,那麼清理能力和正確測試清理工做的能力就會大大增長。

最後,就處理錯誤而言,域不只僅是一個美化的'uncaughtException'處理程序,除了第三方更隱式和不可觀察的行爲。

資源傳播

域的另外一個用例是使用它來沿異步數據路徑傳播數據,一個問題在於,當堆棧中有多個域時(若是異步堆棧與其餘模塊一塊兒工做,則必須假定),什麼時候指望正確的域是模糊的。此外,可以依賴域進行錯誤處理同時還能夠檢索必要的數據之間存在衝突。

下面是一個使用域沿着異步堆棧傳播數據失敗的示例:

const domain = require('domain');
const net = require('net');

const server = net.createServer((c) => {
  // Use a domain to propagate data across events within the
  // connection so that we don't have to pass arguments
  // everywhere.
  const d = domain.create();
  d.data = { connection: c };
  d.add(c);
  // Mock class that does some useless async data transformation
  // for demonstration purposes.
  const ds = new DataStream(dataTransformed);
  c.on('data', (chunk) => ds.data(chunk));
}).listen(8080, () => console.log('listening on 8080'));

function dataTransformed(chunk) {
  // FAIL! Because the DataStream instance also created a
  // domain we have now lost the active domain we had
  // hoped to use.
  domain.active.data.connection.write(chunk);
}

function DataStream(cb) {
  this.cb = cb;
  // DataStream wants to use domains for data propagation too!
  // Unfortunately this will conflict with any domain that
  // already exists.
  this.domain = domain.create();
  this.domain.data = { inst: this };
}

DataStream.prototype.data = function data(chunk) {
  // This code is self contained, but pretend it's a complex
  // operation that crosses at least one other module. So
  // passing along "this", etc., is not easy.
  this.domain.run(() => {
    // Simulate an async operation that does the data transform.
    setImmediate(() => {
      for (let i = 0; i < chunk.length; i++)
        chunk[i] = ((chunk[i] + Math.random() * 100) % 96) + 33;
      // Grab the instance from the active domain and use that
      // to call the user's callback.
      const self = domain.active.data.inst;
      self.cb(chunk);
    });
  });
};

以上顯示,很難有多個異步API嘗試使用域來傳播數據,能夠經過在DataStream構造函數中分配parent: domain.active來修復此示例,而後在調用用戶的回調以前經過domain.active = domain.active.data.parent恢復它。另外,'connection'回調中的DataStream實例化必須在d.run()中運行,而不是簡單地使用d.add(c),不然將沒有活動域。

簡而言之,爲此祈禱有機會使用,須要嚴格遵照一套難以執行或測試的準則。

性能問題

使用域的重要威脅是開銷,使用node的內置http基準測試http_simple.js,沒有域,它能夠處理超過22,000個請求/秒。若是它在NODE_USE_DOMAINS=1下運行,那麼該數字會降低到低於17,000個請求/秒,在這種狀況下,只有一個全局域。若是咱們編輯基準測試,那麼http請求回調會建立一個新的域實例,性能會進一步降低到15,000個請求/秒。

雖然這可能不會影響僅服務於每秒幾百甚至一千個請求的服務器,但開銷量與異步請求的數量成正比,所以,若是單個鏈接須要鏈接到其餘幾個服務,則全部這些服務都會致使將最終產品交付給客戶端的整體延遲。

使用AsyncWrap並跟蹤在上述基準測試中調用init/pre/post/destroy的次數,咱們發現全部被調用事件的總和超過每秒170,000次,這意味着即便爲每種調用增長1微秒的開銷,任何類型的設置或拆除都會致使17%的性能損失。

固然,這是針對基準測試的優化方案,但我相信這演示了域等機制儘量廉價運行的必要性。

展望將來

域模塊自2014年12月以來一直被軟棄用,但還沒有被刪除,由於node目前沒有提供替代功能,在撰寫本文時,正在進行構建AsyncWrap API的工做以及爲TC39準備區域的提議,在這種狀況下,有適當的功能來替換域,它將經歷徹底棄用週期並最終從核心中刪除。


上一篇:流中的背壓

下一篇:如何發佈N-API包

相關文章
相關標籤/搜索