wibaek.page

RDS에 대용량 데이터를 적재하다가 만난 진짜 병목: EBSByteBalance%와 EBSIOBalance%

대용량 데이터를 RDS에 한 번에 밀어 넣는 작업을 하다 보면, 생각보다 빨리 “이상하게 느려지는 순간”을 만난다. 처음에는 보통 CPU를 의심한다. 버스터블 인스턴스라면 CPUCreditBalance도 먼저 보게 된다. 메모리도 마찬가지다. FreeableMemory가 떨어졌다면 메모

대용량 데이터를 RDS에 한 번에 밀어 넣는 작업을 하다 보면, 생각보다 빨리 “이상하게 느려지는 순간”을 만난다.

처음에는 보통 CPU를 의심한다.

버스터블 인스턴스라면 CPUCreditBalance도 먼저 보게 된다. 메모리도 마찬가지다. FreeableMemory가 떨어졌다면 메모리 부족이나 스왑을 떠올리기 쉽다. 실제로 FreeableMemory는 RDS에서 사용 가능한 RAM 크기를 의미하고, Enhanced Monitoring에서는 swap 관련 OS 지표도 볼 수 있다. 다만 일반적인 RDS는 관리형 서비스라 OS에 직접 들어가서 swap을 켜고 끄는 식의 대응은 할 수 없다. (Amazon RDS CloudWatch metrics)

그런데 이번에는 아니었다.

CPUCreditBalance는 처음엔 수상해 보였지만, 뒤에서 RDS 재시작/복구 이벤트까지 같이 놓고 보니 “CPU를 자연스럽게 다 써서 소모됐다”기보다는 재시작 경계에서 그래프가 끊겨 보인 쪽에 가까웠다. RDS의 db.t4g 인스턴스는 Unlimited 모드로 동작한다. RDS CloudWatch 문서에는 DB 인스턴스가 멈추면 CPUCreditBalance가 유지되지 않는다고도 명시돼 있다. 그래서 재시작·복구 경계의 그래프만 보고 CPU가 자연스럽게 크레딧을 모두 소모했다고 단정하기는 어렵다. (DB instance classes, RDS CloudWatch metrics)

메모리도 결정적인 증거가 아니었다.

정작 끝까지 따라가 보니 진짜 문제는 스토리지 쪽, 그것도 EBSByteBalance%EBSIOBalance%였다.


처음엔 CPU나 메모리를 의심했는데, 왜 결론이 바뀌었나

대용량 적재 작업에서 가장 흔한 가설은 대체로 비슷하다.

  • CPU가 꽉 찼다
  • 메모리가 부족하다
  • 쿼리나 인덱스가 비효율적이다
  • 디스크 I/O가 밀린다

이번에도 초반에는 그 순서대로 의심했다.

특히 T 계열 인스턴스였기 때문에 CPU credit을 먼저 보게 됐다. 하지만 실제 장애 감각은 CPU가 꽉 찼을 때의 모습과는 조금 달랐다. CPU가 폭주해서 서버 전체가 버벅이는 느낌보다는, 대량 적재가 진행될수록 디스크가 점점 굼떠지는 느낌에 가까웠다.

그때 눈에 들어온 게 EBSByteBalance%EBSIOBalance%였다.


이번 병목의 핵심은 EBSByteBalance% / EBSIOBalance%였다

AWS 문서에서 EBSByteBalance%RDS 데이터베이스의 burst bucket에 남아 있는 throughput credits 비율, EBSIOBalance%남아 있는 I/O credits 비율로 설명한다. 그리고 둘 다 볼륨 하나만이 아니라 루트 볼륨을 포함한 전체 볼륨 기준으로 계산된다. (Amazon RDS CloudWatch metrics)

지표의 의미를 확인하고 나서야 해석의 방향이 바뀌었다.

이번 문제는 “gp3인데 왜 burst가 있지?”가 아니었다.

정황을 종합하면 gp3 볼륨 자체의 burst 문제가 아니라, DB 인스턴스가 EBS에 대해 가지는 burst bucket이 소진된 문제에 가까웠다.

대용량 적재가 인스턴스 측 burst 여유분을 소모했고, 잔액이 바닥난 뒤 처리량이 baseline 수준으로 제한되면서 체감 성능이 급격히 나빠졌다는 해석이 가장 자연스러웠다.


gp3를 쓰는데도 왜 이런 일이 생기나

여기서 많이 헷갈린다.

RDS의 gp3는 기본적으로 20~399 GiB 구간에서 3,000 IOPS / 125 MiB/s, 그리고 MySQL·MariaDB·PostgreSQL·Db2 계열은 400 GiB 이상부터 12,000 IOPS / 500 MiB/s를 기본 성능으로 제공한다. 즉 gp3는 “볼륨 크기를 키워야 성능이 따라오는 gp2식 모델”과는 결이 다르다. (Amazon RDS storage)

그렇다고 해서 “스토리지만 gp3면 이제 burst 걱정은 끝”이라고 보면 틀린다.

실제 처리량 상한은 대충 이렇게 생각해야 한다.

