You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
계정 한도 초과(429)와 성격이 다르다. 전역 상한은 사용자 잘못이 아니라 서비스가 꽉 찬 것이므로 503 Service Unavailable 이 맞다.
자리는 이미 마련되어 있다. CommonErrorCode.SERVER_BUSY 의 주석이 "load shedding, 자리만 정의: 실제 발생은 후속 이슈" 인데 이 이슈가 그것이다. 문구("지금 요청이 많아요. 잠시 후 다시 시도해 주세요.")도 이 상황에 맞게 이미 쓰여 있다.
검사 순서는 계정별이 먼저
계정 몫도 다 쓰고 전역도 찬 사용자에게 503("지금 요청이 많아요")을 주면, 실제 원인은 자기가 다 쓴 것인데 서버 탓으로 읽힌다. 계정별을 먼저 검사해 429 를 주는 쪽이 원인과 응답이 일치하고 지원 문의 시 추적도 쉽다.
두 축을 한 스크립트로 원자 판정한다
계정 축과 전역 축을 나눠 호출하면 "계정 몫은 깎였는데 전역이 차서 거부" 인 요청이 생긴다. 그 사용자는 Retry-After 안내대로 재시도할 때마다 자기 몫을 잃고, 전역이 풀린 뒤에도 자기 한도에 걸려 429 를 받는다. 재시도 안내가 사용자를 자해하게 만드는 셈이라, 판정과 차감을 한 Lua 스크립트로 묶어 둘 중 하나라도 막히면 어느 카운터도 건드리지 않는다.
상한: 시간당 3000건. 계정 한도(시간당 30)로 환산하면 자기 몫을 꽉 채운 사용자 100명분이다
알림: 66% (시간당 1980건). 상한까지 1020건이 남으므로 원인을 가르고 손 쓸 여유가 창의 3분의 1 남는다. 기존 Discord 알림에 얹는다
계정 한도도 이 작업에서 함께 손댄다.userId별 애플리케이션 레벨 레이트리밋 (인증 사용자별 정밀 quota) #339 는 위시(시간당 10)·토너먼트(시간당 30) 두 축이었는데 이를 계정 하나의 몫 시간당 30 으로 합친다. 축이 둘이면 한 계정의 실제 상한이 둘의 합(40)이 되어 "이 계정이 시간당 얼마나 쓰나" 를 한 숫자로 말할 수 없고, 막으려는 대상은 경로가 아니라 계정의 총 소비이기 때문이다. 합치면 "친구들이 내 토너먼트에 넣어서 내가 내 위시를 못 쓰는" 상황이 생기는데, 그것이 축을 나눴던 이유였고 이번에 감수하기로 했다
근거: 파싱 워커 풀이 maxPoolSize = 8 · queueCapacity = 0 이라 동시 처리가 최대 8건이다. 건당 소요를 파서 12초로 잡으면 이론 처리량이 시간당 14,000건을 넘어 3000 은 여유가 크다(건당 시간은 실측이 아니라 timeout 상한에서 잡은 추정). 다만 헤드리스·LLM 이 섞이면 건당 520초까지 늘어 이론 처리량이 시간당 1,400건까지 떨어지고, 그 구간에서는 이 상한이 워커보다 느슨해 상한에 닿기 전에 PENDING 이 쌓인다(거부가 아니라 대기라 장애는 아니다). 3000 은 파싱 경량화(LLM·헤드리스·프록시 제거, 화이트리스트 전환)로 파서 위주가 되는 것을 전제로 잡은 값이다.
값은 환경변수로 조정한다. 경량화가 진행되는 동안 더 낮게 운영하고 싶으면 배포 없이 내릴 수 있다.
이 축은 마지노선이다
정상 운영에서는 닿지 않아야 하는 선이다. 닿았다면 무언가 잘못됐다는 신호지 "인기가 많아졌다" 는 신호가 아니다. 그래서 알림이 울렸을 때 기본 대응이 "상한을 올린다" 가 아니다. 먼저 원인을 가른다.
정상 성장인가
특정 사용자·IP 의 이상 패턴인가
파싱 실패로 인한 재시도 폭증인가
정상 성장으로 확인된 뒤에만 상한을 올린다. 상한에 닿으면 이미 늦으므로 66% 알림이 실질적인 방어선 역할을 한다.
왜
#339 로 들어간 계정별 한도는 "한 사람이 100번 쓰는 것"을 막지만 "100명이 각자 30번씩 쓰는 것"은 막지 못한다. 정상 사용자가 갑자기 늘어도 파싱량은 그대로 올라가고, 우리 서비스가 감당하겠다고 정한 양이 어디에도 선언되어 있지 않다.
비용 방어가 아니라 가용량 선언이다. "우리는 시간당 N건까지 처리한다" 를 명시하고 그 선을 코드로 지킨다.
무엇을
무엇을 세는가
전체 요청이 아니라 파싱 작업 수다. 기준은 "워커가 처리할 작업이 큐에 들어가나" 이고, 차감 지점은 #339 의 계정별 한도와 완전히 같다(
ItemQuotaGuard.consume을 부르는 7곳에 전역 축을 하나 더 확인하는 형태).POST /wishlistsPOST /wishlists/imagesPOST /wishlists/images/presignedPOST /wishlists/{id}/refreshPOST /tournaments/{id}/items/linkPOST /tournaments/{id}/items/imagesPOST /tournaments/{id}/items/images/presigned/health,/actuator/*POST /tournaments/{id}/items/wishPOST /wishlists/images/confirm새로고침이 세는 쪽에 있는 이유도 같은 기준이다. 이미 있는 item 이지만 파싱을 한 번 더 돌리므로 워커가 처리할 작업이 새로 생긴다.
왜 일일이 아니라 시간당인가
처음에는 "하루 N건" 으로 잡았다가 시간당으로 바꿨다. 일일 상한은 오전에 소진되면 그날 남은 시간 전체가 막힌다. 시간당이면 최악의 정지 구간이 1시간으로 제한되고 다음 창에서 자동 회복된다.
계정별 한도(#339)도 시간당이라 주기가 일관되는 이점도 있다.
응답은 429 가 아니라 503
계정 한도 초과(429)와 성격이 다르다. 전역 상한은 사용자 잘못이 아니라 서비스가 꽉 찬 것이므로 503 Service Unavailable 이 맞다.
자리는 이미 마련되어 있다.
CommonErrorCode.SERVER_BUSY의 주석이 "load shedding, 자리만 정의: 실제 발생은 후속 이슈" 인데 이 이슈가 그것이다. 문구("지금 요청이 많아요. 잠시 후 다시 시도해 주세요.")도 이 상황에 맞게 이미 쓰여 있다.검사 순서는 계정별이 먼저
계정 몫도 다 쓰고 전역도 찬 사용자에게 503("지금 요청이 많아요")을 주면, 실제 원인은 자기가 다 쓴 것인데 서버 탓으로 읽힌다. 계정별을 먼저 검사해 429 를 주는 쪽이 원인과 응답이 일치하고 지원 문의 시 추적도 쉽다.
두 축을 한 스크립트로 원자 판정한다
계정 축과 전역 축을 나눠 호출하면 "계정 몫은 깎였는데 전역이 차서 거부" 인 요청이 생긴다. 그 사용자는
Retry-After안내대로 재시도할 때마다 자기 몫을 잃고, 전역이 풀린 뒤에도 자기 한도에 걸려 429 를 받는다. 재시도 안내가 사용자를 자해하게 만드는 셈이라, 판정과 차감을 한 Lua 스크립트로 묶어 둘 중 하나라도 막히면 어느 카운터도 건드리지 않는다.구현 메모
RedisItemQuotaStore를 그대로 쓴다. 전역 키(quota:item:capacity)를 하나 더 세는 정도다Retry-After헤더는 userId별 애플리케이션 레벨 레이트리밋 (인증 사용자별 정밀 quota) #339 에서 만든RetryAfter인터페이스를 재사용한다값
근거: 파싱 워커 풀이
maxPoolSize = 8·queueCapacity = 0이라 동시 처리가 최대 8건이다. 건당 소요를 파서 12초로 잡으면 이론 처리량이 시간당 14,000건을 넘어 3000 은 여유가 크다(건당 시간은 실측이 아니라 timeout 상한에서 잡은 추정). 다만 헤드리스·LLM 이 섞이면 건당 520초까지 늘어 이론 처리량이 시간당 1,400건까지 떨어지고, 그 구간에서는 이 상한이 워커보다 느슨해 상한에 닿기 전에 PENDING 이 쌓인다(거부가 아니라 대기라 장애는 아니다). 3000 은 파싱 경량화(LLM·헤드리스·프록시 제거, 화이트리스트 전환)로 파서 위주가 되는 것을 전제로 잡은 값이다.값은 환경변수로 조정한다. 경량화가 진행되는 동안 더 낮게 운영하고 싶으면 배포 없이 내릴 수 있다.
이 축은 마지노선이다
정상 운영에서는 닿지 않아야 하는 선이다. 닿았다면 무언가 잘못됐다는 신호지 "인기가 많아졌다" 는 신호가 아니다. 그래서 알림이 울렸을 때 기본 대응이 "상한을 올린다" 가 아니다. 먼저 원인을 가른다.
정상 성장으로 확인된 뒤에만 상한을 올린다. 상한에 닿으면 이미 늦으므로 66% 알림이 실질적인 방어선 역할을 한다.
참고
piki_llm제거. 계정 한도가 대체하므로 별건)