Страница 1 из 2

Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 17:34
Andex
Не столько вопрос, сколько хочу поделиться наблюдениями.
А может быть кто-то подскажет, что я мог упустить.

Итак, имеем относительно посещаемый форум, 70 тыщ юзеров, 500-700 онлайна.
Работает на 3.1, но и для 3.2 все описанное актуально, т.к. изменений по сути в данной фукнциональности нет.

Обнаружил, что таблицы sessions, sessions_keys, login_attempts и confirm, мягко говоря, ненормального размера - 600, 370, 50 тыс. записей соответственно. А confirm так вообще 4,5 млн строк!

Почитал топик: Разлогинивает рандомно + доступ к логину за пределами форума - хоть и не по теме, но он дал понимание о sessions_gc() - функции, которая по крону чистит, как оказалось, все вышеописанные таблицы.

Начал ее смотреть...
В самом начале

Код: Выделить всё

$batch_size = 10;
Эта переменная используется ниже, как limit к запросу:

Код: Выделить всё

		$sql = 'SELECT session_user_id, session_page, MAX(session_time) AS recent_time
			FROM ' . SESSIONS_TABLE . '
			WHERE session_time < ' . ($this->time_now - $config['session_length']) . '
			GROUP BY session_user_id, session_page';
		$result = $db->sql_query_limit($sql, $batch_size);
Запрос, я бы сказал, странный и тяжелый, он пытается получить idшники 10 юзеров у которых истекли сессии, но если у юзера этих сессий было 3 - это будут три строки, будет 10 сессий - будет 10 строк с одним и тем же session_user_id.

Далее из таблицы session удаляются истекшие сессии юзеров с указанными id. Причем, разрабы не предусмотрели что запрос может отдавать одинаковые id и пихают это все в

Код: Выделить всё

			// Delete expired sessions
			$sql = 'DELETE FROM ' . SESSIONS_TABLE . '
				WHERE ' . $db->sql_in_set('session_user_id', $del_user_id) . '
					AND session_time < ' . ($this->time_now - $config['session_length']);
Хотя надо бы сделать как минимум

Код: Выделить всё

$del_user_id = array_unique($del_user_id);

Итак, вычистили мы эти 10 сессий, а дальше gc продолжает работу если истекших сессий больше нет (точнее меньше 10 было в этот раз) А если есть, прекращает работу не меняя время последнего выполнения по крону cron.task.core.tidy_sessions, чтобы запуститься еще раз.

Что получается:
Само задание cron.task.core.tidy_sessions запускается каждые 3600 по умолчанию, или каждый раз при глобальном кроне, если выше кол-во сессий превышает 10 (у меня, кстати, */5 для глобального крона стоит).

Даже за 5 минут, не говоря уж о 60 (если, скажем, снести все сессии вообще) форум посещают большое кол-во пользователей, которые генерят большое кол-во сессий, уж точно больше 10. И со временем, когда доходит время до очистки, этих 10 записей просто недостаточно для того, чтобы нормально чистить таблицу sessions. При этом, для пользователей, для которых такая очистка не происходит, не удаляются все новые и новые сессии и все это накапливается как снежный ком (до них просто дело не доходит).
В итоге, cron.task.core.tidy_sessions выполянется при каждом вызове глобального крона.
Но так как сессий всегда больше 10 (и их кол-во только растет со временем) сборщик мусора не дохолдит до других задач

А там и чистка sessions_keys и login_asttempts и confirm.

В общем, не совсем ясно, что хотели сделать этим LIMIT 10 - ограничить нагрузку, когда крон выполняется при генерации страницы, пользователем, а не через системный? Короче, поставил лимит в 1000 и ежеминутный запуск cron.task.core.tidy_sessions

С confirm тоже кстати забавная история вышла. Как я выше упоминал, ее размер оказался 4,5 млн строк. 4,5 млн капч??? что???
Оказалось, что если Гость пытается открыть какой-нибудь posting.php - происходит переадресация на страницу логина, но при этом генерится капча с типом 3. А такое очень любят делать роботы/спамеры, долбясь тысячами запросов в этот posting.php и генерируя "миллионы капч" в таблице confirm.

В итоге, очистив seesion и дав продолжение работы сборщику мусора он уперся в чистку таблицы confirm вывалившись с ошибкой, поскольку наткнулся на запрос:

Код: Выделить всё

	function garbage_collect($type)
	{
		global $db, $config;

		$sql = 'SELECT DISTINCT c.session_id
			FROM ' . CONFIRM_TABLE . ' c
			LEFT JOIN ' . SESSIONS_TABLE . ' s ON (c.session_id = s.session_id)
			WHERE s.session_id IS NULL' .
				((empty($type)) ? '' : ' AND c.confirm_type = ' . (int) $type);
		$result = $db->sql_query($sql);

		if ($row = $db->sql_fetchrow($result))
		{
			$sql_in = array();
			do
			{
				$sql_in[] = (string) $row['session_id'];
			}
			while ($row = $db->sql_fetchrow($result));

			if (sizeof($sql_in))
			{
				$sql = 'DELETE FROM ' . CONFIRM_TABLE . '
					WHERE ' . $db->sql_in_set('session_id', $sql_in);
				$db->sql_query($sql);
			}
Похоже, последний DELETE сгенерился настолько длинным, что...

Пришлось предыдущий SELECT ограничить в районе LIMIT 1000-10000 и повторить несколько сотен итераций запуска сборщика...


В общем, такая вот штука.
Причина - посещаемый форум, не лучший запрос и его лимит в 10ку строк... и все растет снежным комом с каждой минутой...

Отправлено спустя 2 минуты 41 секунду:
Да, собственно, пока закончилось тем, что я поставил лимит в 1000 выборки из sessions и 10000 для выборки из confirm и оставил выполенение только через глобальный cron:run
Посмотрим, достаточно ли этихъ значений, если session_gc() будет дергаться раз в 3600 секунд, как это установлено по дефолту

После чистки
sessions - 4850 строк
sessions_keys - 99140 (730 дней стоит)
login_attempts - 20
confirm - 225650

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 18:53
nissin
Andex, вы не правы.
Если посмотреть ниже:

Код: Выделить всё

		if ($del_sessions < $batch_size)
		{
			// Less than 10 users, update gc timer ... else we want gc
			// called again to delete other sessions
			$config->set('session_last_gc', $this->time_now, false);

			if ($config['max_autologin_time'])
			{
				$sql = 'DELETE FROM ' . SESSIONS_KEYS_TABLE . '
					WHERE last_login < ' . (time() - (86400 * (int) $config['max_autologin_time']));
				$db->sql_query($sql);
			}

			// only called from CRON; should be a safe workaround until the infrastructure gets going
			/* @var $captcha_factory \phpbb\captcha\factory */
			$captcha_factory = $phpbb_container->get('captcha.factory');
			$captcha_factory->garbage_collect($config['captcha_plugin']);

			$sql = 'DELETE FROM ' . LOGIN_ATTEMPT_TABLE . '
				WHERE attempt_time < ' . (time() - (int) $config['ip_login_limit_time']);
			$db->sql_query($sql);
		}
session_last_gc меняется только после полной очистки.
А у вас проблема скорей всего со значением session_last_gc, которая установлена на значение в далеком будущем, и по этой причине очистка сессий не запускается от слова вообще.

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 19:01
rxu
nissin, всё равно получается, что очищаются только 10 крайних сессий, все другие остаются.

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 19:11
Andex
nissin, с session_last_gc все хорошо, ее значение как раз наоборот, в далеком прошлом (было), и задание cron.task.core.tidy_sessions выполняется при каждом запуске глобального крона (cron:run).

Крон этот, как я уже и говорил, у меня выполняется каждые 5 минут, но за 5 минут успевают нагенерить сессий больше, чем потом удаляется. И через несколько лет мы имеем описанную мной картину...

Отправлено спустя 2 минуты 7 секунд:
rxu, там все сложнее и.. глупее что ли... могу вам ради интереса дать дамп таблички sessions, посмОтрите, какой резалт селекта из этой таблицы...

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 19:16
rxu
Andex, тут в принципе всё понятно. Эта "десятка" тянется еще с версии 3.0.
С тех пор функция практически не переписывалась, за исключением последнего куска, связанного с каптчей.

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 19:25
Andex
Ну и странная история с обавлением капч в confirm, когда неавторизованный пользователь дергает posting.php, даже если у Гостей нет прав размещения сообщений...

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 19:30
rxu
Andex писал(а): 01.11.2019 19:25 странная история с обавлением капч в confirm
Ну "дернуть" скрипт ему никто запретить не может, другое дело, что при отсутствии прав никакой каптчи гость реально не получит. Как и вообще доступа к странице постинга.

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 19:35
Andex
rxu писал(а): 01.11.2019 19:30 при отсутствии прав никакой каптчи гость реально не получит
Это да, будет редирект на форму логина.
Но запись в БД все равно добавится... Но в этом направлении я не копал, не разбирался почему так. Проще, действительно, вычистить. Главное, чтобы до чистки дело дошло.

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 19:41
rxu
Могу сказать одно - последние лет 10 этим функционалом никто не занимался и не пересматривал.
Если будут конкретные предложения, как поменять код - велкам :)

Отправлено спустя 46 секунд:
Можно тупо поменять 10 на 100, или вообще убрать этот лимит. Но тогда неясно, что будет с относительно слабыми серверами.

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 20:03
Siava
В настройках безопасности есть такая строка, "привязать формы к гостевым сессиям" или что-то в таком духе. Быть может её использование генерит кучу confirm?
Так, пальцем в небо тычу..

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 01.11.2019 22:36
Andex
rxu писал(а): 01.11.2019 19:42 ожно тупо поменять 10 на 100, или вообще убрать этот лимит. Но тогда неясно, что будет с относительно слабыми серверами.
Соглашусь
Особенно, если это будет делаться не через юниксовый крон, а просто при генерации страницы пользователю...

Я пока поставил 1000 лимит, сотки не хватает.
В настройки в админку бы вынести этот параметр... Ну или время периодичности запуска

По спойлером запрос удаления (для примера), который получаю, если час не запускать чистку
 

Код: Выделить всё

DELETE FROM phpbb3_sessions
                                WHERE session_user_id IN (2, 372, 451, 516, 1570, 2139, 2271, 3049, 3746, 4205, 4387, 4585, 4759, 5005, 5888, 6129, 6198, 6272, 6323, 6690, 6751, 6927, 6955, 7610, 7682, 8037, 8157, 8345, 8536, 8568, 8601, 8872, 8920, 9196, 9234, 9581, 10068, 10205, 10232, 10294, 10538, 10632, 10683, 10883, 11001, 11103, 11666, 11674, 12032, 12246, 12344, 12447, 12898, 13129, 13797, 14017, 14022, 14446, 14779, 15135, 15471, 15635, 15977, 16060, 16097, 16260, 16559, 16577, 16613, 16793, 16809, 17252, 18056, 18406, 18621, 18771, 18878, 18891, 19163, 19198, 19298, 20210, 20336, 21016, 21029, 21479, 21803, 21920, 21950, 22086, 22095, 22334, 22695, 22759, 22956, 23161, 23521, 23615, 23787, 24045, 24288, 24300, 24324, 24570, 24577, 24854, 24900, 24904, 24925, 24967, 25142, 25605, 25675, 25758, 26240, 26969, 27035, 27315, 27392, 27637, 27645, 27664, 27717, 27744, 27945, 28173, 28313, 28589, 28787, 28845, 28926, 28972, 29348, 29413, 30037, 30054, 30147, 30293, 30297, 30356, 30779, 31386, 31387, 31485, 31528, 31737, 32038, 32090, 32493, 33103, 33742, 34187, 34406, 34579, 34664, 34758, 34781, 34788, 35038, 35076, 35335, 35961, 36020, 36066, 36489, 37021, 37291, 37376, 38195, 38824, 39249, 39423, 39530, 39542, 39569, 39579, 39774, 40670, 41068, 41431, 41732, 41788, 41856, 41871, 41882, 41916, 42002, 42206, 42463, 42470, 42704, 42718, 42802, 42959, 42961, 43145, 43237, 43653, 43685, 43772, 43805, 43811, 43924, 43986, 43997, 45337, 45420, 45910, 45977, 46125, 46538, 46686, 46880, 47175, 47389, 47714, 47737, 47877, 48005, 48664, 48810, 48912, 49520, 49537, 49583, 49872, 49893, 50085, 50265, 50837, 50981, 51049, 51344, 51393, 51725, 52148, 52173, 52438, 52852, 52973, 53166, 53204, 53278, 53484, 53663, 53967, 54221, 54241, 54276, 54435, 54608, 54814, 54957, 55158, 55620, 55690, 55701, 56029, 56032, 56186, 56220, 56341, 56362, 56594, 56667, 56759, 56945, 56963, 57056, 57425, 57560, 57808, 57903, 58092, 58094, 58241, 58254, 58265, 58328, 58348, 58377, 58763, 58842, 58849, 58947, 59039, 59145, 59314, 59394, 59405, 59754, 59833, 60596, 60708, 60781, 60815, 60829, 61115, 61139, 61224, 61239, 61286, 61517, 61573, 61731, 61745, 61791)
                                        AND session_time < 1572626521
Siava писал(а): 01.11.2019 20:03 привязать формы к гостевым сессиям
Видел эту настройку, но не понял, за что она вообще отвечает. Решил не трогать) Попробую локально

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 02.11.2019 7:27
rxu
Andex, т.е. таблица забита в основном не гостевыми сессиями?

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 02.11.2019 14:35
Andex
rxu, да, в основном забивается не гостевые сессиями.

