← 기술 지식

기술 지식 / 컴퓨터 과학 / 동시성 / 비동기 실행·수명

동시 실행·대기열 제한

처리량이 포화된 DB 앞에서 실행 허가·대기 개수·바이트·기한을 따로 제한하고 거절 뒤 재시도까지 제어합니다.

동시성 · 원리·구현·운영
기술 지식 트리

동시성 제한은 빠르게 처리하는 장치가 아니라 하위 자원이 감당할 수 있는 작업만 실행 상태로 보내는 경계입니다. 요청을 수락한 뒤 어디에서 기다리는지, 어떤 단위로 메모리와 시간을 소비하는지, 기한이 지나면 어떻게 버리는지를 함께 정의해야 제한이 실제 보호 효과를 냅니다.

대기열 크기·DB 처리량과 거절 지연의 관계

DB 작업을 동시에 4개 실행하고 나머지는 큐에서 기다리는 서버를 생각해 보겠습니다. 작업 하나가 정확히 100ms 걸린다는 단순한 모형에서는 초당 약 40개를 끝냅니다. 초당 60개가 계속 들어오면 매초 20개씩 쌓입니다. 대기열을 100에서 1,000으로 늘려도 완료율은 그대로이고, 거절하기까지 기다리는 시간만 길어집니다.

실행 수는 하위 자원을 동시에 얼마나 사용하게 할지, 대기열은 아직 시작하지 않은 작업을 얼마나 보관할지 정합니다. 과부하에서 받을 요청을 고르는 진입 제어(admission control)와, 생산자에게 감속·대기를 전달하는 백프레셔(backpressure)는 함께 설계해야 합니다.

요청 대기 위치와 실행 위치 분리

대기와 실행의 별도 예산진입 제어: 기한과 저장 예산 확인, 초과 요청은 즉시 거절. 제한된 대기열: 아직 DB에 제출 안 함, 만료 요청 제거. 실행 허가 4개: 실제 DB 호출만 점유. 실제 완료 확인: 허가 반환 · 다음 작업수락자리 확보완료 또는 종료진입 제어기한과 저장 예산 확인초과 요청은 즉시 거절제한된 대기열아직 DB에 제출 안 함만료 요청 제거실행 허가 4개실제 DB 호출만 점유실제 완료 확인허가 반환 · 다음 작업
대기와 실행의 별도 예산 화살표는 수락된 요청의 이동입니다. 큐는 대기 개수·바이트·시간을 제한하고, 실행 허가는 실제 DB 작업이 끝날 때 반환합니다.
그림을 글로 읽기
  • 진입 제어: 기한과 저장 예산 확인 · 초과 요청은 즉시 거절
  • 제한된 대기열: 아직 DB에 제출 안 함 · 만료 요청 제거
  • 실행 허가 4개: 실제 DB 호출만 점유
  • 실제 완료 확인: 허가 반환 · 다음 작업
  1. 진입 제어 → 제한된 대기열: 수락
  2. 제한된 대기열 → 실행 허가 4개: 자리 확보
  3. 실행 허가 4개 → 실제 완료 확인: 완료 또는 종료

큐 앞에서 세마포어를 기다리는 코루틴을 무한히 만들면 큐가 다른 모양으로 옮겨간 것뿐입니다. 요청 본문·연결·콜백·스택도 메모리를 씁니다. 개수 상한뿐 아니라 요청 크기와 대기자 자체의 상한을 확인해야 합니다.

대기 비용 계산을 위한 시간표

시각 0에 네 작업이 막 시작했고, 큐에 여덟 작업이 있다고 합시다. 모든 작업이 100ms이고 FIFO이며 다른 지연이 없다는 가정입니다.

시각 실행을 시작하는 작업 새 요청의 상태
0ms 기존 실행 4개 큐의 아홉 번째로 들어감
100ms 기존 대기 1~4 계속 기다림
200ms 기존 대기 5~8 계속 기다림
300ms 새 요청 실행 시작
400ms 새 요청 완료 결과 반환 가능

남은 기한이 250ms라면 이 요청은 받아도 제때 끝날 수 없습니다. 실제 작업 시간은 고정이 아니므로 표의 계산은 하한·추정의 재료이지 지연 보장이 아닙니다. 긴 집계가 섞이면 같은 대기 개수에서도 결과가 크게 달라집니다.

안정 상태에서 평균 체류 개수 L = λW라는 리틀의 법칙을 사용할 수 있습니다. 초당 완료 40개, 대기와 실행을 합친 평균 체류 0.3초라면 평균 시스템 내 요청은 12개입니다. 이 값을 워커 12개라는 설정으로 바로 바꾸면 안 됩니다. 평균 관계는 p99를 보장하지 않고, 무한히 적체되는 비정상 상태에는 정상 상태 계산을 그대로 적용할 수 없습니다.

수락과 실행 허가 반환의 원자적 관리

다음 슈도코드는 하나의 소유 실행기가 큐와 실행 카운터를 직렬로 관리한다고 가정합니다. 멀티스레드 구현이면 같은 전이를 잠금 또는 원자 상태 변경으로 보호해야 합니다.

