Infra

DNS 관리 도구를 만들다가, 결국 DNS 서버를 만든 이야기 (2)

minsung 2026. 7. 29. 22:54

0. 들어가며

안녕하세요~ 시스템컨설턴트그룹 25기 인프라 담당 권민성입니다.

오늘은 전편에 이어서 RNDC 방식의 문제점과 이를 해결하기 위한 새로운 아키텍처, 그리고 이와 관련해서 DNS와 관련된 좀 더 기술적인 얘기를 해보려고 합니다.

 

1. RNDC 방식의 문제점

분산 환경에서 분명한 한계가 존재하는 RNDC 방식

 

1편의 RNDC 방식을 요약하면 같은 서버에 있는 BIND9의 설정 파일을 수정하고 RNDC 프로토콜을 통해 적용을 하는 방식입니다. 하지만 이 방식은 저희의 초기 목표와 상충하는 여러 문제가 있었습니다

  • Bindizr와 BIND9이 같은 서버에 있어야 했습니다.
  • 상태가 데이터베이스와 파일 시스템 양쪽에 분산됩니다.
  • 파일 기반 구조는 분산 환경에서 사용하기 어렵습니다.
  • RNDC 프로토콜을 직접 구현해야 했습니다.
  • 복잡한 DNS 레코드 규칙과 zone 파일 파싱 로직을 코드에 구현해야 합니다.

첫 번째 한계는 Bindizr와 BIND9의 배포 위치가 강하게 결합된다는 점입니다. BIND9의 설정 파일과 zone 파일을 직접 수정하려면 Bindizr가 해당 파일에 접근할 수 있어야 하므로, 사실상 같은 서버에서 실행되어야 합니다. SSH로 원격 파일을 수정하는 방식도 가능하지만, 권한 관리, 키 관리, 네트워크 보안, 서버별 경로 차이 등 운영 부담이 커집니다.

 

두 번째 한계는 상태가 DB와 파일 시스템 양쪽에 분산된다는 점입니다. Bindizr의 API로 변경한 zone 정보는 DB에 저장되지만, 실제 BIND9이 참조하는 데이터는 zone 파일입니다. 따라서 파일 생성, 권한 설정, RNDC reload 중 하나라도 실패하면 DB와 BIND9의 실제 상태가 달라질 수 있습니다.

 

세 번째 한계는 분산 환경에 적합하지 않다는 점입니다. 여러 DNS 서버를 운영하려면 모든 서버에 동일한 zone 파일이 존재해야 하고, 변경 사항도 일관되게 반영되어야 합니다. 하지만 파일을 직접 수정하는 방식에서는 일부 서버만 갱신되거나 reload에 실패할 수 있어, 서버마다 다른 DNS 응답을 반환할 위험이 있습니다.

 

네 번째 한계는 RNDC 프로토콜을 직접 구현해야 한다는 점입니다. RNDC는 단순한 명령 전송이 아니라 인증 정보와 메시지 구조를 맞춰 BIND9과 통신해야 합니다. 하지만 당시 Rust에서 바로 사용할 수 있는 RNDC 구현체가 없어, 메시지 생성, 인증 처리, 응답 파싱, 오류 처리 로직을 직접 구현해야 했습니다.

 

다섯 번째 한계는 DNS 레코드와 zone 파일 규칙을 직접 다뤄야 한다는 점입니다. 레코드 타입별 검증, FQDN 처리, 상대 이름 처리, TTL, priority, SOA serial 같은 규칙을 코드에 반영해야 했고, 결과적으로 Bindizr가 DNS 관리 API뿐만 아니라 zone 파일 렌더러와 검증기 역할까지 맡게 되었습니다.

 

이외에도 DNS-01 방식의 인증서 발급 과정에서 nsupdate를 통해 TXT 레코드가 직접 생성되는 경우, Bindizr가 해당 변경 사항을 알 수 없다는 문제도 있었습니다. 외부 도구가 BIND9에 직접 레코드를 추가하면 Bindizr의 DB에는 반영되지 않기 때문에, Bindizr가 관리하는 상태와 실제 DNS 서버의 상태가 달라질 수 있습니다.

 

위 문제점들을 해결하기 위한 새로운 방식을 소개하기 전에 DNS에서 사용되는 개념들에 대해 먼저 알아보겠습니다.


2. DNS 동작 방식과 핵심 개념

DNS 서버는 일반적으로 53번 포트에서 요청을 받습니다. 이때 UDP와 TCP를 모두 사용할 수 있습니다. 일반적인 도메인 조회 요청은 대부분 UDP로 처리되지만, 응답 크기가 크거나 신뢰성 있는 전송이 필요한 경우에는 TCP가 사용됩니다.

UDP와 TCP가 모두 사용되는 DNS 서버

 

UDP는 주로 일반적인 DNS 질의에 사용됩니다. 예를 들어 클라이언트가 example.com의 A 레코드를 조회하면, DNS 서버는 UDP 요청을 받아 메모리에 로드된 zone 데이터를 확인한 뒤 해당 레코드를 응답합니다. UDP는 연결을 맺는 과정이 없기 때문에 빠르고 가볍지만, 패킷 크기 제한이나 손실 가능성이 있습니다.

 

반면 TCP는 더 큰 응답이 필요하거나 안정적인 전송이 필요한 경우에 사용됩니다. 대표적으로 AXFR, IXFR 같은 Zone Transfer(zone 데이터를 다른 DNS 서버로 복제하는 과정)가 TCP를 사용합니다. 또한 응답 크기가 커지는 경우나 UDP 응답이 잘려서 전달되는 경우에도 TCP로 다시 요청할 수 있습니다.


BIND9의 zone 파일과 record

DNS 서버가 응답할 수 있는 데이터는 zone 단위로 관리됩니다. zone은 특정 도메인 영역에 대한 DNS 레코드 집합입니다. 예를 들어 example.com zone 안에는 example.com, www.example.com, api.example.com 등에 대한 레코드가 포함될 수 있습니다. 

 

DNS 레코드에는 여러 종류가 있습니다. 대표적인 레코드는 다음과 같습니다.

  • A: 도메인이 가리키는 IPv4 주소입니다(example.com → 192.0.2.10).
  • AAAA: 도메인이 가리키는 IPv6 주소입니다(example.com → 2001:db8:85a3::8a2e:370:7334).
  • CNAME: 특정 도메인을 별칭으로 연결할 때 사용합니다(api.example.com → example.com).
  • MX: 해당 도메인의 메일을 처리할 메일 서버를 지정합니다.
  • TXT: 임의의 텍스트 값을 저장하는 레코드입니다.
  • NS: 도메인이나 서브도메인을 담당할 네임서버를 지정합니다. 서브도메인을 별도의 DNS 서버로 위임할 때도 사용됩니다.
  • SOA: zone의 시작을 나타내는 레코드입니다. 보통 zone 파일의 맨 앞에 위치하며, serial, refresh, retry, expire 같은 zone의 메타데이터를 담습니다.
  • SRV: 특정 서비스가 어느 호스트와 포트에서 제공되는지 알려줍니다.
  • PTR: IP 주소를 도메인 이름으로 역조회할 때 사용합니다.

