LoginSignup
7
7

More than 5 years have passed since last update.

GAE/Goでflatbuffers

Last updated at Posted at 2015-01-17

Goで使える最近のシリアライザーの資料を調べてたら、flatbuffersのベンチマークがその他と桁違いなので、とりあえず試してみました。
flatbuffersのドキュメントを斜め読みしてたら「あー、シリアライズなんて要らなかったんやー」となりました。

背景

Google App Engine/Go でHTTPプロキシーを書いている。
DDoSが来てもお手軽に対応できるように、いろいろキャッシュしたり最低限の施策をしたい。
インメモリーキャッシュはLRU方式のこれ
基本的にcacheはkey/valueなので、http.Responseをどうにかしてvalueに収める必要がある。

Goのシリアライズといえばgobなので、gobコンパチのhttp.Responseもどきを作ってみました。といっても、既出の丸コピです。HTTP Request/Responseのgobエンコーディングをサポートしているorchestrate-io/dvrをベースにgo-gob-httpをとりあえず作りましたが、gobの既出ベンチマークが桁違いに遅かったので試してません。gobラブな方はぜひ試してちゃんと動くか教えてください。

flatbuffers

2014年にGoogleが発表して、まったり開発されている模様です。
雰囲気的には、使いたい人が自分で実装する、方式のようです。
プルリクにはPython版もあるようです。

http://google.github.io/flatbuffers/index.html に全部書いてあります。
Mac OS Xでのツールのbuildは、ここの説明がキャプチャ入りで、迷うところがありません。

Go版は依存ライブラリもないので、自動生成されたコードをimportするのみです。
namespaceに指定したライブラリ名になります。

flatbaffers
namespace fbs;

enum Status : byte {
  Ok = 0,
  NotFound,
  InternalServerError,
}

table HttpResponse {
  StatusEnum: Status = Ok;
  Headers:    string;
  Body:       string;
}

root_type HttpResponse;

ライブラリ自動生成ツールflatcの呼び出し方法は、こんな感じ。
-gオプションがGo言語ライブラリの指定です。

shell
$ flatc -g -o src http-response.fbs

超お手軽、すばらしい。いまのところサンプルコードは少なく、テストコードも参考になります。

flatbaffersで、複雑な構造体を再現するのは、一苦労でしょう。
当初、flatbaffersでサポートされているらしい[byte]を指定してみたのですが、怪しいコードを生成したので、stringで対応しました。http.Response.StatusCodeは極単純化してenumに収めます。

以下、go-lru-cache-statsと組み合わせたコード抜粋。

go

import (
    flatbuffers "github.com/MiCHiLU/flatbuffers/go"
    lru "github.com/MiCHiLU/go-lru-cache-stats"

    "fbs"
)

var (
    responseGroup lru.Getter

    endOfMIMEType = []byte("\n\n")
    excludeHeader = map[string]bool{
        "Content-Length":    true,
        "Transfer-Encoding": true,
    }

    statusEnumList = [...]int{
        http.StatusOK,
        http.StatusNotFound,
        http.StatusInternalServerError,
    }
)

...

func init() {
    responseGroup = (*lru.NewGroup("response", responseCacheBytes, lru.GetterFunc(responseGetter), responseStats))
}

func proxy(w http.ResponseWriter, r *http.Request) {
    // TODO: Access-Control-Allow-Methods: GET,OPTIONS
    if r.Method != "GET" {
        http.Error(w, "", http.StatusMethodNotAllowed)
        return
    }

    c := appengine.NewContext(r)

    hostPath := r.URL.Host + r.URL.Path
    var responseByte = make([]byte, 0)
    err := responseGroup.Get(c, hostPath, lru.AllocatingByteSliceSink(&responseByte))
    if err != nil {
        http.Error(w, "", http.StatusBadGateway)
        return
    }

    c.Debugf("%v", responseByte)
    response := fbs.GetRootAsHttpResponse(responseByte, 0)

    // status code
    switch statusEnumList[response.StatusEnum()] {
    case http.StatusNotFound:
        http.NotFound(w, r)
        return
    case http.StatusInternalServerError:
        http.Error(w, "", http.StatusInternalServerError)
        return
    }

    // header
    headers := response.Headers()
    if headers != "" {
        reader := textproto.NewReader(bufio.NewReader(strings.NewReader(headers)))
        mimeHeader, err := reader.ReadMIMEHeader()
        if err != nil {
            c.Warningf("fail reader.ReadMIMEHeader(): %s, %s", err.Error(), hostPath)
            err = nil
        } else {
            for k, vs := range mimeHeader {
                for _, v := range vs {
                    w.Header().Set(k, v)
                }
            }
        }
    }

    // body
    body := response.Body()
    if body != "" {
        fmt.Fprint(w, body)
    }

    return
}

func responseGetter(ctx lru.Context, hostPath string, dest lru.Sink) (err error) {
    c := ctx.(appengine.Context)

    url := originUrl(hostPath)

    request, err := http.NewRequest("GET", url, nil)
    if err != nil {
        c.Errorf("%s: %s", err.Error(), url)
        return
    }
    client := urlfetch.Client(c)
    response, err := client.Do(request)
    if err != nil {
        // TODO
        return
    }

    builder := flatbuffers.NewBuilder(0)
    var headersOffset, bodyOffset flatbuffers.UOffsetT

    if response.StatusCode < 400 {
        // get Body
        defer response.Body.Close()
        body, err := ioutil.ReadAll(response.Body)
        if err == nil {
            // Header
            var buffer bytes.Buffer
            response.Header.WriteSubset(&buffer, excludeHeader)
            buffer.Write(endOfMIMEType)
            headersOffset = builder.CreateString(buffer.String())
            // Body
            bodyOffset = builder.CreateString(string(body))
        }
    }

    fbs.HttpResponseStart(builder)
    if err != nil || 500 <= response.StatusCode {
        fbs.HttpResponseAddStatusEnum(builder, fbs.StatusInternalServerError)
    } else if 400 <= response.StatusCode {
        fbs.HttpResponseAddStatusEnum(builder, fbs.StatusNotFound)
    }
    if headersOffset != 0 {
        fbs.HttpResponseAddHeaders(builder, headersOffset)
    }
    if headersOffset != 0 {
        fbs.HttpResponseAddBody(builder, bodyOffset)
    }
    rootTable := fbs.HttpResponseEnd(builder)
    builder.Finish(rootTable)

    err = dest.SetBytes(builder.Bytes[builder.Head():])
    if err != nil {
        c.Errorf("dest.SetBytes() has error: %s, %s", err.Error(), url)
    }

    return
}

本題

で、ここが本題なのですが、Google App Engineには、unsafepackageがimportできない制約があって1flatbaffers/gounsafepackageをimportしているので、GAEにデプロイできません。
見たところ、型のメモリサイズを取得してオフセットに勘案するような感じだったので、「Google App Engineの実行環境はDockerに移行しているだろう」という憶測のもと、というか「Linux/64bit環境だろう」という愚直な推測のもと、Docker imageの念のためgoogle/golang-runtime をGoogle Compute Engine上で取得した値 2 をハードコードで埋め込みました。

デプロイしたところうまく動いてるっぽいので、皆さんも試していただけると大変助かります。

しばらくしたら、flatbaffersにプルリクします。

メモ

ローカルでは問題になりませんが、GAE上だとc.Debugf("%s", responseByte)が常に""を出力するのがハマリポイントです。c.Debugf("%v", responseByte)としましょう。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7