Linux + PostgreSQL + Perl な WEBアプリケーションにおけるタイムゾーンの実装

このエントリーをはてなブックマークに追加

こんにちは、冬は冬眠する生き物、chappie です。


少し前に、アプリケーションの国際化対応の一環として、タイムゾーン対応の実装を担当しました。今回は、そのときの対応の概要について書いてみたいと思います。


WEBアプリケーションにおけるタイムゾーンの扱いはとても基本的なことのように思えますが、意外とまとまった情報がウェブを見てても少なかったように感じました。今後、私たちと似たようなWEBアプリケーションでタイムゾーンサポートを実装する予定がある方のお役に立てる部分があるかもしれません。


■ タイムゾーン対応する、とは?

ここで、タイムゾーン機能をサポートするということは、アプリケーションを操作する人がどのタイムゾーンで日時データを入出力するか指定できるようにする、ということです。

たとえば、ログインして操作するようなアプリケーションであれば、ログインしているユーザは自分のユーザ情報として、お好みのタイムゾーン(たとえば、日本標準時や、西ヨーロッパ時間、など)を設定します。そのユーザがログインして操作する場合、日時データをそのタイムゾーンにあわせて表示したり、入力された日時データはそのタイムゾーンでの時刻だとして保存したりします。

とある保存された日時が、あるユーザから見ると 2012-01-20 10:00 (日本標準時)であり、別のユーザから見ると 2012-01-20 01:00 (GMT) である、というようなことです。


■ タイムゾーンのマスター情報 tz database

オープンソースソフトウェアの世界でタイムゾーン情報の標準的なマスターとなるのは tz database (またの名を Olson DB)です。ボランティアベースでメンテナンスされてきたもので、Linux ベースのOSや、オープンソースのプログラミング言語環境では、これがタイムゾーン情報の参照先となっています。 tz database の内容を最新に保つことで、どのエリアは時差がGMTから何時間なのかとか、夏時間がいつからいつまでなのかなどの情報が実社会に即したものに保つことができます。

Asia/Tokyo や、 Europe/London などの形式でエリアを指定します。エリアは、時差やサマータイムの有無によって区分されています。あるエリアでサマータイムの実施時期などが変わった場合、情報が更新されます。最近の大きな変更では、ロシアが夏時間を廃止したり(2011年)、アメリカでのサマータイムの実施時期が変わったり(2007年)しました。最新の情報は公式サイト(下記参照)から圧縮されたファイルをダウンロードできます。

ちなみに、2011年の10月、ちょうど私たちがタイムゾーン対応をやっていたまさにその最中に訴訟問題が発生し、一時サイトがクローズする、という事件が起こりました。当時は何が起こったのかよく分からず、とにかく法的ないざこざでタイムゾーン情報の入手先であるFTPサイトが閉鎖してしまったらしい、という事態に一瞬、青ざめました。結局は、ICANN および IANA に管理が委ねられることとなったようで、今では詳しい経緯について Wikipedia にも記載されています。

参考:


■ Linux でのタイムゾーン情報

一般的なLinux系ディストリビューションでは、上記の tz database の情報をデフォルトで /usr/share/zoneinfo/ の下に持っています。file コマンドを使ってみると、タイムゾーンデータであることが分かります。(Mac OS X で実行した場合の例)

$ file /usr/share/zoneinfo/Asia/Tokyo

/usr/share/zoneinfo/Asia/Tokyo: timezone data, old version, 3 gmt time flags, 3 std time flags, no leap seconds, 9 transition times, 3 abbreviation chars

特に更新していなければ、インストール時のまま情報が古くなっている可能性があるので、更新しましょう。大体は、以下のいずれかのコマンドで更新できるでしょう。

  • $ yum update tzdata (CentOS などRPMベースのディストリビューション)
  • $ apt-get update tzdata (Ubuntu などDebian系のディストリビューション)

tz database をダウンロードして手動でインストールすることもできます。より詳しくは下記リンク先を参照ください。

参考: http://chrisjean.com/2009/03/10/updating-daylight-saving-time-on-linux/