onRequest(job):
    if stopping or job.deadline <= monotonicNow():
        return rejected
    if queuedCount == MAX_QUEUED or job.bytes > MAX_BYTES - queuedBytes:
        return overloaded
    queue.push(job)
    queuedCount += 1
    queuedBytes += job.bytes
    dispatch()

dispatch():
    while active < MAX_ACTIVE and queue not empty:
        job = queue.pop()
        queuedCount -= 1
        queuedBytes -= job.bytes
        if job.deadline <= monotonicNow():
            finishWithoutExecution(job, expired)
            continue
        active += 1
        try:
            submit(job, onActualTermination)
        catch definitelyNotSubmitted:
            active -= 1
            finishWithoutExecution(job, submissionFailed)

onActualTermination(job, result):
    assert job.executionCompletionNotYetConsumed
    job.executionCompletionNotYetConsumed = false
    active -= 1
    recordActualResult(job, result)
    dispatch()

submit 안에서 완료 콜백이 즉시 호출되는 실행기를 생각해 보겠습니다. 호출 뒤에 작업 수를 늘리면 콜백이 먼저 허가를 반환해 카운터가 어긋날 수 있습니다. 따라서 호출 전에 실행 허가와 작업 상태를 등록합니다.

등록 순서만 맞춘다고 재진입 문제가 모두 해결되지는 않습니다. 완료 이벤트를 소유 실행기의 큐로 전달해 나중에 처리하거나, 즉시 콜백을 받아도 안전한 상태 전이를 구현해야 합니다. 제출 실패 처리와 완료 처리가 같은 작업의 허가를 각각 반환하지 않도록 종결 책임도 하나로 묶습니다.

실행 여부를 알 수 없는 오류는 확실히 제출되지 않은 경우와 다릅니다. 이때 허가를 곧바로 반환할 수 있는지는 실제 API의 완료·취소 계약으로 판단해야 합니다.

대기 중 만료 항목은 실행 자리가 날 때만 제거하지 않고 타이머나 취소 이벤트로도 회수해야 합니다. 그렇지 않으면 DB가 멈춘 동안 이미 끝난 요청의 본문이 큐를 차지합니다. 실행 중인 작업은 사용자의 대기 시간이 끝나도 실제 종료 전까지 허가를 유지합니다. 응답 종료와 실행 종료는 별도의 상태입니다.

요청 비용별 가중치 기반 제한

작은 조회 하나와 큰 보고서 하나가 같은 DB 비용을 쓰지는 않습니다. 작업 종류에 따라 허가 비용을 1·5처럼 정하고, 사용 중 가중치 합이 예산을 넘지 않게 할 수 있습니다. 가중치는 측정값에 기초한 근사이며 실제 메모리나 I/O 상한을 자동 보장하지 않습니다.

무거운 작업이 예산 전체를 기다리는 동안 작은 작업만 계속 들어오면 기아가 생길 수 있습니다. 최대 작업 비용이 전체 예산 이하인지 검사하고, 오래 기다린 작업에 예약 용량을 주거나 무거운 작업을 별도 풀로 분리하는 정책이 필요합니다. 순서대로 막으면 반대로 앞의 큰 작업 때문에 작은 작업이 지연되는 비용도 있습니다.

제한 보호하는 것 혼자서는 못 막는 것
요청률 초당 유입·호출 과금 느려진 요청의 동시 점유
실행 수 또는 가중치 동시에 쓰는 하위 자원 무한 대기자·재시도 폭주
큐 개수·바이트 대기 메모리 기다려도 기한을 못 지키는 요청
전체 기한 무의미한 오래된 작업 실제 취소 불가능한 외부 실행

가중 공정 선택과 실행 용량의 계층 분리

고객 A와 B의 큐를 공정하게 선택하는 것과 한 시점의 실행 비용 합을 10 이하로 제한하는 것은 다른 계층입니다. deficit round robin의 단순 모형에서는 각 활성 고객에게 round마다 quantum을 적립하고, 맨 앞 작업의 추정 비용을 지불할 credit이 있으면 선택합니다. quantum을 A=2, B=4로 두면 계속 backlog가 있는 두 고객에게 추정 비용 단위의 서비스 비율 1:2를 목표로 할 수 있습니다. 작업 개수의 비율이나 벽시계 deadline 보장은 아닙니다.

round A 앞 작업 비용 5·credit B 앞 작업 비용 2·credit 선택 예
1 2, 부족 4 B 두 작업 선택 가능
2 4, 부족 다시 4 B 두 작업 선택 가능
3 6, 충분 다시 4 A 한 작업 후 credit 1, B 두 작업

표는 실행 자리가 충분하고 두 고객이 계속 backlog인 가중 선택 모형입니다. 실제 dispatcher는 credit뿐 아니라 현재 사용 중 실행 가중치와 남은 기한도 검사합니다. 둘을 섞어 credit이 많으니 물리 메모리 상한을 넘겨 실행하면 안 됩니다. 비어 있는 고객의 credit을 무한 적립해 다음 burst가 전체를 점유하지 않게 활성화 시 reset·상한 정책을 둡니다.

