123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- package zones
- import (
- "math/rand"
- "github.com/abh/geodns/health"
- "github.com/abh/geodns/targeting/geo"
- "github.com/miekg/dns"
- )
- // AlwaysWeighted types always honors 'MaxHosts', even
- // without an explicit weight set. (This list is slightly arbitrary).
- var AlwaysWeighted = []uint16{
- dns.TypeA, dns.TypeAAAA, dns.TypeCNAME,
- }
- var alwaysWeighted map[uint16]struct{}
- func init() {
- alwaysWeighted = map[uint16]struct{}{}
- for _, t := range AlwaysWeighted {
- alwaysWeighted[t] = struct{}{}
- }
- }
- func (zone *Zone) filterHealth(servers Records) (Records, int) {
- // Remove any unhealthy servers
- tmpServers := servers[:0]
- sum := 0
- for i, s := range servers {
- if len(servers[i].Test) == 0 || health.GetStatus(servers[i].Test) == health.StatusHealthy {
- tmpServers = append(tmpServers, s)
- sum += s.Weight
- }
- }
- return tmpServers, sum
- }
- // Picker picks the best results from a label matching the qtype,
- // up to 'max' results. If location is specified Picker will get
- // return the "closests" results, otherwise they are returned weighted
- // randomized.
- func (zone *Zone) Picker(label *Label, qtype uint16, max int, location *geo.Location) Records {
- if qtype == dns.TypeANY {
- var result Records
- for rtype := range label.Records {
- rtypeRecords := zone.Picker(label, rtype, max, location)
- tmpResult := make(Records, len(result)+len(rtypeRecords))
- copy(tmpResult, result)
- copy(tmpResult[len(result):], rtypeRecords)
- result = tmpResult
- }
- return result
- }
- labelRR := label.Records[qtype]
- if labelRR == nil {
- // we don't have anything of the correct type
- return nil
- }
- sum := label.Weight[qtype]
- servers := make(Records, len(labelRR))
- copy(servers, labelRR)
- if label.Test != nil {
- servers, sum = zone.filterHealth(servers)
- // sum re-check to mirror the label.Weight[] check below
- if sum == 0 {
- // todo: this is wrong for cname since it misses
- // the 'max_hosts' setting
- return servers
- }
- }
- // not "balanced", just return all -- It's been working
- // this way since the first prototype, it might not make
- // sense anymore. This probably makes NS records and such
- // work as expected.
- // A, AAAA and CNAME records ("AlwaysWeighted") are always given
- // a weight so MaxHosts works for those even if weight isn't set.
- if label.Weight[qtype] == 0 {
- return servers
- }
- if qtype == dns.TypeCNAME || qtype == dns.TypeMF {
- max = 1
- }
- rrCount := len(servers)
- if max > rrCount {
- max = rrCount
- }
- result := make(Records, max)
- // Find the distance to each server, and find the servers that are
- // closer to the querier than the max'th furthest server, or within
- // 5% thereof. What this means in practice is that if we have a nearby
- // cluster of servers that are close, they all get included, so load
- // balancing works
- if location != nil && (qtype == dns.TypeA || qtype == dns.TypeAAAA) && max < rrCount {
- // First we record the distance to each server
- distances := make([]float64, rrCount)
- for i, s := range servers {
- distance := location.Distance(s.Loc)
- distances[i] = distance
- }
- // though this looks like O(n^2), typically max is small (e.g. 2)
- // servers often have the same geographic location
- // and rrCount is pretty small too, so the gain of an
- // O(n log n) sort is small.
- chosen := 0
- choose := make([]bool, rrCount)
- for chosen < max {
- // Determine the minimum distance of servers not yet chosen
- minDist := location.MaxDistance()
- for i := range servers {
- if !choose[i] && distances[i] <= minDist {
- minDist = distances[i]
- }
- }
- // The threshold for inclusion on the this pass is 5% more
- // than the minimum distance
- minDist = minDist * 1.05
- // Choose all the servers within the distance
- for i := range servers {
- if !choose[i] && distances[i] <= minDist {
- choose[i] = true
- chosen++
- }
- }
- }
- // Now choose only the chosen servers, using filtering without allocation
- // slice trick. Meanwhile recalculate the total weight
- tmpServers := servers[:0]
- sum = 0
- for i, s := range servers {
- if choose[i] {
- tmpServers = append(tmpServers, s)
- sum += s.Weight
- }
- }
- servers = tmpServers
- }
- for si := 0; si < max; si++ {
- n := rand.Intn(sum + 1)
- s := 0
- for i := range servers {
- s += int(servers[i].Weight)
- if s >= n {
- sum -= servers[i].Weight
- result[si] = servers[i]
- // remove the server from the list
- servers = append(servers[:i], servers[i+1:]...)
- break
- }
- }
- }
- return result
- }
|