Published on

Pandas read_csv 메모리 폭주 - 10GB 로그 튜닝

Authors

서버 로그를 CSV로 떨궈 두고 pandas.read_csv 한 방에 분석하려다, 노트북이 멈추거나 컨테이너가 OOMKilled 되는 경험은 흔합니다. 특히 10GB급 로그는 “파일 크기 10GB면 메모리도 10GB쯤 쓰겠지”라는 기대를 배신합니다. 실제로는 문자열 컬럼, 추론된 자료형, 중간 버퍼, 인덱스와 오브젝트 오버헤드 때문에 수 배로 튀는 경우가 많습니다.

이 글은 10GB 로그를 대상으로 read_csv 메모리 폭주를 줄이는 실무 튜닝 가이드입니다. 핵심은 “한 번에 다 읽지 않기”, “처음부터 타입을 고정하기”, “문자열을 오브젝트로 두지 않기”, “필요한 컬럼만, 필요한 행만”입니다.

read_csv가 메모리를 다 먹나

1) 문자열이 object로 들어가면 폭발한다

pandas에서 문자열이 기본적으로 object로 들어가면, 각 셀은 파이썬 객체 포인터를 갖고 별도 힙 메모리를 사용합니다. 로그의 url, user_agent, trace_id 같은 컬럼이 수천만 행이면 포인터 배열만으로도 큰데, 실제 문자열 객체까지 합치면 메모리 사용량이 급증합니다.

2) dtype 추론과 파싱 과정의 중간 버퍼

read_csv는 기본적으로 dtype을 추론하고, 숫자처럼 보이는 값을 일단 문자열로 읽어 본 뒤 변환하는 과정을 거칩니다. 이 과정에서 임시 객체와 버퍼가 생깁니다. 특히 결측치 처리, 날짜 파싱, quoting 처리, 복잡한 구분자 등은 비용이 커집니다.

3) 필요 없는 컬럼까지 다 읽는다

로그는 컬럼이 많고, 분석에 필요한 건 일부인 경우가 대부분입니다. 하지만 기본 호출은 모든 컬럼을 로드합니다. 10GB에서 불필요한 컬럼이 절반이면, 그 절반이 그대로 낭비입니다.

4) 인덱스와 복사

읽은 후 df[df[...]] 같은 필터링이나 astype를 하면 내부적으로 복사가 발생할 수 있습니다. “읽기만 했는데 메모리가 늘었다”가 아니라, 읽은 뒤의 전처리에서 한 번 더 터지기도 합니다.

진단: 먼저 메모리 구조를 눈으로 확인하기

분석을 시작하기 전에, 작은 샘플로 dtype과 컬럼별 메모리 사용을 확인하면 방향이 잡힙니다.

import pandas as pd

sample = pd.read_csv(
    "access_log.csv",
    nrows=200_000,
)

print(sample.dtypes)
print(sample.memory_usage(deep=True).sort_values(ascending=False).head(10))
  • deep=True는 문자열 등 객체의 실제 메모리까지 추정해 보여줍니다.
  • 상위 몇 개 컬럼이 대부분을 먹고 있으면, 그 컬럼을 중심으로 전략을 세우면 됩니다.

1단계: usecols로 필요한 컬럼만 읽기

가장 효과가 큰 최우선 튜닝입니다.

usecols = [
    "ts", "status", "method", "path", "latency_ms",
    "trace_id"
]

df = pd.read_csv(
    "access_log.csv",
    usecols=usecols,
)
  • 로그 CSV가 수십 컬럼이면, 이 한 줄로 메모리와 파싱 시간이 크게 줄어듭니다.
  • 컬럼명이 불명확하면 먼저 헤더만 읽어서 확인합니다.
cols = pd.read_csv("access_log.csv", nrows=0).columns
print(list(cols))

2단계: dtype을 명시해서 추론 비용과 오버헤드 줄이기

숫자 컬럼은 가능한 한 작은 정수형으로

상태코드, 카운트, 플래그 등은 int64가 필요 없는 경우가 많습니다.

dtype = {
    "status": "int16",
    "latency_ms": "int32",
    "method": "category",
}

df = pd.read_csv(
    "access_log.csv",
    usecols=["ts", "status", "method", "path", "latency_ms"],
    dtype=dtype,
)
  • category는 값의 종류가 제한적일 때 매우 효과적입니다. method 같은 컬럼은 대표적입니다.

