2015年02月20日

异步dns解析


域名解析是一个很常见的问题,之前以为是简单的从IP到域名的双向映射,调研后发现还是略复杂的,尤其是从开源的异步DNS解析库ADNS中看到的形形色色解析类型。

普通的gethostbyname(3), gethostbyaddr(3)函数能够完成IP与DOMAIN之间的同步转换,转换的过程可能会比较耗时,getaddrinfo_a(3)可以异步的完成如上转换。但是,均无法支持更多的类型查询,譬如MX,在功能上存在不足。

独自实现一套异步解析的库,估计难点在于支持完备的DNS协议(对协议了解全面),做好异步支持。

同步解析


DNS协议

对于gethostbyname(3)等同类型的函数集,可以通过man page查看使用说明。更完整底层的解析函数集是res_*系列函数,可以通过man 3 resovler来了解,下面详细介绍。

在了解res_*系列函数之前,需要对DNS协议有个初步的认识。 查询的Client端会发送一定格式的查询报文到Domain Name Server,消息中包含了多个查询问题,查询问题是由三部分组成:查询名,查询类型,查询类(表示指定使用的地址协议簇,通常是1,表示internet地址)。

DNS查询中,常用有如下的查询类型: * A类型: 表示期望获得查询名的IP地址 * PTR类型:表示期望获得一个ip地址对应的域名 * MX类型:邮件交换查询

来自Domain Name Server的响应报文包含了多个回答字段。每个回答字段以种叫做资源记录(Resource Record, RR)的格式存储的。一个回答字段由六部分组成:域名,类型,类,生存时间,资源数据长度,资源数据。前三者等同于查询问题的前三个字段。生存时间字段是客户程序保留该资源记录的秒数。资源数据长度说明资源数据包含的字节数。资源数据则根据类型字段的值有不同的格式. 对于A类型, 资源数据是IP地址. 对于MX查询, 资源数据是优先值和域名。

Res_* 函数簇

Linux以及其他的Unix,Unix-like系统,都通过了一套地址解析函数。包括res_init,res_query, res_search, res_expand等等。

   #include <netinet/in.h>
   #include <arpa/nameser.h>
   #include <resolv.h>
   extern struct state _res;

   int res_init(void);
   //会读取配置文件,默认是resolv.conf,获取到默认的Name Server Adderesses,如果为空,
   //则使用local host。同时会重载LOCALDOMAIN环境变量。

   int res_query(const char *dname, int class, int type,
          unsigned char *answer, int anslen);
   // 将查询name server。Dname的格式是fully qualified domain name。

   int res_search(const char *dname, int class, int type,
          unsigned char *answer, int anslen);
   //同res_query, 但是default和search rule是受RES_DEFNAMES 和 RES_DNSRCH options影响的

   int res_querydomain(const char *name, const char *domain,
          int class, int type, unsigned char *answer,
          int anslen);
    // 将连接 name,domain,然后调用res_query

   //注意:下面的一系列函数是res_query使用的。
   int res_mkquery(int op, const char *dname, int class,
          int type, char *data, int datalen, struct rrec *newrr,
          char *buf, int buflen);

   int res_send(const char *msg, int msglen, char *answer,
          int anslen);

   int dn_comp(unsigned char *exp_dn, unsigned char *comp_dn,
          int length, unsigned char **dnptrs, unsigned char *exp_dn,
          unsigned char **lastdnptr);

   int dn_expand(unsigned char *msg, unsigned char *eomorig,
         unsigned char *comp_dn, char *exp_dn,
          int length);

在使用res_(3)函数簇时,需要先调用res_init(3)进行初始化,res_query(3)的结果存储在answer buffer中。这一系列的函数都是以同步的方式调用的。更详细的内容请man 3 resovler

异步解析


异步解析才是我们关注的重点。同步解析函数的介绍,可以让我们了解到其中的基本知识。异步解析的方式基本大同小异,应该选取更适合融入到现有程序中的实现方式。

getaddrinfo_a

