Post

[허수아비] mock-radar CSV 청킹으로 메모리 98% 절감

EC2 메모리 고갈의 원인이었던 mock-radar 컨테이너를 CSV 청킹 방식으로 전환해 메모리 사용량을 3.93GiB에서 72MiB로 줄인 트러블슈팅 기록입니다.

[허수아비] mock-radar CSV 청킹으로 메모리 98% 절감

프론트엔드 페이지 렌더링이 갑자기 느려졌습니다. EC2를 확인해보니 메모리가 거의 꽉 차 있었고, 범인은 예상치 못한 곳에 있었습니다. 레이더 데이터를 Kafka로 흘려주는 mock 서비스 birdybuddy-mock-radar가 혼자 3.93GiB를 점유하고 있었습니다. mock_radar.py의 CSV 읽기 방식을 청킹 구조로 전환해 해결했습니다.


[문제] EC2 메모리가 꽉 찼다

프론트엔드 페이지 렌더링이 느려 EC2 #1 서버 상태를 확인한 결과, 전체 메모리(15.6GiB)가 거의 소진된 상태였습니다.

1
2
3
$ free -h
               total        used        free      shared  buff/cache   available
Mem:            15Gi        15Gi       212Mi        25Mi       712Mi       548Mi

docker stats로 컨테이너별 메모리 사용량을 확인했습니다.

1
2
3
4
5
6
7
8
9
birdybuddy-mock-radar             3.932GiB / 15.62GiB   25.18%   ← 주범
birdybuddy-spark-worker           2.467GiB / 3GiB       82.23%
birdybuddy-kafka                  1.448GiB / 2GiB       72.39%
birdybuddy-datanode               1.381GiB / 15.62GiB    8.85%
birdybuddy-namenode               1.233GiB / 15.62GiB    7.90%
birdybuddy-spark-stream           871.6MiB / 15.62GiB    5.45%
birdybuddy-spark-radar-stream     831.5MiB / 15.62GiB    5.20%
birdybuddy-spark-radar-archiver   743.2MiB / 15.62GiB    4.65%
...

mock 서비스임에도 불구하고 birdybuddy-mock-radar3.93GiB를 점유하고 있었으며, 메모리 limit도 설정되어 있지 않았습니다.

docker logs로 컨테이너 상태를 확인했습니다.

1
2026-04-02 10:42:54,520 [INFO] 진행: 6,630,000 / 8,213,608  (25.9 msg/s)

총 8,213,608행 CSV를 1배속(1.0x)으로 순회 중이었으며, 약 80% 진행 상태였습니다. 남은 행(약 1.58M행) 기준으로 완료까지 약 17시간 소요 예상 → 그때까지 4GB 점유 지속.

[원인] mock-radar가 4GB를 잡아먹고 있었던 이유

CSV 8백만 행을 한 번에 통째로 올려놓고 있었다

mock_radar.pyload_csv() 함수:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def load_csv(csv_path):
    usecols = list(COLUMN_RENAME.keys())
    df = pd.read_csv(csv_path, usecols=usecols, dtype="float64")  # ← 8.2M행 전체 로드
    df.rename(columns=COLUMN_RENAME, inplace=True)

    # 정수 필드 변환
    for col in int_cols:
        df[col] = df[col].astype(int)

    # absolute_sec 계산 후 정렬
    df["absolute_sec"] = (df["day"] - base_day) * 86400 + df["corrected_sec"]
    df.sort_values("absolute_sec", inplace=True)   # ← 정렬 중 내부 복사본 생성
    df.reset_index(drop=True, inplace=True)        # ← 또 복사

    return df  # 이후 run()에서 df.iterrows()로 순회하는 동안 계속 메모리 점유

메모리 점유 계산:

  • raw 데이터: 8,213,608행 × 20컬럼 × 8bytes(float64) ≈ 1.3GB
  • sort_values() 내부 복사: ≈ +1.3GB
  • pandas 인덱스, 오버헤드 등: ≈ +수백MB
  • 합계 ~4GB, 전체 순회가 끝날 때까지 메모리에 상주

메모리 상한도 걸려 있지 않았다

docker-compose.ymlmock-radar 서비스에 deploy.resources.limits.memory가 없어 호스트 전체 메모리를 무제한으로 사용 가능한 상태였습니다.

[해결] 조금씩 나눠 읽도록 하자

CSV 청킹(chunked reading)으로 전환

