-
Notifications
You must be signed in to change notification settings - Fork 3
redis‐queue를 이용한 컨테이너 스케줄링
기존에는 사용자의 요청마다 컨테이너를 생성-실행-반납하는 방식으로 구현했습니다.
이 방식은 트래픽이 몰렸을때 1:1로 컨테이너를 만들기 때문에 서버의 리소스에 상당한 무리가 갈 것이라 생각했습니다.
먼저 컨테이너 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는 직접 구현하지않고,redis의 queue를 이용해 구현하였습니다.
@Module({
imports: [
BullModule.forRoot({
redis: {
host: 'localhost',
port: 6379,
},
}),
],
})
export class AppModule {}
[nestjs bull 공식문서] https://docs.nestjs.com/techniques/queues#bull-installation
원하는 이름으로 queue를 redis에 등록
@Module({
imports: [
GistModule,
BullModule.registerQueue({
name: 'docker-queue' // 큐 이름
})
],
controllers: [DockerController],
providers: [DockerProducer, DockerConsumer, DockerContainerPool],
exports: [DockerProducer]
})
export class DockerModule {}
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)); });
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들을 구별하여 사용가능
큐를 이용한 처리 flow
-
api요청을 queue에 삽입
-
작업이 할당되면 pool에서 대기중인 컨테이너 pop
⇒ 없으면 다른 요청에서 반환할 때 까지 wait
-
작업이 끝나면 컨테이너를 반납하며 결과를 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

-
컨테이너를 할당받을 때마다 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 후 반납
⭕ 장점
- 컨테이너를 사용하지 않을때 exited상태 이기때문에 리소스 사용량을 줄일 수 있습니다.
❗단점
- 컨테이너의 실행,중지에 상당한 오버헤드 발생
-
컨테이너를 상시 실행시키고 객체를 할당받으면 디렉토리에 빠르게 실행하고 삭제하여 큐에 반납하는 방식
stateDiagram-v2 direction LR 사용자request --> Pool: 코드실행 요청 state "Pool" as Pool Pool --> Allocated: 컨테이너 할당 Allocated --> Running: 작업 실행 중 Running --> Finish: 작업 완료 Finish --> 사용자request: 코드실행결과 반환 Finish --> Pool: 컨테이너 대기큐 반납
⭕ 장점
- 컨테이너의 실행,반납 시간을 줄여서 응답시간을 높일 수 있습니다.
❗단점
- 컨테이너가 서버 리소스를 항상 잡아두고 있습니다.
- 컨테이너의 수를 이전의 방법보다 줄이는 방법도 고려해야합니다.
-
컨테이너를 하나만 실행하고, 실행할 디렉토리에 특정 id를 이용해 디렉토리단위로 동시접근 하는 방식
stateDiagram-v2 direction LR 사용자request --> SingleContainer: 코드실행 요청 SingleContainer --> Setup: /tmp/id 디렉토리 생 성 및 파일 복사 Setup --> Execution: 코드 실행 Execution --> Init: 결과 반환 및 /tmp/{userId} 디렉토리 삭제 Execution --> 사용자request: 응답 완료
⭕ 장점
- 이전과 속도를 비슷하지만, 관리할 컨테이너를 줄일 수 있습니다.
❗단점
- 실행환경이 격리되지 않아서 파일시스템상의 문제가 발생할 수 있습니다.