실효 최대 처리량 = min(스토리지 한도, 인스턴스 한도, 실제 DB/쿼리 병목)

예를 들어 스토리지 쪽 기본 throughput이 125 MiB/s인 구간이라면, 인스턴스가 순간적으로 그보다 높은 EBS 성능 ceiling을 갖고 있더라도 실제 지속 가능한 처리량은 먼저 스토리지 한도에 막힐 수 있다. 반대로 순간적으로 burst를 타다가 인스턴스 쪽 bucket이 먼저 고갈되면, 그때부터는 갑자기 속도가 꺾이는 그림이 나온다. (Amazon RDS storage)


그래서 적재 작업에서 왜 EBSByteBalance%가 먼저 중요했나

이번 케이스에서는 EBSByteBalance%가 더 강하게 떨어진 쪽으로 해석하는 게 자연스러웠다.

이건 보통 IOPS보다 throughput 쪽 bucket을 더 빨리 소모하고 있었다는 신호다. AWS의 대용량 블록 테스트에서도 EBSByteBalance%가 먼저 고갈되고, 작은 블록 테스트에서는 EBSIOBalance%가 더 빨리 떨어진다. (AWS Compute Blog)

즉 작은 랜덤 I/O가 엄청나게 많이 발생했다기보다, 다음 같은 패턴을 더 의심해볼 만하다.

  • 큰 범위 읽기/쓰기
  • dump / import
  • 배치성 적재
  • 복제나 백업에 가까운 트래픽
  • 큰 청크 단위의 데이터 이동

대용량 적재 작업은 겉으로는 “INSERT 많이 치는 작업”처럼 보여도, 실제로는 그 뒤에서 인덱스 갱신, WAL/binlog, flush, page write가 겹치면서 스토리지 throughput을 지속적으로 갉아먹는 작업이 되기 쉽다.

그래서 이런 상황에서는 CPUUtilization보다 EBSByteBalance%가 더 빨리 본질을 드러낸다.


T4g 계열의 EBS burst는 얼마나 지속될까

AWS의 EBS 최적화 인스턴스 표에는 T4g 계열의 baseline과 maximum throughput이 나와 있다. 예를 들어 t4g.micro는 baseline 10.88 MB/s, maximum 260.62 MB/s이고, t4g.large는 baseline 86.88 MB/s, maximum 347.50 MB/s다. 각주에는 이 인스턴스들이 최대 성능을 최소 30분 동안 유지한 뒤 baseline으로 돌아간다고 명시돼 있다. (AWS EBS-optimized instance types)

AWS가 2026년에 공개한 계산식은 다음과 같다.

burst credit pool = (maximum throughput - baseline throughput) × 1,800초

아래 표는 AWS 표의 MB/s 값을 사용하고, 결과를 GiB로 환산한 근사치다. 유휴 상태의 refill 시간은 credit pool ÷ baseline throughput으로 계산했다.

인스턴스 Baseline Maximum 풀 버킷의 추가 throughput 예산 완전 유휴 시 refill
t4g.nano 5.38 MB/s 260.62 MB/s 약 427.9 GiB 약 23시간 43분
t4g.micro 10.88 MB/s 260.62 MB/s 약 418.7 GiB 약 11시간 29분
t4g.small 21.75 MB/s 260.62 MB/s 약 400.4 GiB 약 5시간 29분
t4g.medium 43.38 MB/s 260.62 MB/s 약 364.2 GiB 약 2시간 30분
t4g.large / xlarge / 2xlarge 86.88 MB/s 347.50 MB/s 약 436.9 GiB 약 1시간 30분

여기서 두 가지를 조심해야 한다.

첫째, 이 값은 인스턴스 측 bucket의 이론적 용량이다. 실제 처리량은 스토리지 한도와 DB 병목에도 제한된다. 예를 들어 20~399 GiB gp3의 기본 throughput이 125 MiB/s라면, t4g.micro의 maximum 260.62 MB/s를 전부 사용하지 못하므로 버킷도 30분보다 천천히 소진될 수 있다.

둘째, refill 속도는 baseline 자체가 아니라 baseline - 현재 사용량이다. 완전히 유휴 상태일 때만 표의 refill 시간이 나온다. 배치를 작게 나눴더라도 평균 사용량이 baseline보다 높으면 bucket은 계속 줄어든다. (AWS burst calculation)

따라서 풀 버킷에서 시작했다면 maximum 성능으로도 최소 30분은 유지돼야 한다. 10~20분 만에 속도가 꺾였다면 작업 시작 전에 잔액이 이미 낮았거나, 루트 볼륨을 포함한 다른 I/O가 함께 bucket을 소비했는지 확인해야 한다.

결국 왜 CPU 병목처럼 보였나

이런 상황은 겉으로 보기엔 CPU 이슈처럼 보일 수 있다.

적재가 느려지고, 애플리케이션 쪽 타임아웃이 늘고, DB 응답이 뭉개지면 사람은 먼저 CPU나 쿼리 플랜을 본다. 거기에 T 계열이면 CPUCreditBalance 그래프까지 눈에 띄니 더 그쪽으로 끌려간다.

