Spark 的特點之一就是 Unified,代表我們只有準備好 Spark cluster 的話,可以直接把 第一個 Spark Application 程式 run 在 cluster 上面,弄一套 Spark cluster 的選項有很多種,有錢的話直接找個雲服務商用他們的服務建立一個就搞定了,像是 AWS 的 EMR 或是 GCS 的 Dataproc,沒錢的話我們只能在 local 端以容器化的方式模擬 Spark cluster 出來了。
找找巨人的肩膀
準備 Spark 環境是最麻煩的地方!感謝 mvillarrealb 寫的 docker-spark-cluster github repo,按造他的步驟可以快速的建立成功 Spark Cluster,但因為我們想執行的版本為 Spark 3.5.4 和 python 3.12,原作者用的 openjdk image openjdk:11.0.11-jre-slim-buster
的 apt install 只能安裝到 python 3.9,
然後 openjdk 11 的 image 最新的 ubuntu 版本看起來只到 11 Bullseye,這個版的 apt install python
也是只到 3.9,
所以我只能基於 repo docker-spark-cluster 的內容,升級 python 和 Spark 版本,升級後的內容可參考我的 gitHub repo docker-spark-cluster。
Docker Image 準備
基於方便,我順便把 第一個 Spark Application 程式中需要用到的 python library 直接在 dockerfile 中用 pip install
的方式裝好,Dockerfile 內容如下,
FROM openjdk:11-jre-slim-bullseye AS builder
# Fix the value of PYTHONHASHSEED
# Note: this is needed when you use Python 3.3 or greater
ENV SPARK_VERSION=3.5.4 \
HADOOP_VERSION=3 \
SPARK_HOME=/opt/spark \
PYTHONHASHSEED=1 \
PYTHON_VERSION=3.12.8
RUN apt update -y && apt upgrade -y && apt install -y curl vim wget ssh net-tools ca-certificates build-essential gdb lcov pkg-config libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev lzma lzma-dev tk-dev uuid-dev zlib1g-dev libmpdec-dev
RUN cd /opt && \
wget "https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz" && \
tar -xzvf "Python-${PYTHON_VERSION}.tgz" && \
cd Python-${PYTHON_VERSION} && \
./configure --enable-optimizations && \
make -j `nproc` && \
ln -s /opt/Python-${PYTHON_VERSION}/python /usr/local/bin/python && \
ln -s /opt/Python-${PYTHON_VERSION}/python /usr/local/bin/python3 && \
ln -s /opt/Python-${PYTHON_VERSION}/python /usr/local/bin/python3.12 && \
rm ../Python-${PYTHON_VERSION}.tgz && \
python -m ensurepip --upgrade && \
pip3 install --upgrade setuptools && \
pip3 install matplotlib==3.10.0 pandas==2.2.3
RUN wget --no-verbose -O apache-spark.tgz "https://archive.apache.org/dist/spark/spark-${SPARK_VERSION}/spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz" \
&& mkdir -p /opt/spark \
&& tar -xf apache-spark.tgz -C /opt/spark --strip-components=1 \
&& rm apache-spark.tgz
FROM builder AS apache-spark
WORKDIR /opt/spark
ENV SPARK_MASTER_PORT=7077 \
SPARK_MASTER_WEBUI_PORT=8080 \
SPARK_LOG_DIR=/opt/spark/logs \
SPARK_MASTER_LOG=/opt/spark/logs/spark-master.out \
SPARK_WORKER_LOG=/opt/spark/logs/spark-worker.out \
SPARK_WORKER_WEBUI_PORT=8080 \
SPARK_WORKER_PORT=7000 \
SPARK_MASTER="spark://spark-master:7077" \
SPARK_WORKLOAD="master" \
SPARK_WORKER_DIR="/opt/workspace"
EXPOSE 8080 7077 7000
RUN mkdir -p $SPARK_LOG_DIR && \
touch $SPARK_MASTER_LOG && \
touch $SPARK_WORKER_LOG && \
ln -sf /dev/stdout $SPARK_MASTER_LOG && \
ln -sf /dev/stdout $SPARK_WORKER_LOG
COPY start-spark.sh /
CMD ["/bin/bash", "/start-spark.sh"]
在來你就可以使用以下命令建立 image。
docker build -t cluster-apache-spark:3.5.4 .
使用 docker compose 啟動 Spark cluster
image 準備好了後,我們就可以試著用 docker compose 啟動 cluster,這次就先來啟動個 1 driver 2 executor 共 3 台機器的環境好了,
services:
spark-master:
image: cluster-apache-spark:3.5.4
ports:
- "9090:8080"
- "7077:7077"
volumes:
- ./apps:/opt/workspace/apps
- ./data:/opt/workspace/data
environment:
- SPARK_LOCAL_IP=spark-master
- SPARK_WORKLOAD=master
spark-worker-a:
image: cluster-apache-spark:3.5.4
ports:
- "9091:8080"
- "7002:7000"
depends_on:
- spark-master
environment:
- SPARK_MASTER=spark://spark-master:7077
- SPARK_WORKER_CORES=1
- SPARK_WORKER_MEMORY=1G
- SPARK_DRIVER_MEMORY=1G
- SPARK_EXECUTOR_MEMORY=1G
- SPARK_WORKLOAD=worker
- SPARK_LOCAL_IP=spark-worker-a
volumes:
- ./apps:/opt/workspace/apps
- ./data:/opt/workspace/data
spark-worker-b:
image: cluster-apache-spark:3.5.4
ports:
- "9092:8080"
- "7003:7000"
depends_on:
- spark-master
environment:
- SPARK_MASTER=spark://spark-master:7077
- SPARK_WORKER_CORES=1
- SPARK_WORKER_MEMORY=1G
- SPARK_DRIVER_MEMORY=1G
- SPARK_EXECUTOR_MEMORY=1G
- SPARK_WORKLOAD=worker
- SPARK_LOCAL_IP=spark-worker-b
volumes:
- ./apps:/opt/workspace/apps
- ./data:/opt/workspace/data
從 docker-compose.yml
中可以看到,我把 repo 的 app
和 data
資料夾 mount 到 Spark cluster 的工作目錄下 /opt/workspace/
,代表我們可以同步的修改程式以及取得輸出結果,
在來就可以用以下命令啟動 Spark cluster,
docker compose up -d
啟動完成後,你就可以用 http://localhost:9090/ 連到 Spark cluster 的 driver 上了,在 UI 上我們也可以看到這個 cluster 中有 2 台 worker 節點可以來執行任務。