BIND9에서는 이러한 zone 정보를 zone 파일 형태로 저장합니다. zone 파일은 특정 zone에 속한 DNS 레코드들을 텍스트 형식으로 표현한 파일입니다. 예를 들면 다음과 같은 형태입니다.

$TTL 3600
@   IN  SOA ns1.example.com. admin.example.com. ( ; 주석은 세미콜론을 사용합니다.
        2026070901 ; serial ; Primary / Secondary 서버 간 동기화 상태 추적을 위한 숫자
        3600       ; refresh
        1800       ; retry
        604800     ; expire
        86400      ; minimum
)

@       IN  NS      ns1.example.com.
ns1     IN  A       192.0.2.10
www     IN  A       192.0.2.20
api     IN  CNAME   www.example.com.
sso     IN  CNAME   www

 

위 zone 파일에서 @는 현재 zone의 루트 도메인을 의미합니다. 예를 들어 현재 zone이 example.com이라면, @는 example.com을 가리킵니다.

또 하나 중요한 규칙은 이름 끝의 점(.)입니다. DNS에서 끝에 점이 붙은 이름은 FQDN(Fully Qualified Domain Name), 즉 루트 도메인까지 포함한 절대 도메인 이름을 의미합니다. 예를 들어 ns1.example.com.처럼 맨 뒤에 점이 있으면 더 이상 다른 도메인을 붙이지 않고 그대로 완전한 도메인 이름으로 해석됩니다.

반대로 맨 뒤에 점이 없으면 현재 zone을 기준으로 한 상대 이름으로 해석됩니다. 예를 들어 www는 www.example.com을 의미하고, api는 api.example.com을 의미합니다.

 

BIND9은 이 zone 파일을 읽어 내부 메모리에 로드합니다. 이후 DNS 질의가 들어오면 매번 zone 파일을 직접 읽는 것이 아니라, 메모리에 올라간 zone 데이터를 기준으로 응답합니다. 즉, 클라이언트가 www.example.com의 A 레코드를 요청하면 BIND9은 메모리에 로드된 zone 데이터에서 해당 레코드를 찾아 응답합니다(www.example.com → 192.0.2.20). 

 

이 구조 때문에  zone 파일을 수정했다고 해서 변경 사항이 바로 DNS 응답에 반영되는 것은 아닙니다. 파일을 수정한 뒤 BIND9이 해당 zone을 다시 읽도록 reload를 수행해야 합니다. RNDC 방식에서는 바로 이 reload 작업을 RNDC 프로토콜을 통해 수행했습니다.


DNS 서버의 이중화

DNS 서버를 실제로 운영할 때는 보통 한 대만 두지 않습니다. DNS 서버 한 대에 장애가 발생하면 해당 도메인의 DNS 조회가 실패할 수 있기 때문입니다. DNS 조회가 실패하면 웹 서버나 API 서버가 정상적으로 떠 있더라도, 사용자는 도메인 이름으로 서비스에 접근할 수 없습니다.

 

그래서 DNS 서버는 보통 여러 대를 함께 운영합니다. 여러 대의 서버를 운영하는 방식은 크게 active-standbyactive-active로 나눠볼 수 있습니다.

 

active-active(좌) 방식과 active-standby(우) 이중화 방식

 

active-standby하나의 서버가 주로 요청을 처리하고, 다른 서버는 장애 상황에 대비해 대기하는 방식입니다. 평소에는 Active 서버가 DNS 요청에 응답하고, Active 서버에 문제가 생기면 Standby 서버가 대신 응답을 처리합니다. 구조를 이해하기 쉽고 단순하다는 장점이 있지만, 평상시에는 Standby 서버가 거의 사용되지 않는다는 단점이 있습니다.

 

반면 active-active여러 DNS 서버가 동시에 요청을 처리하는 방식입니다. 예를 들어 example.com에 대해 ns1.example.com ns2.example.com이라는 두 개의 네임서버를 등록해두면, resolver는 둘 중 하나에 DNS 질의를 보낼 수 있습니다. 이때 두 서버가 모두 같은 Zone 데이터를 가지고 있다면, 어느 서버가 요청을 받더라도 같은 응답을 줄 수 있습니다.

 

DNS에서는 active-active 구성이 꽤 자연스럽습니다. 하나의 도메인에 여러 NS 레코드를 둘 수 있고, 각 네임서버가 동시에 Authoritative DNS 서버로 동작할 수 있기 때문입니다. 즉, ns1만 일하는 구조가 아니라 ns1, ns2가 모두 같은 Zone을 들고 동시에 응답하는 구조를 만들 수 있습니다.

Authoritative DNS 서버란?
DNS 서버라고 해서 모두 같은 역할을 하는 것은 아닙니다. 사용자의 요청을 대신 여러 DNS 서버에 물어봐 주는 서버가 있고, 특정 도메인에 대한 정답을 직접 가지고 있는 서버가 있습니다.
이 중 특정 zone 데이터를 가지고 있고, 해당 도메인에 대한 DNS 응답을 직접 제공하는 서버를 Authoritative DNS 서버라고 합니다. 예를 들어 example.com Zone을 관리하는 DNS 서버라면, www.example.com이나 api.example.com 같은 이름에 대해 authoritative한 응답을 줄 수 있습니다.

반대로 통신사 DNS, 8.8.8.8, 1.1.1.1 같은 서버는 보통 resolver 역할을 합니다. 이들은 모든 도메인의 정답을 직접 가지고 있는 것이 아니라, 필요한 경우 Authoritative DNS 서버를 찾아가 값을 물어보고 그 결과를 사용자에게 돌려줍니다.

 

다만 여기서 중요한 전제가 있습니다. 여러 DNS 서버가 같은 zone 데이터를 가지고 있어야 합니다. 예를 들어 ns1.example.com www.example.com -> 192.0.2.10을 가지고 있는데, ns2.example.com은 예전 값인 www.example.com -> 192.0.2.20을 가지고 있다면 문제가 됩니다. 사용자가 어떤 네임서버에 질의했는지에 따라 서로 다른 IP가 응답될 수 있기 때문입니다.

 

active-active 이중화 방식에서는 모든 Secondary 서버가 Primary 서버의 데이터와 일치해야 합니다

 

따라서 DNS 서버를 여러 대 운영할 때는 단순히 서버를 여러 대 띄우는 것만으로는 충분하지 않습니다. 모든 서버가 같은 Zone 데이터를 유지하도록 동기화하는 방식이 필요합니다.

 

