阿里DNS:三问BIND主辅同步

4 人赞了文章

引子

众所周知,DNS中有主辅伺服器的概念,主辅DNS伺服器通过I/AXFR机制进行数据同步,BIND作为DNS伺服器的代表,自然也不例外。DNS RFC标准中只定义了有关数据同步机制通用的规范,然而BIND在这些通用规范之上还有一些特性化的功能,在BIND的使用过程中,恰是对这些特性化功能的不明确,给工作带来了不少困扰。本文主要关注工作中使用BIND在主辅同步中不确定的三个问题,并且尝试寻根溯源地给出三个问题的解答。水平有限,文中错误不足之处,还望指正。

下面是本文讨论的三个问题:

Q1: BIND做辅配多主的情况下,收到某个主发的NOTIFY的时候如何选主请求SOA,是选择收到NOTIFY的源,还是按照masters配置顺序挨个请求?

Q2: BIND做辅配多主的情况下,需要更新,选到的一个主不通,换主是否会有延时,延时是多少?

Q3: BIND做辅的情况下,请求SOA频率超过了serail-query-rate限制,xfr请求超过了transfers-in限制,会重试吗?

如果你对上述的三个问题有笃定的答案,那么大可跳过下面的内容,倘若你跟笔者一样不是很确定的话,那么就随笔者一起来逐一分解这三个问题。

注:BIND版本使用9.11.3

Q1: BIND做辅配多主的情况下,收到某个主发的NOTIFY的时候如何选主请求SOA,是选择收到NOTIFY的源,还是按照masters配置顺序挨个请求?

首先查阅RFC标准协议中的规定,注意到在RFC 1996( tools.ietf.org/html/rfc )有如下的规定:

Note: Because a deep server dependency graph may have multiple paths from the primary master to any given slave, it is possible that a slave will receive a NOTIFY from one of its known masters even though the rest of its known masters have not yet updated their copies of the zone. Therefore, when issuing a QUERY for the zones SOA, the query should be directed at the known master who was the source of the NOTIFY event, and not at any of the other known masters. This represents a departure from [RFC1035], which specifies that upon expiry of the SOA REFRESH interval, all known masters should be queried in turn.

RFC推荐辅伺服器直接向发送NOTIFY的源请求更新(关键词用should而非must)。

下面我们来分析一下BIND在这种场景下的行为,首先看一看BIND收到NOTIFY之后的处理:

