PowerDNS中DNS解析由各类Backend模块处理。如果解析相关的数据存储于MySQL, Postgres等数据库,Backend需要在这些数据库中查询相应的记录是否存在。查询数据库的性能很低。因而PowerDNS中实现了PacketCache来提高性能。PowerDNS接收到请求后,先在PacketCache中查询是否已经有相应的DNS响应。如果有则直接返回该缓存。否则交由backend处理,处理后再添加到PacketCache中。
PacketCache实现主要位于packetcache.hh和packetcache.cc中。
common_startup.cc中定义了一些全局对象,其中包括一个PacketCache对象PC,PowerDNS中所有线程会共享这个对象。
来看PacketCache的构造函数,它初始化了一个读写锁和一些变量,接着添加了3个统计变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PacketCache::PacketCache() { pthread_rwlock_init(&d_mut, 0 ); d_ttl=-1 ; d_recursivettl=-1 ; S.declare("packetcache-hit" ); S.declare("packetcache-miss" ); S.declare("packetcache-size" ); d_statnumhit=S.getPointer("packetcache-hit" ); d_statnummiss=S.getPointer("packetcache-miss" ); d_statnumentries=S.getPointer("packetcache-size" ); }
PowerDNS有多个线程负责接收请求。线程的简化逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 for (;;) { if (!(P=N->receive(&question))) { continue ; } if (P->couldBeCached() && PC.get(P, &cached)) { NS->send(&cached); continue ; } distributor->question(P, &sendout); }
首先调用NameServer对象的receiver方法接收请求并解析到DNSPacket对象。接着调用DNSPacket::couldBeCached()判断请求是否可以被缓存, 可以看到PowerDNS只缓存Class为IN(Internet)的DNS请求。
1 2 3 4 bool DNSPacket::couldBeCached () { return d_ednsping.empty() && !d_wantsnsid && qclass==QClass::IN; }
如果请求可以被缓存,则调用PC.get方法查询PacketCache中是否有相应缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 int PacketCache::get (DNSPacket *p, DNSPacket *cached) { extern StatBag S; if (d_ttl<0 ) getTTLS(); if (!((++d_ops) % 300000 )) { cleanup(); } ... if (ntohs(p->d.qdcount)!=1 ) return 0 ; string value; bool haveSomething; { TryReadLock l (&d_mut) ; if (!l.gotIt()) { S.inc("deferred-cache-lookup" ); return 0 ; } uint16_t maxReplyLen = p->d_tcp ? 0xffff : p->getMaxReplyLen(); haveSomething=getEntryLocked(p->qdomain, p->qtype, PacketCache::PACKETCACHE, value, -1 , packetMeritsRecursion, maxReplyLen, p->d_dnssecOk, p->hasEDNS()); } if (haveSomething) { (*d_statnumhit)++; if (cached->noparse(value.c_str(), value.size()) < 0 ) { return 0 ; } cached->spoofQuestion(p); return 1 ; } (*d_statnummiss)++; return 0 ; }
get方法在第一次被调用时会调用getTTLS方法获取配置,”cache-ttl”为缓存有效时间。
1 2 3 4 5 6 7 void PacketCache::getTTLS () { d_ttl=::arg().asNum("cache-ttl" ); d_recursivettl=::arg().asNum("recursive-cache-ttl" ); d_doRecursion=::arg().mustDo("recursor" ); }
每进行300000次查询操作(PacketCache::get和PacketCache::getEntry)时,get方法会调用一次cleanup函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 void PacketCache::cleanup () { WriteLock l (&d_mut) ; *d_statnumentries=d_map.size(); unsigned int maxCached=::arg().asNum("max-cache-entries" ); unsigned int toTrim=0 ; unsigned int cacheSize=*d_statnumentries; if (maxCached && cacheSize > maxCached) { toTrim = cacheSize - maxCached; } unsigned int lookAt=0 ; if (toTrim) lookAt=5 *toTrim; else lookAt=cacheSize/10 ; time_t now=time(0 ); DLOG(L<<"Starting cache clean" <<endl ); if (d_map.empty()) return ; typedef cmap_t ::nth_index<1 >::type sequence_t ; sequence_t & sidx=d_map.get<1 >(); unsigned int erased=0 , lookedAt=0 ; for (sequence_t ::iterator i=sidx.begin(); i != sidx.end(); lookedAt++) { if (i->ttd < now) { sidx.erase(i++); erased++; } else ++i; if (toTrim && erased > toTrim) break ; if (lookedAt > lookAt) break ; } *d_statnumentries=d_map.size(); DLOG(L<<"Done with cache clean" <<endl ); }
cleanup函数首先获取”max-cache-entries”选项。如果配置了该选项,并且缓存数目已经超过该选项的值,则应从缓存的map结构中清除一部分过期缓存项。在清除时,搜寻个数为应清除个数的5倍。如果当前缓存个数没有超过“max-cache-entries”, 则搜寻总个数的1/10。
个人感觉,这样实现并不太好,会导致这次请求处理时间过长。可以创建一个独立的线程周期性地清除缓存map中的过期项。
之后,get函数调用getEntryLocked来查找缓存中是否有该请求对应响应结果的缓存,通过map::find实现。
1 2 3 4 5 6 7 8 9 10 11 12 bool PacketCache::getEntryLocked (const string &qname, const QType& qtype, CacheEntryType cet, string & value, int zoneID, bool meritsRecursion, unsigned int maxReplyLen, bool dnssecOK, bool hasEDNS) { uint16_t qt = qtype.getCode(); cmap_t ::const_iterator i=d_map.find(tie(qname, qt, cet, zoneID, meritsRecursion, maxReplyLen, dnssecOK, hasEDNS)); time_t now=time(0 ); bool ret=(i!=d_map.end() && i->ttd > now); if (ret) value = i->value; return ret; }
如果从缓存中找到了响应数据包,则调用DNSPacket::noparse方法将结果保存到需要发回的响应包的相应结构中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int DNSPacket::noparse (const char *mesg, int length) { d_rawpacket.assign(mesg,length); if (length < 12 ) { L << Logger::Warning << "Ignoring packet: too short from " << getRemote() << endl ; return -1 ; } d_wantsnsid=false ; d_ednsping.clear(); d_maxreplylen=512 ; memcpy ((void *)&d,(const void *)d_rawpacket.c_str(),12 ); return 0 ; }
接着调用DNSPacket::spoofQuestion访求将请求包的请求域名部分复制到响应包的请求域名部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void DNSPacket::spoofQuestion (const DNSPacket *qd) { d_wrapped=true ; int labellen; string ::size_type i=sizeof (d); for (;;) { labellen = qd->d_rawpacket[i]; if (!labellen) break ; i++; d_rawpacket.replace(i, labellen, qd->d_rawpacket, i, labellen); i = i + labellen; } }
个人感觉,请求域名部分的复制操作没有必要,缓存中的响应包的域名应该与本次请求包中的域名是相同的。
调用PC.get获取响应后,修改DNS头部的相应标志位及ID后,将响应发送出去。
1 2 3 4 5 cached.d.rd=P->d.rd; cached.d.id=P->d.id; cached.commitD(); N->send(&cached);
下面来看添加缓存的过程。
如果没有找到缓存,PowerDNS交由distributor处理DNS请求。
1 distributor->question(P, &sendout);
这会调用PacketHandler::question函数。question函数会又会调用questionOrRecurse函数。questionOrRecurse函数在一系列检查后,对于CLASS为IN的请求,调用getAuth判断该DNS服务器是否是请求域名的授权服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 bool PacketHandler::getAuth (DNSPacket *p, SOAData *sd, const string &target, int *zoneId) { bool found=false ; string subdomain (target) ; do { if ( B.getSOA( subdomain, *sd, p ) ) { sd->qname = subdomain; if (zoneId) *zoneId = sd->domain_id; if (p->qtype.getCode() == QType::DS && pdns_iequals(subdomain, target)) { found=true ; } else return true ; } } while ( chopOff( subdomain ) ); return found; }
getAuth函数对域名或其子域调用Backend的getSOA函数获得SOA记录,找到则返回。比如,请求解析www.foo.com
,首先查找www.foo.com
是否存在SOA记录。如果没有,接着查找”foo.com”是否存在SOA记录。直到查找部分为””。如果没有找到SOA记录,返回某种错误的响应。接下来,questionOrRecurse函数以QTYPE::ANY
为参数调用Backend的lookup函数,并依次调用B.get
获取找到的记录。
1 2 3 4 5 6 7 8 B.lookup(QType(QType::ANY), target, p, sd.domain_id); rrset.clear(); weDone = weRedirected = weHaveUnauth = 0 ; while (B.get(rr)) { ... rrset.push_back(rr); }
如果没有找到任何记录,则尝试泛解析,处理过程不详述。
1 2 3 4 5 if (rrset.empty()) { if (tryWildcard(p, r, sd, target, wildcard, wereRetargeted, nodata)) { ... } }
如果有CNAME记录,则重新针对CNAME目标域名进行上述逻辑。如果找到符合请求的记录,则添加到响应里发送出去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 if (weRedirected) { BOOST_FOREACH(rr, rrset) { if (rr.qtype.getCode() == QType::CNAME) { r->addRecord(rr); target = rr.content; retargetcount++; goto retargeted; } } } else if (weDone) { bool haveRecords = false ; BOOST_FOREACH(rr, rrset) { if ((p->qtype.getCode() == QType::ANY || rr.qtype == p->qtype) && rr.qtype.getCode() && rr.auth) { r->addRecord(rr); haveRecords = true ; } } if (haveRecords) { if (p->qtype.getCode() == QType::ANY) completeANYRecords(p, r, sd, target); } else makeNOError(p, r, rr.qname, "" , sd, 0 ); goto sendit; }
在发送前,会调用PC.insert将响应添加进PacketCache,实现通过map::insert。
1 PC.insert(p, r, r->getMinTTL());