2014年9月1日月曜日

systemd-timesyncdによる時刻同期

systemd 213からsystemd-timesyncdというSNTPクライアント機能が追加されて、ntpdやchronyの代わりに使えるようになったんだけど、どうも仮想マシンでは起動しないようになってるようで、VM上のArch Linuxでは使えなかった。

timesyncd: do not start in virtualized environments · 01b85ba · systemd/systemd

使い方

ntpdやchronyを使用している場合は無効にする。

sudo systemctl stop ntp.service
sudo systemctl disable ntp.service

systemd-timesyncdの設定をする。

sudo -e /etc/systemd/timesyncd.conf
[Time]
NTP=ntp.nict.jp
FallbackNTP=time1.google.com time2.google.com time3.google.com time4.google.com

systemd-timesyncdを有効にして起動する。

sudo systemctl enable systemd-timesyncd.service
sudo systemctl start systemd-timesyncd.service

statusでactiveになってればOK
仮想マシンの場合はConditionVirtualization=noに引っかかって起動しない(inactiveになる)。

sudo systemctl status systemd-timesyncd.service

2014年8月21日木曜日

FileUtils.chownとFile.chownの違いにハマる

えるしっているか、FileUtils.chownのuid/gidは数値も文字列も受け付けるが、File.chownは数値しか受け付けない。

# エラー
File.chown('yukithm', 'yukithm', '/path/to/file')

# 数値のuid/gidならOK
File.chown(500, 500, '/path/to/file')

# FileUtilsなら文字列でもOK!
FileUtils.chown('yukithm', 'yukithm', '/path/to/file')

FileUtilsの実装を覗いてみたら、Etc.getpwnamを使って数値に変換してた。

2014年5月12日月曜日

ESXi上でVirtualBoxがうまく動かなくてハマった件

VMware ESXi上の仮想マシンでさらにVirtualBox(Vagrantが使いたかった)を動かしたかったのだけど、すんなり動かなくてハマったのでメモ。

現象

vSphere側でハードウェアの仮想化とかVT-xとかの設定は全て有効にした状態でVirtualBoxでゲストOS(64bit)を起動すると、

IO-APIC(apic pin)1-9,1-11,2-0,2-1,.....2-15
 ..MP-BIOS bug: 8254 timer not connected IO-APIC
 ..trying to set up timer as ExtINT..failed.
 ..tyring to set up timer as BP..IRQ..failed.
 kernel panic: IO-APIC + timer dose'nt work!

のようなエラーがでて起動しない。

VirtualBoxのマニュアルによると、Linux kernel 2.6.18にレースコンディションがあってうんたらと書いてあって一見それっぽいんだけど、カーネルのバージョンは2.6.32だったので問題はなさそうだった。

解決方法

VirtualBoxのゲストOSのIO-APICの無効にすると動いた。

VBoxManage modifyvm 仮想マシン名 --ioapic off

VBoxManageコマンドだとこんな感じ。

config.vm.provider :virtualbox do |vb|
  # Don't boot with headless mode
  vb.gui = true

  vb.customize ["modifyvm", :id, "--ioapic", "off"]
end

Vagrantfileだとこんな感じ。

あと試してないけどLinux kernelオプションでnoapicを指定するのでも良さそうな気がする。

ただIO-APICはマルチコアCPUでは必須の機能らしいので、コア数を複数割り当てたい場合は諦めるしかないかもしれない。

とりあえずは動かせるようになったけど、なんだかしっくりこないのと新しい仮想マシンを作るたびにいじらなきゃいけないのでテンポ悪いのがなんとも……。

余談

仮想マシンをネストさせることをnested virtualizationとかnested vmとか言うらしい。ググる時の参考に。
nested virtualizationは色々難儀なのでできるなら避けたほうが良いと書いている人もいた。

