Skip to content

redis‐queue를 이용한 컨테이너 스케줄링

mjh000526 edited this page Dec 1, 2024 · 1 revision

스케줄링이 필요한 이유

기존에는 사용자의 요청마다 컨테이너를 생성-실행-반납하는 방식으로 구현했습니다.

이 방식은 트래픽이 몰렸을때 1:1로 컨테이너를 만들기 때문에 서버의 리소스에 상당한 무리가 갈 것이라 생각했습니다. basesch

스케줄링 적용

container pool

먼저 컨테이너 pool을 만들어서 컨테이너의 생성과 유저의 요청간에 의존성을 제거하였습니다.

⇒무제한으로 container생성을 방지하여 서버 리소스를 직접 통제

초기컨테이너생성

  • 프로젝트가 시작할 때 정해진 갯수만큼 컨테이너를 생성
export class DockerContainerPool implements OnApplicationBootstrap {
  pool: Container[] = [];
  async onApplicationBootstrap() {
    await this.createContainer();
  }

  async createContainer() {
    for (let i = 0; i < MAX_CONTAINER_CNT; i++) {
	    //컨테이너 생성
      this.pool.push(container);
    }
  }
}
  • queue의 작업을 처리할때 사용가능한 컨테이너를 할당
  async getContainer(): Container | null {
    while (this.lock || this.pool.length === 0) {
      await this.delay(10); // 풀 비어 있음 처리
    }
    this.lock = true;
    const container = this.pool.pop();
    this.lock = false;
    return container;
  }
⚠️ 큐의 동시성 이슈로 뮤텍스락을 사용하지 않으면 2개의 작업이 하나의 컨테이너를 동시에 할당받음
  • 사용한 컨테이너 반납
async returnContainer(container: Container) {
    await container.stop();
    this.pool.push(container);
}

job-queue

한정되어있는 컨테이너들을 각 요청에 맞게 실행-반납을 반복하는 과정에서 요청이 무시되지 않도록 해야했습니다.

job-queue는 직접 구현하지않고,redis의 queue를 이용해 구현하였습니다.

redis-bull config

@Module({
  imports: [
    BullModule.forRoot({
      redis: {
        host: 'localhost',
        port: 6379,
      },
    }),
  ],
})
export class AppModule {}

[nestjs bull 공식문서] https://docs.nestjs.com/techniques/queues#bull-installation

redis-queue 등록 및 사용

원하는 이름으로 queue를 redis에 등록

@Module({
  imports: [
    GistModule,
    BullModule.registerQueue({
      name: 'docker-queue' // 큐 이름
    })
  ],
  controllers: [DockerController],
  providers: [DockerProducer, DockerConsumer, DockerContainerPool],
  exports: [DockerProducer]
})
export class DockerModule {}

redis-queue producer

producer는 queue에 작업을 처리하는 레이어입니다.

  • 이전에 등록한 queue의존성 주입

    //이전에 등록한 queue 주입
    constructor(
        @InjectQueue('docker-queue')
        private readonly dockerQueue,
        private dockerContainerPool: DockerContainerPool
      ) {}
  • 사용자의 요청을 파라미터와 함께 queue에 저장

    const job = await this.dockerQueue.add(
          'jobName',// queue안에서 작업을 처리할 key값
          {파라미터 object},//consumer에 전달할 파라미터
          //본 프로젝트에서는 queue의 기록이 redis에 전달될 필요가 없다고 생각하여 
          //log저장은 하지않도록 설정 => nestjs에서 예외로그로써 파악
    			{ jobid:`${startTime}`,removeOnComplete: true, removeOnFail: true },
        );
    • jobid - 특정 id를 부여하지 않으면, 테스트과정에서 같은 요청이 있을때 로직에 상관없이 이전의 테스트결과를 보여주는 버그가 있었다(redis에서 캐싱한 결과를 찾아서 리턴하는것이 아닌가 생각됨)
    • remove옵션 - 성공,실패의 결과를 redis에 남기지않기
  • job을 결과에 따라 결과값을 리턴

    return new Promise((resolve, reject) => {
          job //(finish,isActive,isFailed)등 여러 콜백처리 가능
            .finished()
            .then((result) => {
              resolve(result);
            })
            .catch((error) => reject(error));
        });

redis-queue consumer