visit active tenant in round-robin order:
    deficit[tenant] = min(MAX_CREDIT, deficit[tenant] + quantum[tenant])
    while head exists and head.estimatedCost <= deficit[tenant]:
        if head is expired: remove and report; continue
        if not fitsActualAdmissionBudget(head): break
        reserve execution capacity
        deficit[tenant] -= head.estimatedCost
        dispatch head

MAX_CREDIT은 허용할 최대 작업 비용 이상이어야 큰 작업이 언젠가 선택될 수 있습니다. 물리 용량이 작은 작업에 계속 사용되면 credit만 적립해도 큰 작업이 실행되지 못할 수 있습니다. 오래 기다린 큰 작업을 위한 예약·새 작은 작업 수락 보류·별도 풀을 추가해야 합니다. 예약은 순간 사용률을 낮출 수 있으며, 선행 작업이 끝나지 않거나 OS가 실행 기회를 주지 않는 상황까지 유한 완료를 보장하지는 않습니다.

추정 비용과 실제 관측 기반 보정

가중 허가의 비용을 메모리 MiB로 정했다면 CPU ms를 그대로 더해 같은 단위로 계산하지 않습니다. 공정 선택에는 추정 서비스 비용을, 실행 admission에는 메모리·동시 I/O·연결 등 필요한 별도 차원을 사용할 수 있습니다. 사용자가 신고한 낮은 비용을 그대로 신뢰하지 않고 서버가 작업 종류·입력 바이트·관측 분포로 추정합니다.

예를 들어 과거 대표 비용이 4이고 새 관측이 8이며 EWMA 계수 0.25를 선택했다면 다음 추정은 0.75×4 + 0.25×8 = 5입니다. 이는 예제이지 상한 증명이 아닙니다. 실제 비용/예측 비율과 tail을 기록하고 종류별 안전 여유·최솟값·최댓값을 정합니다. 실제 비용을 뒤에 측정했다면 다음 공정 credit에 차액을 반영할 수 있지만 이미 초과 사용한 메모리를 소급해서 안전하게 만들지는 못합니다. 물리 자원에는 독립 hard limit·취소·격리가 필요합니다.

검증은 작은 작업 지속 유입 속 큰 작업, 빈 고객의 재등장, 비용 과소 추정, 실행 중 취소·완료 중복을 포함합니다. 완료한 추정 비용의 고객별 비율·최장 대기·실제 자원 peak·거절률을 함께 봅니다. 공정성 검사는 요청 수뿐 아니라 선택한 비용 단위로 해야 합니다.

인스턴스 수·재시도와 하위 자원 전체 예산

인스턴스 하나에 실행 허가 20개를 주고 10개를 띄우면 DB에는 최대 200개가 들어갑니다. 자동 확장이 DB 용량을 늘리는 것은 아닙니다. 서비스별 예약 용량과 전체 하위 예산을 맞추어야 합니다.

거절된 클라이언트가 즉시 재시도하면 입구에서 더 큰 폭주가 생깁니다. 재시도 간격·지터·횟수·전체 기한을 안내하고, 클라이언트가 지키지 않아도 서버 내부 상한은 유지해야 합니다. 과부하 응답이 빨라졌다고 평균 지연만 보고 개선됐다고 판단하지 않습니다.

부하 회복 구간까지의 과부하 시험

정상 부하, 한 번의 버스트, 하위 작업이 10배 느려지는 상황을 각각 재현합니다. 입력률·수락률·실제 성공률·거절률·가장 오래된 대기 시간·활성 작업 수를 함께 기록합니다. 순간 active <= MAX_ACTIVE, queuedBytes <= MAX_BYTES가 유지되는지와 제출 실패·취소 뒤 카운터가 원래로 돌아오는지 검사합니다.

마지막에는 유입을 정상 수준으로 되돌립니다. 과부하 동안 쌓인 오래된 작업을 정리하고 지연이 회복되는지 확인해야 합니다. 유효한 요청을 더 많이 제때 끝내는 것이 목적이지, 거절을 없애거나 큐를 가득 채우는 것이 목적은 아닙니다.

예상 결과를 고정한 작은 시험도 유용합니다. MAX_ACTIVE=4, 작업 100ms, 초기 실행 4개와 대기 8개인 상태에서 새 요청의 기한을 250ms로 두면 새 요청은 실행 전에 만료되어야 하며 DB 호출 수는 늘지 않아야 합니다. 유입을 정상화한 뒤에는 queuedCount와 queuedBytes가 감소하고, 완료 콜백을 두 번 보내도 active가 음수가 되지 않는지를 확인합니다.

후속·관련 개념

함께 읽을 문서

전체 기술 지식
선택 연습 · 관련 질문 6개

질문은 이해를 확인하는 보조 자료입니다. 학습 경로와 문서의 범위는 질문 수와 독립적입니다.