Tuesday, December 21, 2010

Running Membase on OpenSolaris

I've been wanting to run membase on my OpenSolaris server for a long time, but I haven't had the time to look into all of the details on getting it there. All of the command line tools compile and works perfectly well on OpenSolaris already, but I haven't been able to get the web ui and cluster management up and running. One of the bugs preventing an easy install of membase on OpenSolaris (and other unsupported platforms) is that the makefile for the admin ui don't have an install target (This is tracked in MB-2700 ).

Earlier today I cloned the layout of the web application on disk from the Ubuntu installation to my OpenSolaris box, and with some work I was able to get the system up and running on my OpenSolaris box. Just check it out:

trond@storm> svcs membase
STATE          STIME    FMRI
online         13:37:26 svc:/application/database/membase:membase

So let's assume you've got all of the dependencies you need to build membase installed on your OpenSolaris (I'll come up with this list later on and create an IPS package for you to install from my IPS server).

Due to MB-3227 we can't use the build method outlined in "Building on OSX from source" to build membase, but I've updated my build environment I blogged about a while ago so that it will build and install everything for you! So let's go ahead and build the stuff!

trond@storm> git clone git://github.com/trondn/tools.git
Initialized empty Git repository in /tmp/tools/.git/
remote: Counting objects: 168, done.
remote: Compressing objects: 100% (163/163), done.
remote: Total 168 (delta 89), reused 0 (delta 0)
Receiving objects: 100% (168/168), 157.56 KiB | 231 KiB/s, done.
Resolving deltas: 100% (89/89), done.
trond@storm> cd tools/membase/smf/membase
trond@storm> ./setup -u -s -z scratch               (scratch is the name of my zfs pool)
trond@storm> cd ../..
trond@storm> chown -R trond:staff /opt/membase
trond@storm> ./setup.sh -d /opt/membase/dev membase
Download commit hook - Ok.
Checking out libmemcached (Bazaar) - Ok.
Checking out bucket_engine (git) - Ok.
Checking out ep-engine (git) - Ok.
Checking out libconflate (git) - Ok.
Checking out libvbucket (git) - Ok.
Checking out memcached (git) - Ok.
Checking out moxi (git) - Ok.
Checking out vbucketmigrator (git) - Ok.
Checking out membase-cli (git) - Ok.
Checking out ns_server (git) - Ok.
Checking out memcachetest (git) - Ok.
Checking out mc-hammer (git) - Ok.
Checking out libmembase (git) - Ok.
Configure build for SunOS
trond@storm> cd membase
trond@storm> gmake install

Due to MB-2926 we need to change the file layout to move the files so that management process in membase can locate the tools. In theory we shouldn't need to create links for the libraries as well (because the runtime path of the binary should pick up the correct one), but unfortunately ns_server use the absolute path for the library.

trond@storm> cd /opt/membase/bin/dev
trond@storm> rm memcached vbucketmigrator moxi
trond@storm> mkdir memcached vbucketmigrator moxi bucket_engine ep_engine
trond@storm> cd memcached
trond@storm> ln -s ../amd64/memcached
trond@storm> ln -s ../../lib/amd64/default_engine.so
trond@storm> ln -s ../../lib/amd64/stdin_term_handler.so
trond@storm> cd ../vbucketmigrator
trond@storm> ln -s ../amd64/vbucketmigrator
trond@storm> cd ../moxi
trond@storm> ln -s ../amd64/moxi
trond@storm> cd ../bucket_engine
trond@storm> ln -s ../../lib/amd64/bucket_engine.so
trond@storm> cd ../ep_engine
trond@storm> ln -s ../../lib/amd64/ep.so
trond@storm> chown -R membase:membase /opt/membase

So let's go ahead and start the service by running:

trond@storm> svcadm enable membase

And navigate your browser to http://localhost:8091/ and start configuring your server. I was able to let my OpenSolaris server join my Ubuntu cluster.

Happy holidays!

Friday, December 17, 2010

Building libmembase

I guess I was a bit too excited yesterday when I announced libmembase, because I completely forgot to mention where to get and build it.

You will find the code at: https://github.com/trondn/libmembase, and building it should be pretty easy once you've installed all of the required dependencies. I've tried to keep the list of dependencies as short as possible (so you might think I'm suffering from the "not invented here syndrome" since I implemented the REST support myself instead of using cURL, but the main reason for that was to keep the list short ;-)):
  • git
  • Auto tools (automake, autoheader, autoconf, libtoolize)
  • A C99 compiler
  • libvbucket
  • libevent
  • SASL library

I have integrated building of libmembase in the build system I'm using to build membase making it easy for you to get the code and run configure with the correct set of options (see this blog entry for a description).

If you want to build it yourself all you need is:

$ git clone git://github.com/trondn/libmembase.git
$ cd libmembase
$ ./config/autorun.sh
$ ./configure --prefix=/opt/membase
$ make all install

Thursday, December 16, 2010

libmembase - a C interface to Membase

Membase is "on the wire" compatible with any memcached server if you connect to the standard memcached port (registered by myself back in 2009), so that you should should be able to access membase with any "memcapable" client. Backing this port is our membase proxy named moxi, and behind the scene it will do SASL authentication and proxy your requests to the correct membase server containing the item you want. One of the things that differs Membase from Memcached is that we store each item in a given vbucket that is mapped to a server. When you grow or shrink the cluster, membase will move the vbuckets to new servers.

There is no such thing as a free lunch, so accessing membase through moxi "costs" more than talking directly to the individual nodes yourself. We like to refer to such clients as "smart clients". As a developer on Memcached I need to test various stuff, so I went ahead and hacked together a quick prototype of such a library to ease my testing. Initially I wanted to extend libmemcached with this functionality, but that seemed to be a big (and risky) change I didn't have the guts to do at the time.

The current state of the library is far from production quality, and with a minimal list of supported features. So why announce it now? Well I don't think I'll find the time to implement everything myself, so I'm hoping that people will join me in adding features to the library when they need something that isn't there...

I've designed the library to be 100% callback based and integrated with libevent, making it easy for you to plug it into your application.

So let's say you want to create a TAP stream and listen to all of the modifications that happens in your cluster. All you need to do would be:

struct event_base *evbase = event_init();

   libmembase_t instance = libmembase_create(host, username, passwd, bucket, evbase);
   libmembase_connect(instance);

   libmembase_tap_filter_t filter;
   libmembase_callback_t callbacks = {
      .tap_mutation = tap_mutation
   };
   libmembase_set_callbacks(instance, &callbacks);
   libmembase_tap_cluster(instance, filter, true);

Then you would implement the tap callback function as:

static void tap_mutation(libmembase_t instance, const void *key, size_t nkey, const void *data, size_t nbytes, uint32_t flags, uint32_t exp, const void *es, size_t nes)
{
   // Do whatever you want with the object
}


And thats all you need to do to tap your entire cluster :-) Let's extend the example to tap multiple buckets from the same code.