문자열은 string[pyarrow] 또는 category를 고려

Pandas 2.x에서는 문자열을 파이썬 객체 대신 Arrow 기반으로 들고 갈 수 있습니다. 환경에 따라 효과가 크게 납니다.

df = pd.read_csv(
    "access_log.csv",
    dtype_backend="pyarrow",
    usecols=["ts", "status", "path", "trace_id"],
)

print(df.dtypes)
  • dtype_backend="pyarrow"는 가능한 컬럼을 Arrow 타입으로 로드합니다.
  • Arrow 문자열은 파이썬 객체 문자열보다 메모리 효율이 좋아지는 경우가 많습니다.
  • 다만 후속 처리에서 일부 연산 호환성이나 성능 특성이 달라질 수 있으니, 파이프라인 전체를 기준으로 검증해야 합니다.

3단계: 청크로 읽고, 읽으면서 집계하기

10GB 로그를 “데이터프레임 하나”로 만드는 발상 자체를 버리면, 메모리 문제의 대부분이 사라집니다. 대개 로그 분석은 최종적으로 집계 테이블이나 지표가 목적이므로, 청크 단위로 읽으면서 누적 집계하세요.

예시: 상태코드별 카운트

import pandas as pd

chunksize = 1_000_000
counts = None

for chunk in pd.read_csv(
    "access_log.csv",
    usecols=["status"],
    dtype={"status": "int16"},
    chunksize=chunksize,
):
    c = chunk["status"].value_counts()
    counts = c if counts is None else counts.add(c, fill_value=0)

counts = counts.sort_index().astype("int64")
print(counts)

예시: 분 단위 요청 수 타임시리즈

ts가 ISO8601 문자열이라고 가정합니다. 날짜 파싱은 비용이 크므로, 먼저 필요한 해상도로만 자르는 전략도 좋습니다.

import pandas as pd

chunksize = 500_000
series = None

for chunk in pd.read_csv(
    "access_log.csv",
    usecols=["ts"],
    chunksize=chunksize,
):
    # 예: "2026-02-24T12:34:56Z"에서 분 단위 키 생성
    minute = chunk["ts"].str.slice(0, 16)  # "YYYY-MM-DDTHH:MM"
    c = minute.value_counts()
    series = c if series is None else series.add(c, fill_value=0)

series = series.sort_index().astype("int64")
print(series.head())
  • 날짜를 무조건 to_datetime로 파싱하기 전에, 목표 해상도에 맞게 문자열 슬라이싱으로 줄이는 것이 훨씬 싸게 먹히는 경우가 많습니다.

4단계: 행 필터링은 “읽는 단계”에서 최대한 해결하기

skiprows는 신중하게

특정 구간을 건너뛰려면 skiprows를 쓸 수 있지만, 복잡한 조건으로 많은 행을 건너뛰면 오히려 느려질 수 있습니다.

실무 패턴: 먼저 grep로 줄이고 pandas로 읽기

10GB 로그에서 특정 서비스, 특정 기간, 특정 상태코드만 필요하면 OS 레벨에서 먼저 줄이는 게 가장 빠르고 메모리 친화적입니다.

# 예: 500 에러만 추출 (CSV에서 status 컬럼이 특정 위치라는 가정이 필요)
# 안전하게 하려면 헤더 포함 여부, 구분자, quoting을 고려해야 함
rg ",500," access_log.csv > access_log_500.csv

그 다음 작은 파일을 read_csv로 읽습니다. 로그가 컨테이너 환경에서 OOM으로 죽는 문제는 대개 “입력 자체를 줄이는” 것만으로도 해결되는 경우가 많습니다.

관련해서, 대용량 환경에서의 장애는 애플리케이션 레벨만이 아니라 플랫폼 레벨에서 같이 터집니다. 예를 들어 파드가 반복 재시작되는 상황이라면 Kubernetes CrashLoopBackOff 원인별 로그·해결 9가지처럼 런타임/리소스 관점 체크도 병행하는 것이 안전합니다.

5단계: 파서 엔진과 옵션 튜닝

engine 선택

  • 기본은 C 엔진이 빠른 편입니다.
  • 복잡한 quoting, 비정형 구분자, 깨진 라인이 많으면 Python 엔진이 필요할 수 있지만, 성능과 메모리는 불리해질 수 있습니다.
