mirror of
https://github.com/coredns/coredns.git
synced 2026-09-01 09:37:05 -04:00
117 lines
4.0 KiB
Go
117 lines
4.0 KiB
Go
|
|
// Package shed bounds concurrent UDP response writes per listener socket so
|
||
|
|
// that an overload storm degrades into counted drops instead of a goroutine
|
||
|
|
// pile-up and the Go runtime's fdMutex overflow panic. See README.md for the
|
||
|
|
// failure mode and fdmutex_test.go for its reproduction.
|
||
|
|
//
|
||
|
|
// Setup installs dnsserver's Config.UDPDecorateWriterFunc: the decorated
|
||
|
|
// Write pushes the packed response onto a bounded evict-oldest stack and one
|
||
|
|
// writer goroutine per socket pops newest-first onto the wire, so the fd
|
||
|
|
// never sees more than one writer. While a socket's stack is full, ServeDNS
|
||
|
|
// drops arrivals before any chain work. Every drop is counted.
|
||
|
|
package shed
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"sync"
|
||
|
|
|
||
|
|
"github.com/coredns/coredns/core/dnsserver"
|
||
|
|
"github.com/coredns/coredns/plugin"
|
||
|
|
"github.com/coredns/coredns/request"
|
||
|
|
|
||
|
|
"github.com/miekg/dns"
|
||
|
|
"github.com/prometheus/client_golang/prometheus"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Shed implements the plugin.Handler interface.
|
||
|
|
type Shed struct {
|
||
|
|
Next plugin.Handler
|
||
|
|
}
|
||
|
|
|
||
|
|
// registry maps a listener socket's *dnsserver.Server to its per-socket
|
||
|
|
// state. It is package level: several server blocks can share a listener,
|
||
|
|
// and every block's ServeDNS must see that socket's one stack.
|
||
|
|
var registry sync.Map // *dnsserver.Server -> *socketState
|
||
|
|
|
||
|
|
// socketState is one listener socket's registry record. owner scopes
|
||
|
|
// shutdown to this instance's entries on reload.
|
||
|
|
type socketState struct {
|
||
|
|
owner *Shed
|
||
|
|
stack *respStack
|
||
|
|
droppedQuery prometheus.Counter
|
||
|
|
}
|
||
|
|
|
||
|
|
// lookupState returns the per-socket state for this request's socket, or nil
|
||
|
|
// — and on nil ServeDNS fails open. A miss happens when the request carries
|
||
|
|
// no *dnsserver.Server in its context (tests, non-dnsserver entry points),
|
||
|
|
// or for a handler still finishing on a server a reload already removed;
|
||
|
|
// neither admits unbounded new work.
|
||
|
|
func (s *Shed) lookupState(ctx context.Context) *socketState {
|
||
|
|
srv := ctx.Value(dnsserver.Key{})
|
||
|
|
if srv == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if v, ok := registry.Load(srv); ok {
|
||
|
|
return v.(*socketState)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// mintState creates one listener socket's state and starts its writer
|
||
|
|
// goroutine. Idempotent; the loaded path only happens in tests.
|
||
|
|
func (s *Shed) mintState(srv *dnsserver.Server) *socketState {
|
||
|
|
st := &socketState{
|
||
|
|
owner: s,
|
||
|
|
stack: newRespStack(stackDepth, droppedTotal.WithLabelValues(srv.Address(), "response")),
|
||
|
|
droppedQuery: droppedTotal.WithLabelValues(srv.Address(), "query"),
|
||
|
|
}
|
||
|
|
if v, loaded := registry.LoadOrStore(srv, st); loaded {
|
||
|
|
return v.(*socketState)
|
||
|
|
}
|
||
|
|
go st.stack.writerLoop()
|
||
|
|
return st
|
||
|
|
}
|
||
|
|
|
||
|
|
// shutdown removes this instance's registry entries and stops their writer
|
||
|
|
// goroutines. A push after removal is rejected by the closed stack and
|
||
|
|
// counted as a dropped response.
|
||
|
|
func (s *Shed) shutdown() error {
|
||
|
|
registry.Range(func(k, v any) bool {
|
||
|
|
if st := v.(*socketState); st.owner == s {
|
||
|
|
// LoadAndDelete makes close-once structural even if sweeps race.
|
||
|
|
if _, loaded := registry.LoadAndDelete(k); loaded {
|
||
|
|
st.stack.close()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ServeDNS implements the plugin.Handler interface.
|
||
|
|
func (s *Shed) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||
|
|
// The failure mechanism is UDP-specific: connectionless writes racing
|
||
|
|
// one fdMutex. TCP must never be starved by UDP-storm shedding.
|
||
|
|
state := request.Request{W: w, Req: r}
|
||
|
|
if state.Proto() != "udp" {
|
||
|
|
return plugin.NextOrFailure(s.Name(), s.Next, ctx, w, r)
|
||
|
|
}
|
||
|
|
st := s.lookupState(ctx)
|
||
|
|
if st == nil {
|
||
|
|
return plugin.NextOrFailure(s.Name(), s.Next, ctx, w, r) // fail open — see lookupState
|
||
|
|
}
|
||
|
|
|
||
|
|
// Coupled shed. full() is a racy read by design: a load-shedding
|
||
|
|
// heuristic, not an invariant. Silent drop: nothing is written, and
|
||
|
|
// RcodeSuccess satisfies plugin.ClientWrite so dnsserver writes nothing
|
||
|
|
// either.
|
||
|
|
if st.stack.full() {
|
||
|
|
st.droppedQuery.Inc()
|
||
|
|
return dns.RcodeSuccess, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
return plugin.NextOrFailure(s.Name(), s.Next, ctx, w, r)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Name implements the plugin.Handler interface.
|
||
|
|
func (s *Shed) Name() string { return pluginName }
|