Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ const server = dns2.createServer({
});
```

### Performance and Benchmarking

see [benchmark/README.md](benchmark/README.md)

### Relevant Specifications

+ [RFC-1034 - Domain Names - Concepts and Facilities](https://tools.ietf.org/html/rfc1034)
Expand Down
70 changes: 70 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# dns2

### Performance and Benchmarking

#### Running the benchmark

A UDP throughput benchmark is included. It starts an in-process echo server,
fires queries at it, and reports RPS and latency percentiles:

```bash
node benchmark/udp.js

# Tune via env vars
TOTAL=50000 CONCURRENCY=200 node benchmark/udp.js

# Target an external server
DNS=127.0.0.1:5353 node benchmark/udp.js
```

Example output on a modern laptop (single Node.js process, loopback UDP):

```
Sending 10000 queries at concurrency 100...

Results
-------
Total queries : 10000
Errors : 0
Wall time : 812 ms
Throughput : 12315 req/s

Latency (ms)
min : 0
mean : 7.92
p50 : 7
p90 : 13
p99 : 24
max : 61
```

#### Limiting concurrency with `maxConcurrent`

Use the `maxConcurrent` option on `createServer` to cap how many handler
invocations can be in flight simultaneously. Requests that arrive when the limit
is already reached receive an immediate `SERVFAIL` response rather than queuing
unboundedly.

```js
const server = dns2.createServer({
udp : true,
maxConcurrent: 500, // at most 500 handler calls in flight at once
handle(request, send) {
// async work here...
},
});
```

> **Note:** handlers must always call `send()` — the active-request counter
> decrements only when `send()` is invoked. A handler that never calls `send()`
> will permanently consume one concurrency slot.

#### Node.js tuning tips

- Run multiple worker processes with the built-in
[`cluster`](https://nodejs.org/api/cluster.html) module to use all CPU cores.
- Increase the V8 new-space size to reduce minor-GC frequency under high load:
`node --max-semi-space-size=64 server.js`
- For production, consider a process manager (PM2, systemd) that auto-restarts
on failure and enables multi-instance clustering.

125 changes: 125 additions & 0 deletions benchmark/udp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env node
/**
* UDP benchmark — measures dns2 server throughput and latency.
*
* Usage:
* node benchmark/udp.js
*
* Environment variables:
* TOTAL – number of queries to send (default: 10 000)
* CONCURRENCY – max in-flight queries (default: 100)
* DNS – host:port of an external server to target instead of the
* built-in echo server (e.g. DNS=127.0.0.1:5353)
*/
'use strict';

const { createServer, UDPClient, Packet } = require('..');

const TOTAL = parseInt(process.env.TOTAL || '10000', 10);
const CONCURRENCY = parseInt(process.env.CONCURRENCY || '100', 10);
const DNS_TARGET = process.env.DNS || null;

function percentile(sorted, p) {
return sorted[Math.max(0, Math.floor(sorted.length * p / 100) - 1 + (p === 100 ? 1 : 0))];
}

async function run() {
let server = null;
let port;
let host = '127.0.0.1';

if (DNS_TARGET) {
const parts = DNS_TARGET.split(':');
host = parts[0];
port = parseInt(parts[1], 10);
console.log(`Targeting external server ${host}:${port}`);
} else {
server = createServer({
udp: true,
handle(request, send) {
const response = Packet.createResponseFromRequest(request);
const [ q ] = request.questions;
response.answers.push({
name : q.name,
type : Packet.TYPE.A,
class : Packet.CLASS.IN,
ttl : 60,
address : '127.0.0.1',
});
send(response);
},
});
const addresses = await server.listen();
port = addresses.udp.port;
console.log(`In-process echo server on 127.0.0.1:${port}`);
}

const client = UDPClient({ dns: host, port });

console.log(`Sending ${TOTAL} queries at concurrency ${CONCURRENCY}...\n`);

const latencies = new Array(TOTAL);
let completed = 0;
let errors = 0;

const wallStart = Date.now();

await new Promise((resolve) => {
let sent = 0;
let inFlight = 0;

function next() {
while (inFlight < CONCURRENCY && sent < TOTAL) {
const idx = sent++;
inFlight++;
const t0 = Date.now();
client('benchmark.test', 'A')
.then(() => {
latencies[idx] = Date.now() - t0;
})
.catch(() => {
latencies[idx] = Date.now() - t0;
errors++;
})
.finally(() => {
inFlight--;
completed++;
if (completed === TOTAL) resolve();
else next();
});
}
}

next();
});

const wallMs = Date.now() - wallStart;
const rps = Math.round(TOTAL / (wallMs / 1000));

const sorted = latencies.slice().sort((a, b) => a - b);
const mean = (sorted.reduce((s, v) => s + v, 0) / sorted.length).toFixed(2);

console.log('Results');
console.log('-------');
console.log(` Total queries : ${TOTAL}`);
console.log(` Errors : ${errors}`);
console.log(` Wall time : ${wallMs} ms`);
console.log(` Throughput : ${rps} req/s`);
console.log('');
console.log(' Latency (ms)');
console.log(` min : ${sorted[0]}`);
console.log(` mean : ${mean}`);
console.log(` p50 : ${percentile(sorted, 50)}`);
console.log(` p90 : ${percentile(sorted, 90)}`);
console.log(` p99 : ${percentile(sorted, 99)}`);
console.log(` max : ${sorted[sorted.length - 1]}`);

if (server) {
await server.close();
}
}

run().catch(err => {
console.error(err);
process.exit(1);
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"example-client-udp": "node example/client/udp.js",
"example-client-tcp": "node example/client/tcp.js",
"example-client-google": "node example/client/google.js",
"example-client-udp-subnet": "node example/client/udp-subnet.js"
"example-client-udp-subnet": "node example/client/udp-subnet.js",
"benchmark": "node benchmark/udp.js"
},
"keywords": [
"dns"
Expand Down
19 changes: 18 additions & 1 deletion server/dns.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const EventEmitter = require('node:events');
const Packet = require('../packet');
const DOHServer = require('./doh');
const TCPServer = require('./tcp');
const UDPServer = require('./udp');
Expand Down Expand Up @@ -34,7 +35,23 @@ class DNSServer extends EventEmitter {
return addresses;
});

const emitRequest = (request, send, client) => this.emit('request', request, send, client);
const maxConcurrent = options.maxConcurrent > 0 ? options.maxConcurrent : 0;
let active = 0;

const emitRequest = (request, send, client) => {
if (maxConcurrent && active >= maxConcurrent) {
const response = Packet.createResponseFromRequest(request);
response.header.rcode = Packet.RCODE.SERVFAIL;
send(response);
return;
}
active++;
const wrappedSend = (...args) => {
active--;
return send(...args);
};
this.emit('request', request, wrappedSend, client);
};
const emitRequestError = (error) => this.emit('requestError', error);
for (const server of servers) {
server.on('request', emitRequest);
Expand Down
62 changes: 62 additions & 0 deletions test/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,3 +452,65 @@ test('server/all#handler can respond with RCODE error codes', async() => {

await server.close();
});

test('server/all#maxConcurrent - requests within limit are served normally', async() => {
const server = createServer({
udp : true,
maxConcurrent : 10,
handle(request, send) {
const response = Packet.createResponseFromRequest(request);
response.answers.push({
name : request.questions[0].name,
type : Packet.TYPE.A,
class : Packet.CLASS.IN,
ttl : 60,
address : '1.2.3.4',
});
send(response);
},
});
const { udp: { port } } = await server.listen();
const client = UDPClient({ dns: '127.0.0.1', port });

const reply = await client('within-limit.test');
assert.equal(reply.header.rcode, Packet.RCODE.NOERROR);
assert.equal(reply.answers[0].address, '1.2.3.4');

await server.close();
});

test('server/all#maxConcurrent - excess requests receive SERVFAIL', async() => {
// Use a handler that holds requests open until we release them, so we can
// saturate the concurrency limit predictably.
const pending = [];
const server = createServer({
udp : true,
maxConcurrent : 2,
handle(request, send) {
pending.push({ request, send });
},
});
const { udp: { port } } = await server.listen();
const client = UDPClient({ dns: '127.0.0.1', port });

// Fire q1 and q2 but don't await — they stay in the handler holding 2 slots.
const p1 = client('q1.test');
const p2 = client('q2.test');

// Wait until both are registered with the handler.
while (pending.length < 2) await new Promise(r => setTimeout(r, 5));

// q3 arrives when the limit is already full — should be shed immediately.
const r3 = await client('q3.test');
assert.equal(r3.header.rcode, Packet.RCODE.SERVFAIL, 'shed request gets SERVFAIL');

// Drain the two held requests so the server can close cleanly.
for (const { request, send } of pending) {
const response = Packet.createResponseFromRequest(request);
response.header.rcode = Packet.RCODE.NOERROR;
send(response);
}
await Promise.all([ p1, p2 ]);

await server.close();
});