Raspberry PiとFreeBSDで高可用性サーバの構築

はじめに

シングルボードコンピュータのRaspberry Pi(以下、RPi)。低価格、名刺サイズ、低消費電力といった特徴が受けて当初の教育用という目的以外にも様々な分野で活躍していますが、本稿ではRPi 2台にFreeBSDをインストールして高可用性(HA)サーバを構築してみます。例として、次のホスト構成で、仮想IPアドレス"192.168.100.10"のDNSサーバを運用することを考えます。

役割ホスト名IPアドレス
Masterhadns1192.168.100.101
Slavehadns2192.168.100.102

利用する主な技術は以下の通りです。

CARP(Common Adress Redundancy Protocol))
複数のサーバでIPアドレスを共有し、冗長化を実現するためのプロトコル
HAST(Highly Available Storage)
2台のサーバ間でファイルシステムを同期して、高可用性ストレージを実現する技術。
Rgular file Filesystem(正式名称不明)
単一ファイルをファイルシステムとして使う技術。昔はswapを増設するために使われていたと書くと一部の年寄り管理者にはピンと来るはず。

これらの技術を使って、以下のような機能の実現を目指します。

  • HASTによるサービスに必要な設定ファイルの共有
  • CARPによるMaster障害発生時の自動切り替え
  • 障害復旧後の再同期

本稿執筆時点でFreeBSDはまだRPi2には対応していませんので、以下ではRPi B+を使って進めていきます。

FreeBSD/ARMのインストール

RPi用FreeBSD/ARMのインストールについては、他にたくさん情報があるのでここでは簡単に述べます。まず、FreeBSDのFTPサイト(あるいは日本のミラー)にアクセスしてイメージをダウンロードします。本稿執筆時点での最新版は、"FreeBSD-10.1-STABLE-arm-armv6-ZEDBOARD-20150417-r281609.img.bz2"です。

イメージをダウンロードしたら、bunzip2コマンドで展開し、ddコマンド等でメモリカードにイメージを書き込みます。

% bunzip2 FreeBSD-10.1-STABLE-arm-armv6-ZEDBOARD-20150417-r281609.img.bz2
% sudo dd if=FreeBSD-10.1-STABLE-arm-armv6-ZEDBOARD-20150417-r281609.img of=[メモリカードのパス] bs=1m

書き込みに成功したら、メモリーカードをRPiに挿して電源を入れます。ブートしてログインプロンプトが表示されたら、rootでログインします。初期状態ではパスワードが空なので、まず設定します。

# passwd
Changing local password for root
New Password: [パスワード入力]
Retype New Password: [パスワード入力]

ディスクイメージの初期状態ではメモリカードの1GB分の領域しか使っていません(/etc/rc.confには「growfs_enable="YES"」という記述があるのですが、本稿執筆時点ではうまく動いていないようです)。

# df -m
Filesystem     1M-blocks Used Avail Capacity  Mounted on
/dev/mmcsd0s2a       906  394   439    47%    /
devfs                  0    0     0   100%    /dev
/dev/mmcsd0s1         16    3    13    21%    /boot/msdos
/dev/md0              28    0    26     0%    /tmp
/dev/md1              14    0    12     0%    /var/log
/dev/md2               4    0     4     0%    /var/tmp

# gpart show
=>      63  15523777  mmcsd0  MBR  (7.4G)
        63     34776       1  !12  [active]  (17M)
     34839   1918224       2  freebsd  (937M)
   1953063  13570777          - free -  (6.5G)

=>      0  1918224  mmcsd0s2  BSD  (937M)
        0      105            - free -  (53K)
      105  1918080         1  freebsd-ufs  (937M)
  1918185       39            - free -  (20K)

これを、残りの領域も使うように拡張します。まずはFreeBSDのスライスから。

# gpart resize -i 2 mmcsd0
# gpart show
=>      63  15523777  mmcsd0  MBR  (7.4G)
        63     34776       1  !12  [active]  (17M)
     34839  15488991       2  freebsd  (7.4G)
  15523830        10          - free -  (5.0K)

=>       0  15488991  mmcsd0s2  BSD  (7.4G)
         0       105            - free -  (53K)
       105   1918080         1  freebsd-ufs  (937M)
   1918185  13570806            - free -  (6.5G)

