帶你從零看清 Node 源碼 createServer 和負載均衡整個過程

2021-03-02 前端大全

(給前端大全加星標,提升前端技能)

作者: 前端巔峰 公號 / Peter 譚金傑

寫在開頭:

作為一名曾經重度使用Node.js作為即時通訊客戶端接入層的開發人員,無法避免調試V8,配合開發addon。於是對Node.js源碼產生了很大的興趣~ 

順便吐槽一句,Node的內存控制,由於是自動回收,我之前做的產品是20萬人超級群的IM產品,像一秒鐘1000條消息,持續時間長了內存和CPU佔用還是會有一些問題

之前寫過cluster模塊源碼分析、PM2原理等,感覺興趣的可以去公眾號翻一翻

Node.js的js部分源碼基本看得差不多了,今天寫一個createServer過程給大家,對於不怎麼熟悉Node.js源碼的朋友來說,可能是一個不錯的開始,源碼在gitHub上有,直接克隆即可,最近一直比較忙,公司和業餘的工作也是,所以原創比較少。

原生Node.js創建一個基本的服務:

var http = require('http');http.createServer(function (request, response) {// 發送 HTTP 頭部 // HTTP 狀態值: 200 : OK// 內容類型: text/plainresponse.writeHead(200, {'Content-Type': 'text/plain'});// 發送響應數據 "Hello World"response.end('Hello World\n');}).listen(8888);// 終端列印如下信息console.log('Server running at http://127.0.0.1:8888/');

我們目前只分析Node.js源碼的js部分的

首先找到Node.js源碼的lib文件夾

然後找到http.js文件

發現createServer真正返回的是new Server,而 Server來自_http_server

於是找到同目錄下的_http_server.js文件,發現整個文件有800行的樣子,全局搜索Server找到函數

function Server(options, requestListener) {if (!(this instanceof Server)) return new Server(options, requestListener);if (typeof options === 'function') {    requestListener = options;    options = {};  } else if (options == null || typeof options === 'object') {    options = { ...options };  } else {throw new ERR_INVALID_ARG_TYPE('options', 'object', options);  }this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;this[kServerResponse] = options.ServerResponse || ServerResponse;  net.Server.call(this, { allowHalfOpen: true });if (requestListener) {this.on('request', requestListener);  }

createServer函數解析:

參數控制有點像redux源碼裡的initState和reducer,根據傳入類型不同,做響應的處理

this.on('request', requestListener);}

 每次有請求,就會調用requestListener這個回調函數

至於IncomingMessage和ServerResponse,請求是流,響應也是流,請求是可讀流,響應是可寫流,當時寫那個靜態資源伺服器時候有提到過

那麼怎麼可以鏈式調用?有人可能會有疑惑。Node.js源碼遵循commonJs規範,大都掛載在prototype上,所以函數開頭有,就是確保可以鏈式調用

if (!(this instanceof Server)) return new Server(options, requestListener);

上面已經將onrequest事件觸發回調函數講清楚了,那麼鏈式調用listen方法,監聽埠是怎麼回事呢?

傳統的鏈式調用,像JQ源碼是return this , 手動實現A+規範的Promise則是返回一個全新的Promise,然後Promise原型上有then方法,於是可以鏈式調用

怎麼實現.listen鏈式調用,重點在這行代碼:

 net.Server.call(this, { allowHalfOpen: true });


 allowHalfOpen實驗結論: 這裡TCP的知識不再做過度的講解

(1)allowHalfOpen為true,一端發送FIN報文:進程結束了,那麼肯定會發送FIN報文;進程未結束,不會發送FIN報文(2)allowHalfOpen為false,一端發送FIN報文:進程結束了,肯定發送FIN報文;進程未結束,也會發送FIN報文;

於是找到net.js文件模塊中的Server函數

function Server(options, connectionListener) {  if (!(this instanceof Server))    return new Server(options, connectionListener);
EventEmitter.call(this);
if (typeof options === 'function') { connectionListener = options; options = {}; this.on('connection', connectionListener); } else if (options == null || typeof options === 'object') { options = { ...options };
if (typeof connectionListener === 'function') { this.on('connection', connectionListener); } } else { throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); }
this._connections = 0;
Object.defineProperty(this, 'connections', { get: deprecate(() => {
if (this._usingWorkers) { return null; } return this._connections; }, 'Server.connections property is deprecated. ' + 'Use Server.getConnections method instead.', 'DEP0020'), set: deprecate((val) => (this._connections = val), 'Server.connections property is deprecated.', 'DEP0020'), configurable: true, enumerable: false });
this[async_id_symbol] = -1; this._handle = null; this._usingWorkers = false; this._workers = []; this._unref = false;
this.allowHalfOpen = options.allowHalfOpen || false; this.pauseOnConnect = !!options.pauseOnConnect;}

這裡巧妙的通過.call調用net模塊Server函數,保證了this指向一致

this._handle = null 這裡是因為Node.js考慮到多進程問題,所以會hack掉這個屬性,因為.listen方法最終會調用_handle中的方法,多個進程只會啟動一個真正進程監聽埠,然後負責分發給不同進程,這個後面會講

Node.js源碼的幾個特色:

遵循conmonjs規範,很多方法掛載到prototype上了

很多object.definepropoty數據劫持

this指向的修改,配合第一個進行鏈式調用

自帶自定義事件模塊,很多內置的函數都繼承或通過Object.setPrototypeOf去封裝了一些自定義事件

代碼模塊互相依賴比較多,一個.listen過程就很麻煩,初學代碼者很容易睡著

源碼學習,本就枯燥。沒什麼好說的了

我在net.js文件模塊中發現了一個原型上.listen的方法:

Server.prototype.listen = function(...args) {  const normalized = normalizeArgs(args);  var options = normalized[0];  const cb = normalized[1];
if (this._handle) { throw new ERR_SERVER_ALREADY_LISTEN(); }
if (cb !== null) { this.once('listening', cb); } const backlogFromArgs = // (handle, backlog) or (path, backlog) or (port, backlog) toNumber(args.length > 1 && args[1]) || toNumber(args.length > 2 && args[2]); // (port, host, backlog)
options = options._handle || options.handle || options; const flags = getFlags(options.ipv6Only); // (handle[, backlog][, cb]) where handle is an object with a handle if (options instanceof TCP) { this._handle = options; this[async_id_symbol] = this._handle.getAsyncId(); listenInCluster(this, null, -1, -1, backlogFromArgs); return this; } // (handle[, backlog][, cb]) where handle is an object with a fd if (typeof options.fd === 'number' && options.fd >= 0) { listenInCluster(this, null, null, null, backlogFromArgs, options.fd); return this; }
// ([port][, host][, backlog][, cb]) where port is omitted, // that is, listen(), listen(null), listen(cb), or listen(null, cb) // or (options[, cb]) where options.port is explicitly set as undefined or // null, bind to an arbitrary unused port if (args.length === 0 || typeof args[0] === 'function' || (typeof options.port === 'undefined' && 'port' in options) || options.port === null) { options.port = 0; } // ([port][, host][, backlog][, cb]) where port is specified // or (options[, cb]) where options.port is specified // or if options.port is normalized as 0 before var backlog; if (typeof options.port === 'number' || typeof options.port === 'string') { if (!isLegalPort(options.port)) { throw new ERR_SOCKET_BAD_PORT(options.port); } backlog = options.backlog || backlogFromArgs; // start TCP server listening on host:port if (options.host) { lookupAndListen(this, options.port | 0, options.host, backlog, options.exclusive, flags); } else { // Undefined host, listens on unspecified address // Default addressType 4 will be used to search for master server listenInCluster(this, null, options.port | 0, 4, backlog, undefined, options.exclusive); } return this; }
// (path[, backlog][, cb]) or (options[, cb]) // where path or options.path is a UNIX domain socket or Windows pipe if (options.path && isPipeName(options.path)) { var pipeName = this._pipeName = options.path; backlog = options.backlog || backlogFromArgs; listenInCluster(this, pipeName, -1, -1, backlog, undefined, options.exclusive);
if (!this._handle) { // Failed and an error shall be emitted in the next tick. // Therefore, we directly return. return this; }
let mode = 0; if (options.readableAll === true) mode |= PipeConstants.UV_READABLE; if (options.writableAll === true) mode |= PipeConstants.UV_WRITABLE; if (mode !== 0) { const err = this._handle.fchmod(mode); if (err) { this._handle.close(); this._handle = null; throw errnoException(err, 'uv_pipe_chmod'); } } return this; }
if (!(('port' in options) || ('path' in options))) { throw new ERR_INVALID_ARG_VALUE('options', options, 'must have the property "port" or "path"'); }
throw new ERR_INVALID_OPT_VALUE('options', inspect(options));};

這個就是我們要找的listen方法,可是裡面很多ipv4和ipv6的處理,最重要的方法是listenInCluster

這個函數需要好好看一下,只有幾十行

function listenInCluster(server, address, port, addressType,                         backlog, fd, exclusive, flags) {  exclusive = !!exclusive;  if (cluster === undefined) cluster = require('cluster');
if (cluster.isMaster || exclusive) { // Will create a new handle // _listen2 sets up the listened handle, it is still named like this // to avoid breaking code that wraps this method server._listen2(address, port, addressType, backlog, fd, flags); return; }
const serverQuery = { address: address, port: port, addressType: addressType, fd: fd, flags, };
// Get the master's server handle, and listen on it cluster._getServer(server, serverQuery, listenOnMasterHandle);
function listenOnMasterHandle(err, handle) { err = checkBindError(err, port, handle);
if (err) { var ex = exceptionWithHostPort(err, 'bind', address, port); return server.emit('error', ex); }
// Reuse master's server handle server._handle = handle; // _listen2 sets up the listened handle, it is still named like this // to avoid breaking code that wraps this method server._listen2(address, port, addressType, backlog, fd, flags); }}

如果是主進程,那麼就直接調用_.listen2方法了

Server.prototype._listen2 = setupListenHandle;

找到setupListenHandle函數

function setupListenHandle(address, port, addressType, backlog, fd, flags) {  debug('setupListenHandle', address, port, addressType, backlog, fd);
// If there is not yet a handle, we need to create one and bind. // In the case of a server sent via IPC, we don't need to do this. if (this._handle) { debug('setupListenHandle: have a handle already'); } else { debug('setupListenHandle: create a handle');
var rval = null;
// Try to bind to the unspecified IPv6 address, see if IPv6 is available if (!address && typeof fd !== 'number') { rval = createServerHandle(DEFAULT_IPV6_ADDR, port, 6, fd, flags);
if (typeof rval === 'number') { rval = null; address = DEFAULT_IPV4_ADDR; addressType = 4; } else { address = DEFAULT_IPV6_ADDR; addressType = 6; } }
if (rval === null) rval = createServerHandle(address, port, addressType, fd, flags);
if (typeof rval === 'number') { var error = uvExceptionWithHostPort(rval, 'listen', address, port); process.nextTick(emitErrorNT, this, error); return; } this._handle = rval; }
this[async_id_symbol] = getNewAsyncId(this._handle); this._handle.onconnection = onconnection; this._handle[owner_symbol] = this;
// Use a backlog of 512 entries. We pass 511 to the listen() call because // the kernel does: backlogsize = roundup_pow_of_two(backlogsize + 1); // which will thus give us a backlog of 512 entries. const err = this._handle.listen(backlog || 511);
if (err) { var ex = uvExceptionWithHostPort(err, 'listen', address, port); this._handle.close(); this._handle = null; defaultTriggerAsyncIdScope(this[async_id_symbol], process.nextTick, emitErrorNT, this, ex); return; }
// Generate connection key, this should be unique to the connection this._connectionKey = addressType + ':' + address + ':' + port;
// Unref the handle if the server was unref'ed prior to listening if (this._unref) this.unref();
defaultTriggerAsyncIdScope(this[async_id_symbol], process.nextTick, emitListeningNT, this);}

裡面的createServerHandle是重點

function createServerHandle(address, port, addressType, fd, flags) {  var err = 0;  // Assign handle in listen, and clean up if bind or listen fails  var handle;
var isTCP = false; if (typeof fd === 'number' && fd >= 0) { try { handle = createHandle(fd, true); } catch (e) { // Not a fd we can listen on. This will trigger an error. debug('listen invalid fd=%d:', fd, e.message); return UV_EINVAL; }
err = handle.open(fd); if (err) return err;
assert(!address && !port); } else if (port === -1 && addressType === -1) { handle = new Pipe(PipeConstants.SERVER); if (process.platform === 'win32') { var instances = parseInt(process.env.NODE_PENDING_PIPE_INSTANCES); if (!Number.isNaN(instances)) { handle.setPendingInstances(instances); } } } else { handle = new TCP(TCPConstants.SERVER); isTCP = true; }
if (address || port || isTCP) { debug('bind to', address || 'any'); if (!address) { // Try binding to ipv6 first err = handle.bind6(DEFAULT_IPV6_ADDR, port, flags); if (err) { handle.close(); // Fallback to ipv4 return createServerHandle(DEFAULT_IPV4_ADDR, port); } } else if (addressType === 6) { err = handle.bind6(address, port, flags); } else { err = handle.bind(address, port); } }
if (err) { handle.close(); return err; }
return handle;}

已經可以看到TCP了,離真正的綁定監聽埠,更近了一步

最終通過下面的方法綁定監聽埠

 handle.bind6(address, port, flags); 或者 handle.bind(address, port);

首選ipv6綁定,是因為ipv6可以接受到ipv4的套接字,而ipv4不可以接受ipv6的套接字,當然也有方法可以接收,就是麻煩了一點

上面的內容,請你認真看,因為下面會更複雜,設計到Node.js的多進程負載均衡原理

如果不是主進程,就調用cluster._getServer,找到cluster源碼

'use strict';
const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';module.exports = require(`internal/cluster/${childOrMaster}`);

找到_getServer函數源碼

// `obj` is a net#Server or a dgram#Socket object.cluster._getServer = function(obj, options, cb) {  let address = options.address;
// Resolve unix socket paths to absolute paths if (options.port < 0 && typeof address === 'string' && process.platform !== 'win32') address = path.resolve(address);
const indexesKey = [address, options.port, options.addressType, options.fd ].join(':');
let index = indexes.get(indexesKey);
if (index === undefined) index = 0; else index++;
indexes.set(indexesKey, index);
const message = { act: 'queryServer', index, data: null, ...options };
message.address = address;
// Set custom data on handle (i.e. tls tickets key) if (obj._getServerData) message.data = obj._getServerData();
send(message, (reply, handle) => { if (typeof obj._setServerData === 'function') obj._setServerData(reply.data);
if (handle) shared(reply, handle, indexesKey, cb); // Shared listen socket. else rr(reply, indexesKey, cb); // Round-robin. });
obj.once('listening', () => { cluster.worker.state = 'listening'; const address = obj.address(); message.act = 'listening'; message.port = (address && address.port) || options.port; send(message); });};

我們之前傳入了三個參數給它,分別是

server,serverQuery,listenOnMasterHandle

這裡是比較複雜的,曾經我也在這裡迷茫過一段時間,但是想著還是看下去吧。堅持下,大家如果看到這裡看不下去了,先休息下,保存著。後面等心情平復了再靜下來接下去看

首先我們傳入了Server、serverQuery和cb(回調函數listenOnMasterHandle),整個cluster模塊的_getServer中最重要的就是:

if (obj._getServerData)    message.data = obj._getServerData();
send(message, (reply, handle) => { if (typeof obj._setServerData === 'function') obj._setServerData(reply.data);
if (handle) shared(reply, handle, indexesKey, cb); // Shared listen socket. else rr(reply, indexesKey, cb); // Round-robin. });

首先我們會先獲取server上的data數據,然後調用send函數

function send(message, cb) {  return sendHelper(process, message, null, cb);}

send函數調用的是cluster模塊的utills文件內的函數,傳入了一個默認值process


function sendHelper(proc, message, handle, cb) { if (!proc.connected) return false;
message = { cmd: 'NODE_CLUSTER', ...message, seq };
if (typeof cb === 'function') callbacks.set(seq, cb);
seq += 1; return proc.send(message, handle);}

這裡要看清楚,我們調用sendHelper傳入的第三個參數是null  !!!

那麼主進程返回也是null

send(message, (reply, handle) => {    if (typeof obj._setServerData === 'function')      obj._setServerData(reply.data);
if (handle) shared(reply, handle, indexesKey, cb); // Shared listen socket. else rr(reply, indexesKey, cb); // Round-robin. });

所以我們會進入rr函數調用的這個判斷,這裡調用rr傳入的cb就是在net.js模塊定義的listenOnMasterHandle函數

Node.js的負載均衡算法是輪詢,官方給出的解釋是簡單粗暴效率高

上面的sendHelper函數就是做到了這點,每次+1

 if (typeof cb === 'function')    callbacks.set(seq, cb);
seq += 1;

function rr(message, indexesKey, cb) {  if (message.errno)    return cb(message.errno, null);
var key = message.key;
function listen(backlog) { return 0; }
function close() { if (key === undefined) return;
send({ act: 'close', key }); handles.delete(key); indexes.delete(indexesKey); key = undefined; }
function getsockname(out) { if (key) Object.assign(out, message.sockname);
return 0; }
const handle = { close, listen, ref: noop, unref: noop };
if (message.sockname) { handle.getsockname = getsockname; }
assert(handles.has(key) === false); handles.set(key, handle); cb(0, handle);}

此時的handle已經被重寫,listen方法調用會返回0,不會再佔用埠了。所以這樣Node.js多個進程也只是一個進程監聽埠而已

此時的cb還是net.js模塊的setupListenHandle即 -  _listen2方法。

官方的注釋:

Faux handle. Mimics a TCPWrap with just enough fidelity to get away
仿句柄。以足夠的保真度來模擬TCPWrap

覺得本文對你有幫助?請分享給更多人

關注「前端大全」加星標,提升前端技能

好文章,我在看❤️

相關焦點

  • Doris源碼解析[一、負載均衡]
    ### PathSlot 為了保證在執行副本修復或均衡過程中,不會導致某些機器因分配任務太多而被打滿,我們為 BE 上的每塊盤指定了固定個數的 Slot。分數越高,表示該 BE 的負載越重。磁碟使用率和副本數量各有一個權重係數,分別為 capacityCoefficient 和 replicaNumCoefficient,其 和衡為1。其中 capacityCoefficient 會根據實際磁碟使用率動態調整。
  • 短連結服務Octopus的實現與源碼開放
    當時為了快速推廣,使用了一些比較知名的第三方短鏈壓縮平臺,存在一些問題:收費貴一些情況下,短鏈域名在部分第三方平臺例如微信會被封殺回源數據沒有辦法定製處理方案,無法打通整個業務鏈路進行數據分析和跟蹤基於此類問題,決定自研一個(長連結壓縮為)短連結服務,當時剛好同步進行微服務拆分,內部很多微服務需要重新命名,組內的一個妹子說不如就用Github的吉祥物去命名octopus cat(章魚貓
  • Node.js 15正式版發布
    英文 | https ://medium.com/@nodejs/node-js-v15-0-0-is-here-deb00750f278前兩天,Node.js
  • 伺服器是整個網絡核心,Windows Server 2008如何查看網卡MAC地址
    今天介紹伺服器是整個網絡的核心,Windows server 2008 R2如何查看計算機網卡MAC地址。伺服器是整個網絡的核心,但接入網絡之前必須先對伺服器進行必要的網絡設置,如安裝網卡驅動程序、設置IP位址信息等。
  • 數據中心內的負載均衡-MPTCP
    為什麼用MPTCP做負載均衡?(1)充分利用網絡資源以手機為例,手機包含兩種上網方式,蜂窩移動數據網絡(2G,3G,4G)和WIFI網絡。我們希望在有WIFI的時候儘量使用WIFI,這樣可以節省成本,沒有WIFI的時候自動切換到蜂窩行動網路,避免斷連。同樣在PC端,我們希望有線網卡和無線網卡可以同時上網,提高網速。
  • 彈性伸縮+負載均衡在高並發業務場景下的應用
    ZStack彈性伸縮+負載均衡基於VPC路由器實現,用戶通過訪問VPC路由器提供的虛擬IP來訪問後端運行業務的雲主機,伸縮策略決定伸縮組中最小/最大雲主機數量及彈性擴容和縮容的條件,同時通過健康檢查來確認後端雲主機的健康狀況,以保障客戶業務持續穩定的運行。下圖是負載均衡和彈性伸縮的示意圖:
  • 從毛片打碼到看Spark源碼,我經歷了什麼
    突然想到我們在學習spark時,也可以有這種精神,當我們能讀懂spark源碼時,spark的技術世界也就真正為我們敞開了大門。臺灣C++大師侯捷說過:源碼面前,了無秘密!那我們就從如何單步調試spark源碼開始講起吧。
  • 一文搞懂 CountDownLatch 用法和源碼!
    當所有的組件和服務都加載完畢後,主線程和其他線程在一起完成某個任務。順著這個場景,你自己就可以延伸、拓展出來很多其他任務場景。CountDownLatch 源碼分析CountDownLatch 使用起來比較簡單,但是卻非常有用,現在你可以在你的工具箱中加上 CountDownLatch 這個工具類了。
  • npx, 你了解嗎?
    npx create-react-app my-react-app上面代碼運行時,npx 將create-react-app下載到一個臨時目錄,使用以後再刪除。所以,以後再次執行上面的命令,會重新下載create-react-app。
  • Linux-C 編程 / 網絡 / 超迷你的 web server
    讓自己感到些許痛苦,但卻會帶來實實在在的充實感和成就感的事情。2. 固定的支撐點很重要:無論發生什麼,無論今天你在什麼地方,處於怎樣的階段,有哪些安排,你都堅持做這件事,而且一大早起來,就開始執行。這是一個很強大的心理暗示:只要擁有這些穩定不變的支撐點,你就有信心和足夠的能力去面對許多生活裡不穩定的東西,並解決掉它們。二、Linux-C 編程 / 超迷你的 web server0. 什麼是 web server?
  • 通過ucd-snmp完成SNMP Agent的源碼
    所以我們一再介紹了相關的源碼的內容。那麼接下來我們主要介紹了一下相關協議的開源開發內容,並且討論採用開放源碼的ucd-snmp 4.2.1軟體包開發自己的SNMP Agent,不涉及SNMP協議包的組包、解析等問題。本文從以下部分進行介紹:一:ucd-snmp 4.2.1簡介及SNMP Agent開發步驟二:MIB庫模塊設計及代碼轉換三:SNMP Agent功能擴展方式四:uCLinux
  • Node.js 實戰系列:幫黃老師完善餓了麼項目
    後臺和前端頁面使用常規的vue+element-ui+vuex+vue-router進行開發。在部署方面,由於這是個人項目,所以我決定用自己沒有用過的技術,自建了個Jenkins,通過jenkins自動拉取和執行腳本建立Docker鏡像對vue項目進行自動化部署。整個流程對於個人項目還算完整。
  • 如何從零開始搭建高性能直播平臺
    交互流程RTMP的交互流程可以分為握手過程、控制命令傳輸與數據傳輸。握手過程RTMP 連接以握手開始,RTMP 握手由三個固定長度的塊組成。客戶端 (發起連接請求的終端) 和伺服器端各自發送相同的三塊。便於演示,本文將從客戶端發送的這些塊指定為 C0、C1 和 C2;將從伺服器端發送的這些塊分別指定為 S0、S1 和 S2。
  • 關於直播帶貨過程中的卡頓問題,帶貨直播源碼有話說!
    直播帶貨作為時代衍生出的新生態,讓商家踏破門檻都想參與其中。但根據用戶反饋,在直播帶貨過程中,會或多或少地出現卡頓問題。出現此種情況的原因有很多,除外界因素外,與帶貨直播源碼也存在著一定的聯繫。今天,小編就帶大家一起來了解一下。
  • Node.js爬蟲實戰 - 爬你喜歡的
    superagent模擬客戶端發送網絡請求,可設置請求參數、header頭信息npm install superagent -Dcheerio類jQuery庫,可將字符串導入,創建對象,用於快速抓取字符串中的符合條件的數據npm install cheerio -D項目目錄:node-pachong
  • 微信小遊戲+商城+小程序+後臺+教程源碼合集
    後端)[20.1M]安裝演示說明文件(1).txt[1.3K]後端源碼.zip[19.9M]┗雲商城前端.zip[150.3K]搭伴拼團+後臺(PHP).zip[1.8M]健康膳食.zip[300.4K]靈動雲商城+php後臺+後臺配置教程.zip[20.1M]貓眼電影含node後端.zip[8.6M]明星圖(含NODEJS後端).zip[3.9M]拼單神器:含leanCloud後端.zip[961.7K
  • PG中的oid和relfilenode之間的關係
    PG中的oid和relfilenode之間的關係PG中的表由一個relfilenode值,即磁碟上表的文件名(除了外表和分區表)。