もろず blog

もろちゃんがITに関しての様々なトピックを解説します

もしもサマータイムが導入されたらどうなるか試してみた

2020年の東京オリンピックに向けてサマータイムを導入するかどうかという話があるようですが、ITエンジニア界隈ではサマータイムの対応が大変だという話が上がっています

日本では縁遠いサマータイムですが、実際にそれを対応するとなった場合に日々開発しているソフトウェアにどんな影響があるんだろうか?というのが気になりますね

今回は、実際にサマータイムが導入された場合の挙動を Ubuntu で試してみました

この記事では

について書きます

いざ、サマータイムの世界へ!

1. サマータイムについておさらい

私たち日本人は国内で生活している限りサマータイムというものを意識する場面はまずないと思うので、実はそんなにサマータイムについて詳しく知らなかったりします まずはシステム的な話題に入る前にサマータイムとはそもそもどういうものなのかを見てみましょう

Wikipedia によるとこんな説明でした

夏時間(なつじかん)またはサマータイム(英: summer time)、デイライト・セービング・タイム(米: daylight saving time (DST)、直訳: 日光節約時間。カナダ、オーストラリアでも用いる)とは1年のうち夏を中心とする時期に太陽が出ている時間帯を有効に利用する目的で、標準時を1時間進める制度またはその進められた時刻のこと

夏時間 - Wikipedia

イギリスでは "summer time"、アメリカでは "daylight saving time" と表現されるそうです

夏は日照時間が長いので、なるべく活動時間帯に日が当たる時間を長くするように時間をズラして生活することで照明の費用を節約しようというものだそうです
そう考えると "summer time" よりは "daylight saving time" の方がより目的をよく表している名前ですね

特に目新しい情報はないのですが「サマータイムは daylight saving time とも呼ぶんだなー」くらいに覚えて頂ければ十分です

記事の先頭に戻る

2. Linux でサマータイムを切り替える仕組み

それではまず、Linux でサマータイムがどのように実現されているかを見てみましょう

tz database

多くの Linux ディストリビューションでは、タイムゾーンの情報は tz database (tzdata, zoneinfo databese ともいうらしい) というデータを利用して管理されています tz database - Wikipedia

tz database 自体はプレーンテキストのデータで、例えばアメリカのサマータイムの定義はこのように記述されています

# Rule   NAME    FROM    TO  TYPE    IN  ON  AT  SAVE    LETTER/S
Rule    US  1918    1919    -   Mar lastSun 2:00    1:00    D
Rule    US  1918    1919    -   Oct lastSun 2:00    0   S
Rule    US  1942    only    -   Feb 9   2:00    1:00    W # War
Rule    US  1945    only    -   Aug 14  23:00u  1:00    P # Peace
Rule    US  1945    only    -   Sep lastSun 2:00    0   S
Rule    US  1967    2006    -   Oct lastSun 2:00    0   S
Rule    US  1967    1973    -   Apr lastSun 2:00    1:00    D
Rule    US  1974    only    -   Jan 6   2:00    1:00    D
Rule    US  1975    only    -   Feb 23  2:00    1:00    D
Rule    US  1976    1986    -   Apr lastSun 2:00    1:00    D
Rule    US  1987    2006    -   Apr Sun>=1   2:00    1:00    D
Rule    US  2007    max -   Mar Sun>=8   2:00    1:00    D
Rule US 2007 max - Nov Sun>=1 2:00 0 S

プログラムから tz database の情報が利用される際にはこれをコンパイルしたバイナリのファイルが利用されます

tz database のデータはこちらのリポジトリで管理・公開されています

github.com

Linux でのタイムゾーンの設定

大抵の Linux ディストリビューションではタイムゾーンの情報は /etc/localtime/usr/share/zoneinfo 配下にあるタイムゾーンごとの定義ファイルの内容に置き換えることで設定されます
例えば日本のタイムゾーンに設定したい場合は ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime のように /etc/localtimeAsia/Tokyo のファイルへのシンボリックリンクを張る方法がよく利用されます

Ubuntu の場合は更に /etc/timezone にタイムゾーンの値を設定する必要がありますが、以下のコマンドでタイムゾーンを設定できるようになっています

$ timedatectl set-timezone Asia/Tokyo

ここで出てくる /usr/share/zoneinfo/Asia/Tokyo のファイルがまさに、tz database のファイルをコンパイルして生成されるタイムゾーンの定義ファイル (zoneinfo) です

zoneinfo の中身を見てみる

コンパイルされた zoneinfo はバイナリなので、中身を見るためには zdump コマンドを利用します
tz database に関係するコマンドは大抵標準でインストールされているようで、Ubuntu 16.04 LTS でも最初から入っていました

