最近ICT界隈ではサマータイムが実施されるのではないかという話題でもちきりです。個人的にはとんでもないことだと思うのですが、仮に実施されることになったとして、一体どんなことが起こるのか事前に調べておきたいと思い、実験サーバを立ててみることにしました。
今回試すのはCentOS7をインストールしたサーバで、アプリケーションとしてWordPressを立ててどのように動作するかを一通り確認してみます。
サーバのサマータイム化
そもそも、サーバをサマータイムに対応させるとはどういうことなのでしょうか。詳細な議論を省いてごく簡単に説明します。
CentOS7(Linux)においては、zoneinfoにおいて定義ファイルを設置することがこれに相当します。現在の設定は、次のようにすると参照できます。
[north@vortex ~]$ zdump -v /usr/share/zoneinfo/Asia/Tokyo /usr/share/zoneinfo/Asia/Tokyo -9223372036854775808 = NULL /usr/share/zoneinfo/Asia/Tokyo -9223372036854689408 = NULL /usr/share/zoneinfo/Asia/Tokyo Sat Dec 31 14:59:59 1887 UTC = Sun Jan 1 00:18:58 1888 LMT isdst=0 gmtoff=33539 /usr/share/zoneinfo/Asia/Tokyo Sat Dec 31 15:00:00 1887 UTC = Sun Jan 1 00:00:00 1888 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo Sat May 1 14:59:59 1948 UTC = Sat May 1 23:59:59 1948 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo Sat May 1 15:00:00 1948 UTC = Sun May 2 01:00:00 1948 JDT isdst=1 gmtoff=36000 /usr/share/zoneinfo/Asia/Tokyo Sat Sep 11 13:59:59 1948 UTC = Sat Sep 11 23:59:59 1948 JDT isdst=1 gmtoff=36000 /usr/share/zoneinfo/Asia/Tokyo Sat Sep 11 14:00:00 1948 UTC = Sat Sep 11 23:00:00 1948 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo Sat Apr 2 14:59:59 1949 UTC = Sat Apr 2 23:59:59 1949 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo Sat Apr 2 15:00:00 1949 UTC = Sun Apr 3 01:00:00 1949 JDT isdst=1 gmtoff=36000 /usr/share/zoneinfo/Asia/Tokyo Sat Sep 10 13:59:59 1949 UTC = Sat Sep 10 23:59:59 1949 JDT isdst=1 gmtoff=36000 /usr/share/zoneinfo/Asia/Tokyo Sat Sep 10 14:00:00 1949 UTC = Sat Sep 10 23:00:00 1949 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo Sat May 6 14:59:59 1950 UTC = Sat May 6 23:59:59 1950 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo Sat May 6 15:00:00 1950 UTC = Sun May 7 01:00:00 1950 JDT isdst=1 gmtoff=36000 /usr/share/zoneinfo/Asia/Tokyo Sat Sep 9 13:59:59 1950 UTC = Sat Sep 9 23:59:59 1950 JDT isdst=1 gmtoff=36000 /usr/share/zoneinfo/Asia/Tokyo Sat Sep 9 14:00:00 1950 UTC = Sat Sep 9 23:00:00 1950 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo Sat May 5 14:59:59 1951 UTC = Sat May 5 23:59:59 1951 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo Sat May 5 15:00:00 1951 UTC = Sun May 6 01:00:00 1951 JDT isdst=1 gmtoff=36000 /usr/share/zoneinfo/Asia/Tokyo Sat Sep 8 13:59:59 1951 UTC = Sat Sep 8 23:59:59 1951 JDT isdst=1 gmtoff=36000 /usr/share/zoneinfo/Asia/Tokyo Sat Sep 8 14:00:00 1951 UTC = Sat Sep 8 23:00:00 1951 JST isdst=0 gmtoff=32400 /usr/share/zoneinfo/Asia/Tokyo 9223372036854689407 = NULL /usr/share/zoneinfo/Asia/Tokyo 9223372036854775807 = NULL
これを見てみると、興味深いデータがすでにzoneinfoに取り込まれていることが分かります。太平洋戦争直後に実施されたという「サンマータイム」の頃のデータであるとか、1888年に日本標準時が制定されたときに18分58秒のオフセットがなされたようだ、ということが分かります…が、脱線なので省略します。
zoneinfoはバイナリファイルなので、これを直接編集できません。ソースコードからコンパイルする必要があります。元となるファイルは、こちらからダウンロードできます。
必要なのはデータのみですので、Latest versionからData Only DistributionをダウンロードすればOKです。これを展開すると、「asia」という名前のテキストファイルが見つかります。エディタで開いてみると以下の行が見つかるでしょう。
# From Takayuki Nikai (2018-01-19): # The source of information is Japanese law. # http://www.shugiin.go.jp/internet/itdb_housei.nsf/html/houritsu/00219480428029.htm # http://www.shugiin.go.jp/internet/itdb_housei.nsf/html/houritsu/00719500331039.htm # ... In summary, it is written as follows. From 24:00 on the first Saturday # in May, until 0:00 on the day after the second Saturday in September. # Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S 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
ここが、先ほどzdumpで表示した過去のサマータイム実施を記述した部分です。マニュアル等を参考にすれば、今すぐにも自分のサーバをサマータイムにできそうです。たとえば2018年8月29日18時から2時間のサマータイムを開始したいと思えば、次のように追記すればよいのです。
Rule Japan 2018 only - Aug 29 18:00 2:00 D
ソースが準備できたら、コンパイルします。私は実験用のサーバで試していますので、いきなりzoneinfoを書き換えてしまいますが、もし皆さんがテストするつもりでしたら、うまく隔離するようにしてください。zoneinfoを破壊してしまった場合の動作の保証は致しかねます。
zoneinfoのコンパイルにはzicを使います。asiaファイルを指定して実行するだけです。
[root@daylight tz]# zic asia "asia", line 84: warning: time zone abbreviation differs from POSIX standard (+04) "asia", line 85: warning: time zone abbreviation differs from POSIX standard (+0430) : :
警告がどっと出ますが、最後までいけるはずです。これで/usr/share/zoneinfoを上書きします。あとは所定の時刻になると、自動的にUTC+11(JDT)に切り替わります。
サマータイムで何が起こるか
さて、本当にサマータイムに設定できたのでしょうか? 試してみると、dateなどの時刻を参照するコマンドはすべてJDTを指すようになっているようです。
[root@daylight tz]# date Mon Sep 3 12:18:10 JDT 2018
ファイルを作成してみると、新規に作ったファイルはJDTになります。難しいのは、JSTの時に作ったファイルは、JSTの時刻を示したままであるということです。
[root@daylight ~]# ls -l old now -rw-r--r-- 1 root root 0 Aug 30 14:41 old -rw-r--r-- 1 root root 0 Aug 31 11:27 now [root@daylight ~]# ls --full-time old now -rw-r--r-- 1 root root 0 2018-08-30 14:41:38.938476571 +0900 old -rw-r--r-- 1 root root 0 2018-08-31 11:27:13.045665513 +1100 now
これはどういうことかというと、サマータイム切替時間中に作成したファイルでは、lsでは一見同時刻に見えるファイルが実は異なる時刻に作成されていたということが有り得る、ということです。Unixのファイルシステムはepoch(1970年1月1日0時0分0秒UTCからの経過時間)で時刻を測っているので一貫性は常にあるのですが、ローカルタイムが変更されうるという状況は、こういう面倒な事象を引き起こすということなのです。
アプリケーションの動作を調べてみましょう。気になるのが、/var/log/messagesのようなログファイルです。実はJDTに切り替わった後も、rsyslog(CentOS7の標準のsyslog)はずっとJSTのままで出力を続けていました。ローカルタイムが切り替わったことに気づかない様子なのです。以下の例では、20:26(JDT)にloggerを使ってログを書いているのに、messagesには18:26(JST)で書かれてしまっています。
[root@daylight tz]# date Thu Aug 30 20:26:22 JDT 2018 [root@daylight tz]# logger -p local0.info logger test [root@daylight tz]# tail /var/log/messages Aug 30 18:01:01 daylight systemd: Created slice User Slice of root. Aug 30 18:01:01 daylight systemd: Starting User Slice of root. Aug 30 18:01:01 daylight systemd: Started Session 2 of user root. Aug 30 18:01:01 daylight systemd: Starting Session 2 of user root. Aug 30 18:01:01 daylight systemd: Removed slice User Slice of root. Aug 30 18:01:01 daylight systemd: Stopping User Slice of root. Aug 30 18:03:38 daylight sshd[1861]: WARNING: 'UsePAM no' is not supported in Red Hat Enterprise Linux and may cause several problems. Aug 30 18:03:41 daylight sshd[1863]: WARNING: 'UsePAM no' is not supported in Red Hat Enterprise Linux and may cause several problems. Aug 30 18:03:46 daylight sshd[1866]: WARNING: 'UsePAM no' is not supported in Red Hat Enterprise Linux and may cause several problems. Aug 30 18:26:39 daylight root: logger test
そもそも、デフォルトのrsyslogはタイムスタンプにタイムゾーン情報を書きません。このため仮にrsyslogがローカルタイムにリアルタイムで追従したとすると、時刻の一貫性がなくなってしまうことになります。
Apacheはどうでしょうか。
61.211.224.11 - - [31/Aug/2018:10:01:50 +0900] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 175 "http://daylight.northcave.sakura.ad.jp/wp-admin/post-new.php" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 61.211.224.11 - - [31/Aug/2018:10:01:52 +0900] "GET / HTTP/1.1" 200 72941 "http://daylight.northcave.sakura.ad.jp/wp-admin/post-new.php" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 61.211.224.11 - - [31/Aug/2018:10:01:52 +0900] "GET /wp-content/uploads/2018/08/dateformat.png HTTP/1.1" 304 - "http://daylight.northcave.sakura.ad.jp/" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"
ApacheもJSTで出力を続けていますが、タイムスタンプには+0900と記録されていて、どのタイムゾーンにいるかがわかるようになっています。これならば時刻のシフトがあっても一貫性は保たれます。
WordPressの動作も確かめてみました。テストサイト上で切替前後に記事を書いたり読んだりしてみたところ、WordPressはサマータイム切替にリアルタイムに追従しているように見えました。編集画面や投稿記事の時刻は、ファイルシステムと同様に書き込んだときのローカルタイムを指し示しています。少し困ったのは、WordPressではタイムゾーンを示すJSTやJDTのコードを日付に入れられるのですが、これは単に現時点でのサーバのローカルタイムを示しているだけで、投稿記事のタイムスタンプとは無関係であるということです。JSTの時に書いた記事でも、サマータイムが実施されると、時刻はそのままでJDTと表示されるという奇妙な状況になってしまうのです。
WordPressのデータベースを覗いてみると、記事にはUTCとlocaltimeの両方が記録されています。問題はlocaltimeにはゾーン情報がまったくなく、UTCからの時差を計算していないということです。このため、WordPressではサマータイム導入前後に記事を投稿すると、時系列の前後が入れ替わってしまうことが有り得ます。ちょっと惜しいですね。
追従するプログラムとしないプログラムの違い
さて、zoneinfoによってローカルタイムがJDTに切り替わると、いくつかのアプリはそれに追従し、いくつかは追従しないことが分かりました。この差はどうして生まれてしまうのでしょうか?
実際にコードを書いて動作を確認してみましょう。10秒毎にepoch、UTC、ローカルタイムを表示するプログラムです。
#include <stdio.h> #include <time.h> int main(void) { time_t now; struct tm *t, *u; for (;;) { now = time(NULL); printf("%ld\t", now); u = gmtime(&now); printf("%04d/%02d/%02d %02d:%02d:%02d", u->tm_year+1900, u->tm_mon+1, u->tm_mday, u->tm_hour, u->tm_min, u->tm_sec); printf("\t"); t = localtime(&now); printf("%04d/%02d/%02d %02d:%02d:%02d", t->tm_year+1900, t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); printf("\n"); fflush(stdout); sleep(10); } return 0; }
これを実行しつつ、JSTからJDTに切り替えてみます。すると、自然に時刻は切り替わることが分かります。
1535688869 2018/08/31 04:14:29 2018/08/31 13:14:29 1535688879 2018/08/31 04:14:39 2018/08/31 13:14:39 1535688889 2018/08/31 04:14:49 2018/08/31 13:14:49 1535688899 2018/08/31 04:14:59 2018/08/31 13:14:59 <---- 切替時刻 1535688909 2018/08/31 04:15:09 2018/08/31 15:15:09 1535688919 2018/08/31 04:15:19 2018/08/31 15:15:19 1535688929 2018/08/31 04:15:29 2018/08/31 15:15:29 1535688939 2018/08/31 04:15:39 2018/08/31 15:15:39 1535688949 2018/08/31 04:15:49 2018/08/31 15:15:49
そう、localtimeがzoneinfoをきちんとチェックするならば、これは当たり前の動作に思えます。ではrsyslogやApacheは、なぜJDTの導入に追従できないのでしょう? そこでrsyslogのソースコードを追いかけてみました。すると、rsyslogではlocaltime_rを利用していることが分かりました。
localtimeは古い仕様の関数で、上記の例のように変換結果はスタティックな構造体のポインタを返すようになっています。これはお行儀が悪いので、現代風のlocaltime_rというのが用意されています。こちらは構造体のポインタを渡すと、そこへ結果を格納して返してくれます。
struct tm *gmtime(const time_t *timep); struct tm *gmtime_r(const time_t *timep, struct tm *result); struct tm *localtime(const time_t *timep); struct tm *localtime_r(const time_t *timep, struct tm *result);
さて、違いはそれだけかと思いきや、manpageをよく読んで見ると、こんな記述がありました。
According to POSIX.1-2004, localtime() is required to behave as though tzset(3) was called, while localtime_r() does not have this require‐ ment. For portable code tzset(3) should be called before local‐ time_r().
localtimeはtzsetをコールするが、localtime_rはその限りではない。互換性を保つにはtzsetを明示的にコールせよ、とあります。
驚いて、先ほどの時刻を表示するプログラムをlocaltime_rで書き直して試してみました。
#include <stdio.h> #include <time.h> int main(void) { time_t now; struct tm t, u; for (;;) { now = time(NULL); printf("%ld\t", now); gmtime_r(&now, &u); printf("%04d/%02d/%02d %02d:%02d:%02d", u.tm_year+1900, u.tm_mon+1, u.tm_mday, u.tm_hour, u.tm_min, u.tm_sec); printf("\t"); /* tzset(); */ localtime_r(&now, &t); printf("%04d/%02d/%02d %02d:%02d:%02d", t.tm_year+1900, t.tm_mon+1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec); printf("\n"); fflush(stdout); sleep(10); } return 0; }
tzsetをコメントアウトしておくと、JDTへの切替には追従せず、プログラムを起動している限りはずっとJSTのままで動作してしまいます。いったんプログラムを停止して再起動すると、localtime_rはzoneinfoを読み直すのでJDTで表示されるようになります。リアルタイムにサマータイムに追従するには、tzsetをコールする必要があります。コメントアウトを外してコンパイルし同じ実験を繰り返すと、きちんとJDTへの切替に追従するようになります。
rsyslogのソースには、tzsetはどこにもありませんでした。Apacheも動作を見る限り、どうやらコールしていないようです。果たしてこれらのプログラムは、ライブラリの仕様を知らずにバグってしまっているのでしょうか? いえ、それはちょっと考えられません。rsyslogもApacheも世界中で使われているソフトウェアです。過去にそれが問題にならない訳がないからです。故意に追従しないように作られているに違いありません。ただ、これはこれで少々面倒な状況を引き起こすことも事実です。というのは、サマータイムは数ヶ月に渡って続くものであり、その間にプロセスやサーバがリブートされてしまうと、結局zoneinfoに従ってローカルタイムが切り替わってしまうからです。するとタイムスタンプの一貫性は損なわれます。Apacheのような形式ならば問題ありませんが、rsyslogのようなローカルタイムしか出力していない場合は大問題になるでしょう。
WordPressのようなアプリケーションがJDT切替に追従しているように見えるのは、もともとWebアプリケーションであるためプロセスが頻繁に起動されているということ、PHPのlocaltimeはtzsetをコールしているようで、常に最新のzoneinfoに従って時刻を返すことから、きちんと追従しているように見えるということのようです。
ということで、結局謎は残ります。どうしてアプリケーションやミドルウェアによって、時刻の変更に追従したりしなかったりするのか? 故意にやっているようだということは分かりましたが、理由がよくわかりません。いずれにせよ、それぞれのプログラムがどう振舞うのかを確認していかないといけないようです。
9月19日追記: その後、社内から「tzsetはリエントラントではないのでは?」と指摘をもらいました。調べてみたところ、tzsetはextern変数に依存しており、確かにスレッドセーフではありませんでした。このためマルチプロセスなプログラムはlocaltimeもtzsetも使えないということなのでしょう。スレッドセーフなバージョンのtzsetが用意されていないようなので、この種のプログラムはそもそもサマータイムに対応できないという結論になってしまうようです。
ワークアラウンド
さて、ここまで判明した事柄から、サーバ管理者としてやっておくべきことをピックアップし、対処を考えてみましょう。
サマータイム対応は誰がどうやるのか?
サマータイムへの切替は、zoneinfoで行うことは上述の通りです。CentOS7や、他の多くのディストリビューションにおいては、これはアップデートを待つということに他なりません。サマータイム導入が正式に決定されれば、tzdataに反映され、updateによってzoneinfoが配布されるでしょう。それを待てばよいということになります。無論、サマータイムが実施されるまでにアップデートを完了させることが必要となります。数台のサーバの管理でしたらそれほど手間ではありませんが、大規模システムだと大事になりそうです。場合によっては、zoneinfoファイルを自分で作成して配布することを検討しなければならないかもしれません。
タイムスタンプの形式は見直すべき
ログファイルのタイムスタンプは、多くの場合ローカルタイムを示しているという前提になっていると思います。これまでは、JSTは絶対であるという前提があったので問題はありませんでしたが、今後はそうはいかなくなると仮定せざるを得ません。となると、タイムスタンプには常にゾーン情報を入れるべきということになります。
たとえばrsyslogの場合、これを変更するためには次のようにします。/etc/rsyslog.confに次のような記載があります。
# Use default timestamp format $ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
この部分をコメントアウトしてrsyslogをリスタートすると、タイムスタンプは次のような形式に変わります。
2018-08-31T20:07:29.834614+11:00 daylight systemd: Stopping System Logging Service... 2018-08-31T20:07:29.835704+11:00 daylight rsyslogd: [origin software="rsyslogd" swVersion="8.24.0" x-pid="811" x-info="http://www.rsyslog.com"] exiting on signal 15. 2018-08-31T20:07:29.852387+11:00 daylight systemd: Starting System Logging Service... 2018-08-31T20:07:29.878645+11:00 daylight rsyslogd: [origin software="rsyslogd" swVersion="8.24.0" x-pid="2977" x-info="http://www.rsyslog.com"] start 2018-08-31T20:07:29.884256+11:00 daylight systemd: Started System Logging Service.
こうすれば、プロセスやサーバの状態に関わらず、一貫した時刻が記録されるようになります。
アプリケーションは再検証が必要
アプリケーションの動作については、事細かに検証することが必要です。一見対応できているように見えるものも、実は動作がおかしいということが有り得ます。特にサマータイムに切り替える瞬間と、その前後の動作は、念入りに確認しなければならないと思います。
サマータイム実施を無視できるか?
サマータイムが導入されると色々難しい問題が起こりそうです。では、これを無視することはできるでしょうか?
上記の通り、zoneinfoにJDTの定義が入り、そのupdateを受け取ってしまうと、サーバはサマータイムを実施するようになってしまいます。これを無視するためには、zoneinfoのアップデートを拒否するか、ローカルのTZ設定を優先順位を高めて入れることになると思います。ここでは具体的な手順には触れませんが、zoneinfoのロード順などの設定でできるような気がします。もしサマータイムの実施が決まり、それを拒否したいと思ったら、手順を確立したいと思います。
ただ、周囲のサーバがすべてJDTで動いている中で自分たちだけ無視し続けることができるかは定かではありません。
まとめ
以上、CentOS7をJDTにして確認できた事柄をまとめてみました。
これまで日本標準時は絶対的な基準で、どちらかと言えばUTCの方が9時間遅れていると考えがちだったのですが、もしサマータイムが導入されるとなると、もう日本国内基準は頼りにせず、UTCを中心に考えたほうがよいのではないかと思えました。特に今後、サーバが国を超えて設置され、バランシングやDRなどで運用されるとなると、ますますその意味は重みを増すのかなと感じます。
今後は、cronの動作を確認したいと思っています。おもしろいことが分かったら、また記事にするかもしれません。