Multiple Redis instances on Mac OS X with Homebrew

Première publication : 2012-07-05

I’ve already written (in french) about the benefit of having multiple Redis instances, and we use this technique on our servers.

On our development setup we use Mac OS X or Linux with Redis installed by a package manager. I’ll focus here on the Mac OS X + Homebrew part, but everything will be easy to adjust to a different OS or package manager.

Why do we need multiple Redis instances?

Every so often, we need or want to replace our local databases with the data from our production environment. We use a handful of different datastores : MySQL, Redis, MongoDB and Solr.

For MySQL, MongoDB and Solr, it’s quite trivial. We download dumps from our backup server and inject them into our development environment.
For Redis, it has never been that easy, here is why and what we’ve done to eliminate the pain.

How Redis stores it’s data

For each instance of a Redis server there is a dump file, usualy called dump.rdb that contains all the data needed by Redis. If you want to backup your Redis data, you just have to copy this file into another location. You don’t even need to stop the server since this file is always consistent. It may lack some data not yet written to disk (the delay depends on the configuratyion, usually a few seconds), but it is safe.

Since we’ve been using Redis, we wanted to separate different data parts from each other, with something more secure than a basic namespace in the keys. We’ve been using different “databases” inside a single Redis instance (there are 16 available by default), but everything is stored in a single dump.rdb file. As of Redis v2.4 I’ve not found a way to dump/restore only a specific database.

As I’ve said, we have 4 different dump files from our servers and only 1 (with 4 internal DBs) on our development environment which makes it impossible to import our production data locally.

The solution is quite simple ; let’s have as many Redis instances locally as on our production server.

Run multiple Redis instances locally

On Mac OS X, you can use Homebrew to manage third-parties packages, including Redis. When you install it, you get a binary (redis-server), a config file and a Launchd file. By default Homebrew doesn’t set Redis to start automaticaly but tells you what to do if you want this behavior.

$ brew info redis
redis 2.4.14
http://redis.io/
/usr/local/Cellar/redis/2.4.13 (9 files, 436K) *
https://github.com/mxcl/homebrew/commits/master/Library/Formula/redis.rb

==> Caveats
If this is your first install, automatically load on login with:
    mkdir -p ~/Library/LaunchAgents
    cp /usr/local/Cellar/redis/2.4.14/homebrew.mxcl.redis.plist ~/Library/LaunchAgents/
    launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.redis.plist

If this is an upgrade and you already have the homebrew.mxcl.redis.plist loaded:
    launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
    cp /usr/local/Cellar/redis/2.4.14/homebrew.mxcl.redis.plist ~/Library/LaunchAgents/
    launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.redis.plist

To start redis manually:
    redis-server /usr/local/etc/redis.conf

To access the server:
    redis-cli

To have many instances of Redis running, you just need 1 binary, but you need to start it with different configurations. That’s what we’ll do.

All our instances share the same basic configuration, except for a few settings : the path of the dump.rdb and pid files, which TCP port or unix socket to use.

Let’s copy the default config file :

$ cp /usr/local/etc/redis{,-common}.conf

In our case we use unix sockets so in this common config file set port 0 the disable the use of TCP ports.

Then we can edit a /usr/local/etc/redis-1.conf with only these values :

include /usr/local/etc/redis-common.conf
pidfile /usr/local/var/run/redis-1.pid
unixsocket /tmp/redis-1.sock
dbfilename dump-1.rdb
vm-swap-file /tmp/redis-1.swap

We can copy this file for each instance we want, and just change the values. All the common configuration is shared. That’s up to you to decide which setting is shared and which is not. And as far as I know, you can override any setting in any configuration file, since the common config file is included at the top.

Now we need to launch those instances. Homebrew gives us a default file (homebrew.mxcl.redis.plist) :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>KeepAlive</key>
    <true/>
    <key>Label</key>
    <string>homebrew.mxcl.redis</string>
    <key>ProgramArguments</key>
    <array>
      <string>/usr/local/bin/redis-server</string>
      <string>/usr/local/etc/redis.conf</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>UserName</key>
    <string>jlecour</string>
    <key>WorkingDirectory</key>
    <string>/usr/local/var</string>
    <key>StandardErrorPath</key>
    <string>/usr/local/var/log/redis.log</string>
    <key>StandardOutPath</key>
    <string>/usr/local/var/log/redis.log</string>
  </dict>
</plist>

The basic principle of Launchd is that if you load such a file, Launchd will take care of it. In this case it starts Redis after boot, makes sure that it is restared if the process dies, runs it with a specific user and with some arguments (the config file for example).

We will copy this file for each instance we want and change the values in it. Here is a ~/Library/LaunchAgents/custom1.redis.plist :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>KeepAlive</key>
    <false/>
    <key>Label</key>
    <string>custom1.redis</string>
    <key>ProgramArguments</key>
    <array>
      <string>/usr/local/bin/redis-server</string>
      <string>/usr/local/etc/redis-1.conf</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>UserName</key>
    <string>jlecour</string>
    <key>WorkingDirectory</key>
    <string>/usr/local/var</string>
    <key>StandardErrorPath</key>
    <string>/usr/local/var/log/redis-1.log</string>
    <key>StandardOutPath</key>
    <string>/usr/local/var/log/redis-1.log</string>
  </dict>
</plist>

You see, we’ve just changed a couple of values.

Then we can load all these Launchd files :

$ launchctl load -w ~/Library/LaunchAgents/custom*.redis.plist

If you want to automate the start/stop of all your Redis instances, you can put this into your shell :

alias redis_start='launchctl load -w ~/Library/LaunchAgents/custom*.redis.plist'
alias redis_stop='launchctl unload -w ~/Library/LaunchAgents/custom*.redis.plist'

Restore production/backup data

Now that we have separate instances, just like on our production environment, we can restore the data for any of them, at any time. We just have to stop our local instance, replace the dump file with the one downloaded from our backup or production server and restart the local instance.

Since the dump file is just a persistence of the data Redis keeps in memory, it’s really important to stop the instance before you replace the dump file.
If you don’t do this and don’t restart the instance, it will never pick the “new” data up. If you restart the instance after you replaced the dump file, Redis will first dump it’s memory data into the file and replace the “new” date.

What have we done?

  • disabled the Launchd management of Redis as provided by Homebrew;
  • made separate config files for each instance (with shared settings to keep the maintainance simple)
  • made custom launchd files
  • imported a backup locally