試しに America/New_York のタイムゾーンの定義を見てみると以下のような内容になっています

$ zdump -v /usr/share/zoneinfo/America/New_York
/usr/share/zoneinfo/America/New_York  -9223372036854775808 = NULL
/usr/share/zoneinfo/America/New_York  -9223372036854689408 = NULL
/usr/share/zoneinfo/America/New_York  Sun Nov 18 16:59:59 1883 UT = Sun Nov 18 12:03:57 1883 LMT isdst=0 gmtoff=-17762
/usr/share/zoneinfo/America/New_York  Sun Nov 18 17:00:00 1883 UT = Sun Nov 18 12:00:00 1883 EST isdst=0 gmtoff=-18000
/usr/share/zoneinfo/America/New_York  Sun Mar 31 06:59:59 1918 UT = Sun Mar 31 01:59:59 1918 EST isdst=0 gmtoff=-18000
/usr/share/zoneinfo/America/New_York  Sun Mar 31 07:00:00 1918 UT = Sun Mar 31 03:00:00 1918 EDT isdst=1 gmtoff=-14400
/usr/share/zoneinfo/America/New_York  Sun Oct 27 05:59:59 1918 UT = Sun Oct 27 01:59:59 1918 EDT isdst=1 gmtoff=-14400

〜

/usr/share/zoneinfo/America/New_York  Sun Nov  2 06:00:00 2498 UT = Sun Nov  2 01:00:00 2498 EST isdst=0 gmtoff=-18000
/usr/share/zoneinfo/America/New_York  Sun Mar  8 06:59:59 2499 UT = Sun Mar  8 01:59:59 2499 EST isdst=0 gmtoff=-18000
/usr/share/zoneinfo/America/New_York  Sun Mar  8 07:00:00 2499 UT = Sun Mar  8 03:00:00 2499 EDT isdst=1 gmtoff=-14400
/usr/share/zoneinfo/America/New_York  Sun Nov  1 05:59:59 2499 UT = Sun Nov  1 01:59:59 2499 EDT isdst=1 gmtoff=-14400
/usr/share/zoneinfo/America/New_York  Sun Nov  1 06:00:00 2499 UT = Sun Nov  1 01:00:00 2499 EST isdst=0 gmtoff=-18000
/usr/share/zoneinfo/America/New_York  9223372036854689407 = NULL
/usr/share/zoneinfo/America/New_York  9223372036854775807 = NULL

これはどういう内容かというと、
<0.zoneinfoファイル名> <1.基準となるUTC時刻> = <2.1のUTCに対するローカルの時刻> <3.サマータイム中かのフラグ> <4.GMTからのオフセット(秒)> という形式でデータが表示されています

簡単に言うと UTC のいつからいつまではこのオフセットというのが定義されています

なので、サマータイムを切り替えは zoneinfo に登録されているレコードに沿って自動で切り替わるようになっています 前述の America/New_York のデータでは 2499年までサマータイムのレコードが入っていますが、年によって実施日が異なるような国もあるようなので、tz database は毎年数回アップデートされています

先ほどの github リポジトリのタグを見ると最新は 2018e となっており、2018年に入ってから既に5回(a〜eまで) tz database が更新されているということが分かります

日本だけの環境を考えると tz database の存在もその情報の鮮度も気にする場面は今までまずありませんでしたが、これだけ更新が入っていますし世界的にみれば tz database の鮮度を気にする必要がある地域やシステムも多くありそうですよね

Asia/Tokyo の中身を見てみる

試しに日本のタイムゾーンである Asia/Tokyo の zoneinfo の中身を見てみるとこんなデータが入っています

