查看原文
其他

闰秒终于要取消了!一文详解其来源及影响

陶松桥 腾讯云开发者 2023-01-13

导读 | 第27届国际计量大会宣布最迟不晚于2035年取消引入闰秒,这一消息引起轰动。上一次闰秒产生,对Reddit、Mozilla、FourSquare等都产生了一定的问题,其中Reddit宕机时间超过1个半小时!本栏目特邀腾讯后台开发工程师陶松桥,带你是深入了解闰秒的来源及其影响,并介绍各类系统常见的闰秒处理方法,其中会分享TencentOS Server 操作系统的解决方案。



闰秒从何而来
世界上有几种计量时间的方式:
世界时(UT1):是一种天文计量的方式,天文学家通过观测地球的自转,并将自转一周的时间(一天)等分为86400份,每份为一秒,受潮汐等因素的影响,地球自转一周的时间并不是恒定的,这也是造成闰秒现象的直接原因。

原子时(TAI):由于上面描述的世界时并不稳定,物理学家用更为稳定的量子计量的方式来统计时间,1967年,国际计量大会用铯133(Cs133)原子基态的两个超精细能级之间的跃迁所对应辐射的9192631770个周期所持续的时间定义为1秒,这个是目前最精确的时间计量方式,其误差为1400000年一秒,基本可忽略不计。

协调世界时(UTC):又称世界标准时间或世界协调时间,UTC以TAI为基础,又要兼顾UT1,当UTC,和UT1之间的偏差接近1秒时,国际地球自转和参考系服务(IERS)会提前6个月公布下一次闰秒的时间。

我们将世界绝大多数地方时区的基本时间称为协调世界时,即 UTC。它源自分布在世界一些国家的大量原子时钟。地球的自转并不是非常恒定的,有时会有一些变化,平均自转速度会缓慢下降。这就是为什么会在 UTC 时标中插入所谓闰秒,它们可将 UTC 时间进程调整到真实地球自转时间。

为什么会多出这一秒呢?它的存在是因为决定昼夜更替的地球绕轴自转会在很长的一段时间内慢下来,主要是由月亮-太阳引力造成。另外,地球也受其内部(地核、地幔)和外部(气候、海洋)构成影响。目前,时间主要由分属几个国家的 250 台原子时钟测量,这些原子钟是通过测量原子的能量转换水平工作。使用这些时钟计算 UTC,同时因为这个时间测量原理周期性地与地球不同步,因此必须使用闰秒进行校正。另外,我们必须考虑到现在的一天比 1820 年的一天要长 2 毫秒。不出所料,地球自转慢慢就与 UTC 不同步了。

国际地球自转服务局IERS 测量的是真实地球自转,并决定何时插入闰秒。插入闰秒一般总是在某个月的最后一天进行,首选六月或十二月的 UTC 午夜时间。过去每次添加闰秒都是在六月或十二月进行。是否添加闰秒的声明,会由 IERS 在其Bulletin C 中发布。目前,在可能添加闰秒日期前半年会公布 Bulletin C 。

因为闰秒是在全世界同时插入,插入闰秒的本地(民用)时间取决于本地时间与 UTC 之间的偏差,例如:2015年7月1日发生闰秒时,在时区 UTC+8h(北京时间) 中,闰秒会在时钟显示午夜后 8 小时的时候插入。

闰秒时计算北京时间的标准方法为:
2015-07-01 07.59.572015-07-01 07.59.582015-07-01 07.59.592015-07-01 07.59.60 <-- 闰秒2015-07-01 08.00.002015-07-01 08.00.012015-07-01 08.00.02

如果系统时钟采用国际原子时(TAI),并使用正确的时区,那么就会列出 23:59:60。但因为在 Unix 的 UTC 使用中不存在 23:59:60,Linux内核会采用倒回一秒的方法在 0:00 UTC 后第一次时钟更新时插入闰秒。在本地时间计时中,根据不同的时区偏差,比如 UTC+8h,在TencentOS Server系统中,您会观察到以下现象:
2015-07-01 07:59:58.0002015-07-01 07:59:58.5002015-07-01 07:59:59.0002015-07-01 07:59:59.5002015-07-01 08:00:00.000 <-- 插入闰秒2015-07-01 07:59:59.0002015-07-01 07:59:59.5002015-07-01 08:00:00.0002015-07-01 08:00:00.500

