ホーム » 技術 » Linux » CentOS7でJDTを試してみた(cron編)

SAKURA Internet Inc.

アーカイブ

CentOS7でJDTを試してみた(cron編)

最近ICT界隈ではサマータイムが実施されるのではないかという話題でもちきりです。できればサマータイムなど導入されなければいいのですが、万が一導入が決定されてしまったらどうすればいいのでしょうか。そもそもサマータイムが実施された場合、どんなことが起こるでしょうか? 現在検討されている実施案では2時間シフトするという話が出ています。これはすなわち、サマータイム実施日に2時間減ったり、終了日に増えたりするということを意味しています。サーバ管理をしている方なら、その間にスケジュールしているcron jobはどうなってしまうのかと心配になることでしょう。

というわけで、CentOS7のcronがどのように振舞うのかを実験してみました。

問題の整理

仮にサマータイムが実施されるとすると、具体的にどんなことが起こるかを整理しておきましょう。

サマータイム実施日には、2時間時計を進めることになります。たとえばニューヨークでは、午前2時に時計を1時間進めて午前3時にするそうです。日本では2時間のシフトを検討しているようなので、これはすなわち 01:59:59(JST) の次が 04:00:00(JDT) になるということです。

サーバ管理をしていると、cronのことが心配になってきます。2時から4時の間といえば、よく夜間バッチをスケジュールする時間帯にあたるからです。たとえば以下の例では、毎週日曜日の2時29分にLet’s Encryptの証明書期限をチェックして、必要があれば再起動を行うバッチです。

29 2 * * sun root /usr/bin/certbot renew --post-hook "systemctl restart httpd" | logger -t certbot-renew -p local0.info

しかし、上述の通りサマータイム実施日には2時から4時の時間が消えてなくなってしまいます。サマータイム開始日にこのjobがあたってしまうと、実行されないことになるのです。

そして、サマータイム終了日には時計が巻き戻ります。境界バグが怖いので、午前3時になったら1時に戻すことにしましょう。つまり 02:59:59(JDT)  の次が 01:00:00(JST) になるということです。すると今度は、午前1時から3時の2時間が2度訪れることになります。もしcronが淡々と仕事をするならば、上記の「2時29分」にスケジュールされたjobは1日に2度実行されることになります。

Let’s Encryptの期限チェックは、1週間ぐらいスキップされても特に問題はありませんし、一日に2度実行しても文句は言われないと思います。でも、世の中にはそうはいかないバッチがたくさんあるはずです。それらのcron jobはどうなってしまうのか? というのがここで検討したい課題です。

cronの対応状況

まず、CentOS7のcronのmanpageを見てみます。

   Daylight Saving Time and other time changes
       Local time changes of less than three hours, such as  those  caused  by
       the  Daylight  Saving Time changes, are handled in a special way.  This
       only applies to jobs that run at a specific time and jobs that run with
       a granularity greater than one hour.  Jobs that run more frequently are
       scheduled normally.

       If time was adjusted one hour forward, those jobs that would  have  run
       in  the  interval  that has been skipped will be run immediately.  Con‐
       versely, if time was adjusted backward, running the same job  twice  is
       avoided.

       Time  changes  of more than 3 hours are considered to be corrections to
       the clock or the timezone, and the new time is used immediately.
  • サマータイムなどの時刻変更について
    • サマータイムに伴うような、3時間未満のローカルタイムの変更は、cronは特別な処理を行う
    • これは、特定の時刻に実行されるjobと、1時間を超える粒度で実行されるjobに対して実施される
    • より頻繁に実行されるjobは通常通り扱われる
    • もし時刻が1時間前にずれ、その間に実施されるはずだったスキップされたjobは、直ちに実行される
    • 一方、時刻が1時間後ろにずれ、同じjobが2度目に実行されることになった場合は無効となる
    • 3時間以上の時間変更は、単なる時刻修正またはタイムゾーンの変更と見なし、新しい時刻をそのまま適用する

なんだかcronは、サマータイムにはちゃんと対応しているよ! と書いてあるみたいです。ただ「1時間を超える粒度」という言葉など、意味が曖昧な部分もあります。これは1時間のうちに2度も3度も実行されるjobは特別処理されない、ということなのでしょうか? 実験をして、動作を確かめてみましょう。

実験