struct event_base *evbase = event_init();

   libmembase_t instance1 = libmembase_create(host, username, passwd, bucket1, evbase);
   libmembase_t instance2 = libmembase_create(host, username, passwd, bucket2, evbase);
   libmembase_connect(instance1);
   libmembase_connect(instance2);

   libmembase_tap_filter_t filter;
   libmembase_callback_t callbacks = {
      .tap_mutation = tap_mutation
   };
   libmembase_set_callbacks(instance1, &callbacks);
   libmembase_set_callbacks(instance2, &callbacks);
   libmembase_tap_cluster(instance1, filter, false);
   libmembase_tap_cluster(instance2, filter, false);

   event_base_loop(evbase, 0);

The instance handle is passed to the callback function so you should be able to tell which bucket each mutation event belongs to.

As I said all of the functions in the API is callback based, so if you want to retrieve an object you have to register a callback for get before calling libmembase_mget. Ex:
libmembase_callback_t callbacks = {
        .get = get_callback
    };
    libmembase_set_callbacks(instance, &callbacks);
    libmembase_mget(instance, num_keys, (const void * const *)keys, nkey);

    // If you don't want to run your own event loop, you can call the following method
    // that will run all spooled commands and wait for their replies before breaking out
    // of the event loop
    libmembase_execute(instance);

The signature for the get callback looks like:
void get_callback(libmembase_t instance, libmembase_error_t error, const void *key, size_t nkey, const void *bytes, size_t nbytes, uint32_t flags, uint64_t cas)
{
   // do whatever you want...
}

