D-7 <altijd in beweging>

Day to day life of a Perl/Go/C/C++/whatever hacker. May include anything from tech, food, and family.

2013年12月

この年の瀬に二人目が産まれました。お恥ずかしながら年子です。二人目もちんちんついてました。



今回も嫁はスパルタで有名な某病院で産んだのですが、体重管理とかも割と目標通りにしたのに産まれてきた子供がなぜか3858gと超巨大でした。その割には完全なる安産で、たったの6時間ほどの分娩時間で産まれてきました。でかすぎる事以外は特に生まれついての疾患等もなく健康に産まれてきてくれました。

幸い長男のほうも妻が入院していても特に情緒不安定になるようなこともなくのほほんとしていてくれてます。ありがたい限り(そして、やっぱり保育園やらベビーシッターやらに預けたりしたのと、風呂、メシ、寝かしつけなどを俺もずっとやっててよかったと思った)。

まぁ本当の地獄?の本番は明日妻と次男が退院してきてからなんだけど、何はともあれ今年中に全てが片付きそうなのでよかったよかった。

ちなみに次男はでかいし貫禄もあるので、親二人の間では「親方」と呼んでおります。
    このエントリーをはてなブックマークに追加 mixiチェック

本日のYak Shavinggo-test-mysqldを書いた。

import (
    "database/sql"
    "log"
    "mysqltest"
)

mysqld, err := mysqltest.NewMysqld(nil)
if err != nil {
   log.Fatalf("Failed to start mysqld: %s", err)
}
defer mysqld.Stop()

db, err := sql.Open("mysql", mysqld.Datasource("test", "", "", 0))
// Now use db, which is connected to a mysql db


go-stf-serverのテストを書き始めるにあたってmysqldをどうしても立ち上げないといけないんだけど、これがまた自動的にやるのは面倒くさくて面倒くさくて・・・

というわけでTest::mysqldからまるまるまるっとぱくってgolangにポートしました。これでmysqldを立ち上げるのも簡単だね!

それほど障壁は高くないと思いますので、是非気軽にお使いください。問題があった場合はissueをお気軽にどうぞ! 
    このエントリーをはてなブックマークに追加 mixiチェック

Perl版のSTFは自分が参加し始める前から、ひとつのスクリプトから複数の種類のワーカープロセスを立ち上げるように作られていたのでそれを踏襲し続けている。ひとつにはワーカーの種類が複数あるのでそれらをひとつひとつ立ち上げるのは結構面倒くさいのと、Perl版のバージョン2.00からはメインプロセスがシステム全体のワーカー数を関しつつ自分がどのワーカーを何個ずつ立ち上げるべきなのかを自律的に計算するようにしたのでこれはこれでよい。

さて、それでgo版だが、goでは言語設計的にfork()は使えないようになっている。なので通常はgoroutineをガツガツ立ち上げる事になるわけだが、この間やったgo版dispatcherのベンチマークなどから見ても、goroutineベースだとシステムリソースをよりガツガツ最後の1%まで使ってやろう感が見えるので、本能的に「これをSTFワーカーでやると問題が起こった時にやばい」と感じた。

正直ここは勘なので これが正解かどうかわかってない。間違ってるかもしれないけど全部goroutineでやるより、fork + exec とgoroutineの組み合わせで実装するほうがよいような気がしたので、それでやってみた。

スーパーデーモンにあたるものが"drone"と呼ばれる。こいつは必要なワーカー種類の分のコマンドをexec.Command経由で立ち上げる。しかしこの子プロセスがなんらかの理由で停止したら、即座に新しいプロセスを立ち上げ直す必要がある。ところがgolangにはノンブロッキングwaitpid()に相当するようなものはない。いや、厳密にはあるんだけど、syscall.Wait4()というポータブルではないところにあるのであった。

まぁそもそもfork+waitというのがUNIXなイディオムだからそりゃそうだ。ということでポータブルにするにはどうするか。ここはなんとなく無駄にも思えるのだけど、goroutineで豪勢にブロックするとよい:

func SpawnWorker(exitChan chan string, executable string) {
fullpath, err := exec.LookPath(executable)
...
cmd := exec.Command(fullpath, ...)
err := cmd.Start()
...
go func() {
cmd.Wait()
exitChan <- executable
}()
}
exitChanは別のところでselect{}経由でノンブロッキングで監視しておいて、もらった実行ファイル名で再度子プロセスをスタートさせる関数にフックしておく

func MainLoop() {
for {
select {
case executable := <-exitChan:
SpawnWorker(exitChan, executable)
...
}
}
}

これで永久機関のできあがりだ!あとはSTDOUT/STDERRをコピーしつつシグナルを受け取ったら良い感じにループを抜けたり、後にゾンビを残さないためにプロセスを止めたりすれば停止もキレイになる。

で、この子プロセスの中では特定のワーカー種別について必要な分だけのgoroutineをばかばか作って、必要な処理をする。方式はだいたいdroneと一緒でこちらではQ4Mにアタッチするgoroutineと、さらにそこからchannel経由でジョブをもらって実際の処理を行う複数のgoroutineを作成する。これがようやくいわゆるワーカーの部分。ここもそうする意味があるのかちゃんとわかってないけど、勘でそうするべきだと思ってmax-reqs-per-workerを設定して、定期的にgoroutineが終了するようにしてる


このマルチプロセス+マルチスレッド方式のほうがいいな、と思った理由はこれだと例えば特定のワーカーのgoroutineが詰まったりした時にそのプロセスだけ殺して再起動できるから。goroutineってthread idに対応するものが外部に出てないからkillしたくても簡単にできねぇんだよね!

で、ここまでできたので、次は残りのワーカー機能の作成と、自律的にワーカー数の調整を行う部分。自律調整の部分は上記のdrone部分でtime.Tickを使って定期的に行う予定。
 

と、ここまで書いたけど、この実装って本当に正しいのかわからんので相変わらずツッコミをお待ちしております。
    このエントリーをはてなブックマークに追加 mixiチェック

色々調べてたらgo でHTTPサーバーをgraceful restartする方法の記事にあたって、そのなかでシグナルを受け取るとリクエストを受け取ったらそれまでに受け付けたリクエストを処理してから停止するmannersというツールを見つけた。さらにゼロダウンタイムでサーバーを再起動するgoagainというツールも見つけたんだけど、これはサーバー内のコードをそれなりに変更しないといけないっぽくてうんざりしてしまった。

「っていうか普通にServer::Starterをかませるようにできればよくね?」と思ったので、go-server-starter-listenerを書いてみた!使い方はREADMEにあるとおり。このようなコードを書いておいてからstart_server --port XXX -- ./your-go-program って感じで動作させればServer::Starterが保持してるポートにバインドする・・・はず!(まだあんまりテストしてない)

あとはmannersとあわせれば良い感じでgraceful, zero-downtime restartができるはずー

ツッコミ歓迎です。 
    このエントリーをはてなブックマークに追加 mixiチェック

色々Goで書いてみてやはりガードオブジェクトが欲しい、と思うことがよくある。一時的に何かの値を有効にしたり、かならず解放・リセット処理を行いたい場合のイディオムだ。これでいいのかわからないのだけど、とりあえず現時点で自分がやっていることをメモしておく。

Goにはdeferという仕組みがあって、関数の最後に必ずこれを実行してくれる。だがこれをオブジェクト構造体 につける方法はない。もちろん、構造体に対する関数呼び出しをdeferすることで同じ事ができるんだけど、そうするとどの関数を呼び出すのかをいちいち覚えていないといけない。一応runtime.SetFinalizer()というものはあるんだけど、これはいつ処理が走るのかわからないし、複数のオブジェクトやリソースが絡んでいる時にどの順番で処理が走るかもわからない。

ということはやはりdefer() を駆使するべきだろう。ということで自分は以下のような方法を使うことで今のところ落ち着いている。

func GuardLike () func() {
... 色々処理 ...
return func() {
... 解放処理 ...
}
}

func main() {
closer := GuardLike()
defer closer()
}  
これであればGuardLike()の中でどんな処理が行われていようと呼び出し側で気にすることなく関数をdeferから呼び出す、ということだけ覚えておけばよい。これでいいのかわからないので「こっちのほうがいいよ」という意見があれば教えて欲しい。

ちなみにPerl等のeval { ... }に相当するものを書きたい時も自分は関数でラップした上でdefer + recoverを使うなどしているので、goは結構このdeferの使い方をうまくすることで色々楽ができそうな気がする。

あとトランザクションのロールバックのような処理はなんか明示的に書いたほうがいいのかと思って今は明示的にTxnRollback()というラッパー関数を呼び出すようにしている。まだこのあたり流儀を統一できてない。
    このエントリーをはてなブックマークに追加 mixiチェック

まだ道半ばなんだけど、GroongaをGo+Perlなフロントエンドから使うシステムを動かしはじめている。

今回はとりあえず現時点での状態をざっくり書き出してみる。まだ本番化はしてないが、とりあえず本番環境からデータの挿入・削除ができるところまでつなぎ込みはした。最終的に全面的に本番化したらまたまとめ直します。

まずデータを突っ込む部分は慣れもあるのでより素早くデータの整形をしたりテーブルスキーマとかを変更したりするためにPerlでワーカーをさくさくっと書いた。ワーカーはQ4Mでデータを受け取り、データを整形してMroonga経由でデータを挿入。仕組みができたところでGroongaが我々が求めているデータ量をハンドリングできるかどうか検証するために全力で平均1行4KB~10KBくらいのデータをmroonga経由で○億件挿入してみた。この挿入処理中様々な地雷を踏んだので、kazeburoさんの助言などを元に挿入先のDBとテーブルをshardingしたりしてそれらを回避。

データは二つのよさげな検証用マシンにそれぞれ5000万件のところまではO(log(N))的な感じで挿入できていたが、それ以降は線形。これを書いている時点で1日約600万件の挿入ができている。5000万件の時点で関連grnファイルを全てあわせるとそれぞれのマシンに360GBずつそのあとはだいたい1日100GBずつくらい増えている。

sharding部分が落ち着いてきたら次は既存のシステムへのつなぎ込み用HTTP APIを作成。ここを今回はGoで作成。Goの部分は 練習もあったので簡単な参照部分をまずストレートなHTTP APIハンドラとして実装してみた。ステートレスなのでstructとかは基本的に作らず、golang.orgのExampleをほぼそのまま。

参照部分でどれだけ性能が出るのかどきどきだったけど、全く問題なし。Goちゃん速い。

次にデータ挿入部分はすでに生成してあるPerl製ワーカーとのつなぎこみだけなんだけど、queueへの書き込み部分はDBへの接続と使用テーブル両方ともある程度ランダムにばらしているのでその部分は全部Goで書き直し。この時点でサーバー側でステートを持ち始める必要ができてきたので、ApiServer型を作ってHTTPハンドラはクロージャで登録するような形に変更。コード的にはこれが大分おおきな変更だったけど、手を動かせばいいだけで、別にはまるところはなかった。定期的にランダムに挿入先をリフレッシュするように書いたんだけど、ここもgoroutineを使ってきれいにできた。time.Ticker便利!シグナル処理部分で一瞬迷ったけど、Perl5のSafe Signalsと同じ仕組みをchannelで実装しているだけだとわかってしまえばあとは簡単だったので、ごにょごにょしたり、一旦握りつぶしたシグナルをsyscall.Kill()でまた流したりとか色々した。

というわけで良い感じになってきた。そのうちこのシステムも皆様が触る部分にお目見えできると思っております。
    このエントリーをはてなブックマークに追加 mixiチェック

このページのトップヘ