dnsdisco

package module
v0.0.0-...-931d631 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2016 License: MIT Imports: 6 Imported by: 0

README

dnsdisco

GoDoc license Build Status Coverage Status Go Report Card codebeat badge

DNS service discovery library for Go programming language.

dnsdisco

Motivation

So you have more than one service (or microservice) and you need to integrate them? Great! I can think on some options to store/retrieve the services address to allow this integration:

  • Configuration file
  • Centralized configuration system (etcd, etc.)
  • Load balancer device ($$$) or software
  • DNS

Wait? What? DNS? Yep! You can use the SRV records to announce your service addresses. And with a small TTL (cache) you could also make fast transitions to increase/decrease the number of instances.

This SRV records contains priority and weight, so you can determinate which servers (of the same name) receives more requests than others. The only problem was that with the DNS solution we didn't have the health check feature of a load balancer. And that's where this library jumps in! It will choose for you the best server looking the priority, weight and the health check result. And as each service has it's particular way of health checking, this library is flexible so you can implement the best health check algorithm that fits for you.

And doesn't stop there. If you don't want to use the default resolver for retrieving the SRV records, you can change it! If you want a more efficient load balancer algorithm (please send a PR 😄), you can also implement it.

This library follows the Go language philosophy:

"Less is more" (Ludwig Mies van der Rohe)

Features

  • Servers are retrieved from a DNS SRV request (using your default resolver)
  • Each server is verified with a health check (simple connection test)
  • Load balancer choose the best server to send the request (RFC 2782 algorithm)
  • Library is flexible so you could change any part with your own implementation
  • Has only standard library dependencies (except for tests)
  • Go routine safe

Install

go get -u github.com/rafaeljusto/dnsdisco

Example

A basic use would be:

package main

import (
  "fmt"

  "github.com/rafaeljusto/dnsdisco"
)

func main() {
  target, port, err := dnsdisco.Discover("jabber", "tcp", "registro.br")
  if err != nil {
    fmt.Println(err)
    return
  }

  fmt.Printf("Target: %s\nPort: %d\n", target, port)
}

For this example we imagine that the domain registro.br. is configured as the following:

registro.br.    172800 IN SOA a.dns.br. hostmaster.registro.br. (
                        2016021101 ; serial
                        86400      ; refresh (1 day)
                        3600       ; retry (1 hour)
                        604800     ; expire (1 week)
                        86400      ; minimum (1 day)
                        )

registro.br.    172800 IN NS a.dns.br.
registro.br.    172800 IN NS b.dns.br.
registro.br.    172800 IN NS c.dns.br.
registro.br.    172800 IN NS d.dns.br.
registro.br.    172800 IN NS e.dns.br.

_jabber._tcp.registro.br. 172800 IN SRV	1 65534 5269 jabber.registro.br.

Check the documentation for more examples.

Documentation

Overview

Package dnsdisco is a DNS service discovery library with health check and load balancer features.

The library is very flexible and uses interfaces everywhere to make it possible for the library user to replace any part with a custom algorithm. Check the examples for more details.

Example (LoadBalancer)

Example_loadBalancer shows how it is possible to replace the default load balancer algorithm with a new one following the round robin strategy (https://en.wikipedia.org/wiki/Round-robin_scheduling).

package main

import (
	"container/ring"
	"fmt"
	"net"

	"github.com/rafaeljusto/dnsdisco"
)

// roundRobinLoadBalancer is a load balancer that selects the server using a
// round robin algorithm.
type roundRobinLoadBalancer struct {
	// servers is a circular linked list to allow a fast round robin algorithm.
	servers *ring.Ring
}

// ChangeServers will be called anytime that a new set of servers is retrieved.
func (r *roundRobinLoadBalancer) ChangeServers(servers []*net.SRV) {
	r.servers = ring.New(len(servers))
	i, n := 0, r.servers.Len()

	for p := r.servers; i < n; p = p.Next() {
		p.Value = servers[i]
		i++
	}
}

// LoadBalance will choose the best target based on a round robin strategy. If
// no server is selected an empty target and a zero port is returned.
func (d roundRobinLoadBalancer) LoadBalance() (target string, port uint16) {
	if d.servers.Len() == 0 {
		return "", 0
	}

	server, _ := d.servers.Value.(*net.SRV)
	d.servers = d.servers.Next()
	return server.Target, server.Port
}

// Example_loadBalancer shows how it is possible to replace the default load
// balancer algorithm with a new one following the round robin strategy
// (https://en.wikipedia.org/wiki/Round-robin_scheduling).
func main() {
	discovery := dnsdisco.NewDiscovery("jabber", "tcp", "registro.br")
	discovery.SetLoadBalancer(new(roundRobinLoadBalancer))

	// Retrieve the servers
	if err := discovery.Refresh(); err != nil {
		fmt.Println(err)
		return
	}

	target, port := discovery.Choose()
	fmt.Printf("Target: %s\nPort: %d\n", target, port)

}
Output:

Target: jabber.registro.br.
Port: 5269

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Discover

func Discover(service, proto, name string) (target string, port uint16, err error)

Discover is the fastest way to find a target using all the default parameters. It will send a SRV query in _service._proto.name format and return the target (address and port) selected by the RFC 2782 algorithm and that passed on the health check (simple connection check).

proto must be "udp" or "tcp", otherwise an UnknownNetworkError error will be returned. The library will use the local resolver to send the DNS package.

Example

ExampleDiscover is the fastest way to select a server using all default algorithms.

package main

import (
	"fmt"

	"github.com/rafaeljusto/dnsdisco"
)

func main() {
	target, port, err := dnsdisco.Discover("jabber", "tcp", "registro.br")
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("Target: %s\nPort: %d\n", target, port)

}
Output:

Target: jabber.registro.br.
Port: 5269
Example (RefreshAsync)

ExampleDiscover_refreshAsync updates the servers list asynchronously every 100 milliseconds.

package main

import (
	"fmt"
	"net"
	"time"

	"github.com/rafaeljusto/dnsdisco"
)

func main() {
	discovery := dnsdisco.NewDiscovery("jabber", "tcp", "registro.br")

	// depending on where this examples run the retrieving time differs (DNS RTT),
	// so as we cannot sleep a deterministic period, to make this test more useful
	// we are creating a channel to alert the main go routine that we got an
	// answer from the network
	retrieved := make(chan bool)

	discovery.SetRetriever(dnsdisco.RetrieverFunc(func(service, proto, name string) (servers []*net.SRV, err error) {
		_, servers, err = net.LookupSRV(service, proto, name)
		retrieved <- true
		return
	}))

	// refresh the SRV records every 100 milliseconds
	stopRefresh := discovery.RefreshAsync(100 * time.Millisecond)
	<-retrieved

	// sleep for a short period only to allow the library to process the SRV
	// records retrieved from the network
	time.Sleep(100 * time.Millisecond)

	target, port := discovery.Choose()
	fmt.Printf("Target: %s\nPort: %d\n", target, port)
	close(stopRefresh)

}
Output:

Target: jabber.registro.br.
Port: 5269

Types

type Discovery

type Discovery interface {
	// Refresh retrieves the servers using the DNS SRV solution. It is possible to
	// change the default behaviour (local resolver with default timeouts) using
	// the SetRetriever method from the Discovery interface.
	Refresh() error

	// RefreshAsync works exactly as Refresh, but is non-blocking and will repeat
	// the action on every interval. To stop the refresh the returned channel must
	// be closed.
	RefreshAsync(time.Duration) chan<- bool

	// Choose will return the best target to use based on a defined load balancer.
	// By default the library choose the server based on the RFC 2782 considering
	// only the online servers. It is possible to change the load balancer
	// behaviour using the SetLoadBalancer method from the Discovery interface. If
	// no good match is found it should return a empty target and a zero port.
	Choose() (target string, port uint16)

	// Errors return all errors found during asynchronous executions. Once this
	// method is called the internal errors buffer is cleared.
	Errors() []error

	// SetRetriever changes how the library retrieves the DNS SRV records.
	SetRetriever(Retriever)

	// SetHealthChecker changes the way the library health check each server.
	SetHealthChecker(HealthChecker)

	// SetLoadBalancer changes how the library selects the best server.
	SetLoadBalancer(LoadBalancer)
}

Discovery contains all the methods to discover the services and select the best one at the moment. The use of interface allows the users to mock this library easily for unit tests.

func NewDiscovery

func NewDiscovery(service, proto, name string) Discovery

NewDiscovery builds the default implementation of the Discovery interface. To retrieve the servers it will use the net.LookupSRV (local resolver), for health check will only perform a simple connection, and the chosen target will be selected using the RFC 2782 considering only online servers.

The returned type can be used globally as it is go routine safe. It is recommended to keep a global Discovery for each service to minimize the number of DNS requests.

type HealthChecker

type HealthChecker interface {
	// HealthCheck will analyze the target port/proto to check if it is still
	// capable of receiving requests.
	HealthCheck(target string, port uint16, proto string) (ok bool, err error)
}

HealthChecker allows the library user to define a custom health check algorithm.

func NewDefaultHealthChecker

func NewDefaultHealthChecker() HealthChecker

NewDefaultHealthChecker returns an instance of the default health checker algorithm. The default health checker tries to do a simple connection to the server. If the connection is successful the health check pass, otherwise it fails with an error. Possible proto values are tcp or udp.

type HealthCheckerFunc

type HealthCheckerFunc func(target string, port uint16, proto string) (ok bool, err error)

HealthCheckerFunc is an easy-to-use implementation of the interface that is responsible for checking if a target is still alive.

Example

ExampleHealthCheckerFunc tests HTTP fetching the homepage and checking the HTTP status code.

package main

import (
	"fmt"
	"net/http"

	"github.com/rafaeljusto/dnsdisco"
)

func main() {
	discovery := dnsdisco.NewDiscovery("http", "tcp", "pantz.org")
	discovery.SetHealthChecker(dnsdisco.HealthCheckerFunc(func(target string, port uint16, proto string) (ok bool, err error) {
		response, err := http.Get("http://www.pantz.org")
		if err != nil {
			return false, err
		}

		return response.StatusCode == http.StatusOK, nil
	}))

	// Retrieve the servers
	if err := discovery.Refresh(); err != nil {
		fmt.Println(err)
		return
	}

	target, port := discovery.Choose()
	fmt.Printf("Target: %s\nPort: %d\n", target, port)

}
Output:

Target: www.pantz.org.
Port: 80

func (HealthCheckerFunc) HealthCheck

func (h HealthCheckerFunc) HealthCheck(target string, port uint16, proto string) (ok bool, err error)

HealthCheck will analyze the target port/proto to check if it is still capable of receiving requests.

type LoadBalancer

type LoadBalancer interface {
	// ChangeServers will be called anytime that a new set of servers is
	// retrieved.
	ChangeServers(servers []*net.SRV)

	// LoadBalance will choose the best target.
	LoadBalance() (target string, port uint16)
}

LoadBalancer allows the library user to define a custom balance algorithm.

func NewDefaultLoadBalancer

func NewDefaultLoadBalancer() LoadBalancer

NewDefaultLoadBalancer returns an instance of the default load balancer algorithm, that selects the best server based on the RFC 2782 algorithm. If no server is selected an empty target and a zero port is returned.

type Retriever

type Retriever interface {
	// Retrieve will send the DNS request and return all SRV records retrieved
	// from the response.
	Retrieve(service, proto, name string) ([]*net.SRV, error)
}

Retriever allows the library user to define a custom DNS retrieve algorithm.

func NewDefaultRetriever

func NewDefaultRetriever() Retriever

NewDefaultRetriever returns an instance of the default retriever algorithm, that uses the local resolver to retrieve the SRV records.

type RetrieverFunc

type RetrieverFunc func(service, proto, name string) ([]*net.SRV, error)

RetrieverFunc is an easy-to-use implementation of the interface that is responsible for sending the DNS SRV requests.

Example

ExampleRetrieverFunc uses a specific resolver with custom timeouts.

package main

import (
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/miekg/dns"
	"github.com/rafaeljusto/dnsdisco"
)

func main() {
	discovery := dnsdisco.NewDiscovery("jabber", "tcp", "registro.br")
	discovery.SetRetriever(dnsdisco.RetrieverFunc(func(service, proto, name string) (servers []*net.SRV, err error) {
		client := dns.Client{
			ReadTimeout:  2 * time.Second,
			WriteTimeout: 2 * time.Second,
		}

		name = strings.TrimRight(name, ".")
		z := fmt.Sprintf("_%s._%s.%s.", service, proto, name)

		var request dns.Msg
		request.SetQuestion(z, dns.TypeSRV)
		request.RecursionDesired = true

		response, _, err := client.Exchange(&request, "8.8.8.8:53")
		if err != nil {
			return nil, err
		}

		for _, rr := range response.Answer {
			if srv, ok := rr.(*dns.SRV); ok {
				servers = append(servers, &net.SRV{
					Target:   srv.Target,
					Port:     srv.Port,
					Priority: srv.Priority,
					Weight:   srv.Weight,
				})
			}
		}

		return
	}))

	// Retrieve the servers
	if err := discovery.Refresh(); err != nil {
		fmt.Println(err)
		return
	}

	target, port := discovery.Choose()
	fmt.Printf("Target: %s\nPort: %d\n", target, port)

}
Output:

Target: jabber.registro.br.
Port: 5269

func (RetrieverFunc) Retrieve

func (r RetrieverFunc) Retrieve(service, proto, name string) ([]*net.SRV, error)

Retrieve will send the DNS request and return all SRV records retrieved from the response.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL