読者です 読者をやめる 読者になる 読者になる

Mercari Engineering Blog

We're the engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

GoでとあるAPIサーバを実装し直した話

Go

サーバサイドエンジニアの @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 サーバ全体の書き直しということで、

という点で書くようにしました。

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 で書き直すという手段を取ったのですが、次のサイトでは各フレームワークのベンチマークを公開しているので、どのフレームワークを利用するか参考になるかもしれません。

www.techempower.com

まとめ

  • 当初 Revel で実装されていた API サーバを net/http ベースで再実装しました
  • サーバの機能自体が膨らむ前に乗り換えてしまったので移行コストは掛からなかったです
  • ひとまずの選択肢として、何らかのフレームワークで実装するのはいいと思いますが、他の軽量フレームワークや net/http だけでも満たせるよね、と見切りがついた時点で舵を切るのはいいことだと思います

*1:ちなみに日々 KEN_ALL.csv によって更新されている

*2:参考