| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Disque is ongoing experiment to build a distributed, in memory, message broker. Its goal is to capture the essence of the "Redis as a jobs queue" use case, which is usually implemented using blocking list operations, and move it into an ad-hoc, self-contained, scalable, and fault tolerant design, with simple to understand properties and guarantees, but still resembling Redis in terms of simplicity, performances, and implementation as a C non-blocking networked server.
Currently (27 April 2015) the project is just an alpha quality preview, that was developed in roughly 120 hours, 244 different commits performed in 72 different days, often at night and during weekends. In short: don't expect much or rock solid production systems here.
WARNING: This is alpha code NOT suitable for production. The implementation and API will likely change in significant ways during the next months. The code and algorithms are not tested enough. A lot more work is needed.
Disque is a distributed and fault tolerant message broker, so it works as middle layer among processes that want to exchange messages.
Producers add messages that are served to consumers. Since message queues are often used in order to process delayed jobs, Disque often uses the term "job" in the API and in the documentation, however jobs are actually just messages in the form of strings, so Disque can be used for other use cases. In this documentation "jobs" and "messages" are used in an interchangeable way.
Job queues with a producer-consumer model are pretty common, so the devil is in the details. A few details about Disque are:
Disque is a synchronously replicated job queue. By default when a new job is added, it is replicated to W nodes before the client gets an acknowledge about the job being added. W-1 nodes can fail and still the message will be delivered.
Disque supports both at-least-once and at-most-once delivery semantics. At least once delivery semantics is where most efforts were spent in the design and implementation, while the at most once semantics is a trivial result of using a retry time set to 0 (which means, never re-queue the message again) and a replication factor of 1 for the message (not strictly needed, but it is useless to have multiple copies of a message around if it will be delivered at most one time). You can have, at the same time, both at-least-once and at-most-once jobs in the same queues and nodes, since this is a per message setting.
Disque at-least-once delivery is designed to approximate single delivery when possible, even during certain kinds of failures. This means that while Disque can only guarantee a number of deliveries equal or greater to one, it will try hard to avoid multiple deliveries whenever possible.
Disque is a distributed system where all nodes have the same role (aka, it is multi-master). Producers and consumers can attach to whatever node they like, and there is no need for producers and consumers of the same queue, to stay connected to the same node. Nodes will automatically exchange messages based on load and client requests.
Disque is Available (it is an eventually consistent AP system in CAP terms): producers and consumers can make progresses as long as a single node is reachable.
Disque supports optional asynchronous commands that are low latency for the client but provide less guarantees. For example a producer can add a job to a queue with a replication factor of 3, but may want to run away before knowing if the contacted node was really able to replicate it to the specified number of nodes or not. The node will replicate the message in the background in a best effort way.
Disque automatically re-queue messages that are not acknowledged as already processed by consumers, after a message-specific retry time. There is no need for consumers to re-queue a message if it was not processed.
Disque uses explicit acknowledges in order for a consumer to signal a message as delivered (or, using a different terminology, to signal a job as already processed).
Disque queues only provides best effort ordering. Each queue sorts messages based on the job creation time, which is obtained using the wall clock of the local node where the message was created (plus an incremental counter for messages created in the same millisecond), so messages created in the same node are normally delivered in the same order they were created. This is not causal ordering since correct ordering is violated in different cases: when messages are re-issued because not acknowledged, because of nodes local clock drifts, and when messages are moved to other nodes for load balancing and federation (in this case you end with queues having jobs originated in different nodes with different wall clocks). However all this also means that normally messages are not delivered in random order and usually messages created first are delivered first.
Note that since Disque does not provide strict FIFO semantics, technically speaking it should not be called a message queue, and it could better identified as a message broker. However I believe that at this point in the IT industry a message queue is often more lightly used to identify a generic broker that may or may not be able to guarantee order in all the cases. Given that we document very clearly the semantics, I grant myself the right to call Disque a message queue anyway.
Disque provides the user with fine-grained control for each job using three time related parameters, and one replication parameter. For each job, the user can control:
Finally, Disque supports optional disk persistence, which is not enabled by default, but that can be handy in single data center setups and during restarts.
Disque implementation of at least once delivery semantics is designed in order to avoid multiple delivery during certain classes of failures. It is not able to guarantee that no multiple deliveries will occur. However there are many at least once workloads where duplicated deliveries are acceptable (or explicitly handled), but not desirable either. A trivial example is sending emails to users (it is not terrible if an user gets a duplicated email, but is important to avoid it when possible), or doing idempotent operations that are expensive (all the times where it is critical for performances to avoid multiple deliveries).
In order to avoid multiple deliveries when possible, Disque uses client ACKs. When a consumer processes a message correctly, it should acknowledge this fact to Disque. ACKs are replicated to multiple nodes, and are garbage collected as soon as the system believes it is unlikely that more nodes in the cluster have the job (the ACK refers to) still active. Under memory pressure or under certain failure scenarios, ACKs are eventually discarded.
More explicitly:
For example, if a node having a copy of a job gets partitioned away during the time the job gets acknowledged by the consumer, it is likely that when it returns back (in a reasonable amount of time, that is, before the retry time is reached) it will be informed about the ACK and will avoid to re-queue the message. Similarly jobs can be acknowledged during a partition to just a single node available, and when the partition heals the ACK will be propagated to other nodes that may still have a copy of the message.
So an ACK is just a proof of delivery that is replicated and retained for some time in order to make multiple deliveries less likely to happen in practice.
As already mentioned, In order to control replication and retries, a Disque job has the following associated properties: number of replicas, delay, retry and expire.
If a job has a retry time set to 0, it will get queued exactly once (and in this case a replication factor greater than 1 is useless, and signaled as an error to the user), so it will get delivered either a single time or will never get delivered. While jobs can be persisted on disk for safety, queues aren't, so this behavior is guaranteed even when nodes restart after a crash, whatever the persistence configuration is. However when nodes are manually restarted by the sysadmin, for example for upgrades, queues are persisted correctly and reloaded at startup, since the store/load operation is atomic in this case, and there are no race conditions possible (it is not possible that a job was delivered to a client and is persisted on disk as queued at the same time).
Disque supports a faster way to acknowledge processed messages, via the FASTACK command. The normal acknowledge is very expensive from the point of view of messages exchanged between nodes, this is what happens during a normal acknowledge:
Note: actual garbage collection is more complex in case of failures and is explained in the state machine later. The above is what happens 99% of times.
If a message is replicated to 3 nodes, acknowledging requires 1+2+2+2 messages, for the sake of retaining the ack if some nodes may not be reached when the message is acknowledged. This makes the probability of multiple deliveries of this message less likely.
However the alternative fast ack, while less reliable, is much faster and invovles exchanging less messages. This is how a fast acknowledge works:
If during a fast acknowledge a node having a copy of the message is not reachable, for example because of a network partition, the node will deliver the message again, since it has a non acknowledged copy of the message and there is nobody able to inform it the message was acknowledged when it returns back available.
If the network you are using is pretty reliable, you are very concerned with performances, and multiple deliveries in the context of your applications are a non issue, then FASTACK is probably the way to go.
Disque can be operated in-memory only, using the synchronous replication as durability guarantee, or can be operated using the Append Only File where jobs creations and evictions are logged on disk (with configurable fsync policies) and reloaded at restart.
AOF is recommended especially if you run in a single availability zone where a mass reboot of all your nodes is possible.
Normally Disque only reloads jobs data in memory, without populating queues, since anyway not acknowledged jobs are requeued eventually. Moreover reloading queue data is not safe in the case of at-most-once jobs having the retry value set to 0. However a special option is provided in order to reload the full state from the AOF. This is used together with an option that allows to shutdown the server just after the AOF is generated from scratch, in order to be safe to reload even jobs with retry set to 0, since the AOF is generated while the server no longer accepts commands from clients, so no race condition is possible.
Even when running memory-only, Disque is able to dump its memory on disk and reload from disk on controlled restarts, for example in order to upgrade the software.
This is how to perform a controlled restart, that works whatever AOF is enabled or not:
At this point we have a freshly generated AOF on disk, and the server is configured in order to load the full state only at the next restart (the aof-enqueue-jobs-once is automatically turned off after the restart).
We can just restart the server with the new software, or in a new server, and it will restart with the full state. Note that the aof-enqueue-jobs-once implies to load the AOF even if the AOF support is switched off, so there is no need to enable AOF just for the upgrade of an in-memory only server.
Disque jobs are uniquely identified by an ID like the following:
DI0f0c644fd3ccb51c2cedbd47fcb6f312646c993c05a0SQ
Job IDs always start with "DI" and end with "SQ" and are always composed of exactly 48 characters.
We can split an ID into multiple parts:
DI | 0f0c644f | d3ccb51c2cedbd47fcb6f312646c993c | 05a0 | SQ
IDs are returned by ADDJOB when a job is successfully created, are part of the GETJOB output, and are used in order to acknowledge that a job was correctly processed by a worker.
Part of the node ID is included in the message so that a worker processing messages for a given queue can easily guess what are the nodes where jobs are created, and move directly to these nodes to increase efficiency instead of listening for messages in a node that will require to fetch messages from other nodes.
Only 32 bits of the original node ID is included in the message, however in a cluster with 100 Disque nodes, the probability of two nodes to have identical 32 bit ID prefixes is given by the birthday paradox:
P(100,2^32) = .000001164
In case of collisions, the workers may just do a non-efficient choice.
Collisions in the 128 bits random part are believed to be impossible, since it is computed as follows.
128 bit ID = SHA1(seed || counter)
Where:
To play with Disque please do the following:
For example if you are running three Disque servers in port 7711, 7712, 7713 in order to join the cluster you should use the disque command line tool and run the following commands:
./disque -p 7711 cluster meet 127.0.0.1 7712 ./disque -p 7711 cluster meet 127.0.0.1 7713
Your cluster should now be ready. You can try to add a job and fetch it back in order to test if everything is working:
./disque -p 7711 127.0.0.1:7711> ADDJOB queue body 0 DI0f0c644ffca14064ced6f8f997361a5c0af65ca305a0SQ 127.0.0.1:7711> GETJOB FROM queue 1) 1) "queue" 2) "DI0f0c644ffca14064ced6f8f997361a5c0af65ca305a0SQ" 3) "body"
Remember that you can add and get jobs from different nodes as Disque is multi master. Also remember that you need to acknowledge jobs otherwise they'll never go away from the server memory (unless the time to live is reached).
Disque API is composed of a small set of commands, since the system solves a single very specific problem. The three main commands are:
ADDJOB queue_name job <ms-timeout> [REPLICATE <count>] [DELAY <sec>] [RETRY <sec>] [TTL <sec>] [MAXLEN <count>] [ASYNC]
Adds a job to the specified queue. Arguments are as follows:
The command returns the Job ID of the added job, assuming ASYNC is specified, or if the job was replicated correctly to the specified number of nodes. Otherwise an error is returned.
GETJOB [TIMEOUT <ms-timeout>] [COUNT <count>] FROM queue1 queue2 ... queueN
Return jobs available in one of the specified queues, or return NULL if the timeout is reached. A single job per call is returned unless a count greater than 1 is specified. Jobs are returned as a three elements array containing the queue name, the Job ID, and the job body itself. If jobs are available into multiple queues, queues are processed left to right.
If there are no jobs for the specified queues the command blocks, and messages are exchanged with other nodes, in order to move messages about these queues to this node, so that the client can be served.
ACKJOB jobid1 jobid2 ... jobidN
Acknowledges the execution of one or more jobs via job IDs. The node receiving the ACK will replicate it to multiple nodes and will try to garbage collect both the job and the ACKs from the cluster so that memory can be freed.
FASTACK jobid1 jobid2 ... jobidN
Performs a best effort cluster wide deletion of the specified job IDs. When the network is well connected and there are no node failures, this is equivalent to ACKJOB but much faster (less messages exchanged), however during failures it is more likely that fast acknowledges will result into multiple deliveries of the same messages.
Note: not everything implemented yet.
INFO
Generic server information / stats.
HELLO
Returns hello format version, this node ID, all the nodes IDs, IP addresses, ports, and priority (lower is better, means node more available). Clients should use this as an handshake command when connecting with a Disque node.
QLEN <qname>
Length of queue
QSTAT <qname> (TODO)
Return produced ... consumed ... idle ... sources [...] ctime ...
QPEEK <qname> <count>
Return, without consuming from queue, count jobs. If count is positive the specified number of jobs are returned from the oldest to the newest (in the same best-effort FIFO order as GETJOB). If count is negative the commands changes behavior and shows the count newest jobs, from the newest from the oldest.
ENQUEUE <job-id> ... <job-id>
Queue jobs if not already queued.
DEQUEUE <job-id> ... <job-id>
Remove the job from the queue.
DELJOB <job-id> ... <job-id>
Completely delete a job from a node. Note that this is similar to FASTACK, but limited to a single node since no DELJOB cluster bus message is sent to other nodes.
SHOW <job-id>
Describe the job.
SCAN <job|queue> <cursor> [STATE ...] [COUNT ...] [MAXIDLE ...] (TODO)
Iterate job IDs.
Disque uses the same protocol as Redis itself. To adapt Redis clients, or to use it directly, should be pretty easy. However note that Disque default port is 7711 and not 6379.
While a vanilla Redis client may work well with Disque, clients should optionally use the following protocol in order to connect with a Disque cluster:
This way producers and consumers will eventually try to minimize nodes messages exchanges whenever possible.
Disque is a full mesh, with each node connected to each other. Disque performs distributed failure detection via gossip, only in order to adjust the replication strategy (try reachable nodes first when trying to replicate a message), and in order to inform clients about non reachable nodes when they want the list of nodes they can connect to.
Being Disque multi-master the event of nodes failing is not handled in any special way.
Nodes communicate via a set of messages, using the node-to-node message bus. A few of the messages are used in order to check that other nodes are reachable and to mark nodes as failing. Those messages are PING, PONG and FAIL. Since failure detection is only used to adjust the replication strategy (talk with reachable nodes first in order to improve latency), the details are yet not described. Other messages are more important since they are used in order to replicate jobs, re-issue jobs trying to minimize multiple deliveries, and in order to auto-federate to serve consumers when messages are produced in different nodes compared to where consumers are.
The following is a list of messages and what they do, split by category. Note that this is just an informal description, while in the next sections describing the Disque state machine, there is a more detailed description of the behavior caused by messages reception, and in what cases they are generated.
NEEDJOBS(queue,count): The sender asks the receiver to obtain messages for a given queue, possibly count messages, but this is only an hit for congestion control and messages optimization, the receiver is free to reply with whatever number of messages. NEEDJOBS messages are delivered in two ways: broadcasted to every node in the cluster from time to time, in order to discover new source nodes for a given queue, or more often, to a set of nodes that recently replies with jobs for a given queue. This latter mechanism is called an ad hoc delivery, and is possible since every node remembers for some time the set of nodes that were recent providers of messages for a given queue. In both cases, NEEDJOBS messages are delivered with exponential delays, with the exception of queues that drop to zero-messages and have a positive recent import rate, in this case an ad hoc NEEDJOBS delivery is performed regardless of the last time the message was delivered in order to allow a continuous stream of messages under load.
YOURJOBS(array of messages): The reply to NEEDJOBS. An array of serialized jobs, usually all about the same queue (but future optimization may allow to send different jobs from different queues). Jobs into YOURJOBS replies are extracted from the local queue, and queued at the receiver node's queue with the same name. So even messages with a retry set to 0 (at most once delivery) still guarantee the safety rule since a given message may be in the source node, on the wire, or already received in the destination node. If a YOURJOBS message is lost, at least once delivery jobs will be re-queued later when the retry time is reached.
This section shows the most interesting (as in less obvious) parts of the state machine each Disque node implements. While practically it is a single state machine, it is split in sections. The state machine description uses a convention that is not standard but should look familiar, since it is event driven, made of actions performed upon: message receptions in the form of commands received from clients, messages received from other cluster nodes, timers, and procedure calls.
Note that: job is a job object with the following fields:
List fields such as .delivered and .confirmed support methods like .size to get the number of elements.
States are as follows:
PROCEDURE LOOKUP-JOB(string job-id):
PROCEDURE UNREGISTER(object job):
PROCEDURE ENQUEUE(job):
PROCEDURE DEQUEUE(job):
ON RECV cluster message: DELJOB(string job.id):
This part of the state machine documents how clients add jobs to the cluster and how the cluster replicates jobs across different Disque nodes.
ON RECV client command `ADDJOB(string queue-name, string body, integer replicate, integer retry, integer ttl, ...):
Step 3: We'll reply to the client in step 4 of GOTJOB message processing.
ON RECV cluster message ADDJOB(object serialized-job):
Step 1: We may already have the job, since ADDJOB may be duplicated.
Step 2: If we already have the same job, we update the list of jobs that may have a copy of this job, performing the union of the list of nodes we have with the list of nodes in the serialized job.
ON RECV cluster message GOTJOB(object serialized-job):
Step 4: As we receive enough confirmations via GOTJOB messages, we finally reach the replication factor required by the user and consider the message active.
TIMER, firing every next 50 milliseconds while a job still did not reached the expected replication factor.
Step 3: We send the message to every node again, so that each node will have a chance to update job.delivered with the new nodes. It is not required for each node to know the full list of nodes that may have a copy, but doing so improves our approximation of single delivery whenever possible.
This part of the state machine documents how Disque nodes put a given job back into the queue after the specified retry time elapsed without the job being acknowledged.
TIMER, firing 500 milliseconds before the retry time elapses:
TIMER, firing when job.qtime time is reached.
Step 1: At most once jobs never get enqueued again.
Step 3: We'll retry again after the retry period.
ON RECV cluster message WILLQUEUE(string job-id):
Step 3: We broadcast the message since likely the other nodes are going to retry as well.
Step 4: SETACK processing is documented below in the acknowledges section of the state machine description.
ON RECV cluster message QUEUED(string job-id):
Step 4: If multiple nodes re-queue the job about at the same time because of race conditions or network partitions that make WILLQUEUE not effective, then QUEUED forces receiving nodes to dequeue the message if the sender has a greater node ID, lowering the probability of unwanted multiple delivery.
Step 5: Now the message is already queued somewhere else, but the node will retry again after the retry time.
This part of the state machine is used in order to garbage collect acknowledged jobs, when a job finally gets acknowledged by a client.
PROCEDURE ACK-JOB(job):
PROCEDURE START-GC(job):
Step 2: this is an ACK about a job we don’t know. In that case, we can just broadcast the acknowledged hoping somebody knows about the job and replies.
ON RECV client command ACKJOB(string job-id):
ON RECV cluster message SETACK(string job-id, integer may-have):
Steps 3 and 4 makes sure that among the reachalbe nodes that may have a message, garbage collection will be performed by the node that is aware of more nodes that may have a copy.
Step 5 instead is used in order to start a GC attempt if we received a SETACK message from a node just hacking a dummy ACK (an acknowledge about a job it was not aware of).
ON RECV cluster message GOTACK(string job-id, bool known):
Step 3: If job.delivered.size is zero, it means that the node just holds a dummy ack for the job. It means the node has an acknowledged job it created on the fly because a client acknowledged (via ACKJOB command) a job it was not aware of.
Step 6: we don't have to hold a dummy acknowledged jobs if there are nodes that have the job already acknowledged.
Step 7: this happens when nobody knows about a job, like when a client acknowledged a wrong job ID.
TIMER, from time to time (exponential backoff with random error), for every acknowledged job in memory:
No, it is a standalone project, however a big part of the Redis networking source code, nodes message bus, libraries, and the client protocol, were reused in this new project. In theory it was possible to extract the common code and release it as a framework to write distributed systems in C. However this is not a perfect solution as well, since the projects are expected to diverge more and more in the future, and to rely on a common fundation was hard. Moreover the initial effort to turn Redis into two different layers: an abstract server, networking stack and cluster bus, and the actual Redis implementation, was a huge effort, ways bigger than writing Disque itself.
However while it is a separated project, conceptually Disque is related to Redis, since it tries to solve a Redis use case in a vertical, ad-hoc way.
Disque is a side project of Salvatore Sanfilippo, aka @antirez.
Currently I consider this just a public alpha: If I see people happy to use it for the right reasons (i.e. it is better in some use cases compared to other message queues) I'll continue the development. Otherwise it was anyway cool to develop it, I had much fun, and I definitely learned new things.
Yes. In Disque it should be relatively simple to use the disk when memory is not available, since jobs are immutable and don't need to necessarily exist in memory at a given time.
There are multiple strategies available. The current idea is that when an instance is out of memory, jobs are stored into a log file instead of memory. As more free memory is available in the instance, on disk jobs are loaded.
However in order to implement this, there is to observe strong evidence of its general usefulness for the user base.
DIStributed QUEue but is also a joke with "dis" as negation (like in disorder) of the strict concept of queue, since Disque is not able to guarantee the strict ordering you expect from something called queue. And because of this tradeof it gains many other interesting things.
| Back | FazBrowse Home | New Git URL |