daemontools とか supervisor とか pm2 とか forever とか foreman とか systemd で同じコマンドを複数のプロセスでサービスにする

これまでスクリプトをデーモン化するために daemontools をよく使っていたのですが、同じコマンドを複数プロセス起動させたいときに煩雑というか、そもそもこのやりかたあってんの? って思ったので、代替になりそうなものをいくつか試しました。

例として、w1.shw2.sh の 2 つのサービスを、w1.sh は 2 プロセス、w2.sh は 3 プロセス起動したいものとします。

daemontools

  • http://cr.yp.to/daemontools.html
  • 定番
  • 下記の SRPM から入れるとインストールが簡単
  • 1サービス=1プロセスが基本
    • 複数プロセスを起動したければその数だけサービスを定義する必要がある
    • もしくはサービスとして起動したプロセスでさらにプロセスマネージャーみたいにするか
# インストール
sudo yum -y install rpm-build
wget http://mirrors.qmailtoaster.com/daemontools-toaster-0.76-1.3.6.src.rpm
rpmbuild --rebuild daemontools-toaster-0.76-1.3.6.src.rpm
sudo rpm -ivh ~/rpmbuild/RPMS/x86_64/daemontools-toaster-0.76-1.3.6.x86_64.rpm

# systemd のユニットファイル
sudo tee /etc/systemd/system/daemontools.service <<EOS
[Unit]
Description = daemontools
After = network.target

[Service]
ExecStart = /command/svscanboot
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target
EOS

# daemontools を起動
sudo systemctl daemon-reload
sudo systemctl start daemontools
sudo systemctl status daemontools
sudo systemctl enable daemontools

# サービスの設定
sudo mkdir /service/.w1-1
sudo mkdir /service/.w1-2
sudo mkdir /service/.w2-1
sudo mkdir /service/.w2-2
sudo mkdir /service/.w2-3

sudo ln -sf /vagrant/w1.sh /service/.w1-1/run
sudo ln -sf /vagrant/w1.sh /service/.w1-2/run
sudo ln -sf /vagrant/w2.sh /service/.w2-1/run
sudo ln -sf /vagrant/w2.sh /service/.w2-2/run
sudo ln -sf /vagrant/w2.sh /service/.w2-3/run

sudo touch /service/.w1-1/down
sudo touch /service/.w1-2/down
sudo touch /service/.w2-1/down
sudo touch /service/.w2-2/down
sudo touch /service/.w2-3/down

sudo ln -sf .w1-1 /service/w1-1
sudo ln -sf .w1-2 /service/w1-2
sudo ln -sf .w2-1 /service/w2-1
sudo ln -sf .w2-2 /service/w2-2
sudo ln -sf .w2-3 /service/w2-3

