问题背景
我们的机房与培训教室共用一套无盘系统(Citrix Provisioning Services,下称 PVS),由两台 PVS 服务器组成一个 Farm,通过网络流式推送同一份系统镜像给两百多台终端,终端本地不装硬盘系统,开机走 PXE 从服务器拉起。这套系统跑了好几年,一直停留在旧的 LTSR 版本上,厂商支持周期临近结束,安全部门要求年内升级到新 LTSR。
按官方文档的说法,同一个 Farm 内可以做滚动升级(rolling upgrade):先升级其中一台服务器,另一台继续对外提供服务,业务不中断,升完第一台再升第二台。我们照着这个思路排了变更:周三晚上八点升级 PVS01,PVS02 保持旧版本兜底,第二天观察一天没问题,周四晚上再升 PVS02。
变更当晚 PVS01 升级过程本身很顺利,Farm 数据库自动升级完成,控制台能正常打开,vDisk 列表和终端列表都在,抽测三台终端开机也正常。我们判断变更成功,收工。真正的问题在第二天早上八点集中爆发。
故障现象
早上八点前后,教室和办公区陆续有人报"电脑开不了机"。到现场看,屏幕停在 PVS 的启动界面上,最后一行是 Contacting Login Server...,卡住十几秒后跳出 Login Server not available 或直接黑屏重启,循环往复。
关键特征有三个,也正是它们把我们一度带偏:
第一,不是全挂,大约一半终端受影响。同一个教室里,前排三台能正常进系统,后排两台起不来;重启若干次之后,原本起不来的偶尔又能起来一次。这种"随机性"让第一反应是网络抖动或 DHCP 问题。
第二,已经开着的机器完全不受影响。前一晚没关机的几台终端整夜运行、业务正常,说明服务器端的流式推送(streaming)通道本身是通的,问题只发生在开机启动阶段。
第三,故障范围与地理位置无关。不同楼层、不同接入交换机下都有成功和失败的机器混杂,排除了单台交换机或单个 VLAN 的问题。
因为是早高峰,影响面直接扩散到两个教室的上课,压力很大。
排查过程
无盘终端的启动链路比普通 PC 长得多,必须按顺序逐段验证,不能凭感觉跳。我们先在白板上把链路画出来:
|
|
第一段,DHCP。 在故障终端所在网段抓包,看到 DHCPOFFER 正常返回,IP、掩码、网关都对,Option 66 指向 10.20.1.30,Option 67 是 ARDBP32.BIN。DHCP 没问题。这里要注意一个细节:Option 66 指向的 10.20.1.30 既不是 PVS01 也不是 PVS02,而是当年为了统一 PXE 管理,单独搭的一台第三方 TFTP 服务器。这个细节后面成了关键。
第二段,TFTP。 抓包看到 bootstrap 文件下载成功,TFTP 传输完整,没有超时和重传。到这里一切正常。
第三段,登录 Stream Service。 抓包看到终端向 UDP 6910 端口发出登录请求,服务器有响应,但立刻返回了一个错误包,随后终端超时。这一步是断点。
进一步比对成功和失败的机器,规律终于浮出来:抓包里失败的终端,请求打到的是 10.20.1.11(PVS01,已升级);成功的终端,请求打到的是 10.20.1.12(PVS02,未升级)。所谓"随机一半",正是 bootstrap 里配置了两台服务器做冗余,终端按算法随机挑一台登录的结果。
方向锁定 PVS01。登上去翻 Stream Service 日志,找到明确记录:
|
|
到这一步,根因基本清楚了:终端拿到的 bootstrap 是旧版本的,跟升级后的 PVS01 协议版本不匹配,被拒绝。
那为什么是旧版本?我们到 PVS01 本地看 C:\ProgramData\Citrix\Provisioning Services\Tftpboot\ARDBP32.BIN,文件时间戳是昨晚,确实被升级程序更新过。再登到那台独立 TFTP 服务器 10.20.1.30 上看同名文件——时间戳是三年前。
真相大白:升级程序只更新了 PVS 服务器本地那份 bootstrap,而终端实际从独立 TFTP 服务器取文件,那份是当年手工拷过去的,从此再没人动过。升级把服务器端协议版本抬高了,客户端 bootstrap 却停在原地。
顺手还验证了另一个更麻烦的问题:我们把新 bootstrap 手工拷到 TFTP 服务器上试了一台机器,结果这台机器反过来在登录 PVS02 时失败了——新 bootstrap 同样不被旧版服务器接受。也就是说,只要 Farm 处于"一台新、一台旧"的混合版本状态,无论用新旧哪份 bootstrap,都必然有一半终端起不来。所谓跨大版本的滚动升级窗口期,实际上是不可用的。
解决方案
早高峰优先止血,不追求一步到位:
- 把流量全部压到未升级的 PVS02。在 TFTP 服务器上保留旧 bootstrap,同时在 PVS01 上停掉 Stream Service,让所有终端只能登录 PVS02。执行后五分钟内,故障终端重启即可正常进系统,上课恢复。此时集群失去冗余,但功能完整。
- 当晚补完另一半升级。把 PVS02 也升到同版本,Farm 脱离混合状态。
- 用控制台重新生成并分发 bootstrap。在 Console 里执行
Configure Bootstrap,重新写入两台服务器 IP 生成新文件,然后手工同步到10.20.1.30的 TFTP 目录,并把这一步固化进升级检查单。 - 灰度升级终端侧软件。vDisk 里的 Target Device Software 版本也需要跟服务器匹配。我们用 vDisk 的 versioning 功能建了一个 maintenance 版本,只在里面升级 Target Device Software,先放一台终端验证,通过后再 promote 为 Production,保留上一版本随时可回退。
全部完成后,两台服务器恢复冗余,随机挑哪台登录都能正常启动。
根因分析
表面根因是 bootstrap 文件版本不同步,深层根因有两条:
一是启动链路的所有权被切割了。 PVS 的开机链路横跨 DHCP、TFTP、Stream Service 三个组件,升级程序只对自己管辖的文件负责,管不到企业自建的第三方 TFTP 服务器。那份三年前手工拷贝的 bootstrap 属于"没有主人的配置",既不在升级流程里,也不在任何一份文档中。
二是把"支持滚动升级"理解错了。 官方所说的滚动升级,适用范围是同一大版本内的累积更新;跨 LTSR 大版本时,客户端协议版本本身发生了变化,混合版本 Farm 在升级窗口期内是不受支持的状态。我们套用了一个前提不成立的方案,并把"抽测三台正常"当成了验证通过——而那三台恰好都随机登录到了未升级的 PVS02 上。
预防措施
- 变更前画完整链路图,逐点列出版本与存放位置:DHCP Option 66/67 指向哪里、bootstrap 实际由谁提供、文件版本、服务器列表、Target Device Software 版本、vDisk 格式版本。任何一处"不知道谁在维护"的配置都必须先认领。
- 跨大版本升级不做原地滚动,改并行 Farm 灰度:新建一套新版本 Farm,用 DHCP 作用域分批把终端切过去,出问题改回 Option 66 即可整体回退,比原地升级安全得多。
- 验证样本必须覆盖全部分支:有负载均衡或多服务器的场景,抽测时要强制指定每一台服务器各测一遍,不能依赖随机分配。这次的教训就是抽测三台全落在旧服务器上。
- 每次变更预置五分钟回退方案:本次的回退动作是"停掉新服务器的 Stream Service",一条命令、影响可控。变更单上没写回退步骤的,不允许开始执行。
- 补监控:对 TFTP 请求数、Stream Service 登录失败计数做采集,开机失败率突增时告警,避免只能靠用户报障发现。
总结
这次故障的技术点并不深,一个文件版本不匹配而已,但它踩中了变更管理里最典型的两个坑:一是链路上存在无人认领的配置,升级工具覆盖不到,文档里也查不到;二是把厂商在特定前提下的能力(滚动升级)当成了普适保证,跨版本时前提早已不成立。
排查过程本身反倒是最有价值的部分——面对"一半好一半坏"的随机现象,不去猜网络抖动,而是把启动链路拆成 DHCP、TFTP、登录三段逐段抓包验证,很快就能把断点收敛到某一跳上;再对比成功与失败样本的差异(打到哪台服务器),根因几乎是自己浮出来的。对于这类跨多个组件的长链路系统,画图、分段、比对永远比经验直觉更可靠。