我开发了一个跟踪业务事件的 Windows 服务。它使用 Windows 时钟为事件添加时间戳。然而,底层时钟可能会非常显着地漂移(例如,每分钟损失几秒钟),尤其是在 CPU 努力工作时。我们的服务器使用 Windows 时间服务与域控制器保持同步,域控制器在后台使用 NTP,但同步频率由域策略控制,在任何情况下,即使每分钟同步一次,仍然会出现明显的偏差。除了使用硬件时钟之外,我们可以使用任何技术来保持时钟更稳定吗?
13 回答
时钟滴答应该是可预测的,但是在大多数 PC 硬件上——因为它们不是为实时系统设计的——其他 I/O 设备中断优先于时钟滴答中断,并且一些驱动程序在中断服务例程中进行大量处理而不是而不是将其推迟到延迟过程调用(DPC),这意味着系统可能无法在(有时)发出信号后很久才能为时钟滴答中断提供服务。
其他因素包括总线主控 I/O 控制器从 CPU 窃取许多内存总线周期,导致它在很长一段时间内缺乏内存总线带宽。
正如其他人所说,时钟生成硬件也可能随着组件值随温度的变化而改变其频率。
Windows 确实允许调整每个中断时添加到实时时钟的滴答数:请参阅 SetSystemTimeAdjustment。但是,这仅在您有可预测的时钟偏差时才有效。如果时钟只是略微偏离,SNTP 客户端(“Windows 时间”服务)将调整此偏差,以使时钟滴答声稍快或稍慢,以趋向正确的时间。
我不知道这是否适用,但是...
Windows 有一个问题,如果你用timeBeginPeriod()改变定时器的分辨率很多,时钟会漂移。
实际上,Java 的 Thread wait()
(和os::sleep()
)函数的 Windows 实现中有一个错误导致了这种行为。它总是在等待之前将计时器分辨率设置为 1 毫秒,以便准确(不管睡眠时间长短),并在完成后立即恢复它,除非任何其他线程仍在睡眠。然后,此设置/重置将混淆 Windows 时钟,它期望 Windows 时间量是相当恒定的。
Sun 实际上自 2006 年以来就知道这一点,并且还没有修复它,AFAICT!
因为这个,我们实际上让时钟快了两倍!一个在循环中休眠 1 毫秒的简单 Java 程序显示了这种行为。
解决方案是自己将时间分辨率设置为较低的值,并尽可能长时间地保持在那里。使用 timeBeginPeriod() 来控制它。(我们将其设置为 1 毫秒,没有任何不利影响。)
对于那些用 Java 编写代码的人来说,解决这个问题的更简单方法是创建一个只要应用程序存在就休眠的线程。
请注意,无论哪个应用程序是真正的罪魁祸首,这将在全球范围内解决机器上的这个问题。
您可以在计划任务 .bat 文件中运行“w32tm /resync”。这适用于 Windows Server 2003。
除了更频繁地重新同步时钟之外,我认为除了购买新主板之外您无能为力,因为您的时钟信号似乎不在正确的频率上。
http://www.codinghorror.com/blog/2007/01/keeping-time-on-the-pc.html
PC 时钟通常应精确到每天几秒钟之内。如果你正在经历巨大的时钟漂移——每天大约几分钟——首先要检查的是你的交流电源。我个人观察到将 UPS 插入另一个 UPS 的系统(顺便说一句,这是一个禁忌),每天增加几分钟。从链中移除不必要的 UPS 解决了时间问题。我不是硬件工程师,但我猜测电源中的某些定时信号被主板上的实时时钟芯片使用。
如前所述,Java 程序可能会导致此问题。
另一个不需要修改代码的解决方案是添加 VM 参数-XX:+ForceTimeHighResolution
(可在NTP 支持页面上找到)。
9.2.3. Windows 和 Sun 的 Java 虚拟机
Sun 的 Java 虚拟机需要使用 >-XX:+ForceTimeHighResolution 参数启动,以避免丢失中断。
有关详细信息,请参阅http://www.macromedia.com/support/coldfusion/ts/documents/createuuid_clock_speed.htm。
从引用的链接(通过Wayback 机器- 原始链接已消失):
ColdFusion MX:CreateUUID 提高 Windows 系统时钟速度
在 Macromedia ColdFusion MX 及更高版本的负载下多次调用 createUUID 函数会导致 Windows 系统时钟加速。这是 Java 虚拟机 (JVM) 的一个问题,其中 Thread.sleep 调用少于 10 毫秒 (ms) 会导致 Windows 系统时钟运行得更快。此行为最初作为 Sun Java 错误 4500388 (developer.java.sun.com/developer/bugParade/bugs/4500388.html) 提交,并已在 1.3.x 和 1.4.x JVM 中得到确认。
在 ColdFusion MX 中,createUUID 函数有一个 1 毫秒的内部 Thread.sleep 调用。当 createUUID 被大量使用时,Windows 系统时钟将每分钟增加几秒。加速率与 createUUID 调用的数量和 ColdFusion MX 服务器上的负载成正比。Macromedia 在 Windows XP、2000 和 2003 系统上的 ColdFusion MX 和更高版本中观察到了这种行为。
增加重新同步的频率。如果同步是与您自己网络上的主服务器同步,则没有理由不每分钟同步一次。
更频繁地同步。查看W32Time 服务的注册表项,尤其是“Period”。“SpecialSkew”听起来会帮助你。
时钟漂移可能是温度的结果;也许您可以尝试使温度更加恒定-也许使用更好的冷却?不过,你永远不会完全放弃漂移。
使用外部时钟(GPS 接收器等),以及将 CPU 时间与绝对时间相关联的统计方法是我们在这里用来同步分布式系统中的事件的方法。
因为听起来你有一个大生意:
拿一台旧的笔记本电脑或其他没什么用的东西,但似乎有一个或多或少可靠的时钟,并将其称为计时器。计时员的唯一工作是每(例如)2 分钟一次,向服务器发送一条消息,告知时间。服务器不会使用 Windows 时钟作为它们的时间戳,而是记下从 Timekeeper 的最后一个信号开始的时间,加上从该信号开始经过的时间。每周用手表检查一次或两次计时员的时钟。这应该足够了。
你在运行什么服务器?在台式机中,我遇到这种情况的时间是启用了扩频 FSB,这会导致中断时序出现一些问题,这就是时钟滴答作响的原因。可能想看看这是否是其中一台服务器上的 BIOS 中的一个选项,如果启用则将其关闭。
您拥有的另一个选项是编辑时间轮询间隔并使用以下注册表项使其更短,很可能您必须添加它(注意这是一个 DWORD 值,该值以秒为单位,例如 600 表示 10 分钟) :
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient\SpecialPollInterval
这是一个完整的检查:KB816042
我曾经写过一个 Delphi 类来处理时间重新同步。它粘贴在下面。现在我看到了 Larry Silverman 提到的“w32tm”命令,我怀疑我是在浪费时间。
unit TimeHandler;
interface
type
TTimeHandler = class
private
FServerName : widestring;
public
constructor Create(servername : widestring);
function RemoteSystemTime : TDateTime;
procedure SetLocalSystemTime(settotime : TDateTime);
end;
implementation
uses
Windows, SysUtils, Messages;
function NetRemoteTOD(ServerName :PWideChar; var buffer :pointer) : integer; stdcall; external 'netapi32.dll';
function NetApiBufferFree(buffer : Pointer) : integer; stdcall; external 'netapi32.dll';
type
//See MSDN documentation on the TIME_OF_DAY_INFO structure.
PTime_Of_Day_Info = ^TTime_Of_Day_Info;
TTime_Of_Day_Info = record
ElapsedDate : integer;
Milliseconds : integer;
Hours : integer;
Minutes : integer;
Seconds : integer;
HundredthsOfSeconds : integer;
TimeZone : LongInt;
TimeInterval : integer;
Day : integer;
Month : integer;
Year : integer;
DayOfWeek : integer;
end;
constructor TTimeHandler.Create(servername: widestring);
begin
inherited Create;
FServerName := servername;
end;
function TTimeHandler.RemoteSystemTime: TDateTime;
var
Buffer : pointer;
Rek : PTime_Of_Day_Info;
DateOnly, TimeOnly : TDateTime;
timezone : integer;
begin
//if the call is successful...
if 0 = NetRemoteTOD(PWideChar(FServerName),Buffer) then begin
//store the time of day info in our special buffer structure
Rek := PTime_Of_Day_Info(Buffer);
//windows time is in GMT, so we adjust for our current time zone
if Rek.TimeZone <> -1 then
timezone := Rek.TimeZone div 60
else
timezone := 0;
//decode the date from integers into TDateTimes
//assume zero milliseconds
try
DateOnly := EncodeDate(Rek.Year,Rek.Month,Rek.Day);
TimeOnly := EncodeTime(Rek.Hours,Rek.Minutes,Rek.Seconds,0);
except on e : exception do
raise Exception.Create(
'Date retrieved from server, but it was invalid!' +
#13#10 +
e.Message
);
end;
//translate the time into a TDateTime
//apply any time zone adjustment and return the result
Result := DateOnly + TimeOnly - (timezone / 24);
end //if call was successful
else begin
raise Exception.Create('Time retrieval failed from "'+FServerName+'"');
end;
//free the data structure we created
NetApiBufferFree(Buffer);
end;
procedure TTimeHandler.SetLocalSystemTime(settotime: TDateTime);
var
SystemTime : TSystemTime;
begin
DateTimeToSystemTime(settotime,SystemTime);
SetLocalTime(SystemTime);
//tell windows that the time changed
PostMessage(HWND_BROADCAST,WM_TIMECHANGE,0,0);
end;
end.
我相信 Windows 时间服务只实现了 SNTP,它是 NTP 的简化版本。完整的 NTP 实施会在决定同步频率时考虑时钟的稳定性。