原創作者: @guanyu,畢業於吉林大學·軟體工程專業,感謝 @guanyu 提供案例以及文章。

問題背景

某天上午,運營同學突然在群裡反饋很多使用者來報登入問題。起初以為是內網介面服務異常了,但介面反饋沒有產生異常的日誌,也就是說異常請求還沒打過去。於是我們登入伺服器,篩選了下 Node。js 服務的日誌:

一次 Node.js 服務線上問題引出的 DNS 快取方案研究與思考

透過日誌,我們可以很直觀的看出問題所在:

DNS 解析失敗

整理思路

作為一個日均流量過千萬的 Node。js 服務,每個請求都需要

解析 N 個內網介面域名

平時還好,如果

DNS 服務出現了問題

,或者

網路抖動

,很容易在 Node。js 服務與內網介面服務都正常的情況下,導致

線上業務不可用。

針對這種情況,我們需要在 Node。js 服務端

對 DNS 解析做一層快取。

首先我們需要明確一點:

!!!Node.js 本身不做 DNS 查詢結果的快取!!!

預設 DNS 查詢方案

Node。js 下預設的 DNS 查詢方案:

Node。js

內建的 http 模組的 http.request() 請求

時,會

使用 dns.lookup() 進行查詢:

方法呼叫鏈條是

http.request() -> net.createConnection() -> dns.lookup()

function

lookupAndConnect

self

options

{

// 。。。

const

lookup

=

options

lookup

||

dns

lookup

defaultTriggerAsyncIdScope

self

async_id_symbol

],

function

()

{

lookup

host

dnsopts

function

emitLookup

err

ip

addressType

{

self

emit

‘lookup’

err

ip

addressType

host

);

// 。。。

});

});

}

透過這段程式碼我們可以看出,

options.lookup 引數可以自行設定,可以傳入 dns.resolve 或者自定義的符合要求的方法。

getaddrinfo 函式

在 C/C++ 程式碼中

p()

方法呼叫到最終,呼叫的是

底層的 getaddrinfo() 函式

(也就是上文報錯點)。

在 C/C++ 程式碼中

getaddrinfo

函式是

同步呼叫

,所以

需要 libuv 透過執行緒池來實現 Node.js 的非同步 I/O。

注:查閱相關資料,我們可以看到

執行緒池預設大小是 4。

當請求在 DNS 查詢階段耗時過長時,由於預設執行緒池過小,服務處理請求的速度跟請求數量遠遠不匹配,

為 1024。

可能會出現的問題

當請求在 DNS 查詢階段耗時過長時,由於預設執行緒池過小,服務處理請求的速度跟請求數量遠遠不匹配,

服務執行時間越長積壓的請求數連線數就越多。

關於預設快取

使用 DNS快取

本身不做 DNS 查詢結果的快取!!!

,Node。js

每次域名請求時都會請求 DNS Server

使用 DNS 快取

注意快取的過期時間

實現 DNS 快取的相關依賴

lookup-dns-cache

lookup-dns-cache 是很成熟的 DNS 快取庫,但比較古老:

一次 Node.js 服務線上問題引出的 DNS 快取方案研究與思考

他的思路比較簡單:

底層查詢使用了

dns.resolve() 來替換 dns.lookup

避免並行的 DNS請求存已經解析出來的 hostname 資訊

避免並行的 DNS 請求

。同一時間只執行一個對相同 hostname 的查詢請求,透過 Map 來實現

dns。resolve

dns。lookup

區別

透過官方文件可以看出:

一次 Node.js 服務線上問題引出的 DNS 快取方案研究與思考

dns.resolve 不使用 getaddrinfo()

dns.resolve 是非同步實現的

dns.resolve 不使用 析本地 hosts 檔案,直接走網路解析

詳情可以檢視:

示例程式碼

const { resolve, lookup } = require(‘dns’);

lookup(‘preview4。xx。xx。com’, (err, address, family) => {

console。log(‘地址: %j 地址族: IPv%s’, address, family); // 地址: “xxx。xxx。xx。xx” 地址族: IPv4

});

resolve(‘preview4。xx。xx。com’, (err, records) => {

console。log(records); // undefined

});

preview4。xx。xx。com

本地 host 配置的域名

由於

dns.resolve() 不使用 getaddrinfo(),所以此時解析出來的地址為 undefined。

避免並行請求實現

利用 Map 對正在查詢的 hostname 做快取。查詢結束後從 Map 中刪除:

let task = this。_tasksManager。find(key);

if (task) {

task。addResolvedCallback(callback);

} else {

task = new ResolveTask(hostname, ipVersion);

this。_tasksManager。add(key, task);

task。on(‘addresses’, addresses => {

this。_addressCache。set(key, addresses);

});

task。on(‘done’, () => {

this。_tasksManager。done(key);

});

task。addResolvedCallback(callback);

task。run();

}

透過

ttl 的快取

透過 dns.reslove 方法設定 ttl:tru

這個庫,作者是 got 的作者 szmarczak。

判斷 hostname 還在快取且未過期時直接返回快取,否則進行查詢

/**

* @param {string} key

* @returns {Address[]|undefined}

*/

find(key) {

if (!this。_cache。has(key)) {

return;

}

const addresses = this。_cache。get(key);

if (this。_isExpired(addresses)) {

return;

}

return addresses;

}

cacheable-lookup

在實際使用中,發現了

dns.resolve()無法解析本地 hosts 配置

的域名,單純使用

lookup-dns-cache

會導致本地開發環境出現報錯。

經過調研,發現了 cacheable-lookup 這個庫,作者是 got 的作者 szmarczak。

一次 Node.js 服務線上問題引出的 DNS 快取方案研究與思考

透過提交記錄我們可以看出,作者還在保持的對庫的持續更新。

對比 lookup-dns-cache

async queryAndCache(hostname) {

if (this。_hostnamesToFallback。has(hostname)) {

return this。_dnsLookup(hostname, all);

}

let query = await this。_resolve(hostname);

if (query。entries。length === 0 && this。_dnsLookup) {

query = await this。_lookup(hostname);

if (query。entries。length !== 0 && this。fallbackDuration > 0) {

// Use `dns。lookup(。。。)` for that particular hostname

this。_hostnamesToFallback。add(hostname);

}

}

const cacheTtl = query。entries。length === 0 ? this。errorTtl : query。cacheTtl;

await this。_set(hostname, query。entries, cacheTtl);

return query。entries;

}

透過原始碼我們可以看出

基於 ttl 的快取方法沒有解析成功時,會使用 lookup 方法進行兜底

,更符合我們

本地開發環境更改 hosts 檔案

的場景。

同時,這個庫也提供了

基於 ttl 的快取

阻止並行請求

等功能點。

於是,我們可以解決我們的問題了!

解決方案

在內網介面呼叫處統一增加

cacheable-lookup

進行 DNS 解析的快取。本文透過一個實際案例,剖析了 Node。js 中關於 DNS cache 的處理和進一步方案,希望對大家有所啟發,當然,相關話題仍然可以挖掘更深,實踐更深,後續我們將會繼續這方面的探索和輸出。

Happy coding!