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,开启轮询。
另外需要设置一下一次查询的超时时间,默认是5s。如果某个服务处理过程中涉及到大量的域名查询,如果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