実験を一晩で終わらせるために、こんな設定を用意してみました。

  • サマータイムは19時開始。ここで2時間スキップする(18:59:59 → 21:00:00)
  • 終了は翌日の午前3時。ここで2時間巻き戻す(02:59:59 → 01:00:00)
  • その間に、各種設定のcronを実行しておく
  • jobは単純にタイムスタンプを押すだけ。これをファイルに保存しておく

まずはzoneinfoの準備です。前回のブログで示したとおり、zoneinfoを自前で用意すれば、自分の実験サーバを好きなようにサマータイムに設定できます。具体的にはこんな風に追記しました。

Rule    Japan   2018    only    -   Sep 5   19:00   2:00    D
Rule    Japan   2018    only    -   Sep 6    3:00   0   S

これをzicでコンパイルして、念のためサーバをリブートしてサーバの時刻をJSTに確定しておきます。

次にタイムスタンプを出力するプログラムです。当初Perlで簡単に書いたコードで実験したところ、どうもローカルタイムを取得する部分で動作がおかしいので、Cで書くことにしました。こんな感じです。

#include <stdio.h>
#include <time.h>

#define MAXSTR  256

int main(void)
{
        char    s[MAXSTR] ="";
        time_t  now;
        struct tm local, utc;

        now = time(NULL);
        gmtime_r(&now, &utc);
        tzset();
        localtime_r(&now, &local);

        printf("%ld\t", (long)now);
        strftime(s, MAXSTR, "%FT%TZ", &utc);
        printf("%s\t", s);
        strftime(s, MAXSTR, "%FT%T%z", &local);
        printf("%s\n", s);

        return 0;
}

実行すると、epoch、UTC、ローカルタイムの順に出力するだけの単純なコードです。

最後にcrontabを用意します。とりあえず思いつくパターンを20種類ほど用意して記述してみました。

*/1 * * * *    root    /root/bin/stamp >>/root/bin/log-per-min
0 * * * *      root    /root/bin/stamp >>/root/bin/log-per-hour00
1 * * * *      root    /root/bin/stamp >>/root/bin/log-per-hour01
59 * * * *     root    /root/bin/stamp >>/root/bin/log-per-hour59
0,30 * * * *   root    /root/bin/stamp >>/root/bin/log-00,30
15,45 * * * *  root    /root/bin/stamp >>/root/bin/log-15,45
0 */2 * * *    root    /root/bin/stamp >>/root/bin/log-per-2hour00
1 */2 * * *    root    /root/bin/stamp >>/root/bin/log-per-2hour01
59 */2 * * *   root    /root/bin/stamp >>/root/bin/log-per-2hour59
0 1-23/2 * * * root    /root/bin/stamp >>/root/bin/log-hour-odd
0 0-22/2 * * * root    /root/bin/stamp >>/root/bin/log-hour-even
0 0-23 * * * root     /root/bin/stamp >>/root/bin/log-hour-all
0 12 * * *     root    /root/bin/stamp >>/root/bin/log-12:00
0 13 * * *     root    /root/bin/stamp >>/root/bin/log-13:00
0 14 * * *     root    /root/bin/stamp >>/root/bin/log-14:00
0 15 * * *     root    /root/bin/stamp >>/root/bin/log-15:00
0 16 * * *     root    /root/bin/stamp >>/root/bin/log-16:00
0 17 * * *     root    /root/bin/stamp >>/root/bin/log-17:00
0 18 * * *     root    /root/bin/stamp >>/root/bin/log-18:00
0 19 * * *     root    /root/bin/stamp >>/root/bin/log-19:00
0 20 * * *     root    /root/bin/stamp >>/root/bin/log-20:00
0 21 * * *     root    /root/bin/stamp >>/root/bin/log-21:00
0 22 * * *     root    /root/bin/stamp >>/root/bin/log-22:00
0 23 * * *     root    /root/bin/stamp >>/root/bin/log-23:00
0 0 * * *      root    /root/bin/stamp >>/root/bin/log-00:00
0 1 * * *      root    /root/bin/stamp >>/root/bin/log-01:00
0 2 * * *      root    /root/bin/stamp >>/root/bin/log-02:00
0 3 * * *      root    /root/bin/stamp >>/root/bin/log-03:00
0 4 * * *      root    /root/bin/stamp >>/root/bin/log-04:00
0 5 * * *      root    /root/bin/stamp >>/root/bin/log-05:00

この状態で一晩待って、翌日に結果を分析してみました。

結果と分析

まず最初に、1分毎に実行したlog-per-minを確認してみます。サマータイム開始時間前後のログはこんな感じです。

