Git Product home page Git Product logo

smudge's Introduction

Smudge

GoDoc Build Status Go Report Card Maintainability

Introduction

Smudge is a minimalist Go implementation of the SWIM (Scalable Weakly-consistent Infection-style Membership) protocol for cluster node membership, status dissemination, and failure detection developed at Cornell University by Motivala, et al. It isn't a distributed data store in its own right, but rather a framework intended to facilitate the construction of such systems.

Smudge also extends the standard SWIM protocol so that in addition to the standard membership status functionality it also allows the transmission of broadcasts containing a small amount (256 bytes) of arbitrary content to all present healthy members. This maximum is related to the limit imposed on maximum safe UDP packet size by RFC 791 and RFC 2460. We recognize that some systems allow larger packets, however, and although that can risk fragmentation and dropped packets the maximum payload size is configurable.

Smudge was conceived with space-sensitive systems (mobile, IoT, containers) in mind, and therefore was developed with a minimalist philosophy of doing a few things well. As such, its feature set is relatively small and mostly limited to functionality around adding and removing nodes and detecting status changes on the cluster.

Complete documentation is available from the associated Godoc.

Features

  • Uses gossip (i.e., epidemic) protocol for dissemination, the latency of which grows logarithmically with the number of members.
  • Low-bandwidth UDP-based failure detection and status dissemination.
  • Imposes a constant message load per group member, regardless of the number of members.
  • Member status changes are eventually detected by all non-faulty members of the cluster (strong completeness).
  • Supports transmission of short broadcasts that are propagated at most once to all present, healthy members.
  • Supports both IPv4 and IPv6.
  • Pluggable logging

Known issues

  • Broadcasts are limited to 256 bytes, or 512 bytes when using IPv6.
  • No WAN support: only local-network, private IPs are supported.

Deviations from Motivala, et al

  • Dead nodes are not immediately removed, but are instead periodically re-tried (with exponential backoff) for a time before finally being removed.
  • Smudge allows the transmission of short, arbitrary-content broadcasts to all healthy nodes.

How broadcasts work

TL;DR a broadcast can be added to the local node by either calling a function or by receiving it from a remote node. A broadcast is send to other nodes a couple of times, piggybacked on membership messages. Then after a while the broadcast is removed from the node.

Emit counter

The emit counter represents the number of times a broadcast message must be send to other nodes. An emit counter is calculated with the following formula: int(2.5 * log(number of nodes) + 0.5). The larger the network the higher the emit counter will be, but the larger the network the slower the emit counter will grow.

Examples:

  • 2 nodes: int(2.5 * log(2) + 0.5) = 2
  • 10 nodes: int(2.5 * log(10) + 0.5) = 6
  • 20 nodes: int(2.5 * log(20) + 0.5) = 8

Broadcasts

When a broadcast is added to Smudge, either because it is added locally (by calling a function of the library) or is received from a remote node, an emit counter initialized with the formula above. The emit counter and the broadcast are then saved to a local buffer.

The emit counter is used to track how many times a broadcast must be send to other nodes in the network. When the emit counter gets below a certain, large negative, thresh-hold the broadcast is removed from the buffer. Only broadcasts with a positive emit counter will be send when they are selected.

When Smudge is about to send a membership message it looks for the broadcast with the largest emit counter. If multiple broadcasts have the same emit counter value, one is arbitrarily chosen. The selected broadcast can have a negative emit counter. If the emit counter is larger then 0 Smudge adds that broadcast to the membership message that will be send. In any case the emit counter is lowered by 1.

When a broadcast is received from another node and that broadcast is already in the buffer it will be ignored. To achieve this the origin IP of the node that added the broadcast to the network is saved as part of the broadcast.

How to build

Although Smudge is intended to be directly extended, a Dockerfile is provided for testing and proofs-of-function.

The Dockerfile uses a multi-stage build, so Docker 17.05 or higher is required. The build compiles the code in a dedicated Golang container and drops the resulting binary into a scratch image for execution. This makes a Makefile or build.sh largely superfluous and removed the need to configure a local environment.

Running the tests

To run the tests in a containerized environment, which requires only that you have Docker installed (not Go), you can do:

make test

Or, if you'd rather not use a Makefile:

go test -v github.com/clockworksoul/smudge

Building the Docker image

To execute the build, you simply need to do the following:

make image

Or, if you'd rather not use a Makefile:

docker build -t clockworksoul/smudge:latest .

Testing the Docker image

You can test Smudge locally using the Docker image. First create a network to use for your Smudge nodes and then add some nodes to the network.

For IPv4 you can use the following commands:

docker network create smudge
docker run -i -t --network smudge --rm clockworksoul/smudge:latest /smudge
# you can add nodes with the following command
docker run -i -t --network smudge --rm clockworksoul/smudge:latest /smudge -node 172.20.0.2

To try out Smudge with IPv6 you can use the following commands:

docker network create --ipv6 --subnet fd02:6b8:b010:9020:1::/80 smudge6
docker run -i -t --network smudge6 --rm clockworksoul/smudge:latest /smudge
# you can add nodes with the following command
docker run -i -t --network smudge6 --rm clockworksoul/smudge:latest /smudge -node [fd02:6b8:b010:9020:1::2]:9999

Building the binary with the Go compiler

Set up your Golang environment

If you already have a $GOPATH set up, you can skip to the following section.

First, you'll need to decide where your Go code and binaries will live. This will be your Gopath. You simply need to export this as GOPATH:

export GOPATH=~/go/

Change it to whatever works for you. You'll want to add this to your .bashrc or .bash_profile.

Clone the repo into your GOPATH

Clone the code into $GOPATH/src/github.com/clockworksoul/smudge. Using the full-qualified path structure makes it possible to import the code into other libraries, as well as Smudge's own main() function.

git clone [email protected]:clockworksoul/smudge.git $GOPATH/src/github.com/clockworksoul/smudge

Execute your build

Once you have a $GOPATH already configured and the repository correctly cloned into $GOPATH/src/github.com/clockworksoul/smudge, you can execute the following:

make build

When using the Makefile, the compiled binary will be present in the /bin directory in the code directory.

If you'd rather not use a Makefile:

go build -a -installsuffix cgo -o smudge github.com/clockworksoul/smudge/smudge

The binary, compiled for your current environment, will be present in your present working directory.

How to use

To use the code, you simply specify a few configuration options (or use the defaults), create and add a node status change listener, and call the smudge.Begin() function.

Configuring the node with environment variables

Perhaps the simplest way of directing the behavior of the SWIM driver is by setting the appropriate system environment variables, which is useful when making use of Smudge inside of a container.

The following variables and their default values are as follows:

Variable                           | Default         | Description
---------------------------------- | --------------- | -------------------------------
SMUDGE_CLUSTER_NAME                |      smudge     | Cluster name for for multicast discovery
SMUDGE_HEARTBEAT_MILLIS            |       250       | Milliseconds between heartbeats
SMUDGE_INITIAL_HOSTS               |                 | Comma-delimmited list of known members as IP or IP:PORT
SMUDGE_LISTEN_PORT                 |       9999      | UDP port to listen on
SMUDGE_LISTEN_IP                   |    127.0.0.1    | IP address to listen on
SMUDGE_MAX_BROADCAST_BYTES         |       256       | Maximum byte length of broadcast payloads
SMUDGE_MULTICAST_ENABLED           |       true      | Multicast announce on startup; listen for multicast announcements
SMUDGE_MULTICAST_ANNOUNCE_INTERVAL |        0        | Seconds between multicast announcements, 0 will disable subsequent anouncements
SMUDGE_MULTICAST_ADDRESS           | See description | The multicast broadcast address. Default: `224.0.0.0` (IPv4) or `[ff02::1]` (IPv6)
SMUDGE_MULTICAST_PORT              |       9998      | The multicast listen port

Configuring the node with API calls

If you prefer to direct the behavior of the service using the API, the calls are relatively straight-forward. Note that setting the application properties using this method overrides the behavior of environment variables.

smudge.SetListenPort(9999)
smudge.SetHeartbeatMillis(250)
smudge.SetListenIP(net.ParseIP("127.0.0.1"))
smudge.SetMaxBroadcastBytes(256) // set to 512 when using IPv6

Creating and adding a status change listener

Creating a status change listener is very straight-forward:

type MyStatusListener struct {
    smudge.StatusListener
}

func (m MyStatusListener) OnChange(node *smudge.Node, status smudge.NodeStatus) {
    fmt.Printf("Node %s is now status %s\n", node.Address(), status)
}

func main() {
    smudge.AddStatusListener(MyStatusListener{})
}

Creating and adding a broadcast listener

Adding a broadcast listener is very similar to creating a status listener:

type MyBroadcastListener struct {
    smudge.BroadcastListener
}

func (m MyBroadcastListener) OnBroadcast(b *smudge.Broadcast) {
    fmt.Printf("Received broadcast from %v: %s\n",
        b.Origin().Address(),
        string(b.Bytes()))
}

func main() {
    smudge.AddBroadcastListener(MyBroadcastListener{})
}

Adding a new member to the "known nodes" list

Adding a new member to your known nodes list will also make that node aware of the adding server. To join an existing cluster without using multicast (or on a network where multicast is disabled) you must use this method to add at least one of that cluster's healthy member nodes.

node, err := smudge.CreateNodeByAddress("localhost:10000")
if err == nil {
    smudge.AddNode(node)
}

Starting the server

Once everything else is done, starting the server is trivial:

Simply call: smudge.Begin()

Transmitting a broadcast

To transmit a broadcast to all healthy nodes currenty in the cluster you can use one of the BroadcastBytes(bytes []byte) or BroadcastString(str string) functions.

Be aware of the following caveats:

  • Attempting to send a broadcast before the server has been started will cause a panic.
  • The broadcast will not be received by the originating member; BroadcastListeners on the originating member will not be triggered.
  • Nodes that join the cluster after the broadcast has been fully propagated will not receive the broadcast; nodes that join after the initial transmission but before complete proagation may or may not receive the broadcast.

Getting a list of nodes

The AllNodes() can be used to get all known nodes; HealthyNodes() works similarly, but returns only healthy nodes (defined as nodes with a status of "alive").

Everything in one place

package main

import "github.com/clockworksoul/smudge"
import "fmt"
import "net"

type MyStatusListener struct {
    smudge.StatusListener
}

func (m MyStatusListener) OnChange(node *smudge.Node, status smudge.NodeStatus) {
    fmt.Printf("Node %s is now status %s\n", node.Address(), status)
}

type MyBroadcastListener struct {
    smudge.BroadcastListener
}

func (m MyBroadcastListener) OnBroadcast(b *smudge.Broadcast) {
    fmt.Printf("Received broadcast from %s: %s\n",
        b.Origin().Address(),
        string(b.Bytes()))
}

func main() {
    heartbeatMillis := 500
    listenPort := 9999

    // Set configuration options
    smudge.SetListenPort(listenPort)
    smudge.SetHeartbeatMillis(heartbeatMillis)
    smudge.SetListenIP(net.ParseIP("127.0.0.1"))

    // Add the status listener
    smudge.AddStatusListener(MyStatusListener{})

    // Add the broadcast listener
    smudge.AddBroadcastListener(MyBroadcastListener{})

    // Add a new remote node. Currently, to join an existing cluster you must
    // add at least one of its healthy member nodes.
    node, err := smudge.CreateNodeByAddress("localhost:10000")
    if err == nil {
        smudge.AddNode(node)
    }

    // Start the server!
    smudge.Begin()
}

Bringing your own logger

Smudge comes with a DefaultLogger that writes log messages to stderr. You can plug in your own logger by implementing the functions of the Logger interface and setting the logger by calling smudge.SetLogger(MyCoolLogger).

smudge's People

Contributors

captaincodeman avatar clockworksoul avatar eelcocramer avatar elewis787 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

smudge's Issues

Race condition: data race

Hi again,

I was building my project with flag -race as someone suggested me.
Suddenly, I found that this warning occurred: https://pastebin.com/Nz7idxhZ

I barely use smudge, not even broadcasting, which you can see from my code: https://github.com/fe1t/blockchain_programming_in_golang/tree/fix_race

Most of the code are in server.go:ConfigServer func(), some other is only in cli.go for customizing logger.

I'm not sure if this will cause the problem.

If you could help me check this, I'll be appreciate and thankful.

Usage Query

Piece of code at the end of the README creates a listener at port 9999. My question is, where is this code located? On all member nodes? Or a separate server?

Thanks

Question: Future direction of this project

Currently, this project supports three means of configuring its behavior, none of which are as fleshed out as they could/should be:

  1. Using the Golang API proper
  2. Environment variables
  3. The CLI (which ostensibly exists as a proof-of-concept for the API)

I've been throwing around the idea developing a distinct Smudge server that can provide -- perhaps by running sidecar next to a larger service -- all Smudge capabilities, without the service needing to implement the Go API. The server could then communicate with the Smudge sidecar via some language-agnostic means

This leads me to a couple of questions:

First, is this worth even doing? Is there any use for this?

Second, I don't think it makes sense to support both CLI and environment variables for specifying variables, so I'm leaning towards dropping environment variable support entirely. Does anybody have any opinions about this?

Third, I think this would be best served by creating a separate server project that uses the API. Does anybody have any thoughts about this?

Thanks for your thoughts!

Description for OnBroadcast() not correct

smudge/events.go

Lines 34 to 38 in bd05cb5

type BroadcastListener interface {
// The OnChange() function is called whenever the node is notified of any
// change in the status of a cluster member.
OnBroadcast(broadcast *Broadcast)
}

smudge/events.go

Lines 60 to 64 in bd05cb5

type StatusListener interface {
// The OnChange() function is called whenever the node is notified of any
// change in the status of a cluster member.
OnChange(node *Node, status NodeStatus)
}

Both got the same descriptions which are OnChange().
Instead, the first one should be description of OnBroadcast().

Include node status change source in the gossip messages

The SWIM document (section 4.2, page 6) defines the messages as:

  • { Suspect M_j: M_i suspects M_j }
  • { Alive M_j: M_l knows M_j is alive }
  • { Confirm M_j: M_h declares M_j as faulty }

You are only passing the info of M_j (let's call it the target node) but you are not passing M_i/M_l/M_h (let's call it the source node). It may not seem to be needed but the source node is quite important. For example, when non-trustworthy nodes can be connected to the network. A mechanism for avoiding node impersonation would also be needed for this purpouse. Other examples of uses for the source address and port come to mind like prioritizing sending him an alive message so that the timeout doesn't proc, etc.

Announcing presence on IPv6 results in error

I'm getting the following error if I try announcing presence on an IPv6 network:

2018-04-23T15:11:21Z |INFO| Announcing presence on [ff02::1]:9998
2018-04-23T15:11:21Z |ERRO| dial udp [ff02::1]:9998: connect: invalid argument

The error occurs here.

I need to validate this issue as it may be caused by my IPv6 stack... I decided to create this issue for reference. It can be assigned to me...

Smudge vs serf/memberlist

This project looks similar to serf and memberlist from hashicorp, yours and serf are based on swim.

Pluggable logging

It would be nice if programs that integrate Smudge could bring their own logger.

I'm willing to create a PR for this. Hopefully somewhere in the coming weeks.

IPv6 support

Request for IPv6 support:

Some considerations:

  • IPv6 does not modify UDP except for the checksum. In both cases, the UDP checksum adds the bytes of the source and target addresses, which in IPv6 are 4 times longer, but I'm quite sure that this field is calculated by the underlying UDP library, so this should not have an impact in Smudge.
  • Transmitting IPv6 addresses inside the messages, however, does have an impact. There are two approaches to this. Use a flag that allows to distinguish IP versions and then use 4 or 16 bytes as necesary, or always use 16 bytes and embed the IPv4 in an IPv6 format (127.0.0.1 -> 7F.00.00.01 -> 0000:0000:0000:0000:0000:FFFF:7F00:0001 -> ::FFFF:7F00:1) what basically means using the last 4 bytes to store the IPv4 address and set all the bits of the 2 previous bytes. Golang's net.IP type will understand this addresses as IPv4.
  • IPv6 increases the minimum message size that every implementation needs to subbort to 1280 bytes - 8 bytes for the UDP header - 40 bytes for the IPv6 header leaves 1232 bytes for the IPv6 extension headers plus the application payload. In IPv4 the maximum IP header is of 60 bytes and the UDP header 8 extra ones what makes the 576 minimum accepted package turn into 508 for the application.

Typo or intentionally?

In the docker section of the readme there are two images referenced:

Testing the Docker image
You can test Smudge locally using the Docker image. First create a network to use for your Smudge nodes and then add some nodes to the network.

For IPv4 you can use the following commands:

docker network create smudge
docker run -i -t --network smudge --rm clockworksoul/smudge:latest /smudge
# you can add nodes with the following command
docker run -i -t --network smudge --rm clockworksould/smudge:latest /smudge -node 172.20.0.2
To try out Smudge with IPv6 you can use the following commands:

docker network create --ipv6 --subnet fd02:6b8:b010:9020:1::/80 smudge6
docker run -i -t --network smudge6 --rm clockworksoul/smudge:latest /smudge
# you can add nodes with the following command
docker run -i -t --network smudge6 --rm clockworksould/smudge:latest /smudge -node [fd02:6b8:b010:9020:1::2]:9999
Building the binary with the Go compiler

Thanks in advance.

Any configuration to work with Smudge locally ?

I normally connect to university network and get public IP.

When I'm using Smudge for transmitting data (1000-1200 bytes/data), sometimes broadcast messages are lost or very slow.

So I'm here for an advice. I need Smudge to run and broadcast messages only in my localhost.

Here are some of my code.

func ConfigServer() error {
	port, err := strconv.Atoi(NODE_ID)
	if err != nil {
		return err
	}

	// Set configuration options
	smudge.SetListenIP(net.ParseIP(baseAddress))
	smudge.SetListenPort(port)
	smudge.SetHeartbeatMillis(500)
	smudge.SetMaxBroadcastBytes(2000)
	smudge.SetLogThreshold(smudge.LogOff)
	smudge.SetMulticastEnabled(false)
	smudge.SetClusterName("KU-Coin")

	smudge.AddStatusListener(MyStatusListener{})
	smudge.AddBroadcastListener(MyBroadcastListener{})

	// Add a new remote node. Currently, to join an existing cluster you must
	// add at least one of its healthy member nodes.

	if nodeAddress != knownNodes[0] {
		node, err := smudge.CreateNodeByAddress(knownNodes[0])
		if err == nil {
			smudge.AddNode(node)
		} else {
			return err
		}
	}

	// start the server
	go func() {
		smudge.Begin()
	}()

	// Handle SIGINT and SIGTERM.
	// quit := make(chan os.Signal, 2)
	// signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	// <-quit
	return nil
}

I've set baseAddress to "127.0.0.1" but still got no luck.

Thanks in advance

Is this project still restricted to LAN?

I love this implementation and am currently in the process of writing C bindings for it. One question I have is this note in the documentation:

No WAN support: only local-network, private IPs are supported.

Is this still true, despite there existing the ability to enter an IP (which can very well be public?) If so, what's restricting this program from supporting a WAN?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.