consumer은 queue에 저장된 작업들을 하나씩 꺼내어 요청하는 레이어

  • 이전에 등록한 queue의 작업을 처리할 클래스

    @Processor('큐이름')
    @Injectable()
    export class DockerConsumer {}
  • 해당 queue-job의 이름으로 등록된 로직을 처리하여 producer로 반환

    @Process({ name: 'jobName', concurrency: 2 })
      async handleDockerRun(job: Job) {
        const params = job.data; // 작업을 저장할 때 주어지는 파라미터
        /*
        
        처리할 비지니스 로직
        */
        return result
    • concurrency ⇒ 병렬처리(worker) 갯수
    • process의 jobName으로 등록된 job들을 구별하여 사용가능

NestJS Queue 사용해보기

Nestjs queue 실사용기(feat. bull)

스케줄링 방식

큐를 이용한 처리 flow

  1. api요청을 queue에 삽입

  2. 작업이 할당되면 pool에서 대기중인 컨테이너 pop

    ⇒ 없으면 다른 요청에서 반환할 때 까지 wait

  3. 작업이 끝나면 컨테이너를 반납하며 결과를 api응답

graph LR
    API[API 요청] -->|요청 추가| RedisQueue[Redis Queue]
        RedisQueue -->|작업 요청| DockerService[Docker 서비스]
    
    DockerService -->|컨테이너 요청| Pool[컨테이너 Pool]
        Pool -->|할당된 컨테이너| Container[컨테이너]
        Container -->|작업 실행| Task[js 실행]
        Task -->|결과 반환| Container
        Container -->|컨테이너 반납| Pool
        Container -->|작업 결과| Response[API 응답]
        DockerService -->|결과 반환| RedisQueue
   

Loading
result

컨테이너 동적 실행

  • 컨테이너를 할당받을 때마다 start,stop으로 사용 및 반납

    stateDiagram-v2
        direction LR
        사용자request --> Pool: 코드실행 요청
        
        state "Pool" as Pool
        Pool --> Allocated: 컨테이너 할당
        Allocated --> Started: 컨테이너 Start
        Started --> Running: 작업 실행 중
        Running --> Finish: 작업 완료
        Finish --> Exited: 컨테이너 stop
        Finish --> 사용자request: 코드실행결과 반환
        Exited --> Pool: 컨테이너 Stop 후 반납
    
    Loading

    ⭕ 장점

    • 컨테이너를 사용하지 않을때 exited상태 이기때문에 리소스 사용량을 줄일 수 있습니다.

    ❗단점

    • 컨테이너의 실행,중지에 상당한 오버헤드 발생

컨테이너 상시 실행

  • 컨테이너를 상시 실행시키고 객체를 할당받으면 디렉토리에 빠르게 실행하고 삭제하여 큐에 반납하는 방식

    stateDiagram-v2
        direction LR
        사용자request --> Pool: 코드실행 요청
        
        state "Pool" as Pool
        Pool --> Allocated: 컨테이너 할당
        Allocated --> Running: 작업 실행 중
        Running --> Finish: 작업 완료
        Finish --> 사용자request: 코드실행결과 반환
        Finish --> Pool: 컨테이너 대기큐 반납
    
    
    Loading

    ⭕ 장점

    • 컨테이너의 실행,반납 시간을 줄여서 응답시간을 높일 수 있습니다.

    ❗단점

    • 컨테이너가 서버 리소스를 항상 잡아두고 있습니다.
    • 컨테이너의 수를 이전의 방법보다 줄이는 방법도 고려해야합니다.

Single 컨테이너

  • 컨테이너를 하나만 실행하고, 실행할 디렉토리에 특정 id를 이용해 디렉토리단위로 동시접근 하는 방식

    stateDiagram-v2
        direction LR
        사용자request --> SingleContainer: 코드실행 요청
        SingleContainer --> Setup: /tmp/id 디렉토리 생 성 및 파일 복사
        Setup --> Execution: 코드 실행
        Execution --> Init: 결과 반환 및 /tmp/{userId} 디렉토리 삭제
        Execution --> 사용자request: 응답 완료
    
    
    Loading

    ⭕ 장점

    • 이전과 속도를 비슷하지만, 관리할 컨테이너를 줄일 수 있습니다.

    ❗단점

    • 실행환경이 격리되지 않아서 파일시스템상의 문제가 발생할 수 있습니다.
Clone this wiki locally