2026年6月9日
2026年6月9日
Knot DNS のパフォーマンスチューニング|計測から workers・TCP・RRL・XDP の調整まで
Knot DNS は既定値のままでも多くの権威 DNS で十分な性能が出るため、チューニングは「計測して足りない箇所を特定し、そこだけ変える」順で進めるのが近道です。この記事では、負荷の測り方、ワーカー数と TCP 関連の設定、応答サイズ、RRL(応答レート制限)、ゾーン更新が多い環境のジャーナル設定、最後の手段としての XDP モードまでを、変更の理由と確認方法とともに説明します。
設定項目名は Knot DNS 3.x 系を前提にしています。公式マニュアルの最新は 3.6 系です(2026年10月現在)。既定値や項目はバージョンで変わることがあるため、knotd --version で手元の版を確かめ、同じ版のリファレンスと照らし合わせてください。
先に知っておくこと:既定値はかなり賢い
Knot DNS の処理スレッド数は、既定で CPU 数に合わせて自動で決まります。UDP の処理スレッド(udp-workers)はオンラインの CPU 数、TCP の処理スレッド(tcp-workers)は CPU 数で最低 10、ゾーンの読み込みや転送などの裏方を担うスレッド(background-workers)は CPU 数で最大 10 です。ゾーンはメモリ上に展開されて応答されるため、ディスクの速さが応答速度に効くことはほとんどありません。
そのため、「とりあえずスレッドを増やす」ような変更は効かないか、かえって遅くなることがあります。遅いと感じたら、まず次のどれが原因かを切り分けます。
- CPU が上限に張り付いている(スレッドや NIC の割り込みの偏りを含む)
- パケットが OS の受信バッファであふれて捨てられている
- TCP 接続が増えすぎて上限やタイムアウトに当たっている
- 大きな応答が切り詰められ、TCP での再問い合わせが増えている
- ゾーン更新や DNSSEC 署名が重く、裏方の処理が詰まっている
手順1:現状を計測する
最初に、本番に近い条件で「1秒あたりの応答数」と「応答時間」を測ります。測定は、対象サーバーとは別のマシンから行います。同じマシンで負荷をかけると、負荷ツール自身が CPU を使って結果がゆがむためです。DNS-OARC の dnsperf がよく使われます。
# queries.txt は1行に「名前 種類」。実際のログから作ると現実に近くなる
cat > queries.txt <<'EOF'
www.example.com A
example.com MX
example.com AAAA
nonexistent.example.com A
EOF
# 60秒間、20クライアントで、毎秒最大 20000 クエリを送る
dnsperf -s 192.0.2.53 -d queries.txt -l 60 -c 20 -Q 20000
結果の「Queries per second」「Average Latency」「Queries lost」を控え、-Q の値を段階的に上げて、取りこぼし(lost)が出始める地点を探します。存在しない名前(NXDOMAIN になる問い合わせ)も混ぜておくと、実際の負荷に近づきます。
測定中は、サーバー側で次の3点を見ます。
# スレッドごとの CPU 使用率(特定のスレッドだけ高くないか)
top -H -p "$(pgrep -x knotd)"
# UDP の受信バッファあふれ(数値が増え続けるなら取りこぼしている)
nstat -az | grep -i RcvbufErrors
# TCP の接続数の概況
ss -s
Knot 側の統計は、mod-stats モジュールを有効にすると knotc stats で読めます。
template:
- id: default
global-module: mod-stats
knotc stats mod-stats
プロトコル別(udp4・tcp4 など)の問い合わせ数や応答コード別の件数が出るので、「TCP の割合が急に増えた」「REFUSED が多い」といった偏りを確かめられます。数値は手順を変えるたびに取り直し、変更前後を比べます。
手順2:ワーカー数とソケットの設定
CPU に余力があるのに取りこぼしが出る場合や、特定のスレッドだけが忙しい場合は、ワーカー数とソケットの割り当てを見直します。
server:
listen: [ 0.0.0.0@53, ::@53 ]
udp-workers: 8 # 既定は CPU 数。他の常駐プロセスに CPU を残したいときは減らす
tcp-workers: 16 # TCP の比率が高い環境で増やす
background-workers: 4 # ゾーン数が多い・更新が多いときに見直す
tcp-reuseport: on # TCP ワーカーごとに SO_REUSEPORT ソケットを持たせる
socket-affinity: on # Linux でソケットとワーカーを結びつける
udp-workers を CPU 数より大きくしても、たいていは速くなりません。むしろ同じサーバーで監視エージェントやログ転送が動いているなら、少し減らして CPU を空けたほうが応答時間が安定することがあります。tcp-reuseport と socket-affinity は、接続の振り分けを OS に任せて偏りを減らす設定ですが、環境によって効果が変わるため、手順1の計測で差を確かめてから残すかどうかを決めます。
ここに挙げた項目は、変更後に knotd の再起動が要ります。knotc reload だけでは反映されない点でつまずきやすいので注意してください。
手順3:TCP 接続の上限とタイムアウト
DNS over TCP は、大きな応答、ゾーン転送、TC ビット付き応答後の再問い合わせなどで使われます。TCP の同時接続数の上限(tcp-max-clients)は、既定でプロセスのファイル記述子上限の半分です。つまり、systemd の LimitNOFILE が小さいと、上限も小さくなります。
server:
tcp-max-clients: 20000
tcp-idle-timeout: 10 # 秒。要求のない接続を閉じるまでの時間
tcp-io-timeout: 500 # ミリ秒。1メッセージの送受信にかけてよい時間
# systemctl edit knot で作るドロップイン
[Service]
LimitNOFILE=65536
tcp-idle-timeout を長くすると、遊んでいる接続がスロットを占有し続けます。攻撃や行儀の悪いクライアントがいる環境では、短めの既定値のままのほうが安全です。上限を上げたら、ss -s で接続数が想定どおりに推移しているかを確かめます。
手順4:応答サイズと EDNS
DNSSEC を使うゾーンでは応答が大きくなり、UDP で収まらない分は TC ビット付きで返され、クライアントが TCP で問い合わせ直します。udp-max-payload の既定値は 1232 バイトで、これは途中経路での IP フラグメントを避けるための値として広く使われているものです。性能のためにこの値を大きくすると、フラグメントの欠落による取りこぼしが増えることがあるため、通常は既定値のままにします。
TCP の比率が高い場合は、値を変えるより応答を小さくする方向で考えます。たとえば、DNSSEC の署名アルゴリズムを RSA から ECDSA(アルゴリズム 13)に変えると署名が短くなり、応答サイズが下がります。ただしアルゴリズムの切り替えは鍵のロールオーバーを伴う作業なので、計画を立てて別途行います。
手順5:RRL で反射攻撃への耐性を上げる
権威 DNS は、送信元を偽った問い合わせで第三者を攻撃する「反射攻撃」の踏み台にされることがあります。mod-rrl モジュールは、同じ送信元(またはネットワーク)への応答の頻度を制限し、こうした負荷を抑えます。正規の利用者への影響を抑えるため、最初は応答を変えずに数えるだけの dry-run で様子を見ます。
mod-rrl:
- id: default
rate-limit: 200 # 1つの送信元あたり毎秒の上限(既定は 50)
slip: 2 # 制限対象の応答のうち、およそ半分を TC 付きで返す
whitelist: [ 192.0.2.0/24 ] # 自社の監視サーバーなど
dry-run: on # 応答は変えず、カウンターだけを更新する
template:
- id: default
global-module: [ mod-rrl/default, mod-stats ]
slip は、制限した応答を捨てるか、TC 付きの短い応答で返すかの割合です。TC 付きで返すと、正規のリゾルバーは TCP で問い合わせ直せるため、巻き込まれても名前解決が止まりにくくなります。dry-run の期間に knotc stats で制限に当たった件数を確かめ、正規の大口リゾルバーまで制限していないことを見てから dry-run を外します。mod-rrl はバージョンアップの過程でオプションが追加・変更されており、古い版には dry-run が無い場合もあります。以前の設定を流用するときは、手元の版のモジュール説明で項目名と既定値を確認してください。
手順6:ゾーン更新が多い環境
動的更新や頻繁なゾーン転送、DNSSEC の自動署名があるとき、裏方の処理が詰まって反映が遅れることがあります。見直す候補は次のとおりです。
database:
journal-db-mode: robust # 既定。asynchronous にすると速いが障害時の耐久性が下がる
journal-db-max-size: 20G # 既定。上限に達すると全ゾーンで変更が拒否される
template:
- id: default
zonefile-sync: 60 # 秒。変更をまとめてゾーンファイルへ書き出す
zonefile-sync の既定は 0 で、変更のたびにゾーンファイルを書き出します。更新が多い大きなゾーンでは、この書き出しが重くなるため、数十秒程度にまとめると負荷が下がります。その代わり、障害時に未書き出しの変更はジャーナルから復元することになるので、ジャーナルが置かれたディスクの空きと journal-db-max-size の監視を合わせて行います。journal-db-mode を asynchronous にする変更は、クラッシュ時に直前の変更を失う可能性と引き換えです。プライマリでは慎重に判断してください。
ゾーン数が数千を超えるような環境では、起動時や一斉更新時に background-workers が足りなくなることがあります。knotc zone-status で refresh や更新の予定がたまっていないかを確かめ、必要に応じて増やします。
手順7:それでも足りないときの XDP モード
Linux では、カーネルの通常のネットワーク処理を通さずに NIC から直接パケットを受け取る XDP モードを使えます。大規模な問い合わせを捌くための機能ですが、運用上の制約が大きいので、ここまでの手順で足りない場合の選択肢と考えてください。公式マニュアルに挙げられている主な注意点は次のとおりです。
- XDP で処理される DNS の通信は、ファイアウォールや tcpdump などの一般的なツールから見えません。iptables や nftables で 53 番を絞っているつもりでも、その制限はかかりません
- 要求と応答が同じ物理 NIC を通る必要があり、動的更新は XDP 経由では扱えません
- 大きな MTU に制限があり、NIC やドライバーによってはゼロコピー動作が不安定なものがあります
- 読み込んだ BPF フィルターは自動では外れないため、やめるときは
ip link set dev <インターフェース名> xdp offを実行します
xdp:
listen: eth0
udp: on
tcp: off
ファイアウォールが効かないことを前提に、アクセス制御は Knot の ACL や上流のネットワーク機器で行う設計に変える必要があります。導入前にステージング環境で、監視やログ収集が今までどおり動くかを確かめてください。
変更の進め方
- 変更は1つずつ行い、そのたびに手順1と同じ条件で計測します。複数を同時に変えると、どれが効いたのか分からなくなります
- 設定を書き換えたら
knotc conf-checkで文法を確かめてから反映します - server セクションの多くの項目は再起動で反映されます。セカンダリが複数ある構成なら、1台ずつ再起動すれば利用者への影響を抑えられます
- 作業前に knot.conf とゾーンファイル、DNSSEC の鍵を含むデータディレクトリをバックアップしておきます
- 問い合わせログを mod-dnstap などで取得していると、それ自体が負荷になります。計測中は有無を揃えて比べます
まとめ
- Knot DNS の既定値は CPU 数に合わせて自動で決まるため、まず dnsperf と
knotc statsで計測し、ボトルネックを特定します。 - ワーカー数やソケットの設定は再起動で反映され、効果は環境で変わるので前後比較で判断します。
- TCP の上限はファイル記述子の上限に連動します。udp-max-payload は既定値のままが無難です。
- RRL は dry-run で影響を確かめてから有効にし、監視サーバーは除外リストに入れます。
- 更新が多い環境は zonefile-sync とジャーナルの上限を見直し、XDP はファイアウォールが効かない前提で最後に検討します。