엔지니어링

Dec 8, 2025

엔지니어링

Backend.AI Sokovan 오케스트레이터가 세션 스케줄링 문제를 해결한 방법

  • 김혁진

    김혁진

    CoreDev Lead

Dec 8, 2025

엔지니어링

Backend.AI Sokovan 오케스트레이터가 세션 스케줄링 문제를 해결한 방법

  • 김혁진

    김혁진

    CoreDev Lead

Backend.AI의 오케스트레이션 레이어 Sokovan이 세션 스케줄링 문제를 어떻게 해결했는지 공유합니다.

안녕하세요, 래블업 CoreDev팀의 리드이자 소프트웨어 엔지니어로 일하고 있는 김혁진입니다.

CoreDev팀에서는 Backend.AI를 구성하는 핵심 오케스트레이션 엔진 Sokovan을 비롯한 Backend.AI의 근간을 이루는 백엔드 구성요소들을 비롯한 핵심 시스템을 개발하고 있어요. 저희 팀의 목표는 더욱 안정적이고 신뢰 가능한 Backend.AI를 만들어 고객들의 안정적인 AI 워크로드 운영을 돕는 것입니다.

저희가 개발하고 있는 Backend.AI는 여러 노드에서 AI 연산 세션을 관리하는 오케스트레이션 플랫폼입니다. 사용자가 세션을 요청하면 적절한 서버를 고르고, 필요한 리소스를 배분한 뒤 컨테이너를 구성합니다. 이 위에 Model Serving이 올라가 vLLM, TGI 같은 서빙 프레임워크를 여러 노드에 분산 배치하고, 복제본을 늘리거나 줄이면서 들어오는 트래픽을 적절히 나눠 보냅니다.

초기 구현에서 AgentRegistry는 이 모든 일을 한 몸으로 떠안은 모놀리식 컴포넌트였습니다. 처음에는 큰 문제가 없어 보였지만, AI 워크로드가 복잡해지고 세션과 모델 서빙 시나리오가 다양해지면서 보완해야 하는 점들이 드러났습니다. Model Service와 일반 세션이 서로 다른 방식으로 세션을 만들고 관리하면서, 비슷한 장애가 반복돼도 원인과 재현 경로가 제각각인 상황이 자주 생기곤 했습니다. 컴포넌트에서 동작해도 설계상 큰 문제가 없었지만, AI 워크로드들이 고도화되고, 세션과 모델 서빙의 복잡성이 증가하면서 여러 개선해야 하는 부분들이 드러났습니다. 따라서 저희 CoreDev 개발팀은 Sokovan 오케스트레이터를 큰 폭으로 개선하기로 결정하였고, 지난 몇 달간 Sokovan 개선 프로젝트를 시작했습니다.

이번 글에서는 Sokovan 오케스트레이터가 세션 스케줄링을 효율적으로 운영할 수 있도록 만들기 위해 기술적인 문제를 해결한 방법에 대해 이야기해봅니다.

모놀리식 스케줄러 구조: 한계와 문제점

모놀리식 스케줄러 구조에서는 호출 스택이 끝없이 깊어졌습니다. Model Service가 세션 생성을 직접 호출하고, 그 안에서 또 다른 서비스를 직렬로 부르다 보니, 레이어 간 명확한 경계가 드러나지 않아 어느 단계에서 실패했는지 추적하기가 점점 어려워졌습니다. 여기에 세션 요청마다 무거운 스케줄링 로직을 즉시 실행하는 방식이라 트래픽이 몰리는 순간 서버 부하도 함께 치솟았고, 네트워크나 에이전트 장애가 한 번 끼어들면 세션이 pending 상태로 그대로 멈춰 버리는 일도 잦았습니다.

책임을 분리하고, 주기적으로 처리하는 Sokovan으로의 개선

