Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

インフラ

Sisimaiによるバウンスメール解析の自動化

こんにちは。管理チームのいとうです。
私は主にインフラの運用管理を担当しています。

今回は弊社でも利用している、"Sisimai" によるバウンスメール解析についてご紹介します。

大規模なメール配信を行っていると、ユーザのメールアドレス登録間違いや、メールアドレス変更の未通知などでバウンスメールが度々発生します。
少数であれば管理者がバウンスメールを確認し、対応を手動で行えばよいかもしれませんが、それが数十~数百アカウントともなれば、到底作業が追いつくはずもありません。
そこで、バウンスメール解析とその後の処理を自動化することで、管理者の負担を減らすことができると考えられます。

本記事では、まず一般的なメールシステムにSisimaiを利用した簡易的なバウンスメール解析アプリケーションを組み込むことで、バウンスメールの情報を扱いやすい形で集約するところまでを目標とします。
また、幾つかのパターンでバウンス解析を行い、結果がどのように出てくるのかについても検証したいと思います。

Sisimaiについて

Sisimaiは株式会社キュービックルート様が開発している、オープンソースのバウンスメール解析用ライブラリです。

製品URL: libsisimai.org

2016年9月8日現在の最新版はv4.18.0です。
mbox形式もしくはMaildir形式のメールを読込み、バウンスメールの内容を解析して出力することができます。
Ruby版とPerl版がリリースされていますが、今回の記事ではRuby版を使用しています。

Ruby版の環境条件

公式サイトにも記載されている通り、Ruby2.1以降もしくはJRuby9.0.4.0以降が必要です。
また、JSON出力用にojが依存ライブラリとしてインストールされます。

余談ですが、以前はbounceHammerという、バウンスメール解析ライブラリと管理用UIがセットになったプロダクトが存在していたのですが、こちらはEOLとなっています(2016年9月現在新規ダウンロードもできないようです)。

Sisimaiの導入

それでは早速Sisimaiを導入していきましょう。
今回は以下の環境で検証を行いました。

検証環境

  • CentOS 6.8
  • Ruby 2.3.1 p112
  • jq (必須ではありませんが、JSON形式の出力を見やすく整形するために利用します。)

  • 導入手順

    jqはCentOSの標準リポジトリには存在しないため、EPELから入手します。
    # yum -y install epel-release
    # yum -y install jq
    

    次にSisimaiを導入します。
    # gem install sisimai
    

    動作確認

    sisimaiの導入が完了したので、早速動作を確認してみましょう。
    sisimaiのgemが導入されたディレクトリ内にset-of-emailsというディレクトリがあります。
    こちらにテスト用に利用できるメールが格納されているので、利用します。
    以下に示すように簡単なワンライナーで、バウンスメールの解析を行うことができます。
    出力される内容は、Sisimaiのデータ構造に詳しく説明されていますので、そちらを参照ください。

    # ruby -r 'sisimai' -e 'puts Sisimai.dump($*.shift)' "set-of-emails/maildir/bsd/x1-01.eml" | jq .
    [
    {
      "timestamp": 1272551685,
      "recipient": "kijitora@example.co.jp",
      "addresser": "shironeko@example.jp",
      "timezoneoffset": "+0900",
      "deliverystatus": "5.0.910",
      "diagnostictype": "",
      "diagnosticcode": "User unknown",
      "subject": "Nyaaaaan",
      "action": "",
      "reason": "filtered",
      "listid": "",
      "alias": "",
      "rhost": "",
      "lhost": "",
      "token": "40c0e0d40e8d1192fb7e4a8e52eee2725fd91671",
      "messageid": "000000000000000000000000000000000000@example.jp",
      "replycode": "",
      "smtpagent": "X1",
      "softbounce": 1,
      "smtpcommand": "",
      "destination": "example.co.jp",
      "senderdomain": "example.jp",
      "feedbacktype": ""
    }
    ]
    

    jqを通すことで、JSONが整形されるのでわかりやすいですね。
    これでSisimaiを利用する環境の準備はOKです。

    バウンスメール解析用プログラムを作る

    それではsisimaiを利用して、バウンスメール解析を自動化するプログラムを作成してみます。
    このシステムでは、解析したデータをデータベースに蓄積していく仕組みを取るようにします。
    今回は、手頃なところでMySQLを利用することにしました。Sequelというgemを利用することで、
    簡単にテーブルにinsertを行えるので、こちらを利用します。
    追加でsequelとmysql2のgemをインストールしておきましょう。
    また、MySQLに以下のような定義のテーブルを持つスキーマを作成しておいてください。

    スキーマ名 sisimai
    テーブル名 bouncemails

    カラム名データ型制約
    idINTPRIMARY KEY, AUTO_INCREMENT
    timestampINT
    recipientVARCHAR(255)
    addresserVARCHAR(255)
    timezone_offsetVARCHAR(32)
    delivery_statusVARCHAR(32)
    diagnostic_typeTEXT
    diagnostic_codeTEXT
    subjectTEXT
    actionVARCHAR(32)
    reasonVARCHAR(32)
    listidVARCHAR(255)
    aliasVARCHAR(255)
    rhostVARCHAR(255)
    lhostVARCHAR(255)
    tokenVARCHAR(255)
    message_idVARCHAR(255)
    reply_codeVARCHAR(32)
    smtp_agentVARCHAR(32)
    softbounceINT
    smtp_commandVARCHAR(32)
    destinationVARCHAR(255)
    sender_domainVARCHAR(32)
    feedback_typeVARCHAR(255)

    次に検証に利用するプログラムを以下に示します。検証用のため、エラー処理などは省いています。

    バウンスメール取り込み用プログラム

    メールボックスのあるサーバに解析プログラムを配置し、直接メールを取り込んでも良いのですが、今回はリモートのサーバからPOP3でアクセスし、メールを取得することにしました。
    以下のプログラムでバウンスメール解析用サーバの適当なディレクトリにメールファイルをダウンロードしてきます。

    bounce-receiver.rb
    # coding: utf-8
    require 'net/pop'
    
    basedir = '/var/apps/bounce-parser'
    mailstore = basedir + '/mailstore'
    
    host = 'mail.example.com'
    port = 110
    user = 'bounce'
    password = 'bounce'
    
    # Connect to pop3 server
    pop = Net::POP3.new(host, port)
    pop.start(user, password)
    
    # Receive mails
    unless pop.mails.empty?
      pop.mails.each do |mail|
        filename = "#{mailstore}/" + Time.now.strftime('%Y%m%d%H%M%S%L') + ".eml"
        fd = File.open(filename, 'w')
        fd.write mail.pop
        mail.delete
        sleep 1
      end
    end
    
    pop.finish
    

    次に、Sisimaiでメールの解析を行うプログラムのサンプルです。
    先ほどダウンロードしたメールのディレクトリを指定することで、ディレクトリ内に存在するメールを一括で解析し、結果をSisimai::Dataオブジェクトに変換してくれます。
    このオブジェクトに対しdamnメソッドを実行すると、hash形式で結果を取り出すことができます。
    取り出したデータはsequelのinsertメソッドを利用し、データベースに投入されます。

    bounce-parser.rb
    # coding: utf-8
    require 'sisimai'
    require 'sequel'
    require 'mysql2'
    
    basedir = '/var/apps/bounce-parser'
    mailstore = basedir + '/mailstore'
    database = 'bounce'
    host = 'localhost'
    port = 3306
    user = 'bounce'
    password = 'bounce'
    encoding = 'utf8'
    
    parsed_records = []
    
    bounce_mails = Sisimai.make(mailstore)
    
    unless bounce_mails.nil?
      bounce_mails.each do |bounce_mail|
        parsed_records << bounce_mail.damn
      end
    else
      puts "mail not found"
      exit(0)
    end
    
    DB = Sequel.mysql2(
           :database => database,
           :host     => host,
           :port     => port,
           :user     => user,
           :password => password,
           :encoding => encoding
         )
    
    unless parsed_records.nil?
      parsed_records.each do |record|
        DB[:mail_logs].insert(
          :timestamp       => record['timestamp'],
          :recipient       => record['recipient'],
          :addresser       => record['addresser'],
          :timezone_offset => record['timezoneoffset'],
          :delivery_status => record['deliverystatus'],
          :diagnostic_type => record['diagnostictype'],
          :diagnostic_code => record['diagnosticcode'],
          :subject         => record['subject'],
          :action          => record['action'],
          :reason          => record['reason'],
          :listid          => record['listid'],
          :alias           => record['alias'],
          :rhost           => record['rhost'],
          :lhost           => record['lhost'],
          :token           => record['token'],
          :message_id      => record['messageid'],
          :reply_code      => record['replycode'],
          :smtp_agent      => record['smtpagent'],
          :softbounce      => record['softbounce'],
          :smtp_command    => record['smtpcommand'],
          :destination     => record['destination'],
          :sender_domain   => record['senderdomain'],
          :feedback_type   => record['feedbacktype']
        )
      end
    end
    

    これで結果の解析からデータの集約まで行うことができました。
    2つの簡易なプログラムで一通りの作業を行えるので楽ですね。
    次のセクションでは、実際にメールシステムに組み込んで検証を行ってみたいと思います。

    メールシステムに組み込んで検証してみる


    よくあるシステム構成パターンに組み込んで動作を確認してみたいと思います。
    今回は検証用に以下のようなメールシステムを構築してみました。
    LAN内にクローズなメール環境を構築したため、他にもアクターは存在しますが、単純化するため一部を省略しています。
    overview
     
    次にサーバの役割をドメイン毎に軽く説明します。

    example.comのメールシステム

    こちらがメール送信元のメールシステムになります。
    メールゲートウェイサーバがメールボックスも受け持つ、シンプルな構成です。
    また、バウンスメール解析用のプログラムを配置したサーバを用意しています。

    メールゲートウェイサーバ
  • postfix
  • dovecot

  • バウンスメール解析サーバ
  • bounce-receiver.rb (pop受信プログラム)
  • bounce-parser.rb (解析プログラム)

  • example.co.jpのメールシステム

    こちらが送信されたメールの宛先となるシステムです。
    外部との接点になるゲートウェイサーバと配信用サーバは別に分かれているという想定です。

    メールゲートウェイサーバ
  • postfix
  • メール配信サーバ
  • postfix
  • dovecot

  • 正常にメールが配信される場合

    まずはメールが正常に配信される場合を見てみましょう。
    delivered

    1. example.comのGWサーバはexample.co.jpのGWサーバにメールをリレーします。
    2. メールを受け取ったexample.co.jpのGWサーバは、自ドメイン宛のメールなので、メール配信サーバへとリレーを行います。
    3. メール配信サーバはGWサーバからのSMTP接続を受け付け、対象のアカウントが存在することを確認し、メールをローカルのメールボックスへ配送します。

    メールがバウンスとなる場合

    次にメールがバウンスとなる場合を見てみましょう。
    メールがバウンスになるパターンは様々ですが、ここでは宛先アカウントが存在しない場合のバウンスメールの流れを例に取ります。
    bounce

    1. example.comのGWサーバはexample.co.jpのGWサーバにメールをリレーする。
    2. メールを受け取ったexample.co.jpのGWサーバは、自ドメイン宛のメールなので、メール配信サーバへリレーしようとする。
    3. 配信サーバはexample.co.jpのGWサーバのSMTP接続を受け付けるが、対象のアカウントが存在しないので、エラーを通知する。
    4. example.co.jpのGWサーバは元メールのEnvelope From宛にバウンスメールを送信する。
    5. example.comのGWサーバはメールを受け取り、対象アカウントのメールボックスにストアする。
    6. バウンスメール解析サーバは、定期的にバウンスメール受信用アカウントのメールボックスをチェックし、取り込みと解析を行う。
    流れがやや複雑ですが、このようにして送信したメールがバウンスメールとなって戻ってきます。

    パターン別でバウンス解析結果を検証する


    今回は以下の3パターンで返されたバウンスメールに対し、Sisimaiの解析結果がどう変化するかを検証してみます。

    パターン1: ハードバウンス
    宛先となるアカウントが存在しなかったり宛先のドメインがないなど、
    恒久的な理由でメール配送されず戻ってきたパターンです。

    パターン2: ソフトバウンス
    宛先となるアカウントは存在するがメールボックスの容量が超過しているため受信できないなど、
    一時的な理由でメールが配送できず戻ってきたパターンです。

    パターン3: ML配信におけるバウンス
    1対1のパターンだけでなく、メーリングリストにメールを送信するパターンも確認してみます。
    今回はML内の一部のアカウントが存在しないという想定で試してみます。

    1. ハードバウンス

    ますはじめにハードバウンスのパターンを確認してみます。
    以下のように、存在しない宛先に向けてメールを送信してみます。

  • 送信元: test@example.com
  • 宛先: john@example.co.jp(実際には存在しない宛先)

  • このパターンでは宛先john@example.co.jpが存在しないので、ハードバウンスとなってメールが返ってくると想定されます。
    では、解析プログラムを通した結果をDBから参照してみましょう。

    pattern1

    結果のカラム数が多いため5行に分割していますがご了承ください。
    バウンスメールに関して、かなり詳細な情報が取得できているようですね。
    この結果を元に、バウンスメールの内容をざっくりと解析してみたいと思います。
    1. addresserとrecipientの内容から、test@example.comが送信元でjohn@example.co.jpが宛先だということがわかります。
    2. reply_codeおよびdelivery_statusが550/5.1.1ということから、「宛先ユーザアカウントが正しくないためメール送信に失敗した」ということがわかります。
    3. softbounceの値は0(ハードバウンス), 1(ソフトバウンス), -1(不明)の3値を取りますが、ここでは"0"となっているので、ハードバウンスであることがわかります。
    4. rhost, lhostに着目するとバウンス発生時にSMTP通信を行っていたリモートホストとローカルホストが分かるので、メールの経路上どこでバウンスになってしまったかがわかります。
    確かに宛先ユーザが存在しないため、ハードバウンスになって返ってきていることがわかりますね。

    2. ソフトバウンス

    次にソフトバウンスのパターンを検証してみます。
    以下に示す宛先に対し、メール配信サーバの受信可能サイズを制限して、メールを送信してみます。

  • 送信元: test@example.com
  • 宛先: paul@example.co.jp

  • このパターンでは、宛先paul@example.co.jpは存在しますが、メール配信サーバの受信サイズ制限に引っかかってしまいます。
    ただ、メールサイズを小さくすれば正常に配信が可能ですので、ソフトバウンスとして返ってくることが想定されます。
    では、解析プログラムの結果を確認しましょう。

    pattern2

    1. addresserとrecipientの内容から、test@example.comが送信元でjohn@example.co.jpが宛先だということがわかります。
    2. reply_codeはNULL(値が取得できなかった)ですが、delivery_statusが5.3.4でありdiagnostic_codeの'message size xxxx exceeds size limit xxx of server ...'の記述から、宛先メールサーバのサイズ制限に引っかかっていることがわかります。
    3. softbounceの値が"1"となっているので、ソフトバウンスであることがわかります。

    3. ML配信におけるバウンス

    最後にメーリングリストにあててメールを送信したパターンを確認します。

  • 送信元: test@example.com
  • 宛先: ml@example.co.jp (john@example.co.jp, paul@example.co.jpのエイリアス)

  • このパターンではml@example.co.jpはjohnとpaulのアカウントのaliasとなっています。
    しかしながらjohnのアカウントは存在しないため、johnに送信されたメールはハードバウンスとして返ってきます。
    次に解析プログラムの結果を確認します。

    pattern3

    パターン1の場合と似た内容ですが、recipientはjohn@example.co.jpに、aliasはml@example.co.jpになっています。
    この内容から、エイリアスから転送を受けたアカウントが存在しないためバウンスメールが発生していることがわかります。
    また、softbounceが"0"であることから、ハードバウンスであることがわかります。

    まとめ

    今回はSisimaiによるバウンスメール解析について紹介しました。
    実際のメールシステムを模した構成で、3パターンでバウンスメール解析の検証を行ってみましたが、いかがでしたか?
    ハードバウンス・ソフトバウンスの判定ができたり、ML所属アカウントからのバウンスもいい感じに解析できるので、
    バウンスメール処理の自動化に活用できるのではないかと思います。
    ユースケースとしては、

  • 定期バッチで特定アカウントからのバウンスメールの数が閾値を超えたら、メール配信を停止する
  • ユーザのメールアドレス登録間違いを検知し、管理者に通知する
  • 日々バウンスデータを取り込み、送達率向上の施策を取るための材料にする

  • など、様々な使いみちが考えられます。
    導入も簡単なので、ぜひ一度試してみてはいかがでしょうか?

    AWS LambdaとAPI Gatewayを利用し、PagerDutyのインシデント発生時にSlackに専用チャンネルを作成する #3

    こんにちは、インフラ及びシステム運用を担当している田中です。

    前回の記事ではPagerDutyのWebhookをLambda関数で受け、SlackのIncident専用のチャンネルを作成してメッセージをpostしたり、IncidentがResolvedになったら専用チャンネルをarchiveしたりするところまでご紹介しました。

    大まかにまとめると、下記のシーケンス図のような仕組みになります。

    sequence_1

    しかし、専用チャンネルまで作成したのにpostしたメッセージは簡単なテキストだけで、見栄えもぱっとしなければ、大した情報も含まれていませんでした。

    そこで今回は、Slackにpostするメッセージの見栄えを改善したり、障害対応に必要な情報をメッセージに含めるなど、前回作ったプログラムに機能を追加していきます。

    また、せっかくLambdaを使用しているのに他のAWSの機能を使わないのはもったいないので、Node.jsのAWS SDKを利用してAWSの機能を利用する方法についても説明していきたいと思います。
    続きを読む

    AWS LambdaとAPI Gatewayを利用し、PagerDutyのインシデント発生時にSlackに専用チャンネルを作成する #2

    こんにちは、インフラ及びシステム運用を担当している田中です。

    前回の記事に引き続き、AWSのAPI Gateway経由でLambdaファンクションを呼び出し、Slackの
    APIを叩いて専用チャンネルを作成する手順を説明していきます。

    前回の記事ではPagerDutyのWebhookをLambda関数で受け取るところまでご紹介しました。

    これからLambda関数内でSlackのAPIをコールするための処理を記述することになります。

    簡単なLambda関数を書くだけであれば特に開発環境が必要になるわけではないのですが、少し複雑な処理を行おうとすると外部モジュールが必要になってきたりしますので、まずは開発環境を作ることから始めましょう。

    また、開発したコードのアップロードも管理コンソールからのアップロードは面倒ですし、ファイルサイズ等の制限もあります。
    そこで、AWS CLIを利用して開発したコードをS3経由でデプロイするようにします。
    続きを読む

    AWS LambdaとAPI Gatewayを利用し、PagerDutyのインシデント発生時にSlackに専用チャンネルを作成する #1

    こんにちは、インフラ及びシステム運用を担当している田中です。

    当社ではサーバーやネットワークに障害が発生した際に、障害対応の担当者へ通知を行う手段としてPagerDutyを利用しています。

    担当者が障害を認知した後は障害内容を調査し、必要に応じて開発者や責任者へエスカレーションを行うことになります。

    障害の発生が平日の日中帯であれば関係者間のコミュニケーションに問題はないのですが、夜間・休日の場合は関係者がそれぞれ別の場所に居ることになるため、障害ごとにSlackの専用チャンネルを作成して関係者間で情報を共有しています。

    SlackにはPagerDutyのWebhookを受け、インシデントの内容を特定のチャンネルにpostすることができる機能が用意されています。
    しかし、あくまでも特定のチャンネルにpostすることができるだけで、インシデントごとに専用のチャンネルを作成したりすることはできません。

    そこで、AWSのAPI Gateway経由でLambdaファンクションを呼び出し、SlackのAPIを叩いて専用チャンネルを作成するようにしています。

    今回から数回に分け、この仕組について説明したいと思います。
    続きを読む

    ローカルUnboundによる名前解決の冗長化の検証


    こんにちは!はんだです。
    今日はDNSサーバの可用性向上についてのエントリです。

    DNSサーバの障害時の影響を防ぐ方法として、
    単純にDNSサーバを複数設置して/etc/resolv.confに記入するという方法がありますが、
    その方法には、タイムアウトの待ち時間という落とし穴があります。

    /etc/resolv.conf
    nameserver 192.168.0.11  //DNS1のIPアドレス
    nameserver 192.168.0.12  //DNS2のIPアドレス
    

    のように設定したとして、
    1行目のDNS1が落ちた状態の場合、2行目のDNS2に移行するまでに、設定次第ですが問い合わせの度に毎回1~数秒のタイムアウトが発生します。名前解決の頻度が高いサービスの場合、この僅かな待ち時間がサービス全体へのパフォーマンスに影響してきます(この問題については[24時間365日]サーバ/インフラを支える技術(技術評論社)の 「DNSサーバの冗長化」の項が詳しいです)。

    この問題への対策として、LVS+Keepalivedなどを利用して冗長性を高める方法がありますが、それに加え

    複数のDNSサーバをローカルのUnboundの参照先として指定することで冗長性を高める

    という方法を検証してみました。

    (なお、今回の記事ではLVS構成の設定及び検証については言及しませんので、その部分については他の解説記事などをご参照下さい)

    検証環境

    検証用の環境として以下を準備しました。
    sv-web01       192.168.0.1       Webサーバ。Unboundを導入
    sv-dns-lvs01     192.168.0.11       node01とnode02でLVS。Keepalivedで死活監視
      sv-dns-node01  192.168.0.101     BIND
      sv-dns-node02  192.168.0.102     BIND
    sv-dns-lvs02     192.168.0.12       node03とnode04でLVS。Keepalivedで死活監視
      sv-dns-node03  192.168.0.103     BIND
      sv-dns-node04  192.168.0.104     BIND

    Unbound



    sv-web01にはUnboundをインストールし、/etc/resolv.confには以下のようにnameserverを設定します。
    nameserver 127.0.0.1       //ローカルのUnbound
    nameserver 192.168.0.11    //sv-dns-lvs01
    nameserver 192.168.0.12    //sv-dns-lvs02
    

    Unboundの設定

    Unboundとは、
    • シンプルな設計になっており高速で動作する。BINDより処理性能が良い(2~3倍)
    • 設定ファイルは1つのみ(unbound.conf)で内容もシンプルなため、設定が容易
    • キャッシュ汚染に対する耐性を強化した設計&DNSSECをサポート
    • デフォルト設定で高いセキュリティが確保された状態で動く
    という特徴を持った、新定番となっているDNSキャッシュサーバです。

    デフォルトの設定でもキャッシュサーバとして問題なく動作しますが、今回は社内の権威DNSサーバへ問い合わせを行いたいため、stub-zoneの設定を行います。
    stub-zoneは、特定のドメインの名前解決について、指定したDNS権威サーバに問い合わせを行うための設定です。

    /etc/unbound/unbound.confで、参照先DNSサーバとしてsv-dns-lvs01とsv-dns-lvs02を指定します。
     stub-zone:
            name:"example.com"
            stub-addr:192.168.0.11
            stub-addr:192.168.0.12
    stub-zone:
            name: "0.168.192.in-addr.arpa"
            stub-addr:192.168.0.11
            stub-addr:192.168.0.12
    
    Unboundはデフォルトではプライベートアドレスの問い合わせを行わないので、以下の設定を追加します。
    private-domain: "example.com"
    (中略)
    local-zone: "0.168.192.in-addr.arpa" transparent
    

    また、特定のドメインについて、指定したDNSキャッシュサーバに再帰問い合わせを行わせるときにはforward-zoneを設定します。
    forward-zoneは、nameオプションに "." を指定することで、全ての問い合わせを転送することもできます。
    自社ドメイン以外は別のDNSサーバ(外部DNSなど)を指定して直接問い合わせを行わせるような場合は、こちらで設定します。
    forward-zone:
      name: "."
      forward-addr: 203.0.113.111
    

    検証したいこと 

    1. stub-zoneに複数DNSサーバを記述した場合、どのような問い合わせ順序になるか。先に書かれているものが優先なのか、ラウンドロビンなのか。

    2. stub-zoneに書いたDNSサーバのうち1つが利用不可になった場合に、どのような挙動になるか。

    3. 利用不可になった理由が、サーバ自体がダウンした場合と、DNSサービスが利用不可になった場合では、挙動に違いはあるか。resolv.confでは、サーバ自体のダウンではなくDNSサービスが落ちていた場合にタイムアウトまでに長くかかることがあるが、Unboundではどうか。

    検証手順

    0.事前準備
    テスト用のスクリプトを作成します。sv-web01から他のサーバにnslookupを行い、最後にunboundのキャッシュを削除するというだけの単純なものです。
    SERVER_LIST="sv-web02 sv-web03 sv-db01 ・・・・・ sv-db20"
    while : ; do
    
        for SERVER in $SERVER_LIST; do
            echo $(date "+%F %H:%M:%S.%N") $(nslookup $SERVER | head -n 1)
        done
    
        unbound-control -q flush_zone example.com
        sleep 1
    
    done | tee test.log
    このスクリプトを実行しながらサーバやサービスを落として、どのサーバで名前解決が行われているかを確認します。
    DNSサーバに何の問題も起こっていない時点では以下のようなログになります。
    127.0.0.1、つまりローカルのUnboundで名前解決が行われているのがわかります。
    test.log
    
    2016-03-23 10:01:47.710318602 Server: 127.0.0.1
    2016-03-23 10:01:47.720166753 Server: 127.0.0.1
    2016-03-23 10:01:47.730168114 Server: 127.0.0.1
    2016-03-23 10:01:47.739930041 Server: 127.0.0.1
    2016-03-23 10:01:47.750394444 Server: 127.0.0.1
    2016-03-23 10:01:47.760969879 Server: 127.0.0.1
    2016-03-23 10:01:47.770984202 Server: 127.0.0.1
    2016-03-23 10:01:47.780918869 Server: 127.0.0.1
    2016-03-23 10:01:47.791561248 Server: 127.0.0.1
    2016-03-23 10:01:47.802000414 Server: 127.0.0.1
    2016-03-23 10:01:47.812778410 Server: 127.0.0.1
    2016-03-23 10:01:47.823340386 Server: 127.0.0.1
    2016-03-23 10:01:47.833941263 Server: 127.0.0.1
    2016-03-23 10:01:47.844795349 Server: 127.0.0.1
    



    検証1. 
    問い合わせ順序の確認

    各LVSサーバでDNSサービスへの接続状態を確認しながら検証用スクリプトを実行してみます。
    [root@i-dns-lvs01 ~]# watch ipvsadm -Ln
    
    IP Virtual Server version 1.2.1 (size=4096)
    Prot LocalAddress:Port Scheduler Flags
    -> RemoteAddress:Port Forward Weight ActiveConn InActConn
    UDP 192.168.0.11:53 rr
    -> 192.168.0.101    Route     0 0 271 
    -> 192.168.0.102     Route    0 0 272
    
    [root@i-dns-lvs02 ~]# watch ipvsadm -Ln
    
    IP Virtual Server version 1.2.1 (size=4096)
    Prot LocalAddress:Port Scheduler Flags
    -> RemoteAddress:Port Forward Weight ActiveConn InActConn
    UDP 192.168.0.12:53 rr
    -> 192.168.0.103    Route     0 0 269 
    -> 192.168.0.104     Route    0 0 271 
    

    InActConn(一定時間以内の接続数)がほぼおなじ数字で推移しています。
    結論:stub-zoneに複数のDNSサーバを記入した場合、順番に均等に問い合わせが割り振られる 。


    検証2.ローカルのUnboundのstub-addrに登録してあるsv-dns-lvs0xのどちらかが落ちた場合


    上記スクリプトを実行した状態で、片方のLVS(sv-dns-lvs01)を落としてみます。
    [root@sv-dns-lvs01 ~]# service network stop
    2016-03-23 11:02:39.654407380 Server: 127.0.0.1
    2016-03-23 11:02:39.666387084 Server: 127.0.0.1
    2016-03-23 11:02:39.678305469 Server: 127.0.0.1
    2016-03-23 11:02:39.690497052 Server: 127.0.0.1
    2016-03-23 11:02:39.702769957 Server: 127.0.0.1
    2016-03-23 11:02:39.714040967 Server: 127.0.0.1
    2016-03-23 11:02:39.725401558 Server: 127.0.0.1
    2016-03-23 11:02:39.736123310 Server: 127.0.0.1
    2016-03-23 11:02:39.746930229 Server: 127.0.0.1
    2016-03-23 11:02:39.758402234 Server: 127.0.0.1
    2016-03-23 11:02:39.769120682 Server: 127.0.0.1
    2016-03-23 11:02:39.781072673 Server: 127.0.0.1
    2016-03-23 11:02:41.296289719 Server: 127.0.0.1
    2016-03-23 11:02:41.308759832 Server: 127.0.0.1
    2016-03-23 11:02:41.422097623 Server: 127.0.0.1
    2016-03-23 11:02:41.434119357 Server: 127.0.0.1
    2016-03-23 11:02:42.849200158 Server: 127.0.0.1
    2016-03-23 11:02:42.861381999 Server: 127.0.0.1
    2016-03-23 11:02:42.873155039 Server: 127.0.0.1
    2016-03-23 11:02:42.884901850 Server: 127.0.0.1
    2016-03-23 11:02:42.896203445 Server: 127.0.0.1
    2016-03-23 11:02:42.906861984 Server: 127.0.0.1
    2016-03-23 11:02:42.917331042 Server: 127.0.0.1
    2016-03-23 11:02:42.927999125 Server: 127.0.0.1
    2016-03-23 11:02:42.938445488 Server: 127.0.0.1
    2016-03-23 11:02:42.948876159 Server: 127.0.0.1
    2016-03-23 11:02:42.959507049 Server: 127.0.0.1
    2016-03-23 11:02:42.970042488 Server: 127.0.0.1
    

    LVSサーバを落としたタイミングで若干処理スピードの低減が発生しましたが、ほぼタイムラグなくsv-dns-lvs01を切り離して引き続きUnboundで名前解決を行ってくれるようです。

    結論:stub-addrとして指定した複数のDNSのうち一つが落ちた場合でも、Unbound内で切り離しをして名前解決が続行される。



    検証3. sv-dns-lvs0x自体が落ちている場合と、sv-dns-lvs0xにぶらさがっている2つのnodeの両方のDNSサービスが利用不可の場合とで挙動は変わるか


    LVS用のサーバが落ちた場合、Unboundがうまく切り離してくれることがわかりました。
    では、LVSサーバではなく、node側のDNSサービスが両方落ちた場合はどうでしょうか。

    sv-dns-lvs01で利用している、sv-dns-node01とsv-dns-node02の両方で、namedサービスを停止してみます。
    [root@sv-dns-node01 ~]# service named stop 
    
    [root@sv-dns-lvs01 ~]# ipvsadm -Ln 
    
    IP Virtual Server version 1.2.1 (size=4096)
    Prot LocalAddress:Port Scheduler Flags
    -> RemoteAddress:Port Forward Weight ActiveConn InActConn
    UDP 192.168.0.11:53 rr
    -> 192.168.0.101    Route     0 0 0 
    -> 192.168.0.102     Route    0 0 0 
    
    2016-03-23 11:26:07.253176732 Server: 127.0.0.1
    2016-03-23 11:26:07.263412675 Server: 127.0.0.1
    2016-03-23 11:26:07.375477267 Server: 127.0.0.1
    2016-03-23 11:26:07.385581955 Server: 127.0.0.1
    2016-03-23 11:26:07.395923909 Server: 127.0.0.1
    2016-03-23 11:26:07.507452435 Server: 127.0.0.1
    2016-03-23 11:26:09.022468808 Server: 127.0.0.1
    2016-03-23 11:26:09.635581100 Server: 127.0.0.1
    2016-03-23 11:26:09.646748991 Server: 127.0.0.1
    2016-03-23 11:26:10.712805036 Server: 127.0.0.1
    2016-03-23 11:26:10.725051359 Server: 127.0.0.1
    2016-03-23 11:26:10.734853913 Server: 127.0.0.1
    2016-03-23 11:26:10.744873105 Server: 127.0.0.1
    2016-03-23 11:26:10.754697333 Server: 127.0.0.1
    2016-03-23 11:26:10.764829874 Server: 127.0.0.1
    2016-03-23 11:26:10.877325395 Server: 127.0.0.1
    2016-03-23 11:26:10.887888348 Server: 127.0.0.1
    2016-03-23 11:26:10.898024415 Server: 127.0.0.1
    2016-03-23 11:26:10.907979399 Server: 127.0.0.1
    
    検証2と同様、若干の処理速度低下はありますが、その後は引き続きUnboundでの名前解決が継続されました。

    結論:LVS自体が落ちている場合とサービスが使用不可の場合で挙動は変わらない

    まとめ 

    検証により、DNSサーバ障害時のパフォーマンスへの影響をローカルUnboundを導入することによりより減らすことができるだろうという結論が得られました。
    ここで一度、Unboundを導入することのメリット・デメリットを整理したいと思います。

    ローカルにUnboundを導入するメリット
    ・サーバ障害時も、キャッシュに保存されている分はTTL内(社内サーバなら1時間)であればローカルで名前解決ができる。
    ・DNSサーバを冗長化しておいた場合に、1台のみの障害であればresolv.confに列記するだけの場合よりもパフォーマンスへの影響を小さくできる。
    ・ローカルのネットワーク流量やDNSサーバの負荷を抑制できる。
    ・ローカルで名前解決ができることによるパフォーマンス向上が期待できる。

    ローカルにUnboundを導入するデメリット(懸念事項)
    ・導入コスト(各サーバへの設定)
    ・運用コスト(local-dataを使う場合はサーバ追加時のlocal-dataの再配布、ドメイン&セグメント追加時のunbound.conf設定修正)
    ・キャッシュが新しいトラブルの原因になったり、トラブル時の切り分けがしづらくなる可能性
    ・サーバ負荷軽減やパフォーマンス向上については、名前解決の問い合わせが少ないサーバや、新しい名前解決が多いサーバの場合には、効果が小さい。

    これを踏まえ、弊社では、
    ・TTL内での名前解決が多く発生し、キャッシュにより問い合わせが大きく減らせる
    ・DNS障害時のタイムラグがサービスのパフォーマンスに影響を与えることが想定できる
    など、Unbound導入によるメリットが大きいサーバから順に導入していくことになりました。
    また、導入・運用のコストについては、AnsibleやFabricといった自動化ツールを導入することである程度カバーできると考えています。

    参考URL

    ・5分でわかるUnbound http://gihyo.jp/admin/feature/01/unbound/0001
    ・Unboundサーバ運用Tips http://gihyo.jp/admin/feature/01/unbound/0004
    ・キャッシュサーバの設定 http://dnsops.jp/event/20140626/cache-config.pdf
    記事検索