load_csv()를 제거하고, pd.read_csv(..., chunksize=50_000)으로 5만 행씩만 메모리에 올려 처리하는 방식으로 변경했습니다. CSV가 absolute_sec 기준 시간순 정렬되어 있다는 전제 하에 sort_values()도 제거했습니다.

핵심 변경 내용:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
CHUNK_SIZE = 50_000

def _read_first_row(csv_path: str) -> tuple[float, int]:
    """base_original(첫 행의 absolute_sec) 계산을 위해 첫 행만 읽음."""
    first = pd.read_csv(
        csv_path,
        nrows=1,
        usecols=list(COLUMN_RENAME.keys()),  # 필요한 컬럼만 선택 (문자열 컬럼 제외)
        dtype="float64",
    )
    first.rename(columns=COLUMN_RENAME, inplace=True)
    base_day = int(first.iloc[0]["day"])
    absolute_sec = float(first.iloc[0]["corrected_sec"])  # base_day 기준이므로 day 차이 = 0
    return absolute_sec, base_day


def _preprocess_chunk(chunk: pd.DataFrame, base_day: int) -> pd.DataFrame:
    """청크 단위 전처리: 컬럼 rename, 타입 변환, absolute_sec 계산."""
    chunk = chunk[[c for c in COLUMN_RENAME.keys() if c in chunk.columns]].copy()
    chunk.rename(columns=COLUMN_RENAME, inplace=True)
    for col in int_cols:
        chunk[col] = chunk[col].astype(int)
    chunk["absolute_sec"] = (chunk["day"] - base_day) * 86400 + chunk["corrected_sec"]
    return chunk


def run(csv_path, bootstrap, topic, sensor_id, speed):
    base_original, base_day = _read_first_row(csv_path)
    producer = build_producer(bootstrap)

    reader = pd.read_csv(
        csv_path,
        usecols=list(COLUMN_RENAME.keys()),
        dtype="float64",
        chunksize=CHUNK_SIZE,   # ← 5만 행씩만 메모리에 올림
    )
    for chunk in reader:
        chunk = _preprocess_chunk(chunk, base_day)
        for _, row in chunk.iterrows():
            # 타이밍 재현 + Kafka 발행 (기존과 동일)
            ...

usecols를 빠뜨렸더니 문자열 컬럼에서 ValueError가 터졌다

청킹 수정 후 첫 배포 시 컨테이너가 재시작을 반복하는 증상이 발생했습니다.

1
ValueError: could not convert string to float: '2021-10-08-09-57-07tracks.txt'

원인: _read_first_row에서 usecols를 지정하지 않아, CSV에 존재하는 파일명 형태의 문자열 컬럼(2021-10-08-09-57-07tracks.txt)까지 읽으려다 dtype="float64" 변환에서 실패했습니다.

수정:

1
2
3
4
5
# 수정 전
first = pd.read_csv(csv_path, nrows=1, dtype="float64")

# 수정 후
first = pd.read_csv(csv_path, nrows=1, usecols=list(COLUMN_RENAME.keys()), dtype="float64")

[결과] 메모리 사용량이 98% 줄었다

수정 후 docker stats:

1
2
3
4
5
6
birdybuddy-spark-worker           2.475GiB / 3GiB       82.50%
birdybuddy-datanode               1.658GiB / 15.62GiB   10.62%
birdybuddy-kafka                  1.485GiB / 2GiB       74.23%
birdybuddy-namenode               1.234GiB / 15.62GiB    7.90%
...
birdybuddy-mock-radar              72.18MiB / 15.62GiB    0.45%   ← 해결
항목수정 전수정 후
mock-radar 메모리3.93 GiB72 MiB
전체 메모리 사용~15 GiB (100%)~12 GiB (~77%)
메모리 감소량약 3.86 GiB (98% 감소)

정상 동작 로그:

1
2
3
4
2026-04-02 11:35:09,442 [INFO] CSV 스트리밍 모드 시작 (chunk=50,000): /data/radar.csv
2026-04-02 11:35:09,499 [INFO] 발행 시작 — topic=radar.events, speed=1.0x
2026-04-02 11:43:00,115 [INFO] 진행: 10,000 건 발행  (21.2 msg/s)
2026-04-02 11:53:30,404 [INFO] 진행: 20,000 건 발행  (18.2 msg/s)
This post is licensed under CC BY 4.0 by the author.