Published on

EMR Spark 이벤트 로그 누적으로 History Server가 느려지고 OOM이 발생했을 때

Authors

이 글은 이벤트 로그 누적과 History Server 지연·OOM이라는 증상에서 확인할 설정을 정리한다. 당시 릴리스·원본 OOM 로그·변경한 힙·전후 조회 시간은 확보되지 않아 원인 확정이나 개선 효과를 입증하는 회고는 아니다. Spark 3.5.7과 4g는 설명 기준과 예시다.

이벤트 로그가 쌓이면서 실행 이력 조회도 문제가 됐다

EMR에서 Spark를 운영하면서 이벤트 로그가 계속 쌓였고, 실행 이력을 확인하는 History Server의 조회가 느려지거나 OOM이 발생하는 문제를 겪었다. 로그 저장 공간과 History Server의 메모리를 함께 살펴봐야 하는 상황이었다.

이 과정에서 확인한 것이 spark.history.fs.cleaner.*, spark.eventLog.rolling.enabled, 그리고 History Server 프로세스에 설정된 -Xmx1g였다. 처음에는 서로 다른 설정처럼 보였지만, 모두 같은 문제에 대응하기 위한 항목이었다. 오래된 로그를 얼마나 남길지, 새로 쌓이는 로그를 어떻게 나눌지, 그 로그를 읽는 서버에 얼마만큼의 힙을 줄지를 각각 정해야 했다.

이 글은 그 경험을 증상 확인 → 로그 보관 정책 정리 → rolling 검토 → History Server 힙 조정 → 적용 결과 확인 순서로 정리한다. 아래 설정값은 설명용 예시이며 당시 적용값과 측정 결과가 없는 부분은 예시와 구분한다.

이 글은 EMR on EC2 클러스터에서 운영하는 Spark History Server를 대상으로 한다. 설정 기본값과 동작은 Apache Spark 3.5.7을 기준으로 설명한다. EMR의 실제 기본값은 릴리스와 사용자 설정에 따라 달라질 수 있으므로 설치된 버전과 설정 파일을 먼저 확인한다. EMR의 Spark 버전 목록


문제를 겪은 대상은 이벤트 로그를 읽는 History Server였다

이번에 다루는 OOM은 History Server에서 발생한 문제다. 따라서 Executor 메모리 설정을 바꾸기보다, History Server가 읽는 이벤트 로그와 해당 JVM의 힙을 확인하는 데 초점을 맞췄다.

이벤트 로그는 Spark UI를 재구성하기 위한 실행 이벤트 기록이다. 애플리케이션의 stdout·stderr나 Log4j 로그, 스트리밍 체크포인트와는 용도가 다르다.

Spark 애플리케이션
  └─ spark.eventLog.dir에 이벤트 기록
       └─ History Server가 spark.history.fs.logDirectory에서 읽음
            └─ 실행 이력 UI 제공

쓰기 경로와 읽기 경로는 같은 저장소를 가리켜야 한다. 여러 노드에서 공유할 경로라면 각 호스트의 로컬 디렉터리가 같은 이름이라는 것만으로는 충분하지 않다. Spark 이벤트 로그 설정

대응 항목이번 문제에서 확인하려는 것
Cleaner과거 로그가 보관 기준 없이 계속 남는지
Rolling큰 로그 파일을 나누어 기록할 필요가 있는지
History Server 힙이벤트 로그를 읽고 UI를 구성하는 JVM의 메모리가 충분한지

이벤트 로그가 많아졌다는 사실만으로 OOM의 세부 원인까지 설명할 수는 없다. 작은 로그가 많이 쌓인 경우와 특정 애플리케이션의 로그 하나가 커진 경우도 다르다. 원본 로그의 개수·크기, History Server의 오류 로그와 JVM 사용량을 함께 봐야 한다. 전체 이벤트 로그 용량을 그대로 필요한 힙 크기로 계산해서도 안 된다.


오래된 로그가 계속 남지 않도록 cleaner로 보관 정책 정하기

힙만 늘려도 로그가 계속 누적되는 상황은 바뀌지 않는다. 그래서 메모리 조정과 함께, 장애 분석에 필요한 이력을 얼마나 보관할지 정하는 것이 필요했다. 이 역할을 하는 설정이 spark.history.fs.cleaner.*다.

Cleaner는 History Server가 저장소의 이벤트 로그를 주기적으로 정리하는 기능이다. 아래 기본값은 Spark 3.5.7의 History 설정 정의를 기준으로 한다.

설정기본값역할
spark.history.fs.cleaner.enabledfalse이벤트 로그 정리 활성화
spark.history.fs.cleaner.interval1d정리 작업 실행 간격
spark.history.fs.cleaner.maxAge7d오래된 로그를 판단하는 기간
spark.history.fs.cleaner.maxNum2147483647로그 개수에 따른 추가 정리 기준

