1、背景
在计算机领域,涉及性能优化动作时首先应被考虑的原则之一便是使用缓存,合理的数据缓存机制能够带来以下收益:
1)缩短数据获取路径,热点数据就近缓存以便后续快速读取,从而明显提升处理效率;
2)降低数据远程获取频次,缓解后端数据服务压力、减少前端和后端之间的网络带宽成本;
从 cpu 硬件的多级缓存设计,到浏览器快速展示页面,再到大行其道的 cdn、云存储网关等商业产品,处处应用了缓存理念。
在公网领域,如操作系统、浏览器和移动端 app 等成熟产品所具备的缓存机制,极大的消解了网络提供商如电信移动联通、内容提供商如各大门户平台和 cdn 厂商直面的服务压力,运营商的 dns 才能从容面对每秒亿万级的 dns 解析,网络设备集群才能轻松承担每秒 tbit 级的互联网带宽,cdn 平台才能快速处理每秒亿万次的请求。
面对公司目前庞大且仍在不断增长的的域名接入规模,笔者所在团队在不断优化集群架构、提升 dns 软件性能的同时,也迫切需要推动各类客户端环境进行域名解析请求机制的优化,因此,特组织团队成员调研、编写了这篇指南文章,以期为公司、客户及合作方的前端开发运维人员给出合理建议,优化 dns 整体请求流程,为业务增效。
本文主要围绕不同业务和开发语言背景下,客户端本地如何实现 dns 解析记录缓存进行探讨,同时基于笔者所在团队对 dns 本身及公司网络环境的掌握,给出一些其他措施,最终致力于客户端一侧的 dns 解析请求规范化。
2、名词解释
1. 客户端
本文所述客户端,泛指所有主动发起网络请求的对象,包括但不限于服务器、pc、移动终端、操作系统、命令行工具、脚本、服务软件、用户 app 等。
2. dns
domain name system(server/service),域名系统(服务器/服务),可理解为一种类数据库服务;
客户端同服务端进行网络通信,是靠 ip 地址识别对方;而作为客户端的使用者,人类很难记住大量 ip 地址,所以发明了易于记忆的域名如 www.jd.com,将域名和 ip 地址的映射关系,存储到 dns 可供客户端查询;
客户端只有通过向 dns 发起域名解析请求从而获取到服务端的 ip 地址后,才能向 ip 地址发起网络通信请求,真正获取到域名所承载的服务或内容。
参考:域名系统 域名解析流程
3. ldns
local dns,本地域名服务器;公网接入环境通常由所在网络供应商自动分配(供应商有控制权,甚至可作 dns 劫持,即篡改解析域名得到的 ip),内网环境由 it 部门设置自动分配;
通常 unix、类unix、macos系统可通过 /etc/resolv.conf 查看自己的 ldns,在 nameserver 后声明,该文件亦支持用户自助编辑修改,从而指定 ldns,如公网常见的公共 dns 如谷歌 dns、114dns 等;纯内网环境通常不建议未咨询it部门的情况下擅自修改,可能导致服务不可用;可参考 man resolv.conf 指令结果。
当域名解析出现异常时,同样应考虑 ldns 服务异常或发生解析劫持情况的可能。
参考:windows系统修改tcp/ip设置(含dns);
4. hosts
dns 系统可以动态的提供域名和ip的映射关系,普遍存在于各类操作系统的hosts文件则是域名和ip映射关系的静态记录文件,且通常 hosts 记录优先于 dns 解析,即本地无缓存或缓存未命中时,则优先通过 hosts 查询对应域名记录,若 hosts 无相关映射,则继续发起 dns 请求。关于 linux 环境下此逻辑的控制,请参考下文 c/c++ 语言 dns 缓存介绍部分。
所以在实际工作中,常利用上述默认特性,将特定域名和特定 ip 映射关系写到 hosts 文件中(俗称“固定 hosts”),用于绕开 dns 解析过程,对目标 ip 作针对性访问(其效果与 curl 的-x选项,或 wget 的 -e 指定 proxy 选项,异曲同工);
5. ttl
time-to-live,生存时间值,此概念在多领域适用且可能有不同意义。
本文涉及到 ttl 描述均针对数据缓存而言,可直白理解为已缓存数据的“有效期”,从数据被缓存开始计,在缓存中存在时长超过 ttl 规定时长的数据被视为过期数据,数据被再次调用时会立刻同权威数据源进行有效性确认或重新获取。
因缓存机制通常是被动触发和更新,故在客户端的缓存有效期内,后端原始权威数据若发生变更,客户端不会感知,表现为业务上一定程度的数据更新延迟、缓存数据与权威数据短时不一致。
对于客户端侧 dns 记录的缓存 ttl,我们建议值为 60s;同时如果是低敏感度业务比如测试、或域名解析调整不频繁的业务,可适当延长,甚至达到小时或天级别;
3、dns解析优化建议
3.1 各语言网络库对 dns 缓存的支持调研
以下调研结果,推荐开发人员参考,以实现自研客户端 dns 缓存。各开发语言对 dns 缓存支持可能不一样,在此逐个分析一下。
c/c++ 语言
(1) glibc 的 getaddrinfo 函数
linux环境下的 glibc 库提供两个域名解析的函数:gethostbyname 函数和 getaddrinfo 函数,gethostbyname 是曾经常用的函数,但是随着向 ipv6 和线程化编程模型的转移,getaddrinfo 显得更有用,因为它既解析 ipv6 地址,又符合线程安全,推荐使用 getaddrinfo 函数。
函数原型:
int getaddrinfo( const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);getaddrinfo 函数是比较底层的基础库函数,很多开发语言的域名解析函数都依赖这个函数,因此我们在此介绍一下这个函数的处理逻辑。通过 strace 命令跟踪这个函数系统调用。
1)查找 nscd 缓存(nscd 介绍见后文)
我们在 linux 环境下通过 strace 命令可以看到如下的系统调用
//连接nscdsocket(pf_local, sock_stream|sock_cloexec|sock_nonblock, 0) = 3connect(3, {sa_family=af_local, sun_path=/var/run/nscd/socket}, 110) = -1 enoent (no such file or directory)close(3)
通过 unix socket 接口/var/run/nscd/socket连接nscd服务查询dns缓存。
2)查询 /etc/hosts 文件
如果nscd服务未启动或缓存未命中,继续查询hosts文件,我们应该可以看到如下的系统调用
//读取 hosts 文件open(/etc/host.conf, o_rdonly) = 3fstat(3, {st_mode=s_ifreg|0644, st_size=9, ...}) = 0...open(/etc/hosts, o_rdonly|o_cloexec) = 3fcntl(3, f_getfd) = 0x1 (flags fd_cloexec)fstat(3, {st_mode=s_ifreg|0644, st_size=178, ...}) = 03)查询 dns 服务
从 /etc/resolv.conf 配置中查询到 dns 服务器(nameserver)的ip地址,然后做 dns 查询获取解析结果。我们可以看到如下系统调用
//获取 resolv.conf 中 dns 服务 ipopen(/etc/resolv.conf, o_rdonly) = 3fstat(3, {st_mode=s_ifreg|0644, st_size=25, ...}) = 0mmap(null, 4096, prot_read|prot_write, map_private|map_anonymous, -1, 0) = 0x7fef2abee000read(3, nameserver 114.114.114.114, 4096) = 25...//连到 dns 服务,开始 dns 查询connect(3, {sa_family=af_inet, sin_port=htons(53), sin_addr=inet_addr(114.114.114.114)}, 16) = 0poll([{fd=3, events=pollout}], 1, 0) = 1 ([{fd=3, revents=pollout}])而关于客户端是优先查找 /etc/hosts 文件,还是优先从 /etc/resolv.conf 中获取 dns 服务器作查询解析,是由 /etc/nsswitch.conf 控制:#/etc/nsswitch.conf 部分配置...#hosts: db files nisplus nis dnshosts: files dns...实际通过 strace 命令可以看到,系统调用 nscd socket 之后,读取 /etc/resolv.conf 之前,会读取该文件newfstatat(at_fdcwd, /etc/nsswitch.conf, {st_mode=s_ifreg|0644, st_size=510, ...}, 0) = 0...openat(at_fdcwd, /etc/nsswitch.conf, o_rdonly|o_cloexec) = 34)验证#include #include #include #include #include #include #include int gethostaddr(char * name);int main(int argc, char *argv[]){ if (argc != 2) { fprintf(stderr, %s $host, argv[0]); return -1; } int i = 0; for(i = 0; i ai_addr; inet_ntop(curr->ai_family, &ipv4->sin_addr, ipstr, inet_addrstrlen); printf(ipaddr:%s, ipstr); } freeaddrinfo(result); return 0;}综上分析,getaddrinfo 函数结合 nscd ,是可以实现 dns 缓存的。
(2)libcurl 库的域名解析函数
libcurl 库是 c/c++ 语言下,客户端比较常用的网络传输库,curl 命令就是基于这个库实现。这个库也是调用 getaddrinfo 库函数实现 dns 域名解析,也是支持 nscd dns 缓存的。
intcurl_getaddrinfo_ex(const char *nodename, const char *servname, const struct addrinfo *hints, curl_addrinfo **result){ ... error = getaddrinfo(nodename, servname, hints, &aihead); if(error) return error; ...}
java
java 语言是很多公司业务系统开发的主要语言,通过编写简单的 http 客户端程序测试验证 java 的网络库是否支持 dns 缓存。测试验证了 java 标准库中 httpurlconnection 和 apache httpcomponents-client 这两个组件。
(1)java 标准库 httpurlconnection
import java.io.bufferedreader;import java.io.inputstream;import java.io.inputstreamreader;import java.io.outputstream;import java.net.httpurlconnection;import java.net.url;public class httpurlconnectiondemo { public static void main(string[] args) throws exception { string urlstring = http://example.my.com/; int num = 0; while (num < 5) { url url = new url(urlstring); httpurlconnection conn = (httpurlconnection) url.openconnection(); conn.setrequestmethod(get); conn.setdooutput(true); outputstream os = conn.getoutputstream(); os.flush(); os.close(); if (conn.getresponsecode() == httpurlconnection.http_ok) { inputstream is = conn.getinputstream(); bufferedreader reader = new bufferedreader(new inputstreamreader(is)); stringbuilder sb = new stringbuilder(); string line; while ((line = reader.readline()) != null) { sb.append(line); } system.out.println(rsp: + sb.tostring()); } else { system.out.println(rsp code: + conn.getresponsecode()); } num++; } }}
测试结果显示 java 标准库 httpurlconnection 是支持 dns 缓存,5 次请求中只有一次 dns 请求。
(2)apache httpcomponents-client
import java.util.arraylist;import java.util.list;import org.apache.hc.client5.http.classic.methods.httpget;import org.apache.hc.client5.http.entity.urlencodedformentity;import org.apache.hc.client5.http.impl.classic.closeablehttpclient;import org.apache.hc.client5.http.impl.classic.closeablehttpresponse;import org.apache.hc.client5.http.impl.classic.httpclients;import org.apache.hc.core5.http.httpentity;import org.apache.hc.core5.http.namevaluepair;import org.apache.hc.core5.http.io.entity.entityutils;import org.apache.hc.core5.http.message.basicnamevaluepair;public class quickstart { public static void main(final string[] args) throws exception { int num = 0; while (num < 5) { try (final closeablehttpclient httpclient = httpclients.createdefault()) { final httpget httpget = new httpget(http://example.my.com/); try (final closeablehttpresponse response1 = httpclient.execute(httpget)) { system.out.println(response1.getcode() + + response1.getreasonphrase()); final httpentity entity1 = response1.getentity(); entityutils.consume(entity1); } } num++; } }}
测试结果显示 apache httpcomponents-client 支持 dns 缓存,5 次请求中只有一次 dns 请求。
从测试中发现 java 的虚拟机实现一套 dns 缓存,即实现在 java.net.inetaddress 的一个简单的 dns 缓存机制,默认为缓存 30 秒,可以通过 networkaddress.cache.ttl 修改默认值,缓存范围为 jvm 虚拟机进程,也就是说同一个 jvm 进程中,30秒内一个域名只会请求dns服务器一次。同时 java 也是支持 nscd 的 dns 缓存,估计底层调用 getaddrinfo 函数,并且 nscd 的缓存级别比 java 虚拟机的 dns 缓存高。
# 默认缓存 ttl 在 jre/lib/security/java.security 修改,其中 0 是不缓存,-1 是永久缓存networkaddress.cache.ttl=10# 这个参数 sun.net.inetaddr.ttl 是以前默认值,目前已经被 networkaddress.cache.ttl 取代
go
随着云原生技术的发展,go 语言逐渐成为云原生的第一语言,很有必要验证一下 go 的标准库是否支持 dns 缓存。通过我们测试验证发现 go 的标准库 net.http 是不支持 dns 缓存,也是不支持 nscd 缓存,应该是没有调用 glibc 的库函数,也没有实现类似 getaddrinfo 函数的功能。这个跟 go语言的自举有关系,go 从 1.5 开始就基本全部由 go(.go) 和汇编 (.s) 文件写成的,以前版本的 c(.c) 文件被全部重写。不过有一些第三方 go 版本 dns 缓存库,可以自己在应用层实现,还可以使用 fasthttp 库的 httpclient。
(1)标准库net.http
package mainimport ( flag fmt io/ioutil net/http time)var httpurl stringfunc main() { flag.stringvar(&httpurl, url, , url) flag.parse() geturl := fmt.sprintf(http://%s/, httpurl) fmt.printf(url: %s, geturl) for i := 0; i < 5; i++ { _, buf, err := httpget(geturl) if err != nil { fmt.printf(err: %v, err) return } fmt.printf(resp: %s, string(buf)) time.sleep(10 * time.second) # 等待10s发起另一个请求 }}func httpget(url string) (int, []byte, error) { client := createhttpcli() resp, err := client.get(url) if err != nil { return -1, nil, fmt.errorf(%s err [%v], url, err) } defer resp.body.close() buf, err := ioutil.readall(resp.body) if err != nil { return resp.statuscode, buf, err } return resp.statuscode, buf, nil}func createhttpcli() *http.client { readwritetimeout := time.duration(30) * time.second tr := &http.transport{ disablekeepalives: true, //设置短连接 idleconntimeout: readwritetimeout, } client := &http.client{ timeout: readwritetimeout, transport: tr, } return client}从测试结果来看,net.http 每次都去 dns 查询,不支持 dns 缓存。
(2)fasthttp 库
fasthttp 库是 go 版本高性能 http 库,通过极致的性能优化,性能是标准库 net.http 的 10 倍,其中一项优化就是支持 dns 缓存,我们可以从其源码看到
//主要在fasthttp/tcpdialer.go中type tcpdialer struct { ... // this may be used to override dns resolving policy, like this: // var dialer = &fasthttp.tcpdialer{ // resolver: &net.resolver{ // prefergo: true, // stricterrors: false, // dial: func (ctx context.context, network, address string) (net.conn, error) { // d := net.dialer{} // return d.dialcontext(ctx, udp, 8.8.8.8:53) // }, // }, // } resolver resolver // dnscacheduration may be used to override the default dns cache duration (defaultdnscacheduration) dnscacheduration time.duration ...}可以参考如下方法使用 fasthttp client 端func main() { // you may read the timeouts from some config readtimeout, _ := time.parseduration(500ms) writetimeout, _ := time.parseduration(500ms) maxidleconnduration, _ := time.parseduration(1h) client = &fasthttp.client{ readtimeout: readtimeout, writetimeout: writetimeout, maxidleconnduration: maxidleconnduration, nodefaultuseragentheader: true, // don't send: user-agent: fasthttp disableheadernamesnormalizing: true, // if you set the case on your headers correctly you can enable this disablepathnormalizing: true, // increase dns cache time to an hour instead of default minute dial: (&fasthttp.tcpdialer{ concurrency: 4096, dnscacheduration: time.hour, }).dial, } sendgetrequest() sendpostrequest()}
(3)第三方dns缓存库
这个是 github 中的一个 go 版本 dns 缓存库
可以参考如下代码,在http库中支持dns缓存
r := &dnscache.resolver{}t := &http.transport{ dialcontext: func(ctx context.context, network string, addr string) (conn net.conn, err error) { host, port, err := net.splithostport(addr) if err != nil { return nil, err } ips, err := r.lookuphost(ctx, host) if err != nil { return nil, err } for _, ip := range ips { var dialer net.dialer conn, err = dialer.dialcontext(ctx, network, net.joinhostport(ip, port)) if err == nil { break } } return },}
python
(1)requests 库
#!/bin/pythonimport requestsurl = 'http://example.my.com/'num = 0while num < 5: headers={connection:close} # 开启短连接 r = requests.get(url,headers = headers) print(r.text) num +=1(2)httplib2 库#!/usr/bin/env pythonimport httplib2http = httplib2.http()url = 'http://example.my.com/'num = 0while num < 5: loginheaders={ 'user-agent': 'mozilla/5.0 (windows nt 5.1) applewebkit/537.36 (khtml, like gecko) maxthon/4.0 chrome/30.0.1599.101 safari/537.36', 'connection': 'close' # 开启短连接 } response, content = http.request(url, 'get', headers=loginheaders) print(response) print(content) num +=1
(3)urllib2 库
#!/bin/pythonimport urllib2import cookielibhttphandler = urllib2.httphandler(debuglevel=1)httpshandler = urllib2.httpshandler(debuglevel=1)opener = urllib2.build_opener(httphandler, httpshandler)urllib2.install_opener(opener)loginheaders={ 'user-agent': 'mozilla/5.0 (windows nt 5.1) applewebkit/537.36 (khtml, like gecko) maxthon/4.0 chrome/30.0.1599.101 safari/537.36', 'connection': 'close' # 开启短连接}num = 0while num < 5: request=urllib2.request('http://example.my.com/',headers=loginheaders) response = urllib2.urlopen(request) page='' page= response.read() print response.info() print page num +=1
python 测试三种库都是支持 nscd 的 dns 缓存的(推测底层也是调用 getaddrinfo 函数),以上测试时使用 http 短连接,都在 python2 环境测试。
总结
针对 http 客户端来说,可以优先开启 http 的 keep-alive 模式,可以复用 tcp 连接,这样可以减少 tcp 握手耗时和重复请求域名解析,然后再开启 nscd 缓存,除了 go 外,c/c++、java、python 都可支持 dns 缓存,减少 dns查询耗时。
这里只分析了常用 c/c++、java、go、python 语言,欢迎熟悉其他语言的小伙伴补充。
3.2 unix/类 unix 系统常用 dns 缓存服务:
在由于某些特殊原因,自研或非自研客户端本身无法提供 dns 缓存支持的情况下,建议管理人员在其所在系统环境中部署dns缓存程序;
现介绍 unix/类 unix 系统适用的几款常见轻量级 dns 缓存程序。而多数桌面操作系统如 windows、macos 和几乎所有 web 浏览器均自带 dns 缓存功能,本文不再赘述。
p.s. dns 缓存服务请务必确保随系统开机启动;
nscd
name service cache daemon 即装即用,通常为 linux 系统默认安装,相关介绍可参考其 manpage:man nscd;man nscd.conf
(1)安装方法:通过系统自带软件包管理程序安装,如 yum install nscd (2)缓存管理(清除):
service nscd restart 重启服务清除所有缓存;
nscd -i hosts 清除 hosts 表中的域名缓存(hosts 为域名缓存使用的 table 名称,nscd 有多个缓存 table,可参考程序相关 manpage)
dnsmasq
较为轻量,可选择其作为 nscd 替代,通常需单独安装
(1)安装方法:通过系统自带软件包管理程序安装,如 yum install dnsmasq (2)核心文件介绍(基于 dnsmasq version 2.86,较低版本略有差异,请参考对应版本文档如 manpage 等) (3)/etc/default/dnsmasq 提供六个变量定义以支持六种控制类功能 (4)/etc/dnsmasq.d/ 此目录含 readme 文件,可参考;目录内可以存放自定义配置文件
(5)/etc/dnsmasq.conf 主配置文件,如仅配置 dnsmasq 作为缓存程序,可参考以下配置
listen-address=127.0.0.1 #程序监听地址,务必指定本机内网或回环地址,避免暴露到公网环境port=53 #监听端口resolv-file=/etc/dnsmasq.d/resolv.conf #配置dnsmasq向自定义文件内的 nameserver 转发 dns 解析请求cache-size=150 #缓存记录条数,默认 150 条,可按需调整、适当增大no-negcache #不缓存解析失败的记录,主要是 nxdomain,即域名不存在log-queries=extra #开启日志记录,指定“=extra”则记录更详细信息,可仅在问题排查时开启,平时关闭log-facility=/var/log/dnsmasq.log #指定日志文件#同时需要将本机 /etc/resolv.conf 第一个 nameserver 指定为上述监听地址,这样本机系统的 dns 查询请求才会通过 dnsmasq 代为转发并缓存响应结果。#另 /etc/resolv.conf 务必额外配置 2 个 nameserver,以便 dnsmasq 服务异常时支持系统自动重试,注意 resolv.conf 仅读取前 3 个 nameserver(6)缓存管理(清除):
kill -s hup `pidof dnsmasq` 推荐方式,无需重启服务
kill -s term `pidof dnsmasq` 或 service dnsmasq stop
service dnsmasq force-reload 或 service dnsmasq restart
(7)官方文档:https://thekelleys.org.uk/dnsmasq/doc.html
3.3 纯内网业务取消查询域名的aaaa记录的请求
以 linux 操作系统为例,常用的网络请求命令行工具常常通过调用 getaddrinfo() 完成域名解析过程,如 ping、telnet、curl、wget 等,但其可能出于通用性的考虑,均被设计为对同一个域名每次解析会发起两个请求,分别查询域名 a 记录(即 ipv4 地址)和 aaaa 记录(即 ipv6 地址)。
因目前大部分公司的内网环境及云上内网环境还未使用 ipv6 网络,故通常 dns 系统不为内网域名添加 aaaa 记录,徒劳请求域名的 aaaa 记录会造成前端应用和后端 dns 服务不必要的资源开销。因此,仅需请求内网域名的业务,如决定自研客户端,建议开发人员视实际情况,可将其设计为仅请求内网域名 a 记录,尤其当因故无法实施本地缓存机制时。
3.4 规范域名处理逻辑
客户端需严格规范域名/主机名的处理逻辑,避免产生大量对不存在域名的解析请求(确保域名从权威渠道获取,避免故意或意外使用随机构造的域名、主机名),因此类请求的返回结果(nxdomain)通常不被缓存或缓存时长较短,且会触发客户端重试,对后端 dns 系统造成一定影响。
功率探头的选型指南
赋能「360行」绿色用电,上能电气引领零碳工商业新风尚
海尔共享空调正式入驻西安交通大学助力西安交通大学5G智慧校园建设
UCIe联盟热之下的思考,Chiplet技术本身更值得关注
LED连接器专用性很强 能保证传输信号的稳定性
各开发语言DNS缓存配置建议
华为与OPPO签订全球专利交叉许可协议
Arm推出PSA安全架构,为物联网互联设备保驾护航
IO模块在钢铁行业的应用:提高效率,降低能耗
【新专利介绍】一种可连接云控制平台的机械式水表
x-ray射线检测设备在质量控制中起到重要作用
IP网络广播系统有哪些优点
LoRa作为物联网时代的“WiFi”,你到底了解它多少呢?
iPhone13/SE3确定缩小或取消刘海屏,并全面植入屏下指纹技术
NI携手上海交通大学新图书馆构建环境监测与节能系统
如何提高太阳能光伏电池片栅线的膜厚一致性?
如何正确使用光谱成像技术进行食品检测
全世界著名的十大自行车排行榜中的顶级自行车品牌
Nordic公司与Atlazo公司达成收购知识产权组合协议
vocs有毒气体监测系统生产厂家