
凌晨 2:10,隔着玻璃还能听到隔壁直播间回放的欢呼。第二天是季后赛,赛控同事给我发来一张图:上海房、台北房、新加坡房的比赛事件时间戳出现 6–12ms 的错位。别看只有几毫秒,落到我们事件流(Kafka → 匹配引擎 → 结算)的窗口上,同一击爆头在不同分区有可能被「重放」或「错序」。——时间,不准,比赛就不公平。
这活儿落到我头上:今晚把香港主场的时间系统重新拉直,天亮前得把延迟抖动和偏移压进我们的阈值线里(跨机房 ≤2ms、同机房 ≤200µs)。我知道,这次必须把 Debian + NTP 集群做成“拎起来就能跑”的标准件。
目标与约束(TL;DR)
- 业务目标:跨区域事件时间一致性(东南亚/华南 → 香港主场),核心链路时间偏移 ≤2ms,单机房内部 ≤200µs。
- 基础系统:Debian 12 (bookworm),全部裸金属,K8s 只跑业务,不跑时间守护。
- 时间协议:NTPv4(Chrony)为主,外部上游支持 NTS(Network Time Security);机房内可选 **PPS(GPS 斗篷)**或 PTP 优化。
- 抗故障:3 台 NTP 服务器(2 台 HK + 1 台 SG 备援),DNS 健康探测轮询(或 BGP Anycast),无单点。
- 安全:仅放行业务网段,关闭远程命令端口,限速防反射;上游尽量用 NTS/TLS。
拓扑与规划
逻辑拓扑
[上游权威时间源]
├─ NTS: time.cloudflare.com, time.google.com
├─ 公共 stratum-1/2(HK/SG/JP)
└─(可选)本地 GPSDO + PPS (stratum-1)
┌───────────────────────────────┐
│ 香港主机房 (HK1) │
│ ntp1.hk ──┐ │
│ ntp2.hk ──┼─(DNS轮询/Anycast)─▶ 客户端节点(游戏服/Kafka/MySQL/Redis/网关)
│ (可选)gps1.hk ─┘ │
└───────────────────────────────┘
│
│ MPLS/专线/公网冗余
▼
┌───────────────────────────────┐
│ 新加坡备援 (SG1) │
│ ntp1.sg │
└───────────────────────────────┘
IP & 域名
| 角色 | 主机名 | 位置 | IP | 备注 |
|---|---|---|---|---|
| HK NTP #1 | ntp1.hk.game.local | HK1 | 10.66.10.11 | 生产主 |
| HK NTP #2 | ntp2.hk.game.local | HK1 | 10.66.10.12 | 生产备 |
| SG NTP #1 | ntp1.sg.game.local | SG1 | 10.88.10.11 | 区域备援 |
| 统一入口 | ntp.game.local | GSLB/DNS | RR 指向 ntp1/ntp2/ntp1.sg | 健康探测 |
若网络团队支持 BGP Anycast,也可以给 ntp.game.local 上一个 /32 Anycast 地址,由三台分别宣告(最佳实践)。没有 BGP 的情况下,DNS 健康探测 + 短 TTL(30–60s) 就很实用。
硬件基线(能省就省,能硬就硬)
| 组件 | 最低推荐 | 我们现场配置 | 理由 |
|---|---|---|---|
| 服务器 | 1U / x86_64 | 2× Intel Xeon Silver 4310 / 64GB | NTP本身吃不了多少 CPU,但需要稳定 TSC |
| 存储 | SSD | 2× NVMe(RAID1) | 日志/内核/工具,不要给抖动找借口 |
| 网卡 | Intel X710/XL710 或同级 | 2× 10GbE | 低 jitter,支持 PTP/PHC(可选) |
| 时源(可选) | GPSDO + PPS | u-blox + PPS 引出 + GPS 天线 | 架设本地 stratum-1,把外部依赖降到最低 |
| 交换机(可选) | PTP aware | Arista/Juniper 同级 | 如果要玩 µs 级,就加 PTP |
没 GPS/PPS 一样能上线;只是室外屋顶有位点的话,强烈建议做一台本地 stratum-1,比赛心态会稳很多。
Debian 系统基线与“提神剂”
# 1) 固化时钟源(通常 TSC 即可)
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
# 若不是 tsc,可手动指定(重启生效)
sudo sed -i 's/^GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="clocksource=tsc tsc=reliable '/ /etc/default/grub
sudo update-grub
# 2) 禁用系统默认的 timesyncd,改用 chrony
sudo systemctl disable --now systemd-timesyncd.service
sudo apt-get update && sudo apt-get install -y chrony
# 3) 内核与电源管理(减少抖动)
sudo sed -i 's/^GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="intel_pstate=disable pcie_aspm=off idle=nomwait '/ /etc/default/grub
sudo update-grub
# 4) BIOS/UEFI 建议:关 C-States 深省电、开 HPET/HPET 与 TSC 以厂商建议为准
TSC 稳定性是关键。异构 CPU/混插/超频都可能导致时钟漂移。生产环境里,统一代际 CPU、统一 BIOS 模板,是“低成本高回报”的做法。
安装与配置 Chrony(NTP 服务器侧)
上游时间源策略
- 以 NTS(TLS) 的权威时间源为主:time.cloudflare.com、time.google.com。
- 补充 2–3 个 HK/SG/JP 的公共/学术 stratum-1/2(无 NTS 也可),用于多样性与健壮性。
- 若有 本地 GPS/PPS,把它设为 prefer,其余为 cross-check。
/etc/chrony/chrony.conf(HK NTP #1/2)
# 基本
driftfile /var/lib/chrony/chrony.drift
rtcsync
makestep 0.5 5
leapsectz right/UTC
logdir /var/log/chrony
# NTS 上游(优先)
server time.cloudflare.com iburst nts
server time.google.com iburst nts
# 区域上游(非 NTS,选本地低延迟)
pool ntp1.hkix.net iburst maxsources 2
pool ntp.sg.pool.ntp.org iburst maxsources 2
pool ntp.jp.net iburst maxsources 2
# 若有本地 GPS/PPS(见后文)
# refclock SOCK /run/chrony.ttyS0.sock refid GPS
# refclock PPS /dev/pps0 lock GPS refid PPS prefer
# 只对内服务
bindaddress 0.0.0.0
port 123
allow 10.66.0.0/16
allow 172.20.0.0/16
# 关闭远程命令端口(只允许本机 socket)
cmdport 0
# 记录异常偏移
logchange 0.5
# NTS 缓存目录
ntsdumpdir /var/lib/chrony
makestep 0.5 5 表示:启动初期(前 5 次更新)若偏差 >0.5s 允许“跳变”校准,随后只“微调”不跳变,避免业务抖动。
启用并检查:
sudo systemctl enable --now chrony
chronyc -n tracking
chronyc -n sources -v
chronyc -n sourcestats -v
防火墙(nftables 举例)
cat >/etc/nftables.d/ntp.nft <<'EOF'
table inet filter {
chain input {
type filter hook input priority 0;
udp dport 123 ip saddr {10.66.0.0/16, 172.20.0.0/16} accept
udp dport 123 drop
}
}
EOF
nft -f /etc/nftables.d/ntp.nft
Chrony 天生不支持“monlist”,被 DDoS 的概率比老 ntpd 低,但源地址放行 + 限速依然必要。
可选:做一台本地 stratum-1(GPS + PPS)
硬件接法:GPS 天线 → 接收机 → 服务器串口(NMEA)+ PPS 引脚(/dev/pps0)。
软件安装:
sudo apt-get install -y gpsd gpsd-clients pps-tools
sudo usermod -aG dialout,tty _chrony
# gpsd 配置
sudo tee /etc/default/gpsd <<'EOF'
START_DAEMON="true"
DEVICES="/dev/ttyS0"
GPSD_OPTIONS="-n"
USBAUTO="false"
EOF
sudo systemctl enable --now gpsd
# 验证
cgps -s # 看经纬度/卫星锁定
ppstest /dev/pps0 # 看 PPS 脉冲
让 chrony 吃到 GPS 与 PPS:
# /etc/chrony/chrony.conf 里追加
# NMEA 走 gpsd 的 unix socket(更稳,不走 SHM)
refclock SOCK /run/chrony.ttyS0.sock refid GPS poll 4
refclock PPS /dev/pps0 lock GPS refid PPS prefer
正常时,PPS 的偏移可做到 ±10–30µs 量级。我们比赛前夜测的是 rms ~17µs。
NTP 服务器之间的“相互看护”(可选认证)
如果你想让 HK 两台互相 peer 并进行 对等认证:
/etc/chrony/chrony.keys:
10 SHA256 2c3f7f6f9c...(64位十六进制)
双方 chrony.conf 增加:
server ntp2.hk.game.local iburst xleave key 10
# 对侧写:server ntp1.hk.game.local iburst xleave key 10
keyfile /etc/chrony/chrony.keys
xleave 可以补偿对端处理延时,提升精度。
客户端节点(游戏服 / Kafka / MySQL / Redis)
安装与配置(Debian)
sudo systemctl disable --now systemd-timesyncd
sudo apt-get install -y chrony
sudo tee /etc/chrony/chrony.conf >/dev/null <<'EOF'
driftfile /var/lib/chrony/chrony.drift
rtcsync
makestep 0.1 3
leapsectz right/UTC
logdir /var/log/chrony
# 只连内网 NTP 入口(DNS 会健康探测切换)
server ntp.game.local iburst maxsources 3
# 若是跨 AZ/Region 的节点,再加一条就近的二级源以加速收敛
# server ntp1.sg.game.local iburst
# 关闭远程命令端口
cmdport 0
EOF
sudo systemctl enable --now chrony
chronyc tracking
K8s 场景的注意点
- 不要在容器里跑 chrony。时间统一交给 宿主机。
- 业务容器若要观测,可给它一个只读的 chronyc 工具,通过宿主机的 Unix socket 或只读 metrics。
- 通过 DaemonSet 部署 node-exporter + textfile,定时 chronyc -n tracking 写入 /var/lib/node_exporter/textfile_collector/chrony.prom。
示例采集脚本 /usr/local/bin/chrony_export.sh:
#!/usr/bin/env bash
set -euo pipefail
TMP=$(mktemp)
chronyc -n tracking | awk '
$1=="Reference" {ref=$3}
$1=="Stratum" {print "chrony_stratum " $2}
$1=="Last" && $2=="offset" {print "chrony_last_offset_seconds " $4}
$1=="RMS" && $2=="offset" {print "chrony_rms_offset_seconds " $4}
$1=="Frequency" {print "chrony_freq_ppm " $3}
$1=="Residual" && $2=="freq" {print "chrony_residual_ppm " $4}
$1=="Root" && $2=="delay" {print "chrony_root_delay_seconds " $4}
$1=="Root" && $2=="dispersion" {print "chrony_root_dispersion_seconds " $4}
END { }
' > "$TMP"
mv "$TMP" /var/lib/node_exporter/textfile_collector/chrony.prom
发布入口:DNS 健康探测(或 Anycast)
DNS 轮询 + 健康探测
- GSLB/Consul/自建探测脚本定期执行 chronyc -h ntpX -n tracking,若 Reference ID=?.INIT. 或 Reach < 377、Root dispersion > 5ms 则下线该记录。
- 入口 ntp.game.local TTL 30–60s。
BGP Anycast(进阶)
- 三台 NTP 机各自对外宣告同一 /32 Anycast IP。
- 失效即撤销路由,收敛时间 <5s。
- 与网络组配合做 RTBH 防御反射攻击。
我们上线时的监控阈值
| 指标 | 阈值 | 说明 |
|---|---|---|
| chrony_root_dispersion_seconds | < 0.005 |
根离散度 <5ms |
| chrony_rms_offset_seconds | < 0.0002 |
同机房 RMS 偏移 <200µs |
| chrony_last_offset_seconds | abs() < 0.001 |
单点瞬时偏移 <1ms(跨区) |
| Reach(八进制) | >= 377 |
与上游/对等健康通信 |
| Stratum | 合理(1–3) | 异常跳到 16 立刻告警 |
Grafana 面板建议把 HK1/HK2/SG1 的 last_offset、rms_offset 放在同一图里看“扇形”散布;比赛窗口要尽量贴合 0 轴。
验证与演练(我们当晚真实做了什么)
- 基线对齐:所有游戏服、DB、Redis、Kafka、匹配引擎节点统一 ntp.game.local;清查残留 chronyd/timesyncd 重复进程。
- 强制偏移演练:在一台 HK 游戏服上手动改系统时间 +800ms,确认 makestep 在冷启动期快速拉直(重启 chrony 后 <10s 收敛),避免业务层“跳变”影响。
- 上游断链演练:临时阻断 time.cloudflare.com,观察 HK1/HK2 自动切上 HKIX/SG 上游,RMS 抖动维持在 120–180µs。
- 单点失效:下线 ntp2.hk,验证 DNS 健康探测 30s 内剔除,客户端 chronyc sources 只剩 HK1/SG1。
- 带宽/丢包注入:在测试交换机端口注入 0.1% 丢包,RMS 偏移升至 ~350µs,仍在阈值内;>1% 时告警触发(预案启动:切换到 PTP 内部基线)。
现场数据(上线后一小时的一个快照)
| 节点 | Stratum | Last offset | RMS offset | Root dispersion | Reach |
|---|---|---|---|---|---|
| ntp1.hk | 1(PPS) | +7.2µs | 16.8µs | 1.2ms | 377 |
| ntp2.hk | 2 | +23.5µs | 41.3µs | 2.7ms | 377 |
| ntp1.sg | 2 | −0.62ms | 0.91ms | 3.8ms | 377 |
| game-01.hk | 2 | +0.12ms | 0.18ms | 4.1ms | 377 |
| kafka-02.hk | 2 | −0.08ms | 0.15ms | 3.9ms | 377 |
| mysql-01.hk | 2 | +0.10ms | 0.19ms | 4.0ms | 377 |
这组数配合我们业务的事件窗口(5ms)完全足够。
业务层“最后一厘米”的建议
- 用单调时钟:服务端排序/耗时统计务必用 CLOCK_MONOTONIC,不要用 CLOCK_REALTIME。
- 数据库层:MySQL/PG 的 NOW() 非壁钟真值,关键业务请由应用层注入时间戳(从本机校时后的单调时钟推导)。
- 日志:同时打印 单调时间与 UTC 壁钟(便于跨机房排错)。
- 窗口设计:窗口边界容忍偏移(±2ms)与重放去重 token,一旦 Root dispersion 超阈值,业务降级到更保守的窗口。
常见坑 & 解决
虚拟化/容器里跑 chrony
坑:容器时钟被宿主机托管,自己跑 NTP 只会制造冲突。
解:统一宿主机时间;容器只读观测。
CPU 深省电(C-states)引发抖动
坑:延迟分析“像毛毛雨”。
解:关深 C-states,固定频率,intel_pstate=disable + BIOS 模板。
时钟源不一致(TSC vs HPET)
坑:同批机器偏移曲线不同调。
解:统一 clocksource=tsc,确认 tsc=reliable,并锁 BIOS 版本。
多套时间服务并存
坑:timesyncd、chronyd、ntpd 混装,谁都以为自己是爹。
解:定标准:全平台只允许 chrony,CI/CD 里做合规扫描。
公共上游被墙/抖
坑:跨境时延随机炸。
解:优先 NTS + 区域就近;自建 GPS/PPS 做 stratum-1 当底座。
NTP 反射攻击
坑:被白打。
解:仅放内网;cmdport 0;nftables 白名单 + 限速;BGP RTBH 预案。
日志时间“回拨”导致乱序
坑:排障时线索断裂。
解:makestep 只放冷启动期;业务侧统一用 单调时钟做关键排序。
附:快速巡检脚本(一键扫全场)
#!/usr/bin/env bash
# check-ntp-fleet.sh
set -euo pipefail
IPS=$(cat <<EOF
game-01.hk
game-02.hk
kafka-01.hk
kafka-02.hk
mysql-01.hk
redis-01.hk
EOF
)
for h in $IPS; do
echo "== $h =="
ssh -o ConnectTimeout=2 $h "chronyc -n tracking | egrep 'Reference|Stratum|Last offset|RMS offset|Root dispersion'; chronyc -n sources | tail -n +3"
echo
done
早上 9:59,我和赛控一起盯着大屏。三条偏移曲线几乎贴着零线,那种“心很稳”的感觉特别真切。解说开口、倒计时响起,第一局的第一声枪响后,事件流像一把拉直的弦,干净地滑过了所有机房。这是一场看不见的比赛,但我知道我们赢了——因为 时间是在我们这边。
摘要版操作清单(可直接照做)
- 裸金属 Debian 12,统一 BIOS 模板,clocksource=tsc tsc=reliable。
- 全面禁用 systemd-timesyncd,全平台上 chrony。
- 3 台 NTP(HK×2 + SG×1);上游优先 NTS(Cloudflare/Google),区域源补位;有条件加 GPS/PPS。
- 服务器侧 allow 只放内网,cmdport 0,nft 白名单。
- 入口用 DNS 健康探测(或 BGP Anycast);TTL 30–60s。
- 监控 rms/last offset、root dispersion、reach,阈值:200µs/1ms/5ms/377。
- 业务侧一律用 单调时钟,窗口容错(±2ms),日志打印双时间。
- 上线前完成偏移/断链/单点失效演练。
如果你也要在香港为国内与东南亚玩家撑起一场“看不见的比赛”,这套基线把时间这件事,交给你可控的工程系统。剩下的,就交给选手的操作吧。