2016-02-29 14 views
7

Tôi đang triển khai lớp lưu trữ Redis bằng cách sử dụng ứng dụng khách Stackexchange Redis và hiệu suất hiện tại là tiếp giáp với không sử dụng được.StackExchange redis client rất chậm so với các bài kiểm tra benchmark

Tôi có môi trường cục bộ nơi ứng dụng web và máy chủ redis đang chạy trên cùng một máy. Tôi chạy thử nghiệm benchmark Redis chống lại máy chủ Redis của tôi và kết quả đã thực sự thực sự tốt (Tôi chỉ bao gồm thiết lập và nhận được hoạt động trong ghi của tôi trở lên):

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 

Vì vậy, theo các tiêu chuẩn tôi đang xem xét trên 100.000 bộ và 100.000 được, mỗi giây. Tôi đã viết một bài kiểm tra đơn vị để làm 300.000 bộ/nhận:

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

} 

này sử dụng các lớp sau đây để thực hiện các hoạt động Redis qua 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); 
     } 

    } 
} 

JSON serializer của tôi là chỉ sử dụng 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 
      }); 

     } 


    } 
} 

Thời gian thử nghiệm của tôi là khoảng 21 giây (cho 300.000 bộ và 300.000 lượt). Điều này mang lại cho tôi khoảng 28.500 hoạt động mỗi giây (ít nhất là 3 lần chậm hơn tôi mong đợi bằng cách sử dụng các tiêu chuẩn). Các ứng dụng tôi đang chuyển đổi để sử dụng Redis là khá chatty và yêu cầu nặng nhất định có thể xấp xỉ 200.000 tổng số hoạt động chống lại Redis. Rõ ràng là tôi đã không mong đợi bất cứ điều gì giống như tôi đã nhận được khi sử dụng bộ nhớ cache thời gian chạy hệ thống, nhưng sự chậm trễ sau khi thay đổi này là đáng kể. Tôi có làm điều gì đó sai với việc thực hiện của tôi và không ai biết tại sao số liệu đo điểm chuẩn của tôi nhanh hơn rất nhiều so với con số kiểm tra Stackechange của tôi?

Cảm ơn, Paul

Trả lời

10

kết quả của tôi từ mã bên dưới:

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 

===

cấu hình

Server: chắc chắn bền bỉ bị vô hiệu hóa, vv

Điều đầu tiên bạn nên làm trong một chuẩn mực là: chuẩn một điều. Tại thời điểm bạn đang bao gồm rất nhiều chi phí serialization, mà sẽ không giúp có được một hình ảnh rõ ràng.Lý tưởng nhất, cho một như-cho-như benchmark, bạn nên sử dụng 3 byte tải trọng cố định, bởi vì:

3 byte tải trọng

Tiếp theo, bạn sẽ cần phải nhìn vào xử lý song song:

50 khách hàng song song

Vẫn chưa rõ liệu thử nghiệm của bạn là song song, bu Nếu chúng ta không phải là chúng ta nên hoàn toàn mong đợi để xem thông lượng thô ít hơn. Thuận tiện, SE.Redis được thiết kế để dễ dàng song song: bạn chỉ có thể xoay nhiều chủ đề nói chuyện với cùng một kết nối (điều này thực sự cũng có lợi thế là tránh phân mảnh gói, vì bạn có thể kết thúc với nhiều thư cho mỗi gói, nơi-như một cách tiếp cận đồng bộ hóa một luồng được đảm bảo sử dụng tối đa một tin nhắn cho mỗi gói tin).

Cuối cùng, chúng ta cần hiểu tiêu chuẩn được liệt kê là gì. Là nó thực hiện:

(send, receive) x n 

hoặc là nó làm

send x n, receive separately until all n are received 

? Cả hai tùy chọn đều có thể. Việc sử dụng API đồng bộ hóa của bạn là phương pháp đầu tiên, nhưng thử nghiệm thứ hai được xác định rõ ràng, và cho tất cả những gì tôi biết: đó là những gì nó đang đo lường. Có hai cách để mô phỏng thiết lập thứ hai này:

  • gửi (n-1) thông điệp đầu tiên với "lửa và quên" lá cờ, vì vậy bạn chỉ thực chờ đợi cuối cùng một
  • sử dụng *Async API cho tất cả các bài viết, và chỉ Wait() hoặc await cuối cùng Task

Dưới đây là một chuẩn mực mà tôi sử dụng ở trên, cho thấy cả hai "đồng bộ hóa mỗi op" (qua API đồng bộ) và "đường ống cho mỗi thread" (sử dụng *Async API và chỉ cần đợi tác vụ cuối cùng cho mỗi chuỗi), cả hai đều sử dụng 50 chủ đề:

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

Tôi tự hỏi tại sao pipelining sản xuất thông lượng thấp hơn so với xử lý đồng bộ? – Kobynet

+0

@Kobynet câu hỏi công bằng; có thể chi phí TPL trong trường hợp cụ thể này; Tôi không có thời gian để kéo nó ra –

+0

Cảm ơn Marc, điều này thực sự đã giúp tôi! Tôi sẽ cần phải tái cấu trúc ứng dụng (kế thừa) này thành đa luồng và không đồng bộ để có được hiệu suất tốt nhất. Tôi xin lỗi rằng nó không phải là tất cả rõ ràng từ mã tôi dán nhưng tôi đã tránh tất cả serialization trong trường hợp thử nghiệm của tôi. Bạn hoàn toàn đúng rằng tôi đã không so sánh như cho (như sử dụng một tải trọng khác nhau và kiểm tra đơn vị của tôi không song song) vì vậy tôi hoàn toàn thấy lý do tại sao các con số rất khác nhau. Cảm ơn một lần nữa! –

1

Bạn đang lấy dữ liệu theo cách đồng bộ (50 khách hàng song song nhưng yêu cầu của từng khách hàng được thực hiện đồng bộ thay vì không đồng bộ)

phương pháp

Một lựa chọn sẽ được sử dụng async/chờ đợi (StackExchange.Redis hỗ trợ điều đó).

Nếu bạn cần nhận nhiều khóa cùng một lúc (ví dụ: để tạo biểu đồ khách truy cập vào trang web giả sử bạn lưu khóa truy cập mỗi ngày) thì bạn nên thử tìm nạp dữ liệu từ redis theo cách không đồng bộ bằng cách sử dụng redis pipelining, nên cung cấp cho bạn hiệu suất tốt hơn nhiều.

+1

Cảm ơn Kobynet, tôi đánh giá cao sự giải thích! Ứng dụng web hiện tại mà tôi đang chuyển đổi cực kỳ cứng nhắc và tuần tự, vì vậy tôi có thể sử dụng bộ nhớ cache System.Runtime cùng với bộ nhớ cache Redis, điều này sẽ cho phép tôi dần dần cấu trúc lại ứng dụng để không đồng bộ hơn (một phần của chuyển đổi cuối cùng, dù sao). Điều này sẽ cho phép tôi làm việc theo hướng khả năng mở rộng tốt hơn trong tương lai. Cảm ơn một lần nữa vì sự giúp đỡ! –

Các vấn đề liên quan