*/1 * * * *
1536141301      2018-09-05T09:55:01Z    2018-09-05T18:55:01+0900
1536141361      2018-09-05T09:56:01Z    2018-09-05T18:56:01+0900
1536141421      2018-09-05T09:57:01Z    2018-09-05T18:57:01+0900
1536141481      2018-09-05T09:58:01Z    2018-09-05T18:58:01+0900
1536141541      2018-09-05T09:59:01Z    2018-09-05T18:59:01+0900
1536141601      2018-09-05T10:00:01Z    2018-09-05T21:00:01+1100
1536141661      2018-09-05T10:01:01Z    2018-09-05T21:01:01+1100
1536141721      2018-09-05T10:02:01Z    2018-09-05T21:02:01+1100
1536141781      2018-09-05T10:03:01Z    2018-09-05T21:03:01+1100
1536141841      2018-09-05T10:04:01Z    2018-09-05T21:04:01+1100
1536141901      2018-09-05T10:05:01Z    2018-09-05T21:05:01+1100

期待通り、18:59の次に21:00が記録されています。UTCでは連続していますので、きちんと1分毎に実行されており、中断や同時実行はないことが分かります。

また終了時も同様に、ローカルタイムが巻き戻っていることが分かります。これは、その日の1時(JDT)に実行したjobが、二度目に訪れた1時(JST)にも実行されたということを示しています。

*/1 * * * *
1536162901      2018-09-05T15:55:01Z    2018-09-06T02:55:01+1100
1536162961      2018-09-05T15:56:01Z    2018-09-06T02:56:01+1100
1536163021      2018-09-05T15:57:01Z    2018-09-06T02:57:01+1100
1536163081      2018-09-05T15:58:01Z    2018-09-06T02:58:01+1100
1536163141      2018-09-05T15:59:01Z    2018-09-06T02:59:01+1100
1536163201      2018-09-05T16:00:01Z    2018-09-06T01:00:01+0900
1536163261      2018-09-05T16:01:01Z    2018-09-06T01:01:01+0900
1536163321      2018-09-05T16:02:01Z    2018-09-06T01:02:01+0900
1536163381      2018-09-05T16:03:01Z    2018-09-06T01:03:01+0900
1536163441      2018-09-05T16:04:01Z    2018-09-06T01:04:01+0900
1536163502      2018-09-05T16:05:02Z    2018-09-06T01:05:02+0900

またUTCではきちんと連続しており、矛盾はありません。

では1時間毎に起動している各種設定はどうでしょうか。時指定に「*」を使ったlog-per-hour00を見てみるとこんな感じです。

0 * * * *
1536130801      2018-09-05T07:00:01Z    2018-09-05T16:00:01+0900
1536134401      2018-09-05T08:00:01Z    2018-09-05T17:00:01+0900
1536138001      2018-09-05T09:00:01Z    2018-09-05T18:00:01+0900
1536141601      2018-09-05T10:00:01Z    2018-09-05T21:00:01+1100
1536145201      2018-09-05T11:00:01Z    2018-09-05T22:00:01+1100
1536148801      2018-09-05T12:00:01Z    2018-09-05T23:00:01+1100
1536152401      2018-09-05T13:00:01Z    2018-09-06T00:00:01+1100
1536156001      2018-09-05T14:00:01Z    2018-09-06T01:00:01+1100
1536159602      2018-09-05T15:00:02Z    2018-09-06T02:00:02+1100
1536163201      2018-09-05T16:00:01Z    2018-09-06T01:00:01+0900
1536166801      2018-09-05T17:00:01Z    2018-09-06T02:00:01+0900
1536170402      2018-09-05T18:00:02Z    2018-09-06T03:00:02+0900

このケースでも特別処理を受けていません。ローカルタイムでみるとサマータイム開始時には時間が飛んでいて、「19時、20時の分」の実行はされていません。一方サマータイムが終了して時間が巻き戻ったときに訪れた二度目の1時、2時のときはそのまま実行されています。UTCでは、連続した1時間毎の実行になっていて矛盾はありません。

これは2時間毎を示す「*/2」という記法でも同じ結果になります。log-per-2hour00を見てみましょう。

