1. 项目概述:为什么我们需要Docker化的Selenium Grid?

如果你做过Web自动化测试,尤其是需要并行执行或者跨浏览器测试,那你一定对Selenium Grid不陌生。传统的Grid搭建过程,从下载各个浏览器的驱动,到配置节点、注册中心,再到处理不同浏览器版本兼容性问题,每一步都可能是个坑。我记得几年前维护一个测试集群,光是处理Chrome和Firefox版本升级导致的驱动不兼容,就耗费了大量精力。

而Docker的出现,彻底改变了这个局面。 docker-selenium 项目将Selenium Grid的各个组件(Hub、Node)以及浏览器(Chrome, Firefox)连同其驱动,全部打包成了标准的Docker镜像。这意味着,环境搭建从一项繁琐的系统工程,变成了几条 docker run 命令。版本管理变得清晰,环境隔离得到保证,横向扩展也异常简单。今天要聊的,就是如何利用 docker-selenium ,在几分钟内搭建起一个支持Chrome和Firefox的、稳定可用的分布式测试环境。无论你是想快速搭建一个本地调试环境,还是为团队构建CI/CD中的测试基础设施,这套方案都能让你事半功倍。

2. 核心组件与架构解析

在动手之前,我们必须理解 docker-selenium 这套体系里有哪些“积木”,以及它们是如何协同工作的。这能帮助你在出问题时快速定位,在需要扩展时知道该动哪里。

2.1 Selenium Grid 4 架构简述

Selenium Grid 4 采用了更现代、更灵活的架构。其核心是一个 Hub ,它现在更准确地被称为 Router (路由器)或 Distributor (分发器)。它不再是一个单点,而是一组服务的集合,包括负责接收测试请求的Router、负责分配任务的Distributor、负责会话管理的Session Map等。但对于入门使用,我们可以先将其理解为一个统一的入口点。

测试脚本(你的 WebDriver 客户端)将请求发送到Hub。Hub并不执行测试,它只是一个调度中心。真正干活的是 Node (节点)。一个Node就是一个能够运行浏览器实例的环境。在我们的场景中,一个Node就是一个Docker容器,里面预装了特定浏览器(如Chrome)和对应的WebDriver。

docker-selenium 项目为我们提供了所有这些组件的官方镜像,我们只需要按需组合。

2.2 Docker-Selenium 镜像家族

这是整个方案的基石。主要镜像包括:

  • selenium/hub : Grid 4 的Hub/路由器组件。所有测试请求都发往它。
  • selenium/node-chrome : 包含Chrome浏览器和ChromeDriver的节点。
  • selenium/node-firefox : 包含Firefox浏览器和GeckoDriver的节点。
  • selenium/node-docker : 一个特殊节点,它本身内部可以运行Docker容器。这用于支持更复杂的“Docker in Docker”场景,比如在容器内启动一个独立环境的浏览器实例,通常用于高级隔离需求,初期可以不用。
  • selenium/standalone-chrome selenium/standalone-firefox : 这是“All-in-One”的镜像,将Hub和Node合二为一。非常适合快速启动一个单节点的本地测试环境,但不适合分布式扩展。

对于我们要搭建的支持Chrome和Firefox的Grid环境,我们需要组合使用 selenium/hub selenium/node-chrome selenium/node-firefox

2.3 网络与端口映射

Docker容器默认运行在隔离的网络中。为了让外部(你的测试脚本、你的浏览器)能够访问Grid Hub和查看实时会话,我们需要进行端口映射。

  • Hub : 默认监听 4442 (TCP协议端口)、 4443 (HTTP协议端口) 和 4444 (HTTP协议端口,用于Grid UI和客户端通信)。通常,我们只需要将主机的某个端口(如 4444 )映射到容器的 4444 端口,就能通过 http://localhost:4444 访问Grid控制台和提交测试。
  • Node : 需要暴露端口供Hub通信,同时为了支持VNC(让我们可以实时观看测试执行过程),还会映射 5900 端口。这些在 docker-selenium 的镜像中已经预设好,我们启动时只需映射VNC端口即可。

理解这些,我们就能明白后续每一条 docker run 命令背后在做什么。

3. 环境准备与快速启动

我们跳过复杂的理论,直接进入实战。假设你的机器上已经安装了Docker和Docker Compose。如果没有,去Docker官网下载Docker Desktop安装,这是最省事的方式。

3.1 方案一:使用Docker Compose(推荐)

