MB-SystemのMBIOの構造

お久しぶりです。

研究関連でMB-Systemのソースコードをよんで修正する機会がありましたので、その時にキーワードとなったMBIOのしくみについてまとめます。

はじめに

MB-Systemとは「MB-System is an open source software package for the processing and display of bathymetry and backscatter imagery data derived from multibeam, interferometry, and sidescan sonars. This software is distributed freely (and for free) in the form of source code for Unix platforms. 」で要するに海底地形データの処理に使用するソフトウェアです。ということでかなりマニアックな領域、情報系とはおよそ関係ない領域のことなので書いてもあんまりじゅようないかなーと思いつつ、私自身どこかにまとめとかないと忘れそうなのでまとめておきます。

MBIOとは

一口に測深データといってもベンダ・機関・機器・versionによって使用しているフォーマットが大きくことなります。 そこでMB-Systemでは測深データを読み込んだ後MB-System独自のデータ形式に変換し統一的に扱えるようにしています。 そのデータ形式(構造体)と操作(関数)を提供するのがMBIOです。ライブラリ化されているので外部からも使えます。 MBIOを使うことで元のデータ形式に依存しない操作ができるようになるわけです。 もちろん各フォーマットへのエクスポートもサポートしています。

MBIOに新フォーマットを対応させる際の作法

今回わたしがやったのは修正ですので、新フォーマットに対応させるということはしていませんが、 調べる途中でだいたい見えてきたのでかいておきます。もしかしたら抜けがあるかもしれません。

src/mbio/配下にmbr.cというファイルを作り(は任意。フォーマット名にするといいかもしれません)、 対応させたいフォーマットからMBIOに変換するための関数を実装します。慣習的に下記のようにmbr_register*という名前になっているようです。

int mbr_register_*(int verbose, void *mbio_ptr,
                int *error);

ここでverboseは出力のレベル、mbio_ptrはMBIOの構造体へのディスクリプタerrorはエラーが起こった時の書き込み先のポインタが渡されます。 基本的にmbio_ptrに、がしがしデータとそれらを扱う関数をセットしていけばおkです。 他のフォーマットの実装を参考にしつつ実装すると楽かと思います。 また、実装しない関数はNULLを入れておくのが作法なようです(ある関数が使えるかどうかの判定が多くの場合NULLかどうかでされている)

つぎにmb_format.hを編集してフォーマットIDを割り当てます。 1000番台の値を割り当てとけばまず、衝突することはないでしょう。 mb_format.hに追記します。

#define MBF_* ID

これと先ほど作成したbr_register_*を紐付ける処理を書きます。 mb_format.cのmb_format_register関数の中に下記のようなelse ifの羅列があるので、そこに追記しましょう。

        else if (*format == MBF_*)
                {
                status = mbr_register_*(verbose, mbio_ptr, error);
                }

また同じくmb_format.cにmb_get_format関数というフォーマットを取得し返す関数があります。 これに新しく追加したフォーマットの判別処理を書きましょう。 例えば独自フォーマットの拡張子が.fooとすると下記のようになります。

        if (found == MB_NO)
            {
            if (strlen(filename) >= 5)
                i = strlen(filename) - 4;
            else
                i = 0;
            if ((suffix = strstr(&filename[i],".foo")) != NULL)
                suffix_len = 4;
            else if ((suffix = strstr(&filename[i],".FOO")) != NULL)
                suffix_len = 4;
            else
                suffix_len = 0;
            if (suffix_len == 4)
                {
                if (fileroot != NULL)
                    {
                    strncpy(fileroot, filename, strlen(filename)-suffix_len);
                    fileroot[strlen(filename)-suffix_len] = '\0';
                    }
                *format = MBF_*;
                found = MB_YES;
                }
            }

基本的に

            if ((suffix = strstr(&filename[i],".foo")) != NULL)

            else if ((suffix = strstr(&filename[i],".FOO")) != NULL)

                *format = MBF_*;

