
1. 为什么非把测试环境容器化不可我踩过的本地环境三个坑先说结论Selenium 4.0容器化测试架构不只是把测试代码塞进Docker里跑那么简单而是把整个浏览器执行环境、节点调度、会话管理、资源伸缩全都纳入了可控的容器编排体系。我最早接触这个方向是因为被测试环境折腾到实在受不了了。当时我们团队维护着一个Web管理后台的自动化测试套件用例量不算大但每次跑全量回归都能跑出一堆灵异事件。本地执行明明全绿一上CI就黄红交替同一个用例在张三机器上是过的在李四机器上就挂。有次排查了一个下午最后发现根因特别离谱三位同事的Chrome分别是三个小版本CI机器上的Chromedriver又是另一个版本浏览器和驱动版本对不上导致元素定位行为不一致。另一个痛点出现在资源管理上。早期方案是租几台固定配置的虚拟机上面装好各种版本的浏览器和驱动然后让测试脚本通过RemoteWebDriver远程连接执行。表面上看环境是统一了但虚拟机本身还是活的跑过几次构建之后系统残留的僵尸进程、缓存文件、临时文件越堆越多浏览器启动越来越慢最终只能定期手动重置快照。遇到并发量上来机器资源不够用想加一台节点也不是即时能完成的整套基础设施的弹性几乎为零。第三个坑和版本演进有关。Selenium 3时代Grid的Hub和Node虽然也支持分布式部署但Hub是单点所有会话请求都打到这一个组件上节点状态维护和会话转发逻辑写在一个进程里排查问题的时候你根本分不清到底是调度慢还是浏览器执行慢。Selenium 4.0把Grid架构整个重构了一遍组件拆分得更细可观测性也强了但这也意味着传统装个JRE然后跑selenium-server-standalone.jar的玩法已经不合适了。用容器化方式去部署这套新架构恰好能最大程度发挥组件拆分的优势。所以我后来确定了思路生产环境怎么做高可用和弹性伸缩测试基础设施也应该怎么做。不要把测试环境当成临时搭一下能跑就行的东西而是当成一个独立的、需要被认真设计的系统来看待。Selenium 4.0加上Docker容器化就是这套系统最合适的底座。接下来我会把整个设计过程和实战经验完整写出来从组件原理讲到Compose搭建从容量测算讲到K8s落地最后补充稳定性和代码适配层面的细节。适合正在维护Selenium测试平台、想把测试环境从能用升级到好用的团队参考。2. Selenium 4.0到底改了什么从单一Hub到四组件分布式网格Selenium 3时代的Grid架构很多老工程师应该还有印象一个Hub节点加若干个Node节点Node启动时通过-hub参数向Hub注册然后测试脚本只需要把WebDriver指向Hub的地址Hub负责把NewSession请求转发给某个合适的Node。这套机制能用但有两个明显问题。第一是Hub承担了太多职责既要接收客户端请求又要维护节点注册表还要做会话转发一旦用例量上来Hub的CPU和内存会先成为瓶颈。第二是节点崩溃或者网络闪断时Hub的会话状态维护不够健壮经常出现会话泄漏Node自己以为还活着但Hub侧已经丢失了这个会话的记录。Selenium 4.0把Grid重构成四个独立组件分别叫Router、Distributor、Session Map和Node另外还引入了一个Event Bus用于组件间通信。2.1 四个组件各自的职责边界Router是统一入口所有外部请求包括创建会话、执行命令、查询状态都先打到Router上。Router不直接操作浏览器它的核心逻辑就是转发如果是NewSession请求就交给Distributor处理如果是已经创建好的会话命令就根据Session Map里的记录找到对应的Node把命令转发过去。Distributor是调度决策组件它维护着所有Node的注册信息和当前承载的会话数收到新建会话请求后会按照注册的插槽信息挑选一个符合条件的Node来创建会话。Selenium 4.0还允许你自己写调度策略默认是轮询的方式但你可以通过实现SlotMatcher接口来自定义匹配逻辑比如按浏览器类型、版本、平台标签来精确匹配。Session Map就是一张分布式的会话映射表记录Session ID和Node地址的对应关系。在Selenium 4.0里这张表支持基于Redis的实现多实例部署时会话信息不再丢在单个进程内存里这对于后文要讲的K8s多副本部署非常关键。Node就是真正执行浏览器的地方每个Node可以配置多个插槽每个插槽代表一个浏览器会话的容量。Node启动后通过Event Bus发送注册事件然后周期性地上报自己的健康状况。2.2 一次完整会话请求的路由过程我画一个简单的流程来描述测试代码里调用WebDriver初始化时会向Router发起一个POST /session请求。Router收到之后把这个请求发布到Event BusDistributor订阅到事件后开始调度它会查询Session Map确认当前没有同名会话冲突然后从可用Node列表里选中一个返回这个Node的地址信息。Router拿到地址后把请求真正地HTTP转发给对应NodeNode启动浏览器进程返回sessionId。后续所有命令请求Router都会根据Session Map找到Node地址然后转发。这套设计最大的优势在于每个组件都可以单独扩展。Router可以跑多个副本做负载均衡Distributor可以独立调优Node更是可以按浏览器类型拆成不同的Deployment。这不就是容器化和K8s最擅长的场景吗2.3 Selenium 4.0对容器化更友好的几个细节除了组件拆分Selenium 4.0还有几个变化对容器化部署很关键。首先是内置了分布式追踪支持Router、Distributor、Node之间可以通过OpenTelemetry协议上报链路数据排查某个会话在哪个环节卡住这类问题时有据可查。第二个是Grid的配置方式全面转向环境变量比如SE_NODE_MAX_SESSIONS、SE_NODE_SESSION_TIMEOUT、SE_NODE_REGISTER_PERIOD这些参数在K8s的Pod环境变量里可以直接注入不用再去改配置文件。第三个是支持动态节点注册和注销Node容器启动后能自动在网格里注册销毁后能自动摘除这为后续做弹性伸缩清理了障碍。3. Docker Compose快速搭建Selenium 4集群一份能直接抄的配置理解了架构之后第一步实操就是先把整个Selenium 4集群在本地用Docker Compose跑起来。这一步的核心目的是用最小成本验证组件之间的通信、会话创建流程和资源消耗基线为后续设计生产级架构提供数据支撑。3.1 镜像选型与版本锁定策略Selenium官方在Docker Hub上维护了完整的镜像体系包括selenium/hub、selenium/node-chrome、selenium/node-firefox、selenium/node-edge、selenium/video等。这里必须重点提醒镜像版本一定要锁定不要用latest标签。Selenium 4.0到4.x的演进过程中组件间的API有过小幅调整镜像版本不一致会出现Router和Node之间握手失败的问题。我习惯使用selenium/hub:4.21.0-20240528这种带具体日期的版本日期后缀代表镜像构建时间相同日期的组件镜像可以保证兼容性。另外还要考虑镜像仓库的网络可达性。如果CI或测试环境服务器在国内直接拉取Docker Hub的镜像可能很慢建议提前把镜像推送到自建的Harbor或阿里云ACR镜像仓库里。我在项目里踩过这个坑K8s集群里的Node节点在拉取selenium/node-chrome时超时导致Pod一直处于ImagePullBackOff状态。3.2 docker-compose.yml完整配置示例基础版的集群包含一个Hub和两个Chrome Node配置如下version: 3.8 services: event-bus: image: selenium/event-bus:4.21.0-20240528 container_name: selenium-event-bus ports: - 4442:4442 - 4443:4443 networks: - selenium-net session-map: image: selenium/session-map:4.21.0-20240528 container_name: selenium-session-map ports: - 5555:5555 depends_on: - event-bus environment: - SE_EVENTBUS_PUBLISHERtcp://event-bus:4442 - SE_EVENTBUS_SUBSCRIBERtcp://event-bus:4443 networks: - selenium-net router: image: selenium/router:4.21.0-20240528 container_name: selenium-router ports: - 4444:4444 depends_on: - event-bus - session-map environment: - SE_EVENTBUS_PUBLISHERtcp://event-bus:4442 - SE_EVENTBUS_SUBSCRIBERtcp://event-bus:4443 - SE_SESSIONS_MAP_HOSTsession-map - SE_SESSIONS_MAP_PORT5555 networks: - selenium-net distributor: image: selenium/distributor:4.21.0-20240528 container_name: selenium-distributor depends_on: - event-bus - session-map - router environment: - SE_EVENTBUS_PUBLISHERtcp://event-bus:4442 - SE_EVENTBUS_SUBSCRIBERtcp://event-bus:4443 - SE_SESSIONS_MAP_HOSTsession-map - SE_SESSIONS_MAP_PORT5555 - SE_ROUTER_HOSTrouter - SE_ROUTER_PORT4444 networks: - selenium-net chrome-node-1: image: selenium/node-chrome:4.21.0-20240528 container_name: selenium-chrome-node-1 shm_size: 2gb depends_on: - event-bus - distributor environment: - SE_EVENTBUS_HOSTevent-bus - SE_EVENTBUS_PUBLISHER_PORT4442 - SE_EVENTBUS_SUBSCRIBER_PORT4443 - SE_NODE_MAX_SESSIONS4 - SE_NODE_OVERRIDE_MAX_SESSIONStrue - SE_NODE_SESSION_TIMEOUT180 - SE_NODE_REGISTER_PERIOD120 - SE_NODE_HEARTBEAT_PERIOD30 - VNC_NO_PASSWORD1 ports: - 5901:5900 networks: - selenium-net chrome-node-2: image: selenium/node-chrome:4.21.0-20240528 container_name: selenium-chrome-node-2 shm_size: 2gb depends_on: - event-bus - distributor environment: - SE_EVENTBUS_HOSTevent-bus - SE_EVENTBUS_PUBLISHER_PORT4442 - SE_EVENTBUS_SUBSCRIBER_PORT4443 - SE_NODE_MAX_SESSIONS4 - SE_NODE_OVERRIDE_MAX_SESSIONStrue - SE_NODE_SESSION_TIMEOUT180 - SE_NODE_REGISTER_PERIOD120 - SE_NODE_HEARTBEAT_PERIOD30 - VNC_NO_PASSWORD1 ports: - 5902:5900 networks: - selenium-net networks: selenium-net: driver: bridge这份配置有两个细节需要解释一下。第一个是shm_size: 2gb这是容器里跑Chrome的硬性要求。Chrome的渲染进程会把共享内存当作临时文件系统来用容器默认的/dev/shm只有64MB一旦页面复杂一点就会报Chrome crashed或Failed to allocate memory之类的错。把这个值调到1GB到2GB基本可以规避掉90%以上的浏览器启动崩溃问题。第二个是SE_NODE_SESSION_TIMEOUT和SE_NODE_OVERRIDE_MAX_SESSIONS。Selenium镜像默认情况下Node能创建的会话数和容器CPU核数绑定SE_NODE_OVERRIDE_MAX_SESSIONStrue之后才能让SE_NODE_MAX_SESSIONS真正生效。3.3 启动与联通性验证执行docker-compose up -d之后等待大概30到60秒让各组件完成注册。然后访问http://localhost:4444/status会看到类似这样的JSON输出{ value: { ready: true, message: Selenium Grid ready., nodes: [ { id: chrome-node-1, uri: http://172.18.0.6:5555, maxSessions: 4, slots: [...] } ] } }看到ready: true和两个Node都出现在列表里就说明集群已经联通。这时候先用一个最简单的脚本验证会话创建能力别急着写完整用例from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities options webdriver.ChromeOptions() driver webdriver.Remote( command_executorhttp://localhost:4444/wd/hub, optionsoptions ) driver.get(https://www.example.com) print(driver.title) driver.quit()跑通之后你可以顺手打开VNC比如通过vnc://localhost:5901连接观察Node容器里的真实浏览器操作画面。我第一次看到这个画面的时候直观感受到了容器化带来的隔离性有多干净每个Node就像一个独立的浏览器实验室用完即销毁完全不影响下一个会话。3.4 视频录制容器的接入如果用例失败后需要回看操作过程可以给每个Node再配一个selenium/video容器配置方式非常简单就是在docker-compose里加上这样一个服务video-chrome-node-1: image: selenium/video:4.21.0-20240528 container_name: selenium-video-node-1 volumes: - /tmp/videos:/videos depends_on: - chrome-node-1 environment: - DISPLAY_CONTAINER_NAMEchrome-node-1 - FILE_NAMEnode1_视频容器会通过共享网络抓取Node的屏幕输出在会话结束后把录屏写入挂载目录。这个功能在排查偶现失败的用例时非常有用后面专门开一节讲稳定性问题时再展开。4. 容量规划与并发度测算别等压测挂了才后悔把这套Compose集群跑起来之后下一个问题立刻就会出现到底应该部署多少个Node每个Node配多少并发会话这决定了测试环境的成本上限和执行效率。很多团队在这步都是拍脑袋决定的直到某次全量回归把测试环境压爆了才想起来做容量评估。4.1 单会话资源消耗的基线数据我在一个4核8GB内存的服务器上用官方selenium/node-chrome镜像做了一个资源消耗基线测试。测试场景是打开一个Vue后台管理系统页面执行登录、菜单跳转、表格翻页、表单填写这一类常规操作每个会话持续约3分钟。实测数据大概是这样单个Chrome浏览器进程的内存占用在300MB到600MB之间页面越复杂内存越高CPU在操作高峰期能占到单核的40%到80%平时则比较空闲。如果开启视频录制服务每个会话还需要额外预留100MB左右的内存和一定的磁盘写入带宽。基于这个基线一个最低配的Node1核2GB只能稳定支撑1到2个并发会话再多就会因为资源竞争导致浏览器启动变慢、元素响应超时。4.2 并发度计算公式与实例演算根据上面的数据我整理了一个比较实用的容量规划公式节点可用内存减去系统占用约1GB再除以单会话平均内存保守取600MB得出的结果就是单节点能支撑的安全并发数然后乘上节点数量就是测试环境的整体容量。举个例子一个Node容器分配4核8GB系统本身占用1GB剩7GB给浏览器。7GB除以0.6GB约等于11.6向下取整再打个八折就是把SE_NODE_MAX_SESSIONS设成8比较稳妥。这个打八折的经验是为了应对内存突然飙升的情况因为用例在截图或处理大页面时内存会瞬时涨到接近1GB。我见过不少团队为了压榨资源把8GB内存的Node强行开到16个并发会话结果就是运行一段时间后大量Chrome崩溃、会话卡死。性能压测不是这么做的测试环境的稳定性比极限吞吐量重要得多。4.3 不同浏览器类型的资源差异项目里如果同时需要Chrome和Firefox要注意两者的资源消耗不是一个量级。Firefox的Gecko引擎在处理现代前端框架时的内存占用通常比Chrome高20%到30%所以混合类型的Node池最好拆开部署分别设置SE_NODE_MAX_SESSIONS。比如Chrome Node可以设成8Firefox Node设成6避免两个Node资源不均导致调度瓦时某个类型被拖垮。还有一个容易忽略的资源点是网络带宽。大规模并发执行时如果测试环境同一时间有几十个浏览器在加载外部静态资源而Node所在主机的带宽不足会出现页面加载超时这类假性失败。这时候在Grid指标里观察到的Node资源明明不紧张但用例就是一直在超时重试。这种情况建议用内网代理或缓存服务把静态资源拦截在本地后面稳定性一节也会提到。5. 从Compose走向K8s调度、自动扩缩容与日志治理当用例规模继续上涨团队成员增多单机Compose的局限性会变得很明显。一台物理机的资源始终是有上限的而且Compose本身没有故障自愈能力Node容器崩溃了不会自动重建Router所在的主机宕机了整个测试环境就挂了。这个时候就该把架构搬到K8s上。5.1 为什么不能把Compose文件直接搬到K8s网上有很多资料教你把docker-compose.yml用kompose convert直接转换成K8s的Deployment和Service但从我的实践看这条路基本走不通。首先是网络模型不一样Compose在同一个机器上用自定义bridge网络就能互通K8s里面涉及Service的NodePort或ClusterIP暴露方式每个组件都要显式配置Service。其次是存储Compose可以直接挂载本地目录K8s里要用PersistentVolumeClaim或者emptyDir视频录制的存储方案要重新设计。最后是配置方式K8s环境里路径更推荐用ConfigMap统一管理而不是把环境变量散落在各个服务里。5.2 K8s部署的核心资源清单实际部署时我建议把Router、Distributor、Session Map、Event Bus这四类基础组件用Deployment部署用ClusterIP Service让它们互相通信对外只为Router暴露一个Ingress。Node则单独用Deployment管理每个Node副本承载一定数量的浏览器插槽通过ReplicaSet实现水平扩缩。这里给出Router的Service和Ingress配置片段作为参考apiVersion: v1 kind: Service metadata: name: selenium-router namespace: test-infra spec: selector: app: selenium-router ports: - protocol: TCP port: 4444 targetPort: 4444 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: selenium-router-ingress namespace: test-infra spec: rules: - host: selenium-grid.internal.example.com http: paths: - path: / pathType: Prefix backend: service: name: selenium-router port: number: 4444Node的Deployment里有几个配置项需要注意。一是requests和limits的资源配置必须写清楚比如requests设为cpu: 1, memory: 2Gilimits设为cpu: 2, memory: 4Gi这样K8s的调度器才能合理分配Pod二是readinessProbe一定要配K8s会用HTTP探针去请求Node上的/status接口只有探针通过之后才会把流量分发过来避免Node没启动完就接会话请求三是shmSize要用emptyDir加上medium: Memory来实现单独设置shm_size在K8s里不直接生效。5.3 HPA自动扩缩容按会话队列长度扩NodeK8s自带的HorizontalPodAutoscaler默认只支持CPU和内存指标但测试环境的扩缩容场景更接近需要处理的会话请求数量这个维度。如果只按CPU扩缩容Node加进来之后CPU还没跑起来HPA就缩回去了导致Node数量一直震荡。更好的方案是暴露一个自定义指标每个Router的待分发会话队列长度。Selenium Grid本身没有直接暴露这个指标但可以从/status接口中解析value.ready和value.message里的节点信息或者通过Prometheus exporter把当前所有Node的已用插槽数和排队中的请求数转成指标。HPA配置大致长这样apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: selenium-chrome-node-hpa namespace: test-infra spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: selenium-chrome-node minReplicas: 2 maxReplicas: 12 metrics: - type: Pods pods: metric: name: selenium_active_sessions target: type: AverageValue averageValue: 6这个HPA的含义是当所有Chrome Node的平均已用会话数超过6个就自动扩容一个Node低于6个时再缩容。我把averageValue设成6和容量规划第4节里的单Node会话数8对应起来留了20%的余量避免扩缩过于灵敏。5.4 日志、录屏和测试产物的收集方案K8s里Pod是随时可能被销毁重建的如果录屏文件只存在Pod的本地磁盘上Pod一删就全没了。视频录制容器需要把输出写到共享存储在K8s里一般用PVC挂载到持久化存储卷。如果你用的是云厂商的K8s可以直接用云盘类StorageClass自建集群可以用NFS或者Ceph。日志方面Router和Node会产生大量stdout日志建议部署一套轻量级的Loki或者全托管的日志服务。我在集群里给每个Pod都打了appselenium-node这样的标签然后在日志查询里直接按标签筛选出某个Node的所有运行日志排查问题时比一台台机器登录去docker logs高效一个数量级。6. 容器化测试架构的稳定性清单那些跑起来之后才遇到的坑K8s集群部署完成、用例开始稳定执行之后你会陆续遇到一批容器化环境特有的稳定性问题。这些问题在单机环境下几乎不会出现但在容器编排环境里非常典型。我把踩过的坑整理成了一份清单每一条都有对应的解决思路。6.1 容器内的时区、DNS和代理问题第一个坑是时区。Selenium官方镜像默认时区是UTC如果你的业务系统对时间敏感或者你需要在录屏文件名里加日期时间做归档UTC和本地时间差8小时会搞得人很懵。解决办法是在Node的环境变量里加TZAsia/Shanghai同步修改video容器的时区。第二个坑是DNS解析。Node容器里访问你们公司的内网测试系统时如果域名解析不到页面会一直转圈。这个问题的根源通常是K8s集群的CoreDNS配置里没有加入内网DNS服务器的上游地址需要在CoreDNS的ConfigMap里加上forward . 10.10.0.2这样的配置10.10.0.2是我示例中的内网DNS地址。第三个坑是和代理相关。测试环境如果在公司网络里Node容器访问外网资源比如Chrome更新、CDN加载时可能因为未配置HTTP_PROXY而超时。但坑在于Chromium有一个特性是读取环境变量里的代理配置如果你设置了代理但代理服务器本身不稳定反而会拖慢所有页面加载。我最后的选择是让Node容器直连内网外网资源通过内部镜像或预缓存解决不在容器层面配代理。6.2 共享内存tmpfs不足和视频文件丢失前文提过shm_size的问题在K8s里同样存在。K8s对每个Pod的默认/dev/shm大小也是64MB如果不显式指定Chrome同样会崩溃掉。正确做法是在Node的Pod定义里给容器加一段emptyDir卷并把挂载点指向/dev/shmvolumes: - name: dshm emptyDir: medium: Memory sizeLimit: 2Gi containers: - name: selenium-chrome-node image: selenium/node-chrome:4.21.0-20240528 volumeMounts: - name: dshm mountPath: /dev/shm录屏文件丢失的问题我前面说过Pod销毁就把文件带走了。这里再补充一个特例如果你用selenium/video容器录制视频视频容器的生命周期和Node容器是绑定的如果Node因为故障被K8s重启那么正在录制的视频也会中断。我的处理方式是在测试代码里增加失败截图和页面HTML源码的采集视频只是辅助证据不能作为唯一的排障依据。6.3 会话超时、僵尸会话与Node失联SE_NODE_SESSION_TIMEOUT这个参数的作用是如果某个会话在指定时间内没有收到任何命令Node会自动结束该会话并释放插槽。很多团队忽略了这个参数导致测试脚本异常退出后浏览器进程还挂在Node容器里占着资源慢慢就把所有插槽占满了。但把SE_NODE_SESSION_TIMEOUT设得太小又会带来另一个问题用例步骤之间的思考时间如果超过这个阈值会话会被Node主动掐断表现为执行到一半突然报Session not found。我的建议是把这个值设为180到300秒同时对测试脚本里的等待逻辑做一次严格约束尽量使用显式等待而不是无脑sleep。还有一个比较隐蔽的场景是Node容器和Router之间的心跳失联。如果Node所在的主机负载过高或者网络抖动Node可能连续几次心跳上报失败Router会把它标记为不可用并停止向它分发新会话。然而Node容器自身并不知道这个状态还在继续傻跑。这种情况下稍等片刻通常会自动恢复但如果持续时间很久就需要检查Node主机的网络健康状态了。6.4 浏览器崩溃后的自动恢复与自愈K8s里NGINX Ingress会定期发起/status探针请求但它只能判断Node进程是否存活判断不了浏览器内核是否已经处于异常状态。一个更可靠的方案是在统一调度层做会话级别的健康检查每个会话创建之后测试框架可以定期发送一个轻量命令比如获取浏览器版本或执行页面标题查询连续多次失败就主动调用driver.quit()并标记用例失败释放插槽而不是干等。这个方案可以和录屏、日志联动形成一条完整的排障链路用例失败时自动保存当前页面截图、上报执行日志、保留录屏片段然后触发基础设施侧的告警如果失败率超过阈值还能自动停掉测试任务避免脏数据污染后续用例。7. 测试代码适配从直接操作本地浏览器到连接分布式网格基础设施搭好了最后一步是让测试代码真正跑在网格上。这个过程看起来只是把WebDriver的启动方式从ChromeDriver()换成RemoteWebDriver但实际落地时有一堆细节需要处理。7.1 RemoteWebDriver的连接方式与配置管理Selenium 4.0里的RemoteWebDriver在使用上有一个小变化它支持Options直接传参不再需要手动拼DesiredCapabilities字典。我们常见的写法是这样from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_driver(grid_url: str): chrome_options Options() chrome_options.set_capability(browserVersion, 120.0) chrome_options.set_capability(selenoid:options, { enableVNC: True, enableVideo: True, }) return webdriver.Remote( command_executorgrid_url, optionschrome_options )注意这里的grid_url应该是Router的地址比如http://selenium-router.test-infra.svc.cluster.local:4444/wd/hub。在K8s集群内执行测试时这个地址可以直接用Service的DNS名不用走Ingress从开发机本地调试时才需要走Ingress地址。连接地址不应该硬编码在测试代码里。实践上我建议通过环境变量注入export GRID_URLhttp://selenium-router.test-infra.svc.cluster.local:4444 export BROWSERchrome export NODE_MAX_INSTANCES4测试框架启动时读取这些变量方便在不同环境本地Compose、K8s测试、CI临时集群之间无缝切换。7.2 并行执行与动态资源获取容器化架构解决了执行环境的问题之后用例本身的并发能力就成了新的瓶颈。如果测试代码里有共享变量、共享文件、固定端口绑定那么就算网格有100个插槽你的用例也跑不出并行效果。想要真正吃满网格容量测试代码要做到用例之间无状态、数据互相隔离、每个用例可以独立运行。工程层面我建议引入一个用例管理平台或测试编排框架例如Java生态里的TestNG或者Python生态里的pytest-xdist把并发数配置成和网格总插槽数一致。这样调度的时候测试进程会在Grid里能够分配到真实的浏览器插槽而不会出现测试层开了50个线程网格层实际只有8个插槽这种资源两层脱节的情况。还有一个小技巧可以在用例执行前动态调用Grid的/status接口读取当前空闲插槽数把它作为并发数的上限值。这样在K8s自动扩容Node尚未完成时测试框架也会自动降低并发提高稳定性Node扩容完成后又能自动提速。7.3 元素定位策略在分布式环境下的注意事项分布式环境下元素定位的策略会比本地执行更敏感。原因在于Node容器里跑的是标准浏览器但页面的响应时间受网络和容器资源调度影响更大。我习惯给所有关键操作加上显式等待并设置合理的超时时间。比如from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, timeout20, poll_frequency1) element wait.until(EC.element_to_be_clickable( (By.CSS_SELECTOR, .submit-btn) ))这里把timeout设为20秒poll_frequency设为1秒比本地执行要更宽松一些。在容器环境里把超时设得太短容易遇到页面资源还没加载完就报元素找不到的假失败。当然timeout也不能无脑放大太长会让失败的用例拖慢整批任务的执行时间。合理的做法是先跑一轮基线用例统计所有操作的平均响应时间把显式等待设置成基线的3倍左右。7.4 测试报告与浏览器画面回放的联动最后说一下报告系统的适配。在容器化架构下测试报告不再只是哪个用例过了哪个挂了最好能把资源信息、录屏回放、Node日志和步骤截图全部关联起来。这样用例失败之后测试人员能像回放监控录像一样看到浏览器当时的状态。我现在的方案是测试框架在用例开始时记录一个session_id结束之后把失败截图、页面HTML、执行日志都统一上传到对象存储文件名都以session_id命名后缀在报告系统里添加一个查看会话详情的链接点进去就能看到该会话对应的所有运行时数据。基于这套联动能力排查一个用例在哪个步骤出了问题从打开日志慢慢找变成了直接看录屏里元素在哪一步开始消失效率提升可以说是质的飞跃。从最早被浏览器版本差异搞得焦头烂额到后来整个测试基础设施运行在可弹性伸缩的容器集群上这个过程给我最大的体会是测试架构不应该是临时凑合的附属品。把环境一致性、资源调度、稳定性保障当成一个正式的系统来设计短期看起来多投入了一些工作量长期节省下来的排查成本和时间成本是相当可观的。这套Selenium 4.0加容器化的方案目前已经在我们团队稳定运行了大半年后续我还打算把用例质量分析、失败聚类和更细粒度的资源监控也接入进来让测试基础设施真正成为整个研发流程里可靠的一环。