親側の仮想マシンに対して仮想化関係の設定を有効にしてあげないと、そもそも入れ子で動かせなかったり、なんか32bitになったりする。
/proc/cpuinfo見てflagsにvmxがあるかどうかがひとつの判断材料になるとかなんとか。
vSphereでいうとCPUの設定のところの「ハードウェアアシストによる仮想化をゲストOSに公開」とか「CPU/MMX仮想化」のところの「Intel VT-x/AMD-Vを命令セット仮想化に使用し、Intel EPT・AMD RVIをMMU仮想化に使用」あたり。

さくらのVPSとかConoHaとかでVirtualBoxを使ってる記事はわりとよく見るので、親がKVMだとわりとうまくいくのかもしれない。

2014年5月1日木曜日

systemd-networkdでのネットワーク設定

systemd v210からsystemdはudevとnetworkdによるネットワーク設定がサポートされるようになった。
これを利用するとNetworkManagerやnetctlといったものを利用せずにsystemd自身でネットワークの設定が行えるようになる(既存のネットワークマネージャの類を使い続けることもできる)。

以下はArch Linuxでの例を挙げるけど、CoreOSやDebianなどでもだいたい同じだと思う。

networkdの設定ファイル

systemd-networkdでの設定は/etc/systemd/networkディレクトリに.network.netdev.linkファイルを配置していくことになる。

.networkファイル …… マッチするデバイスにネットワークを設定するファイル。
.netdevファイル …… マッチする環境に仮想ネットワークデバイスを作成するファイル。
.linkファイル …… マッチするデバイスのリンクを設定するファイル。

仮想デバイスとかブリッジとか込み入ったことをしない場合は、.networkファイルだけで事足りる。

resolv.conf

あと、systemd-resolvedは/etc/resolv.confを更新してくれない。代わりに/run/systemd/resolve/resolv.confを生成するので、これに対してリンクを張る必要がある。

# オリジナルをバックアップ
sudo mv /etc/resolv.conf{,.orig}

# リンクを張る
# systemd 214からファイルパスが変わった模様
sudo ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf

# systemd 213以前はこちらのパス
sudo ln -s /run/systemd/network/resolv.conf /etc/resolv.conf

DHCP

DHCPを設定する場合は素直にdhcpcdでも起動しておくのが一番手っ取り早いけど、それだと話が終わってしまうのでsystemd-networkdで設定してみる。

/etc/systemd/networkdhcp.networkみたいなファイルを作成する。ファイル名は任意。

[Match]
Name=en*

[Network]
DHCP=yes

こんな感じのファイルを作成して、systemd-networkdを再起動してあげると有効になる。
dhcpcdやnetctlを使用している場合は止めておくこと。

sudo systemctl restart systemd-networkd.service

ユニットじゃないのでdaemon-reloadは必要なさそうな気がするけど、もしも設定ファイルを認識しない場合はdaemon-reloadしてみるといいかもしれない。

sudo systemctl daemon-reload

セクション名やキーは大文字小文字を区別するっぽいので注意(nameとか書いてハマった)。

ちなみにMatchのNameのところはワイルドカードで指定できるので、インターフェイスが複数ないのであればざっくり指定してもいいかも。
おそらくsystemdとか使ってるケースではインターフェイス名もeth0とかじゃなくてenp2s0みたいなやつだと思うので(この命名規則については参考に挙げているリンクを参照)。
ざっくり言うとOSが認識した順にeth0, eth1とかするんじゃなくて、NICの刺さってるスロットとかの物理的な位置を名前に使おうよ、というお話。
どんな名前かはip linkifconfigで確認可能。

固定IPアドレス

固定IPアドレスの場合も設定方法はだいたい同じで、.networkに書く内容が違ってくるだけ。

例えばstatic.networkみたいなファイルを作成する。ファイル名は任意。

[Match]
Name=en*

[Network]
Address=192.168.1.22/24
Gateway=192.168.1.254
DNS=192.168.1.254

他にどんな設定が書けるのかはman systemd.network(とかman systemd.link)とかを参照。

ブリッジとか

ブリッジとかVLANとかはCoreOSのIntroduction to networkd, network managment from systemdに具体例があるので参考に。

Docker関係

Dockerを使うとdocker0みたいなブリッジが必要になるけど、この辺はdockerがbridge-utils(brctl)とかでうまくやってくれるみたいで、特に何も設定しなくても動いた。

参考

networkdの設定について

networkdができた背景

インターフェイス名の命名規則

2014年4月24日木曜日

PackerでVagrantのboxを作成

今までVagrantのboxはVeeweeを使って作っていたのだけど、今回からPackerを使ってみることにした。

PackerはVeeweeみたいにテンプレートが付属していないので自分で用意する必要があるのだけど、一から自分で書くのはハマりどころも多くて大変だなーと思っててずっと手を出していなかったんだよね。
でもまぁみんな野良テンプレートとか作ってるだろうから、それをベースにカスタマイズしてみるかーと検索してたら、Opscode改めChefがBentoっていうPackerのテンプレート集を出しているのを知ったのでこれを使うことにした。

Packerのインストール

Packer
http://www.packer.io/

公式サイトからバイナリパッケージをインストール。

Macの場合はHomebrewからもインストールできる。

brew tap homebrew/binary
brew install packer

Bentoをダウンロード

Bento by opscode
http://opscode.github.io/bento/

公式サイトからアーカイブをダウンロードするかgitでcloneする。
きっとどんどん新しくなっていくだろうからcloneしておくのがいいかな。

git clone https://github.com/opscode/bento.git

boxをビルドしてみる

bentoのpacker用テンプレートが入っているディレクトリに移動してpacker buildを実行する。
カスタマイズしたければJSONファイルをいじる。

cd bento/packer
packer build -only=virtualbox-iso \
             -var 'chef_version=latest' \
             -var 'mirror=http://ftp.iij.ad.jp/pub/linux/centos' \
             centos-6.5-x86_64.json

いくつかオプションを指定してCentOS 6.5のboxを作成してみた。
そのままだと、テンプレートで定義されてる全builderでboxが生成されてしまうので、-onlyでVirtualBox用のみ生成するようにした。
-varはテンプレート内の変数を上書き指定するオプションで、ここではISOイメージのミラーサイトを国内にして、最新のChefをインストールするように指定した。
ちなみに、chef_versionを指定しない場合は、omnibus-chefはインストールされないようになっていた。

これでbuildsディレクトリの下にboxが生成されるので、あとはvagrant box addしておしまい。

vagrant box add CentOS-6.5-x86_64 ../builds/virtualbox/opscode_centos-6.5_chef-latest.box

2014年4月16日水曜日

Ohai 7でプラグインの仕様が変わった

先日リリースされたChef Client 11.12.0から密かに同梱されているOhaiが7.0系に差し替わったのだが、これが結構な仕様変更を含んでいて、Ohai Pluginなどを自作して使ってる人は気をつけないとハマる模様。

プラグインの仕様変更

いちおう互換性が保持されるようになっていてv6のプラグインもそのまま動くのだけれど、実行するとこんな感じのWARNINGが出る。

[2014-04-16T15:50:02+09:00] WARN: [DEPRECATION] Plugin at /tmp/example/plugins/foo.rb is a version 6 plugin. Version 6 plugins will not be supported in future releases of Ohai. Please upgrade your plugin to version 7 plugin syntax. For more information visit here: docs.opscode.com/ohai_custom.html

細かい点で色々仕様が変わってはいるのだけど、一番大きいのはプラグインの書き方が変わったことと、プラグイン名を指定していた部分がアトリビュート名を指定するように変わったこと。

新しいプラグインの書き方

従来のプラグインはこんな感じだった。

provides "foo"

require_plugin "baz"

foo Mash.new
foo[:data1] = "foo1"
foo[:data2] = "foo2"

Ohai 7からはこんな感じになる。

Ohai.plugin(:Foo) do
  provides "foo"
  depends  "bar/baz"

  collect_data do
    foo Mash.new
    foo[:data1] = "foo1"
    foo[:data2] = "foo2"
  end

  collect_data(:windows) do
    foo Mash.new
    foo[:data1] = "win_foo1"
    foo[:data2] = "win_foo2"
  end
