golangで書いたアプリケーションのstatic link化

「goで書いたアプリケーションは実行ファイルひとつコピーするだけでいいのでインスコ超ラクチン」なんて思ってたんですが、 go1.4からnetパッケージを使っているアプリケーションは、フツーにビルドするとdynamic linkになるようになってました。

$ cd /path/to/your_app
$ go build
$ file your_app
your_app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

そんなわけで別環境にバイナリコピーしても動かないケースが発生して超絶アタマを悩ませることになるのですが、 そんなときは以下のようにbuildすればstatic linkになってくれるようです。

$ go build -a -tags netgo -installsuffix netgo
$ file your_app
your_app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

参考 https://github.com/golang/go/issues/9369

cgoを完全に使ってない場合は以下のようにcgoを無効化してビルドしてもstatic linkになります。

$ CGO_ENABLED=0 go build
$ file your_app
your_app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

なお、アプリケーションやライブラリ側でcgo使ってる場合は、glibcの関係でそれでもdynamic linkになります。 でも以下のようにstatic linkするようにオプションを追加してあげればOKです。

$ go build -a -tags netgo -installsuffix netgo --ldflags '-extldflags "-static"'

# CentOSの場合、glibc-staticがインスコされてないと、ldでコケます。

build後のバイナリを配布したいとか、dockerでalpineにバイナリコピーしてイメージ作ったんだけど動かねぇ……とかっていう場合にどーぞー。

Goでスケールする実装を書く、を読んで

コチラの記事の感想というかなんというか、です。

Goでスケールする実装を書く - GolangRdyJp

なんつーか、概ね伝えたいコトは同意できるんですけど、用語の誤用っぽいモノがあってスッとアタマに入ってこないのです。
# ワタシの解説がアヤシイ場合は是非コメント等でツッコんでいただけるとウレシイです!

"冪等性"について

ある実装が他のコアやプロセス、他のホストで稼働中の場合、 どこにある実装で処理を実行したとしても 引き渡す情報が同じなら結果も同じであることを「べき等」であるという。

このような実装の場合、いくつもその実装が動く環境を用意しておいて、 順番に使われていない実装に丸投げするように分散させることで スケールして大きく性能を確保できる。

冪等性って「繰り返し同じ処理を行っても、結果が一緒」ってことで、数学的にいうと {f(x) = f(f(x))}だし、 chefとかでいうと「とある状態のサーバ」に何度「cookbookを適用」しても「最終的な結果は一緒」ということです。

冪等性があるからといって「何度繰り返しても」ってトコが並列化できるか、というとそんなことはないですよね?

じゃあ元記事でいいたいことはなにかっていうと、たぶん「参照透過性」のコトだと思います

元記事の全体通して s/べき等性/参照透過性/g として読んでみると、そこの分の違和感はなくなります。

"Lock-Free/Wait-Free"について

いや、もう、なんか、えーっと。。。

  • 排他制御を経由しない。(これはWait-Free扱い)
  • 排他制御はあるが待ちに入る状況にならない。

その説明だと、排他制御内の処理を(並列で)実行しないっていってるようなもんで、なんだかなぁと。


Lock-Freeは言葉通り「スレッドがロックしないこと」ということではあるんですけど、もうちょっと具体的にいうと 「mutexとかの排他処理機構をつかうと同時に実行されるとlockしちゃうから使わないようにしよう!」ということです。

Wait-Freeに関しては「他のスレッドの動作に関係なく、有限のステップで操作を完了させられること」なんですけど、具体的な説明は難しいので雑に説明すると「Lock-Freeかつ、"CAS操作を成功するまで繰り返す"みたいな処理がないこと」です。

そんなわけで、そもそも排他制御があったらLock-Freeって言わないと思いますし、Wait-Freeでもないと思います。

"リードオンリーなデータ参照のLock-Free化"について

いや、readonlyな(言い換えるとimmutableな)データの参照はそもそもスレッドセーフですよね?
Lock-Free云々とか文脈でアレコレ言及する必要は全くないかとー。

なお、まったく関係ないですけど、将来的に「NUMAアーキテクチャに対応するため、readonlyなデータは安全にコピーできるので、各goroutineにコピーを持つようにしよう」とか言われる時代がくるんですかね? どうですかね? 教えてエライヒト!

あと「変更するgoroutineが一つで他が参照だけ」というのは、とても有効なパターンだと思います。
他のgoroutineからの変更を考えなくてよいのでWait-Freeにできる(CAS操作の元の値のチェックでの失敗がなくなる)ので。

