閏秒終於要取消了!一文詳解其來源及影響

騰訊雲開發者發表於2022-12-23

圖片

導讀 | 第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.57
2015-07-01 07.59.58
2015-07-01 07.59.59
2015-07-01 07.59.60 <-- 閏秒
2015-07-01 08.00.00
2015-07-01 08.00.01
2015-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.000
2015-07-01  07:59:58.500
2015-07-01  07:59:59.000
2015-07-01  07:59:59.500
2015-07-01  08:00:00.000 <-- 插入閏秒
2015-07-01  07:59:59.000
2015-07-01  07:59:59.500
2015-07-01  08:00:00.000
2015-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=linu...;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/l...

12年的commit:

https://lkml.org/lkml/2012/3/...

4)linux-2.6.32核心插入閏秒可能出現高CPU消耗

2012年的閏秒插入當時導致了一些網際網路公司的伺服器高cpu消耗,其問題根源在以下網址得到了闡述https://lkml.org/lkml/2012/7/...

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萬行的血淚史

圖片

相關文章