-
Notifications
You must be signed in to change notification settings - Fork 0
사용자별 쿼리 실행 요청 제한
현재 서비스는 다음과 같이 여러 유저가 하나의 DB 서버를 공유하고 있습니다.
해당 상황에서 한 유저가 부하가 많이 가는 쿼리를 악의적으로 많이 실행시킬 시 다른 유저에게 피해를 줄 위험이 있습니다.
이를 위해 다음과 같은 방법으로 쿼리 실행을 제한을 두었습니다.
기존 서비스에서도 동시에 많은 쿼리 실행 요청을 보낼 수 없게 클라이언트단에서 실행중에는 해당 쉘 버튼을 비활성화하였습니다.
하지만 쉘을 많이 생성하면 동시에 실행시킬 수 있고, 서버에 직접 요청을 보내면 이를 제어할 수 없었습니다.
그래서 서버에서 쿼리실행 요청을 받으면 이를 동시에 처리하지 않고 순차적으로 처리할 수 있게 구현하였습니다.
하지만 순차처리만으로 서버의 안정성을 높이기에는 한계가 있었습니다.
- 모든 요청을 받기에 WAS에 부담을 줄 수 있습니다.
- 일시적인 부하량은 제어할 수 있어도, 일정량의 부하를 지속적으로 줄 수 있습니다.
이를 해결하기 위해 사용자별 처리를 제한하는 Rate Limiter를 구현하였습니다.
일반적으로 처리율 제한은 요청 수를 기준으로 적용하는 방식이 많이 사용됩니다.
이 방법은 특정 API의 단일 요청이 예상되는 부하를 유발하거나, 서비스적으로 제어가 필요할 때 유용합니다.
그러나 Q-Lab에서는 사용자가 실행하는 쿼리에 따라 서버 부하가 크게 달라지기 때문에, 요청 수 기반의 제한 방식은 효과적이지 않습니다.
따라서, 서비스에 적합한 방식으로 서버 부하를 측정할 수 있도록 쿼리 실행 시간을 기준으로 삼았습니다.
또한, 쿼리 실행은 단 하나의 쿼리만으로도 높은 부하를 발생시킬 수 있기에 보다 정밀한 제어가 필요합니다.
이를 위해 처리율 제한 방식 중 가장 정교한 제어가 가능한 Sliding Window Log 알고리즘을 기반으로 구현하였습니다.
Sliding Window Log 알고리즘은 위 그림과 같이 움직이는 윈도우(일정 주기)동안 요청을 확인하는 방식입니다.
Q-Lab에선 1분동안 쿼리 실행시간의 합이 20초를 초과하는지 확인하는 방식으로 구현하였습니다.
잔여 실행시간 확인
잔여 시간이란 현 시점 실행할 수 있는 최대 시간입니다.
실행 시점기준 1분안에 실행시간 쿼리의 시간합을 기준 시간인 20초에서 뺀값입니다.
MySQL 쿼리를 통해 쿼리의 최대 실행시간을 조절하였습니다.
SET SESSION MAX_EXECUTION_TIME=(잔여시간)
잔여시간이 0보다 작으면 어떠한 쿼리도 실행하지 못하므로 즉시 에러를 반환합니다.
최대 실행시간 초과 확인
이전에 지정한 쿼리 최대 시간을 넘었는지 확인하는 부분입니다.
이를 초과하면 Mysql에서 쿼리를 중단시키고 에러를 반환합니다.
Redis에 SortedSet을 이용하여 요청 시점 기준 1분이전의 쿼리 실행시간의 합을 구합니다.
쿼리 실행 시간 저장
public async addResponseTime(
requestTime: number,
sessionId: string,
responseTime: number,
): Promise<void> {
this.redis.zadd(sessionId, requestTime, responseTime);
}
쿼리 실행이 완료되면 요청시간을 SortedSet으로 삽입합니다.
사용자별로 처리를 제한하므로 키값은 sessionId로 하고 처리 기준인 쿼리 실행시간 또한 함께 저장합니다.
잔여 시간 확인
public async getRemainTime(sessionId: string): Promise<number> {
const currentTime = Date.now();
const minTime = currentTime - this.TIME_WINDOW_MS;
await this.redis.zremrangebyscore(sessionId, '-inf', minTime);
const responseTimeList = await this.redis.zrange(sessionId, 0, -1);
return (
this.MAX_ALLOWED_DURATION_SEC -
responseTimeList
.map((value) => parseFloat(value))
.reduce((acc, curr) => acc + curr, 0)
);
}
busy waiting
(pub/sub)을 고려하지 않은 이유
Message Queue를 이용하여 순차처리를 구현하지 않은 이유
API Gateway 서버를 분리하여 이를 구현하지 않은 이유