Пакетная обработка данных в PHP

Как создавать долгосрочные задачи

Что делать, если в вашем Web-приложении имеется функция, выполнение которой занимает больше, чем секунду или две? Необходимо найти способ офлайновой обработки. Ознакомьтесь с несколькими методами офлайнового обслуживания долгосрочных операций в вашем PHP-приложении.

Джек Д Херрингтон, главный инженер-программист, Leverage Software Inc.

Джек Д. Херрингтон (Jack D. Herrington) - главный инженер-программист с более чем двадцатилетним опытом работы. Он автор трех книг: "Генерирование кода в действии", "Podcasting Hacks" и "PHP Hacks". Написал более 30 статей. Вы можете связаться с Джеком по адресу jherr@pobox.com.



03.05.2007

Крупные сети продовольственных магазинов сталкиваются с большими проблемами. Когда люди покупают продукты, в каждом магазине ежедневно совершаются тысячи транзакций. Управляющим необходимо обрабатывать имеющиеся данные. Какие товары пользуются спросом? Какие нет? Где больше спрос на товары органического происхождения? Как обстоит дело с мороженым?

Для сбора этих данных организация должна свести все эти транзактные данные в модель данных, пригодную для формирования отчетов в том виде, в котором эти отчеты требуются сотрудникам компании. Однако это занимает время, и с расширением сети магазинов обработка данных, полученных за день, может выполняться дольше, чем один день. Вот в чём суть проблемы.

Ваше Web-приложение может обрабатывать не такие большие объемы данных, но на любом сайте могут быть процессы, требующие больше времени для выполнения, чем согласен ждать клиент. Общепринятое время, в течение которого пользователь может ждать, прежде чем процесс покажется ему "медленным", составляет 200 миллисекунд. Эта цифра основана на работе настольных приложений, и я думаю, что Web приучил нас быть более терпеливыми. Тем не менее, вам едва ли захочется заставлять клиента ждать дольше нескольких секунд. Итак, в данной публикации приведено несколько стратегий для пакетной обработки данных в PHP.

Проза жизни и cron

Центральным игроком в пакетной обработке данных для машин под UNIX® является демон cron. Этот демон считывает файл конфигурации, который сообщает ему, какие командные строки выполнять, и как часто. Затем демон выполняет их, работая как часовой механизм. Он даже отправляет любые сообщения об ошибках по указанному e-mail адресу, так что вы можете устранять ошибки при их возникновении.

Я знаю, что инженеры, отстаивающие преимущества конкурирующей Web-технологии, чье название ассоциируется с кофе, качают сейчас головами. "Потоки! Потоки - вот верный способ выполнять обработку в пакетном режиме. Демон cron - это динозавр."

Я позволю себе с ними не согласиться.

Я работал и с тем, и с другим, и думаю, что cron имеет то преимущество, что в нём соблюдается принцип "Keep It Simple, Stupid" (KISS) - будь проще, дурачок. Он позволяет сохранить фоновую обработку простой. Вместо приложения с многопоточной обработкой задания, выполняемого бесконечно долго и никогда не освобождающего ресурсы памяти, вы имеете простой скрипт, который запускает cron. Скрипт определяет, есть ли работа, которая должна быть выполнена, выполняет ее, а затем его выполнение завершается. Не нужно волноваться об утечках памяти. Не нужно переживать, что поток остановится или попадет в ловушку бесконечного цикла.

Итак, как работает cron? Это зависит от вашего хостинг-решения. Я буду придерживаться старой доброй UNIX-версии cron с командной строкой, а вы можете проконсультироваться с вашим системным администратором, как можно реализовать её в вашем Web-приложении.

Ниже приведена простая настройка cron для выполнения PHP-скрипта раз в день в 11 часов вечера:

0 23 * * * jack /usr/bin/php /users/home/jack/myscript.php

Первые пять полей определяют время запуска скрипта. Затем идет имя пользователя, которое должно использоваться при запуске программы. Оставшаяся часть строки - командная строка для выполнения. В поле времени задаются минута, час, день месяца, месяц и день недели. Ниже приведено несколько примеров.

Команда:

15 * * * * jack /usr/bin/php /users/home/jack/myscript.php

запускает скрипт по отметке 15 минут каждый час.

