Ohouse Spark - 오늘의집의 통합 Spark 환경
확장성 높은 Spark 실행 환경을 위한 통합 이미지 플랫폼 구축기
2025년 6월 13일Kyeom, QQ

Introduction

지난 글 [오늘의집 Spark on Kubernetes 도입 및 개선 여정]에서는 기존 EMR 환경에서 Spark On Kubernetes 기반 환경으로 전환한 여정을 소개해 드렸습니다. 이 전환을 통해 오늘의집 데이터 플랫폼은 Kubernetes 환경 위에서 다양한 Apache Spark application을 스케일 제약 없이 자유롭게 제출할 수 있는 기반을 마련할 수 있었습니다. 그 결과, 오늘의집은 데이터 분석을 비롯한 머신 러닝 파이프라인, Adhoc Query 및 분석을 위한 개발 환경, 데이터 탐색 (EDA) 서비스 등 다양한 영역에서 Apache Spark를 폭넓게 활용하게 되었습니다.

그러나 Apache Spark를 여러 조직과 목적에 따라 다양하게 활용하게 되면서, Kubernetes 상에 제출되는 Apache Spark 기반의 이미지 종류가 다양하게 분화하는 문제가 발생했습니다. 오픈소스 버전과 EMR 기반 이미지 간의 환경 차이, Spark / Python 버전에 따른 패키지 차이, 그리고 팀마다 각기 다른 라이브러리 관리 방식 등으로 인해 새로운 어려움을 겪게 되었습니다. 이번 글에서는 다양한 환경에서 활용 중인 Spark 기반 이미지를 효과적으로 관리하고 통합하기 위해 탄생한 Ohouse Spark의 개발 배경과 그 과정을 공유해 드리고자 합니다.


정리되지 않는 Spark 환경들

오늘의집에서는 이미 다양한 용도로 Spark를 활용하고 있으며, 주요 사용 환경은 다음과 같습니다.

  • JupyterHub는 주로 데이터 분석가나 머신러닝 엔지니어들이 데이터를 탐색하고 ML 모델을 개발하는 용도로 사용되는 기본적인 개발 환경입니다. 주로 인터랙티브한 작업들이 이루어집니다.
  • Airflow는 JupyterHub에서 개발된 작업들을 정규 배치 파이프라인으로 만들고 관리하는 역할을 합니다. 데이터 엔지니어들은 Airflow를 활용해 일정 주기에 따라 반복적으로 수행되는 안정적인 파이프라인을 생성합니다.
  • Kyuubi는 애드혹(Adhoc) 쿼리를 처리할 때 주로 사용되는 환경으로, Athena와 함께 Spark SQL 기반의 쿼리를 작성하고 테스트하는 용도로 활용됩니다.
  • 이 외에도 머신러닝 플랫폼 팀의 EDA(Exploratory Data Analysis) 서비스처럼 데이터 탐색 목적의 서비스에서도 Spark가 폭넓게 사용되고 있습니다.


이처럼 Spark가 다양한 환경과 목적에서 사용되다 보니, 각각 환경이 서로 다른 Docker Image를 기반으로 개별적으로 관리되고 있는 상황이었습니다. 예를 들어,

  • JupyterHub의 경우 기존 EMR 기반의 이미지는 Spark를 cluster mode로만 실행할 수 있는 제약이 있어, Airflow 환경과 달리 OSS(Open Source Software) 버전의 Spark를 사용하여 별도의 Docker Image를 직접 빌드해야 했습니다.
  • Kyuubi는 EMR에서 제공하는 Docker Image를 그대로 사용하고 있었습니다.
  • Airflow에서도 팀이나 프로젝트별로 서로 다른 Docker Image를 따로 관리하고 있었습니다. 예를 들어 데이터 플랫폼팀과 추천팀이 서로 다른 Docker Image를 사용하고 있었는데, Python 버전이나 GPU 지원 여부 같은 디테일한 부분들에서 서로 차이가 있었습니다.

결과적으로, 이렇게 각기 다른 목적과 환경에서 개별적인 Docker Image를 사용하게 되면서 아래와 같은 문제가 발생하게 되었습니다.


1. 이미지 관리 현황 파악의 어려움

각 프로젝트마다 Dockerfile이나 복잡한 shell script로 이미지를 관리하다 보니 전체적인 현황 파악이 매우 어려웠습니다. 실제로 운영 중인 Docker Image가 어떤 기본 이미지(base image)를 기반으로 하고 있는지, 그 위에 어떤 패키지나 라이브러리가 설치되어 있는지를 명확히 알기 힘든 상황이었습니다. 이로 인해 유지보수에도 많은 어려움이 따랐습니다.

▲ 이 이미지는 어디서 오는 걸까
▲ 이 이미지는 어디서 오는 걸까


2. 일관되지 않은 개발 및 운영 환경

Production Batch 작업에 Spark application을 반영하기 전 개발 프로세스는 다음과 같은 단계로 진행됩니다.

  1. JupyterHub이나 Kyuubi에서 코드를 작성하거나 쿼리를 테스트한 뒤
  2. 이를 Airflow의 배치 코드에 반영한 후 Preview deploy 환경에서 다시 실행을 검증하고
  3. 최종적으로 PR merge 이후 운영 환경에 배포됩니다.

그러나 각 환경에서 Docker Image가 독립적으로 관리되고 있다 보니, Spark / Python 버전을 포함한 많은 의존성이 환경마다 제각각이었습니다. 그 결과 개발 환경에서는 잘 테스트가 된 작업을 프로덕션에 반영할 때 의존성의 불일치로 작업이 정상적으로 수행되지 않는 등 배포된 Spark application을 안정적으로 어려웠습니다. 또한 문제 상황을 개발 환경에서 재현하지 못하는 등 일관성을 유지하기 어려운 문제도 있었습니다.

▲ Airflow와 Jupyter가 조금씩 다르긴 한데 어디가 안 맞는 걸까
▲ Airflow와 Jupyter가 조금씩 다르긴 한데 어디가 안 맞는 걸까


3. 사용자 친화적이지 않은 개발 경험

개발 환경과 운영 환경 사이의 불일치뿐만 아니라, Spark application의 개발부터 프로덕션의 반영까지 이어지는 전체 개발 라이프사이클 역시 사용자 친화적이지 않았습니다.

예를 들어 새로운 라이브러리를 추가해야 하는 상황에서, 각 환경별 Dockerfile을 일일이 수정하거나, 일부 환경에서는 라이브러리를 수동 설치해 테스트한 후 이를 기억해서 Airflow 용 이미지에도 하나씩 반영해야 하는 절차를 거쳤습니다. 수동으로 환경을 맞추는 과정이 복잡하고, Preview에서 잘 작동한 코드가 운영 환경에 자연스럽게 배포되는 완전한 CI/CD 구조 또한 부재한 상황이었죠. 새로운 라이브러리를 추가하는 것은 복잡하고 문제를 발생시키기 쉬운 환경이었습니다.


4. 여전히 남아 있는 EMR 의존성과 비용

앞선 글에서 설명한 것처럼 오늘의집은 EMR virtual cluster를 제거하면서 EMR on EKS 환경에서 벗어나게 되었습니다. 하지만 virtual cluster를 걷어냈음도, 여전히 많은 Spark 환경이 여전히 EMR에서 제공하는 Docker Image를 그대로 사용하고 있었기 때문에 이미지 사용에 따른 비용이 지속적으로 발생하고 있었습니다. 오늘의집은 매일 20,000개 이상의 작업이 제출되고 있고 10,000 Core 이상의 리소스를 활용하고 있는 만큼 컨테이너 이미지를 사용하는 것만으로도 추가적인 비용이 발생하고 있었습니다. 오늘의집에서 Spark application을 수행함에 따른 EC2 Instance 비용 중 15%가량의 추가적인 비용이 이미지를 활용하는 것만으로 지출되고 있는 상황이었습니다.

