你真的了解时间吗?
发布于 2021-10-10 08:02
你真的了解闰年和闰月吗?
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当前时间的情况,很遗憾,这种场景并不少见,比如金融业务中的打款结算。
号外!号外!
Java 极客技术微信群中有很多优秀的小伙伴在讨论技术,偶尔还有不定期的资料分享和红包发放!如果你想提升自己,并且想和优秀的人一起进步,请添加下方微信,阿粉会迅速拉你进群。
注意:添加好友时,备注【加群】可以更快的拉你进群哦!
一键四连,你的offer也四连
本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。
相关素材