이전에 Spark에 대한 내용을 포스팅 했습니다. 하지만 Spark에 대해서 좀 더 깊게 공부하기 위해 최근에 "스파크 완벽 가이드"라는 책을 구매했습니다. 앞으로 이 책의 내용을 공부하면서 요약한 내용을 글로 포스팅 하겠습니다. 해당 게시물은 "스파크 완벽 가이드" 책을 참고하여 저의 지식과 함께 작성하는 글입니다.
Spark Application과 다양한 언어 지원
Spark Application은 Driver와 다수의 Executor로 구성됩니다. Driver는 Application의 수행해야하는 Task들의 스케쥴링 및 전반적인 Application의 정보를 관리하기 때문에 핵심 프로스세입니다. Executor는 Driver의 스케쥴링으로 나온 Task들을 실제로 수행하는 프로세스입니다. 그래서 Executor가 많을수록 분산 처리가 더 많아지기 때문에 병렬성이 증가합니다. Executor는 Task들을 수행하면서 진행 상황을 다시 Driver에게 보고합니다.
Spark는 기본적으로 Scala로 개발되었기 때문에, 대표적으로 떠오르는 언어는 Scala입니다. 하지만 그 외에 Python, Java, R을 모두 지원합니다. Python, Java, R 언어로 작성 시 Spark의 동작은 기본적으로 JVM 환경에서 이루어집니다. 그래서 Python이나 R 언어로 Spark를 실행할 때 JVM에서 실행할 수 있는 코드로 변환됩니다.
DataFrame
DataFrame은 Spark에서 뿐만이 아니라 다양한 언어에서도 지원하는 테이블 형식의 API입니다. 하지만 여러 서버를 하나의 클러스터로 구성한 뒤 분산 처리를 진행하는 Spark의 경우 DataFrame을 사용하더라도 Row 단위의 데이터들이 여러 서버에 분산되어 있습니다. 아래의 Spark 코드를 예로 들었을 때, 1000개의 Row 데이터들은 하나의 서버가 아닌 여러 서버에 분산되어 있고 각 서버로부터 데이터 처리가 시작됩니다. 여러 서버에서 분산 처리가 되기 때문에 각 서버의 자원들을 모두 사용할 수 있다는 점에서 Spark는 차별점을 가지고 있습니다.
// Scala
val myRange = spark.range(1000).toDF("number")
# Python
myRange = spark.range(1000).toDF("number")
Transformation
Spark의 핵심적인 내용이라고 한다면 데이터 구조가 불변성(immutable)을 가진다는 점입니다. 하지만 데이터를 변경하고 싶을 땐 어떻게 해야할까요? Spark의 경우 새로운 데이터 구조를 새로 생성하고 이 과정을 Transformation이라고 합니다. Transformation의 특징이라고 한다면 실제 데이터 처리가 수행되지 않는다는 점입니다. Transformation을 통해서 데이터들을 여러가지 방법으로 변형하는 코드를 작성하더라도 내부적으로 데이터는 변화하지 않습니다. 아래에서 소개할 Action 명령어가 수행될 때 비로소 이전의 Transformation들이 반영되며 가장 최적의 처리 로직만 수행됩니다.
Transformation의 경우 Narrow dependency, Wide dependency로 두가지 유형이 있습니다. 두 유형의 차이점은 Partition들이 서로 셔플(Shuffle)의 발생 여부입니다. (이 때 Partition은 데이터를 병렬 처리 하기위해 분할된 청크 단위입니다.) Narrrow의 경우 Partition이 Transformation이 되더라도 자신의 데이터만 변할 뿐입니다. 하지만 Wide의 경우 자신뿐만 아니라 다른 Partition에도 영향을 끼치는 경우로써 이 때 셔플이 발생합니다. 셔플의 과정에서 데이터를 메모리가 아닌 디스크에 저장하는 과정이 필요하기 때문에 셔플이 많이 발생할수록 비싼 비용이 발생합니다.
아래 그림에서 파란색 도형이 Partition이고 여러 개의 Partition들로 이루어진 것이 데이터입니다. Narrow의 경우 map, filter, union 등의 명령어가 있으며 input Partition이 하나의 output Partition에게만 영향을 줍니다. 반면의 Wide의 경우 groupByKey 등의 명령어가 있으며 하나의 input Partition이 다수의 output Partition에게 영향을 주고 있습니다.
Action
여러가지 Transformation이 이루어졌다면 최종적으로 Action의 수행이 필요합니다. Action 명령에는 대표적으로 count가 있습니다. Action이 수행될 때 Spark Job이 DAG(Directed Acyclic Graph) 스케쥴링을 통해 시작되고, 최종 결과만을 위한 최적의 데이터 처리 로직만 수행됩니다. 예를 들자면 DataFrame 중에서 짝수번째 Row의 갯수를 센다면, 굳이 전체테이블을 가져오고 그 중에 짝수번째 Row가 몇개인지 세는 것보다 처음부터 짝수번째 Row만 가져와서 세는 것이 더 최적화된 방법이라는 것입니다.
이렇게 Transformation 명령 때는 데이터 처리를 수행하지 않다가 Action 명령 때 실질적인 데이터 처리를 진행하는 것을 보통 Lazy Evaluation이라고 합니다. Lazy Evaluation의 가장 큰 장점이라고 한다면 지금까지 설명한 최적의 방법으로 데이터 처리를 한다는 점이지만 반대로 단점 중에 하나는 Transformation 명령어를 사용하는 동안에는 실제로 데이터 처리가 이루어지지 않기 때문에 디버깅이 힘들다는 점입니다.