In-Memory Key-Value Databases: Redis (Initial Evaluation)

posted in Septopus for project Unsettled World
Published October 30, 2018
C#
Advertisement

This is the first of at least two posts regarding my evaluation of the addition of an In-Memory(RAM) Key-Value Database system to my server architecture.  If you're unfamiliar, check out https://en.wikipedia.org/wiki/Key-value_database, for some broad spectrum info. 

I'm beginning with Redis, it runs on Linux which is my scale-up server platform of choice, and this database will be the key to the scalability of my server architecture.  It's also open-source and has been around for a while now.

Download: https://redis.io/download

Documentation: https://redis.io/documentation

Installation was pretty straight forward, I created a Centos7 VM using Oracle's VirtualBox software (https://www.virtualbox.org/ one of the easiest ways to use vms locally I've used on Windows),

It has been a couple years since I worked on a  Linux machine, but I still managed to figure it out:

redis_getmakeinstall.png.ca0a106d9d57b35e821e5961e4bb7fd0.png

redis_working_install.png.d082d4a447c013bf003a49bdf33ec0f3.png

So, that's a working installation.  Easy as 1,2,3..

Okay, a simple benchmark,VM is using a single core and 4G of ram, and I'm using the StackExchange.Redis client from NuGet:


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StackExchange.Redis;

namespace TestRedis
{
    class Program
    {
        static Dictionary<string, string> TestDataSet = new Dictionary<string, string>();
        static ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("192.168.56.101");

        static void Main(string[] args)
        {
            //Build Test Dataset
            Console.Write("Building Test Data Set.");
            int cnt = 0;
            for (int i = 0; i < 1000000; i++)
            {
                cnt++;
                TestDataSet.Add(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
                if (cnt > 999)
                {
                    cnt = 0;
                    Console.Write(i.ToString() + "_");
                }
            }
            Console.WriteLine("Done");

            IDatabase db = redis.GetDatabase();

            Stopwatch sw = new Stopwatch();
            sw.Start();
            //Console.WriteLine("Starting 'write' Benchmark");
            //foreach (KeyValuePair<string, string> kv in TestDataSet)
            //{
            //    db.StringSet(kv.Key, kv.Value);
            //}
            Console.WriteLine("Starting Parallel 'write' Benchmark.");
            Parallel.ForEach(TestDataSet, td =>
            {
                db.StringSet(td.Key, td.Value);
            });
            Console.WriteLine("TIME: " + (sw.ElapsedMilliseconds / 1000).ToString());
            sw.Restart();
            Console.WriteLine("Testing Read Verify.");
            //foreach (KeyValuePair<string, string> kv in TestDataSet)
            //{
            //    if (db.StringGet(kv.Key) != kv.Value)
            //    {
            //        Console.WriteLine("Error Getting Value for Key: " + kv.Key);
            //    }
            //}
            Console.WriteLine("Testing Parallel Read Verify");
            Parallel.ForEach(TestDataSet, td =>
            {
                if (db.StringGet(td.Key) != td.Value)
                {
                    Console.WriteLine("Error Getting Value for Key: " + td.Key);
                }
            });
            Console.WriteLine("TIME: " + (sw.ElapsedMilliseconds / 1000).ToString());
            sw.Stop();
            Console.WriteLine("Press any key..");
            Console.ReadKey();
        }
    }
}

Basically I'm creating a 1million record Dictionary filled with Guids(keys & values). Then testing both a standard foreach and a parallel.foreach loop to write to Redis and then read from Redis/compare to dictionary value.

Result times are all in seconds.

Standard foreach loop(iterating through dictionary to insert into redis):

Redis_1000000_standardforeach.png.75b7b64679f16b89ada68115ac442d5b.png

Almost 5minutes to write and another almost 5 minutes to verify..

Using the Parallel.Foreach method:

Redis_1000000_parallelforeach.png.2ea0e6d400ab85a85a02c2982c8806d4.png

and then again with 1M records already present in DB:

Redis_1M-2M_parallelforeach.png.39e75586711984ce6570619762edbf99.png

And then with 2M records present in DB:

Redis_2M-3M_parallelforeach.png.4e225e1782bdde3854bd4498b250c0c3.png

And then with 3M:

Redis_3M-4M_parallelforeach.png.c2bc6dca4584500f71641fa0393712f5.png

and 4M:

Redis_4M-5M_parallelforeach.png.74c9f8b064716c150bea41adb10ed4fb.png

Well, you get the picture.. ;)

So far, I think I really like Redis. 

And now with the async "fire and forget" set method (db write without response), on empty db:


Console.WriteLine("Starting Parallel 'write' Benchmark.");
Parallel.ForEach(TestDataSet, td =>
{
	//db.StringSet(td.Key, td.Value);
	db.StringSetAsync(td.Key, td.Value, flags: CommandFlags.FireAndForget);
});

Easy enough code change.

