DNS 트래픽 NGINX로 로드 밸런싱

이 포스트는 현대적인 어플리케이션 인프라에서 DNS 서버를 운영하는 데 직면하는 과제를 살펴보고, NGINX 오픈소스와 NGINX Plus가 UDP와 TCP 트래픽을 효과적이고 효율적으로 로드 밸런싱할 수 있는 방법을 보여줍니다. (어플리케이션 [Active] Health Check 는 NGINX Plus에서만 가능하지만, 그 외의 정보는 NGINX 오픈소에도 동일하게 적용되므로, 이 포스트에서는 간략히 NGINX Plus로 언급하겠습니다).

NGINX Plus R9부터 NGINX Plus의 Layer 4(L4) 로드 밸런싱 기능을 크게 향상시킨 리버스 프록시 및 UDP 트래픽 로드 밸런싱 기능을 도입했습니다.

목차

1. UDP 트래픽의 부하를 분산시키는 이유는 무엇일까요?
2. DNS는 UDP 프로토콜만이 아닙니다.
3. Microservices 환경의 DNS
4. 기본 DNS 부하 분산 구성
5. 고가용성 (HA)을 위한 DNS 서버 튜닝
 5-1. 프록시 응답 수 및 시간 초과 기간 구성
 5-2. Active Health Check 구성
 5-3. Active Health Check 추가 지시문
6. 규모 조정(Tuning for Scale)

1. UDP 트래픽의 부하를 분산 시키는 이유는 무엇일까요?

TCP와는 달리, UDP는 설계상 데이터의 End-to-End 전달을 보장하지 않습니다.
이는 message by carrier pigeon를 보내는 것과 유사합니다. 메시지가 전송되었음을 확실히 알 수 있지만, 도착 여부는 확실하지 않습니다. 이 “비연결성” 접근 방식에는 TCP보다 낮은 대기 시간이라는 몇 가지 이점이 있습니다.
개별적인 작은 UDP 메시지가 더 적은 대역폭을 사용하며 연결을 설정하기 위한 핸드셰이크(handshake) 프로세스가 없기 때문입니다. UDP는 타임아웃 및 기타 네트워크 수준 문제를 애플리케이션 개발자에게 넘기는 문제가 있습니다.
그러나 DNS에 대해서는 무엇을 의미할까요?

DNS는 다른 몇 가지 UDP 기반 프로토콜과 마찬가지로 요청(Request)-응답(Response) 데이터 흐름을 사용합니다.
예를 들어, DNS 클라이언트는 호스트 이름에 해당하는 IP 주소를 요청하고 응답을 받습니다.
응답이 주어진 타임아웃 기간 내에 도착하지 않으면, DNS 클라이언트는 동일한 요청을 “backup” DNS 서버로 보냅니다. 그러나 요청을 다시 시도하기 전에 타임아웃 기간을 기다려야 하는 경우, 일반적으로 매우 빠른 프로세스(밀리초 단위)가 매우 느린 프로세스(초 단위)로 전환될 수 있습니다.

DNS 조회가 실패하면 클라이언트는 타임아웃 이후 백업 서버로 재시도합니다.

NGINX Plus를 사용하여 DNS 트래픽을 프록시하고 로드 밸런싱하면 클라이언트에서 시간 초과가 발생하는 경우가 줄어듭니다. NGINX Plus 로드 밸런서 뒤에 여러 DNS 서버가 있는 경우 클라이언트와 NGINX Plus 사이에 네트워크 파티션이 있는 경우에만 클라이언트에서 시간 초과가 발생합니다.
DNS 서버 자체의 문제는 NGINX Plus가 애플리케이션 상태 검사를 사용할 때 클라이언트에서 발생하지 않습니다. NGINX Plus는 각 서버의 가용성과 응답 시간을 모니터링하여 클라이언트 요청을 비정상 서버로 보내는 것을 방지합니다.

2. DNS는 UDP 프로토콜만이 아닙니다.

DNS 트래픽의 대부분이 UDP를 통해 이루어지지만, TCP를 사용하는 일반적인 DNS 작업이 있습니다. DNS는 작은 메시지(최대 512바이트)에 대해서는 UDP를 사용하지만, 더 큰 메시지가 필요한(또는 필요할 가능성이 있는) 작업에 대해서는 TCP를 사용합니다. 역사적으로 DNS에서 TCP는 신뢰할 수 있는 Primary 네임 서버에서 Secondary 네임 서버로 영역 전송에만 사용되었습니다. 그러나 컨테이너 및 불변 인프라로의 전환으로 인해 DNS는 SRV 레코드를 통해 기본 Service Discovery 메커니즘으로 사용되는 경우가 늘고 있습니다.

