Table of Contents [expand]
Memcache は、Web アプリとモバイルアプリバックエンドのパフォーマンスとスケーラビリティを改善する技術です。ページの読み込みが遅すぎる場合や、アプリにスケーラビリティの問題がある場合は、Memcache の使用を検討してください。小規模なサイトであっても、Memcache の導入によってページの読み込みを高速化し、将来の変化にアプリを対応させることができます。
このガイドでは、単純な Laravel 5.6 アプリケーションを 作成して Heroku にデプロイし、Memcache を追加してパフォーマンスのボトルネックを軽減する方法を示します。
前提条件
このガイドの手順を完了する前に、以下のすべての条件を満たしていることを確認してください。
- PHP の知識がある (Laravel についても一定の知識があることが理想的です)
- Heroku ユーザーアカウント (無料ですぐにサインアップ)
- 「Heroku スターターガイド (PHP)」の手順を理解している
- PHP、Composer、Heroku CLI がコンピュータにインストールされている
このチュートリアルのベースは 「Laravel 5.2 チュートリアル」および 「Heroku Laravel ガイド」です。 Heroku での Laravel アプリケーションの作成およびデプロイについての詳細は、 これらのリソースを参照してください。
Heroku への Laravel アプリケーションのデプロイ
まず、次のようにして Laravel スケルトンアプリを作成します。
$ composer create-project laravel/laravel --prefer-dist laravel_memcache
Installing laravel/laravel (v5.6.0)
- Installing laravel/laravel (v5.6.0): Loading from cache
Created project in laravel_memcache
...
$ cd laravel_memcache
Heroku 固有のセットアップ
動作する Heroku アプリケーションを作成する前に、Heroku に固有のいくつかの変更をスケルトンに追加する必要があります。
アプリケーションの実行方法を Heroku に指示する単純な Procfile を作成します。
$ echo web: vendor/bin/heroku-php-apache2 public/ > Procfile
アプリケーションで Heroku プロキシを信頼する必要があります。
app/Http/Middleware/TrustProxies.php
で、$proxies
と$headers
を次のように変更します。// ... protected $proxies = '**'; // ... protected $headers = Request::HEADER_X_FORWARDED_AWS_ELB; // ...
この設定は 5.6 よりも前の Laravel には無効です。バージョンが古い場合の手順については、 Stack Overflow のこちらの回答 を参照してください。
Heroku アプリケーションを作成する前に、Git リポジトリを初期化し、前の手順までの作業をコミットする必要があります。
$ git init Initialized empty Git repository in ~/laravel_memcache/.git/ $ git add . $ git commit -m "Laravel skeleton for Heroku" [master (root-commit) 3099e3b] Laravel skeleton for Heroku 84 files changed, 7077 insertions(+) ...
Heroku アプリを作成して設定する
Heroku アプリケーションを作成する準備ができました。
$ heroku create
Creating app... done, ⬢ serene-castle-14546
https://serene-castle-14546.herokuapp.com/ | https://git.heroku.com/serene-castle-14546.git
Laravel スケルトンをデプロイする前に、いくつかの設定を環境設定の形で追加する必要があります。
Laravel 暗号化鍵を設定します。
$ heroku config:set APP_KEY=$(php artisan key:generate --show) Setting APP_KEY and restarting ⬢ serene-castle-14546... done, v3 APP_KEY: base64:E8Ay5w611tCLkqLnGSualCypRR+s8PGSfK20M+0HNIU=
ロガーの書き込み先を
errorlog
に設定します。$ heroku config:set LOG_CHANNEL=errorlog
You can optionally configure
errorlog
to be your default log channel inconfig/loging.php
:'driver' => env('LOG_CHANNEL', 'errorlog'),
ここで、Laravel スケルトンを Heroku にデプロイします。
$ git push heroku master
Counting objects: 113, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (95/95), done.
Writing objects: 100% (113/113), 181.42 KiB | 5.85 MiB/s, done.
Total 113 (delta 9), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Fetching custom git buildpack... done
remote: -----> PHP app detected
...
remote: Verifying deploy... done.
To https://git.heroku.com/serene-castle-14546.git
* [new branch] master -> master
Laravel アプリが Heroku にデプロイされました。heroku open
と入力してブラウザで開きます。
タスクリスト機能の追加
ユーザーがタスクを表示、追加、削除できるタスクリストをアプリに追加しましょう。そのためには、次の手順に従う必要があります。
- データベースをセットアップする
- タスクを保存および管理するためのテーブルを作成する
- ビューとコントローラーロジックを作成する
データベースをセットアップする
Laravel でデータベースを設定する前に、データベースを作成する必要があります。Heroku では、次のようにして、無料の開発用データベースをアプリに追加できます。
$ heroku addons:create heroku-postgresql:hobby-dev
アプリ用の PostgreSQL データベースが作成され、その URL を含む DATABASE_URL
環境設定が追加されます。このデータベースを使用するには、config/database.php
で次のように設定します。
<?php
$dbopts = parse_url(env('DATABASE_URL'));
return [
// ...
'connections' => [
// ...
'pgsql' => [
'driver' => 'pgsql',
'host' => $dbopts['host'],
'port' => $dbopts['port'],
'database' => ltrim($dbopts["path"],'/'),
'username' => $dbopts['user'],
'password' => $dbopts['pass'],
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
'sslmode' => 'prefer',
],
// ...
次のように DB_CONNECTION
環境設定を指定して、Heroku でのアプリ実行時にこの pgsql
接続が使用されるようにします。
$ heroku config:set DB_CONNECTION=pgsql
(必要に応じて) アプリをローカルでテストする場合は SQLite の使用をお勧めします。その場合、php-sqlite
がインストールされていることを確認し、config/database.php
で SQLite 接続を設定します。
'sqlite' => [
'driver' => 'sqlite',
'database' => database_path('database.sqlite'),
'prefix' => '',
],
この接続をローカルで使用するには、アプリの .env
ファイルで DB_CONNECTION=sqlite
のように設定します。
存在しないパラメータから pgsql をセットアップすることに抵抗がある場合は、次のようにダミーのデータベース URL を .env
に追加することもできます:
DATABASE_URL=postgres://u:p@localhost:5432/dummy-db
。
次のようにコミットして、これまでの変更を保存します。
$ git commit -am 'Configure DB connections'
タスクテーブルを作成する
空のデータベースがある今の状態から、テーブルを追加してタスクリストを表現することができます。Laravel でこれを行うには、次のように移行を作成します。
$ php artisan make:migration create_tasks_table --create=tasks
タスクには名前が必要なので、新しく作成した database/migrations/<date>_crate_tasks_table.php
ファイルで tasks
テーブルに name
を追加します。
Schema::create('tasks', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
コードから tasks
テーブルにアクセスしやすいよう、対応する Task
モデルを作成します。
$ php artisan make:model Task
空の Task
モデルが app/Task.php
に作成されます。
モデルのフィールドは、Laravel によって移行から自動的に推測されます。
ローカルで SQLite をセットアップする場合は、データベースを作成して移行を実行します (必要に応じて)。
$ touch database/database.sqlite
$ php artisan migrate --force
最後に、Heroku 上で変更をコミットして移行を実行します。
$ git add .
$ git commit -m 'Add task model'
$ git push heroku master
$ heroku run php artisan migrate --force
...
Do you really wish to run this command? (yes/no) [no]:
> y
...
タスクリストのビューを追加する
データベースに保存されているタスクを表示するために、すべてのタスクを一覧表示するビューを作成します。ボイラープレートレイアウトを使用して開始します。
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>MemCachier Laravel Tutorial</title>
<!-- Fonts -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css"
rel='stylesheet' type='text/css'>
<!-- CSS -->
<link href="{{ elixir('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<nav class="navbar navbar-default">
<!-- Navbar Contents -->
</nav>
</div>
@yield('content')
<!-- JavaScripts -->
<script src="{{ elixir('js/app.js') }}"></script>
</body>
</html>
上記のレイアウトの子ビューとしてタスクリストビューを作成できます。
<!-- resources/views/tasks.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container">
<!-- TODO: New Task Card -->
<!-- Current Tasks -->
@if (count($tasks) > 0)
<div class="card">
<div class="card-body">
<h5 class="card-title">Current Tasks</h5>
<table class="table table-striped">
@foreach ($tasks as $task)
<tr>
<td class="table-text">
<div>{{ $task->name }}</div>
</td>
<td>
<!-- TODO Delete Button -->
</td>
</tr>
@endforeach
</table>
</div>
</div>
@endif
<!-- TODO: Memcache Stats Card -->
</div>
@endsection
TODOs
は今は無視してください (後で入力します)。routes/web.php
の一番上のルートにこのビューを接続して、このビューにアクセスできるようにします。
<?php
use App\Task;
// Show Tasks
Route::get('/', function () {
$tasks = Task::orderBy('created_at', 'asc')->get();
return view('tasks', [
'tasks' => $tasks
]);
});
ローカルでセットアップしている場合、php artisan serve
で Web サーバーを起動し、localhost:8000
でビューにアクセスできます。ただし、タスクリストは空なので、特に見るべきものはまだありません。
タスクの作成を有効にする
タスクリストをより便利にするには、ユーザーがタスクを追加できる必要があります。そのためのカードを作成します。
<!-- resources/views/tasks.blade.php -->
<!-- ... -->
<!-- New Task Card -->
<div class="card">
<div class="card-body">
<h5 class="card-title">New Task</h5>
<!-- Display Validation Errors -->
@include('common.errors')
<!-- New Task Form -->
<form action="{{ url('task') }}" method="POST">
{{ csrf_field() }}
<!-- Task Name -->
<div class="form-group">
<input type="text" name="name" id="task-name" class="form-control"
placeholder="Task Name">
</div>
<!-- Add Task Button -->
<div class="form-group">
<button type="submit" class="btn btn-default">
<i class="fa fa-plus"></i> Add Task
</button>
</div>
</form>
</div>
</div>
<!-- Current Tasks -->
<!-- ... -->
タスク名はユーザーが入力するので、入力の有効性を確認する必要があります。この場合、名前が存在して 255 文字を超えないことが必要です。このルールで入力の検証に失敗した場合、次のエラービューを表示します。
<!-- resources/views/common/errors.blade.php -->
@if (count($errors) > 0)
<div class="alert alert-danger">
<strong>Whoops! Something went wrong!</strong>
<br><br>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
これらの新しいビューを routes/web.php
に追加します。
// ...
use Illuminate\Http\Request;
// Show Tasks
// ...
// Add New Task
Route::post('/task', function (Request $request) {
// Validate input
$validator = Validator::make($request->all(), [
'name' => 'required|max:255',
]);
if ($validator->fails()) {
return redirect('/')
->withInput()
->withErrors($validator);
}
// Create task
$task = new Task;
$task->name = $request->name;
$task->save();
return redirect('/');
});
php artisan serve
でローカル Web サーバーを起動して localhost:8000
にアクセスすると、タスクを追加できるようになっており、興味深さが増しています。
タスクの削除を有効にする
完成したタスクリストでは、完了したタスクを削除できる必要もあります。タスクを削除するには、タスクリストの各項目に 「Delete」 (削除) ボタンを追加します。
<!-- resources/views/tasks.blade.php -->
<!-- ... -->
<!-- Delete Button -->
<form action="{{ url('task/'.$task->id) }}" method="POST">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button type="submit" class="btn btn-danger">
<i class="fa fa-trash"></i> Delete
</button>
</form>
<!-- ... -->
次に、この機能を routes/web.php
の適切なルートに接続します。
// ...
// Show Tasks & Add New Task
// ...
// Delete Task
Route::delete('/task/{task}', function (Task $task) {
$task->delete();
return redirect('/');
});
変更を Heroku にプッシュして結果を確認できるようになりました。
$ git add .
$ git commit -m 'Add task view'
$ git push heroku master
$ heroku open
タスクリストが Heroku 上で動作するようになりました。ここまで完了したら、Memcache を使用してタスクリストのパフォーマンスを向上させる方法を学ぶことができます。
キャッシングを Laravel に追加する
Memcache はインメモリの分散キャッシュです。そのプライマリ API は、SET(key, value)
と GET(key)
の 2 つの操作で構成されます。
Memcache は、複数のサーバーに分散していますが、操作は一定の時間に実行されるハッシュマップ (または辞書) のようなものです。
Memcache の最も一般的な用途は、コストの高いデータベースクエリや HTML レンダリングをキャッシュし、これらの高コスト操作を繰り返す必要をなくすことです。
Memcache のセットアップ
Laravel で Memcache を使用するには、まず実際の Memcache キャッシュをプロビジョニングする必要があります。これは、MemCachier アドオンから無料で簡単に入手できます。
$ heroku addons:create memcachier:dev
ローカルマシンで Memcache を使用するには、以下の手順を完了する必要もあります。
- OS のパッケージマネージャを使用して
php-memcached
PECL 拡張機能をインストールします。 /etc/php/conf.d/memcached.ini
で、;extension=memcached.so
のコメントを解除します。php -m
を実行して、memcached
モジュールがロードされたことを確認します。
(Heroku では、この依存関係はすでにインストールおよび設定されています。)
Laravel で Memcache をセットアップするには、次の依存関係を composer.json
に追加します。
$ composer require ext-memcached
次に、config/cache.php
でキャッシュを設定します。
'memcached' => [
'driver' => 'memcached',
'persistent_id' => 'memcached_pool_id',
'sasl' => [
env('MEMCACHIER_USERNAME'),
env('MEMCACHIER_PASSWORD'),
],
'options' => [
// some nicer default options
// - nicer TCP options
Memcached::OPT_TCP_NODELAY => TRUE,
Memcached::OPT_NO_BLOCK => FALSE,
// - timeouts
Memcached::OPT_CONNECT_TIMEOUT => 2000, // ms
Memcached::OPT_POLL_TIMEOUT => 2000, // ms
Memcached::OPT_RECV_TIMEOUT => 750 * 1000, // us
Memcached::OPT_SEND_TIMEOUT => 750 * 1000, // us
// - better failover
Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT,
Memcached::OPT_LIBKETAMA_COMPATIBLE => TRUE,
Memcached::OPT_RETRY_TIMEOUT => 2,
Memcached::OPT_SERVER_FAILURE_LIMIT => 1,
Memcached::OPT_AUTO_EJECT_HOSTS => TRUE,
],
'servers' => array_map(function($s) {
$parts = explode(":", $s);
return [
'host' => $parts[0],
'port' => $parts[1],
'weight' => 100,
];
}, explode(",", env('MEMCACHIER_SERVERS', 'localhost:11211')))
],
Laravel でそのキャッシュとして Memcache を使用するには、CACHE_DRIVER
環境設定も指定する必要があります。
$ heroku config:set CACHE_DRIVER=memcached
コストの高いデータベースクエリをキャッシュする
Memcache を使用して、コストの高いデータベースクエリの結果をキャッシュすることはよくあります。もちろん、この単純なタスクリストにはコストの高いクエリはありませんが、このチュートリアルの目的上、すべてのタスクをデータベースから取得するのは低速な処理であると仮定します。
Laravel にキャッシングを簡単に追加するには、rememberForever
関数を使用します。次の 2 つの引数を指定します。
- キャッシュキー
- データベースをクエリして結果を返す関数
rememberForever
関数はキャッシュ内のキーを検索します。キーが存在する場合、それに対応する値が返されます。存在しない場合、引数で指定したデータベース関数が呼び出されます。その関数によって返された値は、どんな値であっても、対応するキーと共に、将来の検索のためにキャッシュに保存されます。
つまり、最初に rememberForever
を呼び出したときはコストの高いデータベース関数が呼び出されますが、2 回目以降は rememberForever
を呼び出すたびにキャッシュから値が取得されます。
routes/web.php
のタスクビューコントローラーにキャッシングを簡単に追加するには、rememberForever
関数を使用します。
// Show Tasks
Route::get('/', function () {
$tasks = Cache::rememberForever('all_tasks', function () {
return Task::orderBy('created_at', 'asc')->get();
});
return view('tasks', [
'tasks' => $tasks
]);
});
お気付きかもしれませんが、タスクを追加または削除する場合、問題があります。
rememberForever
はタスクリストをキャッシュから取得するので、データベースへの変更がタスクリストに一切反映されません。したがって、データベースでタスクを変更するたびにキャッシュを無効化する必要があります。
// Add New Task
Route::post('/task', function (Request $request) {
// ...
$task->save();
Cache::forget('all_tasks');
return redirect('/');
});
// Delete Task
Route::delete('/task/{task}', function (Task $task) {
$task->delete();
Cache::forget('all_tasks');
return redirect('/');
});
Memcache の統計を表示する
Memcache のキャッシング操作を理解するために、内部で行われている処理を視覚化することができます。
まず、routes/web.php
でタスクリストがリクエストされるたびに統計を取得します。
Route::get('/', function () {
// ...
$stats = Cache::getMemcached()->getStats();
return view('tasks', [
'tasks' => $tasks,
'stats' => array_pop($stats)
]);
});
次に、統計のカードをタスクビューの一番下に追加します。
<!-- resources/views/tasks.blade.php -->
<!-- ... -->
<!-- Stats Card -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Stats</h5>
<table class="table table-striped">
<tr>
<td>Set commands</td>
<td>{{ $stats['cmd_set'] }}</td>
</tr>
<tr>
<td>Get hits</td>
<td>{{ $stats['get_hits'] }}</td>
</tr>
<tr>
<td>Get misses</td>
<td>{{ $stats['get_misses'] }}</td>
</tr>
</table>
</div>
</div>
変更を Heroku にプッシュして、タスクリストを操作したら統計がどのように変化するかを確認します。
$ git commit -am 'Add caching with MemCachier'
$ git push heroku master
$ heroku open
ページに最初にアクセスしたとき、Get misses
が 1 増加することがわかります。これは、最初に rememberForever
が呼び出されるときは、タスクリストがキャッシュにないからです。タスクリストがキャッシュに保存されるので、Set commands
も増加します。ページを更新した場合、タスクリストはキャッシュから取得されるので、ミスは同じままですが Get hits
は増加します。
タスクを新しく追加またはタスクを削除すると、キャッシュが無効化されるためミスが再び増加します。
ローカルセットアップで統計を表示する場合、.env
ファイルで CACHE_DRIVER=memcached
を設定し、memcached
サーバーをローカルで実行するか MEMCACHIER_*
環境変数を適宜に設定する必要があります。
セッションストレージでの Memcache の使用
Heroku では、再起動時に内容が失われる一時的なファイルシステムが dyno に備わっているため、セッション情報をディスクに保存することは推奨されていません。
Memcache は、タイムアウトがある短命セッションの情報を保存するのには適しています。しかし、Memcache はあくまでキャッシュであって永続的ではないため、寿命の長いセッションについては、データベースなどの永続的なストレージオプションの方が適しています。
SESSION_DRIVER
環境設定を指定すると、セッションストアをファイル (デフォルト) から memcached に簡単に変更できます。
$ heroku config:set SESSION_DRIVER=memcached
$ heroku restart
レンダリングされたパーシャルのキャッシュ
laravel-partialcache を利用すると、レンダリングされたパーシャルを Laravel でキャッシュできます。これは、Ruby on Rails でのフラグメントキャッシングに似ています。HTML のレンダリングは CPU 使用率の高いタスクになることがあるため、アプリケーションに複雑なパーシャルがある場合にそれらをキャッシュするのは良い考えです。
CSRF トークンを使用するフォームが含まれたパーシャルはキャッシュしないでください。
今回の例には複雑なパーシャルは含まれていませんが、このチュートリアルの目的上、タスクリストにタスク名をレンダリングする処理に多くの CPU サイクルが必要でありページの速度が低下すると仮定します。
まず、laravel-partialcache
パッケージをアプリに追加する必要があります。
$ composer require spatie/laravel-partialcache
次に、タスク名をパーシャルに展開します。
<!-- resources/views/task/name.blade.php -->
<td class="table-text">
<div>{{ $task->name }}</div>
</td>
このパーシャルをタスクビューにインポートおよびキャッシュできるようになりました。
<!-- resources/views/tasks.blade.php -->
<!-- ... -->
<table class="table table-striped">
@foreach ($tasks as $task)
<tr>
<!-- Task Name -->
@cache('task.name', ['task' => $task], null, $task->id)
<!-- Delete Button -->
<!-- ... -->
これにより、個々のタスク名パーシャルが、そのキーである ID と共にキャッシュされます。タスクの名前は決して変わることがないため、この例では、キャッシュされたパーシャルを無効化する必要がないことに注意してください。ただし、タスクの名前を変更する機能を追加した場合は、キャッシュされたパーシャルを PartialCache::forget('task.name', $task->id);
で簡単に無効化できます。
アプリケーションでタスク名パーシャルをキャッシュした効果を見てみましょう。
$ git add .
$ git commit -m 'Cache task name partial'
$ git push heroku master
$ heroku open
タスクごとに追加の Get hit
がリストに表示されるようなります。
応答全体のキャッシング
Laravel では、レンダリングされる HTML 応答全体も、laravel-responsecache を使用して簡単にキャッシュできます。これは、Ruby on Rails でのビューキャッシングに似ています。このパッケージは使いやすく、README に詳しい解説があります。ただし、今回の例では、CSRF トークンを使用するフォームがタスクリストに含まれているため、この手法は使用できません。このパッケージを Memcache で使用するには、環境設定 RESPONSE_CACHE_DRIVER
を memcached
に設定する必要があります。