这是管理多容器应用的最佳实践。我们创建一个 docker-compose.yml 文件。

version: '3.8'
services:
  selenium-hub:
    image: selenium/hub:latest
    container_name: selenium-hub
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
    networks:
      - selenium-grid

  chrome-node:
    image: selenium/node-chrome:latest
    container_name: chrome-node
    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
      - SE_VNC_NO_PASSWORD=1 # 启用无密码VNC,仅限本地测试
    volumes:
      - /dev/shm:/dev/shm # 共享内存,提升Chrome稳定性
    ports:
      - "5901:5900" # 将主机5901映射到容器VNC端口5900
    networks:
      - selenium-grid

  firefox-node:
    image: selenium/node-firefox:latest
    container_name: firefox-node
    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
      - SE_VNC_NO_PASSWORD=1
    volumes:
      - /dev/shm:/dev/shm
    ports:
      - "5902:5900" # 将主机5902映射到容器VNC端口5900
    networks:
      - selenium-grid

networks:
  selenium-grid:
    driver: bridge

关键点解析:

  1. 网络 ( networks ) : 我们创建了一个名为 selenium-grid 的桥接网络。所有服务(hub, chrome-node, firefox-node)都加入这个网络。这样,容器之间可以通过服务名(如 selenium-hub )直接通信,无需知道IP地址,这是Docker Compose的一大便利。
  2. 环境变量 ( environment ) : SE_EVENT_BUS_* 这几个变量是Grid 4节点与Hub通信所必需的,必须正确指向Hub的服务名和端口。 SE_NODE_MAX_SESSIONS 定义了单个节点可以同时运行的最大浏览器实例数,这个值需要根据你机器资源(CPU、内存)合理设置,设置太高会导致浏览器崩溃。
  3. 卷 ( volumes ) : - /dev/shm:/dev/shm 这行至关重要。Chrome和Firefox在Docker容器中运行时,如果共享内存太小,极易崩溃。将主机的 /dev/shm 挂载到容器,可以避免大部分因内存不足导致浏览器意外退出的问题。
  4. VNC端口 : 我们将chrome-node的VNC端口映射到主机的5901,firefox-node的映射到5902。这样,你可以分别用VNC客户端连接 localhost:5901 localhost:5902 来实时观看测试执行画面。

保存文件后,在终端进入该文件所在目录,执行一条命令即可启动整个集群:

docker-compose up -d

-d 参数表示在后台运行。使用 docker-compose logs -f 可以查看实时日志,观察启动过程。

启动完成后,打开浏览器访问 http://localhost:4444 ,你应该能看到Selenium Grid的控制台。在“Nodes”部分,应该能看到一个Chrome节点和一个Firefox节点已经注册成功。

3.2 方案二:使用纯Docker命令

如果你不想用Docker Compose,或者想在脚本中动态控制,可以直接使用 docker run 命令。

第一步:启动Hub

docker run -d -p 4442:4442 -p 4443:4443 -p 4444:4444 --name selenium-hub selenium/hub:latest

第二步:启动Chrome节点 这里的关键是,节点容器需要通过 --link 或自定义网络连接到Hub,并且通过 SE_EVENT_BUS_HOST 环境变量指定Hub的地址。由于 --link 已废弃,我们更推荐创建自定义网络。

# 先创建一个网络
docker network create grid

# 启动Hub,并加入网络
docker run -d -p 4444:4444 --network grid --name selenium-hub selenium/hub:latest

# 启动Chrome节点,连接到同一网络,并引用Hub的容器名
docker run -d --network grid --name chrome-node \
  -e SE_EVENT_BUS_HOST=selenium-hub \
  -e SE_EVENT_BUS_PUBLISH_PORT=4442 \
  -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \
  -e SE_NODE_MAX_SESSIONS=4 \
  -e SE_VNC_NO_PASSWORD=1 \
  -v /dev/shm:/dev/shm \
  -p 5901:5900 \
  selenium/node-chrome:latest

第三步:启动Firefox节点

docker run -d --network grid --name firefox-node \
  -e SE_EVENT_BUS_HOST=selenium-hub \
  -e SE_EVENT_BUS_PUBLISH_PORT=4442 \
  -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \
  -e SE_NODE_MAX_SESSIONS=4 \
  -e SE_VNC_NO_PASSWORD=1 \
  -v /dev/shm:/dev/shm \
  -p 5902:5900 \
  selenium/node-firefox:latest

