Win11下用Docker Compose一键部署Spark集群:从配置到实战避坑指南

在Windows 11上折腾大数据环境,对很多开发者来说,曾经是个想起来就头疼的活儿。虚拟机臃肿、环境变量冲突、版本兼容性问题层出不穷,更别提想快速搭建一个分布式的Spark集群来跑点实验或者学习用了。但如今,Docker和Docker Compose的组合,让这一切变得前所未有的简单。你不再需要手动配置Java、Scala、Hadoop那一大堆依赖,也不用担心把本地环境搞得一团糟。一个docker-compose.yml文件,几条命令,就能在几分钟内拉起一个功能完整的Spark集群,而且还能轻松集成Hadoop的HDFS和YARN。

这篇文章就是为你准备的,如果你是一位在Win11上进行数据开发、机器学习或者单纯想学习Spark的工程师。我们将彻底抛开那些繁琐的、容易出错的传统安装步骤,聚焦于如何利用容器化技术,在Windows桌面环境下,高效、稳定地构建一个可用于开发、测试甚至小规模生产的Spark环境。更重要的是,我会结合自己多次搭建和排错的经验,把那些官方文档里不会写的“坑”——比如Win11特有的路径权限、端口占用、资源分配调优——都给你讲清楚,让你真正实现“一键部署,开箱即用”。

1. 环境准备:为Win11上的Docker扫清障碍

在开始编写docker-compose.yml之前,确保你的Windows 11基础环境已经就绪,这是后续一切顺利的前提。很多部署失败的问题,根源都出在这一步。

首先,确认你的Windows版本和虚拟化支持。 Docker Desktop for Windows依赖于Hyper-V或WSL 2后端。对于Win11,我强烈推荐使用WSL 2(Windows Subsystem for Linux 2)作为后端。它不仅性能更好,与Windows文件系统的集成也更顺畅。打开PowerShell(管理员身份),运行以下命令来启用WSL和虚拟机平台功能:

# 启用WSL功能
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

# 启用虚拟机平台功能
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

执行完成后,务必重启计算机。重启后,前往Microsoft Store安装一个Linux发行版,比如Ubuntu 22.04 LTS。安装并启动它,完成初始用户设置。接着,将WSL 2设置为默认版本:

wsl --set-default-version 2

其次,安装并配置Docker Desktop。 从Docker官网下载Docker Desktop for Windows安装程序。安装过程中,确保勾选“使用WSL 2而不是Hyper-V”的选项(如果出现)。安装完成后启动Docker Desktop,在设置(Settings)中,找到“Resources” -> “WSL Integration”,启用你刚安装的Ubuntu发行版的集成。这个步骤至关重要,它允许Docker容器直接访问WSL 2的文件系统,能极大提升卷(volume)挂载的性能和兼容性,避免后续出现文件权限错误。

注意:如果你的机器有安全软件或防火墙,可能需要允许Docker通过。首次启动时如果遇到“WSL 2 installation is incomplete”的错误,通常需要手动更新WSL 2的内核组件,去微软官网下载并安装最新的WSL 2 Linux内核更新包即可。

最后,准备你的工作目录。 在Windows文件系统中选择一个位置作为你的Spark项目目录,例如D:\DockerProjects\spark-cluster。我建议路径中不要包含中文或空格,虽然Docker Desktop现在处理得不错,但为了绝对稳妥,纯英文路径是最佳选择。在这个目录下,我们将创建所有必要的配置文件。

2. 核心配置:编写健壮且可调优的docker-compose.yml

原始的docker-compose.yml往往只实现了最基本的功能。我们要写的,是一个考虑了资源限制、数据持久化、网络配置和健康检查的“生产就绪”版本。下面是一个增强版的配置,我会逐段解释其设计考量。

首先,创建一个名为docker-compose.yml的文件,内容如下:

version: '3.8'

