Veltrix와 트레이스 루프(Trace Loops)가 깨진 날
요약
Veltrix 2.4.1 배포 과정에서 발생한 설정 계층의 결함과 시스템 성능 저하 문제를 다룹니다. DNS 폴링과 잘못된 임계값 설정으로 인한 레이턴시 급증 문제를 해결하기 위해 기존 엔진을 제거하고 새로운 어드미션 컨트롤러를 도입한 과정을 설명합니다.
핵심 포인트
- 잘못된 설정 스키마와 혼합 워크로드로 인한 시스템 불안정성 발생
- DNS 폴링 방식이 p99 레이턴시를 45ms에서 3.2초로 급증시킴
- 모델 스냅샷 다운로드로 인한 포드 생성 지연 문제 확인
- HPA와 트레이스 루프 간의 충돌로 인한 워커 종료 문제 해결
- 단일 어드미션 컨트롤러 도입을 통한 아키텍처 개선
우리가 실제로 해결하고 있었던 문제
지난 10월, 우리는 treasure-hunt-engine이라는 새로운 설정 계층(configuration layer)과 함께 Veltrix 2.4.1을 프로덕션(production) 환경에 배포했습니다. 마케팅 팀의 설명은 매끄러웠습니다. 무한한 서버 확장성(scalability), 제로 콜드 스타트(zero cold starts), 모든 트래픽 급증의 즉각적인 탐지 등이 그것이었습니다. 하지만 우리가 실제로 물려받은 것은 인그레스(ingress) 속도가 200 RPS를 넘어서는 순간 16코어 박스에서 400개의 워커 프로세스(worker processes)를 실행하는 기본 동작을 가진 시스템이었습니다. 우리의 옵저버빌리티(observability) 스택은 중복된 스팬(spans)의 무게를 견디지 못하고 즉시 무너졌으며, 워커 풀 디스커버리(worker pool discovery) 알고리즘이 50ms마다 DNS를 폴링(polling)하는 비지 웨이트 루프(busywait loop)를 사용했기 때문에 p99 레이턴시(latency)가 45ms에서 3.2초로 급증했습니다. 팀은 서비스 디스커버리(service discovery) 캐시에서 왜 포드(pod) 이름이 계속 충돌하는지 디버깅하는 데 사흘을 보냈습니다. 근본 원인은 알고리즘이 아니었습니다. 그것은 모든 운영자가 cpu-threshold = 90 및 memory-threshold = 75로 설정할 것이라고 가정했던 설정 스키마(configuration schema)였습니다. 우리의 박스들은 해당 수치에 도달한 적이 없었는데, 왜냐하면 Go 마이크로서비스(micro-services) 옆에 25%의 Java 레거시 노드(legacy node)가 있는 혼합 워크로드(mixed workloads)를 실행하고 있었기 때문입니다. treasure hunt engine은 우리 하드웨어에 존재하지 않는 보물을 찾고 있었던 것입니다.
우리가 처음에 시도했던 것 (그리고 왜 실패했는가)
우리가 처음 배포한 첫 번째 패치는 DNS 폴링 (DNS poll)을 완전히 비활성화하고 이를 Kubernetes 포드 리소스 요청 (pod resource requests)으로 대체한 것이었습니다. 우리는 클러스터 오토스케일러 (cluster-autoscaler) 동작에 맞추기 위해 cpu-threshold = 60 및 memory-threshold = 60으로 설정했습니다. 그 즉각적인 결과로 워커 이탈 (worker churn)은 40% 감소했지만, 요청 라우팅 지연 시간 (request routing latency)은 800ms 증가했습니다. 모든 새로운 포드가 트래픽을 수용하기 전에 S3로부터 27MB 크기의 모델 스냅샷 (model snapshot)을 다운로드해야 했기 때문입니다. 두 번째 시도에서는 HPA 컨트롤러 (HPA controller)가 관리하는 사전 워밍 풀 (pre-warmed pool)로 전환했습니다. 이는 하루 동안은 잘 작동했으나, 오토스케일러가 사전 워밍 풀을 0으로 축소(scale down)하려고 시도했을 때, 새로운 HPA 타겟이 재계산되기 6밀리초 전에 트레이스 루프 (trace loop)가 모든 워커를 종료시키면서 문제가 발생했습니다. 다음 실제 트래픽 급증이 발생했을 때 우리의 p95 지연 시간 (p95 latency)은 4.1초까지 치솟았는데, 이는 가장 최근의 LLM 임베딩 (LLM embeddings)을 보유하고 있던 웜 캐시 (warm cache)를 우리가 종료시켜 버렸기 때문입니다.
아키텍처 결정 (The Architecture Decision)
우리는 treasure-hunt-engine 구성 레이어를 완전히 제거하고, veltrim이라고 부르는 단일 어드미션 컨트롤러 (admission controller)로 교체했습니다. 이 어드미션 컨트롤러는 HPA 컨트롤러 앞에 위치하며, 다음 두 가지 서술어 (predicates)를 충족하는 축소 (scale-down) 이벤트만 허용합니다: (1) 포드의 15분간 CPU 사용량이 요청량 (request)의 30% 미만일 것, (2) 포드에서 방출된 스팬 (span) 중 llm-cache-miss = true 태그를 포함하는 것이 없을 것. 이 서술어들은 우리가 kube-apiserver에 내장시킨 작은 Lua 정책 엔진 (Lua policy engine)에 의해 평가됩니다. 또한 우리는 포드 템플릿 (pod template)을 변경하여 두 가지 리소스를 선언했습니다: requests.cpu = 500 m 및 requests.memory = 1200 Mi. 이것은 오토스케일러가 모델 스냅샷 작업 중 즉시 OOM-kill (Out Of Memory kill)되는 포드들을 스케줄링하는 것을 막을 수 있는 유일한 방법이었습니다. 우리는 마지막 트래픽 감소 이후 워커 풀 (worker pool)을 최소 30분 동안 유지했으며, 노이지 네이버 (noisy neighbor) 문제를 피하기 위해 max-pods-per-node = 12로 설정했습니다. Lua 엔진은 모든 축소 요청에 6ms를 추가하지만, 새로운 포드가 준비될 때까지 기다리는 동안 이전 시스템이 겪었던 1.8초의 지연 시간을 절약해 줍니다.
그 이후의 수치들이 말해준 것 (What The Numbers Said After)
2주 후 p95 지연 시간(latency)은 최고점인 4.1초에서 57ms로 돌아왔습니다. 워커 이탈률(worker churn rate)은 시간당 180개의 포드(pod)에서 12개로 감소했습니다. 유용한 웜 포드(warm pod)를 종료하는 일을 중단했기 때문에, 클러스터 오토스케일러(cluster-autoscaler)의 스케일 업(scale-up) 이벤트 시간은 평균 4.3분에서 1.7분으로 단축되었습니다. 어드미션 컨트롤러(admission controller)는 여전히 활성 LLM 컨텍스트(context)를 보유하고 있는 포드를 종료하려 했던 237개의 스케일 다운(scale-down) 요청을 거부했으며, 이를 통해 피크 시간 동안 최소 11건의 캐시 미스(cache miss)를 방지했습니다. 에러 예산(error budget)에 대한 SLO 번 레이트(burn rate)는 하룻밤 사이에 트래픽이 두 배로 증가했을 때도 0.2% 미만으로 유지되었습니다. 유일한 퇴보(regression)는 Prometheus 스크랩 간격(scrape interval)에서 나타났습니다. 수백 개의 스케일 이벤트가 동시에 발생할 때 Lua 엔진이 apiserver에서 눈에 띄는 CPU 스파이크(spike)를 생성했기 때문에, 이를 15초에서 30초로 늘려야 했습니다.
내가 다르게 했을 것 (What I Would Do Differently)
나는 원래의 treasure-hunt-engine 스택을, 별도의 ClickHouse 클러스터에 트레이스 로그(trace logs)를 전송하면서 트래픽의 5%를 대상으로 5일 동안 실행되는 카나리(canary) 테스트 없이 절대 프로덕션(production)에 투입하지 않았을 것입니다. 우리는 DNS 디스커버리 루프(discovery loop)가 스테이징(staging) 환경의 합성 부하(synthetic load) 하에서 잘 작동했기 때문에 안전하다고 가정했습니다. 하지만 스테이징 환경에는 결코 25%의 Java 노스탤지어(nostalgia)가 존재하지 않았습니다. 지나고 보니, 우리는 리소스 임계값(resource thresholds)에 대한 단일 진실 공급원(single source-of-truth) 스키마를 정의하고 이를 Terraform 모듈에 내장했어야 했습니다. 그래야 운영자가 데모에서는 좋아 보이지만 확장 시 클러스터를 녹여버릴 값으로 실수로 덮어쓰는 일을 방지할 수 있었을 것입니다. 마지막으로, 나는 모든 어드미션 컨트롤러 정책 주변에 서킷 브레이커(circuit breaker)를 설치할 것을 주장할 것입니다. Lua 엔진의 지연 시간이 20ms를 초과하면, 스케일 다운을 차단하는 대신 기본적으로 허용하도록 설정해야 합니다. 그렇지 않으면 컨트롤러 자체가 유발한 연쇄적인 지연 시간 스파이크(cascading latency spikes)의 위험을 감수해야 합니다. 보물 찾기(treasure hunt)는 보물을 찾아야지, 배를 침몰시킬 이유를 찾아서는 안 됩니다.
나는 AI 도구를 평가하는 것과 동일한 방식으로 이것을 평가했습니다: 무엇이 실패하는가, 얼마나 자주 발생하는가, 그리고 실패했을 때 어떤 일이 일어나는가. 이 항목은 통과입니다: https://payhip.com/ref/dev3
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기