Troubleshooting 2026.08.10 4 min read

rclone Google Drive RATE_LIMIT_EXCEEDED 해결기

이슈 발생: 2026-08-09 · 발행 기준: 2026-08-10 08:26 KST
환경: Kubernetes CronJob, restic, rclone, Google Drive

Kubernetes에서 Ghost 데이터를 restic으로 암호화한 뒤 rclone을 통해 Google Drive에 보내는 백업을 구성했다. 백업 Job은 재시도 끝에 성공하기도 했지만 로그에는 RATE_LIMIT_EXCEEDED가 반복됐다. 성공으로 끝났다는 사실만 보면 지나치기 쉬운 경고였다.

핵심 원인은 Google Drive 용량이 아니라 OAuth client와 API quota였다. rclone의 공용 OAuth client를 여러 사용자가 함께 쓰면 공유된 quota에 영향을 받을 수 있다. 전용 Google OAuth client를 만들어 rclone에 연결한 뒤 수동 백업과 정기 백업 모두에서 오류가 사라졌다.

백업 경로

Ghost content PVC ─┐
                   ├→ backup Job → restic 암호화 snapshot
MySQL dump ────────┘                 → rclone → Google Drive

여기서 restic과 rclone의 역할은 다르다.

  • restic: 파일 묶음, 암호화, 중복 제거, snapshot, 무결성 검사
  • rclone: Google Drive 같은 원격 저장소와의 전송 및 OAuth 인증

RATE_LIMIT_EXCEEDED는 대개 rclone 전송 계층에서 보인다. restic repository 손상과 같은 의미는 아니다.

먼저 확인한 것

kubectl -n backup get cronjob,job,pod
kubectl -n backup logs job/<job-name> --all-containers
kubectl -n backup logs job/<job-name> --all-containers | \
  grep -E 'RATE_LIMIT|quota|403|429|error'

다음 질문으로 범위를 좁혔다.

  1. HTTP 403인가 429인가?
  2. 오류 reason이 rateLimitExceeded, userRateLimitExceeded, dailyLimitExceeded 중 무엇인가?
  3. 재시도 후 snapshot은 실제로 생성됐는가?
  4. 같은 시간에 여러 Job 또는 서버가 같은 OAuth client로 대량 전송했는가?
  5. rclone이 자체 client ID가 아니라 기본 공용 client를 쓰고 있는가?

Google Drive 오류는 코드만 보지 말고 JSON 응답의 reasonmessage를 함께 봐야 한다. 원인에 따라 재시도, 지수 백오프, 요청량 축소, quota 확인, 전용 OAuth client 전환이 각각 다른 해법이 된다.

전용 OAuth client로 분리

Google Cloud Console에서 별도의 Desktop OAuth client를 만들고 Google Drive API를 활성화했다. 그다음 rclone remote를 다시 승인해 새 refresh token을 받았다.

rclone config

rclone 설정에는 대략 다음 항목이 들어간다. 실제 값은 절대 문서나 Git에 넣지 않는다.

[gdrive]
type = drive
client_id = <dedicated-client-id>
client_secret = <dedicated-client-secret>
scope = drive
token = <oauth-token-json>

Kubernetes에서는 이 파일을 Secret으로 주입하되 평문 출력, shell history, CI 로그에 노출되지 않게 해야 한다.

# 값 자체를 출력하지 않고 Secret 존재와 갱신 시각만 확인
kubectl -n backup get secret <rclone-secret-name>
kubectl -n backup describe secret <rclone-secret-name>

kubectl get secret -o yaml은 base64일 뿐 암호화된 비밀 표시가 아니다. 블로그, 메신저, CI 로그에 그대로 복사하면 안 된다.

재시도만으로 끝내면 안 되는 이유

rate limit은 일시적일 수 있으므로 지수 백오프는 필요하다. 하지만 공용 client quota가 구조적 원인이라면 재시도는 증상을 늦출 뿐이다. 특히 백업 시간이 길어지면 다음 CronJob과 겹쳐 요청량이 더 늘 수 있다.

spec:
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3

concurrencyPolicy: Forbid는 이전 백업이 끝나기 전에 새 Job이 겹치는 상황을 막는다. 다만 이것도 전용 OAuth client를 대신하지는 않는다.

수정 후 검증

전용 OAuth client와 새 token을 Kubernetes Secret에 반영하고 먼저 수동 Job을 실행했다. 이어 실제 CronJob 시각까지 기다려 정기 백업도 확인했다.

# 기존 CronJob을 기준으로 일회성 검증 Job 생성
kubectl -n backup create job --from=cronjob/<cronjob-name> <manual-job-name>
kubectl -n backup wait --for=condition=complete \
  job/<manual-job-name> --timeout=30m
kubectl -n backup logs job/<manual-job-name> --all-containers

검증 기준은 단순히 Job이 Complete인지가 아니었다.

  • 로그의 quota/auth 오류 0건
  • 새 restic snapshot 생성
  • restic snapshots에서 시간과 경로 확인
  • restic check 성공
  • 별도 복구 위치에서 실제 restore 성공
  • 다음 정기 CronJob도 같은 결과

백업은 업로드 성공이 아니라 복구 성공으로 완성된다.

재발 방지

  • rclone용 OAuth client를 다른 서비스와 분리한다.
  • refresh token, client secret, restic password를 각각 Secret으로 관리한다.
  • CronJob 중복 실행을 막고 실패 알림을 둔다.
  • 403/429와 Google API reason을 모니터링한다.
  • 정기적으로 restic check와 격리 복구 시험을 수행한다.
  • 비밀값이 로그에 노출되면 즉시 폐기하고 재발급한다.

참고 자료