你真的了解时间吗?

nill0705
发布于 2022-9-27 11:45
浏览
0收藏

时间回到2012年2月29日,微软的云产品Azure出现了一个严重的故障,部分主机停机时长达到8小时,而造成故障的原因竟是用错了时间。

 

你真的了解闰年和闰月吗?

Azure宕机故障回顾

你真的了解时间吗?-鸿蒙开发者社区

Azure的物理架构如图所示,其中的每个格子代表一台物理主机,大约1000台物理主机组成一个Cluster,每个Cluster会有一个名为Fabric Controller(FC)的软件管理,负责主机的生命周期,自动运维等。这样设计的一个好处是主机的故障最多会影响到Cluster,而不会对整个Azure产生影响,提升了可用性。

你真的了解时间吗?-鸿蒙开发者社区

上图展示了Cluster中每台主机的部署架构,对于部署在同一个主机上的每一个虚拟机,都会有一个Guest Agent(GA)用于与物理机的Host Agent(HA)通信,而上文中提到的管理软件FC正是通过这种方式接受部署在主机上的虚拟机的心跳。虚拟机启动时的第一件事情就会生成一个公钥并且通过GA发送给HA,后续安全相关的证书比如HTTPS需要的SSL证书就可以通过这个公钥加密并通过HA发送给GA。

那故障是怎么发生的呢?
为了安全起见,GA发送给HA的公钥指定了为期一年的过期时间:

SYSTEMTIME st;       // declare a SYSTEMTIME variable 
GetSystemTime(&st);  // set it to the current date and time 
st.wYear++;          // increment it by one year 

这段代码平常是OK的,但是如果当前时间是2012年2月29日,那这段代码给出的公钥的过期时间就是2013年2月29日,可是2013年并没有这一天呀!因为生成公钥是虚拟机启动的第一步,这就会导致虚拟机启动失败。HA如果接受不到GA启动时的信号超过25分钟就会重启虚拟机,这样操作3次之后还不能接受到GA的信号,那HA就会判定当前的服务器产生了不可逆转的硬件故障,并上报给FC,FC在接受到该信号后马上启动自恢复流程,置换掉上报故障的主机,灾难从这一刻开始,本来只有新创建容器的主机会因为创建公钥失败而宕机,但是现在一个主机的不可用状态马上蔓延到整个Cluster,最后造成整个Cluster不可用。

 

闰年的那些日子需要注意

1、并不是每年都是365天

 

●  闰年前的最后一天,比如2019年12月31日,这一天加上365天之后并不是2020年的最后一天
●  闰年的第一天, 比如2020年1月1日,这一天加上365天之后并不是2021年的第一天,而是2020年的最后一天
●  闰年的最后一天,比如2020年12月31日,这天是今年的第366天,一个典型的错误如下:

int items[365];
items[dayOfYear - 1] = x;

如果dayOfYear是闰年的最后一天的话,上面的这段代码将会出现异常。

 

2、并不是每年的2月都是28天

 

●  闰年的1月31日,比如2020年1月31日,这一天加上28天之后并不是下一个月的最后一天
●  闰年的2月1日,比如2020年2月1日,这天加上28天之后并不是下一个月的第一天
●  闰年的2月29日,比如2020年2月29日,这天加上365天之后是2021年3月1日,而不是2021年2月29日(不存在这个日期)

你听过闰秒吗?

要想讲明白闰秒是怎么回事,得先了解三个时间概念:

 

UT(世界时)

 

UT(Universal Time,世界时)是一种以格林尼治子夜起算的平太阳时,由于以地球自转为基准,观测精度受限于地球的自转速度的稳定,地球体积不均匀、潮汐引力以及其他星球的扰动的原因,导致地球转速不稳定,每日误差达数毫秒。

 

TAI(国际原子时)

 

TAI(international atomic time)为国际原子时,1971年建立,利用某些元素(如铯、氢、铷)的原子能级跃迁频率有极高稳定性的特性定义时间标准,现为国际计量局(BIPM)的时间部门维持,综合全球约60个实验室中的大约240台各种自由运转的原子钟提供的数据进行处理,得出“国际时间标准”称为国际原子时 (TAI),每日误差为数纳秒。

 

TAI时间原点为UT 1958 年1月1日 00:00:00 ,在此之后TAI就沿着原子秒的节拍一直走下去,和UT误差也越来越大。如果我们不想点办法大约6.5万年后,也许会出现“顶着星星去上课”的情景(如果那时还有学校的话),因为原子时显示的早上8点正是群星密布的晚上8点——原子时和世界时相差12个小时,因此就有了UTC。

 

UTC(协调世界时)

 