■ PostgreSQL でのタイムゾーン情報

PostgreSQL は、バージョン 8.0 以降、OS にインストールされている tz database の情報を利用できるようになりました。(それ以前のバージョンでは、コンパイル済みの情報を PostgreSQL 内部に持っていたようです。その場合は、PostgreSQL 自体をアップデートしなければタイムゾーン情報を更新できません)

コンパイルオプションで

'--with-system-tzdata=/usr/share/zoneinfo'

を指定しておけば、PostgreSQL がインストールされている OS の zoneinfo が更新されれば自動的に最新の情報となります。

すでに稼働している PostgreSQL がある場合は、コマンドラインで

$ pg_config --configure

としてみて、出力内容に 上記のオプションが表示されていれば、そのままでOKでしょう。

参考:

PostgreSQL 8.4 マニュアル


データベースの OS のタイムゾーンを更新した場合は、正しくタイムゾーンが更新されているか確認してみましょう。

たとえば、tzdata_2011g から tzdata_2011h に更新した場合は、ロシアの夏時間廃止が反映されます。psql で日時データの表示を更新の前後で比較してみます。

更新前(2011g):

dbuser=> set time zone 'Europe/Moscow';
SET
dbuser=> select * from timezone_master;
date_1 | date_2
------------------------------+-------------------------------
2011-10-04 13:19:38.796444+04 | 2012-02-02 04:00:00+03

date_1 が夏時間の期間にあたる日時で、date_2 が冬時間の期間にあたる日時です。それぞれ、オフセットが +04、+03 と異なっていることが分かります。しかし、制度廃止によって 2011年の夏時間がそのまま通年の標準となるように変わったので、 2012年の日付である date_2 が +03 になっているのは誤りです。

更新後(2011h):

dbuser=> select * from timezone_master;
date_1 | date_2
------------------------------+-------------------------------
2011-10-04 13:19:38.796444+04 | 2012-02-02 05:00:00+04

tz database の更新後、同じようにデータを表示してみると、 date_2 も +04 になっており、更新が正しく適用されたことが分かります。


■ Perl でのタイムゾーン情報

Perl では CPAN に公開されている DateTime モジュールが日時系の情報を扱う際の標準ですが、このモジュールも tz database の情報を元にタイムゾーン情報を持っています。CPAN からインストールしたファイルではすでに Perl のコードに展開されたものが記述されています。

通常は、CPAN コマンドで DateTime::TimeZone モジュールをアップデートすれば、最新のタイムゾーン情報が反映されるでしょう。

$ cpan update DateTime::TimeZone

諸事情により DateTime モジュールを更新できない場合(機能や関連モジュールを更新できない/しなくない場合など)は、OS の tz database を使って、タイムゾーン情報だけを更新することができます。

以下に、手順例を挙げます。

1. ダウンロードした tz database のファイルを適当な場所に解凍

  • $ tar xzvf tzdata_2011h.tar.gz
  • $ mv tzdata_2011h/tmp/tzdata2011h

2. 更新のベースとする DateTime::TimeZone モジュールをダウンロード

CPAN または Git から、ベースとしたいバージョンをダウンロードします

3. ダウンロードした DateTime モジュールで、上の tzdata を指定して parse_olson スクリプトを実行

  • $ tar xzvf DateTime-TimeZone-0446627.tar.gz
  • $ cd DateTime-TimeZone-0446627
  • $ perl tools/parse_olson --clean --dir /tmp/tzdata2011h/ --version 2011h

これで、指定した tzdata の情報を持つ DateTime::TimeZone モジュールが出来上がります。(lib/DateTime/TimeZone/ 以下に、各エリアのディレクトリ/ファイルが作成されます)

参考:


■ アプリケーションでの実装

日時系の情報がデータベースに入っているなら、話は簡単です。

ユーザがログインしてタイムゾーンが特定できる場合、リクエスト処理の先頭のほうで、以下のようなコマンドをデータベースに対して発行するだけでOKです。

SET TIME ZONE 'Asia/Tokyo'; (PostgreSQLの場合)

参考: http://www.postgresql.org/docs/8.4/static/sql-set.html


これで、このデータベースコネクションの間は、 timestamp with time zone 型のデータについては指定したタイムゾーンで扱われます。

たとえば '2012-01-20 10:30:00' としてクエリーを発行した場合、上記の例であればこれは日本時間での 10時半 という扱いになります。

ちなみに、MySQLでは以下のコマンドで同様の設定ができるようです。

SET time_zone = 'Asia/Tokyo'; (MySQL)

参考: http://dev.mysql.com/doc/refman/5.1/ja/time-zone-support.html


データベースへの単純なデータの出し入れであれば以上で十分ですが、操作しているユーザと異なるタイムゾーンで日時を出力したり送信したりする場面では、適宜タイムゾーンを調整する必要があります。1例を挙げると、ある外部のアプリケーションに日時データを送信するときは常に日本標準時で送らなければいけない、といったケースです。操作中のユーザのタイムゾーンの日時→日本標準時 への調整が必要です。たとえば Perl のコードでは DateTime モジュールに日時を格納した上で、以下のような変換を行います。



use DateTime;

my $dt = DateTime->new(
'year' => 2012, 'month' => 1, 'day' => 20,
'hour' => 19, 'minute' => 20, 'second' => 0,
time_zone => 'America/New_York'
); # DB から取得した日付が New York のタイムゾーンだとして

$dt->set_time_zone('Asia/Tokyo'); # 日本時間に変換

print $dt->strftime('%F %T') . "\n"; # 2012-01-21 09:20:00


他にも、タイムゾーンの異なる日付同士を比較する場合なども、たとえば それぞれ UTC にそろえた上で比較・演算する必要があります。

ちなみに、開発中にテストで入出力した日時が正しいかどうか確認するツールとして、以下のサイトなどを利用するとよいでしょう。日本で何時だったらニューヨークで何時、といった比較が簡単にできます。

worldtime buddy: http://www.worldtimebuddy.com/


なお、ユーザが指定可能なタイムゾーンの選択肢は、 tz database で選択可能なものに限定するようにしましょう。アプリケーション(Perl)、PostgreSQL、それぞれの OS で tz database のバージョンが一致していることが重要です。


■ おまけ: タイムゾーン小ネタ

以上、ざっくりしたタイムゾーンサポート導入の概要でした。

以下は技術ネタとは関係ない蛇足ですが、タイムゾーンまわりのことは調べてみるといろいろ興味深い話があります。

-- 日本のタイムゾーン、サマータイム

2011年は、電力不足の懸念から日本でもサマータイムを導入すべきではないか、という話題が挙がったりしていました。もし本当にサマータイムを導入するとなると、上で述べた tz database も日本中のシステムで更新されることになるでしょう。ちなみに日本でも戦後、GHQの施策でしばらくサマータイムが実施されていた期間があったそうです(僕は全然知りませんでした)。zdump コマンドで zoneinfo をのぞいてみると、1948 年から 1951 年ごろまで、切り替わりを示す情報が記載されいてるのが分かります。


$ zdump -v /usr/share/zoneinfo/Asia/Tokyo
/usr/share/zoneinfo/Asia/Tokyo Fri Dec 13 20:45:52 1901 UTC = Sat Dec 14 05:45:52 1901 CJT isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat Dec 14 20:45:52 1901 UTC = Sun Dec 15 05:45:52 1901 CJT isdst=0
/usr/share/zoneinfo/Asia/Tokyo Fri Dec 31 14:59:59 1937 UTC = Fri Dec 31 23:59:59 1937 CJT isdst=0
/usr/share/zoneinfo/Asia/Tokyo Fri Dec 31 15:00:00 1937 UTC = Sat Jan 1 00:00:00 1938 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat May 1 16:59:59 1948 UTC = Sun May 2 01:59:59 1948 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat May 1 17:00:00 1948 UTC = Sun May 2 03:00:00 1948 JDT isdst=1
/usr/share/zoneinfo/Asia/Tokyo Fri Sep 10 15:59:59 1948 UTC = Sat Sep 11 01:59:59 1948 JDT isdst=1
/usr/share/zoneinfo/Asia/Tokyo Fri Sep 10 16:00:00 1948 UTC = Sat Sep 11 01:00:00 1948 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat Apr 2 16:59:59 1949 UTC = Sun Apr 3 01:59:59 1949 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat Apr 2 17:00:00 1949 UTC = Sun Apr 3 03:00:00 1949 JDT isdst=1
/usr/share/zoneinfo/Asia/Tokyo Fri Sep 9 15:59:59 1949 UTC = Sat Sep 10 01:59:59 1949 JDT isdst=1
/usr/share/zoneinfo/Asia/Tokyo Fri Sep 9 16:00:00 1949 UTC = Sat Sep 10 01:00:00 1949 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat May 6 16:59:59 1950 UTC = Sun May 7 01:59:59 1950 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat May 6 17:00:00 1950 UTC = Sun May 7 03:00:00 1950 JDT isdst=1
/usr/share/zoneinfo/Asia/Tokyo Fri Sep 8 15:59:59 1950 UTC = Sat Sep 9 01:59:59 1950 JDT isdst=1
/usr/share/zoneinfo/Asia/Tokyo Fri Sep 8 16:00:00 1950 UTC = Sat Sep 9 01:00:00 1950 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat May 5 16:59:59 1951 UTC = Sun May 6 01:59:59 1951 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Sat May 5 17:00:00 1951 UTC = Sun May 6 03:00:00 1951 JDT isdst=1
/usr/share/zoneinfo/Asia/Tokyo Fri Sep 7 15:59:59 1951 UTC = Sat Sep 8 01:59:59 1951 JDT isdst=1
/usr/share/zoneinfo/Asia/Tokyo Fri Sep 7 16:00:00 1951 UTC = Sat Sep 8 01:00:00 1951 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Mon Jan 18 03:14:07 2038 UTC = Mon Jan 18 12:14:07 2038 JST isdst=0
/usr/share/zoneinfo/Asia/Tokyo Tue Jan 19 03:14:07 2038 UTC = Tue Jan 19 12:14:07 2038 JST isdst=0


1937年から1938年へ変わるタイミングについての記載は、どうもそれまで「中央標準時」と「西部標準時」の2つのタイムゾーンがあったのが一本化されたことに対応するもののようです。(CJT から JST に変わっています)

参考:


-- タイムゾーンの選択肢をどうするか

ユーザが選択可能なタイムゾーンは、 tz database に含まれているものである限りは何でも構わないのですが、すべてのタイムゾーンを選択可能にすると選択肢が膨大になります。また、それだけのタイムゾーンをサポートするということになると、どこかのエリアでタイムゾーン情報の変更があるたびに tz database を更新しなければならないので、その更新頻度も多くなってしまいます。

最低限という意味では、1日 24時間なんだから 24分割して代表的なエリアを 24個選択肢に含めればいいのではないか?と一瞬 思いますが、エリアによっては UTC との差が n 時間30分差/15 分差だったりします(たとえばインドは UTC + 5:30、ネパールは UTC + 5:45)し、サマータイムを実施しているエリアとそうでないエリアがある(たとえばアメリカやオーストラリアでも、同じくらいの経度の地域でも州によってサマータイムを実施しているところとしていないところがある)ので、そう単純には決まりません。

結局は、サービスを利用するユーザの分布(予測)で決めるしかないでしょう。主要な国や地域を見繕って、あとはなるべく将来の更新頻度を減らすために、ある程度の網羅性を確保できるよういくつかのエリアを補足すればよいでしょう。

参考:


他にも、UTC と GMT の違いや閏秒の話など、ネタはいくつかありますが、おまけのほうが長くなりそうなのでこの辺で今回は終わりとしたいと思います。

では、また。


次の記事
« Prev Post
前の記事
Next Post »
Related Posts Plugin for WordPress, Blogger...