这种方式步骤稍多,但原理更清晰。无论哪种方式,目标都是让Hub和Node能在网络内互通。

4. 编写测试脚本并连接到Grid

环境搭好了,我们得验证它确实能工作。这里以Python的Selenium库为例,展示如何编写测试脚本,让其运行在刚搭建的Grid上。

4.1 基础连接脚本

首先,确保安装了Selenium: pip install selenium

然后,创建一个Python脚本,比如 test_grid.py

from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

# 1. 定义目标Grid Hub的地址
grid_url = "http://localhost:4444"

# 2. 创建Chrome的期望能力对象
chrome_options = webdriver.ChromeOptions()
# 可以添加一些浏览器选项,例如无头模式
# chrome_options.add_argument('--headless')
# chrome_options.add_argument('--no-sandbox')
# chrome_options.add_argument('--disable-dev-shm-usage') # 在容器中运行时常用

desired_caps = DesiredCapabilities.CHROME.copy()
# 将Chrome选项合并到期望能力中
desired_caps.update(chrome_options.to_capabilities())

# 3. 初始化Remote WebDriver,指向Grid Hub
driver = webdriver.Remote(
    command_executor=grid_url,
    desired_capabilities=desired_caps
)

try:
    # 4. 执行你的测试步骤
    driver.get("https://www.baidu.com")
    print(f"页面标题是:{driver.title}")
    # 进行更多操作...
    input("按回车键结束并关闭浏览器...")
except Exception as e:
    print(f"测试执行出错:{e}")
finally:
    # 5. 务必关闭会话,释放Grid节点资源
    driver.quit()

脚本要点:

  • webdriver.Remote : 这是关键。它告诉Selenium客户端不要启动本地浏览器,而是将命令发送到指定的 command_executor (即我们的Grid Hub)。
  • DesiredCapabilities : 用于描述你希望节点提供什么样的浏览器环境。这里我们指定了 CHROME 。如果你想用Firefox,就换成 DesiredCapabilities.FIREFOX
  • driver.quit() : 在Grid环境中尤为重要。它不仅仅关闭浏览器,还会通知Hub释放这个会话,让节点可以接受新任务。如果忘记 quit ,会导致会话泄漏,节点资源被占用。