하지만 실제로는 CPU가 원인이라기보다, 스토리지 병목 때문에 DB 작업이 밀리고 그 결과 다른 지표도 나빠 보이는 경우가 있다.

이번 케이스가 딱 그랬다.

초반에는 CPU credit도 의심했고, 메모리도 봤다.

하지만 정황이 모이면서 더 자연스러운 결론은 이것이었다.

대용량 적재 작업 중 인스턴스 측 EBS burst bucket이 고갈됐고, 그 결과 처리량이 baseline 수준으로 주저앉았다.


이 지표만으로 원인을 확정하면 안 된다

EBSByteBalance%EBSIOBalance%는 루트 볼륨을 포함한 전체 볼륨 기준이다. 따라서 잔액이 낮다는 사실만으로 데이터 볼륨의 적재 작업이 원인이라고 단정할 수는 없다.

이번처럼 EBS burst 고갈을 원인으로 판단하려면 시간축에서 다음 신호가 함께 맞아야 한다.

  • 작업이 느려지기 직전에 EBSByteBalance% 또는 EBSIOBalance%가 0%에 도달한다.
  • 이후 ReadThroughput·WriteThroughput 또는 IOPS가 해당 인스턴스의 baseline 부근에서 제한된다.
  • ReadLatency·WriteLatencyDiskQueueDepth가 함께 상승한다.
  • 같은 시점에 CPU 포화나 메모리 고갈은 관찰되지 않는다.
  • RDS 이벤트 로그에서 재시작·복구·스토리지 변경 같은 별도 원인을 배제한다.

이 조건들이 맞아야 ‘balance가 낮았다’는 상관관계를 ‘EBS burst 고갈이 병목이었다’는 원인 판단으로 좁힐 수 있다.

그럼 어떻게 대응해야 하나

1. 평균 I/O를 baseline 아래로 제한한다

배치를 나누는 것만으로는 충분하지 않다. 청크 사이에 pause를 두거나 처리율을 제한해, 장시간 평균 I/O가 baseline 아래로 내려가야 bucket이 실제로 회복된다.

2. 적재 과정이 만드는 총 I/O를 줄인다

엔진이 지원한다면 일반 INSERT 반복 대신 COPYLOAD DATA 같은 bulk load 경로를 검토한다. 트랜잭션 크기, 인덱스 갱신, WAL/binlog, checkpoint와 flush 패턴도 함께 확인해야 한다. 인덱스를 나중에 만드는 방식은 부하를 줄일 수 있지만, 제약 조건과 서비스 가용성을 먼저 검토해야 한다.

3. 서비스 트래픽이 적은 시간에 실행한다

애플리케이션 트래픽과 적재 작업이 같은 EBS 예산을 두고 경쟁하지 않게 한다. 단, 백업·복제·루트 볼륨 I/O도 지표에 포함될 수 있으므로 작업만 옮겼다고 끝은 아니다.

4. EBS baseline을 기준으로 인스턴스 클래스를 고른다

‘더 큰 인스턴스’라는 이름만 보면 안 된다. 현재 AWS 표에서 t4g.large, t4g.xlarge, t4g.2xlarge의 EBS baseline과 maximum은 동일하다. 이 구간에서 CPU와 메모리만 올려도 EBS 병목은 그대로일 수 있다.

일회성 마이그레이션이라면 작업 동안만 EBS baseline이 충분한 DB 클래스로 변경한 뒤 원복하는 방법을 검토할 수 있다. 반복되는 워크로드라면 burst에 의존하지 않고 필요한 처리량을 지속할 수 있는 클래스를 선택하는 편이 안전하다.

5. 잔액과 지연 시간을 함께 알람으로 묶는다

EBSByteBalance%·EBSIOBalance%의 임계치만 보지 말고, DiskQueueDepth, 읽기·쓰기 지연 시간, throughput을 같은 대시보드에서 본다. 그래야 다음 적재 작업에서 속도가 꺾이기 전에 중단하거나 throttle할 수 있다.

정리

이번 이슈를 한 줄로 요약하면 이렇다.

RDS에 대용량 데이터를 적재하던 중 CPU나 메모리가 아니라, DB 인스턴스의 EBS throughput burst bucket이 먼저 고갈됐다.

T 계열 RDS에서 대량 적재나 마이그레이션을 할 때는 다음 지표를 같은 시간축에서 확인하는 게 좋다.

  • EBSByteBalance%EBSIOBalance%
  • ReadThroughputWriteThroughput
  • ReadLatencyWriteLatency
  • DiskQueueDepth
  • CPUUtilization, CPUCreditBalance, FreeableMemory
  • RDS 이벤트 로그
  • 적재 작업의 배치 크기와 처리율

이 글은 gp3 기준이다. gp2를 사용한다면 볼륨 자체의 I/O credit을 나타내는 BurstBalance도 별도로 확인해야 한다. EBSIOBalance%는 인스턴스 측 bucket, BurstBalance는 gp2 볼륨 측 bucket이므로 서로 다른 지표다.


참고 자료