# サービスを開始する
sudo svc -u /service/*

# サービスの一覧表示
sudo svstat /service/*

# サービスの再起動
sudo svc -t /service/*

# サービスを停止する
sudo svc -d /service/*

# サービスを無効にする
sudo svc -d /service/* && sudo svc -x /service/* && sudo rm /service/*

supervisor

  • http://supervisord.org/
  • 定番
  • epel から yum でインストールできる
  • 設定ファイルの numprocs でプロセス数を指定可能
# インストール
sudo yum -y install supervisor

# supervisor 起動
sudo systemctl start supervisord.service
sudo systemctl status supervisord.service
sudo systemctl enable supervisord.service

# サービスの設定
sudo tee /etc/supervisord.d/app.ini <<EOS
[program:w1]
command=/vagrant/w1.sh
process_name=%(program_name)s-%(process_num)d
numprocs=2
autostart=false

[program:w2]
command=/vagrant/w2.sh
process_name=%(program_name)s-%(process_num)d
numprocs=3
autostart=false
EOS

# 反映
sudo supervisorctl update

# サービスの開始
sudo supervisorctl start all

# サービスの再起動
sudo supervisorctl restart all

# サービスの停止
sudo supervisorctl stop all

pm2

  • http://pm2.keymetrics.io/
  • 基本的には Node.js のアプリ用
    • だけど Node.js 以外にも使える
  • 設定ファイルの instances でプロセス数を指定可能
  • モニタとかデプロイ?とか多機能?
    • 使わなさそう
  • 一般ユーザーで pm2 コマンド実行するとデーモンが立ち上がってしまう
    • ~/.pm2 が pm2 のデータディレクトリになっているため
# インストール
sudo yum install nodejs npm
sudo npm install -g pm2

# systemd のユニットとして登録
sudo pm2 startup systemd

# サービスの設定
cat <<'EOS'> app.json
[
  {
    "name"      : "w1",
    "script"    : "w1.sh",
    "exec_mode" : "fork_mode",
    "instances"  : "2"
  },
  {
    "name"      : "w2",
    "script"    : "w2.sh",
    "exec_mode" : "fork_mode",
    "instances"  : "3"
  }
]
EOS

# サービスの登録と開始
sudo pm2 start app.json

# サービスの一覧表示
sudo pm2 list

# サービスの停止
sudo pm2 stop all

# サービスの開始
sudo pm2 start all

# サービスの再起動
sudo pm2 restart all

# サービスの削除
sudo pm2 delete all

forever

  • https://github.com/foreverjs/forever
  • 基本的には Node.js のアプリ用
    • だけど Node.js 以外にも使える
  • 1サービス=1プロセスが基本
    • 複数プロセスを起動したければその数だけサービスを定義する必要がある
    • もしくはサービスとして起動したプロセスでさらにプロセスマネージャーみたいにするか
# インストール
sudo yum install nodejs npm
sudo npm install -g forever

# サービスの設定
cat <<'EOS' > forever.json
[
  {
    "command": "/bin/bash",
    "script": "w1.sh"
  },
  {
    "command": "/bin/bash",
    "script": "w1.sh"
  },
  {
    "command": "/bin/bash",
    "script": "w2.sh"
  },
  {
    "command": "/bin/bash",
    "script": "w2.sh"
  },
  {
    "command": "/bin/bash",
    "script": "w2.sh"
  }
]
EOS

# サービスの開始
sudo forever start forever.json

# サービスの一覧表示
sudo forever list

# サービスの再起動
sudo forever restartall

# サービスの停止
sudo forever stopall

foreman

  • https://ddollar.github.io/foreman/
  • 他の類似ツールの設定ファイルをエクスポートできる
    • supervisord とか upstart とか systemd とか
  • コマンドラインオプションでプロセス数を指定可能
  • respawn はしない
    • 複数プロセスのどれかが死ぬと全部死ぬ
# インストール
sudo yum -y install ruby rubygems
sudo gem install foreman

# サービスの設定
cat <<EOS> Procfile
w1: ./w1.sh
w2: ./w2.sh
EOS

# サービスの開始(Ctrl+C で終了)
foreman start -c -m w1=2,w2=3

# いろいろなサービス管理ツールの設定にエクスポート
foreman export -c w1=2,w2=3 supervisord ./supervisord
foreman export -c w1=2,w2=3 systemd ./systemd
foreman export -c w1=2,w2=3 upstart ./upstart

systemd

  • systemd でもテンプレートユニットを使えば同じコマンドを複数プロセス起動できそう
sudo tee /etc/systemd/system/app.target <<EOS
[Unit]
StopWhenUnneeded = true
Wants = \
  w1.target \
  w2.target

[Install]
WantedBy = multi-user.target
EOS

sudo tee /etc/systemd/system/w1.target <<EOS
[Unit]
StopWhenUnneeded=true
PartOf = app.target
Wants = \
  w1@1.service \
  w1@2.service
EOS

sudo tee /etc/systemd/system/w2.target <<EOS
[Unit]
StopWhenUnneeded=true
PartOf = app.target
Wants = \
  w2@1.service \
  w2@2.service \
  w2@3.service
EOS

sudo tee /etc/systemd/system/w1@.service <<EOS
[Unit]
Description = w1
After=network.target
PartOf = w1.target

[Service]
ExecStart = /vagrant/w1.sh
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target
EOS

sudo tee /etc/systemd/system/w2@.service <<EOS
[Unit]
Description = w2
After=network.target
PartOf = w2.target

[Service]
ExecStart = /vagrant/w2.sh
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target
EOS

sudo systemctl daemon-reload

# サービスの開始
sudo systemctl start app.target

# サービスの一覧
sudo systemctl list-units "w[12]@*"

# w1 だけ再起動
sudo systemctl restart w1.target

# サービスを停止
sudo systemctl stop app.target

まとめ

  • daemontools はたくさん設定するのが辛い
  • supervisor は良さそう
  • pm2 と forever は Node.js って感じある
    • Node.js 以外でも使えるけどあんまり使われてなさそうな、雰囲気を感じる
  • foreman は respawn しないのが辛そう
    • systemd が foreman を respawn するにせよ
    • 1 プロセス死んだだけで全部が再起動されるのは過剰では?
    • そもそも他とくらべてツールの系統が違う
      • foreman はシステムに 1 つのサービスマネージャーってわけではない
      • 他はシステムに 1 つのサービスマネージャーって感じ
      • 次のような複数の foreman を起動する構成を考えるとこれはこれでありかも?
        • systemd
          • foreman (serviceA) -> serviceA
          • foreman (serviceB) -> serviceB
      • 他のツールだと次のようになって systemd から見ると 1 つのサービスに見える
        • systemd
          • supervisor
            • serviceA
            • serviceB
  • systemd でできるなら systemd でもいいんじゃないか?
    • テンプレートユニットや PartOf や Wants を使えばなんとかなりそう
    • プロセスの数だけ Wants に羅列する必要があるのは辛いか

ほかのツールみたいなおもしろ機能なないけれども(WebUI とか)、どうせ systemd は居るわけなので、systemd でやるのがよいだろうか。

Munin で PING 監視する

Munin マスターで次のようにプラグインのシンボリックリンクを作成します。

sudo ln -s /usr/share/munin/plugins/ping_ /etc/munin/plugins/ping_192.168.33.10

プラグインを実行してみます。

munin-run ping_192.168.33.10

パケットロスト率と応答時間が結果として得られます。

packetloss.value 0
ping.value 0.000605

なお、100% ロストすると ping.value が結果に現れませんでした。

packetloss.value 100

プラグインのコンフィグ確認します。

munin-run ping_192.168.33.10 config

次のように結果が得られます。

graph_title IPv4 ping times to 192.168.33.10
graph_args --base 1000 -l 0
graph_vlabel roundtrip time (seconds)
graph_category network
graph_info This graph shows ping RTT statistics.
ping.label 192.168.33.10
ping.info Ping RTT statistics for 192.168.33.10.
packetloss.label packet loss
packetloss.graph no

パケットロスト率はグラフ無しのようです。また、hostname が含まれていないため、このままだと Munin マスターの localhost への監視として表示されてしまうので、プラグインのコンフィグ(/etc/munin/plugin-conf.d/)で次のようにホスト名を指定します。

[ping_192.168.33.10]
host_name web-ping
env.ping_args -c 3 -w 5
env.packetloss_critical 50

さらに、マスターの設定(/etc/munin/conf.d/)で、↑で指定したホストの設定を追加します。

# 内部監視
[example;web]
address 192.168.33.10
use_node_name yes

# 外形監視
[example;web-ping]
address 127.0.0.1
use_node_name no

このとき、もともと設定している内部監視のホスト名(↑の例では web)とは異なる名前にする必要があります。

munin-node をリスタートして反映します。

sudo systemctl restart munin-node.service

これで PING 監視も行われるようになりますが、内部監視と外形監視を別々のホストとして設定しているので Munin の画面上でも別々にされてしまいます。

01.png

あるいは、次のようにグラフ無しにしてしまう?

[example;web-ping]
address 127.0.0.1
use_node_name no
graph no

が、これだと Web 画面から直近の結果すら確認できないし、アラートが発生しても Problems のところに何も表示されなくて不便です。


なんとなく、これまで Nagios で 1 つのホストに対して外形監視と内部監視をまとめて設定するようにしてきたので(check_nrpecheck_pingcheck_http が 1 つのホストの設定に混在)、Munin で別々のホストとして表示されてしまうことに違和感があるのですが・・・むしろ外形監視は特定のホストには紐つかないものとして、例えば external のような仮のホストへの監視、みたいにすると良いのでしょうか。

設定ファイル的には次のような感じ。

/etc/munin/plugin-conf.d/

[ping_192.168.33.*]
host_name external
env.ping_args -c 3 -w 5
env.packetloss_critical 50

/etc/munin/conf.d/

[example;external]
address 127.0.0.1
use_node_name no

画面的には次のような感じ。

02.png

あるいは、そもそも Munin で PING 監視とかはせずに Nagios とかと併用するのが良いような気もする。

Munin をさらに触ってみた

社内で使っているサーバに munin-node を入れてリソース監視するようにしてみたときのメモ。

最初にちょっと触ってみたときの内容は↓こちら。

Munin マスターの設定ファイルはなるべく小さくした

Munin マスターの設定ファイルには監視対象の Munin ノードの情報を記述する必要がありますが、下記のように、グループ・ノード名、IP アドレス、などの、どうしても必要そうなものだけを記述しました。

[are-servers;sore-server]
use_node_name yes
address 192.0.2.123

通知の閾値は、後述の通り Munin ノード側のプラグインの設定で指定することができます。

通知先は、Munin マスターの設定ファイルのグローバルセクションで contact を登録しているだけです。

contact.mail-ore.command mail -s "Munin ${var:group}::${var:host}" -r sys+munin@example.com ore@example.com
contact.mail-ore.max_messages 10
contact.mail-ore.always_send critical

デフォルトの通知先は登録されている contact すべてなので、ノードごとやグループごとに通知先を変えたりしないのであれば、これだけで大丈夫です。

がしかし、実際のプロジェクトではグループで通知先を変える必要がありそうです。

[are-servers;]
contacts mail-ore

[are-servers;sore-server]
use_node_name yes
address 192.0.2.123

あと、warningcritical で通知先を変更したりはできないものなの?(ググるとよく出てくる always_send はそういう意味の設定じゃないはず)

Munin ノードの設定ファイルで通知の閾値を指定

通知の閾値の指定は、ググると大抵 Munin マスターの設定ファイルで指定している例が出てくるのですが、Munin ノードのプラグイン設定ファイルでも指定できます。

/etc/munin/plugin-conf.d/zz-misc.conf

[load]
env.load_warning 3
env.load_critical 6

Munin ノードでプラグインを config で実行すると、この値が確認できます。

munin-run load config | grep -E '(warning|critical)'
#=> load.warning 3
#=> load.critical 6

Munin マスターで同じフィールドに閾値を指定していなければ、この値が閾値として使用されます(マスターで閾値を指定するとそっちが優先されます)。

メモリ使用量の閾値を設定する項目

Cacti の SNMP での監視と比べると、メモリ使用量のグラフが細かく、積み上げグラフと折れ線グラフが一緒になっているため、どれに閾値を設定すれば良いのか判りにくいです。

vim /usr/share/munin/plugins/memory 曰く、データソースは /proc/meminfo で、積み上げグラフの計算式とその意味は次の通り。

  • apps
    • MemTotal - MemFree - Buffers - Slab - PageTables - SwapCached
  • page_tables
    • PageTables
  • swap_cache
    • SwapCached
  • slab_cache
    • Slab
  • cache
    • Cached
  • buffers
    • Buffers
  • unused
    • MemFree
  • swap
    • SwapTotal - SwapFree

unused だとページキャッシュでいずれ 0 に近づいてしまうだろうし、swap も頻繁にスワップイン/アウトしているならともかく単にスワップしているだけなら問題ないこともあるだろうので、閾値を設定するとしたら apps が一番それぽいでしょうか?

また、メモリ使用量の閾値は % でも指定することができます。swap の場合は SwapTotal に対するパーセンテージで、その他は MemTotal に対するパーセンテージです。

env.apps_warning 80% # 80% 以下を正常値とする
env.swap_warning 50% # 50% 以下を正常値とする

なお、Web 画面や通知のメールの中ではバイト数に計算されたものになるので、とても判りにくいです。

MySQL と nginx の監視

MySQL や nginx のメトリクスもデフォで対応しています。

MySQL の監視を有効にするためには perl の DBD::MySQLCache::Cache が必要です。

yum install perl-DBD-MySQL perl-Cache-Cache

nginx の監視を有効にするためには /nginx_statusstub_status を有効にする必要があります。

location /nginx_status {
    stub_status on;
    access_log off;
    allow 127.0.0.1;
    deny all;
}

プラグインの自動有効化

munin-node-configure で、インストールされているプラグインの一覧を表示したり、利用可能なプラグインを表示したり、利用可能なプラグインを有効にするためのコマンドを一覧表示することができます。

インストールされているプラグインと有効/無効を表示

$ munin-node-configure
Plugin                     | Used | Extra information
------                     | ---- | -----------------
acpi                       | no   |
 :
cpu                        | yes  |
 :
if_                        | yes  | eth1
if_err_                    | no   |
 :

利用可能かどうかも一緒に表示する

$ munin-node-configure --suggest
Plugin                     | Used | Suggestions                            
------                     | ---- | -----------                            
acpi                       | no   | no [cannot read []
 :
cpu                        | yes  | yes
 :
if_                        | yes  | yes (+eth0 -eth1)
if_err_                    | no   | yes (+eth0)

プラグインを利用可能にするコマンドを表示する

$ munin-node-configure --shell
ln -s '/usr/share/munin/plugins/if_' '/etc/munin/plugins/if_eth0'
ln -s '/usr/share/munin/plugins/if_err_' '/etc/munin/plugins/if_err_eth0'

--remove-also を付けると利用不可能になったプラグインの削除もできます。

$ munin-node-configure --shell --remove-also
ln -s '/usr/share/munin/plugins/if_' '/etc/munin/plugins/if_eth0'
ln -s '/usr/share/munin/plugins/if_err_' '/etc/munin/plugins/if_err_eth0'
rm -f '/etc/munin/plugins/if_eth1'

プラグインのファミリ

munin-node-configure--families で対象となるプラグインの種類をしていできます。

families には次のようなものがあります(参考)。

  • auto
    • munin-node-configure で自動で有効にできるプラグイン
  • snmpauto
    • --snmp オプション付きで自動で有効にできるプラグイン
  • manual
    • 手動で有効にするプラグイン
  • contrib
    • いわゆる contrib なプラグイン

未指定の場合は、他に指定されているオプションによって対象となる種類が変わります。

  • なし
    • auto,contrib,manual
  • --suggest--shell
    • auto
  • --snmp
    • snmpauto

つまり、--families は指定しなくても概ね良きに計らってくれます。

プロセス監視

プロセス監視には ps_multips が使えますが、どちらも微妙な感じでした。

  • multipspgrep -f -l <name> | grep <regex> | wc -l の結果がメトリクス
    • がしかし pgrep -f -l の結果が CentOS 6 と CentOS 7 で違う
    • CentOS 6 なのか CentOS 7 なのか意識して設定しなければならない
  • ps_family=auto なプラグイン
    • munin-node-configure --shell --remove-also で自動で追加/削除される
    • と言いたいところだが ps_ は未対応
    • そのため、手動で配置しても munin-node-configure で消されてしまう
    • family=manual の間違いなのでは?
    • あるいは autoconf=no なら --remove-also の対象外になるべきなのでは?
    • munin-node-configure 使わずに個別に追加/削除すればいいのだけどうーん

その他

  • Munin ノードのセットアップは Ansible だけで余裕
    • サーバがたくさんあっても自動化で楽勝
    • Munin マスターも同じインベントリに入れれば、マスターへの監視対象の追加も自動化余裕ですが・・・
      • 監視サーバを複数のプロジェクトで共用する文化なので同じインベントリに含めるのは難しそう
  • Zabbix や Sensu なら監視対象の自動登録もできるのでちょっと楽
    • ただまあオートスケールとかでぼこぼこ増えたり減ったりするんじゃないなら Munin でも十分かな
  • やっぱり Cacti と比べてグラフの閲覧がしょぼい感
    • /var/lib/munin/datafile とかをどうにかして自前で閲覧画面は作れないかな?

Munin を触ってみた

Mackerel のような SaaS のリソース監視サービスが流行ってそうな中、あえていまさら Munin を触ってみました。

ところで Munin のドキュメント、公式っぽいものが下記の 2 箇所にあるっぽいんですけど、どういうことなの?

Attention: All content still relevant for Munin 2.x will be moved from here to Munin Guide. Pages that are in transit or have already moved, will get an info box on top (like here), be set to "Read only" and later will be archived or purged.

移行中? らしいです。とりあえず後者の方が新しいっぽいです。

用語とか

  • Munin マスター
    • 監視する側
    • Munin ノードから定期的にメトリクスを取得して RRD に保存
    • 閾値をチェックして必要なら通知する
    • 閲覧用の Web 画面
  • Munin ノード
    • 監視される側
    • エージェントとして munin-node が動く
  • Munin プラグイン
    • Munin ノードでメトリクスを取得するスクリプト
    • SNMP だと別のホストの SNMP エージェントからメトリクスを取得することもある

とりあえず使ってみる

Vagrantfile

とりあえず使ってみるために、次のような Vagrant 環境を使います。

Vagrant.configure(2) do |config|
  config.vm.box = "bento/centos-7.1"
  config.vm.define "web" do |config|
    config.vm.hostname = "web"
    config.vm.network "private_network", ip: "192.168.33.10", virtualbox__intnet: "munin"
  end
  config.vm.define "munin" do |config|
    config.vm.hostname = "munin"
    config.vm.network "forwarded_port", guest: 80, host: 1234
    config.vm.network "private_network", ip: "192.168.33.11", virtualbox__intnet: "munin"
  end
  config.vm.provision "shell", inline: <<-SHELL
    sudo yum -y install vim-enhanced mailx nc
  SHELL
  config.vm.provider :virtualbox do |v|
    v.linked_clone = true
  end
end

Munin ノード

監視される側です。munin-node を epel からインストールします。

sudo yum -y install epel-release
sudo yum -y install munin-node

設定ファイルを編集します。

sudo vim /etc/munin/munin-node.conf

下記を追記して監視する側(Munin マスター)からのアクセスを許可します。

cidr_allow 192.168.33.11/32

munin-node を開始します。

sudo systemctl enable munin-node
sudo systemctl start  munin-node
sudo systemctl status munin-node

試しにロードアベレージを取得してみます。munin-run コマンドでローカルの munin-node から情報を取得できます。

munin-run load

次のようにロードアベレージの値が表示されます。

load.value 0.30

TCP の 4949 ポートでもアクセスできるはずなので試してみます。

echo "fetch load" | nc localhost 4949

次のようにロードアベレージの値が表示されます。

# munin node at localhost.localdomain
load.value 0.30
.

Munin マスター

監視する側です。munin と Web 画面のために Apache もインストールします。

Apache を先にインストールしないと cgi がログの書き込みでパーミッションエラーになったので、同時にインストールしないほうが良いです。

sudo yum -y install epel-release
sudo yum -y install httpd
sudo yum -y install munin

マスターでも munin-node を開始します。munin-node は監視対象のホストで実行するものですが、マスターそのもののリソース監視も行いたいのと、後述する SNMP 監視も試すので、munin-node をマスターでも実行します。

sudo systemctl enable munin-node
sudo systemctl start  munin-node
sudo systemctl status munin-node

Apache を開始します。

sudo systemctl enable httpd
sudo systemctl start  httpd
sudo systemctl status httpd

Basic認証の ID/PW を設定します。

sudo htpasswd -bc /etc/munin/munin-htpasswd munin pass

設定ファイルを作成します。

sudo vim /etc/munin/conf.d/example.conf

このディレクトリに置いた設定ファイルは自動的に読み込まれます(/etc/munin/munin.confincludedir /etc/munin/conf.d のように指定されています)。

次のように監視対象のノードを追記します。

[example;web.example.com]
address 192.168.33.10
use_node_name yes

TCP 経由で監視対象ノードから監視項目の値が取れることを確認します。 もし、値が取れなければ何かが間違っています。

echo "fetch load" | nc 192.168.33.10 4949

手動で munin-cron を実行します。

sudo -u munin munin-cron

ブラウザで http://localhost:1234/munin/ を開くと Basic 認証が聞かれるので munin:pass を入力すると Munin の画面が表示されます。

通知

監視項目に閾値を設定して、閾値を超えらたメールで通知されるように設定してみます。

Munin マスターで監視の設定を変更します。

sudo vim /etc/munin/conf.d/example.conf

次のように変更します。

contact.ore-no-mail.command mail -s "Munin ${var:group}::${var:host}" -r munin@example.com ore@example.com
contact.ore-no-mail.always_send critical

[example;web.example.com]
address 192.168.33.10
use_node_name yes
cpu.user.critical 50
contacts ore-no-mail

Munin ノードでCPU使用率を高めてみます。

while :; do :; done

しばらく待つと次のような通知が飛んできます

example :: web.example.com :: CPU usage
    CRITICALs: user is 100.00 (outside range [:50]).

Munin マスターの Web 画面でも Critical として表示されます。


監視の設定をざっくり説明します。

contact.ore-no-mail.command mail -s "Munin ${var:group}::${var:host}" -r munin@example.com ore@example.com
  • ore-no-mail という名前で通知先を設定します
  • mail から先は通知時に実行するコマンドです
  • ${var:host} のような変数名が使用可能です
contact.ore-no-mail.always_send critical
  • 常に通知する障害レベルを指定します
  • この例では critical のみが常に通知されます
  • 前回と状態が変わっていなくても通知するという意味です
  • つまり閾値を超えている間、通知され続けます
  • デフォルトは未設定です
  • ググると出てくる日本語の説明は間違っているものが多い気がします
[example;web.example.com]
:
cpu.user.critical 50
  • このノードの cpu プラグインの user フィールドの critical の閾値を 50 にします
[example;web.example.com]
:
contacts ore-no-mail
  • このノードの通知先として ore-no-mail を設定します
  • デフォルトは contact で設定されている通知先全部です

SNMP で監視

Munin ノードに net-snmp をインストールします。

sudo yum -y install net-snmp net-snmp-utils

それっぽく設定します。

sudo tee <<'EOS' /etc/snmp/snmpd.conf
com2sec s_default default oreore
group g_all_ro v1  s_default
group g_all_ro v2c s_default
view v_all included .1
access g_all_ro "" any noauth exact v_all none none
load 12 14 14
EOS

snmpd を起動します。

sudo systemctl enable snmpd.service
sudo systemctl start snmpd.service

Munin マスターに net-snmp-utils を入れます。

sudo yum -y install net-snmp-utils

マスターからノードの snmpd にアクセスできることを確認します。

snmpwalk -v1 -c oreore 192.168.33.10 la

/etc/munin/plugin-conf.d/zzz-snmp に SNMP のコミュニティ名などを設定します。 このディレクトリのファイルは自動で全部読まれるのでファイル名はなんでもいいです。

sudo tee <<'EOS' /etc/munin/plugin-conf.d/zzz-snmp
[snmp_192.168.33.10_*]
env.community oreore
env.version 1
EOS

この後 Munin マスターの /etc/munin/plugins/ にプラグインのシンボリックリンクを作るのですが、munin-node-configure を使うと設定可能な項目を一覧表示できます。

munin-node-configure --snmp 192.168.33.10 --snmpcommunity oreore
Plugin                     | Used | Suggestions
------                     | ---- | -----------
snmp__cpuload              | no   | yes (+192.168.33.10)
snmp__df                   | no   | yes (+192.168.33.10)
snmp__df_ram               | no   | yes (+192.168.33.10)
snmp__fc_if_               | no   | no
 :

--shell オプションを付けると /etc/munin/plugins/ へシンボリックリンクを作成するためのコマンドが表示されます。

munin-node-configure --snmp 192.168.33.10 --snmpcommunity oreore --shell
ln -s '/usr/share/munin/plugins/snmp__cpuload' '/etc/munin/plugins/snmp_192.168.33.10_cpuload'
ln -s '/usr/share/munin/plugins/snmp__df' '/etc/munin/plugins/snmp_192.168.33.10_df'
ln -s '/usr/share/munin/plugins/snmp__df_ram' '/etc/munin/plugins/snmp_192.168.33.10_df_ram'
 :

これを bash にパイプすればシンボリックリンクが作成されます。

munin-node-configure --snmp 192.168.33.10 --snmpcommunity oreore --shell | sudo bash -x

munin.conf を編集します。

sudo vim /etc/munin/conf.d/example.conf

次のように追記します。マスターから見てローカルの munin-node が SNMP で監視対象のノードから情報を取得するので address127.0.0.1 です。セクションの 192.168.33.10 が SNMP で接続する先です。

[example;192.168.33.10]
address 127.0.0.1

マスターの munin-node を再起動します。

sudo systemctl restart munin-node

munin-cron を手動で実行します。

sudo -u munin munin-cron

ブラウザで見てみると snmp で取得した項目が増えています。

この手順だと 192.168.33.10 という名前の名前で Munin の画面上で表示されますが、次のように設定すると任意の名前にできます。

/etc/munin/plugin-conf.d/zzz-snmp

[snmp_192.168.33.10_*]
env.community oreore
env.version 1
host_name web-by-snmp

/etc/munin/conf.d/example.conf

[example;web-by-snmp]
address 127.0.0.1

SNMP の監視で通知を設定するときは、プラグイン名の ._ に読み替えて記述します。例えば、ロードアベレージなら次のように指定します。

[example;web-by-snmp]
address 127.0.0.1
snmp_192_168_33_10_load.load.critical 3
contacts ore-no-mail

snmp_192_168_33_10_load という名前は、Munin の Web 画面でそのグラフを表示したときの URL に、load の方は、その画面の下の方にあるテーブルの Internal name です。

Native SSH Transport

Munin マスターからノードへの接続を SSH にすることができます。

Munin マスターから Munin ノードへ munin アカウントでログインすることになるのですが、パッケージインストール時のデフォだと munin アカウントのログインシェルが /sbin/nologin になっていて SSH でのログインが不可能です。

なので、Munin ノードの munin アカウントのログインシェルを chsh で適当なシェルに変更します。

sudo chsh -s /bin/bash munin

Munin ノードで公開鍵を配置するディレクトリを作成してそれっぽくパーミッションやオーナーを設定します。

sudo mkdir /var/lib/munin/.ssh
sudo touch /var/lib/munin/.ssh/authorized_keys
sudo chmod 700 /var/lib/munin/.ssh
sudo chmod 600 /var/lib/munin/.ssh/authorized_keys
sudo chown -R munin. /var/lib/munin/.ssh

Munin マスターで鍵ペアを作成して公開鍵を Munin ノードに転送します。

sudo -u munin -H ssh-keygen
sudo cat /var/lib/munin/.ssh/id_rsa.pub |
  ssh vagrant@192.168.33.10 sudo -u munin tee /var/lib/munin/.ssh/authorized_keys

Munin マスターからノードにログインできることを確認します。

sudo -u munin -H ssh 192.168.33.10 uname -n

Munin マスターでノードへの接続設定を変更します。

sudo vim /etc/munin/conf.d/example.conf

次のように変更します。

[example;web.example.com]
address ssh://192.168.33.10 /bin/nc 127.0.0.1 4949
use_node_name yes

Munin マスターで munin-cron を手動で実行します。

sudo -u munin munin-cron

Munin ノードでログを確認してみます。

tail /var/log/munin-node/munin-node.log

ローカルホストからの接続になっています。

2015/04/23-21:39:06 CONNECT TCP Peer: "[127.0.0.1]:49566" Local: "[127.0.0.1]:4949"

.

うーん? SSH しかポートが空いてないとか、中継サーバを経由しないとアクセスできないとかの場合に使うものなのでしょうか。

監視間隔を変更

デフォだと監視間隔は 5 分なので 1 分に縮めてみます。

Munin マスターの設定を変更します。

sudo vim /etc/munin/munin.conf

次のように追記します。

update_rate 60

cron の設定も変更します

sudo vim /etc/cron.d/munin

元は 5 分ごとになっているので、1 分ごとに変更します

*/1 * * * *     munin test -x /usr/bin/munin-cron && /usr/bin/munin-cron

