2015/07/01 08:59:60 JST (2015/06/30 23:59:60 UTC) の閏秒が挿入された際の時刻補正の挙動を事前にテストしようと思い、電磁波計測研究所のSNTPサーバ うるう年テストバージョンを使ってみました。結論から言えば確かにテストできるのですけど、しかしこのスクリプトは微妙に使い勝手がよくない気がしました。それは「うるう秒を挿入するタイミングを調整するためにスクリプトを編集せねばならない」からです。
ntpd が親サーバと時刻同期した状態となるには10分程度の待ち時間が発生します。だからニセのntpサーバを起動してから10分後くらいにテストを行いたいわけです。しかしそのように動かすためにスクリプトの変数を手作業で書き換えるのはどうにもメンドクサイです。
そこで「実行時刻の10分後にうるう秒が入る」ように手をいれてみました。パラメータなしで実行すると、実行から10分後に "2015/06/30 23:59:60 UTC" を配信するように動作します。このタイミングを任意に変えたい場合はコマンドライン引数にて --uso-leap-datetime="YYYY:MM:DD hh:mm:ss" と指定します。たとえばここに "2015/06/11 20:00:00" と指定すると、現実の時刻が "2015/06/11 20:00:00" のときに偽の ntp サーバが "2015/06/30 23:59:60 UTC" を配信するように動作します。
偽のNTPサーバがうるう秒を挿入する時刻を変更したい場合は --leap-datetime="YYYY:MM:DD hh:mm:ss" に UTC で時刻指定します。--leap-datetime="2016/01/01 00:00:00" と指定すれば、2016年1月1日に対してうるう秒を挿入するように動作します。
このスクリプトを用いて、以下のように作業を行えばうるう秒挿入時の動作を事前にシミュレーションできます。
- 機材を2台(ニセのNTPサーバ、うるう秒を観察する試験機)
- 試験機はニセNTPサーバを参照する設定とする。ただしntpdは止めておく。
- 試験機側の tzdata や kernel の状態、ntpd の設定については試験目的に合わせて適切に設定しておく。
- ニセのNTPサーバを実行する。
- 試験機で ntpdate ニセNTP ; service ntpd start して、ニセNTPサーバとの時刻同期を開始する。
- 必要に応じて試験機側で watch -n 1 ntpd q とかを実行し、ntpd の同期状況を確認しておく。
- http://www.cuspy.org/diary/2012-06-05/gettimeofday.pl とかを実行して、うるう秒の発生にともなうシステムクロックの動きを目視確認する。
- うるう秒発生に伴う問題の有無を検証する。
#!/usr/bin/perl -wT use strict; use Socket; use Time::HiRes qw( gettimeofday ); use Time::Local; use Getopt::Long; my ( $rb, $sb, $vn, $client, $sec, $msec, $proto, $paddr ); my ( $NTP_LI, $NTP_LEAP, $MODE, $FLAG, $STR, $POLL, $PREC ); my ( $USO_LEAP, $USO_OFFSET ); my %opt=(); # NTP_LI の初期値 my $opt_li_datetime = "2015/06/30 00:00:00"; # NTP_LEAP の初期値 my $opt_leap_datetime = "2015/07/01 00:00:00"; # USO_LEAP の実施時刻は現在時刻の10分後にする。 # これくらいの間をあけないと ntpd の同期が完了しないため。 my $opt_uso_leap_datetime; { my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time + 600); $year += 1900; $mon += 1; $opt_uso_leap_datetime = sprintf "%d/%02d/%02d %02d:%02d:%02d", $year, $mon ,$mday, $hour, $min, $sec } GetOptions ( 'li-datetime=s' => \$opt_li_datetime, 'leap-datetime=s', => \$opt_leap_datetime, 'uso-leap-datetime=s', => \$opt_uso_leap_datetime ); print "NTP_LI = $opt_li_datetime\n"; print "NTP_LEAP = $opt_leap_datetime\n"; print "USO_LEAP = $opt_uso_leap_datetime\n"; $NTP_LI = timegm( &scan_datetime( $opt_li_datetime ) ); $NTP_LEAP = timegm( &scan_datetime( $opt_leap_datetime ) ); $USO_LEAP = timegm( &scan_datetime( $opt_uso_leap_datetime ) ); $USO_OFFSET = $NTP_LEAP - $USO_LEAP; $MODE = 4; # mode = 4 (server) $STR = 1; # stratum 1 $POLL = 6; # poll interval = 2^6 sec $PREC = -16; # precision = 2^-16 sec $proto = getprotobyname('udp'); socket(NTP, PF_INET, SOCK_DGRAM, $proto) or die "socket: $!"; $paddr = sockaddr_in( 123, INADDR_ANY ); bind(NTP, $paddr) or die "bind: $!"; $sb = "\0" x 48; while ("You hate leapseconds") { ($client = recv(NTP, $rb, 256, 0)) or next; ( length($rb) >= 48 ) or next; ($sec, $msec) = gettimeofday; $sec += $USO_OFFSET; $FLAG = $MODE | ( unpack("C",substr($rb,0,1)) & 0x38 ); # version if(($sec >= $NTP_LI) && ($sec <= $NTP_LEAP)) { $FLAG |= 0x40; } if(($sec > $NTP_LEAP)) { $sec -= 1; } # offset substr($sb, 0, 4) = pack("CCCc", $FLAG, $STR, $POLL, $PREC); substr($sb, 4, 4) = pack("N", 0); # delay substr($sb, 8, 4) = pack("N", 0); # dispersion substr($sb,12, 4) = "LOCL"; # Ref ID substr($sb,16, 4) = pack("N", $sec + 0x83AA7E80); substr($sb,20, 4) = pack("N", 0 ); substr($sb,24, 8) = substr($rb,40, 8); substr($sb,32, 4) = pack("N", $sec + 0x83AA7E80); substr($sb,36, 4) = pack("N", ($msec/500000) * 0x80000000 ); ($sec, $msec) = gettimeofday; $sec += $USO_OFFSET; if(($sec > $NTP_LEAP)) { $sec -= 1; } substr($sb,40, 4) = pack("N", $sec + 0x83AA7E80); substr($sb,44, 4) = pack("N", ($msec/500000) * 0x80000000 ); send(NTP, $sb, 0, $client); } sub scan_datetime { my ( $datetime ) = @_; my ( $year, $month, $day, $hour, $min, $sec ) = ( 0, 0, 0, 0, 0, 0 ); if ( $datetime =~ m/(\d+).(\d+).(\d+).(\d+).(\d+).(\d+)/ ) { $year = $1 - 1900; $month = $2 - 1; $day = $3; $hour = $4; $min = $5; $sec = $6; } return ( $sec, $min, $hour, $day, $month, $year ); }