DNS SRV 레코드는 원래 SIP를 사용하여 Voice over IP(VoIP) handset에서 서버를 검색하는 데 사용되었지만, 모든 유형의 서비스에 사용할 수 있습니다. 그러나 SRV 레코드는 대부분의 다른 DNS 레코드 유형보다 훨씬 더 많은 정보를 포함합니다. 따라서 약 10개의 SRV 레코드만 표준 512바이트 UDP 응답에 들어갈 수 있습니다. 반면 약 30개의 A 레코드는 들어갈 수 있습니다. DNS 응답이 512바이트 제한을 초과하면 처음 512바이트가 반환되지만 응답은 “truncated”로 표시됩니다. 이 시점에서 DNS 클라이언트는 절단된 응답을 최대한 처리하거나 동일한 요청을 TCP를 사용하여 다시 시도할 수 있습니다.

이는 모던 네트워크 인프라에서 DNS 서버의 로드 밸런싱을 할 때, NGINX Plus는 UDP와 TCP 트래픽의 혼합을 받을 것으로 예상됩니다.

3. Microservices 환경의 DNS

다음 그림은 두 개의 로드 밸런서가 있는 마이크로서비스 환경을 단순화한 모습을 보여줍니다. 프론트엔드 로드 밸런서는 애플리케이션의 공개 클라이언트에서 요청을 프록시하며, 최적의 마이크로서비스 인스턴스를 선택하고 다른 많은 기능을 수행합니다. 이번에는 마이크로서비스 환경과 마이크로서비스에 대한 Service Discovery 정보를 제공하는 DNS 서버 사이에 위치한 DNS 로드 밸런서에 집중하겠습니다.

NGINX Plus는 Microservice 환경에서 DNS 서버의 부하를 분산합니다.

4. 기본 DNS 부하 분산 구성

NGINX Plus는 Layer 4 로드 밸런싱을 Stream 모듈에서 구현하므로, UDP 및 TCP 로드 밸런싱은 다음 snippet과 같이 stream 블록에서 구성됩니다.

Warning: 이 구성 snippet을 /etc/nginx/conf.d 디렉토리에 새 파일로 추가할 수는 없습니다. (“stream directive is not allowed here” 검증 오류가 발생합니다.) 이는 NGINX Plus nginx.conf 구성 파일이 conf.d 디렉토리의 파일 내용을 http 블록에 포함하기 때문입니다. 가장 간단한 해결책은 전체 stream 블록을 nginx.conf에 직접 포함 시키는 것입니다.

stream {
    upstream dns_servers {
        server 192.168.136.130:53;
        server 192.168.136.131:53;
    }

    server {
        listen 53  udp;
        listen 53; #tcp
        proxy_pass dns_servers;
        error_log  /var/log/nginx/dns.log info; 
    }
}

먼저 DNS 서버의 업스트림 그룹을 정의합니다. server 지시문은 우리 업스트림 서버가 53 포트 (DNS의 잘 알려진 포트)에서 수신 대기 중인 것을 지정합니다.

server{} 블록은 NGINX Plus가 들어오는 DNS 트래픽을 처리하는 방법을 정의합니다. 두 가지 listen 지시문은 NGINX Plus가 UDP 및 TCP 트래픽을 모두 포트 53에서 수신하도록 지시합니다. TCP는 Stream 모듈의 기본 Layer 4 프로토콜이므로 UDP와 같이 명시적으로 매개변수로 지정하지 않습니다.

proxy_pass 지시문은 NGINX Plus가 수신 대기 중인 트래픽을 어떻게 처리해야 하는지를 지정합니다. 여기서 우리는 이러한 트래픽을 dns_servers 업스트림 그룹으로 프록시합니다. NGINX Plus는 클라이언트 UDP 요청을 업스트림 서버로 전달할 때 자동으로 UDP를 사용하며 (클라이언트 TCP 요청에 대해서는 TCP를 사용함), 따라서 업스트림 그룹에서 Layer 4 프로토콜을 명시적으로 지정할 필요가 없습니다.

5. 고가용성 (HA)을 위한 DNS 서버 튜닝

DNS 서버의 가용성을 개선하기 위해 지시문을 몇 개 더 추가하고 Active (애플리케이션) Health Check을 구성할 수 있습니다.

5-1. 프록시 응답수 및 시간 초과 기간 구성

첫 번째 추가 지시문은 proxy_responses 입니다. 이 지시문은 NGINX Plus가 각 프록시된 UDP 요청에 대해 기대하는 응답 수를 지정합니다. NGINX Plus의 경우, 단일 응답을 받은 후 NGINX Plus는 즉시 추가 응답을 기다리지 않고 해당 세션에 사용된 메모리와 소켓을 해제합니다.

