2015年10月19日月曜日

移行しました

Github pagesに移行してみました。
https://kjmkznr.github.io/

2014年12月7日日曜日

Apacheモジュールのアップデート時にrestartは必要か

Apacheのモジュールをアップデートしたときに通常は再起動を行っていますが、そもそも再起動が必要なのか、gracefulだけで大丈夫だったりしないかということを調べてみました。

Apacheのモジュールロードのタイミング

モジュールをロードするコードはこのあたりだろうか。

https://github.com/apache/httpd/blob/2.4.10/modules/core/mod_so.c
https://github.com/apache/httpd/blob/2.4.10/modules/core/mod_so.c

Apache からは APR の apr_dso_load() を使っているみたいなので、APRの方を覗いてみる。
https://github.com/apache/apr/blob/1.5.1/dso/unix/dso.c

複数のOSをサポートするために #ifdef が多用されててちょっと見にくいが、Linux環境であれば dlopen() が使用されるようだ。

さて、dlopen() が使用されるということが分かったので、呼び出し元に戻る。
最初に提示したコードは load_module() の中に記述されている。
これはどこからコールされているのかというと、同じ mod_so.c の中にある。



AP_INIT_TAKE2は引数を二つもつディレクティブで、
RSRC_CONFは, の外に記述するという意味のようです。
最後の EXEC_ON_READ は名前からも推測できますが、設定が読み込まれたときに実行されます。
実行するのは二つ目の引数である load_module() です。

設定が読み込まれたときに実行されると言うことは、graceful でも大丈夫な気がします。

モジュールをアップデートしてみる

graceful でも大丈夫そうということが分かったので、実際に試してみます。
mod_hello という簡単なモジュールを作ってみます。
上記のGistの mod_hello は Hello という設定がコンフィグに出てくると、第一引数の文字列をエラーログに出力するだけのものです。
インストールは apxs で行います。
$ apxs -c mod_hello.c
$ sudo apxs -i -a -n hello mod_hello.la 
httpd.conf に設定を追加します。
+LoadModule hello_module modules/mod_hello.so
+Hello world
Apache を起動します。
$ sudo apache2ctl start
 * Starting apache2 ...
[Sun Dec 07 16:11:41.528152 2014] [:notice] [pid 24998] Hello world
エラーログに "Hello world" と表示されました。
続いて、モジュールを書き換えてみます。

mod_hello.c の21行目を "Hello %s" から "Hell %s" にしてみます。
再度、モジュールをコンパイル・インストールし、graceful してみます。
$ apxs -c mod_hello.c
$ sudo apxs -i -a -n hello mod_hello.la
$ sudo apache2ctl graceful
 * Gracefully restarting apache2 ...
[Sun Dec 07 16:30:57.456100 2014] [:notice] [pid 25436] Hell world
変わりました。

結論

  • モジュールをアップデートするときは graceful で十分

参考文献

追記(2015/02/01)

mod_proxy では設定が反映されなかったりすることもあるそうで、graceful だと不十分な場合もあるようです。
共有メモリを使うようなモジュールだと古い設定がそのまま使われるとかありそうなので、モジュールの中身次第では注意が必要そうです。

2014年12月6日土曜日

Gentoo で FIDO U2F Security Keyを使う


Yubicoのオンラインストアで FIDO U2F Security Key を買ってみました。
他にもFIDO U2Fに対応しているものとしては、高機能なヤツとか小さいヤツとかあり、OpenPGPに対応していると書かれているのでちょっと惹かれましたが、入門としては一番安いヤツでいいかなと思い、これを選びました。

U2Fの使い方については「Googleの二段階認証にセキュリティキーを使う」が分かりやすいですが、Linux環境で動作させる手段は記述がありませんでしたので追加で調べました。

1. 事前準備

まず、と言ってもこれ一つだけですが、ChromiumからSecurity Keyにアクセスできるようにしなければなりません。
YubicoのSecurity Keyは hiddev で提供されます。
これへアクセス出来るように udev ルールを記述する必要があります。

https://github.com/Yubico/libu2f-host/blob/master/70-u2f.rules

udev ルールは上記のGithubに上がっています。
systemd を使用している環境であれば、これで動くらしいです。

私は Gentoo で OpenRC を使用しているので上記のルールでは動作しませんでした。


上記のようにパーミッションとグループについて設定するようにしました。
Security Keyを利用するユーザは plugdev に入っている前提です。

このルールを /etc/udev/rules.d/ に放り込めば準備完了です。

2. 使用する

Google の認証に使用してみます。

https://security.google.com/settings/security/securitykey/add

Security Keys を追加するページにChromiumでアクセスし、案内に従って操作するだけです。
問題なければ、最終的に完了ボタンが押せるようになります。
udev のルールに不備がある場合は、「次にセキュリティキーを挿入してタップしてください」というメッセージが出続けると思います。


せっかくなので Gentoo Advent Calendar 2014 の6日目に登録しました。

2014年11月9日日曜日

ISUCON4 本戦に参加してきました

先日 ISUCON4 の予選参加の件をメモしてましたが、技術的なことについてメモしてませんでした。すみません。
でも、どんなことやったかあまり覚えてないので予選の方は書かないかもしれません。

無事予選を突破することが出来、本戦に参加する資格を得ましたので、本日参戦してきました。
予選と同じメンバーで、Oops!というチーム名で参加してました。

もちろん目標は賞金狙いだったのですが、結果から言うと惨敗でした。
他のチームに負けたと言うより、運営に負けたという感じがしています。
1位2位とそれ以下という感じで、スタートラインに立てなかったようです。


最終スコア

最終的な公式スコアはそのうち isucon.net の方で公開されると思いますが、競技時間内に提出できたスコアは、最終 8,461 で最大が8,548でした。

