2016-02-29 2 views
7

Я реализую слой кэширования Redis с помощью клиента Stackexchange Redis, а производительность прямо сейчас граничит с непригодными.StackExchange клиент redis очень медленный по сравнению с эталонными тестами

У меня локальная среда, в которой веб-приложение и сервер redis работают на одном компьютере. Я побежал тест бенчмарк Redis против моего сервера Redis и результаты были на самом деле очень хорошо (я просто в том числе набор и получить работу в моем подправить):

C:\Program Files\Redis>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 

Итак, в соответствии с критериями, я смотрю на более 100 000 наборов и 100 000 посещений в секунду. Я написал модульное тестирование, чтобы сделать 300000 комплект/получает:

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); 
    } 

} 

При этом используется следующий класс для выполнения операций Redis с помощью клиента 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); 
     } 

    } 
} 

Моя JSON сериализатор только с помощью 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 
      }); 

     } 


    } 
} 

Мое время испытания составляет около 21 секунды (для 300 000 комплектов и 300 000 единиц). Это дает мне около 28 500 операций в секунду (по крайней мере, в 3 раза медленнее, чем я ожидал бы с использованием эталонных тестов). Приложение, которое я конвертирую для использования Redis, довольно чато, и некоторые тяжелые запросы могут приблизиться к 200 000 операций с Redis. Очевидно, я не ожидал ничего подобного в то же время, что и при использовании кеша системы, но задержки после этого изменения значительны. Я что-то не так с моей реализацией, и кто-нибудь знает, почему мои контрольные цифры намного быстрее, чем мои тестовые показатели Stackechange?

Спасибо, Paul

ответ

10

Мои результаты кода ниже:

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 

===

конфигурации

сервера: убедитесь, что сохранение отключено, и т.д.

Первое, что вы должны сделать в бенчмарке: бенчмарка одна вещь. На данный момент вы включаете много накладных расходов на сериализацию, что не поможет получить четкую картину.В идеале, для как-для-как эталон, вы должны использовать 3-байтовую фиксированную полезную нагрузку, так как:

3 байт полезной нагрузки

Далее, вы должны смотреть на параллелизм:

50 параллельных клиентов

это не ясно, будет ли ваш тест параллельно, бушель t, если это не так, мы должны абсолютно ожидать, чтобы увидеть меньше сырой пропускной способности. Удобно, SE.Redis спроектирован так, чтобы его можно было легко распараллелить: вы можете просто развернуть несколько потоков, разговаривая с тем же соединением (на самом деле это также имеет преимущество, заключающееся в том, чтобы избежать фрагментации пакетов, так как вы можете получить несколько сообщений на пакет, где в качестве однопоточного синхронизирующего подхода гарантировано использование не более одного сообщения на пакет).

И, наконец, нам нужно понять, что делает данный тест. Является ли это делать:

(send, receive) x n 

или он делает

send x n, receive separately until all n are received 

? Возможны оба варианта. Ваше использование API-интерфейса синхронизации является первым, но второй тест одинаково хорошо определен и для всех, что я знаю: это то, что он измеряет. Есть два способа моделирования этой второй установки:

  • отправить первые (п-1) сообщения с «огнем и забыть» флаг, так что вы только фактически ждать за один последний
  • использовать *Async API для всех сообщений, и только Wait() или await последний Task

Вот тест, который я использовал в предыдущем примере, который показывает, как «синхронизация по ор» (с помощью синхронизации API) и «трубопровода на поток» (с использованием *Async API и просто ждать последней задачи на поток), используя как 50 потоков:

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([email protected]"{action} 
    {sw.ElapsedMilliseconds}ms for {count} ops on {threads} threads took {sw.Elapsed.TotalSeconds} seconds 
    {(count * 1000)/sw.ElapsedMilliseconds} ops/s"); 
     } 
    } 
} 
+0

Интересно, почему конвейерная обработка обеспечивает меньшую пропускную способность, чем обработка синхронизации? – Kobynet

+0

@Kobynet справедливый вопрос; возможно, накладные расходы TPL в данном конкретном случае; У меня нет времени прямо сейчас, чтобы разобрать его. –

+0

Спасибо, Марк, это действительно помогло мне! Мне нужно будет реорганизовать это (устаревшее) приложение на многопоточное и асинхронное, чтобы получить максимальную производительность. Я извиняюсь, что это совсем не ясно из кода, который я вставил, но я избегал всякой сериализации в моем тестовом примере. Вы совершенно правы, что я не сравнивался с подобными (используя другую полезную нагрузку, и мой модульный тест не был параллельным), поэтому я полностью понимаю, почему цифры были такими разными. Еще раз спасибо! –

1

Вы выборки данных в синхронном пути (50 клиентов параллельно, но запросы каждого клиента выполняется синхронно, а не асинхронно)

Одним из вариантов были бы использовать асинхра/поджидают методов (StackExchange.Redis поддерживает это).

Если вам нужно получить сразу несколько ключей (например, чтобы построить ежедневный график посетителей вашего веб-сайта при условии, что вы сохраняете счетчик посетителей в день), вам следует попытаться извлечь данные из redis асинхронным образом, используя redis pipelining, это должен дать вам гораздо лучшую производительность.

+1

Спасибо Кобинет, я ценю объяснение! Текущее веб-приложение, которое я конвертирую, чрезвычайно жесткое и последовательное, поэтому я, вероятно, возьму двойной подход к использованию кеша System.Runtime вместе с кешем Redis, который позволит мне постепенно реорганизовать приложение на более асинхронный (который был часть возможного преобразования в любом случае). Это позволит мне работать в направлении повышения масштабируемости в будущем. Спасибо еще раз за помощь! –

Смежные вопросы