Гостевые чистятся потому что session_gc() начинается с:

Код: Выделить всё

		// Firstly, delete guest sessions
		$sql = 'DELETE FROM ' . SESSIONS_TABLE . '
			WHERE session_user_id = ' . ANONYMOUS . '
				AND session_time < ' . (int) ($this->time_now - $config['session_length']);
		$db->sql_query($sql);
А потом начинается чистка не гостевых сессий:

Код: Выделить всё

		// Get expired sessions, only most recent for each user
		$sql = 'SELECT session_user_id, session_page, MAX(session_time) AS recent_time
			FROM ' . SESSIONS_TABLE . '
			WHERE session_time < ' . ($this->time_now - $config['session_length']) . '
			GROUP BY session_user_id, session_page';
		$result = $db->sql_query_limit($sql, $batch_size);
Как я говорил, запрос максмально "странный". По логике, там пытаюстся получить 10 идшников юзеров у которых есть самые старые истекшие сессии.
А далее полученные ид без array_unique тыкаются в этот запрос:

Код: Выделить всё

			$sql = 'DELETE FROM ' . SESSIONS_TABLE . '
				WHERE ' . $db->sql_in_set('session_user_id', $del_user_id) . '
					AND session_time < ' . ($this->time_now - $config['session_length']);
			$db->sql_query($sql);
