导语
时间在分布式系统中是一个重要且有趣的问题。时间是我们一直想要准确测量的量。为了知道特定事件在一天中的什么时间发生在特定计算机上,有必要将其时钟与权威的外部时间源同步。时间通常是系统事件完整性、系统日志、系统审计、系统故障排查以及系统取证的基本标准。在现代的复杂系统中很多地方都会和时间发生关系,比如基于时间的访问控制、加密认证等。很显然,在复杂的分布式系统中,准确的时间十分重要。既然时间这么重要,那时间不准确或者出现跳变的情况,会对系统产生影响吗?答案是会的。
手机里的时间准确吗?
当别人问你现在什么时间的时候,你会怎么做?打开手机,看一眼时间,告诉对方。但是这个时间真的准确吗?并不是!现在绝大部分的电子设备都是和协调时间时(UTC)对准的,你可以打开time.is查看当前的UTC时间。
但是这个时间是经过协调的,真正准确的时间是国际原子钟时间(International Atomic Time, TAI)。UTC时间和TAI时间是有差异的。换句话说,你手机上的时间并不是真正意义上的准确时间。
UTC和TAI
国际原子时(缩写TAI,来自其法语名称temps atomique international )是一种高精度的原子 坐标 时间标准,它基于地球大地水准面上适当时间的概念流逝。TAI 是全球 80 多个国家实验室的 450 多个原子钟保持时间的加权平均值。它是一个连续的时间尺度,没有闰秒,它是地球时的主要实现(带有固定的纪元偏移量))。它是协调世界时(UTC) 的基础,它用于地球表面的民用计时,具有闰秒。——维基百科
目前,国际原子时比协调时间时要快37秒。那为何会差距半分多钟呢?其实和维基百科中提到的闰秒(leap second)相关.
闰秒(Leap Second)
什么是闰秒?
闰秒其实国际地球自转和参考系统服务 (IERS)人为添加到UTC时间的一秒,会在某个时间点,加入1s(23:59:60)。
为什么需要闰秒?
地球在围绕其轴自转的速度每天都在波动(月球/潮汐),并且随着时间的推移它会略微减慢。通过在时间计数上增加一秒,我们有效地停止了那一秒的时钟,让地球有机会赶上。添加闰秒的那一分钟为 61 秒,而那一天为 86,401 秒,而不是通常的 86,400 秒。
一般情况下,闰秒都会加在6月30日或者是12月31日的深夜,此时,一些时钟会出现23:59:60的奇怪时间。
已经加入的闰秒
截止到目前,总共添加了27个闰秒,在第一个闰秒加入之前,UTC时间已经慢于TAI时间10秒了。所以,现在UTC时间和TAI时间相差了37秒。
UTC 日期 | UTC 时间 | UTC慢于TAI (s) |
---|---|---|
30/06/1972 | 23:59:60 | 11 |
31/12/1972 | 23:59:60 | 12 |
31/12/1973 | 23:59:60 | 13 |
31/12/1974 | 23:59:60 | 14 |
31/12/1975 | 23:59:60 | 15 |
31/12/1976 | 23:59:60 | 16 |
31/12/1977 | 23:59:60 | 17 |
31/12/1978 | 23:59:60 | 18 |
31/12/1979 | 23:59:60 | 19 |
30/06/1981 | 23:59:60 | 20 |
30/06/1982 | 23:59:60 | 21 |
30/06/1983 | 23:59:60 | 22 |
30/06/1985 | 23:59:60 | 23 |
31/12/1987 | 23:59:60 | 24 |
31/12/1989 | 23:59:60 | 25 |
31/12/1990 | 23:59:60 | 26 |
30/06/1992 | 23:59:60 | 27 |
30/06/1993 | 23:59:60 | 28 |
30/06/1994 | 23:59:60 | 29 |
31/12/1995 | 23:59:60 | 30 |
30/06/1997 | 23:59:60 | 31 |
31/12/1998 | 23:59:60 | 32 |
31/12/2005 | 23:59:60 | 33 |
31/12/2008 | 23:59:60 | 34 |
30/06/2012 | 23:59:60 | 35 |
30/06/2015 | 23:59:60 | 36 |
31/12/2016 | 23:59:60 | 37 |
闰秒故障
在2012年6月30日,IERS准备在这天的最后一秒之后插入闰秒。但是万万没想到,插入的这一个闰秒,导致了很大一部分业务系统宕机。
在这天,2012年6月30日的深夜,许多人和往常一样正在Reddit上快乐水贴,突然发现,自己的回复发不出去了。
闰秒故障在Reddit上发生了,起初,Jason Harvey并没有意识到是闰秒加入的问题,仅仅认为是网络质量差的原因。但是问题持续了半个多小时,他们意识到了问题的严重性。最后他们追溯到他们的一组运行着Linux操作系统的服务器上,他们发现,由于没有正常适应当天晚上加入的闰秒,这组服务器几乎完全停顿下来,无法做任何响应。
这些机器到底发生了啥呢?
John Stultz(Linux Kernel & AOSP devboards)在2012-7-1日的一封邮件链接中提到了这个问题:
从 Stultz 的邮件列表帖子来看,当闰秒来临时,这些 hrtimer 突然比核心操作系统领先一秒,它们同时唤醒无数休眠的应用程序,并使机器的 CPU 过载。Reddit最后通过重启服务器才使得业务恢复。该站点在大约 30 到 40 分钟内几乎无法运行,并且完全离线大约一个半小时,可以说是一个比较严重的现网事故了。
其他故障
在2012-6-30这天,不光Reddit出现了问题,Reddit 只是在格林威治标准时间周六午夜过后遭受闰秒故障的几家网络公司之一,包括 Gawker Media 和 Mozilla,每次进行闰秒调整时都会出现此类问题。例如,据报道,2009 年 1 月闰秒导致 Sun Microsystems 的 Solaris 操作系统和Oracle 软件包出现问题;2016年12月31日的闰秒导致了Cloudflare DNS的崩溃https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/。
启示
因为闰秒问题,很多系统都产生了不同程度的故障。可见系统时间对于系统的重要性。以后在设计复杂系统时,需要特别关注一下时间的问题。但是现在系统设计得越来越复杂,往往是不同的微服务之间相互依赖,怎么去定位或者发现系统中潜在的和系统时间相关的问题呢?
腾讯云混沌演练平台
混沌演练平台提供多场景的故障主动注入,便于用户模拟真实环境的故障扰动,协助用户发现其系统韧性不足之处。本文的场景,其实就可以使用混沌演练平台提供的CVM系统时间跳变混沌故障动作模拟,并且可以注入故障之后回滚操作,可以帮助用户在业务上线之前验证类似复杂系统中时间跳变的场景,帮助用户解决潜在风险。
CVM系统时间演练示例
示例告警系统
假设目前有一个监控系统,里面有两个微服务告警服务/数据采集服务。告警服务依赖于数据采集服务的数据。这里简化为如下程序:
alarm_client
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"log"
"net/http"
"time"
)type JsonResponse struct {
Code intjson:"Code"
Msg stringjson:"Msg"
Data int64json:"Data"
LatestTime int64json:"LatestTime"
}var serverURL string
func init() {
flag.StringVar(&serverURL, "url", "http://localhost:8080/getMetricData", "数据服务")
flag.Parse()
}// getFlowMetricData 从监控数据服务中获得流量总量
// return (flowTotal, latestTime): (流量总量数据,上次请求数据时间)
func getFlowMetricData() (int64, int64) {
if response, err := http.Get(serverURL); err != nil {
log.Println(err)
return 0, 0
} else {
// read response body
if body, err := ioutil.ReadAll(response.Body); err != nil {
log.Println(err)
return 0, 0
} else {
log.Printf("Get Response: %+v\n", string(body))
var rsp JsonResponse
if err := json.Unmarshal(body, &rsp); err != nil {
log.Println(err)
return 0, 0
} else {
// close response body
if err := response.Body.Close(); err != nil {
log.Println(err)
return 0, 0
}
return rsp.Data, rsp.LatestTime
}
}
}
}
func main() {
log.Println("===========Starting Client=============")
var threshold float64
threshold = 0.0
for {
flowMetric, latestTime := getFlowMetricData()
times := float64(time.Now().UnixNano() - latestTime)
// 判断是否每纳秒流量是否超过阈值,这里铁定会超过(用作示例)
if float64(flowMetric)/times > threshold {
// alarm
log.Println("超过阈值,告警。")
// 模拟告警执行用时2s
time.Sleep(time.Second * 2)
}
// 其他逻辑
time.Sleep(time.Second * 1)
}
}
data_server
package main
import (
"github.com/gin-gonic/gin"
"math/rand"
"net/http"
"time"
)type JsonResponse struct {
Code intjson:"Code"
Msg stringjson:"Msg"
Data int64json:"Data"
LatestTime int64json:"LatestTime"
}
func main() {
var latestTime int64
latestTime = -1
r := gin.Default()
r.GET("/getMetricData", func(c *gin.Context) {
// 结构体方式
currentTime := time.Now().UnixNano()
data := JsonResponse{
Code: 200,
Msg: "获取数据成功",
Data: rand.Int63n(1000),
}
if latestTime > 0 {
data.LatestTime = latestTime
latestTime = currentTime
} else {
data.LatestTime = currentTime
latestTime = currentTime
}
c.JSON(http.StatusOK, data)
})
_ = r.Run(":8080")
}
这个简化的程序实际上是存在问题的,下面我们将演示通过腾讯云混沌演练平台进行CVM系统时间跳变来发现这个问题。
创建混沌演练任务
演练目的
通过混沌演练平台CVM系统提供的时间跳变故障动作对上述系统进行混沌演练,发现程序中存在的潜在问题。
步骤一:选择告警服务CVM实例,时间回退5秒钟
步骤二:故障注入成功之后,观察程序是否正常执行
步骤三:启动恢复动作,恢复告警服务CVM实例系统时间
步骤四:恢复动作执行成功之后,观察程序是否恢复正常运行
演练准备
将上述两个程序,编译成可执行文件,并上传到2个不同的CVM实例上(chaos-test-1实例运行告警客户端程序,chaos-test-2实例运行数据采集服务),启动程序。此时两个CVM实例的系统时间是一致的。程序可以正常无误地运行。
创建演练
- 进入腾讯云混沌演练平台,点击演练管理,新建演练
2. 填写基本信息之后,选择chaos-test-1所在实例
3. 点击添加演练动作,选择shell脚本中的CVM时间跳变故障动作。
4. 提交创建演练
开始演练
- 启动告警系统,正常运行
2. 启动故障动作。
3. 发现问题,启动恢复动作,恢复时间。
演练复盘
通过混沌演练,我们发现示例的告警系统在面对系统发生跳变时候,韧性是不足的,出现了理应发生告警的数据时,并没有触发告警逻辑,这对于一个监控系统是相当致命的,如此以来,监控系统就形同虚设了。这个问题是在故障动作注入告警服务的CVM中的,现在回顾这个代码,很容易发现是其在获取当前时间的时候出现了问题。
get_cur_time
times := float64(time.Now().UnixNano() - latestTime)
这个在两台服务器时间统一的情况下,times >= 0, 但是当告警服务所在实例发生系统时间跳变之后,告警服务所在实例的系统时间回退了5s, 使得times < 0,进而无法触发告警逻辑,而且这个异常会一直存在除非时间恢复。经过混沌演练,可以帮助我们发现程序中潜在的问题,并针对其进行优化,这里可以调整数据采集服务接口返回(流量总量,上次结束时间,本次请求时间)来避免这个问题。