Goでgraceful restartに対応したデーモンプロセスをつくる
Goで書かれたWebアプリのプロセスをデーモンにしたり、ダウンタイムなしでデプロイできるようにするための情報をいろいろ調べていたのですが、どうもSupervisorなど外部のツールを使ったりするのが定番か、herokuなどのPaaSにまかせてしまうという手法が多いようです。 でも個人的にGoの一番気に入っているところは依存のない独立した単一バイナリを生成できる点なので、Webアプリもなるべく外部に依存せずにどうにかできないものかと調査していました。
さてGoのデーモン化とgracefulのライブラリはそれぞれいくつかあるのですが、両方に対応したライブラリは見つからず、組み合わせたサンプルなども見当たらなかったので、各種ライブラリのソースを読んだりしながら、最終的に以下のものを取り上げてみました
- daemonize
- graceful restart
この2つを組み合わせてgraceful restartできるデーモンを作ろうとしたわけですが、上記のライブラリではdaemonziseもgracefulも内部的にforkの代わりにos.StartProcess
で外部コマンドとして自分自身を再起動し、環境変数を使って状態の制御をおこなうということをやっています。一緒に使うと、その辺が干渉してうまく動かない。。。
で、いろいろワークアラウンドをいれて、ひと通り動くようになったのが以下のコードです。やっていることはコード内のコメントを参照してください。
package main import ( "flag" "fmt" "io/ioutil" "log" "net/http" "os" "path/filepath" "strconv" "time" "github.com/VividCortex/godaemon" "github.com/facebookgo/grace/gracehttp" "github.com/kardianos/osext" ) var ( logfile = flag.String("l", "goserver.log", "log file") pidfile = flag.String("p", "goserver.pid", "pid file") ) func main() { flag.Parse() // os.Args[0]を絶対パスに書き換える。 // デーモナイズ後、カレントディレクトリが変わるので、 // 相対パスのままだとgracehttpがos.StartProcessするとき自分自身を指し示すファイルパスを解決できない。 bin, err := osext.Executable() if err != nil { log.Fatal(err) } os.Args[0] = bin // pidとlogのファイルのパスも絶対パスにする。 logfilepath, err := filepath.Abs(*logfile) if err != nil { log.Fatal(err) } pidfilepath, err := filepath.Abs(*pidfile) if err != nil { log.Fatal(err) } // デーモナイズする // "LISTEN_FDS"があるときはgraceful restart時なので、スキップさせる。 if os.Getenv("LISTEN_FDS") == "" { godaemon.MakeDaemon(&godaemon.DaemonAttr{}) } // 以下のロジックはデーモナイズ後の状態で実行される。 // ログの出力先をファイルに。 f, err := os.OpenFile(logfilepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal(err) } defer f.Close() log.SetOutput(f) // pidファイルのプロセスIDを書き込む pid := os.Getpid() if err = ioutil.WriteFile(pidfilepath, []byte(strconv.Itoa(pid)), 0666); err != nil { log.Fatal(err) } log.Printf("Generated pidfile %s (%d)\n", pidfilepath, pid) // WebAppサーバ mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { time.Sleep(5 * time.Second) // 動作検証用に5秒リクエストをつかみっぱなしにする(restartしても切れないことを見る) fmt.Fprintf(w, "hello world!") }) server := &http.Server{Addr: ":1234", Handler: mux} // graceful restartをサポートして起動 gracehttp.Serve(server) }
github.com/kardianos/osextは実行ファイルの絶対パスを取得するための便利ライブラリです。
これをビルドし起動した後、graceful restartするには-USR2
シグナルを送る。
kill -USR2 5678
シャットダウンもリクエストが途中で切れないgraceful shutdownになる。
kill 5678
動作させてみたところうまく動いているようではあるが、os.Args[0]
を書き換えたりしてキモいコードになっている。
daemonizeとgracefulをまとめて、ルーティングとmiddlewareまわりの構造を提供してくれる軽量フレームワークがあったらいいなあ、と妄想しています。
iOSでステータスバーの色が写真取得すると黒に戻る件についての対処
ステータスバーの文字の色を
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
で白に変えられる。ところがUIImagePickerControllerで写真アルバムから写真を取得すると、このステータスバーの文字色が黒に戻ってしまう。調べたらstackoverflowが引っかかり、以下のワークアラウンドで回避できることがわかったのでメモ。
まずUIImagePickerControllerのデリゲートを設定し
UIImagePickerController *picker = [[UIImagePickerController alloc] init]; picker.delegate = self;
そして以下のデリゲートメソッドを実装する。
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated { [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]; }
UIImagePickerControllerはUINavigationControllerの子クラスなので、UINavigationControllerのデリゲートメソッドであるwillShowViewController
を実装しておく。ここに色を白にするコードを入れておいてデリゲートにセットしておけばOKというわけです。
以下、stackoverflowの元記事です。
ios7 - UIImagePickerController breaks status bar appearance - Stack Overflow
Goについての雑感
最近Golangでコマンドラインツールを書いているので、ちょっと思うところを書いておく。
気に入っているところ
シングルバイナリをクロスプラットフォームで生成できる
とくにコマンドラインツールを作ってみると、この特性がすごく気持ちよい。シングルバイナリはインストールが楽という利点があり、特に今はgithubのリリースページにバイナリを置いたり、https://bintray.com/とかを使うことでファイルをHTTPでダウンロードすることができ、導入の敷居がすごく下がる。アンインストールもファイルを消せばいいだけなので気楽だ。
ところで自分の場合、RubyやPythonなど自分がメインで使っていない言語の処理系を必要とするツールだと、まずその処理系を「正しく」インストールする方法を調べるのに気を使ったりしてしまう。もちろん本当のところは目的のツールが単に動けばいいだけなので、Macにプリインストールされている処理系を使えばいいのだけど、どうせ使うんだったら、Linuxサーバ上のものと同じ環境にしたいとか、メジャーな方法を使いたいとかでrbenvを調べたりごにょごにょ環境周りをいじって時間を浪費することが多くて、しょんぼりする。
そのへんの事情から開放してくれて他に依存が無いのですぐに使えるし、すぐに捨てられるというシングルバイナリは思いの外、快適だったりする。
静的型付けのコンパイル言語
今はIntelliJでGoを書いているので静的型な言語だとIDEの補完や定義先へのジャンプがかなり強力に作用する。ちょっと作業環境が重いと感じることもあるが、許容できる。プライベートで書いているプログラムだと、ちょっと変数の名前の付け方やディレクトリ構成が気に食わないとか、気分でコード全体をもりっと変えることがよくあって、そういうときもコンパイラのチェックがあるので、ある程度のコードの正当性を担保してくれるのがよい。
また、型推論のためコードの見た目はLLっぽいのも気に入っている。
OSSですでに周辺ライブラリがいろいろある、導入も簡単
Githubで検索すれば必要なライブラリは大抵あるし、それを使ったサンプルコードも検索すれば大体出てくる。
困っているところ
GOPATHとimport
もういろんなところで言及されているので詳細は省くが、GoはGOPATHというパスを起点にプロジェクトのコードと依存する外部パッケージのコードを同列にフラットに管理するという独特の方式を採用している。これのせいで明確に困ることがあって、
GithubのGo言語プロジェクトにPull Requestを送るときのimport問題 | SOTA
のようにforkしたプロジェクトだと問題になる。また自分の作業環境だと、依存物をプロジェクトのディレクトリ配下に収められないのでctagsでタグ生成する範囲が広がりすぎる問題がある。で、この辺は以前書いたようにdirenv
とGom
を使ってGOPATHをプロジェクトごとに複数、切り替えて対応している。
幸いIntelliJもGOPATHを複数設定し、プロジェクトごとに切り替えることができるのでその機能を使っている。 とはいえちょっとややこしくて、非標準のツールに頼っているため悩ましいところではある。
プラグイン的なものが作れない
Goは動的に他のビルド済みのGoのコードをロードすることができないので、後からプロダクトに機能追加できるようにする、いわゆるプラグイン的なものはつくれない。
考えていること
普通のWebアプリだったら依然PHPを使うと思う。
Golang ビルド時にコミットハッシュをソースに埋め込む
メモ
// config.go var CommitHash string
以下のコマンドでビルド
go build -ldflags="-X github.com/kohkimakimoto/hoge/config.CommitHash `git log --pretty=format:%H -n 1`"
MacでSSHログインと同時にターミナルの色を変える
いっぱいターミナルを開いていると間違って本番サーバで開発サーバ用のコマンドを実行しそうになってヒヤリとすることがあります。 そんなわけでSSH接続時、接続先ごとに自動的にターミナルの背景を変えるようにしてみました。以下のスクショのようになります。
解説
背景色の選択はAppleScriptでやります。以下のような関数を~/.zshrc
に書いておく
# ~/.zshrc function terminal-color () { /usr/bin/osascript -e "tell application \"Terminal\" to set current settings of first window to settings set \"$1\"" }
これで、terminal-color 'Red Sands'
のようにターミナルのテーマを指定して実行すると、背景色が変わります。
ついでに、踏み台サーバを経由するような場合も自動でそこまでたどり着けるように、コマンドを自動で連続してターミナルに流し込めるようにするAppleScriptも書いた。
# ~/terminal-exec.scpt on run argv tell application "Terminal" to activate tell application "System Events" to tell process "Terminal" to keystroke "t" using command down tell application "Terminal" repeat with x in argv do script x in front window end repeat end tell end run
これをコマンドで呼び出せるようにシェル関数でラップする。
function terminal-exec() { /usr/bin/osascript ~/terminal-exec.scpt $@ }
ここまでで
terminal-exec "terminal-color 'Pro remote'" "ssh kohkimakimoto@192.168.56.1"
のようにコマンドを実行すれば、背景色を変えてSSH接続できるようになります。踏み台サーバがあるときはSSHコマンドを並べて書くだけでいいです。
terminal-exec "terminal-color 'Pro remote'" "ssh kohkimakimoto@192.168.56.1" "ssh kohkimakimoto@192.168.56.2"
さて、この長ったらしいコマンドを毎回手打ちするわけもなく、pecoを利用したコマンドランチャーを利用します。これは以前のエントリ pecoをランチャーのようにして使う - オープンソースこねこね に書いたが当時のものから少し変わっているので、改めて以下にzshの関数を以下に貼り付けておきます。
# ~/.zshrc function peco-snippets() { local line local snippet local cwd local snippet_file local local_snippet_file local direct_run # Get a snippets file in the current directory if it exists. cwd=`pwd` if [ -e "$cwd/.snippets" ]; then local_snippet_file="$cwd/.snippets" else local_snippet_file="" fi # Do not load home snippets as a local snippets if [ $HOME = $cwd ]; then local_snippet_file="" fi direct_run=0 if [ $# -ge 1 ]; then # Load sub snippets file and selected line will be run directly. direct_run=1 snippet_file=$1 local_snippet_file="" else snippet_file=$ZSH_EXT_ROOT/snippets fi; if [ ! -e "$snippet_file" ]; then echo "$snippet_file is not found." >&2 return 1 fi line=$(cat $local_snippet_file $snippet_file | grep -v "^\s*#" | grep -v '^\s*$' | peco --query "$LBUFFER") if [ -z "$line" ]; then return 1 fi snippet=$(echo "$line" | sed "s/^[ |\*]*\[[^]]*\] *//g") if [ -z "$snippet" ]; then return 1 fi if [ $direct_run -eq 1 ]; then eval $snippet else BUFFER="$snippet" zle clear-screen fi } zle -N peco-snippets bindkey '^x^x' peco-snippets
pecoがインストールされていない環境はインストールしておきます。
そして、以下のようなコマンドを列挙したスニペットファイルを用意します。
# ~/.snippets [ssh connect: hakoniwa-local1] terminal-exec "terminal-color 'Pro remote'" "ssh kohkimakimoto@192.168.56.20 -p 22" [ssh connect: hakoniwa-local2] terminal-exec "terminal-color 'Pro remote'" "ssh kohkimakimoto@192.168.56.21 -p 22" [ssh connect: hakoniwa-local3] terminal-exec "terminal-color 'Pro remote'" "ssh kohkimakimoto@192.168.56.22 -p 22" [ssh connect: hakoniwa-local4] terminal-exec "terminal-color 'Pro remote'" "ssh kohkimakimoto@192.168.56.23 -p 22" [ssh connect: hakoniwa-local5] terminal-exec "terminal-color 'Pro remote'" "ssh kohkimakimoto@192.168.56.24 -p 22"
これでcontrol+x control+xのショートカットでスクショにあるようにssh接続先一覧が出て、そこから選べるようになります。 ちなみに本番サーバはレッド、開発サーバやローカルの仮想環境はブルーで表示されるようにしています。
Codeから遷移したUIViewControllerにstoryboard上でナビゲーションバーを表示する
storyboad上のSegueで画面遷移をつないでいった場合、UINavigationControllerに含まれるViewControllerは自動でナビゲーションバーが表示されて、そこにタイトルとかバーボタンをとかを配置することができるのだけど、コードから遷移させた場合storyboad上ではどことも繋がっていないViewControllerとして表現されてしまって、実際はUINavigationControllerの中にいてナビゲーションバーとかがあるのにそれが表示されていない、ということが起こる。
そんなViewControllerの場合以下のようにしてやればstoryboard上にナビゲーションバーを表示でき、IB上でタイトルやボタンを配置することができます。
- storyboard上でViewControllerを選択する。
- Attributes inspectorで
Simulated Metrics
設定のTop Bar
をTranslucent Navigation Bar
に設定する - storyboard上にナビゲーションバーの領域があらわれる
- ナビゲーションバーの領域に
Navigation Item
をドラッグアンドドロップして配置する
以上。
Cocoapodsにあげたライブラリをアップデートする
毎度公式ドキュメントを参照していたのでメモ。以下のコマンドを叩くだけ
pod trunk push NAME.podspec