maxAge만 지정하고 enabled를 켜지 않으면 정리 작업은 활성화되지 않는다. 다음은 1시간 간격으로 확인하고, 7일을 기준으로 오래된 로그를 정리하는 예시다. 10000은 예시 개수이며 운영 권장값을 뜻하지 않는다.

# History Server가 읽는 spark-defaults.conf
spark.history.fs.cleaner.enabled true
spark.history.fs.cleaner.interval 1h
spark.history.fs.cleaner.maxAge 7d
spark.history.fs.cleaner.maxNum 10000

maxAge와 maxNum은 다른 기준이다

Spark 3.5.7의 기간 기반 정리는 애플리케이션 attempt의 lastUpdated를 기준으로 판단한다. 단순히 애플리케이션을 시작한 지 7일이 지났다는 의미로 해석하면 안 된다. 또한 기간 기반 경로는 완료 상태만으로 대상을 제한하지 않으므로, 오랫동안 이벤트 갱신이 없는 애플리케이션의 로그도 보관 정책을 검토해야 한다.

개수 제한을 초과하면 오래된 애플리케이션부터 완료된 attempt 로그를 추가로 정리한다. 따라서 maxAge=7d라도 maxNum 때문에 더 최근 로그가 지워질 수 있다. 삭제 권한과 완료 상태 등에 따라 개수 제한을 즉시 만족하지 못할 수도 있다. Spark 3.5.7의 cleanLogs 구현

maxNum은 rolling 파일을 애플리케이션마다 몇 개씩 보관할지 지정하는 속성이 아니다. 애플리케이션별 rolling compaction은 뒤에서 다루는 별도 설정을 사용한다.

[!IMPORTANT] Cleaner는 UI 목록만 숨기는 것이 아니라 원본 이벤트 로그를 삭제한다. 장애 분석에 필요한 보관 기간을 먼저 정한다. maxAge는 최소 보관 기간을 보장하는 설정이 아니며, 기간이 지났다고 정확히 그 시각에 삭제되는 것도 아니다.

설정했는데 삭제되지 않을 때

다음 순서로 확인한다.

  1. spark.history.fs.logDirectory가 정리하려는 이벤트 로그 경로와 같은지 확인한다.
  2. History Server 프로세스가 해당 cleaner 설정을 읽고 재시작되었는지 확인한다. 작업의 spark-submit --conf에만 넣어서는 별도 History Server 설정이 바뀌지 않는다.
  3. History Server 로그에서 정리 실행과 삭제 실패를 확인한다. HDFS라면 실행 계정의 권한, S3라면 해당 prefix의 삭제 권한과 보관 정책을 함께 본다.
  4. 단순 파일 생성일 대신 History Server가 인식한 갱신 시점과 완료 상태를 확인한다.

History Server 자체가 OOM으로 계속 종료된다면 cleaner 실행도 기대하기 어렵다. 이런 경우에는 뒤에서 설명하는 힙 조정으로 서비스를 안정화하는 작업을 먼저 진행하고, 이후 정리 작업이 실제로 실행되는지 확인한다.


앞으로 쌓일 큰 로그는 rolling으로 나누기

과거 로그를 정리하는 것과 별개로, 새로 기록하는 이벤트 로그의 파일 크기도 관리할 필요가 있었다. 여기서 확인한 설정이 spark.eventLog.rolling.enabled다. 특히 하나의 애플리케이션이 오래 실행되며 큰 로그를 만드는 경우에 검토할 수 있다.

Rolling은 애플리케이션이 이벤트 로그를 기록하는 방식을 바꾼다. Spark 3.0.0부터 제공하며, 3.5.7 기준으로 enabled 기본값은 false, maxFileSize 기본값은 128m다. Spark rolling 설정

# application.py와 HDFS 경로는 실제 환경에 맞게 변경한다.
# 경로는 미리 만들고 애플리케이션의 쓰기 권한을 준비한다.
spark-submit \
  --master yarn \
  --deploy-mode cluster \
  --conf spark.eventLog.enabled=true \
  --conf spark.eventLog.dir=hdfs:///spark-events \
  --conf spark.eventLog.rolling.enabled=true \
  --conf spark.eventLog.rolling.maxFileSize=128m \
  application.py

이 설정은 애플리케이션을 시작할 때 적용한다. 이미 만들어진 단일 로그 파일이 설정 변경만으로 분할되지는 않는다. Livy를 사용한다면 새 세션에 설정을 전달하고 생성된 로그 형식을 확인한다.

