九台服务器的告警几乎在同一时间涌进运维群,那一刻我的血压和屏幕上的红色一起飙升。我们团队维护的是一套自建的KVM虚拟化集群,三台物理宿主机上跑着九个业务虚拟机,OA、数据库、代码仓库、内网远程协助,全挤在这一层薄薄的虚拟化世界里。周一早上九点,业务方说"系统全挂了",我第一反应是宿主机出问题了,或者是存储阵列炸了。可等我连上虚拟化平台管理界面,看到的却是更诡异的一幕:物理机全活着,虚拟机却集体处于"半死不活"状态——有的能ping通但应用报错,有的在反复重启,有的干脆连SSH都进不去。服务器是虚拟的,惊魂却是实实在在的。这篇文章就把这次事故的完整排查过程、最终根因和事后复盘写清楚,希望能给同样维护虚拟化集群的朋友提个醒。
1. 事故现场:9台服务器集体"罢工"是什么样的
1.1 周一早上的告警风暴
九点过几分,值班群里的告警开始连片往外蹦。"HTTP服务不可用"、"数据库连接失败"、"SSH登录超时"、"远程协助服务离线"……一条接一条,几乎覆盖了我们集群里跑的所有业务。我们总共有三台物理宿主机,每台上面跑三台虚拟机,一共九台。这九台里有内部OA,有MySQL主从,有GitLab代码仓库,有一台自己搭的RustDesk远程协助服务器,还有一台文件共享网关。放在平时,一两台VM出问题是很正常的,重启一下或者回滚个更新就完事。但九台一起出问题,而且症状各不相同,这事就没那么简单了。
业务方的电话很快打了过来,问是不是又在割接网络,领导也过来催恢复时间。我们几个运维围在工位前,第一反应是先把故障面搞清楚:哪些服务完全不可用,哪些还能撑一阵,哪些是连带反应。我让一个同事逐个去ping那九台VM的IP,结果很有意思——有的通,有的不通,通的那些里面,部分能登录,部分登录到一半就卡死。这种"半身不遂"的表现,非常不像纯粹的物理宕机或网络中断,更像是什么东西在应用层和虚拟化层之间"捣鬼"。
1.2 第一反应:物理层还是网络层?
按照运维的常规思路,先查物理硬件,再查网络链路,最后才碰虚拟化层。我打开三台物理机的带外管理界面,也就是IPMI/BMC,三台宿主全部在线,电源、温度、风扇没有异常,CPU和内存使用率也没有爆表。这时候心里稍微踏实了一点,至少不是机房断电或者硬件集体烧了。
接着看网络。核心交换机上,各端口状态都是UP,流量有波动但不算异常,错误包和丢包统计也都是正常水平。再从一台正常物理机上traceroute到九台VM的网关,路径干干净净的,没有环路也没有黑洞。物理层面的嫌疑基本排除之后,我登上了虚拟化平台的管理界面。就在这时,真正让人冒冷汗的画面出现了。
1.3 登录虚拟化平台:一片血红的虚拟机列表
虚拟化平台是开源的Proxmox VE(PVE),基于KVM,我们用了很多年。登录进去,集群状态那一栏直接显示红色:部分节点心跳异常。再点开虚拟机列表,九台VM的状态简直是"群魔乱舞":有的显示运行中,但控制台黑屏;有的显示已停止,可进程还在宿主机上;有的显示"迁移中",还有几台显示"高可用服务恢复中"。我刷新了一下页面,这些状态还在变化,像是有一群看不见的手正在把虚拟机在节点之间推来搡去。
看到这个画面,我心里反而定了——问题出在虚拟化集群的控制面,而不是虚拟机的业务本身。控制面认为某些节点"不健康",于是开始执行隔离、迁移、重启的动作,但因为判断依据是错的,动作就变成了互相拉扯。接下来要做的,是搞清楚控制面到底因为什么误判了节点健康状态。
2. 排查过程:一场从"虚拟"到"现实"的捉迷藏
2.1 物理机全活着,虚拟化层面却在"内讧"
我先在宿主机上执行了virsh list --all,确认九台VM的进程其实都还在。但奇怪的是,部分VM的磁盘锁文件处于异常状态,有的报"Cannot acquire lock",有的报"Another QEMU process is running"。这说明虚拟机底层的数据没丢,只是被集群的锁机制给"卡住"了。虚拟化集群为了保证同一台虚拟机不会在两个节点上同时运行,会在共享存储上写锁文件;如果节点间的心跳判断出了偏差,集群就认为某个节点已经失联,于是尝试在别的节点启动同一台VM,结果锁还没释放,两边就撞车了。
这种时候最忌讳的就是手动去virsh destroy或者virsh start硬重启。因为集群的HA管理器可能正在执行自己的重试逻辑,你手动操作会跟它打架,轻则启动失败,重则把磁盘元数据弄坏。我们的策略是:先观察,收集日志,搞清楚HA到底在干什么,再决定要不要插手。
2.2 集群事件日志里的"心跳超时"与"隔离风暴"
我打开集群日志,果然看到最近几分钟内有大量和心跳相关的记录:"Corosync lost token"、"Fencing node"、"HA service recover and migrate"。这些术语看着吓人,其实原理不难理解:Corosync是PVE集群节点之间维持心心跳的组件,每个节点会定期向其他节点"报平安";如果在一段时间内收不到对方的回应,就判定对方故障,触发fence动作,也就是把"故障"节点隔离出去,同时把上面运行的虚拟机迁移到其他健康的节点。
这次的问题在于:九台虚拟机全部处于"被迁移"的状态,而且迁移来迁移去都落不了地。一是因为锁文件冲突,二是集群里三个节点的心跳状态本身就互相矛盾。日志里甚至能看到某个节点刚被判定"失联",几秒钟后又恢复了心跳,然后另一个节点又被判定失联。这种反复横跳,在集群运维里非常典型——节点之间并不是真的失联,而是它们互相"看"对方的依据出了问题。
2.3 时间偏差:藏在监控盲区里的"小线索"
顺着"心跳判断依据"这个方向,我怀疑到了节点间时间不一致的可能性。于是我在三台宿主机上分别跑了timedatectl和chronyc tracking,这一跑把自己吓了一跳:宿主机A的系统时间比标准时间快了6分多钟,宿主机B慢了将近1分钟,宿主机C倒是准的。也就是说,三台物理机之间的最大时间偏差已经接近7分钟,早就超过了集群默认的容忍范围。
再进虚拟机里看,发现部分VM的时间也偏得离谱,而且不是一个方向地偏,是有快有慢。有个同事说了一句:"原来不是服务器挂了,是时间疯了。"这句话说得很通俗,但方向上完全正确。到这一步,我已经基本确定:这场"集体罢工"的核心,就是时间同步链路出了问题。接下来要搞清楚的是,时间偏差到底怎么引发这一连串故障的,以及为什么我们的监控完全没有发现。
3. 根因复盘:一个时间源故障,怎么就能让9台VM集体停摆
3.1 虚拟化环境的时间同步链条有多脆弱
先说结论:我们内网有一台自建的NTP时间服务器,所有宿主机和大部分VM的时间都指向它。上周五那台服务器的磁盘出了坏道,服务进程已经悄悄死掉了,但没有触发任何告警。更糟的是,前段时间安全加固,防火墙把到外部公共NTP的访问给拦了,等于说内网再没有第二层时间来源。于是从上周五晚上开始,宿主机和VM就一直在靠各自的硬件时钟硬撑。
硬件时钟的漂移速度取决于设备晶振质量和使用环境,通常一天慢几秒到几十秒是很常见的。我们这三台宿主机误差如比大,是因为有的机器本身负载高、温度高,有的还启用了虚拟机的"主机时间同步"功能,错误的时间就像多米诺骨牌一样,从宿主机传到了虚拟机里。等到周一早上,宿主机A已经比标准时间快了6分钟多,而它上面那些启用了跟随宿主机时间的VM,自然也跟着偏出去了。至于其他VM,有自己单独跑NTP客户端的,反而误差小一些。这就解释了为什么九台VM的时间偏差"有快有慢"。
3.2 时钟漂移引发"认证雪崩"与"数据库分裂"
时间偏差达到6分钟以上,对我们这种内部环境是致命的。第一个直接受影响的,就是我们内网的统一认证体系。公司所有办公系统都接了Kerberos/LDAP域认证,Kerberos协议对客户端和服务端之间的时间偏差有严格的容忍上限,默认是5分钟。一旦偏差超过这个值,客户端向认证中心申请的票据(TGT)就会被判定为无效,表现就是:用户明明输对了密码,系统却一直报"用户名或密码错误"或"凭据已过期"。机房里的Linux服务器也大量依赖keytab认证访问,跟着一起登不上。所有依赖域账号登录的OA、文件共享、运维跳板机,瞬间全部失守。
第二个受创的是数据库。我们内部业务用的是MySQL主从架构,主库和备库之间通过半同步复制来确认数据写入。半同步复制有一个超时判断机制,备库认为主库"无回应"和双方之间的心跳超时,很大程度上和时间戳相关。时间偏差一大,备库频繁误判主库失联,触发自动切换;可备库自己时间也不准,切换过去之后新的主从关系又立刻出现异常,于是角色在两个节点之间来回倒,应用层的连接池也跟着全部失效。日志里全是"failed to switchover"和"seconds_behind_master暴涨"的记录。
第三个受害者是各种基于TLS证书和JWT令牌的组件。GitLab、RustDesk Server、还有我们自己的Web网关,它们在做HTTPS握手和令牌校验的时候,会对比证书的签发时间、令牌的有效期和系统当前时间。系统时间快了6分钟,很多令牌直接被判定"过期"或"尚未生效",服务端返回500错误或直接拒绝连接。RustDesk Server那台VM更惨,它本身时间偏了,导致所有客户端的TLS证书校验都失败,内网远程协助工具全线掉线,我们连远程排查的通道都少了一条。最后,虚拟化平台自身的HA、存储锁、备份快照调度这些系统,对时间同样非常敏感。时间一乱,各种"心跳超时""锁超时"就跟着出来了。
3.3 为什么"硬校时"是事故中的第二个坑
真相大白以后,有个同事已经急着要执行ntpdate -u 192.168.x.x去强制同步时间了。我赶紧拦住了他。这里必须说一句:在生产环境里最怕的就是"硬校时",也就是直接跳变系统时钟。系统时间在运行过程中突然往前跳或者往回拨几分钟,会带来一大堆连锁反应:运行中的进程记录的日志时间戳会混乱;依赖单调时钟的算法可能出现ID回拨;TCP的长连接定时器会瞬间触发超时;数据库的binlog时间戳和复制心跳也会被搅乱。更夸张的情况是时间往回拨,那简直是在给分布式系统"上刑"。
这次事故里,我们实际上已经踩到了这个坑的边缘。周末值班的同事发现宿主机时间偏了之后,曾经执行过ntpdate -s强制同步,但当时NTP源本身已经挂了,同步失败,等于白忙一场。如果当时NTP源是好的,直接从一个错误的时间硬跳到正确时间,业务侧多半会在那一瞬间出现大量连接中断,等于把故障面又扩大了一层。所以正确的恢复思路不是"越快校正越好",而是"在尽量小的影响范围内,分步骤、有控制地让时间归位"。这一点,直接决定了后续应急操作的顺序。
4. 应急处置:从惊魂到恢复的分步实战
4.1 止血:先隔离,再保关键业务
恢复时间之前,我们先把动作分了两步:第一步止血,第二步治本。止血要做的事情,是把还在反复迁移、反复重启的VM从HA管理里暂时摘出去,停止这种无意义的"抖动"。在Proxmox VE里,可以针对具体虚拟机执行ha-manager set <vmid> --state disabled,或者直接在管理界面把高可用服务暂停。注意,这个操作不是把VM关掉,只是让HA管理器不再干预它们的状态。摘除HA之后,原来卡住的锁文件需要清理,但一定要确认对应QEMU进程已经不存在,再用virsh undefine之类的方式小心处理元数据锁。我们在处理的时候留了个心眼,每摘一台VM之前都先去宿主上确认没有重复进程,避免误伤。
随后按业务优先级排序,数据库那两台VM优先生效。MySQL主从环境里我们已经看到角色在反复切换,先选一个时间相对准确的节点作为主库,把另一个节点上的复制线程强制停止,避免两边继续互相覆盖。业务侧则通过负载均衡临时把流量切到可用的只读节点或者干脆摘掉故障写入口,先保住"能读"和"能登录"这两个底线。那台RustDesk Server因为证书时间校验的问题完全起不来,也先停着,大家直接用带外管理连物理机操作。
4.2 修复时间同步链路:先修源,再逐级校准
止血动作做完,才开始治本。第一步是修好内网那台NTP服务器。我们过去检查,发现服务进程早已退出,磁盘挂载也异常,重启服务之前先把文件系统修复了一下。服务拉起来后,配置好从外部公共时间源同步,同时把防火墙里UDP 123端口的NTP出口规则重新放开。等这台时间服务器自己先校准准确,再让其他机器来找它同步,顺序绝对不能反过来。
接下来是宿主机。我们选择逐台校准,而不是三台一起操作。具体做法是:先停掉chronyd服务,执行ntpdate -u <内网NTP服务器IP>强制同步一次,然后立即启动chronyd,让它持续跟踪。之所以分台操作,是因为如果三台宿主机在同一秒完成时间跳变,那几秒钟内整个集群的网络会话、监控采集、数据库连接都会同时产生波动,影响反而集中;逐台来,至少能让一部分服务保持稳定。每台校准完成后,立刻执行chronyc tracking确认偏移量降到100毫秒以内,再继续下一台。虚拟机层的逻辑类似,但因为有些VM的时间是跟随宿主机的,我们同步完宿主机以后,再在VM里重启chronyd或ntp服务。这里有个细节,KVM平台默认不建议开"主机时间同步",因为虚拟机的RTC在迁移和关机重启时会产生跳变,正确的做法是让虚拟机自己跑NTP客户端。在这次事故里,部分VM恰恰是开了"跟随宿主机时间"才被错误时间带偏的,所以我们在恢复后把这些选项重新梳理了一遍。
4.3 确认HA集群虚拟机和数据库恢复正常的清单
时间校准完成后,并没有立刻恢复HA管理,而是先做了一次全面确认。我用下面这个清单逐项检查,确认没问题后,才把之前摘出去的VM重新纳入HA托管:
- 在每台宿主机上执行
pvecm status,确认三节点都在线,没有节点处于"disconnected"状态; - 执行
corosync-cfgtool -s,确认心跳令牌传递正常,没有大量重传; - 执行
chronyc tracking和timedatectl,确认所有节点偏移量都在正常范围; - 执行
virsh list --all,确认VM不会反复启动或迁移; - 进入MySQL主库执行
show slave status\G,确认Seconds_Behind_Master归零,复制链路重新建立; - 用域账号实际登录OA和文件共享系统,确认Kerberos认证恢复;
- 登录RustDesk Server,确认TLS连接恢复,远程协助软件能重新接入。
整个确认过程花了大约40分钟,比真正的时间校准还久。业务恢复初期还是有一些零散报错,主要是之前积压的失败任务和连接超时导致的,清理了一波进程,到中午之前全部恢复正常。那三个小时里最消耗人的其实不是技术操作,而是看不到头的不确定性。
5. 事后复盘:虚拟化集群时间管理的几条保命建议
5.1 时间源要冗余,权限要收敛
这次事故的第一个教训,就是时间源不能只有单一节点。内网NTP服务器挂掉之后,整个时间同步链条就断了,而且没有任何告警。现在我们的方案是至少保留两个互为备份的时间源:一台自建NTP服务器作为主源,另一台直接指向外部公共NTP池作为备用源。所有宿主机和VM的chrony配置里都同时写上主备两个源,chronyd本身会在主源失效后自动切换。同时,对NTP服务器的管理权限做了收敛,只有两名资深运维能上去改配置,避免再出现"周末好心同学强制校时"的操作。
5.2 监控里必须加"时间偏移"指标
这次事故最讽刺的是,我们监控了CPU、内存、磁盘、网络,唯独没有监控时间。时间偏移这种指标看起来不起眼,但它在虚拟化、数据库、分布式存储这类场景里,往往是最先出问题的信号。现在我们在所有宿主机和关键VM上都配置了时间偏移监控,用Prometheus的node_exporter采集node_time_seconds与本地标准时间比对,设置5秒即告警。Zabbix用户也可以通过监控项采集NTP状态,道理一样。别只监控宿主机,VM也要纳入,因为很多错误时间会从虚拟化平台复制到VM里。
5.3 应急预案里要有"时间故障"专属章节
以前我们的应急手册里只有网络故障、存储故障、虚拟机故障这些章节,没有专门写时间同步异常的处理流程。这次之后,我们把"时间类故障"单独列了一章,明确写了几个关键原则:严禁直接date -s跳变时间;禁止在多台机器上同时大规模硬同步;必须先恢复时间源,再逐级校准;NTP源失效超过一定时长必须走变更流程,评估对Kerberos和数据库的影响后再动手。这份预案在两个月后的一次演练中真派上了用场,整个处理过程比这次从容很多。
5.4 虚拟化平台的时间同步策略要明确
最后一条建议针对虚拟化平台本身:每台VM的时间同步策略是"跟随宿主机"还是"自己跑NTP",必须事前想清楚。我们现在的统一原则是:宿主机负责主机层面的时间同步,VM内部一律自己配置NTP客户端,除非极个别场景,否则不开启"主机时间同步"。因为在虚拟化环境里,宿主机和VM之间的时间关系本来就容易被忽略,而一旦宿主机时间出错,错误会沿着虚拟化链路迅速传播,最后变成"一批VM集体时间错乱"的场面。明确这个策略,至少能保证一个问题只出现在一个层面,而不是上下两层一起炸。
6. 时间相关故障排查速查表
6.1 常见症状与排查方向
把这次的经验整理成一个速查表,方便遇到类似问题的时候快速定位。表格里列的症状,每个都对应着我们这次实际遇到的场景:
| 故障现象 | 可能的根因方向 | 优先排查 |
|---|---|---|
| 多台VM同时能ping通但应用无法登录 | Kerberos/LDAP认证时间偏差超限 | 对比VM和KDC的系统时间 |
| 数据库主从反复切换、复制中断 | 主备库心跳超时、半同步复制超时 | 检查主备库时间偏差 |
| HA集群节点心跳失联、虚拟机反复迁移 | 控制面误判节点健康 | 检查所有节点系统时间偏差 |
| HTTPS/TLS连接瞬间大量失败 | 证书时间校验失败 | 查看服务端系统时间和证书有效期 |
| 定时任务、备份任务乱序执行 | 系统时间跳变或漂移 | 对比标准时间和任务触发时间 |
| 虚拟化平台报锁文件冲突 | 集群锁超时 | 检查节点间时间偏差和心跳状态 |
排查的第一步永远是timedatectl和chronyc tracking开道,这两条命令几十秒就能出结果。如果发现时间偏移超过一秒,立刻把它当成头等嫌疑,不要等它自己恢复。
6.2 常用排查命令速查
下面这些命令是我在生产环境里实测下来最有用的,都顺手记在这里:
# 查看系统时间和时区 timedatectl # 查看NTP同步状态和偏移量(chrony) chronyc tracking chronyc sources -v # 查看NTP同步状态(老系统用ntpd) ntpq -p # 查看集群心跳状态(Proxmox VE) pvecm status corosync-cfgtool -s # 查看虚拟机列表和状态(KVM) virsh list --all # 手动读取标准时间源(不直接修改时间,先确认差了多少) ntpdate -q <NTP服务器IP> # 强制同步时间(仅在确认影响可控、业务低峰且得到授权后使用) systemctl stop chronyd ntpdate -u <NTP服务器IP> systemctl start chronyd chronyc tracking有一点要再强调一遍:上面最后那个ntpdate -u强同步操作,在生产环境里属于"有风险动作",执行前确认影响面,执行时尽量逐台操作,执行后观察连接和日志的波动。如果偏差不大,更好的做法是让chronyd自动渐进校正,或者执行chronyc makestep在允许的范围内一次性校正,而不是用date -s去手动改系统时间。
写在最后的一点体会
我个人的体会是,这次事故最值钱的教训不是"把时间修好了",而是"再也不敢让时间源处于无人监管状态"。虚拟化让服务器的"生死"变成了软件层面的状态切换,也让故障变得像障眼法一样难以看穿。九台虚拟机集体罢工,物理服务器却一切都好,这种割裂的现场感,只有亲手经历过才懂。后来我们做的改进其实都很简单:时间监控加上了、NTP源做了冗余、应急预案补了章节、同步策略统一了。现在我每次进机房巡检,都会顺手跑一句chronyc tracking看偏移量,就像出门摸钥匙一样自然。这个习惯,是那次"惊魂"给我留下的最好纪念。