この記事は、フラー株式会社 Advent Calendar 2021 の11日目の記事です。 10日目は @nobux42 で「再読:リファクタリング・ウェットウェア」でした。
もう一ヶ月前になりますが、AWS から ClockBound という時間を扱うとても 地味な 有益なソフトウェアがリリースされました。
地味過ぎてネタかぶりしなさそうなので
時間は現代の情報通信の基盤なので、しっかりと検証していきましょう!
日本ではNICTの 時空標準研究室 が標準時を定めています。
名前からしてかっこいい。
ClockBound とは
一言でいうとGoogleの TrueTime のAWS版です。 TrueTime は Google が自社のサーバーセンターに設置している非常に正確な時計です。 Google が提供しているリレーショナルデータベースである Cloud Spanner は、 リージョンをまたいだ一貫性を保証するために TrueTime から生成されたタイムスタンプを利用しています。
ようするに ClockBound を使えば、 AWSのインフラ上に Google Cloud Spanner Clone を構築できる!(?)、というわけですね。 すごい!
ClockBoundD のインストール
なんかすごそうなことがわかったので、とりあえず動かしてみましょう。
ClockBound はタイムスタンプを提供するデーモン「ClockBoundD」と、ClockBoundD からタイムスタンプを取得するためのライブラリ「ClockBoundC」に分かれています。 タイムスタンプの提供元がないと始まらないので、まずは ClockBoundD をインストールしていきましょう。
現時点(2021-12-11現在)ではビルド済みのバイナリは提供されていないようなので、ソースコードからビルドします。 READMEの手順にしたがってやっていきます。
今回は Amazon EC2 上のインスタンスで、 OSは Amazon Linux 2 を使ってやってみます。
Chrony の動作確認
ClockBoundDを動かす前提条件として、Chrony が同じコンピュータで動いている必要があります。 Amazon Linux 2 にはデフォルトでインストールされているので、同期状態を確認してみましょう。
$ chronyc sources
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^* 169.254.169.123 3 4 377 3 -1145ns[-3152ns] +/- 420us
^- 103.202.216.35 3 6 377 4 +3283us[+3281us] +/- 132ms
^- ipv4.ntp2.rbauman.com 2 6 377 13 -631us[ -633us] +/- 3130us
^- i172-105-204-167.poolntp> 3 6 377 11 +118us[ +116us] +/- 3201us
^- time.cloudflare.com 3 6 377 10 +10ms[ +10ms] +/- 84ms
169.254.169.123
と同期しているようです。このIPアドレスは Amazon Time Sync Service のものですね。
Chrony が立ち上がっており時刻同期も取れているので良さそうです。
Rust, Cargo のインストール
ClockBoundD は Rust で書かれています。 コンパイルするために Rust のコンパイラと、 Rust のパッケージマネージャーである Cargo をインストールします。 The Cargo Book を参照しながらインストールコマンドを実行します。
また、ビルドに gcc も必要らしいので併せてインストールしておきます。
curl https://sh.rustup.rs -sSf | sh
sudo yum install gcc
ClockBoundD のビルド
Cargo を使って依存関係の解決とビルドを行います。
cargo install clock-bound-d
ClockBoundD サービスの追加
ClockBoundD はデーモンとして稼働するので、 systemd の管理下に置きましょう。
# バイナリをインストール
sudo mv $HOME/.cargo/bin/clockboundd /usr/local/bin/clockboundd
# デーモン起動用のユーザー追加
sudo useradd -r clockbound
/usr/lib/systemd/system/clockboundd.service
という名前でユニットファイルを作成し、
以下の設定を書き込みます。
[Unit]
Description=ClockBoundD
[Service]
Type=simple
Restart=always
RestartSec=10
ExecStart=/usr/local/bin/clockboundd
RuntimeDirectory=clockboundd
WorkingDirectory=/run/clockboundd
User=clockbound
[Install]
WantedBy=multi-user.target
ClockBoundD サービスの起動
ユニットファイルを systemd に読み込ませ、サービスを有効化します。
sudo systemctl daemon-reload
sudo systemctl enable clockboundd
sudo systemctl start clockboundd
正常に起動したかステータスを確認してみましょう。
$ systemctl status clockboundd
● clockboundd.service - ClockBoundD
Loaded: loaded (/usr/lib/systemd/system/clockboundd.service; enabled; vendor preset: disabled)
Active: active (running) since 日 2021-11-14 08:04:33 UTC; 22s ago
Main PID: 3922 (clockboundd)
CGroup: /system.slice/clockboundd.service
└─3922 /usr/local/bin/clockboundd
11月 14 08:04:33 ip-10-0-2-225.ap-northeast-1.compute.internal systemd[1]: Started ClockBoundD.
11月 14 08:04:33 ip-10-0-2-225.ap-northeast-1.compute.internal clockboundd[3922]: Initialized ClockBoundD
11月 14 08:04:33 ip-10-0-2-225.ap-northeast-1.compute.internal clockboundd[3922]: Local unix socket at path clockboundd.sock does not exist, skipping remove
11月 14 08:04:33 ip-10-0-2-225.ap-northeast-1.compute.internal clockboundd[3922]: Created unix socket at path clockboundd.sock
11月 14 08:04:33 ip-10-0-2-225.ap-northeast-1.compute.internal clockboundd[3922]: Connected to local unix socket at path clockboundd.sock
11月 14 08:04:33 ip-10-0-2-225.ap-northeast-1.compute.internal clockboundd[3922]: Initialized Chrony Poller thread
良さそうですね。
付属の Example で遊んで見る
デーモンの準備が整ったので、クライアントを試してみましょう。 ClockBoundC はライブラリなので実際に使用するにはアプリケーションに組み込む必要がありますが、 付属の Example で試すだけならすぐに出来ます。
ClockBoundC から ClockBoundD に送信できるコマンドには Now, Before, After の3つがあります。 それぞれ試してみましょう。
Now
Now は現在時刻を取得するためのコマンドです。
$ cargo run --example now /run/clockboundd/clockboundd.sock
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/examples/now /run/clockboundd/clockboundd.sock`
The UTC timestamp 2021-11-14 12:23:43.089578323 has the following error bounds.
In nanoseconds since the Unix epoch: (1636892623089459280,1636892623089697366)
In UTC in date/time format: (2021-11-14 12:23:43.089459280, 2021-11-14 12:23:43.089697366)
大抵のプログラミング言語には現在時刻の取得関数がありますが、 Now がそれらと違うのは error bounds を同時に取得できる点です。
例えば上の例では 2021-11-14 12:23:43.089578323
という時刻を得られましたが、
実際の時刻は 2021-11-14 12:23:43.089578323
ちょうどなわけではありません。
コンピューターに内蔵されている時計が刻む時刻は、様々な要因で実際の時刻との間に差があります。
Chrony はこの差をなるべく補正しようとしますが、完全に0にすることはできません。
誤差を完全に0にすることは不可能ですが、NTPプロトコルの通信結果から誤差の上限を見積もることなら可能です。
上の実行結果では 「2021-11-14 12:23:43.089459280
から 2021-11-14 12:23:43.089697366
の間に本当の時刻があるはず」
ということを意味しています。
ClockBound の Bound は、現在時刻をある程度の「範囲」をもって計算するという意味なのです。
Before
Before は「あるタイムスタンプが現在時刻より前のものか」を判定するコマンドです。
$ cargo run --example before /run/clockboundd/clockboundd.sock
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/examples/before /run/clockboundd/clockboundd.sock`
1636892655198601329 nanoseconds since the Unix Epoch is not before the current time's error bounds.
Waiting 1 second...
1636892655198601329 nanoseconds since the Unix Epoch is before the current time's error bounds.
この出力だけだと 1636892655198601329
という数字が何を意味しているのかよくわからないので、
ソースコードのコメント とあわせて見ると良いと思います。
大雑把にこんなデモを実行しています。
- Rust の時間関数を使って現在時刻を取得。
timestamp
変数に代入 timestamp
を ClockBoundD の Before コマンドを使って判定- 1秒待つ
timestamp
を ClockBoundD の Before コマンドを使って判定
注目すべきは 2 のステップで timestamp is not before now
と判定されている点ですね。
ステップ1 → ステップ2 の順番で実行しているので、普通に考えれば ステップ2 の段階では 「timestamp
は現在時刻より前」
timestamp is before now
になるはずです。
これは ステップ1 → ステップ2 の実行速度が早すぎるために timestamp
が “error bounds” の中に入ってしまうからです。
この状態では 「timestamp
は現在時刻より確実に前」とは言い切ることができないので、
timestamp is not before now
という判定になります。
Now の実行結果をみると分かるんですが、
ClockBoundD が正常稼働していれば “error bounds” の幅は200マイクロ秒前後に収まります。
そのためステップ4の時点では timestamp
はほぼ確実に “error bounds” の外にでます。
この状態であれば「timestamp
は現在時刻より確実に前」と言い切ることができます。
After
After は「あるタイムスタンプが現在時刻より後のものか」を判定するコマンドです。
$ cargo run --example after /run/clockboundd/clockboundd.sock
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/examples/after /run/clockboundd/clockboundd.sock`
1636892672641514016 nanoseconds since the Unix Epoch is after the current time's error bounds.
Waiting 2 seconds...
1636892672641514016 nanoseconds since the Unix Epoch is not after the current time's error bounds.
こちらもソースコードのコメントとあわせて見ると理解しやすいと思います。
- Rust の時間関数を使って現在時刻を取得。 現在時刻+1秒を
timestamp
変数に代入 timestamp
を ClockBoundD の After コマンドを使って判定- 2秒待つ
timestamp
を ClockBoundD の After コマンドを使って判定
Go版クライアントを作ってみた
これだけだと少しさみしいかなと思って、ClockBoundC の Go ポートを作ってみました。
サンプルとして Now コマンドを発行する go-clockboundc-now
コマンドを作っておきました。
以下のようにして ClockBoundD から時刻情報を取得することができます。
$ go install github.com/shogo82148/go-clockboundc/cmd/go-clockboundc-now
$ go-clockboundc-now
2021/11/27 10:43:52 Synchronized
2021/11/27 10:43:52 Current: 2021-11-27 10:43:52.95958806 +0000 UTC
2021/11/27 10:43:52 Earliest: 2021-11-27 10:43:52.959488228 +0000 UTC
2021/11/27 10:43:52 Latest: 2021-11-27 10:43:52.959687892 +0000 UTC
2021/11/27 10:43:52 Range: 199.664µs
プロトコル自体は非常に簡単なので、他の言語でも容易に実装できると思います。
まとめ
Googleの TrueTime のAWS版とも言える ClockBound を試してみました。
実際のところ ClockBound 単体ではあまり役に立ちません。 この記事を読んで興味を持った方は ClockBound の応用にチャレンジしてみてはいかがでしょうか?
明日12日は @seto_inugamiで「技術選定を疎かにしたツケを払ったお話し」です。お楽しみに!
参考
- フラー株式会社 Advent Calendar 2021
- Amazon Time Sync Service now makes it easier to generate and compare timestamps
- aws/clock-bound
- Cloud Spanner: TrueTime and external consistency
- Manage Amazon EC2 instance clock accuracy using Amazon Time Sync Service and Amazon CloudWatch – Part 1
- Manage Amazon EC2 instance clock accuracy using Amazon Time Sync Service and Amazon CloudWatch – Part 2
- ClockBound Protocol Version 1
- shogo82148/go-clockboundc