isc_result_tdns_zone_notifyreceive2(dns_zone_t *zone, isc_sockaddr_t *from, isc_sockaddr_t *to, dns_message_t *msg){ unsigned int i; dns_rdata_soa_t soa; dns_rdataset_t *rdataset = NULL; dns_rdata_t rdata = DNS_RDATA_INIT; isc_result_t result; char fromtext[ISC_SOCKADDR_FORMATSIZE]; int match = 0; isc_netaddr_t netaddr; isc_uint32_t serial = 0; isc_boolean_t have_serial = ISC_FALSE; dns_tsigkey_t *tsigkey; dns_name_t *tsig; REQUIRE(DNS_ZONE_VALID(zone)); /* * If type != T_SOA return DNS_R_NOTIMP. We dont yet support * ROLLOVER. * * SOA: RFC1996 * Check that from is a valid notify source, (zone->masters). * Return DNS_R_REFUSED if not. * * If the notify message contains a serial number check it * against the zones serial and return if <= current serial * * If a refresh check is progress, if so just record the * fact we received a NOTIFY and from where and return. * We will perform a new refresh check when the current one * completes. Return ISC_R_SUCCESS. * * Otherwise initiate a refresh check using from as the * first address to check. Return ISC_R_SUCCESS. */ isc_sockaddr_format(from, fromtext, sizeof(fromtext));

注释解释的很明确:

  • 如果接收到NOTIFY的zone正在更新中,这次的NOTIFY会被记录下来,等正在进行的更新完成之后,再进行由NOTIFY触发的更新检查(SOA 查询)。
  • 如果接收到NOTIFY的zone并没有在更新,使用NOTIFY的源IP做为master进行后续的更新流程。

注释是不是和代码实现一致呢?我们继续往下看:

/* * If we got this far and there was a refresh in progress just * let it complete. Record where we got the notify from so we * can perform a refresh check when the current one completes */ if (DNS_ZONE_FLAG(zone, DNS_ZONEFLG_REFRESH)) { DNS_ZONE_SETFLAG(zone, DNS_ZONEFLG_NEEDREFRESH); zone->notifyfrom = *from; // 记录NOTIFY的源地址 UNLOCK_ZONE(zone); if (have_serial) dns_zone_log(zone, ISC_LOG_INFO, "notify from %s: serial %u: refresh in " "progress, refresh check queued", fromtext, serial); else dns_zone_log(zone, ISC_LOG_INFO, "notify from %s: refresh in progress, " "refresh check queued", fromtext); return (ISC_R_SUCCESS); } if (have_serial) dns_zone_log(zone, ISC_LOG_INFO, "notify from %s: serial %u", fromtext, serial); else dns_zone_log(zone, ISC_LOG_INFO, "notify from %s: no serial", fromtext); zone->notifyfrom = *from; // 记录NOTIFY的源地址 UNLOCK_ZONE(zone);

能够清楚地看到,在收到NOTIFY之后,无论该zone是否正在更新(由状态DNS_ZONEFLG_REFRESH标识),NOTIFY的源地址均被记录下来了,接下来想必一定会直接向notifyfrom发起SOA查询吧,继续往下看:

---- zone->notifyfrom Matches (2 in 1 files) ----Zone.c (libdns): zone->notifyfrom = *from;Zone.c (libdns): zone->notifyfrom = *from;

然而代码跟预期相反,notifyfrom只有上面两处赋值的地方,并没有调用的地方。也就是说NOTIFY的源地址被记下来但是并没有地方使用。为了印证这个推论,继续看选master查询SOA的代码:

zone->curmaster = 0; // 查询SOA前初始化master下标为0 for (j = 0; j < zone->masterscnt; j++) zone->mastersok[j] = ISC_FALSE; // 查询SOA前初始化所有master的masterok flag为False /* initiate soa query */ queue_soa_query(zone);again: result = create_query(zone, dns_rdatatype_soa, &message); if (result != ISC_R_SUCCESS) goto cleanup; INSIST(zone->masterscnt > 0); INSIST(zone->curmaster < zone->masterscnt); zone->masteraddr = zone->masters[zone->curmaster]; // 使用curmaster作为masters数组下标选择master isc_netaddr_fromsockaddr(&masterip, &zone->masteraddr);

zone->masters是master地址的数组,zone->curmaster指定选取master的下标,curmaster的初始值是0,也就是说BIND并没有直接向NOTIFY的源发起SOA查询,而是按照顺序遍历masters选主。

为了进一步验证上述结论,我们在测试环境模拟上述场景。

辅伺服器配置多主,只有第二台主给辅发送NOTIFY,验证辅向哪台主发起SOA查询,进而发起更新请求。

实操验证下来可以说实锤了。。。第二台主向辅发送了NOTIFY,按照设计应该直接向第二台主查询SOA进而发起更新请求,然后BIND却按照masters的配置顺序向第一台主请求SOA并更新。所以我们可以得出结论:BIND做辅配多主的情况下,收到某个主发的NOTIFY的时候并没有直接向NOTIFY的源地址查询SOA和请求更新,而是按照masters配置顺序挨个请求。这样违背了RFC标准,更是违背了自己的代码设计。。。简单分析一下,能够成功发送NOTIFY报文的主相比于其他的master,其可用的概率更大,如果还是只按照masters配置顺序挨个尝试,倘若前面配置的主都不可达,那么需要换主直到可用的主,势必会影响数据更新的效率。再有所有的更新压力都集中在第一个master,多主架构的负载均衡实际是没有效果的,第一个master的负载过大也会影响更新效率。

Q2: BIND做辅配多主的情况下,需要更新,选到的一个主不通,换主是否会有延时,延时是多少?

还是照例先看代码:

zone->curmaster = 0; // 查询SOA前初始化master下标为0 for (j = 0; j < zone->masterscnt; j++) zone->mastersok[j] = ISC_FALSE; // 查询SOA前初始化所有master的masterok flag为False /* initiate soa query */ queue_soa_query(zone); else if (isc_serial_eq(soa.serial, oldserial)) { // master SOA序列号 == slave SOA序列号 isc_time_t expiretime; isc_uint32_t expire; /* * Compute the new expire time based on this response. */ expire = zone->expire; get_edns_expire(zone, msg, &expire); DNS_ZONE_TIME_ADD(&now, expire, &expiretime); /* * Has the expire time improved? */ if (isc_time_compare(&expiretime, &zone->expiretime) > 0) { zone->expiretime = expiretime; if (zone->masterfile != NULL) setmodtime(zone, &expiretime); } DNS_ZONE_JITTER_ADD(&now, zone->refresh, &zone->refreshtime); zone->mastersok[zone->curmaster] = ISC_TRUE; goto next_master; } else { // master SOA序列号 < slave SOA序列号 if (!DNS_ZONE_OPTION(zone, DNS_ZONEOPT_MULTIMASTER)) dns_zone_log(zone, ISC_LOG_INFO, "serial number (%u) " "received from master %s < ours (%u)", soa.serial, master, oldserial); else zone_debuglog(zone, me, 1, "ahead"); zone->mastersok[zone->curmaster] = ISC_TRUE; goto next_master; } /* * Skip to next failed / untried master. */ do { zone->curmaster++; } while (zone->curmaster < zone->masterscnt && zone->mastersok[zone->curmaster]);

每个master的masterok标志位初始值均为False,只有在master网路可达,且master SOA序列号<=slave SOA序列号的时候该master的masterok才会被置为True。再来整理一个mastersok标志位取值代表的意义:

  • mastersok == True:
    • master网路可达,且master SOA序列号==slave SOA序列号
    • master网路可达,且master SOA序列号<slave SOA序列号
  • masterok == False:
    • 未尝试请求SOA的master。
    • 网路不可达的master。
    • master网路可达,且master SOA序列号>slave SOA序列号

masterok == False与字面语义不符,容易造成误解。

如上面换主代码逻辑所示,换master会按照配置顺序选下masterok == False的主,也就是说即便是知道某个master上一次SOA查询网路不可达,本次的更新还是会向该master发起SOA查询。选master也没有按照NS和forward的SRTT优选演算法,而是简单的按照配置顺序和标志位选master。那么一个master不通换下一个master是否有延时,继续看代码:

timeout = 15; if (DNS_ZONE_FLAG(zone, DNS_ZONEFLG_DIALREFRESH)) timeout = 30; // SOA查询TCP超时45秒,UDP超时15秒,UDP retry 0 result = dns_request_createvia4(zone->view->requestmgr, message, &zone->sourceaddr, &zone->masteraddr, dscp, options, key, timeout * 3, timeout, 0, zone->task, refresh_callback, zone, &zone->request); if (result != ISC_R_SUCCESS) { zone_idetach(&dummy); zone_debuglog(zone, me, 1, "dns_request_createvia4() failed: %s", dns_result_totext(result)); goto skip_master; } else { if (isc_sockaddr_pf(&zone->masteraddr) == PF_INET) inc_stats(zone, dns_zonestatscounter_soaoutv4); else inc_stats(zone, dns_zonestatscounter_soaoutv6); }

发送SOA查询的socket的超时默认为15秒,dialup refresh配置下为30秒。UDP查询超时直接使用这个hardcode的值,且UDP查询超时retry设为0,而TCP查询超时设为其3倍。

我们在测试环境模拟master不通换master场景:

辅伺服器配置三个master,按照顺序前两个master均不通,第三个master网路可达,验证辅伺服器在启动后向masters发送SOA并且更新数据的场景。

根据上面的分析,因为EDNS默认打开,可以得出如果一个master不可用,切换到下一个master需要等待:

SOA query with EDNS timeout (15s)+ SOA query without EDNS timeout(15s) = 30s的时间

回过头来再看看对于超时时间的处理:

if (udptimeout == 0 && udpretries != 0) { udptimeout = timeout / (udpretries + 1); if (udptimeout == 0) udptimeout = 1;}

可以看到对于UDP超时重试,和超时时间是留了口子处理的,但是BIND代码在这里做了hardcode,控制起来就不是那么灵活了。对于SOA查询超时的处理有两种方案,短超时+重试或长超时,BIND采用了后者。至于为什么这样选,猜想是为了不给上游master增加太多的查询压力。

Q3: BIND做辅的情况下,请求SOA频率超过了serail-query-rate限制,xfr请求超过了transfers-in限制,会重试吗?

先来看看xfr请求超出transfers-in限制的处理:

static voidqueue_xfrin(dns_zone_t *zone) { const char me[] = "queue_xfrin"; isc_result_t result; dns_zonemgr_t *zmgr = zone->zmgr; ENTER; INSIST(zone->statelist == NULL); RWLOCK(&zmgr->rwlock, isc_rwlocktype_write); ISC_LIST_APPEND(zmgr->waiting_for_xfrin, zone, statelink); LOCK_ZONE(zone); zone->irefs++; UNLOCK_ZONE(zone); zone->statelist = &zmgr->waiting_for_xfrin; result = zmgr_start_xfrin_ifquota(zmgr, zone); RWUNLOCK(&zmgr->rwlock, isc_rwlocktype_write); if (result == ISC_R_QUOTA) { dns_zone_logc(zone, DNS_LOGCATEGORY_XFER_IN, ISC_LOG_INFO, "zone transfer deferred due to quota"); } else if (result != ISC_R_SUCCESS) { dns_zone_logc(zone, DNS_LOGCATEGORY_XFER_IN, ISC_LOG_ERROR, "starting zone transfer: %s", isc_result_totext(result)); }}

不论quota检查成功失败与否,都没有处理返回值,因此推断xfr请求在超出transfers-in限制之后,并没有重试,而是等到refresh到期或者收到master notify之后才发起新的xfr请求。对于查询SOA超过serial-query-rate也是相同的处理。对于不是频繁更新的主辅链路,需要设置transfers-in, serial-query-rate为合适的值,否则会影响更新效率。


推荐阅读:
相关文章