Jelajahi Sumber

Add 'closest' flag

This changeset adds a new boolean option 'closest' to the zone options
and to the label options. The label option defaults to the zone option
for 'closest'.

When closest is set on a label within a zone, each A record has its
location determined and stored in memory. When a query is received,
after the targeting process is performed, the geographically closest
group of A records are selected prior to the weight algorithm being
applied.

In practice this means you can use a simple technique (set 'closest'
to be "true") to choose the geographically closest servers. Note
that geographic proximity may not correlate perfectly to topological
proximity.

Note that when 'closest' is selected on any label within the zone,
the GEOIP_CITY_EDITION_REV1 geoip data is required, as that is what
contains latitude and longitude data.

No provision is currently made for servers that change longitude
and latitude (most likely due to a geoip data change) during the
runtime life of the server.

Signed-off-by: Alex Bligh <[email protected]>
Alex Bligh 10 tahun lalu
induk
melakukan
6526d6b229
7 mengubah file dengan 148 tambahan dan 19 penghapusan
  1. 26 1
      geoip.go
  2. 55 2
      picker.go
  3. 12 3
      serve.go
  4. 5 6
      targeting.go
  5. 5 5
      targeting_test.go
  6. 29 1
      zone.go
  7. 16 1
      zones.go

+ 26 - 1
geoip.go