Sokovan은 이러한 병목 문제와 불안정 현상을 해결하기 위해 책임을 잘게 나누고, 모든 무거운 일을 주기적으로 배치 처리하는 구조를 선택했습니다. 이러한 구조의 핵심은 Controller와 Coordinator의 역할 분리입니다. Controller는 외부에서 들어온 API 요청을 검증하고 DB에 상태를 마킹하는 것까지만 담당하고, 실제 스케줄링과 리소스 할당은 Coordinator와 그 아래 Scheduler/Engine이 주기적으로 모아서 처리하는 것이죠. 여기에 짧은 주기와 긴 주기를 함께 쓰는 듀얼 루프를 적용해, 이벤트가 있을 때는 빠르게 반응하면서도, 힌트를 놓치거나 Redis에 문제가 생겨도 일정 시간 안에 반드시 한 번은 처리되도록 했습니다.

아키텍처

이러한 시스템은 3개 계층으로 구성됩니다:

Coordinator의 주기적 처리:

┌─────────────────────────────────────────────────────────┐
│                  SokovanOrchestrator                    │
│                  (Top-level Coordinator)                │
│                                                         │
│     - Creates and manages 3 Coordinators                │
│     - Schedules periodic tasks                          │
│     - Provides common infrastructure (events, locks)    │
└─────────────────────┬───────────────────────────────────┘
                      │
        ┌─────────────┼─────────────┐
        ▼             ▼             ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│  Schedule   │ │ Deployment  │ │    Route    │
│ Coordinator │ │ Coordinator │ │ Coordinator │
│             │ │             │ │             │
│   Session   │ │ Deployment  │ │    Route    │
│  lifecycle  │ │  lifecycle  │ │ management  │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
       │               │               │
       ▼               ▼               ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│  Scheduler  │ │ Deployment  │ │    Route    │
│             │ │   Engine    │ │   Engine    │
│             │ │             │ │             │
│   Actual    │ │   Actual    │ │   Actual    │
│ scheduling  │ │ deployment  │ │  routing    │
│   logic     │ │   logic     │ │   logic     │
└─────────────┘ └─────────────┘ └─────────────┘

데이터 흐름:

시스템의 데이터 흐름은 Controller와 Coordinator로 분리됩니다.

Controller 흐름 (동기적 요청 처리):

Service Layer
    ↓
SessionSchedulingController.enqueue() / mark_terminate()
    ↓
1. 검증 (파라미터/세션 상태)
    ↓
2. DB 마킹 (PENDING/TERMINATING 상태로 저장)
    ↓
3. Redis 힌트 설정 (SCHEDULE/TERMINATE 이벤트)
    ↓
4. 즉시 응답 반환

Coordinator 흐름 (주기적 배치 처리):

Periodic Trigger (2초/60초)
    ↓
Redis 힌트 확인 (있으면 처리, 없으면 스킵 or 강제 실행)
    ↓
DB에서 마킹된 작업 조회 (PENDING/TERMINATING 세션들)
    ↓
Handler 실행 (스케줄링/종료 로직)
    ↓
다음 단계 힌트 설정 (post_process)
  • Orchestrator는 세 개의 Coordinator를 생성하고 주기 작업을 등록합니다. 비즈니스 로직은 포함하지 않습니다.
  • Coordinator는 "언제, 무엇을"만 결정합니다. 주기적으로 작업을 확인하고 적절한 Handler를 실행하며 다음 단계를 트리거합니다.
  • Controller/Engine에서 Controller는 검증과 마킹만 수행하고, Scheduler와 Engine은 실제 스케줄링 알고리즘과 리소스 할당을 처리합니다. Model Service는 Session Scheduling Controller를 재사용합니다. enqueue()와 mark_terminate() 인터페이스만 호출하면 되고, 내부 세션 생성/종료 로직은 신경 쓰지 않아도 됩니다. 이런 방식으로 Session Scheduler 기반의 Controller가 일반 세션과 Model Service를 일관되게 처리하며, 새로운 배포 타입 추가 시 기존 인프라를 재사용할 수 있습니다.

새로운 구조의 핵심: Mark-Execute 패턴

Controller와 Coordinator 분리의 핵심은 Mark-Execute 패턴입니다.

