diff --git a/cmd/redirectord/redirectord.go b/cmd/redirectord/redirectord.go index f63c71ef80c28edea550705e8dd47a82c61139e3..5e862cea2b106baad35d78cd67c78f41adce272a 100644 --- a/cmd/redirectord/redirectord.go +++ b/cmd/redirectord/redirectord.go @@ -18,6 +18,8 @@ var ( staticDir = flag.String("static-dir", "/usr/share/radioai/htdocs/static", "Static content directory") templateDir = flag.String("template-dir", "/usr/share/radioai/htdocs/templates", "HTML templates directory") + lbPolicy = flag.String("lb-policy", "weighted", "Load balancing policy (weighted, leastloaded)") + // Default DNS TTL (seconds). dnsTtl = 5 ) @@ -35,6 +37,6 @@ func main() { dnsRed := fe.NewDnsRedirector(api, *domain, *publicIp, dnsTtl) dnsRed.Run(fmt.Sprintf(":%d", *dnsPort)) - red := fe.NewHttpRedirector(api, *domain) + red := fe.NewHttpRedirector(api, *domain, *lbPolicy) red.Run(fmt.Sprintf(":%d", *httpPort), *staticDir, *templateDir) } diff --git a/fe/http.go b/fe/http.go index 075201f57906081027cec1090f250e1bd34e4e47..0097e181569e02a6d9b51880ca60c242e5fae22d 100644 --- a/fe/http.go +++ b/fe/http.go @@ -6,7 +6,6 @@ import ( "html/template" "io" "log" - "math/rand" "net" "net/http" "path/filepath" @@ -26,18 +25,19 @@ type HttpRedirector struct { domain string client *radioai.RadioAPI template *template.Template + lb LoadBalancingPolicy } -func NewHttpRedirector(client *radioai.RadioAPI, domain string) *HttpRedirector { +func NewHttpRedirector(client *radioai.RadioAPI, domain string, lbpolicy string) *HttpRedirector { return &HttpRedirector{ client: client, domain: domain, + lb: getNamedLoadBalancingPolicy(lbpolicy), } } -// Return an active node, chosen randomly (this is currently our load -// balancing policy, since there is no status information about the -// nodes yet). +// Return an active node, chosen according to the current load +// balancing policy. func (h *HttpRedirector) pickActiveNode() string { nodes, _ := h.client.GetNodes() if nodes == nil { @@ -45,17 +45,21 @@ func (h *HttpRedirector) pickActiveNode() string { } // Filter nodes where Icecast is reported to be up. - okNodes := make([]string, 0, len(nodes)) + okNodes := make([]*radioai.NodeStatus, 0, len(nodes)) for _, n := range nodes { if n.IcecastUp { - okNodes = append(okNodes, n.IP) + okNodes = append(okNodes, n) } } + if len(okNodes) == 0 { + return "" + } - if len(okNodes) > 0 { - return okNodes[rand.Intn(len(okNodes))] + result := h.lb.GetNode(okNodes) + if result == nil { + return "" } - return "" + return result.IP } // Parse the request and extract the mount path. @@ -184,6 +188,8 @@ func (h *HttpRedirector) serveStatusPage(w http.ResponseWriter, r *http.Request) } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) + w.Header().Set("Expires", "-1") + w.Header().Set("Cache-Control", "private, max-age=0") w.Write(buf.Bytes()) } diff --git a/fe/loadbalancing.go b/fe/loadbalancing.go new file mode 100644 index 0000000000000000000000000000000000000000..4a1afe5f8bfd847c20e6d807d3f32d6fde7ebf12 --- /dev/null +++ b/fe/loadbalancing.go @@ -0,0 +1,60 @@ +package fe + +import ( + "log" + + "git.autistici.org/ale/radioai" + "github.com/jmcvetta/randutil" +) + +// A load balancing policy selects a single node from the pool of +// currently active ones. +type LoadBalancingPolicy interface { + GetNode([]*radioai.NodeStatus) *radioai.NodeStatus +} + +// Simple load balancing policy that always returns the nodes with the +// least amount of listeners. +type leastListenersPolicy struct{} + +func (llp leastListenersPolicy) GetNode(nodes []*radioai.NodeStatus) *radioai.NodeStatus { + minIdx := 0 + min := 1000000 + for i, n := range nodes { + if listeners := n.NumListeners(); listeners < min { + minIdx = i + min = listeners + } + } + return nodes[minIdx] +} + +// Simple load balancing policy that selects a node randomly, +// associating a weight to each node inversely proportional to the +// number of active listeners. +type weightedListenersPolicy struct{} + +func (wlp weightedListenersPolicy) GetNode(nodes []*radioai.NodeStatus) *radioai.NodeStatus { + choices := make([]randutil.Choice, 0, len(nodes)) + weightBase := 1000000 + for _, n := range nodes { + w := weightBase / (n.NumListeners() + 1) + choices = append(choices, randutil.Choice{w, n}) + } + result, err := randutil.WeightedChoice(choices) + if err != nil { + return nil + } + return result.Item.(*radioai.NodeStatus) +} + +func getNamedLoadBalancingPolicy(lbpolicy string) LoadBalancingPolicy { + switch lbpolicy { + case "leastloaded", "ll": + return &leastListenersPolicy{} + case "weighted", "wl": + return &weightedListenersPolicy{} + } + log.Fatalf("Unknown load-balancing policy '%s'", lbpolicy) + return nil +}