本次遷移涉及的是公司內部一個業務子系統,該系統是一個多樣化的應用,支撐著公司的多個業務方向。目前,該系統由40多個基於.NET的微服務應用構成,使用數千個CPU核心和數TB記憶體,在數百個Linux容器中執行。每天,該系統需要處理數十億次請求。
該系統其中大部分服務是在2018-2019年左右由老舊.NET Faremwork、Java等系統重構而來,當時使用的是.NET Core 2.1,這幾年業務迭代陸續新建了一些服務,所以該系統大部分服務是.NET Core 2.1,也有小一部分採用的是.NET Core 3.1和.NET5.0。
如今5年過去了,.NET的版本已經來到了7.0,相較於之前的版本它加入了非常多先進的特性、提升了效能、加入可觀測性支援、更加適應容器化環境的部署;而現在的.NET Core 2.1讓我們有很多效能提升和新的特性都無法享受到。
為了享受更新的特性和效能提升,我們團隊在最近的一段時間裡面完成了.NET Core 2.1和.NET 5.0向.NET 6.0的遷移,其中發生踩了一些坑,最後也獲得了不錯的結果,特意在這裡和大家分享這整個過程。
為什麼不是向.NET 7.0遷移?首先是因為.NET7.0在我們內部中的元件還沒得到很好的支援,另外.NET6.0是LTS版本而.NET7.0不是;而且從.NET6.0向.NET7.0遷移非常簡單,後續可以直接升級。所以綜合考慮,我們決定先升級到.NET6.0版本。
那麼有很多朋友會有疑問,現在有很多面向雲原生的程式語言和框架,我們為什麼選擇了使用.NET?我想從幾個方面解答這個問題。
.NET見證了網際網路的起步階段,很多大家能想到的網際網路應用一開始都是基於.NET技術構建,特別是在我們這個行業更是如此;下圖是統計十多個微服務專案程式碼,可以發現有近700萬行.NET程式碼(包括C#、ASP.NET、Razor等等),所以對於我們來說,繼續在.NET上投資是一個很好的選擇,沒有什麼理由更換其它的技術。
大家都知道,在.NET平臺上可以執行很多語言,比如C#、F#、JavaScript、PHP、Python等等,其中使用量最大的就是C#,而C#它有很多先進的語法特性,可以極大的提升我們的生廠力和程式的能。比如:
List<T>
是一個開放的泛型類,而像List<string>
和List<int>
這樣的範例化則避免了對單獨的ListOfString和ListOfInt類的需求,或者像ArrayList那樣依賴於物件和轉換。泛型還能夠在不同的型別之間建立有用的系統(並減少對大量程式碼的需求),比如泛型數學。另外,C#的泛型不是泛型擦除,而是執行時生成泛型本機程式碼,對於值型別可以避免裝箱拆箱,極大降低GC壓力。List<T>
這樣的泛型型別可以提供扁平的、無開銷(無需裝箱拆箱)的值型別集合。另外.NET泛型在替換值型別時提供專門的編譯程式碼,這意味著這些泛型程式碼路徑可以避免昂貴的GC開銷。另外在一些程式語言和框架效能排行上,C#和.NET的效能也是名列前茅的。在TechEmpower釋出的WEB框架效能天梯中,基於C#和.NET構建的ASP.NET Core框架排名第七,在功能完備的WEB框架中僅次於Rust和C++框架。
https://www.techempower.com/benchmarks/
在科學計算的Benchmaks Game中,C# .NET名列第5,僅次於C、C++、Rust等一些編譯型語言;執行速度是JIT語言中最快的,記憶體佔用也是JIT語言中最低的。
https://benchmarksgame-team.pages.debian.net/benchmarksgame/index.html
在評測GRPC效能的grpc_bench中,C#和.NET以141906req/s
的速度和5.76ms
的平均延時取的了第一的成績。
https://github.com/LesnyRumcajs/grpc_bench/discussions/310
可以看到C#語言和.NET框架在極致的效能和生產力之間取得了很好的平衡,我們恰恰就是需要這樣的框架。
C# 和 .NET現階段都是用MIT協定開源,允許使用者在滿足一些簡單條件的前提下,自由地使用、複製、修改和分發軟體,因為MIT協定非常寬鬆,使用者可以自由地使用和分發軟體,不必擔心任何版權或專利問題。
此次遷移要最大的保證業務相容性,就是不修改任何一行業務程式碼,只進行框架遷移。所以實際上改動非常小,幾乎沒有佔用什麼測試人力,因為只需要迴歸一些主要業務流程。
在遷移過程中踩了一些坑,其實這些不應該說是遷移中踩的坑,因為在.NET社群的檔案中,有非常完整的遷移流程,跟著遷移流程來不會有什麼問題,只是有一些要注意的地方。下方是.NET社群提供的每個版本的遷移檔案:
有一些需要注意的地方,主要是以下幾點:
System.Text.Json序列化
我們主要是WebAPI站點,從.NET Core 2.1升級過程中首先遇到的第一個問題就是序列化的支援,因為以前的版本都是使用的Newtonsoft.Json,在.NET Core 3.1以後預設使用System.Text.Json;雖然System.Text.Json更加規範和效能更強,但是不會相容一些非規範的JSON,為了避免介面契約的變化,我們使用Newtonsoft.Json替換了System.Text.Json。
// 根據不同的服務型別,選擇不同的設定
services.AddMvc().AddNewtonsoftJson();
services.AddControllers().AddNewtonsoftJson();
services.AddControllersWithViews().AddNewtonsoftJson();
services.AddRazorPages().AddNewtonsoftJson();
Endpoint處理
.NET新版本使用Endpoint進行路由關係,如果之前設定了app.UseMvc(),而且進行了路由設定,如果不想遷移的話那麼需要關閉Endpoint的路由支援來相容。
services.AddMvc(options=>{
options.EnableEndpointRouting = false;
});
非同步Action處理
如果以前是.NET2.1版本,Controller中有Async結尾的Action,那麼在新版本中Async結尾會預設去除,為了保證應用介面契約相容性,我們關閉這個特性的支援。
services.AddMvc(options=>{
options.SuppressAsyncSuffixInActionNames = false;
});
重複讀流
如果以前是.NET2.1版本,在某些場景中,需要多次讀取請求正文,則需要在app.UseMvc()
或者app.UseEndpoints()
前進行request.EnableRewind();
,在新版本需要改為Request.EnableBuffering();
。
app.Use(async (context, next) =>
{
context.Request.EnableBuffering();
await next(context);
});
並且在使用完成以後需要重置 request.Body.Position = 0;
,不過我們並不建議這樣做,高效能的做法是使用PipeReader來讀流。
request.Body.Position = 0;
using (var reader = new StreamReader(request.Body, Encoding.UTF8, true, 1024, true))
{
......
}
request.Body.Position = 0;
同步讀流
如果以前是.NET2.1版本,預設同步讀request.body
流 ,在新版本中為了效能預設就是非同步讀,如果不想修改為非同步讀流(為了效能不建議同步讀流),那麼需要允許同步讀流。
services.Configure<KestrelServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
啟用動態PGO
.NET5.0以後的一個新的特性,就是Dynamic Profile-guided Optimization(動態設定引導優化),它會在執行時收集程式碼的執行情況,通過分層編譯自動對程式碼進行優化。在其它博主的評測中,某些場景中有高達32%的提升。
# 設定環境變數
export DOTNET_ReadyToRun=0 # 禁用 AOT
export DOTNET_TieredPGO=1 # 啟用分層 PGO
export DOTNET_TC_QuickJitForLoops=1 # 為迴圈使用 tier0程式碼
我們的釋出計劃基本也是進行灰度釋出,可以在7層閘道器對新舊應用進行流量權重分配,更簡單的方式就是直接替換叢集內的某些容器映象達到流量切換的效果,我們選擇更簡單的方式來處理。
在觀察一段時間沒有問題以後,陸續覆蓋20%、50%、100%的應用,完成切流。
遷移後我們驚喜的發現整體的效能都有較大的提升,在某個計算密集型的服務中,CPU佔用率降低30%,而且沒有了CPU毛刺,佔用率曲線更加穩定。
另外記憶體也有一定的下降,雖然這個服務佔用的記憶體很少,不過也是肉眼可見的進步。
在其它服務中,也觀測到了類似的改變,幅度變得更大。
在IO密集型的應用中,我們也驚喜的觀測到了CPU使用率的下降,而且毛刺變少了很多。
我們知道在.NET的新版本中,著重優化了P95耗時,檢視了一些介面的平均耗時,發現相較原來平均耗時降低了50%,非常明顯。
公司架構團隊基於Opentelemetry完善了.NET上的觀測指標,現在我們可以無侵入無埋點的對應用進行監控,還有一些更底層的.NET執行指標也可以監控。
比起以前的APM,現在也有更詳細的鏈路資料展示。
升級.NET6.0以後,帶來了很大的效能提升,在降低CPU和記憶體佔用的情況下,還降低了P95延時,這一切的背後是什麼?
在每年11月.NET即將釋出正式版之前,.NET社群都會總結一個長達數十頁的檔案,從JIT、GC、執行緒各個方面記錄從上一個版本到這一個版本有哪些效能的提升,可以看到.NET社群為效能提升做的努力。
筆者帶大家從.NET Core 2.0開始,看看每個版本中有哪些令人印象深刻的效能改進。
.NET Freamwork 到 .NET Core 效能提升:
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core/
這是一個跨時代的版本,標誌了.NET從此走向開源、跨平臺,當然在整個跨平臺構建過程中,也有很多重大效能進步,下面列出了比較重大的部分:
Queue
類吞吐量提升了6倍、ConcurrentBat
吞吐量提高了~30%,而且極大的降低了GC次數。Select()
吞吐量提升了4倍,ToArry()
效能提升了6倍。ToString()
吞吐量提高了33%,記憶體分配減少了25倍。Socket
連結的寫入和接收都減少50%以上的記憶體開銷。ThreadPool
中優化了佇列演演算法,提升了30%的吞吐量,減少了25%的記憶體分配。.NET Core 2.1 效能提升: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-2-1/
.NET Core 2.1 雖然和 .NET Core 2.0只有一個小版本的區別,但是實際上是經過一年多的開發和優化,其中比較重大的變更有:
EqualityComparer<T>
提升了2.5倍效能、Enum.HasFlag()
提升了50倍效能。Timer
計時器提升了50%的吞吐量、非同步存取熱路徑減少了30%開銷。String
的效能,使用了向量化、Span<T>
等方案,比如:Equals
方法吞吐量提升了30%、IndexOf
方法吞吐量提升了3倍、ToLower/ToUpper
提升了1倍。.NET Core 3.0效能提升:
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-3-0/
.NET Core 3.0 提供了大量的功能,從Windows表單和WPF,到單檔案可執行檔案,到非同步列舉,到平臺內在因素,到HTTP/2,到快速JSON讀寫,到組合可解除安裝性,到增強的加密技術,等等...有大量的新功能值得興奮。然而,對我來說,效能是讓我早上上班時感到興奮的主要功能,而在.NET Core 3.0中,有大量的效能優化點。其中重大改進有:
Span<T>
,以及它的朋友ReadOnlySpan<T>
、Memory<T>
和ReadOnlyMemory<T>
。這些新型別的引入帶來了數百種與之互動的新方法,有些是在新型別上,有些是在現有型別上的過載功能,還有及時編譯器(JIT)中的優化,使其工作非常高效。.NET5效能提升:
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/
.NET 5已經有了大量的效能改進,文中重點介紹了~250個合併請求,這些請求為整個.NET 5的效能改進做出了巨大的貢獻。其中重大改進有:
.NET 6 效能提升: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/
這無疑是.NET社群通力共同作業的一年,.NET6.0總共有超過6500個合併請求,上文整理了~400個關於效能提升的請求,當時.NET社群喊出的口號就是這是最快的.NET版本。其中重大改進有:
.NET 7 效能提升: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/
.NET 7毫無疑問的說,它是迄今為止最快的.NET版本,它效能提升是非常巨大的,以至於筆者開啟上面效能優化的說明網頁,瀏覽器足足卡頓了幾十秒。.NET 7相較於.NET 6有多達7000多個提交,其中有1000多個是和效能息息相關的,上文只挑選了500個提交。其中重大改進有:
Timer
實現從基於C++的實現切換到完全C#程式碼中的實現。兩者均提升了將近30%的效能。總的來說,本次.NET6.0的遷移還是非常成功的,簡單的通過版本升級就能獲得效能提升,而且還可以享受新版.NET和C#帶給我們新的特性,如果有什麼問題請私信或者評論,歡迎交流!
遷移至.NET5.0後CPU佔用降低: https://twitter.com/stebets/status/1442417534444064769
StackOverflow遷移至.NET5.0: https://twitter.com/juanrodriguezce/status/1428070925698805771
StackOverflow遷移至.NET6.0: https://wouterdekort.com/2022/05/25/the-stackoverflow-journey-to-dotnet6/
必應廣告活動平臺遷移至.NET6.0: https://devblogs.microsoft.com/dotnet/bing-ads-campaign-platform-journey-to-dotnet-6/
Microsoft Commerce的.NET6.0遷移之旅: https://devblogs.microsoft.com/dotnet/microsoft-commerce-dotnet-6-migration-journey/
Microsoft Teams服務到.NET6.0的旅程: https://devblogs.microsoft.com/dotnet/microsoft-teams-assignments-service-dotnet-6-journey/
OneService 到 .NET 6.0的旅程 :https://devblogs.microsoft.com/dotnet/one-service-journey-to-dotnet-6/
Exchange 線上版遷移至 .NET Core: https://devblogs.microsoft.com/dotnet/exchange-online-journey-to-net-core/
Azure Cosmos DB 到 .NET 6.0的旅程: https://devblogs.microsoft.com/dotnet/the-azure-cosmos-db-journey-to-net-6/
相信大家在開發中經常會遇到一些效能問題,苦於沒有有效的工具去發現效能瓶頸,或者是發現瓶頸以後不知道該如何優化。之前一直有讀者朋友詢問有沒有技術交流群,但是由於各種原因一直都沒建立,現在很高興的在這裡宣佈,我建立了一個專門交流.NET效能優化經驗的群組,主題包括但不限於:
希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET效能問題和寶貴的效能分析優化經驗。目前一群已滿,現在開放二群。
如果提示已經達到200人,可以加我微信,我拉你進群: ls1075
另外也建立了QQ群,群號: 687779078,歡迎大家加入。