为何解析浏览器地址参数会为null_request 包中出现 DNS 解析超时的探究
事情的起因是這樣的,公司使用自建 dns 服務器,但是有一個致命缺陷,不支持 ipv6 格式的地址解析,而 node 的 DNS 解析默認是同時請求 v4 和 v6 的地址的,這樣會導致偶爾在解析 v6 地址的時候出現超時。
本文鏈接地址 https://blog.whyun.com/posts/request-dns/the-problem-of-dns-timeout-on-request-package/index.html,轉載請注明出處。我們的程序中是使用的 request 這個包,查看了一下官方文檔,請求 options 中并沒有涉及跟 DNS 有關的配置,于是乎求教運維同事。運維同事告訴我在 docker run 的時候加參數 --sysctl net.ipv6.conf.all.disable_ipv6=1,試用了一下,并且寫出來如下測試代碼:
const dns = require('dns'); const domain = process.argv[2] || 'baidu.com'; const begin = Date.now(); dns.lookup(domain,function(err ,address , family) {console.log('耗時',Date.now() - begin, err ,address , family); });代碼 1.1 DNS 查詢測試代碼
運行 代碼 1.1 ,同時使用命令 tcpdump -i eth0 -n -s 500 port domain 來抓取 DNS 解析的數據包:
20:47:28.917563 IP 10.6.13.67.38050 > 10.7.11.219.domain: 40621+ A? baidu.com. (27) 20:47:28.917582 IP 10.6.13.67.38050 > 10.7.11.219.domain: 32393+ AAAA? baidu.com. (27) 20:47:28.921061 IP 10.7.11.219.domain > 10.6.13.67.38050: 40621 2/0/0 A 220.181.38.148, A 39.156.69.79 (59) 20:47:28.921114 IP 10.7.11.219.domain > 10.6.13.67.38050: 32393 0/1/0 (70)從輸出來看依然會請求 ipv6 的地址解析,所以當時我的判斷是運維的配置是不生效的。
后來又有了些空閑的時間,所以研究了一下官方文檔,看看是否有參數可以控制 http 請求的 DNS 協議版本,沒有想到還真有,http.request 的 options 中可以設置 family 參數,可選值為 4 6, 即 ipv4 或者 ipv6,如果不指定這個參數,將同時使用 ipv4 和 ipv6。按理來說看到這里,我就應該死心了,如果不傳這個參數,肯定會同時做 ipv4 和 ipv6 的地址解析,但是我還是抱著試試看的態度寫下了如下測試代碼:
var domain = process.argv[2] || 'baidu.com'; require('http').request('http://' + domain,function(res) {console.log(`STATUS: ${res.statusCode}`);console.log(`HEADERS: ${JSON.stringify(res.headers)}`);res.setEncoding('utf8');res.on('data', (chunk) => {//console.log(`BODY: ${chunk}`);});res.on('end', () => {console.log('No more data in response.');}); }).end();代碼 1.2 http 請求測試
沒有想到 代碼 1.2 執行完成后竟然只做了 ipv4 的解析:
21:01:06.593429 IP 10.6.12.158.48479 > 10.7.11.219.domain: 10352+ A? baidu.com. (27) 21:01:06.596978 IP 10.7.11.219.domain > 10.6.12.158.48479: 10352 2/0/0 A 39.156.69.79, A 220.181.38.148 (59)這就很神奇了,node 的 http 的代碼封裝中肯定做了什么!帶著這個疑問,我閱讀了 node 的源碼,首先看 ClientRequest 的初始化代碼中,連接初始化部分:
// initiate connectionif (this.agent) {this.agent.addRequest(this, options);} else {// No agent, default to Connection:close.this._last = true;this.shouldKeepAlive = false;if (typeof options.createConnection === 'function') {const newSocket = options.createConnection(options, oncreate);if (newSocket && !called) {called = true;this.onSocket(newSocket);} else {return;}} else {debug('CLIENT use net.createConnection', options);this.onSocket(net.createConnection(options));}}代碼 1.3 ClientRequest 類的連接初始化
http.request 沒有加任何參數的情況,默認走到 this.onSocket(net.createConnection(options)); 這句話,然后看 net 包的代碼,其中一端跟 DNS 相關的代碼:
if (dns === undefined) dns = require('dns');const dnsopts = {family: options.family,hints: options.hints || 0};if (process.platform !== 'win32' &&dnsopts.family !== 4 &&dnsopts.family !== 6 &&dnsopts.hints === 0) {dnsopts.hints = dns.ADDRCONFIG;}debug('connect: find host', host);debug('connect: dns options', dnsopts);self._host = host;const lookup = options.lookup || dns.lookup;代碼 1.4 net 包中 DNS 查詢參數代碼
然后我們再看 lookup 函數的源碼:
// Easy DNS A/AAAA look up // lookup(hostname, [options,] callback) function lookup(hostname, options, callback) {var hints = 0;var family = -1;var all = false;var verbatim = false;// Parse argumentsif (hostname && typeof hostname !== 'string') {throw new ERR_INVALID_ARG_TYPE('hostname', 'string', hostname);} else if (typeof options === 'function') {callback = options;family = 0;} else if (typeof callback !== 'function') {throw new ERR_INVALID_CALLBACK(callback);} else if (options !== null && typeof options === 'object') {hints = options.hints >>> 0;family = options.family >>> 0;all = options.all === true;verbatim = options.verbatim === true;validateHints(hints);} else {family = options >>> 0;}if (family !== 0 && family !== 4 && family !== 6)throw new ERR_INVALID_OPT_VALUE('family', family);if (!hostname) {emitInvalidHostnameWarning(hostname);if (all) {process.nextTick(callback, null, []);} else {process.nextTick(callback, null, null, family === 6 ? 6 : 4);}return {};}const matchedFamily = isIP(hostname);if (matchedFamily) {if (all) {process.nextTick(callback, null, [{ address: hostname, family: matchedFamily }]);} else {process.nextTick(callback, null, hostname, matchedFamily);}return {};}const req = new GetAddrInfoReqWrap();req.callback = callback;req.family = family;req.hostname = hostname;req.oncomplete = all ? onlookupall : onlookup; const err = cares.getaddrinfo(req, toASCII(hostname), family, hints, verbatim);if (err) {process.nextTick(callback, dnsException(err, 'getaddrinfo', hostname));return {};}return req; }代碼 1.5 lookup 函數源碼
通過代碼 1.5 發現最終 DNS 查詢是要調用 C++ 綁定類的,于是我又查看了 C++ 的代碼:
void GetAddrInfo(const FunctionCallbackInfo& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsObject());
CHECK(args[1]->IsString());
CHECK(args[2]->IsInt32());
CHECK(args[4]->IsBoolean());
Local req_wrap_obj = args[0].As();
node::Utf8Value hostname(env->isolate(), args[1]);
int32_t flags = 0;
if (args[3]->IsInt32()) {flags = args[3].As()->Value();
}
int family;
switch (args[2].As()->Value()) {
case 0:
family = AF_UNSPEC;
break;
case 4:
family = AF_INET;
break;
case 6:
family = AF_INET6;
break;
default:
CHECK(0 && "bad address family");
}
auto req_wrap = std::make_unique(env,
req_wrap_obj,
args[4]->IsTrue());
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;hints.ai_flags = flags;
TRACE_EVENT_NESTABLE_ASYNC_BEGIN2(
TRACING_CATEGORY_NODE2(dns, native), "lookup", req_wrap.get(),
"hostname", TRACE_STR_COPY(*hostname),
"family",
family == AF_INET ? "ipv4" : family == AF_INET6 ? "ipv6" : "unspec");int err = req_wrap->Dispatch(uv_getaddrinfo, AfterGetAddrInfo, *hostname, nullptr, &hints);
if (err == 0)
// Release ownership of the pointer allowing the ownership to be transferred
USE(req_wrap.release());
args.GetReturnValue().Set(err);
}代碼 1.6 C++ 中 DNS 的查詢代碼
注意 代碼 1.5 中的 family hints 最終會分別轉化為 結構體變量 struct addrinfo hints 中的 ai_family 和 ai_flags。
最終這個結構體 hints 會層層傳遞到 libuv 中:static void uv__getaddrinfo_work(struct uv__work* w) { uv_getaddrinfo_t* req; int err; req = container_of(w, uv_getaddrinfo_t, work_req);err = getaddrinfo(req->hostname, req->service, req->hints, &req->addrinfo); req->retcode = uv__getaddrinfo_translate_error(err);}代碼 1.7 libuv 中的 dns 查詢函數代碼
注意到我們在 代碼 1.4 中的 hints 參數,最終會作為 req->hints->ai_flags 參數,最終我在 man7 文檔上找到了 AI_ADDRCONFIG 的這個參數的說明:If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4addresses are returned in the list pointed to by res only if thelocal system has at least one IPv4 address configured, and IPv6addresses are returned only if the local system has at least one IPv6address configured. The loopback address is not considered for thiscase as valid as a configured address. This flag is useful on, forexample, IPv4-only systems, to ensure that getaddrinfo() does notreturn IPv6 socket addresses that would always fail in connect(2) orbind(2).
大體意思是說,系統配置了 ipv4 才返回 ipv4的地址,系統配置了 ipv6 才返回 ipv6 的地址,而 docker 的啟動參數 --sysctl net.ipv6.conf.all.disable_ipv6=1 等同于系統只支持 ipv4 的聲明,所以操作系統函數 getaddrinfo 就只返回 ipv4 的地址。
重新驗證這個問題,將代碼 1.1 做改造:const dns = require('dns');const domain = process.argv[2] || 'baidu.com';const begin = Date.now();dns.lookup(domain,{hints:32},function(err ,address , family) { console.log('耗時',Date.now() - begin, err ,address , family);});代碼 1.8 使用 ADDRCONFIG 參數做 DNS 查詢
這里面之所以取值的 hints:32,是因為 AI_ADDRCONFIG 的值為32。通過設置環境變量 NODE_DEBUG=net 后啟動 代碼1.2 ,會發現 debug('connect: dns options', dnsopts); 打印的 hints 值為 32。
重新運行,發現果然只查詢了 ipv4 的地址。
到此為止,其實可以算是圓滿收官了,但是對于 request 包還是不死心,心想如果當前開源源碼不支持,是否可以做一個 pull request 呢,于是我看了一下他們的官方源碼,結果就發現了新大陸: var reqOptions = copy(self) delete reqOptions.auth debug('make request', self.uri.href) // node v6.8.0 now supports a `timeout` value in `http.request()`, but we // should delete it for now since we handle timeouts manually for better // consistency with node versions before v6.8.0 delete reqOptions.timeout try { self.req = self.httpModule.request(reqOptions) } catch (err) { self.emit('error', err) return }代碼 1.9 request 源碼片段self.httpModule.request(reqOptions) 等同于 http.request(reqOptions) 或者 https.request(reqOptions),也就是說 http 模塊的所有參數其實在 request 上也是適用的,但是 request 的官方文檔卻沒有指出!
最圓滿的方案出爐了,在調用 request 函數的時候,指定 family 為 4,也可以通過 node 代碼層面屏蔽 ipv6 解析。不過鑒于啟動 docker 時已經添加 sysctl 參數,即使 node 不指定使用 ipv4,請求 http 也會只返回 ipv4 的地址。
總結
以上是生活随笔為你收集整理的为何解析浏览器地址参数会为null_request 包中出现 DNS 解析超时的探究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: pytorch forward_【Pyt
- 下一篇: 用单片机测量流体流速的_流量测量的主要方