本次迁移涉及的是公司内部一个业务子系统,该系统是一个多样化的应用,支撑着公司的多个业务方向。目前,该系统由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/#section=data-r21&test=composite
在科学计算的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,欢迎大家加入。