증분 Sequence 기반의 대용량 로그 테이블 ETL 경험기

“로그 테이블 created_at에 인덱스 하나만 걸어주시면 안 될까요?”
“테이블 사이즈가 수십 TB인데 인덱스만 TB 단위입니다. 안 됩니다.”

데이터 엔지니어로서 대용량 로그성 데이터를 다루다 보면 필연적으로 마주치는 상황이다. UPDATE 없이 쌓이기만 하는(Append-only) 로그 테이블인데, 너무 거대해서 날짜 컬럼(created_at)에 인덱스조차 걸 수 없는 상황.
이 글은 그런 척박한 환경에서 Primary Key(Sequence) 하나만 믿고 고속으로 데이터를 퍼 날랐던 전략과, 그 과정에서 마주친 클러스터 시퀀스(Cluster Sequence)의 함정에 대한 기록이다.

1. 문제: “어제치 로그 가져오는데 DB가 뻗어요”


상황은 이랬다.

  • 대상: 수십억 건이 쌓여있는 대용량 로그 테이블 (History, Audit Log 등).
  • 특징: INSERT만 발생. updated_at 컬럼 없음. 오직 created_at만 존재.
  • 목표: 매일 새벽, “어제 00:00 ~ 오늘 00:00” 사이의 로그를 추출하여 DW에 적재.
SELECT * FROM huge_log_table 
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02'

아주 단순한 쿼리지만, created_at에 인덱스가 없으니 DB는 Full Table Scan을 시작한다. I/O 부하가 폭발하고, 다른 서비스 쿼리들이 줄줄이 대기(Wait) 상태에 빠진다.

DBA에게 인덱스 생성을 요청했지만, “인덱스 생성 자체가 엄청난 부하이고, 스토리지 비용도 감당 안 된다”며 반려되었다. 맞는 말이라 반박할 수 없었다.

2. 해결: “Sequence Index를 날짜처럼 쓰자”


인덱스를 새로 만들 수 없다면, 이미 있는 인덱스를 써야 한다.
대부분의 테이블은 AUTO_INCREMENT 속성의 숫자형 PK를 가진다. 그리고 이 PK는 Clustered Index로 잡혀 있어 조회 성능이 가장 빠르다.

“만약 1월 1일 00시에 생성된 첫 번째 로그의 PK를 알고, 1월 2일 00시의 첫 PK를 안다면?”

우리는 created_at 범위 검색을 PK 범위 검색으로 치환할 수 있다.

-- Full Scan (Bad)
WHERE created_at BETWEEN '2024-01-01' AND '2024-01-02'

-- Index Range Scan (Good)
WHERE id BETWEEN 1000000 AND 2000000

이 방식을 도입하자 수시간 걸리던 추출 작업이 분 단위로 단축되었고, DB 부하도 획기적으로 줄어들었다.

3. 심화: 맨땅에 헤딩하기 (초기 적재와 메타데이터 구축)


문제는 “처음(Initial Load)”이다. 매일매일 적재할 때는 전날 데이터를 참고하면 되지만, 수년 치 데이터가 이미 쌓여 있는 상태에서 “2021년 1월 1일의 시작 PK가 뭐였지?”를 어떻게 알 수 있을까?

이때 사용한 전략이 Sequence Chunking & Staging이다. Spark로 무겁게 계산하는 대신, 일단 Redshift Staging 영역으로 퍼다 나른 뒤 SQL로 분석하는 방식을 택했다.

🏗️ 메타데이터 구축 프로세스 (Reverse Engineering)

[DB Table (Huge Logs)]
ID: 1 ......................................... 100억
    |Chunk 1|Chunk 2|Chunk 3| ... |Chunk N|
    (1~100만) (100만~200만) ...

       ⬇️ (Parallel Spark Extract by ID Range)

[Redshift Staging Area (Temp Table)]
* 일단 다 때려 박는다. (COPY)
* Spark에서 뭔가 하려고 하면 느리다. DW의 파워를 빌리자.

       ⬇️ (SQL Aggregation)

