DNS Seed Provider
DnsSeedProvider löst Seeds beim Start via DNS auf. Zwei Modi:
| Modus | Lookup | Ergebnis |
|---|---|---|
| SRV-Records | _actor-ts._tcp.example.com | Service + Port aus Records |
| A-Records | actor-ts.example.com | Nur IPs; Port kommt aus Config |
SRV ist die flexiblere Wahl (Service-Discovery-Style); A-Records funktionieren für einfachere Setups.
import { Cluster, ClusterOptions, DnsSeedProvider, DnsSeedProviderOptions } from 'actor-ts';
const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('_actor-ts._tcp.example.com') .withSystemName('my-app') .withUseSrv();const provider = new DnsSeedProvider( dnsSeedProviderOptions,);
const seeds = await provider.lookup();const clusterOptions = ClusterOptions.create() .withHost(host) .withPort(port) .withSeeds(seeds);await Cluster.join(system, clusterOptions);Konfiguration
Abschnitt betitelt „Konfiguration“type DnsSeedProviderOptionsType = { hostname: string; // SRV-Record-Name (oder Hostname für A-Record) systemName: string; // wird auf entdeckte Node-Adressen gestempelt port: number; // wird im A-Record-Modus mit jeder IP gepaart useSrv?: boolean; // SRV-Records (die einen Port tragen) A vorziehen cacheTtlMs?: number; // In-Process-TTL-Cache; Default 60_000 ms, 0 deaktiviert pinnedAddresses?: readonly string[]; // nur diese akzeptieren; unbesetzt = alle log?: (message: string, error?: unknown) => void; // meldet ausgefilterte Adressen // DNS-Funktionen überschreiben (Default: node:dns/promises) — v. a. für Tests: resolve?: (hostname: string) => Promise<string[]>; resolveSrv?: (hostname: string) => Promise<Array<{ name: string; port: number }>>;};SRV-Modus (empfohlen)
Abschnitt betitelt „SRV-Modus (empfohlen)“const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('_actor-ts._tcp.my-app.example.com') .withSystemName('my-app') .withUseSrv();new DnsSeedProvider( dnsSeedProviderOptions,);SRV-Records tragen pro Eintrag Host, Port, Weight, Priority.
Der Provider gibt für jeden Eintrag, den die DNS-Abfrage
zurückgegeben hat, host:port zurück.
Um die SRV-Records anzulegen:
_actor-ts._tcp.my-app.example.com. IN SRV 10 100 2552 node-1.my-app.example.com._actor-ts._tcp.my-app.example.com. IN SRV 10 100 2552 node-2.my-app.example.com._actor-ts._tcp.my-app.example.com. IN SRV 10 100 2552 node-3.my-app.example.com.Plus passende A-Records für die Hostnames. Die meisten DNS-SD-Service-Registrierungstools (Consul, Eureka mit DNS-Plugin) publizieren SRV-Records automatisch.
A-Record-Modus
Abschnitt betitelt „A-Record-Modus“const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('actor-ts.example.com') .withSystemName('my-app') .withPort(2552);new DnsSeedProvider( dnsSeedProviderOptions,);Für Setups ohne SRV — DNS trägt nur IPs. Der Provider fragt A-Records ab und paart jede mit dem konfigurierten Port.
A-Records zeigen typischerweise auf einen Loadbalancer oder eine Round-Robin-DNS-Liste; mehrere A-Einträge bedeuten mehrere Seed-Kandidaten.
Aufgelöste Adressen pinnen
Abschnitt betitelt „Aufgelöste Adressen pinnen“Was DNS zurückgibt, ist die Antwort einer Gegenstelle, die der
Node nie authentifiziert hat. pinnedAddresses begrenzt, was
diese Antwort sagen darf: Adressen außerhalb der Liste werden
verworfen, bevor sie dem Cluster als Seeds angeboten werden.
const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('actor-ts.example.com') .withSystemName('my-app') .withPort(2552) .withPinnedAddresses(['10.0.0.0/8']) .withLog((message) => logger.warn(message));Die Form der Einträge hängt vom Modus ab, weil die beiden Modi zu Unterschiedlichem auflösen:
| Modus | Löst auf zu | Pinnen mit |
|---|---|---|
| A-Records | IP-Adressen | CIDRs — '10.0.0.0/8', '2001:db8::/32' |
| SRV-Records | Ziel-Hostnames | Host-Suffixe — 'svc.cluster.local' |
Ein Suffix greift auf einer Label-Grenze, also lässt
svc.cluster.local pod-1.svc.cluster.local und den Apex selbst
zu, aber nicht evilsvc.cluster.local. Ein CIDR wird nie für
einen Hostname herangezogen und ein Suffix nie für eine IP —
beide Formen in einer Liste zu mischen ist deshalb unproblematisch
und praktisch, wenn dieselbe Konfiguration beide Modi bedient.
Eine Liste, die im konfigurierten Modus nichts treffen kann, wird bei der Konstruktion abgelehnt, statt still jeden Record zu verwerfen:
const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('_actor-ts._tcp.example.com') .withSystemName('my-app') .withUseSrv() .withPinnedAddresses(['10.0.0.0/8']);// Wirft OptionsError — SRV-Ziele sind Hostnames, diese Liste würde// jeden Record verwerfen und wie eine leere DNS-Antwort aussehen.new DnsSeedProvider(dnsSeedProviderOptions);Dieselbe Prüfung lehnt ein bloßes '10.0.0.1' ab (schreibe
'10.0.0.1/32'), ebenso eine leere Liste und ein fehlerhaftes
CIDR.
Adressen werden nur in kanonischer Form verglichen
Abschnitt betitelt „Adressen werden nur in kanonischer Form verglichen“Beide Seiten des Vergleichs — die CIDRs, die du pinnst, und die
Adressen, die der Resolver zurückgibt — müssen kanonisch sein:
Dezimal-Oktette ohne führende Null, ohne 0x, ohne Exponent, ohne
Whitespace, und eine schlichte dezimale Präfixlänge.
Das ist keine Pedanterie. 1e1.0.0.1, 010.0.0.1 und
0x0a.0.0.1 lesen sich für einen großzügigen Zahlen-Parser alle
als 10.0.0.1, aber die Socket-Schicht wertet sie als keine IP
und löst sie stattdessen über DNS auf — eine Antwort in einer
dieser Schreibweisen käme also an einem 10.0.0.0/8-Pin vorbei
und verbände sich dann dorthin, wohin der Resolver des Angreifers
zeigt. Alles Nicht-Kanonische gilt jetzt als Hostname, und ein
Hostname wird nie gegen einen CIDR-Pin geprüft.
Zwei Konsequenzen, die beim Upgrade zählen:
- Ein nicht-kanonisch geschriebener Pin-Eintrag (
'010.0.0.0/8') wirft bei der Konstruktion, statt ein anderes Netz zu pinnen als dort steht. - Ein Tippfehler mit abschließendem Slash (
'10.0.0.0/') wirft ebenfalls. Bisher wurde er als/0gelesen — ein Pin, der ein Netz zulassen sollte und den gesamten Adressraum zuließ.
Ein rein numerisches Host-Suffix ('0.1') wird ebenfalls
abgelehnt: Suffix-Matching ist Stringvergleich, ein solcher
Eintrag würde also das Ende unbeteiligter Adressen treffen. Pinne
stattdessen ein CIDR.
Verworfene Adressen sind gefiltert, nicht fatal — ein
veralteter Record kostet diesen Record, nicht den gesamten
Bootstrap. Jeder Wurf geht an log, und das lohnt sich zu
verdrahten: eine zu enge Pin-Liste und eine leere DNS-Antwort
sehen von außen identisch aus, und eine leere Seed-Liste heißt,
dass der Node seinen eigenen Single-Node-Cluster bildet.
Wann du ihn einsetzt
Abschnitt betitelt „Wann du ihn einsetzt“| Umgebung | Verwendung |
|---|---|
| Consul-verwaltete Deployments | SRV-Modus — Consul schreibt sie automatisch. |
| Eureka mit DNS-Plugin | SRV-Modus. |
| Manuelle Deployments mit DNS-Server | Beide Modi; SRV bevorzugt. |
| Cloud mit DNS-basiertem Service Discovery | SRV-Modus. |
| K8s | Nimm KubernetesApiSeedProvider — K8s’ Headless Services produzieren zwar A-Records, aber der K8s-API-Provider ist zuverlässiger. |
DNS-Caching
Abschnitt betitelt „DNS-Caching“lookup() ist kein Einmal-Aufruf — die Bootstrap-Schicht ruft
es beim Start auf, und übergeordnete Retry-Logik kann es erneut
aufrufen. Damit wiederholte Aufrufe DNS nicht überlasten, hält
der Provider einen In-Memory-TTL-Cache pro Instanz:
const dnsSeedProviderOptions = DnsSeedProviderOptions.create() .withHostname('_actor-ts._tcp.example.com') .withSystemName('my-app') .withUseSrv() .withCacheTtlMs(30_000); // Default 60_000 ms; 0 deaktiviert den Cache- Aufrufe innerhalb des
cacheTtlMs-Fensters (Default 60 000 ms) werden aus dem Cache bedient, ohne DNS zu berühren. - Der erste Aufruf nach Ablauf der TTL fragt DNS erneut ab.
- Ein fehlgeschlagener Lookup wird nicht gecacht — er wird beim nächsten Aufruf wiederholt.
Sobald Cluster.join fertig ist, übernimmt die Gossip-Schicht
des Clusters das Membership-Tracking, sodass DNS nicht mehr auf
dem heißen Pfad liegt. Das heißt:
- Ein Node, der nach dem Start hinzugefügt wird, ist für existierende Peers nicht via DNS sichtbar. Er joint über seinen eigenen DNS-Lookup, kontaktiert einen existierenden Seed, Gossip propagiert weiter.
- DNS-Record-TTLs spielen nur eine Rolle, wenn ein Lookup tatsächlich DNS trifft (Start und Re-Queries nach Ablauf), nicht für die Steady-State-Laufzeit des Clusters.
Wann NICHT
Abschnitt betitelt „Wann NICHT“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Discovery im Überblick — das Gesamtbild.
- Config Seed Provider — die einfachere Alternative.
- Kubernetes-API Seed Provider — bevorzugt auf K8s.
- Aggregate Seed Provider — DNS mit statischem Fallback kombinieren.