end

v6ではベタ書きスクリプトっぽかったのが、v7ではオブジェクトっぽく内容を定義する形になった。
上の例には書いていないが、普通にメソッドを書いたり共通処理用のクラスを別途まとめたりもできるようになっているので、複雑な処理が必要な場合にだいぶ書きやすくなったと思う。

collect_dataがデータを収集する部分なんだけど、引数でプラットフォーム毎の内容に分けて書くことができるようになった。
該当するプラットフォームのcollect_dataがあるとそちらが呼ばれて、なければ:defaultが指定されているcollect_dataが呼ばれる(指定しない場合は:default)。

また、v6ではrequire_pluginで依存するプラグインを指定していたが、v7からはdependsで依存するアトリビュートを指定するようになった。
利用する側はプラグイン名を知る必要はなく、「どのアトリビュートが使いたい」というのを指定すれば、Ohaiの本体が自動的にそのアトリビュートを提供するプラグインを読み込んでくれる。ここ割りと重要。

Rubyプログラムから使用する場合の注意点

プラグインの仕様が変わったことで利用の仕方も少し変わった。

普通に全プラグインを使う場合は従来と同じでOhai::System#all_pluginsを呼び出せば全部実行してくれる。

require "ohai"

ohai = Ohai::System.new
ohai.all_plugins

p ohai.data

自作プラグインなど特定のプラグインだけを実行したい場合は少し変わったのだけど、これがv6用プラグインとv7用プラグインで若干違っていたりしてカオスな感じがする。

require_pluginを使うパターン

v6用とv7用が混在している場合はこのやり方しかないように思う。

require "ohai"

Ohai::Config[:plugin_path] << "/path/to/oreore/plugins"

ohai = Ohai::System.new
ohai.load_plugins
ohai.require_plugin("foo/bar")      # v7の場合はアトリビュート名
ohai.require_plugin("hoge_fuga")    # v6の場合はプラグイン名

p ohai.data

まずload_pluginsというのが事前に必要になった。これを呼ばないとプラグインがOhaiに認識されない。
ちなみにload_pluginsを呼んでもv6用プラグインは読み込まれない。なぜならv6用プラグインは「読み込まれる=実行される」なので、問答無用で実行されてしまうから。
ただしv6用のプラグインであることは認識される(ソースファイル内の文字列とかで判定してるんだろうか?)。

require_pluginにはv7用プラグインの場合はアトリビュート名を、v6用の場合はプラグイン名を指定する。
v7用プラグインはload_pluginsによってprovidesなどのメタ情報がロードされるので、それを使って解決しているのだろう。
v6は前述のとおり読み込んでしまったら実行されてしまうので、読み込んでない状態では中に書かれているprovidesとかも認識されない。よってプラグイン名を指定することになるようだ。
ちなみにv7用プラグインのプラグインを指定しても動かない。

このへんの仕組みはOhai::ProvidesMapとかOhai::Loaderがやってるようなので、興味のある人はソースを読んでみるといいかも。

all_pluginsに引数を渡すパターン

このやり方はv6用プラグインが混在している環境では使えない。
というのも、v6用プラグインは指定できず、無条件に実行されてしまうから(この挙動が仕様なのか現バージョンの不具合なのかは不明)。

require "ohai"

Ohai::Config[:plugin_path] << "/path/to/oreore/plugins"

ohai = Ohai::System.new
ohai.all_plugins([
  "foo/bar",
  "languages/ruby"
])

p ohai.data

all_pluginsの引数にアトリビュート名を指定すると、該当するアトリビュートを提供するプラグインのみが実行される。
ただし、(v6用の)プラグイン名は指定できない。そして指定してないにも関わらず、存在するv6プラグインは全て実行される。

ちなみにload_pluginsは不要。というかall_pluginsが内部で呼び出している。

2014年4月7日月曜日

Rubyでクラスが継承されたら何かする

