Phoenix に Cache 機構をいれてみます。
今回は ConCache を使って OpenWeatherMap API のレスポンスをキャッシュしてみます。
Phoenix アプリケーションでの外部 API リクエストについてはコチラを参照してください。
Elixir におけるキャッシュについて
キャッシュ機構の実装というと、シングルトンクラスを作って処理することをイメージするかもしれませんが、これは オブジェクト指向 な考え方であり、Elixir のような プロセス指向 な言語には当てはめられません。
プロセス指向では、その名の通り データをストアしておくプロセス を作成して処理します。プロセスの作成と聞くとなにやら大変そうな気がしますが、Erlang/OTP のおかげでシンプルに実装できるようになっています。
では早速、ConCache を利用してプロセスを用いたキャッシュを体験してみましょう。
依存関係に ConCache を追加する
cache_sample
というアプリケーションを作り、mix.exs
に以下のように追記しましょう。
$ mix phoenix.new cache_sample
defmodule CacheSample.Mixfile do
...
def application do
[mod: {CacheSample, []},
applications: [:phoenix, :phoenix_html, :cowboy, :logger,
:phoenix_ecto, :postgrex,
# ConCache を追記
:con_cache]]
end
...
defp deps do
...
{:cowboy, "~> 1.0"},
# HTTPoison を追記(API リクエスト用)
{:httpoison, "~> 0.7.2"},
# ConCache を追記
{:con_cache, "~> 0.8.1"}] end
end
最後にライブラリのダウンロードを行います。
$ mix deps.get
プロセスを作成する
Erlang/OTP では、子プロセス(worker)は親プロセス(supervisor)に監視させる必要があります。
Phoenix ではこの設定が lib/cache_sample.ex
で管理されていて、以下のように追記することで ConCache プロセスが作成できます。
defmodule CacheSample do
...
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
supervisor(CacheSample.Endpoint, []),
worker(CacheSample.Repo, []),
# ConCache を追記
worker(ConCache, [[], [name: :weather_cache]])
]
...
opts = [strategy: :one_for_one, name: CacheSample.Supervisor]
Supervisor.start_link(children, opts)
end
...
end
children に ConCache プロセスを追加します。この場合は :weather_cache
という名前のキャッシュが作成されます。
Supervisor.start_link(children, opts)
という部分で親プロセスの監視下で各プロセスが立ち上げられています。
API リクエスト部分を実装する
コチラと同じ実装ですので、記載は割愛させていただきます。
以下のリクエストで天気データが返ってくれば OK です。
$ curl -s "http://localhost:4000/api/weather/35.6585373/139.7151887" | jq .
{
"humidity": 74,
"pressure": 1006,
"temp": 26.99,
"temp_max": 31.11,
"temp_min": 24.44
}
キャッシュ部分を実装する
本題のキャッシュ機構を実装しましょう。
今回 Key とする値は「緯度・経度」です。
ConCache には get_or_store/3
という関数が用意されていて、これはその名の通り「指定した Key のキャッシュがあればそれを返却、なければ生成して追加 & 返却」というものです。そうそうこれが欲しかった、とはまさにこのことですね。
page_controller.ex
のレスポンス生成部分を以下のように書き換えます。
defmodule CacheSample.PageController do
...
def weather(conn, %{"lat" => lat, "lon" => lon}) do
key = "#{lat},#{lon}"
value = ConCache.get_or_store(:weather_cache, key, fn() ->
HTTPoison.start
result = HTTPoison.get! "http://api.openweathermap.org/data/2.5/weather?units=metric&lat=#{lat}&lon=#{lon}"
case result do
%{status_code: 200, body: body} -> Map.get(Poison.decode!(body), "main")
%{status_code: code} -> %{error: code}
end
end)
json conn, value
end
end
仕組みは簡単で、最初に Key となる文字列を作成して、それを get_or_store/3
で問い合わせます。
キャッシュされていなかったら、今まで通り OpenWeatherMap API に問い合わせを行い、結果の JSON をストアします。
最後に、get_or_store/3
から返却された JSON を返却して完了です。
とても簡単ですね。
効果を測定する
curl コマンドは以下のようにオプションをつける事でレスポンス速度だけ出力されることができます。
$ curl -kL "http://www.example.com/" -o /dev/null -w "%{time_total}\n" 2> /dev/null
0.354
これを使って同じ緯度経度を用いたリクエストのレスポンス速度が向上されているかを確認してみましょう。
$ curl -kL "http://localhost:4000/api/weather/35.6995477/139.5769432" -o /dev/null -w "%{time_total}\n" 2> /dev/null
0.566
$ curl -kL "http://localhost:4000/api/weather/35.6995477/139.5769432" -o /dev/null -w "%{time_total}\n" 2> /dev/null
0.024
$ curl -kL "http://localhost:4000/api/weather/35.6995477/139.5769432" -o /dev/null -w "%{time_total}\n" 2> /dev/null
0.024
0.566 秒 -> 0.024 秒 となり、しっかりとキャッシュが効いていることが確認できました。
感想
- Erlang/OTP のおかげて、プロセスについてそんなに意識しなくても実装できるようになっている
- ConCache には関数や設定項目が豊富に用意されていて、かなり実用的
- TTL(生存期間)もチューニングできるのは素晴らしい
- ただ、全キャッシュクリアのやり方だけイマイチ分からず...