Session tvoří v tmp desetitisíce souborů za den

Ondra.T
Člen | 4
+
0
-

Mám nainstalované Nette 2.4 a na hostingu nám to v kořenovém adresáři hostingu /tmp vytváří několik desítek tisíc session souborů za den.

Na serveru neběží žádný jiný projekt, je tam jenom projekt s Nette.

Je to velký problém, protože se tím za čtyři dny vyčerpá limit souborů na serveru, který jenom 100 000. Teď za poslední tři dny tam vzniklo 86 000 souborů a přes FTP jde jejich mazání opravdu pomalu. Ty soubory jsou přitom starší než jeden den, i když jsem dal expiraci na jeden den.

V config.neon mám session nastavené takto:

session:
	autoStart: smart
	expiration: 1 days
	savePath: "%tempDir%/sessions"

Tady je příklad obsahu jednoho session souboru:

Název souboru: sess_vvtp5toj37su2lmhgdvdtp54es
Obsah: __NF|a:1:{s:4:"Time";i:1630578848;}

Tady je seznam modulů, které projekt využívá:

"require": {
		"php": ">= 7.4",
		"nette/application": ">= 2.4",
		"nette/bootstrap": ">= 2.4.2",
		"nette/caching": ">= 2.5",
		"nette/database": ">= 2.4",
		"nette/di": ">= 2.4",
		"nette/finder": ">= 2.4",
		"nette/forms": ">= 2.4.10",
		"nette/http": ">= 2.4",
		"nette/mail": ">= 2.4",
		"nette/robot-loader": ">= 2.4",
		"nette/security": ">= 2.4",
		"nette/utils": ">= 2.4",
		"latte/latte": ">= 2.4",
		"tracy/tracy": ">= 2.4",
		"nextras/orm": ">= 3.0.1",
		"tomaj/nette-bootstrap-form": ">= 1.2",
		"ipub/visual-paginator": "@dev",
		"nextras/secured-links": ">= 1.4",
		"ublaboo/datagrid": ">= 5.7.4",
		"webchemistry/forms-multiplier": "dev-master",
		"h4kuna/exchange": ">= 5.0",
		"guzzlehttp/guzzle": ">= 6.3",
		"ublaboo/mailing": ">= 1.1.6",
		"timetoogo/pinq": ">= 3.4"
	},

Zkoušel jsem různá nastavení v config.neon, ale nemá to na to vliv. Napadá někoho, co s tím nebo kde vůbec začít?

Vůbec si s tím už nevím rady, budu moc vděčný za cokoliv, co mě posune dál.

Editoval Ondra.T (2. 9. 2021 23:52)

Polki
Člen | 553
+
-4
-

Úplně přesně dopodrobna jsem způsob ukládání dat do session nezkoumal.
Nicméně dává smysl, že pro každou hodnotu, kterou chceš uložit to vytvoří jeden soubor a pak to má jen seznam těch souborů, přičemž když chceš přistoupit k nějaké hodnotě ze session, tak se nejdřív hledá název souboru podle klíče a poté až se čte obsah. Kdyby bylo vše v 1 souboru, tak by se kvůli tomu, aby jsi našel 1 řádek muselo procházet 5mega dalších řádků, takže toto chování shledávám v pořádku.

Problém bude v ukládání dat do session. Můžeš postnout kód, jak tam ukládáš data?

Ohledně expirace si moc nepomůžeš, protože expirace nemá co mazat soubory. expirace jen zinvaliduje session, ale mazat soubory by bylo náročné, jelikož by se to muselo u všech souborů kontrolovat při každém requestu, která session už je prošlá a ty soubory smazat. To nedává smysl z pohledu performance a ty soubory na disku obyčejně ničemu nějak extra nevadí. No a když potom přistoupíš k nějaké proměnné ze session, tak se prostě jen nepřečte obsah souboru, jelikož je prošlý, ale při uložení nové hodnoty se daný soubor přepíše.

Pokud máš session na ukládání náhodných hodnot od někud a jen na jedno použití a posíláš tam pokaždé jiné parametry a ukládáš to pod jinými klíči, nebo spíše sessionsection, tak je toto chování předvídatelné. Pokud chceš mazat staré soubory, tak nejlepší řešení je prostě si udělat cron, který třeba jednou za 3 hodiny se spustí a soubory se starou(invalidovanou) cache smaže.

Marek Bartoš
Nette Blogger | 1146
+
+5
-

Třeba takové $form->addProtection() session používá a na pohled to vidět není, nemá cenu dopodrobna zkoumat vlastní kód. Obecně vzato – když nevíš odkud se něco volá, dej tam hardcoded debug_backtrace(). V tomhle případě sem:

https://github.com/…/Session.php#L147

$nf['backtrace'] = debug_backtrace();

Uloží ti to při vytvoření session do session informaci o tom, odkud se funkce volala a dohledáš tak, jaký kód session používá. Nastavení session na smart – tzn. zapnutí když je potřeba ti v případě, že se session používá a zapnout se musí, nepomůže.

za poslední tři dny tam vzniklo 86 000 souborů

Nový soubor se vytváří pro každou jednu session (přesněji pro session id, při regeneraci id se vytváří nový) a sessions se vytváří tolik, protože na weby chodí hodně botů a ti si nepamatují session id. Taková indexace googlem, která projde 300 url na kterých se používá session ti jich tak vytvoří 300.

přes FTP jde jejich mazání opravdu pomalu

Jestliže máš přístup přes ssh, používej ssh. Jednoduché rm -r /tmp/* je prakticky okamžité.

Ty soubory jsou přitom starší než jeden den, i když jsem dal expiraci na jeden den.

Expirace na jeden den sama o sobě nezaručí, že se po jednom dni soubor okamžitě smaže. Sessions maže tzv. garbage collector, který má pouze pravděpodobnost, že při spuštění php sessions promaže. Jde o kombinaci nastavení session.gc_probability a session.gc_divisor. A jelikož na spoustě systémů se mazání sessions provádí přes systémový cron, tak nastavení přímo v aplikaci spíše nefunguje, musí se provádět na úrovni systémové php konfigurace.

Pavel Kravčík
Člen | 1180
+
0
-

Taky je dobrý nápad se podívat do access.log. Tohle vypadá spíš na nějaké crawlery/roboty. Tj. pro každý request se tvoří nová session. Pokud máš třeba nákupní košík – určitě nečti session, ale použij $session->isStarted() nebo jak se to jmenuje. Mám dojem, že jsem jednou řešil podobný problém, že dotaz na offsetGet() vytvořil vždy novou session.

Ondra.T
Člen | 4
+
+1
-

Díky moc za odpovědi! Hlavně děkuji Tobě @MarekBartoš za konkrétní návod.

Přidal jsem tedy do session.php to trasování. Výsledkem je:

  1. Hodně se zmírnil počet vytvořených souborů, za poslední hodinu jenom čtyři!
  2. Soubory co se vytvoří jsou prázdné, tj. nic v nich není.

Pro jistotu posílám celou metodu do které jsem ten řádek přidal:

	private function initialize(): void
	{
		$this->started = true;

		/* structure:
			__NF: Data, Meta, Time
				DATA: section->variable = data
				META: section->variable = Timestamp
		*/
		$nf = &$_SESSION['__NF'];

		if (!is_array($nf)) {
			$nf = [];
		}

		// regenerate empty session
		if (empty($nf['Time'])) {
			$nf['Time'] = time();
			if ($this->request->getCookie(session_name())) { // ensures that the session was created in strict mode (see use_strict_mode)
				$this->regenerateId();
			}
		}

		// expire section variables
		$now = time();
		foreach ($nf['META'] ?? [] as $section => $metadata) {
			foreach ($metadata ?? [] as $variable => $value) {
				if (!empty($value['T']) && $now > $value['T']) {
					if ($variable === '') { // expire whole section
						unset($nf['META'][$section], $nf['DATA'][$section]);
						continue 2;
					}
					unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]);
				}
			}
		}
		$nf['backtrace'] = debug_backtrace();
		register_shutdown_function([$this, 'clean']);
	}

EDIT: Musel jsem to zase zakomentovat, protože se nešlo do aplikace přihlásit. Psalo to tuto chybovou hlášku:

Fatal error: Uncaught Exception: Serialization of 'Closure' is not allowed in [no active file]:0 Stack trace: #0 {main} thrown in [no active file] on line 0

