Windows Server 2012 搭建内网 NTP 服务器并进行离线时钟漂移补偿
在隔离或半隔离内网中,可以使用 Windows Server 2012 自带的 W32Time 作为统一 NTP 时间服务器:
上级 NTP(可选)
│
▼
Windows Server 2012
│
├─ Windows 客户端
├─ Linux / Chrony
└─ 网络设备
如果服务器完全无法连接外部时间源,也可以使用本机时钟向内网授时,再通过本文后面的脚本补偿服务器自身稳定的时钟漂移。
一、启用 Windows Time 服务
使用管理员 CMD:
sc config w32time start= auto
net start w32time
启用 NTP Server:
reg add "HKLM\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer" /v Enabled /t REG_DWORD /d 1 /f
将本机设置为可靠时间源:
w32tm /config /reliable:yes /update
重启服务:
net stop w32time
net start w32time
二、配置上级 NTP 服务器
如果服务器能够访问其他标准 NTP 时间源,建议让 Server 2012 先同步上级服务器,再向内网提供时间。
例如:
w32tm /config /manualpeerlist:"<上级NTP服务器>,0x8" /syncfromflags:manual /update
net stop w32time
net start w32time
w32tm /resync /rediscover
检查:
w32tm /query /source
w32tm /query /status
w32tm /query /peers
如果是完全离线内网,没有任何上游 NTP,则可以不配置 manualpeerlist,将 Server 2012 本机时间作为整个内网的基准。
三、调整本地时钟可信度
为了让 Linux Chrony 等客户端能够正常接受 Windows 提供的 NTP 时间,可以设置:
w32tm /config /localclockdispersion:1 /update
或者直接修改注册表:
reg add "HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config" /v LocalClockDispersion /t REG_DWORD /d 1 /f
然后:
net stop w32time
net start w32time
检查:
w32tm /query /configuration
应能看到:
LocalClockDispersion: 1
LocalClockDispersion只是服务器对本地时钟误差的声明,并不会真正提高硬件时钟精度。
四、开放 NTP 服务端口
NTP 使用:
UDP 123
确认系统已经监听:
netstat -ano -p udp | findstr ":123"
同时确保 Windows 防火墙以及内网 ACL 允许客户端访问服务器的 UDP 123。
五、Windows 客户端配置
在客户端管理员 CMD 中:
w32tm /config /manualpeerlist:"<内网NTP服务器>,0x8" /syncfromflags:manual /update
net stop w32time
net start w32time
w32tm /resync
检查:
w32tm /query /source
w32tm /query /status
也可以先测试服务器:
w32tm /stripchart /computer:<内网NTP服务器> /dataonly /samples:10
六、Linux Chrony 客户端配置
编辑:
/etc/chrony.conf
加入:
server <内网NTP服务器> iburst
重启:
systemctl restart chronyd
检查:
chronyc sources -v
chronyc tracking
正常情况下,当前使用的时间源前面会显示:
^*
例如:
^* ntp-server
需要立即修正较大的时间偏差时:
chronyc makestep
七、完全离线环境中的服务器时钟漂移
如果 Server 2012 没有任何上级 NTP、GPS 或北斗授时源,它无法自己知道真正的标准时间。
例如经过多日测量发现服务器:
每天稳定慢约 5 秒
对应频率误差约:
5 / 86400 ≈ 57.9 ppm
可以通过 Windows 的 SetSystemTimeAdjustment() API,让系统时钟持续稍微走快一些,而不是每天突然把时间增加 5 秒。
例如某台机器检测到:
TimeIncrement = 156250
补偿每天约 5 秒时:
TimeAdjustment = 156259
实际补偿约:
+4.9766 秒/天
下面的脚本会自动读取实际 TimeIncrement 并计算,不需要手工填写 156259。
八、完整时钟漂移补偿脚本
保存为:
C:\ClockRateAdjust.ps1
内容:
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class ClockAdjust
{
[StructLayout(LayoutKind.Sequential)]
public struct LUID
{
public uint LowPart;
public int HighPart;
}
[StructLayout(LayoutKind.Sequential)]
public struct TOKEN_PRIVILEGES
{
public uint PrivilegeCount;
public LUID Luid;
public uint Attributes;
}
public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
public const uint TOKEN_QUERY = 0x0008;
public const uint SE_PRIVILEGE_ENABLED = 0x00000002;
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool OpenProcessToken(
IntPtr ProcessHandle,
uint DesiredAccess,
out IntPtr TokenHandle);
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern bool LookupPrivilegeValue(
string lpSystemName,
string lpName,
out LUID lpLuid);
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle,
bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
uint BufferLength,
IntPtr PreviousState,
IntPtr ReturnLength);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool GetSystemTimeAdjustment(
out uint lpTimeAdjustment,
out uint lpTimeIncrement,
out bool lpTimeAdjustmentDisabled);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool SetSystemTimeAdjustment(
uint dwTimeAdjustment,
bool bTimeAdjustmentDisabled);
public static bool EnableSystemTimePrivilege()
{
IntPtr token;
if (!OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
out token))
return false;
try
{
LUID luid;
if (!LookupPrivilegeValue(
null,
"SeSystemtimePrivilege",
out luid))
return false;
TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES();
tp.PrivilegeCount = 1;
tp.Luid = luid;
tp.Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(
token,
false,
ref tp,
0,
IntPtr.Zero,
IntPtr.Zero))
return false;
return Marshal.GetLastWin32Error() == 0;
}
finally
{
CloseHandle(token);
}
}
}
"@
# ==============================
# 漂移补偿参数
# ==============================
# 正数:本机每天慢多少秒
# 例如每天慢5秒填写5.0
#
# 负数:本机每天快多少秒
# 例如每天快3秒填写-3.0
$DriftSecondsPerDay = 5.0
# ==============================
# 启用系统时间调整权限
# ==============================
if (-not [ClockAdjust]::EnableSystemTimePrivilege()) {
throw "Cannot enable SeSystemtimePrivilege. Run as Administrator."
}
Write-Host "SeSystemtimePrivilege enabled."
# ==============================
# 获取当前时钟参数
# ==============================
[uint32]$CurrentAdjustment = 0
[uint32]$TimeIncrement = 0
[bool]$AdjustmentDisabled = $false
if (-not [ClockAdjust]::GetSystemTimeAdjustment(
[ref]$CurrentAdjustment,
[ref]$TimeIncrement,
[ref]$AdjustmentDisabled)) {
throw "GetSystemTimeAdjustment failed."
}
Write-Host ""
Write-Host "Current TimeAdjustment : $CurrentAdjustment"
Write-Host "TimeIncrement : $TimeIncrement"
Write-Host "AdjustmentDisabled : $AdjustmentDisabled"
# ==============================
# 计算新的时钟频率
# ==============================
$Ratio = $DriftSecondsPerDay / 86400.0
[uint32]$NewAdjustment =
[Math]::Round(
$TimeIncrement * (1.0 + $Ratio)
)
$Actual =
(($NewAdjustment / [double]$TimeIncrement) - 1.0) * 86400.0
Write-Host ""
Write-Host "Requested correction : $DriftSecondsPerDay sec/day"
Write-Host "New TimeAdjustment : $NewAdjustment"
Write-Host "Actual correction : $([Math]::Round($Actual,4)) sec/day"
# ==============================
# 应用时钟频率补偿
# ==============================
if (-not [ClockAdjust]::SetSystemTimeAdjustment(
$NewAdjustment,
$false)) {
$err =
[Runtime.InteropServices.Marshal]::GetLastWin32Error()
throw "SetSystemTimeAdjustment failed. Win32 error: $err"
}
Write-Host ""
Write-Host "SUCCESS - Clock rate adjustment applied."
管理员 PowerShell 或 CMD 执行:
powershell.exe -ExecutionPolicy Bypass -File C:\ClockRateAdjust.ps1
九、如何调整补偿量
正常情况下只需要修改:
$DriftSecondsPerDay = 5.0
例如原来设置:
5.0 秒/天
观察一周以后仍然:
慢 4.2 秒
则剩余漂移约:
4.2 ÷ 7 = 0.6 秒/天
新的设置:
$DriftSecondsPerDay = 5.6
如果一周以后反而:
快 4.2 秒
则:
5.0 - 4.2 / 7 = 4.4
改为:
$DriftSecondsPerDay = 4.4
修改后重新执行一次脚本即可,不需要先恢复默认设置。
建议至少观察 3~7 天 后再调整,避免根据短时间误差判断漂移率。
十、恢复 Windows 默认时钟模式
建议同时保留一个恢复脚本:
C:\ClockRateReset.ps1
内容:
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class ClockReset
{
[StructLayout(LayoutKind.Sequential)]
public struct LUID
{
public uint LowPart;
public int HighPart;
}
[StructLayout(LayoutKind.Sequential)]
public struct TOKEN_PRIVILEGES
{
public uint PrivilegeCount;
public LUID Luid;
public uint Attributes;
}
public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
public const uint TOKEN_QUERY = 0x0008;
public const uint SE_PRIVILEGE_ENABLED = 0x00000002;
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool OpenProcessToken(
IntPtr ProcessHandle,
uint DesiredAccess,
out IntPtr TokenHandle);
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern bool LookupPrivilegeValue(
string lpSystemName,
string lpName,
out LUID lpLuid);
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle,
bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
uint BufferLength,
IntPtr PreviousState,
IntPtr ReturnLength);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool GetSystemTimeAdjustment(
out uint lpTimeAdjustment,
out uint lpTimeIncrement,
out bool lpTimeAdjustmentDisabled);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool SetSystemTimeAdjustment(
uint dwTimeAdjustment,
bool bTimeAdjustmentDisabled);
public static bool EnableSystemTimePrivilege()
{
IntPtr token;
if (!OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
out token))
return false;
try
{
LUID luid;
if (!LookupPrivilegeValue(
null,
"SeSystemtimePrivilege",
out luid))
return false;
TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES();
tp.PrivilegeCount = 1;
tp.Luid = luid;
tp.Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(
token,
false,
ref tp,
0,
IntPtr.Zero,
IntPtr.Zero))
return false;
return Marshal.GetLastWin32Error() == 0;
}
finally
{
CloseHandle(token);
}
}
}
"@
if (-not [ClockReset]::EnableSystemTimePrivilege()) {
throw "Cannot enable SeSystemtimePrivilege. Run as Administrator."
}
if (-not [ClockReset]::SetSystemTimeAdjustment(0, $true)) {
$err =
[Runtime.InteropServices.Marshal]::GetLastWin32Error()
throw "Reset failed. Win32 error: $err"
}
[uint32]$CurrentAdjustment = 0
[uint32]$TimeIncrement = 0
[bool]$AdjustmentDisabled = $false
[ClockReset]::GetSystemTimeAdjustment(
[ref]$CurrentAdjustment,
[ref]$TimeIncrement,
[ref]$AdjustmentDisabled
) | Out-Null
Write-Host ""
Write-Host "TimeAdjustment : $CurrentAdjustment"
Write-Host "TimeIncrement : $TimeIncrement"
Write-Host "AdjustmentDisabled : $AdjustmentDisabled"
Write-Host ""
Write-Host "SUCCESS - Windows default clock adjustment restored."
执行:
powershell.exe -ExecutionPolicy Bypass -File C:\ClockRateReset.ps1
恢复后通常可以看到:
TimeAdjustment : 156250
TimeIncrement : 156250
AdjustmentDisabled : True
表示重新使用 Windows 默认时钟模式。
十一、建议的最终部署方式
对于完全隔离的内网:
定期人工核对标准时间
│
▼
Windows Server 2012
W32Time + 漂移补偿
│
┌────────────┼────────────┐
▼ ▼ ▼
Windows Linux 网络设备
应做到:
- 全网只认一台主 NTP Server;
- 测量主服务器长期漂移率;
- 使用
ClockRateAdjust.ps1做连续频率补偿; - 每隔一段时间与可靠标准时间人工核对;
- 如果漂移发生变化,只修改 $DriftSecondsPerDay;
- 保留
ClockRateReset.ps1,便于随时恢复系统默认模式。
需要注意的是,这种方法解决的是本机时钟稳定地快或慢的问题。它不能代替真正的标准时间源。如果内网要求较高的绝对时间精度,仍建议最终使用 GPS/北斗 NTP 授时设备。