LoginSignup
7
5

More than 5 years have passed since last update.

cronでバッチ処理を実行しているサーバをダウンタイムゼロで換装する手法

Last updated at Posted at 2018-09-12

背景

突然の質問ですが、定期的に実行するJobをどのような形で管理していますでしょうか?
サーバレスが流行している2018年においても、昔ながらの cron を利用されている方もいらっしゃるのではないでしょうか?

この記事では、 cron でバッチ処理を実行しているサーバを、ダウンタイムゼロでリプレイスする方法を提案したいと思います。また、どうして、それが可能なのかを、cronのソースコードをちら見しつつ解説したいと思います。(時間の都合上、CentOS7 での cronieのみを取扱います。)

実運用を行う上では、きちんとしたジョブ管理システムを設計すべきではありますが、本記事では泥臭い手法を紹介します。

TL; DR

  • 新サーバを crond が停止した状態で用意
  • 最後のバッチがキックされたタイミングで、旧サーバの crond を止め、新サーバのcrondを起動
  • 最後のバッチの完了を見届け、旧サーバを完全停止

cronってなんでしたっけ?

cronとは、UNIX系OSで利用される時刻ベースのジョブスケジューラであり、定期実行されるタスクをスケジュールするのに適しています。システムの管理にも用いられますし、実サービスのタスク実行にも用いられます。

例えば、crontab -e により、以下のようなジョブを登録すると、毎時0分に集計バッチが実行されます。

0 * * * * /bin/bash -l -c '/home/vagrant/bin/execute_hourly_aggregation'

もっと詳しく知りたい方は、以下のWikipediaの記事や、そこのリファレンスとなっている各種ドキュメントを確認してください。

前提条件

本提案手法では、長時間かかるバッチがあったり、バッチの数が多いなど、常に何らかのバッチが実行されているような環境における、ダウンタイムゼロでのリプレイスを目指します。

しかし、毎分新しいジョブがキックされるようなサーバにおいては、本提案手法では厳密なダウンタイムゼロを達成できないのでお気をつけください。

環境情報

Vagrantで雑に立てた、以下の環境で説明します。解説対象のソースコードとのバージョンは少し異なりますが、大きな変更は入っていないので、仕組みの部分を理解していただければと思います。

[vagrant@localhost ~]$ cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core)
[vagrant@localhost ~]$ yum list installed | grep cron
Failed to set locale, defaulting to C
cronie.x86_64                   1.4.11-19.el7                   @anaconda
cronie-anacron.x86_64           1.4.11-19.el7                   @anaconda
crontabs.noarch                 1.11-6.20121102git.el7          @anaconda

手順

1. crondの停止した新サーバの準備

まずは、新のサーバを準備をしましょう。
ファイル構成としては、旧サーバと同じファイル構成で問題ありませんが、以下の2つを満たすように作ってください。

  • 旧サーバのcrondは動作していること
  • 新サーバのcrontab は旧サーバと同一の設定であること
  • 新サーバのcrond は停止していること

例えば、CentOS7系なら、 service コマンドを利用して止めちゃいましょう。

[vagrant@localhost ~]$ sudo service crond stop
Redirecting to /bin/systemctl stop crond.service
[vagrant@localhost ~]$ service crond status
Redirecting to /bin/systemctl status crond.service
● crond.service - Command Scheduler
   Loaded: loaded (/usr/lib/systemd/system/crond.service; enabled; vendor preset: enabled)
   Active: inactive (dead) since Tue 2018-09-11 17:44:33 UTC; 11s ago
  Process: 3406 ExecStart=/usr/sbin/crond -n $CRONDARGS (code=exited, status=0/SUCCESS)
 Main PID: 3406 (code=exited, status=0/SUCCESS)

2. ジョブがキックされなくなる時を待つ

cronのジョブがキックされなくなる時間を見計らってください。
ここで重要なのは、ジョブが完了するタイミングではなく、ジョブがキックされなくなるタイミングです。
ps auxfなどで crond 以下のプロセスを見つつ、旧サーバでの最後のバッチ処理がキックされたことを確認しましょう。

root      3492  0.0  0.3  26096  1704 ?        Ss   17:45   0:00 /usr/sbin/crond -n
root      3921  0.0  0.4  82144  2488 ?        S    18:05   0:00  \_ /usr/sbin/CROND -n
vagrant   3924  0.0  0.3  12992  1568 ?        Ss   18:05   0:00  |   \_ /bin/bash -l -c echo "start at $(date)" && sleep 120 && echo "end at $(date)"
vagrant   3937  0.0  0.0   7764   352 ?        S    18:05   0:00  |   |   \_ sleep 120