services:
  spark-master:
    image: bitnami/spark:3
    container_name: spark-master
    hostname: spark-master
    environment:
      - SPARK_MODE=master
      - SPARK_RPC_AUTHENTICATION_ENABLED=no
      - SPARK_RPC_ENCRYPTION_ENABLED=no
      - SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
      - SPARK_SSL_ENABLED=no
    ports:
      - "7077:7077" # Spark Master内部通信端口
      - "8080:8080" # Spark Master Web UI
      - "4040:4040" # Spark Application Web UI (当有应用运行时)
    volumes:
      - ./data/share:/opt/share:rw
      - ./logs/spark-master:/opt/bitnami/spark/logs:rw
    networks:
      - spark-network
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 2G
        reservations:
          memory: 1G
    healthcheck:
      test: ["CMD", "bash", "-c", "curl -f http://localhost:8080 || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  spark-worker-1:
    image: bitnami/spark:3
    container_name: spark-worker-1
    hostname: spark-worker-1
    environment:
      - SPARK_MODE=worker
      - SPARK_MASTER_URL=spark://spark-master:7077
      - SPARK_WORKER_MEMORY=2G
      - SPARK_WORKER_CORES=2
      - SPARK_RPC_AUTHENTICATION_ENABLED=no
      - SPARK_RPC_ENCRYPTION_ENABLED=no
      - SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
      - SPARK_SSL_ENABLED=no
    volumes:
      - ./data/share:/opt/share:rw
      - ./logs/spark-worker-1:/opt/bitnami/spark/logs:rw
    depends_on:
      spark-master:
        condition: service_healthy
    networks:
      - spark-network
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 3G
        reservations:
          memory: 1G

  spark-worker-2:
    image: bitnami/spark:3
    container_name: spark-worker-2
    hostname: spark-worker-2
    environment:
      - SPARK_MODE=worker
      - SPARK_MASTER_URL=spark://spark-master:7077
      - SPARK_WORKER_MEMORY=1G
      - SPARK_WORKER_CORES=1
      - SPARK_RPC_AUTHENTICATION_ENABLED=no
      - SPARK_RPC_ENCRYPTION_ENABLED=no
      - SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
      - SPARK_SSL_ENABLED=no
    volumes:
      - ./data/share:/opt/share:rw
      - ./logs/spark-worker-2:/opt/bitnami/spark/logs:rw
    depends_on:
      spark-master:
        condition: service_healthy
    networks:
      - spark-network
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 2G
        reservations:
          memory: 512M

networks:
  spark-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

关键配置解析与避坑指南:

  1. 版本与网络:使用version: '3.8'以支持deploy.resources等现代配置。我们创建了一个自定义的spark-network桥接网络,并指定了子网。这能确保容器在固定的IP段内通信,避免与主机或其他Docker网络冲突,也使得spark-master这个主机名能在网络内被正确解析。
  2. 资源限制(deploy.resources):这是调优核心。limits设置了容器能使用的资源上限,reservations是Docker尝试保证的最小资源。我为Master和Worker设置了不同的CPU和内存配额。避坑点:SPARK_WORKER_MEMORY环境变量设置的是JVM堆内存,而deploy.resources.limits.memory是容器总内存限制。务必确保后者大于前者(通常预留1G左右给操作系统和其他进程),否则容器可能因OOM(内存不足)被系统杀死。
  3. 健康检查(healthcheck):仅为Master服务配置。它定期检查Master的Web UI端口(8080)是否可访问。depends_on中的condition: service_healthy确保了Worker容器会等待Master完全启动并健康后再启动,避免了因Master未就绪而导致的Worker注册失败。
  4. 卷挂载(volumes):
    • ./data/share:/opt/share:rw:将本地的./data/share目录挂载到所有容器的/opt/share,用于共享数据文件(如待处理的文本、CSV文件)。rw表示读写权限。
    • ./logs/...:/opt/bitnami/spark/logs:rw:将Spark日志挂载到主机,方便排查问题。Win11特有避坑点:确保本地目录(如./data, ./logs)已存在。如果遇到权限错误(如Permission denied),问题通常不在容器内,而是WSL 2与Windows文件系统(NTFS)的权限映射。最简单的解决方法是在Docker Desktop设置中,将项目目录(如D:\DockerProjects)添加到“File Sharing”列表,并重启Docker。
  5. 端口映射:除了常见的8080和4040,我们将Master的内部RPC端口7077也映射了出来。这样,如果你在主机上运行Spark Submit(需要额外配置),或者未来连接其他外部服务,可以直接使用localhost:7077。

3. 集群启动、验证与基础实战

配置完成后,打开PowerShell或Windows Terminal,导航到你的项目目录(D:\DockerProjects\spark-cluster)。

启动集群:

docker-compose up -d

-d参数代表后台运行。你会看到Docker开始拉取镜像(如果本地没有),然后依次创建并启动容器。

验证部署: 使用docker-compose ps查看所有服务状态,确保都是Up。然后,在浏览器中访问 http://localhost:8080。你应该能看到Spark Standalone Master的Web UI,其中“Workers”部分应该显示两个Worker节点,状态为ALIVE,并带有你配置的内存和核心数。

进行一个简单的PySpark交互测试: 首先,在本地共享目录./data/share下创建一个简单的文本文件test.txt,内容随意,比如几行英文句子。

然后,进入Master容器启动一个PySpark交互式会话:

docker exec -it spark-master pyspark

这会打开一个PySpark的Python Shell。我们尝试读取共享目录下的文件并进行词频统计:

# 读取本地共享文件(注意容器内路径是 /opt/share)
lines = sc.textFile("file:///opt/share/test.txt")
# 或者,如果你启动了HDFS集群,可以用 hdfs://master:9000/...

# 进行简单的转换操作:分割单词并计数
word_counts = lines.flatMap(lambda line: line.split(" ")) \
                   .map(lambda word: (word, 1)) \
                   .reduceByKey(lambda a, b: a + b)

# 收集结果并打印(数据量小的情况下)
output = word_counts.collect()
for (word, count) in output:
    print(f"{word}: {count}")

# 退出
exit()

如果一切正常,你将看到单词及其出现次数的列表。这个简单的测试验证了:1)集群运行正常;2)卷挂载成功,容器可以访问主机文件;3)PySpark基础功能完好。

