🍥

SouinでCaddyをキャッシュサーバーにしなち

2023/04/02に公開

2024/09/28追記:おそらく最近(v1.7.0)のSouinはCache-Control: max-age=0Cache-Control: no-cacheがあっても正常に動くようになったので、Minimal Configurationにdefault_cache_controlを追加するだけでよさそうです。

Caddyfile
{
	cache
}

localhost:80 {
	cache {
		default_cache_control public, max-age=5, must-revalidate
	}
	reverse_proxy 127.0.0.1:8080
}

以降紹介しているCaddyfileの情報は2023/04/02当時の情報ですので注意してください。

Cache

キャッシュサーバーといえばVarnishを使う人が多いのではないのでしょうか。

https://varnish-cache.org/

他にも簡単に使えるApacheのmod_cacheやNginxのproxy_cacheなんかもあります。

https://caddyserver.com/

ではこのCaddyにもキャッシュサーバー用Moduleが存在するのでは……と思い、探してみたところありました。

https://github.com/caddyserver/cache-handler

Souin cacheをベースにしたCaddy用の分散型HTTPキャッシュモジュールです。

どうやらSouinというHTTPキャッシュシステムをベースにしているようです。

https://github.com/darkweak/souin

元々Caddy独自でCache Moduleを作っていたそうですが、RFC互換で、Vary, Request Coalescing, Stale Cache-Control, RFC7234に関連する仕様に対応しているSouinを取り込むことにしたそうです。
Cache-Status HTTPレスポンスヘッダや、VarnishなどのYKeyグループ、go-esiパッケージにより、ESIタグもサポートしています。

https://github.com/darkweak/go-esi

Souin本家とCaddy Cache Handler Moduleとの違いはほとんどなく、CaddyのOrganizationで管理しているかどうかの違いぐらいだそうです。

https://github.com/caddyserver/cache-handler/pull/17#issuecomment-913143364

ただ両方ともCaddyのModuleとして使用することができますし、Souin本家の方が積極的にアップデートされるため、私はSouin本家のModuleを使ってみることにしました。

ちなみにSouinは単体でバイナリを配布しており、単体でもリバースプロキシやキャッシュサーバーとして機能しますが、Souin独自の設定ファイルconfiguration.ymlを書く必要があるので、Caddyのリバースプロキシを使いたい人やCaddyfileにまとめたい人はCaddyのModuleを使ったほうが良さそうです。

その他にも様々なGo Webフレームワークと統合したり、Cache StorageとしてBadger, Etcd, NutsDB, Olric, Redisを選択できたりものすごく多機能です。
全部説明しても仕方ないので詳しくはSouinのREADME.mdを読んで下さい。

Souin

早速SouinのCaddy ModuleをCaddyに追加してみます。

$ xcaddy build --with github.com/darkweak/souin/plugins/caddy

README.mdに記載されている上記の通り、Custom Caddy Builderのxcaddyを使用しても追加できますが、公式のダウンローダーでも追加できるので今回はこちらの方法を試してみます。
また、CaddyをWindowsのローカル環境で普段使っているので、Windowsのローカル環境で試してみたいと思います。

https://caddyserver.com/download

ダウンロードページへ行き、darkweak/souin/plugins/caddyを選択します。

souin

「Extra features: 1」になっていることを確認したら、「Download」ボタンを押し、しばらく待つとダウンロードが始まります。

> caddy version
v2.6.4 h1:2hwYqiRwk1tf3VruhMpLcYTg+11fCdr8S3jhNAdnPy8=

> caddy list-modules
...

  Standard modules: 100

cache
http.handlers.cache

  Non-standard modules: 2

  Unknown modules: 0

caddy list-modulesコマンドを使い追加されていることを確認できました。

以下のページで使い方を確認してみます。

https://github.com/darkweak/souin/tree/master/plugins/caddy

Caddyfile
{
	order cache before rewrite
}

localhost:80 {
	cache {
		ttl 5s
	}
	reverse_proxy 127.0.0.1:8080
}

Caddyfileを少し修正しましたが、このように書けば動くっぽい?
SouinはデフォルトのTTLが2分なのでテスト用に5秒にしました。

> deno --version
deno 1.32.3 (release, x86_64-pc-windows-msvc)
v8 11.2.214.9
typescript 5.0.3
server.ts
import { serve } from "https://deno.land/std@0.182.0/http/server.ts";

serve((_req) => new Response(String(Math.floor(Date.now() / 1000))), {
  hostname: "127.0.0.1",
  port: 8080,
});

