스냅숏 하나, 스트림 하나
목표
스냅숏 파일을 읽어 CDS·LDS 를 미는 가장 작은 ADS 서버를 직접 쓰고, 그 서버에서 모든 설정을 받는 Envoy 로 ACK·NACK·재접속·핫 리스타트를 스트림 위에서 관찰한다.
왜 중요한가
istiod 와 사이드카 사이에서 일어나는 일이 이것이다. 그런데 메시에서는 그 대화가 보이지 않아 '설정을 바꿨는데 일부만 옛 동작' 같은 증상을 만나면 어디서부터 볼지 모른다. 직접 짠 서버의 로그에서 nonce·ACK·NACK·version_info 를 한 번 읽어 보면, proxy-status 의 SYNCED 와 istiod 의 거절 로그가 무엇을 가리키는지 알게 된다.
단계
- 업스트림 셋을 띄우세요 —
python3 /opt/lab/envoy/upstream.py 8096 ok,… 8097 ok,… 8098 slow. 그리고/root/envd-ads/snapshot.json에 버전"1"의 스냅숏을 쓰세요:clusters에pool(8096·8097 두 엔드포인트,connect_timeout1s)과slow(8098),listeners에127.0.0.1:10088의edge— 경로/marker는 본문snapshot=1을 직접 돌려주고,/slow는slow로, 나머지는pool로 보냅니다. 자원은 Envoy 의 JSON 모양 그대로 씁니다. /root/envd-ads/ads_server.py에envoy.service.discovery.v3.AggregatedDiscoveryService의StreamAggregatedResources를 구현해127.0.0.1:18000에서 띄우세요(/opt/xds/bin/python3로 실행). 요구 사항은 넷 — (1) 스냅숏 파일을 읽어 요청받은 타입(CDS·LDS)의 자원 전부를version_info·nonce와 함께 보낸다, (2) 파일의 버전이 바뀌면 열린 스트림에 요청을 기다리지 않고 새 응답을 민다, (3) 받은 요청마다/root/envd-ads/ads.log에 JSON 한 줄 —event(nonce 가 비면request, error_detail 이 있으면nack, 아니면ack)·stream·node·type·version_info·response_nonce·error— 을 남긴다, (4) 응답을 보낼 때도event: sent와 버전·nonce 를 남긴다./root/envd-ads/bootstrap.yaml에 노드 idenvd-ads-1(clusterenvd-ads), 관리 포트9988,dynamic_resources의ads_config(gRPC·V3, 클러스터xds_cluster)와cds_config·lds_config를ads: {}로 적고, 정적 자원은 컨트롤 플레인에 닿기 위한xds_cluster(127.0.0.1:18000, HTTP/2) 하나만 둡니다.envoy -c /root/envd-ads/bootstrap.yaml --base-id 50 --restart-epoch 0 --concurrency 1 --drain-time-s 5 --parent-shutdown-time-s 10로 띄우세요.- 스냅숏을 버전
"2"로 바꾸세요 —pool에서 8097 를 빼고,/marker본문을snapshot=2로. 파일은 새로 쓴 뒤mv로 갈아 끼웁니다. Envoy 가 받아들인 뒤/root/envd-ads/04-push.txt에marker=(/marker응답 본문)와endpoints=(/clusters에 남은 pool 엔드포인트 수) 두 줄을 적으세요. - 스냅숏을 버전
"3"으로 바꾸되pool의connect_timeout을"0s",/marker본문을snapshot=3으로 하세요. 그다음/root/envd-ads/05-nack.txt에 네 줄을 적으세요 — ads.log 에서 찾은 CDS NACK 의version_info를nack_version_info=, 거절 사유에서ConnectTimeout으로 시작하는 구절을reason=, 그리고 지금 Envoy 의cluster_manager.cds.version_text·listener_manager.lds.version_text를cds_version=·lds_version=으로. - ADS 서버를 멈추고
/root/envd-ads/06-down.txt에traffic=(/요청의 상태 코드)와connected=(control_plane.connected_state) 두 줄을 적으세요. 그다음 스냅숏을 버전"4"(3 에서 connect_timeout 을1s로 되돌리고/marker는snapshot=4)로 고친 뒤 서버를 다시 띄우고, Envoy 가 다시 붙어 버전 4 를 받아들일 때까지 기다리세요. 다시 붙은 스트림의 첫 CDS 요청이 담아 온version_info를resume_cds_version=으로 같은 파일에 덧붙입니다. /slow(3초 걸림)로 요청 하나를 백그라운드로 보내 결과를/root/envd-ads/inflight.txt에 받게 하고, 그 요청이 도는 동안 같은 부트스트랩으로 epoch 1 인 Envoy 를 띄우세요(--base-id 50 --restart-epoch 1 --concurrency 1 --drain-time-s 5 --parent-shutdown-time-s 10). 새 프로세스가 LIVE 가 되고 옛 프로세스가 물러난 뒤/root/envd-ads/07-restart.txt에epoch=(관리 포트/server_info의 restart_epoch),inflight=(백그라운드 요청의 상태 코드),new_stream_version=(새 프로세스가 연 스트림의 첫 CDS 요청 version_info — 비었으면empty) 세 줄을 적으세요./root/envd-ads/08-report.md에 다섯 줄 —first_request_version=(Envoy 가 맨 처음 보낸 CDS 요청의 version_info, 비었으면empty),nack_version_info=,resume_cds_version=(5·6단계 기록),epoch_after_restart=,inflight_code=(7단계 기록) — 을 적고, 그 아래 배운 것을 네 줄 이상 적으세요.
참고
- ADS 서버는
/opt/xds/bin/python3로 실행합니다(grpcio 와 Envoy API 의 protobuf 가 든 가상 환경). - 스냅숏은 새 파일에 쓴 뒤
mv로 갈아 끼우세요. 서버가 반쯤 쓰인 파일을 읽으면 JSON 오류로 건너뜁니다(서버 로그에 snapshot_error 가 남습니다). - Envoy 는 3단계에서
--restart-epoch 0으로 띄우고 7단계 전까지 다시 띄우지 마세요. 다시 띄우면 7단계의 epoch 이 어긋납니다. 끄려면curl -X POST localhost:9988/quitquitquit입니다. - ads.log 는 JSON 한 줄씩입니다.
jq -c 'select(.event=="nack")' ads.log처럼 골라 보세요. - 흔한 실수 —
pkill -f ads_server.py. 그 문자열이 든 셸도 함께 죽습니다.'[a]ds_server.py'로 쓰세요.
밀어 넣을 설정을 스냅숏으로 적는다
업스트림 셋을 띄우세요 — python3 /opt/lab/envoy/upstream.py 8096 ok, … 8097 ok, … 8098 slow. 그리고 /root/envd-ads/snapshot.json 에 버전 "1" 의 스냅숏을 쓰세요: clusters 에 pool(8096·8097 두 엔드포인트, connect_timeout 1s)과 slow(8098), listeners 에 127.0.0.1:10088 의 edge — 경로 /marker 는 본문 snapshot=1 을 직접 돌려주고, /slow 는 slow 로, 나머지는 pool 로 보냅니다. 자원은 Envoy 의 JSON 모양 그대로 씁니다.
컨트롤 플레인의 일은 결국 '지금 이 프록시가 가져야 할 자원 전부' 를 버전 하나로 묶어 두는 것입니다. 그 묶음을 스냅숏이라고 부릅니다. 자원은 Cluster·Listener protobuf 의 JSON 표현 그대로 적으면 되고, 리스너 안의 typed_config 에는 @type 이 필요합니다. 채점기는 이 JSON 을 xds-protos 로 실제 protobuf 로 읽어 봅니다 — 필드 이름 하나가 틀려도 거기서 걸립니다.
가장 작은 ADS 서버를 쓴다
/root/envd-ads/ads_server.py 에 envoy.service.discovery.v3.AggregatedDiscoveryService 의 StreamAggregatedResources 를 구현해 127.0.0.1:18000 에서 띄우세요(/opt/xds/bin/python3 로 실행). 요구 사항은 넷 — (1) 스냅숏 파일을 읽어 요청받은 타입(CDS·LDS)의 자원 전부를 version_info·nonce 와 함께 보낸다, (2) 파일의 버전이 바뀌면 열린 스트림에 요청을 기다리지 않고 새 응답을 민다, (3) 받은 요청마다 /root/envd-ads/ads.log 에 JSON 한 줄 — event(nonce 가 비면 request, error_detail 이 있으면 nack, 아니면 ack)·stream·node·type·version_info·response_nonce·error — 을 남긴다, (4) 응답을 보낼 때도 event: sent 와 버전·nonce 를 남긴다.
gRPC 의 양방향 스트림은 파이썬에서 '요청 반복자를 받아 응답을 yield 하는 함수' 입니다. 그런데 요청을 기다리는 동안에도 스냅숏 변화를 밀어야 하므로, 요청은 다른 스레드가 읽어 큐에 넣고 본 루프는 큐를 짧게 기다리며 스냅숏을 확인하는 모양이 편합니다. 자원은 google.protobuf.any_pb2.Any 에 Pack 해서 담고, JSON 을 protobuf 로 바꿀 때는 json_format.ParseDict 를 씁니다(리스너 안의 HCM·router 타입을 먼저 import 해 둬야 풀립니다). 채점기는 이 서버에 직접 스트림을 열어 CDS 를 요청해 봅니다.
정적 설정 없이 컨트롤 플레인에서 모든 것을 받는다
/root/envd-ads/bootstrap.yaml 에 노드 id envd-ads-1(cluster envd-ads), 관리 포트 9988, dynamic_resources 의 ads_config(gRPC·V3, 클러스터 xds_cluster)와 cds_config·lds_config 를 ads: {} 로 적고, 정적 자원은 컨트롤 플레인에 닿기 위한 xds_cluster(127.0.0.1:18000, HTTP/2) 하나만 둡니다. envoy -c /root/envd-ads/bootstrap.yaml --base-id 50 --restart-epoch 0 --concurrency 1 --drain-time-s 5 --parent-shutdown-time-s 10 로 띄우세요.
부트스트랩에 남는 것은 '컨트롤 플레인을 찾아가는 길' 뿐입니다. 리스너도 업무용 클러스터도 전부 ADS 로 받습니다. ads: {} 는 'CDS·LDS 를 따로 구독하지 말고 ADS 스트림 하나로 받아라' 는 뜻이라, 두 타입이 한 스트림에서 순서대로 오갑니다. --base-id·--restart-epoch 는 7단계의 핫 리스타트를 위해 지금부터 붙여 둡니다. 잘 붙었는지는 통계 control_plane.connected_state 와 curl localhost:9988/config_dump 의 동적 자리로 봅니다.
재시작 없이 엔드포인트를 뺀다
스냅숏을 버전 "2" 로 바꾸세요 — pool 에서 8097 를 빼고, /marker 본문을 snapshot=2 로. 파일은 새로 쓴 뒤 mv 로 갈아 끼웁니다. Envoy 가 받아들인 뒤 /root/envd-ads/04-push.txt 에 marker=(/marker 응답 본문)와 endpoints=(/clusters 에 남은 pool 엔드포인트 수) 두 줄을 적으세요.
서버는 파일이 바뀐 것을 스스로 알아채고 열린 스트림에 새 응답을 보냅니다. Envoy 가 받아들였는지는 ads.log 의 ack(version_info 가 2)와 통계 cluster_manager.cds.version_text 로 확인합니다. 반쯤 쓰인 파일을 서버가 읽지 않도록 새 파일에 쓴 뒤 옮겨 놓으세요.
Envoy 가 거절한 설정은 어떻게 돌아오나
스냅숏을 버전 "3" 으로 바꾸되 pool 의 connect_timeout 을 "0s", /marker 본문을 snapshot=3 으로 하세요. 그다음 /root/envd-ads/05-nack.txt 에 네 줄을 적으세요 — ads.log 에서 찾은 CDS NACK 의 version_info 를 nack_version_info=, 거절 사유에서 ConnectTimeout 으로 시작하는 구절을 reason=, 그리고 지금 Envoy 의 cluster_manager.cds.version_text·listener_manager.lds.version_text 를 cds_version=·lds_version= 으로.
0초 시간 제한은 protobuf 로는 멀쩡해서 서버의 JSON 변환을 통과합니다. 걸리는 곳은 Envoy 의 검증 규칙이고, Envoy 는 그 응답을 적용하지 않은 채 같은 nonce 로 다시 요청하면서 error_detail 에 사유를 담아 돌려줍니다. 그때의 version_info 가 무엇인지 보세요. 그리고 CDS 와 LDS 는 같은 스냅숏에서 왔어도 따로 적용됩니다 — 리스너는 문제가 없으니 받아들여집니다. /marker 로 무엇이 바뀌었는지도 확인해 보세요.
컨트롤 플레인이 죽었다가 돌아온다
ADS 서버를 멈추고 /root/envd-ads/06-down.txt 에 traffic=(/ 요청의 상태 코드)와 connected=(control_plane.connected_state) 두 줄을 적으세요. 그다음 스냅숏을 버전 "4"(3 에서 connect_timeout 을 1s 로 되돌리고 /marker 는 snapshot=4)로 고친 뒤 서버를 다시 띄우고, Envoy 가 다시 붙어 버전 4 를 받아들일 때까지 기다리세요. 다시 붙은 스트림의 첫 CDS 요청이 담아 온 version_info 를 resume_cds_version= 으로 같은 파일에 덧붙입니다.
이미 설정을 받은 Envoy 는 컨트롤 플레인 없이도 그 설정으로 돕니다 — 멈추는 것은 새 설정을 받는 일뿐입니다. 다시 붙을 때 Envoy 는 빈손으로 시작하지 않고 타입마다 마지막으로 받아들인 버전을 첫 요청에 담아 보냅니다. 서버를 다시 띄우면 로그에 server_started 가 새로 찍히니, 그 뒤의 첫 Cluster request 를 보면 됩니다. pkill -f 의 패턴은 '[a]ds_server.py' 처럼 첫 글자를 대괄호로 감싸세요.
프록시도 끊지 않고 새 프로세스로 바꾼다
/slow(3초 걸림)로 요청 하나를 백그라운드로 보내 결과를 /root/envd-ads/inflight.txt 에 받게 하고, 그 요청이 도는 동안 같은 부트스트랩으로 epoch 1 인 Envoy 를 띄우세요(--base-id 50 --restart-epoch 1 --concurrency 1 --drain-time-s 5 --parent-shutdown-time-s 10). 새 프로세스가 LIVE 가 되고 옛 프로세스가 물러난 뒤 /root/envd-ads/07-restart.txt 에 epoch=(관리 포트 /server_info 의 restart_epoch), inflight=(백그라운드 요청의 상태 코드), new_stream_version=(새 프로세스가 연 스트림의 첫 CDS 요청 version_info — 비었으면 empty) 세 줄을 적으세요.
핫 리스타트는 새 프로세스가 옛 프로세스에게서 리스너 소켓을 넘겨받는 방식입니다. 그래서 포트가 한순간도 닫히지 않고, 옛 프로세스는 drain 시간 동안 하던 요청을 마저 끝낸 뒤 물러납니다. 두 프로세스는 --base-id 로 서로를 찾으므로 같아야 하고, epoch 은 하나씩 올립니다. 새 프로세스는 옛 프로세스의 xDS 상태를 물려받지 않습니다 — 컨트롤 플레인에 새로 붙어 처음부터 받습니다. 옛 프로세스가 물러났는지는 pgrep -af 'restart-epoch 0' 으로 봅니다.
스트림 위에서 본 것을 정리한다
/root/envd-ads/08-report.md 에 다섯 줄 — first_request_version=(Envoy 가 맨 처음 보낸 CDS 요청의 version_info, 비었으면 empty), nack_version_info=, resume_cds_version=(5·6단계 기록), epoch_after_restart=, inflight_code=(7단계 기록) — 을 적고, 그 아래 배운 것을 네 줄 이상 적으세요.
값은 ads.log 와 증거 파일에서 옮기세요. 설명 줄에는 'NACK 의 version_info 가 왜 거절한 버전이 아닌가' 와 '컨트롤 플레인 장애와 프록시 재시작이 각각 무엇을 잃게 하는가' 를 자기 말로 적어 두세요.