ISUCON4予選に参加しました
ということで昨日09/28にサークルの人(わたし、りょい君、kakira君)でチーム「☆(ゝω・)vキャピ」として参加しました。
当日まで
2回ほど勉強会を開きました。一回目は肝になりそうな部分をそれぞれがまとめて発表し勉強するという形式。 具体的には負荷とはそもそもなにか?(わたし),MySQLまわり(kakira君),memcachedまわり(りょい君)って感じにしました。これはわたし担当分は普段さわっているから、kakira君は競技プログラマなのでこのあたりデータ構造とかアルゴリズムとか関連付けて理解できそう、りょい君がこのメンバの中で一番Perlかけるので実装の際にしっているとつよいって感じで割り当てました。 AMIからの起動とか軽い動作確認などもこの日にしました。 そして、予選の方針として「推測するな計測せよ」というのを徹底していこうということ、 またそれとあわせ最初一時間はチューニングを始めずに統計をとる、ソースコードを読むなどして構造を理解最初のボトルネックを探す、そして最後の1時間は再起動したらうまくいかなくなり失格なんてことののないようチューニングをやめ確認をしているというふうにすることに。 また、選択した言語はのこり二人が読み書きできるということでPerlにすることに。(個人的にはPythonがよかった...)
二回目の勉強会ではISUCON3の過去問を解くという形。
開始直前
始まる前にチャットサーバとか、ディスプレイの設定(デュアルディスプレイにした)とかそういうことをしておきました。チャットサーバにはDevHubというものを使わせていただきました。これを採用した理由は普通にチャットできる部分とメモとしてマークダウンで書いておいておける場所がある点。チャットはURL貼るという用途に しか使いませんでしたが(普通の会話は口頭でしていた)、メモは色々わかったことなどを各々が書き込んで結構使いました。結構役に立ちました。↓予選後のスクリーンショット
予選開始
とりあえず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ぐらいになり
暫定一位!!!#isucon
— いかろちゃん™@秋の進捗強化月間 (@ikaro1192) September 27, 2014
となりました。やったね!暫定一位!このままいければよかったんだけど...
dstat,topの結果をみているとロードが最大3〜4前後であること、IOというよりCPUがネックになっていそうなこと、時々ポンとIOが来ること、やっぱりMySQLのCPU使用率が高いことが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でチェックしてくれればいいと思うんだけど、難しいのかなぁ...
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のフォーマットがディストリビューションによって異なっていたり、そもそもなかったりするのでgrepやawkの部分注意。
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で並べ替える。
Kuinでlistに対するremoveとremove_ifを実装してみた
こんな感じ。
list操作と関数オブジェクトを受け取る関数の良いサンプルになってるんじゃないかと。
func remove(List : &list<int>, val : int) do List.Head() while ( List .ChkEnd() <> true) if (List.Get() = val) do List.Del() else do List.Next() end if end while end func func remove_if(List : &list<int>, pred : func<(int):bool>) do List.Head() while ( List .ChkEnd() <> true) if (pred(List.Get())) do List.Del() else do List.Next() end if end while end func func Main() var Data : list<int> :: #list<int> do Data.Add(2) do Data.Add(3) do Data.Add(5) do Data.Add(3) do Data.Add(4) do Data.Add(6) do Data.Add(7) do Data.Add(6) foreach datum(Data) do Dbg@Log(datum.ToStr()) end foreach do Dbg@Log("==") do @remove(&Data, 3) foreach datum(Data) do Dbg@Log(datum.ToStr()) end foreach do Dbg@Log("==") func lambda(x : int) : bool return x%2 = 0 end func do @remove_if(&Data,lambda) foreach datum(Data) do Dbg@Log(datum.ToStr()) end foreach do Kuin@Stop() end func
Kuinにはテンプレートとかないのでとりあえずint型のみ。
アルゴリズム自体はC++のとたいして変わらないのでそんなに説明はいらないと思う。
おもしろいのはイテレータ(とはKuinではいわないけど、慣れていて使いやすい言葉なので便宜的に)を内部に持っているっぽいということ。
なのでHeadで先頭にイテレータをもってきてそれでNextとかでガリガリ。
Delで要素を削除してイテレータを前にすすめる。
remove_ifのほうはそれにプレディケータをうけとるようにしたような。こちらはたいして説明することないと思う。ただ、つかうときにラムダ式がないので名前付き関数作ってそれを渡すしかない...まあ、適当にスコープをきってあげれば普通につかえるんじゃないかと。