LabHub
Get started
배우기 러닝패스 코스

Envoy Internals

Why a NACK's version_info Is Not the Rejected Version

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

ADS 는 Envoy 와 컨트롤 플레인 사이에 gRPC 스트림 하나를 열어 두고 모든 타입(CDS·LDS·…)의 설정을 그 위로 주고받는 방식이다. 응답에는 버전과 nonce 가 붙고, Envoy 는 적용해 본 결과를 같은 nonce 의 다음 요청으로 알린다 — 사유가 비었으면 ACK, 차 있으면 NACK.

왜 이게 필요했나

파일 구독도 설정을 무중단으로 바꿔 주지만, 수천 개의 프록시에 파일을 나눠 줄 방법이 없고, 프록시가 그 설정을 받아들였는지 알 길도 없다. 컨트롤 플레인이 가장 먼저 알아야 할 것이 바로 그것이다 — 내가 보낸 설정이 적용됐나, 거절됐나, 거절됐다면 왜인가.

그래서 xDS 에는 스트림 방식이 있다. 프록시가 먼저 연결을 열고(프록시 쪽에서 나가는 연결이라 방화벽 안쪽의 프록시도 붙을 수 있다), 컨트롤 플레인은 그 스트림 위로 필요할 때마다 밀어 넣는다. 타입마다 스트림을 따로 열 수도 있지만, 그러면 "클러스터를 먼저 받고 그것을 쓰는 리스너를 나중에" 같은 순서를 보장할 수 없다. ADS(Aggregated Discovery Service)는 모든 타입을 한 스트림에 모아 컨트롤 플레인이 순서를 정하게 한다. Istio 의 istiod 와 사이드카가 쓰는 것이 이 방식이다.

어떻게 동작하나

한 번 주고받기 — 상태 전체(State of the World, SotW) 방식 기준.

Envoy → 요청  type=Cluster  version_info=""   response_nonce=""     (처음)
서버  → 응답  type=Cluster  version_info="1"  nonce="1"  resources=[pool, slow]
Envoy → 요청  type=Cluster  version_info="1"  response_nonce="1"    ← ACK
          … 파일이 바뀌어 서버가 밀어 넣는다 …
서버  → 응답  type=Cluster  version_info="3"  nonce="5"  resources=[…]
Envoy → 요청  type=Cluster  version_info="2"  response_nonce="5"
              error_detail="…ConnectTimeout: value must be greater than 0s"   ← NACK

읽는 법이 셋이다.

response_nonce 어느 응답에 대한 답인가. 비어 있으면 새 구독 요청이다
error_detail 차 있으면 NACK. 사유가 문자열로 온다
version_info 마지막으로 받아들인 버전. NACK 에서도 거절한 버전이 아니다

마지막 줄이 가장 헷갈린다. NACK 요청의 version_info 는 거절한 3이 아니라 여전히 쓰고 있는 2다. 거절한 응답은 nonce 로 가리킨다.

상태 전체 방식에서 응답은 그 타입의 자원을 전부 담는다. 하나만 바꿔도 전부 다시 보낸다. 자원이 많은 메시에서는 이것이 부담이라 바뀐 것만 보내는 증분(Delta) 방식이 따로 있다.

타입마다 따로 적용된다. 같은 스냅숏 버전 3 에서 리스너는 문제없고 클러스터만 틀렸다면, LDS 는 3 을 받아들이고 CDS 는 2 에 머문다. 통계 cluster_manager.cds.version_textlistener_manager.lds.version_text 가 서로 다른 값을 보이는 순간이다.

컨트롤 플레인이 사라지면 프록시는 지금 설정으로 계속 돈다(control_plane.connected_state 가 0). 다시 붙을 때 Envoy 는 빈손으로 시작하지 않고 타입마다 마지막으로 받아들인 버전을 첫 요청에 담는다 — 서버는 그것을 보고 무엇을 다시 보낼지 판단할 수 있다.

핫 리스타트 — 프록시 자신을 바꿀 때(바이너리 교체, 부트스트랩 변경) 쓰는 장치다. 새 프로세스(epoch N+1)가 같은 --base-id 로 옛 프로세스를 찾아 리스너 소켓을 넘겨받고, 옛 프로세스는 --drain-time-s 동안 하던 요청을 마저 끝낸 뒤 --parent-shutdown-time-s 에 물러난다. 포트가 한순간도 닫히지 않는다. 대신 새 프로세스는 xDS 상태를 물려받지 않고 컨트롤 플레인에 새로 붙어 처음부터 받는다.

현장에서 만나는 모습

"설정을 바꿨는데 일부 프록시만 옛 동작입니다." NACK 가 가장 흔한 원인이다. Istio 에서는 istioctl proxy-status 가 프록시마다 CDS·LDS 가 SYNCED 인지 보여 주고, 거절이 있으면 istiod 로그에 사유가 남는다. 트래픽은 계속 흐르므로 누가 보지 않으면 몇 주가 간다.

"istiod 를 재시작했더니 모든 사이드카가 한꺼번에 다시 붙었습니다." 다시 붙으면서 각자 마지막 버전을 밝히므로, 컨트롤 플레인은 그 수천 개의 첫 요청에 답해야 한다. 컨트롤 플레인을 여러 대로 두는 이유다.

"Envoy 를 재시작하면 연결이 끊깁니다." 게이트웨이처럼 오래 붙어 있는 연결이 많은 곳에서는 핫 리스타트나 drain 을 거친 순차 교체가 필요하다. 쿠버네티스 사이드카는 파드째 바꾸므로 핫 리스타트를 쓰지 않는다 — Istio 가 --disable-hot-restart 로 띄우는 것도 그래서다.

공식 문서: xDS REST and gRPC protocol · Aggregated Discovery Service · Hot restart · Command line options

다음 실습에서 할 것

스냅숏 파일 하나를 읽어 CDS·LDS 를 미는 ADS 서버를 파이썬으로 직접 쓰고, 정적 설정 없이 그 서버에서 모든 것을 받는 Envoy 를 띄운다. 엔드포인트를 빼 재시작 없이 반영되는 것, 일부러 틀린 클러스터를 보내 NACK 와 그 사유가 돌아오는 것, 서버가 죽었다 살아날 때 Envoy 가 밝히는 버전을 로그에서 읽는다. 마지막으로 그 Envoy 를 핫 리스타트해 진행 중인 요청이 끊기지 않는 것을 본다.