crond をリロードして設定を反映します。

sudo systemctl reload crond.service

これで 1 分ごとに監視されるようになりました。

と、言いたいところですが、1回でも munin-cron を実行したことがあると変更しても意図したとおりにはならなさそうです。

どうしても変更したければ rrd ファイルの変換が必要なようです。

update_rate は man しても出てこないので使えないのかなと思ったのですが、このような事情によりアンドキュメントなのかもしれません(適当)。

監視項目の削除

/etc/munin/plugins にあるシンボリックリンクが、そのホストの監視項目(プラグイン)です。

なので /etc/munin/plugins にあるシンボリックリンクを削除すると、その項目は監視されなくなります。

プラグインの実体は /usr/share/munin/plugins/ にあります。

例えば Postfix の監視を削除してみます。

sudo rm /etc/munin/plugins/postfix_mail*
sudo systemctl restart munin-node

これで、Postfix に関する監視は行われなくなります。

もしくは /etc/munin/munin-node.confignore_file で無視するプラグインを指定できます。

sudo vim /etc/munin/munin-node.conf

次のように正規表現で指定します。

ignore_file ^postfix_

ファイル名がアンスコで終わるプラグインは、リンク名が引数として使用されます。

例えば /usr/share/munin/plugins/if_/etc/munin/plugins/if_enp0s3 という名前のシンボリックリンクになっていますが、この場合 enp0s3 がプラグインの引数として用いられます。

また、SNMP 関連のプラグインは、プラグイン名にアンスコが 2 つ含まれており、その部分に SNMP エージェントのホスト名や IP アドレスが入ります。

例えば、/usr/share/munin/plugins/snmp__cpuloadsnmp_192.168.33.10_cpuload のようにリンクされます。

プラグインの作成

自分でプラグインを作ってみます。Munin ノードで次のようにスクリプトを作成します。

sudo vim /usr/local/bin/ore.sh
#!/bin/bash
if [ "$1" = "autoconf" ]; then
    echo yes
    exit 0
fi

if [ "$1" = "config" ]; then
    echo 'graph_title ore no title'
    echo 'graph_args --base 1000 -l 0'
    echo 'graph_vlabel ore'
    echo 'graph_scale no'
    echo 'graph_category oreore'
    echo 'are.label are'
    echo 'are.min 0'
    echo 'are.draw AREA'
    echo 'are.type GAUGE'
    exit 0
fi

echo "are.value 25"

プラグインのディレクトリにシンボリックリンク作成します。

sudo chmod +x /usr/local/bin/ore.sh
sudo ln -s /usr/local/bin/ore.sh /etc/munin/plugins/ore

munin-run でプラグインを実行してみます。

munin-run ore

次のように値が得られます。

are.value 25

ノードの munin-node を再起動します。

sudo systemctl restart munin-node.service

しばらく待ってから Munin マスターをブラウザで表示すると項目が増えています。

テンプレートを変更

Bootstrap ベースのテンプレートに差し替えます。Munin マスターで munin-monitoring/contrib をダウンロードします。

mkdir /tmp/munin-contrib
wget https://github.com/munin-monitoring/contrib/archive/master.tar.gz -O - |
  tar xzf - -C /tmp/munin-contrib --strip-components=1

