ES /docs

QMAws::startTask | ECS failures - job_id: 106701, task_definition: nswgov-capture-3d-reconstruction-

RCA: QMAws::startTask | ECS failures - DRAINING

Overview#

What Happened#

2026-04-20 14:53:09 UTC에 cupixworks-any-compute-agent 서비스에서 ECS StartTask API 호출이 실패했다. job 106701 (3D reconstruction)이 container instance e729b7aa...에 배치를 시도했으나, ScaleIn Lambda가 해당 인스턴스를 거의 동시에(~37ms 차이) DRAINING 상태로 전환하여 ECS가 배치를 거부했다. 이후 job 106701은 24분 이상 재시도했으나 가용한 g6.4xlarge 인스턴스를 확보하지 못해 실행되지 않았다.

Quick Facts#

Field Value
exception.message QMAws::startTask | ECS failures - job_id: 106701, failures: [{"reason":"DRAINING"}]
top_frame qmAws.ts:196
env production, ap-southeast-2

Timeline#

  1. 14:43:09Z — ScaleIn Lambda가 e729b7aa... 포함 4개 container instance를 DRAINING으로 전환
  2. 14:43:12Z — ScaleIn이 e729b7aa...에서 running task를 발견하고 다시 ACTIVE로 전환
  3. 14:53:09.097Z — compute-agent가 e729b7aa...를 ACTIVE/idle로 조회 (job 106701)
  4. 14:53:09.134Z — ScaleIn Lambda가 e729b7aa...를 다시 DRAINING으로 전환 (~37ms 후)
  5. 14:53:09.262Z — ECS StartTask 호출 실패: {"reason":"DRAINING"}
  6. 14:53:14Z — EC2 인스턴스 i-0b5e9608f79f727e1 종료됨
  7. 14:54~15:17Z — job 106701이 ~16회 재시도하나 매번 task size: 0 (가용 인스턴스 없음)

Error Log#

Datadog Logs

text
QMAws::startTask | ECS failures - job_id: 106701, task_definition: nswgov-capture-3d-reconstruction-production, cluster: nswgov-cupixworks-ece-au, container_instance: arn:aws:ecs:ap-southeast-2:002596530511:container-instance/nswgov-cupixworks-ece-au/e729b7aaea62448ab5c0fdd8352994e1, failures: [{"arn":"arn:aws:ecs:ap-southeast-2:002596530511:container-instance/e729b7aaea62448ab5c0fdd8352994e1","reason":"DRAINING"}]

Impact#

  • Service: cupixworks-any-compute-agent
  • Team: sinsw
  • 발생 횟수: 1
  • 최초 발생: 2026-04-20T14:53:09.262Z
  • 최근 발생: 2026-04-20T14:53:09.262Z

DRAINING 에러 자체는 1건이지만, 후속 영향이 더 크다. job 106701 (create_capture_3d_reconstruction, jobable_id: 45397, user: gregory.smart7@det.nsw.edu.au)은 이후 24분 이상 재시도했으나 가용 g6.4xlarge 인스턴스를 확보하지 못해 계속 task size: 0으로 실패했다. 같은 시간대 다른 g6.4xlarge job들(106688, 106748, 106682, 106660, 106770)은 정상 배치되었으므로, 106701의 SQS 재시도 타이밍이 다른 job들과 경합하여 리소스를 확보하지 못한 것으로 보인다.

Root Cause Summary#

ScaleIn Lambda와 compute-agent 사이의 race condition이 직접적 원인이다. compute-agent가 listContainerInstances (ACTIVE 필터) → describeContainerInstances (status==ACTIVE 확인) → startTask 순서로 ECS API를 호출하는데, describeContainerInstances에서 ACTIVE로 확인된 후 startTask 호출까지의 ~165ms 윈도우 동안 ScaleIn Lambda가 해당 인스턴스를 DRAINING으로 전환했다. startTaskdata.failures로 에러를 반환하지만 코드는 이를 로그만 남기고 빈 배열로 resolve하여 재시도 없이 종료된다. 이후 SQS visibility timeout에 의한 재시도에서는 이미 인스턴스가 종료되어 가용 리소스가 0이 된 상태에서 반복 실패했다.

