test: benchmark of server, BIND and Kea with generated data
bench / Benchmark (server, BIND, Kea) (push) Canceled after 1m30s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sir_melonchen
2026-10-15 00:33:24 +02:00
co-authored by Claude Opus 5.5
parent 7c223161a1
commit 43bf44a8ca
13 changed files with 851 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
# SPDX-License-Identifier: Apache-2.0
#
# Benchmarks of server, BIND and Kea (test/bench, docs/SYSTEM-REQUIREMENTS.md).
# Started by hand; the results are printed as a Markdown table in the log
# (and in the job summary where the runner supports it).
name: bench
on:
workflow_dispatch:
push:
branches: [m6-bench]
concurrency:
group: bench-${{ github.ref }}
cancel-in-progress: true
jobs:
bench:
name: Benchmark (server, BIND, Kea)
runs-on: ubuntu-latest
timeout-minutes: 90
env:
# The Docker daemon may be shared: unique compose project.
COMPOSE_PROJECT_NAME: ddi-bench-${{ github.run_id }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false
- name: Tools
run: command -v jq >/dev/null || { apt-get update -qq && apt-get install -y -qq --no-install-recommends jq; }
- name: Build
run: make build-go
- name: Benchmark
run: make bench
+5
View File
@@ -150,6 +150,11 @@ test-integration:
go test -tags integration -count=1 -v -timeout 20m \
./server/internal/configgen/integration/ ./agent/role/integration/
## bench: measure server, BIND and Kea with generated data (needs docker and bin/ from build-go)
.PHONY: bench
bench:
test/bench/run.sh
## vuln: scan dependencies for known vulnerabilities
.PHONY: vuln
vuln:
+6
View File
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# BIND 9.20 for the benchmark. named is started by run.sh once the generated
# files are in place.
FROM internetsystemsconsortium/bind9:9.20
COPY --chmod=0644 bind/named.conf /etc/bind/named.conf
ENTRYPOINT ["tail", "-f", "/dev/null"]
+16
View File
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
//
// Distribution-like named.conf for the benchmark: the three include files
// written by the DNS agent (keys before zones), as set up by the installer.
options {
directory "/var/cache/bind";
pid-file "/run/named/named.pid";
listen-on { any; };
listen-on-v6 { none; };
dnssec-validation no;
include "/etc/bind/ddi-suite/ddi-suite-options.conf";
};
include "/etc/bind/ddi-suite/ddi-suite-keys.conf";
include "/etc/bind/ddi-suite/ddi-suite.conf";
+76
View File
@@ -0,0 +1,76 @@
# SPDX-License-Identifier: Apache-2.0
#
# Benchmark environment: management server with PostgreSQL 17, BIND 9.20
# with dnsperf, Kea 3.2.1 with perfdhcp. Driven by test/bench/run.sh through
# `docker compose exec` and `docker cp`, so it works with a remote or shared
# Docker daemon: no bind mounts, no published ports, no fixed subnet.
#
# TEST ONLY. Passwords and keys are throwaway values.
name: ddi-bench
services:
postgres:
image: postgres:17.11-alpine
environment:
POSTGRES_USER: ddi
POSTGRES_DB: ddi
POSTGRES_PASSWORD: bench-only
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ddi -d ddi"]
interval: 2s
timeout: 3s
retries: 30
server:
build:
context: .
dockerfile: server.Dockerfile
environment:
DDI_DATABASE_URL: postgres://ddi:bench-only@postgres:5432/ddi?sslmode=disable
DDI_HTTP_LISTEN: 127.0.0.1:8080
DDI_MASTER_KEY_FILE: /var/lib/ddi/master.key
DDI_STATE_DIR: /var/lib/ddi/state
DDI_AGENT_API_LISTEN: 0.0.0.0:8444
DDI_AGENT_API_PUBLIC_ADDRESS: server:8444
DDI_AGENT_API_SERVER_NAMES: server
DDI_LOG_LEVEL: warn
# Unreachable license server: the server runs in trial mode.
KEYGEN_URL: http://127.0.0.1:1
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/readyz | grep -q '\"database\":\"ok\"'"]
interval: 2s
timeout: 3s
retries: 60
bind:
build:
context: .
dockerfile: bind.Dockerfile
user: "0:0"
init: true
dnsperf:
build:
context: .
dockerfile: dnsperf.Dockerfile
init: true
kea:
build:
context: .
dockerfile: kea.Dockerfile
user: "0:0"
init: true
environment:
KEA_CONTROL_SOCKET_DIR: /run/kea
perfdhcp:
build:
context: .
dockerfile: perfdhcp.Dockerfile
user: "0:0"
init: true
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
#
# Generates the synthetic data set of the benchmark (test/bench/run.sh).
#
# Network k (0 <= k < 64) is 10.a.b.0/21 with a = k / 32, b = (k % 32) * 8,
# gateway 10.a.b.1 and a DHCP pool over the upper half (10.a.(b+4).1 to
# 10.a.(b+7).254). Each network holds 1,000 address entries in its lower
# half: 70 % static, 20 % manual_dhcp (with MAC), 10 % reserved. All host
# names live in the zone bench.test, so 1,000 entries yield 900 A and 900
# PTR records.
#
# Usage: datagen.sh <from-k> <to-k> <out-dir>
# writes <out-dir>/networks.txt "cidr gateway pool-start pool-end name"
# <out-dir>/addresses-NN.csv import files of at most 5,000 rows
# <out-dir>/queries.txt dnsperf input for networks 0 .. to-k-1
# (A and PTR of existing names, 10 % NXDOMAIN)
set -euo pipefail
from="$1" to="$2" out="$3"
mkdir -p "$out"
rm -f "$out"/addresses-*.csv
awk -v from="$from" -v to="$to" -v out="$out" '
function ip(k, o, a, b) {
a = int(k / 32); b = (k % 32) * 8
return sprintf("10.%d.%d.%d", a, b + int(o / 256), o % 256)
}
BEGIN {
nets = out "/networks.txt"
printf "" > nets
rows = 0; chunk = -1
for (k = from; k < to; k++) {
a = int(k / 32); b = (k % 32) * 8
printf "10.%d.%d.0/21 10.%d.%d.1 10.%d.%d.1 10.%d.%d.254 bench-%02d\n", \
a, b, a, b, a, b + 4, a, b + 7, k >> nets
for (i = 0; i < 1000; i++) {
if (rows % 5000 == 0) {
if (chunk >= 0) close(file)
chunk++
file = sprintf("%s/addresses-%02d.csv", out, chunk)
print "ip,type,mac,hostname,description,owner,tags" > file
}
rows++
m = i % 10
addr = ip(k, i + 2)
if (m <= 6) {
printf "%s,static,,h%d-%d,bench host,bench,bench\n", addr, k, i > file
} else if (m <= 8) {
printf "%s,manual_dhcp,02:bb:00:%02x:%02x:%02x,h%d-%d,bench reservation,bench,bench;dhcp\n", \
addr, k, int(i / 256), i % 256, k, i > file
} else {
printf "%s,reserved,,,bench reserved,bench,bench\n", addr > file
}
}
}
close(file)
}'
# Queries in pseudo-random order (fixed seed), over all networks so far.
awk -v to="$to" '
BEGIN {
srand(42)
for (k = 0; k < to; k++) {
a = int(k / 32); b = (k % 32) * 8
for (i = 0; i < 1000; i++) {
if (i % 10 == 9) continue
o = i + 2
printf "%.8f h%d-%d.bench.test A\n", rand(), k, i
printf "%.8f %d.%d.%d.10.in-addr.arpa PTR\n", rand(), o % 256, b + int(o / 256), a
}
}
n = to * 1800 / 9
for (j = 0; j < n; j++) printf "%.8f nx%d.bench.test A\n", rand(), j
}' | sort -k1,1 | cut -d' ' -f2- >"$out/queries.txt"
+7
View File
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# Load generator for BIND (dnsperf from Debian).
FROM debian:trixie-slim
RUN apt-get update -qq \
&& apt-get install -y -qq --no-install-recommends dnsperf \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["tail", "-f", "/dev/null"]
+5
View File
@@ -0,0 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Kea DHCPv4 3.2.1 for the benchmark (same image as test/e2e). kea-dhcp4 is
# started by run.sh with the generated configuration.
FROM docker.cloudsmith.io/isc/docker/kea-dhcp4:3.2.1
ENTRYPOINT ["tail", "-f", "/dev/null"]
+7
View File
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# Load generator for Kea: perfdhcp from the ISC package repository that the
# Kea image is built from.
FROM docker.cloudsmith.io/isc/docker/kea-dhcp4:3.2.1
USER root
RUN apk add --no-cache isc-kea-perfdhcp || { cat /etc/apk/repositories; apk search isc-kea; exit 1; }
ENTRYPOINT ["tail", "-f", "/dev/null"]
+421
View File
@@ -0,0 +1,421 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
#
# Benchmark of the management server, BIND and Kea with the configuration
# our generator produces (docs/SYSTEM-REQUIREMENTS.md). Two data sets: about
# 5,000 and about 50,000 address entries (datagen.sh). For each:
#
# server CSV import through the API, server and PostgreSQL memory, database
# size, revision creation, config preview, GET /addresses latency
# with filters, latency of single changes, audit and revision growth
# BIND the generated zones in BIND 9.20: load time, memory, dnsperf
# throughput and latency
# Kea the generated kea-dhcp4.conf in Kea 3.2.1: start time, memory,
# perfdhcp DORA rate against a /16 pool at increasing rates, CPU,
# lease file growth
#
# Prints a Markdown report (also appended to $GITHUB_STEP_SUMMARY if set).
# Needs Docker with compose and bin/ddi-server from `make build-go`.
#
# Usage: test/bench/run.sh (make bench)
# Environment: BIN_DIR, KEEP=1 keeps the containers, BENCH_OUT (report dir,
# default a temporary directory), BENCH_SCALES (default "5000 50000").
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
root="$(cd "$here/../.." && pwd)"
bin_dir="${BIN_DIR:-$root/bin}"
case "$bin_dir" in /*) ;; *) bin_dir="$root/$bin_dir" ;; esac
work="$(mktemp -d)"
out="${BENCH_OUT:-$work/out}"
mkdir -p "$out"
res="$out/results.txt"
: >"$res"
read -r -a scales <<<"${BENCH_SCALES:-5000 50000}"
rates=(500 1000 2000 4000 8000)
dc() { docker compose -f "$here/compose.yaml" "$@"; }
bench() { dc exec -T server /bench/bench.sh "$@"; }
cid() { dc ps -q "$1"; }
pg() { dc exec -T postgres psql -U ddi -d ddi -Atc "$1"; }
step() { printf '\n=== %s\n' "$*" >&2; }
fail() {
echo "$*" >&2
exit 1
}
now_ms() { echo $(($(date +%s%N) / 1000000)); }
# put KEY VALUE records a result; get KEY prints it ("–" if missing).
put() {
printf '%s %s\n' "$1" "$2" >>"$res"
printf ' %-40s %s\n' "$1" "$2" >&2
}
get() {
local v
v="$(awk -v k="$1" '$1 == k { $1 = ""; sub(/^ /, ""); v = $0 } END { print v }' "$res")"
printf '%s' "${v:-–}"
}
mib() { awk -v b="$1" 'BEGIN { printf "%.1f", b / 1048576 }'; }
kib_to_mib() { awk -v k="$1" 'BEGIN { printf "%.1f", k / 1024 }'; }
# rss CONTAINER PROCESS prints "VmRSS VmHWM" of the process in MiB.
rss() {
# shellcheck disable=SC2016 # expanded in the container
dc exec -T "$1" sh -c 'p=$(pidof '"$2"' | cut -d" " -f1); grep -E "^Vm(RSS|HWM):" /proc/$p/status' |
awk '/VmRSS/ { r = $2 } /VmHWM/ { h = $2 } END { printf "%.1f %.1f\n", r / 1024, h / 1024 }'
}
# ticks CONTAINER PROCESS prints the CPU time (user + system) in clock ticks.
ticks() {
# shellcheck disable=SC2016 # expanded in the container
dc exec -T "$1" sh -c 'p=$(pidof '"$2"' | cut -d" " -f1); cat /proc/$p/stat' |
awk '{ sub(/^.*\) /, ""); print $12 + $13 }'
}
# cmem CONTAINER prints the memory use of a container (docker stats).
cmem() { docker stats --no-stream --format '{{.MemUsage}}' "$(cid "$1")" | awk '{ print $1 }'; }
report() {
local s
{
echo "## Benchmark DDI-Suite"
echo
echo "Commit \`${GITHUB_SHA:-$(git -C "$root" rev-parse --short HEAD 2>/dev/null || echo unknown)}\`, $(date -u +%Y-%m-%dT%H:%MZ)."
echo
echo "### Hardware (Docker host of the runner)"
echo
echo "| | |"
echo "| --- | --- |"
echo "| CPU | $(get hw.cpu) |"
echo "| Cores (nproc) | $(get hw.ncpu) |"
echo "| RAM | $(get hw.mem) |"
echo "| Kernel / Docker | $(get hw.kernel) / $(get hw.docker) |"
echo
echo "### Management server (PostgreSQL 17.11, defaults of the official image)"
echo
printf '| Measurement |'
for s in "${scales[@]}"; do printf ' %s entries |' "$s"; done
printf '\n| --- |'
for _ in "${scales[@]}"; do printf ' ---: |'; done
printf '\n'
row() {
local label="$1" key="$2"
printf '| %s |' "$label"
for s in "${scales[@]}"; do printf ' %s |' "$(get "s$s.$key")"; done
printf '\n'
}
row "Networks / address entries" nets_entries
row "CSV import through the API (s, total)" import_s
row "CSV import rate (entries/s)" import_rate
row "Server RSS after import / peak (MiB)" server_rss
row "PostgreSQL container memory" pg_mem
row "Database size after import (MiB)" db_mib
row "Table addresses incl. indexes (MiB)" addresses_mib
row "Audit log: entries / size (MiB)" audit
row "POST /revisions, first revision (s)" rev1_s
row "Storage per revision (MiB)" rev1_mib
row "Generated files: DNS files / total size (MiB)" files
row "GET /config/preview (s)" preview_s
row "GET /config/pending (s)" pending_s
row "GET /addresses?limit=100 (median / p95 ms)" lat_all
row "… &network_id=<one network> (ms)" lat_net
row "… &type=manual_dhcp (ms)" lat_type
row "… &q=<host name prefix> (ms)" lat_host
row "… &q=<MAC prefix> (ms)" lat_mac
row "… sort=hostname desc, last page (ms)" lat_deep
row "PUT /addresses/{id}, mean of 50 (ms)" upd_ms
row "Audit growth per change (bytes)" audit_per_change
row "POST /revisions after 50 changes (s)" rev2_s
row "Storage of that revision (MiB)" rev2_mib
row "Server RSS at the end / peak (MiB)" server_rss_end
echo
echo "### BIND 9.20 (files from GET /config/preview)"
echo
printf '| Measurement |'
for s in "${scales[@]}"; do printf ' %s entries |' "$s"; done
printf '\n| --- |'
for _ in "${scales[@]}"; do printf ' ---: |'; done
printf '\n'
row "Zones / A+PTR records" bind_zones
row "Zone files (MiB)" bind_files_mib
row "Start until all zones loaded (s)" bind_load_s
row "named RSS after loading (MiB)" bind_rss
row "dnsperf max: queries/s" bind_qps
row "dnsperf max: mean latency (ms) / lost" bind_lat
row "named CPU during max run (% of one core)" bind_cpu
row "dnsperf at 5,000 queries/s: mean latency (ms)" bind_lat5k
row "named RSS after the runs (MiB)" bind_rss_end
echo
echo "### Kea 3.2.1 (kea-dhcp4.conf from GET /config/preview, /16 pool 10.200.0.0/16)"
echo
printf '| Measurement |'
for s in "${scales[@]}"; do printf ' %s entries |' "$s"; done
printf '\n| --- |'
for _ in "${scales[@]}"; do printf ' ---: |'; done
printf '\n'
row "Subnets / reservations" kea_size
row "kea-dhcp4 -t (s)" kea_test_s
row "Start until DHCP4_STARTED (s)" kea_start_s
row "kea-dhcp4 RSS after start (MiB)" kea_rss
for r in "${rates[@]}"; do
row "perfdhcp $r/s: DORA/s, drops D-O / R-A" "kea_$r"
row "perfdhcp $r/s: mean delay R-A (ms), CPU %, RSS (MiB), lease file (MiB)" "kea_${r}_res"
done
echo
echo "Method: test/bench/run.sh. Each perfdhcp run lasts 10 s with 50,000 simulated clients (later runs renew"
echo "existing leases); dnsperf runs 15 s with 16 clients and up to 500 outstanding queries (A and PTR of existing"
echo "names, 10 % NXDOMAIN). Client and server share the Docker host; the daemon may be shared with other jobs."
} >"$out/report.md"
cat "$out/report.md"
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
cat "$out/report.md" >>"$GITHUB_STEP_SUMMARY" || true
fi
}
cleanup() {
local rc=$?
if [[ $rc -ne 0 ]]; then
echo "FAILED (exit $rc); logs follow" >&2
dc logs --no-color --tail 100 >&2 || true
dc exec -T bind sh -c 'tail -n 50 /tmp/named.log 2>/dev/null' >&2 || true
dc exec -T kea sh -c 'tail -n 50 /tmp/kea.log 2>/dev/null' >&2 || true
fi
report || true
if [[ "${KEEP:-}" != 1 ]]; then
dc down -v --remove-orphans >/dev/null 2>&1 || true
fi
rm -rf "${here:?}/bin" "$work/s"* "$work/cfg"*
}
trap cleanup EXIT
step "Build images"
mkdir -p "$here/bin"
cp "$bin_dir/ddi-server" "$here/bin/ddi-server"
for i in 1 2 3; do
dc build --pull && break
[[ $i == 3 ]] && fail "image build failed"
sleep 15
done
dc up -d --wait
step "Hardware"
put hw.ncpu "$(docker info --format '{{.NCPU}}')"
put hw.mem "$(docker info --format '{{.MemTotal}}' | awk '{ printf "%.1f GiB", $1 / 1073741824 }')"
put hw.kernel "$(docker info --format '{{.KernelVersion}}')"
put hw.docker "$(docker info --format '{{.ServerVersion}}')"
put hw.cpu "$(dc exec -T server sh -c "grep -m1 'model name' /proc/cpuinfo | cut -d: -f2" | sed 's/^ *//')"
step "Set up the server"
password="$(dc exec -T server ddi-server bootstrap-admin | sed -n 's/^password: //p')"
[[ -n "$password" ]] || fail "bootstrap-admin printed no password"
bench setup "$password"
bind_ip="$(dc exec -T bind hostname -i | awk '{ print $1 }')"
kea_ip="$(dc exec -T kea hostname -i | awk '{ print $1 }')"
relay_ip="$(dc exec -T perfdhcp hostname -i | awk '{ print $1 }')"
echo "bind $bind_ip, kea $kea_ip, perfdhcp (relay) $relay_ip" >&2
# ---------------------------------------------------------------- server
server_bench() {
local s="$1" from="$2" to="$3" data="$work/s$1" t=0 n=0 f r sec created
step "Server: data set $s"
"$here/datagen.sh" "$from" "$to" "$data"
dc exec -T server mkdir -p "/bench/data-$s"
docker cp "$data/." "$(cid server):/bench/data-$s/"
bench networks "/bench/data-$s/networks.txt" >/dev/null
for f in "$data"/addresses-*.csv; do
_o="$(bench import "/bench/data-$s/$(basename "$f")")"
read -r sec created <<<"$_o"
echo " import $(basename "$f"): $created in $sec s" >&2
t="$(awk -v a="$t" -v b="$sec" 'BEGIN { print a + b }')"
n=$((n + created))
done
local entries
entries="$(pg "select count(*) from addresses")"
put "s$s.nets_entries" "$(pg "select count(*) from networks") / $entries"
put "s$s.import_s" "$(awk -v t="$t" 'BEGIN { printf "%.1f", t }') ($n new)"
put "s$s.import_rate" "$(awk -v t="$t" -v n="$n" 'BEGIN { printf "%.0f", n / t }')"
_o="$(rss server ddi-server)"
read -r r h <<<"$_o"
put "s$s.server_rss" "$r / $h"
put "s$s.pg_mem" "$(cmem postgres)"
put "s$s.db_mib" "$(mib "$(pg "select pg_database_size('ddi')")")"
put "s$s.addresses_mib" "$(mib "$(pg "select pg_total_relation_size('addresses')")")"
put "s$s.audit" "$(pg "select count(*) from audit_log") / $(mib "$(pg "select pg_total_relation_size('audit_log')")")"
local revsize="select pg_total_relation_size('config_revisions') + pg_total_relation_size('config_revision_files')"
local before after num bytes
before="$(pg "$revsize")"
_o="$(bench revision "bench $s")"
read -r num sec bytes <<<"$_o"
after="$(pg "$revsize")"
put "s$s.rev1_s" "$sec"
put "s$s.rev1_mib" "$(mib $((after - before)))"
echo " revision $num, response $bytes bytes" >&2
local files
_o="$(bench preview "/tmp/cfg-$s" "$relay_ip")"
read -r sec bytes files <<<"$_o"
put "s$s.preview_s" "$sec"
docker cp "$(cid server):/tmp/cfg-$s" "$work/cfg-$s"
put "s$s.files" "$files / $(mib "$(du -sb "$work/cfg-$s" | awk '{ print $1 }')")"
_o="$(bench timed /config/pending)"
read -r sec bytes <<<"$_o"
put "s$s.pending_s" "$sec"
local net med p95 total
net="$(bench network-id bench-03)"
lat() {
_o="$(bench latency 20 "$2")"
read -r med p95 total <<<"$_o"
put "s$s.$1" "$med / $p95"
echo " ($total matches)" >&2
}
lat lat_all "limit=100"
lat lat_net "limit=100&network_id=$net"
lat lat_type "limit=100&type=manual_dhcp"
lat lat_host "limit=100&q=h3-12"
lat lat_mac "limit=100&q=02:bb:00:03:00"
lat lat_deep "limit=100&sort=hostname&order=desc&offset=$((entries - 100))"
local abefore aafter ms cnt
abefore="$(pg "select pg_total_relation_size('audit_log')")"
_o="$(bench updates 50)"
read -r ms cnt <<<"$_o"
aafter="$(pg "select pg_total_relation_size('audit_log')")"
put "s$s.upd_ms" "$ms"
put "s$s.audit_per_change" "$(((aafter - abefore) / cnt)) (row $(pg "select avg(pg_column_size(a.*))::int from audit_log a where action = 'address.update'"))"
before="$(pg "$revsize")"
_o="$(bench revision "bench $s, 50 changes")"
read -r num sec bytes <<<"$_o"
after="$(pg "$revsize")"
put "s$s.rev2_s" "$sec"
put "s$s.rev2_mib" "$(mib $((after - before)))"
_o="$(rss server ddi-server)"
read -r r h <<<"$_o"
put "s$s.server_rss_end" "$r / $h"
}
# ---------------------------------------------------------------- BIND
bind_bench() {
local s="$1" cfg="$work/cfg-$1"
step "BIND: data set $s"
dc exec -T bind sh -c 'pkill -x named 2>/dev/null; sleep 1
rm -rf /etc/bind/ddi-suite /var/lib/bind/ddi-suite /tmp/named.log
mkdir -p /etc/bind/ddi-suite /var/lib/bind/ddi-suite /run/named /var/cache/bind'
docker cp "$cfg/dns/." "$(cid bind):/etc/bind/ddi-suite/"
dc exec -T bind sh -c 'cd /etc/bind/ddi-suite && mv zones/* /var/lib/bind/ddi-suite/ && rmdir zones
chown -R bind:bind /var/lib/bind/ddi-suite /run/named /var/cache/bind'
local zones records
zones="$(dc exec -T bind sh -c 'ls /var/lib/bind/ddi-suite | wc -l')"
records="$(dc exec -T bind sh -c 'cat /var/lib/bind/ddi-suite/* | grep -cE "[[:space:]](A|PTR)[[:space:]]"')"
put "s$s.bind_zones" "$zones / $records"
put "s$s.bind_files_mib" "$(mib "$(du -sb "$cfg/dns/zones" | awk '{ print $1 }')")"
dc exec -T -d bind sh -c 'exec named -u bind -g -c /etc/bind/named.conf >/tmp/named.log 2>&1'
local i
for i in $(seq 600); do
dc exec -T bind grep -q 'running$' /tmp/named.log 2>/dev/null && break
if dc exec -T bind grep -qE 'exiting \(due to fatal error\)|loading configuration: failure' /tmp/named.log 2>/dev/null; then
fail "named failed to start"
fi
[[ $i == 600 ]] && fail "named did not start within 5 minutes"
sleep 0.5
done
# shellcheck disable=SC2016 # awk program
put "s$s.bind_load_s" "$(dc exec -T bind awk '
function t(x, a) { split(x, a, ":"); return a[1] * 3600 + a[2] * 60 + a[3] }
/starting BIND/ && !s { s = t($2) }
/all zones loaded/ { e = t($2) }
END { printf "%.2f", e - s }' /tmp/named.log)"
_o="$(rss bind named)"
read -r r _ <<<"$_o"
put "s$s.bind_rss" "$r"
docker cp "$work/s$s/queries.txt" "$(cid dnsperf):/queries.txt"
local t0 t1 c0 c1 o
c0="$(ticks bind named)"
t0="$(now_ms)"
o="$(dc exec -T dnsperf dnsperf -s "$bind_ip" -d /queries.txt -l 15 -c 16 -q 500 2>&1)"
t1="$(now_ms)"
c1="$(ticks bind named)"
echo "$o" >&2
put "s$s.bind_qps" "$(awk '/Queries per second:/ { printf "%.0f", $4 }' <<<"$o")"
put "s$s.bind_lat" "$(awk '/Average Latency/ { a = $4 * 1000 } /Queries lost:/ { l = $4 } END { printf "%.2f / %s", a, l }' <<<"$o")"
put "s$s.bind_cpu" "$(awk -v c=$((c1 - c0)) -v ms=$((t1 - t0)) 'BEGIN { printf "%.0f", c / (ms / 1000) }')"
o="$(dc exec -T dnsperf dnsperf -s "$bind_ip" -d /queries.txt -l 15 -c 16 -Q 5000 2>&1)"
echo "$o" >&2
put "s$s.bind_lat5k" "$(awk '/Average Latency/ { printf "%.2f", $4 * 1000 }' <<<"$o")"
_o="$(rss bind named)"
read -r r _ <<<"$_o"
put "s$s.bind_rss_end" "$r"
}
# ---------------------------------------------------------------- Kea
kea_bench() {
local s="$1" cfg="$work/cfg-$1"
step "Kea: data set $s"
dc exec -T kea sh -c 'pkill -x kea-dhcp4 2>/dev/null; sleep 1
rm -f /var/lib/kea/kea-leases4.csv* /tmp/kea.log
mkdir -p /run/kea /var/lib/kea && chmod 750 /run/kea'
docker cp "$cfg/dhcp/kea-dhcp4.conf" "$(cid kea):/etc/kea/kea-dhcp4.conf"
put "s$s.kea_size" "$(jq '.Dhcp4.subnet4 | length' "$cfg/dhcp/kea-dhcp4.conf") / $(jq '[.Dhcp4.subnet4[].reservations // [] | length] | add' "$cfg/dhcp/kea-dhcp4.conf")"
local t0 t1 base
t0="$(now_ms)"
dc exec -T kea true
t1="$(now_ms)"
base=$((t1 - t0))
t0="$(now_ms)"
dc exec -T kea kea-dhcp4 -t /etc/kea/kea-dhcp4.conf >/dev/null
t1="$(now_ms)"
put "s$s.kea_test_s" "$(awk -v ms=$((t1 - t0 - base)) 'BEGIN { printf "%.2f", ms / 1000 }')"
dc exec -T -d kea sh -c 'exec kea-dhcp4 -c /etc/kea/kea-dhcp4.conf >/tmp/kea.log 2>&1'
local i
for i in $(seq 600); do
dc exec -T kea grep -q DHCP4_STARTED /tmp/kea.log 2>/dev/null && break
dc exec -T kea pidof kea-dhcp4 >/dev/null || fail "kea-dhcp4 exited"
[[ $i == 600 ]] && fail "Kea did not start within 5 minutes"
sleep 0.5
done
# shellcheck disable=SC2016 # awk program
put "s$s.kea_start_s" "$(dc exec -T kea awk '
function t(x, a) { split(x, a, ":"); return a[1] * 3600 + a[2] * 60 + a[3] }
!s && $2 ~ /^[0-9][0-9]:/ { s = t($2) }
/DHCP4_STARTED/ { e = t($2) }
END { printf "%.2f", e - s }' /tmp/kea.log)"
_o="$(rss kea kea-dhcp4)"
read -r r _ <<<"$_o"
put "s$s.kea_rss" "$r"
local rate o c0 c1 lf
for rate in "${rates[@]}"; do
c0="$(ticks kea kea-dhcp4)"
t0="$(now_ms)"
o="$(dc exec -T perfdhcp timeout 60 perfdhcp -4 -r "$rate" -p 10 -R 50000 "$kea_ip" 2>&1 || true)"
t1="$(now_ms)"
c1="$(ticks kea kea-dhcp4)"
echo "$o" | tail -n 45 >&2
lf="$(dc exec -T kea stat -c %s /var/lib/kea/kea-leases4.csv 2>/dev/null || echo 0)"
_o="$(rss kea kea-dhcp4)"
read -r r _ <<<"$_o"
put "s$s.kea_$rate" "$(awk '
/^Rate:/ { rate = $2 }
/Statistics for: DISCOVER-OFFER/ { sec = "do" }
/Statistics for: REQUEST-ACK/ { sec = "ra" }
/^drops ratio:/ { d[sec] = $3 }
END { printf "%.0f / %s %% / %s %%", rate, d["do"], d["ra"] }' <<<"$o")"
put "s$s.kea_${rate}_res" "$(awk '
/Statistics for: REQUEST-ACK/ { sec = "ra" }
/^avg delay:/ && sec == "ra" { printf "%s", $3; exit }' <<<"$o") / $(awk -v c=$((c1 - c0)) -v ms=$((t1 - t0)) 'BEGIN { printf "%.0f", c / (ms / 1000) }') / $r / $(mib "$lf")"
done
}
prev=0
for s in "${scales[@]}"; do
server_bench "$s" "$prev" $((s / 1000))
prev=$((s / 1000))
bind_bench "$s"
kea_bench "$s"
done
step "Done"
+9
View File
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# Management server for the benchmark. bin/ is filled by run.sh with the
# binaries built by `make build-go` (static, CGO disabled).
FROM alpine:3.22
RUN apk add --no-cache curl jq
COPY --chmod=0755 bin/ddi-server /usr/local/bin/ddi-server
COPY --chmod=0755 server/entrypoint.sh /bench/entrypoint.sh
COPY --chmod=0755 server/bench.sh /bench/bench.sh
ENTRYPOINT ["/bench/entrypoint.sh"]
+178
View File
@@ -0,0 +1,178 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
#
# REST API steps of the benchmark, run inside the server container (curl and
# jq are available there; the API listens on 127.0.0.1 only). Results are
# printed as "key value" lines on stdout; diagnostics go to stderr.
#
# Usage: bench.sh <step> [args]
# setup <bootstrap-password> change the password, generator settings,
# the /16 DHCP test network
# networks <file> create the networks of datagen.sh
# import <csv> import one CSV file: "<seconds> <created>"
# latency <reps> <query> GET /addresses?<query> <reps> times:
# "<median-ms> <p95-ms> <total>"
# network-id <name> print the ID of a network
# updates <n> change the description of n addresses one by
# one: "<avg-ms>"
# revision <comment> POST /revisions: "<number> <seconds> <bytes>"
# timed <path> GET <path>: "<seconds> <bytes>"
# preview <out-dir> <relay-ip> GET /config/preview, write the files to
# <out-dir>/dns and <out-dir>/dhcp (the Kea
# test subnet gets <relay-ip> as relay):
# "<seconds> <bytes> <dns-files>"
# Results of timed_req are split into positional parameters on purpose.
# shellcheck disable=SC2086
set -eu
base=http://127.0.0.1:8080/api/v1
jar=/tmp/bench-cookies
csrf_file=/tmp/bench-csrf
password=bench-admin-password-1
# req METHOD PATH [JSON] prints the response body; fails on HTTP >= 400.
req() {
csrf=$(cat "$csrf_file" 2>/dev/null || true)
out=$(curl -sS -b "$jar" -c "$jar" -X "$1" -H 'Content-Type: application/json' \
-H "X-CSRF-Token: $csrf" ${3:+--data "$3"} -w '\n%{http_code}' "$base$2")
code=$(printf '%s\n' "$out" | tail -n1)
body=$(printf '%s\n' "$out" | sed '$d')
case "$code" in
2*) printf '%s\n' "$body" ;;
*)
echo "$1 $2 -> HTTP $code: $body" >&2
return 1
;;
esac
}
# timed_req METHOD PATH OUTFILE [curl options] writes the body to OUTFILE and
# prints "<seconds> <bytes>"; fails on HTTP >= 400.
timed_req() {
m=$1 p=$2 f=$3
shift 3
csrf=$(cat "$csrf_file" 2>/dev/null || true)
res=$(curl -sS -b "$jar" -c "$jar" -X "$m" -H "X-CSRF-Token: $csrf" -o "$f" \
-w '%{http_code} %{time_total} %{size_download}' "$@" "$base$p")
set -- $res
case "$1" in
2*) echo "$2 $3" ;;
*)
echo "$m $p -> HTTP $1: $(head -c 2000 "$f")" >&2
return 1
;;
esac
}
login() {
req POST /auth/login "{\"username\":\"admin\",\"password\":\"$1\"}" | jq -r .csrf_token >"$csrf_file"
}
session() {
if [ -s "$csrf_file" ] && req GET /auth/session >/dev/null 2>&1; then
return 0
fi
login "$password"
}
case "$1" in
setup)
login "$2"
req POST /auth/password "{\"current_password\":\"$2\",\"new_password\":\"$password\"}" >/dev/null
session
req GET /settings/generator | jq '
.dns.primary_ns = "ns1.bench.test"
| .dns.hostmaster = "hostmaster@bench.test"
| .dns.name_servers = [{"name": "ns1.bench.test", "addresses": ["10.0.0.53"]}]' >/tmp/settings.json
req PUT /settings/generator "$(cat /tmp/settings.json)" >/dev/null
req POST /networks '{"cidr":"10.200.0.0/16","name":"bench-dhcp","gateway":"10.200.0.1","domain":"bench.test",
"dhcp_enabled":true,"pools":[{"start":"10.200.0.10","end":"10.200.255.250"}],"dns_servers":["10.0.0.53"]}' >/dev/null
;;
networks)
session
n=0
while read -r cidr gw start end name; do
req POST /networks "{\"cidr\":\"$cidr\",\"name\":\"$name\",\"gateway\":\"$gw\",\"domain\":\"bench.test\",
\"dhcp_enabled\":true,\"pools\":[{\"start\":\"$start\",\"end\":\"$end\"}],\"dns_servers\":[\"10.0.0.53\"]}" >/dev/null
n=$((n + 1))
done <"$2"
echo "$n"
;;
import)
session
r=$(timed_req POST /addresses/import /tmp/import.json -H 'Content-Type: text/csv' --data-binary "@$2")
set -- $r
created=$(jq -r '.created' /tmp/import.json)
problems=$(jq -r '.problems | length' /tmp/import.json)
if [ "$problems" != 0 ]; then
jq -c '.problems[:5]' /tmp/import.json >&2
exit 1
fi
echo "$1 $created"
;;
latency)
session
reps=$2 query=$3
: >/tmp/lat.txt
i=0
while [ "$i" -lt "$reps" ]; do
r=$(timed_req GET "/addresses?$query" /tmp/lat.json)
echo "${r%% *}" >>/tmp/lat.txt
i=$((i + 1))
done
total=$(jq -r .total /tmp/lat.json)
sort -n /tmp/lat.txt | awk -v total="$total" '{ v[NR] = $1 } END {
p95 = int(NR * 0.95 + 0.999); if (p95 > NR) p95 = NR
med = (NR % 2) ? v[(NR + 1) / 2] : (v[NR / 2] + v[NR / 2 + 1]) / 2
printf "%.1f %.1f %d\n", med * 1000, v[p95] * 1000, total }'
;;
network-id)
session
req GET /networks | jq -r --arg n "$2" '.items[] | select(.name == $n) | .id'
;;
updates)
session
req GET "/addresses?type=static&limit=$2&sort=ip" >/tmp/upd.json
: >/tmp/upd.txt
jq -c '.items[] | {id, ip, type, hostname, description, owner, tags, version}' /tmp/upd.json |
while read -r a; do
id=$(printf '%s' "$a" | jq -r .id)
body=$(printf '%s' "$a" | jq -c '{ip, type, hostname, owner, tags, version, description: (.description + " (changed)")}')
r=$(timed_req PUT "/addresses/$id" /tmp/upd-one.json -H 'Content-Type: application/json' --data "$body")
echo "${r%% *}" >>/tmp/upd.txt
done
awk '{ s += $1 } END { printf "%.1f %d\n", s / NR * 1000, NR }' /tmp/upd.txt
;;
revision)
session
r=$(timed_req POST /revisions /tmp/rev.json -H 'Content-Type: application/json' --data "{\"comment\":\"$2\"}")
set -- $r
echo "$(jq -r .number /tmp/rev.json) $1 $2"
;;
timed)
session
timed_req GET "$2" /tmp/timed.out
;;
preview)
session
out=$2
r=$(timed_req GET /config/preview /tmp/preview.json)
set -- "$out" "$3" $r
rm -rf "$out"
mkdir -p "$out/dns" "$out/dhcp"
jq -r '.dns[] | "\(.path)\t\(.content | @base64)"' /tmp/preview.json |
while IFS="$(printf '\t')" read -r path b64; do
mkdir -p "$out/dns/$(dirname "$path")"
printf '%s' "$b64" | base64 -d >"$out/dns/$path"
done
jq -r '.dhcp[] | select(.path == "kea-dhcp4.conf") | .content' /tmp/preview.json |
jq --arg ip "$2" '(.Dhcp4.subnet4[] | select(.subnet == "10.200.0.0/16")) |= . + {relay: {"ip-addresses": [$ip]}}' \
>"$out/dhcp/kea-dhcp4.conf"
jq -c '{warnings: (.warnings | length), dns_error, dhcp_error}' /tmp/preview.json >&2
echo "$3 $4 $(jq '.dns | length' /tmp/preview.json)"
;;
*)
echo "unknown step $1" >&2
exit 2
;;
esac
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Creates the master key on first start and runs the server.
set -eu
mkdir -p "$(dirname "$DDI_MASTER_KEY_FILE")" "$DDI_STATE_DIR"
if [ ! -f "$DDI_MASTER_KEY_FILE" ]; then
ddi-server gen-master-key >/dev/null
fi
exec ddi-server serve