LY Corporation Tech Blog

LY Corporation과 LY Corporation Group(LINE Plus, LINE Taiwan and LINE Vietnam)의 기술과 개발 문화를 알립니다.

LINE 서비스의 대규모 광고 데이터를 처리하기 위한 Spark on Kubernetes 적용기

들어가며

안녕하세요, LINE 서비스의 광고 시스템에서 데이터 파이프라인과 데이터 플랫폼 운영을 담당하고 있는 박민재, 손정호, 정창권입니다.

LINE 광고 플랫폼(이하 LINE Ads)은 하루에 수십억 건 이상의 광고를 송출하며, 내부에서는 천억 건에 준하는 데이터를 수집 및 가공하고 있습니다. LINE Ads의 데이터 파이프라인 팀은 광고 효율을 높이기 위해 실시간으로 광고 결과 데이터를 수집, 가공, 저장, 전송하는 역할을 수행합니다. 데이터를 처리하는 과정에서 이벤트 적합성 여부(어뷰징 혹은 잘못된 방법으로 생성된 데이터가 아닌지 확인)를 실시간으로 확인하며, 이후 머신러닝 알고리즘 등 해당 데이터를 필요로 하는 광고 플랫폼 내 다른 컴포넌트로 지연/중단 없이 제공합니다.

광고 시스템이 점차 정교해지면서 데이터에 추가되는 피처(feature)가 많아졌고, 이로 인해 데이터 파이프라인에서 필요한 연산량 또한 크게 증가했습니다. 저희 팀에서 생성하는 테이블 중 가장 많이 사용되는 테이블은 피처가 증가하면서 그 크기가 2025년 12월을 기준으로 2022년 12월 대비 약 2.91배 증가했습니다.

그런데 기존에 활용하던 Hadoop 기반의 YARN 환경은 HDFS와 컴퓨팅 자원이 단일 노드에 결합된 구조라는 한계 때문에 자원 경합이 발생해 성능이 저하되고, 운영 비용이 많이 들었으며, 앱의 의존성이 Hadoop 노드에 종속되는 문제도 발생했습니다.

저희 팀은 이런 한계를 극복하고자 Hadoop 환경에 의존하지 않는 새로운 컴퓨팅 엔진을 도입하는 것을 검토했습니다. 그 결과 특정 상품에 의존하지 않으면서 앱의 의존성이 인프라에 구성된 환경에 크게 종속되지 않는 인프라 독립성을 확보할 수 있고 컨테이너 기반으로 유연한 환경을 제공하는 Spark on Kubernetes를 도입했습니다. 이 글에서는 그 적용 과정과 성과를 공유하고자 합니다.

LINE Ads의 대용량 데이터 파이프라인

LINE Ads의 데이터 파이프라인 팀은 광고 성과를 높이기 위해 다양한 내부 시스템에서 필요로 하는 데이터를 안정적으로 제공하는 역할을 담당하고 있습니다. 저희가 제공하는 데이터는 머신러닝 알고리즘뿐 아니라 분석, 모델 학습, 여러 시스템을 연계하기 위한 기반 데이터로도 활용됩니다. 저희는 또한 광고주에게 제공하는 리포트 집계도 담당하고 있습니다.

이와 같은 역할을 수행하기 위해서는 다음과 같은 핵심 요구 사항을 만족해야 합니다.

  • 하루에 수 백억 건 이상, 초당 수십만 건 이상의 이벤트를 실시간으로 처리할 수 있어야 합니다.
  • 플랫폼 성장에 맞춰 더 많은 양, 더 많은 건수의 데이터를 처리해야 할 때 유연하게 대응할 수 있어야 합니다.
  • 데이터 지연 시간은 최소화해야 합니다.
  • 장애가 발생하더라도 서비스에 미치는 영향을 최소화할 수 있어야 하며, 가능한 한 빠르게 복구할 수 있어야 합니다.

기존 Spark on YARN 환경의 한계

LINE Ads는 기존에는 Hadoop 내 YARN 엔진을 활용해 Spark 애플리케이션을 운영하는 구조였습니다. 대용량 데이터를 보관하기 위해서는 Hadoop과 같은 대용량 분산 스토리지가 필요했기에 이를 내부적으로 구축해 사용했습니다. 저희는 적재된 데이터를 분산 처리 방식으로 가공해 사용자에게 제공해야 했는데요. Hadoop 내 유휴 컴퓨팅 자원을 재사용하는 YARN은 컴퓨팅 자원 측면에서도, Hadoop 내부 데이터를 처리할 때 데이터 지역성 측면을 고려해서도 매우 합리적인 선택이었습니다.