Technical Analysis#

Code Path#

  • Entry point: job.manager.ts:91runJob 메서드에서 job 처리 시작
  • Instance type 결정: job.manager.ts:103-104isCreateReconstruction이므로 AWS_ECS_GPU_RECONSTRUCTION_INSTANCE_TYPE (g6.4xlarge) 선택
  • ECS task 배치 시작: job.manager.ts:119qmAws.runTask() 호출
job.manager.ts:91-133typescript
private runJob = async (job: QMJob, _qmAws: QMAws) => {
    setLogMeta({ job: { id: job.id } });
    try {
        let done = false;
        let tasks: Array<AWS.ECS.Task> | undefined = [];
        // ...
        tasks = await _qmAws.runTask(Environment.AWS_ECS_CLUSTER_NAME, job, instance_type, workload);
        done = await job.updateTaskIdToJob(_qmAws, Environment.AWS_ECS_CLUSTER_NAME, tasks);
        if (done) {
            // ...
            await this.deleteMessageFromQueue(job, _qmAws);
        }
    } catch (error) {
        logger.warn('JobManager::runJob | error - %s', error);
        await this.deleteMessageFromQueue(job, _qmAws);  // 에러 시에도 SQS 메시지 삭제
    }
};
  • Container instance 조회: qmAws.ts:238-260listContainerInstances에서 status: 'ACTIVE' 필터로 조회
