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
に指定したライブラリ名になります。
namespace fbs;
enum Status : byte {
Ok = 0,
NotFound,
InternalServerError,
}
table HttpResponse {
StatusEnum: Status = Ok;
Headers: string;
Body: string;
}
root_type HttpResponse;
ライブラリ自動生成ツールflatc
の呼び出し方法は、こんな感じ。
-g
オプションがGo言語ライブラリの指定です。
$ flatc -g -o src http-response.fbs
超お手軽、すばらしい。いまのところサンプルコードは少なく、テストコードも参考になります。
flatbaffersで、複雑な構造体を再現するのは、一苦労でしょう。
当初、flatbaffersでサポートされているらしい[byte]
を指定してみたのですが、怪しいコードを生成したので、string
で対応しました。http.Response.StatusCode
は極単純化してenum
に収めます。
以下、go-lru-cache-statsと組み合わせたコード抜粋。
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には、unsafe
packageがimportできない制約があって1、flatbaffers/go
はunsafe
packageを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)
としましょう。