IERS 确定闰秒后,一些时间传播服务还会发布闰秒通知。这包括德国长波发射机 DCF77 和卫星巡航系统 GPS 示例。因此,可解码从那些系统获取信号的接收器也可以解码闰秒通知。如果在所应用协议中包含闰秒信息(例如接收器传送的时间字符串),则从那些接收器读取时间的应用程序也可以确定闰秒通知。请注意,时间代码接收器只能将闰秒通知转发到应用程序,同时正确计时。正确处理闰秒是应用程序和(/或)操作系统的任务。

从1972年到2020年,平均每21个月就插入一次闰秒。然而,间隔是非常不规则的,而且明显在增加。在1999年1月1日至2004年12月31日的六年中没有闰秒,但在1972-1979年的八年中有九个闰秒。自1972年协调世界时正式使用至今,全球已经实施了27次正闰秒调整,最近一次的闰秒调整是格林尼治时间2016年12月31日。从协调世界时正式使用以来,地球自转一直处于不断减慢的趋势,因此迄今为止的闰秒都是正闰秒。但相关科研发现,自2020年年中以来,地球自转速率呈现加快趋势,这意味着未来也可能会出现负闰秒目前TAI与UTC的秒差为37:
# Value of TAI-UTC in second valid beetween the initial value until
# the epoch given on the next line. The last line reads that NO
# leap second was introduced since the corresponding date
# Updated through IERS Bulletin 64 issued in July 2022
#
#
# File expires on 28 June 2023
#
#
# MJD Date TAI-UTC (s)
# day month year
# --- -------------- ------
#
41317.0 1 1 1972 10
41499.0 1 7 1972 11
41683.0 1 1 1973 12
42048.0 1 1 1974 13
42413.0 1 1 1975 14
42778.0 1 1 1976 15
43144.0 1 1 1977 16
43509.0 1 1 1978 17
43874.0 1 1 1979 18
44239.0 1 1 1980 19
44786.0 1 7 1981 20
45151.0 1 7 1982 21
45516.0 1 7 1983 22
46247.0 1 7 1985 23
47161.0 1 1 1988 24
47892.0 1 1 1990 25
48257.0 1 1 1991 26
48804.0 1 7 1992 27
49169.0 1 7 1993 28
49534.0 1 7 1994 29
50083.0 1 1 1996 30
50630.0 1 7 1997 31
51179.0 1 1 1999 32
53736.0 1 1 2006 33
54832.0 1 1 2009 34
56109.0 1 7 2012 35
57204.0 1 7 2015 36
57754.0 1 1 2017 37


闰秒处理方案

1)运行NTP的系统

系统如果使用 NTP(网络时间协议)守护进程(ntpd)将其本地计时与 NTP 服务器同步,则都应自动进行闰秒调整。进行闰秒调整的前一天,NTP 服务器应通知其客户端第二天的 23:59:59 UTC 会发生发生闰秒,Linux 内核应通过两次显示第 60 秒或彻底删除它,以便添加或者删除额外一秒。因此,在闰秒调整期间,运行 NTP 的系统应有如下计时显示:
2015-06-30 23:59:59 UTC
2015-06-30 23:59:59 UTC
2015-06-30 00:00:00 UTC

发生闰秒时,内核会在系统 log 中写入信息:
Jul 1 07:59:59 TENCENT64 kernel: [579201.951291] Clock: inserting leap second 23:59:60 UTC

使用ntpdate命令方式,与ntp服务器进行时间同步的系统,将不会通过ntp服务器接收到闰秒通知,而是在系统管理员指定的时刻与ntp服务器进行时间同步。例如,系统管理员设定每小时的第52分与ntp服务器进行时间同步,那么在7月1日08:00 CST到09:52之间,系统时间与ntp服务器时间会相差1秒(快1秒)。

2)运行PTP的系统

PTP(精确时间协议)中交换的时间戳通常采用不包含闰秒的TAI(国际原子时);但 ptp4l 和 phc2sys 将设置内核标签,插入闰秒以便系统时钟继续以 UTC 运行。然后该内核就可以正常插入闰秒。

3)未运行NTP或者PTP的系统

默认情况下,不使用 NTP 或者 PTP 同步其计时的 Linux 系统不会修正闰秒,且这些系统报告的时间与修正闰秒后的 UTC 时间有一秒钟的差别。闰秒发生后应手动重置时钟。

您还可以将 tzdata 更新至最新版本,将 /usr/share/zoneinfo/right 目录层级中的正确文件复制到 /etc/localtime,并将时钟重置到正确的本地时间,以便将这些系统配置可正确报告时间。  /usr/share/zoneinfo/right 中的文件包含自该世纪开始,从 1970 年 1 月 1 日 00:00:00 UTC 发生的所有闰秒修正的本地时间信息。  /usr/share/zoneinfo 中的其他时区文件未添加闰秒修正。 从1972年至今,共添加了 27次闰秒。

例如:如果某个系统位于中国时区,您可以将其重新配置为通过运行以下命令报告闰秒修正时间,
cp /usr/share/zoneinfo/right/Asia/Shanghai /etc/localtime

例如在TS2系统中,tzdata包的版本为tzdata-2015a-1.tl2.noarch,执行完上述拷贝后,则会在闰秒发生时间2015年7月1日8点自动插入闰秒。

4)windows系统
早期的Windows版本(Win10版本以前) 时间服务并不表示 Leap 指标的值,当 Windows 时间服务接收到的数据包,包括闰秒。因此,闰秒发生后,正在运行 Windows 时间服务的 NTP 客户端会比实际时间快一秒。这种时间差异在下次同步时解决。

从 Windows 10 Redstone 5 和 Windows Server 2019 起,微软的操作系统能以更精确、UTC 兼容和可追踪的方式处理闰秒。不过从2017年至今,没有发生过闰秒了。


历史影响
对于日常生活而言,正常的上班、下班、工作、学习,生命中偏差的这一秒无关痛痒。然而闰秒对于精确要求时间的行业如航空、航天、军工等,会产生较大影响。对于服务器清一色linux系统的互联网行业而言,闰秒可能会造成机器cpu突然增高,机器宕机、对应的服务挂掉。随着linux的普遍使用,闰秒的影响也被越来越多的被关注。

历史上,因为linux内核的一些问题,闰秒对系统造成多次影响。比如CPU利用率高会给生产环境带了不少挑战。2012年实施闰秒时,国外不少知名网站出现了临时服务中断。当2015年闰秒再度来临时,工程师们修复了部分2012年出现的问题,但却东窗事发——发现了新的问题。后续亦是如此。闰秒让互联网企业如鲠在喉。

1) linux-2.6.22以前内核版本的闰秒死锁

07年的commit:
http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commitdiff;h=746976a301ac9c9aa10d7d42454f8d6cdad8ff2b;hp=872aad45d6174570dd2e1defc3efee50f2cfcc72

每次时钟中断触发时会调用 tick_do_update_jiffies64 更新 jiffies 的 值。因此在更新前对 xtime_lock 加了写锁。闰秒产生时,开发者需要修正 jiffies 的值。在 tick_do_update_jiffies64 里面最终会调用到 second_overflow 这个函数,以处理润秒。在函数 second_overflow 里面,处理润秒的增加和减少前都调用了一个 clock_was_set 函数。该函数内部,请求了 xtime_lock 的读锁。此时,与先前的写锁发生死锁。

该patch在linux内核版本2.6.22中引入,所以只有2.6.22内核之前的系统可会出现该问题,也就是影响sles10和centos5.5系统。在sles10和centos5.5中,clock_was_set()因不支持高精度时钟而被定义为空,所以不造成影响。

2)linux-2.6.25到2.6.27内核版本的系统死锁

