systemd-timesyncd 是一个精简 SNTP 客户端,用配置的时间源同步本地系统时钟。它可以帮助证书检查与墙钟时间戳保持有意义,但不会无条件保证某个精度范围。守护进程能否运行、是否收到可接受样本,以及系统是否报告同步,是需要分别确认的事实。只看到安装了客户端,或者某条命令成功返回,都不足以推导时钟已经准确。

坏掉的样子

守护进程并不总是平滑校正。文档明确说明,大偏差会直接跳时,小偏差则逐渐调整。因此,开机时离正确时间很远的机器,可能发生时间不连续。经过时长测量应使用单调时钟,解释墙钟时间戳的应用则应处理校时,而不是假定时间永远平滑前进。不能把“精简客户端”误写成“绝不会让应用看见时间跳跃”。

FallbackNTP 不是显式服务器不可达时就一定会启用的通用应急列表,它在没有其他已知服务器信息时才适用。全局 NTP 条目和 networkd 提供的链路时间源,优先于这个后备设置。因此,应检查实际来源选择与配置覆盖文件,不能仅因为主来源连接失败,就断定某个写在 fallback 中的服务器必定已经被尝试。

未同步也不能证明 UDP 流量被阻止。服务可能缺失、未启用、被单元条件排除、没有调整时钟的权限,或者正在拒绝不合适样本。反过来,全局同步属性也不能单独说明哪个进程提供了时间。应区分管理器看到的服务状态、客户端观察到的样本,以及内核层同步指示,避免把来自不同层面的一个布尔值当成完整故障结论。

快速验证

这个失败模式实验在一次性 Debian 12 Docker 容器中以 root 运行,PID 1 为 systemd 252.39。初始化进程通过 /usr/bin/setpriv --bounding-set=-sys_time,-sys_module /sbin/init 启动,移除相关能力以防止调整 Docker 虚拟机共享时钟。脚本在请求任何服务启动前检查该前提,并要求安装包中的容器排除条件存在:

from pathlib import Path
import json,subprocess

def run(*args):
    result=subprocess.run(args,text=True,capture_output=True,timeout=10)
    assert result.returncode==0,(args,result.stderr)
    return result.stdout.strip()
def properties(unit):
    output=run('systemctl','show',unit,'-p','ActiveState','-p','SubState','-p','MainPID','-p','ConditionResult')
    return dict(line.split('=',1) for line in output.splitlines())
# This failure-mode exercise requires an isolated container that cannot set time.
status=dict(line.split(':',1) for line in Path('/proc/1/status').read_text().splitlines() if ':' in line)
assert int(status['CapBnd'].strip(),16)&(1<<25)==0,'PID 1 must lack CAP_SYS_TIME'
assert run('systemd-detect-virt','--container')=='docker'
unit='systemd-timesyncd.service';definition=run('systemctl','cat',unit)
assert 'ConditionCapability=CAP_SYS_TIME' in definition
assert 'ConditionVirtualization=!container' in definition
assert properties(unit)['ActiveState']=='inactive'
run('systemctl','start',unit)
state=properties(unit)
assert state=={'MainPID':'0','ActiveState':'inactive','SubState':'dead','ConditionResult':'no'},state
clock=dict(line.split('=',1) for line in run('timedatectl','show','-p','CanNTP','-p','NTP','-p','NTPSynchronized').splitlines())
assert all(value in ('yes','no') for value in clock.values())
print('start command returned zero; unit stayed inactive with PID 0 and failed conditions')
print('clock properties:',json.dumps(clock,sort_keys=True))
print('PID 1 lacks time-setting capability; Docker condition also excludes this packaged service')
print('no clock-setting command was issued and no NTP synchronization was claimed')

启动命令返回零,但单元仍为非活动,主进程编号为零,ConditionResult=no。条件不满足而跳过激活,与守护进程成功运行,是不同结果。样本安装包要求调整时间的能力,并排除容器,而当前环境两项前提都不满足。因此在假设 NTP 报文被网络丢弃之前,就已经有具体证据解释服务为何没有工作。

记录中的时钟属性为 CanNTP=yes、NTP=no、NTPSynchronized=no,分别反映时间服务接口的不同方面。尤其是系统具有 NTP 服务支持,不代表该服务当前活动,也不证明时钟已经同步。脚本输出真实取值,不会把结果替换成预设健康状态;在另一环境中判断时,也应核对当时实际观察,而不是照抄本例的布尔值。

实验没有发出设置时钟的命令,也不声称成功交换 NTP 报文或完成校时。它验证的是一个没有调时权限的容器中的真实激活失败模式。要测试成功校时,需要具备相应权限、可以访问合适时间源的机器,或拥有独立时钟的虚拟机;不能让宿主共享时钟承担一个没有隔离边界的试验,也不能把未运行的客户端写成同步成功。

守护进程真实运行时,可以通过 timedatectl timesync-status 查看客户端自己的服务器与同步信息。但依赖这个接口之前,应先核对服务状态;请求不可用服务时,可能等待 D-Bus 激活并最终超时。本次容器探测达到五秒观察上限,结果被记为没有获得证据,而不是成功同步检查。接口存在、调用返回与内容证明什么,也需要分别判断。

建立能够验证的时间要求

按网络环境选择时间源,并持续观察偏移、延迟与所接受来源的质量。能够连接,并不让服务器自动准确或可信。轮询和根距离设置影响样本处理,但不能仅靠几个配置就把精简客户端变成完整时间分发服务,也不会自动形成某个精度服务承诺。需要什么精度、怎样观察偏差,应由实际应用要求决定。

保存的 /var/lib/systemd/timesync/clock 时间戳,有助于缺少可靠硬件时钟的系统在重启时初始化时间。但它也可能在近期没有成功同步时被周期更新,因此文件存在不证明刚刚接受了网络样本。大致初始化时间与同步到参考时钟,是两个不同里程碑,不应在开机就绪判断中混为一谈。

如果后续工作必须等到时钟同步,应配置并验证相应等待服务,以及排在 time-sync.target 之后的关系。仅启动 timesyncd,不会自动阻止所有应用在准确参考时间到达前运行。应明确启动条件和运行期监控,并以真实部署环境中的证据为准,而不是依赖一个笼统的“NTP 已开启”标签来判断所有时间相关要求已经满足。

延伸阅读

来源