サマータイムとバッチジョブ

一般的にはサマータイムと呼ばれますが、正式名称は夏時間(Daylight saving time)といい、夏季に標準時間を元の時間よりも1時間早くすることを意味します。「夏は日が長いため、冬よりも1時間ずらして生活すれば健康で多くの仕事ができるだろう」という発想から始まった制度で¹⁾、韓国では1988年を最後に現在は実施されていません。しかし、NHNの関連会社がある米国ではこの制度が施行されています。

一方、ソフトウェアエンジニアとして、私たちは多くのバッチジョブを回し²⁾ています。時々動作するマイクロバッチから、1日1回任意の時間に回すもの、必ずその時間に回さなければならないものなどが存在します。毎日同じジョブを回すのがそんなに難しいことかと思われるかもしれませんが、意外に気を使わなければならない部分が多いのがバッチジョブです。そして、夏時間がさらに考慮すべきものを上乗せします。

出典:https://www.flickr.com/photos/notionscapital/25000493314/in/photostream/

アメリカ西部地方の場合、夏時間を3月第2日曜日に開始し、11月の第1日曜日に終了します。2020年の場合は、3月8日午前2時に時刻を午前3時に変更し、11月1日午前2時に時刻を午前1時に変更します。ローカルタイムベースでは、2020年3月8日は02:00〜02:59が存在せず、11月1日は01:00〜01:59が2回存在することになります。ソフトウェアエンジニアの観点で気になるのは、その時刻に実行されるバッチジョブは正常に実行されるかどうかということです。

1時間よりも短い周期で回る、正確には60分の約数単位で実行されるバッチジョブは特に問題ありません。たとえば、10分に1回実行されるバッチジョブなら問題なく10分に1回実行されるでしょう。6回少なく実行されるか、6回多く実行された日があるだけです。

1時間よりも長い周期で回るバッチジョブは検討が必要です。たとえば、2時間ごとに実行されるジョブがある場合は、実行間隔が減少したり増える日があります。1日に1回実行されるジョブがこの時間にわたってある場合は、1年の中で2回実行される日と、まったく実行されない日があるかもしれません。

これを解決するにはいくつかの方法が考えられます。

  1. UTC基準に回す(または夏時間が適用されない時間帯を基準に実行する)
  2. 上記の時間帯を避けて回す
  3. 夏時間の適用/解除による時刻変更を経ても問題なく回るようなバッチジョブ環境を構築する

1は一見妥当に思えますが実はそうではありません。まず、年に2回ほど上司から呼び出されることになるかもしれません。春には7時に提出すべきレポートメールがなぜ8時になって飛んでくるのかと呼ばれるでしょうし、秋にはなぜ6時にメールを送信して寝ている人を起こすのかと呼び出されるでしょう。各バッチジョブを正確な日程に沿って一寸の誤差なく動作させることには成功しましたが、結果を受けた側の立場ではそうではなかったからです。

もし、夏時間に時間が変更される前日に、あなたが真面目にこれらのジョブが回る時刻を変更しても、年に2回この仕事をきちんと取り行う必要があります。夏時間の適用または解除に合わせて、他のバッチジョブの実行時刻の設定を変更するバッチジョブを作成して実行させる方法も考えてみましたが、すべての種類のバッチジョブ管理/実行ツール(またはジョブスケジューラー)に対して適用可能な方法はなさそうです。

2の方法は、午前1時から3時まではバッチジョブを実行しないので、1のような勤勉さを備える必要がありません。その代わり、バッチジョブを実行できる時間を2時間も放棄することになり他の問題が生じます。時間がかかるバッチジョブであればあるほど、夜の時間帯に実行されるようにするのが普通ですが、その貴重な時間帯を2時間も放棄するのは非常にもったいないことです。そのため私たちは3の方法を検討してみましょう。

ところで実際には実行間隔が伸びてもほとんどのバッチジョブでは大きな問題にはなりません。特定の作業回数が早く終わったり、遅く終わる程度の差だけでしょう。問題となるのは次のような場合です。

同じジョブが重複して実行される場合
夏時間が終了したときに発生することがあります。米国西部地域なら、午前1時30分が2回存在する日がありますからね。そのためにはバッチジョブが重複して実行されないようにしたり、重複して実行されても同じ結果が出るようにする必要があります。どちらを選ぶかは、それぞれの状況によって異なると思います。スマートバッチジョブ管理/実行ツールの中には重複実行を防止するケースがあるようなので、自分が使用するツールがどのように動作するのかを知っておくことが必要ですね。

