Mercari Advent Calendar 2019の13日目担当はメルカリSREチームの@catatsuyです。
社内ではGoを書いていることが多いので、今回はmercari/gaurunの事情を通して、GoのHTTP/2事情に関して紹介します。
忙しい人のためのまとめ
- Goは
http.Transport
のTLSClientConfig
を上書きしている場合HTTP/2が使えません - HTTP/2を使いたい場合はGo1.12までは
golang.org/x/net/http2
のhttp2.ConfigureTransport
を自分で呼び出す必要があります - Go1.13の新機能
ForceAttemptHTTP2
を使えば標準機能でHTTP/2を有効にできます - Go1.13の新機能を使うことでgaurunから
golang.org/x/net/
の依存をなくすことができました
gaurunについて
以前から本ブログなどで紹介しているように、メルカリのプッシュ配信には自社でOSSとして開発しているgaurunを使用しています。
現在のgaurunでは以前に紹介したようにFCMとAPNsに対してHTTP/2を使用して通信しています。
今回はAPNsとの通信で抱えた課題について注目していきます。
gaurunでAPNsを使用する場合、pem_cert_path
, pem_key_path
というオプションでSSL証明書を渡す必要があります。現在のAPNsではトークン認証もサポートしていますが、gaurunではサポートしていません。なのでこの証明書はgaurunでのAPNsとの通信に必要です。Goの場合http.Transport
のTLSClientConfig
に証明書を渡せば通信ができます。実際のgaurunで現在使われているコードは以下です。
config := &tls.Config{
Certificates: []tls.Certificate{cert},
}
config.BuildNameToCertificate()
transport := &http.Transport{
TLSClientConfig: config,
MaxIdleConnsPerHost: ConfGaurun.Ios.KeepAliveConns,
Dial: (&net.Dialer{
Timeout: time.Duration(ConfGaurun.Ios.Timeout) * time.Second,
KeepAlive: time.Duration(keepAliveInterval(ConfGaurun.Ios.KeepAliveTimeout)) * time.Second,
}).Dial,
IdleConnTimeout: time.Duration(ConfGaurun.Ios.KeepAliveTimeout) * time.Second,
ForceAttemptHTTP2: true,
}
このコードに至るまでの経緯を紹介していきます。
GoのHTTP/2対応
Go1.6からHTTP/2に標準で対応しました。GoでHTTP/2対応をするコードは以前からgolang.org/x/net/http2
上で開発されていますが、こちらのコードをbundleというツールを使ってGoのソースコード内にいい感じにコピーすることで標準での対応を実現しています。これに関しては以下の記事が分かりやすいです。
簡単にnet/http
でのHTTP/2対応について紹介します。
net/http
がx/net/http2
に依存しようとすると循環importが発生するので依存できない- bundleという独自コマンドで
x/net/http2
のコードをGoのソースコード内にh2_bundle.go
という1つのファイルにまとめてコピーされている- コピーする際に関数の先頭を小文字にするなどの変更をしているので
net/http
のAPIに変更はない - https://github.com/golang/go/blob/2566e21f243387156e8e7f2acad0ce14d9712bbc/src/net/http/http.go#L5
- コピーする際に関数の先頭を小文字にするなどの変更をしているので
- テストはコピーされないので、開発自体は
x/net/http2
上で行われている
それに加えてx/net/http2
の仕様などについても簡単に紹介します。
x/net/http2
のhttp2.ConfigureTransport
を呼び出せばHTTP/2を使えるhttp.Transport
になる- この関数は
http2configureTransport
という関数名でnet/http
内で使える(先頭が小文字なのでunexportedな関数)
- この関数は
x/net/http2
のhttp2.Transport
を直接使うとHTTP/2しか扱えないTransportになるので通常使用しない- HTTP/2しか使用できないAPNsへの通信に使用している例はありますが、基本的に例外と考えて良いでしょう https://github.com/sideshow/apns2/blob/991ef5658ed1d8f5003f9b4b7e0a40c0cd0121bd/client.go#L90-L93
- 取り込まれている
x/net/http2
のバージョンはGitのbranch https://github.com/golang/net/branches で管理されている - 開発は2019/12/13現在、積極的に行われている
TLSClientConfigを上書きするとHTTP/2はデフォルトで使用できない
以上のことを踏まえて、2019/12/13現在の最新から1つ前のGo1.12のソースコードを確認してみます。
if t.TLSClientConfig != nil || t.Dial != nil || t.DialTLS != nil { // Be conservative and don't automatically enable // http2 if they've specified a custom TLS config or // custom dialers. Let them opt-in themselves via // http2.ConfigureTransport so we don't surprise them // by modifying their tls.Config. Issue 14275. return } t2, err := http2configureTransport(t) if err != nil { log.Printf("Error enabling Transport HTTP/2 support: %v", err) return } t.h2transport = t2
https://github.com/golang/go/blob/release-branch.go1.12/src/net/http/transport.go#L299-L312
これまでの説明にもあるようにhttp2configureTransport
を呼び出さなければHTTP/2を使用できません。しかしよく読むとhttp2configureTransport
を呼び出されずにreturnされる条件があることが分かります。
今回関係があるのはt.TLSClientConfig != nil
という条件です。TLSClientConfig
に設定を渡すとHTTP/2を使用できません。既に紹介したようにAPNsとの通信を行うためにTLSClientConfig
の設定を渡す必要がありますが、そうするとAPNsとの通信にHTTP/2を使用できなくなります。
通常ならHTTP/2が使用できない場合はHTTP/1.1を使用するだけなので困ることはあまりないかもしれません。しかしAPNsはHTTP/2しか提供しておらず、HTTP/1.1での通信が行えません。パフォーマンス面でもHTTP/2で使用する必要があるのでHTTP/2で通信する必要があります。
これを回避する方法はコメントにも書かれていますが、x/net/http2
のhttp2.ConfigureTransport
を自分たちで呼び出すのが一番良い方法です。しかしこれには以下の問題がありました。
- Goのコードと同じ挙動にするには使用しているGoのバージョンが使用している
golang.org/x/net/
を使用する必要があるので面倒- ブランチを指定すればよいが、バージョンアップ毎に必要
- Goは互換性が高い言語なので、特にOSSの場合はあまりGoのバージョンを固定したくない
golang.org/x/net/
はhttp2以外にも色々あり、かなり大きなリポジトリで更新も頻繁
これを解決する方法としてGo1.13の新機能を使うことがおすすめです。早速2019/12/13現在の最新のGo1.13のソースコードを確認します。
if !t.ForceAttemptHTTP2 && (t.TLSClientConfig != nil || t.Dial != nil || t.DialTLS != nil || t.DialContext != nil) { // Be conservative and don't automatically enable // http2 if they've specified a custom TLS config or // custom dialers. Let them opt-in themselves via // http2.ConfigureTransport so we don't surprise them // by modifying their tls.Config. Issue 14275. // However, if ForceAttemptHTTP2 is true, it overrides the above checks. return } t2, err := http2configureTransport(t) if err != nil { log.Printf("Error enabling Transport HTTP/2 support: %v", err) return } t.h2transport = t2
https://github.com/golang/go/blob/release-branch.go1.13/src/net/http/transport.go#L353-L367
ForceAttemptHTTP2
というのが追加されています。Go1.13の新機能でここをtrueにすればhttp2configureTransport
を必ず呼び出してくれるようになります。
欠点としてはx/net/http2
の関数や定数は先頭を小文字にしてコピーされているので、例えばエラーがhttp2.GoAwayError
か確認するみたいなことはできません。http2.GoAwayError
はhttp2GoAwayError
という名前でコピーされているのでnet/http
外で使用することができないためです。
しかしこれについては必要性が説明できればGoのコード側で取得する仕組みを用意してくれる可能性があります。本当に必要ならば開発チームに提案するのが正しい方法でしょう。
以上の理由からgaurunはForceAttemptHTTP2
をtrueにして、golang.org/x/net/
への依存をしないように変更しました。これによりGoのバージョンアップの追従も容易になりますし、特殊なコードも減りました。
今回の話題については以前に弊社で開催されたmercari.go #11での私の発表にて私が開発したISUCON9予選のベンチマーカーにおいても全く同じ対応が必要になったのでForceAttemptHTTP2
を使用した話を紹介しています。それ以外の話題についても紹介していますので、興味のある方は是非ご覧下さい。
最後に
Goでミドルウェアに近いレイヤーを触っているとGo自体のソースコードも確認することが必要になることは多々あります。弊社メルカリではそういったことも臆さずに行えて、開発を行える方を歓迎します。興味のある方はご連絡ください。
明日の執筆担当は、メルカリ Backend チームの@Peranikovさんです。それでは引き続きお楽しみください!