运行这个脚本,你会看到Grid控制台 ( http://localhost:4444 ) 的会话列表中出现一个活跃的Chrome会话。同时,如果你用VNC客户端连接 localhost:5901 ,就能实时看到浏览器在容器中打开百度首页的操作。

4.2 实现并行与跨浏览器测试

Grid的核心价值在于并行和跨浏览器。我们可以利用Python的 concurrent.futures 模块或 pytest-xdist 插件轻松实现。这里展示一个简单的多线程示例:

from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import threading
import time

grid_url = "http://localhost:4444"

def run_test(browser_name):
    """一个简单的测试任务"""
    if browser_name == "chrome":
        desired_caps = DesiredCapabilities.CHROME
    elif browser_name == "firefox":
        desired_caps = DesiredCapabilities.FIREFOX
    else:
        return

    print(f"开始启动 {browser_name} 测试...")
    driver = webdriver.Remote(command_executor=grid_url, desired_capabilities=desired_caps)
    try:
        driver.get("https://www.bing.com")
        title = driver.title
        print(f"[{browser_name}] 页面标题: {title}")
        time.sleep(2) # 模拟一些操作
    finally:
        driver.quit()
        print(f"{browser_name} 测试完成。")

if __name__ == "__main__":
    # 定义要测试的浏览器列表
    browsers = ["chrome", "firefox", "chrome"] # 启动两个Chrome,一个Firefox

    threads = []
    for browser in browsers:
        t = threading.Thread(target=run_test, args=(browser,))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

    print("所有并行测试执行完毕。")

运行这个脚本,你会在Grid控制台看到几乎同时创建了三个会话,两个分配给Chrome节点,一个分配给Firefox节点(前提是你的节点 SE_NODE_MAX_SESSIONS 设置足够大)。这完美演示了Grid的并行执行能力。

5. 高级配置与生产环境调优

快速启动只是第一步。要把这个环境用于团队协作或CI/CD流水线,还需要一些优化。

5.1 使用特定版本标签

使用 latest 标签虽然方便,但在生产环境是危险的,因为版本更新可能导致不兼容。 docker-selenium 镜像提供了丰富的版本标签,通常与浏览器版本号绑定。

# 在docker-compose.yml中指定版本
services:
  selenium-hub:
    image: selenium/hub:4.16.0-20231219
  chrome-node:
    image: selenium/node-chrome:4.16.0-20231219
  firefox-node:
    image: selenium/node-firefox:4.16.0-20231219

你可以去 Docker Hub Selenium 官方仓库 查看所有可用的标签。锁定版本能确保环境的一致性。

5.2 配置持久化与日志收集

默认情况下,容器内的日志随着容器销毁而消失。我们需要将日志持久化到主机。

services:
  selenium-hub:
    # ... 其他配置 ...
    volumes:
      - ./logs/hub:/opt/selenium/logs
  chrome-node:
    # ... 其他配置 ...
    volumes:
      - ./logs/chrome:/opt/selenium/logs
  firefox-node:
    # ... 其他配置 ...
    volumes:
      - ./logs/firefox:/opt/selenium/logs

这样,所有容器的日志都会保存在当前目录的 logs 子文件夹下,方便排查问题。

5.3 资源限制与调度优化

docker-compose.yml 中,可以为节点容器设置资源限制,防止单个测试消耗过多资源影响宿主机或其他容器。

services:
  chrome-node:
    # ... 其他配置 ...
    deploy: # 注意,这需要Compose文件版本为3.8+,且通常用于Swarm模式。单机可用`resources`。
      resources:
        limits:
          cpus: '2.0' # 限制最多使用2个CPU核心
          memory: 2048M # 限制最多使用2GB内存
        reservations:
          cpus: '0.5'
          memory: 512M

对于单机Docker Compose,可以使用 resources 字段:

    chrome-node:
      # ... 其他配置 ...
      mem_limit: 2048m
      mem_reservation: 512m
      cpus: '2.0'

同时,根据你给节点设置的内存限制,合理调整 SE_NODE_MAX_SESSIONS 。一个Chrome实例可能就需要500MB-1GB内存,所以一个内存2GB的节点,设置 SE_NODE_MAX_SESSIONS=2 是比较安全的。

5.4 启用VNC密码与NoVNC

之前我们设置了 SE_VNC_NO_PASSWORD=1 是为了方便。在生产环境,应该设置VNC密码。 docker-selenium 镜像通过 SE_VNC_PASSWORD 环境变量来设置密码。

environment:
  - SE_VNC_PASSWORD=your_secure_password_here
  # 移除 SE_VNC_NO_PASSWORD

设置了密码后,连接VNC端口就需要输入密码了。此外,你还可以通过 SE_NO_VNC_PORT 环境变量启用NoVNC,这是一个基于Web的VNC客户端。

environment:
  - SE_VNC_PASSWORD=your_password
  - SE_NO_VNC_PORT=7900 # 启用NoVNC并映射端口
ports:
  - "7901:7900" # 将主机7901映射到容器NoVNC端口

然后你就可以通过浏览器访问 http://localhost:7901 ,输入密码 your_password 来观看测试画面,比安装独立的VNC客户端更方便。

6. 常见问题排查与实战技巧

即便用了Docker,有些坑还是得自己踩过才知道。这里分享几个高频问题和解决思路。

6.1 节点无法注册到Hub

现象 :Hub启动成功,节点容器也运行了,但在Grid控制台看不到节点。 排查步骤

  1. 检查网络 :确保Hub和Node在同一个Docker网络中。使用 docker network inspect grid 查看容器是否都正确连接。
  2. 检查环境变量 :确认Node容器中的 SE_EVENT_BUS_HOST 环境变量值正是Hub的 容器名 (在Compose中)或 服务名 (在自定义网络中)。在Node容器的日志里经常能看到连接错误信息。
  3. 查看日志 :这是最直接的方式。分别查看Hub和Node的日志。
    docker logs selenium-hub
    docker logs chrome-node
    
    在Node日志中,寻找类似 Registered Error 的关键字。常见错误是连接不上 selenium-hub:4442 ,这基本就是网络或主机名配置问题。

6.2 浏览器在容器中崩溃或无响应

现象 :测试脚本能启动会话,但执行一会儿就超时或报错,VNC看到浏览器卡死或消失。 解决方案

  1. 挂载 /dev/shm :这是首要检查项。务必在Chrome和Firefox节点的配置中添加 -v /dev/shm:/dev/shm
  2. 增加共享内存大小 :如果挂载后仍有问题,可以尝试指定更大的shm大小。
    chrome-node:
      # ...
      shm_size: '2gb' # 为容器指定2GB的共享内存
    
  3. 添加浏览器启动参数 :在测试脚本的 ChromeOptions 中添加一些针对容器环境的参数。
    chrome_options.add_argument('--no-sandbox') # 禁用沙盒,容器中有时需要
    chrome_options.add_argument('--disable-dev-shm-usage') # 使用临时文件而非/dev/shm,避免内存不足
    chrome_options.add_argument('--disable-gpu') # 禁用GPU,在无头或虚拟化环境中
    
  4. 检查资源限制 :确保Docker容器有足够的内存和CPU分配。浏览器很吃资源。

6.3 会话超时与清理

Grid 4 有会话超时机制。如果一个会话长时间空闲,Grid会自动清理它。有时这会导致你的长时测试意外中断。 配置 :可以通过环境变量调整Hub的超时设置。

selenium-hub:
  image: selenium/hub:latest
  environment:
    - SE_SESSION_REQUEST_TIMEOUT=300 # 会话请求超时(秒)
    - SE_SESSION_RETRY_INTERVAL=5 # 重试间隔
    - SE_RELAXED_CHECKS=true # 对一些检查放宽,有助于稳定性

但更根本的解决方法是,确保你的测试脚本在完成操作后及时调用 driver.quit() ,并优化测试用例,避免不必要的长时间等待。

6.4 如何扩展更多节点?

横向扩展是Grid的优势。假设流量变大,需要增加一个Chrome节点。 使用Docker Compose :非常简单,你可以直接复制一份 chrome-node 的配置,改个容器名和主机VNC端口映射即可。

chrome-node-2:
  image: selenium/node-chrome:latest
  container_name: chrome-node-2
  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_VNC_NO_PASSWORD=1
  volumes:
    - /dev/shm:/dev/shm
  ports:
    - "5903:5900" # 使用新的端口
  networks:
    - selenium-grid

然后运行 docker-compose up -d chrome-node-2 ,新的节点就会自动注册到现有的Hub上。Grid会自动将新的测试请求分发到空闲节点。

6.5 镜像下载慢怎么办?

如果从Docker Hub拉取镜像速度慢,可以配置国内镜像加速器。修改Docker Desktop的配置(或Linux下的 /etc/docker/daemon.json ):

{
  "registry-mirrors": [
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub-mirror.c.163.com"
  ]
}

修改后重启Docker服务。

7. 集成到CI/CD流水线

将Docker Selenium Grid集成到Jenkins、GitLab CI或GitHub Actions中,可以实现自动化测试的闭环。

以GitHub Actions为例,一个简单的 workflow 可能如下:

name: Selenium Grid Test

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      # 启动Selenium Grid服务
      selenium-hub:
        image: selenium/hub:latest
        ports:
          - 4444:4444
      chrome-node:
        image: selenium/node-chrome:latest
        env:
          SE_EVENT_BUS_HOST: selenium-hub
          SE_EVENT_BUS_PUBLISH_PORT: 4442
          SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
          SE_VNC_NO_PASSWORD: 1
        volumes:
          - /dev/shm:/dev/shm
        options: >-
          --shm-size="2g"
          --link selenium-hub

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.9'
    - name: Install dependencies
      run: |
        pip install selenium pytest
    - name: Wait for Grid to be ready
      run: |
        # 一个简单的脚本,等待Grid Hub就绪
        until curl -s http://localhost:4444/wd/hub/status | grep -q '"ready":true'; do
          echo 'Waiting for Grid...'
          sleep 1
        done
    - name: Run tests
      run: |
        # 运行你的测试脚本,REMOTE_URL指向服务名
        python -m pytest your_test_suite.py --remote-url=http://selenium-hub:4444/wd/hub

关键点 :在CI环境中,容器之间通过服务名通信(如 http://selenium-hub:4444 ),而不是 localhost services 块让GitHub Actions自动管理这些依赖容器的生命周期。

通过以上七个部分的拆解,从概念到实践,从快速启动到生产调优,你应该已经掌握了搭建和管理一个Docker化Selenium Grid环境的全套技能。这套方案的核心优势在于其声明式的配置和极佳的可复现性,一份 docker-compose.yml 文件就是整个环境的蓝图,在任何支持Docker的机器上都能一键拉起完全一致的环境,这为团队协作和自动化测试带来了巨大的便利。剩下的,就是根据你的具体业务测试需求,去编写更健壮、更高效的测试用例了。

更多推荐