# P0 — nginx 1.28.0 소스 대조: `ngx_http_upstream_zone_resolve_handler` - 대상: `nginx-1.28.0.tar.gz` (sha256 `c6b5c6b086c0df9d3ca3ff5e084c1d0ef909e6038279c71c1c3e985f576ff76a`, nginx.org 공식) - 파일: `src/http/modules/ngx_http_upstream_zone_module.c` (라인 번호는 1.28.0 원본 기준) ## 확인 1 — 제거 루프와 추가 루프가 단일 wlock 임계구역 안에 있는가: **사실** 핸들러 진입 직후 wlock 획득(770행), 기존 peer 제거 루프(843–899행), 신규 peer 추가 루프(901–979행)를 모두 수행한 뒤 `done:` 레이블에서 unlock(996행). 비-SRV 구성(`host->service.len == 0`)에서는 중간 unlock 경로가 없음(981–990행의 unlock/relock은 SRV 서비스 + backup peers 전환 전용). ```c 770 ngx_http_upstream_rr_peers_wlock(peers); ... 841 again: 843 for (peerp = &peers->peer; *peerp; /* void */ ) { /* 제거 루프 */ ... 889 *peerp = peer->next; 890 ngx_http_upstream_zone_remove_peer_locked(peers, peer); ... 901 for (i = 0; i < ctx->naddrs; i++) { /* 추가 루프 */ ... 992 done: 994 host->valid = ctx->valid; 996 ngx_http_upstream_rr_peers_unlock(peers); ``` 요청 측 peer 선택도 **같은 `peers->rwlock`** 을 사용: - `ngx_http_upstream_get_round_robin_peer()` — `ngx_http_upstream_rr_peers_wlock(peers)` (`ngx_http_upstream_round_robin.c:618`) - 락 매크로 정의: `ngx_http_upstream_round_robin.h:107–116` (`peers->shpool` 존재 시 `ngx_rwlock_{rlock,wlock}(&peers->rwlock)`) → **결론: 재조회로 인한 "peer 0개 중간 상태"는 요청이 관찰할 수 없음. 스왑은 요청 관점에서 원자적.** "0-peer 스왑 창" 가설은 1.28.0에서 구조적으로 배제됨. ## 확인 2 — NXDOMAIN 시 naddrs=0으로 진행되어 전체 제거로 이어지는가: **사실** ```c 787 if (ctx->state) { ... 795 ngx_log_error(NGX_LOG_ERR, event->log, 0, 796 "%V could not be resolved (%i: %s)", 797 &ctx->name, ctx->state, 798 ngx_resolver_strerror(ctx->state)); ... 801 if (ctx->state != NGX_RESOLVE_NXDOMAIN) { 802 ngx_http_upstream_rr_peers_unlock(peers); 804 ngx_resolve_name_done(ctx); 806 ngx_add_timer(event, ngx_max(uscf->resolver_timeout, 1000)); 807 return; /* SERVFAIL/timeout 등: peer 유지 */ 808 } 810 /* NGX_RESOLVE_NXDOMAIN */ 812 ctx->naddrs = 0; /* 빈 주소 집합으로 계속 진행 */ 813 } ``` - NXDOMAIN 이외 오류(SERVFAIL, resolver timeout 등)는 801–807행에서 **peer를 건드리지 않고 조기 return** → 재시도 타이머 `max(resolver_timeout, 1000ms)` (P3의 안전 경로 근거) - NXDOMAIN은 `naddrs = 0`으로 제거 루프에 진입 → 해당 host의 **모든 peer 제거**, 추가는 0건 → 다음 성공 재조회까지 빈 peer 집합 유지 (P2 가설 A의 소스 근거) - error.log 시그니처: `"... could not be resolved (3: Host not found)"` — NLU 502의 원인 판별 키 ## 확인 3 — `memory exhausted` 경로(할당 실패 시 추가 포기)의 존재: **사실** ```c 915 ngx_shmtx_lock(&peers->shpool->mutex); 916 peer = ngx_http_upstream_zone_copy_peer(peers, NULL); 917 ngx_shmtx_unlock(&peers->shpool->mutex); 919 if (peer == NULL) { 920 ngx_log_error(NGX_LOG_ERR, event->log, 0, 921 "cannot add new server to upstream \"%V\", " 922 "memory exhausted", peers->name); 923 goto done; /* 남은 추가 전부 포기 */ 924 } ... 942 peer->server.data = ngx_slab_alloc(peers->shpool, server->len); 943 if (peer->server.data == NULL) { 944 ngx_http_upstream_rr_peer_free(peers, peer); 946 ngx_log_error(..., "memory exhausted", peers->name); 949 goto done; 950 } ``` 제거 루프는 이미 실행된 뒤이므로, slab(zone) 고갈 시 "제거는 됐는데 추가가 안 되는" 부분/전체 결손이 구조적으로 가능 (가설 B의 소스 근거). error.log 시그니처: `cannot add new server to upstream "be", memory exhausted` ## 부수 확인 — 재조회 주기와 max_fails 관련 - 다음 재조회 타이머(1002행): `timer = 1000 * (ctx->valid > now ? ctx->valid - now + 1 : 1)` → resolver `valid=5s`일 때 실측 재질의 주기는 **약 6초**(valid + 1s) 예상. dns.log로 실측 검증 예정 - 추가되는 peer는 template의 `max_fails`/`fail_timeout`을 상속(965–966행). peer 수가 2 이상이 되면 `ngx_http_upstream_zone_set_single()`(596–611행)이 `peers->single = 0`으로 설정 → 요청 실패 시 `max_fails` 회로가 활성화됨 (가설 C의 소스 근거) - "no live upstreams" 방출 지점: `ngx_http_upstream.c:1611` (peer.get이 `NGX_BUSY` 반환 시, `ngx_http_upstream_round_robin.c:699` — 전 peer가 fail 상태이거나 peer가 0개일 때)