ジョブをスキップする場合
夏時間が開始される時期に発生することがあります。米国西部地域では、2020年3月8日午前2時30分は存在しない時刻ですからね。通常は、次回の周期になってようやくジョブが漏れていることに気づきますが、作業構成やモニタリング設定によっては、より早い時刻に発見することも可能でしょう。

ところで、すでにバッチジョブ管理/実行ツールで同じような問題に対して対応がなされていないでしょうか?代表的によく使われているcrontabのマニュアルページを探してみると、やはり検討していた跡がありました。

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. Conversely, 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.

この説明をそのまま信じても大丈夫でしょうか?周りに聞いてもcrontabでこのように動作することを知っている人がいませんでした。

そこで実験してみることにしました。実験のためTOAST CloudのUSリージョン³⁾で一番安いt2インスタンスを作成しました。そして、実験対象としては、crontabとSpring scheduled taskを選択しました。Spring scheduled taskはcrontabとは異なり、夏時間の例外動作に対する文書が存在しなかったので、実験の比較対象として適していると思われました。

2つのバッチジョブ管理/実行ツールで複数の設定でバッチジョブを回しました。各バッチジョブは、次のような形式でログを残すようにしました。

{local-time} | {UTC} | {job-name}

実行時刻の設定は次のとおりです。crontabとSpring scheduled taskの設定もほぼ同じです。違いは、Spring scheduled taskは秒単位で設定が可能なので、前に「0」がもう1つ付きます。

*/10 * * * * /home/centos/work/PDT_test/test_crontab.sh "Every 10 minutes #1" 001_10min_1
5-55/10 * * * * /home/centos/work/PDT_test/test_crontab.sh "Every 10 minutes #2" 002_10min_2

0 * * * * /home/centos/work/PDT_test/test_crontab.sh "Every 1 hour #1" 011_1hour_1
30 * * * * /home/centos/work/PDT_test/test_crontab.sh "Every 1 hour #2" 012_1hour_2

0 */2 * * * /home/centos/work/PDT_test/test_crontab.sh "Every 2 hours #1" 021_2hours_1
30 */2 * * * /home/centos/work/PDT_test/test_crontab.sh "Every 2 hours #2" 022_2hours_2
0 1-23/2 * * * /home/centos/work/PDT_test/test_crontab.sh "Every 2 hours #3" 023_2hours_3
30 1-23/2 * * * /home/centos/work/PDT_test/test_crontab.sh "Every 2 hours #4" 024_2hours_4

0 2 * * * /home/centos/work/PDT_test/test_crontab.sh "Every 1 day #1" 031_1day_1
30 2 * * * /home/centos/work/PDT_test/test_crontab.sh "Every 1 day #2" 032_1day_2
0 3 * * * /home/centos/work/PDT_test/test_crontab.sh "Every 1 day #3" 033_1day_3
30 3 * * * /home/centos/work/PDT_test/test_crontab.sh "Every 1 day #4" 034_1day_4

0 2,3,5 * * * /home/centos/work/PDT_test/test_crontab.sh "Designated #1" 041_designated_1
30 2,3,5 * * * /home/centos/work/PDT_test/test_crontab.sh "Designated #2" 042_designated_2
0 2,4,5 * * * /home/centos/work/PDT_test/test_crontab.sh "Designated #3" 043_designated_3
30 2,4,5 * * * /home/centos/work/PDT_test/test_crontab.sh "Designated #4" 044_designated_4

 

Spring scheduled taskは常識的に動作しました。PDT 3月8日02:00〜02:59は、Spring scheduled taskには存在しない時間であり、上記のすべてのバッチジョブの設定のうち、当該時間に動作すべきだったすべてのバッチジョブが実行されませんでした。

一方でcrontabでは興味深い結果が出ました。10分単位のジョブと、1時間単位のジョブは何事もなかったかのように動作しました。1日周期または実行時刻を指定していた場合には、PDT 3月8日02:00〜02:59に実行されるべきジョブがすべてPDT 3月8日03:00に実行されました。中でも特異なのは、41番の実験です。毎日午前2、3、5時に動作するようにさせましたが、3月8日03:00に2つのバッチジョブが実行されました。1つは2時に回るべきジョブで、もう1つは本来3時に回るべきジョブです。

