Я пытаюсь протестировать различные способы перемещения данных 1M из одной таблицы в другую, не блокируя целые таблицы, а затем отмечая строки как обработанные.
У меня есть два скрипта: один использует простой выбор вставки (limitedLock.php), а другой можно запускать параллельно (parallel.php)
Оба скрипта используют одни и те же настройки базы данных
Я запускаю каждый скрипт в режиме CLI с помощью следующих команд:
date && limitedLock.php && date
date & php parallel.php & php parallel.php & php parallel.php & wait && date
Удивительно, но оба скрипта работают так же быстро (18 секунд каждый на моем компьютере), но при запуске параллельного.php mariadb использует весь процессор (верхняя команда показывает использование 300% !!!). Я не понимаю, почему Parallel.php использует так много процессора и что я могу сделать, чтобы предотвратить это?
вот код для воспроизведения проблемы:
<?php
//fixture.php
$createFrom = <<<SQL
CREATE TABLE IF NOT EXISTS from_table(
id INT NOT NULL AUTO_INCREMENT,
`state` VARCHAR(10),
PRIMARY KEY(id),
KEY state_idx (state)
)
SQL;
$createTo = <<<SQL
CREATE TABLE IF NOT EXISTS to_table(
id INT,
PRIMARY KEY(id)
)
SQL;
$conn = new PDO('mysql:host=mariadb;dbname=some_db', 'root', 'root');
$conn->exec('DROP TABLE IF EXISTS from_table');
$conn->exec('DROP TABLE IF EXISTS to_table');
$conn->exec($createFrom);
$conn->exec($createTo);
$values = str_repeat("('to_process'),", 1_000_123);
$insert = 'INSERT INTO from_table (`state`) VALUES ' . rtrim($values, ',');
$conn->exec($insert);
ограниченныйLock.php:
<?php
$conn = new PDO('mysql:host=mariadb;dbname=some_db', 'root', 'root');
$lastId = 0;
$limit = 1000;
do {
$select = <<<SQL
SELECT id
FROM from_table
WHERE id > {$lastId}
AND state = 'to_process'
ORDER BY id
LIMIT {$limit}
SQL;
$stmt = $conn->query($select);
$result = $stmt->fetchAll();
$ids = array_column($result, 'id');
$lastId = end($ids);
$inClause = implode(',', $ids);
$insert = <<<SQL
INSERT INTO to_table(id)
SELECT id
FROM from_table
WHERE id IN ({$inClause})
SQL;
$update = <<<SQL
UPDATE from_table
SET state = 'processed'
WHERE id IN ({$inClause})
SQL;
$conn->beginTransaction();
$conn->exec($insert);
$conn->exec($update);
$conn->commit();
} while (count($ids) === $limit);
параллельный.php
<?php
$lockRows = <<<SQL
CREATE TEMPORARY TABLE ids_to_process
SELECT id
FROM from_table
WHERE state = 'to_process'
ORDER BY id
LIMIT 1000
FOR UPDATE SKIP LOCKED
SQL;
$insert = <<<SQL
INSERT INTO to_table(id)
SELECT ft.id
FROM from_table ft
JOIN ids_to_process itp ON itp.id = ft.id
SQL;
$update = <<<SQL
UPDATE from_table ft
JOIN ids_to_process itp ON itp.id = ft.id
SET state = 'processed'
SQL;
$conn = new PDO('mysql:host=mariadb;dbname=some_db', 'root', 'root');
$stmt = $conn->query('SELECT id FROM from_table order by id desc limit 1');
$lastId = (int) $stmt->fetch()['id'];
do {
$conn->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
try {
$conn->beginTransaction();
$conn->exec($lockRows);
$conn->exec($insert);
$conn->exec($update);
$conn->exec('DROP TEMPORARY TABLE IF EXISTS ids_to_process');
$conn->commit();
//echo getmypid() . " - commited\n";
} catch (Exception $e) {
$conn->rollBack();
exit(getmypid() . PHP_EOL . $e->getMessage());
}
//if last id is in to_table then all rows have been transfered and the loop must stop
$stmt = $conn->query("SELECT id FROM to_table WHERE id = {$lastId} limit 1");
$result = $stmt->fetch();
} while (empty($result));
Я занимаюсь бенчмаркингом, не более того, что уже написал: пытаюсь протестировать разные способы перемещения 1М данных из одной таблицы в другую без блокировки целых таблиц. Проблем с подложкой нет.
Какое целое число такое 1_000_123? В любом случае, я не совсем понимаю, как «parallel.php» может выполнять асинхронное перемещение? Я предполагаю, почему ваш процессор перегревается из-за «parallel.php», потому что у вашей временной таблицы нет индекса. Это требует большого объема поиска в памяти без какого-либо более медленного оборудования, сдерживающего процессор.
Почему вы решили, что это проблема? Это параллельно, поэтому несколько ядер будут работать одновременно, не так ли?
@KIKOSoftware каждый параллельный поток обрабатывает 1 тыс. строк за цикл. Дополнительные 123 нужны для того, чтобы увидеть, как ведет себя скрипт, когда размер фрагмента меньше 1 КБ. Parallel.php работает асинхронно благодаря предложению sql «FOR UPDATE SKIP LOCKED». Это позволяет каждому потоку читать разные строки, избегая взаимоблокировок. Возможно, вы правы: временная таблица очень мала (она содержит только 1 КБ строк), без индекса это может быть проблемой, спасибо.
@shingo, ты прав, но если есть способ сэкономить ресурсы, мне было бы очень интересно об этом узнать.