该函数同时支持同步或异步的方式完成域名查询,无法完成MX查询,但提供了异步的思路。函数签名如下:

int getaddrinfo_a(int mode, struct gaicb *list[], int nitems, struct sigevent *sevp);
//mode: GAI_WAIT, GAI_NOWAIT

int gai_suspend(struct gaicb *list[], int nitems, struct timespec *timeout);

int gai_error(struct gaicb *req);

int gai_cancel(struct gaicb *req);

struct gaicb {
    const char            *ar_name;
    const char            *ar_service;
    const struct addrinfo   *ar_request;
    struct addrinfo       *ar_result;
};

struct sigevent{
    int sigev_notify; //notification type
    int sigev_signo; //signal number
    union sigval   sigev_value; //signal value
    void (*sigev_notify_function)(union sigval);
    pthread_attr_t *sigev_notify_attributes;
}

union sigval{
    int sival_int; //integer value
    void *sival_ptr; //pointer value
}

如果函数调用采用GAI_NOWAIT模式,则进行异步的查询。此时,需要设置好struct sigevent来完成回调。函数完成异步解析之后,会根据设置的sigevent来产生信号或启动新的线程来执行指定函数。

adns

adns是一个开源的域名异步解析库,完整的支持DNS协议,并且性能据说不错,唯一不足的是缺乏官方文档介绍。 adns在风格上完整的参照res_函数簇,在源码中可以看到熟悉的函数,基于res_的基础知识,再来深入的理解adns的实现,会有略微容易些。 adns库是基于GPL协议的。

在介绍adns之前,我们再次强调关于adns的事实: * 支持多种类型的查询,功能十分强大,对DNS协议支持很友好。 * 采用异步方式实现,支持select和poll网络模型。 * 存在强大的python-binding,但是异步的接口还需要自己补充。

我们不关注adns协议层面的代码,重点关注adns是如何实现异步的,如何为我们所用。adns的使用方式也特别简单。关键的函数接口如下,更多的可以参考adns源码下的adns.h头文件,暴露了所有对外接口。

int adns_init(adns_state *newstate_r, adns_initflags flags, FILE *diagfile /*0=>stderr*/);

int adns_submit(adns_state ads, const char *owner,  adns_rrtype type,
        adns_queryflags flags, void *context, adns_query *query_r);

int adns_check(adns_state ads, adns_query *query_io, adns_answer **answer_r,
           void **context_r);

void adns_beforeselect(adns_state ads, int *maxfd_io, fd_set *readfds_io,                                                                                    
               fd_set *writefds_io, fd_set *exceptfds_io,
               struct timeval **tv_mod, struct timeval *tv_tobuf,
               const struct timeval *now)

void adns_afterselect(adns_state ads, int maxfd, const fd_set *readfds,                                                                                      
              const fd_set *writefds, const fd_set *exceptfds,
              const struct timeval *now)

从上面的5个关键函数就可以看出adns的使用方式来,使用select等网络模型来完成异步的处理。使用时需要先初始化,然后submit需要查询的内容,通过check来获取检查查询是否完成。通过两个select来注册和获取事件。

下面逐步的分析以上过程(若有错误,敬请谅解,随意指出):

前面强调adns支持多种类型查询,不同类型的查询,可以进行设置。其网络模型同时支持poll和select。

假如我们采用adns作为异步解析的库话,需要进行简单的封装。包括,提交期望解析的域名及查询类型,以及回调函数。

线程模型

adns支持两种类型的网络模型,poll和select,同时还支持一定模式下的轮询。现在我们详细的分析adns的网络模型。

在分析网络模型前,强调一些adns的基本情况。adns存在一个整体的状态维护结构体(好多开源的c代码都似乎是这种风格,一个大的全局结构体,从最外面一直传递到最底层,共享),这个结构体是adns_state,在adns_init初始化时,会读取出name server,然后会为每一个name server创建一个udp的socket fd,该fd会记录在adns_state中,每个fd都会设置O_NONBLOCK flag。

