2011年7月5日 星期二

自製DNS load balance(正確說只有sharing)

前情提要: 因client DNS query是走UDP, 免費套件中比較好的load balance是HAproxy, 但只支援TCP, 所以只好自製. 作法大致上是用pf(pakcet filter)防火牆中的nat+rdr去達成分送後端的功能, 再配合table可動態增刪, 加上自製的一些shell script與一些小軟體, 即可做到DNS load balance server.

所需機器: 一台用作load balance server(簡稱LB), 三台DNS server(簡稱DNS-A, DNS-B, DNS-C), 當然後端要幾台是隨意...

架構: 類似NAT+private ip的網路


















架設步驟:
1. 先把LB的NAT功能用pf防火牆設定好, 在此我們先用IPv4的設定:
LB external IP: 140.113.1.1/24
LB internal IP: 192.168.1.14/24
DNS-A: 192.168.1.1/24
DNS-B: 192.168.1.2/24
DNS-C: 192.168.1.3/24

之後的load balance功能於打造時將會用到pf的table, 因此建議用pf去設定NAT, 若用其他種防火牆的話請不要來信問我怎麼做.
pf.conf 如下:


ext_if="em1"
ext_ip="140.113.1.1"


int_if="em0"
int_ip="192.168.1.14"
int_lan="192.168.1.0/24"


table <dns_servers> persist {192.168.1.1,192.168.1.2,192.168.1.3}


set block-policy return
set skip on lo0

scrub in all

nat on $ext_if from $int_lan to !$int_lan -> $ext_ip

rdr on $ext_if proto udp from any to $ext_ip port 53 -> <dns_servers> port 53

pass all

2. 設定好並確認後端三台DNS server都能正常透過LB的NAT出去後, 再來把後端三台DNS service設定起來, 並確認由LB可以查詢得到後端三台DNS, 此部份若不會請自行找尋架設DNS server的資料, 再此不再著述.

3. 理論上, 這時外部向LB的external ip查詢, 應該已經可以正常回應, 此時即完成第一步, 也就是靜態的load balance, 平均分攤流量給後端三台DNS server.

4.接下來要加上監視後端DNS是否還活著, 若死掉就要從table dns_servers 移除, 反之則加入回來. 我的偵測方式是先用icmp ping, 然後用nsping測試, 故先到/usr/ports/dns/nsping裝nsping起來. 然後因為很可能會受到網路流量過大導致nsping卡住等狀況, 故再到/usr/ports/sysutils/timelimit裝timelimit.

5. 以下是自行撰寫的 icmp_ping.sh

#!/bin/sh
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/usr/X11R6/bin:/root/bin

if [ -z "$1" ];
then
  echo "usage: $0 <IP or hostname>"
  exit
fi

ip=$1

count=3
reply=0
while [ $count -gt 0 ];
do
  feedback=`ping -z 10 -c 1 -t 1 -i0.2 $ip 2>&1 |awk '{if($3=="Operation"){print "retry";exit};if($8=="packet"){if($7=="0.0%"){print "ok";exit}else{print "bad";exit}}}'`


  if [ $feedback = "retry" ];
  then
    continue
  fi

  if [ $feedback = "ok" ];
  then
    reply=`expr $reply + 1`
  fi

  count=`expr $count - 1`
done

if [ $reply -gt 2 ];
then
  echo "ok"
else
  echo "bad"

此範例是去ping看看指定的IP, 會測試3次, 若達2次以上有回應即認定為正常並輸出 "ok", 反之則輸出 "bad". 完成後請自行測試是否能正常運作.


6. 以下是自行撰寫的 ns_ping.sh
#!/bin/sh
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/usr/X11R6/bin:/root/bin

if [ -z "$1" ];
then
  echo "usage: $0 <IP or hostname>"
  exit
fi

ip=$1

timelimit -t 10 -T 20 nsping -c 3 -h www.nctu.edu.tw $ip | awk '{if(NF==18){if($9 >= "2"){print "ok"}else{print "bad"}}}'

