摘要: 大概一年前开始在思考 构造函数中 依赖注入较多,这对系统性能及硬件资源消耗产生一些优化想法。 一般较多公司的项目都使用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. 调整后的代码
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/
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 高级调试扛把子 : @一线码农