首先是一定模式的轮询,这是指设置adns_if_noautosys flag 为true,那么就可以通过while循环,定期的调用adns_check检查adns_state中维护的文件描述符的状态。 这就相当于每次主动的去查看文件描述符的状态,而不是被唤醒。

select模型的使用模式如下:

adns_init _noautosys
loop {
    adns_beforeselect
    select
    adns_afterselect
    ...
    adns_submit / adns_check
    ...
}

上面示例的代码也是select模型的规范用法,进入一个大的循环,开始典型的三部曲编码。select前的准备工作,select,然后是select后的处理动作。
上面的示例也强调了,如果需要添加新的查询请求,需要在select唤醒之后操作。考虑到一种场景(不一定合理),这个主线程在做loop大循环,而另一个线程直接调用了adns_submit,他们都共享的同一个adns_state结构体,那么这可能会存在race condition问题。最好,还是按照上面的模式来操作。在StackOverflow上,存在一个类似的问题,针对epoll的,线程A,在epoll_wait,而线程B在调用epoll_ctl,是否线程安全,具体的答案参考后面给出的参考链接7。这两个问题还有点差别,前者更多的冲突在于adns层面,后者在epoll层面。

对于每一个adns_query,都可以设置超时时间,在adns_beforeselect时,会选取所有query中最早需要完成的任务与当前的时间差,作为select(2)调用的参数,保证一但有任务超时,立即返回。要达到这个目的,需要注意使用adns_beforeselect的接口。

ands_beforeselect的接口略有些复杂,具体如下:

void adns_beforeselect(adns_state ads, int *maxfd, fd_set *readfds, fd_set *writefds, 
            fd_set *exceptfds, struct timeval **tv_mod, struct timeval *tv_buf,
               const struct timeval *now);

前5个参数很熟悉,复杂的是后面三个参数:

adns_beforeselect函数另一个重要的功能是,为select函数准备好需要监听的fd以及fd上的事件。 adns_afterselect函数首先会处理超时的query,(注意:前面提到过,adns_beforeselect可以设置select(3)调用的超时时间,这个与adns_afterselect中的超时会略有区别,前者如果设置了的话,是指有任何一个query超时,就返回,没设置的话,则需要等待fd有事件上来,才返回,返回后再检查是否有超时的fd)。 然后是依次的处理每个文件描述符上的事件。

poll模型和select模型基本上是一样的,只是系统调用不同,poll(2)的使用和性能与select存在差异,目前adns不支持epoll这种模型,不过应该是已经够用了。 对poll模型,不做过多阐述了。

Libevent

Libevent这个强大的网络库也自带了DNS解析库,目前支持的查询类型有限。Libevent是基于Reactor模式开发的,简单理解,注册事件及事件的回调函数,当事件触发时,则会调用回调函数。Libevent支持多种网络模型。

目前版本的Libevent实现了异步的getaddrinfo,是能够跨平台,具有良好可移植性的。也可以用作Domain Name Server。

Libevent dns库的使用方式挺简单,就不做过多介绍了。可以参考后面给出的文档1。

adns封装


在空余时间,对adns进行了简单的封装,采用C++语言,具体的代码可以点这儿;

使用到的adns的接口主要是上面提到的那几个,代码考虑的还存在诸多欠缺,譬如对于answer的解析,只是借用adns中的adns_rr_info得到抽象的结果,还有类型的支持不是很等等,对于answer的解析,这是个很复杂的细致活,adns内部已经封装了typeinfo,每种类型的answer都存在解析方式,这些接口没有暴露出来,可以参考。还有官方文档中提到的python绑定,其中有个简单的answer解析,可以作为参考。

参考文档


  1. libevent dns
  2. man 3 getaddrinfo_a
  3. gethostbyname系列
  4. res系列函数簇介绍
  5. adns home page
  6. adns python binding usage
  7. Is epoll thread safe
前一篇: Sigkill带来的挑战 后一篇: 小结(一)