qmAws.ts:238-245typescript
private listContainerInstances = (awsClusterName: string, instanceType: string): Promise<Array<string>> => new Promise((resolve, reject) => {
    const params: AWS.ECS.ListContainerInstancesRequest = {
        cluster: awsClusterName,
        status: 'ACTIVE',
        filter: `attribute:ecs.instance-type == ${instanceType} and runningTasksCount == 0`,
        maxResults: 10
    };
  • Status 재확인: qmAws.ts:149-151describeContainerInstances 결과로 ACTIVE/idle 상태 확인
qmAws.ts:137-158typescript
for await (const containerInstance of containerInstances) {
    // ... resource 로깅 ...
    if (containerInstance.status == 'ACTIVE'
        && containerInstance.runningTasksCount == 0
        && containerInstance.pendingTasksCount == 0
    ) {
        try {
            tasks = await this.startTask(awsClusterName, containerInstance.containerInstanceArn as string, job);
            if (tasks.length > 0) break;
        } catch (error) {
            return;  // startTask reject 시 즉시 종료, 다른 인스턴스 시도 안 함
        }
    }
}
  • Failure point: qmAws.ts:196-203startTaskdata.failures를 로그하지만 빈 배열로 resolve
qmAws.ts:191-213typescript
this.ecs.startTask(params, (err, data) => {
    if (err) {
        logger.error('QMAws::startTask | end - %s', err.message);
        reject(err);
    } else {
        if (data.failures && data.failures.length > 0) {
            logger.error('QMAws::startTask | ECS failures - job_id: %d, ...',
                job.id, taskDefinition, awsClusterName, containerInstanceArn,
                JSON.stringify(data.failures.map(f => ({ arn: f.arn, reason: f.reason, detail: f.detail })))
            );
        }
        const tasks = data.tasks;
        if (tasks == undefined) {
            logger.info('QMAws::startTask | end - task empty');
            resolve([]);  // failures가 있어도 빈 배열로 resolve
        } else {
            resolve(tasks);
        }
    }
});

기대 동작: startTask가 DRAINING으로 실패하면 다른 container instance에서 재시도해야 한다. 실제 동작: failures를 로그만 남기고 resolve([])로 처리. runTask의 for 루프는 tasks.length > 0이 false이므로 다음 인스턴스로 넘어가지만, 이 경우 e729b7aa...가 유일한 가용 인스턴스였으므로 루프가 종료되고 task size: 0으로 반환된다. 이후 updateTaskIdToJob에서 done=false가 되어 SQS 메시지가 삭제되지 않고, visibility timeout 후 재시도된다.

Log Evidence#

ScaleIn Lambda의 DRAINING 전환과 compute-agent의 조회가 거의 동시에 발생한 것을 확인하는 Datadog 쿼리:

text
service:cupixworks-any-compute-agent ("e729b7aaea62448ab5c0fdd8352994e1" OR "ScaleIn") @environment:production

14:53:09.097Z — compute-agent가 인스턴스를 ACTIVE로 확인:

text
QMAws::runTask | job_id: 106701, container_instance: arn:aws:ecs:ap-southeast-2:002596530511:container-instance/nswgov-cupixworks-ece-au/e729b7aaea62448ab5c0fdd8352994e1, status: ACTIVE, available_memory_mib: 62019, available_cpu: 16384, running_tasks: 0, pending_tasks: 0

14:53:09.134Z — ScaleIn Lambda가 DRAINING 전환 (37ms 후):

text
ScaleIn::draining_container_instances | arns - ['.../e729b7aaea62448ab5c0fdd8352994e1']
ScaleIn::draining_container_instances | ids - ['i-0b5e9608f79f727e1']

14:53:09.262Z — ECS StartTask 실패:

text
QMAws::startTask | ECS failures - job_id: 106701, task_definition: nswgov-capture-3d-reconstruction-production, cluster: nswgov-cupixworks-ece-au, container_instance: arn:aws:ecs:ap-southeast-2:002596530511:container-instance/nswgov-cupixworks-ece-au/e729b7aaea62448ab5c0fdd8352994e1, failures: [{"arn":"arn:aws:ecs:ap-southeast-2:002596530511:container-instance/e729b7aaea62448ab5c0fdd8352994e1","reason":"DRAINING"}]

후속 재시도 패턴 (SQS visibility timeout 기반, ~90초 간격):

text
service:cupixworks-any-compute-agent "106701" "task" @environment:production
text
[14:54:46.974Z] QMAws::runTask | end - job id: 106701, task size: 0
[14:56:21.176Z] QMAws::runTask | end - job id: 106701, task size: 0
[14:57:52.459Z] QMAws::runTask | end - job id: 106701, task size: 0
...
[15:17:12.212Z] QMAws::runTask | end - job id: 106701, task size: 0

16회 이상 재시도했으나 모두 task size: 0. 같은 시간대 다른 g6.4xlarge job들은 다른 인스턴스에 정상 배치됨:

text
[14:55:17Z] job=106688 -> 7a16581dfe51 (g6.4xlarge) - SUCCESS
[14:57:01Z] job=106748 -> 890ad22ea729 (g6.4xlarge) - SUCCESS
[14:59:51Z] job=106682 -> b804ef7ca6e7 (g6.4xlarge) - SUCCESS

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 ScaleIn Lambda와 compute-agent 사이의 race condition으로 DRAINING 인스턴스에 task 배치 시도 14:53:09.097Z ACTIVE 조회 → 14:53:09.134Z DRAINING 전환 → 14:53:09.262Z StartTask 실패. 37ms 윈도우에서 상태 전환 발생. 코드에서 describeContainerInstancesstartTask 사이 별도 lock 없음 (qmAws.ts:149-154) Confirmed
H2 ECS 클러스터의 g6.4xlarge 인스턴스 부족으로 인한 배치 실패 106701이 DRAINING 에러 이후 16회 재시도에서 모두 task size: 0 같은 시간대 다른 g6.4xlarge job들(106688, 106748, 106682)이 정상 배치됨. 인스턴스 자체는 존재하나 106701의 재시도 타이밍에 이미 다른 job이 점유한 상태 Rejected (근본 원인 아님, 부차적 영향)
H3 startTask의 에러 핸들링 결함으로 DRAINING failure 시 다른 인스턴스 재시도 없이 종료 qmAws.ts:196-209: failures가 있어도 resolve([])로 처리. qmAws.ts:156-157: startTask reject 시 return으로 즉시 종료. ECS client에 maxRetries 미설정 (qmAws.ts:21) Confirmed (H1의 영향을 확대시킨 설계 결함)
H4 SQS 메시지 삭제로 job이 영구 소실됨 job.manager.ts:127-129: catch 블록에서 deleteMessageFromQueue 호출 이 케이스에서는 runTask가 throw하지 않고 빈 배열 반환 → updateTaskIdToJob에서 done=false → 메시지 삭제 안 됨 → SQS 재시도 발생. 로그에서 재시도 확인됨 Rejected (이 케이스에서는 해당 없음, 다만 다른 에러 경로에서는 메시지 소실 가능)

Fix Recommendation#

즉시 조치 (Critical)#

qmAws.ts:196-203: startTask에서 data.failures에 DRAINING reason이 포함된 경우, 해당 인스턴스를 제외하고 다른 가용 인스턴스에서 재시도하도록 처리해야 한다. 현재는 failures를 로그만 남기고 빈 배열로 resolve하므로, runTask의 for 루프가 다음 인스턴스로 넘어가긴 하지만, 가용 인스턴스가 하나뿐인 경우 바로 실패한다.

  • startTask에서 data.failures가 있고 data.tasks가 비어있는 경우를 명시적으로 구분하여 로그 레벨을 warn으로 낮추는 것을 검토. DRAINING은 인프라 스케일링의 정상적인 부분이며, 일시적 race condition은 error가 아닌 warn이 적합하다.

단기 개선 (1주 이내)#

  1. qmAws.ts:21: ECS client에 maxRetriesretryDelayOptions를 추가. base 패키지의 AwsEcsManager처럼 maxRetries: 5, retryDelayOptions: { base: 10000 }을 설정하여 AWS API 수준의 transient failure에 대한 복원력을 높인다.

  2. qmAws.ts:156-157: startTask가 reject되었을 때 return으로 즉시 종료하는 대신 continue로 다음 container instance를 시도하도록 변경. 현재는 하나의 인스턴스에서 실패하면 나머지 가용 인스턴스를 시도하지 않고 포기한다.

  3. qmAws.ts:117-168: runTask에서 containerInstanceArns.length >= 1이지만 모든 인스턴스에서 배치 실패한 경우 (task size: 0), 짧은 대기 후 한 번 더 listContainerInstances를 호출하여 새로 ACTIVE된 인스턴스가 있는지 확인하는 재시도 로직을 추가.

장기 개선 (재발 방지)#

  1. ScaleIn Lambda와 compute-agent 간의 조율 메커니즘 도입 검토. 현재 두 시스템이 독립적으로 ECS container instance 상태를 조회/변경하므로 race condition이 구조적으로 발생 가능하다. ScaleIn이 DRAINING 전환 전에 compute-agent에 알리거나, 혹은 compute-agent가 StartTask 실패 시 exponential backoff로 재시도하는 패턴이 필요하다.

  2. ECS StartTask 대신 RunTask 사용 검토. RunTask는 ECS 스케줄러가 자동으로 적합한 인스턴스를 선택하므로 DRAINING 인스턴스를 자동으로 회피한다. StartTask는 특정 container instance를 지정해야 하므로 race condition에 취약하다.

Monitoring#

  • DRAINING failure 발생 빈도 추적:
text
service:cupixworks-any-compute-agent "QMAws::startTask | ECS failures" "DRAINING" @environment:production
  • task size: 0으로 반복 실패하는 job 감지 (리소스 부족 또는 starvation):
text
service:cupixworks-any-compute-agent "QMAws::runTask | end - job id" "task size: 0" @environment:production

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard

1건 발생이며 다른 job들은 정상 배치되었으므로 즉각적 서비스 영향은 제한적이다. 다만 ScaleIn과의 race condition은 구조적 문제이므로, 클러스터 스케일링이 빈번한 시간대에 재발 가능성이 있다. job 106701의 최종 상태(성공 여부)는 로그 윈도우 내에서 확인되지 않았으며, SQS 재시도가 결국 성공했는지는 추가 확인이 필요하다.