하지만 광고 시스템이 정교해지면서 더 많은 피처가 데이터에 추가돼 연산량이 증가했고, 이 때문에 성능 문제가 발생하기 시작했습니다. 그런데 다음과 같은 구조적 한계 때문에 단순히 스케일 아웃으로 문제를 해결할 수는 없었습니다.

  • YARN은 Hadoop 스토리지와 컴퓨팅 자원이 단일 노드에 결합된 환경에서 자원을 할당하므로, Spark 연산에 필요한 리소스가 HDFS 및 다른 Hadoop 컴포넌트와 경합(contention)해 성능 이슈가 발생했습니다.
  • YARN 리소스를 추가 할당하려면 Hadoop 노드를 증설해야 했는데요. 만약 스토리지는 넉넉한데 컴퓨팅 자원이 부족해 Hadoop 노드를 추가하는 상황이 발생한다면 운영 비용과 스토리지 자원 활용 측면에서 비효율적이었습니다.
  • YARN 환경에서는 JVM 및 Spark 버전 등을 자유롭게 설정하기 어려워 최신 Spark 버전의 기능을 활용하기 어려웠습니다.

위와 같은 문제를 해결하고자 YARN이 아닌 다른 컴퓨팅 엔진 도입을 검토했고, 그 결과 Spark on Kubernetes를 도입하기로 결정했습니다.

Spark on Kubernetes 소개

먼저 Spark on Kubernetes가 무엇인지 간단히 살펴보겠습니다.

Spark on Kubernetes 작동 원리

Spark on Kubernetes는 Spark 애플리케이션의 드라이버(driver)와 익스큐터(executor)를 Kubernetes 파드로 실행하는 구조입니다. 기존 YARN 환경에서 클러스터 매니저 역할을 YARN이 수행했다면, Kubernetes 환경에서는 Kubernetes가 이 역할을 대신해 Spark 애플리케이션에 필요한 리소스를 관리합니다.

Spark on Kubernetes 작동 원리
출처: https://spark.apache.org/docs/latest/running-on-kubernetes.html, Licensed under the Apache License, Version 2.0.

Spark on Kubernetes 실행 모드

Spark on Kubernetes는 클러스터 모드와 클라이언트 모드, 두 가지 모드를 지원합니다.

클라이언트 모드클러스터 모드
실행 방식드라이버는 외부(클라이언트 머신)에서 실행되며, 익스큐터만 Kubernetes에서 파드로 실행드라이버가 Kubernetes 내부 파드로 실행되며, spark-submit을 호출한 클라이언트를 작업 제출 후 종료 가능
실행 환경 관리드라이버를 실행하는 환경을 사용자가 직접 관리(로컬/별도 파드/VM)드라이버가 Kubernetes 파드으로 생성되기 때문에 Kubernetes가 기본 관리(오퍼레이터 사용 시 오퍼레이터가 생명 주기 관리)
리소스 관리드라이버 리소스는 Kubernetes 외부에서 따로 관리하며 익스큐터만 Kubernetes 리소스 사용드라이버와 익스큐터 모두 Kubernetes 리소스로 관리(CPU/메모리 요청/제한 통합 관리 가능)
스케줄링익스큐터만 Kubernetes 스케줄러 스케줄링 대상이고 드라이버는 아님드라이버와 익스큐터 모두 Kubernetes 스케줄러가 스케쥴링
로그드라이버 로그는 실행 환경(로컬/파드/서버)에 남고 익스큐터 로그는 쿠버네티스에 남음드라이버와 익스큐터 모두 Kubernetes 로킹 체계로 통합(kubectl logs, EFK 등)
네트워크드라이버와 익스큐터 간 외부와 Kubernetes 간 통신 필요(방화벽, NAT, DNS 이슈 발생 가능)드라이버와 익스큐터 모두 Kubernetes 내부 네트워크 사용하기 때문에 통신이 안정적
사용 목적개발운영

저희는 두 모드 중 오퍼레이터 및 쿠버네티스에 권한이 위임돼 있는 클러스터 모드를 선택했습니다.

클러스터 모드의 실행 흐름

