Skip to content

Latest commit

 

History

History
316 lines (261 loc) · 10.2 KB

TUTORIAL.md

File metadata and controls

316 lines (261 loc) · 10.2 KB

Bender DHCPv6 Tutorial

This tutorial walks through the steps to create a DHCPv6 load tester. Due to nature of the DHCP protocol we cannot easily test standard queries made by users, but instead we may simulate a clients sending queries that are forwarded by a Relay Agent. We won't cover setting up a DHCPv6 server and from this point we will assume you have a working server which can properly answer RELAY queries. If you already know how to Go's DHCP libraries, skip to the "Load Testing" section to get started with Bender and DHCPv6.

Getting Started

You will need to install and configure Go by following the instructions on the Getting Started page. Then follow the instructions on the How To Write Go Code page, particularly for setting up your workspace and the GOPATH environment variable, which we will use throughout this tutorial.

Next we need the DHCP library for Go, which you can fetch using go get as:

cd $GOPATH
go get -u github.com/insomniacslk/dhcp

Finally you will need the latest version of Bender, which you can get by running:

go get -u github.com/pinterest/bender

Load Testing

The next few sections walk through the various parts of the load tester. If you are in a hurry skip to the section "Final Load Tester Program" and just follow the instructions from there.

Intervals

The first thing we need is a function to generate intervals (in nanoseconds) between executing requests. The Bender library comes with some predefined intervals, including a uniform distribution (always wait the same amount of time between each request) and an exponential distribution. In this case we will use the exponential distribution, which means our server will experience load as generated by a Poisson process, which is fairly typical of server workloads on the Internet (with the usual caveats that every service is a special snowflake, etc, etc). We get the interval function with this code:

intervals := bender.ExponentialIntervalGenerator(qps)

Where qps is our desired throughput measured in queries per second. It is also the reciprocal of the mean value of the exponential distribution used to generate the request arrival times (see the wikipedia article above). In practice this means you will see an average QPS that fluctuates around the target QPS (with less fluctuation as you increase the time interval over which you are averaging).

Request Generator

The second thing we need is a channel of requests to send to the DHCPv6 server. When an interval has been generated and Bender is ready to send the request, it pulls the next request from this channel and spawns a goroutine to send the request to the server. This function creates a simple synthetic request generator. We need to generate a SOLICIT requests and executor will take care of wrapping them into a RELAY packet:

// HardwareAddr should return a random hardware address (we'll keep it simple)
func HardwareAddr() (net.HardwareAddr, error) {
	return net.ParseMAC("01:23:45:67:89:ab")
}

// SyntheticDHCPv6Requests generates up to n synthetic DHCPv6 requests
func SyntheticDHCPv6Requests(n int) chan interface{} {
	c := make(chan interface{}, n)
	go func() {
		for i := 0; i < n; i++ {
			// Generate "random" hardware address
			hw, err := HardwareAddr()
			if err != nil {
				log.Println("Error generating MAC:", err)
				continue
			}
			// Genereate solicit for the generated MAC address
			duid := dhcpv6.Duid{
				Type:          dhcpv6.DUID_LLT,
				HwType:        iana.HwTypeEthernet,
				Time:          dhcpv6.GetTime(),
				LinkLayerAddr: hw,
			}
			msg, err := dhcpv6.NewSolicitWithCID(duid)
			if err != nil {
				log.Println("Error generating SOLICIT:", err)
				continue
			}
			c <- msg
		}
		close(c)
	}()
	return c
}

Request Executor

The next thing we need is a request executor, which takes the requests generated above and sends them to the service. We will use a helper function from Bender's dhcpv6 library to do most of the work (connection management, error handling, etc), so all we have to do is write code to verify the request. The executor will perform 4-way handshake (SOLICIT, ADVERTISE, REQUEST, RESPONSE) and the validator will only be executed for a response after successfully processing all of them.

func validator(req, res dhcpv6.DHCPv6) error {
  return nil
}

exec := dhcpv6.CreateExecutor(client, validator)

This validator accepts all requests which succeeded.

Recorder

The last thing we need is a channel that will output events as the load tester runs. This will let us listen to the load testers progress and record stats. We want this channel to be buffered so that we can run somewhat independently of the load test without slowing it down:

recorder := make(chan interface{}, 128)

The LoadTestThroughput function returns a channel on which it will send events for things like the start of the load test, how long it waits between requests, how much overage it is currently experiencing, and when requests start and end, how long they took and whether or not they had errors. That raw event stream makes it possible to analyze the results of a load test. Bender has a couple of simple "recorders" that provide basic functionality for result analysis and all of which use the Record function:

  • NewLoggingRecorder creates a recorder that takes a log.Logger and outputs each event to it in a well-defined format.
  • NewHistogramRecorder creates a recorder that manages a histogram of latencies from requests and error counts.

You can combine recorders using the Record function, so you can both log events and manage a histogram using code like this:

