某酒管集團-單例模式對效能的影響及思考

2023-08-23 21:00:22
摘要:  大概一年前開始在思考 建構函式中 依賴注入較多,這對系統效能及硬體資源消耗產生一些優化想法。 一般較多公司的專案都使用Autofac  依賴注入(Scoped 作用域),但是發現過多的物件產生 會消耗  CPU , 記憶體 並給GC(垃圾回收)造成一定的壓力。那麼開始思考是否能夠使用 單例 (Singleton)來解決這些問題呢? 帶著這些想法開始ReView整個專案的程式碼,排查是否存在 單例 會造成 執行緒安全 或 方法內修改全域性變數的程式碼( 結果是樂觀的.... )。於是開始了效能測試....論證.. 試執行... ,結果是超預期的(CPU 從 60%-降低到--》10%, 記憶體 從 33%-降低到--》20%, 介面平均響應時間 從 120毫秒--降低到--》50毫秒 . 1500/QPS (不含內部服務相互呼叫))  和  @InCerry   溝通結果,說可以寫個 案例 和大家分享分享...  於是乎 有了這一片文章。

基礎概念介紹

1.依賴注入(Dependency Injection  ,  DI)

依賴注入(Dependency Injection,DI)是一種實現控制反轉(IoC)的技術。它是指通過外部的方式將一個物件的依賴關係注入到該物件中,而不是由該物件自己建立或查詢依賴的物件。依賴注入可以通過建構函式、屬性或方法引數等方式實現。

依賴注入的好處是可以降低物件之間的耦合性,提高程式碼的可測試性和可維護性。通過將依賴關係從物件內部移動到外部,我們可以更容易地替換依賴的物件,以及更容易地進行單元測試。同時,依賴注入也可以使程式碼更加靈活和可延伸,因為我們可以通過注入不同的依賴來改變物件的行為。 

日常編碼的時候大家追求的都是高內聚低耦合這種就是良性的依賴,避免 牽一髮動全身的則是惡性依賴重則推到重構、輕則維護困難。

2. 控制反轉 (Inversion of Control , IoC)

控制反轉 (Inversion of Control , IoC) 最早是世界級軟體開發大師 Martin Fowler 提出的一種設計原則,它指導我們將控制權從應用程式程式碼中轉移到框架或容器中。IoC原則要求我們將物件的建立、依賴注入和生命週期管理等控制權交給框架或容器來處理,而不是由應用程式程式碼來直接控制。

這樣做的好處是,可以降低程式碼的耦合性,提高程式碼的可測試性和可維護性。框架或容器負責管理物件的建立和銷燬,以及解決物件之間的依賴關係,應用程式程式碼只需要關注業務邏輯的實現。

3. 依賴倒置原則(Dependence Inversion Principle , DIP)

依賴倒置原則(Dependence Inversion Principle , DIP)是物件導向設計中的一個原則,它指導我們在設計軟體時應該依賴於抽象而不是具體實現。

DIP原則要求高層模組不應該依賴於低層模組,而是應該依賴於抽象介面。這樣做的好處是,當我們需要修改低層模組的實現時,高層模組不需要做任何修改,只需要修改抽象介面的實現即可。這樣可以提高程式碼的靈活性和可維護性。

生命週期

1. 單例模式   (Singleton)

單例模式是指在整個應用程式中只建立一個物件範例,並且該範例在整個應用程式的生命週期內都是可用的。單例模式可以通過IoC容器來管理,容器會在第一次請求該物件時建立一個範例,並在後續的請求中返回同一個範例。在整個應用程式生命週期中只建立一個範例,並且該範例將被共用和重用。

由於只建立一個範例並重用它,因此在效能方面可能更高效。  但是,*** →→→※※※注意:如果該範例包含狀態或可變資料,可能需要考慮執行緒安全性 和 避免修改全域性變數 ※※※⬅⬅⬅***。

2. 作用域模式  (Scoped)

作用域模式是指根據物件的作用域來管理物件的生命週期。常見的作用域包括請求作用域、對談作用域和應用程式作用域。在請求作用域中,每個請求都會建立一個新的物件範例,並且該範例只在該請求的處理過程中可用。在對談作用域中,每個對談都會建立一個新的物件範例,並且該範例在整個對談的生命週期內可用。

在每個請求或作用域內建立一個範例,並且該範例只在該請求或作用域內共用和重用。作用域模式適用於那些需要根據不同的上下文來管理物件生命週期的情況。