Rolling만 켜도 전체 로그 용량이 줄어드는 것은 아니다. 파일을 나누어도 기록한 이벤트는 남는다. 전체 크기를 줄이려면 보관 기간에 따른 정리 또는 compaction을 별도로 검토해야 한다.


전체 용량까지 줄여야 한다면 compaction을 별도로 검토한다

Rolling을 확인하면서 함께 알아둘 기능이 compaction이다. 실제 적용 여부는 과거 UI 정보를 얼마나 보존해야 하는지에 따라 결정한다. History Server에는 다음 설정이 있다.

# History Server 설정: 예시 값이며 기본값이 아니다.
spark.history.fs.eventLog.rolling.maxFilesToRetain 5

이 값은 애플리케이션별로 compaction하지 않고 유지할 최근 rolling 이벤트 파일 수를 정한다. 기본값은 Int.MaxValue로 모든 파일을 유지한다. 예를 들어 rolling 파일이 8개이고 값이 5라면 오래된 3개가 compaction 후보가 된다.

Compaction은 오래된 파일의 이벤트를 분석해 일부를 버리고 다시 기록하는 작업이다. 완료된 job·stage·task 등에 관한 이벤트가 제거되어 UI에서 일부 과거 상세 정보를 볼 수 없게 될 수 있다. 단순한 무손실 압축과는 다르다. 또한 줄일 수 있는 이벤트가 충분하지 않으면 compaction을 생략할 수 있어, 설정 숫자가 전체 파일 수나 총용량의 엄격한 상한은 아니다. Spark History Server의 rolling compaction 설명

배치 작업의 과거 이력을 보관 기간 단위로 정리하려면 cleaner부터 검토한다. 하나의 애플리케이션이 장시간 살아 있고 로그가 계속 커진다면 rolling과 compaction이 필요한지 따로 판단한다.


로그 정리와 함께 History Server의 -Xmx1g도 조정하기

로그 보관 정책은 이후의 누적을 관리하기 위한 조치다. 이미 History Server에서 발생한 메모리 부족에 대응하려면 해당 프로세스의 힙도 조정해야 했다. 당시 확인한 -Xmx1g는 History Server JVM에 적용된 값이었다.

-Xmx1g는 해당 JVM의 최대 heap을 1 GiB로 제한하는 옵션이다. 프로세스 전체 메모리나 클러스터 전체 메모리의 상한은 아니다. JVM은 heap 외에도 메타스페이스, 스레드 스택, 직접 메모리 등을 사용한다.

같은 문제를 진단할 때는 옵션이 붙은 프로세스가 org.apache.spark.deploy.history.HistoryServer인지부터 확인한다. 아래 명령은 EMR 노드에서 확인하는 예시이며, 12345를 실제 PID로 바꾼다.

jps -lv

SHS_PID=12345
ps -ww -p "$SHS_PID" -o args=
jcmd "$SHS_PID" VM.command_line
jcmd "$SHS_PID" VM.flags

jcmd는 해당 JDK 도구가 설치되어 있고 대상 JVM에 접근 가능한 계정으로 실행해야 한다. VM.flags의 MaxHeapSize와 프로세스의 -Xmx를 함께 보면 실제 적용값을 확인할 수 있다.

History Server는 SPARK_DAEMON_MEMORY로 조정한다

Spark History Server의 힙은 SPARK_DAEMON_MEMORY로 지정한다. Apache Spark의 기본값은 1g지만 EMR에서 반드시 그 값이라는 뜻은 아니다. History Server 환경 변수

# EMR의 /etc/spark/conf/spark-env.sh에서 기존 값을 수정하는 예시
export SPARK_DAEMON_MEMORY=4g

4g는 조정 예시다. Primary 노드의 다른 데몬과 OS에 필요한 메모리를 남긴 상태에서 결정한다. 셸에서만 export한 값이 서비스 관리자가 시작하는 History Server에도 전달된다고 가정해서는 안 된다.

AWS도 History Server 메모리가 부족한 경우 이 값을 변경하고 서비스를 재시작한 뒤 -Xmx를 확인하도록 안내한다. AWS의 History Server 메모리 문제 진단

systemd로 관리하는 EMR 환경에서 서비스 이름이 아래와 같다면 다음과 같이 확인한다. EMR 재구성으로 이미 재시작했다면 추가 재시작 대신 상태와 적용값을 확인한다.

sudo systemctl restart spark-history-server
sudo systemctl status spark-history-server --no-pager

재시작 후 PID를 다시 찾고 -Xmx4g 또는 이에 해당하는 MaxHeapSize를 확인한다. 파일의 값만 바뀌고 프로세스가 계속 -Xmx1g로 실행된다면 서비스가 읽는 설정 경로와 환경 변수를 확인해야 한다.

