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 ());