0 */2 * * *
1536130801      2018-09-05T07:00:01Z    2018-09-05T16:00:01+0900
1536138001      2018-09-05T09:00:01Z    2018-09-05T18:00:01+0900
1536145201      2018-09-05T11:00:01Z    2018-09-05T22:00:01+1100
1536152401      2018-09-05T13:00:01Z    2018-09-06T00:00:01+1100
1536159602      2018-09-05T15:00:02Z    2018-09-06T02:00:02+1100
1536166801      2018-09-05T17:00:01Z    2018-09-06T02:00:01+0900
1536174001      2018-09-05T19:00:01Z    2018-09-06T04:00:01+0900

UTCでみると連続した2時間毎の実行になっていますが、ローカルタイムでは開始時の20時の回はスキップされてしまい、終了時の2時の回は2度実行されていることになっています。

さて、一方で時刻を明記した場合を見てみましょう。すべての時刻を指定したlog-hour-allの結果は次の通りです。

0 0-23 * * *
1536130801      2018-09-05T07:00:01Z    2018-09-05T16:00:01+0900
1536134401      2018-09-05T08:00:01Z    2018-09-05T17:00:01+0900
1536138001      2018-09-05T09:00:01Z    2018-09-05T18:00:01+0900
1536141602      2018-09-05T10:00:02Z    2018-09-05T21:00:02+1100
1536141602      2018-09-05T10:00:02Z    2018-09-05T21:00:02+1100
1536141602      2018-09-05T10:00:02Z    2018-09-05T21:00:02+1100
1536145201      2018-09-05T11:00:01Z    2018-09-05T22:00:01+1100
1536148801      2018-09-05T12:00:01Z    2018-09-05T23:00:01+1100
1536152401      2018-09-05T13:00:01Z    2018-09-06T00:00:01+1100
1536156001      2018-09-05T14:00:01Z    2018-09-06T01:00:01+1100
1536159602      2018-09-05T15:00:02Z    2018-09-06T02:00:02+1100
1536170402      2018-09-05T18:00:02Z    2018-09-06T03:00:02+0900
1536174001      2018-09-05T19:00:01Z    2018-09-06T04:00:01+0900
1536177601      2018-09-05T20:00:01Z    2018-09-06T05:00:01+0900

こちらは特別処理を受けたことが分かります。スキップされた19時、20時のcron jobは4行目、5行目にあたりますが、この2つはスキップされた直後の21時に、ただちに実行されています。結果として、19時から21時にスケジュールされていた3つのjobが同時に実行されるという結果になりました。一方、サマータイムが終了した後、二度目に訪れた1時と2時についてはjobは実行されていません。したがってこちらの2回分はスキップされていることがUTCの記録から分かります。

このような結果は、cron jobを1行ずつバラバラに設定した場合でも同じで、動作に違いはありませんでした。

特別処理のロジック

実験結果を見てみると、一つの疑問がわきあがってきます。それは同じ1時間毎という意味にも関わらず、アスタリスク記法で指定した場合と直接時刻を指定した場合で、なぜ特別処理を受けるか否かが変わるのか? ということです。アスタリスク記法の意味については、crontabのmanpageにも次のように書かれています。

       The time and date fields are:

              field          allowed values
              -----          --------------
              minute         0-59
              hour           0-23
              day of month   1-31
              month          1-12 (or names, see below)
              day of week    0-7 (0 or 7 is Sunday, or use names)

       A  field  may  contain  an  asterisk  (*),  which  always  stands   for
       "first-last".

       Ranges of numbers are allowed.  Ranges are two numbers separated with a
       hyphen.  The specified range is inclusive.  For example, 8-11 for an

       Lists are allowed.  A list is a set of numbers (or ranges) separated by
       commas.  Examples: "1,2,5,9", "0-4,8-12".

       Step  values can be used in conjunction with ranges.  Following a range
       with "/<number>" specifies skips of  the  number's  value  through  the
       range.  For example, "0-23/2" can be used in the 'hours' field to spec‐
       ify command execution for every other hour (the alternative in  the  V7
       standard  is  "0,2,4,6,8,10,12,14,16,18,20,22").   Step values are also
       permitted after an asterisk, so if specifying a job to be run every two
       hours, you can use "*/2".

簡単に訳すと、アスタリスクは「最初-最後」と書くのと同等であり、時間指定に「*」と書くのと「0-23」と書くのは同じ意味だとあります。しかしサマータイムの前後において、動作は異なって見えます。

そこで、cronがサマータイムをどのようなロジックで扱っているのか、ソースコードを調べてみることにしました。全部を引用すると長くなってしまうので、抜粋でご覧いただきましょう。該当部分はこんな風になっています。

        /*
         * Calculate how the current time differs from our virtual
         * clock.  Classify the change into one of 4 cases.
         */
        timeDiff = timeRunning - virtualTime;
        check_orphans(&database);