세션 요청마다 스케줄링을 실행하는 기존 방식

기존 AgentRegistry는 세션 요청마다 스케줄링을 즉시 실행했습니다. 검증 후 DB에 pending으로 저장하더라도 바로 스케줄링 함수를 호출하는 방식이었죠. 스케줄링이 distributed lock으로 보호되긴 하지만, 스케줄링 트리거가 10번 호출되면 순차적으로 10번 실행될 수밖에 없었고, 기존에 스케줄링된 세션도 다시 조회하고 처리하는 비효율이 발생했습니다. 스케줄링은 모든 에이전트의 리소스 상태를 조회하고 최적 배치를 계산하는 무거운 작업이기 때문에 요청 수에 비례하여 서버 부하가 증가했습니다. 만약 네트워크 장애 혹은 에이전트에 일시적인 장애가 생긴 경우라면 세션은 Pending 지옥에 빠졌고, 세션 종료 혹은 새로운 세션을 enqueue하는 등 다음 스케줄링이 시도되기 이전까지 'Stuck' 상태로 남아 있었죠.

주기적으로 스케줄링을 실행하는 Sokovan의 개선된 접근 방식

Sokovan은 스케줄링을 주기적 배치로 바꿨습니다. Controller는 검증 후 DB에 PENDING을 표시하고 Redis에 힌트만 남깁니다. 이후 Coordinator가 2초나 60초 주기로 힌트를 보고 pending 세션을 한 번에 처리하는 구조로 변경하여, 10개의 요청이 몰려도 스케줄링은 한 번만 돌고 실패하는 경우 다음 주기에 자동으로 재시도됩니다.

세션 종료도 똑같습니다. 종료 요청이 발현되면 즉시 에이전트와 통신하며 커널이 종료되던 기존 구조에서는 종료 요청이 발생하는 시점에 네트워크 문제가 발생하거나 에이전트가 느린 경우 API 응답이 오래 걸리는 이슈가 있었는데, 이를 Mark - Execute 패턴으로 변경하여 Sokovan은 마킹만 하고 즉시 응답하며, Coordinator가 주기적으로 마킹된 세션을 처리하도록 변경하였습니다. 네트워크 장애 등의 이유로 일시적으로 실행에 실패하도, 다음 주기에 자동으로 재시도되기 때문에 높은 복원력을 지니고, 요청 수가 증가하더라도 서버 부하를 일정하게 유지할 수 있습니다.

설계 고려사항

  • DB를 Single Source of Truth로 사용: DB에 세션 상태를 등록하고, 상태 머신 기반으로 주기적 동작에서 업데이트합니다. 스케줄링은 DB에 있는 값이 날아가지 않는 한 언제든 다시 시도할 수 있습니다. DB 트랜잭션을 통해 상태 변경의 원자성을 보장하며, 모든 세션 정보가 DB에서 관리됩니다.
  • Redis 힌트의 역할: Redis 힌트는 빠른 반응성을 위한 값입니다. 힌트가 있으면 빠른 주기의 이벤트가 발생했을 때 처리되지만, 힌트가 누락되거나 무시되더라도 Long-cycle 루프에서 처리를 보장합니다.

힌트를 기반으로 실행하는 듀얼 루프 구조 설계

그러면, Coordinator는 얼마나 자주 작업을 확인해야 할까요? 자주 확인하면 반응이 빨라지겠지만 그만큼 리소스가 낭비되고, 드물게 확인한다면 효율적인 리소스 사용이 가능하겠지만 반응이 느려질 것입니다. 이러한 문제를 해결하기 위해 저희 팀은 '듀얼 루프' 방식으로 Sokovan이 동작하도록 설계했습니다. Sokovan은 2개의 작업 타입을 지니는데, 2초짜리 Short-cycle과 60초짜리 Long-cycle이 그 예시입니다.