Что может быть причиной высокой загрузки процессора:
Параллельная обработка: сценарий parallel.php использует параллельный подход. Чем больше вы распараллеливаете, тем больше у вас конфликтов (конкуренций) за ресурсы, особенно при доступе к общему ресурсу (в данном случае к базе данных MariaDB). Каждый скрипт пытается заблокировать строки, что приводит к более высокому параллелизму.
ПРОПУСТИТЬ ЗАБЛОКИРОВАНО: опция SKIP LOCKED в вашем parallel.php означает, что если строка заблокирована одной транзакцией, другая транзакция пропустит эту строку. Это позволяет нескольким транзакциям одновременно обрабатывать разные строки, не дожидаясь блокировок. Однако это также может вызвать агрессивную конкуренцию за ресурсы базы данных и стать одной из причин высокой загрузки ЦП.
Создание и удаление временной таблицы. В сценарии parallel.php вы постоянно создаете и удаляете временную таблицу на каждой итерации цикла. Создание и удаление таблиц, даже временных, может быть ресурсоемким.
Транзакции: Оба сценария используют транзакции. Однако параллельный сценарий выполняет множество транзакций параллельно, что может привести к перегрузке сервера MariaDB, особенно при необходимости управлять блокировками.
Цикл занятости: ваш сценарий parallel.php по сути представляет собой цикл ожидания занятости. Он постоянно проверяет, существует ли последний идентификатор в to_table, и прерывается только при его обнаружении. Это может сильно нагружать процессор.
Помните, что параллелизация — это палка о двух концах. Хотя в некоторых случаях это может привести к ускорению обработки, это также может привести к конкуренции за ресурсы, особенно в контексте базы данных. В зависимости от ваших конкретных потребностей, возможно, стоит пересмотреть степень распараллеливания или поискать другие способы ускорить процесс.
Постарайтесь оптимизировать запросы, другими словами, избегайте частого создания и удаления таблиц. Если возможно, создайте временную таблицу один раз в начале и удалите ее в конце. Оптимизируйте стратегию блокировки.
Наконец, всегда помните, что операции с базой данных, особенно операции записи, часто связаны с вводом-выводом. Хотя параллельная обработка может помочь в сценариях, связанных с использованием ЦП, она часто может усугубить проблемы с производительностью в сценариях, связанных с вводом-выводом.
Спасибо за действительно подробный ответ, я постараюсь таким образом оптимизировать свой скрипт.
Кажется, ваш вопрос является хорошим примером проблемы XY (en.wikipedia.org/wiki/XY_problem). Может быть, вам следует начать со структуры таблицы и того, чего вы на самом деле хотите достичь?