Быстрая обработка частых задач в Crontab и PHP
PHP, Ubuntu | Комментировать запись
В Linux есть универсальный инструмент для обработки длительных задач в фоновом режиме в заданное время, он называется crontab. Этот демон отлично подходит для выполнения повторяющихся задач, однако у него есть одно ограничение: минимальный интервал выполнения задачи – 1 минута.
Но современные приложения должны выполнять некоторые задачи чаще, чтобы избежать неудобств в работе пользователей. Например, если для планирования задач обработки файлов на своем веб-сайте вы используете модель очереди заданий, длительное ожидание отрицательно скажется на конечных пользователях.
Другой сценарий – это применение очереди заданий для отправки текстовых сообщений или электронных писем клиентам после того, как они выполнили определенную задачу в приложении (например, перевели деньги получателю). Если пользователю приходится ждать подтверждения целую минуту, он может подумать, что транзакция не удалась, и попытается повторить ее.
Чтобы устранить эти проблемы, вы можете написать сценарий PHP, который циклически повторяет и обрабатывает задачи в течение 60 секунд, после чего демон crontab вызовет его снова (через минуту). Когда скрипт PHP вызывается демоном crontab в первый раз, он может выполнять задачи в тот период времени, который соответствует логике вашего приложения, не заставляя пользователя ждать.
В этом мануале мы создадим на сервере Ubuntu 20.04 тестовую базу данных cron_jobs, а в ней – таблицу tasks, после чего напишем скрипт, который будет выполнять задачи из таблицы с интервалом в 5 секунд с помощью цикла while(…){…} и функции sleep().
Требования
- Сервер Ubuntu 20.04, настроенный согласно этому руководству по начальной настройке.
- Установленный на вашем сервере стек LAMP. Инструкции по установке вы найдете здесь (можно пропустить раздел 4, посвященный виртуальным хостам).
1: Создание базы данных
Прежде всего мы создадим базу данных и таблицу. Подключитесь к серверу по SSH и войдите в MySQL как root:
sudo mysql -u root -p
Введите root-пароль сервера MySQL и нажмите Enter, чтобы продолжить. Затем выполните следующую команду, чтобы создать базу данных по имени cron_jobs.
CREATE DATABASE cron_jobs;
Создайте для базы данных пользователя без привилегий root. Учетные данные этого пользователя потребуются вам позже для подключения к базе данных cron_jobs из PHP. Не забудьте заменить EXAMPLE_PASSWORD надежным паролем:
CREATE USER 'cron_jobs_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD';
GRANT ALL PRIVILEGES ON cron_jobs.* TO 'cron_jobs_user'@'localhost';
FLUSH PRIVILEGES;
Затем перейдите в эту БД:
USE cron_jobs;
Database changed
После этого создайте таблицу tasks. В эту таблицу мы вставим несколько задач, которые будут автоматически выполняться демоном cron. Поскольку минимальный интервал между двумя задачами cron составляет 1 минуту, позже мы создадим сценарий PHP, который позволит нам преодолеть это ограничение и будет выполнять задачи с интервалом в 5 секунд.
А пока создайте таблицу:
CREATE TABLE tasks (
task_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
task_name VARCHAR(50),
queued_at DATETIME,
completed_at DATETIME,
is_processed CHAR(1)
) ENGINE = InnoDB;
Вставьте в таблицу три записи. Используйте функцию MySQL NOW() в столбце queued_at, чтобы записать текущую дату и время помещения задачи в очередь. Для столбца completed_at используйте функцию CURDATE(), чтобы установить время по умолчанию 00:00:00. Позже, – по мере выполнения задач, – ваш скрипт обновит этот столбец:
INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 1', NOW(), CURDATE(), 'N');
INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 2', NOW(), CURDATE(), 'N');
INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 3', NOW(), CURDATE(), 'N');
После выполнения каждой команды INSERT вы получите:
Query OK, 1 row affected (0.00 sec)
...
Убедитесь, что данные на своем месте, выполнив оператор SELECT для таблицы tasks:
SELECT task_id, task_name, queued_at, completed_at, is_processed FROM tasks;
Вы найдете список всех задач:
+---------+-----------+---------------------+---------------------+--------------+
| task_id | task_name | queued_at | completed_at | is_processed |
+---------+-----------+---------------------+---------------------+--------------+
| 1 | TASK 1 | 2021-03-06 06:27:19 | 2021-03-06 00:00:00 | N |
| 2 | TASK 2 | 2021-03-06 06:27:28 | 2021-03-06 00:00:00 | N |
| 3 | TASK 3 | 2021-03-06 06:27:36 | 2021-03-06 00:00:00 | N |
+---------+-----------+---------------------+---------------------+--------------+
3 rows in set (0.00 sec)
В столбце completed_at установлено время 00:00:00, и далее этот столбец будет обновляться – после обработки задач сценарием PHP, который мы создадим вскоре.
Выйдите из командной строки MySQL:
QUIT;
Bye
Теперь у вас есть база данных cron_jobs и таблица tasks. Приступим к написанию сценария PHP, который обрабатывает задачи.
2: Создание PHP-скрипта
На этом шаге мы напишем сценарий, который комбинирует цикл PHP while(…){…} и функцию sleep, что позволяет ему выполнять задачи через каждые 5 секунд.
Откройте новый файл /var/www/html/tasks.php в корневом каталоге вашего веб-сервера:
sudo nano /var/www/html/tasks.php
Создайте новый блок try { после тега <?php и объявите переменные базы данных, которую вы создали в разделе 1. Не забудьте заменить EXAMPLE_PASSWORD настоящим паролем вашего пользователя базы данных:
<?php try { $db_name = 'cron_jobs'; $db_user = 'cron_jobs_user'; $db_password = 'EXAMPLE_PASSWORD'; $db_host = 'localhost';
Объявите новый класс PDO (что значит PHP Data Object) и установите атрибут ERRMODE_EXCEPTION для перехвата ошибок PDO. Кроме того, нужно установить значение false для параметра ATTR_EMULATE_PREPARES, чтобы позволить собственному ядру базы данных MySQL обрабатывать эмуляцию. Эти операторы позволяют отправлять SQL-запросы и данные отдельно – для повышения безопасности и снижения вероятности SQL-инъекций:
$pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Затем создайте переменную $loop_expiry_time и установите в качестве ее значения текущее время плюс 60 секунд. Затем откройте новый оператор PHP while(time() < $loop_expiry_time). Идея этого фрагмента состоит в том, чтобы создать цикл, который выполняется до тех пор, пока текущее время (time()) не совпадет с переменной $loop_expiry_time:
$loop_expiry_time = time() + 60; while (time() < $loop_expiry_time) {
Затем объявите подготовленный SQL-оператор, который извлекает необработанные задачи из таблицы:
$data = [];
$sql = "select
task_id
from tasks
where is_processed = :is_processed
";
Выполните команду SQL и выберите из таблицы tasks все строки, в которых для столбца is_processed установлено значение N – это означает, что строки не обрабатываются.
$data['is_processed'] = 'N'; $stmt = $pdo->prepare($sql); $stmt->execute($data);
Затем выполните цикл по извлеченным строкам с помощью оператора while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {…} и создайте еще один SQL оператор. На этот раз команда будет обновлять столбцы is_processed и completed_at для каждой обработанной задачи. Это гарантирует, что скрипт не будет обрабатывать задачи более одного раза:
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $data_update = []; $sql_update = "update tasks set is_processed = :is_processed, completed_at = :completed_at where task_id = :task_id "; $data_update = [ 'is_processed' => 'Y', 'completed_at' => date("Y-m-d H:i:s"), 'task_id' => $row['task_id'] ]; $stmt = $pdo->prepare($sql_update); $stmt->execute($data_update); }
Примечание: Если вам нужно обработать большую очередь (например, 100 000 записей в секунду), вы можете рассмотреть возможность создания очереди на сервере Redis, поскольку он быстрее, чем MySQL, и больше подходит для обработки подобных объемов.
Прежде чем закрыть первый оператор while (time() < $loop_expiry_time) {, включите оператор sleep(5), чтобы приостановить выполнение задач на 5 секунд и освободить ресурсы сервера.
Вы можете изменить этот интервал в зависимости от вашей бизнес-логики и того, насколько быстро вы хотите выполнять задачи. Например, если вы хотите, чтобы задачи обрабатывались 3 раза в минуту, установите в sleep значение 20.
Не забудьте добавить catch, чтобы перехватить сообщения об ошибках PDO внутри блока } catch (PDOException $ex) { echo $ex->getMessage(); }:
sleep(5); } } catch (PDOException $ex) { echo $ex->getMessage(); }
Готовый скрипт tasks.php будет иметь следующий вид:
<?php try { $db_name = 'cron_jobs'; $db_user = 'cron_jobs_user'; $db_password = 'EXAMPLE_PASSWORD'; $db_host = 'localhost'; $pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $loop_expiry_time = time() + 60; while (time() < $loop_expiry_time) { $data = []; $sql = "select task_id from tasks where is_processed = :is_processed "; $data['is_processed'] = 'N'; $stmt = $pdo->prepare($sql); $stmt->execute($data); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $data_update = []; $sql_update = "update tasks set is_processed = :is_processed, completed_at = :completed_at where task_id = :task_id "; $data_update = [ 'is_processed' => 'Y', 'completed_at' => date("Y-m-d H:i:s"), 'task_id' => $row['task_id'] ]; $stmt = $pdo->prepare($sql_update); $stmt->execute($data_update); } sleep(5); } } catch (PDOException $ex) { echo $ex->getMessage(); }
Сохраните файл, нажав Ctrl + X, Y, затем Enter.
Завершив написание скрипта в файле /var/www/html/tasks.php, мы можем автоматизировать запуск этого скрипта с помощью демона cron. Демон будет запускать скрипт через 1 минуту на следующем шаге.
3: Планирование запуска PHP-скрипта в cron
В Linux вы можете запланировать автоматический запуск задач по истечении установленного времени. Для этого нужно добавить команду в файл crontab. На этом этапе мы настроим crontab для запуска сценария /var/www/html/tasks.php каждую минуту. Итак, откройте файл /etc/crontab с помощью nano:
sudo nano /etc/crontab
Затем добавьте в конец файла следующую строку, чтобы перезапускать http://localhost/tasks.php каждую минуту:
...
* * * * * root /usr/bin/wget -O - http://localhost/tasks.php
Сохраните и закройте файл.
В этом руководстве предполагается, что у вас есть базовые знания о том, как работают задачи cron.
Читайте также: Автоматизация задач с помощью Cron
Как говорилось ранее, демон cron запускает файл tasks.php только раз в 1 минуту, но после первого выполнения файла он будет перебирать открытые задачи в цикле еще 60 секунд. По истечении времени цикла демон cron снова запустит файл, и процесс продолжится.
Обновив /etc/crontab, демон crontab должен немедленно начать выполнение задач MySQL, которые вы вставили в таблицу tasks. Чтобы убедиться, что все работает должным образом, запросите свою базу данных cron_jobs.
4: Тестирование настройки
На этом этапе мы снова откроем свою базу данных и проверим, обрабатывает ли файл tasks.php поставленные в очередь задачи при автоматическом запуске через crontab.
Войдите на свой сервер MySQL как root:
sudo mysql -u root -p
Затем введите root пароль MySQL и нажмите Enter, чтобы продолжить. Затем перейдите в базу данных:
USE cron_jobs;
Database changed
Запустите оператор SELECT для таблицы tasks:
SELECT task_id, task_name, queued_at, completed_at, is_processed FROM tasks;
Вы получите примерно следующий результат. Задачи в столбце completed_at были обработаны с интервалом в 5 секунд. Кроме того, задачи отмечены как выполненные – в столбце is_processed теперь установлено значение Y, что означает YES.
+---------+-----------+---------------------+---------------------+--------------+
| task_id | task_name | queued_at | completed_at | is_processed |
+---------+-----------+---------------------+---------------------+--------------+
| 1 | TASK 1 | 2021-03-06 06:27:19 | 2021-03-06 06:30:01 | Y |
| 2 | TASK 2 | 2021-03-06 06:27:28 | 2021-03-06 06:30:06 | Y |
| 3 | TASK 3 | 2021-03-06 06:27:36 | 2021-03-06 06:30:11 | Y |
+---------+-----------+---------------------+---------------------+--------------+
3 rows in set (0.00 sec)
Это значит, что PHP-скрипт работает правильно; задачи были запущены в более короткий интервал времени, переопределив ограничение crontab.
Заключение
В этом руководстве вы создали БД, а затем составили список задач и поместили его в таблицу. С помощью сценария PHP задачи запускаются с интервалом в 5 секунд. Используя логику из этого мануала, вы можете реализовать и более сложное приложение на основе очереди заданий, в котором задачи должны выполняться несколько раз в течение 1 минуты.
Tags: cron, crontab, PHP, Ubuntu 20.04