속도 제한(Rate limiting
[//]: # (title: 속도 제한(Rate limiting))
필수 의존성: io.ktor:ktor-server-rate-limit
코드 예제: rate-limit
RateLimit 플러그인을 사용하면 클라이언트가 일정 시간 동안 보낼 수 있는 요청 수를 제한할 수 있습니다. Ktor는 속도 제한을 구성하기 위한 다양한 수단을 제공합니다. 예를 들어:
- 애플리케이션 전체에 전역적으로 속도 제한을 활성화하거나, 서로 다른 리소스에 대해 각기 다른 속도 제한을 구성할 수 있습니다.
- IP 주소, API 키 또는 액세스 토큰 등 특정 요청 파라미터를 기반으로 속도 제한을 구성할 수 있습니다.
의존성 추가
RateLimit을 사용하려면 빌드 스크립트에 ktor-server-rate-limit 아티팩트를 포함해야 합니다:
RateLimit 설치
RateLimit 플러그인을 애플리케이션에 설치하려면, 지정된
install 함수에 전달하세요. 아래의 코드 스니펫은 RateLimit을 설치하는 방법을 보여줍니다... - ...
embeddedServer함수 호출 내부에서 설치하는 방법. - ...
Application클래스의 확장 함수인 명시적으로 정의된module내부에서 설치하는 방법.
RateLimit 구성
개요
Ktor는 속도 제한을 위해 토큰 버킷(token bucket) 알고리즘을 사용하며, 다음과 같이 작동합니다:
- 처음에는 용량(토큰 수)으로 정의된 버킷이 있습니다.
- 들어오는 각 요청은 버킷에서 토큰 하나를 소비하려고 시도합니다:
- 용량이 충분하면 서버는 요청을 처리하고 다음 헤더와 함께 응답을 보냅니다:
X-RateLimit-Limit: 지정된 버킷 용량.X-RateLimit-Remaining: 버킷에 남아 있는 토큰 수.X-RateLimit-Reset: 버킷이 다시 채워지는 시간을 지정하는 UTC 타임스탬프(초 단위).
- 용량이 부족하면 서버는
429 Too Many Requests응답을 사용하여 요청을 거부하고, 클라이언트가 다음 요청을 보내기까지 기다려야 하는 시간(초 단위)을 나타내는Retry-After헤더를 추가합니다.
- 용량이 충분하면 서버는 요청을 처리하고 다음 헤더와 함께 응답을 보냅니다:
- 지정된 시간이 지나면 버킷 용량이 다시 채워집니다.
속도 제한기 등록
Ktor를 사용하면 애플리케이션 전체에 전역적으로 속도 제한을 적용하거나 특정 경로에 적용할 수 있습니다:
애플리케이션 전체에 속도 제한을 적용하려면
global메서드를 호출하고 구성된 속도 제한기를 전달하세요.kotlininstall(RateLimit) { global { rateLimiter(limit = 5, refillPeriod = 60.seconds) } }register메서드는 특정 경로에 적용할 수 있는 속도 제한기를 등록합니다.kotlininstall(RateLimit) { register { rateLimiter(limit = 5, refillPeriod = 60.seconds) } }
위의 코드 샘플은 RateLimit 플러그인에 대한 최소한의 구성을 보여주지만, register 메서드를 사용하여 등록된 속도 제한기의 경우 특정 경로에도 이를 적용해야 합니다.
속도 제한 구성
이 섹션에서는 속도 제한을 구성하는 방법을 살펴보겠습니다:
(선택 사항)
register메서드를 사용하면 특정 경로에 속도 제한 규칙을 적용하는 데 사용할 수 있는 속도 제한기 이름을 지정할 수 있습니다:kotlininstall(RateLimit) { register(RateLimitName("protected")) { // ... } }rateLimiter메서드는 두 개의 파라미터로 속도 제한기를 생성합니다:limit은 버킷 용량을 정의하고,refillPeriod는 이 버킷의 리필 주기를 지정합니다. 아래 예제의 속도 제한기는 분당 30개의 요청을 처리할 수 있도록 허용합니다:kotlinregister(RateLimitName("protected")) { rateLimiter(limit = 30, refillPeriod = 60.seconds) }(선택 사항)
requestKey를 사용하면 요청에 대한 키를 반환하는 함수를 지정할 수 있습니다. 키가 서로 다른 요청은 독립적인 속도 제한을 갖습니다. 아래 예제에서는login쿼리 파라미터를 키로 사용하여 서로 다른 사용자를 구분합니다:kotlinregister(RateLimitName("protected")) { requestKey { applicationCall -> applicationCall.request.queryParameters["login"]!! } }키는
equals및hashCode가 잘 구현되어 있어야 합니다.(선택 사항)
requestWeight는 요청에 의해 소비되는 토큰 수를 반환하는 함수를 설정합니다. 아래 예제에서는 요청 키를 사용하여 요청 가중치를 구성합니다:kotlinregister(RateLimitName("protected")) { requestKey { applicationCall -> applicationCall.request.queryParameters["login"]!! } requestWeight { applicationCall, key -> when(key) { "jetbrains" -> 1 else -> 2 } } }(선택 사항)
modifyResponse를 사용하면 각 요청과 함께 전송되는 기본X-RateLimit-*헤더를 오버라이드할 수 있습니다:kotlinregister(RateLimitName("protected")) { modifyResponse { applicationCall, state -> applicationCall.response.header("X-RateLimit-Custom-Header", "Some value") } }
속도 제한 범위 정의
속도 제한기를 구성한 후에는 rateLimit 메서드를 사용하여 특정 경로에 해당 규칙을 적용할 수 있습니다:
routing {
rateLimit {
get("/") {
val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
call.respondText("Welcome to the home page! $requestsLeft requests left.")
}
}
}이 메서드는 속도 제한기 이름을 인자로 받을 수도 있습니다:
routing {
rateLimit(RateLimitName("protected")) {
get("/protected-api") {
val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
val login = call.request.queryParameters["login"]
call.respondText("Welcome to protected API, $login! $requestsLeft requests left.")
}
}
}예제
아래 코드 샘플은 RateLimit 플러그인을 사용하여 서로 다른 리소스에 각기 다른 속도 제한기를 적용하는 방법을 보여줍니다. StatusPages 플러그인은 429 Too Many Requests 응답이 전송된 거부된 요청을 처리하는 데 사용됩니다.
package com.example
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
fun Application.module() {
install(RateLimit) {
register {
rateLimiter(limit = 5, refillPeriod = 60.seconds)
}
register(RateLimitName("public")) {
rateLimiter(limit = 10, refillPeriod = 60.seconds)
}
register(RateLimitName("protected")) {
rateLimiter(limit = 30, refillPeriod = 60.seconds)
requestKey { applicationCall ->
applicationCall.request.queryParameters["login"]!!
}
requestWeight { applicationCall, key ->
when(key) {
"jetbrains" -> 1
else -> 2
}
}
}
}
install(StatusPages) {
status(HttpStatusCode.TooManyRequests) { call, status ->
val retryAfter = call.response.headers["Retry-After"]
call.respondText(text = "429: Too many requests. Wait for $retryAfter seconds.", status = status)
}
}
routing {
rateLimit {
get("/") {
val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
call.respondText("Welcome to the home page! $requestsLeft requests left.")
}
}
rateLimit(RateLimitName("public")) {
get("/public-api") {
val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
call.respondText("Welcome to public API! $requestsLeft requests left.")
}
}
rateLimit(RateLimitName("protected")) {
get("/protected-api") {
val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
val login = call.request.queryParameters["login"]
call.respondText("Welcome to protected API, $login! $requestsLeft requests left.")
}
}
}
}전체 예제는 여기에서 확인할 수 있습니다: rate-limit.