spark submit application
最後,我們就可以把我們在 第一個 Spark Application 寫的程式,submit 在這個 Spark cluster 中執行了,
submit application 前,請先參考 在 第一個 Spark Application 文章,把 movie lens 的資料準備好才行!
命令如下:
docker exec docker-spark-cluster-spark-master-1 /opt/spark/bin/spark-submit \
--master spark://spark-master:7077 \
--driver-memory 1G \
--executor-memory 1G \
/opt/workspace/apps/main.py
submit 後,再進一次Spark UI http://localhost:9090/ 去看,就能看到 Running Application 中有我們剛剛 submit 進去的記錄了,

等它跑完,就去檢查一下 data/output
下有沒有結果輸出成功,

有的話,恭喜你已成功將你寫的程式執行在 Spark cluster 中了!
結語
我們基於 mvillarrealb 寫的 docker-spark-cluster github repo,升級 python 和 spark 版本到 3.12 和 3.5.2,然後用 docker compose 在 local 端啟動了 1 台 Driver 2 台 worker 的 Spark cluster 來執行我們在 第一個 Spark Application 寫的程式。
其實還有挺多可以優化的,像是 Java 版本我們可以用 15,還有就是打包小一點的 python 3.12 出來,現在這個 image 打包出來的 python 太大了,34 M 的樣子,導致整個 docker image size 到 2G 😅
看後面有沒有機會啦!