ロードバランサの冗長化
出典: KLablabWiki
- タイトル
- ロードバランサを冗長化
- 著者名
- KLab(株) 安井 真伸 YASUI Masanobu
- 初出
- 技術評論社刊『WEB+DB PRESS Vol.37』
目次 |
ロードバランサを冗長化する
keepalivedにはVRRPという機能があります。ロードバランサを2台用意してこの機能を使うと、なんらかのトラブルで1台がダウンしても、もう1台のロードバランサが自動的に役割を引き継いでくれるようになります(この動作を「フェイルオーバ」といいます)。
この章ではkeepalivedのVRRP機能を使い、前章の構成で単一故障点だったロードバランサを冗長化する方法を紹介します。
VRRPとは
実際に使ってみる前に、VRRPとはどのようなものかを簡単に説明します。
VRRPはVirtual Router Redundancy Protocolの略で、RFC3768で定義されています。VRRPでは物理ルータ固有のIPアドレスとは別に「仮想ルータアドレス」[1]というIPアドレスを設定します。仮想ルータアドレスは、複数の物理ルータのどれか1台に割り当てられます。仮想ルータアドレスが割り当てられたルータを「マスタルータ」と呼び、それ以外を「バックアップルータ」と呼びます。マスタルータが停止した場合には即座にバックアップルータがマスタルータとなり、仮想ルータアドレスを引き継ぐ事でフェイルオーバを実現します。したがって、VRRPで構成されたルータを利用するサーバは、デフォルトゲートウエイに仮想ルータアドレスを指定しなければいけません。
VRRPの死活監視
マスタルータは、自分が健全であることを広告(Advertisement)するためのメッセージを、定期的にマルチキャストしています。このメッセージを「アドバタイズメントメッセージ」と呼びます。バックアップルータは、アドバタイズメントメッセージを受信できている限り、マスタルータが健全であると判断します。一定時間アドバタイズメントメッセージを受信できなければ、マスタルータがダウンしたと判断して自分がマスタルータになろうとします。バックアップルータがマスタルータに「生きてますかぁ?」とユニキャストで問い合わせるのではなく、マスタルータが「私がマスタですよぉ」とマルチキャストすることで死活監視をするのが、VRRPの大きな特徴です。
マスタの選定方法
複数台あるルータのうち1台がマスタルータになりますが、バックアップルータが2台以上あるときにマスタルータが停止すると、どのバックアップルータがマスタルータになるのでしょうか。
VRRPではマスタルータを選定するために「プライオリティ」という値を、物理ルータ毎に設定する必要があります。バックアップルータはアドバタイズメントメッセージを受信できなくなると、自分でアドバタイズメントメッセージを送出します。アドバタイズメントメッセージの中にはそのルータのプライオリティ値が入っているので、自分よりも高いプライオリティ値をもったアドバタイズメントメッセージを受け取ったバックアップルータはマスタルータになることを断念します。つまり、プライオリティを高く設定してあるルータから順にマスタルータになるわけです。
プリエンプティブモード
プライオリティが一番高いルータが故障して、他のルータがマスタルータになっている状態で、故障していたルータが復旧するとどうなるのでしょうか。
デフォルトでは、復旧したルータが再度マスタになり、肩代わりしてくれていたルータはバックアップに戻ります。一見この動作は問題ないようにみえます。けれども、もしマスタルータが停止と復旧を頻繁に繰り返していたらどうなるでしょう。なんらかのハードウエア障害(ディスク故障やメモリ故障など)が発生した時にシステムがどのような状態になるかは予想できません。綺麗にダウンしてくれるかもしれませんが、中途半端に稼働し続けてしまう可能性もあります。プライオリティの高いルータが中途半端に停止と復旧を繰り返していると、そのたびにフェイルオーバが発生することになります。
VRRPではプリエンプティブモードを変更することでこの挙動を変えることができます。プリエンプティブモードを無効にすると、他のルータがマスタルータとして稼働している場合は、たとえ自分のプライオリティが現マスタルータより高くても、マスタに戻らなくなります。つまりは「早いもの勝ち」になるわけです。
どちらのモードを選ぶかは構成や状況次第で変わってくると思いますが、筆者らのシステムでは訳あってプリエンプティブモードを無効にして運用しています。
VRRPを使ってみよう
ネットワーク構成
ロードバランサを冗長化するには、少なくとも2台のロードバランサが必要です。ネットワーク構成は図1のようになります。lv1と同じ構成のマシンをもう1台用意し、ホスト名とIPアドレスを表1のとおりに設定して下さい。さらにリアルサーバ(w100,w101,w102)のデフォルトゲートウエイを192.168.31.10に、クライアント(cl01)のデフォルトゲートウエイを10.10.31.10に変更して下さい。このアドレスがVRRPの仮想ルータアドレスになります。
| 種別 | ホスト名 | IPアドレス | デフォルトゲートウエイ |
|---|---|---|---|
| ロードバランサ | lv2 | eth0:10.10.31.12/24 | なし |
| eth1:192.168.31.12/24 |
keepalivedに設定を追加
リスト1をlv1とlv2の/etc/keepalived.confに追記します。「あれ?両方ともpriority 100でいいの?」と思ったあなたは素敵です。本来であれば、どちらかのプライオリティを高くすべきですが、今回は以下の理由により同じ設定にします。
- 設定ファイルは同じにした方が楽
- 多少であっても設定ファイルの内容に違いがあると面倒です。複数台のマシンの設定を変更する際に、なにも考えずに同じ内容にできるのとそうでないのとでは、精神的な負担が結構違うものです。
- バックアップルータが1台の構成
- バックアップルータが2台以上の環境でプライオリティを同じにすると、マスタルータが停止したときにどのバックアップルータが新しいマスタルータになるのかを判断しにくくなります。しかし今回はバックアップルータが1台なので、そのような心配はありません。
- プリエンプティブモードを無効にしている
- nopreemptでプリエンプティブモードを無効にしているので、どちらかがマスタルータとして稼働していれば、もう1台はおとなしくしていてくれます。今回はプライオリティ値でマスタルータを選出させるのではなく、「先に起動した方をマスタとする」という方針で稼働させるのでプライオリティ値が同じでも問題になりません。
- なんといっても説明が楽ちん!
- おっと、思わず本音がでてしまいました。このような構成にすることで、この後の説明が楽になるんです、はい。
リスト1 VRRPの設定
vrrp_instance VE {
state BACKUP
interface eth0
garp_master_delay 5
virtual_router_id 1
priority 100
nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass himitsu
}
virtual_ipaddress {
10.10.31.10/24 dev eth0
192.168.31.10/24 dev eth1
}
}
設定を反映する
ここまでできたら、あとはkeepalivedを再起動するだけです。lv1、lv2の順に/etc/init.d/keepalived restartして下さい。これで、仮想ルータアドレス(10.10.31.10と192.168.31.10)がlv1に割り当てられるはずです。図2のようにipコマンドで確認してみましょう。なお、ifconfigコマンドでは確認できませんのでご注意下さい。
図2 仮想ルータアドレスの確認
lv1:~# ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet 10.10.31.11/24 brd 10.10.31.255 scope global eth0
inet 10.10.31.10/24 scope global eth0
lv1:~# ip addr show eth1
3: eth1: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet 192.168.31.11/24 brd 192.168.31.255 scope global eth1
inet 192.168.31.10/24 scope global eth1
————————————————————————————————————
lv2:~# ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet 10.10.31.12/24 brd 10.10.31.255 scope global eth0
lv2:~# ip addr show eth1
3: eth1: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet 192.168.31.12/24 brd 192.168.31.255 scope global eth1
フェイルオーバしてみよう
さて、これで仮想ルータ経由でサイトへアクセスできるようになっているはずです。cl01上でcurl 'http://10.0.0.100/test.html' を実行すると正常にレスポンスが返ってくると思います。
それでは思い切ってlv1をシャットダウンしてみましょう。
lv1:~# shutdown -h now
lv1が停止しても、問題なくhttp://10.0.0.100/test.html へアクセスできていることでしょう。先ほどと同様にIPアドレスを確認すると図3のようになっているはずです。仮想ルータアドレスがlv2に割り当てられていることがわかります。動作確認が済んだらlv1を起動しておきましょう。nopreemptなのでマスタルータはlv2のままです。
図3 フェイルオーバ後の仮想ルータアドレス
lv2:~# ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet 10.10.31.12/24 brd 10.10.31.255 scope global eth0
inet 10.10.31.10/24 scope global eth0
lv2:~# ip addr show eth1
3: eth1: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet 192.168.31.12/24 brd 192.168.31.255 scope global eth1
inet 192.168.31.10/24 scope global eth1
ネットワーク障害時の動作
ロードバランサをシャットダウンしたときにフェイルオーバされることは確認できました。次に、ネットワークケーブルを抜いたらどうなるかを見てみましょう。
おもむろにマスタルータ(lv2)の外向け(eth0)のケーブルを抜いてみて下さい。lv2のkeepalivedはNICのリンクダウンを検出し、仮想ルータアドレスを解放してバックアップルータとなります。lv1はアドバタイズメントメッセージが途絶えるのでマスタルータになります。これでNICがリンクダウンした場合も正常にフェイルオーバできることが確認できます。抜いたケーブルは元に戻しておいて下さい。nopreemptなのでマスタルータはlv1のままです。
リンクダウンしたのに・・・
今度はマスタルータ(lv1)の内向け(eth1)のケーブルを抜いてみましょう。先ほどと同じようにフェイルオーバ・・・と思ったら、おや?なぜかフェイルオーバせずにlv1がマスタのままになっています。この状態ではcl01からのパケットはlv1のeth0までは到達しますが、eth1がリンクダウンしているためリアルサーバへ転送できません。つまり、クライアントからサイトが見えなくなってしまっているのです。
なぜこのような現象が起こるのでしょうか。実はkeepalivedの設定に問題があります。リスト1の設定では、「vrrp_instance VE」のブロックの中で「interface eth0」と定義しています。この設定では、eth0に対してリンクダウンの検出をしますが、eth1に対しては検出してくれません。また、アドバタイズメントメッセージもeth0から送出されているので、バックアップルータであるlv2も障害に気づかず、フェイルオーバすることができなかったのです。
VRRPインスタンスの同期
それではどうすればeth1がリンクダウンしてもフェイルオーバできるようになるのでしょうか。その答えがリスト2です。少々長いですがkeepalived.confに追記したVRRPの設定を、リスト1の内容からリスト2のものに書き換えてみて下さい。
この設定では、eth0用とeth1用にそれぞれ個別のVRRPインスタンスを定義しています。これにより、eth0とeth1の両方でリンクダウンを検出できるようになります。ただ、eth0とeth1を別インスタンスにしただけだと、eth1のケーブルを抜くと「vrrp_instance VI(192.168.31.10)」はフェイルオーバしますが、「vrrp_instance VE(10.10.31.10)」はフェイルオーバしません。それも困りものなので「vrrp_sync_group VG」ブロックを定義して「vrrp_instance VE」と「vrrp_instance VI」の状態を同期するようにしています。ここまでやってはじめて、eth0とeth1のどちらのケーブルを抜いても正常にフェイルオーバできる構成ができあがります。
リスト2 内側と外側のインターフェイスを別のインスタンスにする設定
vrrp_sync_group VG {
group {
VE
VI
}
}
vrrp_instance VE {
state BACKUP
interface eth0
garp_master_delay 5
virtual_router_id 1
priority 100
nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass himitsu
}
virtual_ipaddress {
10.10.31.10/24 dev eth0
}
}
vrrp_instance VI {
state BACKUP
interface eth1
garp_master_delay 5
virtual_router_id 2
priority 100
nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass himitsu
}
virtual_ipaddress {
192.168.31.10/24 dev eth1
}
}
フェイルオーバの影響
さて、クライアントからリアルサーバへTCP接続されている最中にフェイルオーバすると、その接続はどうなるのでしょうか。curlで確認するのは少々面倒なので、telnetを使って確認します。cl01からtelnetで10.0.0.100:80に接続し、その状態のままフェイルオーバします。そしてHTTPリクエストを手入力したところ図4のような結果になりました。本来であれば、HTTPレスポンスが返ってくるはずですが、いくら待っても応答がありません。ipvsadmの-cオプションでIPVSのコネクションテーブルを表示したところ、図5のような結果になりました。
IPVSはTCP接続が確立される際にこのような形式で接続情報を保持しています。TCP接続したままの状態でロードバランサがフェイルオーバすると、新しくマスタになったロードバランサ(lv2)にはこの情報がありません。これでは、10.10.31.100:57781から10.0.0.100:80への接続をどのリアルサーバに転送すればよいのかわかりません。そのため、Webサーバから応答が返ってこなくなってしまったわけです。
引き継がれない接続に関する問題
ここで問題になるのはフェイルオーバ時にESTABLISHEDだった接続のみです。一般的なWebサイトではタイムアウト後の再試行を期待することができるので、許容してもよいケースがほとんどだと思います。そもそも、フェイルオーバが発生するということは、なにかしらの障害が発生しているわけですから、その瞬間に接続していたユーザがタイムアウトになるくらいは大目にみてもらってもよいのではないでしょうか。
コネクションテーブルの同期
「いや!フェイルオーバ時にアクティブな接続を引き継いでくれないなんて納得できん!!」という厳格な方のために奥の手があります。keepalived.confをリスト3のように書き換えて下さい。リスト2からの変更点は1行だけ、「lvs_sync_daemon_interface eth1」を追加しただけです。この設定を追加すると、マスタルータのIPVSのコネクションテーブルの状態をバックアップルータへ同期するようになり、フェイルオーバが発生してもアクティブな接続を維持することができます。
この機能を有効にして、先ほどと同様にtelnetで接続してみましょう。cl01からtelnet 10.0.0.100 80した状態でlv1とlv2のコネクションテーブルの内容を表示すると、図6のような結果になりました。lv2に接続情報が同期されていることがわかると思います。そしてlv1からlv2へフェイルオーバしてからHTTPリクエストを手入力した結果が図7です。今度は見事にHTTPレスポンスが返ってきました。
図4 接続中にフェイルオーバするとどうなるか
cl01:~$ telnet 10.0.0.100 80
Trying 10.0.0.100...
Connected to 10.0.0.100.
Escape character is '^]'. ← これが表示されたらフェイルオーバする
GET /test.html HTTP/1.0 ← この行を入力
← 空行を入力
(・・・応答が返ってこない・・・)
図5 lv1とlv2のコネクションテーブル
lv1:~# ipvsadm -Lnc IPVS connection entries pro expire state source virtual destination TCP 14:59 ESTABLISHED 10.10.31.100:57781 10.0.0.100:80 192.168.31.102:80 lv2:~# ipvsadm -Lnc IPVS connection entries pro expire state source virtual destination
リスト3 コネクションテーブルを同期する設定
vrrp_sync_group VG {
group {
VE
VI
}
}
vrrp_instance VE {
state BACKUP
interface eth0
garp_master_delay 5
virtual_router_id 1
priority 100
nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass himitsu
}
virtual_ipaddress {
10.10.31.10/24 dev eth0
}
}
vrrp_instance VI {
state BACKUP
interface eth1
lvs_sync_daemon_interface eth1 ← この行を追加します
garp_master_delay 5
virtual_router_id 2
priority 100
nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass himitsu
}
virtual_ipaddress {
192.168.31.10/24 dev eth1
}
}
図6 lv1とlv2のコネクションテーブル(syncd使用時)
lv1:~# ipvsadm -Lnc IPVS connection entries pro expire state source virtual destination TCP 14:56 ESTABLISHED 10.10.31.100:39903 10.0.0.100:80 192.168.31.101:80 lv2:~# ipvsadm -Lnc IPVS connection entries pro expire state source virtual destination TCP 02:58 ESTABLISHED 10.10.31.100:39903 10.0.0.100:80 192.168.31.101:80
図7 接続中にフェイルオーバするとどうなるか(syncd使用時)
cl01:~$ telnet 10.0.0.100 80
Trying 10.0.0.100...
Connected to 10.0.0.100.
Escape character is '^]'. ← これが表示されたらフェイルオーバする
GET /test.html HTTP/1.0 ← この行を入力
← 空行を入力
HTTP/1.1 200 OK
Date: Tue, 09 Jan 2007 06:26:48 GMT
Server: Apache/2.2.3 (Unix)
Last-Modified: Tue, 12 Dec 2006 18:31:38 GMT
ETag: "4346-5-7e20f680"
Accept-Ranges: bytes
Content-Length: 5
Connection: close
Content-Type: text/html
w102
Connection closed by foreign host.
最後に
keepalivedは応用次第で様々なシステムを手軽に冗長化することができます。VRRPでDNSやSMTPサーバなどをフェイルオーバできるようにしてみるのも面白いかもしれませんね。
比較的取り回しが楽なので簡単に試してみることができると思います。是非一度遊んでみることをお勧めします。
脚注
- ↑ ここでいう「仮想ルータアドレス」は前章ででてきたIPVSの仮想IPアドレス(10.0.0.100)と全く別のものですので混同しないようにご注意ください。
