1. 程式人生 > >StackExchange.Redis性能調優

StackExchange.Redis性能調優

發送 2.0 保護 1.2 stack 參考 nds 沒有 exceptio

大家經常出現同步調用Redis超時的問題,但改成異步之後發現錯誤非常少了,但卻可能通過前後記日誌之類的發現Redis命令非常慢。

PS: 以後代碼都在Windows bash中運行,StackExchange.Redis版本為1.2.6

先快速重現問題和解決問題,大家先運行下面的代碼

public static async Task Main(string[] args)
{
    ThreadPool.SetMinThreads(8, 8);
    using (var connection = await ConnectionMultiplexer.ConnectAsync("
localhost")) { connection.PreserveAsyncOrder = false; var db = connection.GetDatabase(0); var sw = Stopwatch.StartNew(); await Task.WhenAll(Enumerable.Range(0, 10) .Select(_ => Task.Run(() => { db.StringGet("aaa
"); Thread.Sleep(1000); }))); Console.WriteLine(sw.ElapsedMilliseconds); } }

運行發現拋出StackExchange.Redis.RedisTimeoutException,為什麽呢?是因為當前工作線程根本不夠用,同步等待時已經超時。具體請看源代碼

如果將上面的ThreadPool.SetMinThreads(8, 8)改成ThreadPool.SetMinThreads(100, 100)呢?是不是不拋異常了呢。

再說異步接口變慢的問題,大家先運行下面的代碼:

        public static async Task Main(string[] args)
        {
            var tcs = new TaskCompletionSource<bool>();
            var sw = Stopwatch.StartNew();

            Console.WriteLine($"Main1: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}");

            var task = Task.Run(() =>
            {
                Thread.Sleep(10);
                Console.WriteLine($"Run1: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}");
                tcs.TrySetResult(true);
                Console.WriteLine($"Run2: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}");
                Thread.Sleep(10000);
            });

            var a = tcs.Task.ContinueWith(_ => { Console.WriteLine($"a: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); });
            var b = tcs.Task.ContinueWith(_ => { Console.WriteLine($"b: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); });
            var c = tcs.Task.ContinueWith(_ => { Console.WriteLine($"c: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); });

            await tcs.Task;
            Console.WriteLine($"Main2: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}");
            Thread.Sleep(100);
            await Task.Delay(10);
            Console.WriteLine($"Main3: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}");
        }

最終輸出結果發現Run1和Main2是使用相同的線程吧,而Run2的ElapsedMilliseconds基本上就是在Run1的基礎上加100。

然後再回到調用Redis代碼上

static async Task Main(string[] args)
{
   ThreadPool.SetMinThreads(100, 100);
using (var connection = await ConnectionMultiplexer.ConnectAsync("localhost")) { var db = connection.GetDatabase(0); var sw = Stopwatch.StartNew(); await Task.WhenAll(Enumerable.Range(0, 10) .Select(_ => Task.Run(async () => { await db.StringGetAsync("aaa"); Thread.Sleep(100); }))); Console.WriteLine(sw.ElapsedMilliseconds); } }

你們發現輸出是100多還是1000多?為什麽?原來是因為sdk中有一個特殊的設置,要保護異步代碼執行的順序,然後我們在GetDatabase行之前加一個代碼connection.PreserveAsyncOrder = false;

然後再運行一次看看結果是多少呢?通過上面再做代碼基本上可以確定異步慢是和TaskCompletionSource和關系的,具體請看sdk的源代碼。

總結上面兩點,簡單得通過SetMinThreads和connection.PreserveAsyncOrder = false可以解決絕大部分問題,但更多其他深層次的問題怎麽發現呢?

下面就要介紹StackExchange.Redis兩個神器ConnectionCountersIProfiler

  1. 通過connection.GetCounters().Interactive獲得的對象之後其中有三個屬性非常有用
    public class ConnectionCounters
    {
        /// <summary>
        /// Operations that have been requested, but which have not yet been sent to the server
        /// </summary>
        public int PendingUnsentItems { get; }
    
        /// <summary>
        /// Operations that have been sent to the server, but which are awaiting a response
        /// </summary>
        public int SentItemsAwaitingResponse { get; }
    
        /// <summary>
        /// Operations for which the response has been processed, but which are awaiting asynchronous completion
        /// </summary>
        public int ResponsesAwaitingAsyncCompletion { get; }
    }

    每個屬性表示當前redis連接的待完成的命令當前所處的狀態。通過字面意思就可以知道PendingUnsentItems表示已經進行待發送隊列還未發送出去的命令;SentItemsAwaitingResponse表示已經發送出去但還沒有收到響應結果的命令;ResponsesAwaitingAsyncCompletion則表示已經收到響應的命令,但還沒有調用TaskCompletionSource<T>().TrySetResult()的命令。
    其中PendingUnsentItems和SentItemsAwaitingResponse過大的原因基本上是因為網絡阻塞了,你需要檢查一下網絡帶寬或者redis的value是否很大。
    ResponsesAwaitingAsyncCompletion則是因為await之後的代碼,如上面示例中的代碼,線程占用了很長的同步時間,需要優化代碼和將PreserveAsyncOrder設置為false。

  2. ConnectionCounters分析的是一個線程的瞬時狀態,而IProfiler則可以跟蹤一個請求總共執行了多少的redis命令以及他們分別使用了多長時間,具體細節請大家寫代碼體驗。參考文檔

發現問題就需要解決問題,也就需要深層次得去學習才能解決問題。我不喜歡寫文章,但發現最近有好幾篇說redis超時的問題,最終我還是想把自己的踩坑的心得分享給大家。

這在裏說一個好消息,那就是StackExchange.Redis 2.0已經從重構了異步隊列,使用管道方式解決異步慢的問題

StackExchange.Redis性能調優