3. 瞬時模式  (Transient)

瞬時模式是指每次請求都會建立一個新的物件範例,並且該範例只在該請求的處理過程中可用。瞬時模式適用於那些不需要共用狀態或資源的物件,每次請求都需要一個新的物件範例。  (這種一般實際專案中 用的比較少。)

 Autofac  更多資訊: https://autofac.org/    (檔案)   https://github.com/autofac/Autofac (原始碼) 
Microsoft.Extensions.DependencyInjection  更多資訊:  https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.dependencyinjection?view=dotnet-plat-ext-8.0  (檔案)

單例模式的調整

1. 調整後的程式碼

 1.  因: Services & Repositories 建構函式依賴注入較多,且 注入的class類 的建構函式又有建構函式,由此導致請求需要範例化的物件非常多,較多的物件又會對GC造成一定的影響。(當然你可以調整成屬性注入來解決此問題)

 2.  所: 調整為 Singleton 單例模式 提升系統效能,需要特別注意: 如果範例包含狀態或可變資料,可能需要考慮執行緒安全性 和 避免修改全域性變數 (請做好壓力測試  以及 灰度上線觀察)。

Me Dyx :  單例& 作用域)從底層 解釋一下區別呢?
        老A (蔣老師 Artech)  : 由於方法對應IL沒有本質區別,所以兩者的區別在於一個不需要每次範例化分配記憶體,如果呼叫頻繁,會增加GC壓力。

Me Dyx:   能使用單例的時候  是否應該優先使用 單例呢?  畢竟 new 一個新物件 有開銷,還要垃圾回收 呼叫 GC 。

        老A (蔣老師 Artech)  :  當然 ,  面向GC程式設計

         /// <summary>
		/// 依賴注入  new
		/// </summary>
		public static void RegisterDependencyNew()
		{
			var builder = new ContainerBuilder();
			// 註冊 MVC 容器的實現
			builder.RegisterControllers(Assembly.GetExecutingAssembly());
			// 註冊服務和倉儲
			RegisterTypesBySuffix(builder, "Service");
			RegisterTypesBySuffix(builder, "Repository");
			// 註冊快取管理器和 Redis 快取管理器
			//builder.RegisterInstance(CacheSetting.CacheManager).SingleInstance();
			//builder.Register(r =>
			//{
			//	return CacheSetting.CacheManager;
			//}).AsSelf().SingleInstance();

			//builder.RegisterType<RedisCacheManager>().As<IRedisCacheManager>().SingleInstance();
			// 註冊 Cap 釋出器
			//builder.RegisterInstance(GetCapPublisher()).SingleInstance();

			//builder.Register<ICapPublisher>(r =>
			//{
			//	return CapConfig.Services.BuildServiceProvider().GetRequiredService<ICapPublisher>();
			//}).AsSelf().SingleInstance();

			var container = builder.Build();
			DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
		}
		private static void RegisterTypesBySuffix(ContainerBuilder builder, string suffix)
		{
			var assemblys = BuildManager.GetReferencedAssemblies().Cast<Assembly>();
			builder.RegisterAssemblyTypes(assemblys.ToArray())
				   .Where(t => t.Name.EndsWith(suffix))
				   .AsImplementedInterfaces()
				   .SingleInstance();
		}

  

2. 調整前的程式碼

        /// <summary>
		/// 依賴注入-Old 
		/// </summary>
		public static void RegisterDependencyOld()
		{
			var builder = new ContainerBuilder();
			//註冊mvc容器的實現
			builder.RegisterControllers(Assembly.GetExecutingAssembly());
			//如果有web型別,請使用如下獲取Assenbly方法
			var assemblys = BuildManager.GetReferencedAssemblies().Cast<Assembly>().ToList();
			builder.RegisterAssemblyTypes(assemblys.ToArray()).Where(t => t.Name.EndsWith("Service")).AsImplementedInterfaces();
			builder.RegisterAssemblyTypes(assemblys.ToArray()).Where(t => t.Name.EndsWith("Repository")).AsImplementedInterfaces();

			/*
             //在Autofac中註冊Redis的連線,並設定為Singleton (官方建議保留Connection,重複使用)
             builder.Register(r =>
             {
                 return ConnectionMultiplexer.Connect(DbSetting.Redis);
             }).AsSelf().SingleInstance();
            */
			//在Autofac中註冊CacheManager 快取設定,並設定為Singleton[https://github.com/MichaCo/CacheManager/issues/27]
			//builder.Register(r =>
			//{
			//	return CacheSetting.CacheManager;
			//}).AsSelf().SingleInstance();

			//builder.Register(c => new RedisCacheManager()).As<IRedisCacheManager>().AsSelf().SingleInstance();

			//builder.Register<ICapPublisher>(r =>
			//{
			//	return CapConfig.Services.BuildServiceProvider().GetRequiredService<ICapPublisher>();
			//}).AsSelf().SingleInstance();


			var container = builder.Build();
			DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
		}

