1. Why Spark?
단일 머신의 한계에서 분산 컴퓨팅까지
- Haram Lee
- 2026-08-16
- studies / Topics / Spark
1.1 The Limits of Single-Machine Processing
- 초기에는 단일 머신에서 데이터를 읽고 계산하면 충분했음
flowchart TD storage["Storage"] --> machine["Single Machine"] machine --> cpu["CPU"] machine --> memory["Memory"] machine --> disk["Disk"]
데이터와 연산량의 증가로 한 머신의 자원이 병목이 되는 상황
- CPU, memory, disk~
- 데이터가 메모리보다 크다고 단일 머신에서 처리 자체가 불가능한 것은 아님. 디스크 기반 처리나 데이터 베이스를 사용할 수 있지만, 문제는 데이터가 커질수록 처리 시간이 길어지고 scale-up에도 비용과 물리적 한계가 생긴다는 것
- → 값싼 서버로 scale-out하는 방식을 채택
- → 새로운 문제
- 어떻게 데이터를 나눌 것인가?
어느 머신에 어떤 작업을 보낼 것인가?
머신이 고장 나면 어떻게 복구할 것인가?
머신 사이의 데이터를 어떻게 이동할 것인가? - → 분산 작업을 자동으로 분할, 실행, 복구할 수 있는 프로그래밍 모델이 필요
1.2 Data Processing Before Spark
- MapReduce
concept: map과 reduce 함수만 정의하면 런타임이 입력 분할, 작업 스케줄링, 장애 처리와 통신까지 담당하도록
HDFS → 여러 머신에 데이터를 분할/복제 저장 → MapReduce가 데이터를 분산 처리하는 구조
flowchart TD input["Input Data"] --> map1["Map Task 1"] input --> map2["Map Task 2"] input --> map3["Map Task 3"] map1 --> shuffle["Shuffle and Sort"] map2 --> shuffle map3 --> shuffle shuffle --> reduce1["Reduce Task 1"] shuffle --> reduce2["Reduce Task 2"] reduce1 --> output["Output"] reduce2 --> output
- → 세부 구현을 직접 작성하지 않아도 됨
1.3 The Limitations of Hadoop MapReduce
- MapReduce도 데이터를 한 번 읽어서 변환/결과를 저장하는 단방향 배치 처리에는 효율적
- 반복 작업: 머신러닝/그래프 알고리즘 등
flowchart LR read1["Data Read"] --> compute1["Iteration 1<br/>Compute"] --> write1["Result Write"] write1 --> read2["Result Read"] --> compute2["Iteration 2<br/>Compute"] --> write2["Result Write"] write2 --> read3["Result Read"] --> compute3["Iteration 3<br/>Compute"] --> write3["Result Write"]
- 각 iteration을 별도의 Job으로 구성하면 그 사이의 결과를 HDFS같은 외부 안정적인 저장소에 기록하고 다음 Job에서 읽는 방식
→ 여러 Job이 데이터를 공유하려면 중간 결과를 안정적인 외부 저장소에 물질화하는 방식에 의존
flowchart TD hdfs1["HDFS"] -- "read" --> job1["MapReduce Job 1"] job1 -- "write" --> hdfs2["HDFS"] hdfs2 -- "read" --> job2["MapReduce Job 2"] job2 -- "write" --> hdfs3["HDFS"]
대화형 분석: 사용자가 같은 데이터에 여러 질문을 던져 질의하는 경우
sqlSELECT COUNT(*) FROM logs WHERE status = 500; SELECT service, COUNT(*) FROM logs GROUP BY service; SELECT * FROM logs WHERE latency > 1000;
- 각 쿼리가 별도의 MapReduce Job으로 실행되고 원본 데이터를 다시 읽는다면, 사용자는 쿼리마다 긴 시작 시간과 디스크 읽기를 기다려야 함
1.4 Why Distributed Computing?
- 모두가 아는 세가지
- Parallelism
- Scalability
- Fault Tolerance
장점
- 병렬 처리
- 수평 확장
- 장애 허용
비용
- 네트워크 통신
- 작업 조율
- 데이터 직렬화
- Shuffle
- Data Skew
1.5 How Spark Approaches Distributed Processing
- 2009년 개발 시작해서 2010년 공개
- 여러 연산에서 반복적으로 사용하는 working set을 어떻게 효율적으로 유지할 것인가?
RDD는 다음 정보를 함께 표현함.
- 여러 머신에 파티셔닝된 불변 데이터 집합
- 데이터를 만드는 연산 관계
- 손실된 파티션을 복구하는 정보
- Partitioning: 데이터를 파티션으로 나눔
- 각 파티션은 서로 다른 Executor에서 병렬로 처리할 수 있음
예를 들어 하나의 RDD가 Partition 0부터 Partition 3까지 네 개의 파티션으로 나뉘면, 각 파티션을 서로 다른 Executor에서 동시에 처리할 수 있음.
- DAG: 여러 Transformation의 의존 관계를 DAG로 표현
flowchart TD read["Read"] --> filter["Filter"] filter --> map["Map"] map --> join["Join"] other["Read another dataset"] --> join join --> aggregate["Aggregate"] aggregate --> write["Write"]
- Lazy Execution: 연산을 지연 실행
- 바로 실행하지 않고 연산 관계만 기록 / 실제 action이 호출될 때 실제 작업을 실행
python
result = (
data
.filter(...)
.map(...)
)- 필요한 데이터를 재사용
- 같은 데이터를 여러 Action이나 반복 연산에서 사용한다면 계산된 파티션을 Executor 메모리에 보관할 수 있음
flowchart TD source["Source"] --> transform["Expensive Transformation"] transform --> cached["Cached RDD"] cached --> analysisA["Analysis A"] cached --> analysisB["Analysis B"] cached --> analysisC["Analysis C"]
1.6 Why Spark Is Fast
① 계산을 병렬화한다
- 데이터를 여러 파티션으로 나누고 여러 CPU core에서 병렬처리
text
Partition 0 → Task 0 → Executor A
Partition 1 → Task 1 → Executor B
Partition 2 → Task 2 → Executor C② 불필요한 디스크 I/O와 재계산을 줄인다
- 반복해서 사용하는 데이터는 캐싱
text
MapReduce-style
Read → Compute → Write
Read → Compute → Write
Read → Compute → Writetext
Spark-style
Read → Compute → Cache
├→ Compute
├→ Compute
└→ Compute③ 여러 연산을 하나의 실행 흐름으로 연결한다
map → filter → map처럼 Shuffle이 필요 없는 연산은 한 Task 안에서 이어서 처리하기: Pipelining- 현대 Spark에서는 실행 계획도 최적화한다
- 오늘날 주로 사용하는 DataFrame과 Spark SQL은 데이터 스키마와 연산 구조를 알고 있음
- Spark SQL은 필요한 컬럼만 읽는 컬럼 가지치기, 조인 전략 선택, 통계 기반 최적화, 실행 중 계획을 조정하는 AQE 등을 제공
- 캐싱, 파티션 조정, 조인 전략 선택, 런타임 통계를 사용하는 AQE 등
flowchart TD user["사용자가 작성한 연산"] --> logical["Logical Plan"] logical --> optimized["Optimized Plan"] optimized --> physical["Physical Plan"] physical --> execution["Distributed Execution"]
Spark가 항상 빠른 것은 아님. Executor 시작, Task 스케줄링, 직렬화, 네트워크 통신에는 고정 오버헤드가 발생함.
Spark가 잘 맞는 경우
- 데이터가 한 머신의 처리 범위를 크게 넘어감
- 여러 머신에서 병렬 처리할 수 있음
- 반복해서 재사용할 데이터가 있음
- 복잡한 조인·집계·ETL이 필요함
느려질 수 있는 경우
- 데이터가 매우 작음
- Shuffle이 지나치게 큼
- 특정 Key에 데이터가 몰림
- 파티션 수가 부적절함
- Driver로 데이터를 전부
collect()함