$ zdump -v ~/shared/zoneinfo/Asia/Tokyo 
/home/vagrant/shared/zoneinfo/Asia/Tokyo  -9223372036854775808 = NULL
/home/vagrant/shared/zoneinfo/Asia/Tokyo  -9223372036854689408 = NULL
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Dec 31 14:59:59 1887 UT = Sun Jan  1 00:18:58 1888 LMT isdst=0 gmtoff=33539
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Dec 31 15:00:00 1887 UT = Sun Jan  1 00:00:00 1888 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat May  1 14:59:59 1948 UT = Sat May  1 23:59:59 1948 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat May  1 15:00:00 1948 UT = Sun May  2 01:00:00 1948 JDT isdst=1 gmtoff=36000
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Sep 11 13:59:59 1948 UT = Sat Sep 11 23:59:59 1948 JDT isdst=1 gmtoff=36000
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Sep 11 14:00:00 1948 UT = Sat Sep 11 23:00:00 1948 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Apr  2 14:59:59 1949 UT = Sat Apr  2 23:59:59 1949 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Apr  2 15:00:00 1949 UT = Sun Apr  3 01:00:00 1949 JDT isdst=1 gmtoff=36000
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Sep 10 13:59:59 1949 UT = Sat Sep 10 23:59:59 1949 JDT isdst=1 gmtoff=36000
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Sep 10 14:00:00 1949 UT = Sat Sep 10 23:00:00 1949 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat May  6 14:59:59 1950 UT = Sat May  6 23:59:59 1950 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat May  6 15:00:00 1950 UT = Sun May  7 01:00:00 1950 JDT isdst=1 gmtoff=36000
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Sep  9 13:59:59 1950 UT = Sat Sep  9 23:59:59 1950 JDT isdst=1 gmtoff=36000
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Sep  9 14:00:00 1950 UT = Sat Sep  9 23:00:00 1950 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat May  5 14:59:59 1951 UT = Sat May  5 23:59:59 1951 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat May  5 15:00:00 1951 UT = Sun May  6 01:00:00 1951 JDT isdst=1 gmtoff=36000
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Sep  8 13:59:59 1951 UT = Sat Sep  8 23:59:59 1951 JDT isdst=1 gmtoff=36000
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Sep  8 14:00:00 1951 UT = Sat Sep  8 23:00:00 1951 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  9223372036854689407 = NULL
/home/vagrant/shared/zoneinfo/Asia/Tokyo  9223372036854775807 = NULL

1948年〜1951年の間にかつてサマータイムが実施されていたデータもここに登録されているようです
このデータを見るまで過去に日本でもサマータイムが実施されていたということを知りませんでしたが、第二次世界大戦後に実施されていたことがあったようですね 夏時間 - Wikipedia

日本の zoneinfo を見ると 1951年9月8日 23:00:00(JST) 以降はGMTに対するオフセットは +9時間 (32400秒) と設定されており、これが現在も有効になっているので JST は UTC に対して+9時間として動作するわけですね

日本に限らず様々な国で過去のデータまで登録されているようなんですが、これは過去のタイムスタンプのデータを処理する際にも時刻によってはオフセットが異なるケースに対応するためということのようです
毎年サマータイムの実施時期が異なるような国の時刻を扱うシステムにとってはかなり重要ですね

記事の先頭に戻る

3. tz database を編集する

さて、サマータイムの切り替えは zoneinfo によって切り替わるということが分かりました

こうなると tz database のファイルを編集して日本にサマータイムが導入された場合の挙動を見てみたいですよね
実際にやってみましょう

ファイルの編集

tz database を編集するために先ほど紹介した以下のリポジトリをクローンしてきます https://github.com/eggert/tz

この中の asia のファイルに Asia/Tokyo のタイムゾーンの情報が書かれています
ここで仮に 2018年からは毎年8月15日AM2:00:00〜11月15日AM1:59:59 の間は2時間時計を進めて生活しましょう という法律ができたとして、 それを tz database に追加するとこんな差分になります

$ git diff
diff --git a/asia b/asia
index 3d30864..c6af667 100644
--- a/asia
+++ b/asia
@@ -1476,6 +1476,8 @@ Rule      Japan   1948    only    -       May     Sat>=1  24:00   1:00    D
 Rule   Japan   1948    1951    -       Sep     Sun>=9   0:00   0       S
 Rule   Japan   1949    only    -       Apr     Sat>=1  24:00   1:00    D
 Rule   Japan   1950    1951    -       May     Sat>=1  24:00   1:00    D
+Rule   Japan   2018    max     -       Aug     15      2:00    2:00    D
+Rule   Japan   2018    max     -       Nov     15      2:00    0       S
 
 # From Hideyuki Suzuki (1998-11-09):
 # 'Tokyo' usually stands for the former location of Tokyo Astronomical

このフォーマットについてはちゃんと仕様があるんですが、書きたいことの本題ではないので割愛します

zoneinfo のビルド

変更した tz database をコンパイルするには以下のように zic を利用します
zic は tz database のコンパイラーで、 zic 以外でも様々なコンパイラーが公開されているようです

$ zic -d zoneinfo ./tz/asia

カレントディレクトリ配下に zoneinfo というディレクトリが作成され、変更後の zoneinfo ファイルはここに作成されています
早速、新しく作成された Asia/Tokyo の zoneinfo の中身を見てみましょう

