scrimplb

package module
v0.0.0-...-c9760b3 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2020 License: MIT Imports: 23 Imported by: 0

README

Scrimp LB

Motivation

For a production workload in the cloud, we'd naturally gravitate towards cloud solutions such as the ELB in AWS, or DigitalOcean's Load Balancer.

Those are great at what they do, and for the vast majority of prod workloads they're probably the right way to go. Unfortunately, they're quite expensive for a pet project or for a hobby.

At the time of writing (and, importantly, ignoring data transfer costs):

  • AWS CLB costs ~$20 a month
  • AWS ALB/NLB costs ~$18 a month
  • DigitalOcean Load Balancers cost ~$10 a month

Compare that to running cheap instances:

  • AWS t3.nano instances start at ~$4.1 a month, or ~$1.2 if using spot instances
  • DigitalOcean droplets start at $5 a month

For low-traffic* deployments it can be economically sensible to run a "load balancer" on an instance and avoid the cost of a more traditional load balancer. On AWS you might even have a NAT instance lying around which can be given a second job.

At higher levels of traffic, the economic reasoning changes and you might end up paying a fair amount in data transfer fees versus a cloud load balancer. Plus, running on an instance is basically always going to be a pain compared to running a managed service.

Another thing to bear in mind is that for a side project, the network and cpu utilization per instance will usually not be high; that is, it seems safe to say most personal projects or toys aren't serving a huge amount of traffic from every instance, and that traffic will generally not be highly intensive. As such, we can take a little CPU and network for maintaining our load-balancing topology quite safely - but that's not to ignore that this is a cost which we must pay.

Design

A public-facing "load balancer" sits in front of privately-networked "backend" instances, each of which which run at least one application.

All instances are networked together in a gossip cluster. Membership of the cluster implies that an instance is either a load balancer or a backend server, although the load balancer might not route to to the backend server without first having confirmed of what applications are provided by that server via a broadcast exchange.

Ideally, a load balancer is provisioned first (which could be a NAT instance in an AWS VPC for example, or a cheap VPS generally). The load balancer's IP is pushed into some "seed source" (e.g. S3 - but it's easy to write new seed provisioners).

The load balancers regularly - and with jitter - push their IPs into the seed source they're configured with. This is always safe with one load balancer, and with multiple load balancers we accept the risk of overwriting another load balancer's write since ultimately backend clients only need one IP into order to bootstrap into the cluster.

When a backend instance is brought up, a seed provisioner fetches the load balancer's IP from the source, and joins the cluster using the fetched IP.

After joining, a backend server must announce its supported applications to at least one load balancer, which can then share amongst other load balancers as needed. If the load balancer supports the application type, it adds the backend to its list of upstreams (think e.g. how nginx does load balancing) and soft-reloads itself.

Components

Load Balancer

On creation:

  • If no provider: Create cluster
  • If provider: Join cluster
  • In any case:
    • Start listening for cluster events (join/leave/suspect)

Regularly:

  • Publish IP for providers

On cluster join event:

  • Check if the new instance (via Node metadata) is a load balancer or a backend
    • If the new member is a load balancer: ignore it
    • If the new member is a backend: add it to a list of registered backends along with its applications. Run load balancer config generation.

On cluster leave event:

  • If the member that left is a known backend: Remove from registered backends, run load balancer config generation.
  • If the member that left is a known load balancer: Ignore
  • If the member is not known (i.e. it hasn't responded to queries by this load balancer about its application support): Stop querying it
Backend

On creation:

  • If no provider: Create cluster
  • If provider: Join cluster
  • In any case:
    • Start listening for requests for details from load balancers

On request for load balancer application:

  • Respond with JSON detailing applications on the backend

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddressesForApplication

func AddressesForApplication(upstreamMap map[Upstream][]Application, app Application) (addresses []string)

AddressesForApplication returns a string slice which details all backend addresses for the given application in an UpstreamApplicationMap.

Types

type Application

type Application struct {
	Name            string
	ListenPort      string
	ApplicationPort string
	Protocol        string
	// contains filtered or unexported fields
}

Application is a service running on a backend. A backend will respond with a list of Applications when queried by a load balancer.

func (*Application) DomainSlice

func (a *Application) DomainSlice() []string

DomainSlice returns domains as a []string

func (*Application) DomainString

func (a *Application) DomainString(sep string) string

DomainString returns the domain list as a string separated by `sep`

func (*Application) Equal

func (a *Application) Equal(other Application) bool

Equal implements an equality check for two Applications

type BackendConfig

type BackendConfig struct {
	Applications         []JSONApplication `json:"applications"`
	ApplicationConfigDir string            `json:"application-config-dir"`
}

BackendConfig describes configuration for backend instances

type BackendDelegate

type BackendDelegate struct {
	// contains filtered or unexported fields
}

BackendDelegate listens for messages from other cluster members requesting details about a backend.

func NewBackendDelegate

func NewBackendDelegate(config *BackendConfig) (*BackendDelegate, error)

NewBackendDelegate creates a BackendDelegate from a channel which receives work tasks

func (*BackendDelegate) GetBroadcasts

func (b *BackendDelegate) GetBroadcasts(overhead int, limit int) [][]byte

GetBroadcasts is ingored for BackendDelegate

func (*BackendDelegate) LocalState

func (b *BackendDelegate) LocalState(join bool) []byte

LocalState is ignored for a BackendDelegate

func (*BackendDelegate) MergeRemoteState

func (b *BackendDelegate) MergeRemoteState(buf []byte, join bool)

MergeRemoteState is ignored for BackendDelegate

func (*BackendDelegate) NodeMeta

func (b *BackendDelegate) NodeMeta(limit int) []byte

NodeMeta returns metadata about this backend, including a list of supported applications

func (*BackendDelegate) NotifyMsg

func (b *BackendDelegate) NotifyMsg(msg []byte)

NotifyMsg receives messages from other cluster members. If the message was intended for a backend, it is processed and a reply is scheduled if needed.

type BackendMetadata

type BackendMetadata struct {
	Type         string            `json:"type"`
	Applications []JSONApplication `json:"applications"`
}

BackendMetadata is returned by node metadata in the cluster, and describes supported applications on the backend.

type BackendResponder

type BackendResponder struct {
	Config *BackendConfig
}

BackendResponder responds to queries from load balancers about running applications

func NewBackendResponder

func NewBackendResponder(config *BackendConfig) *BackendResponder

NewBackendResponder creates a new BackendResponder from given config.

type DummyGenerator

type DummyGenerator struct {
}

DummyGenerator produces only dummy config.

func (DummyGenerator) GenerateConfig

func (d DummyGenerator) GenerateConfig(upstreamMap map[Upstream][]Application, config *ScrimpConfig) (string, error)

GenerateConfig returns a stable string and never an error

func (DummyGenerator) HandleRestart

func (d DummyGenerator) HandleRestart() error

HandleRestart returns no error and does nothing

type Generator

type Generator interface {
	GenerateConfig(map[Upstream][]Application, *ScrimpConfig) (string, error)
	HandleRestart() error
}

Generator provides an interface for generating configuration values based on backend configuration

type JSONApplication

type JSONApplication struct {
	Name            string   `json:"name"`
	ListenPort      string   `json:"listen-port"`
	ApplicationPort string   `json:"application-port"`
	Protocol        string   `json:"protocol"`
	Domains         []string `json:"domains"`
}

JSONApplication is a helper for loading applications with string slices for Domains

func (*JSONApplication) ToApplication

func (a *JSONApplication) ToApplication() Application

ToApplication turns a JSON loaded application into an application. This is needed to keep Applications comparable (since slices aren't) and therefore usable as map keys.

type LoadBalancerConfig

type LoadBalancerConfig struct {
	PushPeriodRaw        string `json:"push-period"`
	PushJitterRaw        string `json:"jitter"`
	GeneratorType        string `json:"generator"`
	GeneratorTarget      string `json:"generator-target"`
	GeneratorPrintStdout bool   `json:"generator-stdout"`
	TLSChainLocation     string `json:"tls-chain-location"`
	TLSKeyLocation       string `json:"tls-key-location"`
	Generator            Generator
	PushPeriod           time.Duration
	PushJitter           time.Duration
}

LoadBalancerConfig describes configuration options specific to load balancers.

type LoadBalancerDelegate

type LoadBalancerDelegate struct {
	// contains filtered or unexported fields
}

LoadBalancerDelegate listens for requests from backend instances for information and schedules replies

func NewLoadBalancerDelegate

func NewLoadBalancerDelegate(ch chan<- string) (*LoadBalancerDelegate, error)

NewLoadBalancerDelegate creates a LoadBalancerDelegate from a channel which is used to receive work tasks

func (*LoadBalancerDelegate) GetBroadcasts

func (d *LoadBalancerDelegate) GetBroadcasts(overhead int, limit int) [][]byte

GetBroadcasts is ignored for LoadBalancerDelegate

func (*LoadBalancerDelegate) LocalState

func (d *LoadBalancerDelegate) LocalState(join bool) []byte

LocalState is ignored for LoadBalancerDelegate

func (*LoadBalancerDelegate) MergeRemoteState

func (d *LoadBalancerDelegate) MergeRemoteState(buf []byte, join bool)

MergeRemoteState is ignored for LoadBalancerDelegate

func (*LoadBalancerDelegate) NodeMeta

func (d *LoadBalancerDelegate) NodeMeta(limit int) []byte

NodeMeta returns metadata about this node

func (*LoadBalancerDelegate) NotifyMsg

func (d *LoadBalancerDelegate) NotifyMsg(msg []byte)

NotifyMsg receives messages from other cluster members. If the message was intended for a Load Balancer, it is processed and a reply is scheduled if needed.

type LoadBalancerEventDelegate

type LoadBalancerEventDelegate struct {
	State                       LoadBalancerState
	UpstreamNotificationChannel chan<- *LoadBalancerState
}

LoadBalancerEventDelegate listens for events and updates load balancer state based on node metadata

func NewLoadBalancerEventDelegate

func NewLoadBalancerEventDelegate(notificationChannel chan<- *LoadBalancerState) LoadBalancerEventDelegate

NewLoadBalancerEventDelegate creates a new LoadBalancerEventDelegate

func (*LoadBalancerEventDelegate) NotifyJoin

func (d *LoadBalancerEventDelegate) NotifyJoin(node *memberlist.Node)

NotifyJoin adds new nodes to load balancer state

func (*LoadBalancerEventDelegate) NotifyLeave

func (d *LoadBalancerEventDelegate) NotifyLeave(node *memberlist.Node)

NotifyLeave removes existing nodes from load balancer state

func (*LoadBalancerEventDelegate) NotifyUpdate

func (d *LoadBalancerEventDelegate) NotifyUpdate(node *memberlist.Node)

NotifyUpdate updates existing nodes in load balancer state

type LoadBalancerState

type LoadBalancerState struct {
	MemberMap map[Upstream][]Application
	// contains filtered or unexported fields
}

LoadBalancerState provides state which is maintained by a load balancer relating to the nodes in the cluster that it might forward on to.

func NewLoadBalancerState

func NewLoadBalancerState() LoadBalancerState

NewLoadBalancerState creates a load balancer state

type NginxGenerator

type NginxGenerator struct {
}

NginxGenerator produces nginx upstream blocks for use for by an nginx load balancer

func (NginxGenerator) GenerateConfig

func (n NginxGenerator) GenerateConfig(upstreamMap map[Upstream][]Application, config *ScrimpConfig) (string, error)

GenerateConfig returns nginx upstream config for the given UpstreamApplicationMap

func (NginxGenerator) HandleRestart

func (n NginxGenerator) HandleRestart() error

HandleRestart assumes we're running on a systemd system and that we have access via sudo to restart nginx

type PushTask

type PushTask struct {
	// contains filtered or unexported fields
}

PushTask runs a Pusher on a regular, config-defined basis

func NewPushTask

func NewPushTask(config *ScrimpConfig) *PushTask

NewPushTask creates a new PushTask with the given config

func (*PushTask) Loop

func (p *PushTask) Loop()

Loop should be called in/as a goroutine and will regularly push state

type ScrimpConfig

type ScrimpConfig struct {
	IsLoadBalancer     bool                   `json:"lb"`
	BindAddress        string                 `json:"bind-address"`
	PortRaw            string                 `json:"port"`
	ProviderName       string                 `json:"provider"`
	ProviderConfig     map[string]interface{} `json:"provider-config"`
	ResolverName       string                 `json:"resolver"`
	LoadBalancerConfig *LoadBalancerConfig    `json:"load-balancer-config"`
	BackendConfig      *BackendConfig         `json:"backend-config"`
	Port               int
	Provider           seed.Provider
	Resolver           resolver.IPResolver
}

ScrimpConfig describes JSON configuration options for Scrimp overall.

func LoadScrimpConfig

func LoadScrimpConfig(configFile string) (*ScrimpConfig, error)

LoadScrimpConfig loads the given config file and parses fields which need to be parsed

type Upstream

type Upstream struct {
	Name    string
	Address string
}

Upstream is a condensed version of a backend with a name and an address to be routed towards.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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