AI 에이전트가 발견한 구글 쿠버네티스 엔진의 버그
Lovable 플랫폼의 인프라 엔지니어가 AI 디버깅 에이전트를 활용해 Google Kubernetes Engine(GKE) 내의 네트워킹 버그를 신속하게 추적해냈습니다. 문제의 원인은 Google이 Cilium 기반 네트워크 데몬(anetd)에 통합한 WireGuard 모듈의 동시성 제어 결함과 패킷 크기(MTU) 불일치였습니다. 이 사례는 대규모 인프라에서 발생하는 간헐적 장애를 분석할 때 AI 에이전트가 얼마나 강력한 도구가 될 수 있는지를 보여줍니다.
지난주, 우리 사용자들은 원인을 알 수 없는 에러를 겪기 시작했습니다. 때로는 프로젝트를 열 때 실패하고, 때로는 GitHub에서 코드를 클론(clone)할 때 시간 초과가 발생했습니다. 심지어 우리 모두가 두려워하는 'Connection reset by peer(연결이 상대방에 의해 재설정됨)' 에러까지 보였습니다. 명확한 패턴이 전혀 보이지 않았는데, 이는 항상 가장 최악의 패턴입니다. 피크 시간대에 매 초 50개 이상의 샌드박스를 생성하는 Lovable과 같은 플랫폼에서는 아주 적은 비율의 실패만으로도 사용자에게 큰 문제가 될 수 있습니다. 인프라 내부 어딘가가 불안정했고, 우리는 그 원인을 찾아야만 했습니다.
Sascha, 우리 인프라 엔지니어 중 한 명이 훌륭한 디버깅의 시작점이라 할 수 있는 로그 분석부터 시작했습니다. 하지만 수백만 줄의 로그를 살펴봐야 했고, 어떤 패턴도 쉽게 눈에 띄지 않았습니다. 그는 새로운 시도를 해보기로 했습니다. 그는 이미 디버깅을 위한 AI 에이전트(AI agent)를 실험하고 있었고, 지금이 적기라는 생각이 들었습니다. 그는 Clickhouse 로그에 접근할 수 있는 에이전트를 설정하고 질문을 던지기 시작했습니다.
에이전트는 의심스러운 문제를 하나 찾아냈습니다. Google Kubernetes Engine(GKE) 클러스터 내의 anetd 파드(pod)들이 끊임없이 재시작되고 있었던 것입니다. 6일 동안 파드당 약 120번의 재시작, 즉 거의 매 시간마다 한 번씩 충돌(crash)이 발생하고 있었습니다. 이게 말이 될 리가 없었습니다! 참고로 anetd는 Kubernetes 클러스터 내부의 네트워킹 레이어인 Cilium의 구글 구현체입니다. anetd가 충돌하면 새로운 파드가 네트워크 인터페이스를 얻지 못합니다. 그리고 제품 전체가 계속해서 새로운 샌드박스를 띄우는 데 의존하는 상황에서, 네트워크 불안정성은 즉각적인 사용자 오류로 이어집니다.
Sascha는 크래시 덤프(crash dump)를 파고들었습니다. 스택 트레이스(stack trace)는 동시성 맵 접근(Concurrent map-access) 패닉을 가리켰습니다. 여러 고루틴(goroutine)이 적절한 락(lock) 없이 동시에 동일한 데이터 구조에 읽기 및 쓰기를 시도하고 있었던 것입니다. 핵심은 이 패닉이 발생한 위치였습니다. 바로 anetd의 WireGuard 모듈 내부였습니다.
WireGuard 자체는 오픈소스 암호화 프로토콜로, 구글이 소유하지 않습니다. 하지만 구글은 이를 GKE용 네트워킹 데몬인 anetd에 통합하는 코드를 소유하고 있습니다. 패닉은 구글의 통합 코드, 특히 WireGuard 연결을 추적하는 맵(map) 데이터 구조에 대한 동시 접근을 관리하는 방식에서 발생했습니다. 이것이 중요한 이유는, 버그가 WireGuard 자체가 아니라 구글의 구현체에 있음을 의시하기 때문입니다. 즉, 이 문제를 해결하려면 구글의 도움이 필요했습니다.
우리는 구글의 담당 팀과 통화를 시작했습니다. 일요일이었고 사용자들에게 영향을 미치고 있는 상황이었기에 팀이 급히 구성되었습니다. 구글 측 대표의 권고는 명확했습니다. '투명한 노드 간 암호화(transparent node-to-node encryption)를 비활성화하라'는 것이었습니다. 이렇게 하면 WireGuard 모듈에서 직면한 버그를 완전히 우회할 수 있었습니다. 우리는 트레이드오프를 논의했습니다. 보안 관점에서 노드 간 암호화를 비활성화하는 것은 이상적이지 않았지만, 클러스터는 이미 구글의 프라이빗 네트워크에서 실행되고 있었고, 사용자가 에러를 겪고 있을 때 '안정성이 완벽함보다 낫다'는 판단을 내렸습니다.
변경 사항을 배포하고 모든 anetd 파드를 재시작한 후 대시보드를 지켜보았습니다. 충돌이 멈췄습니다. 약 4시간 동안 우리는 문제가 해결되었다고 생각했습니다. 일부 팀원은 로그오프했습니다. 그런 다음 다시 Slack 알림이 울리기 시작했습니다.
우리는 인메모리 데이터 스토어인 Valkey에 대한 무작위 연결 실패를 목격했습니다. 처음에는 Valkey 자체를 의심했습니다. CPU 사용량이 증가하고 있었기에, 포화 상태가 아닌지 확인하기 위해 노드 수를 두 배로 늘렸습니다. 안타깝게도 에러는 계속 발생했습니다.
통화에 참여 중이었던 또 다른 엔지니어 Erik은 직감이 있었습니다. 응용 프로그램 코드를 전혀 변경하지 않았기 때문에, 문제는 스택의 더 깊은 곳에 있을 것이라는 점이었습니다. 아마도 네트워크 문제일 것이라고 추측했습니다. 그는 몇몇 노드에서 tcpdump를 실행하여 패킷 캡처를 시작했습니다. 나머지 팀원들이 다른 가능성을 추적하는 동안, Erik은 Wireshark로 트래픽을 필터링했습니다. 그리고 마침내 결정적인 증거를 발견했습니다. 'Destination unreachable (Fragmentation needed)(목적지에 도달할 수 없음 - 단편화 필요)' 에러였습니다. 그 순간 모든 것이 명확해지기 시작했습니다.
바로 MTU 불일치 문제, 즉 '우리에겐 더 큰 패킷이 필요합니다'라는 상황이었습니다. 상황은 이랬습니다. WireGuard가 활성화되었을 때, 클러스터는 WireGuard의 오버헤드를 수용하기 위해 1420바이트의 MTU(최대 전송 단위)를 사용하고 있었습니다...