$ zdump -v /home/vagrant/shared/zoneinfo/Asia/Tokyo | more
/home/vagrant/shared/zoneinfo/Asia/Tokyo  -9223372036854775808 = NULL
/home/vagrant/shared/zoneinfo/Asia/Tokyo  -9223372036854689408 = NULL
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Dec 31 14:59:59 1887 UT = Sun Jan  1 00:18:58 1888 LMT isdst=0 gmtoff=33539
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Sat Dec 31 15:00:00 1887 UT = Sun Jan  1 00:00:00 1888 JST isdst=0 gmtoff=32400
・・・
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Tue Aug 14 16:59:59 2018 UT = Wed Aug 15 01:59:59 2018 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Tue Aug 14 17:00:00 2018 UT = Wed Aug 15 04:00:00 2018 JDT isdst=1 gmtoff=39600
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Wed Nov 14 14:59:59 2018 UT = Thu Nov 15 01:59:59 2018 JDT isdst=1 gmtoff=39600
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Wed Nov 14 15:00:00 2018 UT = Thu Nov 15 00:00:00 2018 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Wed Aug 14 16:59:59 2019 UT = Thu Aug 15 01:59:59 2019 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Wed Aug 14 17:00:00 2019 UT = Thu Aug 15 04:00:00 2019 JDT isdst=1 gmtoff=39600
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Thu Nov 14 14:59:59 2019 UT = Fri Nov 15 01:59:59 2019 JDT isdst=1 gmtoff=39600
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Thu Nov 14 15:00:00 2019 UT = Fri Nov 15 00:00:00 2019 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Fri Aug 14 16:59:59 2020 UT = Sat Aug 15 01:59:59 2020 JST isdst=0 gmtoff=32400
/home/vagrant/shared/zoneinfo/Asia/Tokyo  Fri Aug 14 17:00:00 2020 UT = Sat Aug 15 04:00:00 2020 JDT isdst=1 gmtoff=39600
・・・

8/15 01:59:59(JST) → 8/15 04:00:00(JDT) となる設定と、
11/15 01:59:59(JDT) → 11/15 00:00:00(JST) となる設定のレコードが 2018年以降に毎年追加されていることが分かりますね

編集した zoneinfo を OS に適用する

編集した zoneinfo を OS に適用してみます

$ sudo ln -sf $PWD/zoneinfo/Asia/Tokyo /etc/localtime

date コマンドで確かめてみましょう

$ date
Wed Aug 15 15:34:38 JDT 2018

期待した通りに、2時間時計が進んで JDT として扱われていることが分かりますね

これで思いのままにタイムゾーンを設定できる状態になりました!

記事の先頭に戻る

4. プログラム内の時刻の違い

さて、zoneinfo を編集することで自由にサマータイムの挙動をテストできるようになったわけですが、これによってプログラムから利用する時刻にはどのような変化があるかを見ていきましょう

単純に触り慣れているという理由だけで、ここでは Python と node.js での挙動について見ていきましょう

Python での挙動

Python で取得される日時のデータを見てみましょう
(Python 3.6.2 の環境でテストしています)

datetime を使う場合

from datetime import datetime
print(datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z%z'))

> '2018-08-15 16:54:25 '

2時間進んだ現在時刻が表示されていますが、これだけだとオフセットの値は分かりません

https://stackoverflow.com/questions/2532729/daylight-saving-time-and-time-zone-best-practices

dateutil を使う場合

from dateutil import tz
from datetime import datetime
tokyo = tz.gettz('Asia/Tokyo')
print(datetime.now(tz=tokyo))

> 2018-08-15 16:44:59.381116+11:00

オフセットが +11:00 となっており想定の動作をしていますね

pytz を使う場合

from datetime import datetime, timedelta
from pytz import timezone
import pytz
tokyo = timezone('Asia/Tokyo')
print(datetime.now(tz=tokyo))

> 2018-08-15 14:57:18.236325+09:00

こちらは JDT ではなく JST の時刻で表示されてしまっているようで、OS で変更した zoneinfo が反映されていないようです

これについて調べてみたところ、 pytz はパッケージ内に zoneinfo を含んでいるようです

github で公開されているリポジトリのミラーはこちらです
github.com

README.txt を見ても tz database の最新のデータを持ってくる的な内容が書かれていますが、ビルドされた pytz のパッケージには zoneinfo のファイル一式が含まれていることが分かりました なので OS が持っている zoneinfo のファイルは参照されないようで、先ほどの例ではサマータイムが反映されていなかったということです

node.js での挙動

次に、node.js (javascript) で取得される日時のデータを見てみましょう
(node.js v8.11.4 の環境でテストしています)

標準の Date を使う場合

let date = new Date();
console.log('[date.toString]', date.toString());
console.log('[date.toLocaleString]', date.toLocaleString());
console.log('[date.toUTCString]', date.toUTCString());

> [date.toString] Sun Aug 19 2018 18:17:22 GMT+1100 (JDT)
> [date.toLocaleString] 2018-8-19 16:17:22
> [date.toUTCString] Sun, 19 Aug 2018 07:17:22 GMT

date.toString は JDT の時刻を返却していますが、date.toLocaleString はどうやら JST の時刻を返却しているようです 試しに US/Central のタイムゾーンに変更しサマータイム中の日時で試してみたところこの場合は date.toLocaleString もサマータイム中の時刻を表示されていたため、恐らく更新されていない zoneinfo のデータがあるのかもしれません

date.toLocaleString の違いが発生している点について原因はハッキリ分かっていないのですが、 node.js の内部で icu4c というライブラリを利用しており、このライブラリをビルドする時に tz database のデータが必要になるようです そのため、恐らく node.js が利用するライブラリの内部に zoneinfo のデータが含まれていると考えられそうです ※こちらハッキリとは分かっていないので間違っているかもしれませんが

Moment.js を使う場合

const moment = require('moment');
console.log(moment().format());

> 2018-08-19T18:30:11+11:00

こちらは想定通りの動作をしてます JDT の時刻が出力されておりオフセットも +11:00 となっていますね

プログラムから日時を扱う場合の挙動

実際にやって見て分かりましたが、どうやら zoneinfo ファイルは OS のものが必ずしも利用されているわけではなく、パッケージによっては zoneinfo を内包しているものもあるようです このことから分かるのは、新しくサマータイムを適用する場合には公式の zoneinfo が更新された後、zoneinfo を内包しているパッケージが更新されてから、自分のシステムの該当パッケージを更新する必要があるということです

OS の設定変更をすればいい感じに適用されるというわけでは無さそうですね
やばい・・!!!!

記事の先頭に戻る

5. データベースでの挙動

さて、プログラムからの挙動についてはざっくりと雰囲気が分かったわけですが、次にデータベースの挙動を見てみましょう ここでは MySQL の挙動を見てみます

MySQL での挙動を試してみる

MySQL でタイムゾーンを扱うためには、まず MySQL に zoneinfo のデータをインポートする必要があります
こちらのコマンドを実行します

$ mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql -p

ということは、zoneinfo が変更される都度 MySQL にもデータのインポートが必要になるということが分かります

次に my.cnf に以下を追加してデフォルトのタイムゾーンを Asia/Tokyo に設定します

[mysqld]
default-time-zone = 'Asia/Tokyo'

これによって NOW() や CURRENT_TIMESTAMP() に設定する値がタイムゾーンに対応したものとなります

テスト用のテーブルを準備

サマータイムの動作確認用に以下のようにテーブルを作成します 動作の違いを確認するために datetime 型と timestamp 型の2種類作成しておきます

CREATE DATABASE summertime_test;

CREATE TABLE summertime_test.time_records (
  text text,
  created_datetime datetime,
  created_timestamp timestamp,
  utc_datetime datetime,
  utc_timestmp timestamp DEFAULT CURRENT_TIMESTAMP
);

created_datetime、created_timestamp には now() で取得されるタイムスタンプを、utc_datetime、utc_timestmp には utc_timestamp() で取得されるタイムスタンプを保存することにします

サマータイム前 → サマータイム に切り替わる時の挙動

まず、サマータイムに切り替わった時に登録されたレコードがどのように表示されるかを見てみましょう

サマータイム前

サマータイムに入る前と後でそれぞれ 現在時刻を登録してみます

# サマータイム前
INSERT INTO time_records (text, created_datetime, created_timestamp, utc_datetime, utc_timestmp)
VALUES ('サマータイム前', now(), now(), utc_timestamp(), utc_timestamp());

この状態でテーブルのデータを見ます timestamp 型のデータは SQL を実行しているセッションに設定されたタイムゾーンに応じて自動で時刻が変換されることが分かります

mysql> select * from time_records;
+-----------------------+---------------------+---------------------+---------------------+---------------------+
| text                  | created_datetime    | created_timestamp   | utc_datetime        | utc_timestmp        |
+-----------------------+---------------------+---------------------+---------------------+---------------------+
| サマータイム前        | 2018-08-16 15:48:25 | 2018-08-16 15:48:25 | 2018-08-16 06:48:25 | 2018-08-16 06:48:25 |
+-----------------------+---------------------+---------------------+---------------------+---------------------+
1 row in set (0.00 sec)

# オフセットを0に変更 (このセッションのみ)
mysql> set time_zone = '+00:00';
Query OK, 0 rows affected (0.00 sec)

mysql> select * from time_records;
+-----------------------+---------------------+---------------------+---------------------+---------------------+
| text                  | created_datetime    | created_timestamp   | utc_datetime        | utc_timestmp        |
+-----------------------+---------------------+---------------------+---------------------+---------------------+
| サマータイム前        | 2018-08-16 15:48:25 | 2018-08-16 06:48:25 | 2018-08-16 06:48:25 | 2018-08-15 21:48:25 |
+-----------------------+---------------------+---------------------+---------------------+---------------------+
1 row in set (0.01 sec)

サマータイム中

次にサマータイムに入ってからデータを登録して確認してみます

# サマータイム中
INSERT INTO time_records (text, created_datetime, created_timestamp, utc_datetime, utc_timestmp)
VALUES ('サマータイム中', now(), now(), utc_timestamp(), utc_timestamp());

この状態でテーブルのデータを見ます

mysql> select * from time_records;
+-----------------------+---------------------+---------------------+---------------------+---------------------+
| text                  | created_datetime    | created_timestamp   | utc_datetime        | utc_timestmp        |
+-----------------------+---------------------+---------------------+---------------------+---------------------+
| サマータイム前        | 2018-08-16 15:48:25 | 2018-08-16 15:48:25 | 2018-08-16 06:48:25 | 2018-08-16 06:48:25 |
| サマータイム中        | 2018-08-16 17:50:09 | 2018-08-16 17:50:09 | 2018-08-16 06:50:09 | 2018-08-16 06:50:09 |
+-----------------------+---------------------+---------------------+---------------------+---------------------+
2 rows in set (0.00 sec)

# オフセットを0に変更 (このセッションのみ)
mysql> set time_zone = '+00:00';
Query OK, 0 rows affected (0.00 sec)

mysql> select * from time_records;
+-----------------------+---------------------+---------------------+---------------------+---------------------+
| text                  | created_datetime    | created_timestamp   | utc_datetime        | utc_timestmp        |
+-----------------------+---------------------+---------------------+---------------------+---------------------+
| サマータイム前        | 2018-08-16 15:48:25 | 2018-08-16 06:48:25 | 2018-08-16 06:48:25 | 2018-08-15 21:48:25 |
| サマータイム中        | 2018-08-16 17:50:09 | 2018-08-16 06:50:09 | 2018-08-16 06:50:09 | 2018-08-15 21:50:09 |
+-----------------------+---------------------+---------------------+---------------------+---------------------+
2 rows in set (0.00 sec)

オフセットを 0 に設定した際に created_timestamp が UTC 時刻で表示されているところについては想定の通りに動作していそうです
一方、utc_timestmp の値が UTC 時刻よりさらに-9時間になってしまうところが何かおかしいですね

サマータイム → サマータイム後 に切り替わる時の挙動

次にサマータイムが終わる時の挙動を見てみます

サマータイム中

まずはサマータイム中にデータを登録します

INSERT INTO time_records (text, created_datetime, created_timestamp, utc_datetime, utc_timestmp)
VALUES ('サマータイム中(2)', now(), now(), utc_timestamp(), utc_timestamp());

SELECT の結果を確認します

mysql> select * from time_records;
+--------------------------+---------------------+---------------------+---------------------+---------------------+
| text                     | created_datetime    | created_timestamp   | utc_datetime        | utc_timestmp        |
+--------------------------+---------------------+---------------------+---------------------+---------------------+
| サマータイム中(2)        | 2018-08-16 17:54:54 | 2018-08-16 17:54:54 | 2018-08-16 06:54:54 | 2018-08-16 06:54:54 |
+--------------------------+---------------------+---------------------+---------------------+---------------------+
1 row in set (0.00 sec)

# オフセットを0に変更 (このセッションのみ)
mysql> set time_zone = '+00:00';
Query OK, 0 rows affected (0.00 sec)

mysql> select * from time_records;
+--------------------------+---------------------+---------------------+---------------------+---------------------+
| text                     | created_datetime    | created_timestamp   | utc_datetime        | utc_timestmp        |
+--------------------------+---------------------+---------------------+---------------------+---------------------+
| サマータイム中(2)        | 2018-08-16 17:54:54 | 2018-08-16 06:54:54 | 2018-08-16 06:54:54 | 2018-08-15 19:54:54 |
+--------------------------+---------------------+---------------------+---------------------+---------------------+
1 row in set (0.00 sec)

サマータイム終了後

次にサマータイムが終わってからデータを登録します

INSERT INTO time_records (text, created_datetime, created_timestamp, utc_datetime, utc_timestmp)
VALUES ('サマータイム後', now(), now(), utc_timestamp(), utc_timestamp());

SELECT の結果を確認します

mysql> SELECT * FROM time_records;
+--------------------------+---------------------+---------------------+---------------------+---------------------+
| text                     | created_datetime    | created_timestamp   | utc_datetime        | utc_timestmp        |
+--------------------------+---------------------+---------------------+---------------------+---------------------+
| サマータイム中(2)        | 2018-08-16 17:54:54 | 2018-08-16 17:54:54 | 2018-08-16 06:54:54 | 2018-08-16 06:54:54 |
| サマータイム後           | 2018-08-16 16:00:10 | 2018-08-16 16:00:10 | 2018-08-16 07:00:10 | 2018-08-16 07:00:10 |
+--------------------------+---------------------+---------------------+---------------------+---------------------+
2 rows in set (0.00 sec)

# オフセットを0に変更 (このセッションのみ)
mysql> set time_zone = '+00:00';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM time_records;
+--------------------------+---------------------+---------------------+---------------------+---------------------+
| text                     | created_datetime    | created_timestamp   | utc_datetime        | utc_timestmp        |
+--------------------------+---------------------+---------------------+---------------------+---------------------+
| サマータイム中(2)        | 2018-08-16 17:54:54 | 2018-08-16 06:54:54 | 2018-08-16 06:54:54 | 2018-08-15 19:54:54 |
| サマータイム後           | 2018-08-16 16:00:10 | 2018-08-16 05:00:10 | 2018-08-16 07:00:10 | 2018-08-15 20:00:10 |
+--------------------------+---------------------+---------------------+---------------------+---------------------+
2 rows in set (0.00 sec)

想定ではサマータイム後に登録したデータの created_timestamp はオフセットを0に変更した後では 2018-08-16 07:00:10 と取得されてほしいですがどうやら違うようでした 2018-08-16 16:00:10 からサマータイム中に適用されるオフセットの11時間が引かれた時刻が UTC 時刻として計算されています

またこの場合も utc_timestmp の値が想定と違って、今回は UTC 時刻よりさらに-11時間となってしまっています

何度かデータを入れなおしたりしてサマータイムが終わるパターンを試しましたがこの結果と同様の動作をしていました

うむ・・・・ これは何が起きているんでしょうか・・・

僕は MySQL のタイムゾーンの扱いに詳しくないので単純に使い方が間違っているのかもしれませんが、かなり厄介そうだということはよく分かりました

記事の先頭に戻る

6. cron の動作

さて、最後に cron の動作がどうなるかを見ていきましょう cron については過去にソースコードから挙動を調べたことがあり、その際にサマータイムに対応した処理となっていることが分かっていました

moro-archive.hatenablog.com

ただ、正しく挙動を読み解けていたのか自信がなかったので、サマータイムの切り替わりのタイミングでどのような動作をするのか今回実際に調べて見ました

サマータイム前 → サマータイム に切り替わる時の挙動

8/15 18:15:00 をサマータイム開始の起点とし2時間時計を進めるように設定しました つまり、18:14:59 の1秒後が 20:15:00 となります

cron のジョブは以下のように設定しています

# 1分間隔で実行されるジョブ
*/1 * * * * /home/vagrant/shared/job_1min.sh
# 1時間間隔で実行されるジョブ
0 */1 * * * /home/vagrant/shared/job_1hour.sh
# 19:00 に実行されるジョブ
0 19 * * * /home/vagrant/shared/job_19.sh
# 20:00 に実行されるジョブ
0 20 * * * /home/vagrant/shared/job_20.sh

実行結果は以下のようになりました

$ cat corn_case1.log
this 1min job. started at  Wed Aug 15 18:14:01 JST 2018
this 1hour job. started at  Wed Aug 15 20:15:01 JDT 2018
this 1min job. started at  Wed Aug 15 20:15:01 JDT 2018
this 19:00 job. started at  Wed Aug 15 20:15:01 JDT 2018
this 20:00 job. started at  Wed Aug 15 20:15:01 JDT 2018
this 1min job. started at  Wed Aug 15 20:16:01 JDT 2018

1分間隔、1時間間隔のジョブはスキップされた時間に含まれる分は実行されておらず、純粋に 20:15:00 のタイミングで実行されるものだけが実行されています また、スキップされた時間に含まれる 19:00、20:00 開始のジョブはいずれも 20:15:00 のタイミングで実行されていることが分かります

これらは前述の記事で調べた通りに、 サマータイムが始まり時間がスキップされた場合のリカバリ処理では、スキップされた時間帯に含まれる定時実行のジョブのみリカバリされ、間に含まれるワイルドカードのジョブはスキップされる、という挙動をすることが確認できました

サマータイム → サマータイム後 に切り替わる時の挙動

次はサマータイムの終わりを確認するために 8/15 20:25:00 をサマータイム終了の起点とし2時間時計が戻るように設定しました
つまり、20:24:59 の1秒後が 18:25:00 となります

cron のジョブは以下のように設定しています

# 1分間隔で実行されるジョブ
*/1 * * * * /home/vagrant/shared/job_1min.sh
# 1時間間隔で実行されるジョブ
0 */1 * * * /home/vagrant/shared/job_1hour.sh
# 18:00 に実行されるジョブ
0 18 * * * /home/vagrant/shared/job_18.sh
# 19:00 に実行されるジョブ
0 19 * * * /home/vagrant/shared/job_19.sh
# 20:00 に実行されるジョブ
0 20 * * * /home/vagrant/shared/job_20.sh

実行結果は以下のようになりました

$ cat cron.log
this 1min job. started at  Wed Aug 15 20:23:01 JDT 2018
this 1min job. started at  Wed Aug 15 20:24:01 JDT 2018
this 1min job. started at  Wed Aug 15 18:25:01 JST 2018
this 1min job. started at  Wed Aug 15 18:26:01 JST 2018
・・・
this 1min job. started at  Wed Aug 15 18:59:01 JST 2018
this 1hour job. started at  Wed Aug 15 19:00:01 JST 2018
this 1min job. started at  Wed Aug 15 19:00:01 JST 2018
・・・
this 1min job. started at  Wed Aug 15 19:59:01 JST 2018
this 1hour job. started at  Wed Aug 15 20:00:01 JST 2018
this 1min job. started at  Wed Aug 15 20:00:01 JST 2018
this 1min job. started at  Wed Aug 15 20:01:01 JST 2018
・・・

1分間隔、1時間間隔のジョブはスキップされた時間に含まれる分は実行されておらず、純粋に 18:25:00 のタイミングで実行されるものだけが実行されています また、スキップされた時間に含まれる 19:00、20:00 開始のジョブはいずれも実行されていないことがわかります

これらも以前調べた通りに、 サマータイムが終わり時間が巻き戻された場合のリカバリ処理では、 巻き戻された時間帯に含まれる定時実行のジョブ、ワイルドカードのジョブは全てスキップされる、という挙動をすることが確認できました

Ubuntu の cron のソースコード

先ほどの cron についての記事では CentOS について調査していたので cronie のソースを調べていたため、Ubuntu に含まれる cron のコードもざっと処理を見てみましたが、サマータイムに関連する、時刻のズレが発生した部分の処理はコードは若干違いそうでしたが同じ仕様になっているようでした

Ubuntu の cron のソースコードは以下のコマンドで取得できます

$ git clone -b ubuntu/xenial https://git.launchpad.net/ubuntu/+source/cron

さて、cron について今回実際にテストしてみた動作でも、記事で調べた通りの動きをすることが確認できました サマータイムが始まる時にはスキップされた時間に含まれる定時実行の処理がまとめて実行されてしまうので、ジョブの設計によってはここは問題になりそうな気がしますね

記事の先頭に戻る

7. まとめ

さて、今回は日本でサマータイムが導入された場合にどのような対策が必要か、またプログラムで利用するタイムスタンプのデータがどのように変わるかを調べてみました
軽い気持ちでやり始めてサクッと記事にしようと思っていたんですが、結果的に意外とがっつり時間がかかってしまいました・・・

もしも今後サマータイムが導入されたら

今回調べて見た結論としては "めちゃくちゃ面倒くさい!!!!!!!"
サマータイムがある国のエンジニアは本当にすごいと思いました、尊敬します

プログラムからのタイムスタンプの動作や cron の動作についてはある程度想定していた通りでしたが、データベースの処理については画一的な対処は恐らく難しそうですね
ORM を使っているようなアプリケーションは、時刻に関してどのように DB の型へマッピングされているかを詳しく調べる必要があります
また、ライブラリに zoneinfo を含んでいるものを使用している場合は、必然的にライブラリのバージョンアップが必要になるのでそれだけでもかなり面倒だと思います

結局のところ、サービスのどの処理に影響があるかを個別に調査しなければ、OS の設定をちょちょっと変えるとかのレベルでどうにかなるものでもなさそうです

サマータイムは面倒くさい!

実際にサマータイムを導入するとなれば、こういう調査だけでも日本中もしくは日本での利用が想定されている海外にあるアプリケーションを1つ1つやる手間が必要で、その上にサマータイム対応の修正・テストが加わるわけですからとんでもなく壮大な工数がかかることが想定できます

恐らく結果的には無駄にお金が移動するだけで特に何も生み出さないと思うので、サマータイム導入の是非に関しては冷静に議論して頂きたいですね
(とはいえ多くのITベンダーはサマータイム特需によってものすごく潤うはずなので、そういう意味で強行される可能性もありそうだなぁ・・・・)

今回利用した仮想サーバー環境

今回テストに利用した Ubuntu 16.04 LTS の環境は Vagrant で構築して利用しました 同様の環境で確認してみたい方はこちらのリポジトリで環境構築に必要なファイルを一式公開しています github.com

今回やってみて分からなかった MySQL での挙動について詳しい方がいましたら、ぜひはてブなどからツッコミをお願い致します!

ということで、力尽きたのでこの辺で終わりにしたいと思います

エンジョイサマータイム!

記事の先頭に戻る