spark.driver.memory나 spark.executor.memory는 별도 프로세스의 메모리 설정이므로 이 문제의 조정 대상이 아니다. History Server가 읽는 SPARK_DAEMON_MEMORY와 실제 프로세스의 적용값을 연결해서 확인해야 한다.

힙 증설 후에도 실패한다면 로그에서 실제 OOM과 GC 상태를 확인한다. 원본 로그 보관량, 동시에 처리하는 이력의 양도 함께 살펴야 한다. 디스크 캐시를 쓰는 spark.history.store.path와 그 용량 제한은 원본 이벤트 로그의 보관 정책과 별개다. History Server 저장소 설정 정의


EMR 설정 예시: 애플리케이션과 History Server에 각각 적용하기

EMR에서는 spark-defaults로 Spark 속성을, spark-env의 export 하위 분류로 환경 변수를 관리할 수 있다. 아래는 경로가 이미 준비된 환경에서 사용하는 구성 예시다. 기존 설정과 합쳐 적용하고, 경로·보관 기간·개수·힙 크기는 실제 운영 기준으로 바꾼다. EMR Spark 설정 분류, 환경 변수의 export 하위 분류

[
  {
    "Classification": "spark-defaults",
    "Properties": {
      "spark.eventLog.enabled": "true",
      "spark.eventLog.dir": "hdfs:///spark-events",
      "spark.eventLog.rolling.enabled": "true",
      "spark.eventLog.rolling.maxFileSize": "128m",
      "spark.history.fs.logDirectory": "hdfs:///spark-events",
      "spark.history.fs.cleaner.enabled": "true",
      "spark.history.fs.cleaner.interval": "1h",
      "spark.history.fs.cleaner.maxAge": "7d",
      "spark.history.fs.cleaner.maxNum": "10000"
    }
  },
  {
    "Classification": "spark-env",
    "Configurations": [
      {
        "Classification": "export",
        "Properties": {
          "SPARK_DAEMON_MEMORY": "4g"
        }
      }
    ]
  }
]

이 예시는 cleaner와 rolling, 힙 크기를 적용하며 compaction은 포함하지 않는다. 과거 이벤트 일부가 사라져도 되는지 결정한 뒤 History Server의 spark.history.fs.eventLog.rolling.maxFilesToRetain을 추가한다.

History Server 설정은 해당 서비스를 실행하는 노드에 적용하고 재시작 여부를 확인한다. 이벤트 로그 기록 설정은 새로 제출하는 애플리케이션에서 확인한다. EMR 콘솔이 제공하는 영구 애플리케이션 UI와 클러스터 내부 History Server는 구분해야 하며, 이 설정을 모든 UI 서비스의 공통 설정으로 해석하지 않는다.


변경 전후를 비교해 확인할 결과

로그 정리와 힙 증설을 함께 적용했다면, UI가 다시 열린다는 결과만으로 어느 설정이 얼마나 영향을 주었는지 구분하기 어렵다. 저장소 변화와 History Server의 동작을 나누어 기록해야 한다.

확인 항목확인 방법
이벤트 로그 기록새 애플리케이션에서 지정 경로에 rolling 파일이 생성되는지 확인
오래된 로그 정리Cleaner 실행 로그와 정리 전후 저장소의 개수·용량 비교
필요한 이력 보존보관 대상 애플리케이션을 History UI에서 열어 확인
힙 크기 반영재시작한 History Server JVM의 -Xmx와 MaxHeapSize 확인
장애 개선같은 이력을 조회하며 OOM·GC·응답 시간을 변경 전과 비교

HDFS에 저장한다면 다음처럼 실제 경로의 용량을 확인할 수 있다.

hdfs dfs -count -h /spark-events
hdfs dfs -du -s -h /spark-events

한 번 정리되어 용량이 줄어든 결과와, 이후 로그 생성량을 감당하며 유지되는지는 다르다. 일별 증가량과 필요한 보관 기간을 함께 보고 설정값을 조정한다.


결론

이번 문제는 이벤트 로그가 쌓이는 현상과 History Server의 조회 지연·OOM이 함께 나타난 사례였다. 그래서 로그를 얼마나 보관할지 정하는 작업과, 그 로그를 처리하는 History Server의 메모리를 조정하는 작업을 함께 살펴보게 됐다.

Cleaner는 오래된 로그를 정리하고, rolling은 새로 기록하는 큰 로그를 나눈다. History Server의 힙 조정은 로그를 읽는 프로세스의 메모리 부족에 대응한다. 각 설정이 맡는 역할을 구분해야 힙만 늘린 채 로그가 계속 쌓이거나, rolling만 켜고 전체 용량도 줄어들 것으로 기대하는 일을 피할 수 있다.


관련 글