テンプレートと静的ファイルを Munin のディレクトリに上書きします。

sudo rsync -av /tmp/munin-contrib/templates/munstrap/templates/ /etc/munin/templates/
sudo rsync -av /tmp/munin-contrib/templates/munstrap/static/    /etc/munin/static/

既に作成されているファイルを削除して munin-cron を手動実行します。

sudo rm -rf /var/www/html/munin/*
sudo -u munin munin-cron

Munin マスターをブラウザで表示すると見た目が Bootstrap 風になっています。

CGI

デフォでは munin-cron の実行時に HTML やグラフ画像が作成されています。

ll /var/www/html/munin/example/web.example.com

監視対象が増えてくるととても重いので、CGI でオンデマンドに HTML やグラフ画像が作成されるように変更します。

まず、Munin マスターに munin-cgi をインストールします。

sudo yum -y install munin-cgi

Apache を再起動します。

sudo systemctl restart httpd

Munin の設定を変更します。

sudo vim /etc/munin/munin.conf

下記の箇所を変更します。cron ではなく cgi でグラフや html を作る、という意味です。

graph_strategy cgi
html_strategy cgi

生成されている HTML ファイルを削除して munin-cron を手動で実行します。

sudo rm -fr /var/www/html/munin/*
sudo -u munin munin-cron

生成されたファイルを確認してみると static しかないことがわかります。

ll /var/www/html/munin/

ブラウザで http://localhost/ を開くと(http://localhost/munin/ ではなく)、Munin の画面が表示されます。

雑感

お手軽に使えるのが良いですね。

  • インストールが簡単
    • epel から yum で入れられる
    • Cacti も yum で入れれるけど MySQL とかも必要だし
  • 監視サーバの設定が簡単
    • 監視サーバには設定ファイルを置くだけ
    • 逆に GUI での設定は無いけど困らない
    • Cacti だと GUI なのでかなり辛い(CLI もあるけど使いにくい)
  • 監視対象のサーバの設定も簡単
    • munin-node をインストールしてプラグインへのシンボリックリンクを作成するだけ
    • Cacti でも snmpd をインストール&設定するだけなので簡単といえば簡単
  • カスタム監視項目が簡単
    • 簡単なスクリプト1個で項目を追加できる
    • Cacti だと監視項目の追加がとてもつらい
  • SNMP にも対応
    • 既存のサーバやネットワーク機器の監視にも導入しやすい
  • 閾値によるアラートをデフォで対応
    • Cacti でもプラグインでできるけどとても面倒です

ただ、Cacti と比べると閲覧画面の機能がかなり劣っているようにも感じました。

例えば Cacti だと次のようなことが出来たのですが、静的に出力するのが基本な Munin ではそういうのができなさそうです。

  • 表示するグラフの一覧を設定としてあらかじめ作成しておけたり
  • ホスト名の部分一致で条件指定してグラフをずらーっと並べたり
  • さらに任意の時間幅を指定してグラフをずらーっと並べたり

もうちょい突っ込んだ説明

munin.conf

munin.conf は下記の3種類のセクションを記述します。

  • 1つのグローバルセクション
  • ゼロ以上のグループのセクション
  • 1つ以上のホストのセクション

設定ファイルの構成は例えば次のようになります。

# グローバルセクション

[localhost]
# localhost グループの localhost ホストのセクション

[foo.example.com]
# example.com グループの foo.example.com ホストのセクション

[example.com;bar.example.com]
# example.com グループの bar.example.com ホストのセクション
# ↑と同じだがグループを明示的に指定している

[groupname;]
# groupname グループのセクション
# このグループすべてに適用される設定を記述できる

[groupname;baz.example.com]
# groupname グループの baz.example.com ホストのセクション

グループのセクションやホストのセクションには、次の3種類のディレクティブが記述できます。

  • ノードのディレクティブ
  • プラグインのディレクティブ
  • フィールドのディレクティブ

ノードのディレクティブは、これまでの例で書いてきた address とか use_node_name とかです。

プラグインのディレクティブは PLUGIN.DIRECTIVE <VALUE> の形式で記述します。例えば cpu.contacts ore-no-mail のように特定のプラグインで通知の宛先を指定することができます。

フィールドのディレクティブは PLUGIN.FIELD.DIRECTIVE <VALUE> の形式で記述します。例えば cpu.user.critical 50のように特定のプラグインの特定のフィールドの閾値を指定できます。

詳細は下記。

munin-node.conf

munin-node.confmunin-node (Munin のエージェント) の設定ファイルです。

アクセス制御とか無視するプラグインとかを設定できます。

詳細は下記。

plugin-conf.d

/etc/munin/plugin-conf.d にはプラグインの設定ファイルを設置します。

ここにはパッケージからインストールされたファイルが幾つか配置されています。それらのファイルを編集するとアップデートで上書きされてしまうかもしれないため、その代わりに zzz-myconf のようなファイルで設定を上書きします(ファイルはアルファベット順で読まれる)。

設定ファイルは[plugin-name] の形式でプラグインごとにセクションを記述します。プラグイン名の先頭または終端はワイルドカード * にすることができます(両方や中間は不可)。

上の例で行ったように、特定のホストの SNMP のコミュニティ名などを一括で設定したい場合は下記のようにワイルドカードで指定します。

[snmp_192.168.33.10_*]
env.community oreore
env.version 1

設定可能な項目は下記のガイドを参照してください。

env.var <variable content> でプラグインの環境変数を設定できます。どのようなものが設定できるかはプラグインによって異なります。詳細はプラグインのドキュメントで確認できます。

munindoc snmp__cpuload

Please see 'perldoc Munin::Plugin::SNMP' for further configuration information. とのことなので、

perldoc Munin::Plugin::SNMP

snmp 関連のプラグインの共通の情報が表示されました。

munin-cron

munin-cron は下記の 4 つのスクリプトを実行します。

  • /usr/share/munin/munin-update
    • メトリクス値を取得して保存
  • /usr/share/munin/munin-limits
    • 閾値をチェック
  • /usr/share/munin/munin-html
    • html を生成
    • html_strategycgi なら何もしません
  • /usr/share/munin/munin-graph --cron
    • グラフを生成
    • graph_strategycgi なら何もしません

munin-node が返すホスト名

1つの munin-node は複数のホストの情報を提供することができます。例えば↑で設定したような構成だと、Munin マスターで実行されている munin-nodelocalhost.localdomainweb-by-snmp の2つのホストの情報を提供しています。

$ echo nodes | nc localhost 4949 | tail -n +2
localhost.localdomain
web-by-snmp
.

この名前はプラグインに config を付けて実行すると host_name で得られます。

$ munin-run snmp_192.168.33.10_cpuload config | grep host_name
host_name 192.168.33.10

しかし snmp ではない普通のプラグインだと host_name が含まれません。

$ munin-run cpu config | grep host_name

この場合は munin-node そのものの名前になるのですが、その値は /etc/munin/munin-node.conf で指定されています。

host_name localhost.localdomain

もし未設定なら自動的にローカルホストの名前が解釈されます。

Munin マスターが munin-node から取得するデータのホスト名

Munin マスターの設定ファイルで次のように設定したとします。

[localhost]
address 127.0.0.1
use_node_name yes

[example;web.example.com]
address 192.168.33.10
use_node_name yes

[example;web-by-snmp]
address 127.0.0.1

角括弧 [...] で指定しているものは、Munin の Web 画面で表示されるグループ名やホスト名です。

address で指定しているものは、Munin マスターが接続する munin-node のアドレスです。

munin-node は複数のホストの情報を提供するので、Munin マスターが Munin ノードに接続した後、どのホストの情報が欲しいのかを指定する必要があります。

デフォルトでは角括弧 [...] で指定しているホスト名(セミコロン ; の右側)ですが、use_node_name yes の場合は munin-node への接続時に munin-node が名乗った名前をそのまま使います。

なので、

[localhost]
address 127.0.0.1
use_node_name yes

127.0.0.1munin-node に接続して、munin-node が名乗ったホスト名のデータを取得します。 取得したデータは localhost グループの localhost のものとして記録されます。

[example;web.example.com]
address 192.168.33.10
use_node_name yes

192.168.33.10munin-node に接続して、munin-node が名乗ったホスト名のデータを取得します。 取得したデータは example グループの web.example.com のものとして記録されます。

[example;web-by-snmp]
address 127.0.0.1

127.0.0.1munin-node に接続して、web-by-snmp のデータを取得します。 取得したデータは example グループの web-by-snmp のものとして記録されます。

参考になるリンク

正規表現再入門

最近ピザとかも出るようになった社内勉強会(仮)で発表した資料がでてきたので置いておきます。

先日、とあるサイトで再帰的パターンというものを知りまして、改めて PHP の PCRE のページを見てみると、知らない構文とか、知ってはいたけど全く使っていないなー、という構文が結構あったので、改めて一通り見てみました。

その中から、わりと知られていなさそうなものをピックアップしてみました。


ここからの内容は reveal.js でスライド表示されている Markdown ファイルをそのまま張り付けています。

スライドとして見る場合は↑の方にリンクを置いているのでそこから見てください。

.

.

.

正規表現再入門


PHP の PCRE のページ、よく見たら知らないものが いろいろあったので、改めて一通り見てみました

http://php.net/manual/ja/reference.pcre.pattern.syntax.php


デリミタ


正規表現でデリミタといえば、

//

とか

##

のように同じ文字を最初と最後に使いますが


実は対応したカッコも使えます () とか {} とか [] とか <> とか

preg_match("(\d+)", 'abc123xyz', $m);
var_dump($m); // "123"

preg_match("{\d+}", 'abc123xyz', $m);
var_dump($m); // "123"

preg_match("[\d+]", 'abc123xyz', $m);
var_dump($m); // "123"

preg_match("<\d+>", 'abc123xyz', $m);
var_dump($m); // "123"

あんまり使いみちはなさそう


空白要素とコメント


x 修飾子を付けると空白要素やコメントを無視できます

$p = <<<'EOS'
# 英字
[a-zA-Z]+

# ドット
\.

# 数字
\d+
EOS;

preg_match("/$p/x", '123abc.789xyz', $m);
var_dump($m); // "abc.789"

次のように書いているのと同じです

preg_match("/[a-zA-Z]+\.\d+/", '123abc.789xyz', $m);
var_dump($m); // "abc.789"

超長い正規表現を書くときに便利そう


コメント


(?# から次の ) まではコメントになります(ネスト不可)

preg_match('/(?#これはコメントです)\d+/', 'abc123xyz', $m);
var_dump($m); // "123"

次のように書いているのと同じです

preg_match('/\d+/', 'abc123xyz', $m);
var_dump($m); // "123"

x 修飾子の方がいいと思う


言明


マッチ結果には含めずに直前や直後の文字をテストします

// 先読みの言明 ... 数字3つの後が "abc" である文字列にマッチ
preg_match_all('/\d{3}(?=abc)/', '123abc 789xyz', $m);
var_dump($m[0]); // "123"

// 先読みの否定言明 ... 数字3つの後が "abc" ではない文字列にマッチ
preg_match_all('/\d{3}(?!abc)/', '123abc 789xyz', $m);
var_dump($m[0]); // "789"

// 戻り読みの言明 ... 数字3つの前が "abc" である文字列にマッチ
preg_match_all('/(?<=abc)\d{3}/', 'abc123 xyz789', $m);
var_dump($m[0]); // "123"

// 戻り読みの否定言明 ... 数字3つの前が "abc" ではない文字列にマッチ
preg_match_all('/(?<!abc)\d{3}/', 'abc123 xyz789', $m);
var_dump($m[0]); // "789"

そこそこ使う


単語境界の言明


\b で単語の境界の言明

preg_match_all('/\b\d\w*/', '123abc abc456 abc/789 ', $m);
var_dump($m[0]); // "123abc" "789"
  • "123abc""1" が文字列の先頭で単語の始まりなのでマッチする
  • "abc456""4" が単語の始まりじゃないのでマッチしない
  • "abc/789""/" があるので "7" が単語の始まりになりマッチする

