picker.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. package zones
  2. import (
  3. "math/rand"
  4. "github.com/abh/geodns/health"
  5. "github.com/abh/geodns/targeting/geo"
  6. "github.com/miekg/dns"
  7. )
  8. // AlwaysWeighted types always honors 'MaxHosts', even
  9. // without an explicit weight set. (This list is slightly arbitrary).
  10. var AlwaysWeighted = []uint16{
  11. dns.TypeA, dns.TypeAAAA, dns.TypeCNAME,
  12. }
  13. var alwaysWeighted map[uint16]struct{}
  14. func init() {
  15. alwaysWeighted = map[uint16]struct{}{}
  16. for _, t := range AlwaysWeighted {
  17. alwaysWeighted[t] = struct{}{}
  18. }
  19. }
  20. func (zone *Zone) filterHealth(servers Records) (Records, int) {
  21. // Remove any unhealthy servers
  22. tmpServers := servers[:0]
  23. sum := 0
  24. for i, s := range servers {
  25. if len(servers[i].Test) == 0 || health.GetStatus(servers[i].Test) == health.StatusHealthy {
  26. tmpServers = append(tmpServers, s)
  27. sum += s.Weight
  28. }
  29. }
  30. return tmpServers, sum
  31. }
  32. // Picker picks the best results from a label matching the qtype,
  33. // up to 'max' results. If location is specified Picker will get
  34. // return the "closests" results, otherwise they are returned weighted
  35. // randomized.
  36. func (zone *Zone) Picker(label *Label, qtype uint16, max int, location *geo.Location) Records {
  37. if qtype == dns.TypeANY {
  38. var result Records
  39. for rtype := range label.Records {
  40. rtypeRecords := zone.Picker(label, rtype, max, location)
  41. tmpResult := make(Records, len(result)+len(rtypeRecords))
  42. copy(tmpResult, result)
  43. copy(tmpResult[len(result):], rtypeRecords)
  44. result = tmpResult
  45. }
  46. return result
  47. }
  48. labelRR := label.Records[qtype]
  49. if labelRR == nil {
  50. // we don't have anything of the correct type
  51. return nil
  52. }
  53. sum := label.Weight[qtype]
  54. servers := make(Records, len(labelRR))
  55. copy(servers, labelRR)
  56. if label.Test != nil {
  57. servers, sum = zone.filterHealth(servers)
  58. // sum re-check to mirror the label.Weight[] check below
  59. if sum == 0 {
  60. // todo: this is wrong for cname since it misses
  61. // the 'max_hosts' setting
  62. return servers
  63. }
  64. }
  65. // not "balanced", just return all -- It's been working
  66. // this way since the first prototype, it might not make
  67. // sense anymore. This probably makes NS records and such
  68. // work as expected.
  69. // A, AAAA and CNAME records ("AlwaysWeighted") are always given
  70. // a weight so MaxHosts works for those even if weight isn't set.
  71. if label.Weight[qtype] == 0 {
  72. return servers
  73. }
  74. if qtype == dns.TypeCNAME || qtype == dns.TypeMF {
  75. max = 1
  76. }
  77. rrCount := len(servers)
  78. if max > rrCount {
  79. max = rrCount
  80. }
  81. result := make(Records, max)
  82. // Find the distance to each server, and find the servers that are
  83. // closer to the querier than the max'th furthest server, or within
  84. // 5% thereof. What this means in practice is that if we have a nearby
  85. // cluster of servers that are close, they all get included, so load
  86. // balancing works
  87. if location != nil && (qtype == dns.TypeA || qtype == dns.TypeAAAA) && max < rrCount {
  88. // First we record the distance to each server
  89. distances := make([]float64, rrCount)
  90. for i, s := range servers {
  91. distance := location.Distance(s.Loc)
  92. distances[i] = distance
  93. }
  94. // though this looks like O(n^2), typically max is small (e.g. 2)
  95. // servers often have the same geographic location
  96. // and rrCount is pretty small too, so the gain of an
  97. // O(n log n) sort is small.
  98. chosen := 0
  99. choose := make([]bool, rrCount)
  100. for chosen < max {
  101. // Determine the minimum distance of servers not yet chosen
  102. minDist := location.MaxDistance()
  103. for i := range servers {
  104. if !choose[i] && distances[i] <= minDist {
  105. minDist = distances[i]
  106. }
  107. }
  108. // The threshold for inclusion on the this pass is 5% more
  109. // than the minimum distance
  110. minDist = minDist * 1.05
  111. // Choose all the servers within the distance
  112. for i := range servers {
  113. if !choose[i] && distances[i] <= minDist {
  114. choose[i] = true
  115. chosen++
  116. }
  117. }
  118. }
  119. // Now choose only the chosen servers, using filtering without allocation
  120. // slice trick. Meanwhile recalculate the total weight
  121. tmpServers := servers[:0]
  122. sum = 0
  123. for i, s := range servers {
  124. if choose[i] {
  125. tmpServers = append(tmpServers, s)
  126. sum += s.Weight
  127. }
  128. }
  129. servers = tmpServers
  130. }
  131. for si := 0; si < max; si++ {
  132. n := rand.Intn(sum + 1)
  133. s := 0
  134. for i := range servers {
  135. s += int(servers[i].Weight)
  136. if s >= n {
  137. sum -= servers[i].Weight
  138. result[si] = servers[i]
  139. // remove the server from the list
  140. servers = append(servers[:i], servers[i+1:]...)
  141. break
  142. }
  143. }
  144. }
  145. return result
  146. }