ISUCON7の予選は、画像へのアクセスをいかに304で処理して帯域を節約できるかが肝でした。そこでnginxのETagやLast-Modified等の設定周りを見直してみます。
環境
以下の環境で試しました。
- OS: Ubuntu 16.04
- nginx: 1.10.3
- Client: python-requests 2.8.1
初期設定
サーバ上の/public/sample.png
にサンプルの画像を配置し、http://host/sample.png
でアクセスできるようにnginx.confを設定します。以下で拡張子が*.css
や*.js
、*.png
の場合は/public
配下より配信されます。
server { listen 80; location ~* \.(css|js|png)$ { root /public; } }
今回はアクセスのテスト用にPythonのrequestsを使用したサンブルクラスを作成し、それを使って試していきます。
import requests class SessionTest: def __init__(self): self.ses = requests.session() def head(self, url, headers=None): if not headers: headers={} print(headers) res = self.ses.head(url, headers=headers) self._print_response(res) return res def get(self, url, headers=None): if not headers: headers={} print(headers) res = self.ses.get(url, headers=headers) self._print_response(res) return res def post(self, url, headers=None): if not headers: headers={} print(headers) res = self.ses.post(url, headers=headers) self._print_response(res) return res @staticmethod def _print_response(res): print("status_code={}".format(res.status_code)) print("status_history_code={}".format(res.history)) print("=="*10) print("# headers") print("=="*10) for k, v in res.headers.items(): print("{}=>{}".format(k, v))
実際にアクセスするとこんな感じのヘッダが帰ってきます。
ses = SessionTest() _ = ses.get("http://devntu1/sample.png") {} status_code=200 status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 07:46:39 GMT Content-Type=>image/png Content-Length=>81892 Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4" Accept-Ranges=>bytes
当たり前ですがレスポンスコードは200です。これが304になる仕組みをみていきます。
キャッシュ判定の仕組み
先程のレスポンスのヘッダは以下でした。
Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 07:46:39 GMT Content-Type=>image/png Content-Length=>81892 Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4" Accept-Ranges=>bytes
大事なのはLast-Modified
とETag
です。それぞれ以下の意味です。
response header | mean |
---|---|
Last-Modified |
ファイルの最終更新日 |
ETag |
ファイルとその更新情報をもとにしたハッシュ値のようなもの |
たとえば、Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT
となっていますが、これはOS上でみたファイルの更新日です。
root@denzow-ubuntu:/public# ls -l sample.png -rw-r--r-- 1 root root 81892 11月 26 14:39 sample.png ★GMTをJSTに直せば一致する更新時刻
クライアント側はこれらの値を付与してリクエストし、サーバ側はこれらの値が一致すればレスポンスコードを304(Not Modified)にして戻し、実際のデータはやりとせずに済ませます。
それぞれのレスポンスヘッダと対応するリクエストヘッダは以下です。
response header | response header |
---|---|
Last-Modified |
If-Modified-Since |
ETag |
If-None-Match |
クライアント側は初回のアクセスで受け取ったヘッダー値を、それぞれ対応するヘッダにセットしてリクエストすることで2回目以降のアクセスを効率的に行えます。
実際に304が戻るか見てみる
If-Modified-Since
それではリクエストヘッダーとしてIf-Modified-Since
を付与してレスポンスコードを確認してみます。
ses = SessionTest() # 前回のLast-ModifiedをIf-Modified-Sinceにセットしてアクセス _ = ses.get("http://devntu1/sample.png", headers={"If-Modified-Since": "Sun, 26 Nov 2017 05:39:32 GMT" })
結果はこうなりました
{'If-Modified-Since': 'Sun, 26 Nov 2017 05:39:32 GMT'} status_code=304 # 期待通り304が戻っている status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 10:06:30 GMT Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4"
304が戻っていますね。
では、If-Modified-Since
に過去の値を指定してみます。
ses = SessionTest() # 26日 -> 25日 _ = ses.get("http://devntu1/sample.png", headers={"If-Modified-Since": "Sun, 25 Nov 2017 05:39:32 GMT" })
{'If-Modified-Since': 'Sun, 25 Nov 2017 05:39:32 GMT'} status_code=200 status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 11:21:18 GMT Content-Type=>image/png Content-Length=>81892 Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4" Accept-Ranges=>bytes
今度は200になりました。25日以降に変更があれば・・という意味ですので26日の変更日を持っていいる以上200が戻るのは正しい挙動でしょう。
続いて、未来の値(1h後)を指定した結果が以下です。
{'If-Modified-Since': 'Sun, 26 Nov 2017 06:39:32 GMT'} status_code=200 status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 11:27:40 GMT Content-Type=>image/png Content-Length=>81892 Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4" Accept-Ranges=>bytes
こちらも200になりました。(違和感があるのですが、調べきれませんでした)
If-None-Match
続いてリクエストヘッダーとしてIf-None-Match
を付与してレスポンスコードを確認してみます。
_ = ses.get("http://devntu1/sample.png", headers={"If-None-Match": '"5a1a5394-13fe4"' })
{'If-None-Match': '"5a1a5394-13fe4"'} status_code=304 status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 12:38:24 GMT Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4"
無事に304が返せています。地味にハマるのがETagは引用符込で指定する必要があるという点でした。つまり
_ = ses.get("http://devntu1/sample.png", headers={"If-None-Match": "5a1a5394-13fe4" })
はだめで
_ = ses.get("http://devntu1/sample.png", headers={"If-None-Match": '"5a1a5394-13fe4"'}) # ダブルクォテーションもデータに含める
でなければいけないということです。
もちろん誤ったETagを指定すると200が戻ります。
{'If-None-Match': '"99999999"'} ★マッチしないETag status_code=200 ★200が戻される status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 12:40:56 GMT Content-Type=>image/png Content-Length=>81892 Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4" Accept-Ranges=>bytes
ETagの生成方法
このあたりを見る感じ、ETagはLast-ModifiedとContent-Lengthで生成しているようです。
Content-Length=>81892 Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT
から
ETag=>"5a1a5394-13fe4"
を求めてみます。
どうやら-
を境に前方がLastModifedで後方がContent-Lengthのようです。まずは前者から求めます。GMTをJSTにしてエポックタイムにして16進数にしてみます。
from datetime import datetime import time # +9時間しておく d = datetime(2017, 11, 26, 14, 39, 32) # epochタイムに変換 epoch_time = time.mktime(d.timetuple()) hex(int(epoch_time))
結果は0x5a1a5394
となり期待した値でした。
続いて、Content-Length側を求めます。こちらは単に16進数にすればよいでしょう。
hex(81892)
結果は0x13fe4
ですのでこれでETagが求まりました。この結果よりnginxのETagはファイルの更新日とデータサイズに依存していることが確認できました。Apache等ではファイルのi-node番号等も用いているらしく、複数サーバに分散していると同じファイルでもETagが変化してしまうことがあるそうですが、nginxではその心配はなさそうです。
両方指定した場合
では両方指定した場合の挙動を確認します。まずは両方ともあっている場合です。
_ = ses.get("http://devntu1/sample.png", headers={"If-Modified-Since": "Sun, 26 Nov 2017 05:39:32 GMT", "If-None-Match": '"5a1a5394-13fe4"'})
{'If-Modified-Since': 'Sun, 26 Nov 2017 05:39:32 GMT', 'If-None-Match': '"5a1a5394-13fe4"'} status_code=304 status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 13:03:00 GMT Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4"
当然ですが304が帰りますね。続いて、If-None-Match
が誤っている場合です。
{'If-Modified-Since': 'Sun, 26 Nov 2017 05:39:32 GMT', 'If-None-Match': '"9999-9999"'} status_code=200 status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 13:04:18 GMT Content-Type=>image/png Content-Length=>81892 Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4" Accept-Ranges=>bytes
200が返りました。
続いて、If-Modified-Since
がキャッシュの効かない値を指定している場合です。
{'If-Modified-Since': 'Sun, 27 Nov 2017 05:39:32 GMT', 'If-None-Match': '"5a1a5394-13fe4"'} status_code=200 status_history_code=[] ==================== # headers ==================== Server=>nginx/1.10.3 (Ubuntu) Date=>Sun, 26 Nov 2017 13:05:36 GMT Content-Type=>image/png Content-Length=>81892 Last-Modified=>Sun, 26 Nov 2017 05:39:32 GMT Connection=>keep-alive ETag=>"5a1a5394-13fe4" Accept-Ranges=>bytes
こちらも200が戻りました。両方指定がある場合は両方が一致しないといけなさそうです。しかし、ここらへんは他のサイトの情報と一致しないのでもう少し確認しないといけなさそうです。 ※If-None-Matchが優先される的な情報が多かった…
まとめ
とりあえず、ざっくりとnginxに於けるETagやLast-Modifedの挙動を確認してみました。まだまだ理解できていないところもありますが、とりあえずまとめてみて少し理解が深まったと思います。