@@ -3,7 +3,9 @@ package main
 import (
 import (
 	"github.com/abh/geodns/Godeps/_workspace/src/github.com/abh/geoip"
 	"github.com/abh/geodns/Godeps/_workspace/src/github.com/abh/geoip"
 	"github.com/abh/geodns/countries"
 	"github.com/abh/geodns/countries"
+	"github.com/golang/geo/s2"
 	"log"
 	"log"
+	"math"
 	"net"
 	"net"
 	"strings"
 	"strings"
 	"time"
 	"time"
@@ -23,6 +25,27 @@ type GeoIP struct {
 	asnLastLoad time.Time
 	asnLastLoad time.Time
 }
 }
 
 
+const MAX_DISTANCE = 360
+
+type Location struct {
+	latitude  float64
+	longitude float64
+}
+
+func (l *Location) MaxDistance() float64 {
+	return MAX_DISTANCE
+}
+
+func (l *Location) Distance(to *Location) float64 {
+	if to == nil {
+		return MAX_DISTANCE
+	}
+	ll1 := s2.LatLngFromDegrees(l.latitude, l.longitude)
+	ll2 := s2.LatLngFromDegrees(to.latitude, to.longitude)
+	angle := ll1.Distance(ll2)
+	return math.Abs(angle.Degrees())
+}
+
 var geoIP = new(GeoIP)
 var geoIP = new(GeoIP)
 
 
 func (g *GeoIP) GetCountry(ip net.IP) (country, continent string, netmask int) {
 func (g *GeoIP) GetCountry(ip net.IP) (country, continent string, netmask int) {
@@ -38,7 +61,7 @@ func (g *GeoIP) GetCountry(ip net.IP) (country, continent string, netmask int) {
 	return
 	return
 }
 }
 
 
-func (g *GeoIP) GetCountryRegion(ip net.IP) (country, continent, regionGroup, region string, netmask int) {
+func (g *GeoIP) GetCountryRegion(ip net.IP) (country, continent, regionGroup, region string, netmask int, location *Location) {
 	if g.city == nil {
 	if g.city == nil {
 		log.Println("No city database available")
 		log.Println("No city database available")
 		country, continent, netmask = g.GetCountry(ip)
 		country, continent, netmask = g.GetCountry(ip)
@@ -50,6 +73,8 @@ func (g *GeoIP) GetCountryRegion(ip net.IP) (country, continent, regionGroup, re
 		return
 		return
 	}
 	}
 
 
+	location = &Location{float64(record.Latitude), float64(record.Longitude)}
+
 	country = record.CountryCode
 	country = record.CountryCode
 	region = record.Region
 	region = record.Region
 	if len(country) > 0 {
 	if len(country) > 0 {

+ 55 - 2
picker.go

@@ -6,13 +6,13 @@ import (
 	"github.com/abh/geodns/Godeps/_workspace/src/github.com/miekg/dns"
 	"github.com/abh/geodns/Godeps/_workspace/src/github.com/miekg/dns"
 )
 )
 
 
-func (label *Label) Picker(qtype uint16, max int) Records {
+func (label *Label) Picker(qtype uint16, max int, location *Location) Records {
 
 
 	if qtype == dns.TypeANY {
 	if qtype == dns.TypeANY {
 		var result []Record
 		var result []Record
 		for rtype := range label.Records {
 		for rtype := range label.Records {
 
 
-			rtypeRecords := label.Picker(rtype, max)
+			rtypeRecords := label.Picker(rtype, max, location)
 
 
 			tmpResult := make(Records, len(result)+len(rtypeRecords))
 			tmpResult := make(Records, len(result)+len(rtypeRecords))
 
 
@@ -45,6 +45,59 @@ func (label *Label) Picker(qtype uint16, max int) Records {
 		result := make([]Record, max)
 		result := make([]Record, max)
 		sum := label.Weight[qtype]
 		sum := label.Weight[qtype]
 
 
+		// 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 qtype == dns.TypeA && location != nil && 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++ {
 		for si := 0; si < max; si++ {
 			n := rand.Intn(sum + 1)
 			n := rand.Intn(sum + 1)
 			s := 0
 			s := 0

+ 12 - 3
serve.go

@@ -81,7 +81,7 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 		ip = realIP
 		ip = realIP
 	}
 	}
 
 
-	targets, netmask := z.Options.Targeting.GetTargets(ip)
+	targets, netmask, location := z.Options.Targeting.GetTargets(ip, z.HasClosest)
 
 
 	m := new(dns.Msg)
 	m := new(dns.Msg)
 	m.SetReply(req)
 	m.SetReply(req)
@@ -133,9 +133,14 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 					ip.String(),
 					ip.String(),
 				}
 				}
 
 
-				targets, netmask := z.Options.Targeting.GetTargets(ip)
+				targets, netmask, location := z.Options.Targeting.GetTargets(ip, z.HasClosest)
 				txt = append(txt, strings.Join(targets, " "))
 				txt = append(txt, strings.Join(targets, " "))
 				txt = append(txt, fmt.Sprintf("/%d", netmask), serverID, serverIP)
 				txt = append(txt, fmt.Sprintf("/%d", netmask), serverID, serverIP)
+				if location != nil {
+					txt = append(txt, fmt.Sprintf("(%.3f,%.3f)", location.latitude, location.longitude))
+				} else {
+					txt = append(txt, "(?,?)")
+				}
 
 
 				m.Answer = []dns.RR{&dns.TXT{Hdr: h,
 				m.Answer = []dns.RR{&dns.TXT{Hdr: h,
 					Txt: txt,
 					Txt: txt,
@@ -159,7 +164,11 @@ func serve(w dns.ResponseWriter, req *dns.Msg, z *Zone) {
 		return
 		return
 	}
 	}
 
 
-	if servers := labels.Picker(labelQtype, labels.MaxHosts); servers != nil {
+	if !labels.Closest {
+		location = nil
+	}
+
+	if servers := labels.Picker(labelQtype, labels.MaxHosts, location); servers != nil {
 		var rrs []dns.RR
 		var rrs []dns.RR
 		for _, record := range servers {
 		for _, record := range servers {
 			rr := dns.Copy(record.RR)
 			rr := dns.Copy(record.RR)

+ 5 - 6
targeting.go

@@ -24,20 +24,19 @@ func init() {
 	cidr48Mask = net.CIDRMask(48, 128)
 	cidr48Mask = net.CIDRMask(48, 128)
 }
 }
 
 
-func (t TargetOptions) GetTargets(ip net.IP) ([]string, int) {
+func (t TargetOptions) GetTargets(ip net.IP, hasClosest bool) ([]string, int, *Location) {
 
 
 	targets := make([]string, 0)
 	targets := make([]string, 0)
 
 
 	var country, continent, region, regionGroup, asn string
 	var country, continent, region, regionGroup, asn string
 	var netmask int
 	var netmask int
+	var location *Location
 
 
 	if t&TargetASN > 0 {
 	if t&TargetASN > 0 {
 		asn, netmask = geoIP.GetASN(ip)
 		asn, netmask = geoIP.GetASN(ip)
 	}
 	}
-
-	if t&TargetRegion > 0 || t&TargetRegionGroup > 0 {
-		country, continent, regionGroup, region, netmask = geoIP.GetCountryRegion(ip)
-
+	if t&TargetRegion > 0 || t&TargetRegionGroup > 0 || hasClosest {
+		country, continent, regionGroup, region, netmask, location = geoIP.GetCountryRegion(ip)
 	} else if t&TargetCountry > 0 || t&TargetContinent > 0 {
 	} else if t&TargetCountry > 0 || t&TargetContinent > 0 {
 		country, continent, netmask = geoIP.GetCountry(ip)
 		country, continent, netmask = geoIP.GetCountry(ip)
 	}
 	}
@@ -80,7 +79,7 @@ func (t TargetOptions) GetTargets(ip net.IP) ([]string, int) {
 	if t&TargetGlobal > 0 {
 	if t&TargetGlobal > 0 {
 		targets = append(targets, "@")
 		targets = append(targets, "@")
 	}
 	}
-	return targets, netmask
+	return targets, netmask, location
 }
 }
 
 
 func (t TargetOptions) String() string {
 func (t TargetOptions) String() string {

+ 5 - 5
targeting_test.go

@@ -41,7 +41,7 @@ func (s *TargetingSuite) TestGetTargets(c *C) {
 	geoIP.setupGeoIPASN()
 	geoIP.setupGeoIPASN()
 
 
 	tgt, _ := parseTargets("@ continent country")
 	tgt, _ := parseTargets("@ continent country")
-	targets, _ := tgt.GetTargets(ip)
+	targets, _, _ := tgt.GetTargets(ip, false)
 	c.Check(targets, DeepEquals, []string{"us", "north-america", "@"})
 	c.Check(targets, DeepEquals, []string{"us", "north-america", "@"})
 
 
 	if geoIP.city == nil {
 	if geoIP.city == nil {
@@ -50,20 +50,20 @@ func (s *TargetingSuite) TestGetTargets(c *C) {
 	}
 	}
 
 
 	tgt, _ = parseTargets("@ continent country region ")
 	tgt, _ = parseTargets("@ continent country region ")
-	targets, _ = tgt.GetTargets(ip)
+	targets, _, _ = tgt.GetTargets(ip, false)
 	c.Check(targets, DeepEquals, []string{"us-ca", "us", "north-america", "@"})
 	c.Check(targets, DeepEquals, []string{"us-ca", "us", "north-america", "@"})
 
 
 	tgt, _ = parseTargets("@ continent regiongroup country region ")
 	tgt, _ = parseTargets("@ continent regiongroup country region ")
-	targets, _ = tgt.GetTargets(ip)
+	targets, _, _ = tgt.GetTargets(ip, false)
 	c.Check(targets, DeepEquals, []string{"us-ca", "us-west", "us", "north-america", "@"})
 	c.Check(targets, DeepEquals, []string{"us-ca", "us-west", "us", "north-america", "@"})
 
 
 	tgt, _ = parseTargets("@ continent regiongroup country region asn ip")
 	tgt, _ = parseTargets("@ continent regiongroup country region asn ip")
-	targets, _ = tgt.GetTargets(ip)
+	targets, _, _ = tgt.GetTargets(ip, false)
 	c.Check(targets, DeepEquals, []string{"[207.171.1.1]", "[207.171.1.0]", "as7012", "us-ca", "us-west", "us", "north-america", "@"})
 	c.Check(targets, DeepEquals, []string{"[207.171.1.1]", "[207.171.1.0]", "as7012", "us-ca", "us-west", "us", "north-america", "@"})
 
 
 	ip = net.ParseIP("2607:f238:2:0::ff:4")
 	ip = net.ParseIP("2607:f238:2:0::ff:4")
 	tgt, _ = parseTargets("ip")
 	tgt, _ = parseTargets("ip")
-	targets, _ = tgt.GetTargets(ip)
+	targets, _, _ = tgt.GetTargets(ip, false)
 	c.Check(targets, DeepEquals, []string{"[2607:f238:2::ff:4]", "[2607:f238:2::]"})
 	c.Check(targets, DeepEquals, []string{"[2607:f238:2::ff:4]", "[2607:f238:2::]"})
 
 
 }
 }

+ 29 - 1
zone.go

@@ -14,6 +14,7 @@ type ZoneOptions struct {
 	MaxHosts  int
 	MaxHosts  int
 	Contact   string
 	Contact   string
 	Targeting TargetOptions
 	Targeting TargetOptions
+	Closest   bool
 }
 }
 
 
 type ZoneLogging struct {
 type ZoneLogging struct {
@@ -24,6 +25,7 @@ type ZoneLogging struct {
 type Record struct {
 type Record struct {
 	RR     dns.RR
 	RR     dns.RR
 	Weight int
 	Weight int
+	Loc    *Location
 }
 }
 
 
 type Records []Record
 type Records []Record
@@ -41,6 +43,7 @@ type Label struct {
 	Ttl      int
 	Ttl      int
 	Records  map[uint16]Records
 	Records  map[uint16]Records
 	Weight   map[uint16]int
 	Weight   map[uint16]int
+	Closest  bool
 }
 }
 
 
 type labels map[string]*Label
 type labels map[string]*Label
@@ -60,7 +63,7 @@ type Zone struct {
 	Options    ZoneOptions
 	Options    ZoneOptions
 	Logging    *ZoneLogging
 	Logging    *ZoneLogging
 	Metrics    ZoneMetrics
 	Metrics    ZoneMetrics
-
+	HasClosest bool
 	sync.RWMutex
 	sync.RWMutex
 }
 }
 
 
@@ -128,6 +131,7 @@ func (z *Zone) AddLabel(k string) *Label {
 	label.Label = k
 	label.Label = k
 	label.Ttl = 0 // replaced later
 	label.Ttl = 0 // replaced later
 	label.MaxHosts = z.Options.MaxHosts
 	label.MaxHosts = z.Options.MaxHosts
+	label.Closest = z.Options.Closest
 
 
 	label.Records = make(map[uint16]Records)
 	label.Records = make(map[uint16]Records)
 	label.Weight = make(map[uint16]int)
 	label.Weight = make(map[uint16]int)
@@ -185,3 +189,27 @@ func (z *Zone) findLabels(s string, targets []string, qts qTypes) (*Label, uint1
 
 
 	return z.Labels[s], 0
 	return z.Labels[s], 0
 }
 }
+
+// Find the locations of all the A records within a zone. If we were being really clever
+// here we could use LOC records too. But for the time being we'll just use GeoIP
+
+func (z *Zone) SetLocations() {
+	qtypes := []uint16{dns.TypeA}
+	for _, label := range z.Labels {
+		if label.Closest {
+			for _, qtype := range qtypes {
+				if label.Records[qtype] != nil && len(label.Records[qtype]) > 0 {
+					for i := range label.Records[qtype] {
+						label.Records[qtype][i].Loc = nil
+						rr := label.Records[qtype][i].RR
+						if a, ok := rr.(*dns.A); ok {
+							ip := a.A
+							_, _, _, _, _, location := geoIP.GetCountryRegion(ip)
+							label.Records[qtype][i].Loc = location
+						}
+					}
+				}
+			}
+		}
+	}
+}

+ 16 - 1
zones.go

@@ -214,6 +214,11 @@ func readZoneFile(zoneName, fileName string) (zone *Zone, zerr error) {
 			zone.Options.Contact = v.(string)
 			zone.Options.Contact = v.(string)
 		case "max_hosts":
 		case "max_hosts":
 			zone.Options.MaxHosts = valueToInt(v)
 			zone.Options.MaxHosts = valueToInt(v)
+		case "closest":
+			zone.Options.Closest = v.(bool)
+			if zone.Options.Closest {
+				zone.HasClosest = true
+			}
 		case "targeting":
 		case "targeting":
 			zone.Options.Targeting, err = parseTargets(v.(string))
 			zone.Options.Targeting, err = parseTargets(v.(string))
 			if err != nil {
 			if err != nil {
@@ -252,7 +257,7 @@ func readZoneFile(zoneName, fileName string) (zone *Zone, zerr error) {
 	//log.Println("IP", string(Zone.Regions["0.us"].IPv4[0].ip))
 	//log.Println("IP", string(Zone.Regions["0.us"].IPv4[0].ip))
 
 
 	switch {
 	switch {
-	case zone.Options.Targeting >= TargetRegionGroup:
+	case zone.Options.Targeting >= TargetRegionGroup || zone.HasClosest:
 		geoIP.setupGeoIPCity()
 		geoIP.setupGeoIPCity()
 	case zone.Options.Targeting >= TargetContinent:
 	case zone.Options.Targeting >= TargetContinent:
 		geoIP.setupGeoIPCountry()
 		geoIP.setupGeoIPCountry()
@@ -261,6 +266,10 @@ func readZoneFile(zoneName, fileName string) (zone *Zone, zerr error) {
 		geoIP.setupGeoIPASN()
 		geoIP.setupGeoIPASN()
 	}
 	}
 
 
+	if zone.HasClosest {
+		zone.SetLocations()
+	}
+
 	return zone, nil
 	return zone, nil
 }
 }
 
 
@@ -289,6 +298,12 @@ func setupZoneData(data map[string]interface{}, Zone *Zone) {
 			case "max_hosts":
 			case "max_hosts":
 				label.MaxHosts = valueToInt(rdata)
 				label.MaxHosts = valueToInt(rdata)
 				continue
 				continue
+			case "closest":
+				label.Closest = rdata.(bool)
+				if label.Closest {
+					Zone.HasClosest = true
+				}
+				continue
 			case "ttl":
 			case "ttl":
 				label.Ttl = valueToInt(rdata)
 				label.Ttl = valueToInt(rdata)
 				continue
 				continue