1. 项目概述:离线的机房,和不联网的交付
做实施这行久了,你会发现一个特别有意思的现象:现在所有软件都往云上跑、往容器里塞、往微服务里拆,但是真正到了交付现场,客户机房的物理环境往往比你想象的原始得多。不联网、没外网、甚至内网都是孤岛,这种情况在医院、军工、电力、某些政企内网里太常见了。
这个项目就是这样一套交付工程——软件本身开发完了,测试也通过了,但是目标机房是个彻彻底底的物理隔离环境,没有外网、没有镜像仓库、没有公共的包管理器,所有东西都得靠移动介质一拷一搬运进去。标题里说“还没上过战场”,意思是这套交付流程已经在预演环境里完整跑通了,方案和介质都准备好了,就差去现场真刀真枪装一把。
这种项目的核心难点其实只有一个:如何在不可上网的封闭环境里,把一个依赖第三方开源组件、运行库、镜像、中间件的软件系统,完整、可重复、可校验地部署起来。
不只是把安装包拷贝进去那么简单,里面牵扯到依赖解析、镜像离线化、版本锁定、双人复核、回滚预案等一系列工程化操作。这篇就好好聊聊这套交付工程是怎么设计的,介质怎么做,现场怎么装,以及预演中踩过的坑。
适合谁来读?如果你也在做软件实施交付、系统集成、运维部署,或者你只是好奇“没有网怎么装软件”这件事,都能从这里面找到有用的思路。
2. 离线交付的整体设计:先想清楚交付物的边界
2.1 离线部署的本质是“依赖搬运”
很多人对离线部署有个误解,以为就是把安装包复制过去、双击安装就行。实际上现代软件几乎没有单文件能跑的,一套系统背后挂着操作系统补丁、运行库、数据库驱动、中间件依赖、软件包依赖树,再加上容器镜像里的每一层文件系统。只要有任何一个依赖没带上,现场装上就是报错给你看。
所以我把离线交付这件事拆成了三层:
- 系统层:操作系统基础依赖、内核参数、时区、字符集、DNS配置、Yum/Apt源(如果有内网源)。
- 应用层:数据库、中间件、微服务框架、应用的二进制包和配置文件。
- 数据层:初始化SQL脚本、基线数据、字典表、测试数据(如果允许带)。
三层各自打包、各自校验、各自安装,顺序不能乱。现场实施的时候也是照着这个顺序走,先系统层、再应用层、最后数据层,一层不通不往下走。
2.2 为什么选择自建离线仓库,而不是直接拷安装包
最开始我们的方案很原始,就是把所有RPM包用yumdownloader批量拉下来,放到U盘里,现场一个个装。实测下来发现一个问题:RPM依赖关系太复杂,手动装经常遇到循环依赖、冲突、缺依赖的情况,一个包装不上就卡半天。
后来换成了自建离线仓库的方案。在一台能上网的跳板机上装好Nginx,把CentOS 7的Base、Extras、Updates全部同步到本地目录,生成repodata元数据。到了现场,直接把整个仓库目录同步到内网服务器上,配置一个本地.repo文件,yum就能像连了外网一样正常工作。
这个做法最大的价值是:解决了“依赖树”的问题,安一个软件包时yum会自动把依赖从本地仓库解析出来,不用人工处理依赖关系。对于容器化应用,离线镜像仓库(如Harbor、Docker Registry)也是同样的思路。
2.3 介质选型:U盘、移动硬盘还是光盘
交付介质的选型看着是个小事,实际上翻车概率最高的就是这里。预演的时候我们用的是普通U盘,结果到了现场发现服务器前面的USB口在机柜后面,拔插特别费劲,而且U盘容量和文件系统格式都可能掉链子。
总结下来我的选型经验是这样的:
| 介质 | 适用场景 | 坑点 |
|---|---|---|
| U盘(32GB以内) | 小系统、纯安装包交付 | 文件系统FAT32无法存超过4GB的单个文件 |
| 移动硬盘(固态) | 镜像仓库、大数据量 | 供电不足、NTFS格式Linux不能直接读取 |
| 光盘DVD | 合规审计要求严格的项目 | 速度慢、容量小、不改动数据 |
| 高速固态移动硬盘(exFAT) | 推荐首选 | 兼容性好、容量大、速度快 |
最终我们选的是固态移动硬盘,格式化成exFAT,因为exFAT在Windows和Linux两边都能直接挂载,不用装额外的驱动。如果你还在用NTFS,现场Linux服务器上可能要ntfs-3g工具才能读,会多一层麻烦。
注意:如果你的系统里某些安装包单文件超过4GB(现在很多数据库安装包都超过这个数),FAT32的U盘是装不下的,一定要用exFAT或者ext4格式的移动硬盘。
3. 软件准备与离线介质制作:预先把每一层依赖都锁死
3.1 全量依赖采集:不止是安装包本身
准备工作是整个项目里花费时间最长的一项,原因在于要采集的东西比想象中多得多。以我们这套系统为例,它依赖:
- Nginx 1.20:作为反向代理和静态资源服务
- PostgreSQL 13:存业务数据
- Redis 6.2:做缓存和会话管理
- Java 11运行环境:跑Spring Boot微服务
- 前端静态资源:Nginx托管
- 系统级依赖:epel-release、gcc、openssl-devel、zlib-devel、readline-devel等
- Python 3.8 + pip依赖:跑运维脚本和数据处理任务
每一项都要在跳板机上用包管理器把安装包和依赖一次性拉全。比如CentOS 7下,先配置好外网源,然后执行:
# 创建存放目录 mkdir -p /opt/offline/rpms # 下载指定软件包及其依赖 yumdownloader --resolve --destdir=/opt/offline/rpms nginx postgresql-server redis java-11-openjdk # 下载系统常用基础依赖 for pkg in gcc gcc-c++ make openssl-devel zlib-devel readline-devel libffi-devel epel-release; do yumdownloader --resolve --destdir=/opt/offline/rpms $pkg donePython依赖就用pip的离线模式拉:
mkdir -p /opt/offline/pip pip download -r requirements.txt -d /opt/offline/pip --platform manylinux2014_x86_64 --python-version 38 --only-binary=:all:注意这里加了平台和Python版本限制,避免下载到跟现场环境不兼容的包。
3.2 容器镜像的离线化保存
我们这套系统用了Docker Compose来做编排,几个核心镜像分别是postgres:13-alpine、redis:6.2-alpine、nginx:1.20-alpine,加上自己构建的应用镜像。到了现场既不可能去Docker Hub拉镜像,也不可能在客户内网里构建,所以必须在跳板机上提前把镜像保存成tar文件。
操作并不复杂:
# 在跳板机上拉取镜像 docker pull postgres:13-alpine docker pull redis:6.2-alpine docker pull nginx:1.20-alpine # 标记自己的应用镜像 docker tag myapp:release-1.0.0 registry.internal/myapp:1.0.0 # 逐个保存成tar文件 docker save -o /opt/offline/images/postgres-13-alpine.tar postgres:13-alpine docker save -o /opt/offline/images/redis-62-alpine.tar redis:6.2-alpine docker save -o /opt/offline/images/nginx-120-alpine.tar nginx:1.20-alpine docker save -o /opt/offline/images/myapp-100.tar myapp:1.0.0保存完之后记得做一件事:校验文件完整性。每个tar文件生成一个SHA256校验值,把校验清单也存到硬盘里,现场导入完镜像后跑一遍校验,确保文件没损坏、没被篡改。
sha256sum /opt/offline/images/*.tar > /opt/offline/sha256sums.txt cat sha256sums.txt # 打印结果并记录3.3 离线仓库目录结构设计
介质目录必须提前规划,否则到了现场容易混乱。我们最终确定的结构是这样的:
/offline_delivery/ ├── 00_README.md # 安装手册入口 ├── 01_system/ # 系统级依赖 │ ├── rpms/ # 所有RPM包 │ ├── local.repo.template # 本地源配置文件模板 │ └── setup_local_repo.sh # 一键配置本地源的脚本 ├── 02_applications/ # 应用安装包 │ ├── jdk-11.tar.gz │ ├── postgresql-13.tar.gz │ ├── redis-6.2.tar.gz │ └── nginx-1.20.tar.gz ├── 03_docker_images/ # 容器镜像 │ ├── postgres-13-alpine.tar │ ├── redis-62-alpine.tar │ ├── nginx-120-alpine.tar │ └── myapp-100.tar ├── 04_database/ # 数据库脚本 │ ├── init_ddl.sql # 建表脚本 │ ├── init_dml.sql # 基础字典数据 │ └── init_admin.sql # 管理员账号初始化 └── 05_scripts/ # 自动化部署脚本 ├── install_all.sh # 一键部署主脚本 ├── check_env.sh # 环境检查脚本 └── rollback.sh # 回滚脚本这个结构背后的逻辑非常朴素:谁在前谁在后,脚本、配置、数据分离,现场操作时按数字顺序走就行,哪怕不是开发这套系统的人,照着00_README也能上手。
3.4 预演环境与正式介质的一致性保障
“还没上过战场”不等于没有验证。我们在预演环境(一台虚拟机,规格和现场服务器对齐)里完整跑了两遍全流程部署,第一遍发现缺三个依赖包,第二遍才完全通过。
这里有一个经验值得分享:把预演的虚拟机改成和现场完全一致的配置,包括操作系统大版本(小版本也要核对)、CPU核数、内存大小、磁盘分区方式。因为有些软件在2核4G和4核8G上装出来的行为完全不同,有的内存不够直接装不上,有的安装脚本会根据内存自动调整参数,现场机器和预演机器配置差异越大,出问题的可能性越高。
4. 现场实施流程:进机房之后一步一步怎么走
4.1 入场检查清单
现场实施最怕的就是“以为环境是那样的”,结果到了现场发现根本不是。所以进场后第一步不是安装,而是检查环境,我专门做了一张检查清单:
- 服务器是否上电、能ping通管理IP
- 操作系统版本:cat /etc/redhat-release 核对大版本
- 磁盘空间:df -h 确认根分区和数据分区空间足够
- 内存大小:free -m 确认符合最低要求
- 网络:内网通不通、有没有机架交换机隔离
- SELinux状态:getenforce,建议现场设置为disabled
- 防火墙:firewalld/iptables状态,先确认再决定是否放行端口
- 时钟同步:date 命令看当前时间和时区是否正确,时间不对会影响证书校验
这些检查项看着基础,但是每一项都有可能让后续安装功亏一篑。比如SELinux没关,Nginx可能无法绑定80端口;时钟不对,HTTPS证书马上失效;根分区空间不够,镜像导入直接失败。
4.2 系统层和应用层安装
系统层用的是自建yum本地源的方式。现场操作步骤:
# 1. 备份原有的yum源配置 mkdir -p /etc/yum.repos.d/backup mv /etc/yum.repos.d/CentOS-*.repo /etc/yum.repos.d/backup/ # 2. 挂载交付介质 mount /dev/sdb1 /mnt/usb # 根据实际情况改设备名 # 3. 创建本地源配置 cat > /etc/yum.repos.d/local.repo << 'EOF' [local] name=Local Repository baseurl=file:///mnt/usb/01_system/rpms enabled=1 gpgcheck=0 EOF # 4. 清缓存并验证 yum clean all yum repolist这个步骤的关键在于:源配好后先用repolist确认本地仓库被识别,再安装任何东西。我见过有人配完源直接yum install,结果走了原来的外网源(如果现场还有残留外网权限),装了半天装不上才发现问题。
应用层的安装顺序是:JDK -> PostgreSQL -> Redis -> Nginx。数据库要先初始化数据目录和系统表,Redis是编译安装,Nginx是编译安装或者二进制包。每一步装完都要验证服务能启动。
4.3 容器镜像导入与编排启动
应用层装好后,开始导入容器镜像:
# 导入镜像 docker load -i /mnt/usb/03_docker_images/postgres-13-alpine.tar docker load -i /mnt/usb/03_docker_images/redis-62-alpine.tar docker load -i /mnt/usb/03_docker_images/nginx-120-alpine.tar docker load -i /mnt/usb/03_docker_images/myapp-100.tar # 验证镜像 docker images导入完之后,把docker-compose.yml和.env配置文件拷贝到指定目录下,然后启动:
cd /opt/myapp docker-compose up -d docker-compose ps这里有一个在离线环境尤其容易踩的坑:docker-compose.yml里如果写了image: xxx(没有指定tag),默认会去拉latest镜像,离线环境拉不到会一直卡住。所以离线交付的compose文件里必须全部写明镜像名加tag,并且确保这个tag在镜像tar包里存在。
4.4 数据库初始化和数据校验
数据库是整套系统最核心的一部分,也是“最容易出问题但最不容易被发现”的一部分。我们采用了Docker容器化的PostgreSQL,但数据库脚本是直接挂载卷在宿主机上执行的。
# 进入容器执行SQL脚本 docker exec -i myapp-db psql -U postgres -d myappdb < /opt/myapp/sql/init_ddl.sql docker exec -i myapp-db psql -U postgres -d myappdb < /opt/myapp/sql/init_dml.sql docker exec -i myapp-db psql -U postgres -d myappdb < /opt/myapp/sql/init_admin.sql执行完务必做数据校验,不要只看有没有报错,而是真去查几条关键表:
-- 检查核心表是否存在 \dt -- 检查字典表是否有数据 SELECT count(*) FROM sys_dict; -- 检查管理员账号是否创建 SELECT username, status FROM sys_user WHERE username='admin';我预演时遇到过SQL脚本执行完没有任何报错,但实际上partitions建错了、外键引用缺失、编码不对导致中文乱码的情况。所以校验不是走过场,最好把预期结果也写在交付文档里,比如“admin用户应存在,状态为1,默认密码为xxx”。
4.5 验证测试用例:不能只看“服务起来了”
服务起来不代表系统可用。这是这次交付里我最强调的一点:“能访问”和“功能正确”是两回事。
我们在交付文档里预置了一组冒烟测试用例,覆盖登录、新增、查询、修改、删除、权限、日志等核心链路,现场验证完以后把结果记录下来。还测了重启恢复能力——把整套docker-compose停掉再启动,看服务是否自动恢复,数据库连接是否会自动重连。
一套软件如果经不起“停机后重启”的测试,现场迟早要出问题。
5. 实际遇坑问题记录与排查思路
5.1 常见坑NO.1:yum本地源配置后仍然无法安装
现象:配置完local.repo,yum repolist能看到仓库,但是yum install nginx时报“No package nginx available”。
排查过程:先用yum list | grep nginx看了下确实没有,然后看repodata目录,发现rpm包放进去了但repodata是空的。原因很简单:直接把RPM包拷贝到了目录里,但没有重新生成repodata索引。
解决方案:在跳板机上用createrepo重新生成索引,然后重新拷贝整个目录。现场如果已经带过去了,就在现场服务器上执行:
yum install -y createrepo # 如果现场有,或者从介质里装 createrepo /mnt/usb/01_system/rpms yum clean all && yum repolist经验:制作离线yum源不是拷文件就行,一定记得最后一步执行createrepo生成repodata,并且拷过去之后用repolist验证一遍。这个坑在离线交付里出现频率极高。
5.2 常见坑NO.2:Docker镜像导入耗时过长甚至卡死
现象:现场docker load一个2GB的镜像,进度条卡在99%不动了。
排查过程:第一反应是磁盘空间不足,df -h一看确实根分区只剩2GB。docker load过程中临时文件和相关层数据占用了空间,磁盘满了以后进程卡住。
解决方案:清理无用的中间镜像和容器,或者换到空间充足的分区存储docker数据根目录。最彻底的办法是在安装规划阶段就把docker的数据目录设置为独立的大分区:
# 修改docker数据根目录 cat > /etc/docker/daemon.json << 'EOF' { "data-root": "/data/docker" } EOF systemctl restart docker另一个建议:镜像tar文件在导入前就检查大小,现场磁盘至少要留有镜像tar文件3倍以上的空闲空间(一层是tar的大小,一个是解压后的镜像大小,一个是运行时的日志数据)。
5.3 常见坑NO.3:内网里服务IP配置错误导致无法访问
现象:部署全部完成后,从办公网访问不到应用页面,但服务器本机curl是通的。
排查过程:先ping了应用服务器的IP,通了;再telnet应用端口,不通。说明问题出在防火墙或者路由上。一查firewalld还在运行,且没放行8080端口。另外nginx监听地址写的是127.0.0.1,外部访问当然进不来。
解决方案:修改nginx配置把监听地址改为0.0.0.0,放行防火墙端口。这里要提醒一点:很多现场机器开机默认开着firewalld,你需要根据客户的安全策略来决定禁用它还是放行特定端口。如果客户要求不能关防火墙,那就在安装文档里写清楚每个服务的端口和放行规则,不要一刀切。
5.4 常见坑NO.4:数据库中文乱码问题
现象:初始化完成后,查询中文数据全是乱码。
排查过程:数据库初始化时没有指定字符集,默认成了SQL_ASCII,业务表没有显式定义字符集,导致数据库层面就无法存储中文。
解决方案:在初始化PostgreSQL容器时,必须显式设置字符集:
POSTGRES_INITDB_ARGS="--encoding=UTF8 --locale=en_US.UTF-8"或者执行完初始化后检查验证:
SHOW SERVER_ENCODING; SHOW LC_COLLATE;这个坑在离线交付里特别隐蔽,因为安装过程不会报错,直到真正写入中文数据才发现。所有交付项目里,数据库字符集必须作为环境检查清单里的强制项。
5.5 问题速查表
| 问题现象 | 可能原因 | 排查重点 |
|---|---|---|
| yum install 提示找不到包 | repodata未生成 | 检查repodata目录,执行createrepo |
| docker load 卡住 | 磁盘空间不足 | df -h查看分区用量 |
| 应用无法外部访问 | 防火墙拦截/监听地址错误 | telnet端口测试、查看nginx监听配置 |
| 中文乱码 | 数据库字符集未指定 | SHOW SERVER_ENCODING确认UTF8 |
| 服务启动后自动退出 | 日志路径权限/内存不足 | docker logs 查看日志、free -m查看内存 |
| 时区显示不一致 | 容器和宿主机时区不同 | 挂载/etc/localtime或者设置TZ环境变量 |
| 证书校验失败 | 服务器时间不同步 | date命令核对时间,配置ntp或手动设置 |
6. 交付文档与知识传承:让“没上过战场”也能被信任
6.1 交付文档的“新手可执行”标准
做交付的不只是把软件装上,更是把“怎么装”变成一套可复现的方法论。这套项目我在写文档时要求自己遵循一个标准:找一个从没参与过这个项目的人,只凭文档和介质,能独立完成部署。
所以文档里除了安装步骤,还必须有:
- 每个步骤的预期输出(比如执行完后应该出现什么提示)
- 每个命令的作用说明(不是冷冰冰的命令堆砌)
- 常见错误提示和对应的解决手段(就是上面第5章这类内容)
- 每个环节验证方法和验收标准(不是装完就算完)
6.2 双人复核与“模拟交付”
这次虽然没有正式进场,但我们是按正式交付标准做了两轮预演。第一轮由项目开发人员自己装,基本是“神仙操作”,很多步骤靠肌肉记忆,文档没暴露问题。第二轮我刻意安排了一个没参与过开发的新同事来装,结果暴露出一堆问题:文档里漏了创建目录的步骤、系统要求写得不明确、某个环境变量没交代来源。
双人复核的意义就在这:你永远不能用自己的“知道”去代替文档的“写清楚”,开发人员觉得理所当然的东西,对实施人员来说可能就是天书。
6.3 回滚方案与现场应急包
最后说一个很多交付团队容易忽略的东西——回滚方案。部署前必须想清楚:如果安装到一半失败,现场该怎么办?我们的策略是:
- 每次变更前拍摄虚拟机的快照(如果是虚机环境)
- 每个阶段的脚本都有幂等设计(重复执行不会产生副作用)
- 部署日志全过程留痕(script命令记录终端输出)
- 返回现场时携带一份应急U盘,里面除了交付介质还有系统级别的救援工具(比如rescue模式工具、磁盘工具等)
很多现场问题不是装不上,而是装了一半卡住进退两难。有回滚预案,至少能把现场恢复到某个可控状态,而不是陷在故障里干等。
这套交付工程虽然还没有正式上战场,但整个准备过程本身就是一次高质量的系统工程演练。我个人最大的感受是:离线部署不是什么高深技术,真正考验人的是细致程度和预案意识。把每一层依赖锁住、把每一步验证做扎实、把每一类问题想在前面,到了现场就算环境还有意外,你也已经有了应对的底气和框架。