{"id":"IL-0268","org":"AdGuard DNS","date":"2025-11-25","docs":[],"title":{"en":"AdGuard DNS resolvers in Amsterdam, Frankfurt and London overloaded and answered with timeouts and SERVFAIL","ko":"유럽 여러 거점의 AdGuard DNS 리졸버가 과부하에 걸려 DNS 조회가 타임아웃되거나 SERVFAIL 을 냈다"},"domain":["networking"],"impact":{"scope":"AdGuard DNS resolution in several European locations: intermittent timeouts and SERVFAIL for a significant portion of traffic, for up to 2.5 hours","users":"some AdGuard DNS users","region":"Europe, primarily Amsterdam, Frankfurt and London"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"How nodes behaved depended on the interaction between available RAM, Go's garbage collector and the connlimit setting. On 64-GiB nodes a high limit let the heap grow large under heavy concurrency and pipelining, and the garbage collector then spent more and more CPU, a known worst case for high-memory applications in which memory is not exhausted but latency rises and throughput falls. On 32-GiB nodes the same configuration reached practical memory limits sooner because it had not been scaled down. Together with high pipelining and client retries, GC pressure, connection saturation and retries formed a feedback loop.","ko":"노드마다 다르게 움직인 데에는 사용할 수 있는 RAM, Go 가비지 컬렉터, connlimit 설정이 서로 맞물린 것이 컸다. 메모리가 큰 서버에서 connlimit 을 너무 높게 잡으면 힙이 크게 자란 뒤에야 가비지 컬렉션이 강하게 돈다. 그러면 메모리는 남아도 CPU 시간이 가비지 컬렉션에 점점 더 쓰여, 지연이 늘고 처리량이 준다. AdGuard 는 이것을 메모리를 많이 쓰는 애플리케이션에서 connlimit 이 만드는 알려진 최악의 경우라고 했다. 메모리가 작은 서버는 캐시 크기와 연결 한도를 RAM 에 맞춰 줄이지 않아서 더 일찍 실패했다. 여기에 높은 파이프라이닝과 많은 재시도가 겹쳐 가비지 컬렉션 압박, 연결 포화, 클라이언트 재시도가 서로를 키우는 고리가 생겼다."},"related":[],"sources":[{"url":"https://adguard-dns.io/en/blog/partial-adguard-dns-outage-11-25.html","kind":"blog","archived":"https://web.archive.org/web/20260606144456/https://adguard-dns.io/en/blog/partial-adguard-dns-outage-11-25.html"}],"summary":{"en":"AdGuard DNS resolvers need users' filtering settings locally before they can apply filters. At startup a resolver loads a cached copy of all user data from a local file and then asks the business-logic service only for changes since that snapshot; if the cache is stale or corrupted, it must request the full dataset, which is slow and puts much more load on both sides. A new version deployed on 25 November to improve this saved only the recently changed subset, often very small or empty, into the cache file, so many resolvers started with little or no user data. The first deployment began at 14:30 UTC and failed about an hour later on a network issue, leaving about half the servers on the new version. When the deployment was retried between 15:30 and 16:00, many resolvers found their caches empty or invalid at once and full-synced, which spiked traffic to the business-logic service and resource use on the resolvers.\n\nAlerts fired for DNS error rates and server load, and the investigation found the caching bug. Between 16:00 and 16:30 the team decided to roll back the buggy version on all servers at the same time. Under full syncs and client retries, the mixed Amsterdam cluster failed first on its 32-GiB nodes, whose cache sizes and connection limits had not been scaled down from the 64-GiB settings, and then on the 64-GiB nodes; part of Amsterdam's traffic was rerouted to other European locations, overloading Frankfurt and London. There, connection limits and a pipeline depth of 5 produced many concurrent goroutines and a large heap. AdGuard describes connlimit as the setting that controls how much memory the Go runtime uses for the managed heap before collecting aggressively; set high on 64-GiB nodes, it let the heap grow until the garbage collector used more and more CPU, raising latency. GC pressure, saturated connections and client retries reinforced each other.\n\nFrom 16:30 to 17:30 configuration changes reduced connection concurrency and pipeline depth in Frankfurt and London and fitted limits to the real hardware, which gradually stabilised the clusters, while corrupted cache files and user data snapshots were repaired to stop further full syncs. Between 17:30 and 18:00 the rollback was completed, caches were consistent, traffic distribution was normalised and error rates were back to baseline.","ko":"AdGuard DNS 리졸버는 필터를 적용하려면 사용자의 필터링 설정을 로컬에 갖고 있어야 한다. 리졸버는 시작할 때 로컬 파일에서 전체 사용자 데이터의 캐시 사본을 읽고, 그 뒤로는 비즈니스 로직 서비스에 바뀐 부분만 주기적으로 묻는다. 로컬 캐시가 오래됐거나 손상됐으면 비즈니스 로직 서비스에서 전체 데이터를 받아야 하고, 이 작업은 느리고 양쪽에 훨씬 큰 부하를 준다. AdGuard 는 2025년 11월 25일 14시 30분 UTC 에 이 방식을 개선하는 새 버전을 배포하기 시작했다. 이 버전은 버그 때문에 캐시 파일에 최근 바뀐 일부만 저장해서, 캐시가 아주 작거나 비어 있었다. 첫 배포는 네트워크 문제로 한 시간쯤 뒤 중간에 실패해 서버의 절반쯤만 새 버전으로 돌았다.\n\n15시 30분에 배포를 다시 시도하자 많은 리졸버가 동시에 캐시가 비었거나 쓸 수 없다는 것을 알고 전체 동기화를 시작했다. 비즈니스 로직 서비스로 가는 트래픽과 리졸버의 자원 사용이 치솟았다. DNS 오류율과 서버 부하 알림이 울렸고, 조사해서 캐시 버그를 빨리 찾았다. 16시부터 모든 서버에서 동시에 롤백하기로 했다. 암스테르담에서는 32 GiB 서버가 먼저 떨어져 나가고 64 GiB 서버도 이어서 실패했다. 트래픽 일부를 다른 유럽 거점으로 돌리자 프랑크푸르트와 런던의 부하가 크게 늘었다.\n\n프랑크푸르트와 런던은 하드웨어가 같았지만 잘못 잡힌 한도와 높은 동시성 때문에 크게 나빠졌다. 파이프라이닝 깊이가 5여서 연결 하나에 처리 중인 요청이 여러 개 실렸다. 요청마다 고루틴과 버퍼가 따로 생겨 힙이 커졌고, Go 가비지 컬렉터에 압박이 걸렸다. AdGuard 의 설명에 따르면 connlimit 은 Go 런타임이 가비지 컬렉션을 강하게 돌리기 전까지 관리 힙에 쓰는 메모리의 최댓값을 정하는 설정이다. 64 GiB 서버에서는 이 한도가 높아 힙이 크게 자란 뒤 가비지 컬렉션이 CPU 를 오래 썼다. 16시 30분부터 17시 30분 사이에 프랑크푸르트와 런던의 연결 동시성과 파이프라인 깊이를 줄이고 한도를 실제 하드웨어에 맞췄다. 손상된 캐시 파일과 사용자 데이터 스냅샷도 고쳤다. 프랑크푸르트에서 파이프라인 깊이를 5에서 1로 줄이자 매우 빨리 회복됐다. 17시 30분부터 18시 사이에 롤백이 끝나 캐시가 일관된 상태로 돌아왔다. 트래픽 분배도 정상으로 돌아와 오류율이 평소 수준으로 내려왔다."},"trigger":{"en":"A new version of AdGuard DNS, deployed on 25 November to improve how resolvers cache user data, had a bug that saved only the recently changed subset of user data into the local cache file. The first deployment failed partway on a network issue and left about half the servers on the new version; the second attempt made many resolvers start full synchronizations at once.","ko":"AdGuard 는 11월 25일에 사용자 데이터 캐시 방식을 개선하려는 새 AdGuard DNS 버전을 배포했다. 버그 때문에 리졸버가 로컬 캐시 파일에 전체 사용자 데이터가 아니라 최근에 바뀐 일부만 저장했다. 다시 시도한 배포에서 많은 리졸버가 한꺼번에 전체 동기화를 시작했다."},"severity":"unknown","symptoms":["dns-resolution-failures","intermittent-timeouts","high-latency","memory-pressure"],"timeline":{"detected_after":null,"resolved_after":"2h 30m"},"date_kind":"incident","detection":{"en":"Alerts for increased DNS error rates and server load fired during the second deployment attempt, between 15:30 and 16:00 UTC.","ko":"15시 30분에서 16시 UTC 사이, 두 번째 배포를 시작한 뒤 DNS 오류율과 서버 부하가 늘었다는 알림이 울렸다."},"components":["recursive-dns-resolver","language-runtime"],"root_cause":"deploy-rollout","remediation":[{"text":{"en":"Rolled back the buggy version on all servers","ko":"버그가 있는 버전을 모든 서버에서 동시에 롤백했다. 롤백은 17시 30분에서 18시 UTC 사이에 끝났다."}},{"text":{"en":"Partially rerouted Amsterdam's traffic to other European locations","ko":"암스테르담의 트래픽 일부를 다른 유럽 거점으로 돌렸다. 암스테르담은 안정됐지만 프랑크푸르트와 런던의 부하가 늘었다."}},{"text":{"en":"Reduced connection concurrency and request pipeline depth in Frankfurt and London, from 5 to 1 in Frankfurt","ko":"프랑크푸르트와 런던에서 실제 연결 동시성과 파이프라인 깊이를 줄였다. 프랑크푸르트에서 파이프라인 깊이를 5에서 1로 줄이자 매우 빨리 회복됐다."}},{"text":{"en":"Adjusted limits to the actual hardware capacity","ko":"한도를 실제 하드웨어 용량에 맞게 조정했다."}},{"text":{"en":"Repaired corrupted cache files and user data snapshots to stop further full synchronizations","ko":"전체 동기화가 더 일어나지 않도록 손상된 캐시 파일과 사용자 데이터 스냅샷을 고쳤다."}},{"text":{"en":"Normalised the traffic distribution once the rollback was complete","ko":"트래픽 분배를 정상으로 돌렸고 오류율이 평소 수준으로 내려왔다."}}],"action_items":[{"text":{"en":"Add automated tests that the full user dataset is correctly written to and restored from the local cache file, including detection of empty or truncated caches","ko":"전체 사용자 데이터가 로컬 캐시 파일에 제대로 직렬화되고 그 파일에서 제대로 복원되는지 확인하는 자동 테스트를 넣는다. 캐시가 비었거나 잘린 것도 잡아내게 한다."}},{"text":{"en":"Add integration tests that simulate resolver startup with an empty or corrupted cache under realistic load","ko":"실제와 비슷한 부하에서 빈 캐시나 손상된 캐시로 리졸버를 시작하는 통합 테스트를 넣어, 시스템이 무리 없이 동작하고 비즈니스 로직 서비스에 과부하를 주지 않는지 확인한다."}},{"text":{"en":"Standardize server hardware profiles per location, or keep separate configurations per hardware class","ko":"거점마다 서버 하드웨어 사양을 하나로 맞춰 RAM 크기가 크게 다른 서버가 한 클러스터에 섞이지 않게 하거나, 하드웨어 종류마다 설정을 따로 관리한다."}},{"text":{"en":"Review and adjust connlimit, cache sizes and connection limits for each standard node type","ko":"표준 노드 종류마다 connlimit, 캐시 크기, 연결 한도를 다시 살펴 조정해 32 GiB 노드와 64 GiB 노드가 모두 안전한 가비지 컬렉션과 메모리 범위 안에서 돌게 한다."}},{"text":{"en":"Reevaluate and lower pipelining depth where needed","ko":"최대 부하에서도 고루틴과 힙이 지나치게 늘지 않도록 파이프라이닝 깊이를 다시 따져 보고 필요한 곳은 낮춘다."}},{"text":{"en":"Reevaluate how connections and goroutines are limited so resource use stays bounded with decent throughput after a connection break","ko":"연결 수와 고루틴 수를 제한하는 방식을 다시 따져 보고, 자원 사용을 적절히 제한하면서도 연결이 끊긴 뒤에도 처리량이 충분히 나오게 한다."}},{"text":{"en":"Monitor Go heap size, GC time, GC cycles per second and per-cluster request latency distributions","ko":"모니터링이 CPU 와 전체 메모리뿐 아니라 Go 힙 크기, 가비지 컬렉션 시간, 초당 가비지 컬렉션 횟수, 클러스터별 요청 지연 분포도 보게 넓힌다."}},{"text":{"en":"Alert on sustained increases in SERVFAIL rate and request duration while upstreams are healthy","ko":"상위 DNS 가 정상인데 SERVFAIL 비율과 요청 처리 시간이 계속 오르면 울리는 알림을 따로 둬, 내부 경합과 가비지 컬렉션 문제를 사용자가 겪기 전에 잡는다."}},{"text":{"en":"Refine DNS resolver deployment and rollback procedures to avoid simultaneous cache invalidation across a large part of the fleet","ko":"DNS 리졸버의 배포와 롤백 절차를 다듬어 전체 서버의 큰 부분에서 캐시가 동시에 무효화되지 않게 한다."}},{"text":{"en":"Document and rehearse playbooks for adjusting pipeline depth, connection limits and connlimit during live incidents","ko":"암스테르담과 프랑크푸르트에서 본 동작을 바탕으로, 사고 중에 파이프라인 깊이, 연결 한도, connlimit 을 빨리 조정하는 플레이북을 문서로 만들고 연습한다."}}],"contributing":["warm-cache-dependency","retry-storm","manual-action-worsened","test-coverage-gap"],"recurrence_of":null,"symptoms_text":{"en":"Between about 15:30 and 18:00 UTC on 25 November 2025 some AdGuard DNS users saw slow or failed DNS resolution, with timeouts and SERVFAIL responses. Alerts fired for increased DNS error rates and server load, and a significant part of the traffic in several European locations, primarily Amsterdam, Frankfurt and London, had intermittent DNS failures for up to 2.5 hours.\n\nThe upstream DNS servers kept answering quickly and correctly throughout. The failures came from the AdGuard DNS resolver layer itself, which could not process and return responses before internal or client-side timeouts expired, so request duration and the number of SERVFAIL responses rose sharply.\n\nThe trouble started when a retried deployment of a new version made a large number of resolvers find at the same time that their local user-data caches were effectively empty or invalid, and all of them began full synchronizations with the business-logic service. In Amsterdam the cluster mixed machines with 32 GiB and 64 GiB of RAM, while connection limits, cache sizes and memory settings were tuned for 64 GiB. Under the full syncs and a storm of retrying DNS clients, the 32-GiB machines ran out of usable memory and effectively stopped serving traffic; their load moved to the 64-GiB machines, which then hit their connection limits as queues grew. Partly rerouting Amsterdam's traffic to other European locations stabilised Amsterdam but put heavy extra load on Frankfurt and London.\n\nIn Frankfurt CPU usage never looked fully saturated at the system level, yet Go garbage collection and internal contention drove up tail latency. The cluster hit its connection limit and stayed there: existing connections were held longer than usual, new clients struggled to connect, and their retries kept the limit saturated. With a request pipeline depth of 5, each connection carried several in-flight queries, each handled by separate goroutines and buffers, which grew the heap and the pressure on the garbage collector. When the pipeline depth was reduced from 5 to 1 in Frankfurt, the system recovered very quickly.","ko":"DNS 오류율과 서버 부하가 늘었다는 알림이 울렸다. 일부 AdGuard DNS 사용자는 DNS 조회가 느려지거나 실패했고, 타임아웃과 SERVFAIL 응답을 받았다.\n\n영향은 주로 유럽, 그중에서도 암스테르담, 프랑크푸르트, 런던에 몰렸다. 이 거점들에서 트래픽의 상당 부분이 최대 2.5시간 동안 간헐적으로 실패했다. 상위 DNS 공급자들은 내내 정상으로 빠르게 응답하고 있었다. 실패는 리졸버 계층이 내부 타임아웃이나 클라이언트 타임아웃이 지나기 전에 응답을 처리해 보내지 못해서 났다.\n\n암스테르담 클러스터는 RAM 32 GiB 서버와 64 GiB 서버가 섞여 있었고, 설정은 64 GiB 에 맞춰져 있었다. 32 GiB 서버가 먼저 쓸 수 있는 메모리를 다 쓰고 내부 한도에 닿아 사실상 트래픽을 처리하지 못했다. 부하가 64 GiB 서버로 몰리자 그 서버들도 연결 한도에 닿고 큐가 길어지며 실패하기 시작했다.\n\n프랑크푸르트에서는 시스템 수준의 CPU 사용률이 꽉 차 보이지 않았는데도 꼬리 지연 시간이 크게 늘었다. 클러스터가 연결 한도에 닿은 채 머물렀고, 기존 연결이 평소보다 오래 붙잡혀 있어서 새 클라이언트는 연결을 맺기 어려웠다. 메모리 압박으로 Go 가비지 컬렉션이 잦아지고 비용이 커져서 쿼리마다 리졸버 안에서 머무는 시간이 길어졌다.\n\n클라이언트와 재귀 리졸버는 실패한 쿼리를 다시 시도했다. 재시도가 부하를 더 키워 연결 한도가 계속 꽉 찬 상태로 남았다. 가비지 컬렉션 압박, 연결 포화, 클라이언트 재시도가 서로를 키웠다.\n\nAdGuard 는 암스테르담을 안정시키려고 트래픽 일부를 다른 유럽 거점으로 돌렸다. 그러자 암스테르담은 안정됐지만 프랑크푸르트와 런던에 부하가 크게 늘었다."},"updated_at":"2026-11-14T12:33:51.959763Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0268","org_slug":"adguard-dns"}