StackExchange redis client très lent par rapport aux tests de référence

j'implémente une couche de mise en cache Redis en utilisant le client Redis Stackexchange et la performance en ce moment est proche de l'inutilisable.

j'ai un environnement local où l'application web et le serveur redis tournent sur la même machine. J'ai lancé le test Redis benchmark contre mon serveur Redis et les résultats étaient en fait vraiment bons (j'inclus juste les opérations set et get dans mon écriture):

C:Program FilesRedis>redis-benchmark -n 100000
====== PING_INLINE ======
  100000 requests completed in 0.88 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

====== SET ======
  100000 requests completed in 0.89 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

99.70% <= 1 milliseconds
99.90% <= 2 milliseconds
100.00% <= 3 milliseconds
111982.08 requests per second

====== GET ======
  100000 requests completed in 0.81 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

99.87% <= 1 milliseconds
99.98% <= 2 milliseconds
100.00% <= 2 milliseconds
124069.48 requests per second

donc, selon les points de repère que je regarde plus de 100 000 ensembles et 100 000 gets, par seconde. J'ai écrit un test unitaire pour faire de 300.000 set/get:

private string redisCacheConn = "localhost:6379,allowAdmin=true,abortConnect=false,ssl=false";


[Fact]
public void PerfTestWriteShortString()
{
    CacheManager cm = new CacheManager(redisCacheConn);

    string svalue = "t";
    string skey = "testtesttest";
    for (int i = 0; i < 300000; i++)
    {
        cm.SaveCache(skey + i, svalue);
        string valRead = cm.ObtainItemFromCacheString(skey + i);
     }

}

utilise la classe suivante pour effectuer les opérations Redis via le client Stackexchange:

using StackExchange.Redis;    

namespace Caching
{
    public class CacheManager:ICacheManager, ICacheManagerReports
    {
        private static string cs;
        private static ConfigurationOptions options;
        private int pageSize = 5000;
        public ICacheSerializer serializer { get; set; }

        public CacheManager(string connectionString)
        {
            serializer = new SerializeJSON();
            cs = connectionString;
            options = ConfigurationOptions.Parse(connectionString);
            options.SyncTimeout = 60000;
        }

        private static readonly Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() => ConnectionMultiplexer.Connect(options));
        private static ConnectionMultiplexer Connection => lazyConnection.Value;
        private static IDatabase cache => Connection.GetDatabase();

        public string ObtainItemFromCacheString(string cacheId)
        {
            return cache.StringGet(cacheId);
        }

        public void SaveCache<T>(string cacheId, T cacheEntry, TimeSpan? expiry = null)
        {
            if (IsValueType<T>())
            {
                cache.StringSet(cacheId, cacheEntry.ToString(), expiry);
            }
            else
            {
                cache.StringSet(cacheId, serializer.SerializeObject(cacheEntry), expiry);
            }
        }

        public bool IsValueType<T>()
        {
            return typeof(T).IsValueType || typeof(T) == typeof(string);
        }

    }
}

Mon sérialiseur JSON est seulement à l'aide de Newtonsoft.JSON:

using System.Collections.Generic;
using Newtonsoft.Json;

namespace Caching
{
    public class SerializeJSON:ICacheSerializer
    {
        public string SerializeObject<T>(T cacheEntry)
        {
            return JsonConvert.SerializeObject(cacheEntry, Formatting.None,
                new JsonSerializerSettings()
                {
                    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                });
        }

        public T DeserializeObject<T>(string data)
        {
            return JsonConvert.DeserializeObject<T>(data, new JsonSerializerSettings()
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            });

        }


    }
}

mes temps de test sont d'environ 21 secondes (pour 300.000 sets et 300.000 gets). Cela me donne environ 28.500 opérations par seconde (au moins 3 fois plus lent que je m'y attendais en utilisant le référence.) L'application que je suis en train de convertir Pour utiliser Redis est assez bavarde et certaines requêtes lourdes peuvent avoisiner les 200.000 opérations totales contre Redis. De toute évidence, Je ne m'attendais pas à ce que ce soit les mêmes moments que lorsque j'utilisais le cache d'exécution du système, mais les retards après ce changement sont importants. Est-ce que je fais quelque chose de mal avec mon implémentation et est-ce que quelqu'un sait pourquoi mes chiffres référencés sont tellement plus rapides que mon test Stackechange? les chiffres?

Merci, Paul

8
demandé sur Paul Witherspoon 2016-02-29 22:24:30

2 réponses

les résultats de Mes le code ci-dessous:

Connecting to server...
Connected
PING (sync per op)
    1709ms for 1000000 ops on 50 threads took 1.709594 seconds
    585137 ops/s
SET (sync per op)
    759ms for 500000 ops on 50 threads took 0.7592914 seconds
    658761 ops/s
GET (sync per op)
    780ms for 500000 ops on 50 threads took 0.7806102 seconds
    641025 ops/s
PING (pipelined per thread)
    3751ms for 1000000 ops on 50 threads took 3.7510956 seconds
    266595 ops/s
SET (pipelined per thread)
    1781ms for 500000 ops on 50 threads took 1.7819831 seconds
    280741 ops/s