Short-cycle은 Redis 힌트 플래그를 확인합니다. Controller가 세션을 enqueue하거나 종료를 마킹하면 Redis에 힌트를 설정하고, Coordinator는 2초마다 이 힌트를 확인합니다. 힌트가 있으면 해당 작업을 처리하고, 없으면 아무것도 하지 않습니다. DB 접근 없이 Redis 조회 한 번으로 끝나므로 부하가 거의 없습니다.

Long-cycle은 힌트와 무관하게 무조건 실행됩니다. 60초마다 모든 pending 세션, 종료 대기 세션을 강제로 확인하고 처리합니다. 힌트가 누락되거나 Redis에 문제가 생겨도 60초 이내에는 반드시 처리됩니다.

이러한 듀얼 루프 구조를 통해 대부분의 작업은 이벤트 발생 후 평균 2초 이내에 처리되며, 힌트가 없으면 작업을 스킵하여 CPU와 DB 부하를 최소화합니다. 간혹 힌트를 놓쳐도 Long-cycle 타입이 놓친 힌트를 감지하여 60초 이내에는 반드시 처리할 수 있게 됩니다.

타임라인 예시:

0초    [Short] 힌트 없음 → 스킵
2초    [Short] 힌트 없음 → 스킵
3초    사용자가 세션 생성 요청 → Controller가 Redis에 힌트 설정
4초    [Short] 힌트 있음! → 스케줄링 처리
6초    [Short] 힌트 없음 → 스킵
...
60초   [Long] 힌트 무관 → 무조건 스케줄링 처리

Handler Registry

세션이나 Model Serving은 여러 상태를 거치며 진행됩니다:

세션: PENDING → SCHEDULED → PREPARING → CREATING → RUNNING → TERMINATING → TERMINATED
Model Serving: PENDING → PROVISIONING → RUNNING → DESTROYING → TERMINATED

Sokovan은 각 상태/작업 타입마다 독립적인 Handler를 만들고, Coordinator가 적절한 것을 선택해서 실행합니다.

Handler는 네 가지 책임을 가집니다: 메트릭을 위한 name(), 필요하면 분산 락을 위한 lock_id, 실제 작업을 수행하는 execute(), 그리고 후처리를 담당하는 post_process()입니다.

Coordinator는 schedule_type에 맞는 Handler를 선택하고, 필요하면 분산 락을 획득한 후 execute()를 호출합니다. 결과가 성공이면 post_process()를 통해 다음 단계를 트리거하거나 이벤트를 브로드캐스트합니다.

각 Handler는 하나의 작업만 수행합니다. 예를 들어 ScheduleSessionsHandler는 스케줄링만, CheckPreconditionHandler는 사전조건 확인만, CreateSessionsHandler는 세션 생성만 담당합니다.

Handler들은 Redis 힌트를 통해 체인으로 연결됩니다. ScheduleSessionsHandler가 스케줄링을 완료하면 post_process()에서 CHECK_PRECONDITION 힌트를 설정하고, CheckPreconditionHandler가 완료되면 PREPARE 힌트를 설정하는 식입니다. 이렇게 각 Handler가 성공적으로 작업을 완료하면 다음 단계의 힌트를 설정하여 전체 워크플로우가 빠르게 진행될 수 있습니다.

Sokovan의 지향점: 안정성과 복원력

Sokovan의 핵심은 무엇보다도 서버를 흔들리지 않게 만드는 것입니다. 초기에는 요청이 한꺼번에 몰리면 스케줄링이 실패하거나 지연되는 일이 있었지만, Sokovan의 지속적인 업데이트를 통해 외부 요청이 늘어나더라도 서버 부하가 일정한 선에서 유지되도록 개선되었습니다. 이를 위해 기본적으로 Sokovan은 모든 작업을 배치 처리하도록 동작합니다. 세션별로 따로 쏘던 이벤트 브로드캐스트를 한 번에 묶어서 보내고, 캐시 무효화도 영향받은 access_key를 모아 한 번에 처리하여 순간 트래픽 변화와 상관없이 처리 비용을 예측 가능하게 만들죠.

여기에 분산 락을 통해 동시성 문제를 해결합니다. Handler가 lock_id를 넘기면 Coordinator가 작업 전에 해당 락을 획득하고, 예를 들어 LOCKID_SOKOVAN_TARGET_PENDING은 PENDING 세션 처리에 사용되어 여러 Manager 프로세스가 동시에 같은 세션을 스케줄링하지 못하게 막는 것이죠.

실행 방식은 힌트 기반 듀얼 루프로 더 가벼워졌습니다. 이벤트가 없을 때는 가벼운 Redis 조회 한 번만 수행하고 DB는 건드리지 않아 불필요한 부하를 줄이고, 이벤트가 발생했을 때만 필요한 작업을 빠르게 태워 안정성과 효율을 함께 가져갑니다.

마치며

결과적으로, Sokovan은 Mark-Execute 패턴, 듀얼 루프, Handler Registry를 통해 안정성과 복원력을 크게 끌어올렸습니다.

Sokovan은 Mark-Execute 패턴, 듀얼 루프, Handler Registry를 통해 안정성과 복원력을 확보했습니다. 모든 작업을 배치와 분산 락으로 안전하게 처리해 다수의 매니저 프로세스가 동시에 같은 세션을 건드리는 상황을 막고, 이벤트가 없을 때는 가벼운 힌트 조회만 수행해 CPU와 DB 부하를 줄입니다. 시스템 규모가 커질수록 'Controller는 트리거, Coordinator는 조율, Handler는 실행'이라는 규칙이 개발자의 실수를 구조적으로 막아 주며, 테스트도 각 컴포넌트 단위로 명확히 나눠 작성할 수 있습니다.

규모가 작은 대부분의 경우에는 단순한 구조가 유리합니다. 어떤 패턴을 적용해야 할 지 확실한 결론이 내려지지 않은 상태라면, 코드를 유연하게 수정하며 요구사항을 빠르게 반영하는 것이 더 낫습니다. 그러나 시간이 지나 규모가 커지면서 반복되는 패턴이 식별되고, 이러한 반복이 명확해지는 시점에는 구조로 제약을 두는 쪽으로 수정하는 것이 좋습니다.

Controller/Coordinator/Handler와 같은 식으로 한 군데에서 수행하던 작업을 여러 개로 쪼개게 되면 코드 수정의 난이도는 그만큼 높아지지만, 잘못된 레이어에 기능이 구현되는 것을 막을 수 있습니다. Controller는 트리거만 담당하므로 누군가 여기에 무거운 스케줄링 로직을 넣으려 해도 구조상 불가능합니다. 이에 더해 서로간의 작업이 분리됨으로 인해 책임이 분리되고, 구조적으로 보장할 수 있는 부분이 늘어나는 장점도 있습니다. 동작에 대해 신경써야 하는 요소가 줄어들고, 테스트 작성도 쉬워집니다. 각 Handler를 독립적으로 테스트하고, Coordinator의 경우 호출 순서만 검증하면 됩니다.

이런 패턴은 Sokovan에만 국한된 것이 아니라, 트래픽과 상태 전이가 복잡해지는 대부분의 시스템에서 효과를 볼 수 있습니다. 여러분의 시스템 규모가 늘어나며 비슷한 문제를 겪고 있다면, 이러한 접근 방식을 통해 문제를 해결하는 힌트를 얻을 수 있을 것입니다.


참고

소스 코드: Backend.AI GitHub - Sokovan 아키텍처 문서: src/ai/backend/manager/sokovan/README.md

도움이 필요하신가요?

내용을 작성해 주시면 곧 연락 드리겠습니다.

문의하기

본사 및 HPC 연구소

KR Office: 서울특별시 강남구 선릉로 577 CR타워 8층 US Office: 3003 N First st, Suite 221, San Jose, CA 95134

© Lablup Inc. All rights reserved.

개인정보를 소중히 여깁니다

사용자 경험 향상, 사이트 트래픽 분석 및 방문자 동향 파악을 위해 쿠키를 사용합니다. "모두 수락"을 클릭하면 쿠키 사용에 동의하는 것입니다. 자세히 보기