こんにちは!今回は、PHPの標準関数であるsession_gc()について詳しく解説していきます。期限切れのセッションデータを削除できる、セッション管理の最適化に重要な関数です!
session_gc関数とは?
session_gc()関数は、期限切れのセッションデータをガベージコレクション(削除)する関数です。
通常はPHPが自動的に実行しますが、この関数を使うことで手動でセッションストレージをクリーンアップできます。PHP 7.1.0以降で使用可能です!
基本的な構文
session_gc(): int|false
- 引数: なし
- 戻り値: 削除されたセッション数、失敗時は
false
重要な注意点
// PHP 7.1.0以降で使用可能
if (function_exists('session_gc')) {
$deleted = session_gc();
echo "削除されたセッション: {$deleted}件\n";
} else {
echo "session_gc()は使用できません\n";
}
// session.gc_maxlifetimeに基づいて削除
// デフォルトは1440秒(24分)
echo "GC最大有効期限: " . ini_get('session.gc_maxlifetime') . "秒\n";
// セッションが開始されている必要はない
// session_gc()はセッション開始前でも実行可能
$deleted = session_gc();
// 自動ガベージコレクションの設定
// session.gc_probability / session.gc_divisor = 実行確率
echo "GC確率: " . ini_get('session.gc_probability') . "/" . ini_get('session.gc_divisor') . "\n";
// デフォルト: 1/100 = 1%の確率で実行
// ファイルベースセッションでのみ動作
// カスタムセッションハンドラーでは動作が異なる
ガベージコレクションの仕組み
// セッションの自動ガベージコレクション
// 1. session.gc_maxlifetime (デフォルト1440秒)
// この時間を超えたセッションファイルが削除対象
ini_set('session.gc_maxlifetime', 3600); // 1時間
// 2. session.gc_probability (デフォルト1)
// ガベージコレクション実行の確率(分子)
ini_set('session.gc_probability', 1);
// 3. session.gc_divisor (デフォルト100)
// ガベージコレクション実行の確率(分母)
ini_set('session.gc_divisor', 100);
// 実行確率 = gc_probability / gc_divisor
// デフォルト = 1/100 = 1%
// session_start()時に確率的に実行される
session_start(); // 1%の確率でGCが実行される
// 手動で確実に実行
$deleted = session_gc();
echo "削除: {$deleted}件\n";
基本的な使用例
シンプルなガベージコレクション
// 期限切れセッションを削除
echo "ガベージコレクション実行前\n";
// 削除を実行
$deleted = session_gc();
echo "削除されたセッション: {$deleted}件\n";
GC設定の確認と変更
// 現在のGC設定を確認
echo "=== GC設定 ===\n";
echo "gc_maxlifetime: " . ini_get('session.gc_maxlifetime') . "秒\n";
echo "gc_probability: " . ini_get('session.gc_probability') . "\n";
echo "gc_divisor: " . ini_get('session.gc_divisor') . "\n";
echo "実行確率: " . (ini_get('session.gc_probability') / ini_get('session.gc_divisor') * 100) . "%\n";
// GC設定を変更
ini_set('session.gc_maxlifetime', 7200); // 2時間
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 10); // 10%の確率
echo "\n=== 変更後 ===\n";
echo "gc_maxlifetime: " . ini_get('session.gc_maxlifetime') . "秒\n";
echo "実行確率: " . (ini_get('session.gc_probability') / ini_get('session.gc_divisor') * 100) . "%\n";
// GCを実行
$deleted = session_gc();
echo "\n削除されたセッション: {$deleted}件\n";
セッションファイルの確認
// セッションファイルの状態を確認
$sessionPath = session_save_path();
if (empty($sessionPath)) {
$sessionPath = sys_get_temp_dir();
}
echo "セッションパス: {$sessionPath}\n";
// セッションファイル数をカウント
$files = glob($sessionPath . '/sess_*');
echo "GC実行前のセッションファイル数: " . count($files) . "\n";
// ガベージコレクション実行
$deleted = session_gc();
echo "削除されたセッション: {$deleted}件\n";
// 実行後のファイル数
$files = glob($sessionPath . '/sess_*');
echo "GC実行後のセッションファイル数: " . count($files) . "\n";
定期的なクリーンアップ
// 毎回確実にGCを実行したい場合
function ensureSessionCleanup() {
// GC確率を100%に設定
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 1);
session_start();
// さらに手動でも実行
$deleted = session_gc();
return $deleted;
}
$deleted = ensureSessionCleanup();
echo "削除されたセッション: {$deleted}件\n";
実践的な使用例
例1: セッションクリーンアップシステム
class SessionCleanupManager {
private $gcMaxLifetime;
private $sessionPath;
/**
* クリーンアップマネージャーを初期化
*/
public function __construct($gcMaxLifetime = null) {
$this->gcMaxLifetime = $gcMaxLifetime ?? ini_get('session.gc_maxlifetime');
$this->sessionPath = session_save_path();
if (empty($this->sessionPath)) {
$this->sessionPath = sys_get_temp_dir();
}
}
/**
* ガベージコレクションを実行
*/
public function runGarbageCollection() {
// 実行前の状態を記録
$beforeStats = $this->getSessionStats();
// GC実行
$startTime = microtime(true);
$deleted = session_gc();
$duration = microtime(true) - $startTime;
// 実行後の状態を記録
$afterStats = $this->getSessionStats();
return [
'deleted_count' => $deleted,
'duration' => $duration,
'before' => $beforeStats,
'after' => $afterStats,
'freed_space' => $beforeStats['total_size'] - $afterStats['total_size']
];
}
/**
* セッション統計情報を取得
*/
public function getSessionStats() {
$files = glob($this->sessionPath . '/sess_*');
$totalSize = 0;
$oldestTime = time();
$newestTime = 0;
$expiredCount = 0;
$cutoff = time() - $this->gcMaxLifetime;
foreach ($files as $file) {
$size = filesize($file);
$mtime = filemtime($file);
$totalSize += $size;
if ($mtime < $oldestTime) {
$oldestTime = $mtime;
}
if ($mtime > $newestTime) {
$newestTime = $mtime;
}
if ($mtime < $cutoff) {
$expiredCount++;
}
}
return [
'total_sessions' => count($files),
'expired_sessions' => $expiredCount,
'active_sessions' => count($files) - $expiredCount,
'total_size' => $totalSize,
'total_size_mb' => round($totalSize / 1024 / 1024, 2),
'oldest_session_age' => time() - $oldestTime,
'newest_session_age' => time() - $newestTime,
'gc_maxlifetime' => $this->gcMaxLifetime
];
}
/**
* クリーンアップレポートを生成
*/
public function generateCleanupReport() {
$stats = $this->getSessionStats();
$report = "=== Session Cleanup Report ===\n";
$report .= "Total Sessions: {$stats['total_sessions']}\n";
$report .= "Active Sessions: {$stats['active_sessions']}\n";
$report .= "Expired Sessions: {$stats['expired_sessions']}\n";
$report .= "Total Size: {$stats['total_size_mb']} MB\n";
$report .= "GC Max Lifetime: {$stats['gc_maxlifetime']} seconds\n";
$report .= "Oldest Session Age: {$stats['oldest_session_age']} seconds\n";
return $report;
}
/**
* 条件付きクリーンアップ
*/
public function cleanupIfNeeded($threshold = 100) {
$stats = $this->getSessionStats();
if ($stats['expired_sessions'] >= $threshold) {
return $this->runGarbageCollection();
}
return [
'cleaned' => false,
'reason' => 'Threshold not reached',
'expired_sessions' => $stats['expired_sessions'],
'threshold' => $threshold
];
}
/**
* スケジュールされたクリーンアップ
*/
public function scheduledCleanup($interval = 3600) {
$lastRunFile = $this->sessionPath . '/.last_gc_run';
// 最後の実行時刻を確認
if (file_exists($lastRunFile)) {
$lastRun = (int)file_get_contents($lastRunFile);
$timeSinceLastRun = time() - $lastRun;
if ($timeSinceLastRun < $interval) {
return [
'cleaned' => false,
'reason' => 'Too soon since last run',
'time_since_last_run' => $timeSinceLastRun,
'interval' => $interval
];
}
}
// クリーンアップ実行
$result = $this->runGarbageCollection();
// 実行時刻を記録
file_put_contents($lastRunFile, time());
$result['scheduled'] = true;
return $result;
}
/**
* アグレッシブクリーンアップ(短い有効期限で実行)
*/
public function aggressiveCleanup($customLifetime = 600) {
// 一時的に有効期限を変更
$originalLifetime = ini_get('session.gc_maxlifetime');
ini_set('session.gc_maxlifetime', $customLifetime);
// GC実行
$deleted = session_gc();
// 元に戻す
ini_set('session.gc_maxlifetime', $originalLifetime);
return [
'deleted_count' => $deleted,
'custom_lifetime' => $customLifetime,
'original_lifetime' => $originalLifetime
];
}
/**
* クリーンアップ履歴を記録
*/
public function logCleanup($result) {
$logFile = $this->sessionPath . '/gc_log.json';
$logs = [];
if (file_exists($logFile)) {
$logs = json_decode(file_get_contents($logFile), true) ?? [];
}
$logs[] = [
'timestamp' => time(),
'deleted_count' => $result['deleted_count'],
'duration' => $result['duration'] ?? 0,
'freed_space' => $result['freed_space'] ?? 0
];
// 最新100件のみ保持
if (count($logs) > 100) {
$logs = array_slice($logs, -100);
}
file_put_contents($logFile, json_encode($logs));
}
/**
* クリーンアップ統計を取得
*/
public function getCleanupStats() {
$logFile = $this->sessionPath . '/gc_log.json';
if (!file_exists($logFile)) {
return ['error' => 'No cleanup logs available'];
}
$logs = json_decode(file_get_contents($logFile), true);
$totalDeleted = 0;
$totalDuration = 0;
$totalFreed = 0;
foreach ($logs as $log) {
$totalDeleted += $log['deleted_count'];
$totalDuration += $log['duration'] ?? 0;
$totalFreed += $log['freed_space'] ?? 0;
}
return [
'total_runs' => count($logs),
'total_deleted' => $totalDeleted,
'average_deleted' => round($totalDeleted / count($logs), 2),
'average_duration' => round($totalDuration / count($logs), 4),
'total_freed_mb' => round($totalFreed / 1024 / 1024, 2),
'last_run' => $logs[count($logs) - 1]['timestamp'] ?? null
];
}
}
// 使用例
echo "=== セッションクリーンアップシステム ===\n";
$cleanup = new SessionCleanupManager();
// 現在の統計
echo $cleanup->generateCleanupReport();
// ガベージコレクション実行
echo "\n=== GC実行 ===\n";
$result = $cleanup->runGarbageCollection();
echo "削除されたセッション: {$result['deleted_count']}件\n";
echo "実行時間: " . round($result['duration'], 4) . "秒\n";
echo "解放された容量: " . round($result['freed_space'] / 1024, 2) . " KB\n";
// ログに記録
$cleanup->logCleanup($result);
// 条件付きクリーンアップ
echo "\n=== 条件付きクリーンアップ ===\n";
$conditional = $cleanup->cleanupIfNeeded(5);
if ($conditional['cleaned']) {
echo "クリーンアップ実行: {$conditional['deleted_count']}件削除\n";
} else {
echo "クリーンアップ不要: {$conditional['reason']}\n";
echo "期限切れセッション: {$conditional['expired_sessions']}件\n";
}
// スケジュールされたクリーンアップ
echo "\n=== スケジュールクリーンアップ ===\n";
$scheduled = $cleanup->scheduledCleanup(60);
if ($scheduled['cleaned']) {
echo "実行: {$scheduled['deleted_count']}件削除\n";
} else {
echo "スキップ: {$scheduled['reason']}\n";
}
// クリーンアップ統計
echo "\n=== クリーンアップ統計 ===\n";
$stats = $cleanup->getCleanupStats();
if (!isset($stats['error'])) {
echo "総実行回数: {$stats['total_runs']}\n";
echo "総削除数: {$stats['total_deleted']}\n";
echo "平均削除数: {$stats['average_deleted']}\n";
echo "解放容量合計: {$stats['total_freed_mb']} MB\n";
}
例2: カスタムガベージコレクター
class CustomGarbageCollector {
private $rules = [];
/**
* カスタムルールを追加
*/
public function addRule($name, $callback) {
$this->rules[$name] = $callback;
}
/**
* カスタムガベージコレクションを実行
*/
public function collect() {
$sessionPath = session_save_path();
if (empty($sessionPath)) {
$sessionPath = sys_get_temp_dir();
}
$files = glob($sessionPath . '/sess_*');
$deleted = 0;
$results = [];
foreach ($files as $file) {
$sessionId = substr(basename($file), 5); // 'sess_'を除去
// セッションデータを読み込み
$data = file_get_contents($file);
$mtime = filemtime($file);
// 各ルールを適用
foreach ($this->rules as $name => $callback) {
if ($callback($file, $data, $mtime, $sessionId)) {
if (unlink($file)) {
$deleted++;
$results[] = [
'session_id' => $sessionId,
'rule' => $name,
'deleted' => true
];
}
break; // 削除したらループを抜ける
}
}
}
return [
'deleted_count' => $deleted,
'details' => $results
];
}
/**
* 標準的なルールセットを適用
*/
public function applyStandardRules() {
// ルール1: 古いセッション(1時間以上)
$this->addRule('old_sessions', function($file, $data, $mtime, $sessionId) {
return (time() - $mtime) > 3600;
});
// ルール2: 空のセッション
$this->addRule('empty_sessions', function($file, $data, $mtime, $sessionId) {
return empty($data) || strlen($data) < 10;
});
// ルール3: 巨大なセッション(1MB以上)
$this->addRule('large_sessions', function($file, $data, $mtime, $sessionId) {
return strlen($data) > 1048576;
});
}
/**
* セキュリティルールを適用
*/
public function applySecurityRules() {
// ルール: 疑わしいデータを含むセッション
$this->addRule('suspicious_data', function($file, $data, $mtime, $sessionId) {
// 例: 特定のパターンを検出
$suspicious = [
'eval(',
'base64_decode(',
'system(',
'exec('
];
foreach ($suspicious as $pattern) {
if (strpos($data, $pattern) !== false) {
return true;
}
}
return false;
});
// ルール: 異常に古いセッション(24時間以上)
$this->addRule('very_old_sessions', function($file, $data, $mtime, $sessionId) {
return (time() - $mtime) > 86400;
});
}
/**
* パフォーマンスルールを適用
*/
public function applyPerformanceRules() {
// ルール: 非アクティブなセッション(30分以上)
$this->addRule('inactive_sessions', function($file, $data, $mtime, $sessionId) {
return (time() - $mtime) > 1800;
});
// ルール: 重複セッション(同じユーザーの古いセッション)
$this->addRule('duplicate_sessions', function($file, $data, $mtime, $sessionId) {
// 簡易実装: セッションデータからuser_idを抽出して判定
// 実際の実装ではもっと複雑な判定が必要
return false; // プレースホルダー
});
}
/**
* ドライランモード(削除せずに候補を表示)
*/
public function dryRun() {
$sessionPath = session_save_path();
if (empty($sessionPath)) {
$sessionPath = sys_get_temp_dir();
}
$files = glob($sessionPath . '/sess_*');
$candidates = [];
foreach ($files as $file) {
$sessionId = substr(basename($file), 5);
$data = file_get_contents($file);
$mtime = filemtime($file);
foreach ($this->rules as $name => $callback) {
if ($callback($file, $data, $mtime, $sessionId)) {
$candidates[] = [
'session_id' => $sessionId,
'file' => $file,
'rule' => $name,
'size' => filesize($file),
'age' => time() - $mtime
];
break;
}
}
}
return $candidates;
}
/**
* 選択的削除(特定のルールのみ)
*/
public function collectByRule($ruleName) {
if (!isset($this->rules[$ruleName])) {
throw new Exception("Rule not found: {$ruleName}");
}
$sessionPath = session_save_path();
if (empty($sessionPath)) {
$sessionPath = sys_get_temp_dir();
}
$files = glob($sessionPath . '/sess_*');
$deleted = 0;
$callback = $this->rules[$ruleName];
foreach ($files as $file) {
$sessionId = substr(basename($file), 5);
$data = file_get_contents($file);
$mtime = filemtime($file);
if ($callback($file, $data, $mtime, $sessionId)) {
if (unlink($file)) {
$deleted++;
}
}
}
return [
'rule' => $ruleName,
'deleted_count' => $deleted
];
}
}
// 使用例
echo "=== カスタムガベージコレクター ===\n";
$collector = new CustomGarbageCollector();
// 標準ルールを適用
$collector->applyStandardRules();
$collector->applySecurityRules();
// ドライラン(削除候補を確認)
echo "削除候補:\n";
$candidates = $collector->dryRun();
foreach (array_slice($candidates, 0, 5) as $candidate) {
echo " {$candidate['session_id']} - {$candidate['rule']} (age: {$candidate['age']}s, size: {$candidate['size']}bytes)\n";
}
echo "総候補数: " . count($candidates) . "\n";
// 実際に削除
echo "\nカスタムGC実行:\n";
$result = $collector->collect();
echo "削除されたセッション: {$result['deleted_count']}件\n";
// 特定のルールのみで削除
echo "\n古いセッションのみ削除:\n";
$oldResult = $collector->collectByRule('old_sessions');
echo "削除: {$oldResult['deleted_count']}件\n";
例3: セッションストレージ最適化システム
class SessionStorageOptimizer {
private $sessionPath;
private $targetSize; // 目標サイズ(バイト)
/**
* 最適化システムを初期化
*/
public function __construct($targetSize = 10485760) { // デフォルト10MB
$this->sessionPath = session_save_path();
if (empty($this->sessionPath)) {
$this->sessionPath = sys_get_temp_dir();
}
$this->targetSize = $targetSize;
}
/**
* ストレージを最適化
*/
public function optimize() {
$beforeStats = $this->getStorageStats();
if ($beforeStats['total_size'] <= $this->targetSize) {
return [
'optimized' => false,
'reason' => 'Already below target size',
'current_size' => $beforeStats['total_size'],
'target_size' => $this->targetSize
];
}
// 最適化戦略を実行
$results = [];
// 1. 標準GCを実行
$gcResult = $this->runStandardGC();
$results['standard_gc'] = $gcResult;
// まだ目標を超えている場合
$currentStats = $this->getStorageStats();
if ($currentStats['total_size'] > $this->targetSize) {
// 2. 古いセッションを積極的に削除
$aggressiveResult = $this->aggressiveCleanup();
$results['aggressive_cleanup'] = $aggressiveResult;
}
// まだ目標を超えている場合
$currentStats = $this->getStorageStats();
if ($currentStats['total_size'] > $this->targetSize) {
// 3. 最も古いセッションから順に削除
$priorityResult = $this->priorityCleanup();
$results['priority_cleanup'] = $priorityResult;
}
$afterStats = $this->getStorageStats();
return [
'optimized' => true,
'before' => $beforeStats,
'after' => $afterStats,
'freed_space' => $beforeStats['total_size'] - $afterStats['total_size'],
'results' => $results
];
}
/**
* ストレージ統計を取得
*/
private function getStorageStats() {
$files = glob($this->sessionPath . '/sess_*');
$totalSize = 0;
foreach ($files as $file) {
$totalSize += filesize($file);
}
return [
'total_sessions' => count($files),
'total_size' => $totalSize,
'total_size_mb' => round($totalSize / 1024 / 1024, 2),
'average_size' => count($files) > 0 ? round($totalSize / count($files), 2) : 0
];
}
/**
* 標準GCを実行
*/
private function runStandardGC() {
$deleted = session_gc();
return [
'method' => 'standard_gc',
'deleted' => $deleted
];
}
/**
* アグレッシブクリーンアップ
*/
private function aggressiveCleanup() {
// 有効期限を一時的に短く設定
$originalLifetime = ini_get('session.gc_maxlifetime');
ini_set('session.gc_maxlifetime', 600); // 10分
$deleted = session_gc();
ini_set('session.gc_maxlifetime', $originalLifetime);
return [
'method' => 'aggressive',
'deleted' => $deleted,
'lifetime_used' => 600
];
}
/**
* 優先度ベースのクリーンアップ
*/
private function priorityCleanup() {
$files = glob($this->sessionPath . '/sess_*');
$deleted = 0;
// ファイルを古い順にソート
usort($files, function($a, $b) {
return filemtime($a) - filemtime($b);
});
// 目標サイズに達するまで削除
$currentSize = $this->getStorageStats()['total_size'];
foreach ($files as $file) {
if ($currentSize <= $this->targetSize) {
break;
}
$fileSize = filesize($file);
if (unlink($file)) {
$deleted++;
$currentSize -= $fileSize;
}
}
return [
'method' => 'priority',
'deleted' => $deleted
];
}
/**
* 自動最適化(定期実行用)
*/
public function autoOptimize($checkInterval = 3600) {
$lastCheckFile = $this->sessionPath . '/.last_optimization';
// 最後のチェック時刻を確認
if (file_exists($lastCheckFile)) {
$lastCheck = (int)file_get_contents($lastCheckFile);
if (time() - $lastCheck < $checkInterval) {
return [
'checked' => false,
'reason' => 'Too soon since last check'
];
}
}
// 最適化を実行
$result = $this->optimize();
// チェック時刻を記録
file_put_contents($lastCheckFile, time());
$result['auto'] = true;
return $result;
}
/**
* 圧縮可能なセッションを検出
*/
public function findCompressible() {
$files = glob($this->sessionPath . '/sess_*');
$compressible = [];
foreach ($files as $file) {
$size = filesize($file);
if ($size > 1024) { // 1KB以上
$data = file_get_contents($file);
$compressed = gzcompress($data);
$compressedSize = strlen($compressed);
$ratio = $compressedSize / $size;
if ($ratio < 0.7) { // 30%以上圧縮可能
$compressible[] = [
'file' => basename($file),
'original_size' => $size,
'compressed_size' => $compressedSize,
'ratio' => round($ratio * 100, 2) . '%',
'savings' => $size - $compressedSize
];
}
}
}
return $compressible;
}
/**
* 最適化レポートを生成
*/
public function generateOptimizationReport() {
$stats = $this->getStorageStats();
$compressible = $this->findCompressible();
$totalSavings = array_sum(array_column($compressible, 'savings'));
$report = "=== Storage Optimization Report ===\n";
$report .= "Current Size: {$stats['total_size_mb']} MB\n";
$report .= "Target Size: " . round($this->targetSize / 1024 / 1024, 2) . " MB\n";
$report .= "Total Sessions: {$stats['total_sessions']}\n";
$report .= "Average Session Size: {$stats['average_size']} bytes\n";
$report .= "\nCompression Potential:\n";
$report .= "Compressible Sessions: " . count($compressible) . "\n";
$report .= "Potential Savings: " . round($totalSavings / 1024, 2) . " KB\n";
if ($stats['total_size'] > $this->targetSize) {
$report .= "\nRECOMMENDATION: Run optimization\n";
$excess = $stats['total_size'] - $this->targetSize;
$report .= "Excess Size: " . round($excess / 1024, 2) . " KB\n";
} else {
$report .= "\nSTATUS: Storage within target\n";
}
return $report;
}
}
// 使用例
echo "=== セッションストレージ最適化 ===\n";
$optimizer = new SessionStorageOptimizer(5242880); // 5MB目標
// 最適化レポート
echo $optimizer->generateOptimizationReport();
// 最適化実行
echo "\n=== 最適化実行 ===\n";
$result = $optimizer->optimize();
if ($result['optimized']) {
echo "最適化完了\n";
echo "削除前: {$result['before']['total_size_mb']} MB\n";
echo "削除後: {$result['after']['total_size_mb']} MB\n";
echo "解放容量: " . round($result['freed_space'] / 1024 / 1024, 2) . " MB\n";
} else {
echo "最適化不要: {$result['reason']}\n";
}
// 圧縮可能なセッション
echo "\n=== 圧縮可能なセッション ===\n";
$compressible = $optimizer->findCompressible();
foreach (array_slice($compressible, 0, 5) as $item) {
echo "{$item['file']}: {$item['original_size']}→{$item['compressed_size']} bytes ({$item['ratio']})\n";
}
例4: 定期クリーンアップスケジューラー
class SessionCleanupScheduler {
private $schedules = [];
private $logFile;
/**
* スケジューラーを初期化
*/
public function __construct($logFile = '/tmp/session_cleanup_schedule.log') {
$this->logFile = $logFile;
}
/**
* スケジュールを追加
*/
public function addSchedule($name, $interval, $callback) {
$this->schedules[$name] = [
'interval' => $interval,
'callback' => $callback,
'last_run' => 0
];
}
/**
* スケジュールされたタスクを実行
*/
public function run() {
$executed = [];
$currentTime = time();
foreach ($this->schedules as $name => $schedule) {
// インターバルチェック
if ($currentTime - $schedule['last_run'] >= $schedule['interval']) {
// タスクを実行
$startTime = microtime(true);
$result = call_user_func($schedule['callback']);
$duration = microtime(true) - $startTime;
// 実行時刻を更新
$this->schedules[$name]['last_run'] = $currentTime;
$executed[] = [
'name' => $name,
'result' => $result,
'duration' => $duration,
'timestamp' => $currentTime
];
// ログに記録
$this->log($name, $result, $duration);
}
}
return $executed;
}
/**
* ログに記録
*/
private function log($name, $result, $duration) {
$logEntry = sprintf(
"[%s] %s: %s (%.4fs)\n",
date('Y-m-d H:i:s'),
$name,
json_encode($result),
$duration
);
file_put_contents($this->logFile, $logEntry, FILE_APPEND);
}
/**
* 標準的なスケジュールを設定
*/
public function setupStandardSchedules() {
// 毎時間: 標準GC
$this->addSchedule('hourly_gc', 3600, function() {
return ['deleted' => session_gc()];
});
// 毎日: アグレッシブクリーンアップ
$this->addSchedule('daily_aggressive', 86400, function() {
$original = ini_get('session.gc_maxlifetime');
ini_set('session.gc_maxlifetime', 3600);
$deleted = session_gc();
ini_set('session.gc_maxlifetime', $original);
return ['deleted' => $deleted, 'lifetime' => 3600];
});
// 毎週: 完全クリーンアップ
$this->addSchedule('weekly_full', 604800, function() {
$original = ini_get('session.gc_maxlifetime');
ini_set('session.gc_maxlifetime', 0); // すべて削除
$deleted = session_gc();
ini_set('session.gc_maxlifetime', $original);
return ['deleted' => $deleted, 'type' => 'full'];
});
}
/**
* 次回実行時刻を取得
*/
public function getNextRun() {
$nextRuns = [];
$currentTime = time();
foreach ($this->schedules as $name => $schedule) {
$timeSinceLastRun = $currentTime - $schedule['last_run'];
$timeUntilNextRun = $schedule['interval'] - $timeSinceLastRun;
$nextRuns[$name] = [
'interval' => $schedule['interval'],
'last_run' => $schedule['last_run'],
'next_run' => $currentTime + $timeUntilNextRun,
'time_until' => max(0, $timeUntilNextRun)
];
}
return $nextRuns;
}
/**
* 強制実行
*/
public function forceRun($name) {
if (!isset($this->schedules[$name])) {
throw new Exception("Schedule not found: {$name}");
}
$schedule = $this->schedules[$name];
$startTime = microtime(true);
$result = call_user_func($schedule['callback']);
$duration = microtime(true) - $startTime;
$this->schedules[$name]['last_run'] = time();
$this->log($name, $result, $duration);
return [
'name' => $name,
'result' => $result,
'duration' => $duration,
'forced' => true
];
}
/**
* スケジュールステータスを取得
*/
public function getStatus() {
$status = [];
$currentTime = time();
foreach ($this->schedules as $name => $schedule) {
$status[$name] = [
'interval_hours' => $schedule['interval'] / 3600,
'last_run' => $schedule['last_run'] > 0
? date('Y-m-d H:i:s', $schedule['last_run'])
: 'Never',
'hours_since_last' => round(($currentTime - $schedule['last_run']) / 3600, 2),
'is_due' => ($currentTime - $schedule['last_run']) >= $schedule['interval']
];
}
return $status;
}
}
// 使用例
echo "=== 定期クリーンアップスケジューラー ===\n";
$scheduler = new SessionCleanupScheduler('/tmp/cleanup_schedule_test.log');
// 標準スケジュールを設定
$scheduler->setupStandardSchedules();
// カスタムスケジュールを追加
$scheduler->addSchedule('custom_5min', 300, function() {
$deleted = session_gc();
return ['type' => 'custom', 'deleted' => $deleted];
});
// ステータス確認
echo "スケジュールステータス:\n";
$status = $scheduler->getStatus();
foreach ($status as $name => $info) {
echo " {$name}:\n";
echo " 間隔: {$info['interval_hours']}時間\n";
echo " 最終実行: {$info['last_run']}\n";
echo " 実行必要: " . ($info['is_due'] ? 'Yes' : 'No') . "\n";
}
// スケジュール実行
echo "\n=== スケジュール実行 ===\n";
$executed = $scheduler->run();
foreach ($executed as $task) {
echo "{$task['name']}: 実行完了\n";
echo " 結果: " . json_encode($task['result']) . "\n";
echo " 実行時間: " . round($task['duration'], 4) . "秒\n";
}
// 次回実行時刻
echo "\n=== 次回実行予定 ===\n";
$nextRuns = $scheduler->getNextRun();
foreach ($nextRuns as $name => $info) {
echo "{$name}: " . round($info['time_until'] / 60, 2) . "分後\n";
}
// 強制実行
echo "\n=== 強制実行 ===\n";
$forced = $scheduler->forceRun('hourly_gc');
echo "{$forced['name']}: 強制実行完了\n";
echo " 削除: {$forced['result']['deleted']}件\n";
例5: セッションヘルスモニター
class SessionHealthMonitor {
private $thresholds;
private $alerts = [];
/**
* ヘルスモニターを初期化
*/
public function __construct() {
$this->thresholds = [
'max_total_sessions' => 1000,
'max_expired_sessions' => 100,
'max_storage_mb' => 50,
'max_average_size_kb' => 10,
'max_oldest_age_hours' => 48
];
}
/**
* しきい値を設定
*/
public function setThreshold($key, $value) {
$this->thresholds[$key] = $value;
}
/**
* ヘルスチェックを実行
*/
public function checkHealth() {
$this->alerts = [];
$sessionPath = session_save_path();
if (empty($sessionPath)) {
$sessionPath = sys_get_temp_dir();
}
$files = glob($sessionPath . '/sess_*');
$totalSize = 0;
$expiredCount = 0;
$oldestTime = time();
$gcMaxLifetime = ini_get('session.gc_maxlifetime');
$cutoff = time() - $gcMaxLifetime;
foreach ($files as $file) {
$size = filesize($file);
$mtime = filemtime($file);
$totalSize += $size;
if ($mtime < $cutoff) {
$expiredCount++;
}
if ($mtime < $oldestTime) {
$oldestTime = $mtime;
}
}
$metrics = [
'total_sessions' => count($files),
'expired_sessions' => $expiredCount,
'active_sessions' => count($files) - $expiredCount,
'total_size_mb' => round($totalSize / 1024 / 1024, 2),
'average_size_kb' => count($files) > 0
? round($totalSize / count($files) / 1024, 2)
: 0,
'oldest_age_hours' => round((time() - $oldestTime) / 3600, 2)
];
// しきい値チェック
$this->checkThresholds($metrics);
$health = [
'status' => empty($this->alerts) ? 'healthy' : 'unhealthy',
'metrics' => $metrics,
'alerts' => $this->alerts,
'checked_at' => time()
];
return $health;
}
/**
* しきい値チェック
*/
private function checkThresholds($metrics) {
if ($metrics['total_sessions'] > $this->thresholds['max_total_sessions']) {
$this->addAlert('warning', 'total_sessions',
"Total sessions ({$metrics['total_sessions']}) exceeds threshold ({$this->thresholds['max_total_sessions']})");
}
if ($metrics['expired_sessions'] > $this->thresholds['max_expired_sessions']) {
$this->addAlert('warning', 'expired_sessions',
"Expired sessions ({$metrics['expired_sessions']}) exceeds threshold ({$this->thresholds['max_expired_sessions']})");
}
if ($metrics['total_size_mb'] > $this->thresholds['max_storage_mb']) {
$this->addAlert('warning', 'storage_size',
"Total storage ({$metrics['total_size_mb']} MB) exceeds threshold ({$this->thresholds['max_storage_mb']} MB)");
}
if ($metrics['average_size_kb'] > $this->thresholds['max_average_size_kb']) {
$this->addAlert('info', 'average_size',
"Average session size ({$metrics['average_size_kb']} KB) exceeds threshold ({$this->thresholds['max_average_size_kb']} KB)");
}
if ($metrics['oldest_age_hours'] > $this->thresholds['max_oldest_age_hours']) {
$this->addAlert('warning', 'oldest_age',
"Oldest session age ({$metrics['oldest_age_hours']} hours) exceeds threshold ({$this->thresholds['max_oldest_age_hours']} hours)");
}
}
/**
* アラートを追加
*/
private function addAlert($severity, $type, $message) {
$this->alerts[] = [
'severity' => $severity,
'type' => $type,
'message' => $message,
'timestamp' => time()
];
}
/**
* 自動修復を実行
*/
public function autoRepair() {
$health = $this->checkHealth();
if ($health['status'] === 'healthy') {
return [
'repaired' => false,
'reason' => 'System is healthy'
];
}
$actions = [];
foreach ($health['alerts'] as $alert) {
switch ($alert['type']) {
case 'expired_sessions':
case 'total_sessions':
case 'oldest_age':
// GCを実行
$deleted = session_gc();
$actions[] = [
'action' => 'gc',
'trigger' => $alert['type'],
'deleted' => $deleted
];
break;
case 'storage_size':
// アグレッシブクリーンアップ
$original = ini_get('session.gc_maxlifetime');
ini_set('session.gc_maxlifetime', 1800); // 30分
$deleted = session_gc();
ini_set('session.gc_maxlifetime', $original);
$actions[] = [
'action' => 'aggressive_cleanup',
'trigger' => $alert['type'],
'deleted' => $deleted
];
break;
}
}
// 修復後のヘルスチェック
$afterHealth = $this->checkHealth();
return [
'repaired' => true,
'before' => $health,
'after' => $afterHealth,
'actions' => $actions
];
}
/**
* ヘルスレポートを生成
*/
public function generateReport() {
$health = $this->checkHealth();
$report = "=== Session Health Report ===\n";
$report .= "Status: " . strtoupper($health['status']) . "\n";
$report .= "Checked: " . date('Y-m-d H:i:s', $health['checked_at']) . "\n\n";
$report .= "Metrics:\n";
foreach ($health['metrics'] as $key => $value) {
$report .= " {$key}: {$value}\n";
}
if (!empty($health['alerts'])) {
$report .= "\nAlerts:\n";
foreach ($health['alerts'] as $alert) {
$report .= " [{$alert['severity']}] {$alert['message']}\n";
}
} else {
$report .= "\nNo alerts - system is healthy\n";
}
return $report;
}
/**
* 継続的モニタリング
*/
public function monitor($duration = 60, $interval = 10) {
$endTime = time() + $duration;
$samples = [];
echo "Monitoring for {$duration} seconds...\n";
while (time() < $endTime) {
$health = $this->checkHealth();
$samples[] = $health;
echo "[" . date('H:i:s') . "] Status: {$health['status']}, ";
echo "Sessions: {$health['metrics']['total_sessions']}, ";
echo "Expired: {$health['metrics']['expired_sessions']}\n";
if ($health['status'] === 'unhealthy') {
echo " ALERT: System unhealthy, auto-repairing...\n";
$repair = $this->autoRepair();
echo " Repaired: " . ($repair['repaired'] ? 'Yes' : 'No') . "\n";
}
sleep($interval);
}
return $samples;
}
}
// 使用例
echo "=== セッションヘルスモニター ===\n";
$monitor = new SessionHealthMonitor();
// しきい値を設定
$monitor->setThreshold('max_total_sessions', 50);
$monitor->setThreshold('max_expired_sessions', 10);
// ヘルスチェック
echo $monitor->generateReport();
// 自動修復
echo "\n=== 自動修復 ===\n";
$repair = $monitor->autoRepair();
if ($repair['repaired']) {
echo "修復完了\n";
echo "実行されたアクション: " . count($repair['actions']) . "件\n";
foreach ($repair['actions'] as $action) {
echo " {$action['action']}: {$action['deleted']}件削除\n";
}
} else {
echo "修復不要: {$repair['reason']}\n";
}
// 継続的モニタリング(コメントアウト)
// echo "\n=== 継続的モニタリング ===\n";
// $samples = $monitor->monitor(30, 5); // 30秒間、5秒間隔
例6: GC統計分析システム
class GarbageCollectionAnalytics {
private $dataFile;
/**
* 分析システムを初期化
*/
public function __construct($dataFile = '/tmp/gc_analytics.json') {
$this->dataFile = $dataFile;
}
/**
* GCを実行して記録
*/
public function recordGC($metadata = []) {
$beforeStats = $this->getSessionStats();
$startTime = microtime(true);
$deleted = session_gc();
$duration = microtime(true) - $startTime;
$afterStats = $this->getSessionStats();
$record = [
'timestamp' => time(),
'deleted' => $deleted,
'duration' => $duration,
'before' => $beforeStats,
'after' => $afterStats,
'metadata' => $metadata
];
$this->saveRecord($record);
return $record;
}
/**
* セッション統計を取得
*/
private function getSessionStats() {
$sessionPath = session_save_path();
if (empty($sessionPath)) {
$sessionPath = sys_get_temp_dir();
}
$files = glob($sessionPath . '/sess_*');
$totalSize = 0;
foreach ($files as $file) {
$totalSize += filesize($file);
}
return [
'count' => count($files),
'size' => $totalSize
];
}
/**
* レコードを保存
*/
private function saveRecord($record) {
$records = $this->loadRecords();
$records[] = $record;
// 最新1000件のみ保持
if (count($records) > 1000) {
$records = array_slice($records, -1000);
}
file_put_contents($this->dataFile, json_encode($records));
}
/**
* レコードを読み込み
*/
private function loadRecords() {
if (!file_exists($this->dataFile)) {
return [];
}
return json_decode(file_get_contents($this->dataFile), true) ?? [];
}
/**
* 統計分析を実行
*/
public function analyze($period = 86400) { // デフォルト24時間
$records = $this->loadRecords();
$cutoff = time() - $period;
// 期間内のレコードをフィルター
$periodRecords = array_filter($records, function($r) use ($cutoff) {
return $r['timestamp'] >= $cutoff;
});
if (empty($periodRecords)) {
return ['error' => 'No records in specified period'];
}
$totalDeleted = array_sum(array_column($periodRecords, 'deleted'));
$totalDuration = array_sum(array_column($periodRecords, 'duration'));
$deletedCounts = array_column($periodRecords, 'deleted');
$analysis = [
'period_hours' => $period / 3600,
'total_runs' => count($periodRecords),
'total_deleted' => $totalDeleted,
'average_deleted' => round($totalDeleted / count($periodRecords), 2),
'median_deleted' => $this->median($deletedCounts),
'max_deleted' => max($deletedCounts),
'min_deleted' => min($deletedCounts),
'total_duration' => round($totalDuration, 4),
'average_duration' => round($totalDuration / count($periodRecords), 4),
'efficiency' => $totalDuration > 0
? round($totalDeleted / $totalDuration, 2)
: 0 // sessions/second
];
return $analysis;
}
/**
* 中央値を計算
*/
private function median($values) {
sort($values);
$count = count($values);
$middle = floor($count / 2);
if ($count % 2 == 0) {
return ($values[$middle - 1] + $values[$middle]) / 2;
}
return $values[$middle];
}
/**
* トレンド分析
*/
public function analyzeTrend() {
$records = $this->loadRecords();
if (count($records) < 10) {
return ['error' => 'Insufficient data for trend analysis'];
}
// 最近の50%と古い50%を比較
$half = intval(count($records) / 2);
$older = array_slice($records, 0, $half);
$newer = array_slice($records, $half);
$olderAvg = array_sum(array_column($older, 'deleted')) / count($older);
$newerAvg = array_sum(array_column($newer, 'deleted')) / count($newer);
$change = (($newerAvg - $olderAvg) / $olderAvg) * 100;
$trend = 'stable';
if ($change > 20) {
$trend = 'increasing';
} elseif ($change < -20) {
$trend = 'decreasing';
}
return [
'trend' => $trend,
'change_percent' => round($change, 2),
'older_average' => round($olderAvg, 2),
'newer_average' => round($newerAvg, 2)
];
}
/**
* レポートを生成
*/
public function generateReport() {
$analysis24h = $this->analyze(86400);
$analysis7d = $this->analyze(604800);
$trend = $this->analyzeTrend();
$report = "=== GC Analytics Report ===\n\n";
$report .= "Last 24 Hours:\n";
if (!isset($analysis24h['error'])) {
$report .= " Total Runs: {$analysis24h['total_runs']}\n";
$report .= " Total Deleted: {$analysis24h['total_deleted']}\n";
$report .= " Average Deleted: {$analysis24h['average_deleted']}\n";
$report .= " Efficiency: {$analysis24h['efficiency']} sessions/second\n";
} else {
$report .= " " . $analysis24h['error'] . "\n";
}
$report .= "\nLast 7 Days:\n";
if (!isset($analysis7d['error'])) {
$report .= " Total Runs: {$analysis7d['total_runs']}\n";
$report .= " Total Deleted: {$analysis7d['total_deleted']}\n";
$report .= " Average Deleted: {$analysis7d['average_deleted']}\n";
} else {
$report .= " " . $analysis7d['error'] . "\n";
}
$report .= "\nTrend Analysis:\n";
if (!isset($trend['error'])) {
$report .= " Trend: {$trend['trend']}\n";
$report .= " Change: {$trend['change_percent']}%\n";
} else {
$report .= " " . $trend['error'] . "\n";
}
return $report;
}
}
// 使用例
echo "=== GC統計分析 ===\n";
$analytics = new GarbageCollectionAnalytics('/tmp/gc_analytics_test.json');
// GCを実行して記録
echo "GC実行と記録:\n";
for ($i = 0; $i < 5; $i++) {
$record = $analytics->recordGC(['run' => $i + 1]);
echo " Run " . ($i + 1) . ": {$record['deleted']}件削除 ({$record['duration']}秒)\n";
sleep(1);
}
// 分析実行
echo "\n=== 24時間の分析 ===\n";
$analysis = $analytics->analyze(86400);
if (!isset($analysis['error'])) {
echo "総実行回数: {$analysis['total_runs']}\n";
echo "総削除数: {$analysis['total_deleted']}\n";
echo "平均削除数: {$analysis['average_deleted']}\n";
echo "中央値: {$analysis['median_deleted']}\n";
echo "効率: {$analysis['efficiency']} sessions/second\n";
}
// レポート生成
echo "\n" . $analytics->generateReport();
GC設定のベストプラクティス
// 環境に応じたGC設定
// 開発環境(頻繁にクリーンアップ)
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 10); // 10%
ini_set('session.gc_maxlifetime', 1800); // 30分
// 本番環境(バランス重視)
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 100); // 1%
ini_set('session.gc_maxlifetime', 86400); // 24時間
// 高トラフィック環境(手動管理)
ini_set('session.gc_probability', 0); // 自動GC無効
ini_set('session.gc_divisor', 1);
// cronで定期的にsession_gc()を実行
// 低トラフィック環境(確実にクリーンアップ)
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 1); // 100%
ini_set('session.gc_maxlifetime', 3600); // 1時間
まとめ
session_gc()関数の特徴をまとめると:
できること:
- 期限切れセッションデータの削除
- セッションストレージのクリーンアップ
- ストレージ容量の最適化
- 手動でのガベージコレクション実行
重要な注意点:
- PHP 7.1.0以降で使用可能
session.gc_maxlifetimeに基づいて削除- セッション開始前でも実行可能
- 削除されたセッション数を返す
自動GCの設定:
session.gc_probability / session.gc_divisor= 実行確率- デフォルト: 1/100 = 1%
session_start()時に確率的に実行
推奨される使用場面:
- 定期的なセッションクリーンアップ
- ストレージ容量管理
- パフォーマンス最適化
- セッション数の制御
- cronジョブでの自動化
ベストプラクティス:
// 1. 定期的な手動実行(cronジョブ)
// 毎時間実行
$deleted = session_gc();
error_log("Session GC: {$deleted} sessions deleted");
// 2. しきい値ベースの実行
$sessionCount = count(glob(session_save_path() . '/sess_*'));
if ($sessionCount > 1000) {
session_gc();
}
// 3. 条件付きアグレッシブクリーンアップ
$totalSize = /* 計算 */;
if ($totalSize > 50 * 1024 * 1024) { // 50MB
$original = ini_get('session.gc_maxlifetime');
ini_set('session.gc_maxlifetime', 600);
session_gc();
ini_set('session.gc_maxlifetime', $original);
}
// 4. モニタリング付き実行
$before = count(glob(session_save_path() . '/sess_*'));
$deleted = session_gc();
$after = count(glob(session_save_path() . '/sess_*'));
// 統計を記録
関連設定:
session.gc_maxlifetime: 有効期限(秒)session.gc_probability: 実行確率(分子)session.gc_divisor: 実行確率(分母)session.save_path: セッションファイルのパス
パフォーマンス考慮事項:
- 大量のセッションがある場合、GC実行に時間がかかる
- 本番環境では自動GCを無効にしてcronで実行を推奨
- ストレージI/Oに負荷がかかることを考慮
session_gc()は、セッションストレージを健全に保つための重要な関数です。定期的なクリーンアップでパフォーマンスとストレージ容量を最適化しましょう!
