큐레이션 요약
Spark Connect on Kubernetes #1: 견고한 Spark Connect 만들기
토스증권은 여러 사용자가 안정적으로 사용할 수 있는 Production급 Spark Connect를 Kubernetes에서 운영하고 있습니다. Spark Connect는 Driver를 애플리케이션마다 실행하는 대신 장기 실행 서버로 분리해 가벼운 클라이언트와 빠른 세션 생성을 제공하지만, 여러 세션이 하나의 SparkContext를 공유하면서 장애 전파와 리소스 경합 문제가 발생합니다. 이를 해결하기 위해 글로벌 장애 카운터를 사실상 비활성화하고, 결과 크기를 제한하며, 여러 Replica로 Driver와 SparkContext를 분리하는 전략을 사용합니다.
Classic Spark의 구조와 Spark Connect의 등장
- Spark는 작업을 계획·지휘하는 Driver와 실제 연산을 수행하는 Executor로 구성됩니다.
- Classic Spark의 배포 방식은 다음과 같습니다.
- Client mode: 클라이언트 프로세스가 Driver 역할을 수행합니다.
- Cluster mode: 작업 제출 시 클러스터에 Driver가 생성되고 작업 종료 후 사라집니다.
- 두 방식 모두 애플리케이션마다 Driver가 하나씩 생성되고, 애플리케이션의 수명과 함께 종료됩니다.
- Spark Connect는 Spark 3.4부터 도입됐으며, 4.0에서는 기존 Dataset/DataFrame API와 거의 동등한 수준에 도달했습니다.
- 글의 구현과 설정은 Spark 4.1을 기준으로 합니다.
Spark Connect의 동작 방식
- Spark Connect에서는 Driver를 애플리케이션별 프로세스가 아니라 미리 실행해 둔 서버로 운영합니다.
- 클라이언트는 Spark 라이브러리와 JVM을 직접 포함하지 않는 Thin Client입니다.
- 클라이언트의 DataFrame·SQL 연산은 다음 과정으로 처리됩니다.
- 연산을 Unresolved Logical Plan으로 변환
- Protocol Buffer로 인코딩
- gRPC를 통해 서버로 전송
- 서버가 분석, 최적화, 스케줄링, 실행 수행
- 결과를 Arrow 기반으로 클라이언트에 스트리밍
- 구조적으로는 JDBC 클라이언트가 데이터베이스 서버에 질의하는 모델과 유사합니다.
Spark Connect의 장점
- 클라이언트에 무거운 Spark 의존성이나 JVM이 없어도 됩니다.
- Python, SQL, 노트북, BI 도구 등 다양한 클라이언트가 같은 서버에 접속할 수 있습니다.
- Driver가 이미 실행 중이므로 매번 프로세스를 생성하고 리소스를 협상할 필요가 없습니다.
- 클라이언트가 종료되거나 네트워크가 끊겨도 서버에서 실행 중인 작업은 보호할 수 있습니다.
- 반면 하나의 장기 실행 서버에 여러 사용자가 접속하면서, Spark의 기존 “애플리케이션 하나에 워크로드 하나”라는 전제가 깨집니다.
공유 Driver가 만드는 단일 장애점
- 여러 세션이 하나의 SparkContext와 Driver JVM을 공유합니다.
- Driver가 장애를 일으키면 해당 서버의 모든 세션, 실행 중인 Job, 캐시가 함께 사라집니다.
spark.executor.maxNumFailures는 Executor 실패를 애플리케이션 전체 단위로 누적합니다.- 기본 임계값은
max(3, 2 × executor 수)입니다. - 임계값을 초과하면
stopApplication()이 호출되고, 결과적으로sys.exit(11)로 서버 전체가 종료됩니다.
- 기본 임계값은
- 이 카운터는 다음 이유로 멀티세션 환경에서 위험합니다.
- 개별 쿼리의 Task 실패가 아니라 Executor 실패를 전역적으로 집계합니다.
- 시간이 지나도 실패 기록이 계속 누적됩니다.
- 서로 다른 사용자의 실패가 합산됩니다.
- 문제가 없는 세션도 장애를 함께 겪게 됩니다.
세션 격리와 리소스 경합의 한계
newSession()은 SQL 네임스페이스 등 세션 상태만 분리합니다.- CPU, 메모리, Executor, Task 슬롯은 모든 세션이 공유합니다.
- 한 사용자가 대규모 Job을 제출하면 다른 사용자의 쿼리 응답도 느려질 수 있습니다.
- 기본 FIFO 스케줄링에서는 먼저 제출된 작업이 우선하며, 선점이 없어 이미 실행 중인 Task를 중단할 수 없습니다.
- Fair Scheduler를 사용해도 Task 슬롯을 배분하는 순서만 조정할 뿐, 사용자별 CPU·메모리 격리는 제공하지 않습니다.
- Spark Connect에서는
spark.scheduler.pool이 기본적으로 제대로 전파되지 않아 모든 쿼리가 Default Pool에 들어갑니다.- Classic Spark에서는
setLocalProperty()가 Driver 스레드에 직접 적용됩니다. - Spark Connect에서는 클라이언트와 Driver가 분리되어 서버의 요청 처리 스레드에 값을 별도로 설정해야 합니다.
- 토스증권은 서버 스레드에 사용자별 Pool을 직접 설정하는 방식으로 이 문제를 보완했습니다.
- Classic Spark에서는
- 사용자별 Pool을 적용하려면 먼저 요청의 사용자를 식별해야 하며, 인증·인가와 연결됩니다.
- 궁극적인 CPU·메모리 격리는 Spark 스케줄러가 아니라 Spark 외부의 리소스 관리 계층에서 해결해야 합니다.
고정된 서버 스케일 문제
- Spark Connect 서버는 이미지, Driver·Executor 리소스, Spark 설정이 고정된 상태로 실행됩니다.
- Dynamic Resource Allocation으로 Executor 수는 조절할 수 있지만, 서버 자체의 기본 스펙은 실행 중 바뀌지 않습니다.
- 서버를 필요에 따라 생성·교체하거나 팀 단위로 격리하는 문제는 후속 글에서 다룹니다.
글로벌 장애 카운터 비활성화
- 서버 전체를 종료시키는 Executor 실패 경로를 차단하기 위해 다음과 같이 설정합니다.
spark.executor.maxNumFailures: 사실상 무한대로 설정해 글로벌 종료 조건을 비활성화spark.executor.failuresValidityInterval: 오래된 실패 기록을 주기적으로 제거spark.task.maxFailures: 동일 Task의 반복 실패를 제한spark.stage.maxConsecutiveAttempts: Shuffle Fetch 실패로 Stage가 반복 실행되는 상황을 제한
task.maxFailures는 OOM이나 예외처럼 동일 Task가 반복 실패하는 경우를 담당합니다.stage.maxConsecutiveAttempts는 Shuffle Fetch 실패로 Stage 전체가 반복되는 경우를 담당합니다.- 이 방식으로 문제가 있는 쿼리만 실패시키고 서버와 다른 사용자의 세션은 유지할 수 있습니다.
- 다만 실패 허용 횟수를 지나치게 낮추면 일시적인 장애에도 정상 쿼리가 실패할 수 있으므로 워크로드에 맞춰 여유를 둬야 합니다.
Driver 메모리 보호와 결과 크기 제한
- Spark Connect에서는 쿼리 결과가 Driver를 거쳐 클라이언트로 스트리밍됩니다.
- 사용자가 대규모 테이블을
collect하면 Driver 메모리가 고갈될 수 있습니다. spark.driver.maxResultSize는 한 액션에서 반환되는 Task 결과의 누적 크기를 제한합니다.- 제한을 초과하면 Driver가 결과를 모두 가져오기 전에 Job을 중단하므로, 대규모 결과가 Driver 메모리에 유입되는 것을 막을 수 있습니다.
- 기본값인 1GB는 애플리케이션 하나만 실행하는 환경의 값이므로, 여러 세션이 동시에 결과를 가져가는 멀티세션 서버에서는 더 보수적으로 설정해야 합니다.
- 이 설정만으로 Driver OOM이나 노드 장애까지 막을 수는 없습니다.
여러 Replica를 통한 장애 영향 축소
- Driver 자체의 OOM이나 노드 소실처럼 설정으로 막을 수 없는 장애에 대비해 Spark Connect 서버를 여러 Replica로 구성합니다.
- 각 Replica는 독립적인 다음 요소를 갖습니다.
- SparkContext
- Driver
- Executor
- 한 Replica가 장애로 종료되어도 장애 범위가 해당 Replica에 한정되고, 다른 Replica가 새로운 세션 요청을 처리할 수 있습니다.
- 단일 서버의 장애가 Spark Connect 전체로 확산되는 구조를 여러 독립 실행 단위로 나누는 것이 핵심입니다.
실용적인 운영 방향
- 멀티세션 Spark Connect에서는 전역 장애 카운터를 그대로 두지 말고, Task·Stage·Job 단위의 실패 제한으로 문제 쿼리를 격리하는 것이 안전합니다.
spark.driver.maxResultSize를 동시 세션 수와 쿼리 특성에 맞게 보수적으로 설정해야 합니다.- 스케줄러 Pool만으로는 CPU·메모리 격리가 불가능하므로, 강한 격리가 필요하면 Replica나 Kubernetes 리소스 정책을 활용해야 합니다.
- 단일 Driver를 그대로 공유하기보다 여러 Replica를 운영해 장애의 영향 범위를 줄이는 것이 Production 환경에 적합합니다.
관련 글
큐레이션 요약을 이어서 읽어보세요.