Rubyではクラスが継承されると親クラスのClass#inheritedというメソッドが呼ばれる。

class Foo
  def self.inherited(subclass)
    puts "#{self} is inherited by #{subclass}"
  end
end

class Bar < Foo
end

Baz = Class.new(Foo)
Foo is inherited by Bar
Foo is inherited by #<Class:0x007fdd11969330>

Class.newでサブクラスを生成すると無名のクラスが生成されて、最初に定数に代入されたタイミングでクラスの名前が設定されるというキモい仕様があるので、Class.new(Foo)のタイミングでは名前が出ていない。

Class#inheritedが呼ばれるタイミングは「クラス定義文の実行直前」とのことなので、上記のBarクラスにあれこれメソッドとか定義してあっても、inheritedが呼ばれた段階ではまだ記述したメソッドが存在しないことに注意。
ちょうど上記のように単に継承しただけの状態と同じクラスになっている。

似たようなものにModule#includedModule#extendedがある。それぞれモジュールがinclude/extendされた時に呼ばれる。

こんな機能を使うことはそうそうないと思うけど、仕事で使ってたとあるライブラリではプラグインを自動的にプラグインマネージャ的なものに登録する仕組みとして使われていた。

参考

2014年3月26日水曜日

forkとゾンビプロセス

とあるアプリから外部コマンドを実行する機能を実装していたんだけど、ふと気がついたらゾンビプロセスが大量にできていて焦った。

  • とあるアプリはデーモンプロセスでずっと生きている
  • 外部コマンドは終了を待たなくていいし出力も取らなくていい

ゾンビができちゃうダメな実装

とにかく実行すればいいってことで単純に次のようなコードを書いていた。
forkしてexecするだけという至って単純な実装(実際にはSTDOUTを/dev/nullに向けるとか色々あるけど)。

# 外部コマンドを実行する
# コマンドの出力とか終了には興味がない
def run_command(command)
  fork do
    puts "child: pid=#{Process.pid}, ppid=#{Process.ppid}"
    exec(command)
  end
end

puts "app: pid=#{Process.pid}"
run_command("/bin/date")

# この後イベントループとかで
# 終了しないデーモンアプリだと思って
sleep 10

で、これを実行するとこうなる。

$ ruby zombie.rb
app: pid=4083
child: pid=4114, ppid=4083
2014年  3月 25日 火曜日 23:49:05 JST

sleepのところで終了しないうちにプロセスを確認するとこんな感じ(関係ない行は省略)。

$ ps fo ppid,pid,stat,cmd
 PPID   PID STAT CMD
 1491  1493 Ss   /bin/zsh
 1493  4083 Sl+   \_ ruby zombie.rb
 4083  4114 Z+        \_ [date] <defunct>

実行した外部コマンドであるところのdateが見事にゾンビと化している。

で、zombie.rbが終了するとゾンビプロセスも消滅するのだが、先にも書いたとおり、このアプリは実際にはずーっと起動しているアプリなので終了しない。
ということは、ずーっとゾンビが残ったまま。しかもイベントを受けて定期的に実行する機能なので、どんどんゾンビが増えていく。バイオハザード状態。まずい。

なぜこんなことに

通常、子プロセスを生成した後はwaitでその終了を待って終了ステータスを得るのだが、逆に言うとwaitを呼ぶまではたとえ子プロセスが終了していても終了ステータスを保持しておかなければならない。いつ親からwaitを呼ばれるかわからないから。後になってwaitを呼ばれた時に「もうなくなっちゃったから分からないよ」では困るから。これがゾンビプロセスという状態。

つまりさっきの現象はforkするだけしてwaitしていなかったので、終了ステータスの情報だけどんどん残っていってる状態だったわけだ。

initに任せる

waitしなかったのが問題なのでwaitすればいいわけだけど、コマンドの実行終了を待ちたくないのでwaitしたくない。
じゃあどうするかというと、問題のプロセスをinitに引き取ってもらう。

