Re: golang の channel を使って Dispatcher-Worker を作り goroutine 爆発させないようにする
こちらを読みました。
channel自体にdispatch機構があるからもっとシンプルに書けるのでは?と思って書き直したのがこちら。
コードだけぶん投げてもアレなので、あとで解説書きます。
ついでに「go1.7で標準化されたcontext使ったらどうなるか」も気力次第で書くかもしれません。
というわけで追記というか、本編というか、解説です。
channel整理
元のコードを読んでいくと、
- dispatcherのqueueにjobを突っ込む
- dispatcherがidle状態のworkerをpoolから取り出す
- 同じ行で取り出したworkerのqueueにjobを渡す
- workerがjobを受け取って処理する。
というカンジの処理の流れになってるんですが、dispatch機構自体がchannelにはあるので、
- dispatcherのqueueにjobを突っ込む
- workerがjobを受け取って処理する。
とするだけでOKです。
ここまでで、idle状態のworkerを保存するpoolとworker毎のqueueのchannelが不要になりました。
終了処理
終了処理用にわざわざ別でquiteというchannelを使っていますが、このくらいシンプルなケースではchannelをcloseするだけでOKです。
worker側も単純にforループ回すだけになります。 ループはchannelがcloseされて空になった時点で抜けてくれます。
ここまででworkerが単なるgoroutineになりました。
また、queueをcloseすれば最終的に全workerが終了するので、dispatcherがworkerのリストを保持する必要もなくなりました。
Wait??
元コードのWaitは「queueにつっこんだjobの処理が全部終わって完全にidleになるまでブロックする」というコードになってます。
main()で一回しか呼ばれていないのでなんともいえないのですが、このWait()の実装意図が以下のように「ループで登録されたモノが全部処理されるまで待ち合わせたい」ものだとこの実装ではマズイです。
func run(d *Dispatcher) { for i := 0; i < 100; i++ { url := fmt.Sprintf("http://placehold.it/%dx%d", i, i) d.Add(url) } d.Wait() // 上でAddした処理が全部終わったらナニカしたい!!! }
このrun()が並列でよばれると、自分で登録したモノだけじゃなく、他ので登録されたモノ全部が終わるまで待ってしまうからです。 この場合はrun()内で、個別にWaitGroupを作ってあげて、ゴニョゴニョしてあげる必要があります。
また、もしかするとWait()の実装意図は単に終了処理用で「全部のjobの処理が終わるまで待ちたい!」というモノかもしれません。 その場合はStop()に統合しちゃえばいいわけですし、書き直した方ではqueueをcloseした後、全部のgoroutineが処理を終えるまで待つようにしています。
Stop??
元コードは引数でimmediatelyかどうかを区別していますが「Stop()にboolな引数がある」というのが個人的には驚き最小ではないのでStop()とStopImmediately()でメソッド自体分けてしまってます。
こうしておけば「Stop()があってStopImmediately()があるということは、StopImmediately()はきっとなにか通常ではやらないことをするんだろうな」と身構えることができるので。
んでImmediatelyな実装の方は「queueにつっこまれたけど処理しないjob」が発生するというモノなんですけど、それを単にqueueから読み捨てるというカタチで書きなおしています。
ただし、workerで処理中のモノがあればその分だけは処理完了するまで待ち合わせます。
元コードが処理中のモノは終了処理用のchannelに書き込むだけで、処理中のモノの完了を待ち合わせていない(!?)ので、その点は挙動が異なっています。
余談ですけど、値に意味の無い終了処理用のchannelは、ダミー値を書き込むよりもcloseした方がいいです。
readしようとしているgoroutineが一つだけだったらいいんですけど、複数あったらその分書き込まなきゃいけませんから。
// コレよりも w.quit <- struct{}{} // こっちがベター close(w.quit)
感想
書きなおしてみると、ただのworkerパターンになっちゃいました(;・∀・)
dispatcher周りのサンプルとしてなら、pub/subパターンの方を実装した方が良かったんじゃないかなぁ……
あと、これにcontextを使うと、標準的な方法でworker側で処理の中断制御ができるようになる……というハナシはまた後日で。
Re: Dockerに載せたサービスをホットデプロイする
こちらを拝見したところ、やりたいコトはdocker1.12のswarmモードで解決するんじゃないかなー、と思ってみたので試してみたテスト。
とりあえず、最新版のdocker(1.12)をインストールです。
手元の環境はCentOS7なので、インストールガイドに従ってゴニョゴニョと。
んでもって、swarm初期化。
# docker swarm init
今回1台だけなので、ノード追加は行いません。
次に実験用にnginxのサービスを立ち上げます。
ポイントはimage差し替え時に「停止→起動」の順番で動くので、あらかじめレプリカ数を2にしておいて、ちゃんとローリングで切り替わるようdelayを設定することです。
# docker service create --update-delay 20s -p 80:80 --name test --replicas 2 nginx
この状態でアクセスすると、オーバーレイネットワークでラウンドロビンしながら各コンテナが応答してくれます。
imageの更新は以下の様なカンジです。
# docker service update --image nginx:stable test
swarmがローリングでアップデートしてくれます。
応答も途切れません。インストールもdocker最新版だけでいいので最小限だし、オペレーションも簡単!やったね!
……のつもりだったんですけど、何度もservice updateを繰り返しているウチにレプリカ2コのウチ片方にアクセスが寄ってしまうという現象が発生してしまいました。
当然その状態で寄ってる方から停止すると、無応答時間が発生するわけで……(´;ω;`)ブワッ
再現したりしなかったりで、今のとこ原因特定には至ってません(;・∀・)
なにか、私の設定がマズイのかなぁ……
golangのGCとかgoroutineの状況を確認するライブラリ
golangで作った長時間動かすアプリで「goroutineリークやメモリリークがないか知りたい」とか「GCの影響がどの程度か知りたい」とかないですか?ありますよね?
そのためのログをダラダラ出力するためのライブラリを公開しました。
# 元々はクローズドなトコで作ったモノを、公開のため完全フルスクラッチで書きなおしてます。
使い方はmainのアタマとかに適当に組み込むだけです。
func main() { // 1分ごとにログにjson出力 t := stats.SchedulePeriodically(time.Minute, func(s *stats.Stats) { log.Println(s) }) defer t.Stop() // あと本来の処理を…… }
# 標準ロガーがアレなら、お好みのロガー使ってください。
String()で生成されるjsonはその時点のgoroutineの数とruntime.MemStatsをそのままMarshalしただけのやる気ないモノです。
……はい、なんというか、完全に手抜きですね(;・∀・)
とはいえ、まったく情報がないのと比較すると、トラブったときの調査の捗り方が違います( ー`дー´)キリッ
なお、個人的には適当にローカルに書き出しておいて後からjqで眺めてみたりぐらいしかやってないんですけど、ElasticsearchなりNorikraなりでアレコレするのも面白いかと思います。
# その場合はfluent/fluent-logger-golangを使うと捗る……のかな???
そんなこんなで、もうすぐリリースされるハズのgo1.7でGCがどれくらい改善されたか、とか確認できるといいですねー
marisa-trieのgo bindingを書いた
id:s-yataさんが公開してくださっているmarisa-trieをgolangで使えるようなバインディングを公開しました。
marisa-trieは非常に省メモリなtrie実装です。
特徴等は公式ドキュメントを参照してください。
インストールも普通にgo getするだけでOKです。よければどーぞ。
# g++とかstdlib++とかは必要になりますけど。
使い方等のドキュメントは……今後の課題ということで(;・∀・)
go getでインスコできるc/c++なライブラリのラッパーの作り方?
go buildはパッケージディレクトリにおいている.cや.ccファイルは一緒にコンパイルしてくれるのですが、サブディレクトリまでは面倒見てくれません。
かといってライブラリのソースをパッケージディレクトリにコピーしたりするとライブラリの更新の追従とかがしんどくなってきます。
そこで今回は、元ライブラリをgitのsubmoduleとした上で、コンパイル対象のファイルをincludeしただけのやる気ない.ccファイルを用意してみました。
このやり方がいいのかわかりませんが、とりあえずはgo getでまとめて全部コンパイルしてくれるし、ライブラリ更新の追従もgit submoduleでゴニョるだけでよいので、結果的にシンプルに出来たような気がします。
他にこういう時のうまいやり方があるのなら、教えて頂けるとウレシイなぁ。。。
golangで書いたアプリケーションをどう動かすか?
まとまりなく、何パターンか列挙します。
アプリケーションコンテナで動かす
通常ステートレスなアプリに限られると思いますけど、dockerで動かすというやり方です。
# 個人的にはdocker 1.12で組み込まれたswarmモードがすごくお手軽でよいと最近思ってます。
バイナリはstatic linkでビルドして、alpineで動かすと軽量でイイカンジです。
Dockerfileは以下みたいなカンジ
FROM alpine RUN apk add --no-cache ca-certificates COPY your_app /usr/local/bin/ CMD ["your_app"]
外部サービスにssl/tls接続するのに必要なのでca-certificatesを突っ込んでます。
証明書周りを自分でなんとかするんなら、busyboxにするのもアリかと。
supervisordで動かす
定番ですね。
個人的にはOSのお決まりのサービス管理方法(init.dやsystemd)と異なるレイヤが増えるので好きではないです。
initスクリプトを頑張って書く
/etc/init.d/hogehoge みたいなスクリプトを用意しておいて、以下のように使うイメージです。
# service hogehoge start
中身はnohupを使ってなんちゃってdaemonize、とかですね。
スクリプト書くのは正直ダルいですけど、前処理/後処理を柔軟に書けるので、systemd以前はコレを好んでやってました。
systemdのサービスで動かす
/etc/systemd/system/ 以下にserviceファイルを書いて、systemctlで頑張ります。
serviceファイルはミニマムだと以下のようなカンジです。
[Unit] Description=hogehoge [Service] Type=simple ExecStart=/path/to/hogehoge ExecStop=/bin/kill -SIGTERM $MAINPID [Install] WantedBy = multi-user.target
ExecStartPre/ExecStartPost/ExecStopPostで前処理、後処理も書けます。
ExecStopPreは存在しませんが、停止時の前処理はExecStopが複数記述できるので、それで対応しましょう
なお、limitsの設定とか実行ユーザの指定も簡単です。
[Service] LimitNOFILE=65536 User=hogehoge
また、Type=simpleの場合は、起動したタイミングでsystemdは起動完了とみなしてくれるんですが、 アプリケーション的に初期化に時間が掛かるケースもあると思います。
そんな時はアプリをsd_notifyに対応させた上で、Type=notifyにしてあげればイイカンジになります。
というわけでsd_notifyに対応するためのライブラリを書いてみました。
使い方はsampleを見ればすぐわかると思います。
Google App Engineで動かす
書いといてアレですけど、すみません、やったコトないです(;・∀・)
docker以上に軽量でいいんじゃないですかね? 知らんけど。
Herokuで動かす
やったコトないですけど、選択肢の一つとして。
AWS lamda?
公式対応があるといいなぁ。。。
# node経由やり方もある、らしいけど。。。
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でスケールする実装を書く、を読んで
コチラの記事の感想というかなんというか、です。
なんつーか、概ね伝えたいコトは同意できるんですけど、用語の誤用っぽいモノがあってスッとアタマに入ってこないのです。
# ワタシの解説がアヤシイ場合は是非コメント等でツッコんでいただけるとウレシイです!
"冪等性"について
ある実装が他のコアやプロセス、他のホストで稼働中の場合、 どこにある実装で処理を実行したとしても 引き渡す情報が同じなら結果も同じであることを「べき等」であるという。
このような実装の場合、いくつもその実装が動く環境を用意しておいて、 順番に使われていない実装に丸投げするように分散させることで スケールして大きく性能を確保できる。
冪等性って「繰り返し同じ処理を行っても、結果が一緒」ってことで、数学的にいうとだし、 chefとかでいうと「とある状態のサーバ」に何度「cookbookを適用」しても「最終的な結果は一緒」ということです。
冪等性があるからといって「何度繰り返しても」ってトコが並列化できるか、というとそんなことはないですよね?
じゃあ元記事でいいたいことはなにかっていうと、たぶん「参照透過性」のコトだと思います
元記事の全体通して s/べき等性/参照透過性/g
として読んでみると、そこの分の違和感はなくなります。
"Lock-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に
- 排他制御が必要なら、その範囲をできるだけ狭く本当に必要な分だけにする
という元記事の結論と大差ない内容なのですね。
ですので、元記事は説明がアレで本当にもったいないなぁと思う次第でありまして……