Adds a "consul rtt" command.

This commit is contained in:
James Phillips 2015-10-16 00:03:16 -07:00
parent 787f946f8d
commit fb8900156a
5 changed files with 370 additions and 0 deletions

147
command/rtt.go Normal file
View File

@ -0,0 +1,147 @@
package command
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/serf/coordinate"
"github.com/mitchellh/cli"
)
// RttCommand is a Command implementation that allows users to query the
// estimated round trip time between nodes using network coordinates.
type RttCommand struct {
Ui cli.Ui
}
func (c *RttCommand) Help() string {
helpText := `
Usage: consul rtt [options] node1 node2
Estimates the round trip time between two nodes using Consul's network
coordinate model of the cluster.
By default, the two nodes are assumed to be nodes in the local datacenter
and the LAN coordinates are used. If the -wan option is given, then the WAN
coordinates are used, and the node names must be prefixed by the datacenter
and a period (eg. "dc1.sever").
It is not possible to measure between LAN coordinates and WAN coordinates
because they are maintained by independent Serf gossip pools, so they are
not compatible.
The two node names are required. Note that these are node names as known to
Consul as "consul members" would show, not IP addresses.
Options:
-wan Use WAN coordinates instead of LAN coordinates.
-http-addr=127.0.0.1:8500 HTTP address of the Consul agent.
`
return strings.TrimSpace(helpText)
}
func (c *RttCommand) Run(args []string) int {
var wan bool
cmdFlags := flag.NewFlagSet("rtt", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.BoolVar(&wan, "wan", false, "wan")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// They must provide a pair of nodes.
nodes := cmdFlags.Args()
if len(nodes) != 2 {
c.Ui.Error("Two node names must be specified")
c.Ui.Error("")
c.Ui.Error(c.Help())
return 1
}
// Create and test the HTTP client.
conf := api.DefaultConfig()
conf.Address = *httpAddr
client, err := api.NewClient(conf)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
coordClient := client.Coordinate()
var source string
var coord1, coord2 *coordinate.Coordinate
if wan {
// Parse the input nodes.
parts1 := strings.Split(nodes[0], ".")
parts2 := strings.Split(nodes[1], ".")
if len(parts1) != 2 || len(parts2) != 2 {
c.Ui.Error("Node names must be specified as <datacenter>.<node name> with -wan")
return 1
}
dc1, node1 := parts1[0], parts1[1]
dc2, node2 := parts2[0], parts2[1]
// Pull all the WAN coordinates.
dcs, err := coordClient.Datacenters()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting coordinates: %s", err))
return 1
}
// See if the requested nodes are in there.
for _, dc := range dcs {
for _, entry := range dc.Coordinates {
if dc.Datacenter == dc1 && entry.Node == node1 {
coord1 = entry.Coord
}
if dc.Datacenter == dc2 && entry.Node == node2 {
coord2 = entry.Coord
}
}
}
source = "WAN"
} else {
// Pull all the LAN coordinates.
entries, _, err := coordClient.Nodes(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting coordinates: %s", err))
return 1
}
// See if the requested nodes are in there.
for _, entry := range entries {
if entry.Node == nodes[0] {
coord1 = entry.Coord
}
if entry.Node == nodes[1] {
coord2 = entry.Coord
}
}
source = "LAN"
}
// Make sure we found both coordinates.
if coord1 == nil {
c.Ui.Error(fmt.Sprintf("Could not find a coordinate for node %q", nodes[0]))
return 1
}
if coord2 == nil {
c.Ui.Error(fmt.Sprintf("Could not find a coordinate for node %q", nodes[1]))
return 1
}
// Report the round trip time.
dist := coord1.DistanceTo(coord2).Seconds()
c.Ui.Output(fmt.Sprintf("Estimated %s <-> %s rtt=%.3f ms (using %s coordinates)", nodes[0], nodes[1], dist*1000.0, source))
return 0
}
func (c *RttCommand) Synopsis() string {
return "Estimates round trip times between nodes"
}

162
command/rtt_test.go Normal file
View File

@ -0,0 +1,162 @@
package command
import (
"fmt"
"strings"
"testing"
"time"
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/serf/coordinate"
"github.com/mitchellh/cli"
)
func TestRttCommand_Implements(t *testing.T) {
var _ cli.Command = &RttCommand{}
}
func TestRttCommand_Run_BadArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &RttCommand{Ui: ui}
if code := c.Run([]string{}); code != 1 {
t.Fatalf("expected return code 1, got %d", code)
}
if code := c.Run([]string{"node1"}); code != 1 {
t.Fatalf("expected return code 1, got %d", code)
}
if code := c.Run([]string{"node1", "node2", "node3"}); code != 1 {
t.Fatalf("expected return code 1, got %d", code)
}
if code := c.Run([]string{"-wan", "node1", "node2"}); code != 1 {
t.Fatalf("expected return code 1, got %d", code)
}
if code := c.Run([]string{"-wan", "dc1.node1", "node2"}); code != 1 {
t.Fatalf("expected return code 1, got %d", code)
}
if code := c.Run([]string{"-wan", "node1", "dc1.node2"}); code != 1 {
t.Fatalf("expected return code 1, got %d", code)
}
}
func TestRttCommand_Run_LAN(t *testing.T) {
updatePeriod := 10 * time.Millisecond
a := testAgentWithConfig(t, func(c *agent.Config) {
c.ConsulConfig.CoordinateUpdatePeriod = updatePeriod
})
defer a.Shutdown()
waitForLeader(t, a.httpAddr)
// Inject some known coordinates.
c1 := coordinate.NewCoordinate(coordinate.DefaultConfig())
c2 := c1.Clone()
c2.Vec[0] = 0.123
req1 := structs.CoordinateUpdateRequest{
Datacenter: a.config.Datacenter,
Node: a.config.NodeName,
Coord: c1,
}
var reply struct{}
if err := a.agent.RPC("Coordinate.Update", &req1, &reply); err != nil {
t.Fatalf("err: %s", err)
}
req2 := structs.CoordinateUpdateRequest{
Datacenter: a.config.Datacenter,
Node: "dogs",
Coord: c2,
}
if err := a.agent.RPC("Coordinate.Update", &req2, &reply); err != nil {
t.Fatalf("err: %s", err)
}
// Wait for the updates to get flushed to the data store.
time.Sleep(2 * updatePeriod)
ui := new(cli.MockUi)
c := &RttCommand{Ui: ui}
// Try two known nodes.
func() {
args := []string{
"-http-addr=" + a.httpAddr,
a.config.NodeName,
"dogs",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d: %#v", code, ui.ErrorWriter.String())
}
// Make sure the proper RTT was reported in the output.
dist_str := fmt.Sprintf("%.3f ms", c1.DistanceTo(c2).Seconds()*1000.0)
if !strings.Contains(ui.OutputWriter.String(), dist_str) {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}()
// Try an unknown node.
func() {
args := []string{
"-http-addr=" + a.httpAddr,
a.config.NodeName,
"nope",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("bad: %d: %#v", code, ui.ErrorWriter.String())
}
}()
}
func TestRttCommand_Run_WAN(t *testing.T) {
a := testAgent(t)
defer a.Shutdown()
waitForLeader(t, a.httpAddr)
ui := new(cli.MockUi)
c := &RttCommand{Ui: ui}
node := fmt.Sprintf("%s.%s", a.config.Datacenter, a.config.NodeName)
// We can't easily inject WAN coordinates, so we will just query the
// node with itself.
func() {
args := []string{
"-http-addr=" + a.httpAddr,
"-wan",
node,
node,
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d: %#v", code, ui.ErrorWriter.String())
}
// Make sure there was some kind of RTT reported in the output.
if !strings.Contains(ui.OutputWriter.String(), "rtt=") {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}()
// Try an unknown node.
func() {
args := []string{
"-http-addr=" + a.httpAddr,
"-wan",
node,
"dc1.nope",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("bad: %d: %#v", code, ui.ErrorWriter.String())
}
}()
}

View File

@ -114,6 +114,12 @@ func init() {
}, nil
},
"rtt": func() (cli.Command, error) {
return &command.RttCommand{
Ui: ui,
}, nil
},
"version": func() (cli.Command, error) {
ver := Version
rel := VersionPrerelease

View File

@ -39,6 +39,7 @@ Available commands are:
members Lists the members of a Consul cluster
monitor Stream logs from a Consul agent
reload Triggers the agent to reload configuration files
rtt Estimates round trip times between nodes
version Prints the Consul version
watch Watch for changes in Consul
```

View File

@ -0,0 +1,54 @@
---
layout: "docs"
page_title: "Commands: RTT"
sidebar_current: "docs-commands-rtt"
description: >
The `rtt` command estimates the netowrk round trip time between two nodes using Consul's network coordinate model of the cluster.
---
# Consul RTT
Command: `consul rtt`
The 'rtt' command estimates the network round trip time between two nodes using
Consul's network coordinate model of the cluster. While contacting nodes as part
of its normal gossip protocol, Consul builds up a set of network coordinates for
all the nodes in the local datacenter (the LAN pool) and remote datacenters (the WAN
pool). Agents forward these to the servers and once the coordinates for two nodes
are known, it's possible to estimate the network round trip time between them using
a simple calculation.
It is not possible to measure between LAN coordinates and WAN coordinates
because they are maintained by independent Serf gossip pools, so they are
not compatible.
## Usage
Usage: `consul rtt [options] node1 node2`
The two node names are required. Note that these are node names as known to
Consul as `consul members` would show, not IP addresses.
The list of available flags are:
* `-wan` - Instructs the command to use WAN coordinates instead of LAN
coordinates. If the -wan option is given, then the node names must be prefixed
by the datacenter and a period (eg. "dc1.sever"). By default, the two nodes are
assumed to be nodes in the local datacenter the LAN coordinates are used.
* `-http-addr` - Address to the HTTP server of the agent you want to contact
to send this command. If this isn't specified, the command will contact
"127.0.0.1:8500" which is the default HTTP address of a Consul agent.
## Output
If coordinates are available, the command will print the estimated round trip
time beteeen the given nodes:
```
$ consul rtt n1 n2
Estimated n1 <-> n2 rtt=0.610 ms (using LAN coordinates)
$ consul rtt -wan dc1.n1 dc2.n2
Estimated dc1.n1 <-> dc2.n2 rtt=1.275 ms (using WAN coordinates)
```