BIND9에서는 이를 위해 Primary / Secondary DNS 이중화 구조와 Zone Transfer 프로토콜를 제공합니다.


Primary - Secondary DNS와 Zone Transfer

앞에서 DNS 서버를 여러 대 운영하려면 모든 서버가 같은 zone 데이터를 가지고 있어야 한다고 이야기했습니다. BIND9에서는 이를 위해 Primary / Secondary DNS 이중화 구조를 제공합니다.

 

Primary DNS 서버는 zone 데이터의 원본을 가지고 있는 서버입니다. zone 파일을 직접 관리하고, 레코드 추가나 수정도 보통 Primary 서버를 기준으로 이루어집니다.

 

반면 Secondary DNS 서버는 zone 데이터를 직접 수정하지 않습니다. 대신 Primary 서버로부터 zone 데이터를 복제해서 가져오고, 그 데이터를 기준으로 DNS 요청에 응답합니다. 즉, Secondary 서버는 독립적으로 다른 데이터를 관리하는 서버가 아니라 Primary의 zone 상태를 따라가는 서버입니다.

 

Primary에서 Secondary로 zone 데이터를 복제하는 과정을 Zone Transfer라고 합니다. BIND9에서는 대표적으로 두 가지 방식이 사용됩니다.

처음 Secondary 서버가 zone 데이터를 가져올 때는 보통 AXFR이 사용됩니다. 아직 로컬에 zone 데이터가 없기 때문에 Primary 서버로부터 전체 zone을 받아와야 합니다.

 

이후 일부 레코드만 변경된 경우에는 IXFR을 사용할 수 있습니다. IXFR은 전체 zone을 다시 가져오는 대신 변경된 부분만 받아오기 때문에, zone 크기가 커질수록 더 효율적입니다.

 

이때 변경 여부를 판단하는 기준이 zone 파일 예시의 상단에 있던 SOA serial입니다. SOA 레코드에는 zone의 버전 번호처럼 사용되는 serial 값이 들어 있습니다. Primary에서 zone 데이터가 변경되면 serial 값이 증가하고, Secondary는 이 값을 비교해서 자신이 가진 zone이 최신인지 확인합니다.

Primary / Secondary 서버 간 동기화에 사용되는 AXFR / IXFR 프로토콜

 

전체 동기화 흐름을 간단히 정리하면 다음과 같습니다.

  1. Primary 서버에서 Zone 데이터가 변경됩니다.
  2. Primary의 SOA serial 값이 증가합니다.
  3. Secondary 서버가 Primary의 serial을 확인합니다.
  4. Primary의 serial이 더 크면 Secondary가 Zone Transfer를 요청합니다.
  5. Primary는 AXFR 또는 IXFR로 Zone 데이터를 전달합니다.
  6. Secondary는 받은 데이터를 로드하고 DNS 응답에 사용합니다.

BIND9에서 Primary / Secondary 서버 구성하기

개념을 봤으니, 이제 BIND9에서는 Primary / Secondary 구성을 어떻게 설정하는지 간단히 살펴보겠습니다.

 

예시로 example.com zone을 다음과 같이 구성한다고 가정하겠습니다.

  • Primary DNS: ns1.example.com (192.0.2.10)
  • Secondary DNS: ns2.example.com (192.0.2.20)
  • Zone: example.com

Primary 서버는 zone 파일의 원본을 가지고 있습니다. 따라서 Primary 서버의 named.conf에는 해당 zone을 primary로 선언하고, Secondary 서버가 Zone Transfer를 요청할 수 있도록 allow-transfer를 설정합니다.

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { 192.0.2.20; };
};

 

그리고 Primary 서버에는 아래와 같은 실제 zone 파일이 존재해야 합니다.

$TTL 3600
@   IN  SOA ns1.example.com. admin.example.com. (
        2026072901 ; serial
        3600       ; refresh
        1800       ; retry
        604800     ; expire
        86400      ; minimum
)

@       IN  NS      ns1.example.com.
@       IN  NS      ns2.example.com.
ns1     IN  A       192.0.2.10
ns2     IN  A       192.0.2.20
www     IN  A       192.0.2.100

 

여기서 중요한 값은 allow-transferSOA serial입니다. allow-transfer는 어떤 서버가 이 zone을 복제해 갈 수 있는지 지정합니다. 위 예시에서는 Secondary 서버인 192.0.2.20example.com zone을 가져갈 수 있습니다. 이 설정이 없다면 원하지 않는 서버가 Zone Transfer를 요청할 수 있기 때문에, 운영 환경에서는 반드시 제한하는 것이 좋습니다.

 

Secondary 서버에서는 같은 zone을 secondary로 선언합니다. Secondary는 직접 zone 파일을 작성하지 않고, Primary 서버에서 받아온 zone 데이터를 임시로 저장할 로컬 파일 경로를 지정합니다.

zone "example.com" {
    type secondary;
    masters { 192.0.2.10; };
    file "/var/cache/bind/db.example.com";
};

 

여기서 masters는 zone 데이터를 가져올 Primary 서버를 의미합니다. Secondary 서버는 192.0.2.10에 있는 Primary 서버로부터 example.com zone을 받아오고, 받은 데이터를 /var/cache/bind/db.example.com에 저장합니다.

 

이제 설정을 적용하면 Secondary 서버는 Primary 서버의 SOA serial을 확인합니다. 자신이 가진 serial보다 Primary의 serial이 더 크거나, 로컬에 zone 데이터가 없는 경우 Zone Transfer를 수행합니다. 처음에는 zone 전체를 가져오는 AXFR이 사용되고, 이후 변경분만 가져올 수 있는 상황에서는 IXFR이 사용될 수 있습니다(상황에 따라 AXFR이 사용되기도 합니다).

 

앞서 배운 내용을 정리하면 다음과 같습니다.

  • Primary는 zone 원본 파일을 가진다.
  • Primary는 allow-transfer로 Secondary의 접근을 허용한다.
  • Secondary는 masters로 Primary 서버를 지정한다.
  • zone 변경 시 SOA serial을 증가시킨다.
  • Secondary는 serial을 비교한 뒤 필요하면 Zone Transfer를 수행한다.

이제 여기서 한 가지 아쉬운 점이 생깁니다. Secondary가 Primary의 변경을 알아차리려면 기본적으로 주기적으로 SOA serial을 확인해야 합니다. 하지만 변경이 발생했을 때 Secondary가 다음 확인 주기까지 기다려야 한다면 반영이 늦어질 수 있습니다.

 

이 문제를 해결하기 위해 BIND9에서는 NOTIFY라는 기능을 제공합니다.


NOTIFY

앞에서 Secondary 서버는 Primary 서버의 SOA serial을 확인해서 zone 데이터가 바뀌었는지 판단한다고 했습니다. 그런데 여기에는 한 가지 문제가 있었습니다.

 

