工作原理
每台机器都有随温度和老化漂移的石英振荡器。NTP 或 PTP 定期估计偏移,并平滑或步进调整时钟。虚拟机会暂停,恢复后时钟可能向前跳。即使管理良好的集群也会有毫秒级偏移,配置错误的主机可能偏差数秒甚至数分钟。
墙上时钟回答“这里现在几点”,而不是“跨机器哪个事件先发生”。单调时钟不受跳变影响,适合测量单机时长,但其纪元任意,不能跨主机比较。逻辑时钟,例如 Lamport 时间戳和向量时钟,不依赖物理时间即可捕获因果关系。
危害
最后写入者获胜的冲突解决使用时间戳,偏移可能丢弃更新的写入。基于租约的锁假设持有者的时钟会在另一节点接管前到期,偏移可能让两个持有者同时有效。JWT 校验、缓存 TTL 和限流窗口也依赖时间。多主机日志合并后可能顺序错乱,增加事故分析难度。
缓解
保持 NTP 健康并监控偏移。不要把墙上时钟用于正确性排序;使用版本号、向量时钟或集中式定序器。对租约使用栅栏令牌,使过期持有者无法提交,并加入大于最大预期偏移的安全余量。需要严格界限的系统可以使用带不确定性区间的专用时钟,并在提交前等待不确定性过去。
权衡与示例
逻辑时钟捕获因果,但随参与者数量增长并需要裁剪。集中式时间戳简化排序,却增加依赖和延迟。有界不确定性时钟需要基础设施并引入等待。
示例:两个副本更新同一条记录。副本 A 的时钟快 300 毫秒,因此它较旧的写入携带更晚的时间戳,在最后写入者获胜策略下胜出,静默丢失了更新的值。版本向量或服务器分配的序列号可以保留两次更新,并暴露冲突而不是掩盖它。
代码示例
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
struct FakeClock {
millis: Arc<AtomicU64>,
}
impl FakeClock {
fn new() -> Self {
Self {
millis: Arc::new(AtomicU64::new(0)),
}
}
fn now(&self) -> Duration {
Duration::from_millis(self.millis.load(Ordering::SeqCst))
}
fn advance(&self, delta: Duration) {
self.millis
.fetch_add(delta.as_millis() as u64, Ordering::SeqCst);
}
}
struct Token {
expires_at: Duration,
}
impl Token {
fn issue(clock: &FakeClock, ttl: Duration) -> Self {
Self {
expires_at: clock.now() + ttl,
}
}
fn expired(&self, clock: &FakeClock) -> bool {
clock.now() >= self.expires_at
}
}
fn main() {
let clock = FakeClock::new();
let token = Token::issue(&clock, Duration::from_secs(60));
clock.advance(Duration::from_secs(59));
println!("expired at 59s: {}", token.expired(&clock));
clock.advance(Duration::from_secs(1));
println!("expired at 60s: {}", token.expired(&clock));
}
package main
import (
"fmt"
"sort"
"sync"
"time"
)
type SlidingWindowLimiter struct {
mu sync.Mutex
limit int
window time.Duration
now func() time.Time
events []time.Time
}
func NewSlidingWindowLimiter(limit int, window time.Duration, now func() time.Time) *SlidingWindowLimiter {
if limit <= 0 || window <= 0 {
panic("limit and window must be positive")
}
if now == nil {
now = time.Now
}
return &SlidingWindowLimiter{limit: limit, window: window, now: now}
}
func (l *SlidingWindowLimiter) prune(current time.Time) []time.Time {
cutoff := current.Add(-l.window)
first := sort.Search(len(l.events), func(i int) bool {
return l.events[i].After(cutoff)
})
l.events = append(l.events[:0], l.events[first:]...)
return l.events
}
func (l *SlidingWindowLimiter) Allow() bool {
l.mu.Lock()
defer l.mu.Unlock()
current := l.now()
active := l.prune(current)
if len(active) >= l.limit {
return false
}
l.events = append(active, current)
return true
}
func (l *SlidingWindowLimiter) Remaining() int {
l.mu.Lock()
defer l.mu.Unlock()
active := l.prune(l.now())
if remaining := l.limit - len(active); remaining > 0 {
return remaining
}
return 0
}
func main() {
clock := time.Date(2026, 10, 21, 12, 0, 0, 0, time.UTC)
now := func() time.Time { return clock }
limiter := NewSlidingWindowLimiter(3, time.Second, now)
for i := 0; i < 4; i++ {
clock = clock.Add(100 * time.Millisecond)
fmt.Printf("t=%dms allow=%v remaining=%d\n", (i+1)*100, limiter.Allow(), limiter.Remaining())
}
clock = clock.Add(700 * time.Millisecond)
fmt.Println("after window slides:", limiter.Allow())
}