要するに (?<!\w)(?=\w) だと思う

preg_match_all('/(?<!\w)(?=\w)\d\w*/', '123abc abc456 abc/789 ', $m);
var_dump($m[0]); // "123abc" "789"

直前が \w ではなく、かつ、\w にマッチ


知ってたけど使ったこと無い


マッチ結果の位置のリセット


マッチ結果の開始位置を \K の位置にリセットする つまり \K より前の文字がマッチ結果に含まれなくなる

preg_match('/abc\Kxyz/', 'abcxyz', $m);
var_dump($m); // "xyz"

正規表現は "abcxyz" にマッチしているけど結果は "xyz" だけ


ただしサブパターンによるキャプチャには影響しない

preg_match('/(abc\Kxyz)/', 'abcxyz', $m);
var_dump($m); // "xyz", "abcxyz"

あんまり使いみちはなさそう


名前付きサブパターン


サブパターンに名前を付けてキャプチャ結果を連想配列にする

preg_match('/(?P<sub>\d+)/', 'abc123xyz', $m);
var_dump($m['sub']); // "123"

preg_match('/(?<sub>\d+)/', 'abc123xyz', $m);
var_dump($m['sub']); // "123"

preg_match("/(?'sub'\d+)/", 'abc123xyz', $m);
var_dump($m['sub']); // "123"

なにかの WAF で Routing に使われてたかも?


重複した後方参照番号


次の例だと (abc)(yz) は異なる数字添字

preg_match_all('/123(?:(abc)|x(yz))/', '123abc 123xyz', $m);
var_dump($m[1]); // "abc" ""
var_dump($m[2]); // "" "yz"

?| を使うと (abc)(yz) は同じ数字添字

preg_match_all('/123(?|(abc)|x(yz))/', '123abc 123xyz', $m);
var_dump($m[1]); // "abc" "yz"

知っていれば使うこともあるかも


独占的量指定子


量指定子の後に + を付けるとバックトラックしなくなる

// 貪欲的
preg_match('/\w*a/', '123abc123abc#123', $m);
var_dump($m); // "123abc123a"

// 非貪欲的
preg_match('/\w*?a/', '123abc123abc#123', $m);
var_dump($m); // "123a"

// 独占的
preg_match('/\w*+a/', '123abc123abc#123', $m);
var_dump($m); // no match

言葉で説明するのは難しい・・・


/\w*+a/\w* を最も長くマッチした後に a にマッチしなければテストは失敗します


日本語的に説明すると

  • 貪欲的
    • なるべく長くマッチしてダメだったら少し短くして再試行
  • 非貪欲的
    • なるべく短くマッチしてダメだったら少し長くして再試行
  • 独占的
    • ひたすら長くマッチしてダメだったら後は知らん

次のように独占的でもそうじゃなくても同じなら (マッチしないときの)性能の向上が見込める

// 貪欲的
preg_match('/\d*a/', '123123abc', $m);
var_dump($m); // "123123a"

// 非貪欲的
preg_match('/\d*?a/', '123123abc', $m);
var_dump($m); // "123123a"

// 独占的
preg_match('/\d*+a/', '123123abc', $m);
var_dump($m); // "123123a"

次のような使い方もできます

// .jpg で終わる連続する非空白要素にマッチ
preg_match_all('/\S++(?<=\.jpg)/', 'a.jp b.jpg c.jpghoge', $m);
var_dump($m); // "b.jpg"

次のようにしても結果は同じですけど

// .jpg で終わる連続する非空白要素にマッチ
preg_match_all('/\S+\.jpg(?!\S)/', 'a.jp b.jpg c.jpghoge', $m);
var_dump($m); // "b.jpg"

覚えておいて損はないかも


後方参照の相対指定


\g{-1} のように負数を指定すると相対で後方参照できる

preg_match('/(\d+)(\w+)\g{-1}\g{-2}/', '123abcabc123', $m);
var_dump($m); // "123abcabc123" "123" "abc"
  • \g{-1} は1つ前の (\w+) でマッチした文字列
  • \g{-2} は2つ前の (\d+) でマッチした文字列

これは次の例と同じです

preg_match('/(\d+)(\w+)\2\1/', '123abcabc123', $m);
var_dump($m); // "123abcabc123" "123" "abc"
  • \2 は2つ目のサブパターンの (\w+) でマッチした文字列
  • \1 は1つ目のサブパターンの (\d+) でマッチした文字列

あまり使わなさそう


再試行無しのサブパターン