Secondary 서버가 Primary 서버를 계속 실시간으로 감시하는 것이 아니기 때문에 zone 데이터가 바뀌었음에도 serial이 변경되었는지 주기적으로 확인하는 시점(SOA 레코드의 refresh 값에 해당)이 오기 전에는 Secondary가 동기화를 시작하지 않습니다. 예를 들어 refresh 값이 3600초라면, Secondary 서버는 대략 1시간마다 Primary 서버의 SOA serial을 확인하고, 이 설정에서는 최악의 경우 업데이트가 1시간 뒤에 이루어질 수도 있습니다.

 

즉, Primary에는 최신 레코드가 반영되었지만 Secondary는 아직 이전 데이터를 응답하는 시간이 생길 수 있습니다. 이 지연을 줄이기 위해 BIND9에서는 NOTIFY 기능을 제공합니다.

zone 데이터가 바뀌었음을 Secondary 서버에 알려주는 NOTIFY

 

쉽게 말해서 NOTIFY는 Primary 서버가 Secondary 서버에게 보내는 변경 알림입니다. Primary에서 zone 데이터가 변경되면, Primary는 설정된 Secondary 서버들에게 “이 zone이 변경됐으니 확인해 봐”라는 신호를 보냅니다.

 

여기서 중요한 점은 NOTIFY가 zone 데이터를 직접 보내는 기능은 아니라는 점입니다. NOTIFY는 말 그대로 알림이기 때문에 Primary가 Secondary에게 “데이터가 바뀐 것 같으니 확인해 봐”라고 알려줄 뿐이고, 실제 zone 데이터 전송은 여전히 AXFR 또는 IXFR을 통해 이루어집니다.

 

NOTIFY를 받은 Secondary 서버는 바로 Primary 서버의 SOA serial을 확인합니다(DNS SOA 쿼리 요청). 그리고 자신이 가지고 있는 serial보다 Primary의 serial이 더 크다면, 그때 Zone Transfer를 요청합니다.

 

전체 흐름을 정리하면 다음과 같습니다.

  1. Primary 서버에서 zone 데이터가 변경됩니다.
  2. Primary의 SOA serial 값이 증가합니다.
  3. Primary가 Secondary 서버에게 NOTIFY를 보냅니다.
  4. Secondary는 NOTIFY를 받고 Primary의 SOA serial을 확인합니다(DNS SOA 쿼리 요청).
  5. Primary의 serial이 더 크면 Secondary가 Zone Transfer를 요청합니다.
  6. Primary는 AXFR 또는 IXFR로 Zone 데이터를 전송합니다.
  7. Secondary는 받은 zone 데이터를 로드하고 이후 DNS 응답에 사용합니다.

BIND9에서는 Primary Zone 설정에 also-notify를 추가해서 NOTIFY를 보낼 Secondary 서버를 지정할 수 있습니다.

zone "example.com" {
    type primary;
    file "/etc/bind/zones/db.example.com";
    allow-transfer { 192.0.2.20; };
    also-notify { 192.0.2.20; };
};

 

allow-transfer는 “이 서버가 zone 데이터를 가져가도 된다”는 허용 목록이었다면 also-notify는 “변경이 생기면 이 서버에게 알려준다”는 알림 대상 목록에 해당합니다.


Catalog Zone과 zone 목록 동기화

앞서 Primary / Secondary 구조, Zone Transfer, NOTIFY에 대해 살펴봤습니다. 이 구조를 사용하면 이미 등록되어 있는 zone의 데이터는 Secondary 서버로 복제할 수 있습니다. 예를 들어 Primary와 Secondary 양쪽에 example.com zone이 설정되어 있다면, www.example.com 레코드가 변경되었을 때 Secondary는 AXFR 또는 IXFR을 통해 변경된 Zone 데이터를 가져올 수 있습니다.

 

하지만 여기에는 아직 해결되지 않은 문제가 하나 있습니다.

 

Zone Transfer는 “이미 Secondary 서버가 알고 있는 zone”에 대해서만 동작합니다. 즉, Secondary 서버가 example.com이라는 zone을 관리해야 한다는 사실을 알고 있어야 Primary에게 해당 zone 데이터를 요청할 수 있습니다.

 

예를 들어 Primary 서버에 example.com zone을 새로 추가했다고 가정해보겠습니다. 이때 Primary 서버에는 example.com zone 파일이 있고, 해당 zone을 서비스할 준비도 되어 있습니다. 하지만 Secondary 서버의 설정에 example.com zone이 없다면 Secondary는 이 zone을 자동으로 가져오지 않습니다.

 

Secondary 입장에서는 example.com이라는 zone이 새로 생겼는지 알 방법이 없기 때문입니다. 기존 방식에서는 아래와 같이 새로운 zone을 추가할 때마다 Secondary 서버에도 직접 zone 설정을 추가해야 했습니다.

zone "example.com" {
    type secondary;
    masters { 192.0.2.10; };
    file "/var/cache/bind/db.example.com";
};

 

사실 zone이 한두 개라면 이 방식도 큰 문제가 되지 않긴하지만 관리해야 할 zone이 많아지고 Secondary 서버도 여러 대가 되면 점점 번거로워집니다.

 

예를 들어 zone을 하나 추가할 때마다 다음 작업을 해야 합니다.

  1. Primary 서버에 zone을 추가합니다.
  2. 각 Secondary 서버의 named.conf에도 같은 zone을 추가합니다.
  3. Secondary 서버마다 설정 파일을 다시 로드합니다.
  4. 모든 서버가 정상적으로 zone을 가져왔는지 확인합니다.

서버가 한두 대라면 괜찮지만, Secondary 서버가 여러 대라면 설정 누락이나 오타가 생기기 쉽습니다. 어떤 Secondary에는 zone이 추가되고, 어떤 Secondary에는 빠지는 상황도 발생할 수 있습니다.

zone 목록 자체를 동기화하기 위한 Catalog Zone

 

이 문제를 해결하기 위해 사용할 수 있는 기능이 Catalog Zone입니다. Catalog Zone은 쉽게 말하면 “Secondary 서버가 어떤 Zone들을 가져와야 하는지 알려주는 목록” 입니다. 사실 Catalog Zone 기능은 2016에 발표된 꽤 최신(?) 기능이지만 이후 표준화가 진행되어 BIND9뿐만 아니라 다양한 DNS 서버들도 지원합니다.

일반적인 zone 파일이 example.com 안의 A, CNAME, TXT 같은 DNS 레코드를 담고 있다면, Catalog Zone은 서비스용 레코드가 아니라 관리 대상 zone 목록을 담는 특수한 zone 파일입니다. 즉, “example.com도 가져오고, example.net도 가져오고, example.org도 가져와라” 같은 정보를 담는 특수한 zone이라고 볼 수 있습니다.

 

Catalog Zone을 사용했을 때의 동작 흐름을 살펴봅시다.

