前段日子一直在做公司的DNS調度程序,不過由於性能比較差,方案最終廢棄掉了。兩個半月心血,不想白白浪費掉,於是改了改,把商業秘密相關的部分去掉,變成了一個公共的DNS服務器。其實說的簡單點,就是一個可以做DNS解析和應答的程序(廢話,DNS服務器不就是干這個的)。功能比較簡單,只做了A地址和CNAME的解析,安全性不涉及,性能也沒有測試過,因為本身是個玩具,測性能沒有意義(理論上如果用pypy的話,水平一般的機器也能跑到1萬以上的QPS)。本程序多處借鑒了 isnowfy 同學的程序(相關博客:
http://www.isnowfy.com/introduction-to-gevent/, github:
https://github.com/isnowfy/dns),在此表示敬意。
介紹一下這個程序吧。
首先,服務器的基本思想是開通一個UDP服務器接收請求,等待接收包。如果接收到的包是DNS包,那么進行DNS包的解析,在數據庫中查詢域名,然后構造相應的DNS應答包,最后返回。不過這種方案就是單線程的接收->解析->應答過程,效率比較低。於是我對此進行了改造:接收到的包統一放進一個緩存中,然后,開通多條協程來取數據,進行並行處理。每條協程取一個包進行解析和應答。但是根據經(xiā)驗(cāi)我知道,經常訪問的域名只有那么一部分,同一個域名應該返回的是同一個應答包,那么,對所有包都解析是比較白痴的。因此,我又開了另一個緩存——一個LRU緩存。關於LRU緩存的原理和用法,可以見我之前的博客
http://www.cnblogs.com/anpengapple/p/5565461.html 。這樣,獲取到一個DNS包之后,就可以先在LRU緩存中進行查找,發現查詢過,就直接返回(之前記得替換ID),沒有查詢過再進行解析、應答和存入LRU緩存。
在整個這個過程中,我使用到了:①gevent用來開協程;②gevent.Queue用來當做接收包的緩存隊列;③dnslib庫用來解析DNS包;④pylru庫用來做LRU緩存;⑤僅使用了一個簡單的文本文件作為數據庫。
整體程序流程如下:
# 0、啟動UDP服務。
class DNSServer(object): @staticmethod def start(): # 緩存隊列,收到的請求都先放在這里,然后從這里拿數據處理 DNSServer.deq_cache = Queue(maxsize=deq_size) if deq_size > 0 else Queue() # LRU Cache,使用近期最少使用覆蓋原則 DNSServer.dns_cache = pylru.lrucache(lru_size) # 啟動協程,循環處理緩存隊列 gevent.spawn(_init_cache_queue) # 啟動DNS服務器 print 'Start DNS server at %s:%d\n' % (ip, port) dns_server = SocketServer.UDPServer((ip, port), DNSHandler) dns_server.serve_forever()
# 1、接收請求包,存入緩存隊列。
class DNSHandler(SocketServer.BaseRequestHandler): def handle(self): # 若緩存隊列沒有存滿,把接收到的包放進緩存隊列中(存滿則直接丟棄包) if not DNSServer.deq_cache.full(): # 緩存隊列保存元組:(請求包,請求地址,sock) DNSServer.deq_cache.put((self.request[0], self.client_address, self.request[1]))
# 2、從緩存隊列中取數據。
def _init_cache_queue(): while True: data, addr, sock = DNSServer.deq_cache.get() gevent.spawn(handler, data, addr, sock)
# 3、如果請求是DNS包,解析出其查詢域名。
dns.header.set_qr(dnslib.QR.RESPONSE) qname = dns.q.qname try: dns = dnslib.DNSRecord.parse(data) except Exception as e: print 'Not a DNS packet.\n', e
# 4、判斷是否存在於LRU緩存中。若存在,進行5;否則,進行6。
response = DNSServer.dns_cache.get(qname) if response: # goto 5 else: # goto 6
# 5、獲得LRU緩存中這條DNS的應答數據,將ID替換為本條DNS查詢的ID,然后返回給客戶端。
response[:2] = data[:2]
sock.sendto(response, addr)
# 6、從數據庫中查找這條DNS的應答,封裝成DNS包,存入LRU緩存,然后返回給客戶端。
answers, soa = query(str(qname).rstrip('.')) answer_dns = pack_dns(dns, answers, soa) DNSServer.dns_cache[qname] = answer_dns.pack() sock.sendto(answer_dns.pack(), addr)
反正大概過程就是醬嬸的。我在“數據庫”里面加了幾條數據做實驗(第一條是SOA) :
然后測試:
dig ccc.apple.tree @dns-ip -p dns-port
得到結果,成功解析,嘔液~
有一點需要注意,作為數據庫的文本文件如果是在windows下寫的,拿到linux下用,可能會出現換行符惡心人的問題。需要先使用dos2unix這個工具轉換一下,或者自己寫代碼。具體情況和解決辦法見:
http://www.cnblogs.com/anpengapple/p/5664235.html
這里使用的csv文件僅僅是為了演示方便,
沒有任何性能及安全方面的考慮。改進可以考慮:
第一、在開啟服務器時將內容全部加載到內存,這樣可以去掉LRUCache;
第二、使用redis或mysql之類的數據庫;
第三、注意數據的驗證,例如判斷ip的正則,域名的內容等等。
其實作為一個DNS服務器來講,這個程序欠缺的還很多,只能作為一個模型來參考,或者說一個玩具用來玩。大概就醬吧。本身用python來做DNS服務器就是個笑話。
完整的代碼我放在github上面了,地址:
https://github.com/anpengapple/apple_dns,有興趣的同學可以拿去玩,有意見的同學可以提,反正我是不會改的。吾之懶癌逾重矣。
后記:(1)我司決定放棄powerdns,改投bind的懷抱了。雖然第二季度的績效基本上就泡湯了,但是能用上bind還是極好的。畢竟bind用的人多,就算出問題也能有個地方問問題。而且,powerdns我已經快走投無路了。
(2)最近發現有網站轉載了我的幾篇博客,首先還是很高興的,說明我寫的東西還是比較有用的,得到了別人的認可,但是高興之余覺得有點不對勁,轉載不通知我一聲,連轉載的字樣都沒有出現,這令我有點不滿。所以聲明一下本人博客目前就只有一個,地址在:
http://www.cnblogs.com/anpengapple/ 以后如果開了其他博客或者微信公眾號什么的,我也會在這個博客中告知。
(3)有無聊的同學可以幫我測試一下QPS,記得在數據庫中添加好數據,還有用pypy來跑。測試工具queryperf的使用見:
http://www.cnblogs.com/anpengapple/p/5211557.html,pypy的安裝及使用見:
http://www.cnblogs.com/anpengapple/p/5586678.html