其中要用什麼DN/IP做測試請自行修改, 此範例是去解看看 www.nctu.edu.tw, 會嘗試測試3次, 若達2次以上回應即認定為正常並輸出 "ok", 反之則輸出 "bad". 完成後請自行測試是否能正常運作.

7. 接下來是最重要的, 進行偵測並自動調整 table dns_servers, 以下是dns_detect.sh的程式碼:

#!/bin/sh
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/usr/X11R6/bin:/root/bin

lockfile="/tmp/dns_detect.lock"
pwd="/root/bin/dns_detect"
pfcmd="pfctl -t dns_servers -T"

if [ -z "$1" ];
then
  echo "detect dns servers if alive and remove/add to round-robin table"
  echo "usage:$0 <IP1> <IP2> ..."
  exit
fi

if [ -e $lockfile ];
then
  exit
fi

touch $lockfile

while [ "$1" ];
do

  if [ "`$pwd/icmp_ping.sh $1`" = "ok" ];
  then
    if [ "`$pwd/ns_ping.sh $1`" = "ok" ];
    then
      echo "$1 is alive"
      $pfcmd add $1
    else
      echo "$1 icmp ok, but dns query failed"
      $pfcmd delete $1
    fi
  else
    echo "$1 icmp failed"
    $pfcmd delete $1
  fi

  shift
done

rm $lockfile

在寫此程式時考量到可以一次測試所有的back-end DNS server, 故使用 shift 即可將一整排back-end server IPs 以空隔區分, 都當做參數放進來.

因為這個程式執行時可能會持續比較久, 為了避免重複呼叫, 故簡單用個lock file機制避免重複執行. 而此程式主要動作就是: 1. 進行icmp ping看看, 不通就直接跳去刪除該IP的動作, 通的話進行下一步驟. 2.進行ns query測試, 若不通也跳去刪IP動作, 若ok就加入該IP.

整個程式動作就是簡單說就是先ping看看指定的IP, 有反應再測試query, 都pass就把pf的table dns_servers加入該IP, 反之若任何一項測試不過就刪除該IP. 因pf會檢查是否重複, 故不用擔心重複加入或刪除.

8. 什麼? 還有? 是的, crontab只能設定到分鐘, 也就是若不幸真的有故障的back-end DNS server, 最遭的狀況會要一分鐘才會知道這件事. 為了讓偵測頻繁點, 故另寫一個簡單的shell script, 在一分鐘內間隔小一點, 重複檢查幾次再結束.
以下為dns_detect_loop.sh的程式碼:

#!/bin/sh
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/usr/X11R6/bin:/root/bin

dns_servers="192.168.1.1 192.168.1.2 192.168.1.3"
lockfile="/tmp/dns_detect_loop.lock"
pwd="/root/bin/dns_detect"
sleeptime="20"

if [ -z "$1" ];
then
  echo "detect dns servers per 10 seconds"
  echo "usage:$0 <echo|batch>"
  exit
fi

if [ -e $lockfile ];
then
  exit
fi

touch $lockfile

timesleft=2
while [ $timesleft -gt 0 ];
do

  if [ $1 = "echo" ];
  then
    $pwd/dns_detect.sh $dns_servers
  else
    $pwd/dns_detect.sh $dns_servers > /dev/null 2>&1
  fi

  timesleft=`expr $timesleft - 1`
  if [ $timesleft -gt 0 ];
  then
    sleep $sleeptime
  fi

done

rm $lockfile

簡單說就是每次測試2次, 中間暫停20秒, 這樣可以縮短到30秒以內一定偵測到故障發生, 或故障已排除. 可自行調整暫停的時間與要重複幾次, 並請做測試, 要在60秒內完成, 以免延誤到下一分鐘的偵測.


大致上就這樣, 應該可以讓你的DNS負荷分攤掉, 並於單一一台故障停機時不會造成任何問題.

沒有留言:

張貼留言