• Stars
    star
    115
  • Rank 305,916 (Top 7 %)
  • Language
    Elixir
  • Created over 7 years ago
  • Updated about 6 years ago

Reviews

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

Repository Details

A sketch of a microservice architecture in Elixir, using clustered VMs, docker-compose, Redis, HAProxy

About

This is a sketch of a microservice architecture using Elixir, Redis, HAProxy, and docker-compose. It showcases a kind of command/query responsibility separation, a load-balanced web API, and clustered queue-workers that are capable of message-passing amongst themselves.


Data Flow

For the purposes of this architectural skeleton, the data flow is like this:

  1. work is accepted via http POST on a web API
  2. work is pushed by API onto a queue (Redis)
  3. the web API returns unique callback id, where client may check work status
  4. pending work is popped from queue by elixir workers (separate docker instances)
  5. work is completed and result is written by worker to storage (also Redis), with a TTL
  6. (optional) given callback ID previously, client may retrieve completed work within TTL

Clustering

This architectural skeleton also features a lightweight, self-contained approach for automatic registration & clustering of the queue workers (Elixir nodes).

  1. Clustering means nodes may communicate by engaging in message passing, and even process spawning
  2. Self-contained means there is no external consul to configure, and no zookeeper to install, etc. Under the hood a plain docker Redis image is dropped into docker-compose.yml with no additional hackery.
  3. Lightweight means this registration mechanism is somewhat better than a toy, but by avoiding the complexity of something like libcluster we also lose the huge feature set. For better our worse, this approach has no hardcoded host/seed lists, no noisy UDP broadcasting, no kubernetes prerequisites, etc etc.

Some might (reasonably) object that any networking/message-passing amongst workers compromises the "purity" of the architecture, since part of the point of queue workers and command/query separation is leveraging a principle of isolation that implies workers should not need to communicate. That's true, but on the other hand,

  1. nothing is forcing workers to communicate
  2. individual queue-worker types often gradually morph into more significant services in their own right
  3. and besides, Erlang VM clustering is extremely interesting :)

One might alternatively view this worker-clustering impurity as a stepping stone to a lightweight "service mesh" since that term is in vogue lately, and marvel at how easy Elixir / Erlang's VM makes it to take those first steps.

To quote from the distributed task docs,

Elixir ships with facilities to connect nodes and exchange information between them. In fact, we use the same concepts of processes, message passing and receiving messages when working in a distributed environment because Elixir processes are location transparent. This means that when sending a message, it doesn’t matter if the recipient process is on the same node or on another node, the VM will be able to deliver the message in both cases.

Prequisites

You need to have docker and docker-compose already installed. An Elixir stack is not necessary on your dev host, rather, one will be provided and used via docker-compose.

Usage & Demo

Build the software by using the docker proxies for standard elixir mix commands.

$ docker-compose up deps.get
$ docker-compose up compile

Start Queue & Registration Service in the background. It's normally ok if you don't do this explicitly, the docker-compose.yml file ensures it will be started when required by other services.

$ docker-compose up -d redis

Start System-monitor Service in the foreground, which will automatically start the registration service (Redis). After running the command below, then cluster status and membership will be displayed in a loop on the terminal, and a (unauthenticated!) web console is available at http://localhost:5984.

$ docker-compose up sysmon

Start one or more Elixir worker nodes in the foreground of another terminal. Scale up and down by changing the numeric value in the command below, and you can watch the system monitor console as registration/peering automatically updates.

$ docker-compose scale worker=2

Start one or more API nodes in the background, so we can POST and GET work to them. You can ensure it started ok afterwards by using the logs or ps subcommands.

$ docker-compose scale api=2
$ docker-compose ps

Start the HAProxy load balancer in the background, so the API instances are accessible. You can ensure it started ok afterwards by using the logs or ps subcommands.

$ docker-compose up -d lb
$ docker-compose logs lb

POSTing work with curl, can be done like so. Note the callback ID in the response, which is just a simple hash of the input data. By running this command repeatedly and inspecting the accepted_by field, you can confirm that the load balancer is hitting different instances of the web API.

$ curl -XPOST -d '{"data":"foo"}' http://localhost/api/v1/work
{status: "accepted", "accepted_by": "api@....", callback: "ACBD18DB4CC2F85CEDEF654FCCC4A4D8"}

Check the status of submitted work with a command like what you see below. Status is one of accepted, pending, working, or worked (for our purposes the "work" done for all input submissions is to just pause a few seconds.) Note that the record for completed work is removed automatically after a timeout is reached, and requesting it after that point from the web API simply results in a 404. (This TTL prevents the need for additional janitor processes acting against the data store, etc)

$ curl -X GET http://localhost/api/v1/work/ACBD18DB4CC2F85CEDEF654FCCC4A4D8
{status: "pending"}
$ curl -X GET http://localhost/api/v1/work/ACBD18DB4CC2F85CEDEF654FCCC4A4D8
{status: "working"}
$ curl -X GET http://localhost/api/v1/work/ACBD18DB4CC2F85CEDEF654FCCC4A4D8
{status: "worked"}
$ curl -X GET http://localhost/api/v1/work/ACBD18DB4CC2F85CEDEF654FCCC4A4D8
{"status":"404. not found!"}

Further Experiments

Inspect the environment with the shell if you like. To make your dockerized Elixir node instances interactive (i.e. run the node registration loop + open the iex shell), use this command (note the usage here of run vs up)

$ docker-compose run shell

Simulate network failures if you like, just to show that Elixir/Erlang style "happy path" coding is really working and that this system is crash resistant and self-healing.

Try taking down Redis while watching the system monitor, and you'll see that while registration and cluster-join tasks will fail repeatedly, neither our monitor or our workers should crash when they can't read/write registration data.

$ docker-compose stop redis

Bring Redis back up and keep an eye on the system monitor to watch the system recover:

$ docker-compose up redis

Caveats

Is this ProductionReadyâ„¢? Not exactly, although tools like kompose and ecs-cli compose continue to improve, and are making additional work at the infrastructure layer increasingly unnecessary.

At the architecture layer, the cluster registration ledger should really be separate from the work-tracking K/V store. In this case for simplicity, we use the same Redis instance for both.

At the application layer, there is only a naive hashing algorithm to generate keys, little to no treatment of duplicate work submissions, retries, etc.

Ideas for Extension

  • Add integration/infrastructure tests
  • Just for fun, split registration/work tracking persistence among redis and cassandra instead of using 1 data store
  • Add some treatment for retries/failures
  • Add a brief guide for production(ish) deployments
  • Testing with ecs-cli compose for AWS and kompose for kubernetes translations
  • Add demo for polyglot workers, maybe using erlport
  • Add demo for pubsub
  • Find a way to use observer with docker-compose (probably requires X11 on guest and XQuartz on OSX host)
  • Add more worker types and message types, exploring the line between plain queue-workers and AOP with ACLs

More Repositories

1

cookiecutter-elixir-project

cookiecutter template for an Elixir project
Elixir
18
star
2

pygenie

unofficial mirror of pygenie, a tool for measuring cyclomatic complexity
Python
14
star
3

smash

SMArtSHell: python/bash hybrid shell
Python
12
star
4

airflow-ubuntu-ansible-terraform

Demo deployment of Apache Airflow on EC2/Ubuntu with Docker-compose, Terraform, and Ansible
Makefile
11
star
5

corkscrew

another layer on top of flask (milli-framework?) it supports settings files, basic authentication, and object oriented views
JavaScript
6
star
6

openvpn-ubuntu-ansible-terraform

Top-to-bottom OpenVPN setup for Ubuntu/AWS using Terraform/Ansible
HCL
5
star
7

cortex

DEPRECATED Cortex: An environment/framework/toolbox for agent-oriented programming and distributed computing in Python
Python
5
star
8

rapidmind

loads wikimedia data into neo4j with elixir
Elixir
4
star
9

ixle

ixle indexes files
Python
4
star
10

spock

Logical programming for Python
Python
3
star
11

ebssh

ssh and scp helpers for using fabric with elastic beanstalk
Python
3
star
12

kinbaku

examples, use cases, and patterns for rope, a library for automatically refactoring python code
Python
3
star
13

twisted-demos

python code demonstrating some of the functionality of twisted
Python
3
star
14

makefiles

makefile-based automation library
Makefile
3
star
15

tunl

command line ssh tunnel manager
Python
3
star
16

ansible-role

The missing "ansible-role" command: apply an ansible role from the command line without a playbook
Python
2
star
17

ymir

http://mattvonrocketstein.github.io/ymir
Python
2
star
18

elixir-python

Demo for elixir-python interoperability
Python
2
star
19

hammock

playing with couch/flask
Python
1
star
20

ambient-calculus

An implementation of the Ambient Calculus in elixir
Elixir
1
star
21

ansible-supercomposer

1
star
22

smashlib

support library for smash
Python
1
star
23

channels

backend-agnostic sugar for message passing
Python
1
star
24

toybox

vagrant/puppet recipe for mongo, neo4j, rabbitmq, among other things
Ruby
1
star
25

sourcecodegen

unofficial version of sourcecodegen with a few changes
Python
1
star
26

data-science-stack

Jupyter Notebook
1
star
27

reporting

reporting is better than printing
Python
1
star
28

penrose

Experiments with plane tilings, 3d solid geometry, Python and OpenSCAD
OpenSCAD
1
star