요청 접수와 처리 흐름
ngx_http_limit_req_handler 함수는 클라이언트 요청이 들어왔을 때 실행되는 핵심 진입점이다. 설정된 모든 limit zone을 순회하며 클라이언트를 조회하고 excess를 계산한 후, 그 결과에 따라 요청을 허용, 거부, 또는 지연 처리한다.
전체 흐름 한눈에 보기
전체 소스코드
src/http/modules/ngx_http_limit_req_module.c
static ngx_int_t ngx_http_limit_req_handler(ngx_http_request_t *r) {
/* 변수 선언 */
uint32_t hash; // CRC32 해시값 (Red-Black Tree 검색용)
ngx_str_t key; // 클라이언트 식별 키 ($binary_remote_addr 등)
ngx_int_t rc; // lookup 함수 반환값
ngx_uint_t n, excess; // n: zone 순회 인덱스, excess: 스케일된 요청 잉여 (요청 수 × 1000)
ngx_msec_t delay; // 요청 지연 시간 (밀리초)
ngx_http_limit_req_ctx_t *ctx; // 공유 메모리 zone 컨텍스트
ngx_http_limit_req_conf_t *lrcf; // 현재 location의 limit_req 설정
ngx_http_limit_req_limit_t *limit, *limits; // limit: 현재 처리 중인 zone, limits: 전체 zone 배열
/* 1. 중복 처리 체크: 이미 처리된 요청인지 확인 */
if (r->main->limit_req_status) { // 이 요청이 이미 limit_req 모듈에서 처리된 경우
return NGX_DECLINED; // NGX_DECLINED를 반환하여 다음 핸들러로 전달
}
/* 2. 설정 로드: 현재 location의 limit_req 설정 로드 */
lrcf = ngx_http_get_module_loc_conf(r, ngx_http_limit_req_module); // 현재 location의 limit_req 설정 가져오기
limits = lrcf->limits.elts; // zone 배열의 첫 번째 요소 포인터 가져오기
excess = 0; // excess 변수를 0으로 초기화
rc = NGX_DECLINED; // 반환값을 제한 없음(NGX_DECLINED)으로 초기화
#if (NGX_SUPPRESS_WARN) // NGX_SUPPRESS_WARN 매크로가 정의된 경우
limit = NULL; // limit 변수를 NULL로 초기화하여 컴파일러 경고 방지
#endif // NGX_SUPPRESS_WARN
/* 3. limit zones 순회: 설정된 모든 zone을 순회하며 체크 */
for (n = 0; n < lrcf->limits.nelts; n++) { // zone을 순회하며
limit = &limits[n]; // n번째 zone의 포인터를 limit에 저장
ctx = limit->shm_zone->data; // zone의 공유 메모리 컨텍스트 가져오기
/* 4. 클라이언트 키 생성: $binary_remote_addr 등의 변수를 평가하여 키 생성 */
if (ngx_http_complex_value(r, &ctx->key, &key) != NGX_OK) { // 키 생성에 실패한 경우 (메모리 부족 등)
ngx_http_limit_req_unlock(limits, n); // 지금까지 Lock한 zone들을 모두 Unlock
return NGX_HTTP_INTERNAL_SERVER_ERROR; // 500 에러를 반환하여 함수 종료
}
if (key.len == 0) { // 키가 빈 값인 경우 (변수 값이 없음)
continue; // 다음 zone으로 건너뛰기
}
/* 5. 키 검증: 키 길이가 유효한지 확인 (최대 65535 바이트) */
if (key.len > 65535) { // 키 길이가 65535 바이트를 초과하는 경우
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "the value of the \"%V\" key is more than 65535 bytes: \"%V\"", &ctx->key.value, &key); // 에러 로그 출력: 어떤 변수에서 생성되었는지와 실제 키 값
continue; // 다음 zone으로 건너뛰기
}
/* 해시 계산: CRC32 해시로 Red-Black Tree에서 빠르게 조회 */
hash = ngx_crc32_short(key.data, key.len); // 키 데이터를 CRC32 해시값으로 변환
/* 공유 메모리 Lock: 다른 worker process와 동시 접근 방지 */
ngx_shmtx_lock(&ctx->shpool->mutex); // 공유 메모리에 Lock을 걸어 동시 접근 방지
/* 6. excess 계산: Leaky Bucket 알고리즘으로 초과 요청 수 계산 */
rc = ngx_http_limit_req_lookup(limit, hash, &key, &excess, (n == lrcf->limits.nelts - 1)); // lookup 함수를 호출하여 excess 계산 (마지막 zone인 경우 새 노드 생성 가능)
/* 공유 메모리 Unlock */
ngx_shmtx_unlock(&ctx->shpool->mutex); // 공유 메모리 Lock 해제
ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "limit_req[%ui]: %i %ui.%03ui", n, rc, excess / 1000, excess % 1000); // 디버그 로그 출력: zone 인덱스, 반환값, excess 값 (1000 단위로 스케일됨)
/* 7. 결과 판단: NGX_AGAIN이 아니면 루프 종료 */
// NGX_AGAIN: 노드를 찾았지만 account=false → 다음 zone 체크
if (rc != NGX_AGAIN) { // 마지막 zone에 도달했거나 에러가 발생한 경우
break; // 루프를 종료하고 다음 단계로 이동
}
}
/* NGX_DECLINED: 제한 없음 (즉시 통과) */
if (rc == NGX_DECLINED) { // 적용 가능한 limit이 없는 경우 (키가 비어있거나 limit 설정 없음)
return NGX_DECLINED; // NGX_DECLINED를 반환하여 요청을 즉시 통과
}
/* NGX_BUSY/ERROR: 제한 초과 (요청 거부) */
if (rc == NGX_BUSY || rc == NGX_ERROR) { // 제한 초과 또는 에러가 발생한 경우
if (rc == NGX_BUSY) { // 제한 초과로 인한 거부인 경우
ngx_log_error(lrcf->limit_log_level, r->connection->log, 0, "limiting requests%s, excess: %ui.%03ui by zone \"%V\"", lrcf->dry_run ? ", dry run" : "", excess / 1000, excess % 1000, &limit->shm_zone->shm.name); // 제한 초과 로그 출력
}
ngx_http_limit_req_unlock(limits, n); // Lock이 걸린 모든 zone들을 Unlock
if (lrcf->dry_run) { // dry_run 모드가 활성화된 경우
r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_REJECTED_DRY_RUN; // 요청 상태를 REJECTED_DRY_RUN으로 변경
return NGX_DECLINED; // NGX_DECLINED를 반환하여 요청을 통과시킴
}
r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_REJECTED; // 요청 상태를 REJECTED로 변경
return lrcf->status_code; // 설정 파일에서 지정한 상태 코드를 반환 (기본값: 503)
}
/* rc == NGX_AGAIN || rc == NGX_OK */
// NGX_AGAIN: 마지막 zone에서 새 노드 생성, NGX_OK: 제한 이내
if (rc == NGX_AGAIN) { // 마지막 zone에서 새 클라이언트 노드를 생성한 경우
excess = 0; // excess를 0으로 리셋
}
/* 8. delay 계산: 모든 zone을 업데이트하고 지연 시간 계산 */
delay = ngx_http_limit_req_account(limits, n, &excess, &limit); // account 함수를 호출하여 지연 시간 계산
/* 9. delay = 0: 즉시 통과 */
if (!delay) { // 지연이 필요 없는 경우
r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_PASSED; // 요청 상태를 PASSED로 변경
return NGX_DECLINED; // NGX_DECLINED를 반환하여 요청을 즉시 통과
}
/* 10. delay > 0: 타이머 설정하여 지연 처리 */
ngx_log_error(lrcf->delay_log_level, r->connection->log, 0, "delaying request%s, excess: %ui.%03ui, by zone \"%V\"", lrcf->dry_run ? ", dry run" : "", excess / 1000, excess % 1000, &limit->shm_zone->shm.name); // 지연 처리 로그 출력
if (lrcf->dry_run) { // dry_run 모드가 활성화된 경우
r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_DELAYED_DRY_RUN; // 요청 상태를 DELAYED_DRY_RUN으로 변경
return NGX_DECLINED; // NGX_DECLINED를 반환하여 요청을 통과시킴
}
r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_DELAYED; // 요청 상태를 NGX_HTTP_LIMIT_REQ_DELAYED로 변경
if (r->connection->read->ready) { // 읽을 데이터가 이미 준비되어 있는 경우
ngx_post_event(r->connection->read, &ngx_posted_events); // posted events 큐에 읽기 이벤트 추가
} else { // 읽을 데이터가 아직 준비되지 않은 경우
if (ngx_handle_read_event(r->connection->read, 0) != NGX_OK) { // 읽기 이벤트를 epoll에 등록, 실패 시
return NGX_HTTP_INTERNAL_SERVER_ERROR; // NGX_HTTP_INTERNAL_SERVER_ERROR 반환
}
}
r->read_event_handler = ngx_http_test_reading; // 읽기 이벤트 핸들러를 ngx_http_test_reading으로 설정
r->write_event_handler = ngx_http_limit_req_delay; // 쓰기 이벤트 핸들러를 ngx_http_limit_req_delay로 설정
r->connection->write->delayed = 1; // 지연 플래그를 1로 설정
ngx_add_timer(r->connection->write, delay); // write 이벤트에 delay 밀리초 타이머 추가
return NGX_AGAIN; // NGX_AGAIN을 반환하여 요청 처리 일시 중단
}참고 자료
Last updated on