Bug 479765 - Leap second message can hang the kernel 描述了leap second会对系统产生影响的原因:
当一个leap second被插入或删除时,内核会打印一条相关信息:
[69596.647516] Clock: inserting leap second 23:59:60 UTC
而该信息的打印会因xtime_lock而造成系统死锁。

下面是2.6.26内核下该问题出现时的栈信息(this is with Fedora 8 and
kernel kernel-2.6.26.6-49.fc8.x86_64):
#0  ktime_get_ts (ts=0xffffffff8158bb30) at include/asm/processor.h:691
#1  0xffffffff8104c09a in ktime_get () at kernel/hrtimer.c:59
#2  0xffffffff8102a39a in hrtick_start_fair (rq=0xffff810009013880, 
    p=<value optimized out>) at kernel/sched.c:1064
#3  0xffffffff8102decc in enqueue_task_fair (rq=0xffff810009013880, 
    p=0xffff81003fb02d40, wakeup=1) at kernel/sched_fair.c:863
#4  0xffffffff81029a08 in enqueue_task (rq=0xffffffff8158bb30, 
    p=0xffff81003b8ac418, wakeup=-994836480) at kernel/sched.c:1550
#5  0xffffffff81029a39 in activate_task (rq=0xffff810009013880, 
    p=0xffff81003b8ac418, wakeup=20045) at kernel/sched.c:1614
#6  0xffffffff8102be38 in try_to_wake_up (p=0xffff81003fb02d40, 
    state=<value optimized out>, sync=0) at kernel/sched.c:2173
#7  0xffffffff8102be9c in default_wake_function (curr=<value optimized out>, 
    mode=998949912, sync=20045, key=0x4c4b40000) at kernel/sched.c:4366
#8  0xffffffff810492ed in autoremove_wake_function (wait=0xffffffff8158bb30, 
    mode=998949912, sync=20045, key=0x4c4b40000) at kernel/wait.c:132
#9  0xffffffff810296a2 in __wake_up_common (q=0xffffffff813d3180, mode=1, 
    nr_exclusive=1, sync=0, key=0x0) at kernel/sched.c:4387
#10 0xffffffff8102b97b in __wake_up (q=0xffffffff813d3180, mode=1, 
    nr_exclusive=1, key=0x0) at kernel/sched.c:4406
#11 0xffffffff8103692f in wake_up_klogd () at kernel/printk.c:1005
#12 0xffffffff81036abb in release_console_sem () at kernel/printk.c:1051
#13 0xffffffff81036fd1 in vprintk (fmt=<value optimized out>, 
    args=<value optimized out>) at kernel/printk.c:789
#14 0xffffffff81037081 in printk (
    fmt=0xffffffff8158bb30 "yj$\201????\2008\001\t") at kernel/printk.c:613
#15 0xffffffff8104ec16 in ntp_leap_second (timer=<value optimized out>)
    at kernel/time/ntp.c:143
#16 0xffffffff8104b7a6 in run_hrtimer_pending (cpu_base=0xffff81000900f740)
    at kernel/hrtimer.c:1204
#17 0xffffffff8104b86a in run_hrtimer_softirq (h=<value optimized out>)
    at kernel/hrtimer.c:1355
#18 0xffffffff8103b31f in __do_softirq () at kernel/softirq.c:234
#19 0xffffffff8100d52c in call_softirq () at include/asm/current_64.h:10
#20 0xffffffff8100ed5e in do_softirq () at arch/x86/kernel/irq_64.c:262
#21 0xffffffff8103b280 in irq_exit () at kernel/softirq.c:310
#22 0xffffffff8101b0fe in smp_apic_timer_interrupt (regs=<value optimized out>)
    at arch/x86/kernel/apic_64.c:514
#23 0xffffffff8100cf52 in apic_timer_interrupt ()
    at include/asm/current_64.h:10
#24 0xffff81003b9d5a90 in ?? ()
#25 0x0000000000000000 in ?? ()



从上面的栈信息我们可以发现:该问题的出现原因是当对leap second进行操作(插入或删除)之前,已经获取了xtime_lock锁;而之后在调用printk()打印日志信息时,printk()中会尝试唤醒klogd内核线程,在唤醒过程中会调用到公平调度类的相关函数,其中会调用ktime_get()获取时间信息,其中会再次尝试获取xtime_lock锁,从而造成死锁