저희가 선택한 클러스터 모드의 실행 흐름은 다음과 같습니다.

  1. 다음과 같이 spark-submit 실행
    • spark-submit \
          --master k8s://https://<k8s-apiserver> \
          --deploy-mode cluster \
          --name spark-app \
          --class com.example.Main \
          --conf spark.executor.instances=3 \
          local:///app.jar
      
  1. 드라이버 파드 생성
    • Spark는 드라이버 파드 스펙을 생성하고, Kubernetes 스케줄러가 적절한 노드에 배치합니다.
    • 드라이버 컨테이너가 실행되면 SparkContext를 생성하고 DAG(Directed Acyclic Graph)를 구성한 후 익스큐터를 요청합니다.
  2. 익스큐터 파드 생성
    • 각 익스큐터는 독립적인 파드로 실행됩니다.
    • 파드 단위로 CPU와 메모리가 할당되며, Kubernetes 스케줄러가 노드에 스케줄링합니다.
    • 익스큐터 파드는 다음과 같이 드라이버 파드에 종속된 형태로 생성됩니다.
      Driver Pod
           ├── Executor Pod 1
           ├── Executor Pod 2
           └── Executor Pod 3
  3. 작업 수행
    • 드라이버가 DAG을 스테이지로 분리하고 태스크를 익스큐터에 분배합니다.
    • 익스큐터는 파드 내부에서 태스크를 실행하며 연산을 수행합니다.
    • 셔플(shuffle) 데이터는 외부 셔플 서비스 없이 기본적으로 익스큐터 파드의 생명 주기에 종속됩니다.

기존 YARN 대비 Spark on Kubernetes의 장점

Spark on YARN과 Spark on Kubernetes의 차이점을 표로 정리하면 다음과 같습니다.

관점YARNKubernetes
리소스 모델컨테이너파드
환경 관리노드 단위이미지 단위
버전 관리클러스터 영향독립적
성능Hadoop의 파일 I/O 작업이 Spark 잡의 CPU 성능에 악영향을 끼침CPU 자원을 온전히 Spark 잡에만 사용 가능
확장성Hadoop 종속클라우드 네이티브

기존 YARN과 비교할 때 Spark on Kubernetes는 다음과 같은 장점이 있습니다.

  • 완전한 컨테이너 기반 실행
    • Kubernetes 환경: Docker 이미지에 모든 의존성을 포함하므로 환경을 재현하기 쉽고, CI/CD 파이프라인과 자연스럽게 연결해 사용할 수 있습니다.
    • YARN 환경: Spark 배포 시 노드별 라이브러리 버전을 관리해야 했으며, 환경 의존성 문제가 발생할 가능성이 있었습니다.
  • 인프라 독립성 확보
    • Kubernetes 환경: S3, GCS, HDFS 등 다양한 스토리지를 자유롭게 선택할 수 있으며, 클라우드 네이티브 환경에 최적화되어 있습니다.
    • YARN 환경: 꼭 Hadoop 클러스터를 선택해야 하며 HDFS 의존도가 높았습니다.
  • 쉬운 오토 스케일링
    • Kubernetes: 클러스터 오토스케일러(autoscaler)를 지원하고 파드 기반으로 스케일링할 수 있어서 클라우드 VM과 직접 연동할 수 있습니다.
    • YARN 환경: 노드 확장하는 방법이 비교적 복잡하고 온프레미스(on-premise) 중심으로 설계돼 있습니다.
  • 멀티 워크로드 통합
    • Kubernetes 환경: Spark뿐만 아니라 Airflow, 머신러닝, API 서버 등 다양한 워크로드가 동일 클러스터에 공존할 수 있어 데이터 플랫폼을 통합할 수 있습니다.
    • YARN 환경: Hadoop 에코시스템 전용에 가까웠습니다.
  • 네임스페이스 기반 격리 및 유연한 운영 거버넌스
    • 팀별 네임스페이스 분리, 리소스 쿼터(ResourceQuota) 설정, RBAC(role-based access control) 기반 권한 제어 등 YARN과 비교해 운영 거버넌스를 유연하게 설정할 수 있습니다.
  • 운영 자동화 도입 용이
    • Helm, ArgoCD, GitOps, 롤링 업데이트 등 다양한 DevOps 도구를 활용해 Spark 애플리케이션을 DevOps 흐름에 포함시켜서 운영 자동화를 실현할 수 있습니다.

Spark on Kubernetes 시스템 개요

저희 팀이 구축한 Spark on Kubernetes 시스템의 대략적인 전체 구조는 다음과 같습니다. 

