NTP設定

環境

  • RHEL5 ( ntp-4.2.2p1 )
  • RHEL6 ( ntp-4.2.4p8 )
  • NTPは結構微妙な修正を入れてくるのでバージョンが異なる場合は注意が必要。
    • manが当該バージョンの情報源としてはまず優先。

最近の変更

  • RHEL6ではRHEL5で /etc/init.d/ntpd のみで行っていたことを /etc/init.d/ntpdate と /etc/init.d/ntpd に分離している。
    • ntpd起動前のntpdate実行について分離している。ntpdを使うのであればntpdateも同時に有効にするべき(ntpdate実行を避けたい場合以外)。

設計

何もよりも先に

『参考』にあげた富士通の『Linuxユーザーズマニュアル RHEL6編』がNTPの説明として詳しい・わかりやすい。まずはこれを読む。

NTPサーバの階層設計
  • NTPを使う目的は『正しい時刻の維持』と『時刻の統一』にある。
    • あるサーバと別のサーバが連携する処理に問題が発生した場合などには、ログを突き合わせて調査する。この際には『正しい時刻』ということよりも『時刻の統一』が大事になる。
    • 『時刻の統一』が大事なグループ単位でNTPサーバを構成しておく。
    • 現在のサーバやネットワークなら組織内の共通NTPサーバだけでも大概は十分さばけるだろう。


可用性
  • 構成
    • システム内で複数台(3台以上が望ましい)を代表のNTPサーバとし社内共通や外部のNTPサーバを上位サーバとする。
    • それらの間でpeerを使用して精度を高める。
    • 上位NTPのすべてと分断された場合には、システム内での時刻統一を堅持する。
    • システム内の残りの機器はシステム内のNTPサーバと同期する。
  • -xオプションを使用するか、tinker step を指定して、slew調整できる範囲をデフォルトからは変更する。
  • tinker panic 1000 はデフォルトのままとし、時刻差が開きすぎた場合には NTP ダウンをプロセス監視・ログ監視で検知する。
セキュリティ
  • ntpdはクライアントでありサーバである
    • ntpdは上位NTPサーバに対してはクライアントとして振る舞う。しかし同時に下位NTPサーバ(NTPクライアント)に対してはサーバとして振る舞う。
    • 他のサーバからの参照を受けつけない末端のクライアントであってもntpdを使う場合には、それがポートをリスニングする"サーバプログラム"であることを意識しなければならない。
    • ntpdateは完全にNTPクライアントである。
  • NTPの通信は UDP のポート123 を使用する。サーバ側だけでなくクライアントの発信側も UDP のポート 123 を使うことに注意。
    • ntpdateは -u オプションにより発信ポートを1024以降にできる。
  • NTPのアクセスコントロールは restrict コマンドにて行う。restrict にて全アクセスを抑止した後、ホワイトリスト的に restrict で限定的にアクセス許可すること。
  • ntpqおよびntpdcからの設定変更はローカルホストからのものを含めて禁止する。設定変更は設定ファイルを変更してNTPサーバの再起動を行う。ローカルホストだからといって全て許可するのは危険(ログインできればntpユーザやrootユーザ以外でもntpqやntpdcを実行できる)
運用
  • NTP起動前に ntpdate が実行されるように設定する。
  • VMなど大きく時刻がずれることがあるものも、tinker panic 0 ではなくNTPの再起動(間にntpdate)で対処するのを基本とする。
  • システム時刻をハードウェア時刻に定期的に同期する。(1日に1回、cronでhwclockを使えば良い)

個別調査

tinkerコマンドと -x オプションで設定する step, stepout, panic 値の関係


ドリフトファイルとは何物か
  • driftfile /var/lib/ntp/drift として指定しているファイルに書込まれるものは、『そのサーバのクロックソースが本質的にどの程度の誤差を生じるか』である。NTPにより『真の時刻の進み方』がわかるため『そのサーバにおける時刻の進み方』がどの程度ずれるかがわかるということ。『NTPの参照先と参照元の"時間"がどれだけずれているか』はオフセットであり、ドリフトではない。ドリフトは『時刻の"進み方"のずれ』である。
    • 『時刻の進み方のずれ』は PPM(Parts-per-Million) 単位で表されており、ドリフトファイルに 1 と記録されていれば、1[PPM] である。1[PPM]は100万分の1秒の誤差を生じるということなので、仮に100[PPM]であれば1日(86,400秒)に8.64秒の誤差となる。
    • ドリフトファイルに書込まれるのはあくまでクロックソースが本質的にもっている誤差の情報である。オフセットやオフセットを小さくするための時刻の進め方に関する情報はドリフトファイルには反映されない。
  • なぜNTPにとってこの『時刻の進み方のずれ』が重要なのか。
    • 『時刻の進み方のずれ』は時刻を調整する上で必須の情報だ。進みすぎた時刻を補正する(遅く進める)という行為をするためには、進み方が速いという情報を知っていなければならない。
      • 例えば、1秒あたりに+1PPMずれるクロックソースを使用している環境で、オフセット1PPMを補正するには、次の1秒で-1PPMではなく-2PPMしなければならない(その1秒間でまたずれるから)。
    • 『時刻の進み方のずれ』が把握できていれば、上位NTPサーバとの通信回数を増やして、一致を確認(オフセットの確認)する頻度を高くしなくても、自己調整でかなり正しい時刻にできる(クロックソースの持つ誤差(癖と言った方が良いのかも)が本来的で基本的に変わらない、ということが前提)。
      • NTPサーバは変動周期で参照先の上位NTPサーバへ問い合わせを行うが、同期がとれてくると問い合わせ周期を段階的に長くする。
      • これはNTPによるネットワーク負荷の軽減と参照される上位NTPサーバの負荷の軽減を目的とした実装である。
      • NTPサーバも日本標準時を管理しているNICTともなると毎秒数十万以上の問い合わせを処理しているらしいので必要な実装だろう。
      • 上位NTPサーバへの問い合わせ間隔の上限(maxpoll)はデフォルトで210秒(約17分)だが、最大値は217秒(約36時間)まで指定できる。これほど間隔をのばせるのは『時刻の進み方のずれ』をもとに補正を続けられるからこそ。NTPv3では最高1024秒間隔だった問い合わせ間隔が、約36時間までのばせるのは、NTPv4でこの『時刻の進み方のずれ』に関するアルゴリズムが改善されからのようだ。

Network Time Protocol Version 4 Core Protocol Specification p.1 より

Additional improvements include a new clock discipline algorithm which is more responsive to system clock hardware frequency fluctuations and can hold accuracy within a few microseconds with precision time sources and within a few hundred microseconds with poll intervals equal to the maximum NTPv3 poll interval of 1024 seconds. With NTPv4 the poll interval can be extended to 36 hours with only modest loss of accuracy. The new algorithm can calibrate the intrinsic system clock frequency offset accurately within 15 minutes and increase the poll interval more rapidly.

  • なぜドリフトファイルを残すのか、消してはいけないのか
    • 『ドリフトファイルを作成し次のNTP起動時に使えるように消さずにおくこと』という説明はところどころのサイトなどで見るが、なぜこのドリフトファイル、そこに記録される『時刻の進み方のずれ』が重要なのかは説明されない。
    • 『時刻の進み方のずれ』を記録しておき、次のNTP起動時に使えるようにすることが求められる理由は、『NTPが起動してから、"ずれ"がどの程度なのか判断するのにはある程度の時間をかけた観察(NTPデーモンによる)が必要で、前回の観察結果があればその観察に必要な時間が短縮できる』ということ。だいたい15分から1時間程度観察することで確かな"ずれ"が計算できるらしい。保存しておけば起動後すぐに、ある程度正しい"ずれ"を入手できるということ。
    • 逆に他のサーバからコピーしたドリフトファイルの場合、当該サーバのクロックソースの誤差と異なる情報をドリフトファイルから得てしまうため、場合によってはずれを大きくしてしまう可能性がある。(クロックソースの進みが遅いサーバで作られたドリフトファイルをクロックソースの進みが速いサーバへコピーすると、時刻の進み方がより速くなってしまうだろう。(NTP同期がとれれば解消されていくが))
NTPによる調整(discipline)とLinuxカーネルによる調整
  • ntpd の -x オプションや tinker コマンドの step オプションについて、下記の様な注意書きが見られる。
  • ステップを0(無限大)にすることは現実的でないと思われるが、-x (ステップに切り替わる閾値を 0.128 秒から600 秒にする) は前述の富士通のマニュアルでも使用が推奨されているオプション。つまり、この補記が述べている『カーネルによる調整は無効化される』は十分該当しうるものだ。

ntp-4.2.4p8-2.el6.x86_64 の man ntp_misc より抜粋

The argument is the step threshold, by default 0.128 s. It can be set to any positive number in seconds. If set to zero, step adjustments will never occur. Note: The kernel time discipline is disabled if the step threshold is set to zero or greater than the default.

調整方法がステップに切り替わる閾値。デフォルトは 0.128 秒。秒単位の正の数を指定できる。0を指定した場合には(無限大と解釈して)ステップによる調整は行わなくなる。補記:この閾値を0(つまり無限大)またはデフォルトより大きくした場合には『カーネルによる調整は無効化される』(訳注: 実装はデフォルトより大きい場合ではなく、0.5より大きい場合なので記載誤りと思われる)

  • カーネルによる調整(kernel time discipline)とNTPによる調整(NTP daemon discipline)の違いは文字通りどちらが主体で時刻の調整を行うかということである。これらは両立するべきではないとされる(調整を2カ所で連携せずに行うことによる問題を懸念して)。
    • カーネルによる調整』は Linux の adjtimex() システムコールに調整を任せることを意味する。オフセットの情報を adjtimex() に与えると、adjtimex() 内で適切に時間の引き延ばし・短縮を行いオフセットを無くす方向に調整する。NTPはこれに期待して adjtimex() を粗い間隔でしか呼ばなくなる(1秒間隔で実行されるadj_host_clock()の先頭あたりで kern_enable の場合は return している)。NTPはどの程度のずれがあるかをカーネルに伝えるのがメインの仕事になる。※ステップで調整する場合にはNTPが settimeofday() を呼び出す。
    • 『NTPによる調整』は adjtimex() システムコール(というよりは従来のUNIXでサポートしているあまり"賢くない" adjtime() システムコール)には任せない調整である。NTP側で時間の引き延ばし・短縮をどの程度行えばよいか計算し、その計算結果に基づいてカーネルに細かく命令をして調整する。なお、この細かく命令する際にも adjtimex() を使用する。
    • 従来の adjtime() は調整できる範囲が1秒以内(glibcだと2145秒?))程度に制限(32ビットのオーバーフローが起こるらしい)されているため、大きなオフセットについては調整を任せることが無理だった。そのため、大きなオフセットのもとで adjtime() を使用する『カーネルによる調整』はできないものとして NTP は実装された経緯がある。Linux が持っている adjtimex() は adjtime() より"賢い”ようなのだが、NTPからするとUNIXのなかでそれはやや異質(想定していない進歩であり)という見方のようだ。

Technology Reviews & Software Reviews From Fixunix より David L. Millis 教授の投稿
In principle, there is no limitation on the step threshold or even if it is disabled. However, there are gotchas:

1. The original Unix adjtime() function uses a constant slew rate of 500 PPM. This results in very long convergence times for a large initial offset of several seconds or more.
2. In an apparently misguided attempt to "fix" this problem, Linux (and Solaris) added variable slew feature to adjtime(). This creates an extra poll in the discipline loop resulting in unstable behavior, especially with initial offsets of a second or more.
3. The reason for the 0.5 second limitation in the kernel code is that the ntp_adjtime() function is limited to a one-second interval due to overflow in the 32-bit word.

  • カーネルによる調整』は"部下(カーネル)"を信頼して指示を出し、時々確認と追加の指示を出す"上司(NTP)"のモデル。『NTPによる調整』は"部下"を信頼せず、あれやれこれやれと逐次指示を出す"上司"のモデル。

  • 確認するには strace を使い adjtimex の呼び出しを確認すればよい。
# カーネルによる調整を有効にする場合(ntp.conf で tinker を指定していないこと)
strace -o /tmp/ntpd.strace ntpd -d -n -u ntp:ntp -p /var/run/ntpd.pid -g

# カーネルによる調整を無効にした場合(ntp.conf で tinker を指定していないこと)
strace -o /tmp/ntpd.strace ntpd -d -n -u ntp:ntp -p /var/run/ntpd.pid -g -x

# 上記のトレースをしている状態でトレースファイルを確認する。カーネルによる調整をしていない場合、1秒おきにadjtimex()の実行が確認できる。
# 上記は ntpd のデバッグモードも有効になっているのでコンソールには ntpd からのデバッグログが表示される
tail -f /tmp/ntpd.strace | grep adjtimex
  • NTPによる調整は1秒おきにadjtimex()の呼び出しがあるため、カーネルによる調整に比較してやや負荷をかける可能性がある。ただし、topで見る限りではntpdのCPU使用率は0%でありインパクトは小さい。定期的な動作なので省電力を意識すると多少問題があるかもしれない。
  • 今のところ、ステップによる調整に切り替える閾値が0.128秒や0.5秒では小さすぎると思うので、-x オプションを使用したい。-x オプションについては(ベンダー推奨もあるぐらいなので)実績があると判断する。
iburstによる同期高速化
  • ntpdは単純な同期ではなく、複数回の通信で得られた時刻から"総合的に"同期先・時刻を決定している。そのため、上位NTPサーバとも複数回通信し、時刻を取得した上でなければ同期先として妥当とは判定しない。
  • この通信回数(とその応答が得られたかどうか)は ntpq -p の reach フィールドに示されている。これが 0 (00000000-0回) とか 3 (00000011-2回) などでは同期先にならないということ。
    • どの程度通信できれば同期先として妥当なのかは未調査 (回数だけでなくstratumや時刻のばらつきも見ているはず)
  • この通信回数を通常のポーリング間隔で増やそうとすると、最低でも minpoll (デフォルト64秒) × 必要最低限の通信回数 の時間がかかることになる。仮に 8 回の通信を 64 秒間隔で実施すると 512 秒 (8分半) かかることとになり、同期までに長い時間を要する。
  • iburst オプションをつけた場合、初回(初回以外でも条件を満たせばよいが、条件が厳しい)の通信時に2秒間隔で8パケット(8回分のポーリング)を一気に送る。これにより8回分の通信結果を短時間(8回送出するまでに16秒程度)で得る事ができ、同期先を選択するための材料を揃えることができる。
  • バーストによる通信は1回分の通信として扱われる。reach 0 からバーストを使用しても reach 1 にしかならない。よって ntp -q ではバーストしたことは読み取れない。ntpdc ならある程度動作が確認できる。
    • iburst を指定したサーバ xxx.xxx.xxx.xxx とした場合、『watch --interval=1 "ntpdc -c 'pstats xxx.xxx.xxx.xxx'"』とした状態でntpdを再起動すると "packets sent" で送出パケットが2秒ごとに +1 されることがわかる。また、"time until next send" がバースト中は 2 秒になっていることもわかる。
  • iburst オプションは起動時(要は初回)または『応答有りから応答無し(reachが0、過去7回全滅すること)に遷移したとき』にしか発動しない。一度バーストで8パケット送ると、相手が『応答有り』に遷移するまでは二度とバーストせず、通常のポーリングとなる。
    • これは『無応答のサーバにパケットを大量におくらない』ための仕組みである。
    • ntpd起動時のバーストで同期できなかった場合(相手の起動があとになったとか)、通常のポーリングでの同期になる。よってiburstを使う場合には起動時に上位NTPサーバが応答できる(起動していてかつさらに上位と同期している)ようにしておくべき。
    • 『応答有りから応答無しに遷移したとき』も reach が 0 になったタイミングでしかバーストせず、その後相手が応答するようになった場合にはバーストではなく通常のポーリング間隔で通信する。うまく reach 0 になったタイミングでちょうど相手が応答可能になれば効果がある(だろう)。
    • iburst は Initial burst の名前の通り、起動時のみの効果に期待すること。応答無しになった相手と同期を高速に回復することは期待してはいけない。
    • ちなみに下記の記述は ntpd (ntp-4.2.4p8) 実装とは異なっている。下記の記述(バーストの最初のパケットに応答がなければ後続のバーストは送らない)通りなら、応答無しになった相手との同期の高速回復も期待できたが・・・。

Association Management の Burst Options より

The iburst option is useful where the system clock must be set quickly or when the network attachment requires an initial calling or training sequence, as in PPP or ISDN services. In general, this option is recommended for server and pool commands. A burst is sent only when the server is unreachable; in particular, when first starting up.

The burst option is useful in cases of severe network jitter or when the network attachment requires an initial calling or training sequence. This option is recommended when the minimum poll exponent is larger than 10 (1024 s). A burst is sent only when the server is reachable. The number of packets in the burst is determined by the poll interval so that the average interval between packets (headway) is no less than the minimum poll interval for the association.

  • burst はiburstと異なり、(過去7回のいずれかで)応答があるサーバに帯して常にバーストする。そのため、ポーリング周期が短い場合には負荷が増える。burst を使用する場合は、minpoll 10 (1024秒周期以上を強制) とペアで使う。
  • バーストするときに送るパケット数は8で2秒間隔で固定。これは実装上定数 NTP_BURSTとBURST_DELAY になっている。
  • 実際に比較。
    • minpoll 6 maxpoll 6 を指定して iburst なしとした場合は約 196 秒で reach が 17 (000001111-4回) となった段階で同期した。64秒間隔で4回のポーリングとほぼ一致する時間。4回で同期できるようだ。
    • minpoll 6 maxpoll 6 を指定して iburst ありとした場合は約 7秒で reach が 1 (00000001-1回) で同期した。前述の通りバーストモードによる通信は reach では 1 としか扱われない。実際には 4 回通信した時点である(0秒時点で1回目、6秒時点で4回目)。
  • バーストの動きは http://www.eecis.udel.edu/%7Emills/database/brief/flow/flow.pdf をベースに ntp_proto.c を解析すれば追う事ができる。
    • 『Poll process: poll() procedure』が該当部分の説明。ただし、poll() は ntp_proto.c の transmit() に該当。
    • この資料と実装では処理順などが若干異なる部分があるので注意・・・
  • 上記の通り、iburstにはメリットが多いので、server には付与しておきたいオプション。ただし、下記の注意書きがあるため peer に対しては iburst (burstも) をつけないこと。

