天眼平台V3.2到V4.0升级实战:平滑升级与回滚全记录
“天眼”是我手里一套智能视频监控分析平台的名字。别笑干运维和AI这行的给系统起这种名字的太多了——监控、告警、流媒体、算法识别挂个“眼”字显得什么都能看到。实际情况是它负责接摄像头、做视频解码、跑算法识别、推告警公司里几十个点位都挂在它上面。这次要干的不是小打小闹而是从 V3.2 整盘升级到 V4.0前后折腾了小两周中间踩了不少坑。这篇文章就是完整升级复盘升级前怎么评估、升级怎么设计、回滚怎么留以及“版本升级了但好像没生效”这类问题到底怎么解决。如果你也在准备升级自研平台、或者被安排去做类似系统版本升级这篇可以直接当作战清单用。先说结论升级本身不难难的是你永远不知道旧系统里藏了多少“当年临时改的、后来没人记得”的东西。这次升级让我把家里翻了个底朝天值了。1. 升级前想清楚三件事1.1 为什么非升不可很多人一听到版本升级第一反应是“能用就别动”“又不是不能用”。这种心态可以理解但这次天眼升级真不是我们闲得慌。第一是技术债。旧版本底层用的框架已经停更很久CVE公共漏洞没人管想打个补丁都不知道去哪找。每次有同事提需求我们改代码的时候都像是在老地基上加楼层越加越心虚。第二是设备兼容性。公司新采购的一批摄像头走的是新协议旧版本的天眼对接不上。这就好比你装了智能门锁但手里还是老的机械钥匙门能开但只能开一半功能。第三是算法模型的换代。我们在用的目标识别模型要升级到新一代版本推理准确率确实提升明显但输出的数据结构变了坐标格式、置信度阈值语义都不同。模型可以换平台不跟着改等于新酒装旧瓶。第四是安全问题。信息安全扫描扫出来服务器底层组件一堆漏洞OpenSSH、OpenSSL都是老版本必须跟着系统组件一起升。这些“看不见的升级”往往比功能升级更紧迫。所以升级前一定要做的第一件事就是把“为什么升”写清楚哪些是业务驱动、哪些是安全驱动、哪些是接新设备的需求。不写清楚后面验收的时候根本没有标准很容易变成“升完就完事”。1.2 升级方式选型为什么我选了平滑升级确定了要升接下来就是怎么升。常见的方案有三种。停机升级最简单粗暴选个深夜窗口停服务替换代码启动验证完事。对内网小系统可行但天眼是监控平台别的部门晚上可能不看监控大屏报警服务却一直挂着。而且我们有个硬性要求升级过程中摄像头接入不能长时间中断。蓝绿部署是更稳的路子准备两套完整环境旧环境和新环境同时跑验证完毕再把流量切到新环境。缺点是资源占用翻倍我们这边只有一台生产服务器加一台备用机物理上就不支持再来一整套。滚动升级和平滑升级是最终选择容器逐个更新配合 nginx 平滑切换让部分服务先升、部分服务后升整个过程对外只感受到极短暂的连接抖动。这也是现在很多系统升级的主流思路。代价是方案复杂度高需要对每个服务有清晰的依赖关系和健康检查机制。选平滑升级的理由很直白它同时保住了“别中断太久”和“别一次赌太大”两个底线。而且被动辄几十台机器搞蓝绿是不现实的平滑升级才是中小团队最务实的选择。1.3 影响面盘点与备份先行升级前最忌讳“直接干”尤其是这种牵扯到十几类组件的升级一定要先盘影响面。我把天眼涉及的模块列了一个表按风险等级做了标注。模块当前状态升级方式风险等级Web前端Vue2 旧构建链重建工程升级Vue3高接入网关Nginx 1.16平滑reload低流媒体服务基于ffmpeg的转码服务滚动重启中算法推理服务模型V2、接口旧新旧模型灰度切换高消息队列单节点Redis需确认持久化配置中数据库PostgreSQL老版本pg_upgrade迁移高基础组件OpenSSH、OpenSSL、Docker系统级升级高除了业务模块还有个容易被忽略的部分操作系统底层的 OpenSSH、OpenSSL、Docker 这些组件也得一起动。它们不像应用服务可以灰度属于整个环境的“地基”。地基动了楼体也跟着晃所以必须单独列出来评估。影响面盘完接下来就是备份。我的原则是备份到能做“完整回退”的程度而不是备份个文件就算了。具体做了四件事数据库物理备份直接停写片刻做基础备份再导出逻辑备份作为第二道保险。配置文件目录整体打包包括 Nginx、应用、系统 sshd 的配置打包后加时间戳存到独立目录。镜像和安装包归档把当前正在跑的镜像 tag、离线安装包全部留存保证随时能重新起一套旧环境。敏感数据校验比如确认备份文件能正常被恢复而不是备份完了自己都没验过。这一步我踩过深刻的教训。以前有一次升级数据库我自认备份做得很好结果要回滚了才发现备份文件不完整。从那以后每次备份完了我都会做一次小范围的恢复演练至少确认客户端能连上、关键表能查到数据。备份不是给硬盘看的是给未来那个手忙脚乱的自己准备的。2. 核心细节解析升级路上的“隐藏雷区”2.1 版本号与构建管理为什么“升级了却还是旧版”热搜里有一批特别有意思的问题“gcc升级后为啥还是旧版本”“升级node后不生效”“centos升级openssh后版本没变”。这些都是我在升级天眼过程中真切遇到过的只是对象从 gcc 换成了 node、换成了 openssh。先说个最经典的场景你编译安装或更新了一个工具运行 version 命令看到的还是老版本。这时候先别怀疑软件本身99%是下面这几个原因。第一PATH 环境变量顺序不对。系统找命令是顺着 PATH 一个目录一个目录找的第一个找到就用。如果你新版本装到了/usr/local/bin但系统 PATH 里/usr/bin排在前面实际执行到的一定是老版本。排查命令很简单which node which gcc type -a nodewhich告诉你实际会执行哪个路径type -a能看到所有候选路径。如果 which 结果是/usr/bin/node而你的新版本在/usr/local/bin/node那就说明 PATH 顺序有问题。第二shell 的命令哈希缓存。bash 会把最近执行过的命令路径缓存起来工具升级后内存里还记着旧路径。执行hash -r清一下就好。第三动态库缓存没刷新。gcc 这类工具牵扯一堆动态库系统用ldconfig维护库缓存新库装了但缓存没更新写程序时链接到的还是老库。处理方式ldconfig ldconfig -p | grep libstdc第四软链接没改。很多安装包会把二进制放到/usr/bin/xxx-4再用软链接指到/usr/bin/xxx。升级完软链接还指向旧文件白升。所以说版本升级这事最怕的不是版本变了而是你以为变了、其实没变。后来我给天眼的构建定了一条规矩构建机上所有工具版本统一写进 Dockerfile用 Docker 固定环境不再依赖宿主机上“究竟哪个版本”。这直接消灭了“本机能跑、服务器上就是跑不起来”的玄学问题。2.2 接口兼容与前端升级content-type与axios的变化前端重构是这次升级里最“坑队友”的部分。旧版天眼的 Web 端是基于老技术栈写的所有请求默认走表单提交浏览器发的报文是application/x-www-form-urlencoded后端接口也按这个格式解析。升级前端时顺手把请求库换成了 axiosaxios 默认的行为是把对象序列化成 JSON 发送Content-Type 变成application/json。后端如果没跟着适配就会出现“请求明明成功但后端收到的参数全是空”的诡异现象或者直接 415、400。这种问题最烦人因为它不会报“接口不存在”而是报一堆看起来像是参数传错了的错。这次升级我做了一个中间兼容层而不是让所有接口立刻切换。具体做法是在 axios 请求拦截器里显式设置Content-Type: application/json;charsetUTF-8保证新前端始终以 JSON 形式交互。后端网关加了一个协议适配如果请求头声明是老格式就转成新格式再往内部服务传如果是 JSON直接放行。前端请求统一走封装好的request.js而不是每个页面调 axios 裸方法。// request.js 关键部分 import axios from axios const service axios.create({ baseURL: /api }) service.interceptors.request.use(config { config.headers[Content-Type] application/json;charsetUTF-8 return config }) service.interceptors.response.use(res { const data res.data if (data data.code ! 0) { return Promise.reject(new Error(data.message)) } return data }) export default service有个朋友看了我这段代码说设置 Content-Type 还要单独写拦截器吗还真要。因为老接口和新接口同时存在的过渡期里你根本不知道哪个组件图省事直接调了原生 axios。统一封装 显式 Content-Type是从源头上防止“格式漂移”。除了请求格式前端还有一个隐藏雷区静态资源缓存。升级完成后很多同事反馈“怎么页面还是老样子”排查了半天发现是浏览器和 Nginx 都把老的 JS、CSS 缓存在本地了。解决办法是构建时给文件名加 hashapp.8f3a2b.js同时 Nginx 静态资源配置禁用强缓存。2.3 数据迁移数据库与算法模型的“格式换代”数据库升级是整个项目风险最高的一环。天眼用的 PostgreSQL 服务版本比较老新版本环境要跨大版本升级。这个过程我建议大家先小版本后大版本比如先升到当前大版本的最终版再跨到新大版本。跨版本迁移有两种主流方式物理升级pg_upgrade和逻辑导出导入pg_dump/pg_restore。我选了后者因为迁移过程顺便校验了数据一致性而且生产环境数据量不算大到导出导入会等太久。转换前先用演练环境跑了一遍确认所有表、视图、函数、触发器的兼容性。这中间也碰到一个经典坑老库用的某个数据类型在新版本里已经废弃逻辑导出时直接报错。后来是在导出阶段加了自定义类型转换相当于先给数据“换了个新容器”再导入。算法模型换代则完全是另一类问题。新模型输出的坐标格式从“左上角 宽高”改成了“中心点 宽高”置信度阈值从 0.5 起调整到动态阈值。如果直接切数据库里存的告警记录坐标全乱地图上打点位置全偏。我的做法是数据库表里同时保留x1,y1,x2,y2和新字段cx,cy,w,h给数据加个model_ver标记。算法服务先灰度跑一周新模型结果只写入model_ver2的记录不参与实时告警。灰度期结束后再写一个数据迁移脚本把历史告警坐标统一换算成新格式。如果你也遇到类似的模型升级千万别试图一步到位“全量重算”生产环境数据多重算一次足够把 IO 打满服务排队雪崩。分批跑、限速跑稳得多。2.4 底层系统组件升级OpenSSH、OpenSSL、Docker升级的惯性应用层升完还有底层环境没动。OpenSSH 升级、OpenSSL 升级、Docker 升级每一项单独拿出来都有人写过教程但合在一起就是连环坑。CentOS 7 上手动编译升级 OpenSSH我强烈建议大家把这三个点牢牢记死。第一sshd 的权限。编译安装完新版后如果/etc/ssh/ssh_host_rsa_key这类主机密钥文件权限不对比如是 644 而不是 600sshd 会直接拒绝启动。而且不会给你什么特别醒目的报错就一个Permissions ... too open不看日志根本发现不了。第二SELinux 的上下文。在 CentOS 上编译安装后新文件可能带的是错误的 SELinux 标签导致 SSH 服务起不来或者端口监听失败。遇到这种情况执行restorecon -Rv /etc/ssh /usr/sbin/sshd重置上下文再用getenforce确认状态。第三OpenSSL 依赖方向问题。新版 OpenSSH 编译时如果链到了旧版 OpenSSL即使二进制是新版本实际加密用的可能还是老库。所以升级 OpenSSH 前最好先确认 OpenSSL 版本并且把编译参数里--with-ssl-dir指到新库路径。Docker 升级相对好一些官方源直接换 yum 源后升级即可。但有个细节升级 Docker 版本后老的容器不会全部自动重启执行docker update --restartno改过重启策略的容器一定要自己留意。升级过程中别批量重启所有容器除非你想体验“全部服务一起挂掉”的场面。底层的系统组件升级没有“小升级”一说每个版本跳跃都按完整流程走这是我的经验。很多人栽跟头就栽在以为版本升级是小补丁随手一升完事结果连不上服务器了。3. 实操过程与核心环节实现3.1 升级环境准备与前置检查磨刀不误砍柴工。正式升级前我先跑了一套检查脚本把所有能提前发现的问题全部暴露出来。这张清单你可以直接抄作业检查项命令合格标准磁盘空间df -h数据盘剩余空间 ≥ 数据库实际体积的 3 倍内存free -h可用内存 ≥ 日常峰值内存的 1.5 倍数据库版本psql --version记录旧版本确认迁移路径当前服务版本docker ps/cat version.txt明确新旧版本差异备份完整性恢复演练关键表数据可查询时间同步timedatectlNTP同步正常避免日志时间紊乱磁盘空间这个指标很关键。数据库升级过程中往往需要同时保留新旧两份数据文件加上迁移日志和临时表空间不够的直接升级中断而且这种中断往往伴随数据文件损坏。前置检查做完了我把旧的配置文件逐一加了.bak-2025xxxx后缀备份好才新建了升级后的配置文件。注意不要简单覆盖一定要保留一个独立的、完整的旧配置快照。这个习惯在后来回滚时救了大忙。3.2 服务平滑升级docker滚动更新与nginx平滑切换应用服务的平滑升级我把它拆成了“容器层”和“网关层”两个环节。网关层用 Nginx 做平滑切换。原理不复杂Nginx reload 是一个平滑过程worker 进程会逐步替换不会中断正在处理的请求。所以只要保证 Nginx 配置没问题reload 是安全的。但千万别用kill -9这种方式去“重启” Nginx那会断掉所有维持中的连接。天眼后端的 upstream 配置大概是这样的upstream tianyan_backend { server 10.0.0.11:8080 max_fails3 fail_timeout30s; server 10.0.0.12:8080 backup; } server { listen 80; server_name tianyan.internal; location /api/ { proxy_pass http://tianyan_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }升级时我把新版本的容器先起在新端口比如 18080确认接口健康后修改 upstream把流量切到新节点完全稳定了再下线旧容器。这就是最简单的蓝绿切换了但它基于 Nginx 的平滑特性整个过程用户无感知。容器层的滚动更新则更细分。我们的 gateway、algorithm、media 三个服务可以单独滚动更新的编排大致是这样# 构建新版本镜像 docker build -t registry.internal/tianyan/gateway:4.0.0 . # 推送镜像 docker push registry.internal/tianyan/gateway:4.0.0 # 摘除一个节点流量后更新这个容器 docker-compose up -d gateway这里有个特别需要注意的点docker-compose 的up -d gateway是会先杀掉老容器再启动新容器的本质上是一次短暂中断。要真正做到无感必须配合 Nginx 先把该容器的服务从 upstream 里摘除或者配好健康检查后等容器 ready 再切流量。健康检查这块我用的是容器健康探针比如healthcheck: test: [CMD, curl, -f, http://localhost:8080/healthz] interval: 10s timeout: 3s retries: 3 start_period: 20s加了健康检查之后Docker 才知道什么时候算“真正可用”而不是进程起来了就算完。我见过太多把“进程运行中”当成“服务可用”的情况结果流量切过去接口全部超时。升级过程中我还有一个经验每次只滚一个节点滚完立刻看监控观察 10 分钟再操作下一个。千万不要 all in一上来就把所有服务全更新了如果新版本存在内存泄漏或者死锁你连快速回滚的余地都没有。3.3 数据库与数据迁移脚本执行数据库迁移这次我选的路径是先做逻辑导出备份再搭建新实例最后执行迁移脚本。迁移脚本编写时有几个原则顺序执行失败即停不跳过任何一步。先执行 DDL再执行数据回填。数据回填要分批每批只处理一百万行加 sleep 间隔。比如给告警记录加新坐标字段的时候SQL 大致是这种形态-- 兼容阶段新增字段允许为空 ALTER TABLE alarm_records ADD COLUMN center_x float, ADD COLUMN center_y float, ADD COLUMN model_ver smallint default 1; -- 回填新字段按主键分批 -- 假设一次处理5000条 UPDATE alarm_records SET center_x (x1 x2) / 2.0, center_y (y1 y2) / 2.0, model_ver 2 WHERE id BETWEEN :start AND :end AND model_ver 1;如果数据量大建议用一个独立脚本在不同时间段跑而不是在数据库迁移窗口里一口气跑完。迁移窗口的时间只做结构性变更回填延后到低峰期慢慢跑。这个设计虽然多花了点时间但避开了“迁移过程中数据库大量读写导致锁表”的灾难。迁移完成后我做了一次完整的数据校验新旧库记录总数是否一致。抽样验证告警记录坐标字段换算是否准确。验证所有数据库触发器、存储过程、定时任务是否真实迁移成功而不只是导出成功。你可能会问存储过程这种东西为什么会漏因为逻辑导出的时候视图和函数默认都导出来了但有些函数内部引用了旧版本的方言函数导入后不报错也不执行只有调度跑起来才发现是坏的。3.4 升级后的版本验证与收尾升级动作全部做完不代表结束。版本验证要有一套明确的验收标准不能靠“感觉正常”。我列了一张验收表逐项打勾验收项验证方法结果页面正常访问浏览器登录后端控制台通过摄像头在线数量对比升级前后设备在线统计一致视频推流延迟实测延迟小于1000ms通过告警能正常下发模拟触发一次识别告警通过日志无异常堆栈查看ERROR级别日志无新增系统资源水位CPU、内存、磁盘使用率稳定其中摄像头在线数量这项是最容易暴露问题的指标。如果升级后在线设备少了多半是协议兼容层没适配好新网关对旧设备的认证握手出了差异。这时候不要急着说“设备坏了”先看网关日志再看设备侧的回包。验收通过后我做了三件收尾的事把旧镜像完整留存不打 delete 标签至少保留两周。把所有的升级脚本、配置文件、迁移脚本打成一个归档包存到专门目录。在内部文档里更新了版本记录包括每个服务当前版本号、启动方式、回滚方式。有人觉得归档太占空间删了就行。但实际上很多升级刚完的两三天内还会有一些间歇性问题冒出来比如某个定时任务昨天跑得正常、今天报错这时候手里有完整回滚包就是最大的安全感。3.5 顺手说一下“升级访问”弹窗这个和天眼本身无关纯粹是热搜里几个词提醒了我什么“页面升级访问永久更新”“紧急通知页面升级访问正在跳转”“页面升级访问每日正常更新跳转新域”。如果你在网上或者某些软件里看到这种强迫你点击“升级访问”“紧急跳转”的页面请一律当骗子处理。正规系统升级不会在浏览器里给你弹这种盖板更不会用一个看起来特别紧急的域名让你立刻“访问更新入口”。这类弹窗绝大多数是流量劫持、恶意推广或者钓鱼脚本轻则诱导安装推广软件重则窃取账号信息。我们系统有一次外网访问也被插入过这种脚本排查后发现是链路某处被注入了内容不是系统自身在升级。所以看到这类页面直接关掉别点。真需要升级软件去官方渠道下载别走弹窗给的链接。4. 常见问题与排查技巧实录这次升级过程中积累了不少问题我把高频的、容易卡人的整理成速查表按“现象、排查方法、解决方案”的格式列出来。现象可能原因排查思路解决办法升级后访问页面还是旧版浏览器/CDN/Nginx缓存CtrlF5强刷无效看响应头静态资源加hash、Nginx关闭强缓存gcc版本显示旧版PATH顺序或hash缓存which、type -a调整PATH、hash -rnode升级后仍旧版环境变量未刷新echo $PATH修改~/.bashrc或~/.zshrc服务升级完起不来依赖库缺失看systemd日志或容器logs补依赖、重新编译axios升级后接口参数丢失Content-Type不匹配浏览器Network面板看请求头统一拦截器设置JSON格式容器频繁重启健康检查探针失败docker inspect、查看事件调长超时、修健康检查端口PostgreSQL迁移后启动失败版本不兼容查看pg日志用pg_upgrade检查、修复权限OpenSSH升级后无法登录SELinux/权限/防火墙日志、getenforcerestorecon、重置权限数据库资源消耗过高迁移脚本并发太大慢查询观察分批执行、加限流下面挑几个重点展开说。4.1 升级后页面还是旧版别急着骂前端很多人一看到“页面没变”就以为是应用没升成功其实先看 Network 面板如果 JS/CSS 请求的 URL 还是旧的哈希文件名就是你命中了缓存。Nginx 层面的处理是给静态资源配置location /static/ { root /app/dist; add_header Cache-Control public, max-age31536000, immutable; }这个配置是配合文件名 hash 使用的。文件名带 hash就可以长缓存文件名没变就必须保证Cache-Control: no-cache。二者缺一个就会遇到“升级了但页面没变”的经典事故。4.2 升级后服务总是自动重启容器自动重启这个问题我排查到凌晨才定位到是健康检查探针的坑。Docker 默认的健康检查如果连续多次失败就会把容器标记为 unhealthy编排系统或者 systemd 就可能把它重启。但容器本身其实是活的只是探针发到 /healthz 的接口一直没有正常返回。后来我发现是探针用了内网 IP而新容器的网络模式从 bridge 换成了 host导致探针发到了错误的地址。改回探针指向本机 localhost 后问题彻底消失。4.3 数据库迁移中断后的正确姿势数据迁移中断是最慌的但千万别做“重新全量迁移”这种傻事。迁移脚本必须设计成幂等的同一个脚本跑两遍结果和跑一遍一致。比如用ALTER TABLE ... ADD COLUMN IF NOT EXISTS用UPDATE ... WHERE model_ver 1这种条件限制确保断点续跑是安全的。我的经验是先跑一个 dry-run 模式脚本里加--dry-run参数只打印要执行的 SQL 不真实执行。全量 SQL 的每个 update 都由 select count 验证结果确认影响行数和预估一致再真正执行。4.4 OpenSSH升级后连不上机器怎么办这个属于“升级翻车现场”里最让人头大的情况。如果你升级完远程一刷新就断连但电脑还活着这种情况我的建议是别慌试试以下顺序如果用云服务器直接用控制台登录恢复现场。检查/etc/ssh/sshd_config是否被覆盖尤其注意PermitRootLogin、PasswordAuthentication这两项。检查主机密钥文件权限chmod 600 /etc/ssh/ssh_host_*。restorecon -Rv /etc/ssh重置 SELinux 上下文。确认防火墙是否放行新端口如果你手贱改了端口。务必在本地把回退方案写清楚再动手远程升级。我认识一个同行升级 OpenSSH 把自己挡在服务器外面最后只能去机房用物理终端解决折腾了一整晚。4.5 内容型升级的“格式坑”回到 axios 和接口风格的问题我再补一条实战技巧排查这类问题最好用的工具是浏览器 Developer Tools 的 Network 面板先看 Request Headers 里 Content-Type 是什么再看 Request Payload 里的数据格式。如果请求头是application/x-www-form-urlencoded而 payload 是 JSON 字符串基本就是接口两边没对齐。新版前端统一走封装层后这个问题就变成了“谁绕过封装直接调原生 axios”查代码一次就能揪出来。最后再分享几个人体会。这次升级天眼最值钱的不是版本号从 3 变成 4而是把陈年的配置、死代码、无人知晓的临时修改全部翻了一遍顺手清了几个暗坑。升级过程本质上就是一次压缩版的“架构体检”你早晚要做。有两个小建议送给正在准备升级的人。第一升级前花一小时建一个UPGRADE.md把每一步做了什么、为什么做、遇到的问题全部写下来。别嫌麻烦一个月后你再看这份文档能救你一命。第二千万别信浏览器里弹出来的“紧急升级访问”“永久更新通道”这种窗口那些与真实升级毫无关系。真升级的人是安静的吵闹的弹窗十有八九是骗子。