DNS客户端异常如何解决?

DNS客户端异常如何解决?

作者:夏志培

本文为原创文章,转载请注明作者及出处

背景

运维的职业生涯中会碰到各种各样的故障、问题,如何解决问题成为工作的关键点。运维的技术文章都会从问题点或者故障现象入手,逐步分析故障原因,最后寻求问题的解决方案。

问题

本次问题源于前段时间开发同事反映 salt-minion 工作异常,无法正常做解析,经过重现现场的跟踪,很快定位到是缓存 DNS 的问题。

当时的现象是: 缓存 DNS 的地址为被迁移掉机房的 DNS 的 IP,但检查当前的 /etc/resolv.conf 配置文件中的 nameserver 地址为新机房服务器的 IP, Linux 系统中的 /etc/resolv.conf 中配置 DNS 正确,但是 salt-minion 应用使用的仍然是错误的 DNS 。

于是翻查 python salt-minion 的中代码,查看 salt 是否有存在使用第三方 DNS 库作为 resolve,查找过程中发现 salt-minion 未调用第三方的 DNS 库,而是使用 python socket.getaddrinfo,python 中的 socket.getaddrinfo 实际是 glibc 库getaddrinfo 的封装(老的 glibc 版本中是 getbyhostname)。

当应用层 (salt) 在使用 getaddrinfo 函数的时候,实际会使用 glibc 的相关函数,glibc 在应用第一次域名解析的时候会触发 res_init() 函数的调用,res_init() 函数的作用是读取 /etc/resolv.conf 的内容, 如 nameserver 地址、负载均衡策略、重试次数、超时时间等,并将读取的这些数据放到 static 类型的 _res_ 结构体中。

由于 Linux 的进程在使用 glibc 动态链接库全局静态变量的时候,都会在用户进程空间生成自己独立的变量副本(感兴趣的同学可以查看每个进程的 smaps 文件,glibc 在每个用户进程空间都有可读、可写的 segment),所以每个发起 DNS 解析的进程都具有独立的 _res_ 结构体变量。

回到 salt-minio 这个具体案例,salt-minion 进程启动后发起第一次域名解析请求,该请求最后会调用底层的 res_query,第一次 res_query 会调用 res_init(),res_init() 初始化后,nameserver 地址被初始化到当前进程的 _res_ 结构体中, 然而 res_init() 对于每个进程来说,只会执行一次。

运维同学在用户进程运行的时候修改掉 /etc/resolv.conf 中的配置,但修改配置后无法生效的根本原因就在此(因为用户进程空间的 _res 结构体中仍然存放的是老的 nameserver 配置)。

以下是 2 个平台对应 glibc 关于 res_init() 的说明:

  • BSD 平台下的:

    The res_init() function is the Resolver function that initialize the__res_state structure for use bsy other Resolver functions. Initialization normally occurs on the first call to any of the IPaddress resolution routines commonly called the XL C/C++ Runtime LibraryResolver.The res_init() routine does its initialization by passing the__res_state structure to the CS for z/OS® Resolver.The Resolver reads the "TCPIP.DATA" configuration file andupdates the __res_state structure.The data in the __res_state structure is filled in based on the contents of the "TCPIP.DATA" configuration file and can then be referenced inthe _res variable.Global configuration and state information that is used by the Resolverroutines is kept in the structure _res.Most of the values have reasonable defaults and can be left unchanged.

  • Linux平台下的:

    The functions described below make queriesto and interpret the responses from Internet domain name servers.The API consists of a set of more modern, reentrant functions and an older set of nonreentrant functions that have been superseded. The traditional resolver interfaces such as res_init() and res_query() use some static (global) state stored in the _res structure, rendering these functions non-thread-safe. BIND 8.2 introduced a set of new interfaces res_ninit(), res_nquery(), and so on, which take a res_state as their first argument, so you can use a per-thread

    resolver state.The res_ninit() and res_init() functions read the configuration files (see resolv.conf(5)) to get the default domain name and name server address(es).If no server isgiven, the local host is tried. If no domain is given, that associated with thelocal host is used. It can be overridden with the environment variable LOCALDOMAIN. res_ninit() or res_init() is normally executed by the first call to one of the other functions.

Centos6.5 的 glibc-2.12 中的代码:

Res_init() 函数会调用 __res_vinit() ,__res_vinit 会使用 res_state 数据结构,res_state 最终会使用 __res_state 的结构体。

DNS客户端异常如何解决?

DNS客户端异常如何解决?

DNS客户端异常如何解决?

解决办法

既然知道问题的根本原因, 以下是解决办法:

1. 用户进程直接调用底层的接口,并且定期调用 res_init() 以防止 /etc/resolov.conf 文件被修改而无法感知(有一定的实现成本)。

2. 直接重启用户进程,使用户进程在做第一次域名解析的时候触发 res_init() (实现成本低)

3. 用户进程实现 DNS 的 client 全部功能(实现成本很高)。

4. 通过非常规手段修改用户进程空间的数据(实现成本很高)。

将这个问题点再延伸一点,假设公司所有业务的配置文件中采用域名的方案,像 redis,memcached,rabbitmq,codis,zookeeper,DB 等,调用端一般都会采用连接池的方式使用这些基础服务,那么同样也面临上面的问题,由于故障/迁移等不可控的原因,导致基础服务的 IP 发生变化,不论是需要去修改成百上千的业务配置文件,还是重启成百上千的应用服务,都将是一个复杂而痛苦的过程。

这个场景下,最好的解决方案就是直接使用底层的域名解析函数,而不是使用java/.NET 自带的域名解析函数,在连接池中的连接发生重连的时候,应用端需要重新执行一次 glibc 的 res_init(),然后再调用 res_query() 做域名解析,可以很好地解决上面问题。所有长连接的应用场景使用自研 DNS SDK,一旦发现 IP 有变更,只需要修改 DNS 解析,然后将旧 IP 上的服务关停即可完成基础业务的切换。

今天就到这啦,有关运维的后续文章会在「沪江技术学院」中发出,敬请期待!