From abb3df6acd24d7d14c22582b92b292eae72fbf38 Mon Sep 17 00:00:00 2001 From: kedixa <1204837541@qq.com> Date: Tue, 26 Dec 2023 19:27:12 +0800 Subject: [PATCH] docs for dns (#1459) --- docs/about-dns.md | 124 ++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 77 deletions(-) diff --git a/docs/about-dns.md b/docs/about-dns.md index 2d960334..f5cd938c 100644 --- a/docs/about-dns.md +++ b/docs/about-dns.md @@ -1,100 +1,70 @@ # 关于DNS +当使用域名请求网络时,首先需要通过域名解析获取服务器地址,再使用网络地址进行后续的请求。Workflow已经实现了完备的域名解析和缓存系统,通常来说用户无需知晓内部机制即可流畅地发起网络任务。 -DNS(域名服务协议)是一种分布式网络目录服务,主要用于域名与IP地址的相互转换。 -在进行通信访问的时候,需要对非IP的域名进行DNS解析,这个过程就是域名到IP地址的转换过程。 -DNS解析是一个比较大的消耗,不管是服务器还是本地操作系统,通常都会有自己的DNS Cache负责减少不必要的请求。 -有一些程序也会在自己的进程内设计自己的DNS Cache,包括常见流行的浏览器、通信框架等。 -workflow也设计了自己的DNS Cache,为了方便用户使用,DNS这部分功能被框架完全接管“隐藏”了起来。 +## DNS相关配置 +Workflow中的全局配置包括 -### TTL - -全称是“生存时间(Time To Live)”,简单的说TTL表示DNS记录在DNS Cache上缓存的时间。 - -### 框架的DNS方法 - -#### 同步请求 -通过调用系统函数getaddrinfo获取结果,一些细节: -1. 当命中框架自己的DNS Cache且TTL有效时,DNS解析不会发生。 -2. 当域名是ipv4、ipv6、unix-domain-socket,DNS解析不会发生。 -3. DNS解析是一个特殊的计算任务,被封装成了一个WFThreadTask。 -4. DNS解析使用的是一个完全独立隔离的线程池,即不占用计算线程池、也不占用通信线程池。 - -#### 异步请求 -框架实现了完备的DNS协议解析,在Unix系统下默认将默认使用框架内置的DNS解析器,不会创建DNS线程池。 -如需想恢复为多线程dns解析,需要在全局配置里将resolv_conf_path参数设置为NULL。 - -### 全局DNS配置 - -在[WFGlobal.h](../src/manager/WFGlobal.h)文件里,可以看到我们一个全局配置信息: ~~~cpp struct WFGlobalSettings { - EndpointParams endpoint_params; + struct EndpointParams endpoint_params; + struct EndpointParams dns_server_params; unsigned int dns_ttl_default; unsigned int dns_ttl_min; int dns_threads; int poller_threads; int handler_threads; int compute_threads; + int fio_max_events; const char *resolv_conf_path; const char *hosts_path; }; - -static constexpr struct WFGlobalSettings GLOBAL_SETTING_DEFAULT = -{ - .endpoint_params = ENDPOINT_PARAMS_DEFAULT, - .dns_server_params = ENDPOINT_PARAMS_DEFAULT, - .dns_ttl_default = 12 * 3600, /* in seconds */ - .dns_ttl_min = 180, /* reacquire when communication error */ - .dns_threads = 4, - .poller_threads = 4, - .handler_threads = 20, - .compute_threads = -1, - .resolv_conf_path = "/etc/resolv.conf", - .hosts_path = "/etc/hosts", -}; ~~~ -其中,与DNS相关的配置包括: - * dns_server_params:对dns服务器的最大并发数,超时等配置。 - * dns_threads: DNS线程池线程数,默认4。只有当resolv_conf_path配置为空时,这个参数才会起作用。否则我们并不会创建dns线程。 - * dns_ttl_default: DNS Cache中默认的TTL,单位秒,默认12小时,dns cache是当前进程的,即进程退出就会消失,配置也仅对当前进程有效。 - * dns_ttl_min: dns最短生效时间,单位秒,默认3分钟,用于通信失败重试是否尝试重新dns的决策。 - * resolv_conf_path: resolv.conf配置文件路径,为NULL表示使用多线程DNS解析。 - * hosts_path: hosts配置文件路径。可以为NULL。 -简单来讲,每次通信都会检查TTL来决定要不要重新进行DNS解析。 -默认检查dns_ttl_default,通信失败重试时才会去检查dns_ttl_min。 +其中与域名解析相关的配置项有 -全局的DNS配置,可以通过upstream功能,被单独的地址配置覆盖。 -Upstream每一个AddressParams也有dns_ttl_default和dns_ttl_min配置项,使用方式与Global相仿。 -具体结构详见[upstream文档](./about-upstream.md#Address属性)。 +* dns_server_params + * address_family: 该项会在后续展开说明 + * max_connections: 向DNS服务器发送请求的最大并发数,默认为200 + * connect_timeout/response_timeout/ssl_connect_timeout: 参考[超时](about-timeout.md)相关说明 +* dns_threads: 当使用同步方式实现域名解析时,解析操作会在独立的线程池中执行,该项指定线程池的线程数,默认为4 +* dns_ttl_default: 域名解析成功的结果会被放到域名缓存中,该项指定其存活时间,单位为秒,默认值12小时,当解析结果过期后会重新解析以获取最新内容 +* dns_ttl_min: 当通信失败时,有可能出现缓存的结果已经失效的情况,该项指定一个较短的存活时间,当通信失败时以更频繁的速率更新缓存,单位为秒,默认值3分钟 +* resolv_conf_path: 该文件保存了访问DNS相关的配置,在常见的Linux发行版上通常位于`/etc/resolv.conf`,若该项配置为`NULL`则表示使用多线程同步解析的模式 +* hosts_path: 该文件是一个本地的域名查找表,若被解析的域名命中该表则不会向DNS发起请求,在常见的Linux发行版上通常位于`/etc/hosts`,若该项配置为`NULL`则表示不使用查找表 + +### resolv.conf扩展功能 +Workflow对`resolv.conf`配置文件进行了扩展,用户可以通过修改配置以支持`DNS over TLS(DoT)`功能,**注意**直接修改`/etc/resolv.conf`会影响其他进程,可以将该文件复制一份用于修改,并将Workflow的`resolv_conf_path`配置修改为新文件的路径。例如使用`dnss`协议的`nameserver`会通过SSL进行连接 -## SSL DNS (DoT)的支持 -我们的主分支代码(不包括windows分支)支持通过SSL连接访问DNS服务器,即DoT。 -我们简单的扩展了**resolv.conf**的格式,你可以通过以下方式加入一个SSL dns server: ~~~bash nameserver dnss://8.8.8.8/ -~~~ -我们用**dnss://** 来表示一个SSL dns server地址。通过以上的配置,我们所有的DNS请求都将通过SSL连接与全球DNS server 8.8.8.8通信。 -全球DNS server完美支持SSL连接,大家可以立刻试用一下这个功能。 -为了不用修改系统的**resolv.conf**文件,你可能需要创建一个私有的**resolv.conf**,并修改workflow配置: -~~~cpp -#include -#include - -int main() -{ - struct WFGlobalSettings settings = GLOBAL_SETTINGS_DEFAULT; - settings.resolv_conf_path = "./myresolv.conf"; - WORKFLOW_library_init(&settings); - - WFHttpTask *task = WFTaskFactory::create_http_task("https://www.example.com", ...); - ... -} +nameserver dnss://[2001:4860:4860::8888]/ ~~~ -### DNS解析策略与DNS cache过期策略 -* 同一个域名,同时只会发送一个DNS请求,这是通过一种异步锁机制实现的。 - * 只有一种例外,两个不同的upstream指向同一个域名,但分别要求只用IPv4和只用IPv6,可能会同时发起两个请求。 -* 对DNS server的最大并发,受dns_server_params.max_connections控制,默认为200。 -* 某个域名DNS cache过期一瞬间,如果多个请求同时需要请求该域名,会有一个任务重新发起DNS请求,并把原cache有效期临时延长5秒,尽量减少DNS cache过期引起的访问中断。 +### Address Family +在某些网络环境下,虽然本机支持IPv6,但因未被分配公网IPv6地址而无法与外部通信(例如本地IPv6地址以`fe80`开始)。此时可以将`endpoint_params.address_family`设置为`AF_INET`来强制域名解析时仅解析IPv4地址。同样的,`resolv.conf`文件中可能同时指定了`nameserver`的IPv4地址和IPv6地址,此时可以将`dns_server_params.address_family`设置为`AF_INET`或`AF_INET6`来强制仅使用IPv4或IPv6地址来访问DNS。 + +### 使用Upstream配置 +全局配置默认对每个域名生效,若需要对某些域名单独指定不同的配置,则可使用[Upstream](./about-upstream.md#Address属性)功能。使用Upstream可以单独指定`dns_ttl_default`、`dns_ttl_min`配置项,以及通过`endpoint_params.address_family`单独指定该域名使用的IP地址类别。 + + +## 域名解析与缓存策略 +网络任务通常需要通过域名解析获取到需要访问的IP地址,Workflow中域名解析相关策略如下 + +1. 检查域名缓存是否有该域名对应的IP地址,若有缓存且未过期,则使用该组IP地址 +2. 检查域名是否为IPv4、IPv6地址或`Unix Domain Socket`,若是则直接使用该地址,无需发起域名解析 +3. 检查`hosts_path`文件中是否包含该域名对应的IP地址,若有则直接使用该地址 +4. 获取异步锁,保证同一域名的解析请求在同一时刻仅发起一次,并向DNS发起解析请求 +5. 解析成功后会将解析结果保存到当前进程的域名缓存中,以供下次使用,并释放异步锁 +6. 解析失败后会释放异步锁且将失败原因通知给等在同一个异步锁上的所有任务,通知结束后再发起的新的任务则会再次请求DNS + +许多需要大量发起网络请求的场景都会配备域名缓存组件,如果每次发起网络任务时都向DNS发起解析请求,则DNS必然会不堪重负。Workflow设置了缓存存活时长(dns_ttl_default和dns_ttl_min)来保证缓存会在合理的时间后过期,以及时更新域名的解析结果。当某个域名的缓存项过期后,首先发现过期的任务会将其存活时间延长5秒并向DNS发起解析请求,5秒内同一域名上的请求会直接使用缓存的DNS解析结果,而无需等待本次解析结束。 + +异步锁机制可以保证**同一域名**的解析请求在同一时刻仅发起一次,在没有锁保护的情况下,若短时间内对同一域名发起大量网络任务,每个任务都会因无法从缓存中获取结果而向DNS发起解析请求,这会对DNS带来很大且不必要的负担。这里的同一域名表示的是`(host, port, family)`三元组,若通过Upstream的方式对某域名分别要求只使用IPv4和IPv6,则他们会被不同的异步锁保护,也就有可能同时发起DNS请求。 + + +### 异步域名解析 +Workflow实现了完备的DNS任务(参考[dns_cli](./tutorial-17-dns_cli.md)),若指定了`resolv_conf_path`配置项,则向DNS发起域名解析时会使用异步请求的方式进行,在类Unix系统下,Workflow默认使用`/etc/resolv.conf`作为该配置的值。异步域名解析不会阻塞任何线程,也不会独占线程池,可以更高效地完成域名解析的任务。 + +### 同步域名解析 +若指定`resolv_conf_path`为`NULL`,则会通过调用`getaddrinfo`函数来实现同步域名解析,该方式会使用独立的线程池,其线程数通过`dns_threads`参数配置。若短时间内需要发起较多的域名解析请求,则同步的方式会带来较大的延迟。