Association Management の Symmetric Active/Passive Mode より

A peer is configured in symmetric active mode using the peer command and specifying the other peer DNS name or IPv4 or IPv6 address. The burst and iburst options should not be used in symmetric modes, as this can upset the intended symmetry of the protocol and result in spurious duplicate or dropped messages.

NTPデーモンの非root権限実行と設定ファイルのパーミッションの関係
  • ntpdに-uオプションをつけて実行ユーザをroot以外にする場合でも、設定ファイルの読み込みまではrootユーザで行う(その後指定ユーザに切り替える)。
    • 設定ファイルは root だけが読み込めれば良い (公開鍵認証用のほうは確証なし)
    • ntpdmain関数内の getconfig() (引数解析・ntp.conf解析・共通鍵読み込み), crypto_setup() (公開鍵認証用の鍵読み込み) 呼び出しは setuid() よりも前。
  • ntpdateの-Uオプションは ntp-4.2.4p8 (それ以降もかも) オリジナルではサポートしていない。RedHatが適用している ntp-4.2.4p0-droproot.patch によって -U オプションや setuid などの処理が追加される。そのため、NTPの公式のオプションとしては -u (123番ポートを使わない) はあっても -U はない。
    • ntpd起動前の ntpdate 実行の際に /etc/ntp/step-tickers または /etc/ntp.conf から同期先サーバを読み取るが、ntpdate 呼び出し前に root ユーザとして読み取り引数で与える。よって、ntp.conf や step-tickers については root ユーザが読み取れれば ntpdate の実行上問題は無い。
    • -U はユーザ名のみ取得しており、グループは指定ユーザのプライマリグループを使用する(ntpユーザの場合はntpグループなので ntpd の -u ntp:ntp と同じになる)。
  • ドリフトファイルと統計情報ログは setuid() 後に書き込みを行うため -u で指定したユーザの書き込みが可能なファイル・ディレクトリでなければならない。
同期先選択のフロー (clock_select関数@ntp_proto.c (ntp-4.2.4p8))

