Node.js DNS 模塊的小優(yōu)化
這幾天看到了一個(gè)關(guān)于緩存 Node.js DNS 結(jié)果的 PR,然后看了下 c-ares 的代碼,發(fā)現(xiàn) Node.js DNS 模塊也有些可以改進(jìn)的小地方,所以提交了兩個(gè) PR 嘗試進(jìn)行優(yōu)化,本文簡單介紹下相關(guān)的內(nèi)容。
c-ares
c-ares 庫是一個(gè)異步的 DNS 解析庫,在 Node.js 中的工作原理如下。
- Node.js 調(diào)用 c-ares 發(fā)起一個(gè) DNS 查詢,并注冊(cè)回調(diào)。
- c-ares 創(chuàng)建一個(gè) socket,通知 Node.js 監(jiān)聽該 socket 的讀事件。
- 該 socket 可讀,Node.js 通知 c-ares,c-ares 讀取響應(yīng),并通知 Node.js。
- Node.js 調(diào) c-ares 接口解析 DNS 響應(yīng)。
- c-ares 回調(diào) Node.js。
另外 Node.js 還會(huì)定時(shí)調(diào) c-ares 函數(shù),c-ares 會(huì)判斷是否查詢請(qǐng)求是否超時(shí)。大致了解 c-ares 的基礎(chǔ)后,接下來看看相關(guān)的內(nèi)容。
1. DNS 緩存
c-ares 支持緩存 DNS 響應(yīng),具體的緩存時(shí)間取決于 DNS 響應(yīng)報(bào)文的 ttl 和 c-ares 的配置。下面是處理 DNS 響應(yīng)時(shí)記錄緩存的代碼。
static ares_status_t ares_qcache_insert_int(ares_qcache_t *qcache,
ares_dns_record_t *qresp,
const ares_dns_record_t *qreq,
const ares_timeval_t *now)
{
ares_qcache_entry_t *entry;
unsigned int ttl;
// DNS 響應(yīng)報(bào)文信息
ares_dns_rcode_t rcode = ares_dns_record_get_rcode(qresp);
ares_dns_flags_t flags = ares_dns_record_get_flags(qresp);
// 獲取 DNS 響應(yīng)報(bào)文的 ttl
ttl = ares_qcache_calc_minttl(qresp);
// 和用戶配置的 ttl 比較,取最小值
if (ttl > qcache->max_ttl) {
ttl = qcache->max_ttl;
}
// 插入緩存
entry->dnsrec = qresp;
entry->expire_ts = (time_t)now->sec + (time_t)ttl;
entry->insert_ts = (time_t)now->sec;
entry->key = ares_qcache_calc_key(qreq);
ares_htable_strvp_insert(qcache->cache, entry->key, entry);
ares_slist_insert(qcache->expire, entry);
return ARES_SUCCESS;
}
下面是 DNS 查詢時(shí)緩存的處理。
if (!(flags & ARES_SEND_FLAG_NOCACHE)) {
status = ares_qcache_fetch(channel, &now, dnsrec, &dnsrec_resp);
// 存在緩存直接返回
if (status != ARES_ENOTFOUND) {
callback(arg, status, 0, dnsrec_resp);
return status;
}
}
但是 c-ares 1.31.0 后自動(dòng)開啟了緩存,這對(duì)于用戶來說可能不是預(yù)期的行為,所以 Node.js 提交了 PR 關(guān)閉了緩存能力,保證了兼容性。同時(shí),Node.js 后續(xù)會(huì)提供選項(xiàng)讓用戶可以自定義配置緩存的時(shí)間。具體可以參考以下 PR。
- https://github.com/c-ares/c-ares/pull/786
- https://github.com/nodejs/node/pull/57640
- https://github.com/nodejs/node/pull/58404
在了解這個(gè) PR 的同時(shí),也發(fā)現(xiàn)了兩個(gè) Node.js DNS 模塊的優(yōu)化點(diǎn)。
2. 定時(shí)器的超時(shí)時(shí)間
Node.js 會(huì)定時(shí)調(diào)用 c-ares 函數(shù),讓 c-ares 判斷查詢請(qǐng)求是否超時(shí),目前 Node.js DNS 模塊的定時(shí)器邏輯如下。
void ChannelWrap::StartTimer() {
int timeout = timeout_;
if (timeout == 0) timeout = 1;
if (timeout < 0 || timeout > 1000) timeout = 1000;
uv_timer_start(timer_handle_, AresTimeout, timeout, timeout);
}
可以看到當(dāng) timeout 小于 0 時(shí),定時(shí)間隔為 1000ms,也就是說 Node.js 會(huì)每隔 1000ms 回調(diào) c-ares 判斷查詢是否超時(shí),而當(dāng) timeout 等于 0 時(shí),Node.js 設(shè)置的定時(shí)間隔為 1ms,但是在 c-ares 中當(dāng) timeout 等于 -1 和等于 0 時(shí)的邏輯是一樣的,都是使用默認(rèn)的超時(shí)時(shí)間 2s,相關(guān)代碼如下。
if (optmask & ARES_OPT_TIMEOUTMS) {
// 小于 0 則使用默認(rèn)值
if (options->timeout <= 0) {
optmask &= ~(ARES_OPT_TIMEOUTMS);
} else {
channel->timeout = (unsigned int)options->timeout;
}
}
if (channel->timeout == 0) {
channel->timeout = DEFAULT_TIMEOUT; // 2s
}
所以如果用戶設(shè)置 timeout = 0,Node.js 就會(huì)頻繁地調(diào)用(每隔 1ms)c-ares 判斷是否超時(shí),但這是沒必要的。優(yōu)化后的代碼如下。
void ChannelWrap::StartTimer() {
int timeout = timeout_;
if (timeout <= 0 || timeout > 1000) timeout = 1000;
uv_timer_start(timer_handle_, AresTimeout, timeout, timeout);
}
優(yōu)化的邏輯很簡單,保證 timeout 等于 0 和等于 -1 時(shí)的邏輯一致即可。通過測(cè)試大概 CPU 使用率下降 2% 左右,測(cè)試?yán)尤缦隆?/span>
const { Resolver } = require('dns');
const { createSocket } = require('dgram');
const socket = createSocket('udp4');
socket.bind(0, 'localhost', () => {
const resolver = new Resolver({ timeout: 0, tries: 4 });
resolver.setServers([`${socket.address().address}:${socket.address().port}`])
resolver.resolve('nodejs.org', () => {
socket.close();
});
});
具體可以參考 PR:https://github.com/nodejs/node/pull/58441。
3. 最大超時(shí)時(shí)間
Node.js DNS 解析有 timeout 和 tries 兩個(gè)參數(shù),timeout 表示對(duì)于一個(gè) DNS 服務(wù)器,一個(gè) DNS 首次查詢的超時(shí)時(shí)間,tries 表示超時(shí)次數(shù),但是超時(shí)間隔是按照一定算法計(jì)算的(比如指數(shù)退避),而不是固定的。看一個(gè)例子。
const { Resolver } = require('dns');
const { createSocket } = require('dgram');
const socket = createSocket('udp4');
socket.bind(0, 'localhost', () => {
const resolver = new Resolver({ timeout: 1000, tries: 3 });
resolver.setServers([`${socket.address().address}:${socket.address().port}`])
const start = Date.now();
resolver.resolve('nodejs.org', () => {
socket.close();
console.log(`time: ${Date.now() - start}`);
});
});
例子中輸入的時(shí)間大概為 8s,說明不是等間隔重試的,但是有些時(shí)候我們希望可以快點(diǎn)重試,比如服務(wù)器宕機(jī)時(shí)快速感知超時(shí),服務(wù)重啟時(shí)快速獲取結(jié)果等,所以我們希望有一種方式可以控制每次重試時(shí)的超時(shí)時(shí)間,而不是使用 c-ares 的默認(rèn)算法,這個(gè)配置就是 c-ares 的 max timeout 配置,最近提了一個(gè) PR 支持該特性,測(cè)試?yán)尤缦隆?/span>
const { Resolver } = require('dns');
const { createSocket } = require('dgram');
const socket = createSocket('udp4');
socket.bind(0, 'localhost', () => {
const resolver = new Resolver({ timeout: 1000, tries: 3, maxTimeout: 1000 });
resolver.setServers([`${socket.address().address}:${socket.address().port}`])
const start = Date.now();
resolver.resolve('nodejs.org', () => {
socket.close();
console.log(`time: ${Date.now() - start}`);
});
});
上面代碼輸出是 4s 左右,說明每次重試間隔都是 1s。具體可以參考 PR:https://github.com/nodejs/node/pull/58440。
Node.js DNS 模塊是比較穩(wěn)定的模塊,功能上變化不大,但是仍然有一些小地方可以進(jìn)行優(yōu)化,也算是不斷完善 Node.js 的功能。