Catalog Zone을 통한 zone 목록 동기화 흐름

 

Primary 서버는 Catalog Zone에 관리할 zone 목록을 등록하고, Secondary 서버는 이 Catalog Zone을 동기화하도록 설정합니다. 그러면 Secondary는 Catalog Zone을 보고 자신이 어떤 zone을 가져와야 하는지 알 수 있습니다.

 

예를 들어 Catalog Zone에 example.com이 추가되면, Secondary는 이를 보고 example.com zone을 자동으로 Secondary zone으로 추가합니다. 이후 실제 example.com의 zone 데이터는 기존과 동일하게 AXFR 또는 IXFR을 통해 Primary에서 가져옵니다. 반대로 Catalog Zone에서 example.com이 제거되면, Secondary도 더 이상 해당 zone을 관리하지 않도록 정리할 수 있습니다.

 

리하면 역할은 이렇게 나눌 수 있습니다.

  • Zone Transfer는 zone 안의 레코드 데이터를 복제합니다.
  • NOTIFY는 zone 데이터가 변경되었음을 알려줍니다.
  • Catalog Zone은 어떤 zone들을 관리해야 하는지 알려줍니다.

즉, AXFR과 IXFR이 example.com 안의 레코드를 동기화하는 기능이라면, Catalog Zone은 example.com이라는 zone 자체를 Secondary 서버에 자동으로 등록하게 해주는 기능입니다.

 

흐름을 간단히 정리하면 다음과 같습니다.

  1. Primary 서버에 Catalog Zone을 설정합니다.
  2. Secondary 서버는 Primary의 Catalog Zone과 동기화하도록 설정합니다.
  3. Primary의 Catalog Zone에 example.com을 추가합니다.
  4. Secondary는 Catalog Zone 변경을 감지합니다(Primary에서 NOTIFY를 보내거나 refresh).
  5. Secondary는 example.com을 자동으로 Secondary Zone으로 등록합니다.
  6. 이후 example.com의 실제 레코드 데이터는 AXFR 또는 IXFR로 가져옵니다.

이 구조를 사용하면 새로운 zone을 추가할 때마다 모든 Secondary 서버의 설정 파일을 직접 수정할 필요가 줄어듭니다. Primary의 Catalog Zone에 zone을 등록하면, Secondary 서버들이 그 목록을 보고 필요한 zone을 자동으로 구성할 수 있기 때문입니다.


BIND9에서 Catalog Zone 구성하기

Catalog Zone도 결국 하나의 DNS zone이기 때문에 Primary 서버에서는 Catalog Zone을 일반 zone처럼 설정합니다. 다만 이 zone은 사용자 요청에 응답하기 위한 zone이라기보다는, Secondary 서버에게 “어떤 zone들을 가져가야 하는지” 알려주기 위한 관리용 zone일 뿐입니다.

 

BIND9에서 Catalog Zone을 직접 구성해 봅시다.

  • Primary DNS: ns1.example.com (192.0.2.10)
  • Secondary DNS: ns2.example.com (192.0.2.20)
  • Catalog Zone: catalog.example.com
  • Member Zone: example.com

먼저 Primary 서버에 Catalog Zone을 추가합니다.

zone "catalog.example.com" {
    type primary;
    file "/etc/bind/zones/db.catalog.example.com";

    allow-transfer { 192.0.2.20; };
    also-notify { 192.0.2.20; };
};

 

여기서 catalog.example.com은 Catalog Zone 자체입니다. Secondary 서버도 이 Catalog Zone을 받아가야 하므로 allow-transfer에 Secondary 서버의 IP를 추가합니다. 그리고 Catalog Zone이 변경되었을 때 Secondary가 빠르게 감지할 수 있도록 also-notify도 함께 설정합니다.

 

Catalog Zone 파일은 아래와 같은 형태입니다.

$ORIGIN catalog.example.com.
$TTL 3600

@   IN  SOA . . (
        2026072901 ; serial
        3600       ; refresh
        1800       ; retry
        604800     ; expire
        86400      ; minimum
)

@       IN  NS      invalid.
version IN  TXT     "2"

example-com.zones IN PTR example.com.

 

여기서 version IN TXT "2"는 Catalog Zone의 포맷 버전을 의미합니다. 그리고 example-com.zones IN PTR example.com.example.com을 Catalog Zone의 member zone으로 등록하는 부분입니다. 즉, 이 레코드는 Secondary 서버에게 “example.com zone도 가져와서 관리해야 한다”라고 알려주는 역할을 합니다.

 

이제 Secondary 서버 설정을 보겠습니다. 먼저 Secondary 서버는 Catalog Zone 자체를 Primary에서 받아와야 합니다.

zone "catalog.example.com" {
    type secondary;
    primaries { 192.0.2.10; };
    file "/var/cache/bind/db.catalog.example.com";
};

 

여기까지는 일반적인 Secondary zone 설정과 거의 같습니다. Secondary는 Primary 서버인 192.0.2.10에서 catalog.example.com zone을 받아와 로컬에 저장합니다.

 

하지만 이 설정만으로는 BIND9이 해당 zone을 Catalog Zone으로 사용하지 않습니다. 그래서 별도로 catalog-zones 설정을 추가해야 합니다.

catalog-zones {
    zone "catalog.example.com" {
        default-primaries { 192.0.2.10; };
    };
};

 

default-primaries는 Catalog Zone에 등록된 member zone들을 어느 Primary 서버에서 가져올지 지정합니다. 위 예시에서는 example.com이 Catalog Zone에 추가되면, Secondary 서버는 192.0.2.10에서 example.com zone 데이터를 가져옵니다.

 

이제 모든 설정이 끝났습니다. 만약 Primary 서버에서 Catalog Zone에 다음 레코드를 추가하면:

example-net.zones IN PTR example.net.

 

Secondary 서버는 example.net을 새로 관리해야 할 zone으로 인식합니다. 그리고 default-primaries에 지정된 Primary 서버에서 example.net의 실제 zone 데이터를 AXFR 또는 IXFR로 가져옵니다.

 

이제 기존 RNDC 방식의 문제점을 해결하기 위해 고안한 새로운 방식을 이해하는데 필요한 모든 준비가 끝났습니다. 앞서 배운 내용들이 Bindizr에 어떻게 적용되는지 살펴봅시다.


3. BIND9 동기화 구조를 그대로 사용한 새로운 방식

앞에서 살펴본 내용을 다시 정리하면, BIND9은 이미 Primary / Secondary 구조를 통해 여러 DNS 서버가 같은 zone 데이터를 유지할 수 있는 방식을 제공합니다. Primary 서버가 zone의 원본을 가지고 있고, Secondary 서버는 AXFR 또는 IXFR을 통해 Primary의 Zone 데이터를 가져옵니다. NOTIFY를 사용하면 Primary의 변경 사항을 Secondary가 더 빠르게 감지할 수 있고, Catalog Zone을 사용하면 어떤 zone을 동기화해야 하는지에 대한 목록까지 전달할 수 있습니다.

 

그렇다면 Bindizr도 이 구조를 그대로 활용할 수 있지 않을까 생각했습니다.

 

기존 RNDC 방식에서는 Bindizr가 BIND9의 zone 파일을 직접 수정하고, RNDC를 통해 reload를 호출했습니다. 이 방식은 동작 자체는 단순했지만, 결국 파일 수정이라는 문제로 인한 제약이 매우 많았습니다.

 

그래서 새로운 방식에서는 파일을 직접 수정하는 방향을 버리고, BIND9이 원래 DNS 서버 간 동기화에 사용하는 프로토콜을 그대로 사용하기로 했습니다. 핵심 아이디어는 다음과 같습니다.

BIND9이 DNS 서버 간 동기화 시 사용하는 AXFR / IXFR 프로토콜을 Bindizr가 직접 제공하자.

이 구조에서는 Bindizr가 단일 원천인 데이터베이스를 기준으로 zone 데이터를 관리하고 각 BIND9 서버는 Bindizr를 Primary 서버처럼 보고, 필요한 zone 데이터를 AXFR 또는 IXFR을 통해 가져갑니다. 즉, Bindizr가 각 BIND9 서버의 파일 시스템에 접근하는 것이 아니라, BIND9 서버들이 DNS 표준 프로토콜을 통해 Bindizr로부터 데이터를 가져오는(동기화 받는) 구조입니다.

BIND9의 Zone Transfer 동기화를 그대로 사용하는 새로운 아키텍처

 

전체 구조는 크게 세 부분으로 볼 수 있습니다.

 

먼저 Control Plane입니다. 이 계층에는 Bindizr의 API, CLI 그리고 Zone / Record DB가 있습니다. 사용자가 API나 CLI로 zone 또는 record를 변경하면 그 내용은 DB에 저장됩니다. 결국 이 구조에서는 DB가 Bindizr가 관리하는 zone 상태의 기준이 됩니다.

 

다음은 XFR 서버입니다. XFR 서버는 DB에 저장된 zone 데이터를 읽어 BIND9 서버들에게 전달하는 역할을 합니다. SOA Serial Manager는 zone의 serial을 관리하고, XFR Server는 BIND9에서 들어오는 AXFR / IXFR 요청을 처리합니다. BIND9 Secondary 서버가 Zone Transfer를 요청하면, Bindizr는 DB 상태를 기준으로 응답을 만들어 돌려줍니다.

 

마지막은 DNS Layer입니다. 여기에는 실제 DNS 질의에 응답하는 BIND9 서버들이 있습니다. 그림의 S1, S2, SN이 여기에 해당합니다. 이 서버들은 클라이언트의 DNS Query를 직접 처리하는 Authoritative DNS 서버입니다. 대신 zone 데이터의 원본을 직접 관리하지 않고, Bindizr XFR 서버에서 AXFR 또는 IXFR로 데이터를 받아와 사용합니다.

 

이 구조를 흐름으로 보면 다음과 같습니다.

  1. 사용자가 Bindizr API / CLI 통해 zone이나 record를 변경합니다.
  2. 변경 사항은 Bindizr의 DB에 저장됩니다.
  3. SOA Serial Manager가 해당 zone의 serial을 갱신하고 BIND9 Secondary 서버들에게 NOTIFY를 보냅니다.
  4. BIND9 Secondary 서버들은 Bindizr XFR 서버를 Primary처럼 보고 AXFR 또는 IXFR을 요청합니다.
  5. Bindizr XFR 서버는 DB를 기준으로 zone 데이터를 응답합니다.
  6. BIND9 서버들은 받은 데이터를 로드하고, 이후 클라이언트의 DNS Query에 응답합니다.

이 방식의 가장 큰 차이는 Bindizr가 더 이상 BIND9 서버의 파일 시스템에 직접 접근하지 않는다는 점입니다. zone 파일을 생성해서 각 서버에 복사하거나, SSH로 원격 서버에 접속하거나, RNDC reload를 직접 호출할 필요가 없습니다. BIND9 서버들은 자신들이 원래 Secondary 서버로 동작할 때 사용하던 방식 그대로 zone 데이터를 가져오면 됩니다.

 

또한 DNS 응답 경로와 관리 경로도 분리됩니다. 클라이언트의 DNS Query는 Bindizr가 아니라 BIND9 서버들이 처리합니다. Bindizr는 DNS 요청을 직접 받아 응답하는 서버가 아니라, zone과 record 상태를 관리하고 이를 BIND9 서버들에게 동기화해 주는 Control Plane에 가깝워지게 됩니다.

 

이 덕분에 여러 BIND9 서버를 추가하는 것도 훨씬 자연스러워졌습니다. 새로운 BIND9 서버를 Secondary로 구성하고 Bindizr XFR 서버를 바라보게 하면, 해당 서버는 AXFR / IXFR을 통해 필요한 zone 데이터를 가져와 Authoritative DNS 서버로 동작할 수 있습니다.

 

또한 여기에 앞서 소개한 Catalog Zone을 사용해 zone 목록 관리도 단순화했습니다. 새로운 Zone이 추가될 때마다 모든 BIND9 서버의 설정 파일을 직접 수정하는 대신, Bindizr가 Catalog Zone을 통해 “어떤 Zone을 동기화해야 하는지”를 전달합니다. BIND9 서버들은 Catalog Zone을 보고 필요한 zone을 자동으로 등록하고, 실제 zone 데이터는 다시 AXFR 또는 IXFR로 가져갑니다.

 

정리하면 새로운 Bindizr 구조는 다음과 같습니다. DB를 단일 원천으로 두고, Bindizr XFR 서버가 BIND9 Secondary 서버들에게 AXFR / IXFR로 Zone 데이터를 제공하는 구조입니다.

 

이 방식으로 Bindizr는 기존 RNDC 방식의 파일 접근 문제를 제거하면서도, BIND9이 원래 제공하는 Primary / Secondary 동기화 모델을 그대로 활용할 수 있게 되었습니다.


새로운 방식의 문제점

사실 새로운 구조가 모든 문제를 쉽게 해결해준 것은 아닙니다. RNDC 방식의 문제는 줄어들었지만, 그 대신 Bindizr가 직접 책임져야 하는 부분이 늘어났습니다.

 

첫 번째 어려움은 AXFR / IXFR 프로토콜을 직접 구현해야 했다는 점입니다. BIND9 입장에서는 Bindizr가 Primary 서버처럼 동작해야 하므로, 단순히 DB 데이터를 JSON으로 내려주는 수준이 아니라 실제 DNS Zone Transfer 요청에 맞는 응답을 만들어야 했습니다. 하지만 Rust에서 바로 사용할 수 있는 구현체가 없어, DNS 메시지 구조와 AXFR / IXFR 동작 방식을 직접 분석해 구현해야 했습니다.

 

