两年前我第一次在 CARLA 0.9.16 上跑 ScenarioRunner 时差点被 Python 版本折腾到怀疑人生。当时宿主机是 Ubuntu 22.04自带 Python 3.10而 ScenarioRunner 0.9.16 这套代码和 CARLA 客户端的二进制扩展库对 Python 版本极其敏感推荐环境是 Python 3.8。换个说法你在 py310 下装好的依赖甚至 import 一个 carla 模块都会直接给你飘红。后来我把整套环境塞进 Docker 容器里用 py38 基础镜像固定版本配合 CARLA 容器做联调才算彻底消停。这篇学习笔记就是记录整个方案怎么落地、容器怎么写、网络怎么串、坑怎么填给同样想用 docker py38 玩 ScenarioRunner 0.9.16 的朋友一个可以直接抄的作业。1. 为什么最后选择 Docker 来承载 py38 ScenarioRunner1.1 版本锁死的现实0.9.16 和 Python 3.8 为什么绑得这么死CARLA 0.9.16 的 Python 客户端库实际上是编译好的二进制和你系统里的 Python 版本、GLIBC 版本都有关系。官方虽然也提供了 Linux 版本但内部默认适配的是 Python 3.8 这一档。ScenarioRunner 只是外层场景调度框架它依赖carla模块和一堆科学计算库底层调用链一旦遇到 Python 版本不匹配表现出的报错往往是“No module named carla”或者编译某个库时 glibc 版本冲突光是排查方向就能耗掉一整天。很多人在这一步会想那我直接在系统里装 py38 行不行可以但非常脏。系统自带的 pip、uvicorn、Anaconda 环境可能都在用别的 Python 版本update-alternatives、PYTHONPATH、egg目录到处打架。更麻烦的是你改的是全局环境后面其他项目一跑就崩这是纯自找麻烦。1.2 容器隔离带来的实际收益我最后选择 Docker核心原因就三点版本固定镜像里是 py38容器里就是 py38和宿主机 Python 完全隔离。只要镜像不重写ScenarioRunner 跑在什么环境里几乎可以复现。依赖清理成本为零CARLA 需要的pygame、shapely、networkx、lxml这些包可能带一堆系统级依赖比如 libgl1、libsdl2。在容器里装完不想要直接docker rm销毁不用碰宿主机。团队协作友好给同事发一个 docker-compose 文件对方拉镜像就能跑不用再读二十页环境配置文档。1.3 镜像选型思路对比当时我试了三种方案差异挺明显方案优点缺点适合场景用 CARLA 官方镜像直接在容器里跑 Server省事版本对应镜像巨大需要 GPU 透传ScenarioRunner 还得另行装只是做快速验证用python:3.8-slim自建 ScenarioRunner 容器轻量、可控不影响 CARLA Server依赖需要自己补大多数个人学习/二次开发用 Anaconda 镜像 py38 环境包管理方便镜像更大环境冲突时反而不容易定位重度依赖数据分析的场景我最后采用的是第二套CARLA Server 跑在官方容器里ScenarioRunner 跑在自建的 py38 容器里两个容器通过宿主机网络互通。这套结构最清晰后面想改 ScenarioRunner 代码也方便。2. 环境准备宿主机 Docker 安装与 Windows 避坑2.1 Linux 宿主机装 Docker Engine 的速通步骤如果服务器是干净的 Ubuntu不要直接闭眼用发行版自带的 docker.io用 Docker 官方源更稳。我这里给一套精简步骤亲测可以跑起来sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo usermod -aG docker $USER装完以后务必要重新登录一次 SSH让docker组的权限生效否则每次都要敲 sudo很容易后面脚本出权限问题。然后验证docker run --rm hello-world看到 Hello from Docker 就算环境通了。2.2 Windows 下 Docker Desktop 启动失败排查很多人在 Windows 上卡在“Docker Desktop failed to start because virtualization support wasn‘t detected”这类报错。说白了就是虚拟化相关开关没打开或者 WSL2 没就绪。这个和后面跑 CARLA 容器不是直接关系但属于前置条件我还是把排查顺序列出来先去 BIOS 确认 Intel VT-x / AMD SVM 已开启。任务管理器 - 性能 - CPU - 虚拟化显示“已启用”才正常。确认 Windows 功能里勾选了“适用于 Linux 的 Windows 子系统”。执行wsl --update把 WSL 内核升级到新版然后wsl --set-default-version 2。Docker Desktop 设置里把 “Use the WSL 2 based engine” 打开不需要的发行版可以停掉。提示如果用的远程开发服务器通常不会碰 Docker Desktop 的问题但如果你是在 Windows 本地折腾这套排查路径大概率能解决 90% 的启动失败问题。2.3 验证 Docker 基本可用还不够要确认网络模式支持情况跑 ScenarioRunner 时客户端容器需要连上 CARLA Server 的端口通常会用到--networkhost。在 Linux 上这是个很省事的方案因为容器共享宿主机的网络栈localhost:2000就能访问到 CARLA Server。但在 Docker Desktop 里宿主机网络和容器网络的隔离逻辑不太一样--networkhost在 WSL2 下的表现也有差别。我建议你在真正开始前先测一下docker run --rm --networkhost curlimages/curl curl -s http://localhost:2000/ping在 CARLA Server 没启动时会失败这是正常的但如果返回“connection refused”说明网络能通只是服务没起如果返回“couldn‘t connect to host”之类那说明本地网络联通性还有问题。这个区分很重要后面排查连接超时时能省很多力气。3. Dockerfile 与服务编排复现我调好的 py38 环境3.1 准备 ScenarioRunner 0.9.16 源码和依赖ScenarioRunner 的源码要选对 tag别直接拉 master。因为 master 随时在变可能已经适配 CARLA 0.9.17 甚至更新的接口和 0.9.16 的 Server 不兼容。我用的命令是git clone --branch 0.9.16 --depth 1 https://github.com/carla-simulator/scenario_runner.git克隆完能看到根目录下的scenario_runner.py、route_scenario.py、requirements.txt这些关键文件。我需要的是这套固定版本代码方便后续控制变量。3.2 Dockerfile 内容及每一行的理由我最终使用的 Dockerfile 长这样FROM python:3.8-slim ENV PYTHONUNBUFFERED1 \ PIP_NO_CACHE_DIR1 RUN apt-get update apt-get install -y --no-install-recommends \ libgl1 \ libglib2.0-0 \ libsdl2-2.0-0 \ libx11-6 \ libxext6 \ libxi6 \ libxcursor1 \ libxrandr2 \ libxinerama1 \ ca-certificates \ rm -rf /var/lib/apt/lists/* WORKDIR /workspace/scenario_runner COPY requirements.txt . RUN pip install --upgrade pip pip install -r requirements.txt COPY . . ENV PYTHONPATH/workspace/scenario_runner:/workspace/carla_python_api:$PYTHONPATH ENTRYPOINT [python]每行都不是白写的我解释几个容易忽略的地方libgl1opencv、pygame 这些库做图像渲染时需要 OpenGL 的软链接slim 镜像里没有不装就直接报libGL.so.1: cannot open shared object file。libsdl2-2.0-0pygame 底层的 SDL 动态库没有它 pygame 无法初始化就会报pygame.error。libx11-6和libxi6等一系列 X11 库虽然我们大多数时候是无头模式跑 ScenarioRunner但 pygame 初始化时仍会尝试加载 X11 相关库缺一个都会在初始化阶段被拖死。PYTHONPATH里加carla_python_api是为了存放 CARLA 客户端 API 的文件。我习惯把carla-0.9.16-py3.8-linux-x86_64.egg这类文件从 CARLA 发布包里拷出来放到单独的目录再通过PYTHONPATH指进去。你如果用的是.whl文件那也可以直接pip install carla-0.9.16-py3.8-linux-x86_64.whl二选一即可。提示不建议直接把整个 CARLA PythonAPI 目录塞进镜像。因为 CARLA 不同小版本的 Python API 文件可能有变化把它作为外部挂载目录升级时不用重新构建镜像。3.3 用 docker-compose 编排复现环境把 CARLA Server 和 ScenarioRunner 两个容器统一交给 docker-compose 管理会直观很多。这里给一份我实际使用的docker-compose.yml片段version: 3.8 services: carla-server: image: carlasim/carla:0.9.16 container_name: carla-server network_mode: host environment: - DISPLAY${DISPLAY} volumes: - /tmp/.X11-unix:/tmp/.X11-unix:rw command: /bin/bash -c /opt/carla-simulator/CarlaUE4.sh -RenderOffScreen scenario-runner: build: . container_name: sr-runner network_mode: host depends_on: - carla-server volumes: - ./:/workspace/scenario_runner - ./carla_python_api:/workspace/carla_python_api working_dir: /workspace/scenario_runner command: [scenario_runner.py, --list]这里有个关键设计ScenarioRunner 容器把宿主机当前目录挂载进去这样我修改代码后不需要 rebuild 镜像容器重启就能生效。carla_python_api也挂载进去避免把二进制拷贝死在镜像层里。depends_on只是控制启动顺序并不保证 CARLA 已经启动完毕所以实际跑业务脚本时还要自己加一个端口探测等待。4. 连接 CARLA 并跑通 ScenarioRunner 的关键细节4.1 启动 CARLA Server 容器的正确姿势CARLA 0.9.16 官方镜像默认会启动CarlaUE4.sh直接跑会尝试开图形窗口。在纯服务器环境里必须加-RenderOffScreen参数也就是离屏渲染docker run --rm --networkhost -e DISPLAY$DISPLAY carlasim/carla:0.9.16 /bin/bash /opt/carla-simulator/CarlaUE4.sh -RenderOffScreen如果你是带 GPU 的工作站并且需要画面做可视化可以把-RenderOffScreen换成正常显示同时挂载 X11 socket。但对于学习 ScenarioRunner 来说离屏渲染完全够用还省了显卡透传的坑。启动后不要急着连CARLA 初始化静态地图和载入车辆模型需要时间。我的做法是写一个小等待脚本循环请求健康检查接口直到就绪until docker exec carla-server bash -c echo /dev/tcp/127.0.0.1/2000 2/dev/null; do echo waiting for carla server... sleep 2 done4.2 ScenarioRunner 的基础命令和输出理解进入 ScenarioRunner 容器后先跑一个最简单的列场景命令docker exec -it sr-runner python scenario_runner.py --list如果能正常输出场景列表说明 py38 环境、carla Python API、ScenarioRunner 三方全部打通。--list是个很好的探针命令我强烈建议第一次运行时只做这一步不要一上来就跑完整场景。接着可以跑一个自带的测试场景docker exec -it sr-runner python scenario_runner.py --scenario FollowLeadingVehicle_1跑的时候终端会打印每个行为的执行状态Running、Succeeded、Failed。第一次跑通会看到 CARLA 世界在离屏模式下默默加载然后 ego 车辆开始执行跟车逻辑这个过程就说明链路全通了。4.3 无显示器环境下的 pygame 头ScenarioRunner 在运行过程中会创建 pygame 窗口用于实时渲染车辆视角。在无显示器的 Docker 环境里窗口创建会直接抛异常。有两个处理办法一是给容器设置SDL_VIDEODRIVERdummy环境变量让 SDL 走虚拟显示模式不实际打开窗口。二是用xvfb虚拟 framebuffer在容器里起一个虚拟 X Server 再运行 ScenarioRunner。我更推荐第一种配置简单environment: - SDL_VIDEODRIVERdummy但是要注意dummy模式下你拿不到画面日志只能看行为输出的文本。如果你在调试视觉相关功能还是用-RenderOffScreen的 CARLA Server 输出画面加上xvfb一套跑起来更接近真实环境。4.4 路由场景和 OpenSCENARIO 文件的加载除了内置场景ScenarioRunner 还支持从.xosc等文件加载自定义场景。比较常见的是跑 route 场景会涉及一个路线文件和场景文件python scenario_runner.py --route /workspace/scenario_runner/saved_routes/route_1.xml /workspace/scenario_runner/saved_routes/debug.xml --reload这行命令的--reload参数意思是每次运行前都重新加载 CARLA 世界。如果不开第二次跑可能因为地图状态残留出现问题我实际测试中遇到过车卡在地图里的情况加--reload能大幅减少这种玄学问题。5. 高频踩坑与问题排查实录5.1 “No module named carla” 的三种典型原因这个报错几乎每个人都会遇到但根源往往不一样Python 版本不匹配容器内是 py39/py310而 carla API 文件是 py38 的import 阶段直接失败。用docker exec sr-runner python -V确认版本。PYTHONPATH 没指对carla不是一个标准 pip 包它通常以.egg或.whl方式提供必须确保存放它的目录在PYTHONPATH里。我吃过亏的是把.egg放到目录后发现文件名带平台后缀大小写不一致就找不到了打印sys.path检查最直接。二进制和 Server 版本不一致如果你从 CARLA 0.9.15 的包里拷了 carla API 文件给 0.9.16 用import 可能正常但连接 Server 时协议不兼容表现成一堆莫名其妙的异常。解决办法是严格从 CARLA 0.9.16 官方发布包里提取。排查时我用的是docker exec -it sr-runner python -c import sys; print(sys.path) docker exec -it sr-runner python -c import carla; print(carla.__file__)5.2 pygame 在容器里无法初始化报错通常长这样pygame.error: Cannot open font resource或SDL_VIDEODRIVER相关提示。最直接的原因就是容器里没有图形库或没有显示设备。当时我排查过程是先在容器里手动执行python -c import pygame; pygame.init()确认是不是 pygame 本身的问题然后再去看 SDL 相关动态库是不是没装全。如果你已经装了libsdl2-2.0-0但依然报错就检查环境变量是否设置成功docker exec -it sr-runner env | grep SDL如果变量不存在说明 docker-compose 里的配置没生效需要重新docker compose up -d而不是只exec进去设置。5.3 连接 Server 超时但端口明明是通的CARLA 客户端连 Server 时如果版本匹配通常秒连。但我在 0.9.16 上遇到过一种情况/ping端口整个不通服务日志里也在正常显示加载可就是连不上。后来发现是 CARLA Server 容器启动参数里缺了-RenderOffScreen导致 UE 引擎试图初始化图形设备失败后没有进入工作状态。也遇到过完全相反的情况端口通但客户端脚本卡住。这时候先把复杂场景放一边用官方提供的最小客户端测docker exec -it sr-runner python -c import carla client carla.Client(localhost, 2000) print(client.get_server_version()) 能打印出版本号就说明客户端到服务端这条链路没有断。5.4 镜像构建时网络依赖拉不下来的问题场景依赖里有几个包走的是 GitHub 源或相对慢的源在容器构建时经常卡住。我当时的解决方法是给 Docker 配置国内镜像源同时构建时指定--networkhostdocker build --networkhost -t sr-env:0.9.16 .如果 Docker daemon 用的是非默认 DNS遇到 pip 无法解析域名可以在/etc/docker/daemon.json里加 DNS 配置重启 Docker 后生效。但注意改了 DNS 后要确认docker-compose.yml里的容器网络配置不受影响。5.5 场景跑到一半崩溃看不到日志ScenarioRunner 有日志和 trace 机制。难的是容器退出过快连输出都没来得及看。我的建议是改ENTRYPOINT为[bash]进入容器后再手动敲命令或者用command: [scenario_runner.py, --scenario, FollowLeadingVehicle_1, --output]把结果输出到文件再挂载出来。不要把日志只打到 stdout容器一旦退出日志就丢了大半。6. 收尾一点个人化建议整套 docker py38 ScenarioRunner 0.9.16 的组合跑通之后我最大的感受是版本约束是自动驾驶仿真场景里绕不过去的一道坎而 Docker 并不是什么万能灵药它的价值在于把“环境不可复现”这个最让人头疼的问题提前解决掉。如果你也准备走上这条路我建议从最小闭环开始先跑--list再跑单个场景最后再碰自定义脚本和批量 route 测试每一步都确认无误再往前走。碰到报错不要急着百度打印路径、确认版本、拆解动态库依赖这三板斧能覆盖大部分坑。最后再提醒一句CARLA 本身的版本升级很频繁0.9.16 和 0.9.17 的接口差异可能比你想象中大锁死版本才是安心学习的前提。