2014年10月11日土曜日

共有ライブラリのアップデート時にプロセスの再起動はいつ必要か

共有ライブラリのアップデート時、対象のライブラリが起動中のプロセスから利用されているかを見て、プロセスの再起動を行うかどうかの判断をしている。

例えばこんなのとかですね。
$ sudo grep libssl.so.1.0.1e /proc/*/maps | cut -d/ -f3 | sort -u | xargs -r -- ps uf
プロセスを再起動すれば、更新されたライブラリが使用されるという認識で居るのだけれど、実際にそうなのか調べたことがなかった。
ライブラリをアップデートしたときに、どのタイミングで差し変わるか気になったので調べてみた。

共有ライブラリのサンプル


プロセス再起動時

まずは、普通に再起動を行ってみる。
$ ./test
Hell World
PID is 16856
Press Any Key To Exit...
別のターミナルで maps を参照してみる。
$ grep libexample /proc/16856/maps
7fee12576000-7fee12577000 r-xp 00000000 00:0f 14844378                   /usr/local/lib64/libexample.so
7fee12577000-7fee12776000 ---p 00001000 00:0f 14844378                   /usr/local/lib64/libexample.so
7fee12776000-7fee12777000 r--p 00000000 00:0f 14844378                   /usr/local/lib64/libexample.so
7fee12777000-7fee12778000 rw-p 00001000 00:0f 14844378                   /usr/local/lib64/libexample.so
libexample.soを更新してみます。
$ ./test
Hello World
PID is 17109
Press Any Key To Exit...
$ grep libexample /proc/17109/maps
7f33b9223000-7f33b9224000 r-xp 00000000 00:0f 14844450                   /usr/local/lib64/libexample.so
7f33b9224000-7f33b9423000 ---p 00001000 00:0f 14844450                   /usr/local/lib64/libexample.so
7f33b9423000-7f33b9424000 r--p 00000000 00:0f 14844450                   /usr/local/lib64/libexample.so
7f33b9424000-7f33b9425000 rw-p 00001000 00:0f 14844450                   /usr/local/lib64/libexample.so
まあ、普通に更新されますね。

他のプロセスがライブラリをロードした状態で、更新する

プロセスAで libexample.so を使用した状態で、libexample.so を更新し、プロセスBを起動する。 そのときプロセスBは、古いライブラリと新しいライブラリのどちらを使用することになるのか。
プロセスA
$ ./test 
Hell World
PID is 17248
Press Any Key To Exit...
プロセスAは起動させたままで、ライブラリを更新する。
$ vim libexample.c
$ make
$ sudo make install
プロセスBを起動する。
$ ./test 
Hello World
PID is 17343
Press Any Key To Exit...
このとき maps は以下のようになる。
$ grep libexample /proc/*/maps 2> /dev/null
/proc/17248/maps:7f707a138000-7f707a139000 r-xp 00000000 00:0f 14844464                   /usr/local/lib64/libexample.so (deleted)
/proc/17248/maps:7f707a139000-7f707a338000 ---p 00001000 00:0f 14844464                   /usr/local/lib64/libexample.so (deleted)
/proc/17248/maps:7f707a338000-7f707a339000 r--p 00000000 00:0f 14844464                   /usr/local/lib64/libexample.so (deleted)
/proc/17248/maps:7f707a339000-7f707a33a000 rw-p 00001000 00:0f 14844464                   /usr/local/lib64/libexample.so (deleted)
/proc/17343/maps:7f9413f52000-7f9413f53000 r-xp 00000000 00:0f 14844475                   /usr/local/lib64/libexample.so
/proc/17343/maps:7f9413f53000-7f9414152000 ---p 00001000 00:0f 14844475                   /usr/local/lib64/libexample.so
/proc/17343/maps:7f9414152000-7f9414153000 r--p 00000000 00:0f 14844475                   /usr/local/lib64/libexample.so
/proc/17343/maps:7f9414153000-7f9414154000 rw-p 00001000 00:0f 14844475                   /usr/local/lib64/libexample.so
libexample.so が置き換わってるので古いのを利用しているプロセスは (deleted) がつく。

fork時

fork した子プロセスでライブラリの関数を利用するプログラムを用意します。 1秒ごとに子プロセスを生成して、その間にライブラリを更新します。
$ ./test-fork
[Parent] PID is 18271
[Parent] Hell World
[Child] Hell World
[Child] PID is 18272
[Child] Hell World
[Child] PID is 18273
[Child] Hell World
[Child] PID is 18274

プログラムは起動したまま、ここでライブラリを差し替え。
$ vim libexample.c
$ make
$ sudo make install
プログラムの出力を見てみると、
[Child] Hell World
[Child] PID is 18276
[Child] Hell World
[Child] PID is 18277
[Child] Hell World
[Child] PID is 18279
[Child] Hell World
[Child] PID is 18280
変わらず "Hell" のままですね。
細かな動作までは調べてないので推測ですが、fork() の場合は動的にリンクされたあとのメモリ空間をコピーするためではないかと思います。

まとめ

  • 共有ライブラリをアップデートしたときはプロセスの再起動が必要
  • 子プロセスの再起動だけでは不足
  • 同じ共有ライブラリを他のプロセスで利用していたとしても、新しく起動したプロセスでは更新後の共有ライブラリが利用される

備考

今回は試してないですが、Dynamic Loadingというライブラリを動的にロードする方式がありこの場合どのような動作になるのかが気になります。
恐らく dlopen/dlclose で開き直せば新しいライブラリがロードされるのではないかと思いますが..
Dynamic Loading は恐らく Apache のモジュールのロードで利用されてるはずです。
nginx ではビルド時に組み込むはずなので、Dynamic Loading ではないはず。