生產執行狀態監控

1. CPU 

 

 

 

2. 記憶體

 

3. 介面響應時間

 

關於效能優化

1. 框架版本

*  *  .NET Framework和.NET Core是微軟的兩個不同的開發平臺。

1. .NET Framework:.NET Framework是微軟最早釋出的開發平臺,它是一個完整的、統一的Windows應用程式開發框架。它支援多種程式語言(如C#、VB.NET等)和多種應用型別(如Windows桌面應用、ASP.NET Web應用等)。.NET Framework依賴於Windows作業系統,並且只能在Windows上執行。

2. .NET Core:.NET Core是微軟在.NET Framework基礎上進行的重寫和改進,它是一個跨平臺的開發平臺。.NET Core具有更小、更快、更模組化的特點,可以在Windows、Linux和macOS等多個作業系統上執行。.NET Core支援多種程式語言(如C#、F#、VB.NET等)和多種應用型別(如控制檯應用、Web應用、移動應用等)。

* *   升級到.NET Core版本對效能有以下好處:

1. 更高的效能:.NET Core在效能方面進行了優化,具有更快的啟動時間和更高的吞吐量。它採用了新的JIT編譯器(RyuJIT)和優化的垃圾回收器(CoreCLR),可以提供更好的效能。

2. 更小的記憶體佔用:.NET Core採用了更精簡的執行時庫,可以減少應用程式的記憶體佔用。這對於雲端計算和容器化部署非常有利。

3. 跨平臺支援:.NET Core可以在多個作業系統上執行,包括Windows、Linux和macOS等。這使得開發人員可以更靈活地選擇執行環境,並且可以更好地適應不同的部署需求。

4. 更好的可延伸性:.NET Core提供了更多的開發工具和庫,可以更方便地構建可延伸的應用程式。它支援微服務架構和容器化部署,可以更好地應對大規模應用的需求。

   升級到.NET Core版本可以帶來更高的效能、更小的記憶體佔用、更好的跨平臺支援和更好的可延伸性。這些優勢使得.NET Core成為現代應用程式開發具有效能優勢。

2. 升級外掛 (.NET Upgrade Assistant  外掛, .NET Framework  升級至跨平臺的  .NET Core)

1. 在 VS 2022 中進行 .NET Upgrade Assistant 的安裝。

2. 按照 提示下一步 等待片刻 即可:

 3.   開啟您需要升級的專案,在專案上點選右鍵就會出現 Upgrade 按鈕:

4. 升級後... 可能編輯器會提示N個錯誤...別慌.. 很多都是一個原因導致的,升級相關第三方元件支援 .net core, 靜下心來 逐個解決,上線前做好 充足的測試。

(保守估計,在您不修改專案原有邏輯,整體效能會提升 30%+ ,什麼你不信? ^_^  接著往下看 其他公司案例... ) 

  因 .NET Core 的底層全部重構了具有後發優勢(重新開發,重新面向雲原生設計 從 core 1.0 / 1.1 /2.0 / 2.1 「不完善比較坑」 , 到現在的 3.1 ,5.0, 6.0 ,7.0, 以及即將釋出的 8.0 經過不斷完善改進 目前已經非常穩定可靠 ), 拋棄了原有的.NET Framework 底層和Window深度捆綁。

 使用 .NET 升級助手將 ASP.NET Framework 新式化為 ASP.NET Core - Training | Microsoft Learn

從 ASP.NET 更新到 ASP.NET Core | Microsoft Learn

 

3. 其他 (升級後的收穫分享)

1. 同程旅行 .Net 微服務遷移至.Net 6.0的故事: https://mp.weixin.qq.com/s/I8BQERm0xXHKgF2OxMCVTA

2. 遷移至.NET5.0後CPU佔用降低:https://twitter.com/stebets/status/1442417534444064769 

3. StackOverflow遷移至.NET5.0: https://twitter.com/juanrodriguezce/status/1428070925698805771

 

4. StackOverflow遷移至.NET6.0: https://wouterdekort.com/2022/05/25/the-stackoverflow-journey-to-dotnet6/

5. 必應廣告活動平臺遷移至.NET6.0: https://devblogs.microsoft.com/dotnet/bing-ads-campaign-platform-journey-to-dotnet-6/

6. Microsoft Commerce的.NET6.0遷移之旅: https://devblogs.microsoft.com/dotnet/microsoft-commerce-dotnet-6-migration-journey/

7. Microsoft Teams服務到.NET6.0的旅程: https://devblogs.microsoft.com/dotnet/microsoft-teams-assignments-service-dotnet-6-journey/

8.OneService 到 .NET 6.0的旅程 :https://devblogs.microsoft.com/dotnet/one-service-journey-to-dotnet-6/

 9. Exchange 線上版遷移至 .NET Core: https://devblogs.microsoft.com/dotnet/exchange-online-journey-to-net-core/

10. Azure Cosmos DB 到 .NET 6.0的旅程: https://devblogs.microsoft.com/dotnet/the-azure-cosmos-db-journey-to-net-6/

....歡迎補充 ,其他的案例分享。 

4 .  提升效能的寫法和技巧


1. 使用非同步程式設計:使用非同步方法可以提高應用程式的響應效能,特別是在處理I/O密集型操作時。通過使用async和await關鍵字,可以將長時間執行的操作放在後臺執行緒上,從而釋放主執行緒並提高應用程式的吞吐量,  Channel 通道,程序內佇列 (Queue)。

2. 使用記憶體池:在.NET Core中,可以使用MemoryPool<T>類來管理記憶體分配和回收。通過重用記憶體塊,可以減少垃圾回收的頻率,從而提高效能。

3. 避免頻繁的裝箱和拆箱:裝箱和拆箱操作會引入額外的開銷,可以通過使用泛型和值型別來避免這些操作。

4. 使用Span<T>和Memory<T>:Span<T>和Memory<T>是.NET Core中的新型別,用於高效地處理記憶體。它們提供了一種零拷貝的方式來存取和操作記憶體,可以減少記憶體分配和複製的開銷。

5. 使用並行程式設計:在處理大量資料或執行密集計算的情況下,可以使用並行程式設計來利用多核處理器的效能。通過使用Parallel類或PLINQ,可以將工作分解成多個並行任務,並利用所有可用的處理器核心。

6. 使用快取:在適當的情況下,可以使用快取來儲存計算結果或頻繁存取的資料。通過減少重複計算或資料庫查詢,可以顯著提高效能。

7. 使用非同步資料庫存取:如果應用程式需要頻繁地存取資料庫,可以考慮使用非同步資料庫存取。通過使用非同步方法,可以在等待資料庫響應時釋放執行緒,並允許其他請求繼續執行。

8. 使用快取策略:在使用快取時,可以使用不同的快取策略來平衡效能和資料一致性。例如,可以使用基於時間的過期策略或基於依賴項的過期策略來控制快取的有效期。

9. 使用連線池:在使用資料庫連線或其他資源時,可以使用連線池來管理連線的建立和回收。連線池可以減少連線的建立和銷燬開銷,並提高應用程式的效能。

10. 使用批次操作:在執行資料庫操作時,可以考慮使用批次操作來減少與資料庫的通訊次數。通過將多個操作合併為一個批次操作,可以減少網路延遲和資料庫開銷。

11. 使用效能分析工具:使用效能分析工具,如.NET Core Profiler或dotTrace,可以幫助識別效能瓶頸和優化機會。通過分析應用程式的效能特徵,可以找到效能瓶頸並採取相應的優化措施。
除了效能分析工具,還有其他一些效能優化工具可以幫助識別和解決效能問題。例如,可以使用效能監視器來監視應用程式的效能指標,並根據需要進行調整。
       * * 效能 分析平臺(火焰圖): grafana/pyroscope: Continuous Profiling Platform. Debug performance issues down to a single line of code (github.com) 
       * * 系統執行異常實時監控面版: exceptionless/Exceptionless: Exceptionless application (github.com)

.NET 診斷工具 :  https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/tools-overview      WinDebug 高階偵錯扛把子 : @一線碼農