GET (pipelined per thread)
    1977ms for 500000 ops on 50 threads took 1.9772623 seconds
    252908 ops/s

===

configuration du serveur: Assurez-vous que la persistance est désactivée, etc

la première chose à faire dans un benchmark est: benchmark one thing. En ce moment, vous incluez beaucoup de sérialisation au-dessus, ce qui n'aidera pas à obtenir une image claire. Idéalement, pour un périmètre de référence, vous devriez utiliser une charge fixe de 3 octets, parce que:

3 octets charge utile

Ensuite, vous devez regarder le parallélisme:

50 clients parallèles

Il n'est pas clair si votre test est parallèle, mais si ce n'est pas que nous devrions absolument attendre voir moins de débit brut. Comme par hasard, SE.Redis est conçu pour être facile à paralléliser: vous pouvez tout simplement faire tourner plusieurs threads parlant de la même connexion (en fait, cela a aussi l'avantage d'éviter les paquets fragmentation, car vous pouvez vous retrouver avec plusieurs messages par paquet, où-comme une approche de synchronisation à un seul thread est garanti d'utiliser au plus un message par paquet).

enfin, nous devons comprendre ce que fait le benchmark. Est-il en train de faire:

(send, receive) x n

ou est-il en train de faire

send x n, receive separately until all n are received

? Les deux options sont possibles. Votre utilisation de l'API de synchronisation est la première, mais le second test est également bien défini, et pour autant que je sache: c'est ce qu'il mesure. Y deux façons de simuler cette deuxième configuration:

  • envoyer les premiers (n-1) messages avec le drapeau "fire and forget", donc vous seulement en fait, attendre le dernier
  • *Async API pour tous les messages, et seulement Wait() ou await la dernière Task

voici un benchmark que j'ai utilisé dans ce qui précède, qui montre à la fois "sync per op" (via L'API sync) et "pipeline per thread" (en utilisant le *Async API et juste en attendant le dernière tâche par thread), Les Deux utilisant 50 threads:

using StackExchange.Redis;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

static class P
{
    static void Main()
    {
        Console.WriteLine("Connecting to server...");
        using (var muxer = ConnectionMultiplexer.Connect("127.0.0.1"))
        {
            Console.WriteLine("Connected");
            var db = muxer.GetDatabase();

            RedisKey key = "some key";
            byte[] payload = new byte[3];
            new Random(12345).NextBytes(payload);
            RedisValue value = payload;
            DoWork("PING (sync per op)", db, 1000000, 50, x => { x.Ping(); return null; });
            DoWork("SET (sync per op)", db, 500000, 50, x => { x.StringSet(key, value); return null; });
            DoWork("GET (sync per op)", db, 500000, 50, x => { x.StringGet(key); return null; });

            DoWork("PING (pipelined per thread)", db, 1000000, 50, x => x.PingAsync());
            DoWork("SET (pipelined per thread)", db, 500000, 50, x => x.StringSetAsync(key, value));
            DoWork("GET (pipelined per thread)", db, 500000, 50, x => x.StringGetAsync(key));
        }
    }
    static void DoWork(string action, IDatabase db, int count, int threads, Func<IDatabase, Task> op)
    {
        object startup = new object(), shutdown = new object();
        int activeThreads = 0, outstandingOps = count;
        Stopwatch sw = default(Stopwatch);
        var threadStart = new ThreadStart(() =>
        {
            lock(startup)
            {
                if(++activeThreads == threads)
                {
                    sw = Stopwatch.StartNew();
                    Monitor.PulseAll(startup);
                }
                else
                {
                    Monitor.Wait(startup);
                }
            }
            Task final = null;
            while (Interlocked.Decrement(ref outstandingOps) >= 0)
            {
                final = op(db);
            }
            if (final != null) final.Wait();
            lock(shutdown)
            {
                if (--activeThreads == 0)
                {
                    sw.Stop();
                    Monitor.PulseAll(shutdown);
                }
            }
        });
        lock (shutdown)
        {
            for (int i = 0; i < threads; i++)
            {
                new Thread(threadStart).Start();
            }
            Monitor.Wait(shutdown);
            Console.WriteLine($@"{action}
    {sw.ElapsedMilliseconds}ms for {count} ops on {threads} threads took {sw.Elapsed.TotalSeconds} seconds
    {(count * 1000) / sw.ElapsedMilliseconds} ops/s");
        }
    }
}
10
répondu Marc Gravell 2016-03-01 14:22:11

extraction de données de façon synchrone (50 clients en parallèle, mais chaque client, les demandes sont présentées de façon synchrone au lieu de l'asynchrone)

une option serait d'utiliser la méthode async/en attente (StackExchange.Redis soutien).

si vous avez besoin d'obtenir plusieurs clés à la fois (par exemple pour construire un graphique quotidien des visiteurs à votre site Web en supposant que vous enregistrez le compteur de visiteurs par jour clés), alors vous devriez essayer de récupérer des données à partir de redis de manière asynchrone en utilisant redis pipelining, cela devrait vous donner de bien meilleures performances.

1
répondu Kobynet 2016-03-01 13:24:42