Команда:

15,45 * * * * jack /usr/bin/php /users/home/jack/myscript.php

запускает скрипт по отметке 15 и 45 минут каждый час.

Команда:

*/1 3-23 * * * jack /usr/bin/php /users/home/jack/myscript.php

запускает скрипт каждую минуту с 3 ч. утра до 11 ч. вечера.

Команда:

30 23 * * 6 jack /usr/bin/php /users/home/jack/myscript.php

запускает скрипт в 11:30 вечера по субботам (день недели задан цифрой 6).

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

Используйте директиву MAILTO, чтобы указать, куда должны отправляться сообщения об ошибках в виде e-mail:

MAILTO=jherr@pobox.com

Примечание: Для пользователей Microsoft® Windows® существует аналогичная система "Назначенные задания" для запуска выполнения процессов из командной строки (как PHP-скрипт) через заданные промежутки времени.


Основы архитектуры пакетной обработки данных

Пакетная обработка данных - относительно простой процесс. В большинстве случаев она сводится к одной или двум последовательностям операций. Первая используется для отчетов: скрипт выполняется раз в день, генерирует отчет и рассылает его обозначенной группе людей. Вторая представляет собой непосредственно пакетное задание, выполняемое по запросу. Например, я регистрируюсь в некоем Web-приложении и прошу разослать всем зарегистрированным в системе пользователям сообщение о потрясающей новой функции. Это действие должно выполняться в пакетном режиме, поскольку в системе зарегистрировано 10000 пользователей. На выполнение такой задачи у PHP уйдет некоторое время, поэтому это должно быть сделано заданием вне браузера.

При выполнении второй последовательности операций Web-приложение просто сбрасывает информацию в область, использующуюся также приложением, выполняющим пакетную обработку. Эта информация определяет характер задания (например, "Отправить данное сообщение всем зарегистрированным в системе пользователям"). Программа обработки пакетов выполняет задание, а затем удаляет его. Иначе программа помечает задание как выполненное. В любом случае, задание должно быть помечено как выполненное, чтобы программа оно повторно не выполнялось.

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


Очередь сообщений

Первым вариантом является специализированная система очередей сообщений. В данной модели БД содержит таблицу со списком сообщений e-mail, которые должны быть разосланы разным людям. В Web-интерфейсе для добавления e-mail в очередь используется класс mailouts. Программа обработки e-mail использует класс mailouts для выборки ждущих обработки сообщений и их последующего удаления из очереди.

Модель начинается схемой MySQL.

Листинг 1. mailout.sql
DROP TABLE IF EXISTS mailouts;
CREATE TABLE mailouts (
  id MEDIUMINT NOT NULL AUTO_INCREMENT,
  from_address TEXT NOT NULL,
  to_address TEXT NOT NULL,
  subject TEXT NOT NULL,
  content TEXT NOT NULL,
  PRIMARY KEY ( id )
);

Эта схема достаточно проста. Каждая строка содержит адреса from (откуда) и to (куда) , а также тему и содержание e-mail.

Оболочкой таблицы mailouts в БД служит класс PHP mailouts.

Листинг 2. mailouts.php
<?php
require_once('DB.php');

class Mailouts
{
  public static function get_db()
  {
    $dsn = 'mysql://root:@localhost/mailout';
    $db =& DB::Connect( $dsn, array() );
    if (PEAR::isError($db)) { die($db->getMessage()); }
    return $db;
  }
  public static function delete( $id )
  {
    $db = Mailouts::get_db();
    $sth = $db->prepare( 'DELETE FROM mailouts WHERE id=?' );
    $db->execute( $sth, $id );
    return true;
  }
  public static function add( $from, $to, $subject, $content )
  {
    $db = Mailouts::get_db();
    $sth = $db->prepare( 'INSERT INTO mailouts VALUES (null,?,?,?,?)' );
    $db->execute( $sth, array( $from, $to, $subject, $content ) );
    return true;
  }
  public static function get_all()
  {
    $db = Mailouts::get_db();
    $res = $db->query( "SELECT * FROM mailouts" );
    $rows = array();
    while( $res->fetchInto( $row ) ) { $rows []= $row; }
    return $rows;
  }
}
?>