두 번째는 IXFR 구현이 생각보다 까다로웠다는 점입니다. AXFR은 현재 zone 전체를 전송하면 되지만, IXFR은 특정 serial에서 현재 serial까지 어떤 레코드가 추가되고 삭제되었는지 변경분을 계산해야 합니다. 즉, 단순히 현재 DB 상태만 가지고는 부족하고, 이전 상태와 현재 상태의 차이를 추적하거나 변경 이력을 관리해야 합니다.

 

세 번째는 SOA serial 관리입니다. Secondary 서버는 serial 값을 기준으로 zone이 변경되었는지 판단합니다. 따라서 record가 추가, 수정, 삭제될 때마다 적절한 시점에 serial을 증가시켜야 하고, 여러 변경이 동시에 들어오는 경우에도 serial 순서가 꼬이지 않도록 관리해야 합니다. serial이 잘못 관리되면 Secondary가 변경을 감지하지 못하거나, 반대로 불필요한 Zone Transfer가 반복될 수 있습니다.

 

이 중에서 IXFR를 구현하기 위한 변경분 저장과 계산 로직에 대해 조금 더 알아봅시다.


IXFR를 위한 변경분 저장과 계산 그리고 롤백

IXFR과 달리 AXFR은 비교적 단순합니다. 요청이 들어오면 현재 zone 전체를 만들어서 보내면 됩니다. 하지만 IXFR은 “현재 상태”만으로는 응답을 만들 수 없습니다. Secondary 서버는 자신이 가진 serial을 기준으로 “이 serial 이후에 바뀐 내용만 보내달라”고 요청하기 때문입니다.

 

예를 들어 Secondary가 2026072901 serial을 가지고 있고, 현재 Primary의 serial이 2026072904라면, XFR 서버는 그 사이에 있었던 2026072902, 2026072903, 2026072904에서 각각 어떤 변경이 있었는지 알고 있어야 합니다.

 

그래서 Bindizr는 zone의 현재 상태만 저장하지 않고, serial별 변경 이력도 함께 저장하도록 했습니다. 레코드 수정도 내부적으로는 삭제와 추가로 나누어 기록합니다. 예를 들어 www.example.com의 A 레코드가 192.0.2.10에서 192.0.2.20으로 바뀌었다면, 변경 이력은 다음과 같은 형태가 됩니다.

serial: 2026072902
delete: www.example.com. A 192.0.2.10
add:    www.example.com. A 192.0.2.20

 

DNS 레코드 관점에서 “수정”은 결국 기존 레코드를 제거하고 새로운 레코드를 추가하는 것과 같습니다. 그래서 IXFR 응답을 만들려면 각 serial마다 삭제된 레코드와 추가된 레코드를 구분해서 보관해야 했습니다.

 

이렇게 변경 이력을 쌓아두면 Secondary가 특정 serial을 기준으로 IXFR을 요청했을 때, Bindizr는 해당 serial 이후의 변경분만 모아서 응답할 수 있습니다.

Secondary serial: 2026072901
Current serial:   2026072904

IXFR 응답에 필요한 변경분:
- 2026072902의 delete / add
- 2026072903의 delete / add
- 2026072904의 delete / add

 

구현은 단순하지 않았지만, 이 구조 덕분에 zone 전체를 매번 전송하지 않아도 되었습니다. zone 크기가 커질수록 AXFR보다 IXFR의 장점이 커지고, Secondary 서버도 필요한 변경분만 받아 더 빠르게 동기화할 수 있습니다.

 

여기서 잘 생각해보면, serial별 변경 이력을 이용해 이전 상태를 역산할 수도 있습니다. 각 serial마다 어떤 레코드가 추가되고 삭제되었는지 알고 있다면, 변경분을 거꾸로 적용해서 특정 serial 시점의 zone 상태를 다시 만들 수 있습니다. 예를 들어 현재 serial이 2026072904이고 2026072902 시점으로 되돌리고 싶다면, 2026072904, 2026072903에서 발생한 변경을 역순으로 적용하면 됩니다.

정방향 변경:
delete old-record
add    new-record

역방향 적용:
delete new-record
add    old-record

 

즉, IXFR을 위해 저장한 변경 이력은 단순히 Secondary 동기화에만 쓰지 않고. 같은 데이터를 이용해 특정 serial 시점의 zone 상태를 재구성할 수 있고, 이를 바탕으로 snapshot이나 rollback 기능도 구현할 수 있습니다.

 

지금까지 다룬 내용을 정리하면 아래 그림과 같습니다.

IXFR을 구현하기 위한 변경분 저장 방식과 계산, 그리고 이를 역이용하여 롤백 구현


4. 그래서 실제로 쓸 만한가?

지금까지는 기존 RNDC 방식의 한계와 이를 해결하기 위해 바꾼 아키텍처를 살펴봤습니다. 하지만 구조가 좋아 보인다고 해서 실제로 운영에 쓸 수 있는 것은 아닙니다. DNS 서버를 관리하는 도구라면 결국 중요한 질문이 남습니다.

그래서 성능은 충분한가? 기존 DNS 서버들에 비해서 쓸만한 수준인가?

 

zone을 몇 개 관리하는 수준에서는 대부분의 방식이 큰 차이를 보이지 않을 수 있습니다. 하지만 레코드 수가 많아지고, 변경 요청이 자주 발생하고, 여러 Secondary 서버가 동시에 동기화를 요청하는 상황에서는 이야기가 달라집니다.

 

특히 Bindizr는 단순히 REST API만 제공하는 서버가 아니라, BIND9 Secondary 서버들에게 AXFR / IXFR 응답을 직접 제공해야 합니다. 즉, DB에서 zone 데이터를 읽어오고, DNS 메시지 형식으로 변환한 뒤, BIND9이 기대하는 방식으로 전송해야 합니다. 따라서 새로운 구조가 실제로 의미 있으려면 다음 질문에 답할 수 있어야 했습니다.

  • 레코드 생성, 수정, 삭제 성능은 어느 정도인가?
  • 많은 레코드를 가진 zone을 얼마나 빠르게 가져올 수 있는가?
  • AXFR로 zone 전체를 전송할 때 충분히 빠른가?
  • IXFR로 변경분만 전송할 때 실제로 이점이 있는가?
  • 변경된 레코드가 BIND9 서버에서 DNS 응답으로 보이기까지 얼마나 걸리는가?

그래서 다음으로는 Bindizr의 성능을 측정한 벤치마킹 방식과 그 결과를 살펴보겠습니다.


어떻게 측정했나

