Heroku での PHP セッション処理
最終更新日 2023年03月16日(木)
Table of Contents
HTTP はステートレスプロトコルですが、ほとんどのアプリケーションでは、リクエスト間で特定の情報 (ログイン状態やショッピングカートの内容など) を保持する必要があります。
セッションがその解決策であり、情報はクライアント側の暗号化された HTTP Cookie またはサーバー側のストレージのどちらにも保存できます (HTTP Cookie はセッション ID のみを保持するため、サーバーはクライアントを識別できます)。
どちらのアプローチにも長所と短所がありますが、サーバー側のセッションに関しては、いくつかの設定が必要です。この記事では、Heroku 上の PHP アプリケーションでセッションを確実に処理する方法を示します。
セッションの必要性
アプリケーションが複数のサーバー (または、Heroku の場合のように dyno) 上で実行され、dyno がロードバランサーからランダムにトラフィックを受信する場合は常に、標準のファイルベースのセッションを使用しないように PHP のセッションサポートを設定する必要があります。
セッションが各 dyno のファイルシステムに保存される場合、ユーザー (この例では、このユーザーは以前にログインしたことがあると想定され、リクエストにはセッション Cookie が含まれる) からの次のリクエストは、セッション情報が存在しない別の dyno で終了する可能性があります。
これまでの一般的な解決策は、ロードバランサーが常にユーザーを同じバックエンドサーバーに送ることを保証するという “スティッキーセッション” の概念でした。このアプローチはさまざまな理由で問題がありますが、中でも、バックエンドサーバーで障害が発生した場合などのスケーラビリティと耐久性に悪影響を及ぼします。
幸いにも PHP では、水平方向のスケーラビリティ (より大規模なサーバーを使用して垂直方向にスケーリングする代わりに、追加のサーバーを使用してスケールアウトすること) が常に中心的な考え方であったため、分散セッションのための無数のオプションを PHP で、またすべての一般的なフレームワークでネイティブに利用できます。
セッションのストレージオプション
セッションは通常、Memcache や Redis などの分散データストアまたはスケーラブルなデータストア、あるいは PostgreSQL や MySQL などのリレーショナルデータベースに保存するのが適しています。
memcached
や redis
などの一般的な PHP 拡張機能では、簡単に使えるネイティブのセッションハンドラーが提供されており、predis Redis ライブラリなどの userland ライブラリでも、セッションハンドラーを (多くの場合は例)と共に) 提供することがよくあります。
次の例では、Heroku アドオンを使用してセッションを Memcached に保存します。
Memcached へのセッションの保存
アプリケーションのセットアップ
Git リポジトリと Heroku アプリケーションをまだ作成していない場合、最初に作成します。
$ mkdir heroku-session-example
$ cd heroku-session-example
$ git init -b main
$ heroku create
memcached
拡張機能の有効化
まず、memcached
PHP 拡張機能をアプリケーションの要件としてリスト化し、プッシュ時にアクティブ化することを Heroku に知らせる必要があります。
(開発とテストのために拡張機能をローカルにインストールしてある場合の) 最も簡単な方法は、composer require ext-memcached:*
を実行して拡張機能への依存関係を宣言することです。
または、依存関係を手動で composer.json
に追加することができます。どちらの場合も、composer.json
は次のようになります (プロジェクトに他の依存関係がないことが前提です)。
{
"require": {
"ext-memcached": "*"
}
}
次に、composer.lock
を更新し、この変更を Git リポジトリにコミットします。
$ composer update
$ git add composer.json composer.lock
$ git commit -m "require memcached ext"
Memcache アドオンのプロビジョニング
次に、Heroku アドオンマーケットプレイスから Memcached アドオンを追加します。この例では MemCachier を使用しています。Memcached Cloud の場合、環境変数名はもちろん異なりますが、それを除けば手順は同様です。
$ heroku addons:create memcachier
memcached をセッションに使用するように PHP を設定する
memcached
セッションハンドラーを使用するように PHP を設定するには、アプリケーションのルート (または、カスタムドキュメントルートを使用している場合はそのディレクトリ) にある .user.ini
ファイルでカスタム PHP 設定を定義するだけです。ファイルの内容は次のようになります。
session.save_handler=memcached
session.save_path="${MEMCACHIER_SERVERS}"
memcached.sess_binary=1 # for ext-memcached 2 / PHP 5
memcached.sess_binary_protocol=1 # for ext-memcached 3 / PHP 7
memcached.sess_sasl_username="${MEMCACHIER_USERNAME}"
memcached.sess_sasl_password="${MEMCACHIER_PASSWORD}"
これらの設定では、使用するサーバーを設定し、(認証を使用できるように) 接続でバイナリプロトコルをオンに切り替えて、SASL 認証の詳細を設定します。
このアプローチでは、環境変数を参照することを可能にする php.ini
の機能を利用しています。Heroku の環境設定はすべてアプリケーションの環境に公開されており、Memcachier アドオンをプロビジョニングしたときに、上記で参照している 3 つの MEMCACHIER_...
変数を含むいくつかの環境設定が自動的に設定されています。
MemCachier アドオンではなく Memcached Cloud を使用している場合は、対応する MEMCACHEDCLOUD_...
環境変数名を代わりに使用してください。
SASL 認証では接続に一定のオーバーヘッドが発生するため、セッション接続を永続化することをお勧めします。PHP 5 (および ext-memcached
2.x) を使用している場合、セッション保存パスのプレフィックスを使用してこれを行うことができます。
session.save_path="PERSISTENT=myapp_session ${MEMCACHIER_SERVERS}"
PHP 7 および ext-memcached
バージョン 3 の場合、代わりに明示的な設定を使用してプロトコルを有効にします。
memcached.sess_persistent=On
.user.ini
の代わりに、いくつかの ini_set()
呼び出しをコードで使用できます。
ini_set('session.save_handler', 'memcached');
ini_set('session.save_path', getenv('MEMCACHIER_SERVERS'));
if(version_compare(phpversion('memcached'), '3', '>=')) {
ini_set('memcached.sess_persistent', 1);
ini_set('memcached.sess_binary_protocol', 1);
} else {
ini_set('session.save_path', 'PERSISTENT=myapp_session ' . ini_get('session.save_path'));
ini_set('memcached.sess_binary', 1);
}
ini_set('memcached.sess_sasl_username', getenv('MEMCACHIER_USERNAME'));
ini_set('memcached.sess_sasl_password', getenv('MEMCACHIER_PASSWORD'));
準備ができたら、変更を追加およびプッシュします。
$ git add .user.ini # or the PHP file you added the ini_set() calls to
$ git commit -m "session settings for memcached"
セッションのテスト
コードでセッションを簡単にテストできるようになりました。たとえば、テスト用のクイック index.php
で次のようにします (上記の .user.ini
アプローチを使用していることが前提。そうでない場合は ini_set()
呼び出しを挿入する)。
<?php
// any ini_set() for session configuration goes here when not using .user.ini
session_start();
if (!isset($_SESSION['count'])) {
$_SESSION['count'] = 0;
}
$_SESSION['count']++;
echo "Hello #" . $_SESSION['count'];
Heroku に追加、コミット、およびプッシュする準備ができました。
$ git add index.php
$ git commit -m "session test";
$ git push heroku main
heroku open
を実行するかブラウザでアプリケーションに手動でアクセスして、"Hello #1" のような挨拶を確認し、ページを更新するたびにカウントが増え続ける様子を観察します。別のブラウザを使用するかキャッシュをクリアすると、カウントは 1 にリセットされます。
eco
および basic
の dyno タイプでは、プロセスタイプあたり最大 1 つの dyno の実行しかサポートされません。スケーリングするには、professional dyno タイプを使用する必要があります。
ここで dyno の数をスケールアップして、リクエストがどの dyno にヒットしてもセッションデータが dyno 間で必ず共有されるようにすることができます。
$ heroku ps:scale web=5
新しい dyno が起動するまで数秒待ってから、ページを何回か更新して、カウントが 1 に戻らない様子を観察します。興味がある場合は、heroku logs
(または heroku logs --tail
。再び終了するには Ctrl-C
を使用) を実行して、(web.1
~ web.5
の) 異なる dyno インスタンスでリクエストがどのように処理されるかを確認してください。
5 つの Web dyno は今は必要でないため、1 つにスケールバックしてください。
$ heroku ps:scale web=1