clock_select()

  1. 全てのピアの状態を reject (ntpqでは空白) にセットする。全てのピアを走査し、peer_unfit() で不適切と判定されなかったピアのリスト(peer_list)を作成する。この際、ローカルクロックがあれば typelocal 変数に保持し、prefer キーワードが無い限りはピアのリストに含めない(含めた場合はローカルクロックを過半数の形成に参加させることになる)。※peer_unfit() ではじかれたピアは reject 状態となる。
    1. peer_unfit() では下記のチェックを実施している。チェックに違反すると不適切として扱う。
      1. TEST10: ピアの stratum が ntp.conf の tos コマンドで指定している floor および ceiling オプションの範囲から外れていないこと。ピアが上位NTPサーバ(かその他の時刻ソース)と同期状態になったことがあること。
      2. TEST11: ピアについての root_distance(peer) 計算結果が sys_maxdist (tos コマンドの maxdist オプション) を超えていないこと(上位NTPサーバ(かその他時刻ソース)との同期が長時間切れている場合、jitter(過去8回分のオフセットの分散(オフセットが急に変化した場合に大きくなる))が大きくなっている場合はここで除外される)
      3. TEST12: ループにならないこと(ピアから自サーバへ参照されている)、同じ上位NTPサーバと同期していないこと
      4. TEST13: 過去8回のポーリングで1回は応答があること。server コマンドの noselect オプションがついていないこと
  2. インターセクション・アルゴリズムを実施
    1. peer_list の各ピアの状態を falsetick(ntpqではx) にセットする。
    2. peer_list の各ピアについて [offset - root_distance(perr), offset + root_distance(peer)] を範囲とするインターバル集合を構成する。※unfit_peer() にて root_distance(peer) は sys_maxdist を超えないことをチェックしている。tos コマンドの maxdist オプションのデフォルトが 1.0 (4.2.6p3では1.5になっていたので注意) であるので、通常は各ピアについて [offset - 1.0, offset + 1.0] の範囲となる。
    3. 2*A < peer_list内のピア数 の範囲(peer_listからA個のピアを捨てても過半数は残せる範囲)で a = 0 .. A まで順番に、(peer_list内のピア数 - a) 個のピアについて、インターバルが重複している範囲が存在しないか確認していく。(peer_list内のピア数 - a)個のピアのインターバルが重なっている範囲が存在した場合には、下限と上限を得て処理を終了する(範囲に入らないピアの数aを次第に緩和していく方式のため、最初に発見した範囲が最良)。
      1. 過半数となる範囲が存在しなかった場合、同期するべきピアが決定できずに、 no servers reachable (またはローカルクロックとの同期)に向かう(下限 > 上限、の反転状態で次の処理に進むため、次の処理で不適当なサーバばかりとしてはじかれる)。1サーバの場合には1サーバの範囲で決められるため、インターセクション・アルゴリズムによって同期先がなくなることはない。2サーバの場合、万が一お互いの時刻が sys_maxdist 以上ずれると、インターセクション・アルゴリズムにより、過半数のサーバで合意できるインターバルがない、となり同期先が排除されることになる(実際に排除するのは次のステップだが、下限 > 上限の条件を作るのはここ)。
    4. オフセットが、インターセクション・アルゴリズムの決定した [下限, 上限] に入っていないピアを除外する。
    5. 除外されなかった各ピアの状態を selected(ntpqでは#) にセットする。※除外されたピアは falsetic(ntpqではx) のままになる。
    6. 除外されなかったピアについて d = root_distance(peer) + ピアのstratum * sys_maxdist を計算し、その値(d)に基づいてピアをソートする(小さいものを優先)。※root_distance(peer)はsys_maxdistを超えないため、ソート結果はピアのstratumによって大方決まる。stratum (n+1) のピアはどんなに小さい root_distance(peer) を持っても、どんなに大きな root_distance(peer)を持った(sys_maxdist内ではあるが) stratum n のピアより優先されることはない。同じ stratum を持っているピア同士について、root_distance(peer) の比較となる。これは階層的に上位のstratumを持つピアを優先するというNTPの動作を反映している。
    7. ソートした上位 NTP_MAXASSOC 件(50件)だけを残す。
  3. 候補0件チェックを実施 (全てのピアがインターセクション・アルゴリズムで除外された場合のみ)
    1. ローカルクロックが設定されていた場合、ローカルクロックを候補としてpeer_listにセットする (ローカルクロックはこのタイミングで候補になるため、過半数の合意はいらない)
    2. ローカルクロックもない場合、no servers reachable 出力(すでに no servers reachable だった場合は省略)
  4. 候補のピア数が sys_minsane (tosコマンドのminsaneオプション、デフォルト1) を下回った場合には処理を打ち切る (十分な数が揃っていないと判断する)
  5. クラスタリングアルゴリズムを実施
    1. この時点で残った各ピアの状態を outlyer(ntpqでは-) にセットする。※除外されたピアは selected(ntpqでは#) のままになる。
    2. 候補数が sys_minclock (tosコマンドのminclockオプション、デフォルト3) 以下になるか、下記で除外しようとするピアが prefer または true オプションを持つピアになるまで下記を実施。※インターセクションが決めた[下限, 上限]内での分散をみることで、範囲内のオフセットのなかでさらに多くのオフセットが集まっている範囲に絞り込む。
      1. 候補の各ピアについて、ピアのオフセットと残りのピアのオフセットとの分散を計算
      2. もっとも大きな分散*(root_distance(peer) + stratum * sys_maxdist) を持っているピアを除外する(候補のなかでほかのピアから最も離れていると思われるピアを除外)。ここでもインターセクション・アルゴリズム内で使用したソート基準(root_distance(peer) + stratum * sys_maxdist())を重みとして使用しているため、小さな stratum を持つピアが除外される可能性は低くされている。
    3. この時点で残った各ピアの状態を candidat(ntpqでは+) にセットする。※除外されたピアは outlyer(ntpqでは-) のままになる。
    4. 下記の条件で同期先を決定する(ほかにも条件があるがメインだけ)
      1. prefer キーワードがついているピアがのこっていれば同期先とする(最優先)。この場合はコンバイン・アルゴリズムを実行せず、単一サーバから直接オフセットやjitterを得る。
      2. 現在の同期先のピアが残っていれば同期先とする。現在の同期先がなければ peer_list の先頭のピアを同期先とする (セレクション・アルゴリズムでソートした順番)。同期先確定後、candidat(ntpqでは+)状態のピアを使い、コンバイン・アルゴリズムを実行し、オフセットやjitterはcandidate状態のピアの情報も反映して決定される。
    5. 決定した同期先のピアの状態を sys.peer(ntpqでは*) にセットする。※同期先にならなかった候補ピアは candidat(ntpqでは+) のままになる。
  6. clock_update() 呼び出し

各ピアの状態は include/ntp_control.h で下記の通り定義されており、ntpq などのシンボルと対応している。CTL_PST_SEL_CORRECTはソース上どこからも参照されていないので今はない?

#define CTL_PST_SEL_REJECT      0       /*   reject */
#define CTL_PST_SEL_SANE        1       /* x falsetick */
#define CTL_PST_SEL_CORRECT     2       /* . excess */
#define CTL_PST_SEL_SELCAND     3       /* - outlyer */
#define CTL_PST_SEL_SYNCCAND    4       /* + candidat */
#define CTL_PST_SEL_DISTSYSPEER 5       /* # selected */
#define CTL_PST_SEL_SYSPEER     6       /* * sys.peer */
#define CTL_PST_SEL_PPS         7       /* o pps.peer */
ntpqでピアの状態が意図した通りにならない場合の要因
記号 要因
空白(reject) unfit_peer()のチェックではじかれた。疎通できていない、上位NTPサーバ自体が同期できていない、ループ検知などの可能性がある
x(falsetick) 過半数のピアで合意できるオフセットの範囲(±sys_maxdist(1.0※4.2.6p3では1.5になっている))が形成できない。上位NTPサーバの時刻不一致、参照するNTPサーバが設計上少ないなどの可能性がある
#(selected) sys_minsane (tosコマンドのminsaneオプション)を変更していてクラスタリングアルゴリズムに進めない、または参照先が多いため評価順下位で除外された
-(outlyer) クラスタリングアルゴリズムで排除された。他のピアに比べてやや時間がずれている可能性がある
+(candidat) 現在の同期先またはpreferオプションがあるピアが優先された。同期先が使えなくなれば使われるので問題は無い
*(sys.peer) 意図していないピアが同期される場合はpreferオプション有無を確認する
NTPの可用性(1): ローカルクロックの目的
  • ntp.conf の中に最初から含まれている下記のコマンドは何物か。どういう目的のものか。結論としては『何とも同期していないNTPサーバはせいぜい1日でどのクライアントからも無視される状態になる』ため、それを避けたい場合に使用するのである。なお、一度も同期状態になれないNTPサーバは最初から無視されるので、上位NTPサーバを持たないNTPサーバ(そのシステム内でのルート)には必要である(ただし、127.127.1.0 ではなく、他の外部時刻ソースであることが望ましいうえ、その場合には stratum は 0 である※server 127.127.x.x で指定するドライバによりデフォルト値は決まるらしい)。
    • 逆に言えば、NTPは同期している上位NTPサーバが同期状態でなくなった場合、最長でもせいぜい1日で同期先としてふさわしくないと無視するようになる仕組みを内包している。※1度も同期状態になっていない上位NTPサーバは最初から同期先にしない。
    • また、他のNTPクライアントから参照されることがない末端のNTPクライアントにとっては"無視されるもなにもだれも参照してこない"ということで不要な設定である。
server 127.127.1.0
fudge  127.127.1.0 stratum 10
  • 127.127.x.x はIPアドレスとして扱われているわけではなく、NTPのなかで特殊な意味を持っている。
  • 127.127.1.0 は自ホストのシステム時刻を表すもので、"server 127.127.1.0" は自身で自身を参照するループを意味する。後続の"fudge 127.127.1.0 stratum 10"は参照する 127.127.1.0 (システム時刻) の stratum を10にするということ。よって、システム時刻を参照するループ状態になった場合には、NTPサーバが外部に通知するstratumは10+1の11である。
    • 正しいUTC時刻とは言えないので、stratumは大きく(階層的には下へ)するべきである(特にインターネットから見える場合には10未満はよろしくないらしい)。
  • なぜループを作ってまでNTPデーモンが"同期している"状態を維持するか。
    • NTPデーモンはクライアントからリクエストのパケットを受けると、戻りパケットに『Root Dispersion』をセットして返す。この値の計算方法を理解はしていないが、下記の事は調査できた。
      • 戻りパケットの『Root Dispersion』にセットされる値を保持している内部変数は sys_rootdispersion である。
      • sys_rootdispersion は NTPデーモンが時刻を調整するために1秒ごとに呼び出している adj_host_clock()@ntp_loopfilter.c 内で clock_phi ずつインクリメントされている。この clock_phi は定数 CLOCK_PHI の値をそのまま使ったもので、CLOCK_PHI は 0.000015 (15マイクロ) である@ntp_loopfilter.c。これは Kernel time descipline の場合でも、NTP daemon time descipline の場合でも変わらない(Kernel time descipline の場合は、このインクリメントの後で return している)。
      • sys_rootdispersion が変更されるのは clock_update()@ntp_proto.c が呼ばれた場合か、内部変数をクリアするような事態(step調整をした)が発生した場合だけである。clock_update() は『同期先の選択(clock_select())』のあとに"同期先が選択できたら(候補が全滅しなかったら)"呼ばれる。この clock_select() は同期先のNTPサーバからポーリングに対する応答が帰ってきた際やポーリング時に過去3回以上連続で応答が無いサーバをみつけた場合に呼ばれる。同期先が無いNTPサーバについては、常に"同期先が選択できない"状態であり clock_update() が呼ばれない。※応答する上位NTPサーバが残っていても、その時刻が同期するに値しないと clock_select() が判断すれば、同期先のサーバが選べず、clock_update() は呼ばれない。よって、NTPサーバが上位NTPサーバと同期していない間は sys_rootdispersion は単調増加を続けるということである(step調整自体まれであるし、そもそもstep調整も clock_update() から呼び出す local_clock() で実施するものなので起こりえない)。これは、そのNTPサーバがクライアントに対して返すパケットの『Root Dispersion』が1秒ごとにCLOCK_PHIずつ単調増加していくことを意味している。
    • NTPデーモンは上位NTPサーバから戻りパケットを受け取ると、『同期先の選択(clock_select())』を行うが、この中で下記の peer_unfit() を呼び出し、該当しないものは切り捨て(無視)している。
      • 『root_distance(peer) >= sys_maxdist + clock_phi * ULOGTOD(sys_poll)』を満たさない上位NTPサーバを無視するフラグ(TEST11: root distance exceeded)がセットされること。これにより、peer_unfit() 呼び出し側はこの上位NTPサーバは適切ではない(unfit)と判断する。
      • sys_maxdist は ntp.conf の tos コマンドの maxdist オプションで指定でき、デフォルトは 1.0 (4.2.6p3では1.5になっている) である。
      • clock_phi * ULOGTOD(sys_poll) は root_distance() 内で計算される clock_phi * (current_time - peer->update) を考慮したものと思われる。最後に応答があった時刻(peer->update)から毎秒やはり clock_phi (=CLOCK_PHI) ずつ増加したと想定して計算している。sys_poll は同期先のポーリング間隔だが、いずれのタイミングでも ntp.conf の minpoll, maxpoll オプションの制約は受けるので、上限は17である。よって clock_phi * ULOGTOD(sys_poll) は 0.000015*217(=1.96) を超えないし、通常は maxpoll = 10 程度として 0.01536 程度となる。
      • 『root_distance(peer) >= sys_maxdist + clock_phi * ULOGTOD(sys_poll)』について root_distance() の計算を展開すると、『max(sys_mindisp, dist + peer->delay) / 2 + peer->rootdispersion + peer->disp + clock_phi * (current_time - peer->update) + peer->jitter >= sys_maxdist + clock_phi * ULOGTOD(sys_poll)』になる。ここで、maxpoll = 10 とすれば、右辺の clock_phi 以降はほぼ無視できる(前項)。また、左辺から項目をばっさり落とす。基本的に項目を落とせば値は小さくなるので条件は厳しくなるのでざっくり考える上では問題ない。すると『peer->rootdispersion + clock_phi * (current_time - peer->update) > sys_maxdist』が残る(current_time - peer->update は最後の応答から時間が経つほど大きくなるため残す)。この peer->rootdispersion が上位NTPサーバが返した『Root Dispersion』。
    • 『peer->rootdispersion + clock_phi * (current_time - peer->update) > sys_maxdist』は2つのことを意味する。
      1. たとえ応答していてもさらに上位と同期していない上位NTPサーバについてはいずれ不適切と判定されて無視される。peer->rootdispersion (上位NTPの sys_rootdispersion) の増加を続くので、いずれ sys_maxdist を超えて不適切(unfit)と判定される。sys_maxdistが 1.0 だとし、上位NTPの sys_rootdispersion が毎秒CLOCK_PHIごと増加とすると、1.0/CLOCK_PHI = 66,666秒 = 18.5時間 (sys_maxdistが1.5の場合は27時間程度) で超える。約1日で超えることになる。
      2. 応答しないサーバについても不適切と判定されて無視される(clock_phi * (current_time - peer->update) が増加するから)。※ポーリング時に過去3回以上連続でポーリングの応答が無いと、clock_select() が呼ばれるので、"時刻データの不足"などの別の条件でより速い条件で落とされるだろうが・・・
/*
 * Determine if the peer is unfit for synchronization
 *
 * A peer is unfit for synchronization if
 * > TEST10 bad leap or stratum below floor or at or above ceiling
 * > TEST11 root distance exceeded
 * > TEST12 a direct or indirect synchronization loop would form
 * > TEST13 unreachable or noselect
 */
int                             /* FALSE if fit, TRUE if unfit */
peer_unfit(
        struct peer *peer       /* peer structure pointer */
        )
{
        int     rval = 0;

        ・・・中略・・・

        /*
         * A distance error occurs if the root distance is greater than
         * or equal to the distance threshold plus the increment due to
         * one poll interval.
         */
        if (root_distance(peer) >= sys_maxdist + clock_phi *
            ULOGTOD(sys_poll))
                rval |= TEST11;         /* distance exceeded */

        ・・・中略・・・
}

/*
 * root_distance - compute synchronization distance from peer to root
 */
static double
root_distance(
        struct peer *peer
        )
{
        double  dist;

        /*
         * Careful squeak here. The value returned must be greater than
         * the minimum root dispersion in order to avoid clockhop with
         * highly precise reference clocks. In orphan mode lose the peer
         * root delay, as that is used by the election algorithm.
         */
        if (peer->stratum >= sys_orphan)
                dist = 0;
        else
                dist = peer->rootdelay;
        dist += max(sys_mindisp, dist + peer->delay) / 2 +
            peer->rootdispersion + peer->disp + clock_phi *
            (current_time - peer->update) + peer->jitter;
        return (dist);
}
NTPの可用性(2): 上位NTPサーバとの接続トポロジ

『6.2.2 NTP の時刻同期の仕組み』の『peer 設定』より

peer 設定での必須条件 peer 設定を行う際は、以下の条件を満たす必要があります。
・同一stratum階層内のNTPサーバであること
・peer 設定によってペアを組む同一階層のサーバ間で、同一の上位 NTP サーバを参照してはならない

  • ところが、ntpdではないとはいえ、CISCONetwork Time Protocol:ベスト プラクティス White Paper に示された『NTP の展開例』では下位が上位をすべて参照しており、同一の上位NTPサーバを参照しまくっているのである。WikipediaNetwork Time Protocol - Wikipedia でも同じ上位のサーバを参照している。
  • わけがわからないので実験してみた。まず2サーバでやってみた結果。
    • 構成は下記の2パターン。prefer を入れるのは、同じサーバを参照すると peer の意味が無いため。

    • CASE1でサーバAの時刻をわざと +2秒 した場合のクライアント a と b の状態推移は下記の通り。
      • (1)は同期がとれている状態。この時に サーバAの時刻を +2秒ずらす。すると(2)の状態に遷移する。これは急激に時刻が変化したAを排除する動きだろう。サーバaとbでntpq -pを実行したところ、サーバAのjitterが大きくなっていることが確認できた。しかしさらに時間をおくと(3)の状態に遷移し、サーバaもbも同期状態でなくなってしまう。サーバAが+2秒ずれた後は安定して時刻をキープしているため、再びピアとして戻ってきたと考えられる。すると、ピアAとピアBで重ならないオフセットを与えるため、過半数の合意が得られるオフセットがなく同期できない状態に陥る。peer 指定により a と b はお互いがお互いのピアに含まれる状態ではあるが、 それぞれが最後に同期したサーバ B が一致しているため、ループと認識し、最初から除外されてしまう(そのためにrejectを示す空白状態になる)。よってpeer 指定により過半数を得られる状態に戻ることはない。
 (1)       (2)       (3)
  a  b      a  b      a  b
 -------------------------
 *A +A      A  A     xA xA
 +B *B >>> *B *B >>> xB xB
 +b +a      b  a      b  a
    • CASE2でサーバAの時刻をわざと +2秒 した場合のクライアント a と b の状態遷移は下記の通り。
      • (1)は同期がとれている状態。この時に サーバAの時刻を +2秒ずらす。すると(2)の状態に遷移する。これは急激に時刻が変化したAを排除する動きだろう。サーバaでntpq -pを実行したところ、サーバAのjitterが大きくなっていることが確認できた。このときbは何の影響も受けないため引き続きBと同期状態にある。そのため、aは peer の接続を使用して b を同期先とする。しかしさらに時間をおくと(3)の状態に遷移し、サーバaは同期状態でなくなってしまう。これはCASE1の状態と同じで、サーバAがピアとして戻ってくると、ピアAとピアbで重ならないオフセットを与えるため、過半数の合意が得られるオフセットがなく同期できない状態に陥る(peerが設定されていない場合はそのままサーバAの値を信じてしまう)。しかし最後までサーバbは同期状態を保ち、サーバaまたはbを参照するクライアントに時刻を提供し続けることができる。
 (1)       (2)       (3)
  a  b      a  b      a  b
 -------------------------
 *A         A        xA
    *B >>>    *B >>>    *B
 +b +a     *b  a     xb  a
    • CASE2でサーバAの時刻がゆっくりとずれていった場合はどうなるだろうか。ゆっくりとずれる場合は、jitterがあまり大きくならないため、サーバaがサーバAと同期したまま追従する可能性が高い。するとあるタイミングでサーバaとサーバbはお互いが離れすぎた状態となり、サーバaではサーバAとサーバbが不一致、サーバbではサーバBとサーバaが不一致となり過半数が形成できなくなる。
    • 2サーバ同士の上下のNTPサーバの接続に関してはクロス参照すると、上位NTPサーバの1つで時刻のずれが発生した場合に下位のNTPサーバが全滅してしまう。よって、クロスをしないほうがまだよいと考えられる("まし"ということで可能なら3サーバ以上を"参照する"構成にするべき)。ただし、問題の本質は上位NTPサーバが2つしかない状態である限り、上位NTPサーバで発生した不一致により、破綻してしまう可能性は避けられない。
  • 次に3サーバでやってみた結果。
    • 構成は下記の2パターン。prefer を入れるのは、同じサーバを参照すると peer の意味が無いため。

    • CASE1でサーバAの時刻をわざと +2秒 した場合のクライアント a, b, c の状態推移は下記の通り。
      • (1)は同期がとれている状態。この時に サーバAの時刻を +2秒ずらす。すると(2)の状態に遷移する。これは急激に時刻が変化したAを排除する動きだろう。サーバa, b, cでntpq -pを実行したところ、サーバAのjitterが大きくなっていることが確認できた。この時点ではサーバAがreject(jitterが大きすぎる)されているが、サーバBやCが過半数を形成できるため同期は維持できる。さらに時間をおくと(3)の状態に遷移し、サーバAについてはxのfalsetick(過半数のサーバと離れたオフセット)となる。サーバAのjitterが小さくなりインターセクション・アルゴリズムまでは進むが、サーバBやCが過半数を形成しており、サーバAの誤りを検知している。結果として、サーバa, b, cはすべて同期状態を失わずに済んでいる。
 (1)          (2)          (3)
  a  b  c      a  b  c      a  b  c
 ----------------------------------
 *A +A +A      A  A  A     xA xA xA
 +B *B +B     *B *B +B     *B *B +B
 +C +C *C >>> +C +C *C >>> +C +C *C
    +a +a         a +a         a +a
 +b    +b      b    +b      b    +b
 +c +c        +c +c        +c +c   
    • CASE2でサーバAの時刻をわざと +2秒 した場合のクライアント a, b, c の状態推移は下記の通り。
      • (1)は同期がとれている状態。この時に サーバAの時刻を +2秒ずらす。すると(2)の状態に遷移する。これは急激に時刻が変化したAを排除する動きだろう。特に目新しい動作は無い。ポイントはサーバAがピアとして再度扱われるようになっても、peer を組んでいるbとcにより過半数が形成され、サーバAをfalsetickとして認識できている点。2サーバの場合にはこれができなかった。それにより同期状態は保つ事ができる。ただし、同期先はサーバbまたはcのいずれであっても、stratumはbまたはcより+1となり、同等ではなくなる。
 (1)          (2)          (3)
  a  b  c      a  b  c      a  b  c
 ----------------------------------
 *A            A           xA      
    *B           *B           *B   
       *C >>>       *C >>>       *C
    +a +a        +a  a        +a  a
 +b    +b     +b    +b     +b    +b
 +c +c        *c +c        *c +c   
    • 3サーバ同士の場合であれば、クロス参照をしても上位1サーバの時刻のずれで破綻することはない。
  • 『peer 設定によってペアを組む同一階層のサーバ間で、同一の上位 NTP サーバを参照してはならない』については、クロスを禁止するというより、同一の上位NTPサーバを参照した場合には、相手を無視する実装(ループ回避)に関する注意のように思われる。上位NTPサーバが2台の場合には差が見られるが、上位NTPサーバを3台にすると動作上差がないので、下記のように落ち着く。
  • 上位NTPサーバは3台以上にする
  • 上位NTPサーバが3台以上であれば、上位NTPサーバと下位NTPサーバの関係はクロス(多:多)でも1:1でもよい。
NTPの可用性(3): 全ての上位NTPサーバと疎通できなくなった場合
  • NTPの通信をどのセグメント(ネットワーク)で流すかはシステムレベルではなく、組織レベルでの決定事項と思う。システムを新たに作成するときに、組織内の共通NTPサーバに同期するとすれば、NTP通信に使用するネットワークは指定されることになるだろう。
  • NTPに使用するネットワークの冗長化の度合いにより、システム内の代表NTPサーバが上位NTPサーバ(組織内の共通NTPサーバ)から隔離される可能性が決まる。
    • NTPサーバに至るネットワークが2つ以上存在する場合は1ネットワークの障害により全ての上位NTPサーバと通信できなくなることはない。※がそこまで普通されていないと思う
    • NTPサーバに至るネットワークが1つの場合でも、経路がNICやスイッチ・ルーターを含めて冗長化されていれば通信できなくなることはない。※がはたしてNTPの通信にそんな良いネットワークが?
  • ともかく、上位NTPサーバと疎通できなくなった場合のことを考えておく。
    • これはもともとネットワークから隔離されていて、かつGPSなどの時刻ソースを用意できない場合にも当てはまる。
  • 上位NTPの与える"真の時刻"がない状況下では、"システム内での時刻統一"が次善の状態である。システム内での時刻が統一できていれば、ログの照合などは大概できる。
  • NTPを隔離されたネットワークで運用する方式は以下の2つ。そのうち、Orphan Mode は 4.2.2 以降にサポートされたもので、ローカルクロックを利用した運用を置き換えることができるらしい(下記参照)。ただし、Orphan Mode自体はNTPをより自律的なものにするためのもので、ローカルクロックの置き換えという狭い範囲を目的とした訳ではないと思われる。ローカルクロックを使用すると、ループが発生し状態が不安定となるため、Orphan Modeを使うのが良いだろう。
    1. ローカルクロックを使用する方法
    2. Orphan Modeを使用する方法 ※Orphan: 孤児・孤立無援の人

Orphan Mode より

Sometimes an NTP subnet becomes isolated from all UTC sources such as local reference clocks or Internet time servers. In such cases it may be necessary that the subnet servers and clients remain synchronized to a common timescale, not necessarily the UTC timescale. Previously, this function was provided by the local clock driver to simulate a UTC source. A server with this driver could be used to synchronize other hosts in the subnet directly or indirectly.

There are many disadvantages using the local clock driver, primarily that the subnet is vulnerable to single-point failures and multiple server redundancy is not possible. Orphan mode is intended to replace the local clock driver. It provides a single simulated UTC source with multiple servers and provides seamless switching as servers fail and recover.

  • 各方法による設定を示す。
  • ローカルクロックを使用して上位NTPサーバから隔離された状態に対処する構成は server 127.127.1.0 および fudge コマンドによりローカルクロックを設定すること(CASE 2)。
    • ローカルクロックを使用した場合、状態が安定するまでに不安定期間が生じる。同じstratumのローカルクロックをもった3サーバで構成して試してみたところ、a→b→c→a の参照関係が偶然でき、ループでしばらく同期を続けた。NTPはa→b→aは直接検知できても3サーバ以上によるループ検知は直接はできない。がstratumが下位が+1されるという構成上矛盾があるため、すこしずつstratumが落ちる。半日後に状態を確認すると、a→c、b→c でcのローカルクロックへ同期していた。最初にstratumが16になったサーバがaだとすると、cがaをstratumの下限条件にひっかかり参照できず、bはc自身を参照しているため参照できず、ローカルクロックに落ちる。するとbは引き続きcへ同期する。aはbとcのどちらかと同期できるようになる(cからの参照がなくなったのでcも参照できる)が、bがcを参照しているのであれば、stratum観点からcに決まる。
    • 海外のNTPスレッドでは、ローカルクロックのstratumをa=8, b=10, c=12 など階層にするとよい、とあるのだが実際にやってみたところ特に差がなかった。これはそもそもローカルクロックにpreferがついていない限り、インターセクション・アルゴリズムなどの対象にならず、1サーバも残らなかった場合のみローカルクロックを使うという動作から明らかだ。他に同期できるサーバがあればまずそれが優先される。ローカルクロックのstratumに階層をつけなければ、『自サーバにあるローカルクロックを参照してしまう』という主張は成り立たない(どんなに低いstratumだろうと同期できるサーバがローカルクロックより優先されるため)。また、階層構造をつけたところで、かならず一番よいstratum(この場合8)を参照するわけでもなく、あくまでどこでループがstratum=16となり破綻するかに依存する。
  • Orphan Modeを使用して上位NTPサーバから隔離された状態に対処する構成は tos コマンドの orphan オプションを設定すること(CASE 1)。
    • orphan オプションに8を指定した場合、stratum 7以下の上位NTPサーバと全く疎通できなくなると、Orphan Modeでの動作が開始される。peerで接続された各サーバ(図ではa,b,c)は互いに通信し、1サーバを代表にきめ、そのサーバと同期する。この場合、どのサーバのstratumも他のNTPクライアントから参照すると8である。refidは127.0.0.1となる。この場合、ループなどおかしな状態は発生しない。
    • Orphan Modeについては日本語による資料はみたことがない。RFCにも記載がなく、今のところはntpd実装によるサポートというところなのだろう。Orphan Modeで動作しているNTPサーバをntpd以外が参照できることは確認したほうがよいだろう。


構築

設定対象
  • RHEL5
    • /etc/ntp.conf
    • /etc/sysconfig/ntpd
  • RHEL6
    • /etc/ntp.conf
    • /etc/sysconfig/ntpd
    • /etc/sysconfig/ntpdate
手順
  1. NTPの設計に基づいて ntp.conf を作成
  2. ntpパッケージがインストールされていることを確認
  3. ntp.conf を配置
  4. /etc/sysconfig/ntpd, /etc/sysconfig/ntpdate (RHEL5はなし) を設定
  5. /etc/ntp/keys ファイルを設定 (鍵を使わない場合は空ファイル)
  6. /etc/ntp/step-tickers を設定 (ntpdate 実行時の同期先を変えない場合は空ファイル)
  7. /var/ntp/ntpstats ディレクトリを作成(RHEL5のみ、RHEL6は自動作成)
  8. /var/lib/ntp/drift が存在する場合かつ値の絶対値が500を超えている場合は0に書き換える
  9. 定期的にhwclockを実行するスクリプト(hwclock.cron)を配置
  10. 統計情報ログファイルの削除用スクリプト(ntpstats.cron)を配置
  11. 所有者・バーミッションの設定・確認
    1. root:root 644 /etc/ntp.conf (最も厳しくするなら600でも可)
    2. root:root 755 /etc/ntp/
    3. root:root 600 /etc/ntp/keys
    4. root:root 644 /etc/ntp/step-tikers (ntp.confと同じパーミッションにする)
    5. root:ntp 770 /etc/ntp/crypto ※RHELのデフォルト設定(妥当性は未確認※AutoKey使用時のみ関係)
    6. ntp:ntp 755 /var/lib/ntp/ (driftファイル配置先)
    7. ntp:ntp 644 /var/lib/ntp/drift (存在する場合のみ)
    8. ntp:ntp 755 /var/log/ntpstats/ (統計情報書き込み先)
    9. root:root 700 /etc/cron.daily/hwclock.cron (使用時のみ)
    10. root:root 700 /etc/cron.daily/ntpstats.cron (使用時のみ)
  12. 上位NTPサーバとの通信経路確保(ntpdateが通ればよい)
  13. 動作確認
    1. service ntpdate start (RHEL5はなし)
    2. service ntpd start
    3. syslogを確認し、エラーログがないことを確認
    4. ps -elf | grep ntpd にて ntpd が指定ユーザ (ntp) で実行されていることを確認
    5. watch ntpq -p で上位NTPサーバと同期することを確認
    6. 下位NTPサーバやクライアントと同期する事を確認 (問い合わせ側)
    7. stepやpanicの値を変更した場合には動作を確認
      1. 時刻をずらす場合は date --set='+1000 second' などを使用
    8. 上位NTPサーバと通信できなくなった場合の動作確認
    9. service ntpd stop
    10. service ntpdate stop (RHEL5はなし)
  14. 自動起動設定(必要時)
    1. chkconfig --level 35 ntpdate on (RHEL5はなし)
    2. chkconfig --level 35 ntpd on
/etc/sysconfig/ntpd (RHEL5)
# ntpd のオプション
#
#     -u ntp:ntp : NTPサービスの実行ユーザおよびグループを ntp とする
#     -x : 時刻差600秒までslewモードで調整する(ステップ調整しない)
#     -p : PIDファイル指定
#
OPTIONS="-u ntp:ntp -p /var/run/ntpd.pid -x"

# ntpd 起動前の ntpdate 実行後にハードウェア時刻に反映するか(yes|no)
# ※ntpdate 直後にハードウェア時刻に反映する運用が望ましいので yes
# ※yes としても ntpdate が失敗した場合には反映されない
SYNC_HWCLOCK=yes

# ntpd 起動前の ntpdate 実行時の追加オプション (特になし)
# ※同期先は /etc/ntp/step-tinkers ファイルから取得できなければ ntp.conf の server/peer を自動取得する
# ※/etc/init.d/ntpd 内で下記オプションは自動で付加される
#
#     -s : ntpdate のログを syslog に出力する
#     -b : オフセットによらず step による調整を行う
#     -U : ntpd の OPTIONS にて -u を指定した場合、ntpdate も当該ユーザで実行する
#
NTPDATE_OPTIONS=""
/etc/sysconfig/ntpd (RHEL6)
# ntpd のオプション
#
#     -u ntp:ntp : NTPサービスの実行ユーザおよびグループを ntp とする
#     -x : 時刻差600秒までslewモードで調整する(ステップ調整しない)
#     -p : PIDファイル指定
#
OPTIONS="-u ntp:ntp -p /var/run/ntpd.pid -x"
/etc/sysconfig/ntpdate (RHEL6)
# ntpdate 実行時の追加オプション
# ※同期先は /etc/ntp/step-tinkers ファイルから取得できなければ ntp.conf の server/peer を自動取得する
#
#     -s : ntpdate のログを syslog に出力する
#     -b : オフセットによらず step による調整を行う
#     -U : ntpdate を ntp ユーザで実行する
#
OPTIONS="-U ntp -s -b"

# ntpd 起動前の ntpdate 実行後にハードウェア時刻に反映するか(yes|no)
# ※ntpdate 直後にハードウェア時刻に反映する運用が望ましいので yes
# ※yes としても ntpdate が失敗した場合には反映されない
SYNC_HWCLOCK=yes
/etc/ntp.conf テンプレート
####
#### ステップ動作制御
#### ※先頭に記述しなければならない(VMwareが明示的に指示している)
####
# 通常は tinker コマンドを使用する必要は無い
# レジュームにより上位NTPサーバとの時間が大きくずれるVMで、かつNTPサーバをパニック(終了)させたくない場合は下記を使用する
# ※この設定ではレジューム以外の意図しない要因で時間差がでたときに検知できなくなる
# ※NTPサービスの再起動(その際にntpdateも実施されるようにする)をする運用で対処するのがよいのでは
# ※panicのデフォルト値は1000秒。よってデフォルトでは1000秒以上上位サーバとずれた場合にNTPサーバはパニック(終了)する
# ※-g オプションは -q オプションとあわせて使う場合にのみ意味がある。-g オプションは同期がとれた時点で無効化され、その後のパニックは一度も回避できない。
#
#     tinker panic 0
#
# ステップ調整に切り替えるオフセットの閾値(step)およびステップ調整を実施するまでの猶予期間(stepout)を指定する場合は下記を使用する
# ※ stepを超えるずれが発生しても、stepout以内にずれが回復すればステップ動作にはならない
# ※ stepのデフォルトは 0.128秒、stepoutのデフォルトは900秒。stepoutはデフォルトで十分長い
# ※ stepは ntpd に -x オプションを指定することで 600秒 に設定できる。600秒でよい場合は tinker ではなく -x オプションを指定する
# ※ slewによる調整は最大0.5ミリ秒/秒である。1秒の修正には2000秒(約33分)、100秒には約2日と8時間、600秒には約14日かかる
#
#     tinker step xxx stepout xxx
#


####
#### アクセスコントロール
####
# 認証機能を有効にする
enable auth

# すべてのパケットを無視する(以降のホワイトリストで解除したもの以外)
restrict -4 default ignore
restrict -6 default ignore

# 自ホスト(localhost)からのアクセスは ntpq および ntpdc による変更を禁止する
# ※ 認証機能を有効にしていれば変更を防止することは可能だがアクセスレベルでも抑止する
# ※ NTPの設定をオンラインで変更するような運用は避けること(設定変更・再起動を行う)
restrict -4 127.0.0.1 nomodify
restrict -6 ::1 nomodify

# 上位NTPサーバへのアクセスを許可する
# ※DNSで複数のIPにバランスされる場合はすべて記述する必要がある
restrict xxx.xxx.xxx.xxx noquery nomodify nopeer notrap
restrict xxx.xxx.xxx.xxx noquery nomodify nopeer notrap
restrict xxx.xxx.xxx.xxx noquery nomodify nopeer notrap

# ピアNTPサーバへのアクセスを許可する(必要時)
restrict xxx.xxx.xxx.xxx noquery nomodify notrap

# 下位NTPサーバ・クライアントからのアクセス頻度を制限する
# ※通常はデフォルトでよいので discard とだけ指定しておく
discard

# 下位NTPサーバ・クライアントからのアクセスを許可する
# ※ネットワークレベルの許可は mask を使って設定する
# ※noquery は ntpq, ntpdc を拒否するだけなので問い合わせには影響ない
restrict xxx.xxx.xxx.xxx mask xxx.xxx.xxx.xxx limited kod noquery nomodify nopeer notrap
restrict xxx.xxx.xxx.xxx mask xxx.xxx.xxx.xxx limited kod noquery nomodify nopeer notrap
restrict xxx.xxx.xxx.xxx mask xxx.xxx.xxx.xxx limited kod noquery nomodify nopeer notrap


####
#### サーバ
#### ※serverおよびpeerによる通信相手の過半数が正しい時刻を提供できるように注意すること
#### ※過半数の通信相手から一定範囲内のずれに収まるオフセットを得られない場合、NTPは同期先を選択できない
####
# 上位NTPサーバ
# ※3サーバ以上が望ましい。特に優先して使いたいサーバがあれば prefer オプションをつける
# ※iburstは同期を迅速に実施するための指定(無応答のサーバに対して無駄にバーストする事は無いので使用を推奨)
# ※burstは使用しないこと(Do not use burst unless you have a very good reason for doing so.)
# ※minpoll と maxpoll は上位サーバの負荷を考慮して決める
#   (内部のNTPサーバなら小さくしてもよいが、公開NTPサーバの場合は周期が最終的に1024秒以上になるようにしておく)
# ※共通鍵を使用して認証する場合は "key 鍵番号" を追加する (keys および trustedkey 指定が必要)
server xxx.xxx.xxx.xxx iburst minpoll 6 maxpoll 10 prefer
server xxx.xxx.xxx.xxx iburst minpoll 6 maxpoll 10
server xxx.xxx.xxx.xxx iburst minpoll 6 maxpoll 10

# ピアNTPサーバ(必要時)
# ※ピアNTPサーバは同じ strutum 同士でなければならない(同じ strutum の上位NTPサーバを参照している)
# ※ピアNTPサーバ同士は異なる上位NTPサーバを参照できるようにする(同じ上位NTPサーバと同期している間はお互いに無視する)
# ※ピアNTPサーバへは iburst も burst も使用するべきではない(想定していない動作であり悪影響の懸念がある)
# ※ピアNTPサーバ同士の通信周期は 64秒 など固定周期が推奨されている
# ※minpoll と maxpoll を同一にすることで一定周期を強制できる。minpoll 6 maxpoll 6 は 64秒周期固定になる。
peer xxx.xxx.xxx.xxx minpoll 6 maxpoll 6


####
#### バックアップ(上位NTPサーバとピアと接続が切れた場合)
#### ※NTPクライアント用途のみの場合は不要
#### ※自サーバの時刻が上位NTPサーバと大きくずれた場合も使われてしまうことに注意(特にレジュームしたVM)
####
# Orphan Modeによる方法(新しい)
# ※Orphan Mode を使用する場合は下記を使用する(stratumは上位NTPサーバのstratumより大きくする)
#
#     tos orphan 10
#
# ローカルクロックによる方法(古い)
# ※サーバ自身のローカルタイム(システム時刻)を参照させる (127.127.x.x はNTPで特別な意味を持つ)
# ※同期していないNTPサーバはクライアントからサーバとして利用できないためダミー(自身)とでも同期する必要がある
# ※server の直後に fudge を記述しなければならない
# ※stratum は上位NTPサーバのいずれよりも低く設定する
# ※VMwareはVMに tinker panic 0 の設定を"行う"こととあわせて、本設定を"行わない"ことを推奨している
# ※server に prefer をつける場合意味を十分理解すること(つけた場合はインターセクション・アルゴリズムの対象にローカルクロックが含まれる)
#
#     server 127.127.1.0
#     fudge  127.127.1.0 stratum 10	
#

####
#### NTPデーモンの統計情報出力
####
# デフォルトでは無効"らしい"ので"念のため"有効化 (なくてもどうも動く)
enable stats

# 出力先ディレクトリ指定 (RHEL6のntpは /var/log/ntpstats を作成する)
# ※ntpユーザが書込めること、他ユーザの書き込みは禁止すること
# ※末尾のスラッシュは必須(filegenで指定したファイル名とそのまま結合してパスが構成される)
statsdir /var/log/ntpstats/

# peerstats出力 (上位NTPサーバ・ピアNTPサーバとの時刻差などの統計情報)
# loopstats出力 (サーバ自身の時刻更新に関する情報)
# ※type month 指定によりファイル名は xxxxstats.YYYYMM となる
# ※type day 指定によるファイル名は xxxxstats.YYYYMMDD となる
# ※peerstats はピア数とポーリング回数に比例して増加する
# ※旧世代の自動削除は外部的に行う必要がある
# ※link を指定しているためファイル名 xxxxstats も"ハードリンク"で作成される
# sysstats には拒否・認証失敗の回数が記録される(内部NTPサーバであれば通常は不要)
statistics peerstats loopstats
filegen peerstats file peerstats type month link enable
filegen loopstats file loopstats type month link enable


####
#### その他
####
# ドリフトの保存先
# ※ntpユーザが書込めること、他ユーザの書き込みは禁止すること
# ※書き込み時は再作成になるのでリンクにはできない
# ※書き込み周期は1時間ごと(ファイル名の後に分単位で指定できるが変更の必要は無い)
driftfile /var/lib/ntp/drift

# 共通鍵指定
# ※ /etc/ntp/keys はntpユーザのみが読み取れるようにする
# ※ 鍵を使わない場合には /etc/ntp/keys は空ファイルにする (デフォルトの状態はコメントをはずすと危険)
keys /etc/ntp/keys

# server に key オプションを使用する場合は対応する鍵を trustedkey として記述する
# controlkey(ntpqからの変更操作の認証用)は指定せず認証させない
# requestkey(ntpdcからの変更操作の認証用)は指定せず認証させない
# controlkey, requestkey に指定する鍵も trustedkey に指定する。
# controlkey, requestkey は同一の鍵にできる。
#
#     trustedkey
#     controlkey
#     requestkey
#
統計情報ログファイルの削除用スクリプト例 ( /etc/cron.daily/ntpstats.cron )
#!/bin/bash

#
# 使用前提
# (1) ntp.conf 内で statsdir コマンドを指定していること
#     (指定が無い場合は /var/log/ntpstats と仮定する)
# (2) ntp.conf 内で出力対象の統計情報ごとに filegen コマンドを指定していること
#     statistics のみの指定には対応しない(削除対象から漏れる)
#     (file オプションを指定する場合は filegen xxx file xxx の順とすること)
# (3) NTPCONF に ntp.conf のフルパスを指定すること
# (4) AGE に保存世代数(保存ファイル数)を指定すること(全統計情報で共通)
#
NTPCONF=/etc/ntp.conf
AGE=93

# statsdir で指定した統計情報出力先ディレクトリを取得
statsdir=$(/bin/awk '$1=="statsdir" {print $2}' $NTPCONF)
[ -z "$statsdir" ] && statsdir=/var/log/ntpstats

# filegen で指定されている出力対象の種類を取得
filegen_list=$(/bin/awk '$1=="filegen" {print $3=="file"? $4 : $2}' $NTPCONF)

# statsdir 存在チェック
if [ -n "$filegen_list" -a ! -d "$statsdir" ]; then
    /usr/bin/logger -p user.warn -t ntpstats.cron \
        "statsdir($statsdir) not found. exiting without dropping logfiles."
    exit 0
fi

# ファイル削除
for stats in $filegen_list; do
    /usr/bin/find $statsdir -name "$stats.*" \
        | /bin/sort -r | /usr/bin/tail -n +$((AGE+1)) \
        | /usr/bin/xargs /bin/rm -rf
done
exit 0
定期的にhwclockを実行するスクリプト例 ( /etc/cron.daily/hwclock.cron )
#!/bin/bash

#
# 使用前提
# (1) 定期的な呼び出しの場合、1日1回とする(通常十分な頻度)。
#

/sbin/hwclock --systohc >/dev/null 2>&1
if [ $? -ne 0 ]; then
    /usr/bin/logger -p user.notice -t hwclock.cron \
        "failed to sync hardware clock to system time."
else
    /usr/bin/logger -p user.info -t hwclock.cron \
        "syncing hardware clock to system time, done."
fi
exit 0

ログ出力例

# 上位NTPサーバと同期した場合 (起動後最初か no servers reachable の後が該当)
# 同期先の上位NTPサーバを切り替えた場合 (連続で synchronized to ... がでた場合はより適切と思われる上位NTPサーバに随時切り替えているという意味)
Aug  7 02:42:30 daemon.info localhost ntpd[25449]: synchronized to 172.16.96.132, stratum 2

# 同期可能なNTPサーバが無い場合
Aug  7 00:12:44 daemon.info localhost ntpd[22029]: no servers reachable

# ローカルクロックと同期した場合
Aug  5 19:08:13 daemon.info localhost ntpd[5131]: synchronized to LOCAL(0), stratum 10

# ステップ調整した場合
Aug  7 00:14:24 daemon.notice localhost ntpd[22029]: time reset +2.000073 s

# tinker panic の設定値(1000秒) を超えて時刻がずれた場合 (ntpdプロセスは終了)
Aug  5 19:48:10 daemon.err localhost ntpd[5862]: time correction of -1168 seconds exceeds sanity limit (1000); set clock manually to the correct UTC time.

その他はntpd System Log Messagesを参照。

その他の豆知識

  • NTPで同期がとれるまで待ちたい場合には ntp-wait コマンド(perlスクリプト) を使用できる (RHEL6ではrpmから消滅)。
    • manもヘルプ出力も無いのでソース上そうなっていた、という情報。
    • オプションは -v (verbose output) -n (リトライ回数) -s (リトライ間隔[秒])
    • s秒間隔でn回リトライする間にNTPの同期がとれれば0を返して終了。同期できなければ1を返して終了。
  • うるう秒
  • stratum が優先されるのはインターセクション・アルゴリズムにより過半数の形成がされた後。stratum 1 のサーバが1台で時刻N、stratum 10のサーバが2台で時刻Mを示した場合、過半数を形成するstratum 10のサーバ2台が候補となり、stratum 1のサーバはfalsetick(x)にされる。過半数の形成においてはstratum 1でもstratum 10 でも同じ1票になる。
  • ソースコード解析
    • 見るファイル
      • rootユーザからntpユーザへの切り替え: ntp-4.2.4p8/ntpd/ntpd.c (ntpdmain関数 (setuid呼び出しで検索))
      • 設定ファイル・オプションの解析呼び出し: ntp-4.2.4p8/ntpd/ntpd.c (ntpdmain関数内の getconfig() 呼び出し)
      • 設定ファイルの読み込み: ntp-4.2.4p8/ntpd/ntp_config.c, ntp_loopfilter.c (tinker関係)
      • ntpdのオプション解析: ntp-4.2.4p8/ntpd/cmd_args.c
      • discipline処理: ntp-4.2.4p8/ntpd/ntp_loopfilter.c (local_clock関数)
      • ntpq,ntpdcからのリクエスト処理: ntp-4.2.4p8/ntpd/ntp_control.c, ntp_request.c
    • グローバル変数とオプションなどの対応
      • -gオプション: allow_panic
      • -xオプション: clock_max ※tinker stepと同じ
      • -qオプション: mode_ntpdate
      • -uオプション: droproot ※オプションで指定したユーザとグループは user, group に格納される
      • tinker panic: clock_panic
      • tinker step: clock_max
      • tinker stepout: clock_minstep
      • enable kernel: kern_enable
      • controlkey: ctl_auth_keyid
      • requestkey: info_auth_keyid
      • enable auth: sys_authenticate
      • step調整用関数: step_systime() ※local_clock()@ntp_loopfilter.c
      • カーネルモードによる調整用関数: ntp_adjtime() ※local_clock()@ntp_loopfilter.c
      • NTPによる調整用関数: adj_systime() ※timer()@ntp_timer.c → adj_host_clock()@ntp_loopfilter.c → adj_systime()
    • わかりにくかったところ
      • local_clock()でステップ調整をstepout秒猶予する部分: mu = peer->epoch - sys_clocktime で計算した mu が stepout(clock_minstep) を超えるまではステップ調整をしないようになっている。ここで peer は同期している上位NTPサーバのこと。sys_clocktime は最後に状態遷移した時刻(SYNC状態から猶予状態のSPIK状態に遷移した時刻)のこと。sys_clocktimeは状態遷移したときに rstclock() を呼び出して更新している。猶予状態の間は sys_clocktime が更新されないので、これがカウンタの役割を果たす。(この仕組み上stepを超えるずれが生じてからジャストstepout秒後にステップ調整をするわけではない。だいたいそのぐらい猶予できる、と考えるべき)
      • AutoKey(公開鍵暗号)対応のコードが #ifdef OPENSSL で至る所に埋め込まれている。AutoKey に関する調査ではなく、ntpd の動きを調査したい場合には #ifdef OPENSSL の部分は無視するのがよい(elseがあればそちらだけをみる)。