(?> で始まるサブパターンは再試行されません

preg_match('/(?>\w*)a/', '123abc123abc#123', $m);
var_dump($m); // no match

preg_match('/(?>\d*)a/', '123123abc', $m);
var_dump($m); // "123123a"

独占的量指定子とあまり変わらないような気がします

preg_match('/\w*+a/', '123abc123abc#123', $m);
var_dump($m); // no match

preg_match('/\d*+a/', '123123abc', $m);
var_dump($m); // "123123a"

条件付きサブパターン


条件に応じてパターンを使い分けることができます

(?(条件)真パターン)
(?(条件)真パターン|偽パターン)

パターンには次のものが使えます。

  • 数字
    • その番号のサブパターンにマッチしていれば真
  • 言明
    • その言明にマッチすれば真
  • "R"
    • 再帰パターンに再帰していると真
    • 再帰していないトップレベルだと偽

良い例が思いつかないのでパス


再帰的パターン


こういうの

$str = "dummy(1+1), dummy(2+(3*4)), dummy(5-dummy(6*7)), dummy(2+((6/3)*(4-1)))";
$pattern = 'dummy (\( (?: [-+*\/0-9]++ | (?1) )* \))';
preg_match_all("/$pattern/x",$str,$match);

print_r($match[0]);
/*
Array
(
    [0] => dummy(1+1)
    [1] => dummy(2+(3*4))
    [2] => dummy(6*7)
    [3] => dummy(2+((6/3)*(4-1)))
)
*/

開き括弧と閉じ括弧の対応にマッチします


わかりにくいのでバラしてみます


  • dummy
  • ( ※1番目のサブパターンの開始
    • \( ※開き括弧
    • (?:
      • [-+*\/0-9]++ ※式っぽい文字の繰り返し
      • |
      • (?1) ※1番目のサブパターンに再帰
    • )
    • *
    • \) ※閉じ括弧
  • ) ※1番目のサブパターンの終了

  • (?R) でパターン全体に再帰
  • (?1) とか (?2) とかはサブパターンに再帰
  • (?P>name) とか (?&name) とかで名前付きサブパターンに再帰

回分にマッチする正規表現もできました

$pattern = '/(.)(?:(?R)|.)?\1/u';

$str = "キツツキがトマトを食べたらしんぶんしがたけやぶやけた";
preg_match_all($pattern, $str, $m);
var_dump($m[0]); // "キツツキ" "トマト" "しんぶんし" "たけやぶやけた"

$str = "ああ、みたいな2文字でも回文になってしまうので不完全";
preg_match_all($pattern, $str, $m);
var_dump($m[0]);

Wikipedia の回分のページにあった7文字以上の回分

$doc = new DOMDocument();
$doc->loadHTMLFile('https://ja.wikipedia.org/wiki/%E5%9B%9E%E6%96%87');
$xpath = new DOMXpath($doc);
$str = $xpath->query("id('mw-content-text')")[0]->textContent;

$pattern = '/(.)(?:(?R)|.)?\1/u';
preg_match_all($pattern, $str, $m);
$a = array_unique(array_filter($m[0], function ($s) { return mb_strlen($s) >= 7; }));

print_r($a);
// akasaka
// わかみかものとかなかとのもかみかわ
// もくよとんとことんとよくも
// しみしかししかしみし
// みなくさのなははくとしれくすりなりすくれしとくははなのさくなみ
// たのむそのいかにもにかいのそむのた
// さかのなはやとりたりとやはなのかさ
// の世しばしよしばし世の
// かなのよしはしよしはしよのなか
// まさかさかさま
// アニマルマニア
// スキトキメキトキス

"たいもくよとんとことんとよくもいた" のように上手くマッチしないものもあって不完全


正規表現・・・奥が深い


おわり

GlusterFS を使ってみた

先日、もう社内勉強会でいいや的な何か(仮)で、GlusterFS について話したりデモしたりしたときの資料が出てきたので置いておきます。

212 番煎じぐらいで真新しいものではありません。


GlusterFS とは

  • いわゆる分散ファイルシステム
    • 分散並列フォールトトレラントファイルシステム
    • POSIX互換(ファイルシステムとしてマウントできる)
  • 当初は Gluster, Inc. で開発されていたが Red Hat に買収された
    • RHEL では Red Hat Gluster Storage という名前になっている
  • ネームノードとかメタデータノードとか分散ロックマネージャーとかのようなノードが無い
    • クライアントががんばる
    • レプリケーションもクライアントががんばって全部に書き込んでいる
  • NFS のように普通のファイルシステムの上に分散ファイルシステムが構築される
    • 必要なメタ情報はファイルシステムの拡張属性に保存される
    • ので、下位ファイルシステムはある程度限定される(xfs/ext4/etc..)
    • ので、下位ファイルシステム上には分散ファイルシステム上のファイルがそのまま見える
  • レプリケーションやストライピングも可能
    • レプリケーションで HA にすることもできる
    • 復帰時の再レプリケーションも簡単
    • ストライピングは1つのファイルを分散配置できる
    • ファイル単位の分散配置もできる
  • ファイルシステムなのにユーザー空間で動く
    • マウントには fuse を使用
  • ライブラリ(libglusterfs)を用いて直接アクセスも可能
    • オーバーヘッドが少ない
  • NFS でもマウントできる
    • NFSv3 のみ
  • REST API でもアクセス可能
    • OpenStack Swift 互換らしい
  • 小さいファイルが大量にあるのは苦手
    • 大きなファイルが少量が得意
    • ストライプドじゃなくても大きいファイルは得意なのか?

用語とか

  • ブリック
    • GlusterFS が使う下位ファイルシステム上のディレクトリ
    • ファイルシステムは XFS を奨励
  • ボリューム
    • 複数のノードのブリックで構成された GlusterFS 上の仮想的なボリューム
  • ディストリビューテッドボリューム
    • 複数のブリックにファイルを分散して配置する
    • ファイル単位で配置が分散される(ストライピングではない)
  • レプリケーテッドボリューム
    • 複数のブリックに同じファイルを複製して配置する
    • 可用性を求めるなら必須
  • ストライプドボリューム
    • 複数のブリックに一つのファイルを分散して配置する
    • いわゆるストライピング
    • あまり奨励されていない?(実験的?)
    • 消し飛んでも構わないファイル用?
  • ヒーリングデーモン
    • レプリケーテッドで整合性が失われた時に自動的に復旧するためのデーモン
  • リバランス
    • ボリュームにブリックを追加/削除したときにファイルを再配置すること
    • 手動で実行する必要がある(自動でリバランスはされない)

インストール

CentOS のリポジトリに GlusterFS 関連のパッケージが幾つかあるのですが・・なぜか glusterfs-server が無いので使えません。

yum list | grep ^glusterfs
glusterfs.x86_64                        3.6.0.29-2.el7                 base     
glusterfs-api.x86_64                    3.6.0.29-2.el7                 base     
glusterfs-api-devel.x86_64              3.6.0.29-2.el7                 base     
glusterfs-cli.x86_64                    3.6.0.29-2.el7                 base     
glusterfs-devel.x86_64                  3.6.0.29-2.el7                 base     
glusterfs-fuse.x86_64                   3.6.0.29-2.el7                 base     
glusterfs-libs.x86_64                   3.6.0.29-2.el7                 base     
glusterfs-rdma.x86_64                   3.6.0.29-2.el7                 base 

なので、GlusterFS の yum リポジトリの設定をダウンロードします

cd /etc/yum.repos.d/
wget http://download.gluster.org/pub/gluster/glusterfs/LATEST/CentOS/glusterfs-epel.repo

サーバには glusterfs-server をインストールします

yum -y install glusterfs-server

クライアント(マウントする側)は glusterfs-fuse だけで十分です。

yum -y install glusterfs-fuse

下記のバージョンがインストールされました。

glusterfs.x86_64                  3.7.3-1.el7                         @glusterfs-epel
glusterfs-api.x86_64              3.7.3-1.el7                         @glusterfs-epel
glusterfs-cli.x86_64              3.7.3-1.el7                         @glusterfs-epel
glusterfs-client-xlators.x86_64   3.7.3-1.el7                         @glusterfs-epel
glusterfs-fuse.x86_64             3.7.3-1.el7                         @glusterfs-epel
glusterfs-libs.x86_64             3.7.3-1.el7                         @glusterfs-epel
glusterfs-server.x86_64           3.7.3-1.el7                         @glusterfs-epel

Vagrant box

ここまでの作業が適用された Vagrant box を作成します。

Vagrant.configure(2) do |config|
  config.vm.box = "ngyuki/centos-7"

  config.vm.provision "shell", inline: <<-SHELL
    cd /etc/yum.repos.d/
    wget http://download.gluster.org/pub/gluster/glusterfs/LATEST/CentOS/glusterfs-epel.repo
    yum -y install glusterfs-server glusterfs-fuse
    yum clean all
  SHELL

  config.vm.provider :virtualbox do |vb|
    file_to_disk = "#{ENV["HOME"]}/glusterfs.vdi"
    unless File.exist?(file_to_disk)
      vb.customize ['createhd', '--filename', file_to_disk, '--size', 20 * 1024]
      vb.customize ['storageattach', :id,
        '--storagectl', 'SATA Controller',
        '--port', 1,
        '--device', 0,
        '--type', 'hdd',
        '--medium', file_to_disk]
    end
  end
end

provision で glusterfs-server と glusterfs-fuse をインストールし、さらに GlusterFS のためのディスクを作成してアタッチしています。

glusterfs という名前で Box を作成&追加します。

vagrant up
vagrant package --output ~/glusterfs.box
vagrant box add glusterfs ~/glusterfs.box --force
rm -f ~/glusterfs.box
vagrant destroy -f

Vagrant up

次の Vagrantfile で4台の GlusterFS のノードと、GlusterFS をマウントする1台のクライアントを作成します。

Vagrant.configure(2) do |config|

  config.vm.box = "glusterfs"

  config.vm.define :g1 do |cfg|
    cfg.vm.hostname = "g1"
    cfg.vm.network "private_network", ip: "192.168.33.11", virtualbox__intnet: "glusterfs"
  end

  config.vm.define :g2 do |cfg|
    cfg.vm.hostname = "g2"
    cfg.vm.network "private_network", ip: "192.168.33.12", virtualbox__intnet: "glusterfs"
  end

  config.vm.define :g3 do |cfg|
    cfg.vm.hostname = "g3"
    cfg.vm.network "private_network", ip: "192.168.33.13", virtualbox__intnet: "glusterfs"
  end

  config.vm.define :g4 do |cfg|
    cfg.vm.hostname = "g4"
    cfg.vm.network "private_network", ip: "192.168.33.14", virtualbox__intnet: "glusterfs"
  end

  config.vm.define :cl do |cfg|
    cfg.vm.hostname = "cl"
    cfg.vm.network "private_network", ip: "192.168.33.21", virtualbox__intnet: "glusterfs"
  end

end

起動します。

vagrant up

すべてのゲストに ssh で接続します。

vagrant ssh g1
vagrant ssh g2
vagrant ssh g3
vagrant ssh g4
vagrant ssh cl

起動

ゲストの hosts にノードの一覧を追記しておきます。

cat <<EOS> /etc/hosts
127.0.0.1       localhost localhost.localdomain localhost4 localhost4.localdomain4
192.168.33.11   g1
192.168.33.12   g2
192.168.33.13   g3
192.168.33.14   g4
192.168.33.21   cl
EOS

glusterfs-server の インストール時に /var/lib/glusterd/glusterd.info というファイルの中にノードの UUID が作成されています。

cat /var/lib/glusterd/glusterd.info
UUID=7096ff3d-e640-494f-8409-b0fa52e74b8c
operating-version=30702

この値はクラスタのノードで重複しないようにする必要があります。もし、共通の AMI などからインスタンスを作ったのであれば、UUID が重複しないように GlusterFS の起動前にこのファイルを削除しておく必要があります。

今回は Vagrant box から起動したため重複しています。なので次のように削除しておきます。

rm -f /var/lib/glusterd/glusterd.info

と、思ったんだけど作成されていなかった。。。 3.7.2 で検証してたときはインストール時に作成されていた気がするのだけど・・? 3.7.3 から変わったのか、あるいは勘違いしていたのか・・・

GlusterFS のデーモンを起動します。

systemctl enable glusterd.service
systemctl start  glusterd.service
systemctl status glusterd.service

起動時に UUID は自動生成されます。 初回の起動時にも /var/lib/glusterd/glusterd.info は作成されなくて、gluster pool list したら作成されました。

gluster pool list
cat /var/lib/glusterd/glusterd.info

Brick の作成

適当なディレクトリにファイルの置き場所となる Brick(ブリック)を作ります。

追加ディスクにパーティションを切ります。動作要件に i-node サイズが 512B とあるらしいのでファイルシステムの作成時に指定します。

parted -s -a optimal /dev/sdb mklabel msdos -- mkpart primary xfs 1 -1
mkfs.xfs -i size=512 /dev/sdb1
mkdir -p /glfs/vols

cat <<EOS>>/etc/fstab
/dev/sdb1 /glfs/vols xfs defaults 0 0
EOS

mount /glfs/vols

ブリックのディレクトリを作成します。

mkdir -p /glfs/vols/data

GlusterFS クラスタ

GlusterFS のノードでクラスタを組んでストレージプールを作ります。

gluster peer status でクラスタのピア数を表示してみます。

gluster peer status

まだ 0 個です、つまり自分以外は居ません。

Number of Peers: 0

gluster pool list でノードの一覧を表示してみます。

gluster pool list

自分しか表示されません。

UUID                                    Hostname        State
89a1b9b4-f7e0-4091-aeff-80279d8c0024    localhost       Connected

gluster peer probe <remote> でピアを追加できます。下記を g1 で実行してみます。

gluster peer probe g2
gluster peer probe g3
gluster peer probe g4

成功したっぽいメッセージが表示されます。

peer probe: success.

もう一度 gluster peer status とか gluster pool list とかを実行してみると、それっぽく表示されます。

gluster peer status
Number of Peers: 3

Hostname: g2
Uuid: 9256e049-e3f9-43a7-9fda-3ce631d4e5c1
State: Peer in Cluster (Connected)

Hostname: g3
Uuid: 9a5eaa0d-3db9-40af-a58e-e5caa804cb78
State: Peer in Cluster (Connected)

Hostname: g4
Uuid: 8fde5fcc-2e33-489b-976f-76171fcd162f
State: Peer in Cluster (Connected)
gluster pool list
UUID                                    Hostname        State
9256e049-e3f9-43a7-9fda-3ce631d4e5c1    g2              Connected 
9a5eaa0d-3db9-40af-a58e-e5caa804cb78    g3              Connected 
8fde5fcc-2e33-489b-976f-76171fcd162f    g4              Connected 
89a1b9b4-f7e0-4091-aeff-80279d8c0024    localhost       Connected 

Volume を作成

ボリュームを作成します。

この例だと、ボリュームの名前は data で、g1=g2 と g3=g4 がそれぞれレプリカで、それぞれの組にファイルが分散されます。

gluster volume create data replica 2 \
  g1:/glfs/vols/data \
  g2:/glfs/vols/data \
  g3:/glfs/vols/data \
  g4:/glfs/vols/data

正常に作成されれば次のようなメッセージが表示されます。

volume create: data: success: please start the volume to access data

ボリュームを開始します。

gluster volume start data

正常に開始されれば次のようなメッセージが表示されます。

volume start: data: success

ボリュームの情報を見てみます。

gluster volume info data

それっぽい内容が表示されます。

Volume Name: data
Type: Distributed-Replicate
Volume ID: 48bf6f46-c6d8-4743-b65e-4ecba2e27969
Status: Started
Number of Bricks: 2 x 2 = 4
Transport-type: tcp
Bricks:
Brick1: g1:/glfs/vols/data
Brick2: g2:/glfs/vols/data
Brick3: g3:/glfs/vols/data
Brick4: g4:/glfs/vols/data
Options Reconfigured:
performance.readdir-ahead: on

ボリュームのステータスを見てみます。

gluster volume status data

それっぽい内容が表示されます。

Status of volume: data
Gluster process                             TCP Port  RDMA Port  Online  Pid
------------------------------------------------------------------------------
Brick g1:/glfs/vols/data                    49152     0          Y       2800 
Brick g2:/glfs/vols/data                    49152     0          Y       2752 
Brick g3:/glfs/vols/data                    49152     0          Y       2754 
Brick g4:/glfs/vols/data                    49152     0          Y       2753 
NFS Server on localhost                     N/A       N/A        N       N/A  
Self-heal Daemon on localhost               N/A       N/A        Y       2828 
NFS Server on g4                            N/A       N/A        N       N/A  
Self-heal Daemon on g4                      N/A       N/A        Y       2781 
NFS Server on g2                            N/A       N/A        N       N/A  
Self-heal Daemon on g2                      N/A       N/A        Y       2780 
NFS Server on g3                            N/A       N/A        N       N/A  
Self-heal Daemon on g3                      N/A       N/A        Y       2782 
 
Task Status of Volume data
------------------------------------------------------------------------------
There are no active volume tasks

クライアントからマウント

クライアントのノードからマウントしてみます。まずマウントポイントを作ります。

mkdir -p /glfs/data

適当なノードを指定してマウントする必要があります。そのため、次の例では g1 が停止しているとマウントに失敗します。

mount -t glusterfs g1:/data /glfs/data

次のようにマウントオプションを指定すると、g1 が停止しているときは g2 にフォールバックされるようにできます。

mount -t glusterfs g1:/data /glfs/data -o backupvolfile-server=g2

一旦マウントしてしまえばどのノードをマウントで指定したかは関係なくなるので、前者の方法でも g1 が SPOF になるというわけではありませんが・・どうせなら後者の方が良いと思います。

replica運用してるglusterfsボリュームでmount時のフォールバックオプションを付ける - Qiita

動作確認

クライアントから適当に書き込んでみます。

echo 1 > /glfs/data/1.txt
echo 2 > /glfs/data/2.txt
echo 3 > /glfs/data/3.txt
echo 4 > /glfs/data/4.txt

g1g2 のブリックを確認してみると、

ssh g1 ls -l /glfs/vols/data
ssh g2 ls -l /glfs/vols/data
-rw-r--r-- 2 root root 2  7月  3 16:48 4.txt

両方に 4.txt だけが保存されています。

g3g4 のブリックを確認してみると、

ssh g3 ls -l /glfs/vols/data
ssh g4 ls -l /glfs/vols/data

両方に残りのファイルが保存されています。

-rw-r--r-- 2 root root 2  7月  3 16:48 1.txt
-rw-r--r-- 2 root root 2  7月  3 16:48 2.txt
-rw-r--r-- 2 root root 2  7月  3 16:48 3.txt

レプリケーション&分散配置されています。

ノード障害時の動作

Vagrant のホストから、おもむろに g1 を強制終了します。

vagrant halt -f g1

適当なノード(例えば g2)から、ストレージプールの状態などを確認してみます。

gluster peer status すると g1 が Disconnected になっています。

gluster peer status
Number of Peers: 3

Hostname: g1
Uuid: 89a1b9b4-f7e0-4091-aeff-80279d8c0024
State: Peer in Cluster (Disconnected)

Hostname: g3
Uuid: 9a5eaa0d-3db9-40af-a58e-e5caa804cb78
State: Peer in Cluster (Connected)

Hostname: g4
Uuid: 8fde5fcc-2e33-489b-976f-76171fcd162f
State: Peer in Cluster (Connected)

gluster pool list でも同上です。

gluster pool list
UUID                                    Hostname        State
89a1b9b4-f7e0-4091-aeff-80279d8c0024    g1              Disconnected 
9a5eaa0d-3db9-40af-a58e-e5caa804cb78    g3              Connected 
8fde5fcc-2e33-489b-976f-76171fcd162f    g4              Connected 
9256e049-e3f9-43a7-9fda-3ce631d4e5c1    localhost       Connected 

gluster volume status data だと g1 がいません。

gluster volume status data
Status of volume: data
Gluster process                             TCP Port  RDMA Port  Online  Pid
------------------------------------------------------------------------------
Brick g2:/glfs/vols/data                    49152     0          Y       2752 
Brick g3:/glfs/vols/data                    49152     0          Y       2754 
Brick g4:/glfs/vols/data                    49152     0          Y       2753 
NFS Server on localhost                     N/A       N/A        N       N/A  
Self-heal Daemon on localhost               N/A       N/A        Y       2780 
NFS Server on g4                            N/A       N/A        N       N/A  
Self-heal Daemon on g4                      N/A       N/A        Y       2781 
NFS Server on g3                            N/A       N/A        N       N/A  
Self-heal Daemon on g3                      N/A       N/A        Y       2782 
 
Task Status of Volume data
------------------------------------------------------------------------------
There are no active volume tasks

いかにも g1 が死んでいる風になっていますが、g1g2 でレプリカになっているはずなのでクライアントから読み書きできるか試してみます。

クライアントからファイルを読んでみると・・なにごともなく読むことができます。

cat /glfs/data/1.txt
cat /glfs/data/2.txt
cat /glfs/data/3.txt
cat /glfs/data/4.txt

次はファイルを書き込んでみると・・なにごともなく書き込むことができます。

echo 5 > /glfs/data/5.txt
echo 6 > /glfs/data/6.txt
echo 7 > /glfs/data/7.txt
echo 8 > /glfs/data/8.txt

生きているノードのブリックを確認してみます。

まずは g2

ssh g2 ls -l /glfs/vols/data
-rw-r--r-- 2 root root 2  7月  3 16:48 4.txt
-rw-r--r-- 2 root root 2  7月  3 16:58 8.txt

次は g3

ssh g3 ls -l /glfs/vols/data
-rw-r--r-- 2 root root 2  7月  3 16:48 1.txt
-rw-r--r-- 2 root root 2  7月  3 16:48 2.txt
-rw-r--r-- 2 root root 2  7月  3 16:48 3.txt
-rw-r--r-- 2 root root 2  7月  3 16:58 5.txt
-rw-r--r-- 2 root root 2  7月  3 16:58 6.txt
-rw-r--r-- 2 root root 2  7月  3 16:58 7.txt

さらに g4

ssh g4 ls -l /glfs/vols/data
-rw-r--r-- 2 root root 2  7月  3 16:48 1.txt
-rw-r--r-- 2 root root 2  7月  3 16:48 2.txt
-rw-r--r-- 2 root root 2  7月  3 16:48 3.txt
-rw-r--r-- 2 root root 2  7月  3 16:58 5.txt
-rw-r--r-- 2 root root 2  7月  3 16:58 6.txt
-rw-r--r-- 2 root root 2  7月  3 16:58 7.txt

g2 に保存されたファイルは g2 にしか存在しない状態です。

ノード復帰時の動作

Vagrant ホストから g1 を復帰させます。

vagrant up g1
vagrant ssh g1

適当なノード(例えば g2)からいろいろ確認してみます。

gluster peer status で見ると g1 が Connected になっています。

gluster peer status
Number of Peers: 3

Hostname: 192.168.33.11
Uuid: 89a1b9b4-f7e0-4091-aeff-80279d8c0024
State: Peer in Cluster (Connected)

Hostname: g3
Uuid: 9a5eaa0d-3db9-40af-a58e-e5caa804cb78
State: Peer in Cluster (Connected)

Hostname: g4
Uuid: 8fde5fcc-2e33-489b-976f-76171fcd162f
State: Peer in Cluster (Connected)

gluster pool list も同上です。

gluster pool list
UUID                                    Hostname        State
89a1b9b4-f7e0-4091-aeff-80279d8c0024    g1              Connected 
9a5eaa0d-3db9-40af-a58e-e5caa804cb78    g3              Connected 
8fde5fcc-2e33-489b-976f-76171fcd162f    g4              Connected 
9256e049-e3f9-43a7-9fda-3ce631d4e5c1    localhost       Connected 

gluster volume status data にも g1 が追加されています。

gluster volume status data
Status of volume: data
Gluster process                             TCP Port  RDMA Port  Online  Pid
------------------------------------------------------------------------------
Brick g1:/glfs/vols/data                    49152     0          Y       1055 
Brick g2:/glfs/vols/data                    49152     0          Y       2752 
Brick g3:/glfs/vols/data                    49152     0          Y       2754 
Brick g4:/glfs/vols/data                    49152     0          Y       2753 
NFS Server on localhost                     N/A       N/A        N       N/A  
Self-heal Daemon on localhost               N/A       N/A        Y       2780 
NFS Server on g4                            N/A       N/A        N       N/A  
Self-heal Daemon on g4                      N/A       N/A        Y       2781 
NFS Server on 192.168.33.11                 N/A       N/A        N       N/A  
Self-heal Daemon on 192.168.33.11           N/A       N/A        Y       2212 
NFS Server on g3                            N/A       N/A        N       N/A  
Self-heal Daemon on g3                      N/A       N/A        Y       2782 
 
Task Status of Volume data
------------------------------------------------------------------------------
There are no active volume tasks

ただし、まだ g1g2 は同期されていません。g1 が停止していた間のファイルは g1 にはありません。

ssh g1 ls -l /glfs/vols/data
-rw-r--r-- 2 root root 2  7月  3 16:48 4.txt
ssh g2 ls -l /glfs/vols/data
-rw-r--r-- 2 root root 2  7月  3 16:48 4.txt
-rw-r--r-- 2 root root 2  7月  3 16:58 8.txt

クライアントからそのファイルを読み込むと g1 のブリックにファイルが複製されました。

他にも次のようなタイミングで同期されます。

  • Self-heal daemon によって自動的に
  • gluster volume heal <volume> で手動で開始

ノードの交換

Vagrant のホストから、おもむろに g1 をぶっ壊して作り直します。

vagrant destroy g1
vagrant up g1
vagrant ssh g1

g1 を再セットアップします。まず、hosts を更新します。

cat <<EOS> /etc/hosts
127.0.0.1       localhost localhost.localdomain localhost4 localhost4.localdomain4
192.168.33.11   g1
192.168.33.12   g2
192.168.33.13   g3
192.168.33.14   g4
192.168.33.21   cl
EOS

適当な他のノードから、以前の g1 の UUID を調べます。

ssh g2 gluster pool list

新規の g1 の UUID を↑で調べた値に変更します。

vim /var/lib/glusterd/glusterd.info
UUID=89a1b9b4-f7e0-4091-aeff-80279d8c0024
operating-version=30703

ブリックを作ります。

parted -s -a optimal /dev/sdb mklabel msdos -- mkpart primary xfs 1 -1
mkfs.xfs -i size=512 /dev/sdb1
mkdir -p /glfs/vols

cat <<EOS>>/etc/fstab
/dev/sdb1 /glfs/vols xfs defaults 0 0
EOS

mount /glfs/vols
mkdir -p /glfs/vols/data

glusterd を起動します。

systemctl enable glusterd.service
systemctl start  glusterd.service
systemctl status glusterd.service

g1 から g1 以外のすべてに gluster peer probe します。

gluster peer probe g2
gluster peer probe g3
gluster peer probe g4

gluster pool list で確認するとクラスタが復帰していることがわかります。

gluster pool list

これだけだとボリュームの情報が g1 に無いことがあるようです。

gluster volume list

そんなときは glusterd を再起動すると良いようです(gluster volume sync g2 all でもいいのかもしれない?)。

systemctl stop  glusterd.service
systemctl start glusterd.service

ボリュームの情報が得られることを確認します。

gluster volume info data

なぜか gluster volume status data で見るとボリュームが Online になっていないことがあります。

gluster volume status data
Status of volume: data
Gluster process                             TCP Port  RDMA Port  Online  Pid
------------------------------------------------------------------------------
Brick g1:/glfs/vols/data                    N/A       N/A        N       N/A  
Brick g2:/glfs/vols/data                    49152     0          Y       2794 
Brick g3:/glfs/vols/data                    49152     0          Y       2754 
Brick g4:/glfs/vols/data                    49152     0          Y       2753
...snip...

そんなときは glusterd を再起動すると良い?らしい??です???(さっきもしたけど・・)

systemctl stop  glusterd.service
systemctl start glusterd.service

3.4.0 以降だとこれでもダメなようです。

このとき、ブリックのログに次のように記録されています。

[2015-07-14 05:59:08.521960] E [posix.c:6012:init] 0-data-posix: Extended attribute trusted.glusterfs.volume-id is absent
[2015-07-14 05:59:08.521972] E [xlator.c:426:xlator_init] 0-data-posix: Initialization of volume 'data-posix' failed, review your volfile again
[2015-07-14 05:59:08.521978] E [graph.c:322:glusterfs_graph_init] 0-data-posix: initializing translator failed
[2015-07-14 05:59:08.521983] E [graph.c:661:glusterfs_graph_activate] 0-graph: init failed
[2015-07-14 05:59:08.522710] W [glusterfsd.c:1219:cleanup_and_exit] (--> 0-: received signum (0), shutting down

ブリックのディレクトリに trusted.glusterfs.volume-id 拡張属性が無いためらしいのですが、次のコマンドで再生成できます。

gluster volume start data force

今度こそボリュームがオンラインになっていることを確認します。

gluster volume status data
Status of volume: data
Gluster process                             TCP Port  RDMA Port  Online  Pid
------------------------------------------------------------------------------
Brick g1:/glfs/vols/data                    49153     0          Y       3281 
Brick g2:/glfs/vols/data                    49152     0          Y       2794 
Brick g3:/glfs/vols/data                    49152     0          Y       2754 
Brick g4:/glfs/vols/data                    49152     0          Y       2753
...snip...

オンラインになっても自動的に同期はされません。

ll /glfs/vols/data/
total 0

gluster volume heal <volume> full で再同期します。

gluster volume heal data full
Launching heal operation to perform full self heal on volume data has been successful 
Use heal info commands to check status

ご覧の通りです。

ll /glfs/vols/data/
total 4
-rw-r--r-- 2 root root  0 Jul  7 20:15 4.txt
-rw-r--r-- 2 root root 29 Jul  7 20:15 8.txt

の、はずなんですが、3.7.3 にバージョンアップしたらなぜか上手く行きませんでした、ディレクトリが空のままです。 マウントしているクライアントからファイルのアクセスするとレプリケートされましたけど・・・??

さらにブリックの拡張属性を見てみると trusted.glusterfs.dht がなかったりするし・・・いっそのこと rsync で --xattr を付けて拡張属性ごとブリックをコピーしてしまえばいいのだろうか??

古いメモ

これはだいぶ前に検証したときのメモです。

分散ハッシュテーブル

ファイル名を元にハッシュ値が計算される。ハッシュ値を元にどのブリックに保存するかが決定される。

ハッシュテーブルはディレクトリごとに異なる。

ハッシュレンジはディレクトリの拡張属性に保存される。

getfattr -d -m . /glfs/vols/data/
getfattr: Removing leading '/' from absolute path names
# file: glfs/vols/data/
trusted.afr.data-client-0=0sAAAAAAAAAAAAAAAA
trusted.afr.data-client-1=0sAAAAAAAAAAAAAAAA
trusted.afr.dirty=0sAAAAAAAAAAAAAAAA
trusted.gfid=0sAAAAAAAAAAAAAAAAAAAAAQ==
trusted.glusterfs.dht=0sAAAAAQAAAAAAAAAA/////w==
trusted.glusterfs.volume-id=0sE6WciNVBR7ekZgK1wu3xgw==

ファイルをリネームしてハッシュ値が変わり、別のブリックに保存される事になった場合、新しい保存先のブリックには sticky ビットのついた空のファイルが作成されて、拡張属性で元のブリックへのリンクが記録される。

ll 9.txt
---------T 2 root root 0 12月 13 17:12 9.txt
getfattr -n trusted.glusterfs.dht.linkto 9.txt
# file: 9.txt
trusted.glusterfs.dht.linkto="data-replicate-1"

リバランスによってこのようなファイルを本来の位置に再配置できる。

既存ボリュームにブリックを追加すると

  • 追加直後は、既存のディレクトリは新しいブリックを使用しない
    • ハッシュテーブルに当該ブリックのエントリがないため
  • 新規作成したディレクトリには新しいブリックを含むハッシュテーブルが作成される
  • リバランスすれば既存ディレクトリに新しいブリックを含むハッシュテーブルが作成される

既存ボリュームからブリックを削除すると

  • 事前に削除対象ブリックを除いた新しいハッシュテーブルを作成して再配置する
  • 再配置によって削除対象ブリックからファイルが無くなった後に当該ブリックを削除する

性能

大きなファイルをストライプ構成することで性能向上が見込めるが、小さい大量のファイルを扱う場合は性能が遅くなる(NFS より遅くなる可能性もある)。

v3.4.0 からのバグ

直接拡張属性を書いても大丈夫。

grep volume-id /var/lib/glusterd/vols/data/info | cut -d= -f2 | sed 's/-//g'

setfattr -n trusted.glusterfs.volume-id \
  -v 0x$(grep volume-id /var/lib/glusterd/vols/data/info | cut -d= -f2 | sed 's/-//g') \
    /glfs/vols/data

getfattr -n trusted.glusterfs.volume-id -e hex /glfs/vols/data

service glusterd restart

所感

  • 分散ファイルシステムにしては構築が異様に簡単
  • ノード交換時の作業が GlusterFS のバージョンがちょっと代わるたびに上手く行かなくなる気がする
    • 検証するたびに試行錯誤している
  • CentOS 7 の公式リポジトリだと server が無いので GlusterFS の公式のリポジトリを使うしか無い
    • バージョンアップに追従していくのは大変そう
    • ストレージなのでなるべく安定していた方がいい
  • RHEL で Red Hat Gluster Storage とかのほうが良いかもしれない
    • ストレージにはお金かけても良いと思う
    • サーバの要件が異様に厳しそうだけど・・(RHCS は異様に厳しかった気がするし)

参考

Consul を使ってみた

先日、社内勉強会以外の伺か(仮)で、Consul について話したときの資料がでてきたので置いておきます。

213 番煎じぐらいで真新しいものではありません。

Consul is なに?

Packer や Vagrant の HashiCorp 社が作っているオーケストレーションツール。

Docker コンテナ

Consul を試す環境として Docker で以下の環境を作ります。

  • server
    • 10.88.0.10
    • 8080 => 8080
  • node1
    • 10.88.0.11
  • node2
    • 10.88.0.12

次の Dockerfile を使います。 最近の CentOS 7 のコンテナは systemd のサービスが普通に動くので、軽量な仮想環境として使うのに便利です。

FROM centos:7

RUN yum install -y epel-release &&\
    yum install -y wget unzip bind-utils dnsmasq nginx rsync &&\
    yum clean all

RUN wget -q https://dl.bintray.com/mitchellh/consul/0.5.2_linux_amd64.zip &&\
    unzip 0.5.2_linux_amd64.zip &&\
    mv consul /usr/local/bin/consul &&\
    rm -vf 0.5.2_linux_amd64.zip

RUN wget https://dl.bintray.com/mitchellh/consul/0.5.2_web_ui.zip &&\
    unzip 0.5.2_web_ui.zip &&\
    mkdir -p /opt/consul/dist/ &&\
    rsync dist/ /opt/consul/dist/ -av &&\
    rm -rvf 0.5.2_web_ui.zip dist/

ENTRYPOINT /sbin/init

yum は説明不要だと思います。他の2つの RUN は、consul のコマンドとか UI とかをダウンロードしているのですが、後ほど説明します。

最後の ENTRYPOINT /sbin/init は、コンテナの中で systemd のサービスを動かすため です。

ビルドします。

docker build -t example/consul .

コンテナを起動します。--privileged は pipework を使うために必要です。

docker run --privileged -d --name server -h server -p 8080:8080 example/consul
docker run --privileged -d --name node1 -h node1 example/consul
docker run --privileged -d --name node2 -h node2 example/consul

pipework でコンテナに固定IPを付与します。

sudo pipework br1 server 10.88.0.10/24
sudo pipework br1 node1 10.88.0.11/24
sudo pipework br1 node2 10.88.0.12/24

インストール

Go 言語なのでバイナリいっこ落とすだけ。 (Dockerfile でやってるので不要です)

wget https://dl.bintray.com/mitchellh/consul/0.5.2_linux_amd64.zip
unzip 0.5.2_linux_amd64.zip
sudo mv consul /usr/local/bin/consul

Consul Server

server にログインします。

docker exec -it server /bin/bash

次のように consul を実行します。

consul agent -server -bootstrap-expect=1 -data-dir=/tmp/consul -node=server \
  -bind=10.88.0.10 -ui-dir=/opt/consul/dist

別のターミナルでログインします。

docker exec -it server /bin/bash

メンバーの一覧を表示してみます。

consul members

まだ自分しかいません。

Node    Address          Status  Type    Build  Protocol  DC
server  10.88.0.10:8301  alive   server  0.5.2  2         dc1

consul の DNS インタフェースを dig で呼んでみます。

dig server.node.consul @127.0.0.1 -p 8600

次のように結果が返ります。

;; ANSWER SECTION:
server.node.consul.     0       IN      A       10.88.0.10

port 番号とかを指定するのが面倒なので dnsmasq を使います。

.consul をローカルの 8600 ポートにフォワードするように設定します。

cat <<EOS> /etc/dnsmasq.d/consul.conf
server=/consul/127.0.0.1#8600
strict-order
EOS

デフォルトのネームサーバをローカルにするために nameserver 127.0.0.1/etc/resolv.conf の先頭に挿入します。

sed -i '1i nameserver 127.0.0.1' /etc/resolv.conf

設定を反映します。

systemctl restart dnsmasq.service

名前解決してみます。

dig server.node.consul

先ほどと同じように結果が帰ります。

;; ANSWER SECTION:
server.node.consul.     0       IN      A       10.88.0.10

Consul Client

node1 と node2 に nginx を入れて consul を使って DNS ラウンドロビンするようにしてみます。

node1 と node2 にそれぞれログインします。

docker exec -it node1 /bin/bash
docker exec -it node2 /bin/bash

nginx を起動します。

systemctl start nginx.service
systemctl status nginx.service

ドキュメントルートに、どちらのホストを見ているのか判るようにホスト名が書かれたファイルを置きます。

uname -n > /usr/share/nginx/html/consul.html
curl http://127.0.0.1/consul.html

consul のサービスの設定ファイルを作成します。 この例だと web というサービス名で、curl でサービスの監視をしています。

mkdir -p /etc/consul.d/

cat <<EOS> /etc/consul.d/web.json
{
  "service": {
    "name": "web",
    "tags": [ "nginx" ],
    "port": 80,
    "check": {
      "script": "curl http://127.0.0.1:80/consul.html >/dev/null 2>&1",
      "interval": "10s",
      "timeout": "5s"
    }
  }
}
EOS

node1 と node2 でそれぞれ consul を起動します。

consul agent -data-dir=/tmp/consul -node=$(uname -n) \
  -bind=10.88.0.11 -config-dir=/etc/consul.d/ -join=10.88.0.10
consul agent -data-dir=/tmp/consul -node=$(uname -n) \
  -bind=10.88.0.12 -config-dir=/etc/consul.d/ -join=10.88.0.10

server のターミナルでメンバーの一覧を表示してみます。

consul members

node1 と node2 が追加されています。

Node    Address          Status  Type    Build  Protocol  DC
server  10.88.0.10:8301  alive   server  0.5.2  2         dc1
node1   10.88.0.11:8301  alive   client  0.5.2  2         dc1
node2   10.88.0.12:8301  alive   client  0.5.2  2         dc1

名前解決もできます。

dig node2.node.consul a

次のようにサービス名を指定すると、node1 と node2 の両方のアドレスが返ってきます。

dig web.service.consul a

サービス名の URL を複数回表示すると、DNS ラウンドロビンされているのがわかります。 (と思ったんだけどなぜかラウンドロビンされない・・Vagrant で環境作った時にはできたんだけど・・?)

curl http://web.service.consul/consul.html

node1 の nginx を止めてみます。

docker exec node1 systemctl stop nginx.service

メンバーの一覧は特に変わりません。

consul members

サービス名で名前解決してみると、node2 のアドレスしか返らなくなっています。

dig web.service.consul a

もちろん、サービス名の URL も node2 にのみアクセスします。

curl http://web.service.consul/consul.html

node1 の nginx を再開します。

docker exec node1 systemctl start nginx.service

サービス名で node1 と node2 の両方が返るように戻ります。

dig web.service.consul a

サービス名の URL も両方に振り分けられるように戻ります。 (と思ったんだけどやっぱりラウンドロビンされない?)

curl http://web.service.consul/consul.html

クエリ

serf でやったようなクエリの機能はデフォで有効です。

次のようにすると、すべてのホストでの uname -a の結果が得られます。

consul exec uname -a

特定のノードを指定することもできます。

consul exec -node=node2 uname -a

特定のサービスを指定することもできます。

consul exec -service=web uname -a

次のように、web サーバだけを対象に nginx を再起動させたりできます。

consul exec -service=web systemctl restart nginx.service

UI

consul には Web の UI があります。

UI のファイルをダウンロードして、適当なディレクトリ(consul agent--ui-dir に指定したディレクトリ)に展開します。 (Dockerfile でやっているので不要です)

wget https://dl.bintray.com/mitchellh/consul/0.5.2_web_ui.zip
unzip 0.5.2_web_ui.zip
mkdir -p /opt/consul/dist/
rsync dist/ /opt/consul/dist/ -av

nginx をそれっぽく設定します。

cat <<EOS> /etc/nginx/conf.d/consul.conf;
server {
    listen 8080 default_server;
    server_name server.node.consul;

    location / {
        proxy_pass http://127.0.0.1:8500;
    }
}
EOS

nginx を開始します。

systemctl start nginx.service
systemctl status nginx.service

以下の URL を開くと Web 画面が見えます。

open http://localhost:8080/ui/

さいごに

このデモでは、サーバが1台と、クライアントが2台で試しましたが、高可用にするためにはサーバは3台か5台にするべきらしいです(マルチデータセンターならデータセンターごとに3台か5台)。

また、Consul サーバはそれなりに重たいので、Consul サーバ専用のホストとして構築するべきらしいです。

逆にクライアントはとても軽量なので、他のサービスと一緒に動作させて問題ありません(というかそうしなければ意味が無い)。

つまり、Consul を使ったクラスタでは、Consul サーバのためだけに最低3台の専用のホストが必要となります。

・・・ちょっとした小規模クラスタに使う感じではありませんね。