Redis (REmote DIctionary Server) is an open source in-memory data structures server. Similarly to key-value stores, like Memcached, data on Redis is held in key-value pairs. Differently though, while in key-value stores both the key and the value are strings, in Redis the key can be any binary sequence, like a string, but also a digest (e.g. the output of a SHA-2 function) and the value can be of different types, among them:
- Lists, e.g.:
- Sets, e.g.:
- Hashes, e.g.:
- Bit arrays/bitmaps, e.g.:
10000001010010 # up to 2^32 different bits
Because values are typed, Redis is capable of manipulating the content accordingly, e.g.: prepending or appending new elements to a list, computing the intersection between two sets, replacing single elements in a hash, etc.
Azure Cache for Redis is a managed in-memory data store service, based on Redis, offered by Microsoft Azure.
I have recently had the opportunity to work on an enterprise software system, featuring a traditional 3-tier architecture, fully hosted in Azure, made of the following components:
- web and mobile application clients
- mid-tier backed by a Node.js application, leveraging the Express framework and Azure Mobile Apps SDK
- data layer, backed by a General Purpose, Gen5, 2 vCores Azure SQL database
Despite the optimisations performed on tables and queries, the latency of client calls hitting a few specific tables was still considered too high. Hence, adopting the cache-aside pattern leveraging Azure Cache for Redis was explored as a possible solution and the original vs. new behaviour profiled.
Creating the cache in Azure
Setting up Redis in Azure is pretty straightforward, as detailed in the quickstart documentation. After completion, host name, ports, and access keys are available to be used by external applications for connection. The tier selected for the development and benchmarking configuration was Basic C0.
Changes to the mid-tier source code
Upon request reception, the related path is serialised into a string, which is used as a key in Redis: the key is checked for existence in the cache and, in case of a hit, returned to the client. Otherwise, in case of a miss, the database is queried, the result returned to the client and the key-value pair inserted into the cache.
The data held in the tables of interest exhibits a synchronous, low frequency update, which allows the setting of a long time to live for the related cache keys (6 hours).
To benchmark the solution including Redis vs. the original one, the keys/requests were extracted from the cache, close to the end of the expiration window, to maximise the representativeness of the sample. The requests were then utilised as input to the profiling Apache Benchmark (AB) tool, which issues requests against the given endpoints and reports on timings.
The tool used to extract the keys is the redis-cli. Given the limited support from the Azure Console, to run commands against the Redis service, a Redis Docker image was started locally and the relevant redis-cli commands were executed to connect to the remote cache and download the keys:
AB is the tool chosen for profiling the responsiveness of the server. It simulates a client behaviour by exercising a server’s HTTP endpoints. Between the most notable ones are:
- n : the number of requests
- c : the degree of parallelism (the number of multiple requests to perform at a time, the default is one)
The forecasted maximum number of concurrent users in the system under examination is 20.
A sample report looks like this:
Concurrency Level: 1 Time taken for tests: 14.522 seconds Complete requests: 10 Failed requests: 0 Total transferred: 1241440 bytes HTML transferred: 1231310 bytes Requests per second: 0.69 [#/sec] (mean) Time per request: 1452.156 [ms] (mean) Time per request: 1452.156 [ms] (mean, across all concurrent requests) Transfer rate: 83.49 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 140 164 29.6 161 244 Processing: 1109 1288 169.8 1280 1685 Waiting: 968 1154 167.7 1167 1538 Total: 1271 1452 175.7 1442 1851 Percentage of the requests served within a certain time (ms) 50% 1442 66% 1455 75% 1558 80% 1588 90% 1851 95% 1851 98% 1851 99% 1851 100% 1851 (longest request)
Leveraging AB, for both the with/without Redis scenarios, a bash script was implemented to:
- retrieve the keys/urls
- loop through those
- target each one, running the AB command
- store the output in a file
- parse the output to extract the mean and standard deviation of the latency
The output of the previous step is a set of 2 files, representing the latency for the two solutions, with and without cache. The format of the files is as follows:
mean sd url 1452 175.7 0 1429 96.6 1 1541 287.6 2 1224 57.6 3 1241 153.0 4 ...
Charting the results
Last, a small script in R plots, in the same graph, the data sourced from the two files:
Here are the charts, produced by running the bash script with different values of total_ab_requests and parallel_ab_requests (assigned to AB’s n and c parameters respectively):
Introducing a cache in the mid-tier layer has reduced the backend latency by a factor ranging from a few times to hundreds of times, depending on the degree of parallelism of the requests. It is worth noting, that with the increase of parallel requests, the standard deviation increases too, reducing the confidence of fulfilling the client requests in a deterministic time frame. In the updates to the Node.js service described previously, Redis was used as a pure key-value store. Further enhancements will be explored, to exploit its capability to accept and manipulate data structures hosted in the values.