升级PVS服务器:一次无盘终端批量启动失败的排查与回退

PVS服务器跨版本升级后半数无盘终端开机卡在联系登录服务器,从启动链路逐段定位并回退的实录。

问题背景

我们的机房与培训教室共用一套无盘系统(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 长得多,必须按顺序逐段验证,不能凭感觉跳。我们先在白板上把链路画出来:

1
2
3
4
终端上电 → 网卡 PXE → DHCP 拿 IP + Option 66/67
        → 从 Option 66 指定的 TFTP 服务器下载 bootstrap 文件
        → bootstrap 里写死了 PVS 服务器列表,向其中一台的 Stream Service 登录
        → 服务器分配 vDisk → 开始流式读盘 → 进系统

第一段,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 日志,找到明确记录:

1
Login request rejected: unsupported client protocol version

到这一步,根因基本清楚了:终端拿到的 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,都必然有一半终端起不来。所谓跨大版本的滚动升级窗口期,实际上是不可用的。

解决方案

早高峰优先止血,不追求一步到位:

  1. 把流量全部压到未升级的 PVS02。在 TFTP 服务器上保留旧 bootstrap,同时在 PVS01 上停掉 Stream Service,让所有终端只能登录 PVS02。执行后五分钟内,故障终端重启即可正常进系统,上课恢复。此时集群失去冗余,但功能完整。
  2. 当晚补完另一半升级。把 PVS02 也升到同版本,Farm 脱离混合状态。
  3. 用控制台重新生成并分发 bootstrap。在 Console 里执行 Configure Bootstrap,重新写入两台服务器 IP 生成新文件,然后手工同步到 10.20.1.30 的 TFTP 目录,并把这一步固化进升级检查单。
  4. 灰度升级终端侧软件。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、登录三段逐段抓包验证,很快就能把断点收敛到某一跳上;再对比成功与失败样本的差异(打到哪台服务器),根因几乎是自己浮出来的。对于这类跨多个组件的长链路系统,画图、分段、比对永远比经验直觉更可靠。

使用 Hugo 构建
主题 StackJimmy 设计