• Stars
    star
    343
  • Rank 119,441 (Top 3 %)
  • Language
    Scala
  • License
    Apache License 2.0
  • Created over 12 years ago
  • Updated 7 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Ordasity is Boundary's library for building stateful clustered services on the JVM.

Ordasity

Table of Contents

  1. Overview, Use Cases, and Features
  2. A Clustered Service in 30 Seconds
  3. In Action at Boundary
  4. Distribution / Coordination Strategy
  5. Rebalancing
  6. Draining and Handoff
  7. Wrapping Up
  8. API Documentation

Building Stateful Clustered Services on the JVM

Ordasity is a library designed to make building and deploying reliable clustered services on the JVM as straightforward as possible. It's written in Scala and uses Zookeeper for coordination.

Ordasity's simplicity and flexibility allows us to quickly write, deploy, and (most importantly) operate distributed systems on the JVM without duplicating distributed "glue" code or revisiting complex reasoning about distribution strategies.


Primary Use Cases

Ordasity is designed to spread persistent or long-lived workloads across several machines. It's a toolkit for building systems which can be described in terms of individual nodes serving a partition or shard of a cluster's total load. Ordasity is not designed to express a "token range" (though it may be possible to implement one); the focus is on discrete work units.


Features

  • Cluster membership (joining / leaving / mutual awareness)
  • Work claiming and distribution
  • Load-based workload balancing
  • Count-based workload balancing
  • Automatic periodic rebalancing
  • Graceful cluster exiting ("draining")
  • Graceful handoff of work units between nodes
  • Pegging of work units to a specific node

A Clustered Service in 30 Seconds

Let's get started with an example. Here's how to build a clustered service in 25 lines of code with Ordasity:

    import com.yammer.metrics.scala.Meter
    import com.twitter.common.zookeeper.ZooKeeperClient
    import com.boundary.ordasity.{Cluster, ClusterConfig, SmartListener}

    class MyService {
      val listener = new SmartListener {
		
		// Called after successfully joining the cluster.
        def onJoin(client: ZooKeeperClient) { } 

        // Do yer thang, mark that meter.
        def startWork(workUnit: String, meter: Meter) { }

        // Stop doin' that thang.
        def shutdownWork(workUnit: String) { }

		// Called after leaving the cluster.
        def onLeave() { }
      }

      val config = ClusterConfig.builder().setHosts("localhost:2181").build()
      val cluster = new Cluster("ServiceName", listener, config)

      cluster.join()
    }

Maven folks and friends with compatible packaging systems, here's the info for your pom.xml:

        <!-- Dependency -->
        <dependency>
            <groupId>com.boundary</groupId>
            <artifactId>ordasity-scala_2.9.1</artifactId>
            <version>0.4.5</version>
        </dependency>

        <!-- Repo -->
        <repository>
            <id>boundary-public</id>
            <name>Boundary Public</name>
            <url>http://maven.boundary.com/artifactory/external</url>
        </repository>

In Action at Boundary

At Boundary, the library holds together our pubsub and event stream processing systems. It's a critical part of ensuring that at any moment, we're consuming and aggregating data from our network of collectors at one tier, and processing this data at hundreds of megabits a second in another. Ordasity also helps keep track of the mappings between these services, wiring everything together for us behind the scenes.

Ordasity's distribution enables us to spread the work of our pubsub aggregation and event stream processing systems across any number of nodes. Automatic load balancing keeps the cluster's workload evenly distributed, with nodes handing off work to others as workload changes. Graceful draining and handoff allows us to iterate rapidly on these systems, continously deploying updates without disrupting operation of the cluster. Ordasity's membership and work claiming approach ensures transparent failover within a couple seconds if a node becomes unavailable due to a network partition or system failure.


Distribution / Coordination Strategy

Ordasity's architecture is masterless, relying on Zookeeper only for coordination between individual nodes. The service is designed around the principle that many nodes acting together under a common set of rules can cooperatively form a self-organizing, self-regulating system.

Ordasity supports two work claiming strategies: "simple" (count-based), and "smart" (load-based).

Count-Based Distribution

The count-based distribution strategy is simple. When in effect, each node in the cluster will attempt to claim its fair share of available work units according to the following formula:

      val maxToClaim = {
        if (allWorkUnits.size <= 1) allWorkUnits.size
        else (allWorkUnits.size / nodeCount.toDouble).ceil
      }

If zero or one work units are present, the node will attempt to claim up to one work unit. Otherwise, the node will attempt to claim up to the number of work units divided by the number of active nodes.

Load-Based Distribution

Ordasity's load-based distribution strategy assumes that all work units are not equal. It's unlikely that balancing simply by count will result in an even load distribution -- some nodes would probably end up much busier than others. The load-based strategy is smarter. It divides up work based on the amount of actual "work" done.

Meters Measure Load

When you enable smart balancing and initialize Ordasity with a SmartListener, you get back a "meter" to mark when work occurs. Here's a simple, contrived example:

    val listener = new SmartListener {
      ...
      def startWork(workUnit: String, meter: Meter) = {

        val somethingOrOther = new Runnable() {
          def run() {
            while (true) {
              val processingAmount = process(workUnit)
              meter.mark(processingAmount)
              Thread.sleep(100)
            }
          }
        }

        new Thread(somethingOrOther).start()
      }
  
      ...
    }

Ordasity uses this meter to determine how much "work" each work unit in the cluster represents. If the application were a database or frontend to a data service, you might mark the meter each time a query is performed. In a messaging system, you'd mark it each time a message is sent or received. In an event stream processing system, you'd mark it each time an event is processed. You get the idea.

(Bonus: Each of these meters expose their metrics via JMX, providing you and your operations team with insight into what's happening when your service is in production).

Knowing the Load Lets us Balance

Ordasity checks the meters once per minute (configurable) and updates this information in Zookeeper. The "load map" determines the actual load represented by each work unit. All nodes watch the cluster's "load map" and are notified via Zookeeper's Atomic Broadcast mechanism when this changes. Each node in the cluster will attempt to claim its fair share of available work units according to the following formula:

    def evenDistribution() : Double = {
      loadMap.values.sum / activeNodeSize().toDouble
    }

As the number of nodes or the load of individual work units change, each node's idea of an "even distribution" changes as well. Using this "even distribution" value, each node will choose to claim additional work, or in the event of a rebalance, drain its workload to other nodes if it's processing more than its fair share.


Rebalancing

Ordasity supports automatic and manual rebalancing to even out the cluster's load distribution as workloads change.

To trigger a manual rebalance on all nodes, touch "/service-name/meta/rebalance" in Zookeeper. However, automatic rebalancing is preferred. To enable it, just turn it on in your cluster config:

    val config = ClusterConfig.builder().
      setHosts("localhost:2181").
      setAutoRebalance(true).
      setRebalanceInterval(60 * 60).build() // One hour

As a masterless service, the rebalance process is handled uncoordinated by the node itself. The rebalancing logic is very simple. If a node has more than its fair share of work when a rebalance is triggered, it will drain or release this work to other nodes in the cluster. As the cluster sees this work become available, lighter-loaded nodes will claim it (or receive handoff) and begin processing.

If you're using count-based distribution, it looks like this:

    def simpleRebalance() {
      val target = fairShare()

      if (myWorkUnits.size > target) {
        log.info("Simple Rebalance triggered. Load: %s. Target: %s.format(myWorkUnits.size, target))
        drainToCount(target)
      }
    }

If you're using load-based distribution, it looks like this:

    def smartRebalance() {
      val target = evenDistribution()

      if (myLoad() > target) {
        log.info("Smart Rebalance triggered. Load: %s. Target: %s".format(myLoad(), target))
        drainToLoad(target.longValue)
      }
    }

Draining and Handoff

To avoid dumping a bucket of work on an already-loaded cluster at once, Ordasity supports "draining." Draining is a process by which a node can gradually release work to other nodes in the cluster. In addition to draining, Ordasity also supports graceful handoff, allowing for a period of overlap during which a new node can begin serving a work unit before the previous owner shuts it down.

Draining

Ordasity's work claiming strategies (count-based and load-based) have internal counterparts for releasing work: drainToLoad and drainToCount.

The drainToCount and drainToLoad strategies invoked by a rebalance will release work units until the node's load is just greater than its fair share. That is to say, each node is "generous" in that it will strive to maintain slightly greater than a mathematically even distribution of work to guard against a scenario where work units are caught in a cycle of being claimed, released, and reclaimed continually. (Similarly, both claiming strategies will attempt to claim one unit beyond their fair share to avoid a scenario in which a work unit is claimed by no one).

Ordasity allows you to configure the period of time for a drain to complete:

    val config = ClusterConfig.builder().setHosts("localhost:2181").setDrainTime(60).build() // 60 Seconds

When a drain is initiated, Ordasity will pace the release of work units over the time specified. If 15 work units were to be released over a 60-second period, the library would release one every four seconds.

Whether you're using count-based or load-based distribution, the drain process is the same. Ordasity makes a list of work units to unclaim, then paces their release over the configured drain time.

Draining is especially useful for scheduled maintenance and deploys. Ordasity exposes a "shutdown" method via JMX. When invoked, the node will set its status to "Draining," cease claiming new work, and release all existing work to other nodes in the cluster over the configured interval before exiting the cluster.

Handoff

When Handoff is enabled, Ordasity will allow another node to begin processing for a work unit before the former owner shuts it down. This eliminates the very brief gap between one node releasing and another node claiming a work unit. Handoff ensures that at any point, a work unit is being served.

To enable it, just turn it on in your ClusterConfig:

    val clusterConfig = ClusterConfig.builder().
      setHosts("localhost:2181").
      setUseSoftHandoff(true).
      setHandoffShutdownDelay(10).build() // Seconds

The handoff process is fairly straightforward. When a node has decided to release a work unit (either due to a rebalance or because it is being drained for shutdown), it creates an entry in Zookeeper at /service-name/handoff-requests. Following their count-based or load-based claiming policies, other nodes will claim the work being handed off by creating an entry at /service-name/handoff-results.

When a node has successfully accepted handoff by creating this entry, the new owner will begin work. The successful "handoff-results" entry signals to the original owner that handoff has occurred and that it is free to cease processing after a configurable overlap (default: 10 seconds). After this time, Ordasity will call the "shutdownWork" method on your listener.


Registering work units

Work units are registered by creating ZooKeeper nodes under /work-units. (If you have set Cluster.workUnitName to a custom value then this ZooKeeper path will change accordingly.)

The name of the work unit is the same as the name of the ZooKeeper node. So, for example to create 3 work units called "a", "b", and "c", your ZK directory should look like this:

/work-units
    /a
    /b
    /c

Any String that is a valid ZK node name can be used as a work unit name. This is the string that is passed to your ClusterListener methods.

The ZK node data must be a JSON-encoded Map[String, String]. This may be simply an empty map ({}), or you may want to include information about the work unit, for use by your cluster nodes.

Note that Ordasity does not pass the ZK node data to your ClusterListener, so you will have to retrieve it yourself using the ZK client. It also does not provide a helper to deserialize the JSON string.

Pegging

The ZK node data can also be used for pegging work units to specific nodes.

To do this, include a key-value pair of the form "servicename": "nodeId" in the JSON map.

Here servicename is the name of the cluster, as specified in Cluster's constructor, and nodeId is the unique ID of a node, as set in ClusterConfig.

For example to peg a work unit to Node node123 in cluster mycluster, set the ZK node's data to {"mycluster": "node123"}.


Wrapping Up

So, that's Ordasity! We hope you enjoy using it to build reliable distributed services quickly.

Questions

If you have any questions, please feel free to shoot us an e-mail or get in touch on Twitter.

Bug Reports and Contributions

Think you've found a bug? Sorry about that. Please open an issue on GitHub and we'll check it out as soon as possible.

Want to contribute to Ordasity? Awesome! Fork the repo, make your changes, and issue a pull request. Please make effort to keep commits small, clean, and confined to specific changes. If you'd like to propose a new feature, give us a heads-up by getting in touch beforehand. We'd like to talk with you.

More Repositories

1

folsom

Expose Erlang Events and Metrics
Erlang
587
star
2

flake

A decentralized, k-ordered id generation service in Erlang
Erlang
573
star
3

high-scale-lib

A fork of Cliff Click's High Scale Library. Improved with bug fixes and a real build system.
Java
408
star
4

scalang

Scalang is a scala wrapper that makes it easy to write services that interface with erlang.
Scala
203
star
5

wireshark

wireshark + boundary IPFIX decode patches
C
165
star
6

fasttuple

Java
142
star
7

firespray

Blazingly fast streaming charts
JavaScript
106
star
8

html5-node-diagram

JavaScript
86
star
9

bear

a set of statistics functions for erlang
Erlang
68
star
10

overlock

Boundary's suite of concurrent scala utilities.
Scala
67
star
11

gen_lb

A generic library to load balance communication between Erlang nodes
Erlang
64
star
12

zoocreeper

A ZooKeeper backup tool.
Java
49
star
13

folsom_cowboy

A Cowboy based Folsom HTTP Wrapper.
Erlang
39
star
14

libdnet

updated fork of libdnet from https://code.google.com/p/libdnet/
C
39
star
15

folsom_webmachine

folsom based metrics via HTTP and JSON
Erlang
37
star
16

small_wonder

A Deployment Tool
Ruby
25
star
17

knife-plugins

Ruby
21
star
18

khial

a fake network driver to test network applications
C
19
star
19

atomicmap_challenge

A multithreaded scala programming challenge.
Scala
16
star
20

winpcap-installer

fork of the NMAP's silent WinPCAP installer
NSIS
13
star
21

labs-graphs

JavaScript
13
star
22

boundary_scripts

Shell
12
star
23

archaius-consul

A consul-backed configuration source for Archaius
Java
10
star
24

childspec_validator

Verify your Erlang childspecs before you use them.
Erlang
7
star
25

bprobe_cookbook

The Boundary bprobe cookbook.
Ruby
7
star
26

pulse-api-cli

Command line tools for Boundary APIs
Python
6
star
27

chef-boundary-annotations-handler

Chef exception handler for adding annotations to Boundary.
Ruby
5
star
28

boundary-event-plugins

Python
4
star
29

meter-plugin-rabbitmq

Metrics Plugin for RabbitMQ
Java
4
star
30

dw-consul

Tools for integrating consul with dropwizard applications.
Java
4
star
31

dropwizard-kafka

Kotlin
3
star
32

meter-plugin-sdk-lua

This is the framework for making the creation and maintenance of plugins easy.
Lua
3
star
33

boundary_splunk_app

A Splunk App for integration with Boundary
Python
3
star
34

meter-plugin-sdk-java

Java framework for creating plugins
Java
3
star
35

boundary-client-js

JavaScript
3
star
36

ibrowse

Erlang
3
star
37

boundary-vmware

Boundary Enterprise Integration with VMWare
Java
3
star
38

boundary-plugin-disk-summary

Disk Summary Plugin
Lua
2
star
39

boundary-plugin-lua-test

Repo for testing the LUA plugin framework
Lua
2
star
40

jenkins-boundary-annotations-plugin

Java
2
star
41

boundary_puppet

Boundary Meter puppet module
Puppet
2
star
42

java-streaming-client-example

A Java WebSocket client example for the Boundary Streaming API
Java
2
star
43

boundary-vagrant-snmp

Environment for testing SNMP integrations and plugins
Puppet
2
star
44

meter-plugin-zookeeper

Lua
2
star
45

chef-boundary-events-handler

Ruby
2
star
46

meter-plugin-docker

Lua
2
star
47

dataCommander

Data manager using Backbone: query, cache, merge
JavaScript
2
star
48

boundary-meter_cookbook

Boundary Meter Chef Cookbook
Ruby
2
star
49

boundary-plugin-aws-elb

Plugin that extracts metrics from AWS Cloud Watch on ELBs(Elastic Load Balancers)
Python
2
star
50

boundary-event-sdk

Boundary Event SDK
Java
2
star
51

meter-plugin-developer-guide

Documentation on how to development and deploy Boundary metric plugins
Makefile
2
star
52

boundary-plugin-aws-rds

Collects metrics from the Amazon Relational Database Service (RDS)
Python
2
star
53

meter-plugin-nginx-plus

This plugin is for Nginx+ Users to Monitor Nginx
Lua
2
star
54

jenkins-boundary-event-plugin

Java
1
star
55

boundary-plugin-syslog

Boundary Plugin for Syslog
1
star
56

meter-plugin-activemq

Collect metrics from an active MQ instance
Lua
1
star
57

meter-plugin-snmp

Boundary Plugin for SNMP
1
star
58

boundary-plugin-consul-healthchecks

Queries Consul health checks and returns events to Boundary on a status change of a given health check
Lua
1
star
59

meter-plugin-cassandra

Collects metrics from a cassandra instance
Java
1
star
60

boundary-plugin-diskrw_summary

Lua
1
star
61

webhook-action-example

Provides an example of the handling of the webhook call from a Boundary action
1
star
62

vagrant-plugin-dev-env

Vagrant environment for developing plugins
Shell
1
star
63

meter-plugin-wmi

Lua
1
star
64

ZenPacks.boundary.EventAdapter

Zenoss 4.x event adapter for Boundary
Python
1
star
65

meter-plugin-apache-tomcat

Java
1
star
66

libstatsite

C
1
star
67

meter-plugin-solr

Lua
1
star
68

plugin-flask

boundary meter flask plugin
Lua
1
star
69

boundary-plugin-elasticsearch

Collects metrics from a elasticsearch
Lua
1
star
70

boundary-plugin-riak

Plugin for extracting metrics from a Riak instance.
Lua
1
star
71

boundary-plugin-aws-redshift

Collects metrics from Amazon Redshift
Python
1
star
72

meter-plugin-windows-process

PowerShell
1
star
73

tsi-lab

Virtual machine for examples on getting data into TrueSight Intelligence
Python
1
star
74

boundary-action-handler

Endpoint to handle actions from Boundary Premimum
Java
1
star
75

meter-plugin-hello-world-lua

Example plugin in "Hello World" fashion.
Lua
1
star
76

meter-plugin-http

Meter plugin to measure http request page load
Python
1
star