……という元記事とはまったく違う解説をしてみるテスト


なお「変更するgoroutineが一つ」という場合でも、単純に変数に値を代入するだけだと危険なケースがあります。他goroutineから参照されるケースでは黙ってatomicパッケージのStore系の操作を使いましょう。
# 詳細はgoのメモリモデルを見てください。

そんなこんなで、、、

ここまでツッコんでおいてアレなんですけど、個人的に並列化しやすいアプリを組む場合に気をつけてることは、というと……

  • mutableよりimmutable.
  • 参照透過性を意識して実装する
  • 排他制御をできるだけ排除する
    • できればLock-Freeに。
    • さらに可能ならWait-Freeに
  • 排他制御が必要なら、その範囲をできるだけ狭く本当に必要な分だけにする

という元記事の結論と大差ない内容なのですね。

ですので、元記事は説明がアレで本当にもったいないなぁと思う次第でありまして……

golangのチャンネルでセマフォ的なナニカ

mattnさんのエントリに触発されて、某所で使用したちょっと変わったgolangのチャンネルの使い方をご紹介します。

mattn.kaoriya.net


特定の処理の並列度をある程度までに抑えたい、みたいなコトありますよね?
例えば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メモ

len==capのところは、https://play.golang.org/p/CBvBeQ-jO8 のように select を使うとraceが発生しないんじゃないかな

2016/07/07 10:38
b.hatena.ne.jp

上記コメントのPlaygroundのリンクです。

( ゚д゚)ハッ!
channelのwrite時にもselect使えるって知りませんでした! ありがとうございます!!!


ちなみにさくっと諦めてエラーを返すヤツですけど、Web APIみたいなので503 Service Temporarily Unavailable 返すのに使ったことあります。

もっと作りこめばバックプレッシャー制御もできるかもしれませんね。夢ひろがりんぐ

某記事のフォロー

会社でかいた記事はてブのツッコミのフォローをゆるゆる書いてみるテスト

記事中取り消し線多くてゴメンナサイ。

理解が乏しく恐縮なのですがこれは、CSSスプライトみたいなことをアーカイブファイルとインデックスファイルで実現した、という理解でよろしいのでしょうか。 - ngsw のコメント / はてなブックマーク

CSSスプライトみたいに「ページ表示に必要な複数の画像を効率よく配信する方法」ではなく、「それほどアクセス多くない画像ファイルをストレージ上にどう保存するか」という観点で読んで頂けると幸いです。

Swiftはオブジェクトが増えると書き込みパフォーマンスが下がる話、GREEでも言ってたけどやっぱり厳しいんだな。/ Log structured files 自前実装は気合入ってる。 - ono_matope のコメント / はてなブックマーク

オンラインで更新があるわけではないので記事上では"log structured"という表現は避けたのですが、最終的にアーカイブファイルと同じフォーマットのファイルを作る"log structuredなHotな分散ストレージ"の設計も実は進めてました。

異動で完全になかったコトになりましたけど。

退職エントリかとおもったら、異動エントリだった。 - coyu8 のコメント / はてなブックマーク

さすがに会社の公式エンジニアブログで退職エントリを書く勇気はないですw

lsが重たくなるのってさらにファイル名の末尾文字でサブディレクトリに分けて逃げたりするけど、そういうレベルではない?一括コピーはloopデバイス使うとか - nakag0711 のコメント / はてなブックマーク

「1ディレクトリに大量のファイルを作ってしまう」というレベルのハナシではなく、「ファイルシステム全体で数億ファイルとかになったら、IO負荷が上がりやすくなってしまう」という内容でした。

わかりにくくてすみません。

学びあるエントリだった。ただところどころよくわからん単語が出てくることがあったんだけどもあれは社内用語的なものなのか、それとも単純にぼくの無知なのか。 - luccafort のコメント / はてなブックマーク

いろんな単語を解説なしに散りばめてしまったのはゴメンナサイ。
記事内では社内限りの用語は排除したつもりです。

大体はググッていただければ何かしら引っかかる用語だと思いますが、 "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

  1. あるslaveでMySQLを停止してコールドバックアップをとって起動
  2. バックアップをコピって別サーバでslaveを構築
  3. MySQL起動したら、 元サーバ のio_runningが停止。

原因は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ファイルは残ります。
たいしたサイズじゃないので、構わず放置してますけど(ぉ