次にスライス2上のFreeBSD領域を拡張します。

# gpart resize -i 1 mmcsd0s2
# gpart show
=>      63  15523777  mmcsd0  MBR  (7.4G)
        63     34776       1  !12  [active]  (17M)
     34839  15488991       2  freebsd  (7.4G)
  15523830        10          - free -  (5.0K)

=>       0  15488991  mmcsd0s2  BSD  (7.4G)
         0       105            - free -  (53K)
       105  15480704         1  freebsd-ufs  (7.4G)
  15480809      8182            - free -  (4.0M)

最後にgrowfsコマンドを使って/パーティションを拡張します。

# growfs /
Device is mounted read-write; resizing will result in temporary write suspension for /.
It's strongly recommended to make a backup before growing the file system.
OK to grow filesystem on /dev/mmcsd0s2a, mounted on /, from 937MB to 7.4GB? [Yes/No] Yes
super-block backups (for fsck_ffs -b #) at:
 1918400, 2397952, 2877504, 3357056, 3836608, 4316160, 4795712, 5275264, 5754816, 6234368, 6713920, 7193472, 7673024, 8152576, 8632128, 9111680,
 9591232, 10070784, 10550336, 11029888, 11509440, 11988992, 12468544, 12948096, 13427648, 13907200, 14386752, 14866304, 15345856

# df -m
Filesystem     1M-blocks Used Avail Capacity  Mounted on
/dev/mmcsd0s2a      7314  394  6334     6%    /
devfs                  0    0     0   100%    /dev
/dev/mmcsd0s1         16    3    13    21%    /boot/msdos
/dev/md0              28    0    26     0%    /tmp
/dev/md1              14    0    12     0%    /var/log
/dev/md2               4    0     4     0%    /var/tmp

ここで念のため一旦再起動します。

# reboot

CARPの設定

次にCARPの設定を行います。

まず、carpカーネルモジュールの読み込み設定を両サーバで記述します。

/boot/loader.conf
carp_load="YES"

次に/etc/rc.confにIPアドレスの設定を追記します。

Master
ifconfig_ue0="inet 192.168.100.101 netmask 255.255.255.0"
ifconfig_ue0_alias0="vhid 1 pass [パスフレーズ] 192.168.100.10/32"
Slave
ifconfig_ue0="inet 192.168.100.102 netmask 255.255.255.0"
ifconfig_ue0_alias0="vhid 1 advskew 50 pass [パスフレーズ] 192.168.100.10/32"

ifconfig_ue0_alias0がCARPの設定になります。各パラメータの意味は次の通りです。

vhid [id]
バーチャルホストID。CARPで冗長化するIPアドレスごとの識別子
advskew [n]
死活監視への応答遅延(n/256秒)。結果、値が小さいほどそのサーバの優先度が高くなる。省略時のデフォルトは"0"
pass [phrase]
パスフレーズ。冗長化するIF間で合わせる
IPアドレス/マスク
ネットマスクは必ず32にする

最後に、/etc/hostsに両方のサーバのIPアドレスを追記します。

/etc/hosts
192.168.100.101 hadns1
192.168.100.102 hadns2

hadns1,hadns2を順に再起動してCARPがうまく動作しているか確認します。

hadns1
# ifconfig ue0
ue0: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500
	options=80001<RXCSUM,LINKSTATE>
	ether b8:27:eb:72:b0:b8
	inet 192.168.100.101 netmask 0xffffff00 broadcast 192.168.100.255 
	inet 192.168.100.10 netmask 0xffffffff broadcast 192.168.100.10 vhid 1 
	carp: MASTER vhid 1 advbase 1 advskew 0
	media: Ethernet autoselect (100baseTX <full-duplex>)
	status: active
	nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
hadns2
# ifconfig ue0
ue0: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500
	options=80001<RXCSUM,LINKSTATE>
	ether b8:27:eb:d1:82:5c
	inet 192.168.100.102 netmask 0xffffff00 broadcast 192.168.100.255 
	inet 192.168.100.10 netmask 0xffffffff broadcast 192.168.100.10 vhid 1 
	carp: BACKUP vhid 1 advbase 1 advskew 50
	media: Ethernet autoselect (100baseTX <full-duplex>)
	status: active
	nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>

この状態でhadns1を再起動して、hadns2のcarpが'MASTER'に変わることを確認します。 hadns1の再起動完了後もhadns2が'MASTER'のままとなるのが正常な動作です。

hadns1を'MASTER'に戻すには、hadns2で以下のコマンドを実行します。

# ifconfig ue0 vhid 1 state backup

HASTの準備

CARPの準備と動作確認がうまくいったら、HASTの準備を行います。

# mkdir /var/hast
# dd if=/dev/zero of=/var/hast/disk1.img bs=1m count=64
64+0 records in
64+0 records out
67108864 bytes transferred in 9.181383 secs (7309233 bytes/sec)

/etc/hast.confを作成し、以下を記述します。

replication fullsync
resource disk1 {
        on hadns1 {
                local /var/hast/disk1.img
                remote hadns2
        }
        on hadns2 {
                local /var/hast/disk1.img
                remote hadns1
        }
}

disk1を初期化します。

hadns1
# hastctl create disk1
# /etc/rc.d/hastd onestart
# hastctl role primary disk1
hadns2
# hastctl create disk1
# /etc/rc.d/hastd onestart
# hastctl role secondary disk1

hastctlで状態を確認します。

hadns1
# hastctl status
Name	Status	 Role		Components
disk1	complete primary        /var/hast/disk1.img	hadns2
hadns2
 # hastctl status
Name	Status	 Role		Components
disk1	complete secondary      /var/hast/disk1.img	hadns1

Statusが"complete"になったら、hadns1でファイルシステムを初期化します。

# newfs -U -J /dev/hast/disk1	
/dev/hast/disk1: 64.0MB (131056 sectors) block size 32768, fragment size 4096
	using 4 cylinder groups of 16.00MB, 512 blks, 2048 inodes.
	with soft updates
super-block backups (for fsck_ffs -b #) at:
 192, 32960, 65728, 98496

最後に/etc/rc.confにHAST起動の設定を追加します。

hastd_enable="YES"

DNSサーバ(BIND)のインストールとHASTの設定

FreeBSDのバージョン10以降ではBINDが取り除かれてしまいましたので、インストールします。ここではRPi上でコンパイルする正攻法を説明しますが、かなり時間がかかる作業となるので、付録に示すクロスコンパイル環境で独自リポジトリを構築する方法がおすすめです。

RPiにログインし、root権限でPorts treeを展開します。

portsnap fetch extract

BINDをインストールします。

cd /usr/ports/dns/bind910/
make && make install

インストールが完了したら、/usr/local/etc/namedの下に設定ファイルができるので、HASTのディスクにコピーします。

mkdir -p /hast/disk1/etc/namedb
cp -rp /usr/local/etc/namedb/ /hast/disk1/etc/namedb

コピー先のHASTのディレクトリが/usr/local/etc/namedbにマウントされるように、/etc/fstabに以下の記述を追加します。後述するcarpctlコマンドでマウントを制御するため、fstabではnoautoオプションを指定します。

/hast/disk1/etc/namedb /usr/local/etc/namedb nullfs rw,noauto 0 0

HASTのPrimaryとなっているホストでマウントします。

mount /hast/disk1/etc/namedb

/usr/local/etc/namedb/named.confを編集してDNSの設定を行います。本稿ではnamed.confの説明は省略します。

フェイルオーバーの設定

CARPの状態によるファイルオーバーの設定を行います。ここで重要な役割を果たすのがdevdというデーモンです。devdはカーネルイベントが発生した時にユーザーランドのプログラムを実行するデーモンで、CARPの状態の切り替わりをトリガとしてHASTとサービスの切り替え設定を自動化することができます。

まず、carpctlを/usr/local/sbin/の下に置き、パーミッションをセットます。

# ls -l /usr/local/sbin/carpctl
-rwxr-x---  1 root  wheel  4784 Jan  1 00:00 /usr/local/sbin/carpctl

このファイルにより、NICの状態が変化した時にHASTの切り替えやマウント、サービスの起動/停止を実行します。ファイルの冒頭に設定があるので、必要に応じて変更します。

hast_if
HASTの制御に使うNICを指定します。Raspberry Piの場合は"ue0"です。
resources
HASTで切り替えるリソース(ディスク)をスペース区切りで列挙します。リソース名はhast.confで定義したものです。
services
CARPの状況に応じて制御するサービスをスペース区切りで列挙します。本稿では"named"のみ制御します。

次に、devdの設定ファイルを設置します。標準のdevdの設定では/usr/local/etc/devd以下のファイルを読み込んでくれますので、ディレクトリを作成してその下に設置します。

mkdir /usr/local/etc/devd

ここに、/usr/local/etc/devd/carp.conf というファイルを作り、以下の内容を記述します。

notify 0 {
        match "system"          "CARP";
        match "subsystem"       "[0-9]+@[0-9a-z]+";
        match "type"            "(INIT|MASTER|BACKUP)";
        action "/usr/local/sbin/carpctl $type $subsystem";
};

簡単に説明すると、notifyステートメントでmatch行で定義されるカーネルイベントに対してaction行で指定したコマンドを実行します。

system
システム名。"CARP"になります。
subsystem
サブシステム名。CARPの場合は [vhid]@[if名] という書式になります。
type
通知の種別。CARPの場合は"INIT", "MASTER", "BACKUP"のいずれかになります。
action
実行するコマンド。carpctlに対してtypeとsubsystemを引数として渡すようにします。

ここまでの設定が終わったら、CARPのMASTER側のRPiでifconfigコマンドを使って切り替え試験を行います。

# ifconfig ue0 vhid 1 state backup

しばらくすると、BACKUPだったRPiでディスクがマウントされ、namedが起動します。

# df
Filesystem             1K-blocks    Used   Avail Capacity  Mounted on
(略)
/dev/hast/disk1            63124     244   57832     0%    /hast/disk1
/hast/disk1/etc/namedb     63124     244   57832     0%    /usr/local/etc/namedb

# /usr/local/etc/rc.d/named onestatus
named is running as pid ####.

基本的には以上なのですが、このままでは起動時にMASTERとなるRPiでディスクのマウントやサービスの起動に失敗します。これは、標準のブート手順ではdevdの方がhastdより先に起動してしまうためです。

これを回避するため、/etc/rc.d/devdの

# REQUIRE: netif

という行を、

# REQUIRE: netif hastd

と書き換えます。

さらに、条件がよくわかっていないのですが、MASTER側をshutdownやrebootするとたまにカーネルがパニックしてデバッガが起動し、遠隔地から操作できなくなってしまうので、これに対応します。

まず、/etc/sysctl.confに以下の行を追加します。

debug.debugger_on_panic=0

そして、/usr/local/etc/rc.d/ddb という名前で以下のスクリプトを設置します。

#!/bin/sh

# PROVIDE: ddb
# BEFORE: sysctl

. /etc/rc.subr

name="ddb"
start_cmd="ddb_start"
stop_cmd=":"
rcvar="ddb_enable"

ddb_start ()
{
    /sbin/ddb script kdb.enter.panic=reset
}

load_rc_config $name
run_rc_command "$1"

スクリプトの実行ビットを立てます。

# chmod 750 /usr/local/etc/rc.d/ddb
# ls -l /usr/local/etc/rc.d/ddb
-rwxr-x---  1 root  wheel  228 May  3 18:40 /usr/local/etc/rc.d/ddb

最後に、/etc/rc.confに以下の行を追加します。

ddb_enable="YES"

shutdownやrebootしてデバッガで止まらずに「15秒後に再起動」メッセージが表示されて再起動すれば正しく設定できています。本来であればpanicの原因解明と対策をした方がいいのですが、本稿の運用方式であればMASTER側をリブートすると必ずBACKUPとなり、もう一方のサーバからHASTの同期をやり直すので、致命的なデータ喪失等は起きないはずです。

障害対応シナリオ

本稿で説明した設定では、障害発生時には以下のような動作となります。

初期状態
  • hadns1がCARP MASTER/HAST Primaryでnamedが起動
  • hadns2がCARP BACKUP/HAST Secondaryでnamedは起動しない
hadns1で障害発生
  • hadns2が自動的にCARP MASTERに切り替わる
  • hadns2がHAST Primary(degrated)となる
  • hadns2でnamedが起動する
hadns1が復旧
  • hadns2はCARP MASTERのままで、namedサービス継続
  • hadns1はCARP BACKUPとなる
  • hadns1はHAST Secondaryとなり、hadns2(Primary)と再同期する

つまり、本稿で説明している設定ではCARPのadvskew設定は使用しておらず、一旦MASTERになると、RPiの再起動やifconfigコマンドで明示的にBACKUPに切り替えない限りはMASTERであり続けます。これは、hadns1で障害が発生してMASTERが切り替わってから復旧するまでの間に、hadns2の方で設定が変更されたりしていると、その修正が失われてしまうからです。

hadns1の復旧後、再度MASTERに戻したい場合HASTの状態がsecondaryでcompleteになっているのを確認してから、hadns2の方のCARPを手動でBACKUPに切り替えるようにします。

まとめ

本稿ではRaspberry Piを使った高可用なFreeBSDサーバの構築手順を説明しました。今回はDNSサービスのみ取り上げましたが、carpctlは汎用性を考えて作ってありますので、同ファイル内のservice設定や/etc/fstabを適宜修正すれば、他のサービスでも使えると思います。総額でも1.5万円(UPS除く)程で冗長化サーバが構築できるというのは、なかなか魅力的ではないでしょうか。

[付録] RPi用のクロスコンパイル環境と独自リポジトリの構築

RPiは非力なのでPortsのコンパイルにはかなりの時間を要します。そこで、他のアーキテクチャのFreeBSD環境でRPi用のパッケージを構築する方法を説明します。以下の説明はRPiと同じ10.1の環境がすでにあるという前提で進めます。amd64の環境で確認しています。

まず、ARMプロセッサのエミュレータをインストールします。

pkg install emulators/qemu-sbruno
pkg install emulators/qemu-user-static

binmiscctlコマンドを使ってARM用のバイナリイメージを認識できるようにします。

binmiscctl add armv6 --interpreter "/usr/local/bin/qemu-arm-static" --magic "\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00" --mask "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff" --size 20 --set-enabled
# binmiscctl list
name: armv6
interpreter: /usr/local/bin/qemu-arm-static
flags: ENABLED USE_MASK 
magic size: 20
magic offset: 0
magic: 0x7f 0x45 0x4c 0x46  0x01 0x01 0x01 0x00  0x00 0x00 0x00 0x00 
       0x00 0x00 0x00 0x00  0x02 0x00 0x28 0x00 
mask:  0xff 0xff 0xff 0xff  0xff 0xff 0xff 0x00  0xff 0xff 0xff 0xff 
       0xff 0xff 0xff 0xff  0xfe 0xff 0xff 0xff 

package管理ツール(Poudriere)をインストールします。クロスコンパイル環境のためにはdevelバージョンが必要なようです。

pkg install poudriere-devel

必要に応じて/usr/local/etc/poudriere.confを編集します。ここではデフォルトで進めます。

Ports treeを展開します。

poudriere ports -c

ARMアーキテクチャ用のjailを構築します。

poudriere jail -c -j 10armv6 -m svn -a arm.armv6 -v stable/10

パッケージを作ります。例: bind910

poudriere options -j 10armv6 dns/bind910
poudriere bulk -j 10armv6 dns/bind910

これで、/usr/local/poudriere/data/packages/10armv6-default以下にリポジトリができるので、Webサーバを使って公開します。例えばApache 2.2.xの場合は、Aliasを使って以下のように定義します。

Alias /10armv6 /usr/local/poudriere/data/packages/10armv6-default

<Directory /usr/local/poudriere/data/packages/10armv6-default>
   Order allow,deny
   Allow from all
</Directory>

RPiの方でこのリポジトリを使うように設定します。まずディレクトリを作成します。

mkdir -p /usr/local/etc/pkg/repos

ここにリポジトリの設定ファイル(MyREPO.conf)を設置します。

FreeBSD: { enabled: no }

MyREPO: {
url: "http://[リポジトリサーバ]/10armv6",
mirror_type: "http",
enabled: yes
}

コメント:
Name: Comment:
Last modified:2015/05/04 21:20:42
Keyword(s):
References:[SideMenu]