隨著.NET Core 在 Linux 下的熱動,相信動不動就要分散式或集群的應用的需求,會慢慢火起來。所以這段時間一直在研究和思考分散式集群的問題,同時也在思考把幾個框架的思維相對提升到這個Level。最近大力重構了框架兩個點:一個是分散式緩存,一個是資料庫主從備。 今天,先分享分散式緩存的改進的... ...
背景:
隨著.NET Core 在 Linux 下的熱動,相信動不動就要分散式或集群的應用的需求,會慢慢火起來。
所以這段時間一直在研究和思考分散式集群的問題,同時也在思考把幾個框架的思維相對提升到這個Level。
最近大力重構了框架兩個點:一個是分散式緩存,一個是資料庫主從備。
今天,先分享分散式緩存的改進的兩個點:
1、高可用:能動態增加或減少Redis、MemCache的實例,而不影響程式。
2、高性能:保障在高併發下的穩定性及性能。
1、Redis、MemCache 分散式下的高可用。
要實現分佈下應用下的高可用,就兩種方法:
1、建立集群,內部調度,對外只提供一個介面。
優點:對所有不同的開發語言和客戶端都通用。
缺點:建立相對複雜,需要有專業的運維知識,而且對於提供出口的伺服器,也要再做一次主備,整體硬體成本高。
2、由客戶端進行調度。
優點:比較簡單,不需要專業知識,上千個Redis實例,隨時就動起來。
缺點:如果項目有混合多種後端語言開發的,還得有多種客戶端實現。
而 CYQ.Data 就是.NET Core 下集成了操作Redis的一種客戶端實現。
下麵來看簡單的使用過程:
1、指定配置外鏈:
原有的配置
<add key="RedisServers" value="127.0.0.1:6379,127.0.0.1:6380 - pwd123456"/> <add key="RedisServersBak" value="127.0.0.1:6381 - pwd123456"/>
將配置寫在原的config中,1是不適合大量的實例,要寫很長;2是當修改時,會引發(Window下)程式重啟(關鍵是NetCore下還不重啟)。
改進後配置(文件尾碼可以指定*.ini,*.txt):
<add key="RedisServers" value="redis.ini"/> <add key="RedisServersBak" value="redisbak.txt"/>
對應的redis.init 文件(一行一個實例):
127.0.0.1:6379 127.0.0.1:6380 - pwd123456 127.0.0.1:6381 - pwd123456 127.0.0.1:6382 - pwd123456 127.0.0.1:6383 - pwd123456
...
可以無限加
將配置外置後,程式會自動監控文件的變化,每次修改都會即時生效,內部自動調整演算法,真正實現高可用。
接下來,你就可以無限的找伺服器,啟動N多個實例,想加就加,想減就減。
一個高可用的分散式緩存就是這麼簡單了,當然了,以後要複雜,那就慢慢學習了。
2、關於一致性Hash及主備的說明:
在框架內部的演算法中,如果節點失敗,會檢測有沒有設置備用節點:
如果沒有,會直接失敗。
如果有,會從備用實例中獲取來維持服務,如果備用也實例也失敗,而本次請求失敗(下一次請求會更換備用節點)。
這樣可以避免在高併發的情況下產生滾雪球的情景,由一個點失敗最終導致把所有的伺服器都壓倒。
PS:在CYQ.Data 中,Redis 和 MemCache 的底層實現是一樣的,所以使用方式都是一樣的。
2、Redis、MemCache 分散式下的性能測試。
對於原有的代碼改進重構,大約變化了50%左右的代碼。
本次在性能上的優化:除了底層Socket小調整,就是命令的合併操作,以及隊列池的優化,以及多線程下的穩定保障。
其實,Web are 只是個客戶端,要說高性能,其實都是比較出來,只要比別人的快一些,好像就真的高性能了。
所以,測試都需要有個比較的對象。
目前測試的是本機,Win7,Redis 老版本 2.4.6。
1、先自己和自己比:新改動的版本和舊版本比較:
舊版本的測試數據:
新版本的測試數據:
跑在Linux下時(1核2G記憶體的CentOS7,Redis 版本 3.2.12):
經過整體的代碼改動,性能還是整體提升不少的,還支持了高可用的擴展。
只是離官方傳說的10w/s,還差了幾倍,我猜如果把5個命令打包一起發送,應該就差不多了。
2、看看其它客戶端的:
一開始我是沒做比較測試,只是剛好在網上看到另一個客戶端,寫著就是高性能Redis客戶端。
看到上面的測試數據,我有點驚訝,這有6w的數據是怎麼飄出來的:
SET_JSON using 1 threads run 100000 [use time 5.76s 17,370.26/sec] SET_JSON using 4 threads run 100000 [use time 1.87s 53,407.17/sec] SET_JSON using 8 threads run 100000 [use time 1.65s 60,517.8/sec]
但上面沒寫明是在window還是linux也沒寫版本,
於是,我就下載了它的客戶端(它自帶Test運行程式),
然後連上我的redis,運行了一下:
實際上它的數據是這樣的:
這是我打開的姿勢不對麽,還是對環境有特定的要求?
3、和Redis自帶的redis-benchmark.exe工具進行比較。
想找找 Redis 在Window平臺測試相關的文章,發現都是Redis原生的測試工具的介紹,於是,也用它測試了一下:
發現原生的果然強悍,最高的時候可以飄到6w,看來用C就是不一樣。
搞的我都懷疑,你們都是在用併發測試,而我只是用多線程。
4、用了傳說中性能好到要收費的:StackExchange.Redis
感覺也差不多啊,收費我還是支持的。
關鍵是這貨相同的NetCore代碼,放Linux CentOS7 跑不動:
竟然連不上了,直接返回錯誤了:
{"success":false,"msg":"It was not possible to connect to the redis server(s). InternalFailure (None, last-recv: 803) on 127.0.0.1:6379/Interactive, Idle, last: GET, origin: ReadFromPipe, outstanding: 2, last-read: 0s ago, last-write: 0s ago, unanswered-write: 0s ago, keep-alive: 60s, state: ConnectedEstablished, mgr: 9 of 10 available, in: 0, last-heartbeat: never, global: 0s ago, v: 2.0.571.20511"}
總結:
CYQ.Data 經過重構升級後,整體提升了不少。
這裡要瞭解一下:CYQ.Data 集成的分散式緩存操作,和其它單獨的客戶端是不一樣的。
因為其它客戶端是把所有的Redis命令都實現了,你可以其它客戶端操作完整的Redis。
而 CYQ.Data 只是:
Get、Set、DEL、Contains、Clear。
統一了所有類型並保持最簡單的緩存操作介面。
最後,獻上測試代碼:
AppConfig.Cache.RedisServers = "127.0.0.1:6379";//,127.0.0.1:6380 - c123456,127.0.0.1:6381 - c123456,127.0.0.1:6382 - c123456,127.0.0.1:6383 - c123456"; //AppConfig.Cache.RedisServers = "redis.txt"; //AppConfig.Cache.RedisServersBak = "redis.bak.txt"; int readCount = 100000, userDBCount = 1; ThreadPool.SetMaxThreads(1000, 1000); new ThreadRun(1, readCount, userDBCount).Start(); new ThreadRun(4, readCount, userDBCount).Start(); new ThreadRun(8, readCount, userDBCount).Start(); new ThreadRun(10, readCount, userDBCount).Start(); new ThreadRun(20, readCount, userDBCount).Start(); new ThreadRun(50, readCount, userDBCount).Start(); new ThreadRun(100, readCount, userDBCount).Start(); new ThreadRun(200, readCount, userDBCount).Start(); new ThreadRun(500, readCount, userDBCount).Start(); new ThreadRun(1000, readCount, userDBCount).Start(); new ThreadRun(2000, readCount, userDBCount).Start(); new ThreadRun(5000, readCount, userDBCount).Start(); new ThreadRun(2000, readCount, userDBCount).Start(); new ThreadRun(2000, readCount, userDBCount).Start(); new ThreadRun(500, readCount, userDBCount).Start(); new ThreadRun(200, readCount, userDBCount).Start(); new ThreadRun(100, readCount, userDBCount).Start(); new ThreadRun(50, readCount, userDBCount).Start(); new ThreadRun(20, readCount, userDBCount).Start(); new ThreadRun(10, readCount, userDBCount).Start(); new ThreadRun(8, readCount, userDBCount).Start(); new ThreadRun(4, readCount, userDBCount).Start(); new ThreadRun(1, readCount, userDBCount).Start(); CacheManage.RedisInstance.Clear();//操作對象 Console.WriteLine("End"); Console.WriteLine(CacheManage.RedisInstance.WorkInfo); Console.WriteLine(CacheManage.RedisInstance.CacheInfo.ToJson(false, false)); ---------------------------------------------------------- public class ThreadRun { int threadCount, setOrReadCount, useDBCount; CacheManage cache; public ThreadRun(int threadCount, int setOrReadCount, int useDBCount) { cache = CacheManage.RedisInstance;//操作對象 cache.Clear(); this.threadCount = threadCount; this.setOrReadCount = setOrReadCount; this.useDBCount = useDBCount; } System.Diagnostics.Stopwatch gloWatch = new System.Diagnostics.Stopwatch(); int runEndCount = 0; bool isEnd = false; public void Start() { AppConfig.Cache.RedisUseDBCount = useDBCount; gloWatch.Start(); for (int i = 0; i < threadCount; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadStart), setOrReadCount / threadCount); } while (!isEnd) { Thread.Sleep(10); } } public void ThreadStart(object readCount) { string rndKey = Guid.NewGuid().ToString().Substring(0, 5); int max = (int)readCount; for (int i = 0; i < max; i++) { string key = rndKey + "key" + i; if(cache.Set(key, Guid.NewGuid().ToString())) { } else { Console.WriteLine("Set 失敗 key :" + key); } } Interlocked.Increment(ref runEndCount); if (runEndCount >= threadCount && !isEnd) { isEnd = true; //gloWatch.Stop(); //Ng 2000ms //x 1000ms long t = gloWatch.ElapsedMilliseconds; Console.WriteLine(string.Format("ThreadCount : {0} , Run : {1} Time {2} ms ,{3} requests per second. ", threadCount, setOrReadCount, t.ToString(), setOrReadCount * 1000 / t)); } } }