FPs

Bind DLZ With MySQL

前言

要做好運維自動化,內網域名的自動化管理不可少,得提供豐富的API 供其他內部系統調用。
之前在網易杭研實習,見過他們管理內部的域名,還是手工編輯bind 的zone 文件,苦不堪言,改一個得反覆檢查和找人Review,因爲zone 如果出現一絲錯誤,會導致大量域名解析出錯。一個方式是用DNSPod,CloudXNS等,這些服務商一般都有完善的API 支持,但是這樣內網域名就暴露在公網,可被人暴力遍歷,爲滲透之類的提供信息,所以內網自己搭建一個權威DNS 服務器是更好的選擇。

Name server 的選擇上,有很多選擇,如Bind, PowerDNS, NSD 等。PowerDNS 對MySQL 有原生支持, 性能上相對Bind DLZ MySQL 會強很多,並且有非常多開源的WebFrontend 支持。不過我最終還是選擇了Bind DLZ,性能的問題可以通過後文提到的架構解決掉,並且還可以省去對 MySQL 之類的優化工作,另外自己寫一個對數據庫的CRUD 操作的Web 界面並不困難,工作量也並不大。

性能與架構

Bind DLZ 官方提供了一份Performance Tests,雖然是比較老的測試了,但是還是有不少參考價值。
bind-dlz-perf-tests

我自己使用queryperf 壓測,結果與上圖中類似,基本來說Bind DLZ MySQL 相比Bind 原生的方式,QPS 差20倍左右,因爲MySQL 只能跑在單線程下。由於這個原因,建議採用Bind DLZ 作爲Master,Bind 作爲Slave的方式。多個Slave 結合LVS 做高可用和負載均衡, Master可以另外針對Bind DLZ 和MySQL 做高可用,這樣的設計,可以發揮Bind 原生的高性能,也可以利用Bind DLZ 的靈活性。
但是這樣會帶來一個問題,在Master 上修改完記錄之後,可能不會立即同步到Slave,會帶來不一致的問題。不過這個問題可以通過自動發送Notify 來解決掉。
bind-dlz

另外可參考:

DNS 查詢走UDP 協議,前端轉發這塊一般用LVS + Keepalived 之類的做高可用,當然用最近出的Nginx UDP Stream 也可以,不過性能上差很多。
同時也需要優化一下服務器網絡UDP 相關的參數。

pnotify.pl

pnotify - A simple, portable Perl script for sending DNS NOTIFY packets with TSIG support.

使用這個腳本,在每次對域名記錄做更改之後都對幾臺Slave發送一下notify 請求。

/etc/resolv.conf

把內網NS 加入到/etc/resolv.conf 之前需要注意一些事情,一般來說resolv.conf 會有多個nameserver,默認情況下會從上到下發送域名解析請求,當然可以配置成輪詢(options:rotate)。建議是多個ns slave 爲一組,一組有一個lvs 做高可用,將多個lvs的vip 分成多行寫到resolv.conf 中,然後配置options: rotate,開啓輪詢。
另外需要設置一下一次查詢的超時時間,默認是30s。如果某個服務處理過程中涉及到大量的域名查詢,如果resolv.conf 中某一個nameserver 異常,默認的30s 超時將會導致請求大量堆積。建議改小timeout 的值,特別NS 在同一個內網的情況下。
更多配置細節請參考:resolv.conf - resolver configuration file

安裝配置

Bind 的版本選擇上,建議使用官方推薦的stable 版本。DLZ 需要自行編譯安裝,官方的Bind 源碼包裏已帶DLZ 的相關代碼,編譯時開啓對應選項即可。MySQL 的表設計直接參考官網內容,和MySQL_Example
建議設置一下MySQL 的trigger,自動更新SOA 記錄的serial 字段的值。

作爲Slave 的Bind 強烈建議使用包管理系統直接安裝。如果出現安全問題,Slave 是直接對外提供服務的,需要快速修復,直接 aptitude|yum 更新會方便和快速很多(相信上游倉庫打包者的速度)。

另外在投入使用之前,搭建者應該已經閱讀過官方的手冊,BIND 9 Documentation

安裝配置好之後,如果NS 要開放在公網訪問,推薦使用intoDNS 進行檢測,可以發現一些細節問題。另外還可以使用DNS Spy 對安全性做一番檢查。

開發的Web 前端在數據輸入上必須做好校驗,空格和異常字符等要檢測和處理掉,域名需要符合規範(只能包含數字,字母,連接符,點號等,細節可以Google下),不然某條記錄出錯,依然可能導致大量域名無法解析。

安全

  • 關注官方的 Security Advisories,RSS訂閱;
  • 隱藏版本:options 中自定義 version;
  • chroot,另外使用非root 賬戶跑bind 服務;
  • 限制請求,利用ACL 限制查詢來源,如果開放在公網最好關閉遞歸查詢,防止被用於DNS 放大攻擊;
  • 控制好域傳送,配置allow-transfer;
  • 控制好allow-notify;
  • 控制好allow-update;
  • 使用DNSSEC;
  • 等等等。

備份

  • MySQL 備份;
  • Slave 的zone 文件備份,方便快速恢復;
  • 全部域名記錄可以選擇定期備份到DNSPod 或者CloudXN 之類的,以防萬一。

監控

  • Bind 進程監控;
  • Bind 端口監控;
  • Bind 解析功能監控;
  • Bind 各類請求量和響應監控等。

Nagios 有一個開源的插件可以使用:check_bind.sh,不過很老了,可能需要自己改改,使用rndc 這個命令來獲取Bind 的狀態,採點繪圖。
上面的腳本簡單改改,可用於OpenFalcon,Bind9.9 版本:

  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
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/env bash