▲ EMR Virtual Cluster 를 사용하지 않아도 청구되는 EMR 비용
▲ EMR Virtual Cluster 를 사용하지 않아도 청구되는 EMR 비용


EMR에서 제공하는 Spark 배포판은 OSS 버전과 비교해 다양한 최적화가 이루어져 있다는 장점이 있습니다. 하지만 OSS 버전과 EMR 배포판을 혼용하면서 관리 복잡성이 높아지고 불필요한 EMR 의존성이 유지되는 문제가 발생했습니다.

위와 같은 이유들로 복잡한 환경 관리 문제를 해결하여 개발자들이 더 중요한 업무에 집중할 수 있도록, 보다 일관적이고 관리하기 쉬운 새로운 Spark 환경을 구축할 필요성을 느꼈습니다. 이에 따라 데이터 플랫폼팀은 새로운 Spark 관리 환경인 Ohouse Spark를 개발하게 되었습니다.


Ohouse Spark: 통합 Spark 환경 훑어보기

Ohouse Spark는 Spark application Image를 구성하는 계층에 존재하는 다양한 관심사와 개발 라이프사이클이 하나의 Dockerfile 정의로 결합되어 있는 것을 해결합니다. 따라서 이미지를 구성하는 구성 요소가 관심사에 따라 Base Image, Derived Image로 구분이 되며, Derived Image는 다시 Spark application을 개발하는 라이프사이클에 따라 Derived Image와 Derived Adhoc Image로 구분할 수 있습니다.

이번 단락에서는 각 구성요소에서 어떤 관심사를 분리하여 Spark application Image를 관리하는지 소개하도록 하겠습니다.

▲ Ohouse Spark의 각 컴포넌트의 구조와 활용 관계도
▲ Ohouse Spark의 각 컴포넌트의 구조와 활용 관계도


1. Base Image: 오늘의집의 일관성 있는 Spark application을 위한 토대

다양한 Spark application을 운영하고 지원하는 데이터 플랫폼팀 입장에서는, 여러 사용자들이 Spark application을 안정적으로 실행할 수 있도록 일관성 있는 기반 위에서 동작하기를 기대합니다. 그렇다면 이러한 일관성 있는 기반을 이루는 요소는 어떤 것들이 있을까요?

  • Spark application을 실행하기 위한 Java / Python 버전과 같은 실행 환경
  • Glue Metastore와 같은 카탈로그 접근 및 Iceberg 와 같은 데이터 포맷에 대한 입출력을 위한 library
  • AWS S3 와 같은 Cloud 환경 접근을 위한 AWS library
  • 비즈니스 로직의 기반이 되는 Spark Extension, UDF와 같은 확장 요소
  • 실행 환경, Spark 버전을 포함한 의존성의 업데이트에 대한 일관성 보장
  • Airflow, Jupyter 등 Spark를 활용하는 여러 시스템에서 동일한 환경 보장

위와 같은 구성 요소들을 모든 사용자가 일관성 있게 활용할 수 있게끔 데이터 플랫폼팀에서는 ‘공통 기반 환경’을 제공해야 했습니다. Base Image는 위와 같은 기능들을 포함한 이미지 배포판으로써, 사용자가 직접 필요한 패키지와 라이브러리를 따로 고려하지 않아도 되는 토대를 제공합니다. 따라서 이를 기반으로 빌드된 모든 Spark application은 데이터 플랫폼이 제공하는 데이터 환경을 손쉽게 활용할 수 있는 기반을 제공합니다.

데이터 플랫폼팀에서는 각 Spark application의 비즈니스 로직 및 이를 수행하기 위한 추가적인 의존성과 독립적으로 기본 기능셋을 지원하는 패키지를 관리하고 지속적으로 릴리즈 할 수 있으며, GitHub Release를 통해 각 Base Image가 제공하는 패키지 현황을 파악할 수 있습니다. Base Image는 Airflow와 같은 Batch 작업을 위한 Batch Image, Jupyterhub에서 활용하기 위한 Jupyter Image를 빌드하여 배포합니다. Batch Image와 Jupyter Image는 Jupyter 실행을 위해 필요한 의존성을 제외하고는 동일한 환경을 가지고 있어 여러 시스템에서 일관성 있는 Spark application을 활용할 수 있습니다.

▲ Ohouse Spark base image의 Github Release
▲ Ohouse Spark base image의 Github Release


또한 Base Image의 새로운 릴리즈마다 통합 테스트를 수행하여 Base Image에 대한 forward compatibility를 제공할 수 있도록 합니다. Base Image의 빌드는 docker buildx를 통해 Multi-platform Build 및 Batch Image / Jupyter Image를 동시에 빌드하는 것을 지원하며, [docker buildx bake] 기능을 활용하여 HCL 형식으로 Multi-staged Build 및 빌드에 필요한 변수를 관리합니다. 새로운 Spark 버전, Python 버전을 지원하는 이미지를 빌드 할 때에는 아래 Build Profile 변수를 수정하고 새로운 Github Release를 생성하는 것만으로 신규 Base Image를 생성할 수 있습니다. 새로운 Github Release 생성으로 Github Action을 트리거 하며, Github Action 내에서는 신규 이미지 빌드 및 통합 테스트를 수행합니다.

# Ohouse Spark Base Image의 docker-bake.hcl 내 BUILD PROFILE 설정 Variable "BUILD_PROFILE" { default = { VERSION = "2.1.0" UBUNTU_VERSION = "22.04" PYTHON_VERSION = "3.11" SCALA_VERSION = "2.12" JDK_VERSION = "17" SPARK_VERSION = "3.5.4" HIVE2_VERSION = "2.3.9" HADOOP_VERSION = "3.4.1" ICEBERG_VERSION = "1.7.2" } }


2. Derived Image: Spark application을 위한 의존성 명세

Base Image를 통해 다양한 Spark application을 생성할 수 있는 준비가 되었습니다. 그렇다면 단순히 Dockerfile의 Base Image만 변경하면 모든 문제가 해결될까요? 실제 사용자들이 Spark application을 쉽고 안정적으로 빌드하고 관리하기 위해서는 다음과 같은 요소들이 필요합니다.

  • 명세 기반의 의존성 관리: Dockerfile에서 의존성을 개별적으로 관리하는 대신 필요한 의존성을 스펙에 맞게 명시하여 현재 이미지의 상태를 한눈에 확인할 수 있어야 합니다.
  • Multi-release 관리: Spark application의 목적에 따라 여러 배포판을 관리할 수 있어야 합니다.
  • 반복 빌드의 효율을 위한 Layer Caching: 단순한 파일 변경 등의 상황에서 전체 빌드가 일어나지 않도록 Layer Caching을 Out-of-Box로 제공합니다.
  • 배포된 Spark application Release의 손쉬운 사용: Airflow와 같은 시스템에서 배포된 최신 이미지를 자동으로 참조하여 Spark Job 제출이 가능해야 합니다.

따라서 Derived Image는 Base Image를 바탕으로 Spark application 수행에 필요한 의존성을 명세하기 위한 Spec 및 이를 Airflow에서 실행할 수 있는 시스템을 제공합니다. Derived Image는 toml 형식의 Buildspec을 사용하여, 필요한 Python 라이브러리 및 시스템 패키지를 목적에 따라 명시할 수 있도록 구성되어 있습니다. 따라서 사용자는 필요에 맞게 Buildspec을 정의하면 Derived Image 빌드 시스템은 명세를 바탕으로 Spark application Image를 빌드합니다. 내부적으로는 Base Image와 마찬가지로 toml 형식을 docker buildx bake 명세로 변환합니다. 이에 Docker build step의 일관성을 가지는 만큼 Layer Caching을 적극 활용하여 반복적인 빌드에 있어서는 불필요한 빌드 스텝을 건너뛸 수 있도록 했습니다.

[project] name = "discovery" # 이미지 릴리즈들을 관리하기 위한 그룹 단위, 각 CI/CD는 하나의 프로젝트 단위로 동작 project_type = "scratch" # 프로젝트 내의 릴리즈 정의, 필요에 따라 복수 개를 정의할 수 있음 [[project.releases]] release = "py311_gpu_vllm" # 프로젝트 내 구별할 수 있는 릴리즈 이름, 모든 Derived Image는 (prject, release)로 구분 base_image_version = "2.1.0" # 해당 릴리즈에서 사용한 Ohouse spark base image 버전 platforms = ["linux/amd64"] # Apt Install 등 필요한 시스템 패키지 설치를 위한 script 명세 setup_script = """ # CUDA Toolkit 설치 apt-get update && \ apt-get -y install cuda-toolkit-12-4 && \ rm -rf /var/cache/apt/* && rm -rf /var/lib/apt/lists/* pip install torch==2.5.1 """ # Pyspark application을 위한 python library의존성 명세 requirements = """ vllm==0.7.2 flash-attn==2.7.4.post1 # ... pandas==2.2.3 """ # File Copy, 환경변수 설정 등 추가적인 docker instruction docker_steps = """ COPY --from=project ds/llm /discovery/ds/llm/ COPY --from=project ds/utils /discovery/ds/utils ENV PYTHONPATH="/discovery:${PYTHONPATH:-}" """ on: workflow_dispatch: inputs: env: description: "Build trigger environment" required: true type: choice options: - prod - qa # Test purpose (do not update airflow Variable, just build pyspark image) - test push: branches: - main - develop paths: - pyspark/** - common/python/** jobs: build-ohouse-pyspark: uses: ./.github/workflows/build-ohouse-spark-derived.yaml # Derived Image Build 공통 Action with: buildspec_path: ./pyspark/buildspec.toml env: >- ${{ github.event_name == 'workflow_dispatch' && inputs.env || (github.ref_name == 'main' && 'prod' || 'qa') }} secrets: inherit


Derived Image를 손쉽게 배포하기 위해 Github Action template을 제공, Github Action CI/CD로 등록하게 되면 이미지의 빌드부터 배포, 그리고 Airflow Variable의 등록까지 한 번에 수행될 수 있도록 하였습니다.

사용자는 Buildspec, Github Action을 정의하기만 하면 Pull Request 머지시 필요에 따라 Image 빌드를 수행하고 해당 릴리즈에 대한 최신 image tag를 Airflow Variable로 등록할 수 있도록 하였습니다. Airflow Variable로 등록된 릴리즈는 이전 글 [오늘의집 Spark on Kubernetes 도입 및 개선 여정]에서 소개한 SparkSubmitOperator를 통해 runtime_release 필드에 빌드한 릴리즈 이름을 주입하기만 하면 Spark application 실행 시 Variable을 참조하여 적절한 이미지를 제출 할 수 있도록 하였습니다.

cdc_table_task = PysparkOperator( task_id=cdc_table.task_id, # eg. mongo_source.goods_cdc # Spark submit 실행체 submitter=SparkOnKubernetesSubmitter(), # Derived Image Release 선택 runtime_release=("pyspark", "spark35"), resource=SparkResourceConfig.small().with_executor_disk(100), job="jobs.datapl.mongo_source.load_binlog_from_kafka", arguments=dict( broker=config.artifact.kafka.get_data_platform_broker(), cdc_table_uri=cdc_table.uri, start_datetime="{{ kst_datehour | add_duration(hours=1 - %d) }}" % interval_hours, end_datetime="{{ kst_datehour | add_duration(hours=1) }}", ), )


3. Derived Adhoc Image: Derived Image의 변경을 안전하게

Derived Image는 고정된 이미지가 아니며 Spark application이 지속적으로 변경됨에 따라 Derived Image 또한 지속적으로 변경됩니다. 만약 기존 Pyspark Job을 변경하면서 새로운 의존성이 필요해지게 된 상황에서 production 환경에 제출하기 전 안전하게 테스트해 볼 수 있는 환경을 제공할 수 없을까요? 그리고 그 환경은 기존에 사용 중인 Airflow, Jupyterhub과 같은 개발 환경에 매끄럽게 통합이 된다면 개발 라이프사이클에 자연스럽게 녹아들어 갈 수 있지 않을까요?

따라서 Derived Image를 라이프사이클 측면에서 분리한 Derived Adhoc Image를 제공합니다. 각 브랜치에서 Buildspec을 변경한 후 Github Action을 통해 Derived Adhoc Image를 트리거 하게 되면 Derived Image와 분리된 각 개발 브랜치를 위한 Derived Adhoc Image를 생성합니다.

▲ Github Action을 활용한 Derived Adhoc Image 빌드
▲ Github Action을 활용한 Derived Adhoc Image 빌드


Derived Adhoc Image의 빌드는 production 환경의 Airflow Variable을 조작하지 않습니다. 각 브랜치에서 테스트할 수 있는 별도의 preview Airflow 환경을 업데이트하여 preview 환경에서 자유롭게 새로 빌드한 Derived Adhoc Image를 테스트할 수 있도록 합니다. 또한 Jupyterhub 개발 환경에서도 Derived Adhoc Image Tag를 지정하여 Python Notebook을 통해 신규 이미지상에서 자유롭게 코드를 실행해 볼 수 있는 환경을 제공합니다.

▲ jupyterhub에서 Derived Adhoc Image를 사용하는 예시
▲ jupyterhub에서 Derived Adhoc Image를 사용하는 예시


이를 통해 기존 Spark application 개발 프로세스를 크게 변경하지 않으면서도, 안전하게 Derived Image를 지속적으로 개발해 나갈 수 있는 환경을 제공하였습니다.


전환 과정에서의 이슈들

Base Image, Derived Image, 그리고 Derived Adhoc Image를 통해 오늘의집에서는 더욱 편리한 Spark application 개발 환경을 제공할 수 있을 것이라 판단했습니다. 기존 시스템에서 Ohouse Spark 기반의 어플리케이션으로 순차적으로 전환하면 무리 없이 마이그레이션을 마칠 수 있을 것으로 예상했습니다.

그러나 대부분의 작업들은 손쉽게 새로운 이미지로의 전환이 가능했으나, Ohouse Spark의 도입과 함께 EMR 의존성에서 벗어나다 보니 오픈소스 기반의 Spark와 다른 동작을 보이는 케이스들을 발견할 수 있었습니다. 이를 해결하기 위해 내부적으로 Spark를 패치하여 EMR과 동작이 일치하도록 추가적인 작업이 필요하게 되었습니다. EMR 기반 Spark와 Open Source Spark 기반의 Ohouse Spark에서의 기능적 불일치를 줄이기 위해 오늘의집 데이터 플랫폼 팀에서는 Spark Plan을 분석하고, 더 나아가 Debugger, Btrace등의 도구를 활용한 Dynamic Tracing 등 여러 방면에서 동작의 불일치를 확인하여 필요에 따라 직접 Open Source Spark에 대한 Patch Set을 생성하여 Base Image를 빌드하였습니다. 이를 위해 다양한 패치를 진행하였으나 이번 문서에서는 간단한 케이스 중 하나를 소개하고자 합니다.


HiveTable의 Partition Overwrite시 대상 테이블과 읽는 테이블이 동일한 경우

Latest snapshot, 리텐션과 같은 정보를 계산하기 위해서는 이전 일자의 파티션에서 데이터를 읽어서 새로운 파티션에 덮어써야 하는 유형의 쿼리들이 있습니다. 아래는 소스 테이블과 파티션을 덮어쓰는 대상 테이블이 동일한 경우 대한 쿼리의 간단한 예시입니다.

INSERT OVERWRITE latest_user_snapshot_table PARTITION (dt = ‘@today’) SELECT user_id, … columns FROM latest_user_snapshot_table WHERE dt = ‘@yesterday’


EMR에서는 위와 같은 유형의 쿼리가 동작하고 있었으나, 오픈소스 버전에서는 Analysis Exception: cannot overwrite a path that is also being read from와 같은 에러가 발생하며 쿼리 수행에 실패했습니다. 이를 우회하기 위해서는  Dynamic Partition Overwrite를 활용하도록 쿼리 및 Spark Configuration을 변경해 주는 작업이 필요합니다만, 이미 많은 작업들이 위와 같은 유형의 쿼리로 작성이 되어있으며 사용자들 또한 익숙한 쿼리 형태인 만큼 동일한 기능을 지원할 필요가 있었습니다. 따라서 동일한 쿼리를 EMR 버전의 Spark와 Ohouse Spark에서 실행해 보며 그 차이를 분석하였습니다.

이를 해결하기 위한 단서는 오픈소스 버전에서의 예외 Stack Trace와 EMR 버전의 실행 계획에서 발견할 수 있었습니다. 오픈소스 버전의 Spark의 Stacktrace에서는 DataSourceAnalysis를 수행하고 그 과정에서 InsertIntoStatement를 검증하는 단계에서 실패한 반면, EMR 버전에서는 성공적으로 실행한 쿼리의 실행계획에서 InsertIntoHiveTable를 실행한 것을 확인하였습니다. 즉, 쿼리문을 분석하여 Analyzed Logical Plan으로 변경하는 과정에서 차이가 발생하였다고 추측할 수 있었습니다.

o.a.s.sql.AnalysisException: [UNSUPPORTED_OVERWRITE.TABLE] Can't overwrite the target that is also being read from. at o.a.s.sql.errors.QueryCompilationErrors$.cannotOverwriteTableThatIsBeingReadFromError(QueryCompilationErrors.scala:2627) at o.a.s.sql.execution.command.DDLUtils$.verifyNotReadPath(ddl.scala:1049) at o.a.s.sql.execution.datasources.DataSourceAnalysis$$anonfun$apply$1.applyOrElse(DataSourceStrategy.scala:228) at o.a.s.sql.execution.datasources.DataSourceAnalysis$$anonfun$apply$1.applyOrElse(DataSourceStrategy.scala:135) at o.a.s.sql.catalyst.plans.logical.AnalysisHelper.$anonfun$resolveOperatorsDownWithPruning$2(AnalysisHelper.scala:170) at o.a.s.sql.catalyst.trees.CurrentOrigin$.withOrigin(origin.scala:76)
▲ EMR 버전에서의 Spark 실행 계획
▲ EMR 버전에서의 Spark 실행 계획


이와 동시에 Spark Jira 에서도 유사한 이슈 SPARK-32838를 확인하였고, 오픈소스 버전의 Spark의 HiveStrategy를 수행할 때 InsertIntoHiveTableInsertIntoStatement로 변환하는 것을 확인하였습니다. 따라서 오픈소스 Spark의 소스코드를 수정하여 Source / Destination이 같은 테이블의 파티션을 직접 Overwrite하는 경우에 이를 InsertIntoStatement로 변경하지 않도록 직접 패치하여 빌드한 결과 쿼리가 정상적으로 수행하는 것을 확인하였습니다. 위 결과를 바탕으로 Ohouse Spark Base Image에서 Spark을 빌드할 때 직접 작성한 Patch를 적용한 후 빌드하도록 하여 문제가 된 작업들 또한 성공적으로 전환할 수 있었습니다.


성과

지금까지의 과정을 통해, 데이터 플랫폼 팀은 성공적으로 Ohosue Spark를 도입함으로써 아래와 같은 개선 사항을 이뤘습니다.

이미지 관리의 편의성 확보

  • Spark application Image와 Base Image를 독립적으로 관리하게 되어, 새로운 Spark version 출시 및 Python 환경 업데이트와 같은 변경 사항을 선제적으로 지원할 수 있게 되었습니다.
  • EMR의 신규 이미지 릴리즈를 기다리는 대신 선제적으로 Base Image를 생성함으로써 빠르게 신규 환경에 대한 평가를 진행하고 Spark application에서는 Base Image 버전을 업데이트 하기만 하면 최신의 환경을 활용할 수 있게 되었습니다.

Spark application 의 빠른 온보딩

  • 사용자들은 Spark 작업 작성과 함께 buildspec만 작성하는 것으로 빠르게 Spark application을 작성할 수 있습니다.
    • 또한 동일한 환경을 여러 환경 Jupyter / Airflow에서 쉽게 사용할 수 있어 어떤 시스템을 활용하든 일관된 환경 하에서 작업을 테스트해보고 개발할 수 있게 되었습니다.
  • Derived Adhoc Image를 활용하여 안전하게 Spark application Image를 지속적으로 관리할 수 있게 되었습니다.

EMR 의존성 제거 및 비용 절감

  • 오늘의집에서는 EMR 기반의 이미지를 사용하는것 만으로도 인스턴스 사용에 대한 비용 대비 15% 가량의 추가 비용을 지출하고 있었습니다. 이를 완전히 Open Source Spark 기반의 Ohouse Spark로 전환하여 동일한 기능과 퍼포먼스를 유지하면서 이미지 사용에 대한 비용을 절감할 수 있게 되었습니다.


Ohouse Spark로 만들어가는 미래

지금까지 Ohouse Spark를 통해 Spark application Image 관리의 복잡도를 줄이고 비용 절감 등의 성과를 이뤄냈습니다. 그러나 Ohouse Spark는 지금까지의 성과에 그치지 않고 계속해서 오늘의집의 데이터 환경을 한 단계 더 발전시키기 위한 다양한 도전을 이어갈 예정입니다.

더 나은 Data Governance를 위한 Spark Plugin

오늘의집에서는 Open Metadata를 활용하여 Data Governance를 구축하고 있습니다. 데이터 리니지, Description과 같이 데이터의 가시성을 확보하기 위한 많은 정보들을 Open Metadata로 수집하고 있으며 지속적으로 더 많은 정보들을 확보하기 위해 노력하고 있습니다.

Ohouse Spark의 Base Image를 활용한다면 Data Governance를 위한 Plugin을 기본 제공하여 작업별 데이터 활용 현황 및 Data Quality와 같은 많은 정보들을 Out-of-Box로 수집할 수 있을 것입니다.

더 빠른 Spark를 위한 Native Execution Engine 적용

JVM 기반 Spark Executor의 한계를 넘어서 C++ / Rust를 활용하여 Spark의 실행속도를 높이기 위해 [Apache Glueten], [Apache Datafusion Comet] 과 같은 프로젝트들이 등장하고 있습니다.

위와 같은 프로젝트를 Base Image에 통합하여 지속적으로 성능을 개선해 나가고 적은 비용으로 더 많은 Spark application을 실행하기 위한 환경을 제공하기 위한 노력을 기울이고 있습니다.



오늘의집 데이터 플랫폼 팀은 지금과 같은 성과에 만족하지 않고 더욱 복잡한 문제를 구조적으로 풀어내고, 더 나은 방향을 훌륭한 동료분들과 함께 끊임없이 고민하며 나아가고 있습니다. 매번 보다 훌륭한 데이터 인프라를 구축하기 위해 훌륭한 동료분들과 협업하고 있는 오늘의집 데이터 플랫폼팀은 현재 다음 챕터를 만들어갈 동료분들을 기다리고 있습니다.

지금까지 긴 글 읽어주셔서 감사합니다!

오늘의집에서 당신을 찾고 있습니다!
Technical Lead & Manager, BackendTechnical Lead & Manager, FrontendTechnical Lead & Manager, Machine Learning (Vision/Gen AI)Technical Lead & Manager, AndroidSenior Software Engineer, BackendSenior Software Engineer, FrontendSenior Software Engineer, Machine LearningSenior Software Engineer, DataSoftware Engineer, BackendSoftware Engineer, FrontendSoftware Engineer, Frontend, Space AISoftware Engineer, AdsSoftware Engineer, Machine LearningSoftware Engineer, Machine Learning (Vision/Gen AI)Software Engineer (전문연구요원)QA EngineerQA Engineer, Automation[오늘의집페이] Senior Software Engineer, BackendQA Assistant (계약직)
목록으로 돌아가기