香港机房的看不见的比赛:用 Debian+Chrony/NTS 构建 NTP 集群,守住国内与东南亚电竞赛况一致
技术教程 2025-09-20 10:32 208


凌晨 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,我和赛控一起盯着大屏。三条偏移曲线几乎贴着零线,那种“心很稳”的感觉特别真切。解说开口、倒计时响起,第一局的第一声枪响后,事件流像一把拉直的弦,干净地滑过了所有机房。这是一场看不见的比赛,但我知道我们赢了——因为 时间是在我们这边。

摘要版操作清单(可直接照做)

  1. 裸金属 Debian 12,统一 BIOS 模板,clocksource=tsc tsc=reliable。
  2. 全面禁用 systemd-timesyncd,全平台上 chrony。
  3. 3 台 NTP(HK×2 + SG×1);上游优先 NTS(Cloudflare/Google),区域源补位;有条件加 GPS/PPS。
  4. 服务器侧 allow 只放内网,cmdport 0,nft 白名单。
  5. 入口用 DNS 健康探测(或 BGP Anycast);TTL 30–60s。
  6. 监控 rms/last offset、root dispersion、reach,阈值:200µs/1ms/5ms/377。
  7. 业务侧一律用 单调时钟,窗口容错(±2ms),日志打印双时间。
  8. 上线前完成偏移/断链/单点失效演练。

如果你也要在香港为国内与东南亚玩家撑起一场“看不见的比赛”,这套基线把时间这件事,交给你可控的工程系统。剩下的,就交给选手的操作吧。