最近在debug生產環境的問題時,發現了ServiceStack 4.0.60版本RedisClient存在一個非常嚴重的性能問題。在高並發下,PooledRedisClientManager.GetClient和Redis.DisposeClient會導致High CPU,並且持續非常長的時間才能自動修復。下面是Demo程序壓測還原問題后,工具的分析結果。
通過分析源代碼發現:原來獲取RedisClient的邏輯中通過鎖方式實現,並且當連接被占滿后再獲取連接時,需要循環遍歷數組中所有的連接對象判斷是否有可用連接,會非常消耗CPU。Dispose方法也存在循環遍歷的問題。嘗試了很多種修改方案后,都不盡人意,果斷把這兩段邏輯重寫,下面是相關代碼,已經經過壓測。
PooledRedisClientManager.cs:
private ConcurrentQueue<RedisClient> deactiveClientQueue = new ConcurrentQueue<RedisClient>(); private static object lckObj = new object(); private static object waitObj = new object(); private int redisClientSize = 0; private int maxRedisClient = 500; //PooledRedisClientManager的構造函數中初始化此值:maxRedisClient = this.Config.MaxWritePoolSize;
//GetReadOnlyClient方法也可按此方式修改 public IRedisClient GetClient() { RedisClient client = null; var poolTimedOut = false; DateTime startTime = DateTime.Now; while (true) { bool getResult = deactiveClientQueue.TryDequeue(out client); if (getResult == false) { if (redisClientSize >= maxRedisClient) { Thread.Sleep(3); if (PoolTimeout.HasValue) { // wait for a connection, cry out if made to wait too long if ((DateTime.Now - startTime).TotalMilliseconds >= PoolTimeout.Value) { poolTimedOut = true; break; } } } else { client = CreateRedisClient(); if (client != null) return client; } } else { if (client != null) { InitClient(client); return client; } else { client = CreateRedisClient(); if (client != null) return client; } } } if (poolTimedOut == true) { throw new TimeoutException(PoolTimeoutError); } return client; } private RedisClient CreateRedisClient() { if (redisClientSize >= maxRedisClient) return null; lock (lckObj) { if (redisClientSize >= maxRedisClient) return null; Random dom = new Random((int)DateTime.Now.Ticks); var newClient = InitNewClient(RedisResolver.CreateMasterClient(dom.Next(100))); newClient.OnDispose += (isRecycle) => { if (isRecycle == true) { try { deactiveClientQueue.Enqueue(newClient); } catch { lock (lckObj) { redisClientSize--; } } } else { lock (lckObj) { redisClientSize--; } } }; redisClientSize++; return newClient; } }
RedisClient.cs:
public event RedisClientDisposeEventHandler OnDispose; public override void Dispose() { if (OnDispose != null) OnDispose(this.HadExceptions == false); base.Dispose(); }
RedisClient.cs:
public delegate void RedisClientDisposeEventHandler(bool isRecycle);
下面是修改前后的結果對比:
1.100個線程,每個線程完成2000次Redis調用,每次調用GetClient。 改造前12s,改造后8.5s,提升近50%。老版本CPU消耗稍高,並具有持續性。
2.200個線程,每個線程完成2000次Redis調用,每次調用GetClient。 改造前378s,改造后19s,提升提升近20倍。老版本CPU消耗非常高(解決100%),並具有持續性。新版本CPU占用了僅有原來的一半。
3.300個線程,每個線程完成2000次Redis調用,每次調用GetClient。 改造前1580s(26分鍾),改造后29s,提升提升近55倍。老版本CPU消耗非常高(解決100%),並具有持續性。新版本CPU占用了僅有原來的一半。
通過上述三個場景的測試可以看出,當RedisClient訪問壓力持續增加時,原版本的響應時間呈現指數性增長,當達到一定壓力時,RedisClient訪問幾乎阻塞,需要非常長時間才能緩解。重構后的RedisClient在性能上有大幅度提升,特別是在高並發下的性能表現,直接秒殺原版本!