#check bind status for falcon agent
#fork from https://exchange.nagios.org/directory/Plugins/Network-Protocols/DNS/check_bind-2Esh/details
#author: fangpeishi

ST_OK=0
ST_WR=1
ST_CR=2
ST_UK=3
path_pid=""
name_pid="named.pid"
path_rndc=""
path_stats=""
path_tmp=""
pid_check=1
HOST=`hostname`
DATE=`date +%s`


check_pid() {
    if [ -f "$path_pid/$name_pid" ]
    then
        retval=0
    else
        retval=1
    fi
}

trigger_stats() {
    if [ -n "$path_chroot" ]
    then
        sudo chroot $path_chroot $path_rndc/rndc stats
    else
        sudo $path_rndc/rndc -c /named/etc/rndc.conf stats
    fi
}

copy_to_tmp() {
    tac $path_stats/named_stats.txt | awk '/--- \([0-9]*\)/{p=1} p{print} /\+\+\+ \([0-9]*\)/{p=0;if (count++==1) exit}' > $path_tmp/named.stats.tmp
    }

get_vals() {
    succ_1st=`grep 'resulted in successful answer' $path_tmp/named.stats.tmp | awk '{ print $1 }' | grep -m1 ''`
    succ_2nd=`grep 'resulted in successful answer' $path_tmp/named.stats.tmp | awk '{ print $1 }' | sort -n | grep -m1 ''`
    ref_1st=`grep 'resulted in referral' $path_tmp/named.stats.tmp | awk '{ print $1 }' | grep -m1 ''`
    ref_2nd=`grep 'resulted in referral' $path_tmp/named.stats.tmp | awk '{ print $1 }' | sort -n | grep -m1 ''`
    nxrr_1st=`grep 'resulted in nxrrset' $path_tmp/named.stats.tmp | awk '{ print $1 }' | grep -m1 ''`
    nxrr_2nd=`grep 'resulted in nxrrset' $path_tmp/named.stats.tmp | awk '{ print $1 }' | sort -n | grep -m1 ''`
    nxdom_1st=`grep 'resulted in NXDOMAIN' $path_tmp/named.stats.tmp | awk '{ print $1 }' | grep -m1 ''`
    nxdom_2nd=`grep 'resulted in NXDOMAIN' $path_tmp/named.stats.tmp | awk '{ print $1 }' | sort -n | grep -m1 ''`
    rec_1st=`grep 'caused recursion' $path_tmp/named.stats.tmp | awk '{ print $1 }' | grep -m1 ''`
    rec_2nd=`grep 'caused recursion' $path_tmp/named.stats.tmp | awk '{ print $1 }' | sort -n | grep -m1 ''`
    fail_1st=`grep 'resulted in SERVFAIL' $path_tmp/named.stats.tmp | awk '{ print $1 }' | grep -m1 ''`
    fail_2nd=`grep 'resulted in SERVFAIL' $path_tmp/named.stats.tmp | awk '{ print $1 }' | sort -n | grep -m1 ''`
    dup_1st=`grep 'duplicate queries received' $path_tmp/named.stats.tmp | awk '{ print $1 }' | grep -m1 ''`
    dup_2nd=`grep 'duplicate queries received' $path_tmp/named.stats.tmp | awk '{ print $1 }' | sort -n | grep -m1 ''`

    if [ "$succ_1st" == '' ]
    then
        success=0
    else
        success=`expr $succ_1st - $succ_2nd`
    fi
    if [ "$ref_1st" == '' ]
    then
        referral=0
    else
        referral=`expr $ref_1st - $ref_2nd`
    fi
    if [ "$nxrr_1st" == '' ]
    then
        nxrrset=0
    else
        nxrrset=`expr $nxrr_1st - $nxrr_2nd`
    fi
    if [ "$nxdom_1st" == '' ]
    then
        nxdomain=0
    else
        nxdomain=`expr $nxdom_1st - $nxdom_2nd`
    fi
    if [ "$rec_1st" == '' ]
    then
        recursion=0
    else
        recursion=`expr $rec_1st - $rec_2nd`
    fi
    if [ "$fail_1st" == '' ]
    then
        failure=0
    else
        failure=`expr $fail_1st - $fail_2nd`
    fi
    if [ "$dup_1st" == '' ]
    then
        duplicate=0
    else
        duplicate=`expr $dup_1st - $dup_2nd`
    fi
    if [ "$drop_1st" == '' ]
    then
        dropped=0
    else
        dropped=`expr $drop_1st - $drop_2nd`
    fi
}

falcon() {
    echo -n "{\"metric\": \"$1\", \"endpoint\": \"$HOST\", \"tags\": \"\", "
    echo -n "\"value\": $2,"
    echo -n " \"timestamp\": $DATE, \"counterType\": \"GAUGE\", \"step\": 60}"
}

get_perfdata() {
    echo -n "["
    falcon "caused_recursion" $recursion
    echo -n ","
    falcon "duplicate_queries_received" $duplicate
    echo -n ","
    falcon "failure_responses" $failure
    echo -n ","
    falcon "nxdomain_responses" $nxdomain
    echo -n ","
    falcon "nxrrset_responses" $nxrrset
    echo -n ","
    falcon "referral_responses" $referral
    echo -n ","
    falcon "success_responses" $success
}

if [ ${pid_check} == 1 ]
then
    check_pid
    if [ "$retval" = 1 ]
    then
        echo -n "["
        falcon "check_bind" 1
        echo -n "]"
        exit $ST_CR
    fi
fi

trigger_stats
copy_to_tmp
get_vals
get_perfdata

echo -n ","
falcon "check_bind" 0
echo -n "]"

Exit $ST_OK
2016-05-11 dns bind devops