4. 进阶集成:将Hadoop HDFS纳入集群

单纯的Spark Standalone集群对于学习是足够的,但真实的数据平台往往离不开HDFS。我们可以基于现有的bitnami/spark镜像,构建一个集成了Hadoop的自定义镜像。这样,Spark就能原生读写HDFS上的数据了。

步骤一:准备构建上下文。 在你的项目根目录下,创建一个Dockerfile和一个config文件夹。config文件夹里存放从可靠来源(如Apache官网或一些开源项目)获取的Hadoop配置文件(core-site.xml, hdfs-site.xml, yarn-site.xml等),这些文件需要根据我们的容器网络环境(如NameNode RPC地址)进行定制。一个关键的配置是core-site.xml中的fs.defaultFS,应设置为hdfs://spark-master:9000,因为我们的Master容器主机名就是spark-master。

步骤二:编写Dockerfile。 这是一个简化但可用的示例,它安装了Hadoop,配置了SSH免密登录(用于HDFS守护进程通信),并设置了启动脚本。

FROM bitnami/spark:3

USER root

# 安装必要的软件:SSH服务、Hadoop客户端
RUN apt-get update && \
    apt-get install -y openssh-server openssh-client wget

# 设置SSH免密登录(容器内)
RUN ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa && \
    cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys && \
    chmod 600 ~/.ssh/authorized_keys

# 下载并安装Hadoop(这里以3.3.5为例,注意镜像内可能已有JAVA_HOME)
ENV HADOOP_VERSION=3.3.5
ENV HADOOP_HOME=/opt/hadoop
ENV PATH=$HADOOP_HOME/bin:$PATH

RUN wget -q -O /tmp/hadoop.tar.gz https://archive.apache.org/dist/hadoop/common/hadoop-${HADOOP_VERSION}/hadoop-${HADOOP_VERSION}.tar.gz && \
    tar -xzf /tmp/hadoop.tar.gz -C /opt && \
    mv /opt/hadoop-${HADOOP_VERSION} ${HADOOP_HOME} && \
    rm /tmp/hadoop.tar.gz