Redis_1M_fireandforgetSet.png.f9b4b3e8a77c9935331c8e45509889e5.png

Well, that's pretty fancy.  1 Million records added in under 10 seconds, and not a single error.. :D

So, even though I think this is my winning candidate, I should at least perform the same benchmarks on NCache to see if it somehow blows Redis out of the water. 

So, keep posted, as NCache will be my next victim.  It may be somewhat unfair though since I'll be installing NCache on my Windows machine directly and Redis was sequestered into a tiny vm with very limited resources, so if I need to(if NCache is ridiculously faster ootb) I'll be re-running these benchmarks again on a bigger VM. ;)

 

A few more benchmark results(11/1/18):

I've repeated the final "fire and forget" with a couple bigger data sets.  Same VM, so 1 single core, and 4G ram.

2 Million Records:

redis_2m_faf_midreadverifytop.png.af0af493bd74c47132662e2d7449ba74.png

This shows the top output snapped somewhere in the middle of the "write" test.

 

redis_2m_faf.thumb.png.165173d3dd3687bc9525460e2e9c2f02.png

This shows the benchmark output, the Redis cli console, and top for the Redis server.  Post benchmark.

 

Now 10Million Records:

redis_10m_faf_midinsertop.png.22eec6eb80a4160070ddcd80f8847cf1.png

Redis Server Top output somewhere in the middle of the "write" test.  We're hitting that single core ceiling here, but it's trucking along anyhow.

 

redis_10m_faf_midreadverify.thumb.png.28104d98fdaf8e9254b66c2dc7d9b523.png

Here's a snap from somewhere in the middle of the read/verify test.  Working hard, but not taxing that single core.  Up to 2G of ram now, 10m records.. not too bad.

 

redis_10m_faf.thumb.png.746e5bb8acdb187c675c5a8ff2d40f6d.png

And the completed test.  Frankly, I'm impressed.  It pegged the single core for a fraction of that 83 seconds, but it didn't lose a single record in the process.

 

redis_1m_10m_faf.thumb.png.c756ef1f88bc7b531e3c16ad0efb9aa8.png

And the final test result: a 1million record write, with 10million records already in the DB. :D  Boom, still no errors.

 

So, let us really break it good.

I rewrote the benchmark script to Parallel.Foreach through 10 individual 1million record benchmark tests.  Roughly simulating separate concurrent clients to the Redis server.

And here is where we start finding limits in (this) installation/configuration.

Redis_Excepted.png.b27abbe3c9128ccf1ba7b0ae62e1adaf.png

Redis_Excepted_Multiple_threads.thumb.png.2c1cfdba1560c5f5b860ae4ed4712c5a.png

So, it got to about 3Million records inserted from 6 different connections before the load on the server caused the additional connections to time out(more or less).

Okay, lets try it with 6 connections.  hmm, I have 6 servers.. hmm. ;)

redis_1mX6_midwrite.thumb.png.9834a4e49fc4fb37d7af85c8689adf25.png

Mid write, good to go.

 

redis_1mX6_midreadverify.thumb.png.b81abdd2aa36ed092d0c8824bf12868a.png

Mid read/verify, good to go.

 

redis_1mX6_noworries.thumb.png.bd05f32ec4906da4cb8e7a26f0fac6fc.png

And would you look at that, no exceptions, no read errors. :D  

0 likes 1 comments

Comments

evolutional

A general observation of your use of Parallel.Foreach and StringSetAsync; the test you're performing there is actually one of how fast you are putting work onto the task scheduler - you're not awaiting the completion of the task (which itself could be problematic in the event of an exception).

November 01, 2018 10:35 AM
Septopus
5 hours ago, evolutional said:

A general observation of your use of Parallel.Foreach and StringSetAsync; the test you're performing there is actually one of how fast you are putting work onto the task scheduler - you're not awaiting the completion of the task (which itself could be problematic in the event of an exception).

Very true indeed.  Which was why it wasn't the only test.  Regardless, out of the box Redis didn't stumble.  I wasn't really going for a complete benchmark test here either.  More establishing a behavioral baseline which I could use to compare to other products.  I'll be finding its actual hard limits later, I'm sure.  That being said though, if I ever have 1000000 records that need to go into the database in under 10 seconds, I'll probably already have clusters of servers to split the load, so it will likely never be actually used like that in production anyhow.

November 01, 2018 03:57 PM
Septopus

Also important to note, I wasn't attempting to benchmark the abilities of Redis to handle errors.  I was benchmarking its ability to handle data WITHOUT errors. ;)  I should have reiterated that test with multiple millions of records until it started to fail on retrieval.  That would give me the upper limit if Redis' capabilities.  And now that I think about it, I want to know that information. 

Thanks!

November 01, 2018 04:04 PM
Septopus

Many more benchmark results added(to this blog article) today. ;)

 

November 01, 2018 06:27 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement