フタなしカンヅメ

徒然なるままに @happytar0

Apacheのmod_rewriteを使ってフェイルオーバー?

Apachemod_rewriteを使って、フェイルオーバーみたいなことができないかと思い、ちょっとやってみることにしました。

フェイルオーバーといったら大げさな感じですが、もうちょっと本格的にやるならmod_proxy_balancerを使ったほうがいいと思うので。。。

現在下記のような構成で運用しているサーバがありますが、静的ファイルを配信するサーバBが落ちている場合やオフになっている場合などは、サーバBから配信したいという感じです。

サーバA -> CGIの処理や比較的軽いHTMLを処理
サーバB -> 主に画像などの重たい静的ファイルを処理

かなり限定的な環境だと思うのですが、サーバAで一度リクエストを受け、画像ファイルだけをリダイレクトでサーバBに転送しています。
なので、サーバBが落ちていたりした場合、サーバBに転送せずにサーバAで返却したいというわけです。サーバAでも同じデータを持っている必要があります。

調べたところRewriteMapを使うことで外部プログラムを呼び出すことができるようです。今回はこれを使ってみます。
サーバが稼働しているか定期的にヘルスチェックをする必要がありますが、今回はサーバAにリクエストされた際に、外部プログラムを通して5秒間隔でサーバBにHTTPリクエストをおこなってチェックしてみます。

外部プログラムはRubyを使ってみます。外部プログラムの呼び出しは起動時に一回のみおこなわれ、STDIN、STDOUTを通して処理されるとのことで、オーバーヘッドはあまり気にする必要はなさそうです。
こんな感じで作ってみました。

## /usr/local/bin/healthcheck.rb

#!/usr/bin/env ruby
# Buffered I/Oを使わない
$stdin.sync = 1
$stdout.sync = 1

require 'net/http'

STATUS_DOWN = 'DOWN'
STATUS_UP = 'UP'
STATUS_NULL = 'NULL'
CHECK_PERIOD = 5 # ヘルスチェックの間隔(秒)
HTTP_TIMEOUT = 2 # HTTPコネクションと読み込みのタイムアウト(秒)

lastcheck = 0
laststatus = STATUS_NULL

while true
  buffer = $stdin.gets
  hostname, port, path = buffer.chomp.split(/:/, 3)

  nowcheck = Time.now.to_i
  if (lastcheck + CHECK_PERIOD) > nowcheck
    $stdout.puts laststatus
  else
    response = nil
    if hostname
      begin
        http = Net::HTTP.new(hostname, port ? port : 80)
        http.open_timeout = HTTP_TIMEOUT
        http.read_timeout = HTTP_TIMEOUT
        http.start {|p| response = p.head(path ? path : '/') }
      rescue
        #
      end
    end

    if response and response.code.to_i == 200 
      $stdout.puts laststatus = STATUS_UP
    else
      $stdout.puts laststatus = STATUS_DOWN
    end
    lastcheck = nowcheck
  end
end

注意:上記のスクリプトにはバグがあります。
RubyのNet::HTTPでハマる - フタなしカンヅメ

Apacheの設定ファイルは下記のように書きました。

<IfModule mod_rewrite.c>
  RewriteEngine On
  # 必ずロックファイルを指定
  RewriteLock /tmp/map.lock
  # RewriteMapで使う外部プログラムを指定
  RewriteMap healthcheck-map prg:/usr/local/bin/healthcheck.rb
</IfModule>

<Directory />
  RewriteEngine On
  # RewriteMapを呼び出す時は下記のように指定
  # RewriteRuleでも呼び出せます
  RewriteCond %{REQUEST_FILENAME} \.(gif|png|jpg|jpeg|bmp)$ [NC]
  RewriteCond ${healthcheck-map:192.168.1.3:80:/} ^UP$ [NC]
  RewriteRule (.*) http://example.com/$2 [L,R]
</Directory>
RewriteCond ${healthcheck-map:192.168.1.3:80:/} ^UP$ [NC]
- healthcheck-map => 使用するRewriteMapの名前
- 192.168.1.3 => ヘルスチェックするホスト名またはIPアドレス
- 80 => ポート番号
- / => チェック先のリクエストパス
のように「:」で区切って呼び出します

一番ハマったのが、RewriteMapで外部プログラムを指定する部分。いろいろいじっても下記のようなエラーが・・・。

map lookup FAILED: map=healthcheck-map key=localhost:80:/

原因は、ディレクティブの中で、RewriteEngine Onを指定していなかったせいでした。そんなんあり?

参考サイト
mod_rewriteでサーバーの負荷が高いときだけリダイレクトする - inspfightmanの日記
Apache module mod_rewrite
mod_rewrite - RewriteMap - とみぞーノート
rubyスクリプトでRewriteMapを書く際の注意点 -- Stack-Style
mod_rewrite:RewriteMapで柔軟なURL書き換え | ゆーすけぶろぐ