# 复制自定义的Hadoop配置文件
COPY config/* ${HADOOP_HOME}/etc/hadoop/

# 创建HDFS数据目录
RUN mkdir -p /opt/hadoop-data/namenode /opt/hadoop-data/datanode

# 复制启动脚本
COPY start-hadoop.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/start-hadoop.sh

# 格式化NameNode(注意:生产环境不应在构建时格式化,这里仅为演示)
RUN ${HADOOP_HOME}/bin/hdfs namenode -format -force

# 修改Spark入口点,使其同时启动SSH(可选,也可在start-hadoop.sh里做)
# 更常见的做法是在docker-compose的command中指定启动顺序
# 这里我们保持Spark默认入口点,HDFS通过额外命令启动
USER 1001

start-hadoop.sh脚本内容大致是启动SSH服务,然后启动HDFS和YARN守护进程。

步骤三:修改docker-compose.yml。 更新docker-compose.yml中所有服务的image字段,指向你构建的新镜像(例如my-spark-hadoop:latest)。同时,为spark-master服务添加Hadoop相关端口的映射,并修改其command或使用entrypoint覆盖,确保在启动Spark Master的同时也启动HDFS NameNode。

# 在spark-master服务下添加或修改
ports:
  - "7077:7077"
  - "8080:8080"
  - "4040:4040"
  - "9870:9870" # HDFS NameNode Web UI
  - "8088:8088" # YARN ResourceManager Web UI
command: >
  bash -c "
  /usr/sbin/sshd &&
  $$HADOOP_HOME/sbin/start-dfs.sh &&
  $$HADOOP_HOME/sbin/start-yarn.sh &&
  /opt/bitnami/scripts/spark/run.sh --master spark://spark-master:7077
  "

注意,上述command是一个多命令串联的示例,实际使用中可能需要更精细的进程管理。更稳健的做法是使用Supervisor等工具来管理多个守护进程。

步骤四:构建、启动与测试。 在包含Dockerfile和config的目录下运行:

docker-compose build
docker-compose up -d

访问http://localhost:9870,你应该能看到HDFS的Web UI。在容器内,你可以使用hdfs dfs -ls /等命令来操作HDFS。在PySpark中,现在可以使用sc.textFile("hdfs://spark-master:9000/path/to/file")来读取HDFS上的数据了。

5. 常见问题排查与性能调优思路

即使按照上述步骤操作,你可能还是会遇到一些问题。这里汇总几个Win11+Docker环境下的典型“坑”及其解决方案。

问题一:端口冲突。 错误信息可能显示Bind for 0.0.0.0:8080 failed: port is already allocated。

  • 排查:在PowerShell中运行 netstat -ano | findstr :8080,找到占用端口的进程ID(PID)。
  • 解决:
    1. 终止占用进程(如果非必要)。
    2. 修改docker-compose.yml中的端口映射,例如将8080:8080改为8088:8080,然后通过http://localhost:8088访问。
    3. 检查是否之前启动的Docker容器未正确停止,运行docker-compose down后再up。

问题二:卷挂载权限错误。 在容器内访问挂载的卷时出现Permission denied。

  • Win11特有原因:WSL 2与Windows NTFS文件系统的权限映射问题。
  • 解决:
    1. 首选方案:如前所述,在Docker Desktop设置 -> Resources -> File Sharing中,添加并确保你的项目根目录(如D:\DockerProjects)在共享列表中。应用并重启Docker。
    2. 备选方案:在docker-compose.yml的卷挂载中,尝试为容器指定一个已知有权限的用户ID。例如,在服务配置中添加user: "1000:1000"(假设1000是你的WSL Linux用户的UID)。但这可能影响镜像内某些需要特定用户运行的进程(如bitnami镜像默认用1001用户)。

问题三:Worker节点无法连接Master。 在Master的Web UI上看不到Worker,或者Worker日志显示连接失败。

  • 排查:
    1. 检查docker-compose.yml中SPARK_MASTER_URL的值,必须是spark://spark-master:7077,确保主机名和端口正确。
    2. 检查自定义的spark-network网络是否正常工作。运行docker network inspect spark-cluster_spark-network,查看所有容器是否都连接在此网络下,并拥有正确的IP地址。
    3. 进入Worker容器,尝试ping spark-master,看网络是否通畅。
    4. 检查Master容器的7077端口是否在监听。进入Master容器,运行netstat -tlnp | grep 7077。

性能调优思路: 当你的Spark作业在本地Docker集群上运行缓慢时,可以考虑以下几点:

  • 资源配置:根据你的主机硬件,合理调整docker-compose.yml中的deploy.resources.limits和SPARK_WORKER_MEMORY。一个经验法则是,留给操作系统和其他进程的内存至少1-2GB。对于CPU,可以超分配(如主机4核,给容器总计分配6核),但要注意上下文切换的开销。
  • 数据本地性:如果数据在HDFS上,确保HDFS的DataNode和Spark Worker部署在同一个容器或主机上,可以减少网络传输。在我们的单机Docker Compose部署中,所有容器都在同一主机,网络延迟极低,这本身就是一个优势。
  • 持久化与序列化:在Spark作业中,对频繁使用的RDD/DataFrame进行persist()或cache()。根据数据类型选择高效的序列化器,如Kryo。
  • JVM垃圾回收:对于长时间运行、内存消耗大的作业,可以在spark-submit或Spark配置中调整JVM GC参数。但这属于更深入的调优范畴。

最后,记得当你完成实验后,使用docker-compose down -v来停止并删除所有容器、网络以及匿名卷(由容器运行时创建,未在docker-compose.yml中定义的卷)。如果你想保留数据卷,请移除-v参数。这个习惯能帮你保持开发环境的整洁。

更多推荐