共计 12197 个字符,预计需要花费 31 分钟才能阅读完成。
导读 | 第 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 万行的血泪史