该现象部分因为hrtick_start_fair()函数的引入。是由commit 8f4d37ec (high-res preemption tick)引发,这大概在2.6.25版本引入。但是在2.6.25之前的内核,不会发生这个死锁。

2.6.28版本引入了commit b845b517。printk()中的wake_up_klogd()不会直接wake_up klogd(),也就不会触发后续的xtime_lock,最终避免了死锁的发生。所以,该原因引起的系统死锁只可能发生在linux内核2.6.25到2.6.27版本下

Sles11使用2.6.27内核,属于比较危险的部分内核。但是Novell声称已经引入了commit b845b517b5e3706a3729f6ea83b88ab85f0725b0,因而不存在该问题,而且几个小时的实验后系统仍然正常。

此问题影响的版本还有 RHEL4:kernel-2.6.9.89.EL之前的版本,RHEL5.3:kernel-2.6.18-128.37.1.el5之前的版本。现网centos5.5使用的内核版本是2.6.18-194.el5,其不受影响。

3)linux-3.4内核版本的系统活锁

08年的commit中为了解决之前遇到的leap second问题而将对leap second的处理从second_overflow()中独立出来,使用定时器来完成此工作。

但是12年的commit认为该patch存在如下可能的livelock场景:
CPU 0                                                                         CPU 1
do_adjtimex()
spin_lock_irq(&ntp_lock);
process_adjtimex_modes();                                   timer_interrupt()
process_adj_status();                                        do_timer()
ntp_start_leap_timer();                                     write_lock(&xtime_lock);
hrtimer_start();                                             update_wall_time();
hrtimer_reprogram();                                     ntp_tick_length()
tick_program_event()                                     spin_lock(&ntp_lock);
clockevents_program_event()
ktime_get()
seq = req_seqbegin(xtime_lock);



问题在于,引入ntp_lock的commit(http://patches.linaro.org/5122/)
是在3.4内核版本,且在3.4内核得到了修复。所以此问题对3.4以前和以后的内核无影响。

08年的commit:https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=7dffa3c673fbcf835cd7be80bb4aec8ad3f51168
12年的commit:
https://lkml.org/lkml/2012/3/15/616

4)linux-2.6.32内核插入闰秒可能出现高CPU消耗

2012年的闰秒插入当时导致了一些互联网公司的服务器高cpu消耗,其问题根源在以下网址得到了阐述:https://lkml.org/lkml/2012/7/1/203
leap-a-day.c为一个小测试程序,编译后加-s参数运行,可每10秒插入或者删除一个闰秒,用户可自行下载编译测试。2015年7月1日的闰秒将会出现以下现象:
Setting time to Wed Jul  1 07:59:50 2015
Scheduling leap second for Wed Jul  1 08:00:00 2015
Wed Jul  1 07:59:57 2015 +     98 us (3883)     TIME_INS
Wed Jul  1 07:59:57 2015 + 500248 us (3883)     TIME_INS
Wed Jul  1 07:59:58 2015 +    366 us (3883)     TIME_INS
Wed Jul  1 07:59:58 2015 + 500483 us (3883)     TIME_INS
Wed Jul  1 07:59:59 2015 +    598 us (3883)     TIME_INS
Wed Jul  1 07:59:59 2015 + 500740 us (3883)     TIME_INS
Wed Jul  1 07:59:59 2015 +    910 us (3883)     TIME_OOP
Wed Jul  1 07:59:59 2015 + 501046 us (3883)     TIME_OOP
Wed Jul  1 08:00:00 2015 +   1214 us (3884)     TIME_WAIT
Wed Jul  1 08:00:00 2015 + 501359 us (3884)     TIME_WAIT
Wed Jul  1 08:00:01 2015 +   1481 us (3884)     TIME_WAIT
Wed Jul  1 08:00:01 2015 + 501599 us (3884)     TIME_WAIT
Wed Jul  1 08:00:02 2015 +   1650 us (3884)     TIME_WAIT