В скрипте использован класс доступа к БД Pear::DB. Затем он определяет класс mailouts с тремя основными статическими функциями: add), delete и get_all. Метод add() предназначен для добавления почтового сообщения в очередь и должен использоваться пользователем внешнего интерфейса. Метод get_all() возвращает все данные из таблицы. Метод delete() удаляет отдельный метод.

Вы, возможно, спросите, почему бы не ввести метод delete_all() - "удалить все", который вызывался бы в конце работы программы. Такого метода не существует по двум причинам: если я удаляю каждое сообщение после отправки, то исключаю возможность повторного отправления сообщения в случае перезапуска скрипта при возникновении неполадок; и, кроме того, между началом и завершением пакетного задания могут быть добавлены новые сообщения.

Следующим этапом является написание простой тестового скрипта для добавления записи в очередь.

Листинг 3. mailout_test_add.php
<?php
require 'mailout.php';

Mailouts::add( 'donotreply@mydomain.com',
  'molly@nocompany.com.org',
  'Test Subject',
  'This is a test of the batch mail sendout' );
?>

В данном случае я добавляю mailout и пишу Молли из какой-то компании (по адресу molly@nocompany.com.org), а также тему и содержание письма e-mail. Я могу запустить эту программу из командной строки: php mailout_test_add.php.

Для отправки сообщения e-mail мне нужен другой скрипт в качестве программы обработки.

Листинг 4. mailout_send.php
<?php
require_once 'mailout.php';

function process( $from, $to, $subject, $email ) {
  mail( $to, $subject, $email, "From: $from" );
}

$messages = Mailouts::get_all();
foreach( $messages as $msg ) {
  process( $msg[1], $msg[2], $msg[3], $msg[4] );
  Mailouts::delete( $msg[0] );
}
?>

В скрипте используется метод get_all() для выборки всех почтовых сообщений, а затем метод PHP mail() для поочередной отправки сообщений. После успешной рассылки всех сообщений, метод delete() удаляет эту отдельную запись из очереди.

Скрипт будет выполняться с заданным интервалом при помощи демона cron. Частота выполнения скрипта зависит от ваших потребностей и нужд вашего приложения.

Примечание: В информационном архиве PHP Extension and Application Repository (PEAR) имеется отличная практическая реализация системы очередей сообщений, которую можно загрузить бесплатно.


Ближе к делу

Замечательно, что найдено решение проблемы отправки почтовых сообщений, но как насчет чего-то более конкретного? Того, что позволит мне отправлять сообщения, генерировать отчеты и выполнять другие длительные операции без необходимости ждать при работе в браузере.

Воспользуемся тем преимуществом, что PHP является интерпретируемым языком, и сохраним код PHP в очереди в БД, чтобы выполнить его позже. Для этого нам потребуются две таблицы, что показано в Листинге 5.

Листинг 5. generic.sql
DROP TABLE IF EXISTS processing_items;
CREATE TABLE processing_items (
  id MEDIUMINT NOT NULL AUTO_INCREMENT,
  function TEXT NOT NULL,
  PRIMARY KEY ( id )
);

DROP TABLE IF EXISTS processing_args;
CREATE TABLE processing_args (
  id MEDIUMINT NOT NULL AUTO_INCREMENT,
  item_id MEDIUMINT NOT NULL,
  key_name TEXT NOT NULL,
  value TEXT NOT NULL,
  PRIMARY KEY ( id )
);

Первая таблица, processing_items, содержит функции вызываемые программой обработки заданий. Во второй таблице, processing_args, приведены значения аргументов, передаваемых функциям как хэш-таблица посредством пар ключ/значение.

Эти две таблицы, как и таблица mailouts, имеют в качестве оболочки PHP-класс ProcessingItems.

Листинг 6. generic.php
<?php
require_once('DB.php');