l := log.New(os.Stdout, "", log.LstdFlags)
h := hist.NewHistogram(60000, 10000000)
bender.Record(recorder, bender.NewLoggingRecorder(l), bender.NewHistogramRecorder(h))

The histogram takes two arguments: the number of buckets and a scaling factor for times. In this case we are going to record times in milliseconds and allow 60,000 buckets for times up to one minute. The scaling factor is 1,000,000 which converts from nanoseconds (the timer values) to milliseconds.

It is relatively easy to build recorders, or to just process the events from the channel yourself, see the Bender documentation for more details on what events can be sent, and what data they contain.

Final Load Tester Program

Create a directory for the load tester:

mkdir -p src/$PKG/hellobender

Then create a file named main.go in that directory and add these lines to it:

package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"os"
	"time"

	"github.com/insomniacslk/dhcp/dhcpv6"
	"github.com/insomniacslk/dhcp/dhcpv6/async"
	"github.com/insomniacslk/dhcp/iana"
	"github.com/pinterest/bender"
	bdhcpv6 "github.com/pinterest/bender/dhcpv6"
	"github.com/pinterest/bender/hist"
)

// HardwareAddr should return a random hardware address (we'll keep it simple)
func HardwareAddr() (net.HardwareAddr, error) {
	return net.ParseMAC("01:23:45:67:89:ab")
}

// SyntheticDHCPv6Requests generates up to n synthetic DHCPv6 requests
func SyntheticDHCPv6Requests(n int) chan interface{} {
	c := make(chan interface{}, n)
	go func() {
		for i := 0; i < n; i++ {
			// Generate "random" hardware address
			hw, err := HardwareAddr()
			if err != nil {
				log.Println("Error generating MAC:", err)
				continue
			}
			// Genereate solicit for the generated MAC address
			duid := dhcpv6.Duid{
				Type:          dhcpv6.DUID_LLT,
				HwType:        iana.HwTypeEthernet,
				Time:          dhcpv6.GetTime(),
				LinkLayerAddr: hw,
			}
			msg, err := dhcpv6.NewSolicitWithCID(duid)
			if err != nil {
				log.Println("Error generating SOLICIT:", err)
				continue
			}
			c <- msg
		}
		close(c)
	}()
	return c
}

func validator(req, res dhcpv6.DHCPv6) error {
	return nil
}

var target = flag.String("target", "", "target DHCPv6 server")

func main() {
	flag.Parse()
	if *target == "" {
		log.Fatalln("You must specify target")
	}

	// Create client
	addr, err := net.ResolveUDPAddr("udp6", net.JoinHostPort(*target, "547"))
	if err != nil {
		log.Fatalln("Error resolving UDP:", err)
	}
	// We need to manually set the local address since we're using Relay port
	ip, err := getGlobalAddr("eth0")
	if err != nil {
		log.Fatalln("Error getting address:", err)
	}
	client := async.NewClient()
	client.LocalAddr = &net.UDPAddr{IP: *ip, Port: dhcpv6.DefaultServerPort, Zone: ""}
	client.RemoteAddr = addr

	err = client.Open(100)
	if err != nil {
		log.Fatalln("Error opening client:", err)
	}
	defer client.Close()

	// Subscribe to client errors channel
	go func() {
		for err := range client.Errors() {
			log.Println("Client error:", err)
		}
	}()

	// Load test
	intervals := bender.ExponentialIntervalGenerator(10)
	requests := SyntheticDHCPv6Requests(100)
	exec := bdhcpv6.CreateExecutor(client, validator)
	recorder := make(chan interface{}, 100)

	bender.LoadTestThroughput(intervals, requests, exec, recorder)

	l := log.New(os.Stdout, "", log.LstdFlags)
	h := hist.NewHistogram(60000, int(time.Millisecond))
	bender.Record(recorder, bender.NewLoggingRecorder(l), bender.NewHistogramRecorder(h))
	fmt.Println(h)
}

// getGlobalAddr finds a global IPv6 address for the given interface
func getGlobalAddr(ifname string) (*net.IP, error) {
	ifaces, err := net.Interfaces()
	if err != nil {
		return nil, err
	}
	for _, iface := range ifaces {
		if iface.Name != ifname {
			continue
		}
		ifaddrs, err := iface.Addrs()
		if err != nil {
			return nil, err
		}
		for _, ifaddr := range ifaddrs {
			if ifaddr, ok := ifaddr.(*net.IPNet); ok {
				if ifaddr.IP.To4() == nil && !ifaddr.IP.IsLinkLocalUnicast() {
					return &ifaddr.IP, nil
				}
			}
		}
	}
	return nil, fmt.Errorf("No global address found for interface %v", ifname)
}

Run the Load Tester

Run these commands to compile and install the load tester:

go install $PKG/hellobender

Because both server and load tester must run on port 547 it is not possible to run the test end the server on the same machine. We will assume that your server is running on machine example.com. Client takes ownership of port 547 so we need root access to run it:

sudo ./bin/hellobender -target example.com

You should see a long sequence of outputs and a final print out of the histogram data.