【負荷対策】Apacheのmod_cahceで動的APIレスポンスをキャッシュする


昨今はiOS、Android等のスマホ向けネィティブアプリやフロントエンドJavaScript向けの「サーバ側」機能として、RESTfulなHTTPプロトコルベースの Web API を作る機会が多いかと思います。

その際は PHP や Perl、Python、Ruby など諸々の言語でAPIスクリプトを作成しますが、サーバに対するAPIリクエスト数が増えてくるとサーバの負荷も増え、今度は負荷対策に悩まされることになってきます。

WEBサーバとして Apache を利用している場合は、mod_cache モジュールを利用して簡単にAPIレスポンスをApacheレイヤーにキャッシュできますので、今回はその方法を紹介します！

![WS000000.JPG](https://qiita-image-store.s3.amazonaws.com/0/35733/65c39f22-2b7c-9aa5-5dd0-c8c2a1b5da3e.jpeg)

# 要点

下記に長々と設定例を書きますが、簡単に要点を説明すると、

1. Apacheの設定で、mod_cacheモジュールの有効化とモジュールの初期設定をする
1. キャッシュさせたいAPIレスポンスのHTTPレスポンスヘッダに `Cache-Control: max-age=秒数` を付与する(秒数のところはサーバにキャッシュさせたい時間を指定)

これだけです！後は mod_cache が同一内容の2度目以降のAPIリクエストに対して有効期限のキャッシュありなしを判定し、キャッシュがあればスクリプトに処理が渡る前に、キャッシュの内容をクライアント端末へレスポンスしてくれます。

# 前提

## キャッシュ対象にすべきAPI

APIレスポンスをキャッシュするということは、スクリプトに処理が渡らないということです。なので、どういうAPIをキャッシュできるかというと、次のようなAPIになるかと思います。

* サーバ側のDBデータを更新しない
* 複数のクライアント端末で同じGETリクエスト(URL、クエリ文字列)＆同じレスポンス内容で構わない

つまり「マスター参照系のAPI」です。

※キャッシュされるのは `Cache-Control: max-age=秒数` を付与したAPIだけなので、キャッシュしたらまずいAPIは何もしなければいいだけです。

## 私が動作確認した環境

* EC2: Amazon Linux AMI 2013.09.2 (64-bit)
* Apache/2.2.26

>このApacheは[このエントリ](http://qiita.com/hkusu/items/35663a83896c26cd5079)のように yum でインストールしたものです

# Apacheの設定

## mod_cache(mod_disk_cache)の有効化

もし有効になっていなかったら`httpd.conf`を編集します(私の環境ではApacheのインストール時点で初めから有効になっていました)。

```apache:httpd.conf
LoadModule cache_module modules/mod_cache.so
LoadModule disk_cache_module modules/mod_disk_cache.so
```

mod_mem_cache(ファイルでなくメモリにキャッシュ)というのもあるのですが、どうやらApacheがworkerモードで動作してないと意味が無いようなので、今回は mod_disk_cache を利用しています。

## mod_cache(mod_disk_cache)の設定

`httpd.conf`を編集します。
※編集後にApacheを再起動して、設定内容をApacheへ反映するのを忘れないように。

```apache:httpd.conf
CacheRoot /var/www/html/cache
CacheIgnoreCacheControl On
CacheIgnoreHeaders Set-Cookie

CacheEnable disk /api/
#CacheEnable disk /api/hoge.php
#CacheDisable /api2/
#CacheDisable /api2/hoge.php

#CacheIgnoreQueryString On
#CacheIgnoreURLSessionIdentifiers hoge
```

最低限、必要な記載は、

`CacheRoot /var/www/html/cache`
`CacheEnable disk /api/`

の2行だけです。`CacheRoot`にはキャッシュファイルを保存するディレクトリを指定します(Apacheから書き込めるよう`chmod 777`しておく)。

`CacheEnable`にはキャッシュ対象とするHTTPリクエストのURLを指定します。この例だと`http://hoge.com/api/`配下のURLに対してキャッシュが有効になります(ただし前述のとおりHTTPレスポンスヘッダに`Cache-Control: max-age=秒数`を付与した場合のみ)。
例ではコメントアウトしていますが、どうやら前方一致のようなので、スクリプト名をダイレクトに指定することも出来ます。

一応、それ以外の設定を説明をすると、

`CacheIgnoreCacheControl On`
..クライアント端末から `Cache-Control: no-cache` が送られてきた場合もキャッシュ対象にする。一応記載しておいた方がよさそう。

`CacheIgnoreHeaders Set-Cookie`
..キャッシュにcookieセットを含めない。確かにまずそうなのでこれも一応記載しておく。

`CacheDisable /api2/`
..`CacheEnable`と逆に、キャッシュしないURLを指定。

`CacheIgnoreQueryString On`
..これはAPIによっては重要な設定。クエリ文字列を無視してURLだけでキャッシュありなしを判定する場合は On にする。

`CacheIgnoreURLSessionIdentifiers hoge`
..これもAPIによっては重要な設定。特定のクエリ文字列を無視する場合はここに記載する。


詳しくはこのエントリの最後に記載している参考URLの、Apache公式ドキュメントをご覧ください。

# 動作確認

PHPの場合は次のようなスクリプトを用意し、2回目のリクエストが瞬時に応答すればキャッシュは効いています。キャッシュファイルは `httpd.conf` の `CacheRoot` で指定したディレクトリに作成されます。

また60秒後にちゃんとキャッシュが無効になっていることも確認してみてください！

```php:test.php
<?php

//重い処理を模するために10秒スリープ
sleep(10);

header('Content-Type: text/plain; charset=UTF-8');
header('Cache-Control: max-age=60');
echo "Hello World!";

?>
```

# おまけ：.htaccessでキャッシュ制御をしてみる

スクリプト言語側で `Cache-Control` ヘッダを吐いてもいいのですが、既存のプログラムをいじりたくないという場合は、次のように`.htaccess`でもヘッダを制御できます。
mod_headerとmod_rewriteを利用しています。

```apache:.htaccess
RewriteEngine on

# Api for 60 seconds cache
RewriteCond %{REQUEST_URI} ^/api/hoge1\.php$ [OR]
RewriteCond %{REQUEST_URI} ^/api/hoge2\.php$ [OR]
RewriteCond %{REQUEST_URI} ^/api/hoge3\.php$
RewriteCond %{REQUEST_METHOD} ^GET$
RewriteRule .* - [E=X_USE_CACHE_60SEC:]

# Api for 120 seconds cache
RewriteCond %{REQUEST_URI} ^/api/hoge4\.php$ [OR]
RewriteCond %{REQUEST_URI} ^/api/hoge5\.php$ [OR]
RewriteCond %{REQUEST_URI} ^/api/hoge6\.php$
RewriteCond %{REQUEST_METHOD} ^GET$
RewriteRule .* - [E=X_USE_CACHE_120SEC:]

# Default setting
Header set Cache-Control "max-age=0"
Header unset Last-Modified
Header unset Etag
Header unset Expires
Header unset Vary

# Cache setting
Header set Cache-Control "max-age=60" env=X_USE_CACHE_60SEC
Header set Cache-Control "max-age=120" env=X_USE_CACHE_120SEC
```

この例の場合は、`hoge1.php``hoge2.php``hoge3.php`は60秒キャッシュされ、`hoge4.php``hoge5.php``hoge6.php`は120秒キャッシュされます。
ここに記載しないphpファイルは、`max-age=0` が付与されるのでキャッシュされません(max-ageを指定しなければキャッシュされないようですが念のため 0 を指定)。

`Header unset`で4つのヘッダを一応削除しています(残ってるとなんとなく気持ち悪いので)。
ただ Vary だけは、サーバ環境によってはきちんと削除しておかないといけません。例えば Vary の値が User-Agent となっていると、リクエストパラメータ(URL、クエリ文字列)かつユーザエージェントが完全に一致しないと、キャッシュが効かなくなります。いまの時代、ユーザエージェントは膨大な種類があるので..

# おわりに

例をapiとしていますが、動的なWEB系スクリプト処理であれば全て同じようにキャッシュできます！また１時間(max-age=3600)、１日(max-age=86400)という長いキャッシュ時間を設定することも可能です。

今回は同一WEBサーバ内の動的コンテンツをキャッシュする設定を説明しましたが、応用して次のようなキャッシュの使い方も可能だと思われます。

* リバースプロキシサーバとしてApacheを立て、コンテンツ生成サーバで作成される動的コンテンツをプロキシサーバにキャッシュする。
* 外部サイトで提供されるのAPIが重いor不安定な場合に、ローカルサーバに外部サイトのAPIレスポンスをキャッシュする

これらは近いうちに検証して、また別のエントリで説明します。

# 参考URL

[Apache2.2のmod_cache公式ドキュメント](http://httpd.eu.apache.org/docs/2.2/mod/mod_cache.html)