做测试环境搭建这事儿,看着不难,但坑是真不少。同一个部署文档,在 CentOS 7 上执行得顺顺利利,换到 Ubuntu 20.04 上就报错,或者反过来亦然——包管理器不同、软件源格式不同、防火墙规则不同、服务管理方式也不同。我见过太多次测试环境“薛定谔的可用”:上午跑得好好的用例,下午一跑就崩,查到最后发现是某台机器的软件版本跟其他机器不一致。后来我下决心把测试环境的部署过程固化下来,整理成一套覆盖 CentOS 7 和 Ubuntu 20.04 双版本的完整流程,从基础系统配置、软件源替换、开发工具链,到 Docker 容器化中间件部署,全部脚本化、可复现。这篇手册就是这套流程的完整记录,适合需要快速搭出稳定测试环境的人:测试工程师、独立开发者、小团队运维,或者刚转岗过来被环境问题折磨的新手,照着操作就能少踩一大半的坑。
1. 项目概述与设计思路
1.1 为什么同时覆盖 CentOS 7 与 Ubuntu 20.04
先说选型逻辑。目前各团队的在用系统基本被两大阵营瓜分:一边是 RHEL 系,CentOS 7 是典型代表,存量服务器和不少传统项目的集成测试环境都跑在它上面;另一边是 Debian 系,Ubuntu 20.04 LTS 是长期支持版本,企业级支撑到 2025 年之后,很多新项目组直接拿它当基准环境。如果团队既要兼容老业务系统,又要适配新项目,测试环境就必须同时覆盖这两个系统。实测下来,一些关键差异集中在这几点:
- 包管理器:CentOS 7 用 yum,Ubuntu 20.04 用 apt,命令习惯完全不同。
- 软件源配置:CentOS 的源文件在 /etc/yum.repos.d/ 下的 .repo 文件,Ubuntu 则通过 /etc/apt/sources.list 管理。
- 防火墙:CentOS 默认启用的 firewalld 和 Ubuntu 的 ufw 操作方式差异明显。
- 服务管理:CentOS 7 使用 systemd 管理服务,Ubuntu 20.04 同样使用 systemd,这方面相对统一,但个别服务的配置路径和默认行为仍有不同。
我做这套手册时没有把两个系统拆开成两篇独立的文档,而是放在同一套流程中对比推进。原因很简单:测试环境不是单机孤立存在的,同一个测试任务往往需要在两个系统上分别跑一遍,分开写文档会导致执行标准不一致,最后配置出来的环境还是对不齐。
1.2 测试环境部署的整体分层
整个部署过程我拆成了三个层次:系统层、工具层、应用服务层。系统层负责基础系统配置,比如时区、防火墙、软件源更新、编译依赖安装;工具层负责开发与测试必需的软件,比如 Git、JDK、Maven、Python;应用服务层则负责被测系统依赖的中间件,比如 MySQL、Redis,这些我统一用 Docker 部署,避免在机器上直接装一堆服务导致端口冲突和卸载不干净的问题。
在资源规划上,测试环境不需要像生产那样追求极高可用性,但也不能太寒酸。我一般建议最低配置 4 核 CPU、8GB 内存、100GB 以上磁盘空间。如果跑的是集成测试,最好 2 台机器组成最小集群;如果是小项目单机测试,一台也够用。所有虚拟机在安装完系统之后立刻做一次快照,后面随便折腾都不怕。
1.3 双系统安装阶段的前置准备
安装系统前有几个细节值得提前注意。CentOS 7 的 ISO 体积较大,包含的软件包也偏全,安装时建议选择“最小化安装”模式,不带图形界面;Ubuntu 20.04 的 Server 版本默认就是无图形界面,安装过程相对简洁。分区方面,我习惯把 / 分区独立出来,swap 单独分配 2GB 左右,/home 如果不需要可以并入根分区,避免后续扩容麻烦。
安装完成后先不要急着配业务,先把两台机器的网络互通测试一遍,确认 IP 能通、DNS 能解析,再进入下一步。测试环境最忌讳“装了一半发现基础不通”,后面排查起来非常浪费时间。
2. 系统初始化与基础配置
2.1 最小化安装后的统一调整
装完系统后第一件事不是装软件,而是先把系统底子调平。我通常按这个顺序操作:设置主机名、配置时区、关闭不必要的防火墙策略、检查 SELinux 状态。
CentOS 7 上默认的 SELinux 状态是 enforcing,这在测试环境下有时候会挡住一些调试操作。通过临时关闭或修改配置文件永久关闭后,可以减少很多访问权限上的困惑。这里需要说明一点:我只是在测试环境这样处理,生产环境务必保持 SELinux 开启状态,两者场景完全不同,不要拿测试环境的配置方案直接套用到生产。
Ubuntu 20.04 默认没有开启 SELinux,主要注意 ufw 防火墙规则即可。如果测试机之间需要互通端口,建议一开始就放开需要的端口范围,避免逐个服务去配白名单。
时区配置:
# CentOS 7 与 Ubuntu 20.04 通用 timedatectl set-timezone Asia/Shanghai systemctl enable --now chronyd # CentOS 7 使用 chrony timedatectl set-ntp true # Ubuntu 20.04 使用 systemd-timesyncd注意 CentOS 7 默认安装的 chrony 需要显式启动,Ubuntu 20.04 则默认启用时间同步。我有一个习惯是设置完主机名后,把两台机器的 /etc/hosts 加上彼此的内网 IP,后续用主机名互相访问,比记住 IP 方便且不容易连错机器。
2.2 软件源替换与基础工具安装
默认软件源在国外,下载速度慢是普遍痛点。在实际操作中,我会把系统源替换为国内开源镜像地址。这一步需要分别处理:
CentOS 7:
# 备份原配置 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak # 下载对应镜像源配置(实际操作中注意匹配系统版本) curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.internal.example.com/centos/7/base.repo yum clean all && yum makecacheUbuntu 20.04:
cp /etc/apt/sources.list /etc/apt/sources.list.bak # 编辑 sources.list,将 archive.ubuntu.com 与 security.ubuntu.com 替换为国内镜像地址 sed -i 's/archive.ubuntu.com/mirrors.internal.example.com/g' /etc/apt/sources.list apt update配置完成后先装一批基础工具,保证后续编译和下载操作不卡壳:
# CentOS 7 yum install -y epel-release yum install -y vim wget curl net-tools unzip zip tree gcc gcc-c++ make # Ubuntu 20.04 apt install -y vim wget curl net-tools unzip zip tree build-essential我在这一步踩过的坑是:CentOS 7 的 EPEL 源非常实用,里面有不少默认源没有的软件包,尤其后面装 Python 依赖时作用很大,所以一定不要漏掉。Ubuntu 的 build-essential 包则一次性把 gcc、g++、make 等编译工具都装齐了,比逐个安装省心。
2.3 测试账号与权限规划
测试环境尽量别用 root 直接操作,既容易误删文件,也不利于权限管理。我习惯创建专用账号:
useradd tester echo 'Tester@123456' | passwd --stdin tester usermod -aG wheel tester # CentOS 7,wheel 组有 sudo 权限 usermod -aG sudo tester # Ubuntu 20.04,sudo 组有 sudo 权限然后把 tester 账号的 sudo 配置成免密或者限免,比如只允许执行特定命令。测试环境里频繁执行 sudo 操作,如果每次都要输密码会让人非常烦躁,但全免密又有安全风险。我的折衷方案是:tester 账号临时免密 sudo,但只能在测试网段内使用,防止其他网段的人搭个顺风车。
3. 核心工具链与开发环境部署
3.1 Git 与代码协作工具链
测试环境连 Git 是刚需。CentOS 7 自带版本偏低,Ubuntu 20.04 自带的则比较新,功能差异主要体现在一些命令和分支操作上。如果统一从源码编译安装,可以保证两个环境版本完全一致。我实际使用的是源码编译方式,虽然耗时稍长,但能确保测试环境与生产环境的 Git 行为一致。
编译安装前需要安装依赖库:
# CentOS 7 yum install -y curl-devel expat-devel gettext-devel openssl-devel perl-devel zlib-devel # Ubuntu 20.04 apt install -y libcurl4-openssl-dev libexpat1-dev gettext libssl-dev perl zlib1g-dev之后从官方源码包构建安装,配置 PATH 和 man 路径后验证版本。这一步没必要写死参数,实际执行时按当前最新稳定版即可。装完 Git 顺手配置全局身份信息,否则后续拉代码操作时经常出现身份未配置的提示:
git config --global user.name "Tester" git config --global user.email "tester@example.com" git config --global push.default simple3.2 JDK 与 Maven 构建环境
测试环境里跑自动化脚本和编译项目,JDK 基本绕不开。我以 OpenJDK 11 为例,因为目前大部分 Web 项目的最低要求是 JDK 8,但 JDK 11 的兼容性更稳,很多团队已经迁移到 11 甚至 17,测试环境没必要守旧。
# CentOS 7 yum install -y java-11-openjdk java-11-openjdk-devel # Ubuntu 20.04 apt install -y openjdk-11-jdk装完后设置 JAVA_HOME 环境变量,写入 /etc/profile.d/java.sh:
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATHCentOS 7 上 java 目录路径不带 amd64 后缀,写法稍有不同,建议先执行readlink -f $(which java)查看实际安装路径再写进去。Maven 这边我选择手动下载二进制包解压到 /opt 目录,然后配置软链接到 /usr/bin 下,这样版本完全可控,不需要依赖包管理器里可能过旧的版本。同时修改 settings.xml 里的本地仓库路径,把默认的 ~/.m2 改为 /data/m2repo,避免大量依赖缓存占用用户目录空间。
如果项目是前端加后端一体化,还需要 Node.js,我的做法是使用 nvm 管理版本,方便在多个 Node 版本间切换:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 16 && nvm install 18 && nvm alias default 183.3 Python 多版本管理
测试环境里 Python 最常见的问题是“这脚本要 py3.8,那个工具要 py3.10”,直接改系统默认版本容易把依赖库搞乱。我使用 pyenv 做多版本隔离:
# 安装编译依赖(CentOS 7) yum install -y zlib-devel bzip2-devel readline-devel sqlite-devel libffi-devel # Ubuntu 20.04 apt install -y zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev libffi-dev接着安装 pyenv,然后编译指定版本 Python:
pyenv install 3.9.18 pyenv global 3.9.18注意编译过程比较慢,要有耐心。如果编译失败,绝大多数情况是某个依赖没装全,把上面列出的依赖逐个对一遍即可。pyenv 的好处是每个项目目录可以写上.python-version文件,切目录自动切换版本,非常舒服。
4. Docker 与中间件服务部署
4.1 Docker 在双版本下的安装差异
测试环境里最大变量就是中间件数量,如果每个版本直接在宿主机上装一套,两三个项目就能把机器端口占满,卸载还卸载不干净,副作用极大。所以我坚持用 Docker 部署所有中间件。
CentOS 7 上安装 Docker 的常规流程是先装依赖,再添加 Docker 官方源:
yum install -y yum-utils device-mapper-persistent-data lvm2 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y docker-ce docker-ce-cli containerd.io systemctl start docker && systemctl enable dockerUbuntu 20.04 的安装方式相似,但 APT 源格式不一样:
apt install -y apt-transport-https ca-certificates curl software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu focal stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null apt update && apt install -y docker-ce docker-ce-cli containerd.io systemctl start docker && systemctl enable docker实测下来最容踩坑的地方是 CentOS 7 安装后 Docker 服务启动失败,多数原因是系统内核版本和 overlay2 文件系统驱动的兼容性。处理方式是升级内核或者把存储驱动切换为 vfs,但切换 vfs 后性能下降明显。比较靠谱的做法是先用docker info查看当前存储驱动,如果报错再考虑调整。
4.2 用 Docker Compose 一键拉起测试依赖
单个中间件手工 docker run 还凑合,一旦有 MySQL、Redis、RabbitMQ、MinIO 好几个一起启动时,手工操作就会丢三落四。我统一用 docker-compose.yml 管理测试依赖。
以下是一个我实际使用的编排示例:
version: "3.8" services: mysql: image: mysql:5.7 container_name: test-mysql restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: testdb volumes: - /data/mysql:/var/lib/mysql command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci redis: image: redis:6.2-alpine container_name: test-redis restart: always ports: - "6379:6379" volumes: - /data/redis:/data rabbitmq: image: rabbitmq:3.10-management container_name: test-rabbitmq restart: always ports: - "5672:5672" - "15672:15672" environment: RABBITMQ_DEFAULT_USER: test RABBITMQ_DEFAULT_PASS: test123 volumes: - /data/rabbitmq:/var/lib/rabbitmq启动和停止流程:
docker compose up -d docker compose ps docker compose logs -f docker compose down需要注意:MySQL 5.7 的镜像在 Docker Hub 上的标签已经比较久远,但测试环境用着没问题。如果项目要求 MySQL 8.0,改镜像标签即可,其他配置几乎不用动。数据卷挂载到宿主机 /data 目录下,目的是容器重建后数据不会丢,这个是测试环境最实用的一步。
4.3 容器镜像管理与离线部署
测试环境有时候会碰到网络受限的机房,所有机器只能在内网通信。这种情况下如果每台机器都去拉公共镜像,基本会卡死。我的做法是在一台可以联网的机器上把镜像拉下来,然后导出成 tar 包拷贝到内网:
docker pull mysql:5.7 docker pull redis:6.2-alpine docker save mysql:5.7 redis:6.2-alpine -o test-images.tar # 也支持一条命令导入 docker load -i test-images.tar这里有个经验:导出 tar 前先确认镜像标签,不要把latest盲目导出。latest标签会随时间变化,导致输出的 tar 包在不同时间内容不一致,测试环境版本漂移往往就是这么产生的。最好在 compose 文件里锁定明确版本号。
镜像多起来之后,定期清理无效镜像和停止容器也是必要的:
docker system df docker image prune docker container prune不用太频繁,一个月清一次就够。
5. 自动化初始化脚本与环境校验
5.1 编写幂等的一键初始化脚本
当需要部署多台测试机时,手动一条条命令敲非常容易出错,而且每台机器状态不同,有的人已经装了部分软件,有的人是全新系统。最高效的方案是写一个幂等初始化脚本,无论跑多少次结果都一样。
我一般把脚本放在 /opt/init_env.sh,核心思路是先检测系统发行版,再执行对应分支:
#!/bin/bash set -e if [ -f /etc/redhat-release ]; then yum install -y epel-release yum update -y yum install -y vim wget curl net-tools git java-11-openjdk-devel elif [ -f /etc/lsb-release ]; then apt update -y apt install -y vim wget curl net-tools git openjdk-11-jdk else echo "Unsupported OS" exit 1 fi echo "Basic environment initialized."执行前务必备份原配置文件,比如 /etc/hosts 和软件源文件,防止脚本跑了半路出错导致系统处于不可用状态。脚本里加上set -e可以让后续命令在前置失败时立刻退出,不至于带病往下跑。
幂等处理的关键是安装命令本身要具备“不重复装”的能力。yum install 和 apt install 在包已安装时会提示 alreay installed,并不会报错,所以基础安装命令天然具备幂等性。但如果是手动修改配置文件这种操作,就需要在改动前做备份,或者写判断语句先检查文件内容是否已满足条件。
5.2 环境信息采集与验收检查
环境部署完不意味着万事大吉,验收环节同样重要。我自己写了一个环境信息采集脚本 deploy_check.sh,输出每台机器的关键信息,统一放到 /tmp/env_report 文件里:
#!/bin/bash echo "主机名: $(hostname)" echo "系统版本: $(cat /etc/os-release | grep PRETTY_NAME)" echo "内核版本: $(uname -r)" echo "CPU 核数: $(nproc)" echo "内存(MB): $(free -m | awk 'NR==2{print $2}')" echo "磁盘可用(GB): $(df -h / | awk 'NR==2{print $4}')" echo "Docker 版本: $(docker --version 2>/dev/null || echo 'not installed')" echo "运行中的容器: $(docker ps -q 2>/dev/null | wc -l)" echo "监听端口: $(ss -tlnp | awk '{print $4}' | grep -v '^$' | sort -u)"把每台机器上跑出的报告汇总,对比哪些端口不一样、哪些依赖版本有差异,基本一眼就能看出环境漂移。这里我强烈建议:环境部署完后把这份报告留存归档,后续任何人接手这台机器时首先看报告,而不是花半天时间去探测环境里到底有什么。
6. 常见问题与排查经验
6.1 软件源失效导致下载安装异常
CentOS 7 官方软件源在系统停止维护后会出现部分源失效的情况,表现为 yum 安装时提示 404 或者连接超时。处理思路是尽快把 Base 源和 epel 源切换到可用的镜像地址。如果镜像地址也暂时不可用,可以临时改用本地缓存安装包,或者从离线包仓库拷贝,但这不是常规方案,尽量很快恢复镜像配置。
Ubuntu 20.04 相对好一些,官方源还在正常存活,但国内访问速度不稳定。遇到软件源问题先执行apt update查看报错的具体源地址,再针对性替换,不要盲目地去清理 entire sources.list。
6.2 编译失败与依赖缺失
编译安装 Python、Git 或者 Nginx 时,最容易遇到的问题是某个头文件或者开发库没装。比如编译 Python 时少了 libffi-devel,会报_ctypes模块找不到;编译 Git 时少了 openssl-devel,会导致 HTTPS 拉代码失败。这类问题排查相对简单,直接看编译日志里的checking for ... not found,再安装对应的-devel包即可。我在初始化脚本里会把常用编译依赖一次性装全,省得每次都要重新编译。
6.3 Docker 相关故障与日志膨胀
Docker 容器运行一段时间后,磁盘被日志文件占满是最常见的故障。默认情况下容器日志没有轮转机制,长时间运行的容器能把一个日志文件写到几个 GB。解决办法是在 /etc/docker/daemon.json 里配置日志轮转:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" } }修改后重启 Docker 服务使配置生效,注意重启 Docker 会连带重启所有容器,最好挑在测试空闲期操作。另外宿主机磁盘分区如果不够大,建议把 /data 目录单独挂一块磁盘,避免容器数据卷占满根分区导致系统异常。
6.4 CentOS 7 与 Ubuntu 20.04 命令差异速查
为了方便复制粘贴,我把两个系统下的常用操作差异整理成一张对照表,直接收藏备用:
| 操作目的 | CentOS 7 | Ubuntu 20.04 |
|---|---|---|
| 安装软件包 | yum install 包名 | apt install 包名 |
| 编译基础依赖 | yum groupinstall "Development Tools" | apt install build-essential |
| 防火墙开放端口 | firewall-cmd --add-port=8080/tcp --permanent | ufw allow 8080/tcp |
| 关闭防火墙 | systemctl stop firewalld | ufw disable |
| 查看服务状态 | systemctl status 服务名 | systemctl status 服务名 |
| 修改主机名 | hostnamectl set-hostname 主机名 | hostnamectl set-hostname 主机名 |
| 查找安装包 | yum provides */文件路径 | apt-file search /文件路径 |
| 清理缓存 | yum clean all | apt autoclean |
另外,CentOS 7 上处理 SELinux 相关权限问题时,常见操作是临时用 setenforce 0 禁用,验证完再恢复。Ubuntu 上对应的坑主要在 AppArmor 配置文件,如果测试服务被拦截,检查 /etc/apparmor.d/ 下对应的配置文件。
最后一个我自己的习惯:每次部署完环境,把执行过的所有命令和配置文件内容追加到 /opt/deployment.log 里,标注日期和操作人。这看起来只是个小动作,但等到一个月后有人问“这台机器上的 MySQL 密码是什么”“Redis 端口为何改了”,打开这份日志立刻就能找到答案,效率翻倍。测试环境部署的核心不是“能跑就行”,而是“可复现、可追溯、可审计”——这套双版本手册的最终目标,也正在于此。