DNS 壓力測試

2010-01-01

網域名稱服務Domain Name Service〉效率決定網路服務器名稱與 IP 解析之速度,如果網域名稱服務效率差,使得用戶在使用網路應用系統時,會因過多等待而放棄瀏覽,進而影響企業整個電子商務之推廣。

因此建置一套高效率網域名稱服務便顯得重要,然而在選擇網域名稱服務系統前,應該針對該系統實施壓力測試,據以選擇適當網域名稱服務系統。大型電子商務企業,為服務更多用戶其網域名稱服務器是成群集式架構如下:

  • 網路負載平衡器:為群集式網路服務器,利用演算法將用戶網路服務請求,分配給群集服務器之一,避免多個用戶集中請求某一台服務器,造成負載過重不均。為簡化用戶請求網路負載平衡器提供公共 IP 給服務請求者,網路負載平衡器接受用戶請求後再分配給適當服務器提供網路服務。

    網路負載平衡器

  • DNS 緩存器〈DNS Cache〉:大多數網路服務系統其網域名稱與 IP 不甚變動,每當用戶提出名稱解析服務,就由網域名稱服務器自其資料庫查找,再將 IP 解析資訊傳回用戶。
    此種架構當請求量大時,服務便易被拖垮。因此需要建置 DNS 緩存器,將已解析過資訊加以緩存,當其他用戶已請求解析過之資訊,直接傳回用戶而無需再向網域名稱服務器請求解析,大大地降低網域名稱服務器負載。

    DNS 緩存器

Python 是一種物件導向直譯式電腦語言,語法簡明易懂,並提供豐富程式庫,使得 Python 應用系統更多樣性,選用 Python 做為撰寫壓力測試程式語言。
在測試網域名稱服務系統前,需要先下載大量測試資料

測試資料檔內容範例如下:


    www.ibm.com.              A
    www.hinet.net.            A
    168.95.1.1.in-addr.apra.  PTR

由於測試資料檔過大,為有效實施壓力測試,找出現有網域名稱服務能力,故拆分為每檔 10,000 筆測試資料,存放於各壓力測試器中。
在 *NIX 作業系統中,執行 split -l 10000 dns 拆分原測試資料檔成許多 dns 起頭之檔案群。

在原測試資料檔中存在許多錯誤資料,必須先撰寫過濾程式將錯誤筆錄剔除,過濾程式如下:

過濾程式
#include <iostream>
#include <fstream>
#include <string>
#include <vector>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

int GetIP(char *dnsRecord, std::vector<std::string> *ipList) {
    struct hostent *host;

    if ((host = gethostbyname(dnsRecord)) == NULL)
        return (-1);

    for (int i = 0;; i++) {
        struct in_addr ip;

        if (host->h_addr_list[i] == NULL) break;

        ip.s_addr = *(ulong *) host->h_addr_list[i];
        ipList->push_back(std::string(inet_ntoa(ip)));
    }

    return (0);
}

int GetName(char *ip, std::string &hostName) {
    struct sockaddr_in iip;
    struct hostent *host;

    inet_aton(ip, &iip.sin_addr);

    if ((host = gethostbyaddr((char *) & iip.sin_addr, 4, AF_INET)) == NULL)
        return (-1);

    hostName.append(host->h_name);

    return (0);
}

int main(int argc, char** argv) {
    std::string dnsFile = "";

    if (argc == 1) return (1);

    dnsFile = argv[1];

    std::ifstream dnsFileStream;
    std::string dnsRecord;
    char name_ip[64], flag[8];

    dnsFileStream.open(dnsFile.c_str(), std::ios::in);

    if (dnsFileStream.is_open()) {
        while (!dnsFileStream.eof()) {
            std::getline(dnsFileStream, dnsRecord);

            memset(name_ip, 0, sizeof (name_ip));
            memset(flag, 0, sizeof (flag));

            sscanf(dnsRecord.c_str(), "%s %s", name_ip, flag);
            if (name_ip[strlen(name_ip) - 1] == '.') name_ip[strlen(name_ip) - 1] = 0;

            if (!strcmp(flag, "A")) {
                std::vector<std::string> ipList;

                if (!GetIP(name_ip, &ipList)) {
                    std::cout << dnsRecord << std::endl;
                }
            } else if (!strcmp(flag, "PTR")) {
                std::string hostName = "";

                char *mark = strstr(name_ip, ".in-addr.arpa");
                if (mark != NULL) {
                    *mark = 0;
                }

                if (!GetName(name_ip, hostName)) {
                    std::cout << dnsRecord << std::endl;
                }
            }
        }

        dnsFileStream.close();
    }

    return (0);
}
編譯g++ -o DNSFilter dnsFilter.cpp
有了過濾程式後,撰寫一個簡單命令稿,分別過濾所有測試資料小檔,內容如下:

    # 找出所有測試資料小檔,過濾後產生新檔,其附檔名為 .dat
    for dnsf in dns* ; do
        ./DNSFilter $dnsf > $dnsf.dat &
    done

為找出 DNS 整體機制瓶頸,撰寫一個簡單命令稿〈run.sh〉,記錄測試單一資料檔耗時,內容如下:


    start=`date +%H%M%S`
    # 使用說明:dnsperf -h 
    dnsperf -d $1
    stop=`date +%H%M%S`
    echo $1:$stop-$start

每 10,000 筆收集 run.sh {測試資料小檔} 輸出至一個文字檔,編輯成內容如下:


    10K 140204 140200
    20K 140234 140230
    30K 140304 140300
    40K 140334 140330
    ...

將此文字檔匯入至 Microsoft Excel 再繪製成折線圖,觀察 DNS 整體機制效能。
在壓力測試過程中,要隨時觀察測試機資源狀態〈top〉,如果已達滿載則不需要再測試。