(略)
        load_database(&database);
(略)
        /* shortcut for the most common case */
        if (timeDiff == 1) {
            virtualTime = timeRunning;
            oldGMToff = GMToff;
            find_jobs(virtualTime, &database, TRUE, TRUE, oldGMToff);
        }
        else {
            if (timeDiff > (3 * MINUTE_COUNT) || timeDiff < -(3 * MINUTE_COUNT))
                wakeupKind = large;
            else if (timeDiff > 5)
                wakeupKind = medium;
            else if (timeDiff > 0)
                wakeupKind = small;
            else
                wakeupKind = negative;

まず冒頭で、timeDiffというのを求めています。timeRunningは現在時刻、virtualTimeはcronの内部時計にあたり、両方ともepochからの経過時間を[分]で表した上でローカルタイムに調整したものです。その差を求めているので、timeDiffの単位も[分]となります。

「ほとんどの場合」というコメントがあるif文は、要するに1分毎に起動されるcronのチェック機構のことを示しています。つまり、前回と今回の時刻の差timeDiffは、ほぼ常に1になるということです。そのときの処理は単純です。

  • 内部時計を現在時刻に合わせる
  • GMToff(ローカルタイムとUTCの差[秒])をoldGMToffにセーブする
  • find_jobを起動し、crontabなどを捜査して現時刻に実行するべきjobを実行する

さて、差が1分に当たらない場合は、システム時計が大きく変更されたことを意味します。その差の大きさや符号の向きに応じて4つのケースに分類しています。

  • small — 5分以内の比較的小さい差 → 取りこぼしたjobを通常実行する
  • medium — 3時間未満の変更 → サマータイム開始と見なす
  • large — 3時間以上の変更 → システム時計が修正された
  • negative — 3時間未満の時刻の巻き戻り → サマータイム終了と見なす

そこで、mediumのケースを調べてみましょう。

            case medium:
                /*
                 * case 2: timeDiff is a medium-sized positive
                 * number, for example because we went to DST
                 * run wildcard jobs once, then run any
                 * fixed-time jobs that would otherwise be
                 * skipped if we use up our minute (possible,
                 * if there are a lot of jobs to run) go
                 * around the loop again so that wildcard jobs
                 * have a chance to run, and we do our
                 * housekeeping.
                 */
                Debug(DSCH, ("[%ld], DST begins %d minutes to go\n",
                        (long) pid, timeDiff));
                /* run wildcard jobs for current minute */
                find_jobs(timeRunning, &database, TRUE, FALSE, GMToff);

                /* run fixed-time jobs for each minute missed */
                do {
                    if (job_runqueue())
                        sleep(10);
                    virtualTime++;
                    if (virtualTime >= timeRunning)
                        /* always run also the other timezone jobs in the last step */
                        oldGMToff = GMToff;
                    find_jobs(virtualTime, &database, FALSE, TRUE, oldGMToff);
                    set_time(FALSE);
                } while (virtualTime < timeRunning && clockTime == timeRunning);
                break;

find_jobsは2回に分けて実行されているのが分かります。コメントによると1つめは「ワイルドカードjobを現在時刻で」という部分、2つめは「固定時刻のjobをスキップされた時刻で」という部分です。find_jobsのヘッダと処理の該当部分を見ると、こうなっています。

static void find_jobs(int vtime, cron_db * db, int doWild, int doNonWild, long vGMToff) {
:
:
:
                    if ((doNonWild &&
                            !(e->flags & (MIN_STAR | HR_STAR))) ||
                        (doWild && (e->flags & (MIN_STAR | HR_STAR))))
                        job_add(e, u);  /*will add job, if it isn't in queue already for NOW. *

つまり第3、第4引数に [TRUE, FALSE] と渡すとアスタリスクを含むjobのみ実行、[FALSE, TRUE]と渡すとアスタリスクを含まないjobのみを実行するのです。これを踏まえてcase mediumを読み解きましょう。

最初のfind_jobsのコールでは、アスタリスクを含むjobを、サマータイム適用直後のオフセット(GMToff)でチェックし実行します。そして続くdo-whileループにおいて、サマータイム適用時刻へ向けて1分ずつインクリメントしながらサマータイム適用直前オフセット(oldGMToff)でチェックします。このループでは、アスタリスクを含まないjobだけをチェックし実行するので、特別処理を受けるのはこちらだけになります。この差異が、上述のような「0-23」と書いたときと「*」と書いたときの動作の差になって現れるようです。

コードを読んでみて分かったことがもう一つあります。「1時間を超える粒度」という言葉には、1時間のうちに2度以上実行されるjobは除外されるようなニュアンスを感じますが、そのようなロジックはどこにもないということです。事実、アスタリスクを使わずに5分毎に実行する下記のようなルールを書いてサマータイムに移行してみると、2時間分(24回分)のjobが一気に同時刻に実行されてしまいます。

0-59/5 0-23/1 * * *
1536212701      2018-09-06T05:45:01Z    2018-09-06T14:45:01+0900
1536213001      2018-09-06T05:50:01Z    2018-09-06T14:50:01+0900
1536213301      2018-09-06T05:55:01Z    2018-09-06T14:55:01+0900
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213601      2018-09-06T06:00:01Z    2018-09-06T17:00:01+1100
1536213602      2018-09-06T06:00:02Z    2018-09-06T17:00:02+1100
1536213602      2018-09-06T06:00:02Z    2018-09-06T17:00:02+1100
1536213901      2018-09-06T06:05:01Z    2018-09-06T17:05:01+1100
1536214201      2018-09-06T06:10:01Z    2018-09-06T17:10:01+1100
1536214501      2018-09-06T06:15:01Z    2018-09-06T17:15:01+1100

「*/5」と書けるところを、上記のようにする人はいないとは思うのですが、サマータイムに伴う特別処理を実施・迂回するに当たって奇妙なワークアラウンドがあるというのは大変興味深い事実だと思います。それとも「粒度」という言葉は、もしかしたらアスタリスク記法を時・分フィールドで利用すること、という意味だったのかもしれません。実に分かりづらいですね!

まとめ

結果が複雑になってしまったので、改めてまとめなおしてみましょう。

  • サマータイム開始時の特別処理
    開始時にスキップされる時刻のjobは、サマータイム適用後にまとめて実行されます
  • サマータイム終了時の特別処理
    終了時に重複する時刻のjobは、2度目の実行が無効化されます
  • 時・分フィールドに「*」を含む場合、特別処理は行いません
    たとえば、2時間毎の実行を意味する「*/2」という指定は常に2時間毎に実行されます。ローカルタイムの変化には影響されません
  • サマータイム対応を希望するならば、時刻を明記するべきです

以上が実験の内容と結果です。一応、manpageに書かれていた通りの動作が期待できるということ、書かれていない意外な一面があるという事が分かりました。意外に便利な機能が実装されていたんだなという気がしますが、問題がまったくないというわけではないと思います。

第一に、サマータイム開始日における「スキップされた時間帯のjobは即実行される」という動作です。もし相互に順序依存性のあるjobを一定間隔ごとに時刻指定して実行する場合、意図せずして複数のjobが同時に実行されてしまう可能性があるのです。cronはこのような問題を一切解決してくれません。

第二に、サマータイム終了日における「二度目の時間帯のjobは無効化される」という動作も、常に歓迎されるわけではないということです。これも順序依存であるjobが連続して実行されるときに、意図しない大きな間隔が生じてしまう可能性があるわけで、何かまずい事態を引き起こす原因になるかもしれません。

ただ、いずれのケースも共通していえるのが「時刻を基準にして順序依存をスケジュールすることに問題があるのだ」ということだと思います。定められた時刻になったら是が非でも○○をやる、という類のjobスケジューリングは、何かの拍子にうまくいかなくなることが有り得ます。それはサマータイムだけでなく、うるう秒や他の暦の変更や、タイムゾーンをまたぐ場合などで問題が顕在化するかもしれません。そういうことはしてはいけない、と考え方を改めるべきでしょう。

他には、2時間分のjobが固まって実行される可能性があるので、切替時刻にjobが一気に実行されることでサーバ負荷が集中してしまう懸念があるなと思いました。ハウスキーピング・ジョブについてはRANDOM_DELAYなどの仕組みもありますが、ランダム化した時刻がサマータイム開始によって消えたときの処理に関するcrontabの記述が非常に曖昧に書かれているので、これも別途実験が必要かもしれません。

みなさんのcronの設定はいかがでしょうか? もしサマータイムが導入されたらどうなりますか? 気軽にリブートできるサーバならばいいのですが、そうはいかない場合も多いと思います。海外のadminがどうしているのか、調査をする機会があればまた記事にしたいと思います。