batch_pull! error on batch arn:aws:ecs:us-west-2:002596530511:task/cupix-tesla-ece/f08cf04288c144f38
RCA: AwsTask.batch_pull! ECS DescribeTasks Rate exceeded
Overview#
What Happened#
cupixworks-worker의 AwsTask.batch_pull! cron job 이 AWS ECS DescribeTasks API 를 호출하는 과정에서 AWS Throttling(Rate exceeded) 예외가 반복적으로 발생. 2026-07-13 16:06 KST 부터 16:34 KST 까지 약 28분 동안 169건의 error 로그가 기록되었으며, 모든 로그는 동일 host (ip-10-1-17-34.ap-southeast-1.compute.internal) 의 whenever cron runner 에서 발생함.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | AWS ECS Throttling (rescued as StandardError — 원본 class 미기록) |
| exception.message | Rate exceeded |
| top_frame | app/models/aws_task.rb:87 |
| runtime | Ruby (Rails whenever cron runner) |
| deploy | production-ap-southeast-1-20260713T0734Z0-69260da5-cupixworks |
| env | production, ap-southeast-1 (worker) → us-west-2 (ECS API 대상) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-worker (AwsTask cron) | 169 | ECS task 상태 폴링 지연 — pending/blank 상태 record 가 최신화되지 못함 |
에러 자체가 사용자 트래픽을 직접 차단하지는 않지만, AwsTask.after_save 콜백을 통해 Job#run_task_stopped_callbacks 를 트리거하는 파이프라인이 지연될 수 있음.
Timeline#
- 2026-07-13 16:06 KST — 최초
batch_pull! ... Rate exceeded발생 (first_seen) - 2026-07-13 16:28–16:34 KST — 초당 여러 건 페이스로 반복 (169건 누적, last_seen 16:34 KST)
- 2026-07-13 16:58 KST — 동일 코드 경로에서 별개 오류 (
ecs:DescribeTasks not authorized,taskId length should be one of [32,36]) 관측 — 본 클러스터와 별도로 조사 필요 - Status board —
svc:cupixworks-worker::unknown인시던트2026-07-13-svc-cupixworks-worker--unknown-2에 포함, 다른 2개 cluster (8a242739...,1ad7f6fa...) 와 함께 그룹화됨
Error Log#
batch_pull! error on batch arn:aws:ecs:us-west-2:002596530511:task/cupix-tesla-ece/f08cf04288c144f384c0a517fc3ec322,arn:aws:ecs:us-west-2:002596530511:task/cupix-tesla-ece/522564c7ebb14f7a8ce1ba152e821400,arn:aws:ecs:us-west-2:002596530511:task/cupix-tesla-ece/ed4c0e4d621e47e89a0d92f933a76d10 - Rate exceeded
Impact#
- Service:
cupixworks-worker - 발생 횟수: 169
- 최초 발생: 2026-07-13 16:06 KST
- 최근 발생: 2026-07-13 16:34 KST
Root Cause Summary#
Cupix::Cron::AwsTask.pull_blank / pull_pending cron 이 2분마다 실행되면서 AwsTask.batch_pull! 를 통해 AWS ECS DescribeTasks API 를 호출하는데, blank scope 가 500 개 이상의 record 를 매번 반환하고 있어 (16:38 KST 로그에서 blank scope 에 15086 ~ 15611 등 526 IDs 관측) BATCH_SIZE = 100 슬라이스 기준으로 한 cron 실행당 6+ 회의 API 호출이 발생. pull_pending 등과 동시 실행되면서 AWS ECS DescribeTasks account/region-level throttling 한도를 초과, Rate exceeded throttling 예외가 반복 발생. 코드의 rescue StandardError 는 로그만 남기고 재시도/backoff 없이 다음 슬라이스로 진행하며, 예외 class 정보(Aws::ECS::Errors::ThrottlingException 등) 도 로그에 남기지 않음.
Technical Analysis#
Code Path#
- Entry point:
lib/cupix/cron/aws_task.rb:3(pull_blank— whenever every 2 minutes) - Batch call site:
app/models/aws_task.rb:54-59(ecs.describe_tasks) - Failure point:
app/models/aws_task.rb:86-88(rescue clause)
Cron 스케줄에서 pull_pending 와 pull_blank 가 2분마다 실행됨:
every 2.minutes do
# runner 'Cupix::Cron::AwsTask.pull_all'
runner 'Cupix::Cron::AwsTask.pull_pending'
runner 'Cupix::Cron::AwsTask.pull_blank'
runner 'Cupix::Cron::CopyRequest.run_waiting_copy_reqeusts'
runner 'Cupix::Cron::SiteinsightsService.publish_polling_events_by_schedule'
end
blank scope 는 last_status, desired_status 가 모두 nil 인 record 를 반환:
scope :blank, -> { where(archived_at: nil).where(last_status: nil, desired_status: nil) }
각 cron 실행마다 batch_pull! 로 넘어가 100개 단위로 describe_tasks 를 반복 호출:
def self.batch_pull!(tasks)
tasks = tasks.to_a
return if tasks.empty?
ecs = tasks.first.ecs_client
cluster_name = $AWS[:ecs][:cluster_name]
task_ids = tasks.filter_map(&:task_id)
tasks_by_task_id = tasks.index_by(&:task_id)
task_ids.each_slice(BATCH_SIZE) do |batch_ids|
resp = ecs.describe_tasks({
tasks: batch_ids,
cluster: cluster_name,
include: ['TAGS']
})
# ... 응답 처리 ...
rescue StandardError => e
Cupix::Logger.error("batch_pull! error on batch #{batch_ids.first(3).join(',')} - #{e.message}", class: name, function: __method__)
end
end
ecs_client 는 매 record 마다 새 Aws::ECS::Client 인스턴스를 생성하며, region 은 환경변수로부터 결정:
def ecs_client
Aws::ECS::Client.new region: ENV['CUPIX_REGION'] || 'us-west-2'
end
기대 동작 vs 실제 동작:
- 기대:
blank/pendingscope 크기가 소량으로 유지되며 API 호출이 산발적으로 발생 - 실제:
blankscope 에 500+ record 누적, 2분마다 6+ 회의DescribeTasks호출, 여기에pull_pending,pull_running(일 1회),after_commit :pull(task 생성 시마다 개별 호출) 이 겹치면서 AWS ECSDescribeTasks의 계정/region 단위 throttle 을 초과. AWS SDK 의 default retry (retry_limit: 3, exponential backoff) 이후에도 실패하는 케이스가 rescue 로 흡수됨
Log Evidence#
Datadog 쿼리:
service:cupixworks-worker "batch_pull!" "Rate exceeded"
핵심 로그 (raw):
{
"timestamp": "2026-07-13T07:34:22.712Z",
"status": "error",
"class": "AwsTask",
"function": "batch_pull!",
"message": "batch_pull! error on batch arn:aws:ecs:us-west-2:002596530511:task/cupix-tesla-ece/f08cf04288c144f384c0a517fc3ec322,... - Rate exceeded",
"host": "ip-10-1-17-34.ap-southeast-1.compute.internal",
"region": "ap-southeast-1",
"environment": "production",
"version": "production-ap-southeast-1-20260713T0734Z0-69260da5-cupixworks"
}
blank scope 크기가 500+ 임을 보여주는 info 로그 (첫 사고 시각 이후 첫 관측치):
Task pulling with blank scope : [15086, 15087, 15088, ... 15611] (526 IDs)
service: cupixworks-worker, class: Cupix::Cron::AwsTask, function: pull_blank
2026-07-13T07:38:21Z
Datadog 쿼리:
service:cupixworks-worker "Task pulling"
에러 발생 빈도 (동일 host, 동일 함수): 16:28:52 ~ 16:34:22 KST 사이 약 5분 30초 동안 최소 10건이 2초 간격으로 발생 — each_slice 반복 내에서 슬라이스마다 throttle 됨을 시사.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | blank scope record 누적 + BATCH_SIZE=100 + 2분 cron 으로 인해 AWS ECS DescribeTasks throttle 초과 |
16:38 KST info 로그에서 blank scope 에 526 IDs 관측; each_slice(BATCH_SIZE) 반복 구조 (app/models/aws_task.rb:54); 에러 메시지가 정확히 AWS Throttling 시그니처 Rate exceeded |
— | Confirmed |
| H2 | 외부 AWS 리전 장애 | — | Status board 조회 결과 dep:* 인시던트 없음 (svc:cupixworks-worker::unknown-2 만 존재); AWS us-west-2 관련 dep 인시던트 미검출 |
Rejected |
| H3 | IAM 권한 문제 (not authorized) 가 root cause |
동일 코드 경로에서 16:58 KST 부터 not authorized to perform: ecs:DescribeTasks 로그 관측 |
시간대가 본 클러스터 범위 (16:06–16:34 KST) 이후이며, 에러 메시지 시그니처가 다름 (Rate exceeded ≠ not authorized); 별개 사건으로 판단 |
Rejected (별개 이슈 — 별도 조사 필요) |
| H4 | after_commit :pull 개별 콜백 폭주 |
app/models/aws_task.rb:29 에서 create 시마다 개별 describe_tasks 호출 발생 가능 |
발생 로그가 batch_pull! 시그니처만 있고 개별 Task fetched per-record 에러 시그니처 없음; 개별 호출 실패는 pull 의 rescue 로 흡수되어 로그 없음 |
Inconclusive (기여 가능성 있음) |
Fix Recommendation#
즉시 조치 (Critical)#
-
app/models/aws_task.rb:86-88rescue 세분화 및 재시도 추가Aws::ECS::Errors::ThrottlingException(그리고 SDK 가 최종 반환하는Aws::Errors::ServiceError하위 throttling 계열) 을 별도로 rescue 해 warn 레벨로 다운그레이드하고, exponential backoff + jitter 로 슬라이스 단위 재시도 수행rescue StandardError는 남겨두되 원본 exception class 를 함께 로깅 (e.class.name) — 현재는e.message만 남겨 후속 분석이 어려움- AWS SDK 의 기본 retry 설정 (
retry_limit,retry_base_delay) 이 이 케이스에 대해 충분한지 검토 —Aws.config.update(config/environments/production.rb:129-135) 에retry_limit,retry_mode: 'adaptive'명시적 설정 고려
-
config/schedule.rb:29-35cron 빈도 조정pull_blank를 별도 슬롯으로 분리하고 실행 간격을 5분 이상으로 늘려pull_pending과 동일 분에 겹치지 않도록 함- 또는
pull_blank는 record 개수가 임계값 (예: 200개) 을 초과할 때만 실행하도록 guard 추가
단기 개선 (1주 이내)#
blankscope 누적 원인 조사blankscope 는last_status와desired_status가 모두 nil 인 record — 정상 흐름에서는 create 직후after_commit :pull로 상태가 채워져야 하지만 526 개가 남아있는 것은pull!실패 record 가 archive/cleanup 되지 않고 축적됨을 의미AwsTask#pull(app/models/aws_task.rb:100-106) 이 실패시 조용히false반환하고 record 를 방치 — 실패 이력 추적 필드 추가 (예:pull_failed_at,pull_attempts) 또는 일정 시간 이상 blank 상태인 record 자동 archive
ecs_client인스턴스 캐싱batch_pull!내부에서tasks.first.ecs_client로 한 번만 생성되므로 문제 없음; 다만 개별after_commit :pull경로는 record 마다 새Aws::ECS::Client인스턴스를 생성 (app/models/aws_task.rb:91-93) — thread-local / process-level 캐싱 고려
장기 개선 (재발 방지)#
- 폴링 → 이벤트 기반 전환: AWS ECS Task state change 는 EventBridge event 로 push 가능. cron polling 대신 EventBridge → SQS/Lambda 로 상태 반영 시 API 호출을 근본적으로 줄일 수 있음
- Cross-region 호출 감시: 로그의 host region (
ap-southeast-1) 과 ECS ARN region (us-west-2) 이 상이 — 의도된 구성인지 확인 필요; 아니라면 region 별 workload 분리 - Throttling 관측성: AWS ECS API 호출 count 및 throttling rate 를 Datadog 대시보드에 노출해 임계값 초과 전 알림
Monitoring#
- 추가할 메트릭/알림
sum:trace.aws.ecs.errors{service:cupixworks-worker,error_type:throttling}.as_count()
logs("service:cupixworks-worker @class:AwsTask @function:batch_pull! \"Rate exceeded\"").index("*").rollup("count").by("host").last("15m")
logs("service:cupixworks-worker @class:Cupix::Cron::AwsTask @function:pull_blank \"Task pulling with blank scope\"").index("*").rollup("count").last("30m")
Note: 위 log-based query 들은 dashboard timeseries widget 에서 사용 가능한 형식. blank scope 실행 로그 카운트가 record 크기와 함께 급증할 때 알림.
Risk Assessment#
- Risk level: medium (사용자 트래픽 직접 영향 없음, 그러나 task 상태 최신화 지연으로
Job#run_task_stopped_callbacks지연 발생 가능) - 예상 복잡도: standard (rescue 세분화 + backoff 로직 추가는 표준적 변경, cron 간격 조정도 low-risk. blank scope cleanup 은 신중 필요)