の部分を書き換えればよさ気な雰囲気です(基本的に拡張子しかみてないんですね...

以上をするとフォーマットを自動判別して適切な変換関数を呼び出しあとはよしなに扱ってくれます。

ISUCON4予選に参加しました

ということで昨日09/28にサークルの人(わたし、りょい君、kakira君)でチーム「☆(ゝω・)vキャピ」として参加しました。

当日まで

2回ほど勉強会を開きました。一回目は肝になりそうな部分をそれぞれがまとめて発表し勉強するという形式。 具体的には負荷とはそもそもなにか?(わたし),MySQLまわり(kakira君),memcachedまわり(りょい君)って感じにしました。これはわたし担当分は普段さわっているから、kakira君は競技プログラマなのでこのあたりデータ構造とかアルゴリズムとか関連付けて理解できそう、りょい君がこのメンバの中で一番Perlかけるので実装の際にしっているとつよいって感じで割り当てました。 AMIからの起動とか軽い動作確認などもこの日にしました。 そして、予選の方針として「推測するな計測せよ」というのを徹底していこうということ、 またそれとあわせ最初一時間はチューニングを始めずに統計をとる、ソースコードを読むなどして構造を理解最初のボトルネックを探す、そして最後の1時間は再起動したらうまくいかなくなり失格なんてことののないようチューニングをやめ確認をしているというふうにすることに。 また、選択した言語はのこり二人が読み書きできるということでPerlにすることに。(個人的にはPythonがよかった...)

二回目の勉強会ではISUCON3の過去問を解くという形。

開始直前

始まる前にチャットサーバとか、ディスプレイの設定(デュアルディスプレイにした)とかそういうことをしておきました。チャットサーバにはDevHubというものを使わせていただきました。これを採用した理由は普通にチャットできる部分とメモとしてマークダウンで書いておいておける場所がある点。チャットはURL貼るという用途に しか使いませんでしたが(普通の会話は口頭でしていた)、メモは色々わかったことなどを各々が書き込んで結構使いました。結構役に立ちました。↓予選後のスクリーンショット

f:id:ikaro1192:20140928173547p:plain

予選開始

とりあえずAMIを立ち上げベンチマークを走らせます。(10:11) 最初に決めたようにわたしはサーバ周り、りょい君、kakira君にはアプリケーションのソースコードを読んだり、サイト構造を把握してもらったりをお願いしました。

まず、psやnetstatからサーバで動いているプロセスリッスンしているポートを把握。前回までと大きく構成変わっているのはwebサーバデフォルトでhttpdではなくnginxであること、 memcachedが動いていないことでしょうか? そしてわたしは各種計測ツール導入。入れたものとしてはdstat,percona-toolkitなんかをいれました。 また、nginxでアクセスログの先頭に処理時間を表示するように設定。ついでに設定を見るとワーカーが1だったので(一時間はチューニングしないという話でしたが単純な変更で済むので)ワーカを4にして再起動。 MySQLについてもスロークエリを出力するように設定して再起動。また、今回はsupervisordがRuby実装ということで使うPerl実装に切り替えました。 ほんとはSSHもisuconユーザでログインできるように設定するはずだったのですが、なんか焦ってしまいできない...こんなところで時間潰すのももったいないので他の人には不便をしいますが(ほんと申し訳ない...)、とりあえず一旦やめ再度ベンチマークを走らせます。(10:44) 最初走らせた手応え、前回までの感じで大体workload8あたりがベストなのでこの時点でその程度の値にして実行。その結果スコアが3200ぐらいになり

となりました。やったね!暫定一位!このままいければよかったんだけど...

dstat,topの結果をみているとロードが最大3〜4前後であること、IOというよりCPUがネックになっていそうなこと、時々ポンとIOが来ること、やっぱりMySQLCPU使用率が高いことがw狩りました。また、ログ出力の関係からか常にディスクアクセス(書き込み)が発生しています。また、メモリは全然使い切れていないようなのでメモリにのせる実装が良さそうですねとか考えました。 スロークエリの結果からはなんかlogin処理であるクエリが殆どをしめているっぽいです。login_logというテーブルを参照しているのでログインまわりかなとか考えつつ。 また、ページごとのアクセス数の偏り、なんかをみてみても静的ファイルを除いてログインまわりに結構アクセスが来ています。↓

 5611 /
3087 /login
565 /mypage
2 /report
1 /tmUnblock.cgi

ソースコード解析しているkakira君からも該当クエリがめっちゃ効率わるいという話だったので、とりあえずここをチューニングしようという話に。 ソースコードが単純であることからmemcachedに全部のっければいいのでは?という話に。 これはメモリ上にのせるというよりも検索コストをさげるのとSQLでCPU使ってるのでKVSとしてO(1)でアクセスするという意味合いがつよいです。

ということでソースコード書き換え開始とその前にisuconユーザのホームディレクトリ配下をgit管理するようにしました。 kakira君がSQLソースコードをよみ大まかな設計をし、りょい君が実装していきます。わたしはそれにアルゴリズムまわりにちょっと口を出す程度。 わたしはnginxで静的ファイルを返すように変更したり、MySQLのパラメータを変えたりと細かなチューニングをしていきます。

とりあえずIPでの遮断の方を書き換えベンチを走らせると6000超えました。この調子でいけるか?ということでユーザロックのほうも実装開始。 これはいけるか?って思いましたがここからが... お昼も食べつつ13:30ぐらいに再度ベンチを走らせるものの/mypageがfailしまたreportとの付きあわせがうまく行かないらしく、スコアが送信されない状態... 送られれば100000越えてたのに...って感じでデバッグが開始されます。が、なかなかバグの原因がわからない。きっとmypageにアクセスできないはずのユーザがログインできているだろうと推測はできたのですが、手動でしらべるのは手間がかかる、またエラーログとしてページが予想されたものとことなりますだけでは情報がすくない... さらにPerlのデータ構造まわりの参照なのかスカラなのかハッシュあのかよくわからないというあれが発生し、さらに泥沼に... 改善したと思ったら今度はユーザロックをし続けているっぽい... 15:30頃一旦差し戻そうというはなしになり、スコアが3000に逆戻り... ソースコードをあらためて綺麗な心で書き換えはじめます。 うまくいかない...って感じですでに16:30。もう無理そうなので一旦諦めチューニングして別のところをチューニングし始めます。 ところでおなじBANなのになんでIPのほうはfailしてないのって話になりどうやらIPで遮断されたものは一回もないようなので、とりあえずそこのコードを単純にreturnするようにしました。これで6000程度に戻る。 あとはユーザのテーブルにインデックスをはったりして6500前後まで行きました。 もしかしてMySQLのデータを全部/tmp/shmにおけばいいのでは?とか考えて設定ファイル書き換えたらなんかMySQLが立ち上がらない... 設定ファイル差し戻してもダメ...結局これの解決をしていたら時間が来てしまいました...

反省点

  • 実装力低い...

これが大きかったです...とくにわたしがPerl読み書きできないので...また、コンテストの性質上コードを読んでいる時間が長くなるのですぐに実装できるより読みやすい言語のほうが良さそうです。次回参加する際はわたしもプログラミングまわりで力になれるようにしておきたいです。

*ダブルチェックをする

実は途中でわたしがsupervisordを再起動しとうとおもって、bashの履歴機能つかって再起動していたら実は複数回memcachedを再起動していてperlのエラーログがうまく出されないという事象がありまして、これは実サービスでやったらプロセスによってはとんでもないことになり非常に反省しております。また、今回でも余計に時間をかけさせてしまったというので申し訳ない。 再起動など影響のある作業するときはきちんとコマンドがあっているかチェックをする。声掛けだけではなく、ダブルチェックするなどを怠らないようにしないといけませんね。

*ダメそうだったら途中ですぐに諦める

loginまわりでかなり時間をとっていしまいました。 これは途中で方針変更できるはずなのに強行突破しとうとしたのは間違いでした。 MySQLのテーブル設計をきちんとする(一回でも成功しているかしていないかの判定があったのでそこのテーブルをわけるなど)、その上でクエリの改善をするなどで十分高速化できたはずで、その場合加点が0というわけではなく多少は加点があり、さらに別のポイントのチューニングにも時間をまわせました。

感想

今年が初参加でしたが、とてもおもしろかったです。 また、技術者として自分に足りない部分を再度確認できるよい機会でもありました。 問題自体は去年とはことなり、複数問題点が含まれているというより大きな問題がありそれを解決するって感じだったのかなって感じてます(もしかしたらたくさんある問題点をわたしたちがみつけられなかっただけなのかもしれませんが...)やっぱり計測大事ですね。 来年はもっとスコア伸ばしていきたいです。

参加者・運営の皆様お疲れ様でした!

追記

学生枠5位になんとか入り本戦出場できることになりました。 わたしは研究の関係で参加できないので残り二人に頑張ってもらいます。

MB-Systemのmbeditコマンドがセグフォで落ちてハマった話

最近Qiitaはじめてそれとブログの使い分けを模索しております。ということでブログにはいろいろ試行錯誤した結果、Qiitaには完全に手順化されたり、検証された事実ベースで書いていこうかなと思います。ということで今日ははまったはなしとその対策。

MB-Systemというのは「Seafloor Mapping Software」ということらしいです。
研究で使うことになるツールの一つ。
これをFedora20にソースから入れたんだけどmbeditが起動せずにセグフォで落ちて困ってた。
configureの出力見てもちゃんと全部の機能Enabledになっているし、ldd mbeditしても特に共有ライブラリ解決できてないとか、変なの読み込んでいるとかなさそうだった...
しかたなくソースを読むことに。printfでどこでセグフォが起きているかの特定の作業。
幸いなことに起動時に落ちることからmainであたりをみていけば良い。do_mbedit_init関数を呼び出しているところでセグフォが起きているっぽいのでそちらをさらにしらべていく。どうやらフォント読み込んで適応しているところで落ちているっぽい。勘でロードのミスっていてないものを適応しようとして落ちているのかなーと思ってX Window Systemの登録されているフォント調べるためにxlsfontsで確認。案の定ない。yum searchしてそれっぽいパッケージあったのでインストール。ちゃんと動きました。
フォント関係もconfigureでチェックしてくれればいいと思うんだけど、難しいのかなぁ...

Fedora20でEMOBILEの無線LAN接続が不安定になった話

Fedora20でkernelをアップデートしたらEMOBILEのGL06Pの無線LAN接続が不安定になった。
原因はおそらくこれなので古いカーネルに差し戻したら(幸いカーネル自体は残っていたのでブートローダでkernelを変更するだけ)今までどおり使えるようになった。
他に困っている人がいるかもしれないのでメモ。
無線LANドライバが原因かなと思うけど他のルータにつないでもふつうに使えるのが謎い...

and演算子の短絡評価にはまった話

以下のコードで論理演算でなぜか右の関数funcが呼び出されてない><

    return a && func();

ってことになやまされ、とりあえず問題を分解するために

    auto f = func();
    return a && f;

としたら正常に動いた。
なんで?って思ってTwitterでつぶやいたら一瞬で反応がかえってきた。
どうやら短絡評価というらしい。
これはつまり評価の際「false && func()」の時点で答えはfuncの返す値にかかわらずfalseになってしまうのでここで評価をやめてfuncは永久に呼び出されないというもの。funcが重い処理だったりするときに便利だけどfuncがboolを返す以外の副作用をもってたりすると今回みたいに意図した結果を返さずハマる...評価させたいものはバラすか、あるいは左にもってくるかしないとダメということですね。

もう忘れない。踏みぬいたバグの数だけ強くなれるよね(と信じたい

CentOS6.3+nginxでJenkinsを導入した時のメモ

とりあえず入れてみたので手順メモ。
構成としてはnginxをリバースproxyにして、/jenkinsにきたものをjenkinsに転送させている。

まずjenkinsを入れる。

$sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
$sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
$sudo yum install jenkins

また、jenkinsを動かすためにはjava-1.6.0とかがひつようなようなので入れる。

$sudo yum install java-1.6.0-openjdk

設定ファイルを編集。念の為バックアップをとっておく。

$sudo cp /etc/sysconfig/jenkins{,.bak}
$ sudo diff /etc/sysconfig/jenkins{,.bak}
133c133
< JENKINS_ARGS="--prefix=/jenkins --httpPort=${JENKINS_PORT} --ajp13Port=${JENKINS_AJP_PORT}"
---
> JENKINS_A||<RGS=""

つぎにnginxの設定。

$ sudo cp /usr/local/nginx/conf/<設定ファイル名>{,.bak}
$ sudo vim /usr/local/nginx/conf/<設定ファイル名>
$ sudo diff /usr/local/nginx/conf/<設定ファイル名>{,.bak}
13,16d12
<     	
< 	location /jenkins {
<       	    proxy_pass http://localhost:8080; 
<     	}

特に複雑な設定がいるわけではない(ちなみに実際の運用に使っているものには一応BASIC認証をかけた)

そして起動。

$sudo /etc/init.d/jenkins start
$ sudo /usr/local/nginx/sbin/nginx -s stop
$ sudo /usr/local/nginx/sbin/nginx

アクセスするとちゃんとうごいた。
とりあえずJenkinsちょっとつづ使っていこう。

iノード(inode)の使用量をディレクトリごとに表示するワンライナー

ファイルのメタ情報を格納するiノード(inode)というものがあるがこれが枯渇するとそれ以上ファイルが作れなくなってしまう。
iノードの数が少なくなってきた時どこが主にiノードを食っているのか確認したいことがあると思う。
が、特定ディレクトリのiノードを表示するコマンドは残念ながらない。
しかしiノードはファイル/ディレクトリに対して(ハードリンクをしない限り)ユニークに割り当てられるということを用いて大体の数を算出することができる。
以下がそのワンライナー。カレントディレクトリにあるディレクトリのiノードを表示してくれる。
使い方としてはdf -iでiノードが減少しているディスクをさがしそこのディレクトリに移動、肥大化しているディレクトリを調べる。
その肥大化しているディレクトリに移動...を繰り返して範囲を適切に狭めていく。

    for dir in `ll|grep ^d|grep -v "\./"|awk '{print $9}'`; do echo  `find ./$dir -true|wc -l` `pwd`/$dir; done | sort -nr

していることとしては

    ll|grep ^d|grep -v "\./"|awk '{print $9}'

でllの出力のうち先頭のパーミッションを解析してディレクトリのものをフィルタし、awkでディレクトリ名を取り出している。
ちなみにllのフォーマットがディストリビューションによって異なっていたり、そもそもなかったりするのでgrepawkの部分注意。
ls -lを使ったほうが無難かもしれない。

さてその結果の各行ごとに回しながら取り出して

    for dir in `ll|grep ^d|grep -v "\./"|awk '{print $9}'`

でdirに束縛する。

そして

    do echo `find ./$dir -true|wc -l` `pwd`/$dir

で./dir以下にあるファイルすべてを列挙しwc -lでその数を数えている。
あとは見やすいようにpwdとdirでそのディレクトリを表示している。
これだけでもいいがファイル数順(=iノードを消費している順)にならんだほうがみやすいのでsort -nrで並べ替える。