該原文是Ayende Rahien大佬業餘自己在使用C# 和 .NET構建一個簡單、高效能相容Redis協定的資料庫的經歷。
首先這個"Redis"是非常簡單的實現,但是他在優化這個簡單"Redis"路程很有趣,也能給我們在從事效能優化工作時帶來一些啟示。
原作者:Ayende Rahien
原連結:https://ayende.com/blog/197441-A/high-performance-net-building-a-redis-clone-analysis
另外Ayende大佬是.NET開源的高效能多正規化資料庫RavenDB所在公司的CTO,不排除這些文章是為了以後會在RavenDB上相容Redis協定做的嘗試。大家也可以多多支援,下方給出了連結
RavenDB地址:https://github.com/ravendb/ravendb
在上一篇文章中,我用最簡單的方式寫了一個Redis克隆版本。它能夠在我們的測試範例上每秒命中近100萬個查詢(c6g.4xlarge,使用16個核心和64 GB記憶體)。在我們更深入地進行優化之前,值得了解CPU時間實際花費在哪裡。我在探查器下執行伺服器,以檢視各種程式碼所耗費的成本。
我喜歡使用dotTrace作為探查器,同時使用它的跟蹤模式,因為它返回的資料中給了我各個模組、類和程式碼的執行時間以及呼叫次數。通常,我可以僅從這些細節中推斷出很多關於系統效能的原因。
看看下面的統計資料,這是連線實際處理過程中的成本細分:
展開耗費CPU最多的System code,如下所示:
您可以看到FlushAsync()
方法耗費的CPU做多。我們在這裡做一個假設,當我們呼叫StreamWriter
的FlushAsync()
方法時,同樣會重新整理底層的流。深入研究下呼叫棧,似乎我們在TCP層面為每個命令都都進行了分包,這樣效率是很低的。
如果我們將StreamWriter
的AutoFlush
屬性改為true
,這將導致它立即向網路流中寫入資料,但不會在TCP流上呼叫flush
,這會讓TCP流更有效的利用緩衝空間。
涉及的程式碼更改是刪除FlushAsync()
呼叫並初始化StreamWiter
,如下所示:
using var writer = new StreamWriter(stream)
{
NewLine = "\r\n",
AutoFlush = true,
};
讓我們再次執行基準測試,這將給我們(在我的開發機器上):
[13.8w/s]
– 使用 AutoFlush = true[13.9w/s]
– 使用 FlushAsyncvar line = await reader.ReadLineAsync();
await writer.FlushAsync();
// 修改為以下程式碼
var lineTask = reader.ReadLineAsync();
if(lineTask.IsCompleted == false)
{
await writer.FlushAsync();
}
var line = await lineTask
我在這裡所做的是直接寫入StreamWriter
,並且只有在沒有更多的輸入時才重新整理緩衝區。這應該會大大減少包的傳送次數,而且它確實做到了。再次執行基準測試可以得出以下結論:
[22.9w/s]
– 使用延時重新整理StreamWriter
緩衝區,它自己會自動的重新整理。我們只會在沒有其它需要讀取的資料時手動重新整理StreamWriter
,這個操作是和讀取並行進行的。FlushAsync
(請參閱ExecuteCommand&FlushAsync),現在我們更少呼叫它了。TryAddInternal
中。我們知道在這種情況下存在很高的爭用,但92%的時間直接花在了這個方法上嗎?讓我們看一下程式碼,它在做什麼就會很明顯:ConcurrentDictionary
對鎖之間的呼叫進行分片。鎖的數量由我們預設擁有的CPU核心數量定義。我們的的並行越多,我們就越能從增加分片數量中獲益。我嘗試將其設定為1024,並在分析器下執行它,這給我帶來了幾個百分點的改進,但並不是很多。很有價值,但不是我期望的水平。
現在,我們需要找出如何在讓集合操作變得更快,但我們還必須考慮總體GC成本以及字串處理細節。在下一篇文章中會有更多關於這一點的資訊。