두 번째 추가 지시문인 proxy_timeout 은 서버에서 응답을 받을 때까지 NGINX Plus가 대기하는 시간을 결정합니다. (기분 10분을 1초로 줄입니다.) NGINX Plus는 이 기간 내에 응답을 받지 못하면 upstream 그룹의 다음 서버를 시도하고, 응답이 없는 upstream 서버를 일정기간 (기본 값으로 10초) 동안 사용 불가능한 상태로 표시하여 다른 클라이언트도 해당기간 동안 타임아웃에 의한 지연을 겪지 않도록 합니다.

server {
    listen 53  udp;
    listen 53; #tcp
    proxy_pass      dns_servers;
    error_log       /var/log/nginx/dns.log info;
    proxy_responses 1;
    proxy_timeout   1s;
}

NGINX Plus는 업스트림 그룹의 server 지시문에 fail_timeout 옵션을 포함하여 서버가 사용할 수 없는 시간을 변경할 수 도 있습니다.
다음 설정을 사용하면 NGINX Plus가 실패한 업스트림 서버를 60초 동안 사용할 수 없는 것으로 표시합니다.

upstream dns_servers {
    server 192.168.136.130:53 fail_timeout=60s;
    server 192.168.136.131:53 fail_timeout=60s;
}

이를 통해 DNS 서버 중 하나가 실패한 경우 클라이언트가 경험하는 delay를 조절할 수 있습니다. 그러나 만약 실패한 DNS 서버에 대해 TCP 요청이 시도된다면 TCP의 내재적인 오류 검사로 인해 NGINX Plus가 자동으로 해당 서버를 사용 불가능한 상태로 표시하여 이후 TCP 또는 UDP 요청이 해당 서버를 피하도록 할 수 있습니다.

5-2. Active Health Check 구성

NGINX Plus의 Active Health Check 기능은 DNS를 포함한 모든 로드 밸런싱 서비스의 고가용성에 대한 추가적이고 귀중한 도구입니다.
DNS 클라이언트의 실제 TCP 요청이 실패하기 전에 DNS 서버를 다운으로 표시하기 전에 NGINX Plus가 주기적으로 53번 포트에서 TCP 연결을 시도하여 DNS 서버가 올바르게 작동하는지 확인할 수 있습니다.
이를 위해 server{} 블록에서 health_check 지시문과 그 포트 (53) 매개 변수를 포함 시킵니다.
(NGINX Plus는 기본적으로 listen 지시문에서 지정된 포트, 해당 포스트의 경우 53으로 health check를 보냅니다.
따라서 여기에서는 기본값을 명시적으로 구성하기 위해 매개 변수를 사용하지만, DNS 서버를 수정하여 해당 포트의 트래픽에 응답하도록 지정할 수 있습니다.)

UDP의 경우, 알려진 레코드에 대한 실제 DNS 조회를 수행하는 Active Health Check를 구성할 수 있습니다.
예를 들어, 마이크로서비스 환경에서 Service Discovery에 사용되는 하위 도메인과 동일한 존 파일에 다음 CNAME 레코드를 배치할 수 있습니다.

healthcheck    IN CNAME    healthy.svcs.example.com.

UDP의 경량화된 특성을 고려하면 네트워크 트래픽을 감시하고 DNS 조회를 나타내는 바이트 문자열을 쉽게 추출할 수 있습니다.
그런 다음 해당 문자열을 send 지시문의 매개변수로 사용하여 일치하는 구성 블록을 생성합니다.
expect 지시문은 서버가 건강하다고 간주되기 위해 반환해야 하는 응답을 지정합니다.

match dns_lookup {
    send x00x01x00x00x00x01x00x00x00x00x00x00x06x68x65x61 ...;
    expect ~* "healthy.svcs.example.com.";
}

이러한 애플리케이션 수준의 깊은 상태 점검의 장점은 네임 서버가 가동 중이더라도 실제 프로덕션 도메인의 DNS 조회를 수행하여 문제를 일으킬 수 있는 구성 문제 및 데이터 손상 등을 발견할 수 있다는 것입니다.

NGINX STORE는 DNS 조회 및 기타 프로토콜에 대한 UDP 상태 점검 준비에 대해 도움을 드릴 수 있습니다

5-3. Active Health Check 추가 지시문

다음 Snippet은 Active Health Check 점검에 필요한 추가 지시문을 강조합니다.

stream {
    upstream dns_servers {
        zone dns_mem 64k;
        server 192.168.136.130:53 fail_timeout=60s;
        server 192.168.136.131:53 fail_timeout=60s;
    }

    match dns_lookup {
        send x00x01x00x00x00x01x00x00x00x00x00x00x06x68x65x61 ...;
        expect ~* "healthy.svcs.example.com.";
    }

    server {
        listen 53  udp;
        listen 53; #tcp
        health_check match=dns_lookup interval=20 fails=2 passes=2 udp;
        health_check                  interval=20 fails=1 passes=2 port=53; #tcp
        proxy_pass      dns_servers;
        error_log       /var/log/nginx/dns.log debug;
        proxy_responses 1;
        proxy_timeout   1s;

    }
}

zone 지시문은 dns_mem이라는 공유 메모리 영역을 정의합니다.
이틀 통해 Health Check의 결과와 다른 상태 정보를 모든 NGINX Plus Worker 프로세스에서 사용할 수 있습니다.

match 지시문은 위에서 설명한 내용과 동일합니다.

health_check 지시문은 환경에 맞게 조정할 수 있는 여러 매개변수를 가지고 있습니다.
여기에서는 UDP와 TCP에 대한 별도의 Health Check를 정의합니다. UDP와 TCP의 차이로 인해 DNS 서버를 비정상으로 표시하기 전에 연속된 두 개의 UDP Health Check 실패가 필요하지만 TCP는 하나만 필요합니다.
어떤 프로토콜이던지 불안정한, “flapping” 서버에 요청을 보내지 않기 위해 두 개의 성공적인 응답이 필요합니다.

UDP와 TCP 트래픽 모두에 대해 하나의 업스트림 그룹의 DNS 서버를 정의하는 이점은 어떤 프로토콜 에서든지 Health Check 실패 시 서버를 비정상으로 표시하고 로드 밸런싱 Pool에서 제거할 수 있다는 것입니다.

6. 규모 조정(Tuning for Scale)

2개의 백엔드 서버를 배포하는 것이 효과적인 고가용성 솔루션이 될 수 있지만, NGINX Plus의 로드 밸런싱 기능을 사용하면 클라이언트의 인식 없이 백엔드 서버를 수평으로 확장할 수 있습니다.

위에서 설명한 샘플 마이크로서비스 환경은 백엔드 DNS 서버의 확장이 필요하지 않을 것으로 보입니다.
그러나 모든 가입자에게 DNS 서비스를 제공하는 ISP는 지속적인 로드와 막대한 스파이크 가능성으로 인해 대규모 DNS 서버 및 프론트엔드 프록시가 필요합니다.

NGINX 및 NGINX Plus의 모든 로드 밸런싱 알고리즘은 HTTP 뿐만 아니라 TCP 및 UCP에 대해서도 사용할 수 있습니다.

  • Round Robin (기본값)
  • 일반 hash와 그의 consistent variant (ketama 알고리즘)
  • IP Hash
  • Least Connections
  • Least Time (NGINX Plus 전용)

NGINX Plus는 가중치(weights)를 구성하여 효율성을 높일 수도 있습니다.

HTTP 요청은 백엔드 서버에 부하와 처리 요구 사항을 크게 달리할 수 있지만, DNS 요청을 일반적으로 모두 동일한 부하를 생성합니다.
이러한 이유로 Least Connections와 Least Connections 알고리즘은 Round Robin보다 우위를 가질 가능성이 적습니다.
특히 Least Connections는 NGINX Plus가 업스트림 서버로부터 아직 응답을 기다리고 있는 UDP 요청을 연결 수에 포함시킵니다.
proxy_responsesproxy_timeout 값이 충족되지 않은 경우, NGINX Plus는 이미 작업을 완료한 업스트림 서버에 대한 연결을 계속해서 계산합니다.

클라이언트가 많이 있고 RADIUS challenge‑response 흐름(flow)과 같이 다중 메시지가 클라이언트와 서버 사이에서 교환되는 프로토콜인 경우, Source IP 해시를 사용하면 해당 대화를 단일 백엔드 서버에서 수행할 수 있습니다. 다시 말해 세선 지속성을 설정하여 NGINX Plus는 동일한 클라이언트의 모든 요청을 동일한 서버로 전달합니다. 다음 예제는 동일한 RADIUS 인증 서버 두 개에 대해 해시 로드 밸런싱 알고리즘을 구성하며, 키로 $remode_addr 변수에서 캡처한 Source (클라이언트) IP 주소를 사용합니다.

upstream radius_servers {
    hash $remote_addr; # Source-IP hash
    server 192.168.136.201:1812;
    server 192.168.136.202:1812;
}

NGINX Plus를 사용해 보려면 지금 무료 30일 평가판을 시하거나 NGINX STORE에 연락하여 사용 사례에 대해 논의하십시오.

아래 뉴스레터를 구독하여 NGINX 및 NGINX Plus의 다양한 최신 소식들을 빠르게 받아보세요.