Spark on Kubernetes 시스템의 대략적인 전체 구조

이 시스템은 크게 아래 네 개 레이어로 구성됩니다.

  • 배포 레이어: GitHub Actions와 ArgoCD를 활용해 코드 베이스로 빠르게 배포합니다.
  • 컴퓨팅 레이어(Kubernetes 레이어): Spark 애플리케이션 구동 환경과 컴퓨팅 자원을 제공하는 핵심 레이어입니다.
  • 스토리지 레이어: 데이터 저장소 역할을 담당합니다.
  • 모니터링 레이어: 워커와 Kubernetes 환경에서 실행되는 Spark 애플리케이션을 모니터링합니다.

각 레이어를 하나씩 자세히 살펴보겠습니다.

배포 레이어

GitHub Actions와 ArgoCD를 활용해 코드 베이스로 빠르게 배포하는 레이어입니다.

  • GitHub Actions: 저희 회사는 코드를 지속적으로 통합하기 위해 GitHub과 연동되는 GitHub Actions를 사용하며, 리포지터리 이벤트를 이용해 다양한 워크플로를 실행합니다.
  • ArgoCD: GitHub Actions가 리포지터리 이벤트로 트리거돼야 워크플로를 실행할 수 있다는 단점을 보완합니다. 배포된 애플리케이션의 상태를 모니터링하고 문제 발생 시 롤백 등을 쉽게 수행할 수 있도록 돕는 서비스입니다.

컴퓨팅 레이어(Kubernetes 레이어)

Spark 애플리케이션 구동 환경과 컴퓨팅 자원을 제공하는 핵심 레이어입니다.

  • SparkApplication: Kubeflow에서 제공하는 Spark Operator 기반의 Kubernetes 커스텀 리소스(CRD)로, Spark 앱을 배포합니다.
  • Apache YuniKorn: 배치 잡을 스케줄링하기 위해 사용하며, 리소스 조정과 갱 스케줄링 등을 지원합니다. 자세한 내용은 아래 Apache YuniKorn 챕터에서 설명합니다.
  • LogSender: 파드 내 로그를 Verda(사내 클라우드 서비스) OpenSearch(ElasticSearch)에 적재하는 역할을 합니다.
  • ClusterMonitoring: 파드에 노출된 Prometheus 지표를 IMON(Prometheus)으로 전송하는 역할을 수행합니다.

스토리지 레이어

저희는 실시간으로 데이터를 처리하기 위한 Kafka와 장기 분석을 위한 Hadoop을 운영하고 있습니다.

  • Kafka: Apache Kafka는 높은 처리량과 낮은 지연 시간, 확장성을 특징으로 하는 오픈소스 분산 이벤트 스트리밍 플랫폼입니다. LINE 서비스 사용자의 광고 액션 등을 실시간으로 처리하기 위한 저장소로 사용합니다.
  • Hadoop: HDFS(Hadoop Distributed File System)는 Hadoop 프레임워크의 핵심 구성 요소로, 저비용 상용 하드웨어에서 초대용량 데이터를 여러 서버에 분산 저장하고 처리하는 분산 파일 시스템입니다.

모니터링 레이어

모니터링 레이어에서는 다음 툴을 활용해 워커와 Kubernetes 환경에서 실행되는 Spark 애플리케이션을 모니터링합니다.

  • Verda OpenSearch: 파드에서 출력된 로그를 확인할 수 있습니다.
  • IMON Flash: 파드에 노출된 Prometheus 지표를 확인할 수 있습니다. 또한 노드의 자원 사용률, 네트워크, 디스크 I/O 등의 현황을 확인할 수 있습니다.
  • Grafana 대시보드: IMON Flash에서 수집한 지표와, Apache YuniKorn에서 가져온 지표 등을 대시보드 형태로 확인할 수 있습니다.

Apache YuniKorn

Spark Job의 자원을 안정적으로, 효율적으로 관리하기 위해서 저희는 Spark on Kubernetes과 함께 사용할 스케줄러로 Apache YuniKorn을 선택했습니다.

Apache YuniKorn은 배치 잡 스케줄링을 담당하는 스케줄러입니다. Kubernetes 환경에서 작동 가능하며, 기존 Kubernetes 스케줄러에서 지원하지 않는 자원 조정과 갱(gang) 스케줄링을 지원합니다.