めも

  • 07:30 起床。第一関門の起床コンテストの壁突破。
  • 08:00 出発。
  • 09:00 渋谷着。予選の時と同じくヒカリエ近くのかつやでカツ丼を食べる。
  • 09:30 ヒカリエ到着。
  • 10:00 開場。カフェスペース奥のちょっと長めの机をゲット。
  • 10:30 諸々準備が終わり、競技開始まで待ちぼうけ。
  • 10:50 サーバのIPアドレス、パスワードが書かれた封筒を貰う。
  • 11:00 競技説明。
  • 11:19 
    • 初期スコア計測。
    • 予選はPHPを選択したが、本戦では Go を選択。
    • この時点で 8,059 。
    • ただし、ローカル環境でのベンチで、公式のスコアとは違う。
    • iperf でノード間の帯域を調べてみたら 33Gbps とか出ていたので、ネットワークは余裕っしょという感じになる
  • 11:20 - 12:30 
    • 何回かベンチを走らせ、ログを集計したりした。
    • プログラムを眺めて修正すべき箇所のあたりをつけた
  • 12:30 - 13:20
    • 何をやってたんだか不明。
  • 13:20
    • 公式のスコアを計測。
    • 7,665
  • 13:20 - 15:40
    • nginx の設定とかを秘伝のたれに差し替え。
      • この辺は @2matzzz がやったはず
    • その間に動画の配信を nginx からにするようにプログラム書き換えた。
      • 元々は動画自体がRedisに突っ込まれていた
      • アップされた動画の SHA1 を求めて、Redisにハッシュを登録
      • 動画自体はファイルに書込。書き込んだファイルは nginx で公開。
      • 広告の JSON 内の URL を書き換えて、動画のURLを変更。
        • /assets/xxxx.mp4 みたいなURLにした。
    • ローカル環境スコアが20,391
  • 15:40 - 16:10
    • 3台のマシンでファイルを共有できるように NFS を構成してくれたので、そっちに動画を描き込むように修正。
    • その他 nginx を調整。
    • 動画ファイルの共有のためにRedisに登録された動画を ngx_redis を使い取り出せないかとやってみたけど、効果がなかったため止めた。
    • ローカル環境スコアが23,613
  • 17:00
    • 公式スコアを計測
    • 6,779
    • スコアが下がってしまい焦る。
  • 17:00 - 18:30
    • 今まで1台のみでベンチマークしていたが、ノードB,Cの2台構成で試したところ、レポートのあたりでFAILED
    • ログをノード間で共有してなかった
    • Redisに突っ込んだりしてみたけど、最終的にNFSに書き込むようにした。
    • スコアは2台でも変わらなかった
  • 18:30 - 18:50
    • やってることはストップして、環境の整理
    • 再起動とかを行う。
    • ログの出力を止めたりし、最終の公式スコアを測定。
  • 18:50
    • finish.txt とかを確認
    • 終了モード。

反省点

  • クライアントからのリクエストはちゃんと調べましょう。
    • 普通の環境だとブラウザにキャッシュさせるとかは普通にやるけれど、ISUCONではそのあたりをまったく考慮していなかった。
    • 過去のISUCONの問題で予習していくうちに、ISUCONの枠に囚われてしまった感がある。
  • ローカルのベンチマーク環境とリモートのベンチマーク環境の違いをよく考えよう
    • なぜリモートのベンチマークだとスコアが下がるのかをよく考えていなかった
  • 落ち着こう
    • 一つのこと(今回はGo言語のプログラムの修正)に集中しすぎた
    • チームメンバーとのコミュニケーションをもっととれば良かった
  • Gentooとかコンパイルオプションとかそういうことの前にやることはたくさんある。
    • Gentooを使うのは最終手段とすべき。
松鵜先生のチームに勝てたのは嬉しかったが五十歩百歩という感じであまり喜べないですね。

最後に、楽しいイベントを開催してくれた運営の皆様ありがとうございました。
きっとまた来年も参加させていただきます。

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 ではないはず。

2014年9月28日日曜日

ISUCON4 オンライン予選に参加してきた

ISUCON4のオンライン予選をLINEカフェにお邪魔させて頂きながらやってました。
細かくメモってないから、覚えてる範囲で日記にしておく。

メンバーは専門学校時代の同級生とその同僚で3人。
言語はPHPを選択しました。


  • 08:50 渋谷到着。普段出勤するより早い。
  • 09:00 近くのかつやでカツ丼ミニを食す。
  • 09:20 ヒカリエ11Fに着。ローソンで昼飯飲み物を購入。
  • 09:30 LINEカフェに突入。
  • 10:00 予選開始。bitbucket のリポジトリにコードなどをつっこむ。
  • 10:50 PHP などの初期設定など、もろもろ雑作業を終えて最初のベンチマーク(スコア: 1,646)
  • 11:18 スコア 3,297
  • 11:49 スコア 3,252
  • 12:36 スコア 2,460
  • 13:36 スコア 2,507
  • 13:51 スコア 24,397
  • 14:25 スコア 28,160
  • 14:42 スコア 28,785
  • 15:10 スコア 39,377 この時点で5位。このスコア以降はプログラムを大幅に書き換えようとして制限時間内に終わらず。

最終的に暫定10位以内に入ることは出来なかった。無念。
ami 提出のため、楽天カフェに移動し反省会。
再起動したら 46,274 までスコアが上がった。無念。

今回の反省点。

  • プログラムの大幅な書き換えと平行して、そのままのコードでチューニングしていく人がいるべきだった。
  • SQL力が足りなく、コード内のSQLが何をやっているのかを把握し切れていなかった
この2点が大きいと思った。

技術的な点はそのうちひっそりと追記します。