성능을 이야기하려면 먼저 "무엇을, 어떻게 쟀는지"가 명확해야 합니다. 우선 우리의 비교 대상은 상용으로 많이 사용되는 PowerDNS, Technitium DNS, Knot DNS, CoreDNS, 그리고 전통적 방식인 BIND9 + nsupdate / rndc입니다. 또한 서로 아키텍처가 다른 시스템을 비교하는 것이기 때문에  공정성에 신경을 많이 썼습니다.

  • 동일한 워크로드 — 모든 시스템이 같은 어댑터 인터페이스를 구현하고, 동일한 데이터셋을 동일한 순서로 받습니다. REST API냐 dynamic update냐 하는 차이는 어댑터 안에 숨깁니다.
  • 동일한 자원 제한 — 모든 컨테이너에 4 CPU / 4 GB 제한을 걸고, 한 번에 한 시스템만 실행해 호스트 자원 경합을 없앴습니다.
  • 5회 반복 평균 — 모든 수치는 5회 실행의 평균이며 표준편차를 함께 기록합니다.
  • End-to-end 지표 — 시스템마다 write의 의미가 다르므로(메모리에 바로 반영 vs DB 커밋 후 전파), write 경로의 정직한 비교 기준은 "API 호출부터 DNS 조회로 보일 때까지"의 시간으로 잡았습니다.

측정 항목은 레코드 CRUD TPS, 대량 import, 변경 전파 지연, AXFR/IXFR, 대형 존, DB 백엔드별 비교, DNS 쿼리 성능, 자원 사용량까지 9가지였습니다. 이 중에서 몇 가지만 살펴보겠습니다.

결과

가장 먼저 확인하고 싶었던 것은 DNS 쿼리 성능이었습니다. 새로운 구조에서 Bindizr는 클라이언트의 DNS Query에 직접 응답하지 않습니다. 실제 DNS 응답은 BIND9 Secondary 서버가 처리합니다. 따라서 구조가 의도대로 동작한다면, 쿼리 성능은 Bindizr를 붙였다고 해서 크게 달라지지 않아야 합니다.

 

실제로 측정 결과도 비슷했습니다. Bindizr + BIND9 구성은 62,448 QPS, native BIND9은 61,629 QPS로 거의 같은 수준이었습니다. 이 부하 동안 Bindizr의 CPU 사용률은 0.7% 정도였습니다. 이를 통해 DNS 쿼리 경로에서 Bindizr가 빠져 있고, BIND9이 그대로 응답을 처리한다는 점을 확인할 수 있었습니다.

 

대량 import 성능도 확인했습니다. 10,000개 레코드가 들어 있는 zone 파일은 76ms에 import되었고, 처리량으로 보면 약 132,720 records/sec입니다.

 

또한 현재 구조에서 가장 중요한 부분 중 하나인 IXFR의 동작도 확인하였습니다. 100,000개 레코드가 있는 zone에서 레코드 1개만 변경했을 때, IXFR로 전송된 데이터는 736 bytes였습니다. 전체 zone 크기가 약 5.5 MB였다는 점을 생각하면, 실제로 변경분만 전송되고 있음을 확인할 수 있었습니다.

 

반대로 write 경로에서는 다른 시스템보다 느린 부분도 있었습니다. 레코드 생성 API는 약 6.4ms에 응답했지만 변경된 레코드가 실제 BIND9 Secondary 서버에서 DNS 응답으로 보이기까지는 중앙값 65.7ms, p95 83ms가 걸렸습니다. TPS 기준으로 보면 571 TPS(가장 빠른 PostgreSQL 기준)까지 올라가지만, 메모리에서 바로 반영되는 Technitium 같은 통합형 DNS 서버가 약 9,000 TPS 수준을 보인 것과 비교하면 낮은 수치입니다. 즉, Bindizr는 쿼리 성능이나 XFR 성능에서는 충분히 좋은 결과를 보였지만, 레코드 변경이 DNS 응답으로 보이기까지의 시간과 write 처리량에서는 더 빠른 시스템들이 있었습니다.

 

사실 이는 구조적인 이유에서 발생합니다. Technitium처럼 API와 DNS 응답을 같은 서버가 처리하는 구조에서는 레코드 변경을 메모리에 바로 반영할 수 있습니다. 반면 Bindizr는 Control Plane과 DNS 응답 계층을 분리했습니다. Bindizr는 API 요청을 받아 DB에 커밋하고 zone serial을 증가시킨 뒤, BIND9 Secondary 서버가 NOTIFYIXFR을 통해 변경분을 가져가야 합니다. Secondary가 변경된 zone 데이터를 로드한 뒤에야 실제 DNS Query 결과로 보입니다.


벤치마크가 이끈 최적화

사실 위 숫자들은 처음부터 나온 것이 아닙니다. 벤치마크를 만든 진짜 이유는 비교표를 뽑는 것보다 측정 → 병목 발견 → 수정 → 재측정의 피드백 루프를 돌리는 것이었고, 실제로 몇 차례의 최적화를 거쳤습니다.

 

  1. DB가 하는 일 줄이기
    초기에는 records 테이블에 인덱스가 없어서 모든 write의 충돌 조회가 full table scan이었습니다. (zone_id, name) 인덱스를 추가하고, 인덱스를 타기 어렵게 만들던 조회들을 수정했습니다. 또한 레코드를 하나씩 insert하던 bulk 경로를 multi-row insert로 바꿔 DB round trip을 줄였습니다.
  2. 계측을 넣고 다시 측정
    파싱 → 검증 → 기존 레코드 조회 → insert 중 어디서 시간이 많이 쓰이는지 확인하기 위해 bulk 경로에 단계별 타이머를 넣고, 디버그 로그 한 줄로 각 단계의 소요 시간을 내보내게 했습니다. 이 계측을 통해 MySQL에서는 insert보다 기존 레코드를 조회하는 과정이 더 큰 병목이라는 것을 확인했습니다. 이후 IN 조회 청크 크기를 조정하면서 처리량을 개선했습니다.
  3. XFR 경로 최적화
    초기 AXFR 구현은 요청이 들어올 때마다 DB에서 zone 전체를 읽고 DNS 응답을 다시 만들었습니다. 같은 serial의 zone을 반복해서 전송하는 경우에는 불필요한 작업이 많았습니다. 그래서 zone serial을 기준으로 캐시를 추가했습니다. zone이 변경되면 serial이 증가하므로, 캐시된 serial이 현재 serial과 같다면 해당 응답은 그대로 재사용할 수 있습니다.
    IXFR 쪽에서는 serial별 변경 이력을 여러 번 조회하던 방식을 줄였습니다. 필요한 serial 범위를 한 번의 range query로 가져오도록 바꿔 DB 조회 횟수를 줄였습니다.

 

다음 글에서는 DNS를 통해 HTTPS 인증서를 갱신하는 DNS-01(DNS challenge) 방식과 이 과정에서 사용되는 nsupdate(RFC 2136) 프로토콜에 대한 이야기를 해보려 합니다. 긴 글 읽어 주셔서 감사합니다!!