대표적인 특징은 다음과 같습니다.

  • 갱(gang) 스케줄링: 하나의 잡에 필요한 모든 자원을 ‘전부 할당’하거나 ‘전부 할당하지 않음’으로 스케줄링하는 방식입니다. 잡이 부족한 자원으로 작동해서 태스크가 실패하는 것을 방지합니다.
  • 계층적 자원 큐: 계층적 큐를 제공하기 때문에 테넌트별로 자원을 세밀하게 제어할 수 있습니다. Kubernetes 컨피그맵(ConfigMap)으로 설정 가능합니다.
  • 애플리케이션 인식(application-aware) 스케줄링: 사용자, 앱, 큐별로 공정(fair) 방식, FIFO, 우선순위 큐 등 다양하게 스케줄링할 수 있습니다.
  • 중앙 관리 콘솔: 테넌트별로 큐 사용 현황을 확인할 수 있는 웹 UI를 제공합니다.

Spark 잡은 필요한 익스큐터 수가 충분히 확보되지 않으면 성능이 저하되거나 실패할 수 있습니다. YuniKorn은 필요한 모든 익스큐터 수가 확보될 때까지 작업을 대기시키는 갱 스케줄링을 사용해 이러한 문제를 방지합니다. 이는 YARN의 ApplicationMaster 방식과는 차별화되는 스케줄링 철학입니다.

트러블슈팅

Spark on Kubernetes 환경은 Spark on YARN 환경과 자원 가상화 방식이 달라서 Spark의 인 메모리 연산과 로컬 디스크 사용 방식 측면에서 여러 가지를 고려해야 했는데요. 특히, 기존 Hadoop 노드 환경과 달리 파드 단위에서 수행해야 하는 작업이 많아 트러블슈팅이 필요했습니다.

메모리 오버헤드 이슈

우선, Spark 메모리 오버헤드가 무엇인지 설명하겠습니다. Spark 메모리 오버헤드는 익스큐터 JVM 온 힙(on-heap) 메모리 외에 컨테이너에 요청하는 추가 오프 힙(off-heap) 메모리입니다. Python 프로세스, Netty, Apache Parquet 등 서드 파티 라이브러리에서 사용하는 오프 힙 영역을 위해 사용합니다. 기본값은 익스큐터 메모리의 10%(최소 384MB)이며, spark.executor.memoryOverhead 설정으로 조절할 수 있습니다. Spark 메모리 오버헤드는 전체 컨테이너 메모리에 포함돼 ‘OOM Kill(out of memory kill)’이 발생하는 것을 막는 역할을 합니다.

최초에 Spark on Kubernetes에서 구동한 앱을 프로덕션 환경에 배포했을 때 JVM이 아닌 컨테이너 OOM 이슈가 지속적으로 발생했는데요. 세 가지 사유 때문이었습니다. 먼저 Spark on Kubernetes 환경에서는 기존 YARN 환경보다 컨테이너 오버헤드로 JVM 밖에서 사용하는 연산량이 많습니다. 따라서 Spark on Kubernetes에서는 기본 YARN 대비 더 많은 오버헤드 메모리를 할당하는 것을 권장합니다. 두 번째로 Apache Parquet 직렬화/역직렬화 과정에서 JVM에서 사용하지 않는 오프 힙 메모리를 사용합니다. 마지막으로 그 외에도 Kubernetes에서 컨테이너 상태를 유지하기 위해 추가 메모리를 사용합니다.

이 이슈는 기존 익스큐터 메모리의 0.1로 설정돼 있던 오버헤드 메모리를 익스큐터 메모리의 0.2 이상으로 추가 할당하는 것으로 해결했습니다.

노드 및 파드 실패 이슈

노드를 교체해 파드가 종료됐을 때나 혹은 그 외 이유로 파드가 종료(killed)됐을 때 내부 작동을 살펴보려고 합니다. 먼저 기준과 참고 사항을 말씀드리겠습니다.

  • 기준: Spark 3.5.3 버전
  • 참고: Kubernetes 볼륨 종류는 다음과 같습니다.
    • emptyDir: 파드가 할당된 노드의 볼륨을 사용하며, 파드가 제거되면 함께 삭제됩니다.
    • PVC(블록 스토리지): 파드에 블록 스토리지를 사용하며, 익스큐터 파드가 제거되더라도 새롭게 실행되는 익스큐터 파드에서 볼륨 마운트를 시도합니다.

Spark on Kubernetes 환경에서 노드나 파드 실패가 발생하면 어떤 파드냐에 따라 다르게 작동합니다. 먼저 드라이버 파드의 경우, Spark 구조에 따라 드라이버는 하나만 운영돼야 하기 때문에 드라이버 파드가 종료되면 어쩔 수 없이 앱도 종료됩니다.

다음으로 익스큐터 파드의 경우, 익스큐터는 연산을 수행할 때 파드 볼륨에 데이터를 저장해 가며 연산하기 때문에 여러 가지 경우가 존재할 수 있습니다. 다음은 사용한 볼륨 종류와 용량 및 종료 사유별 작동 방식을 정리한 표입니다.

emptyDir: 1GiPVC-BlockStorage: 50GiemptyDir: 50Gi
OOM이 아닌 이유로 종료
  • 잡 재시도
  • 새로운 익스큐터를 실행하고 실패한 스테이지 재수행
  • 잡 재시도
  • PVC에 저장된 데이터를 새롭게 생성된 익스큐터에서 재사용해 실패한 스테이지 재수행
  • 잡 재시도
  • 새로운 익스큐터를 실행하고 실패한 스테이지 재수행
OOM으로 종료 (캐시되지 않은 잡)
  • 잡 재시도
  • 새로운 익스큐터를 실행하고 실패한 스테이지 재수행
OOM으로 종료(캐시된 잡)
  • 잡 실패
  • 메모리와 디스크 데이터가 삭제되면서 캐시된 파티션이 소실된 것이 원인
  • 잡 실패
  • 강제 종료되면서 익스큐터 파드가 Spark가 정의한 유효한 상태가 아니기 때문에 새 익스큐터가 재사용할 수 없으므로 손상으로 판단해 실패
  • 잡 실패
  • 메모리와 디스크 데이터가 삭제되면서 캐시된 파티션이 소실된 것이 원인
디스크 스필(disk spill)
  • 잡 실패
  • 매우 낮은 디스크 제한 때문에 1Gi까지만 대응 가능
  • 잡 성공
  • 할당한 디스크 제한만큼 대응
  • 잡 성공
  • 할당한 디스크 제한만큼 대응

Spark on Kubernetes 도입 효과

Spark on Kubernetes 도입 후 연산 성능이 향상되고 비용은 줄었으며, 다양한 연산 환경을 확보할 수 있었습니다. 

연산 성능 향상

Hadoop 연산을 병행할 필요가 없어지면서 단일 코어의 태스크 수행 성능이 크게 향상됐습니다. 소스와 타깃이 Kafka인 Spark Structured Streaming 앱을 예시로 살펴보겠습니다.

먼저 Spark on YARN 환경의 스펙과 이 환경에서 측정된 성능은 다음과 같습니다.

  • 인스턴스 수: 50
  • 코어 수: 4
  • 총 코어 수: 200
  • 메모리: 4G
  • 평균 프로세스/초: 200K

Spark on YARN 환경에서 측정한 성능

다음으로 Spark on Kubernetes 환경의 스펙과 이 환경에서 측정된 성능은 다음과 같습니다.

  • 인스턴스 수: 50
  • 코어 수: 2
  • 총 코어 수: 100
  • 메모리: 4G
  • 평균 프로세스/초: 653K

Spark on Kubernetes 환경에서 측정한 성능

Spark on Kubernetes 환경에서는 코어를 기존 대비 절반만 사용했는데도 스트리밍 잡에서 성능이 약 226% 향상됐습니다. Spark on YARN 환경에서 속도가 더 느린 이유는 다음과 같습니다. 기존 YARN의 CPU, 메모리, 네트워크, 디스크 I/O는 Hadoop의 ResourceManager가 격리해 관리하고 있는데요(Linux cgroup 사용). 데이터 노드 역할도 수행하는 Hadoop 내부에서는 매우 많은 네트워크 및 디스크 I/O가 발생합니다. 따라서 Hadoop의 데이터 노드 역할을 수행하고 있는 노드를 컴퓨팅 자원으로 사용할 때 I/O를 처리하면서 발생하는 시스템 호출과 컨텍스트 스위칭 때문에 CPU 성능이 저하될 수 있습니다.

스테이지 단위로 성능을 측정해 본 결과 HDFS 파일을 읽고 쓰는 배치 작업에서도 전반적인 성능을 유지했으며 일부 연산에서는 오히려 향상되기도 했습니다. Kubernetes 클러스터가 위치한 데이터 센터의 네트워크에 Hadoop이 구성돼 있다는 가정 하에 테스트해 봤는데요. 읽을 때 지역성 수준을 충족하기 어려운 데이터에 대한 읽기/쓰기 성능은 기존과 유사한 수준이었지만, 파싱이나 애그리게이션(aggregation)과 같은 연산 구간에서는 성능이 눈에 띄게 향상됐습니다. 상기한 이유와 동일하게 Hadoop 연산의 병행 없이 Spark 잡에만 집중해 연산을 수행하면서 단일 CPU의 처리 성능이 증가했기 때문입니다.

비용 최적화

실제 비용 절감 효과는 대외비로 정확한 수치를 공개하기는 어렵습니다. 내부 지표를 바탕으로 확인했을 때, 상대적으로 비싼 Hadoop용 노드를 사용하는 대신 컴퓨팅에 최적화된 노드를 사용하고, 컴퓨팅 노드가 오로지 컴퓨팅에만 집중하도록 해 성능을 향상시켜서 Hadoop의 스토리지 자원을 제외하고 컴퓨팅 자원 기준으로 연간 비용을 40% 이상 절감할 수 있었습니다. 단, 이는 운영 및 Hadoop 라이선스 비용을 고려하지 않고 오로지 Spark 잡을 수행할 수 있는 컴퓨팅 자원만 검토했을 때의 결과입니다.

다양한 연산 환경 확보

YARN 환경에 의존하지 않고 상황에 맞게 다양한 Spark 버전 및 의존성 환경에서 연산을 수행할 수 있게 되었습니다. 실제 상기한 앱들은 저버전의 Spark를 사용하고 있었으나 YARN 환경을 수정하지 않고도 상위 버전의 Spark를 사용할 수 있게 되었습니다. 또한 상위 버전의 Spark에서 지원하는 Spark Connect 기능을 이용해 다양한 환경에서 Spark 연산을 호출하고 그 값을 애플리케이션에서 활용하는 등 어디서든 Spark 잡을 호출할 수 있는 다양한 연산 환경을 확보했습니다(참고).

마치며

지금까지 LINE Ads 데이터 파이프라인 팀의 Spark on Kubernetes 도입기를 공유했습니다. 기존 YARN 환경에서 겪었던 낮은 비용 효율과 성능 저하, 버전 관리의 한계를 극복하기 위해 시작한 여정은 성공적으로 마무리됐고, 광고 시스템의 대용량 데이터 처리 환경을 한 단계 발전시키는 중요한 전환점이 되었습니다.

새로운 환경은 스트리밍 잡에서 226%의 성능 향상과 40% 이상의 연간 컴퓨팅 비용 절감이라는 눈에 띄는 성과를 가져왔습니다. 또한 인프라의 독립성을 확보하고 완전한 컨테이너 기반 환경에서 다양한 Spark 버전과 의존성을 유연하게 운영할 수 있는 토대를 마련했다는 점에서 더욱 의미가 큽니다.

저희는 전사적으로 YARN 환경에서 Spark 잡을 수행하기 위한 컴퓨팅 자원이 부족해지고 있는 상황에 대비하는 한편, 다양한 의존성 환경에서 Spark 잡을 구동할 수 있도록 광고 조직 내에서 Spark on Kubernetes 클러스터를 지속 운영할 계획입니다. 또한 Kubernetes 환경에서는 의존성을 변경하는 것이 YARN보다 자유롭기 때문에 추후 Iceberg와 같은 테이블 형식을 사용하는 등 다양한 기술을 시도해 보며 더 발전시켜 나가려고 합니다. 이를 통해 많은 동료들이 사내에서 데이터를 더욱 쉽게 활용할 수 있도록 데이터 활용성을 극대화하는 것이 목표입니다.

이 글에서 공유한 저희의 적용 사례가 비슷한 고민을 하고 계신 많은 분들께 도움이 되기를 바라며 글을 마치겠습니다. 긴 글 읽어 주셔서 감사드립니다.

박민재

Name:박민재

Description:LINE Ads에서 데이터 플랫폼과 데이터 파이프라인을 운영하고 있습니다.

손정호

Name:손정호

Description:LINE Ads에서 데이터 플랫폼과 데이터 파이프라인을 운영하고 있습니다.

정창권

Name:정창권

Description:LINE Ads에서 데이터 플랫폼과 데이터 파이프라인을 운영하고 있습니다.