메인 콘텐츠로 건너뛰기
page thumbnail

DNS 지연과 스레드 블로킹, 마이크로서비스 성능 최적화 가이드

설탕사과
설탕사과
조회수 35
요약

어느 날 평소랑 똑같이 배포를 했는데, 갑자기 서비스 전체 응답 속도가 미묘하게 느려지기 시작합니다. APM을 들여다봐도 애플리케이션 로직은 빠른데, 이상하게 사용자 체감 속도는 점점 나빠집니다.

많은 팀이 이 지점에서 놓치는 범인이 바로 “DNS 지연”입니다. 특히 마이크로서비스 환경과 비동기 코드를 쓰는 서비스에서는, 느려진 DNS가 스레드를 잡고 늘어지면서 전체 성능을 무너뜨리기 쉽습니다.

지금부터 DNS가 왜 느려지고, 어떻게 스레드를 블로킹하며, 무엇을 해야 이 병목을 막을 수 있는지 실전 위주로 정리해 보겠습니다.

DNS 지연, 눈에 잘 안 보이는 성능 병목의 정체

DNS는 우리가 친숙한 도메인 이름을 서버끼리 통신 가능한 IP 주소로 바꿔주는 거대한 인터넷 전화번호부입니다. 사용자의 브라우저나 백엔드 서비스가 다른 서비스에 연결할 때는 항상 먼저 “이 도메인의 IP가 뭐야?”라고 DNS에게 묻는 과정을 거칩니다.

문제는 이 질문에 대한 답을 받으려면 네트워크를 왔다 갔다 해야 한다는 점입니다. 서버와 DNS 사이의 물리적 거리, 네트워크 품질, DNS 서버의 부하, 잘못된 설정 같은 요소들이 겹치면 이 왕복 시간이 눈에 띄게 길어지기 시작합니다.

사용자 입장에서는 화면이 한참 동안 멈춰 있는 것처럼 느껴질 뿐이지만, 내부에서는 “요청들이 줄을 서서 DNS 응답을 기다리는” 상황이 벌어지고 있습니다. 요청이 출발도 못 하고 DNS 앞에서 막혀 있는 셈이죠.

이 지연이 짧을 때는 잘 티가 나지 않지만, 50ms, 100ms씩 늘어나기 시작하면 전체 서비스 레이턴시에 상당한 영향을 미치게 됩니다.

마이크로서비스에서 DNS 지연이 치명적인 이유

모놀리식 서비스에서는 한 번의 사용자 요청이 보통 한두 번의 외부 호출로 끝납니다. 하지만 마이크로서비스 구조로 바뀌는 순간 이야기가 완전히 달라집니다.

주문 하나를 처리한다고 가정해 보겠습니다. 인증 서비스, 상품 서비스, 재고 서비스, 결제 서비스, 알림 서비스, 로그/트레이스 수집 서비스 등 여러 개의 내부 서비스가 연쇄적으로 호출됩니다. 이 과정에서 실제로는 수십 개의 내부 HTTP/gRPC 호출이 발생하는 것이 흔한 패턴입니다.

문제는 이 각각의 호출이 DNS 조회를 동반할 수 있다는 점입니다. DNS 조회가 한 번에 50~200ms 정도만 느려져도, 호출이 여러 번 중첩되면 전체 응답 시간이 금방 몇 초까지 늘어나는 일이 벌어집니다.

여기에 동시 접속자가 많아지는 피크 타임이 겹치면 사태는 더 심각해집니다. 느려진 DNS를 기다리는 요청들이 한꺼번에 몰리면서 서버의 스레드가 모두 묶여버리고, 처리량이 급격히 떨어지는 현상이 발생합니다.

“비동기니까 괜찮다”는 착각: 스레드 블로킹의 함정

많은 개발자가 이렇게 말합니다. “우리 서비스는 비동기/논블로킹으로 짰으니까, IO 때문에 스레드가 막힐 일은 없어요.”

안타깝게도 DNS는 예외인 경우가 많습니다. 라이브러리나 운영체제 수준에서 DNS 조회를 동기 방식으로 처리하는 경우가 여전히 많기 때문입니다.