So what is missing from the library right now?
  • Proper error handling. Right now I'm using asserts and abort() to handle error situations, causing your application to crash... you don't want that in production ;-)
  • Timeouts.. Right now it will only time out on TCP timeouts../
  • A lot of operations! I'm only supporting get/add/replace/set...
  • Fetch replicas..
  • Gracefully handle change in the vbucket list
  • +++

Do you feel like hacking on some of them?

Saturday, October 30, 2010

Running Moxi on Solaris

I have been working on getting membase up'n'running on OpenSolaris as a side project. Most of it is already in place, but there are still some Makefile issues to sort out. I thought that while we're waiting to complete that task, I could show you how to easily run moxi as a service controlled by SMF.

I've created some scripts to make it easier for you to build and install everything, so the first we need to do is to check out (or update your clone) of my tools repository:

trond@opensolaris> git clone git://github.com/trondn/tools.git
trond@opensolaris> cd tools/membase

Next up we need to create some new ZFS datasets for our moxi installation. I've created a script that creates the zfs datasets and set up the mountpoints:

trond@opensolaris> ./smf/moxi/setup.sh -u -z rpool

The -u option tells the script to create authorizations, profiles, users and groups we need, and the -z option tells the script to create the zfs filesystems in the zfs pool named rpool.

Next up we need to compile (and install) the source code. The directory /opt/membase is not writable for us, so let's change the ownership so I can install files there...:

trond@opensolaris> pfexec chown trond:staff /opt/membase
trond@opensolaris> ./setup.sh -d /opt/membase moxi
Download commit hook - Ok.
Checking out libmemcached (Bazaar) - Ok.
Checking out bucket_engine (git) - Ok.
Checking out ep-engine (git) - Ok.
Checking out libconflate (git) - Ok.
Checking out libvbucket (git) - Ok.
Checking out memcached (git) - Ok.
Checking out moxi (git) - Ok.
Checking out vbucketmigrator (git) - Ok.
Checking out membase-cli (git) - Ok.
Checking out ns_server (git) - Ok.
Checking out memcachetest (git) - Ok.
Configure build for SunOS
trond@opensolaris> cd moxi/SunOS
trond@opensolaris> make all install

Now we've got everything installed to /opt/membase, so let's change the ownership to membase:membase and install the SMF script to manage moxi:

trond@opensolaris> chown -R membase:membase /opt/membase
trond@opensolaris> cd ../../smf/moxi
trond@opensolaris> ./setup.sh -s
moxi installed as /lib/svc/method/moxi
moxi.xml installed as /var/svc/manifest/application/moxi.xml

So let's check out the configuration options we got for our new SMF service:

trond@opensolaris> svccfg
svc:> select moxi
svc:/application/database/moxi> listprop
manifestfiles                                        framework
manifestfiles/var_svc_manifest_application_moxi_xml  astring  /var/svc/manifest/application/moxi.xml
general                                              framework
general/action_authorization                         astring  solaris.smf.manage.moxi
general/entity_stability                             astring  Unstable
general/single_instance                              boolean  true
general/value_authorization                          astring  solaris.smf.value.moxi
multi-user-server                                    dependency
multi-user-server/entities                           fmri     svc:/milestone/multi-user-server
multi-user-server/grouping                           astring  require_all
multi-user-server/restart_on                         astring  none
multi-user-server/type                               astring  service
moxi                                                 application
moxi/corepattern                                     astring  /var/opt/membase/cores/core.%f.%p
moxi/downstream_max                                  astring  8
moxi/port                                            astring  11211
moxi/threads                                         astring  4
moxi/url                                             astring  http://membase:8091/pools/default/bucketStreaming/default
moxi/version                                         astring  1.6.0
tm_common_name                                       template
tm_common_name/C                                     ustring  Membase
tm_man_moxi                                          template
tm_man_moxi/manpath                                  astring  /opt/membase/share/man
tm_man_moxi/section                                  astring  1
tm_man_moxi/title                                    astring  moxi

You will most likely want to set the URL parameter to point to the bucket you want to use..

svc:/application/database/moxi> setprop moxi/url=http://myserver:8091/pools/default/bucketStreaming/default

Let's refresh the configuration and start the service:

trond@opensolaris> svccfg refresh moxi
trond@opensolaris> svcadm enable moxi
trond@opensolaris> svcs moxi
STATE          STIME    FMRI
online          9:45:41 svc:/application/database/moxi:moxi

Installing Python script from automake, Fixup :)

In my previous blog post I added a wrapper script to start the Python script, but it turns out that this script don't work unless you pass --libdir=something to configure. I didn't catch that originally because I always specify the library directory due to the fact that I'm building both 32bit and 64bit binaries on my Solaris machine.

The following script should address the problem:

#! /bin/sh
prefix=@prefix@
exec_prefix=@exec_prefix@
root=@libdir@/python

if test -z "${PYTHONPATH}"; then
   PYTHONPATH=$root
else
   PYTHONPATH=$root:${PYTHONPATH}
fi
export PYTHONPATH
exec $root/`basename $0`.py "$@"

Thursday, October 28, 2010

Installing Python scripts from automake...

I've been working on making it easier for developers to compile and install membase, and today I learned some more automake magic. I'm one of those developers who don't want to spend a lot of time working on the build system, I want to spend my time working on the code. At the same time I don't want to do unnecessary boring manual work that the build system should do for me.

Parts of membase is implemented in Python, and I've been trying to figure out how to install those pieces. I don't like to mess up the /bin directory with "library" files, so I needed a way to package the Python bits better. I've been using a wrapper script that sets the PYTHONPATH variable earlier, but I've never tried to integrate that into an automake generated makefile.

As always I started out asking google for help, but I didn't end up with a good and easy example so I ended up reading through the automake manual. It turns out that it's fairly easy to do exactly what I want, so I decided to share the knowledge in a blog post :-)

We don't want to hardcode the path to our binary anywhere, so the first thing we need to do is to update configure.ac to also generate our wrapper script:

AC_CONFIG_FILES(Makefile python_wrapper)

I've got multiple programs implemented with Python, and I don't want to create a ton of wrappers, so my python_wrapper.in looks like:

#! /bin/sh
if test -z "${PYTHONPATH}"; then
   PYTHONPATH=@libdir@/python
else
   PYTHONPATH=@libdir@/python:${PYTHONPATH}
fi
export PYTHONPATH
exec @libdir@/python/`basename $0`.py "$@"

This means that if I install this script as /opt/membase/bin/stats, it will try to execute /opt/membase/lib/python/stats.py with the same arguments. So let's go ahead and add a rule to Makefile.am to generate the scripts with the correct names:

PYTHON_TOOLS=stats
${PYTHON_TOOLS}: python_wrapper
    cp $< $@

BUILT_SOURCES += ${PYTHON_TOOLS}
CLEANFILES+= ${PYTHON_TOOLS}
bin_SCRIPTS+= ${PYTHON_TOOLS}

 Now we've got the wrapper script in place, and we've generated all of the scripts to start our programs. The next thing up would be to create the destination directory for the python bits, and install all of them there. To do so we need to create a variable that ends with "dir" to contain the name of the directory. Let's name our "pythonlibdir" and put it in a subdirectory named python of the specified libdir:

pythonlibdir=$(libdir)/python


Finally we need to list all of the files we want to go there:

pythonlib_DATA= \
                mc_bin_client.py \
                mc_bin_server.py \
                memcacheConstants.py
  
pythonlib_SCRIPTS= \
                stats.py \

The reason I use pythonlib_SCRIPTS for the last one is because I want the execute bit set on file.

Sunday, October 17, 2010

Writing your own storage engine for Memcached, part 3

Right now we've got an engine capable of running get and set load, but it is doing synchrounus filesystem IO. We can't serve our client faster than we can read the item from disk, but we might serve other connections while we're reading the item off disk.

In this entry we're going to fix our get and store method so that they don't block the engine API. As I've said earlier, the intention of this tutorial is to focus on the engine API. That means I'm not going to try to make an effective design, because that could distract the focus from what I'm trying to explain. If people are interested in how we could optimize this, I could add a second part of the tutorial in the future... Just let me know.

In order to implement asynchronous operations in our engine, we need to make use of the API the server makes available to us in create_instance. Let's extend our engine structure to keep track of the server API:

struct fs_engine {
   ENGINE_HANDLE_V1 engine;
   SERVER_HANDLE_V1 sapi;
};

