golangのチャンネルでセマフォ的なナニカ
mattnさんのエントリに触発されて、某所で使用したちょっと変わったgolangのチャンネルの使い方をご紹介します。
特定の処理の並列度をある程度までに抑えたい、みたいなコトありますよね?
例えばCPUヘビーな処理の並列数をたかだかコア数くらいまでに抑えたい、とか。
そんなときはバッファ付きチャンネルを用意しておいて、当該処理の前後でそのチャンネルにwrite/readをすることで、セマフォ的な制御ができます。
以下のようなカンジです。
package main import ( "fmt" "sync" "time" ) var ch chan int = make(chan int, 4) // 並列度を4に制限 func heavyFunc(i int) { ch <- 1 // チャンネルのバッファがイッパイになっていたら、ブロックする defer func() { <-ch }() fmt.Println("start:", i) time.Sleep(time.Second) fmt.Println("end:", i) } func main() { wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go func(i int) { heavyFunc(i) wg.Done() }(i) } wg.Wait() }
並列数が上限に達している際にブロックさせたくない場合は、ちょっと変えて以下のようなカンジにすると良いです。
func heavyFunc(i int) error { if len(ch) == cap(ch) { // バッファがイッパイだったらさっさとerrorを返す return errors.New("too busy!") } ch <- 1 defer func() { <-ch }() fmt.Println("start:", i) time.Sleep(time.Second) fmt.Println("end:", i) return nil }
(追記もみてください)
厳密にはlen/capのチェックを同時にくぐり抜けたgoroutineがあるとブロックしてしまう可能性があります。
回避するにはさらなる同期処理が必要になります……けど、そこまでする必要があるケースは少ないんじゃないかなぁ……と。
余談ですけど、これ使うときに並列数をコア数より少なめに設定しちゃうと、結果的にCPUを使い切れなくなるので
- 並列数はコア数
- GOMAXPROCSはコア数より少し多め
としておくと重い処理でCPUをフルで使いつつ、平行してそれ以外の処理も細々と処理できるという状況を作れます。
2016-07-07 追記
golangのチャンネルでセマフォ的なナニカ - okzkメモb.hatena.ne.jplen==capのところは、https://play.golang.org/p/CBvBeQ-jO8 のように select を使うとraceが発生しないんじゃないかな
2016/07/07 10:38
( ゚д゚)ハッ!
channelのwrite時にもselect使えるって知りませんでした! ありがとうございます!!!
ちなみにさくっと諦めてエラーを返すヤツですけど、Web APIみたいなので503 Service Temporarily Unavailable 返すのに使ったことあります。
もっと作りこめばバックプレッシャー制御もできるかもしれませんね。夢ひろがりんぐ
某記事のフォロー
会社でかいた記事のはてブのツッコミのフォローをゆるゆる書いてみるテスト
記事中取り消し線多くてゴメンナサイ。
理解が乏しく恐縮なのですがこれは、CSSスプライトみたいなことをアーカイブファイルとインデックスファイルで実現した、という理解でよろしいのでしょうか。 - ngsw のコメント / はてなブックマーク
CSSスプライトみたいに「ページ表示に必要な複数の画像を効率よく配信する方法」ではなく、「それほどアクセス多くない画像ファイルをストレージ上にどう保存するか」という観点で読んで頂けると幸いです。
オンラインで更新があるわけではないので記事上では"log structured"という表現は避けたのですが、最終的にアーカイブファイルと同じフォーマットのファイルを作る"log structuredなHotな分散ストレージ"の設計も実は進めてました。
異動で完全になかったコトになりましたけど。
さすがに会社の公式エンジニアブログで退職エントリを書く勇気はないですw
「1ディレクトリに大量のファイルを作ってしまう」というレベルのハナシではなく、「ファイルシステム全体で数億ファイルとかになったら、IO負荷が上がりやすくなってしまう」という内容でした。
わかりにくくてすみません。
いろんな単語を解説なしに散りばめてしまったのはゴメンナサイ。
記事内では社内限りの用語は排除したつもりです。
大体はググッていただければ何かしら引っかかる用語だと思いますが、 "small_light系"だけそのままだと検索できないかもしれません。"mod_small_light"や"ngx_small_light"で調べて頂けるとよいかと。
それでも出てこない用語があった場合はコメント等でツッコんでください。
golangのよくあるtickerのサンプルがイケてない件
golangでのtickerのサンプルで以下のようにgoroutine内でrangeを使ってforループを回すのをよく見かけます。
package main import "time" import "fmt" func main() { ticker := time.NewTicker(10 * time.Millisecond) go func() { for t := range ticker.C { fmt.Println("Tick at", t) } }() time.Sleep(35 * time.Millisecond) ticker.Stop() }
コレ、以下のようにちょっと変更して実行してみましょう。
package main import "time" import "fmt" func main() { ticker := time.NewTicker(10 * time.Millisecond) go func() { for t := range ticker.C { fmt.Println("Tick at", t) } fmt.Println("Reachable?") }() time.Sleep(35 * time.Millisecond) ticker.Stop() time.Sleep(100 * time.Millisecond) }
"Reachable?"とは表示してくれませんね?
tickerのStop()してもチャンネルをcloseしてはくれないので、残念ながらこのforループは抜けることはないのです。 その結果、goroutineがリークしてしまいます。
んじゃあどうすればいいかっつーと、以下のようにする必要があります。
package main import "time" import "fmt" func main() { ticker := time.NewTicker(10 * time.Millisecond) stop := make(chan bool) go func() { loop: for { select { case t := <-ticker.C: fmt.Println("Tick at", t) case <-stop: break loop } } fmt.Println("Reachable!") }() time.Sleep(35 * time.Millisecond) ticker.Stop() close(stop) time.Sleep(100 * time.Millisecond) }
……別でチャンネル用意したりとか正直クソメンドクサイですね。
ただ単にバックグラウンドでの繰り返し処理をするためだけに毎回こんなコード書くとか耐えられません。
ということで、ラッパーをつくりました。
https://github.com/okzk/ticker
このラッパーを使えば以下のようになります。
package main import ( "fmt" "github/okzk/ticker" "time" ) func main() { ticker := ticker.New(10*time.Millisecond, func(t time.Time) { fmt.Println("Tick at", t) }) time.Sleep(35 * time.Millisecond) ticker.Stop() }
ラクチン!
MySQLのコールドバックアップから別slave作ってハマった件
以下の手順でハマった。対象のバージョンは5.6.27
原因はauto.cnfを消してなかったのでserver_uuidが衝突してしまったから。orz...
# server_uuidは5.6からなので、完全にやらかしてもうた。
ちなみにshow slave statusで出てたエラーメッセージは以下の様なカンジ
A slave with the same server_uuid as this slave has connected to the master
教訓
- コールドバックアップからslaveを作る際はauto.cnfは消そう。
- もしくはバックアップからそもそもauto.cnfを除外しよう。
愚痴
server_uuidが衝突したらレプリ止まるのは仕方ないとしても、後勝ち(先に接続したレプリが止まる)ではなく、先勝ち(後から接続に来たレプリがコケる)にしなかったのはなんでなんだろう?
「start slaveしたら別サーバのレプリが止まる」とか、どう考えても罠やん。。。
MySQLのTransportable Tablespacesのおはなし
今更感あるけど、以下の補足。 kakerukaeru.hatenablog.com
なお、対象のMySQLのバージョンは5.6.26と5.6.27です。
といってもオペレーション自体は特記することもなくて、以下のページをよく読んでそのままやればダイジョウブです。カンタンカンタン。 http://dev.mysql.com/doc/refman/5.6/en/tablespace-copying.html
以下、注意点、というかハマったところ。
複数のテーブルをまとめてエキスポートする場合
FLUSH TABLESは以下のように複数のテーブルをまとめることが可能。
FLUSH TABLES t1, t2, t3, t4 FOR EXPORT;
# (ファイルコピーしてから)
UNLOCK TABLES;
同一セッション内で複数に分けてテーブルをLOCKすることはできないので、以下はダメ。
FLUSH TABLES t1 FOR EXPORT; FLUSH TABLES t2 FOR EXPORT; FLUSH TABLES t3 FOR EXPORT; FLUSH TABLES t4 FOR EXPORT; # (ファイルコピーしてから) UNLOCK TABLES;
移行先がレプリを組んでいる場合
マスタ/スレーブ全台にエキスポートしたファイル(.ibd/.cfg)を配置してから、マスタで以下のSQLを投げればいいだけ...
ALTER TABLE テーブル名 IMPORT TABLESPACE;
なんですけど、このALTER TABLE文、結構IO使うし、テーブルサイズによってはそこそこ時間がかかるので、コレ起因でレプリ遅延が発生orz...
以下のようにレプリさせずに個別に全サーバでIMPORTすれば回避可能なはず。次回があれば試してみます。
SET sql_log_bin = 0; ALTER TABLE テーブル名 IMPORT TABLESPACE; SET sql_log_bin = 1;
インポート用にDB分離しておいて、slave_parallel_workersで複数sql_running動くようにしてもいい気もする。が、そのためだけにslave_parallel_workers設定するのは...
その他
IMPORT後も移行先で.cfgファイルは残ります。
たいしたサイズじゃないので、構わず放置してますけど(ぉ
Rubyから(ry → はやい
2年以上放置していたけど、なんかフィボナッチ数の計算を高速化するのが流行ってると聞いて(違
フィボナッチ数が64bitで表現できる場合に限ると、以下がO(1)でほぼ最速だと思う。
FIB = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049, 12586269025, 20365011074, 32951280099, 53316291173, 86267571272, 139583862445, 225851433717, 365435296162, 591286729879, 956722026041, 1548008755920, 2504730781961, 4052739537881, 6557470319842, 10610209857723, 17167680177565, 27777890035288, 44945570212853, 72723460248141, 117669030460994, 190392490709135, 308061521170129, 498454011879264, 806515533049393, 1304969544928657, 2111485077978050, 3416454622906707, 5527939700884757, 8944394323791464, 14472334024676221, 23416728348467685, 37889062373143906, 61305790721611591, 99194853094755497, 160500643816367088, 259695496911122585, 420196140727489673, 679891637638612258, 1100087778366101931, 1779979416004714189, 2880067194370816120, 4660046610375530309, 7540113804746346429, 12200160415121876738].freeze def fib(n) FIB[n] end puts fib(40)
解説もベンチマークも要らない...よね?
YARN-467/YARN-527でハマッタ件
CDH4.1.2のYarnクラスタで、${yarn.nodemanager.local-dirs}/filecache/以下にディレクトリがいっぱい(3万以上)できてしまってタスクが起動しなくなるという現象に見舞われました。
# 他のnodeでリカバリしてjob自体は成功していましたが。。。
どうやらDistoributed Cacheを利用していると、どんどんディレクトリが作成されていく一方で、自動でcreanupはしてくれない模様。
jiraにも上がっていますが、取りあえず古いディレクトリを削除するようcronを仕込んで対応完了としました。
(参考)
https://issues.apache.org/jira/browse/YARN-467
https://issues.apache.org/jira/browse/YARN-527