サーバサイドエンジニアの @b4b4r07 です。この記事は Go Advent Calendar 2016 の 19 日目です。今回は Go (Revel フレームワーク) で書かれていた API サーバをフルスクラッチで書き直したお話をします。
Revel とは
A high productivity, full-stack web framework for the Go language
公式の説明にあるように、Revel は高機能でフルスタックな Web フレームワークです。
複雑なルーティングや、パラメータのパーシング、テンプレート機能など、Web アプリケーションを作ろうとなったときに必要な手段はたいてい兼ね揃えているようです。公式ドキュメントに詳しく書かれています。
Revel 以外にも Go 製の Web フレームワークは多数あり、有名どころだと以下のようなものが挙げられます。
その中でもはやり Revel は「重量級」「全部入り」「Rails のような」など表現されることが多いように思います。何をやりたいのか (どこまでやりたいのか) もしくは、API の将来的な全体像がはっきりしないときの選択肢として採用されることが多いのかもしれません。
最初は Revel で書かれていた
ある手段の実現のために、メインサービスとは別に新規の API を作りたい、という話になり Go で書くことになりました。要件としては簡単で、数百万規模のレコードを持つテーブルから SELECT して JSON 形式で返す API です。説明上のイメージとして、ここでは郵便番号から住所情報を引く API とします *1。
$ curl $API_URL/zip_code/1066118 | jq . { "zip_code": 1066118, "prefecture": "東京都", "city": "港区", "address1": "六本木6-10−1", "address2": "六本木ヒルズ森タワー", }
最初、別の人が1週間くらいで実装しており稼働直前の状態で僕に引き継がれました。当初は「Revel なのか」くらいな印象で若干の整備とセットアップを加えてサービスインしました。
この API サーバでやりたかったこと
- より速くレスポンスを返すこと
- Hot deploy ができること
パフォーマンスが求められたのは、メインサービスからこの API を叩くからです。実際にユーザを抱えているサービス内から呼ばれるとなると 10 msec 単位でチューニングしていきたいものです。また参照系の API しか持たない上、直近においても将来的においてもシンプルな設計で十分に活躍できる API であったため、よりシンプルな net/http
を利用することにしました。
加えて、日々の刻々と変わる可能性がある要件に応じて API に修正を加えることが求められており、より一層リクエストの取りこぼしなくリスタートできる必要性がありました。
Revel では Graceful Restart をサポートする P-R が過去に取り込まれており一見対応しているようだったのですが、Revert されており、自前で書き直す機運が高まった要素の一つでした。ちなみにこのとき、内部では rcrowley/goagain が使われていたようです。
これらの理由などにより、現時点で持つエンドポイントも少ないし SLOC 的にも書き直しにかかる工数は少ないであろうと見込めたため、乗り換えました。
フレームワークを使わずに書き直した
API サーバ全体の書き直しということで、
- 標準パッケージ
net/http
で書く- 補助として ant0ine/go-json-rest を併用
- ゼロ-ダウンタイム実現のため lestrrat/go-server-starter を組み込む
- 詳しくは作者 lestrrat さんの記事に書いてあります
という点で書くようにしました。
RESTful な API を心がける
今まで Revel で書いていたとき、叩かれるエンドポイントによっては JSON を返したり 400, 500 系のエラーのときはそうじゃなかったりと、統一的でなかったレスポンスを今回の書き直しに当たり整備しました。また、生えている API と期待される結果の予測がつきやすく、かつレスポンスの形式が統一されていると、API 全体としてのスタイリッシュさが際立ちとても清々しいです。
これらを利用して API サーバを書くのは簡単で、DEMO として以下のコードを例にとって説明します*2。
package main import ( "fmt" "log" "net" "net/http" "os" "github.com/lestrrat/go-server-starter/listener" ) func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello DEMO") } func newHandler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/", hello) return mux } func main() { var l net.Listener if os.Getenv("SERVER_STARTER_PORT") != "" { listeners, err := listener.ListenAll() if err != nil { fmt.Println(err) return } if 0 < len(listeners) { l = listeners[0] } } if l == nil { var err error l, err = net.Listen("tcp", fmt.Sprintf(":8080")) if err != nil { fmt.Println(err) return } } log.Printf("Start to serve") fmt.Println(http.Serve(l, newHandler())) }
net/http
で Web サーバを書く要領で書いていき、go-server-starter
で Listen するようにしてやればよいです。実際のコードではもうちょっと複雑ですが、JSON 形式になるように参照結果をレンダーして返しています。
ベンチマーク
ベンチマークを取るにあたり、Apache Bench を使用しました。
以下のベンチマーク結果は ab -n 10000 -c 1
(ローカルマシンで計測) した結果です。
revel | net/http |
---|---|
706.68 [#/sec] (mean) |
1350.54 [#/sec] (mean) |
Revel から net/http
に切り替えただけで倍近く速くなっており、乗り換えた価値はあったかなと思いました。この P-R 出すまでに掛かった時間は数時間で、変更量は +-600 くらいでした。
ちなみにこのタイミングでヘルスチェック用のエンドポイント (/hc
) やその他の機能追加をしたのですが、その際に ant0ine/go-json-rest を利用しました。このパッケージでは簡単に JSON レスポンスをいい感じにしてくれるのでおすすめです。これを使用しないバージョンでもベンチマークを取ったのですが、誤差の範囲に留まり大きな差は認められませんでした。
ちなみに、今回は net/http
で書き直すという手段を取ったのですが、次のサイトでは各フレームワークのベンチマークを公開しているので、どのフレームワークを利用するか参考になるかもしれません。
まとめ
- 当初 Revel で実装されていた API サーバを
net/http
ベースで再実装しました - サーバの機能自体が膨らむ前に乗り換えてしまったので移行コストは掛からなかったです
- ひとまずの選択肢として、何らかのフレームワークで実装するのはいいと思いますが、他の軽量フレームワークや
net/http
だけでも満たせるよね、と見切りがついた時点で舵を切るのはいいことだと思います