df = pd.read_csv(
    "access_log.csv",
    engine="c",
    on_bad_lines="skip",
)
  • on_bad_lines="skip"는 깨진 행을 건너뛰지만, 데이터 품질 이슈를 숨길 수 있으니 건너뛴 개수를 따로 기록하는 방식을 추천합니다.

na_filter와 결측치 처리

결측치 탐지는 비용이 있습니다. 결측치가 거의 없고, 빈 문자열을 그대로 두어도 된다면 다음을 고려합니다.

df = pd.read_csv(
    "access_log.csv",
    na_filter=False,
)
  • 대신 빈 값이 진짜 결측인지, 빈 문자열인지 구분이 필요하면 이 옵션은 맞지 않습니다.

6단계: 문자열 컬럼 최적화 레시피

category가 잘 먹히는 컬럼

  • method (GET, POST 등)
  • region, az, cluster 같은 고정 집합
  • status_class (2xx, 4xx)
dtype = {
    "method": "category",
    "status": "int16",
    "service": "category",
}

category가 위험한 컬럼

  • trace_id, request_id, user_id처럼 고유값이 매우 많은 컬럼

고유값이 많으면 category는 오히려 메모리 이득이 적거나 손해가 날 수 있습니다. 이런 컬럼은 Arrow 문자열이 더 나은 선택일 때가 많고, 목적이 조인이나 그룹키가 아니라 “존재 확인” 정도라면 아예 읽지 않는 게 최선입니다.

7단계: 결과를 다시 저장할 때도 메모리를 아끼기

10GB를 읽어 1GB짜리 요약 테이블을 만들었다면, 다시 CSV로 저장하는 순간 또 비용이 큽니다. 가능하면 Parquet로 저장하세요.

# 요약 결과를 Parquet로 저장
result.to_parquet("summary.parquet", index=False)

# 다음 분석에서 훨씬 빠르게 로드
df2 = pd.read_parquet("summary.parquet")

Parquet는 컬럼형 포맷이라 필요한 컬럼만 다시 읽기도 쉬워, “다음 작업”의 비용까지 줄여줍니다.

8단계: 컨테이너 및 운영 환경에서의 실전 팁

  • 메모리 제한이 있는 환경에서는 chunksize 기반 스트리밍 처리로 설계를 바꾸는 게 가장 확실합니다.
  • 파드가 OOMKilled 되면 재시작이 반복되며 다른 장애처럼 보일 수 있습니다. 이때는 애플리케이션 로그뿐 아니라 이벤트, 리소스 제한, 스로틀링까지 함께 봐야 합니다.
  • 로그 분석이 네트워크 호출이나 API 스트리밍과 결합된 파이프라인이라면, 타임아웃과 재시도 설계가 중요합니다. 스트리밍이 끊기는 경우의 복구 패턴은 OpenAI Responses API 스트리밍 끊김 타임아웃 완전 복구 가이드에서 다룬 체크포인팅 접근이 데이터 처리에도 그대로 응용됩니다. 청크 단위로 처리하고, 처리 완료 오프셋을 저장하면 중단되어도 이어갈 수 있습니다.

실무용 체크리스트: 10GB 로그를 안전하게 읽는 순서

  1. 헤더만 읽어서 컬럼 확인: nrows=0
  2. 샘플로 메모리 폭탄 컬럼 찾기: memory_usage(deep=True)
  3. usecols로 최소 컬럼만
  4. dtype 명시: 정수는 작은 타입, 저카디널리티 문자열은 category
  5. 가능하면 dtype_backend="pyarrow" 검토
  6. chunksize로 읽고, 읽으면서 집계 또는 필터링
  7. 결과는 Parquet로 저장

마무리

pandas.read_csv가 메모리를 다 먹는 문제는 대개 “대용량을 데이터프레임 하나로 만들려는 구조”에서 시작합니다. 10GB 로그는 CSV라는 포맷 자체가 비효율적인 면도 있어, usecols와 dtype 지정만으로도 개선되지만, 궁극적으로는 청크 기반 처리와 컬럼형 저장 포맷으로의 전환이 가장 큰 효과를 냅니다.

다음 단계로는 “집계 목적에 맞는 스키마 설계”와 “중단 복구 가능한 배치 파이프라인”을 함께 고민하면, 분석 코드가 운영 환경에서도 안정적으로 돌아가기 시작합니다.