Apache Flink와 RocksDB 튜닝으로 광고 Frequency Capping 실시간 집계를 7일까지 확장하기
핵심 내용
Apache Flink와 RocksDB 튜닝으로 광고 노출 집계를 1분~7일까지 실시간화했다.
자세히 보기
Frequency Capping은 사용자별 광고 노출 횟수를 정확히 세어 예산 낭비와 노출 기회 손실을 막는 핵심 메커니즘이다. 짧은 구간은 이미 Flink로 처리하고 있었지만, 장기 구간까지 실시간으로 확장하려면 집계 정확도와 운영 안정성을 함께 맞춰야 했다.
기존 구조는 Airflow DAG와 Redis 조회를 조합한 배치 기반 설계였다. 서빙 시점에 최대 4번의 Redis 조회로 7일 집계를 만들었고, 시간 단위로 절삭된 배치 특성상 이벤트 단위의 정밀한 슬라이딩 집계를 제공하기 어려웠다.
해결 방향은 State를 SSOT로 두고, 1분~7일 구간을 단일 Redis 조회로 제공하는 실시간 구조를 만드는 것이었다. 이를 위해 코드는 공유하되, 병목 패턴이 다른 minutes / hours / days 세 개의 Flink 앱으로 분리했다.
- minutes: 1분~30분 구간의 잦은 만료 처리로 Write Stall이 병목이었고,
managed500MB → 1200MB,WBR0.25 → 0.5로 늘려 해결했다. - hours: TTL이 길어져 State가 커지면서 Filter Block Cache Miss가 CPU를 잡아먹었고,
partitioned-index-filters=true,target-file-size-base=256MB,max-size-level-base=1GB,use-dynamic-size=true로 개선했다. - days: 7일 집계는 SST와 Savepoint 규모가 커져 checkpoint I/O가 병목이었고, 레벨 최적화와 대규모 memory 배분이 핵심이었다.
가장 까다로운 문제는 전환 정합성이었다. **백필(Backfill)**은 카운트만 올리고 타이머를 두지 않으며, **캐치업(Catch-up)**은 같은 이벤트를 다시 읽어 카운트와 만료 타이머를 함께 복원한다. 이 둘을 한 파이프라인에 넣으면 과거 데이터를 쌓는 도중 타이머가 먼저 발화해 값이 틀어지므로, 두 단계는 반드시 분리해야 했다.
전환 경계에서는 세 가지가 맞물려야 했다. Redis 쓰기 조건은 eventTime이 백필 종료 시점 이후여야 하고, withIdleness는 Bounded Source 종료 직전의 MAX_WATERMARK 누락을 막도록 60초로 잡았으며, timerState TTL은 슬라이딩 윈도우 만료보다 충분히 길게 유지해 장애나 재시작 시에도 감소 로직이 빠지지 않도록 했다.
운영 단계에서는 RocksDB 병목을 앱별로 따로 풀었다. minutes에서는 WBM 압박이 Flush와 Write Stall을 만들었고, hours에서는 Filter Block이 디스크에서 반복적으로 읽히며 CPU를 소모했으며, days에서는 레벨 구조 자체를 줄여야 했다. 같은 코드베이스라도 RocksDB 설정은 완전히 달라야 했던 이유다.
Direct I/O도 공통으로 적용했다. Kubernetes 환경에서 OS Page Cache는 노드 전체 메모리와 충돌할 수 있으므로, useOsPageCache=false와 DirectIoRocksDBOptionsFactory를 통해 Flink가 제어 가능한 Block Cache 중심으로 메모리를 운영했다.
이 한국어 요약은 AI가 자동으로 만들었습니다. 원문의 주장과 맥락은 원문에서 확인해 주세요. 저작권은 원저작자에게 있습니다.