DenoでUnixtimeを返すWebサーバーをサクッと作りました。

> deno run --allow-net server.ts
> caddy run
> curl -i http://localhost/

コマンドプロンプトを3枚用意してcurlでテストします。

> curl -i http://localhost/
HTTP/1.1 200 OK
Cache-Control:
Cache-Status: Souin; fwd=uri-miss; stored; key=GET-http-localhost-/
Content-Length: 10
Content-Type: text/plain;charset=UTF-8
Date: Sun, 02 Apr 2023 13:45:25 GMT
Server: Caddy
Vary: Accept-Encoding

1680443125
> curl -i http://localhost/
HTTP/1.1 200 OK
Age: 2
Cache-Control:
Cache-Status: Souin; hit; ttl=3; key=GET-http-localhost-/
Content-Length: 10
Content-Type: text/plain;charset=UTF-8
Date: Sun, 02 Apr 2023 13:45:25 GMT
Server: Caddy
Vary: Accept-Encoding

1680443125
> curl -i http://localhost/
HTTP/1.1 200 OK
Cache-Control:
Cache-Status: Souin; fwd=uri-miss; stored; key=GET-http-localhost-/
Content-Length: 10
Content-Type: text/plain;charset=UTF-8
Date: Sun, 02 Apr 2023 13:45:30 GMT
Server: Caddy
Vary: Accept-Encoding

1680443130

ちゃんと5秒間キャッシュされていることがわかります。

ブラウザで確認してみましょう。

……あれ?

キャッシュされていない???

原因はリクエストヘッダーにいるこいつです。

Cache-Control: max-age=0

curlでもこのように叩けばキャッシュされていないことがわかります。

> curl -i -H "Cache-Control: max-age=0" http://localhost/

詰んだか???

Caddy

CaddyのIssuesを確認したところ全く同じ問題を見つけました。

https://github.com/caddyserver/cache-handler/issues/48

どうやらDirective Orderを使い、先行してCache-Controlのリクエストヘッダーを書き換えてしまえばよさそうです。

https://github.com/caddyserver/cache-handler/issues/48#issuecomment-1472893951

Cache-Controlをすべて消してしまうと、ChromeやEdgeでCtrlキー + F5キーを押したとき(いわゆるスーパーリロード)や、DevToolsのネットワークタブの「キャッシュを無効化」にチェックを入れたとき送信されるリクエストヘッダーのCache-Control: no-cacheも消してしまいキャッシュを更新したくても更新できなくなるので、max-age=0の値をTTLと同じ値へ置換することにしました。

Caddyfile
{
	order request_header before rewrite
	order cache before rewrite
}

localhost:80 {
	request_header Cache-Control max-age=0 max-age=5
	cache {
		ttl 5s
	}
	reverse_proxy 127.0.0.1:8080
}

これで完璧です。

> curl -i -H "Cache-Control: max-age=0" http://localhost/
HTTP/1.1 200 OK
Cache-Control:
Cache-Status: Souin; fwd=uri-miss; stored; key=GET-http-localhost-/
Content-Length: 10
Content-Type: text/plain;charset=UTF-8
Date: Sun, 02 Apr 2023 14:03:42 GMT
Server: Caddy
Vary: Accept-Encoding

1680444222
> curl -i -H "Cache-Control: max-age=0" http://localhost/
HTTP/1.1 200 OK
Age: 2
Cache-Control:
Cache-Status: Souin; hit; ttl=3; key=GET-http-localhost-/
Content-Length: 10
Content-Type: text/plain;charset=UTF-8
Date: Sun, 02 Apr 2023 14:03:42 GMT
Server: Caddy
Vary: Accept-Encoding

1680444222
> curl -i -H "Cache-Control: max-age=0" http://localhost/
HTTP/1.1 200 OK
Cache-Control:
Cache-Status: Souin; fwd=uri-miss; stored; key=GET-http-localhost-/
Content-Length: 10
Content-Type: text/plain;charset=UTF-8
Date: Sun, 02 Apr 2023 14:03:47 GMT
Server: Caddy
Vary: Accept-Encoding

1680444227

あとはTTLとあわせてCache-ControlやVaryを各自調整して使ってください。

Ref

VarnishやESIなど、RFC以外のCacheに関する情報は全ていわなちゃんさんのブログを参考にしました。
ありがとうございました。

https://blog.xcir.net/
https://blog.xcir.net/?p=2806
https://blog.xcir.net/?p=706

Discussion