最後撰寫一個簡單命令稿,〈runAll.sh〉,記錄測試所有資料檔耗時,內容如下:


    for dnsf in dns*.dat ; do
         sh run.sh $dnsf &
    done

收集 runAll.sh 輸出至一個文字檔〈如前述〉,再將此文字檔匯入至 Microsoft Excel 再繪製成折線圖,觀察 DNS 整體機制效能。

每萬筆每秒查詢數
每萬筆查詢耗時增率

如果不使用 dnsperf 也可以使用 Python 撰寫各式壓力測試程式如下:

讀取標準輸入管道解析
#!/usr/bin/python

import os
import re
import time
import sys
import string
import dns.resolver
import fileinput 

def main():
    addressType = 'A'
    if len(sys.argv) > 1:
       dnsFile = sys.argv[1:][0]
    startTime = time.clock()
    for line in sys.stdin.readlines():
        myLine = line.rstrip()
        myParams = myLine.split()
        myDomain = myParams[0].rstrip('.')
        # 過濾 網域名稱 錯誤資料
        if myDomain.find('.') == -1:
           continue
        # 過濾 網域名稱反解 資料
        if myDomain.endswith('.in-addr.arpa'):
           continue
        try:
            myResolved = dns.resolver.query(myDomain, addressType)
            for myIPObject in myResolved:
                myIP = str(myIPObject)
                print "%s.\t%s\t%s" % (myDomain, addressType, myIP)
        except:
            myIP = '0.0.0.0'
    stopTime = time.clock()
if __name__ == "__main__":
    sys.exit(main())
讀取網域資料檔解析
#!/usr/bin/python

import os
import re
import time
import sys
import string
import dns.resolver
import fileinput

def main():
    addressType = 'A'
    dnsFile = 'dnsTest.txt'
    if len(sys.argv) > 1:
       dnsFile = sys.argv[1:][0]
    startTime = time.clock() 
    for line in fileinput.input(dnsFile):
        myLine = line.rstrip()
        myParams = myLine.split()
        myDomain = myParams[0].rstrip('.') 
        # 過濾 網域名稱 錯誤資料
        if myDomain.find('.') == -1:
           continue
        # 過濾 網域名稱反解 資料
        if myDomain.endswith('.in-addr.arpa'):
           continue
        try:
            myResolved = dns.resolver.query(myDomain, addressType)
            for myIPObject in myResolved:
                myIP = str(myIPObject)
                print "%s.\t%s\t%s" % (myDomain, addressType, myIP)
        except:
            myIP = '0.0.0.0'
    stopTime = time.clock()
if __name__ == "__main__":
    sys.exit(main())
讀取網域資料檔多執行緒解析
#!/usr/bin/python

import os
import re
import time
import sys
import string
from threading import Thread
import dns.resolver
import fileinput

class dnsBulkResolve(Thread):
def __init__ (self, domainList, addressType, numOfBulk):
Thread.__init__(self)
self.domainList = domainList
self.addressType = addressType
self.numOfBulk = numOfBulk
self.timeUsed = 0
self.result = []
def run(self):
startTime = time.clock()
for
domain in self.domainList:
try
:
answers = dns.resolver.query(domain, self.addressType)
for
rdata in answers:
myIP = str(rdata)
resolved = (domain, myIP, self.addressType)
self.result.append(resolved)
except:
resolved = (domain, '0.0.0.0', self.addressType)
self.result.append(resolved)
stopTime = time.clock()
self.timeUsed = stopTime - startTime
def doTask(domainList, addressType, threadList, numOfBulk):
current = dnsBulkResolve(domainList, addressType, numOfBulk)
threadList.append(current)
current.start()
def main():
dnsFile = 'dnsTest.txt'
numRecordPerThread = 2
addressType = 'A'
if
len(sys.argv) > 1:
dnsFile = sys.argv[1:][0]
if
len(sys.argv) > 2:
numRecordPerThread = string.atoi(sys.argv[2:][0])
if
len(sys.argv) > 3:
sqliteDbName = sys.argv[3:][0]
domainList = []
threadList = []
numOfRecord = 0
numOfBulk = 0
startTime = time.clock()
for
line in fileinput.input(dnsFile):
myLine = line.rstrip()
myParams = myLine.split()
myDomain = myParams[0].rstrip('.')
# 過濾 網域名稱 錯誤資料
if myDomain.find('.') == -1:
continue

# 過濾 網域名稱反解 資料
if myDomain.endswith('.in-addr.arpa'):
continue
if
numOfRecord % numRecordPerThread == 0:
domainList.append([])
domainList[numOfBulk].append(myDomain)
numOfRecord = numOfRecord + 1
if
numOfRecord % numRecordPerThread == 0:
doTask(domainList[numOfBulk], addressType, threadList, numOfBulk)
print ">>Task-%05d Initiated" % (numOfBulk)
numOfBulk = numOfBulk + 1
if
numOfRecord % numRecordPerThread > 0:
doTask(domainList[numOfBulk], addressType, threadList, numOfBulk)
del domainList
totalTimeUsed = 0
for
myThread in threadList:
myThread.join()
justOnce = 0
for
(myDomain, myIP, myAddressType) in myThread.result:
if
myIP == '0.0.0.0':
continue
if
justOnce == 0:
print "%05d\t%05ld -" % (myThread.numOfBulk, myThread.timeUsed)
justOnce = 1
print "\t%32s\t%-17s" % (myDomain, myIP)
totalTimeUsed = totalTimeUsed + myThread.timeUsed
del threadList
print "(Total) %ld" % (totalTimeUsed)
if
__name__ == "__main__":
sys.exit(main())