Ubuntu20.04でCrontabを使用して1分間に複数回PHPジョブを実行する方法
著者は、 Write for DOnations プログラムの一環として、 Girls WhoCodeを選択して寄付を受け取りました。
序章
Linuxでは、用途の広い crontabツールを使用して、特定の時間にバックグラウンドで長時間実行されるタスクを処理できます。 デーモンは反復的なタスクを実行するのに最適ですが、1つの制限があります。タスクを実行できるのは、最小時間間隔1分のみです。
ただし、多くのアプリケーションでは、ユーザーエクスペリエンスの低下を防ぐために、ジョブをより頻繁に実行することをお勧めします。 たとえば、ジョブキューモデルを使用してWebサイトでファイル処理タスクをスケジュールしている場合、大幅な待機はエンドユーザーに悪影響を及ぼします。
もう1つのシナリオは、ジョブキューモデルを使用して、クライアントがアプリケーションで特定のタスク(たとえば、受信者に送金する)を完了した後、テキストメッセージまたは電子メールをクライアントに送信するアプリケーションです。 ユーザーが確認メッセージの配信まで1分待たなければならない場合、ユーザーはトランザクションが失敗したと考え、同じトランザクションを繰り返そうとする可能性があります。
これらの課題を克服するために、crontabデーモンが1分後に再度呼び出すのを待つ間、タスクを60秒間繰り返しループして処理するPHPスクリプトをプログラムできます。 PHPスクリプトがcrontabデーモンによって初めて呼び出されると、ユーザーを待たせることなく、アプリケーションのロジックに一致する期間でタスクを実行できます。
このガイドでは、Ubuntu20.04サーバー上にサンプルcron_jobs
データベースを作成します。 次に、tasks
テーブルと、PHPwhile(...){...}
ループおよびsleep()
関数を使用して5秒間隔でテーブル内のジョブを実行するスクリプトを設定します。
前提条件
このチュートリアルを完了するには、次のものが必要です。
-
root以外のユーザーでセットアップされたUbuntu20.04サーバー。 Ubuntu20.04を使用したサーバーの初期設定ガイドに従ってください。
-
サーバーにセットアップされたLAMPスタック。 Linux、Apache、MySQL、PHP(LAMP)スタックをUbuntu20.04にインストールする方法ガイドを参照してください。 このチュートリアルでは、ステップ4 —Webサイトの仮想ホストの作成をスキップできます。
ステップ1—データベースのセットアップ
このステップでは、サンプルデータベースとテーブルを作成します。 まず、サーバーにSSH
して、rootとしてMySQLにログインします。
- sudo mysql -u root -p
MySQLサーバーのrootパスワードを入力し、ENTER
を押して続行します。 次に、次のコマンドを実行して、cron_jobs
データベースを作成します。
- CREATE DATABASE cron_jobs;
データベースのroot以外のユーザーを作成します。 PHPからcron_jobs
データベースに接続するには、このユーザーの資格情報が必要です。 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;
次に、cron_jobs
データベースに切り替えます。
- USE cron_jobs;
OutputDatabase changed
データベースを選択したら、tasks
テーブルを作成します。 この表には、cronジョブによって自動的に実行されるいくつかのタスクを挿入します。 cronジョブを実行するための最小時間間隔は1
分であるため、後でこの設定をオーバーライドするPHPスクリプトをコーディングし、代わりに5秒間隔でジョブを実行します。
今のところ、tasks
テーブルを作成します。
- 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;
タスクテーブルに3つのレコードを挿入します。 queued_at
列のMySQLNOW()
関数を使用して、タスクがキューに入れられた現在の日時を記録します。 また、completed_at
列の場合は、MySQL 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
コマンドを実行した後、出力を確認します。
OutputQuery OK, 1 row affected (0.00 sec)
...
tasks
テーブルに対してSELECT
ステートメントを実行して、データが適切に配置されていることを確認します。
- SELECT task_id, task_name, queued_at, completed_at, is_processed FROM tasks;
すべてのタスクのリストがあります。
Output+---------+-----------+---------------------+---------------------+--------------+
| 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;
OutputBye
これで、cron_jobs
データベースとtasks
テーブルが配置され、ジョブを処理するPHPスクリプトを作成できるようになりました。
ステップ2—5秒後にタスクを実行するPHPスクリプトを作成する
このステップでは、PHP while(...){...}
ループとsleep
関数の組み合わせを使用して、5秒ごとにタスクを実行するスクリプトを作成します。
nanoを使用して、Webサーバーのルートディレクトリにある新しい/var/www/html/tasks.php
ファイルを開きます。
- sudo nano /var/www/html/tasks.php
次に、<?php
タグの後に新しいtry {
ブロックを作成し、手順1で作成したデータベース変数を宣言します。 EXAMPLE_PASSWORD
をデータベースユーザーの実際のパスワードに置き換えることを忘れないでください。
<?php
try {
$db_name = 'cron_jobs';
$db_user = 'cron_jobs_user';
$db_password = 'EXAMPLE_PASSWORD';
$db_host = 'localhost';
次に、新しいPDO(PHPデータオブジェクト)クラスを宣言し、属性ERRMODE_EXCEPTION
を設定してPDOエラーをキャッチします。 また、ATTR_EMULATE_PREPARES
をfalse
に切り替えて、ネイティブ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秒を加えた値に設定します。 次に、新しいPHPwhile(time() < $loop_expiry_time) {
ステートメントを開きます。 ここでの考え方は、現在の時刻(time()
)が変数$loop_expiry_time
と一致するまで実行されるループを作成することです。
[label /var/www/html/tasks.php]
$loop_expiry_time = time() + 60;
while (time() < $loop_expiry_time) {
次に、tasks
テーブルから未処理のジョブを取得する準備済みのSQLステートメントを宣言します。
[label /var/www/html/tasks.php]
$data = [];
$sql = "select
task_id
from tasks
where is_processed = :is_processed
";
SQLコマンドを実行し、tasks
テーブルからis_processed
列がN
に設定されているすべての行をフェッチします。 これは、行が処理されないことを意味します。
[label /var/www/html/tasks.php]
$data['is_processed'] = 'N';
$stmt = $pdo->prepare($sql);
$stmt->execute($data);
次に、PHP while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {...}
ステートメントを使用して取得した行をループし、別のSQLステートメントを作成します。 今回は、SQLコマンドにより、処理されたタスクごとにis_processed
列とcompleted_at
列が更新されます。 これにより、タスクを複数回処理しないようになります。
[label /var/www/html/tasks.php]
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);
}
注:処理するキューが大きい場合(たとえば、1秒あたり100,000レコード)、MySQLよりも高速であるためRedisサーバーでジョブをキューに入れることを検討してください。ジョブキューモデルの実装になります。 それでも、このガイドではより小さなデータセットを処理します。
最初のPHPwhile (time() < $loop_expiry_time) {
ステートメントを閉じる前に、sleep(5);
ステートメントを含めて、ジョブの実行を5秒間一時停止し、サーバーリソースを解放します。
ビジネスロジックとタスクの実行速度に応じて、5秒の期間を変更できます。 たとえば、タスクを1分間に3回処理する場合は、この値を20秒に設定します。
} catch (PDOException $ex) { echo $ex->getMessage(); }
ブロック内のPDOエラーメッセージをcatch
することを忘れないでください。
[label /var/www/html/tasks.php]
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
ファイルのロジックのコーディングが完了したら、次のステップで1分ごとにファイルを実行するようにcrontabデーモンをスケジュールします。
ステップ3—1分後に実行するPHPスクリプトのスケジュール
Linuxでは、crontabファイルにコマンドを入力することにより、規定の時間後に自動的に実行されるようにジョブをスケジュールできます。 このステップでは、毎分後に/var/www/html/tasks.php
スクリプトを実行するようにcrontabデーモンに指示します。 したがって、nanoを使用して/etc/crontab
ファイルを開きます。
- sudo nano /etc/crontab
次に、ファイルの最後に次を追加して、1
分ごとにhttp://localhost/tasks.php
を実行します。
...
* * * * * root /usr/bin/wget -O - http://localhost/tasks.php
ファイルを保存して閉じます。
このガイドは、cronジョブがどのように機能するかについての基本的な知識があることを前提としています。 Cronを使用してUbuntuでタスクを自動化する方法に関するガイドを読むことを検討してください。
前に示したように、cronデーモンは1分ごとにtasks.php
ファイルを実行しますが、ファイルが初めて実行されると、開いているタスクをさらに60秒間ループします。 ループ時間が経過するまでに、cronデーモンがファイルを再度実行し、プロセスが続行されます。
/etc/crontab
ファイルを更新して閉じた後、crontabデーモンはtasks
テーブルに挿入したMySQLタスクの実行をすぐに開始する必要があります。 すべてが期待どおりに機能しているかどうかを確認するには、次にcron_jobs
データベースにクエリを実行します。
ステップ4—ジョブの実行を確認する
このステップでは、データベースをもう一度開いて、tasks.php
ファイルがcrontabによって自動的に実行されたときにキューに入れられたジョブを処理しているかどうかを確認します。
rootとしてMySQLサーバーに再度ログインします。
- sudo mysql -u root -p
次に、MySQLサーバーのルートパスワードを入力し、ENTER
を押して続行します。 次に、データベースに切り替えます。
- USE cron_jobs;
OutputDatabase changed
tasks
テーブルに対してSELECT
ステートメントを実行します。
SELECT task_id, task_name, queued_at, completed_at, is_processed FROM tasks;
次のような出力が表示されます。 completed_at
列では、タスクは5
秒間隔で処理されています。 また、is_processed
列がY
、つまりYES
に設定されているため、タスクは完了としてマークされています。
Output
+---------+-----------+---------------------+---------------------+--------------+
| 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デーモンによって設定された1分間の制限をオーバーライドすることにより、より短い時間間隔でタスクを実行しました。
結論
このガイドでは、Ubuntu20.04サーバーにサンプルデータベースをセットアップしました。 次に、テーブルにジョブを作成し、PHPwhile(...){...}
ループおよびsleep()
関数を使用して5秒間隔で実行します。 次に、タスクを1分間に複数回実行する必要があるジョブキューベースのアプリケーションを実装する場合は、このチュートリアルのロジックを使用してください。
その他のPHPチュートリアルについては、PHPトピックページをご覧ください。