上面已经说过,科学上有两种时间计量系统:基于天文测量而得出的“世界时”和以物理学发展发现的原子振荡周期固定这一特性而来的“原子时”,UTC就是用于将世界时(UT,天文时间)和国际原子时(TAI,原子时间)协调起来另外一套计量系统, 1971年国际计量大会通过决议,使用UTC(协调世界时)来计量时间, 协调的原则就是UTC以原子秒长节拍,在时刻上尽量接近于世界时(UT)。

 

目前UTC是事实上的时间标准,比如所有计算机中的时间就是UTC时间(通过时区换算为本地时间), 而闰秒,实际上就是UTC特有的。

 

闰秒产生的原因?

地球自转被称为“世界时间”。不过,由于潮汐、地壳运动、冰川融化、地震等自然现象,地球的自转速度并非恒定,而是有时快,有时慢。

 

1967 年原子钟的出现意味着人类计时不用再依赖于地球的自转,时间的计量标准正式由天文学的宏观领域过渡到物理学的微观领域,也就是所谓的“原子时间”。

 

细心的科学家发现,两者之间存在微妙差异。于是,国际地球自转和参考系服务会(IERS)在差异超过 0.9秒时,会协调“世界时间”加上或减去 1 秒,消除这个误差。

 

这多出来的1秒,就是闰秒。

 

闰秒一般发生在年末或者年中,有IERS负责提前半年发出公告

 

如果时间倒流,你的系统会不会崩?

在2017年的时候,Cloudflare的DNS服务出现了故障,故障峰值时刻影响到了0.2%的DNS查询以及2%的HTTP请求,而故障的原因竟是时间发生了倒流!

你真的了解时间吗?-鸿蒙开发者社区

上图展示了Cloudflare公司的DNS解析架构,所有的DNS解析都会先到达名为RRDNS的负载均衡器,然后再由它选择性能比较好的DNS解析器来处理请求,为了在众多的DNS解析器中选择出性能较好的一个解析器RRDNS记录了每次后端DNS解析器的系统用时,其相应的go代码如下:

// Update upstream sRTT on UDP queries, penalize it if it fails
if !start.IsZero() {
  // 注意这行代码,start是发起请求时的时间
 rtt := time.Now().Sub(start)
 if success && rcode != dns.RcodeServerFailure {
  s.updateRTT(rtt)
 } else {
  // The penalty should be a multiple of actual timeout
  // as we don't know when the good message was supposed to arrive,
  // but it should not put server to backoff instantly
  s.updateRTT(TimeoutPenalty * s.timeout)
 }
}

请注意我们注释中指出的那行代码,变量start是请求DNS解析器开始时调用time.Now()获得的时间,time.Now()是DNS解析器返回的时间,那time.Now().Sub(start)就是DNS解析器的解析上一次DNS请求花费的时间。

 

由于闰秒的存在,2017年1月1日7时59分59秒后面增加1秒,也就是说在UTC时间里2017年1月1日7时59分59秒的下一秒还是2017年1月1日7时59分59秒,相当于时间倒退了1秒,由于正常DNS解析的耗时在毫秒级别,所以会导致rtt := time.Now().Sub(start)这行代码计算出来的变量rtt为负数,为了计算的准确,RRDNS也会拿几次rtt计算的平均值作为rtt的估算值,但是在闰秒来临的那一刻,也会有好多rtt的平均值变为负数,当着个小于0的rtt作为种子传入到rand.Int63n方法中后灾难发生了:

Int63n returns, as an int64, a non-negative pseudo-random number in [0,n) from the default Source. It panics if n <= 0.

 

这个方法不能处理小于0的参数!

 

Java有没有类似的问题

当然也是有的,我们翻一下java.util.Date的文档,可以看到:

Although the Date class is intended to reflect coordinated universal time (UTC), it may not do so exactly, depending on the host environment of the Java Virtual Machine.

而在符合POXIS规范的主机上不允许出现闰秒的,JVM在这样的机器上是没有任何办法,这又一次拆穿了"write once run anywhere"这个谎言。相比于直接让时间倒流,以谷歌为代表的公司都会人工干预集群内的NTP(Network Time Protocol)服务器,将1秒平分到一天中,让闰秒所在的那一天每一秒都比平常慢一点。这种方案对于集群内的机器是是比较靠谱的,但是如果其他的集群并没有人工干预NTP(Network Time Protocol),仍然会出现时间倒流的现象:

你真的了解时间吗?-鸿蒙开发者社区

如上图所示,在闰秒来临的那一天,假如集群A中的NTP已经被人工干预变慢,但是集群B中采用的是标准的NTP服务,如果集群A查询当前集群的NTP服务作为业务时间传到集群B,有可能会出现上游的业务时间晚于集群B当前时间的情况,很遗憾,这种场景并不少见,比如金融业务中的打款结算。

分类
标签
已于2022-9-27 11:45:29修改
收藏
回复
举报
回复
    相关推荐