はじめに
あるAPIサービスが、CloudFront + API Gateway + Lambda という構成で稼働しており
CloudFrontは、URL+QueryString
ごとにキャッシュし、応答速度をあげるために使用しています。
事象
このAPIサービスには、Go/PHPそれぞれで実装されたプログラムがHTTPリクエストを送ります。
CloudFrontの設定は以下の通り。(関連設定のみ)
設定項目 | 設定値 |
---|---|
Cache Based on Selected Request Headers | None |
Object Caching | Customize MinTTL:0 MaxTTL: 86400 DefTTL: 86400 |
Forward Cookies | None |
Query String Forwarding and Caching | Forward all, cache based on all |
Compress Objects Automatically | No |
Go/PHPそれぞれから同じ URL+QueryString でリクエストを送信した場合は、2回目のリクエストは
キャッシュHitする想定でしたが、なぜかMissになります。(X-Cacheヘッダで確認)
原因
Go実装のプログラムでは 標準のnet/httpパッケージを使ってリクエストを送信しています。
net/httpのhttpクライアントはレスポンスがgzip圧縮されていても自動的に展開してくれるため
デフォルトでAccept-Encoding: gzip
ヘッダが付きます。
CloudFrontは、このAccept-Encodingヘッダの有無でキャッシュが別管理になるため、
同じURL+QueryStringであっても、ヘッダ有無が異なれば再度オリジンアクセスする、という動作になります。
これは、レスポンスが圧縮されているかに関わらず上記の動作になりました。
コメントで指摘あったので追記)
Vary: Accept-Encoding
がレスポンスヘッダにあればAccept-Encodingヘッダ有無で
キャッシュが別になるのは理解できますが、今回使用しているオリジンは、レスポンスを圧縮しない、かつ
Varyヘッダを返さない仕様です。
DefaultClientでリクエスト送信 実装例
package main
import (
"fmt"
"net/http"
)
func main() {
url := "http://example.com/"
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
fmt.Errorf("%v", err)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Errorf("%v", err)
return
}
defer resp.Body.Close()
}
今回CloudFront設定を素直に?以下のように解釈してたので発見が遅れました
- リクエストヘッダでキャッシュ判定しない
- URL+QueryStringごとにキャッシュする
- 自動圧縮は無効化してるのでAccept-Encodingを見ない
↑このAccept-Encodingを見ないというのが勘違いで、オリジン側では通常Accept-Encodingヘッダによって
gzip圧縮するか判定しているはずで、CloudFrontも正しくキャッシュ(非圧縮を期待しているクライアントに圧縮
データを応答しないこと)するためにAccept-Encodingヘッダをキャッシュ判定に使用していると思われます。
Go側でリクエスト送信時にAccept-Encodingヘッダを付けないようにすればキャッシュがうまく使われるように
なります。以下Go実装の修正イメージ
Accept-Encodingヘッダを付加しないリクエスト送信 実装例
DisableCompressionをtrueにしたTransportを生成して、それを渡してclientを生成してclient.Doする形
package main
import (
"fmt"
"net/http"
)
func main() {
url := "http://example.com/"
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
fmt.Errorf("%v", err)
return
}
client := &http.Client{
Transport: &http.Transport{DisableCompression: true},
}
resp, err := client.Do(req)
if err != nil {
fmt.Errorf("%v", err)
return
}
defer resp.Body.Close()
}
おわりに
CloudFrontの公式ドキュメントに明確な記述が見つけられなかったので、今回記事にしました。
誰かの助けになれば幸いです。