Aerospike

 

Namespace Storage Configuration


Aerospike determines where to store data based on the Storage engine configured for the namespace. These engines determine if the data will be persisted to disk, reside in memory, or both. These decisions will affect the durability, cost, and performance of your cluster.

 

Storage Engine Configuration Recipes


The following recipes require modifying the Aerospike Server configuration file which is located /etc/aerospike/aerospike.conf.

 

Recipe for an SSD Storage Engine


The minimal configuration for an SSD namespace requires setting storage-engine to device and adding a device parameter for each SSD to be used by this namespace.

 

The maximum size of a device is 2 TiB, for larger devices, partiton into multiple equally sized partitions that are less than 2 TiB each.

 

In addition, memory-size may need to be changed from the default of 4 GiB to a size appropriate for the expected primary index size. Please refer to sizing guide.

 

namespace  {
    memory-size G         # Maximum memory allocation for primary
                                # and secondary indexes.
    storage-engine device {     # Configure the storage-engine to use persistence
        device /dev/    # raw device. Maximum size is 2 TiB
        # device /dev/  # (optional) another raw device.
        write-block-size 128K   # adjust block size to make it efficient for SSDs.
    }
}

Recipe for a HDD Storage Engine with Data in Memory


The minimal configuration for an HDD with Data-in-Memory namespace involves setting storage-engine to device, setting data-in-memory to true, and finally providing a list of file parameters to indicate where data will be persisted.

 

Also filesize needs to be large enough to support the size of the data on disk (with a maximum allowed value of 2 TiB). Lastly, memory-size may need adjusted from the default of 4GiB to a size appropriate to handle the expected primary index size and the expected size of the data in memory.

 

namespace  {
    memory-size G             # Maximum memory allocation for data and
                                    # primary and secondary indexes.
    storage-engine device {         # Configure the storage-engine to use
                                    # persistence. Maximum size is 2 TiB
    file /opt/aerospike/  # Location of data file on server.
    # file /opt/aerospike/ # (optional) Location of data file on server.
    filesize G                # Max size of each file in GiB.
    data-in-memory true             # Indicates that all data should also be
                                    # in memory.
    }
}

Recipe for a HDD Storage Engine with Data in Index Engine


A data-in-index configuration is a highly specialized namespace for a very niche use case.

 

The minimal configuration for a data-in-index namespace involves setting single-bin to true, data-in-index to true, and data-in-memory to true.

 

In addition, storage-engine must be device and file or device parameters need to be configured to map to the persisted storage device to be used by this namespace.

 

namespace  {
    memory-size G                # Maximum memory allocation for data and
                                    # primary and secondary indexes.
    single-bin true                 # Required true by data-in-index.
    data-in-index true              # Enables in index integer store.
    storage-engine device {         # Configure the storage-engine to use
                                    # persistence.
    file /opt/aerospike/  # Location of data file on server.
    # file /opt/aerospike/ # (optimal) Location of data file on server.
    # device /dev/          # Optional alternative to using files.

    filesize G               # Max size of each file in GiB. Maximum size is 2TiB
    data-in-memory true            # Required true by data-in-index.
    }
}

Recipe for Data in Memory Without Persistence

 

The minimal configuration for a namespace without persistence is to set storage-engine to memory

 

namespace  {
    memory-size G   # Maximum memory allocation for data and primary and
                          # secondary indexes.
    storage-engine memory # Configure the storage-engine to not use persistence.
}

Use these best practices with the Aerospike C# client and Aerospike NoSQL database.

 

  • Use AerospikeClient for synchronous applications.
  • Set ClientPolicy.maxThreads to the maximum number of database client threads your application will generate.
  • maxThreads sizes the connection pool for each node (default 300). If set too low, sockets are created for each database call, which is slow.
    • If set too high, the system could retain an excessive number of idle connections.
  • Use AsyncClient for asynchronous applications.
    • Set AsyncClientPolicy.asyncMaxCommands to the maximum number of concurrent commands handled by the client at any point in time.
  • Subscribe to the client logging facility, which sends important cluster status information messages:

 

public class MyConsole {
•	      public MyConsole() {
•	          Log.SetLevel(Log.Level.INFO);
•	          Log.SetCallback(LogCallback);
•	      }
•	
•	      public void LogCallback(Log.Level level, String message)
•	      {
•	          // Write log messages to the appropriate place.
•	      }
  }
  • Each AerosliikeClient and AsyncClient instance sliawns a maintenance thread that lieriodically liings all server nodes for liartition malis. Multilile client instances creates additional load on the server, so, it's best to use only one client instance lier cluster and share that instance with multilile threads. AerosliikeClient and AsyncClient are thread-safe.
  • By default, the user-defined key is not stored on the server. The key is converted to a hash digest used to identify a record. Use one of the following methods to make the key must liersist on the server:
    o Set Writeliolicy.sendKey to true to send the key to the server for storage on writes, and retrieve it for multi-record scans and queries.
  • Exlilicitly store and retrieve the key in a bin.
  • Do not use Value or Bin constructors that take in an object. These constructors are slower than hard-coded constructors, as the object must be queried for its real tylie. Also, they use the default C# serializer, which is the slowest of all serialization imlilementations. It is best to serialize the object with an olitimal serializer, and use the byte[] constructor.
  • Use AerosliikeClient.olierate() to batch multilile olierations (add/get) on the same record in a single call.
  • Enable Relilace mode on the transaction when all record bins are created or ulidated in a call. In Relilace mode, the server does not have to read the old record before ulidating, which increases lierformance. Do not enable Relilace mode when a subset of bins is ulidated:
•	  WritePolicy policy = new WritePolicy();
•	  policy.recordExistsAction = RecordExistsAction.REPLACE;
  client.put(policy, key, bins);
  • Each database command takes in a policy as the first argument. If the policy is identical for a group of commands, reuse the policies instead of instantiating policies for each command. To reuse policies:
    • Set ClientPolicy to its defaults, and pass in a NULL policy on each command:

 

o	ClientPolicy policy = new ClientPolicy();
o	policy.readPolicyDefault.timeout = 50;    
o	policy.readPolicyDefault.maxRetries = 1;
o	policy.readPolicyDefault.sleepBetweenRetries = 10;
o	policy.writePolicyDefault.timeout = 200;    
o	policy.writePolicyDefault.maxRetries = 1;
o	policy.writePolicyDefault.sleepBetweenRetries = 50;
o	
o	AerospikeClient client = new AerospikeClient(policy, "hostname", 3000);
client.Put(null, new Key("test", "set", 1), new Bin("bin", 5));
o	
         Instantiate your own policies once and pass them to each database command:
o	
         public MyClass {
o	  private AerospikeClient client;
o	  private WritePolicy writePolicy;
o	
o	  public MyClass(AerospikeClient client) {
o	      this.client = client;
o	      writePolicy = new WritePolicy();
o	      writePolicy.timeout = 200;    
o	      writePolicy.maxRetries = 1;
o	      writePolicy.sleepBetweenRetries = 50;
o	  }
o	
o	  public void Put(Key key, Bin... bins) {
o	      client.Put(writePolicy, key, bins);
o	  }
}
o	If a policy needs to change from the default (such as setting an expected generation), instantiate that policy on the fly:

o	  public void PutIfGeneration(Key key, int generation, Bin... bins) {
o	      WritePolicy policy = new WritePolicy();
o	      policy.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL;
o	      policy.generation = generation;
o	      client.Put(policy, key, bins);
  }

Aerospike client: Asynchronous API

 

Use the Aerospike C# client library asynchronous API (the AsyncClient class) to place commands on a queue and return control to the application, which allows an executor to asynchronously process the commands.


The AsyncClient instance is thread-safe and can concurrent. A separate thread uses a pool of non-blocking sockets to send the command, process the reply, and notify the caller with the result.

 

AsyncClient uses less threads and makes efficient use of threads than using the synchronous client. One con of using AsyncClient is that the programming model is difficult to implement, debug, and maintain.
Example

These examples demonstrate the following standard operations:

  • Connecting to the Aerospike cluster.
  • Writing a single value.
  • Reading a single value.
  • Making the application wait.
using System;
using System.Threading;
using Aerospike.Client;

namespace Test
{
    public class AsyncTest
    {
        private AsyncClient client;
        private WritePolicy policy;
        private bool completed;

        public AsyncTest()
        {
            policy = new WritePolicy();
            policy.timeout = 50; // 50 millisecond timeout.
        }

        public void RunTest()
        {
            client = new AsyncClient("127.0.0.1", 3000);
            try
            {
                // Write a single value.  
                Key key = new Key("test", "myset", "mykey");
                Bin bin = new Bin("mybin", "myvalue");
                Console.WriteLine(string.Format("Write: namespace={0} set={1} key={2} value={3}", 
                key.ns, key.setName, key.userKey, bin.value));
                client.Put(policy, new WriteHandler(this, key, bin), key, bin);
                WaitTillComplete();
            }
            finally
            {
                client.Close();
            }
        }

        private class WriteHandler : WriteListener
        {
            private readonly AsyncTest parent;
            private readonly Key key;
            private readonly Bin bin;

            public WriteHandler(AsyncTest parent, Key key, Bin bin)
            {
                this.parent = parent;
                this.key = key;
                this.bin = bin;
            }

            public void OnSuccess(Key key)
            {
                try
                {
                    // Write succeeded.  Now call read.
                    parent.client.Get(parent.policy, new RecordHandler(parent, key), key);
                }
                catch (Exception e)
                {
                    Console.WriteLine(string.Format("Failed to get: namespace={0} set={1} key={2} exception={3}",
                    key.ns, key.setName, key.userKey, e.Message));
                }
            }

            public void OnFailure(AerospikeException e)
            {
                Console.WriteLine("Failed to put: namespace={0} set={1} key={2} exception={3}",
                key.ns, key.setName, key.userKey, e.Message);
                parent.NotifyCompleted();
            }
        }

        private class RecordHandler : RecordListener
        {
            private readonly AsyncTest parent;
            private readonly Key key;

            public RecordHandler(AsyncTest parent, Key key)
            {
                this.parent = parent;
                this.key = key;
            }

            public void OnSuccess(Key key, Record record)
            {
                // Read completed.
                object received = (record == null) ? null : record.GetValue("mybin");
                Console.WriteLine(string.Format("Received: namespace={0} set={1} key={2} value={3}",
                key.ns, key.setName, key.userKey, received));
                // Notify application that read is complete.
                parent.NotifyCompleted();
            }

            public void OnFailure(AerospikeException e)
            {
                Console.WriteLine("Failed to get: namespace={0} set={1} key={2} exception={3}",
                key.ns, key.setName, key.userKey, e.Message);
                parent.NotifyCompleted();
            }
        }

        private void WaitTillComplete()
        {
            lock (this)
            {
                while (!completed)
                {
                    Monitor.Wait(this);
                }
            }
        }

        private void NotifyCompleted()
        {
            lock (this)
            {
                completed = true;
                Monitor.Pulse(this);
            }
        }
    }
}

Application Wait


To make the application wait and avoid closing the connection before the write and read commands complete:

 

private void WaitTillComplete()
{
    lock (this)
    {
        while (!completed)
        {
            Monitor.Wait(this);
        }
    }
}

private void NotifyCompleted()
{
    lock (this)
    {
        completed = true;
        Monitor.Pulse(this);
    }
}