А учитывая, что истекших сессий у одного и того же пользователя может быть несколько, то за раз мы удалим сессии даже менее 10 юзеров...
Казалось бы, стоит убрать из запроса session_page, но оно так же заюзано вот здесь:

Код: Выделить всё

		while ($row = $db->sql_fetchrow($result))
		{
			$sql = 'UPDATE ' . USERS_TABLE . '
				SET user_lastvisit = ' . (int) $row['recent_time'] . ", user_lastpage = '" . $db->sql_escape($row['session_page']) . "'
				WHERE user_id = " . (int) $row['session_user_id'];
			$db->sql_query($sql);
Что тоже, в общем-то, криво, так как с учетом нескольких возможных сессий одного и того же юзера, могут вноситься не совсем верные данные в user_lastvisit и user_lastpage...


В общем, я бы наверно первый селект разбил на два запроса:

Код: Выделить всё

SELECT session_id, session_user_id, MAX(session_time) AS recent_time
                        FROM phpbb3_sessions
                        WHERE session_time < 1572685105
                        GROUP BY session_user_id
Здесь выбрал сначала всех пользователей у которых есть истекшие сессии и идшник самой последней такой сессии.
Это индексовый запрос, он выполняется быстро.

Далее выбираем сессии по полученным выше session_id:

Код: Выделить всё

SELECT session_id, session_user_id, session_page, session_time AS recent_time
                        FROM phpbb3_sessions
                        WHERE session_id IN ('566551e28e054ff2e41d90a83a7255a3',..........)
                        GROUP BY session_user_id
или

Код: Выделить всё

SELECT session_id, session_user_id, session_page, MAX(session_time) AS recent_time
                        FROM phpbb3_sessions
                        WHERE session_time < 1572685105 AND session_user_id IN (27298,....)
                        GROUP BY session_user_id
чтобы укоротить длину запроса, убрав оттуда хэши(session_id)

Полученные данные пихаем в UPDATE USERS_TABLE
Ну и далее как и ранее удаляем истекшие сессии для полученных выше идшников юзеров.

Ну и на всякий случай в первом селекте можно оставить тоже лимит в 100-1000 на всякий случай. А можно и вовсе бещ лимита

В остальном запросы все по индексам и должны обрабатываться максимально быстро...

PS. у себя на продакшене я просто изменил 10 на 1000, т.к. сервак относительно мощный и этого достаточно (не хочу лишний раз менять код в движке), но в целом, предложенное мной выше решение кажется более практичным

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 02.11.2019 14:45
rxu
Возможно, проще будет отобрать все валидные сессии, а затем удалить всё, кроме них.

Re: Большие таблицы sessions, sessions_keys, confirm и login_attempts - что не так с session_gc()

Добавлено: 02.11.2019 14:51
Andex
rxu, не получится, т.к. там есть UPDATE USERS_TABLE, в который нужно передать данные из невалидных сессий, т.е. их все равно придется отбирать...

В любом случае, повторюсь, описаное мной решение с разбиением селекта с группировкой (по полю без индекса) на два отдельных запроса должно решать проблему.