Jestliže máš přístup přes ssh, používej ssh. Jednoduché rm -r /tmp/*

To by byla nádhera, bohužel ssh přístup nemám. Když jsem byl přes prázdniny na týden pryč tak se tam nahromadilo kolem dvou miliónů souborů, tak to mazala podpora, protože přes FTP nebyla šance.

Taky je dobrý nápad se podívat do access.log

Ten jsem na serveru nenašel. Jde o pronajatý hosting, kromě tmp tam vidím akorát svůj projekt. A v Nette jsem tento log nenašel.

Pokud máš třeba nákupní košík – určitě nečti session, ale použij $session->isStarted() …

Nejdřív potřebuju najít odkud se ta session volá, já totiž pokud vím ji nikde nevytvářím. Ale je to projekt, který jsem převzal, takže se mi hodí spíš ta detektivní práce přes výpisy, jak navrhoval Marek v postu výše.

Editoval Ondra.T (3. 9. 2021 13:06)

Marek Bartoš
Nette Blogger | 1146
+
+1
-

Fatal error: Uncaught Exception: Serialization of ‚Closure‘ is not allowed in [no active file]:0 Stack trace: #0 {main} thrown in [no active file] on line 0

Tak tedy znova a lépe. Opomněl jsem, že jsou v backtrace i neserializovatelná data.

$backtrace = debug_backtrace();
foreach ($backtrace as $key => $value) {
	unset($value['object'], $value['args']);
	$backtrace[$key] = $value;
}
$nf['backtrace'] = $backtrace;
David Grudl
Nette Core | 8082
+
+1
-

Asi by stačilo $nf['backtrace'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);

Ondra.T
Člen | 4
+
0
-

Takže tady je výsledek detektivky. Pokud tomu rozumím správně, tak se ta session vytváří při spuštění Nette. Zatím jsou to akorát dva případy za poslední 3 hodiny, nejsou to stovky souborů za hodinu, možná je to tedy něčím jiným.

Toto je obsah session z 16:49:

__NF | a: 2: {
    s: 4: "Time";i: 1630680582;s: 9: "backtrace";a: 5: {
        i: 0;a: 5: {
            s: 4: "file";s: 72: "/data/www/23490/buranteatr_cz/www/vendor/nette/http/src/Http/Session.php";s: 4: "line";i: 107;s: 8: "function";s: 10: "initialize";s: 5: "class";s: 18: "Nette\Http\Session";s: 4: "type";s: 2: "->";
        }
        i: 1;a: 5: {
            s: 4: "file";s: 88: "/data/www/23490/buranteatr_cz/www/temp/cache/nette.configurator/Container_4ed7ec85d6.php";s: 4: "line";i: 2755;s: 8: "function";s: 5: "start";s: 5: "class";s: 18: "Nette\Http\Session";s: 4: "type";s: 2: "->";
        }
        i: 2;a: 5: {
            s: 4: "file";s: 87: "/data/www/23490/buranteatr_cz/www/vendor/nette/bootstrap/src/Bootstrap/Configurator.php";s: 4: "line";i: 231;s: 8: "function";s: 10: "initialize";s: 5: "class";s: 20: "Container_4ed7ec85d6";s: 4: "type";s: 2: "->";
        }
        i: 3;a: 5: {
            s: 4: "file";s: 51: "/data/www/23490/buranteatr_cz/www/app/bootstrap.php";s: 4: "line";i: 22;s: 8: "function";s: 15: "createContainer";s: 5: "class";s: 18: "Nette\Configurator";s: 4: "type";s: 2: "->";
        }
        i: 4;a: 3: {
            s: 4: "file";s: 47: "/data/www/23490/buranteatr_cz/www/www/index.php";s: 4: "line";i: 17;s: 8: "function";s: 7: "require";
        }
    }
}

Druhá session je totožná, akorát s jiným časem z 17:50.

Toto je obsah Container_4ed7ec85d6.php kolem řádku 2755:

public function initialize()
	{
		(function () {
			$response = $this->getService('http.response');
			$response->setHeader('X-Powered-By', 'Nette Framework 3');
			$response->setHeader('Content-Type', 'text/html; charset=utf-8');
			$response->setHeader('X-Frame-Options', 'SAMEORIGIN');
			$response->setCookie('nette-samesite', '1', 0, '/', null, null, true, 'Strict');
		})();
		// řádek 2755
		$this->getService('session.session')->exists() && $this->getService('session.session')->start();
		// tracy.
		(function () {
			Tracy\Debugger::getLogger()->mailer = [new Tracy\Bridges\Nette\MailSender($this->getService('mail.mailer')), 'send'];
		})();
		Contributte\FormMultiplier\Multiplier::register('addMultiplier');
	}

Je možné, že to je ono, odpovídalo by to tím, že si to tam pro nějaké účely ukládá čas, což bylo předtím ve všech deseti z těch tisíců souborů, které jsem prohlédl.

Taky to odpovídá tomu co dělá inicializace Nette Session, která si tam ukládá aktuální čas:
https://github.com/…/Session.php#L128

Zatím to pro každou návštěvu nedělá. Problém je, že za zatím neznámých okolností to nejspíš začne tuto session vytvářet pro úplně každou návštěvu stránky a tím pak vznikne ten problém s generováním statisíců session souborů.

Napadá vás k tomu něco?

David Grudl
Nette Core | 8082
+
+2
-

Zkus použít vývojovou verzi nette/http, jestli to pomůže.

V composeru dáš např "nette/http": "^3.1@dev",

Ondra.T
Člen | 4
+
0
-

Zkoušel jsem updatovat nette/http na verzi 3.1 dev, ale composer mi hlásí spoustu neřešitelných konfliktů.

Když jsem to updatoval celé, tak se mi projekt nedaří rozjet, protože se to updatovalo na Nette 3.1 a s tím tam spousta věcí není kompatabilních.

Takže to zatím nechám tak, protože zatím to tvoří jenom 20 session souborů za den a s tím se dá žít. Když se to zase začne tvořit statisíce souborů, tak investuju čas do přechodu na nejnovější Nette s nadějí, že to ten problém vyřeší.

Díky za pomoc s odhalením příčiny chyby. Původně jsem myslel, že to dělá nějaká zapomenutá funkce, nenapadlo mě, že to může dělat samotné Nette v rámci své inicializace.

Editoval Ondra.T (6. 9. 2021 14:57)