즉, 애플리케이션 코드에서는 async/await로 깔끔하게 짜여 있어도, 내부 어딘가에서는 실제로 스레드가 “DNS 응답이 올 때까지” 멈춰있을 수 있습니다.

그 결과,

  • 스레드 풀이 DNS를 기다리느라 바닥나고

  • 새로운 요청은 큐에 쌓이기만 하며

  • 전체 시스템 처리량(QPS)이 눈에 띄게 떨어지는 상황이 발생합니다.

이래서 DNS 지연은 단순한 “몇 밀리초 더 느린 요소”가 아니라, 시스템 전체를 조용히 옥죄는 숨은 병목이 됩니다.

그래프에는 안 보이는데 서비스는 느려진다: DNS가 남기는 흔적

DNS 레이턴시는 대부분 “전체 응답 시간” 안에 숨어 들어갑니다. 많은 모니터링 대시보드가 DB 쿼리, 애플리케이션 로직, 외부 API 호출 시간은 나눠서 보여주지만, DNS 조회 시간은 별도 지표로 잘 보여주지 않습니다.

그래서 이런 상황이 자주 벌어집니다.

  • APM 상으로는 비즈니스 로직, DB, 캐시 모두 빠른데

  • 사용자 체감 응답 시간은 비정상적으로 길고

  • P95, P99 레이턴시는 계속 나빠지며

  • 에러율도 살짝 오르지만 원인을 특정하기 어려운 상태

이때 네트워크 I/O나 소켓 연결 단계 시간을 세분화해서 들여다보지 않으면, “DNS가 원인”이라는 사실을 눈치채기 어렵습니다. 결국 “코드 문제인가?”, “DB 문제인가?” 하면서 엉뚱한 곳을 튜닝하다 시간을 보내게 됩니다.

DNS 캐시 전략: 가장 적은 비용으로 가장 큰 효과 얻기

DNS 지연을 줄이는 가장 손쉬운 방법은 “캐시”입니다. 한 번 조회한 도메인의 IP를 일정 시간 동안 기억해 두고, 같은 도메인에 대해 재요청이 들어오면 DNS를 다시 묻지 않고 바로 IP를 사용하는 방식입니다.

이 캐시는 두 가지 레벨에서 고려할 수 있습니다. 하나는 애플리케이션 내부에서 자주 사용하는 도메인에 대해 캐시를 두는 방식이고, 다른 하나는 서버나 노드 단위로 로컬 DNS 리졸버(node-local DNS, sidecar, 시스템 리졸버 등)를 구성하는 방식입니다.

여기서 핵심은 TTL(Time To Live) 설정입니다. TTL을 너무 짧게 잡으면 매번 새로 조회하느라 캐시 효과가 줄어들고, 너무 길게 잡으면 실제 IP가 바뀌었을 때 반영이 늦어져 장애로 이어질 수 있습니다.

일반적인 마이크로서비스 환경에서는 수십 초 단위 TTL이 “성능”과 “서비스 변경의 유연성” 사이에서 무난한 절충점이 되는 경우가 많습니다. 여기에 더해, 캐시 히트율과 DNS 조회 횟수를 모니터링하면 우리 서비스에 맞는 적정 TTL을 데이터 기반으로 조정할 수 있습니다.

DNS 조회를 비동기로: 스레드 블로킹 줄이는 설계법

두 번째 축은 DNS 조회 자체를 비동기/논블로킹으로 바꾸는 것입니다.

우선 지금 사용하는 HTTP 클라이언트, gRPC 클라이언트, DB 드라이버가 DNS 쿼리를 어떻게 처리하는지 문서를 확인해 보는 것이 좋습니다. 비동기 DNS 라이브러리를 지원하는지, OS의 블로킹 호출에 의존하는지에 따라 설계 전략이 달라집니다.

비동기 DNS를 사용하면 DNS 응답을 기다리는 동안 스레드가 다른 작업을 처리할 수 있습니다. 같은 서버 자원으로 더 많은 동시 요청을 처리할 수 있고, 피크 타임에도 스레드 고갈을 어느 정도 막을 수 있습니다.

다만 DNS 서버 자체가 느려진 상태라면, 비동기 처리만으로 지연이 사라지지는 않습니다. 그래서 비동기화는 어디까지나 “블로킹을 줄이는 기술”일 뿐이고, 캐시와 인프라 최적화와 함께 사용될 때 비로소 효과가 극대화됩니다.

추가로, 서비스 시작 시 또는 주기적으로 자주 사용하는 도메인을 미리 조회해 캐시를 채워 두는 “워밍업” 전략도 유용합니다. 첫 번째 요청이 느려지는 것을 막고, 피크 타임 전에 미리 DNS 경로를 달궈두는 효과가 있습니다.

DNS 인프라와 네트워크 경로, 이렇게 최적화하자

코드를 손대기 전에, DNS 인프라와 네트워크 경로를 점검하는 것만으로도 큰 개선을 얻을 수 있습니다.

클라우드를 사용 중이라면 AWS Route 53, GCP Cloud DNS, Azure DNS 같은 관리형 DNS 서비스를 적극 활용하는 것이 좋습니다. 이 서비스들은 각 클라우드의 내부 네트워크와 지리적으로 분산된 리졸버를 활용해 기본적인 레이턴시를 크게 줄여줍니다.

쿠버네티스나 컨테이너 기반 환경에서는 클러스터 내부에 DNS 캐시나 로컬 리졸버를 두어, 각 Pod가 멀리 떨어진 외부 DNS 서버에 직접 질의하지 않도록 설계하는 편이 안정적입니다.

또 하나 중요한 포인트는 “불필요하게 긴 DNS 경로를 만들지 않는 것”입니다. 사설 DNS와 공용 DNS(8.8.8.8, 1.1.1.1 등)를 섞어 쓸 때, 레코드 체인이 복잡하게 꼬여 쓸데없이 많은 홉을 거치고 있지는 않은지 주기적으로 점검해야 합니다. 이런 체인 하나만 정리해도 레이턴시가 눈에 띄게 줄어드는 사례가 꽤 많습니다.

모니터링과 알림: DNS 사고를 가장 빨리 눈치채는 방법

DNS 문제의 가장 큰 특징은 “느려지기 전까지는 아무도 관심을 두지 않는다”는 점입니다. 그래서 의도적으로 DNS 관련 지표를 수집하고 알림을 설정하는 것이 중요합니다.

특히 다음 항목들을 별도로 추적해 두면 도움이 됩니다.

  • 도메인별 평균/최대 DNS 조회 시간

  • DNS 실패율과 재시도 횟수

  • 캐시 히트율과 미스율

이 정도만 모니터링해도 “지금 느려진 게 애플리케이션 코드 때문인지, DNS 때문인지”를 훨씬 빠르게 구분할 수 있습니다.

또한 HTTP 클라이언트나 서비스 간 통신에서 재시도 정책을 세울 때, “DNS 오류/타임아웃”에 대한 재시도 횟수와 간격을 일반 네트워크 오류와 분리해서 튜닝하는 게 좋습니다. DNS가 아예 죽어있는 상황에서 무의미한 재시도를 반복하면 오히려 시스템을 더 괴롭히게 되니까요.

마지막으로 헬스 체크와 SLO 정의에 DNS 구간을 포함시키는 것도 중요합니다. DNS가 느려졌는데도 “애플리케이션은 살아 있으니 OK”라고 판단해 버리면, 이미 사용자는 서비스가 죽었다고 느끼고 있을 수 있습니다.

오늘 당장 해볼 수 있는 DNS 성능 점검 루틴

이 글을 읽고 있는 지금, 간단한 점검을 한 번 해보세요.

먼저 우리 서비스에서 가장 자주 호출되는 내부/외부 도메인을 5~10개 정도만 추려봅니다. 그다음 실제 환경에서 이 도메인들의 DNS 조회 시간을 측정해 보세요. 의외로 큰 편차가 보일 수도 있습니다.

이후에는 서버 또는 컨테이너 레벨에서 로컬 DNS 캐시나 리졸버를 구성하고, 적정하다고 생각하는 TTL을 정해 적용해 봅니다. 적용 전후의 응답 시간과 P95/P99 레이턴시를 비교하면 캐시 효과를 바로 체감할 수 있습니다.