我们测试后发现,在TS1.2发行版下,可出现“ERROR: hrtimer early expiration failure observed”提示。
/* Test for known hrtimer failure */
void test_hrtimer_failure(void)
{
         struct timespec now, target;
         clock_gettime(CLOCK_REALTIME, &now);
         target = timespec_add(now, NSEC_PER_SEC/2);
         clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &target, NULL);
         clock_gettime(CLOCK_REALTIME, &now);
 
         if (!in_order(target, now)){
                   printf("ERROR: hrtimer early expiration failure observed.\n");
         }




分析代码可以发现:使用clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &target, NULL);这种定时器方式,在插入闰秒后,该定时器本应该0.5秒到期,却立刻到期。本质原因是内核中记录时间的数据结构中并没有表达闰秒的地方,因此在增加闰秒时需要特别调整这些数据结构。而很多定时器并不直接使用“绝对”时钟而使用相对的时间间隔,这样,在定时器代码中就应该对闰秒做额外的检查。

但问题是这样的检查之前被删掉。对于许多应用来说,定时器的一次提前触发并不是什么问题。但有些定时器则不然,他们会反复启动自己,这样的后果就是它们反复地被快速唤醒,于是系统负载就出现了观察到的尖峰现象。闰秒的插入没有调用clock_was_set(),来提醒hrtimer子系统改变。定时器在插入闰秒后,其基准比系统时间快一秒,因此会提前一秒到期。

在观察到cpu高消耗后,解决方法很简单,执行下述命令即可:
date -s "`date`"  

其原理就是date再设置一下当前系统时间,clock_settime(CLOCK_REALTIME,&ts)会调用clock_was_set()。为了应对ntpd同步可能出现的该问题,我们在2015年特意编写了一个解决程序,该程序经过编译后可以添加到crontab任务:
58 7 1 7 * /data/solve_hrtimer_failure.o > /data/solve_hrtimer_failure.log 2>&1

在7月1日7点58分开始,每隔100ms检测闰秒是否插入了,当插入闰秒后,该程序调用clock_settime函数,进而修复了该问题。

取消闰秒

1)为何取消闰秒

对闰秒最为敏感的莫过于计算机相关领域。由于闰秒的出现没有固定规律,对应的时间调整无法从一开始就写在计算机程序里。在万物互联时代,很多领域都依托计算机网络传输信息,实施闰秒也会影响航空、通信、金融及其他需要精准对时的领域。

今年7月Meta公司两名工程师发文称:“闰秒是一种弊大于利的冒险做法,我们认为现在是时候引入新技术来取代它了。”这一表态引来各大公司称道。


2)取消闰秒的后续可能
负责协调世界时的国际计量局(BIPM)表示,科学家和政府代表18日在法国举行的一次会议上投票决定到2035年取消闰秒。BIPM时间部门负责人帕特里齐亚·塔维拉表示,这项“历史性决定”将允许“秒数连续流动,而不会出现目前由不规则闰秒造成的不连续性。

闰秒是目前把世界时和国际原子时联系起来的手段。由于世界时是基于地球自转确定的,又称天文时或太阳时。没有闰秒意味着人们使用的时间与地球自转、太阳位置不关联,时间和天文学呈现割裂状态。

第27届国际计量大会决议要求多机构协商,提出一个可以将协调世界时持续至少百年的新方案并制定实施计划,纳入下一届大会的决议草案中。根据决议,闰秒将暂时继续正常添加。但到2035年,世界时和国际原子时之间的差异将被允许增长到大于一秒的值。

也许解决这个问题的可能方法是让世界时和国际原子时之间的差异增加到一分钟,但专家估计调整时长在50到100年之间。而有提议指出,无需在时钟上增加闰分钟,而是将某一天的最后一分钟变为需要两分钟;也有人建议停止校正,同时公布世界时和国际原子时之间不断增长的时刻差。

腾讯工程师技术干货直达:

1、算法工程师深度解构ChatGPT技术

2、10分钟!从架构视角读懂K8s

3、探秘微信业务优化:DDD从入门到实践

4、祖传代码重构:从25万行到5万行的血泪史

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存