[Metadata Table]
| target_date | min_id    | max_id    | status |
|-------------|-----------|-----------|--------|
| 2021-01-01  | 1         | 1,540,231 | DONE   |
| 2021-01-02  | 1,540,232 | 3,100,500 | DONE   |
...
  1. Chunking: 전체 ID 범위를 적절한 크기(예: 100만 건)로 나눈다. 이때 청크 사이즈는 RDBMS가 감당 가능한(부하 없는) 수준으로 최적화해야 한다.
  2. Load: Spark가 각 청크를 읽어 Redshift Staging 테이블에 적재한다.
  3. Analyze: Redshift에서 GROUP BY TRUNC(created_at) 쿼리를 돌려 날짜별 MIN/MAX(id)를 추출하고 메타 테이블을 완성한다.

4. 함정: “클러스터 시퀀스는 시간을 배신한다”


하지만 데이터를 검증하던 중 심각한 문제를 발견했다.
소스 DB가 Oracle RAC (또는 Aurora) 같은 클러스터 환경이었는데, 여기서 Sequence GapOut of Order 문제가 발생한 것이다.

💡 토막 상식: Cluster Sequence (RAC/Aurora)

클러스터 DB는 성능 최적화를 위해 각 노드(Instance)별로 시퀀스를 캐싱(Cache)한다.

  • Node A: 1~100번 미리 할당
  • Node B: 101~200번 미리 할당

만약 09:00:00에 Node B에서 요청이 들어오면 PK 101이 발급되고,
09:00:01에 Node A에서 요청이 들어오면 PK 1이 발급된다.

즉, 더 늦게 생성된 데이터(Node A)가 더 작은 PK 값을 가질 수 있다. (PK 순서 ≠ 시간 순서)

시나리오: 이어달리기(Incremental Cursor)의 실패

이런 상황에서 만약 “어제 10000번까지 가져왔으니, 오늘은 10001번부터 가져와야지”라고 단순하게 생각하면 망한다.
중간에 비어있던 번호나, 다른 노드에서 늦게 발급된 작은 번호들이 영원히 누락될 수 있기 때문이다.

5. 보완책: 매일 새로 찾고, 넉넉하게 퍼서, 메모리에서 바른다


결국 “마지막으로 읽은 ID + 1” 방식은 클러스터 환경에서 사용할 수 없다.
조금 비효율적이어도 매일매일 그 날짜의 Min/Max ID를 독립적으로 새로 찾아야 한다.

전략: Independent Scan & In-Memory Filtering

  1. Boundary Scan: 매일 자정, 메타데이터 구축 로직과 유사하게 created_at 기준의 Min/Max ID를 새로 조회한다.
  2. Overlap: 어제와 오늘의 ID 스캔 범위가 상당 부분 겹치게(Overlap) 된다.
  3. Filter: 중복 스캔의 비효율은 있지만 Full Scan보단 낫다. 일단 가져온 뒤 Spark 메모리 상에서 created_at으로 정확하게 필터링하여 중복을 제거한다.
# 1. DB: Index Range Scan (범위가 겹치더라도 일단 인덱스 타고 가져옴)
df = spark.read.jdbc(
    query="SELECT * FROM table WHERE id BETWEEN Today_Min_ID AND Today_Max_Max"
)

# 2. Spark: In-Memory Filter (정확한 날짜로 커팅)
df = df.filter(col("created_at").between("start_date", "end_date"))

6. 결론 및 한계


“Sequence는 시간 순서와 일치하지 않는다.”
특히 클러스터 DB 환경에서는 이 명제가 더욱 확실해진다.

PK를 활용해 인덱스를 타게 만드는 것은 훌륭한 전략이지만, 명확한 한계점도 존재한다.

⚠️ 한계점 (Limitations)

  • 이 전략은 UPDATE가 발생하지 않는 Append-only (Log) 테이블에서만 유효하다.
  • 데이터의 상태가 변해서 updated_at이 갱신되는 트랜잭션 테이블인데 인덱스마저 없다? 그건 Sequence로도 해결할 수 없다. (CDC 도입이나 Full Scan 외엔 답이 없다.)

기술적 제약(인덱스 부재)을 우회하려다 만난 또 다른 제약(클러스터 시퀀스). 이를 해결해 나가는 과정이야말로 엔지니어링의 묘미가 아닐까.


Posted

in

, ,

by

댓글 남기기

이 사이트는 Akismet을 사용하여 스팸을 줄입니다. 댓글 데이터가 어떻게 처리되는지 알아보세요.