애플리케이션 코드 차원에서는, 현재 사용하는 네트워크 클라이언트들이 DNS 단계에서 스레드 블로킹을 일으키는지 문서를 확인하고, 필요하다면 비동기 DNS를 지원하는 라이브러리로 교체를 검토해 보세요.

마지막으로 모니터링 도구에 “DNS 레이턴시”와 “DNS 오류율” 지표를 추가하고, 임계치를 정해 알림을 걸어두면 이후 장애 분석이 훨씬 수월해집니다.

마무리하며: 보이지 않던 구간을 한 줄 더 의식해 보기

DNS는 평소에는 거의 신경 쓰이지 않는 존재입니다. 하지만 한 번 느려지기 시작하면, 코드 최적화, DB 튜닝, 캐시 도입으로도 해결되지 않는 묘한 지연을 만들어내는 전형적인 숨은 병목 지점입니다.

장기적으로 안정적인 성능을 원한다면,

  • DNS 캐시

  • 비동기/논블로킹 처리

  • 인프라 및 경로 최적화

  • 모니터링과 알림

이 네 가지를 세트로 설계하는 것이 가장 효과적입니다.

새로운 서비스를 설계하거나 기존 시스템을 점검할 때, “이 호출에서 DNS 구간은 얼마나 걸릴까?” 이 질문을 한 번만 더 떠올려 보세요.

그 한 줄의 의식이, 언젠가 올 수 있는 대형 장애를 조용히 막아 줄 수 있습니다.

출처 및 참고 : DNS 지연과 스레드 블로킹: 성능 저하를 막는 실전 이해 가이드

DNS 지연을 시각화하는 다이어그램 아이디어

DNS 지연과 스레드 블로킹을 팀에 설명할 때는 글만으로 전달하기 어려운 흐름과 병목 지점을 그림으로 보여주는 것이 효과적입니다. 예를 들어, “사용자 요청 → DNS 조회 → 서비스 간 호출”로 이어지는 전체 요청 플로우를 수평 타임라인으로 그려보면, DNS 구간이 어디에 끼어 있고 얼마나 자주 반복되는지 한눈에 이해할 수 있습니다. 각 단계별 평균 소요 시간(예: DNS 80ms, 애플리케이션 로직 20ms, DB 30ms)을 박스 아래에 적어 두면, DNS 지연이 전체 응답 시간에서 차지하는 비중을 직관적으로 비교할 수 있습니다.

마이크로서비스 환경에서는 “주문 요청 1건”이 실제로 어떤 서비스 체인을 타는지 서비스 간 호출 다이어그램으로 표현해 보세요. 중앙에 API Gateway 또는 BFF를 배치하고, 주변에 인증/상품/재고/결제/알림/로그 수집 서비스가 둘러싼 형태로 배치한 뒤, 각 서비스 간 호출 화살표 옆에 “DNS 조회 아이콘”이나 “(DNS)” 같은 표시를 추가합니다. 이렇게 하면 “DNS 호출이 한 번만 있는 것 같지만 실제로는 여러 내부 호출 중간마다 반복된다”는 점을 쉽게 강조할 수 있습니다.

스레드 블로킹과 캐시 효과는 두 개의 대비 다이어그램으로 설명할 수 있습니다. 첫 번째 그림에서는 서버의 스레드 풀 박스를 그리고, 그 안에 DNS 응답을 기다리며 블로킹된 스레드를 여러 개 색깔을 달리해 표시합니다. 동시에 요청 대기 큐가 길게 늘어선 모습을 그려 “DNS가 느려지면 스레드가 소진되고 큐가 쌓인다”는 상태를 보여줍니다. 두 번째 그림에서는 로컬 DNS 캐시와 비동기 DNS 리졸버를 추가한 구조를 그려, 스레드 박스 안의 “대기” 스레드 수가 줄어든 모습과 요청 처리량(QPS)이 높아진 상태를 대비하면, 캐시와 비동기 설계가 왜 중요한지 시각적으로 설득할 수 있습니다.

![https://server.tilnote.io/images/pages/f37aae4b-020f-499c-b2ae-ab2816081473.png]

이 노트는 요약·비평·학습 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.