class ProcessingItems
{
  public static function get_db() { ... }
  public static function delete( $id )
  {
    $db = ProcessingItems::get_db();
    $sth = $db->prepare( 'DELETE FROM processing_args WHERE item_id=?' );
    $db->execute( $sth, $id );
    $sth = $db->prepare( 'DELETE FROM processing_items WHERE id=?' );
    $db->execute( $sth, $id );
    return true;
  }
  public static function add( $function, $args )
  {
    $db = ProcessingItems::get_db();

    $sth = $db->prepare( 'INSERT INTO processing_items VALUES (null,?)' );
    $db->execute( $sth, array( $function ) );

    $res = $db->query( "SELECT last_insert_id()" );
    $id = null;
    while( $res->fetchInto( $row ) ) { $id = $row[0]; }

    foreach( $args as $key => $value )
    {
        $sth = $db->prepare( 'INSERT INTO processing_args
  VALUES (null,?,?,?)' );
        $db->execute( $sth, array( $id, $key, $value ) );
    }

    return true;
  }
  public static function get_all()
  {
    $db = ProcessingItems::get_db();

    $res = $db->query( "SELECT * FROM processing_items" );
    $rows = array();
    while( $res->fetchInto( $row ) )
    {
        $item = array();
        $item['id'] = $row[0];
        $item['function'] = $row[1];
        $item['args'] = array();

        $ares = $db->query( "SELECT key_name, value FROM
   processing_args WHERE item_id=?", $item['id'] );
        while( $ares->fetchInto( $arow ) )
            $item['args'][ $arow[0] ] = $arow[1];

        $rows []= $item;
    }
    return $rows;
  }
}
?>

Этот класс имеет три важных метода: add(), get_all() и delete(). Как и система mailouts, машина предварительной обработки данных использует add(), а обрабатывающий движок - get_all() и delete().

Тестовый скрипт добавления элемента в обрабатывающийся фрагмент очереди приведен в Листинге 7.

Листинг 7. generic_test_add.php
<?php
require_once 'generic.php';
ProcessingItems::add( 'printvalue', array( 'value' => 'foo' ) );
?>

В данном случая я обращаюсь к функции printvalue с аргументом value со значением foo. Я использую интерпретатор командной строки PHP для запуска скрипта и помещаю вызов метода в очередь. Затем я использую следующий скрипт обработки для выполнения метода.

Листинг 8. generic_process.php
<?php
require_once 'generic.php';

function printvalue( $args ) {
  echo 'Printing: '.$args['value']."\n";
}

foreach( ProcessingItems::get_all() as $item ) {
  call_user_func_array( $item['function'],
    array( $item['args'] ) );
  ProcessingItems::delete( $item['id'] );
}
?>

Этот скрипт удивительно прост. Он берет обрабатываемые элементы, возвращенные методом get_all(), а затем использует call_user_func_array - внутреннюю функцию PHP - для вызова метода динамически с заданными аргументами. В этом случае вызывается локальная функция printvalue.

Для иллюстрации этой возможности я покажу, что происходит в командной строке:

% php generic_test_add.php 
% php generic_process.php 
Printing: foo
%

Здесь не на что особо смотреть, но вы уловили суть: посредством данного механизма вы можете отложить на более позднее время выполнение любой функции PHP.

Мысль о размещении имен функций PHP и их аргументов в БД вызывает у вас отвращение - альтернативой является включение карты схемы в PHP-код, содержащий соответствие имени "типа задания на обработку данных", хранящегося в БД, и действительной функции обработки PHP. Таким образом, если потом вы задумаете изменить серверную часть обработки PHP на что-то другое, программа будет работать, если совпадают строки "типа задания на обработку данных".


Отказ от использования баз данных

В завершение серии примеров программного кода я взгляну на проблему под другим углом, а именно, буду использовать файлы в директории, а не в БД для хранения пакетных заданий. Предлагая это, я не говорю вам "Делайте так, а не с помощью БД", а привожу альтернативный вариант проектного решения, к которому вы можете прибегнуть, (а можете этого и не делать) по своему усмотрению.

Несомненно, что схемы здесь нет, поскольку мы не используем БД. Поэтому я начну с класса, который содержит те же типы методов add(), get_all() и delete(), что и использованные в приведенном выше примере.

Листинг 9. batch_by_file.php
<?php
define( 'BATCH_DIRECTORY', 'batch_items/' );
class BatchFiles
{
  public static function delete( $id )
  {
    unlink( $id );
    return true;
  }
  public static function add( $function, $args )
  {
    $path = ';
    while( true )
    {
        $path = BATCH_DIRECTORY.time();
        if ( file_exists( $path ) == false )
            break;
    }

    $fh = fopen( $path, "w" );
    fprintf( $fh, $function."\n" );
    foreach( $args as $k => $v )
    {
        fprintf( $fh, $k.":".$v."\n" );
    }
    fclose( $fh );

    return true;
  }
  public static function get_all()
  {
    $rows = array();
    if (is_dir(BATCH_DIRECTORY)) {
        if ($dh = opendir(BATCH_DIRECTORY)) {
            while (($file = readdir($dh)) !== false) {
                $path = BATCH_DIRECTORY.$file;
                if ( is_dir( $path ) == false )
                {
                    $item = array();
                    $item['id'] = $path;
                    $fh = fopen( $path, 'r' );
                    if ( $fh )
                    {
                        $item['function'] = trim(fgets( $fh ));
                        $item['args'] = array();
                        while( ( $line = fgets( $fh ) ) != null )
                        {
                            $args = split( ':', trim($line) );
                            $item['args'][$args[0]] = $args[1];
                        }
                        $rows []= $item;
                        fclose( $fh );
                    }
                }
            }
            closedir($dh);
        }
    }
    return $rows;
  }
}
?>

Класс BatchFiles имеет три базовых метода: add(), get_all(), и delete(). Вместо обращения к БД, класс читает и записывает файлы из директории с именем batch_items.

Для добавления нового элемента пакета я использую следующий программный код.

Листинг 10. batch_by_file_test_add.php
<?php
require_once 'batch_by_file.php';

BatchFiles::add( "printvalue", array( 'value' => 'foo' ) );
?>

Стоит заметить, что за исключением имени класса - BatchFiles - нет никакого способа узнать, как было сохранено задание. Довольно просто потом изменить этот код так, чтобы хранение осуществлялось в БД, но без изменения интерфейса.

Наконец, вот код программы обработки.

Листинг 11. batch_by_file_processor.php
<?php
require_once 'batch_by_file.php';

function printvalue( $args ) {
  echo 'Printing: '.$args['value']."\n";
}

foreach( BatchFiles::get_all() as $item ) {
  call_user_func_array( $item['function'], array( $item['args'] ) );
  BatchFiles::delete( $item['id'] );
}
?>

Данная программа практически полностью совпадает с версией для БД, изменены только некоторые имена классов и файлов.


Заключение

Как уже было отмечено ранее, хорошо развита поддержка обработки сообщений на серверах для выполнения пакетной обработки в фоновом режиме. Конечно, в некоторых случаях немного проще прибегнуть к вспомогательному потоку для обработки небольших заданий. Но несложно заметить, что при помощи таких традиционных инструментальных средств, как cron, MySQL, стандартный объектно-ориентированный PHP, а также Pear::DB, пакетные задания в PHP-приложениях просто создавать, использовать и поддерживать в рабочем состоянии.

Ресурсы

Научиться

  • Оригинал статьи: Batch processing in PHP.
  • PHP.net - отличный ресурс для программистов на PHP.
  • PEAR Mail_Queue package - это основательная реализация очередей сообщений с машиной БД.
  • В справочном руководстве crontab приведено сложное, но подробное описание конфигурации cron.
  • Справочное руководство по PHP, в одном из разделов которого рассматривается использование PHP из командной строки. Изучение этого раздела поможет вам понять, как скрипт запускается демоном cron.
  • Узнайте больше о PHP, посетив раздел ресурсы по проекту PHP на IBM developerWorks.
  • Семинары и обучение на IBM developerWorks Россия.
  • Ознакомьтесь с перечнем планируемых конференций, демонстраций, Web-трансляций, и других событий во всем мире, которые могут быть интересны разработчикам ПО с открытым исходным кодом IBM.
  • Посетите раздел ПО с открытым исходным кодом на developerWorks для получения разнообразной информации о средствах и методах разработки ПО, инструментальных средств и обновлений проектов, что поможет вам в разработке технологий с открытым исходным кодом и использовании их с программными продуктами IBM.
  • Прослушайте интересные для разработчиков ПО интервью и дискуссии, посетив раздел подкасты на developerWorks.

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=216993
ArticleTitle=Пакетная обработка данных в PHP
publish-date=05032007