3. 旧サーバでcrondを停止し、新サーバでcrondを起動する

いよいよ、リプレイス作業に移ります。
まずは、旧サーバでcrondを停止し、新サーバでcrondを起動しましょう。
この時点で、旧サーバで、新たなバッチ処理がキックされることはなくなります。

4. 旧サーバでバッチ処理が完了するのを見届ける

crond は停止しましたが、 crond がキックしたジョブは残っています。バッチ処理が完了するのを待ってあげましょう。このとき、crond によりキックされたジョブは、大本の親を失い /usr/sbin/CROND 以下にぶら下がった状態になっています。

root      4199  0.0  0.4  82144  2488 ?        S    18:18   0:00 /usr/sbin/CROND -n
vagrant   4201  0.0  0.3  12992  1564 ?        Ss   18:18   0:00  \_ /bin/bash -l -c echo "start at $(date)" && sleep 120 && echo "end at $(date)"
vagrant   4214  0.0  0.0   7764   352 ?        S    18:18   0:00  |   \_ sleep 120

5. 旧サーバを完全に停止する

最後のバッチ処理が完了したのを見届けてから、丁重に停止してあげましょう。

種明かし

どうして、このようなことが可能なのでしょうか?
少し、cron のコードを読みつつ、実行時のプロセスの状態を再確認してみましょう。

まず、cronSIGINTSIGTERM のシグナルを受け取るまで、以下の処理をwhileループしています。

  • 1分 sleepする
  • 時刻をチェックして、必要なジョブを実行する
    while (!got_sigintterm) {
        int timeDiff;
        enum timejump wakeupKind;

        /* ... wait for the time (in minutes) to change ... */
        do {
            cron_sleep(timeRunning + 1, &database);
            set_time(FALSE);
        } while (!got_sigintterm && clockTime == timeRunning);
        if (got_sigintterm)
            break;
        timeRunning = clockTime;
// ~ 中略 ~
        handle_signals(&database);
    }

つまり、新しいバッチが実行されるのは、このチェックが走る(おおよそ)1分ごとということがわかります。

新しいバッチは、 find_jobs() によって発見され、 job_runqueue(), do_command(), child_process() を経由して、もともとの crond の孫プロセスとして実行されます。

root      3492  0.0  0.3  26096  1704 ?        Ss   17:45   0:00 /usr/sbin/crond -n
root      3921  0.0  0.4  82144  2488 ?        S    18:05   0:00  \_ /usr/sbin/CROND -n
vagrant   3924  0.0  0.3  12992  1568 ?        Ss   18:05   0:00  |   \_ /bin/bash -l -c echo "start at $(date)" && sleep 120 && echo "end at $(date)"
vagrant   3937  0.0  0.0   7764   352 ?        S    18:05   0:00  |   |   \_ sleep 120

では、ここで SIGTERM によって停止するとどうなるのでしょうか?
最後のお掃除部分のコードを見てみましょう。


#if defined WITH_INOTIFY
    if (inotify_enabled && (EnableClustering != 1))
        set_cron_unwatched(fd);

    if (fd >= 0 && close(fd) < 0)
        log_it("CRON", pid, "INFO", "Inotify close failed", errno);
#endif

    log_it("CRON", pid, "INFO", "Shutting down", 0);

    (void) unlink(_PATH_CRON_PID);

    return 0;

気づきましたでしょうか。子や孫プロセスをwaitすることも、killすることもせずに、子に孫の処理を託して、親はひっそりと死んでいるのです。

root      4199  0.0  0.4  82144  2488 ?        S    18:18   0:00 /usr/sbin/CROND -n
vagrant   4201  0.0  0.3  12992  1564 ?        Ss   18:18   0:00  \_ /bin/bash -l -c echo "start at $(date)" && sleep 120 && echo "end at $(date)"
vagrant   4214  0.0  0.0   7764   352 ?        S    18:18   0:00  |   \_ sleep 120

その結果、新規ジョブはキックされない一方で、すでにキック済みのジョブは最後まで完了させることが可能になっているのです。(ちょっぴり切ないですね)

まとめ

crond はとてもシンプルに実装されているので、本提案手法のような雑な方法でも、ダウンタイムゼロでのリプレイスが可能になっています。もちろん、実運用においては、きちんとしたシステム化をすべき領域ではありますが、身近にある cron にも興味を持ってもらえたら幸いです。

参考

7
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5