Sat Mar 7 02:00:02 PST 2020 | Sat Mar 7 10:00:02 UTC 2020 | Designated #1
Sat Mar 7 03:00:01 PST 2020 | Sat Mar 7 11:00:01 UTC 2020 | Designated #1
Sat Mar 7 05:00:01 PST 2020 | Sat Mar 7 13:00:01 UTC 2020 | Designated #1
Sun Mar 8 03:00:01 PDT 2020 | Sun Mar 8 10:00:01 UTC 2020 | Designated #1
Sun Mar 8 03:00:01 PDT 2020 | Sun Mar 8 10:00:01 UTC 2020 | Designated #1
Sun Mar 8 05:00:01 PDT 2020 | Sun Mar 8 12:00:01 UTC 2020 | Designated #1
Mon Mar 9 02:00:02 PDT 2020 | Mon Mar 9 09:00:02 UTC 2020 | Designated #1
Mon Mar 9 03:00:01 PDT 2020 | Mon Mar 9 10:00:01 UTC 2020 | Designated #1
Mon Mar 9 05:00:01 PDT 2020 | Mon Mar 9 12:00:01 UTC 2020 | Designated #1

不思議なのは2時間周期で実行させたバッチジョブでした。文書によると、実行周期が1時間より長いため、PDT 3月8日02:00〜02:59に実行されるべきだったジョブが、すべてPDT 3月8日03:00に実行されると思われましたが、実際はそうではありませんでした。すべて無視されました。

Sat Mar 7 20:00:01 PST 2020 | Sun Mar 8 04:00:01 UTC 2020 | Every 2 hours #1
Sat Mar 7 22:00:01 PST 2020 | Sun Mar 8 06:00:01 UTC 2020 | Every 2 hours #1
Sun Mar 8 00:00:01 PST 2020 | Sun Mar 8 08:00:01 UTC 2020 | Every 2 hours #1
Sun Mar 8 04:00:01 PDT 2020 | Sun Mar 8 11:00:01 UTC 2020 | Every 2 hours #1
Sun Mar 8 06:00:01 PDT 2020 | Sun Mar 8 13:00:01 UTC 2020 | Every 2 hours #1

こうなるなら、3時間単位のジョブも作っておくべきでした。⁴⁾

crontabのこのような過剰な親切は、他の悩み事を生じさせます。それはバッチジョブの実行間隔についてです。crontabのこのような動作は、各バッチジョブが互いに独立するという仮定に基づいていると考えられます。しかし、現実はそうでないことがあります。1番のバッチジョブの実行結果を2番のバッチジョブの入力として使うといったように、一連のバッチジョブを構成するケースが少なからず存在するためです。

このような場合、最も簡単に選択する方法は、2つのバッチジョブの実行時刻を十分に広げておくことです。「1番は通常30分程度で実行が完了するので、2番は1番より1時間遅れて開始するようにすれば十分だろう。1番を2時に、2番を3時に実行しよう」と考えたらなら、このバッチジョブは3月8日未明に失敗しているでしょう。なぜなら、両方とも3時に実行されるためです。そのため、異なるバッチジョブが相互に依存関係にある場合は、実行時刻を基準に実行するのは気をつけた方がよいでしょう。または、イベント基盤で動作するバッチジョブ管理/実行ツールを選択する方法も考えらます。

TOAST Cloud USリージョンの使用コスト合計3,393ウォン(約300円)で個人的な好奇心は満たされました。面白い結果でしたので共有します。恥ずかしいレベルのコーディングですが、この実験に使用したソースコードと実行ログもGitHubに載せておきます。
https://github.com/iizs/daylight-saving

脚注)

  1. 正確にはベンジャミン・フランクリンが「early to bed and early to rise makes a man healthy, wealthy, and wise」という表現を使用。(出典:https://en.wikipedia.org/wiki/Daylight_saving_time
  2. 厳密には「バッチジョブを実行する」というのが現代に合った表現ですが、「回す」ほどバッチジョブに似合う動詞はないと思います。
  3. TOAST Cloudで、昨年8月から米国リージョンが使用できるようになりました!
  4. 実際にはタイムサーバーの同期を切って実験インスタンスの基準時刻を変更すれば(つまり、サーバーの時刻を過去に回せば)追加実験は可能ですが、すでにインスタンスを返却してしまっており、新しくインスタンス上げてセットアップするのが面倒でした。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop