NAME
AnyEvent::Redis::Federated - Full-featured Async Perl Redis client
SYNOPSIS
use AnyEvent::Redis::Federated;
my $r = AnyEvent::Redis::Federated->new(%opts);
# batch up requests and explicity wait for completion
$redis->set("foo$_", "bar$_") for 1..20;
$redis->poll;
# send a request with a callback
$redis->get("foo1", sub {
my $val = shift;
print "cb got: $val\n";
});
$redis->poll;
DESCRIPTION
This is a wrapper around AnyEvent::Redis which adds timeouts, connection retries, multi-machine cluster configuration (including consistent hashing), and other magic bits.
HASHING AND SCALING
Keys are run through a consistent hashing algorithm to map them to "nodes" which ultimately map to instances defined by back-end host:port entries. For example, the redis_1
node may map to the host and port redis1.example.com:63791
, but that'll all be transparent to the user.
However, there are features in Redis that are handy if you know a given set of keys lives on a single insance (a wildcard fetch like KEYS gmail*
, for example). To facilitate that, you can specify a "key group" that will be hashed insead of hashing the key.
For example:
key group: gmail
key : foo@gmail.com
key group: gmail
key : bar@gmail.com
Put another way, the key group defaults to the key for the named operation, but if specified, is used instead as the input to the consistent hashing function.
Using the same key group means that multiple keys end up on the same Redis instance. To do so, simply change any key in a call to an arrayref where item 0 is the key group and item 1 is the key.
$r->set(['gmail', 'foo@gmail.com'], 'spammer', $cb);
$r->set(['gmail', 'bar@gmail.com'], 'spammer', $cb);
Anytime a key is an arrayref, AnyEvent::Redis::Federated will assume you're using a key group.
PERSISTENT CONNECTIONS
By default, AnyEvent::Redis::Federated will use a new connection for each command. You can enable persistent connections by passing a persistent
agrument (with a true value) in new()
. You will likely also want to set a idle_timeout
value as well. The idle_timeout defaults to 0 (which means no timeout). But if set to a posistive value, that's the number of seconds that a connection is allowed to remain idle before it is re-established. A number up to 60 seconds is probably reasonable.
SHARED CONNECTIONS
Because creating AnyEvent::Redis::Federated objects isn't cheap (due mainly to initializing the consistent hashing ring), there is a mechanism for sharing a connection object among modules without prior knowledge of each other. If you specify a tag
in the new()
constructor and another module in the same process tries to create an object with the same tag, it will get a reference to the one you created.
For example, in your code:
my $redis = AnyEvent::Redis::Federated->new(tag => 'rate-limiter');
Then in another module:
my $r = AnyEvent::Redis::Federated->new(tag => 'rate-limiter');
Both $redis
and $r
will be references to the same object.
Since the first module to create an object with a given tag gets to define the various retry parameters (as described in the next section), it's worth thinking about whether or not you really want this behavior. In many cases, you may--but not in all cases.
Tag names are used as a hash key internally and compared using Perl's normal stringification mechanism, so you could use a full-blown object as your tag if you wanted to do such a thing.
CONNECTION RETRIES
If a connection to a server goes down, AnyEvent::Redis::Federated will notice and retry on subsequent calls. If the server remains down after a configured number of tries, it will go into back-off mode, retrying occasionally and increasing the time between retries until the server is back on-line or the retry interval time has reached the maximum configured vaue.
The module has some hopefully sane defaults built in, but you can override any or all of them in your code when creating an AnyEvent::Redis::Federated object. The following keys contol this behvaior (defaults listed in parens for each):
* max_host_retries (3) is the number of times a server will be
re-tried before starting the back-off logic
* base_retry_interval (10) is the number of seconds between retries
when entering back-off mode
* retry_interval_mult (2) is the number we'll multiply
base_retry_interval by on each subsequent failure in back-off
mode
* retry_slop_secs (5) is used as the upper bound on a whole number
of seconds to add to the retry_interval after each failure that
triggers an increase in the retry interval. This parameter helps
to slightly stagger retry times between many clients on different
servers.
* max_retry_interval (600) is the number of seconds that the retry
interval will not exceed
When a server first goes down, this module will warn()
a message that says "redis server $server seems down\n" where $server is the $host:$port pair that represents the connection to the server. If this is the first time that server has been seen down, it will additionally warn()
"redis server $server down, first time\n".
If a server remainds down on subsequent retries beyond max_host_retries, the module will warn()
"redis server $server still down, backing off" to let you know that the back-off logic is about to kick in. Each time the retry_interval is increased, it will warn()
"redis server $server retry_interval now $retry_interval".
If a down server does come back up, the module will warn()
"redis server $server back up (down since $down_since)\n" where $down_since is human readable timestamp. It will also clear all internal state about the down server.
TIMEOUTS
This module provides support for connection timeouts and command timeouts. A connection timeout applies to the time required to establish a connection to a Redis server. Generally speaking, that's only a problem if there are network problems preventing you from getting a positive or negative response from the server. In normal circumstances, you'll either connect or be refused almost immediately.
By default connect_timeout
is 1 second. You can set it to whatever you like when creating a new AnyEvent::Redis::Federated object. Using 0 will have the effect of falling back to the OS default timeout. You may use floating-point (non-integer values) such as 0.5 for the connection timeout. You can get or set the current connect_timeout by calling the connect_timeout()
method on an AnyEvent::Redis::Federated object.
When a connect timeout is hit, the logic in CONNECTION RETRIES (above) kicks in.
IMPORTANT: In high-volume contexts, such as running under Apache/mod_perl handling hundreds of requests per server per second, USE CARE to choose a wise value! It's not unreasonable to use 100ms (0.1 seconds).
The command timeout controls how long we're willing to wait for a response to a given request made to a Redis server. Redis usually responds VERY quickly to most requests. But if there's a temporary network problem or something tying up the server, you may wish to fail quickly and move on.
NOTE: these timeouts are implemented using alarm()
, so be careful of also using alarm()
calls in your own code that could interfere.
MULTI-KEY OPERATIONS
Some operations can operate on many keys and might cross server boundries. They are currently supported provided that you remember to specify a hash key to ensure the all live on the same node. Example operations are:
* mget
* sinter
* sinterstore
* sdiff
* sdiffstore
* zunionstore
Previous versions of this module listed these as unsupported commands, but that's rather limiting. So they're supported now, provided you know what you're doing.
METHODS
AnyEvent::Redis::Federated inherits all of the normal Redis methods. However, you can supply a callback or AnyEvent condvar as the final argument and it'll do the right thing:
$redis->get("foo", sub { print shift,"\n"; });
You can also use call chaining:
$redis->set("foo", 1)->set("bar", 2)->get("foo", sub {
my $val = shift;
print "foo: $val\n";
});
CONFIGURATION
AnyEvent::Redis::Federated requires a configuration hash be passed to it at instantiation time. The constructor will die() unless a unless a 'config' option is passed to it. The configuration structure looks like:
my $config = {
nodes => {
redis_1 => { address => 'db1:63790' },
redis_2 => { address => 'db1:63791' },
redis_3 => { address => 'db2:63790' },
redis_4 => { address => 'db2:63791' },
},
'master_of' => {
'db1:63792' => 'db2:63790',
'db1:63793' => 'db2:63791',
'db2:63792' => 'db1:63790',
'db2:63793' => 'db1:63791',
},
};
The "nodes" and "master_of" hashes are described below.
NODES
The "nodes" configuation maps an arbitrary node name to a host:port pair.
Node names (redis_N in the example above) are VERY important since they are the keys used to build the consistent hashing ring. It's generally the wrong idea to change a node name. Since node names are mapped to a host:port pair, we can move a node from one host to another without rehashing a bunch of keys.
There is unlikely to be a need to remove a node.
Adding nodes to a cluster is currently not well-supported, but is an area of active development.
MASTER_OF
The master_of
configuration describes the replication structure of the cluster. Replication provides us with a hot standby in case a machine fails. This structure tells a slave node which node is its master. If there is no mapping for a given host, it's a master. The format is 'slave' => 'master'.
EVENT LOOP
Since this module wraps AnyEvent::Redis, there are two main ways you can integrate it into your code. First, if you're using AnyEvent, it should "just work." However, if you're not otherwise using AnyEvent, you can still take advantage of batching up requests and waiting for them in parallel by calling the poll()
method as illustrated in the synopsis.
Calling poll()
asks the module to issue any pending requests and wait for all of them to return before returning control back to your code.
EXPORT
None.
SEE ALSO
The normal AnyEvent::Redis perl client perldoc AnyEvent::Redis
.
The Redis API documentation:
http://redis.io/commands
Jeremy Zawodny's blog describing craigslist's use of redis sharding:
http://blog.zawodny.com/2011/02/26/redis-sharding-at-craigslist/
That posting described an implementation which was based on the regular (non-async) Redis client from CPAN. This code is a port of that to AnyEvent.
BUGS
This code is lightly tested and considered to be of beta quality.
AUTHOR
Jeremy Zawodny, <jzawodn@craigslist.org>
Joshua Thayer, <joshua@craigslist.org>
COPYRIGHT AND LICENSE
Copyright (C) 2009-2011 by craigslist.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.10.0 or, at your option, any later version of Perl 5 you may have available.