尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

测试环境Docker化实战:从Docker Compose到微服务环境治理

测试环境Docker化实战:从Docker Compose到微服务环境治理 说个真实场景我接手转转测试环境治理时白天最常听到的一句话是“环境怎么又挂了”。微服务从几十个涨到上百个虚拟机里部署的老环境像一栋住了几十户人家的老楼管线互相纠缠谁动了哪个配置没人说得清。测试经常要等环境恢复开发联调要排时间发布前做的预发验证也常常被环境问题干扰。那段时间我们团队反复在问一个问题测试环境能不能像代码一样有一套可复现、可快速创建、用完就能扔掉的管理方式。答案就是docker。把测试环境docker化之后问题方向一下就变了——不再是怎么修环境而是怎么更快地造环境。这篇文章我按转转测试环境docker化这个项目把从架构选型到落地踩坑的完整过程整理出来。无论你是维护测试环境的后端、测试开发还是准备在公司内部推容器化的工程师这篇都有可以抄作业的地方。全文不聊虚的都是我们实际改过、跑过、摔过的细节。1. 为什么要把转转测试环境容器化从痛点说起1.1 传统测试环境到底痛在哪先说背景。转转的业务线多3C、图书、服装、二手交易等每条业务线下又拆了一堆微服务再加上风控、搜索、推荐、消息推送这类公共组件整体服务数量很快破了百。早期测试环境用虚拟机一个服务一个VM部署全靠手工脚本日子长了问题非常明显。第一个痛点是资源利用率极低。一台16核32G的物理机上面挂几个VM每个VM里只跑一个Java服务但JVM一上来就吃掉几个G内存。实际算下来90%的服务并不需要独占一台VM但架构决定了你必须这么浪费着给。第二个痛点是环境搭建周期太长。新同学入职光把本地依赖装齐就要一两天。新建一套完整测试环境申请资源、装mysql、redis、ES、MQ再拉代码、build、部署快则一个上午慢则一天。联调高峰期环境不够用各个团队就排队等。第三个痛点是环境之间互相污染。测试环境对应的中间件是共享的mysql里谁都能建库redis里key满天飞ES的索引经常被冲。有次A团队在上线前联调B团队跑自动化脚本顺手清理了redis缓存两边差点打起来。根本原因就是共享环境里没有隔离。还有一个很隐蔽的痛点不可复现。虚拟机环境里依赖的配置、版本、数据都是“飘”的今天能用不代表明天能用。谁改了什么没人记录出问题只能靠猜。而对测试来说最怕的就是“我这里明明好的环境上就是复现不出来”。1.2 Docker化解决了什么隔离、复现、弹性docker是什么其实一句话就能说明白把应用和它需要的运行环境JDK、依赖库、配置文件一起打包成一个镜像再用容器隔离地跑起来。简单理解就是——每个应用都自带小房间里面干干净净。拿之前一个特别烦人的case举例业务线A的老项目要用JDK 8业务线B的新项目非JDK 17不可放在同一台机器上光切JAVA_HOME就能切到怀疑人生。容器化之后两个JDK版本各自封进镜像互不干扰各跑各的问题直接消失。docker化带给测试环境的收益是实打实的环境创建时间从过去的三四个小时压缩到几分钟。docker compose up -d一套环境就起来了。资源复用一台8C16G的宿主机同时跑二十套精简测试环境没有问题而以前跑两套虚拟机环境就快卡死了。隔离性每套环境有独立的mysql、redis、MQ容器实例业务线之间不再互相影响。可复现镜像里包含完整运行环境代码版本、依赖版本、基础环境版本全都固化下来同一套镜像在任何机器上行为一致。至于为什么不直接上Kubernetes后面会细说。这里先给个结论在测试环境这个场景下Docker Compose能覆盖80%需求且学习成本、运维成本都低得多。K8s是终局但绝不是起点。2. 整体架构设计与技术选型先跑得动再管得好2.1 基础设施层Docker Engine、私有镜像仓库、镜像加速测试环境docker化的第一层是基础设施。我们当时选了Ubuntu 20.04做宿主机系统docker-ce固定在20.10.x版本。为什么不用最新版因为测试环境上有大量存量脚本和监控依赖特定版本API升级容易牵一发动全身稳定压倒一切。镜像仓库选的Harbor。测试环境每天要构建大量镜像全部走官方Docker Hub不现实一是慢二是镜像共享和权限管理需要内网自建。Harbor部署在企业内网项目名按业务线分权限按团队分镜像push和pull都走内网速度快很多。镜像拉取速度快的另一个原因是配置了registry mirror。国内直连Docker Hub经常超时这个坑做容器化的人都懂。我们在每台宿主机上配置了/etc/docker/daemon.json{ registry-mirrors: [ https://docker.m.daocloud.io ], data-root: /data/docker, log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }顺带提一下data-root和日志配置。容器日志默认无限增长不限制的话用不了多久/var/lib/docker就会被撑爆。我们统一把数据目录挪到大磁盘分区同时限制单容器日志最大10M、最多保留3个文件排查问题时日志还在但不会长期占着磁盘。2.2 服务编排层为什么先用 Docker Compose基础设施就位后下一个问题是服务之间怎么编排。我们有上百个服务依赖关系复杂直接一个一个docker run显然不行。团队里也讨论过要不要直接上K8s最终决定先上Docker Compose原因很务实docker compose用yaml描述整条服务链一个文件就是一套环境的“清单”测试想建一套完整环境一条命令搞定。不引入额外的编排组件没有Kubelet、etcd、ingress这些概念对团队成员来说是透明的。排错路径短docker compose ps和docker logs足够解决日常问题。Compose文件的核心价值是“环境即代码”。我们为每条业务线维护一份docker-compose.yml里面定义它需要的中间件、应用服务、网络。服务之间的调用不再依赖写死IP而是直接用服务名比如order-service要连mysql配置里就写mysql:3306compose自动完成DNS解析。环境变量这块使用单个项目的.env文件管理。不同环境环境参数不同staging和testing分别维护一份.env避免把密码、端口写死在代码里。这套方案的维护成本很低新人理解起来也快。2.3 镜像构建策略可复现、可追溯、可回滚镜像策略是整个项目里最容易被低估的部分。很多团队上了docker镜像是打了但tag只有latest出问题根本不知道跑的是哪次代码。我们的方案是镜像tag里必须包含git commit短SHA。例如gateway-8f3a2f1-staging从tag就能反推到代码提交记录。配合CI流水线每次代码提交自动打包镜像并push到Harbor同时保留最近N个版本旧版本不删方便回滚。这里推荐一个关键技巧多阶段构建。以Java服务为例FROM maven:3.8.6-eclipse-temurin-8 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM eclipse-temurin:8-jre WORKDIR /app COPY --frombuilder /build/target/app.jar ./app.jar EXPOSE 8080 ENTRYPOINT [java, -Xmx512m, -jar, /app/app.jar]多阶段构建的好处第一阶段是完整构建环境第二阶段只保留运行需要的JRE和jar包最终镜像体积从700M降到200M以下推送、拉取都快很多。而且builder阶段先COPY pom.xml再拉依赖能大量利用构建缓存后面每次编译只需几秒。同理Python服务用python:3.9-slim做运行时镜像Node服务用node:18-alpine原则都一样能用体积精简的就不带完整开发工具链。3. 核心实操细节把整套测试环境“装进”容器3.1 数据库、缓存、消息队列的容器化实操中间件容器化是测试环境docker化的重头戏也是最容易出现“看起来能用、用起来难受”的部分。这里拿最典型的mysql、redis、mq聊一聊。MySQL我们用的是官方镜像mysql:8.0.32。一开始同事图省事直接用latest结果有次官方发新版环境一重建字符集和认证方式变化服务直接连不上。血的教训测试环境也得锁版本别用latest。docker-compose中我们的配置如下mysql: image: mysql:8.0.32 container_name: biz-mysql environment: MYSQL_ROOT_PASSWORD: test123 MYSQL_DATABASE: biz_core command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci - --lower_case_table_names1 ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1, -p test123] interval: 10s timeout: 5s retries: 5 volumes: mysql_data:几个细节要说明。数据目录用命名卷volume而不是bind mount原因很简单Windows和macOS上bind mount有路径转换和文件权限问题而命名卷由docker管理跨宿主机一致性更好测试环境不用关心数据具体存在哪个目录。字符集必须显式指定utf8mb4不指定的话插入emoji或特殊字符时会出现编码错误这种问题排查起来很费时间。lower_case_table_names在MySQL8上有严格限制如果是从5.7迁移过来的库表名大小写习惯不一致这个参数一定要确认好。外部访问容器内的mysql这个也是高频问题。容器启动后本机通过localhost:3306访问同网络的其他容器通过mysql:3306访问另一台机器通过宿主机IP:3306访问。注意端口映射有个限制同一宿主机的3306端口只能映射给一个容器如果同时起多套环境就得把宿主机端口改成3307、3308之类的不同端口或者只在内部网络访问而不暴露到宿主机。测试环境我建议后者尽量“能不映射就不映射”减少端口冲突。Redis主从环境也用了容器模拟。两个容器一个主一个从从库配置通过挂载redis.conf实现。redis 6以上主从复制推荐用replicaof需要设置masterauth密码否则从库会连不上主库导致一直SYNC失败。测试环境的redis可以不启用持久化跑挂了重启就好但生产环境千万别这么干。配置如下redis-master: image: redis:7.0.5 command: redis-server --requirepass test123 ports: - 6379:6379 redis-slave: image: redis:7.0.5 command: redis-server --slaveof redis-master 6379 --masterauth test123 depends_on: - redis-master消息队列如果业务依赖可以按需容器化。我们用rabbitmq:3-management镜像带管理后台测试排查队列积压更方便。Kafka需要zk加上kafka两个容器编排文件稍微复杂但套路是一样的。3.2 应用服务容器化Dockerfile、资源限制与日志处理中间件搞定后应用服务容器化没什么神秘感就是把每个服务做成镜像然后按依赖关系编排。这里有几个比较容易踩坑的点逐个说。第一JVM参数。容器里跑Java服务JVM如果默认按物理内存分配堆大小4G内存的宿主机跑三四个Java容器就可能OOM。我们统一在Dockerfile的ENTRYPOINT里写死最大堆大小比如-Xmx512m。容器的内存上限也同时处理services: order-service: image: harbor.example.com/trade/order-service:latest mem_limit: 1g cpus: 1.0 environment: SPRING_PROFILES_ACTIVE: staging注意Java 10以上对容器内存感知更智能但老项目用的JDK 8不显式限制的话灾难不可避免。测试环境资源是有限的每个服务都限制CPU和内存才能保证一台上跑多套环境不互相“抢饭”。第二日志。容器最佳实践是让应用把日志写到stdout用docker logs查看。但对业务线多、日志量大的场景stdout全收进来不利于按需检索。我们的做法是应用日志继续按老路径写入容器内挂载的宿主机目录为每套测试环境单独建log目录和旧日志采集链路无缝对接。节点上挂载目录记得别挂到根路径下面单独用一个数据盘管理。第三健康检查。服务是否真正启动成功不能只看容器状态是Up。Java服务启动要几十秒如果编排里不等它就拉起依赖它的服务调用方会疯狂报连接失败。compose的depends_on默认只保证顺序不保证就绪。所以重要服务都配healthcheck——中间件直接配应用服务就配一个spring boot actuator的/actuator/health探针curl通了才算健康。3.3 网络与端口规划多套测试环境并存的小技巧测试环境往往同时存在多套日常测试一套、版本发布预验一套、灰度回归一套。如果每套环境把所有端口都映射到宿主机端口冲突会把人搞崩溃。我们的方案是内部服务默认不映射端口只保留对外入口。具体说一个典型业务线服务链包含nginx入口、若干业务服务、中间件。nginx容器映射80和443业务服务和中间件都跑在compose的虚拟网络里互相通过服务名访问。对外部测试人员来说访问入口只需要一个域名或IP不管背后是几十个服务还是几百个服务都在nginx里按路径或域名转发。多套环境隔离用compose的project namedocker compose -p trade-stg -f docker-compose.yml up -d。这样同一套compose文件可以起多个独立环境每套环境的容器、网络都有独立前缀互不干扰。这个设计带来的一个好处是端口几乎不再占用了宿主机上不怕多套环境冲突。另一个好处是网络拓扑清晰服务名直接当域名用业务配置里写注册中心地址时也不用关心IP可读性高不少。4. 落地过程中踩过的坑与问题排查实录4.1 安装与启动阶段的坑先讲一个非常常见的现象按教程装了docker执行docker ps报permission denied while trying to connect to the docker api。这个错误本质是当前用户没有访问docker socket的权限。解决方法很简单sudo usermod -aG docker $USER然后重新登录或执行newgrp docker让用户组生效。要注意的是修改用户组之后需要重新打开终端否则权限信号不生效很多人卡在这里。另一个高频问题Windows上装Docker Desktop启动时报virtualization support not detected或者vpx的提示。这个问题大概率是BIOS里的CPU虚拟化没开或者Windows的WSL2/Hypervisor平台功能没有启用。排查路径是先开BIOS虚拟化然后在“控制面板-程序-启用或关闭Windows功能”里勾选“适用于Linux的Windows子系统”和“虚拟机平台”重启后再试。之前我们组一个同事卡了一下午最后发现是BIOS里的虚拟化开关被关了开机进BIOS一开就好。Ubuntu下docker服务启动失败常见原因是daemon.json配置错误、containerd版本不匹配、或者磁盘空间不足。排查建议按顺序来先看docker service的日志journalctl -u docker | tail -100再看docker info最后检查/etc/docker/daemon.json格式是否正确。JSON格式一个逗号写错都能导致服务起不来这个点真的值得留意。4.2 镜像与构建阶段的坑docker pull镜像下载慢几乎是所有用docker的中国工程师都遇到过的问题。慢的根源是默认走官方Docker Hub跨网络拉取不稳定。解决方式是配置registry mirror前面提过的daemon.json里已经配了。配好之后记得systemctl daemon-reload systemctl restart docker不然不生效。镜像构建缓存是个很讲究的细节。Dockerfile里COPY .这条指令会破坏之前所有缓存因为目录内容有任何一个字节变化后面的层都要重新构建。所以正确写法是把依赖声明文件先COPY、先拉依赖再COPY源码。Java项目就先COPY pom.xml然后mvn dependency:go-offlinePython项目就先COPY requirements.txt然后pip install -r这样改代码不影响依赖层构建速度快几个量级。还有一个基础镜像的坑。公共的openjdk镜像老版本已从Docker Hub下线或标记为废弃如果Dockerfile里写了openjdk:8pull的时候可能404。这时候要么改用eclipse-temurin:8这类维护中的镜像要么把基础镜像提前推到自己的Harbor里统一管理。测试环境镜像构建失败的原因有相当一部分就出在这里。4.3 运行与网络阶段的坑容器启动了但服务连不上排查的第一件事永远是看日志。docker logs容器名看得最直接。常见错误是应用启动参数不对、依赖的中间件还没就绪就被强连、环境变量缺失。我们的标准排查顺序是docker compose ps看状态docker compose logs看报错docker exec进入容器手工测试连接三步下来90%的问题能定位。容器内的端口映射问题也常遇到。检查映射是否生效用docker port容器名会直接列出宿主机关联的端口。如果发现容器起来了外部就是访问不了多数情况是容器里监听的不止一个端口比如Java服务默认监听8080但代码里写了server.port8081端口映射写的却是8080访问自然失败。容器间网络不通的问题套路也不复杂。先确认两个容器是否在同一network里用docker inspect看Networks字段然后进入容器ping服务名如果ping不通检查是否用了自定义网络而不是默认bridge。默认bridge模式下容器间不能用服务名互访自定义bridge网络才支持DNS解析。我们统一要求所有服务都通过compose默认创建的bridge网络这样服务名解析就是可用的。还有一种常见的情况是MySQL容器初始化还没完成服务就开始连接报“access denied for user”或“Cant connect to MySQL server”。这不是密码错了是mysql的初始化脚本还没跑完root用户都还没建好。在compose里配healthcheck就能解决让依赖服务等待mysql变成healthy状态再启动。数据卷误删是另一个比较隐蔽的风险。docker system prune时会连带清理未被使用的volume一执行测试环境里的历史数据可能就没了。团队成员操作时一定要带docker system prune --volumes确认甚至干脆不用prune改用docker image prune只清理悬空镜像。我们后来把数据卷全部加了明确的volume名并且重要数据定时备份到宿主机目录防止手一抖数据没了。4.4 常见问题速查表现象可能原因解决方式docker: permission denied while trying to connect to the docker api当前用户不在docker组usermod -aG docker $USER 后重新登录Docker Desktop启动报virtualization supportBIOS虚拟化未开、WSL2未启用开启BIOS虚拟化启用Windows Virtual Machine Platform和WSL2docker pull镜像慢默认走官方仓库配置registry-mirrors重启docker容器启动即退出启动参数错误、端口被占用、OOM看docker logs检查端口占用调大mem_limit容器内访问宿主机MySQL失败网络隔离或宿主IP配置不对同一网络用服务名访问跨宿主机用host.docker.internal或真实IP服务A连不上服务B不同网络、依赖未就绪确认在同一自定义网络配healthcheck数据卷被prune清空执行了docker system prune数据卷命名管理慎用prune磁盘被容器日志占满日志无保留策略配置log rotation限制max-size和max-file5. 最后说几句我的个人体会项目落地到现在测试环境docker化已经跑了将近一年。我个人最大的体会是环境容器化并不只是技术升级它改变的是整个团队的协作方式。以前测试环境是一个需要人“伺候”的系统现在它变成了一个可以通过代码描述、一键重建、用完即扔的普通资源。开发和测试不再等环境而是按需创建环境这一层改变带来的效率提升远比省了几台服务器重要得多。如果你也要做类似的事情我的建议是第一不要一上来就上K8s先用compose把业务跑通把镜像规范、日志规范、数据卷管理这些基本功打牢后面迁移到K8s只是换一个编排层第二一定要定好镜像tag规范没有可追溯性就没有容错空间第三容器化之后定期清理镜像和日志是必做的事否则用不了多久磁盘就会被撑爆。按这个路径一步步走测试环境docker化带来的收益远比你想象中来得快。
返回列表