2012年1月8日日曜日

タイムアウトを指定してコマンド実行

コマンドを実行する際、タイムアウトが指定できないと困ることがよくあります。一定時間内にコマンドが終了することをほぼ担保する必要がある場合にどうしたらよいか、というのが本日の主題です。

データベースを監視する目的で一定時間ごとにデータベースにアクセスしてSQL文を発行する必要がありました。Veritas Storage Foundation HAVeritas Cluster ServerVCS)にMySQLを組み込むに当たり、monitorスクリプトを作る必要があった、というのが発端です。

VCSの仕様として、フェールオーバー処理が走り始めた場合、アクティブ機にて各種リソースを停止すべくofflineスクリプト(cleanスクリプト)が呼び出され、monitorスクリプトで停止を確認できるまで次の処理に進まない、というのがあります。monitorスクリプトが所定の時間内に結果を返さなかった場合、状況は不明ということで、一定時間後にまたmonitorスクリプトが呼び出され、所定の時間内に結果を返さなければ、このままループし続けます。いつまで待ってもスタンバイ機がアクティブにならないのです。スプリットブレイン対策を考えると、これはこれで仕方ない部分もあります。 作成するmonitorスクリプトは、一定時間内に結果を返す必要がある、と考えて作っておくべきです。

某社が納品してきたmonitorスクリプトでは、
ps -ef | grep -v grep | grep デーモン名
が成功するとonline、失敗するとofflineと判断するように書かれていましたが、ネットワーク障害試験を実施した際に、LDAPサーバとの通信ができない場合にpsコマンドが戻ってこず、フェールオーバー処理が途中で止まる、ということがありました。プロセスを実行しているuidの名前解決のためにLDAPサーバと通信が発生する環境であったため、psコマンドが結果をすぐには返してくれなかったのです。uidの名前解決は監視をする上で不必要なことであったという点に思いをいたしていれば防げた事例となります。たとえば、以下のように書かれていれば。
ps -eo cmd|grep [/]usr/libexec/mysqld
監視プログラムは、可能な限り軽量に作っておくべきです。その大前提を守ったうえで、予期せぬ出来事をどこまで考慮すべきか。予期していないことを考慮するのは非常に難しいです。
難しいですが、対応できるものについては、できるだけ配慮しておくべきです。

psコマンドのオプション指定の話はさておき、監視用のSELECT文が一定時間内に予期している応答を返さなかった場合にフェールオーバーさせる、という要件がでてきました。リトライはするとして、一定時間後までに結果をチェックする必要があります。タイムアウトを指定してSQL文を発行したい、という要件に読み替えて検討しました。

expectパッケージに付属する/usr/bin/timed-runコマンドを利用するとタイムアウトを指定できるようになります。

使い方は
timed-run [timeout(sec)] [command args...]
となっています。

RHEL6expectパッケージでは以下のように実装されています。

</usr/bin/timed-run>
1
2
3
4
5
6
7
8
9
#!/bin/sh
# \
exec expect -f "$0" ${1+"$@"}
# run a program for a given amount of time
# i.e. time 20 long_running_program

set timeout [lindex $argv 0]
eval spawn [lrange $argv 1 end]
expect

Linuxで作るアドバンストシステム構築ガイド』(http://www.shuwasystem.co.jp/products/7980html/2454.html)の「3.3.3 タイムランの実装例」に、
このtimed-runプログラムはタイムアウトが発生した場合にも正常終了してしまうため、その点で不都合があります。
ということで、タイムアウトした時には戻り値が「1」となるように改造する例が以下のように記載されています。

</usr/local/bin/timedrun>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/sh
# \
exec expect -f "$0" ${1+"$@"}
# run a program for a given amount of time
# i.e. time 20 long_running_program

proc abort {} {
        send_error "Timeout!!\n"
        exit 1
}

set timeout [lindex $argv 0]
eval spawn [lrange $argv 1 end]
expect {
        timeout abort
        eof
}

いろいろと動作確認してみると、不満な点が出てきました。

このtimedrunプログラムは、指定したコマンドが指定時間内に失敗した場合にも正常終了してしまうため、その点で不都合があります。

以下のように修正してみました(戻り値も変更してあります)。

</usr/local/bin/timed-run-ex>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh
# \
exec expect -f "$0" ${1+"$@"}
# run a program for a given amount of time
# i.e. time 20 long_running_program

proc abort {} {
  send_error "Timeout!!\n"
  exit 127
}

set timeout [lindex $argv 0]
eval spawn [lrange $argv 1 end]
expect {
  timeout abort
  eof
}
catch wait err
if { [lindex $err 2] == -1 } {
  send_error "OS System Error: [lindex $err 3]\n"
  exit 126
}
exit [lindex $err 3]

以下のように動作確認をしております。
1
2
3
4
5
6
7
8
9
10
11
12
13
$ /usr/local/bin/timed-run-ex 3 true; echo $?
spawn true
0
$ /usr/local/bin/timed-run-ex 3 false; echo $?
spawn false
1
$ /usr/local/bin/timed-run-ex 3 sleep 1; echo $?
spawn sleep 1
0
$ /usr/local/bin/timed-run-ex 3 sleep 5; echo $?
spawn sleep 5
Timeout!!
127

戻り値が126となるケース(19行目の分岐で20,21行目を実行する部分)がテストできていません。
man expect すると

wait normally returns a list of four integers.  The first integer
is the pid of the process that was waited upon.  The second inte-
ger is the corresponding spawn id.  The third integer is -1 if an
operating system error occurred, or 0 otherwise.   If  the  third
integer  was  0, the fourth integer is the status returned by the
spawned process.  If the third integer was -1, the fourth integer
is  the  value  of errno set by the operating system.  The global
variable errorCode is also set.
とあるのでこのように実装しておいたのですが、どうテストすべきか悩んでおります。

指定コマンドが127や126を返してくるケースを区別するのが面倒ですが、標準エラー出力を解析すれば区別できるはずなので、よしとします。そのような区別をする要件に出くわすことはないと思っておりますが、あればその時に考えることにします。

2 件のコメント:

  1. 新しめの GNU coreutils に入ってる timeout(1) でよさげ。

    返信削除
  2. fumiyas さん、情報ありがとうございます。
    RHEL6 で採用されている coreutils-8.4-16.el6 で試してみました。
    こちらだとシグナルも指定できてうれしい機能です。
    タイムアウト時の戻り値は124になっていました。

    $ info coreutils 'timeout invocation'

    (略)

    Exit status:

    124 if COMMAND times out
    125 if `timeout' itself fails
    126 if COMMAND is found but cannot be invoked
    127 if COMMAND cannot be found
    the exit status of COMMAND otherwise

    RHEL5 に標準で入っていないのが残念です。
    RHEL6 の VCS を構築する機会がくれば試してみたいと思います。

    返信削除