initは平たく言うとOSの起動時に最初に実行(PID=1)されてOSの終了時までずっといるやつで、親プロセスがいなくなって孤児になったプロセス(親が先に終了して子だけ残ったプロセス)はinitが引き取ってくれることになっている(initの子プロセスになる)。更にいうと引き取られたプロセスのwaitも面倒見てくれる。

件のプロセスをinitに押し付けるには、forkして自分はすぐに終了するだけというプロセスを一段挟むようにする。

次のような流れになる。

  1. 親:forkして子を生成する
  2. 親:子をwaitする
  3. 子:forkして孫を生成する
  4. 子:終了する
  5. 孫:execする

親:アプリケーション
子:initに押し付けるためのプロセス
孫:実行したいコマンド

親は子をforkしたあとにwaitするんだけど、子はforkしたあとにすぐに終了しちゃうので目的のコマンドの終了を待たずともすぐに制御が返ってくる。
孫は(孫の)親であるところの子がwaitせずにとっとと終了してしまうので親がinitに切り替わる。
そして孫の終了はinitが最後まで面倒を見てくれるのでゾンビにはならない。めでたしめでたし。

修正した実装

というわけで、上記のように修正したのがこちら(修正したメソッドのみ)。
実行した時に動きがわかりやすいようにsleepとputsが入っている。

# 外部コマンドを実行する
# コマンドの出力とか終了には興味がない
def run_command(command)
  pid = fork do
    puts "child1: pid=#{Process.pid}, ppid=#{Process.ppid}"
    fork do
      puts "child2: pid=#{Process.pid}, ppid=#{Process.ppid}"
      sleep 2
      puts "child2: pid=#{Process.pid}, ppid=#{Process.ppid}"
      exec(command)
    end
    sleep 1
  end
  Process.waitpid(pid)
end

これを実行するとこうなる。

app: pid=5064
child1: pid=5095, ppid=5064
child2: pid=5098, ppid=5095   # まだchild1が生きてる時
child2: pid=5098, ppid=1      # child1が終了した後(ppidが1になっている)
2014年  3月 26日 水曜日 01:00:53 JST

プロセスの変遷はこのような感じ。

# まだchild1があるとき
$ ps fo ppid,pid,stat,cmd
 PPID   PID STAT CMD
 1491  1493 Ss   /bin/zsh
 1493  5064 Sl+   \_ ruby no_zombie.rb          # 親
 5064  5095 Sl+       \_ ruby no_zombie.rb      # 子
 5095  5098 Sl+           \_ ruby no_zombie.rb  # 孫

# child1が終了した後
$ ps fo ppid,pid,stat,cmd
 PPID   PID STAT CMD
 1491  1493 Ss   /bin/zsh
 1493  5064 Sl+   \_ ruby no_zombie.rb          # 親
    1  5098 Sl+  ruby no_zombie.rb              # 孫(PPIDが1になってる)

# 実行が終了してアプリだけ残ってる状態
# ゾンビプロセスは残ってない!
$ ps fo ppid,pid,stat,cmd
 PPID   PID STAT CMD
 1491  1493 Ss   /bin/zsh
 1493  5064 Sl+   \_ ruby no_zombie.rb

参考

2014年3月18日火曜日

capture3とtimeout

Rubyで外部コマンドを実行するには様々な方法があるけど、出力とステータスを取りたい場合はOpen3.capture3を使うと簡単に得られる。

# リファレンスマニュアルのサンプルから引用
require "open3"

o, e, s = Open3.capture3("echo a; sort >&2", :stdin_data=>"foo\nbar\nbaz\n")
p o #=> "a\n"
p e #=> "bar\nbaz\nfoo\n"
p s #=> #<Process::Status: pid 32682 exit 0>

面倒なパイプの制御などをしなくても簡単に標準入出力が扱える。

ところが、capture3では次のようなタイムアウト処理はうまく動かない。

require "timeout"
require "open3"

cmd = "/tmp/heavy.sh"

