Jupyter 를 서버 한 대에 두고 Spark 클러스터 N 대에 작업을 보내는 구성에서, 노트북에서는 잘 되던 코드가 실행 단계에서 ModuleNotFoundError 로 실패한다.
PySpark 코드는 두 곳에서 실행된다. import 와 DataFrame 조립처럼 계획을 세우는 부분은 드라이버에서 돌고, UDF 나 mapPartitions 안의 파이썬 코드는 각 익스큐터의 파이썬 프로세스에서 돈다. 익스큐터 쪽에서 쓰는 모듈은 클러스터 노드에 있어야 한다. Jupyter 서버에만 설치하면 드라이버에서는 통과하고 익스큐터에서 실패한다.
드라이버가 어디에 있는지도 함께 본다. Jupyter 에서 세션을 만들면 보통 client 모드라 드라이버는 Jupyter 서버에서 돈다. 이 경우 드라이버용 의존성은 Jupyter 서버에, 익스큐터용 의존성은 클러스터 노드에 있어야 한다.
관리 도구(Ansible 등)로 전 노드에 같은 패키지를 설치한다. 가장 단순하지만 버전이 어긋나기 쉽고, 작업마다 다른 버전이 필요하면 감당하기 어렵다.
가상 환경을 통째로 묶어 익스큐터에 배포한다. 노드에 손대지 않고 작업 단위로 환경을 고정할 수 있어 실무에서 가장 무난하다.
conda create -y -p ./pyenv python=3.11 pandas numpy scikit-learn
conda pack -p ./pyenv -o pyenv.tar.gz
from pyspark.sql import SparkSession
spark = (SparkSession.builder
.master("yarn")
.config("spark.submit.deployMode", "client")
.config("spark.archives", "hdfs:///envs/pyenv.tar.gz#env")
.config("spark.executorEnv.PYSPARK_PYTHON", "./env/bin/python")
.config("spark.yarn.appMasterEnv.PYSPARK_PYTHON", "./env/bin/python")
.getOrCreate())
#env 는 익스큐터의 작업 디렉터리에 풀릴 이름이다. venv-pack 으로 만든 virtualenv 아카이브도 같은 방식으로 쓴다.
직접 만든 모듈 정도면 압축해 보내는 것으로 충분하다.
spark.sparkContext.addPyFile("hdfs:///lib/mylib.zip")
numpy 처럼 네이티브 확장을 포함한 패키지는 이 방식으로 배포하면 안 된다. 빌드 환경이 다르면 로드에 실패한다.
드라이버와 익스큐터의 파이썬 마이너 버전이 다르면 pickle 호환 문제로 실패한다.
import sys
print(sys.version)
print(spark.sparkContext.parallelize([0]).map(lambda _: sys.version).collect())
두 출력이 같은 마이너 버전이어야 한다. 환경 변수로 명시해 두면 노드의 기본 파이썬이 바뀌어도 영향을 받지 않는다.
export PYSPARK_PYTHON=/opt/python311/bin/python3
export PYSPARK_DRIVER_PYTHON=/opt/python311/bin/python3
익스큐터에서 실제로 어떤 파이썬이 도는지 직접 확인하는 것이 가장 빠르다.
def probe(_):
import sys, importlib.util
return (sys.executable, sys.version.split()[0],
importlib.util.find_spec("pandas") is not None)
print(spark.sparkContext.parallelize(range(4), 4).map(probe).collect())
spark.archives 로 보내는 아카이브는 작업마다 전송된다. 수백 MB 짜리를 매번 보내면 기동이 느려지므로 HDFS 에 올려 두고 참조한다.