0) return $ts; } return 0; } /** * 重建日志:过滤掉 cutoff 之前的行;若剩余仍 > maxBytes,再从最旧端按行丢弃到 ≤ maxBytes。 * 调用方必须已持有 $fh 的排他锁。 * @param resource $fh 日志文件句柄(读写) * @param string $logFile 日志路径(临时文件建在同目录) * @return bool 成功 true */ function logMaintainRebuild($fh, $logFile, $maxBytes, $cutoff) { $tmp = $logFile . '.tmp'; // ===== 第一遍:读原文件,过滤超龄行,写入临时文件 ===== $out = @fopen($tmp, 'wb'); if (!$out) return false; if (rewind($fh) === false) { fclose($out); @unlink($tmp); return false; } $keepPrev = true; // 无时间戳的续行跟随上一行;首行无时间戳时保守保留 $kept = 0; while (($line = fgets($fh)) !== false) { $ts = logMaintainLineTs($line); if ($ts > 0) { $keepPrev = ($ts >= $cutoff); } if ($keepPrev) { fwrite($out, $line); $kept += strlen($line); } } fflush($out); fclose($out); clearstatcache(true, $tmp); // ===== 第二步:剩余仍超水位线 → 从最旧端按整行丢弃到 ≤ 90% maxBytes ===== // 水位线(4.5MB)留 0.5MB 缓冲:若压到 5MB 上限,下一条日志写入即再次超标, // 会导致「每次写入都全量重建」的 I/O 放大;留缓冲后常态每 ~2500 条日志才重建一次。 $target = (int) ($maxBytes * 9 / 10); $skipBytes = ($kept > $target) ? ($kept - $target) : 0; $in = @fopen($tmp, 'rb'); if (!$in) { @unlink($tmp); return false; } if ($skipBytes > 0) { $acc = 0; // 逐行累计丢弃;按整行丢,最终大小 kept-acc 必 ≤ target while ($acc < $skipBytes && ($line = fgets($in)) !== false) { $acc += strlen($line); } } // ===== 写回日志本体(仍持锁,写入方在锁外等待,不会丢)===== if (ftruncate($fh, 0) === false) { fclose($in); @unlink($tmp); return false; } rewind($fh); while (!feof($in)) { $chunk = fread($in, 1048576); if ($chunk === false) break; if ($chunk !== '') fwrite($fh, $chunk); } fflush($fh); fclose($in); @unlink($tmp); return true; } /** * 入口:日志写入后调用。任一条件超标(>5MB 或 含 3 天前的行)即清理, * 清理到两个条件同时满足为止(最多 3 轮,正常第 1 轮后复查即通过)。 * * @param string $logFile 日志文件绝对路径 * @param int $maxBytes 大小上限(默认 5MB) * @param int $keepDays 保留天数(默认 3 天) * @return bool true=干净(无需清理或已清理完成);false=清理失败 */ function logMaintain($logFile, $maxBytes = 5242880, $keepDays = 3) { if (!is_string($logFile) || $logFile === '') return true; clearstatcache(true, $logFile); if (!is_file($logFile)) return true; $size = @filesize($logFile); if ($size === false || $size <= 0) return true; $cutoff = time() - $keepDays * 86400; // ---- 廉价预检:两条都没超标就直接返回(绝大多数调用在这里结束)---- $need = ($size > $maxBytes); if (!$need) { $fh = @fopen($logFile, 'rb'); if ($fh) { $oldest = logMaintainOldestTs($fh); fclose($fh); $need = ($oldest > 0 && $oldest < $cutoff); } } if (!$need) return true; // ---- 清理:排他锁内执行(写入方 file_put_contents(LOCK_EX) 短暂等待)---- $fh = @fopen($logFile, 'c+'); if (!$fh) return false; if (!flock($fh, LOCK_EX)) { fclose($fh); return false; } $ok = false; for ($round = 0; $round < 3; $round++) { // 锁内复查:可能别的进程刚清理完 $stat = fstat($fh); $size = $stat ? (int) $stat['size'] : 0; $oldest = logMaintainOldestTs($fh); if ($size <= $maxBytes && !($oldest > 0 && $oldest < $cutoff)) { $ok = true; break; } if (!logMaintainRebuild($fh, $logFile, $maxBytes, $cutoff)) { break; // 重建失败(如目录不可写)→ 解锁退出,下次写入重试 } } flock($fh, LOCK_UN); fclose($fh); clearstatcache(true, $logFile); return $ok; } } // end if (!function_exists('logMaintain'))