o, e, s = nil
begin
  Timeout.timeout(3) do
    o, e, s = Open3.capture3(cmd)
  end
rescue Timeout::Error
  puts "execution expired"

  # pidが取れないのでkillできない
  # それどころか、コマンドが終了するまでここに到達しない
end

p o, e, s

上記のスクリプトを実行すると3秒でTimeout::Errorが発生しそうだけど、実際には発生しない。
何故かと言うと、capture3が内部で使用しているpopen_runという実装の中で、次のようにensureでコマンドの終了を待っているから。

  def popen_run(cmd, opts, child_io, parent_io) # :nodoc:
    pid = spawn(*cmd, opts)
    wait_thr = Process.detach(pid)
    child_io.each {|io| io.close }
    result = [*parent_io, wait_thr]
    if defined? yield
      begin
        return yield(*result)
      ensure
        parent_io.each{|io| io.close unless io.closed?}
        wait_thr.join
      end
    end
    result
  end

というわけで、タイムアウトしたら中断したい場合は、この辺を紐解いて自前で同じような実装をすることになる。

この場合だとブロックを渡さなければIOと子プロセスのスレッドが返ってくるので、capture3の実装をブロックを使わないで書いたオレオレバージョンとかを作ればいいかもしれない。
ただしそれだとPIDが取れない(Process::StatusはPIDを持っているけど、これは終了するまで手に入らない)ので、popen_runのあたりも含めて自前で実装するはめになる。

なので、前回の外部コマンドを実行してタイムアウトしたらkillするではあえてspawnを直接使って実装したのでした。

2014年3月14日金曜日

外部コマンドを実行してタイムアウトしたらkillする

Rubyから外部コマンドを起動して、一定時間が経過しても終了しなかったらkillする、という処理をしようと思って次のようなコードを書いてみた(実際にはSTDOUT, STDERRを取るなどもっと複雑)。

#!/bin/sh
# Rubyから呼び出される外部コマンド

echo "do something"
sleep 30
echo "done something"
# 外部コマンドを実行してタイムアウトしたらkillする
require "timeout"

cmd = "/tmp/heavy.sh"

pid = spawn(cmd)
thr = Process.detach(pid)
begin
  Timeout.timeout(3) do
    thr.join
  end
rescue Timeout::Error
  puts "execution expired"
  Process.kill(:TERM, pid)
end

status = thr.value
p status

で、早速実行してみると、一見うまく動いているように見えるんだけど、実際にはheavy.shの中で実行しているコマンド(ここではsleep 30)は生き残っている。

実行中

$ ps xjf
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    1  1599  1599  1599 ?           -1 Ss     500   6:43 tmux
 1599  7765  7765  7765 pts/6     9309 Ss     500   0:00  \_ /usr/local/bin/zsh
 7765  9309  9309  7765 pts/6     9309 Sl+    500   0:00      \_ ruby run_command.rb
 9309  9311  9309  7765 pts/6     9309 S+     500   0:00          \_ /bin/sh /tmp/heavy.sh
 9311  9315  9309  7765 pts/6     9309 S+     500   0:00              \_ sleep 30

killされた後(heavy.shは終了したけどsleep 30は生きてる)

$ ps xjf
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    1  9315  9309  7765 pts/6     7765 S      500   0:00 sleep 30

これはどういうことかというと、UNIX系のOSでは親プロセスが死んで孤児になったプロセスはinitの養子になることが決まっているから(sleep 30のPPIDが1=initになっている)。
道連れになって親と一緒に死んだりはしない。

で、子だけ生き残って処理が続行されるのは都合が悪いという場合は、次のようなアプローチが考えられる。

  • killしようとしているプロセスの子プロセスを列挙して全部killする
  • 新しいプロセスグループにしてまとめてkillする

幸いにもRubyのspawnには:pgroupというプロセスグループを指定するオプションがあるので、これを指定して新しいプロセスグループで実行させることができる。
そしてProcess.killのpidに負数を指定すると、そのIDのプロセスグループ全体に対してシグナルを投げることができる(ちなみにkillコマンドも同じ仕様)。

通常、プロセスグループIDはグループリーダー、つまりそのグループの最初のプロセスIDになるので、ここではsapwnで返ってきたpidをマイナスにした値(pid=1234なら-1234)を使えばよいことになる。

というわけで、先のRubyスクリプトをこんな感じに修正することで、タイムアウトしたら外部コマンドとそこからさらに実行されたコマンド全てをkillすることができましたとさ。めでたしめでたし。

require "timeout"

cmd = "/tmp/heavy.sh"

pid = spawn(cmd, :pgroup => true)  # :pgroup => trueを追加
thr = Process.detach(pid)
begin
  Timeout.timeout(3) do
    thr.join
  end
rescue Timeout::Error
  puts "execution expired"
  Process.kill(:TERM, -pid)        # -pidに変更
end

status = thr.value
p status

参考

2014年3月12日水曜日

Rubyの並列処理ライブラリいろいろ

Rubyでワーカースレッドな並列処理がしたくて、なんかいいライブラリないかなーと探してたら見つけたのが次のふたつ(+おまけ)。
並列処理だとparalellが有名だけど、これは最初に決まった数のタスクがたくさんあって、mapやeachするとその部分がスレッド化してわーっと一斉にやるような感じなので、ちょっと自分が求めているものとは違った。
今回欲しかったのはそういうのではなくて、ちょうどWebサーバみたいな感じにぽつぽつとタスクがやってきてそれをワーカースレッドに割り振って処理させるようなやつ。
で、rubygems.orgやruby-toolbox.comを見て良さそうだなと絞ったのが上記のふたつ(これのほうがいいよ!とかあったら教えて下さい)。

RubyのQueueははじめからスレッド間のFIFOとして設計されているので、これを使えば自分でも簡単にワーカースレッドパターンは作れるのだけど、アプリケーション終了時のワーカーの安全な停止だとか色々な部分に気を配り始めると結構面倒なことになってくるので、それならすでにあるgemを使うほうがいいな、と。

workers

workersは何種類かの並列処理パターンを提供していて使い勝手が良さそうに思った。
目当てのワーカースレッドの他にもparallelと同じようなmap処理や周期的なタイマー処理もあったりして、わりと色々なことができる。
それぞれの分かりやすい使用例が1ページのドキュメントまとまっているので、ここにサンプルコードを書くよりはそっちを見てもらったほうがよいかも。

Workers
http://rubydoc.info/gems/workers/0.2.2/frames

thread

標準ライブラリともろかぶりな名前のこちらは、require "thread/pool"のようにして組み込みのThreadクラスを拡張する形でスレッドプールやfuture/promise/delayといったいくつかの並列処理パターンを実現してくれる。Everyを使えばworkersのタイマー処理と同じこともできる。
これも各パターンの使用例が1ページにまとまっているので見てもらったほうがよい。

thread
http://rubydoc.info/gems/thread/0.1.3/frames

celluloid

上記の他にcelluloidという、いわゆるアクターモデルによる並列処理をするgemもあるんだけど、ちょっと自分には巨大すぎて扱える気がしなかったのでパスした。
ちょうどcelluloidを利用している別のライブラリで不可解な動きにハマった経験もあったので、容易には手を出すべきではないな、と。

今回はworkersを使ってみた

threadはちょこっと並列処理をするにはシンプルでよいと思うんだけど、今回の用途としてはちょっとシンプル過ぎたのでworkersを採用してみた。
workersはsynchronizeがあったり、リトライがあったり、成功と失敗をグループ分けできたりと色々な機能がついてるのがちょっと嬉しかった。
そのうちpromiseとかパイプライン的な処理がしたくなったりしたらthreadを使ってみるのもいいかなーと思っている。

技術メモ用のブログ始めてみます

技術メモを残すためのブログを始めてみます。

日々の作業の中で知ったことをどんどん書いていければいいなと思います。
完全で美しい記事ではないかもしれないけれど、手早く新鮮なうちに。