忍不住想先吐槽一句:还在靠人工一个个浏览器去点、去截图、去比对样式?我见过太多测试同学把大把时间耗在“打开Chrome-登录-截图-关掉-打开Firefox-再登录-再截图”这种循环里,一天下来手点酸了,兼容性 bug 该漏还是漏。这个事儿真不该这么干。
Selenium Grid + Docker 这套组合,是我这些年搭跨浏览器兼容性测试环境用得最顺手的一套方案。你不用买一堆真机,也不用在本地装五六个浏览器互相打架,一条docker compose up就能拉起一个包含 Chrome、Firefox、Edge 多节点的测试集群,测试脚本想跑哪个浏览器就指定哪个,想并行跑几个用例就并发扔进去。这篇文章我会把完整的配置流程、参数选择的原因、以及我在实际项目中踩过的坑全部写出来,照着抄就能用。
1. 为什么是 Selenium Grid + Docker,而不是其他方案
1.1 手动测试的痛点到底在哪里
先说个场景。你负责一个后台管理系统的测试,光浏览器就要求兼容 Chrome、Firefox、Edge 三个内核,再加上不同屏幕分辨率下的布局表现,组合出来至少几十个用例变体。手动测试的话,先在 Chrome 里跑完 20 个模块的用例,截图记录,再切 Firefox 重跑一遍,遇到样式错位再截图提 Bug,完了还要换 Edge 再来一轮。光是环境切换和重复操作就耗掉大半天,真正用来思考测试逻辑的时间反而少得可怜。
更麻烦的是,手动测的覆盖率和一致性完全取决于人的状态。同一个页面,今天看 Chrome 没问题,明天换 Firefox 可能就忘了某个交互细节没点,这种遗漏在回归测试里特别要命。我当时在 X 项目里就吃过这个亏,上线前手动过了一遍主要流程,结果用户用 Safari 打开下单页,价格把按钮挤没了,那个 Bug 后来被运维同事截图发到工作群里,测试组集体社死。从那以后我就下定决心,兼容性测试必须自动化,而且必须覆盖多浏览器。
1.2 Grid + Docker 组合的底层逻辑
Selenium Grid 不是新东西,老一代的 Grid 2/3 用 hub 和 node 两种角色,hub 负责分发请求,node 注册到 hub 上执行具体测试。到了 Grid 4,架构重写了,组件拆得更细,但核心思路没变:一台机器做调度中心,若干台机器(或容器)做执行节点,每个节点可以跑不同的浏览器。
Docker 的价值在于把这些 node 变成了可随时创建、销毁、扩容的“棋子”。你以为容器只是省了安装浏览器的功夫?远不止。第一,隔离性强,每个容器里只有一个浏览器,不会因为某个节点的依赖弄脏宿主机;第二,并发能力可控,一个 Chrome 节点可以限定最多开几个会话,资源不够就再起一个容器;第三,环境一致性,本地开发、CI 流水线、预发环境用同一套镜像,不会再出现“我机器上能跑你机器上不行”的扯皮。
当时我也对比过云测平台,按分钟计费跑大规模回归,账单挺吓人的,而且脚本调试阶段反复拉取设备,效率反而不高。自己用 Docker 在测试服务器上搭一套 Grid,一次性投入,长期免费跑,性价比优势非常明显。你想往 CI 里集成也方便,测试代码不变,把webdriver.Remote的地址指到 Grid 的 URL 就行。
2. 环境准备与前置条件
2.1 硬件与系统层面的最低要求
开始之前先说下环境底线。如果你只是想本地体验一下,一台 8G 内存的笔记本跑个 hub 加两个 node(Chrome 和 Firefox),勉强能转起来,但开四个并发会话会有明显卡顿。要是想正经在公司用,建议准备一台 16G 内存以上的 Linux 服务器,或者你自己的开发机配置足够也行。我实际用的机器是 32G 内存的,跑一个 hub 加五个 node(Chrome 两个节点、Firefox 两个节点、Edge 一个节点),每个节点限 4 个会话,并发 20 个用例毫无压力。
操作系统方面,Linux 是首选,Docker 在 Linux 上性能最稳。Windows 上用 Docker Desktop 也能跑,但要注意 Hyper-V 和 WSL2 的内存占用,别把宿主机搞到卡死。Mac 上用 Docker Desktop 同样可以,M 系列芯片跑 ARM 版本的镜像,兼容性也还行,不过有些旧镜像只有 x86 版本,需要额外开模拟,速度会慢一些。
2.2 Docker 与 Docker Compose 的安装要点
Docker 安装这里我不展开讲太细,不同系统步骤不同,但你务必确认两个点:一是 Docker 版本别太老,建议 24.0 以上,Compose 插件版本在 v2.15 以上,旧版 Compose 的version字段和网络配置写法有差异,照着下面配置写容易踩坑;二是确认docker compose(V2 写法)命令可用,而不是只有docker-compose(V1)。如果只有 V1,也不影响核心功能,但下面的 YAML 写法有些字段需要微调。
装好之后,验证一下:
docker --version docker compose version顺便检查下 Docker 守护进程的存储驱动,Linux 上overlay2是最佳选择,aufs或vfs会出现镜像层性能问题。如果你发现存储驱动不对,多半是 Docker 安装时少配了参数,建议先解决这个问题再往下走。
镜像方面,Selenium 官方提供了配套 Docker 镜像,常用的就是这几个:
selenium/hubselenium/node-chromeselenium/node-firefoxselenium/node-edgeselenium/node-chromium(不带 Chrome 的商标,适合合规要求严格的环境)
版本号建议锁死,比如4.15.0,不要用latest。我见过有人图省事用 latest,结果镜像升级后浏览器版本变了,脚本里某个旧 API 直接报错,排查了半天才发现是镜像悄悄变了,气得不行。
2.3 Selenium Grid 4 的组件角色速览
配置之前,花三分钟理解一下 Grid 4 的架构,不然你看到 docker-compose 里那么多端口和环境变量会一头雾水。
Grid 4 把传统 hub 拆成了四个组件:
- Router:统一接收所有外部请求,相当于网关,对外暴露 4444 端口,也就是你测试脚本里
command_executor指向的地址。 - Distributor:负责把会话创建请求分配给合适的 node,它会查询每个 node 的可用会话数和浏览器类型,然后做一个智能调度。
- Session Map:维护当前所有活动会话的映射关系,用来跟踪哪个会话由哪个节点执行。
- Event Bus:内部通信总线,各组件之间通过它发布和订阅消息。端口 4442 是发布端口,4443 是订阅端口。
而 node 本身只做一件事:接收命令,在它自己的浏览器实例里执行,返回结果。node 启动时会主动向 Event Bus 注册自己,告诉 Grid “我有一个 Chrome 节点,最多可以同时跑 4 个会话”。
为什么要拆这么细?因为 Grid 4 要支持大规模分布式部署,Router 和 Distributor 可以分开部署在不同机器上,灵活度远比 Grid 3 高。但对小团队来说,用 docker-compose 把这些组件都跑在一台机器上,逻辑上依然是完整的 Grid 4 架构,只是在部署形态上做了合并。这个设计带来的直接好处是,你学会了这套搭建,以后要拆成多机部署也不费劲。
3. docker-compose.yml 完整配置与逐项说明
3.1 可直接抄的 compose 文件
下面这个配置是我在实际项目中打磨过的版本,直接复制到docker-compose.yml文件里,改一下镜像版本号就能用。
services: selenium-hub: image: selenium/hub:4.15.0 container_name: selenium-hub restart: unless-stopped ports: - "4442:4442" - "4443:4443" - "4444:4444" environment: - SE_GRID_MAX_SESSION=20 - SE_SESSION_REQUEST_TIMEOUT=300 - SE_SESSION_RETRY_INTERVAL=5 networks: - grid-network chrome-node-1: image: selenium/node-chrome:4.15.0 container_name: chrome-node-1 restart: unless-stopped shm_size: 2gb depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 - SE_NODE_MAX_SESSIONS=4 - SE_NODE_SESSION_TIMEOUT=180 - SE_NODE_OVERRIDE_MAX_SESSIONS=true ports: - "5555:5555" networks: - grid-network chrome-node-2: image: selenium/node-chrome:4.15.0 container_name: chrome-node-2 restart: unless-stopped shm_size: 2gb depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 - SE_NODE_MAX_SESSIONS=4 - SE_NODE_SESSION_TIMEOUT=180 - SE_NODE_OVERRIDE_MAX_SESSIONS=true networks: - grid-network firefox-node-1: image: selenium/node-firefox:4.15.0 container_name: firefox-node-1 restart: unless-stopped shm_size: 2gb depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 - SE_NODE_MAX_SESSIONS=4 - SE_NODE_OVERRIDE_MAX_SESSIONS=true networks: - grid-network edge-node-1: image: selenium/node-edge:4.15.0 container_name: edge-node-1 restart: unless-stopped shm_size: 2gb depends_on: - selenium-hub environment: - SE_EVENT_BUS_HOST=selenium-hub - SE_EVENT_BUS_PUBLISH_PORT=4442 - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 - SE_NODE_MAX_SESSIONS=3 - SE_NODE_OVERRIDE_MAX_SESSIONS=true networks: - grid-network networks: grid-network: driver: bridge如果你不需要 Edge,把edge-node-1整个删掉就行。同理,想加 Safari 或 Opera,逻辑一样,把镜像名换一下。
3.2 关键端口与网络参数的含义
看到4444:4444你肯定能猜到是 Grid 控制台和请求入口。但 4442 和 4443 是干嘛的?这是 Grid 4 里 Event Bus 的发布和订阅端口,node 要注册到 hub 上,就必须通过这两个端口完成消息交换。所以你在 node 的环境变量里看到SE_EVENT_BUS_PUBLISH_PORT=4442和SE_EVENT_BUS_SUBSCRIBE_PORT=4443,指的就是这两个端口。
有个很容易搞混的地方:node 为什么要映射5555:5555?node 在注册到 Grid 后,会把自己的地址和端口报告给 Distributor,Grid 后续会把测试命令转发到这个端口上。容器内 node 默认监听 5555,所以你把它映射出来,一方面物理机能访问到这个端口,另一方面调试的时候可以直接curl localhost:5555/status查看节点状态。如果你起多个 node 但不想暴露端口,把ports段去掉也可以,Grid 内部通过 Docker 网络依然能通。
shm_size: 2gb这个参数非常关键。浏览器跑测试要读写/dev/shm共享内存,默认容器只有 64MB,Chrome 一跑复杂页面就直接崩,报错信息多半是 “SessionNotCreatedException: chrome failed to start” 或者 “DevToolsActivePort file doesn't exist”。我当时第一次搭 Grid 就是漏了这个,Chrome 节点怎么都起不来会话,后来把shm_size加大到 1GB 以上才稳定。这也是为什么我在每个 node 上都显式声明了 2GB。
网络模式我用的自定义bridge网络。为什么不直接用默认的 bridge?因为 Compose 会自动创建项目专属网络,服务名可以直接当主机名解析,selenium-hub这个服务名在 node 容器里就能解析成 hub 的容器 IP,不需要写死 IP 地址。用自定义网络可以保证服务间通信不受宿主机网络环境影响,而且容器重启后 IP 变了也不影响互相访问。
3.3 环境变量逐个拆解
这里我把配置文件里用到的environment字段逐个说透。
SE_GRID_MAX_SESSION=20是 Grid 全局最大并发会话数。注意,这个值不是单节点并发上限,而是整个 Grid 同一时间能处理的会话总数。你设置 20,实际所有节点加起来并发就受这个数约束。节点多的时候,这个值可以设大点,比如 50,避免成为瓶颈。
SE_SESSION_REQUEST_TIMEOUT=300表示客户端请求创建会话时,如果超过 300 秒还没分配到节点,就返回超时。之前有同事写脚本时没有释放 WebDriver 实例,把节点会话都占满了,后来新请求全卡住,就是靠着这个超时时间让测试快速失败而不是傻等。
SE_SESSION_RETRY_INTERVAL=5是 Distributor 在节点资源不足时,每隔 5 秒重试一次分配。这个值调大点能降低 Grid 内部的重试频率,但会让排队等待的时间变长。实测 5 秒是比较合理的平衡。
SE_NODE_MAX_SESSIONS=4是指单个 node 最多同时跑 4 个会话。为什么是这个数?容器里跑浏览器,每个会话大约占 300MB 到 600MB 内存,2GB 的 shm 加上镜像我分配给 node 的内存配额,4 个会话比较稳妥。之前我试过设成 8,结果并发一上来,容器内存飙满直接 OOM,整个 node 挂掉,连带其他会话也中断,反而更不稳定。
SE_NODE_OVERRIDE_MAX_SESSIONS=true这个变量比较隐蔽。官方镜像里,如果 node 检测到 CPU 核数少,会自动下调默认最大会话数,有时候你以为设了SE_NODE_MAX_SESSIONS是 8,实际生效却只有 2。加上这个变量可以强制使用你设置的值,避免被自动探测覆盖。
SE_NODE_SESSION_TIMEOUT=180表示单个测试会话如果 180 秒内没有任何活动,节点会主动结束会话,释放资源。
最后一个depends_on字段,Compose 用它来保证 hub 先启动,node 后启动。但注意,depends_on只保证启动顺序,不保证 hub 已经就绪。所以第一次启动时,node 可能启动后连不上 hub 自动退出,需要你手动docker compose restart node名重启一次。为了避免这情况,可以在 node 的启动命令里加一个等待逻辑,但为了配置简单,我更推荐启动后统一检查,后面会讲。
4. 启动、验证与日常运维
4.1 一条命令拉起整个测试集群
在docker-compose.yml所在目录执行:
docker compose up -d加-d参数让容器在后台运行。第一次启动要拉镜像,耗时取决于网速,耐心等一会儿。执行完之后用docker compose ps查看所有服务状态,正常情况下每个服务的状态都应该是Up。
如果看到某个 node 在反复重启(RESTARTING),大概率是连接不上 hub。先看看日志:
docker compose logs -f chrome-node-1日志里如果出现Failed to register或者Connection refused,十有八九是 Event Bus 端口没配对,回过来检查SE_EVENT_BUS_HOST和端口配置。
所有容器起来之后,打开浏览器访问http://localhost:4444,你应该能看到 Selenium Grid 的 UI 控制台。在控制台里能看到已注册的节点列表,每个节点会显示浏览器类型、最大会话数、当前会话数。这一步能直观确认环境是否就绪。
4.2 用 status 接口做健康检查
除了 UI,我平时更习惯用命令行快速检查节点状态。Grid 4 提供了几个标准接口:
# 查看所有节点详细信息 curl http://localhost:4444/status # 查看某个节点自身状态 curl http://localhost:5555/status返回的 JSON 里有value.nodes数组,每个元素包含一个节点的信息,比如浏览器名称、版本、maxSessions、sessionCount等。写自动化巡检脚本的时候,这个接口可以直接拿来用,定时检查节点在线数和空闲数,如果有节点掉线就报警。我曾经在一个项目里就是用这个接口配合一个简单的 Python 脚本,每天早上自动检查 Grid 健康状况,有异常直接发工作群通知,省了不少人工盯梢的功夫。
4.3 节点扩容与缩容的正确姿势
测试高峰期来了,用例量大,并发需求高,这时候再手动改 compose 文件加节点就慢了。我常用的做法是:在 YAML 文件里写好多个 node 配置,比如之前定义了chrome-node-1和chrome-node-2,平时只启动其中一个,需要扩容时再单独启动另一个:
# 只启动 chrome-node-2 docker compose up -d chrome-node-2这个方法的好处是,容器名称和网络配置在 YAML 里都已经声明好了,直接拉起就自动注册到 Grid,不用改任何配置。用完以后:
docker compose stop chrome-node-2节点就从 Grid 下线,并发容量恢复原样。整个操作几秒钟完成,非常灵活。
另外提醒一句,Grid 的节点注册不是实时的,node 启动后要几秒钟到十几秒钟才能完成注册,刚启动就压测可能报 “No node are available”。测试脚本里可以加一个重试机制,或者等控制台里看到节点在线再跑。
4.4 日志管理与容器清理
跑得久了,容器日志会越积越多,尤其是频繁跑测试的情况下,一天几个 GB 很正常。我一般会限制日志大小,在 compose 文件里加上:
logging: driver: "json-file" options: max-size: "50m" max-file: "3"每个服务的日志上限 50MB,保留 3 个文件,循环写入。不然服务器磁盘被日志占满,整个 Grid 直接瘫痪,这个事故我见过不止一次。
容器清理方面,升级镜像或改配置后,旧的容器和镜像留着会占空间:
# 删除已停止的容器 docker compose rm -f # 清理悬空镜像 docker image prune -f这两个命令可以定期执行,保持服务器干净。
5. 测试脚本接入与并行执行策略
5.1 Remote WebDriver 的配置方法
环境搭好了,测试脚本怎么连过来?核心就是把你本地用的 ChromeDriver/FirefoxDriver 换成RemoteWebDriver,然后把地址指向 Grid。
Python 示例:
from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_driver(browser="chrome", version=None): if browser == "chrome": options = Options() options.browser_version = version # 可选 options.set_capability("se:name", "测试用例名-登录功能") # 给会话命名,方便在Grid UI中辨认 elif browser == "firefox": options = webdriver.FirefoxOptions() elif browser == "edge": options = webdriver.EdgeOptions() else: raise ValueError(f"不支持的浏览器: {browser}") options.set_capability("platformName", "linux") driver = webdriver.Remote( command_executor="http://localhost:4444", options=options ) return driverJava 示例也贴一下,毕竟很多公司的自动化框架是 Java 写的:
import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.RemoteWebDriver; import java.net.MalformedURLException; import java.net.URL; public class GridDriverFactory { public static WebDriver createDriver(String browser) throws MalformedURLException { ChromeOptions options = new ChromeOptions(); options.setCapability("platformName", "linux"); return new RemoteWebDriver(new URL("http://localhost:4444"), options); } }关键点在于command_executor的 URL 指向 hub 的 4444 端口,而不是本地某个 driver 的端口。只要你通过options指定了浏览器类型,Grid 会自己找一个能跑这种浏览器的节点来执行。如果你的测试脚本跑在另一台机器上,把localhost换成 Grid 所在机器 IP。
se:name这个 capability 是 Grid 4 特有的,设置后会在 Grid UI 上显示为测试名称,特别方便在跑大量用例时一眼看出每个会话是哪个用例。我在项目里都会让框架自动把用例名传进来,排查问题时不用猜这个会话在跑什么,体验提升非常明显。
5.2 并发执行时的参数调优策略
并行跑测试不是简单的pytest -n 8就完事,要和 Grid 的并发能力匹配。Grid 节点总并发是 4 + 4 + 4 + 3 = 15(按上面的 compose 配置),你本地开 16 个线程去跑,多出来的一个会排队等节点释放,等的时间比跑的时间还长,反而拖慢整体速度。
更合理的做法是,先统计你的用例平均执行时间,再计算并发度。比如平均每个用例 30 秒,节点上会话时间 180 秒才超时,那么一个节点 4 个会话,一条链路上最多能跑 24 个用例。你控制总并发在 12 到 15 之间,既能跑满 Grid 资源,又不会因为排队导致等待时间失控。
具体到 pytest,我常用pytest-xdist插件:
pytest -n 12 --dist loadscope -q tests/--dist loadscope会让同一个测试类/模块的用例尽量分配到同一个 worker,减少因为共享数据导致的冲突。这个配置在我实际项目中确实减少了并发时的资源争抢。
如果你用的是 Java + TestNG,可以在 testng.xml 里配置parallel="methods" thread-count="12",效果类似。
5.3 视频录制与失败重试的加分项
官方镜像支持录制测试执行过程的视频,这个功能在排查问题时非常香。开启方式很简单:在 node 的环境变量里加SE_SCREENSHOT_GRID_URL,然后在测试脚本里添加se:recordVideocapability:
options.set_capability("se:recordVideo", True)执行完测试后,Grid UI 的会话详情页里能看到视频下载链接。文件存在容器里,也可以自行挂载到宿主机目录来持久化。
失败重试建议在测试框架层面做,而不是在 Grid 层做。你在编写用例时用pytest-rerunfailures或 TestNG 的retryAnalyzer,重试次数不要太多,2 次就够。为什么?如果每次重试都重新创建会话,节点资源会被反复占用,Grid 调度压力增大。而且重试只应该针对偶发的环境问题,比如网络抖动或页面加载超时,真正的功能 Bug 重试多少次都是挂,纯粹浪费时间。
5.4 浏览器选项注入的注意事项
很多测试脚本需要在浏览器里配置代理、设置无头模式、禁用 GPU 等。这些选项在 RemoteWebDriver 场景里依然生效,只要在options上设置即可。但有个细节要注意:如果不同节点跑不同浏览器,你必须在代码里根据浏览器类型生成对应的Options对象。比如 Chrome 的add_argument("--headless")不能用在 Firefox 的FirefoxOptions上。
另外,在 Grid 分布式环境下,无头模式往往不是必须的。容器里的浏览器本身就没有可见界面,和本地的无头模式效果差不多。所以你就别额外加--headless了,省得制造不必要的差异。我见过有人不加这个参数,担心测试会在服务器弹出浏览器窗口,其实完全不会,容器环境根本没有图形界面。
6. 常见问题与排查实录
6.1 节点注册不上的排查思路
这应该是搭建 Grid 时遇到最多的问题。症状是控制台里看不到节点,或者docker compose logs显示 node 一直连接失败。
排查顺序我总结成一套口诀:先看网络,再看端口,最后看配置。
第一步,确认selenium-hub容器起来了并且 4442/4443 端口在监听。在宿主机执行:
curl http://localhost:4442/status如果返回 JSON,说明 hub 正常。如果不通,可能是 hub 启动失败,看下日志。
第二步,确认 node 和 hub 在同一网络中。在 node 容器里执行:
docker exec -it chrome-node-1 ping selenium-hub如果 ping 不通,说明 Compose 网络没配对。大多数情况是因为服务名写错,比如拼写为selenium_hub,而 Compose 网络里的主机名用的是服务名selenium-hub,中间是短横线。这个问题我当时查了很久才发现。
第三步,检查SE_EVENT_BUS_PUBLISH_PORT和SE_EVENT_BUS_SUBSCRIBE_PORT是否和 hub 暴露的端口一致。这俩端口配置错了,node 永远注册不上去,而且日志里不会直接说“端口错了”,只会显示连接失败。
6.2 浏览器启动失败与 shm 内存不足
表现:你的测试脚本能创建会话,但页面一复杂就报chrome not reachable或者直接崩溃,日志里能看到 “SessionNotCreatedException”。
最常见两个原因:一个是前面反复强调的shm_size太小,一个是容器内存溢出。前者改shm_size就能解;后者要关注容器实际内存占用,用docker stats查看每个容器的内存和 CPU 使用情况。
我之前在项目里遇到过 Chrome 占用内存异常增长,后来发现是测试代码里有内存泄漏,每次打开新页面都创建了不必要的 WebDriver 实例,没有及时quit(),导致同一个会话里资源越来越多。排查这个问题最好的办法就是在测试的tearDown里强制driver.quit(),确保每个用例结束后释放会话和浏览器进程。这个习惯养成了,很多稀奇古怪的崩溃都会消失。
6.3 并发达到上限导致请求排队
当你用较大并发跑测试,发现部分用例等待时间明显变长,甚至超过SE_SESSION_REQUEST_TIMEOUT报错,这说明 Grid 的资源已经饱和了。处理策略就是前面提到的扩容节点或减少并发度。
顺带说一句,SE_SESSION_REQUEST_TIMEOUT这个值别设太大,我之前为了“宽容”设成了 1200 秒,结果某次用例集体卡死,所有请求都排队等一个僵尸会话释放,测试跑了一晚上都没结束。后来改成 300 秒,排不上就直接失败,反而能倒逼我们优化并发布局。
6.4 测试结果不稳定与超时率上升
Grid 搭好以后,有时会发现同样一套用例,今天全绿,明天挂两三成,重启容器又好了。这个“玄学”问题,多半是节点资源碎片化,或者镜像缓存老化。
我的处理习惯是每天早上 CI 跑之前,执行一次docker compose restart,把节点环境重置到干净状态。配合定时任务就行:
# 每天凌晨2点重启 Grid 所有服务 0 2 * * * cd /opt/grid && docker compose restart这样能显著降低偶发性失败。注意,在容器启动后等 15 秒再开始跑测试,给节点留足注册时间。可以在 CI 脚本里加一个sleep 15,或者在测试框架里做节点可用性轮询。
6.5 日志定位的一个独家技巧
Grid 的日志分散在各个容器里,排查问题时要一个个docker logs翻,效率很低。我习惯修改 compose 文件,把 node 的日志输出集中到宿主机目录:
volumes: - ./logs/chrome-node-1:/var/log/selenium这样日志写到了宿主机./logs下,排查时一条grep -r "ERROR" ./logs/能看到所有节点的错误。这个做法对多节点场景特别有用,也方便后续接入日志分析平台。如果你用 Docker Desktop for Mac/Windows,要确认共享目录权限有没有放开,不然容器里写不了文件。
7. 从单机到持续集成的延伸思考
环境搭好只是第一步,真正的价值在于把 Grid 接入 CI 流水线,让每次代码提交都自动触发跨浏览器兼容性测试。我的做法是:在 CI 的测试阶段用docker compose up -d拉起一个精简版 Grid(比如只跑 Chrome 和 Firefox 节点),测试脚本跑完后自动docker compose down清理环境。
这样一个干净、可复现、用完即焚的测试环境,比在固定服务器上常驻一个 Grid 更稳妥,不会出现环境被上次测试污染的问题。当然代价是每次 CI 都要拉镜像启动容器,时间会多几十秒,但这个成本远小于人工排查环境问题的成本。
具体到镜像仓库的管理,建议把 Selenium 官方镜像构建一次后推送到自己的镜像仓库,CI 和开发环境都从内部仓库拉取。一方面加速拉取时间,另一方面保证版本统一可控。别再让 CI 随机拉 latest 了,出问题都不知道哪个节点跑在哪个版本上。
无论你选哪种方式,这套 Docker + Grid 的组合都能把“跨浏览器兼容性测试”从一个费时费力的手工活,变成一条自动化的流水线。我在实际项目中体会最深的一点是:搭建这个环境本身不难,难的是你真正理解每个参数的含义、每个端口的作用,以及出现问题时能快速定位。把这篇文章里的知识点消化掉,你自己也能搭出一套稳定、可靠的跨浏览器测试平台。
最后再分享一个我在生产环境中一直坚持的细节:每次跑完测试,记得看一眼 Grid 控制台里的会话记录,抽查几个视频和截图,确认测试真的在浏览器里跑出了预期效果。自动化测试最怕的不是环境挂了,而是环境跑着但测的东西根本不是你以为的那个页面。保持对测试结果的“怀疑感”,比任何工具都重要。