object wait / notify 를 활용한 API call count 제한
<현상>
- AWS Athena사용시 quota제한 으로 인하여 다음과 같은 문제가 발생하였습니다.
- 동시에 많은 요청이 몰리는 것을 방지하기 위해서 다음과 같이 API호출수를 제한하도록 하였으며 object wait/notify를 활용하였습니다.
<로직>
- API 호출시 기본적으로 @Async호출
- API호출전에 현재 호출count를 확인하여 일정수치 이상일 경우 object.wait()로 대기
- 호출이 성공하면 count를 증가
- 비동기호출을 마치고 응답을 받으면 callback으로 호출count를 감소시키고 object.notify
CompletableFuture<Map<String, Integer>> completableFutureForRule;
...
synchronized (this){
waitBeforeCall();
completableFutureForRule = apiCallService.call(request);
resultCount.incrementAndGet();
}
completableFutureForRule.thenAccept(retMap -> {
synchronized (this){
notifyAfterCall();
}
private void waitBeforeCall(){
try {
while (apiCallService.getCallCount() >= SIZE) {
this.wait();
}
} catch (InterruptedException e) {
log.error("Interruped");
}
apiCallService.incrementAndGet();
}
private void notifyAfterCall(){
apiCallService.decrementAndGet();
this.notifyAll();
}
- blocking queue를 구현할때 와 매우 유사한 로직임을 알수 있습니다.
<참고사항>
- caller service 와 callee service가 같은 비율로 증가한다면 이론상 문제는 없습니다. (e.g 10, 20, 30)
그러나 실제는 두 서비스간에 연관관계가 없을 것으로 예상되며, 또한 이 로직은 한 jvm내 단일 service에서만 유용하기 때문에 ECS와 같이 scale out 이 되는 구조에서 전체 API 호출수를 제한하기 위해서는 다른 방법을 사용해야 합니다.
e.g) A. cluster shared lock
B. API gateway를 활용한 limit rate
C. Redis를 활용한 global count개념
- wait후 notify의 경우 잠들어 있는 하나의 쓰레드만 깨우기 때문에 여러가지 예외 상황에 대응하거나 개별제어가 어려워서 일반적으로 notifyAll을 많이 사용합니다.
- while()문으로 체크해야 하는 이유는 여러 쓰레드가 다시 lock을 획득하기 위해 경쟁하기 때문입니다. 또한 Spurious wakeups 처럼 이유없이 쓰레드가 깨어난 경우에도 다시 wait로 진입하기 위해서 필요합니다.
- 이와 같이 일정시간 대기하는 로직을 작성할때 sleep을 사용하는 경우를 간혹 볼 수 있는데, sleep의 경우 wait와 다르게 lock을 반환하지 않으므로 주의해서 사용해야 합니다. (stackoverflow 참조)
https://stackoverflow.com/questions/1036754/difference-between-wait-and-sleep
- 로직이 복잡해질 경우 CountDownLatch 를 이용해서 구현하는 것을 권장합니다!