...

MEMCACHED_PUBLIC_API
ENGINE_ERROR_CODE create_instance(uint64_t interface,
                                  GET_SERVER_API get_server_api,
                                  ENGINE_HANDLE **handle)
{

...

   h->engine.item_set_cas = fs_item_set_cas;
   h->engine.get_item_info = fs_get_item_info;
   h->sapi = *get_server_api();

...

      

To implement an asynchronous function in the engine interface, the engine needs to dispatch the request to another thread before it return ENGINE_EWOULDBLOCK from the engine function. When the backend is done processing the result, it notifies the memcached core by using the notify_io_complete function in the server interface. If an error occurred while processing the request, the memcached core will return the error message to the client. If your engine called notify_io_complete with ENGINE_SUCCESS, the memcached core will call the engine interface function once more with the same argument as the
first time.

If you look at the server api, you'll see that the it got an interface for storing an engine-specific pointer. This will make our life easier when we want to implement async io. So let's go ahead and update our fs_get method:

static ENGINE_ERROR_CODE fs_get(ENGINE_HANDLE* handle,
                                const void* cookie,
                                item** item,
                                const void* key,
                                const int nkey,
                                uint16_t vbucket)
{
   struct fs_engine *engine = (struct fs_engine *)engine;
   void *res = engine->sapi.cookie->get_engine_specific(cookie);
   if (res != NULL) {
      *item = res;
      engine->sapi.cookie->store_engine_specific(cookie, NULL);
      return ENGINE_SUCCESS;

   }

...
      

The next thing we need to do is to create a function that runs asynchronously and stores the result in the engine_specific setting for the cookie. Since we're going to use async tasks for all of the engine methods, let's go ahead and create a function to run tasks in another thread:

static ENGINE_ERROR_CODE execute(struct task *task)
{
   pthread_attr_t attr;
   pthread_t tid;

   if (pthread_attr_init(&attr) != 0 ||
       pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) != 0 ||
       pthread_create(&tid, &attr, task->callback, task) != 0) {
      return ENGINE_FAILED;
   }

   return ENGINE_EWOULDBLOCK;
}
      

As you can see from the code I'm going to create a new thread to execute each operation. This isn't very efficient, because creating a new thread got a substansial overhead. In your design you would probably want a pool of threads to run your tasks.

The newly created thread would run the specialized callback with a pointer to the task as it's only argument. So what does this task structure look like?

struct task {
   struct fs_engine *engine; /* Pointer to the engine */
   const void *cookie; /* The cookie requesting the operation */
   void *(*callback)(void *arg);
   union {
      struct {
         char key[PATH_MAX];
         size_t nkey;
      } get; /* Data used by the get operation */
      struct {
         item *item;
         ENGINE_STORE_OPERATION operation;
      } store; /* Data used by the store operation */
   } data;
};
      
So let's finish up fs_get:

static ENGINE_ERROR_CODE fs_get(ENGINE_HANDLE* handle,
                                const void* cookie,
                                item** item,
                                const void* key,
                                const int nkey,
                                uint16_t vbucket)
{
   struct fs_engine *engine = (struct fs_engine *)handle;
   /* Check to see if this is the callback from an earlier ewouldblock */
   void *res = engine->sapi.cookie->get_engine_specific(cookie);
   if (res != NULL) {
      *item = res;
      engine->sapi.cookie->store_engine_specific(cookie, NULL);
      return ENGINE_SUCCESS;
   }

   /* We don't support keys longer than PATH_MAX */
   if (nkey >= PATH_MAX) {
      return ENGINE_FAILED;
   }

   /* Set up the callback struct */
   struct task *task = calloc(1, sizeof(*task));
   if (task == NULL) {
      return ENGINE_ENOMEM;
   }

   task->engine = (struct fs_engine *)handle;
   task->cookie = cookie;
   task->callback = fs_get_callback;
   memcpy(task->data.get.key, key, nkey);
   task->data.get.nkey = nkey;

   ENGINE_ERROR_CODE ret = execute(task);
   if (ret != ENGINE_EWOULDBLOCK) {
      free(task);
   }
   return ret;
}
      

If you look at the code above, you'll see that we specify gs_get_callback as the function to execute. So let's go ahead and implement the callback:

static void *fs_get_callback(void *arg)
{
   struct task *task = arg;
   char *fname = task->data.get.key;
   task->data.get.key[task->data.get.nkey] = '\0';

   struct stat st;
   if (stat(fname, &st) == -1) {
      task->engine->sapi.cookie->notify_io_complete(task->cookie,
                                                    ENGINE_KEY_ENOENT);
      free(task);
      return NULL;
   }

   struct fs_item* it = NULL;
   ENGINE_ERROR_CODE ret = fs_allocate((ENGINE_HANDLE*)task->engine,
                                       task->cookie, (void**)&it,
                                       task->data.get.key,
                                       task->data.get.nkey,
                                       st.st_size, 0, 0);
   if (ret != ENGINE_SUCCESS) {
      task->engine->sapi.cookie->notify_io_complete(task->cookie,
                                                    ENGINE_ENOMEM);
      free(task);
      return NULL;
   }

   FILE *fp = fopen(fname, "r");
   if (fp == NULL) {
      fs_release((ENGINE_HANDLE*)task->engine, task->cookie, it);
      task->engine->sapi.cookie->notify_io_complete(task->cookie,
                                                    ENGINE_FAILED);
      free(task);
      return NULL;
   }

   size_t nr = fread(it->data, 1, it->ndata, fp);
   fclose(fp);
   if (nr != it->ndata) {
      fs_release((ENGINE_HANDLE*)task->engine, task->cookie, it);
      task->engine->sapi.cookie->notify_io_complete(task->cookie,
                                                    ENGINE_FAILED);
      free(task);
      return NULL;
   }

   task->engine->sapi.cookie->store_engine_specific(task->cookie, it);
   task->engine->sapi.cookie->notify_io_complete(task->cookie,
                                                 ENGINE_SUCCESS);
   return NULL;
}
      

As you see it's quite easy to add asynchronous support for the engine functions. Let's go ahead and do the same for fs_store:

static ENGINE_ERROR_CODE fs_store(ENGINE_HANDLE* handle,
                                  const void *cookie,
                                  item* item,
                                  uint64_t *cas,
                                  ENGINE_STORE_OPERATION operation,
                                  uint16_t vbucket)
{
   struct fs_engine *engine = (struct fs_engine *)handle;
   /* Check to see if this is the callback from an earlier ewouldblock */
   void *res = engine->sapi.cookie->get_engine_specific(cookie);
   if (res != NULL) {
      *cas = 0;
      engine->sapi.cookie->store_engine_specific(cookie, NULL);
      return ENGINE_SUCCESS;
   }


   /* Set up the callback struct */
   struct task *task = calloc(1, sizeof(*task));
   if (task == NULL) {
      return ENGINE_ENOMEM;
   }

   task->engine = (struct fs_engine *)handle;
   task->cookie = cookie;
   task->callback = fs_store_callback;
   task->data.store.item = item;
   task->data.store.operation = operation;

   ENGINE_ERROR_CODE ret = execute(task);
   if (ret != ENGINE_EWOULDBLOCK) {
      free(task);
   }
   return ret;
}

And fs_store_callback looks like the following:

static void *fs_store_callback(void *arg)
{
   struct task *task = arg;
   struct fs_item* it = task->data.store.item;
   char fname[it->nkey + 1];
   memcpy(fname, it->key, it->nkey);
   fname[it->nkey] = '\0';

   FILE *fp = fopen(fname, "w");
   if (fp == NULL) {
      task->engine->sapi.cookie->notify_io_complete(task->cookie,
                                                    ENGINE_NOT_STORED);
      free(task);
      return NULL;
   }

   size_t nw = fwrite(it->data, 1, it->ndata, fp);
   fclose(fp);
   if (nw != it->ndata) {
      remove(fname);
      task->engine->sapi.cookie->notify_io_complete(task->cookie,
                                                    ENGINE_NOT_STORED);
      free(task);
      return NULL;
   }

   task->engine->sapi.cookie->store_engine_specific(task->cookie, it);
   task->engine->sapi.cookie->notify_io_complete(task->cookie,
                                                 ENGINE_SUCCESS);
   return NULL;
}

If you look closely at the code above you'll see that we still don't differentiate between add/set/replace, but we'll fix that in the next session.