fangpsh's blog

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 版本:

#!/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