草庐IT

从.net framework 到 .net core:车家号项目的升级过程及经验

轻风博客 2023-03-28 原文

 

一、背景

      车家号作为一个PGC平台,聚合了全网大多数汽车行业的专家及意见领袖,每天为用户提供大量的汽车类优质内容。用户日浏览量在几千万级,后端的接口也承载亿级的日访问量。

      车家号WEB、API、后台管理等系统采用 .net4.5进行开发。一直以来为用户及调用方提供了稳定的服务。由于其只能运行于Windows平台上,其扩展及迁移的能力受到了极大限制。需要将车家号业务转移到Linux平台,可以进行更为灵活的运维,并且具有容器化能力。

       方案之一,用java重写,这个对于一个已经维护多年的有大量的业务逻辑在里面的系统来说,工作量是相当大的。只重写接口相对简单,但如果将PC 及后台管理进行重写工作量极大。或前后分离NodeJS 方式,这样也会给前端开发及测试带来巨大大的压力。还有更重要的一点,需求不断的提出来,还要不断有新特性加入进来,如果维护两套异构语言的系统,会给业务及系统的稳定性带来很大的风险。

      另一个方案,项目从.net framework 升级为dotnet core,基本上语法方面不需要变化,业务代码不需要变化,新需求的加入,单向同步代码即可。同时,.net core 是跨平台的,另外,可以使用docker进行弹性扩容缩容。.net core 在性能方面从官方数据及后来的一些测试看的确有了明显的提升。

       所以,基于这些主要原因,我们选择了方案二,.net core ,并且容器化。

 

二、人员投入

时间

天数

人员

人日

2月21日-2月27日

5

1人

5人日

4月1日 -5月13日

27

0.5人

13.5人日

5月14日-5月24日

9

1.5 人

13.5人日

5月27日-6月6日

9

2.5人

22.5人日

合计:

   

54.5人日

 升级前期,因存在尝试、学习等因素,人员投入较少。后面调整重心,加强人力投入到升级工作中,快速推进升级上线。

 公司范围内,我们是第一个将较大项目进行.net core 升级的团队。没有太多dotnet core的积累,无成功案例可借鉴,同时大量工作也用在了构建基础设施。未来类似规模项目进行升级,时间及人力投入方面会大大减少。

 

三、升级效果

1.性能提升

从5.25日开始逐步灰度,tp99也从100ms逐步降到45ms左右,到发稿为止稳定保持于45ms以下。

2.无感知升级

接口,WEB,后台管理等从外观及行为上没有的变化,做到用户及调用方无感知。

3.弹性

由于core 可运行于Linux上,可以借助于之家云平台,在容器中弹性扩容。

 

四、主要过程

在业务需求不断变更的同时,在第一次全量完成代码同步后,严格从原有业务主干(.net framework + TFS)单向同步到新版本业务主干(.net core+ git),保证业务行为的一致性,并不断同步发布新版本进行灰度,两个版本的行为保持一致性,升级过程无缝切换。如下图所示:

 

1.准备阶段

由于.net core兼容性及某些功能的缺失,需要使用重新编译或编写新组件进行兼容,这些作为基础组件,需要进行更多的单元测试及测试项目中的测试,保证其它正确性,稳定性及兼容性。

  • .net core 已经无法使用.net framework的组件,所有原有组件都需要重新编译适配。(见组件列表)

  • Asp.net core 中 有些功能已经不再支持,需要重新适配。见附兼容组件 

2.编译通过阶段可运行阶段

  • 自底向上,逐个项目进行升级。

  • 卸载所有项目。

  • 从基础的底层的类库进行升级,通常其依赖着准备阶段中提到的各种组件。

  • 底层公共项目,通常都可以顺利编译通过。

  • 对于实在无法编译通过的项目,根据其作作决定是否先注释掉,并用异常抛出的方式先行替代(throw new NotImplementedException()),防止未来的误调用或忘记实现。

  • 根据原有项目间依赖并系,逐步上向。

  • 先将接口项目编译通过,由于其不依赖UI等,比较容易通过,替换兼容,运行。

  • WEB 项目

  • Asp.net Core 与 asp.net 项目结构不同需要进行调整。

  • 批量替换命名空间

  • 编译,如果有不能通过的,看是否能快速解决或重要性,决定是否跳过。

经过上面的操作,项目基本可以运行,完成升级过程的第一步。

3.细化解决问题并同步代码

Core 版代码使用git 进行管理,从 TFS某个时间点进行快照,并Push到git库。作为基础,进行升级改造,对组件及代码级别的不兼容进行调整。后面新业务也是在这个版本的基础上进行增量同步。

后面的需求还在继续在TFS上提交,采用由TFS到Git单向同步的方式,TFS上的新需求由负责的开发人员同步至git 上。这种情况将持续到全量上线。

这个阶段经历了大致两个星期。

4.测试

  • 开发自测:开发人员 自动用例生成工具,生成测试用例,使用JMeter 测试,大规模的覆盖,快速发现并修复问题。另外,使用自动化接口对比工具,完成全量接口对数据对比,完全节省了人工对于接口的重复测试,快速发现并定位问题。

  • 测试人员测试:对于一些重要场景,如作者发现文章及与财务相关的功能,需要人工方式进行二次验证。

5.灰度上线

虽然经过了多次测试,但一些场景可能会无法覆盖,需要采用灰度方式。将新版加入负载,经过从万分之一,百分之一,十分之一,二分之一,最终全量。

在灰度过程中,对错误日志、访问日志、性能日志等多维度进行监测,保证系统负载在可控范围内,并且发现有异常及时修复或回调流量。

6.全量切换

系统上是先从PC开始的,等PC上线稳定以后,再上线接口,服务,对外接口等其它系统。

PC:在构成方面虽然比较复杂,但它前面有CDN 及SCS,万一出现问题,来得及进行补救。

接口:流量大,但结构相对简单,如果出现问题,影响面比较大,所以,在PC成功切换后,有了经验及信心,再进行切换。

后台管理:运营使用,有问题可能快速反馈,并能快速修复。

服务:核心业务已经上线,服务,也要跟着上线了。

Open接口:由于流量及用户都不多,并且大多数是Post接口,需要更充分的测试,选择最后上线。

6月6日 最终完成所有系统的全量的上线。

 

五、经验

1.备用方案,随时可回滚

在最初制订方案的时候,考虑到极有可能出现意外,导致新系统有不可预知的问题。需要具有随时切换回旧系统的能力,当然,这个也是灰度的基础。

保证可以切回,要保证:

    (1)最重要的,代码要保持同步。

    (2)同步发布,新旧两个环境都要保持最新,同步。

    (3)在完成稳定迁移,旧的环境一直保持高可用。

在本次迁移过程中,由于Core所在集群故障,紧急切回旧系统,这样避免了一次非常严重的事故。 

2.扩容要谨慎

升级过程中某天,系统的流量突然异常。系统开始变慢,但还能勉强顶住。

随后做出了扩容决定,从8个实例,直接扩容为30个。接下来,就是连锁反应,WEB,API,相继告急,性能极度下降。数据库不断报警。

对于我们从传统的单体应用进行迁移至Docker, 不要急于扩容,一定要看到你的系统是否有单点,这个单点是否可以避免。

3.自测工具

本次升级,测试人员投入大概两个半天,进行测试。剩下的都由开发人员进行自测。使用工具进行大规模的覆盖。

测试工具,对比工具等都根据实际业务进行自主开发。在未来也可以重复用于其它或日常工作中。

接口对比及批量对比测试:

 

自动生成用例及在JMeter中测试:

 

4.日志工具

.net core 在 Linux下是没有相应的访问日志,另一个途径也可以从Nginx获取访问日志,但它与我们现有日志分析及监控系统不兼容。

基于.net core 中间件,对访问进行拦截并记录日志。使用Udp 方式发送至日志收集系统。

这样对系统的访问数量及处理性能通过报表有直观的了解。

5.监控工具

.net core 由于是一个新的生态,没有类似于windows下的性能记数器,或Java的Jmx这种工具对性能进行监控。所以,我们需要自己来做一套监控解决方案。

(1)  做SDK,收集系统运行性能指标。

(2)  提交至Logstash

(3)  Logstash 写入 ES

(4) 通过Grafana进行图形化展示。

 

6.缓存注解

从Java中学习到Spring boot 的Cache注解,按其思路实现注解并应用于.net core , 一个注解,省去大片代码。

 

代码中使用注解示例效果:

 

使用注解前后效果TP 99对比:

 

 

 

7.缓存的一致性

由于我们的系统极度依赖于缓存层,并且服务会更新缓存,新旧的系统如果可以使用同一套缓存,使用同一个Key,那可以减少很多必要的麻烦。

我们重新编译了ServiceStack.Redis 。保证了缓存的行为的一致性,这为我们节省了至少三分之一的工作量,使得新旧各系统中都是一致的缓存,一致数据。

8.   Json 框架

在当前系统中用到了多个框架,Json.net , FastJson , MS Json , 在代码调试过程中,新旧系统使用返回数据不一致,也发生各种序列化异常,经过分析,原来一旧代码与新代码使用不同的Json序列化方式。

在系统迁移过程中,由于多处使用了这三个框架,只重构掉了一种MS Json,现在还有 Json.net , FastJson 两种在运行着。

后面系统稳定后,应该仅保留一种序列化框架。

9.Docker性能

当时转core一个原因就是想使用 docker 进行动态扩容、缩容的特性。本次升级过程中,由于所在机房资源不足,docker集群不稳定,迁移机房暂时不具备条件等原因。只能将部分业务,如后台,服务,Open接口等运行在docker上,其它随时压力较大的如WEB ,接口等运行在VM上,在迁移机房后,再将大流量产品进行迁移。

10.  先运行再解决Bug

过程中不要把问题放在一个点上,先保证系统可以整体运行,再去解决某个点的技术问题,为团队树立信心,让每个人都可以看得到它是可运行的。

 

六、最后

在本次升级过程中,让我们感受到升级.net core很小的学习曲线,快速入手,性能的提升也是一种惊喜。还有,大多数常用的开源组件对core提供了支持,改极少或不用改代码即可使用。

同时,升级过程中,积累了经验,产出了通用性功能及组件,对于其它系统的升级具有指导和借鉴意义。

还有,这个成功的案例,也给有升级想法的团队,树立信心,使他们可以更快迈出这一步。

升级过程发现了一些问题,促使我们对于架构进一步的思考,在后面通过对架构的优化使系统具有更好的扩展能力。

 

七、附录

1.组件

  • dotcoreActionMessageSdk  发送HttpMessageQuene 组件

  • dotNews.ServiceStack.Redis  Redis连接组件

  • dotcoreAutohome.DataHelper  Sqlserver连接组件

  • dotCoreCasClient   SSO 组件

  • dotcorejobclient   分布式Job组件

  • dotcoreNews.Comm 通用类型组件

  • dotcoreNews.Common.Extends.Log ELK 日志组件

  • dotcoreNews.Common.Extends.Upload 图片上传组件

  • dotcoreNews.Common.UploadObject 文件上传组件

  • dotcoreWebdiyer.MvcPagerCore  ajax分页组件

2.兼容手段

  • dotcoreHttpContextCurrentCore组件  asp.net core 已经没有HttpContext.Current, 本项目实现 System.Web.HttpContext.Current 功能,最大程度兼容旧代码,减少重写量

  •  core 不再支持 WCF ,未来版本是否支持社区还在争论中,我们采用重写路由的方式进行兼容。

  •  Html.Action 不再支持,重写这个方法,为了区别更别为Html.RenderAction,参数功能不变。

  • EF Repository 模式代码,基于 sqlSugarCore 改写。保持调用外观不变。

  • Microsoft.Practices.ServiceLocation 不再支持,使用 Castle.Core

  • using System.Web.Mvc,不再支持, 使用  usingMicrosoft.AspNetCore.Mvc;

  • Areas 的支持不再,使用 router

  • Ashx 一般处理程序不支持,改用 router兼容

  • [assembly: PreApplicationStartMethod] 不支持,改用 StartUp.cs中调用

  • HTTP handlers and modules 改为 middleware

3. 工具支持

  • 自制应用程序指标监控

  • 自制批量用例自动生成

  • 自制指量接口比较工具

  • Jmeter

 

作者| 张保维

有关从.net framework 到 .net core:车家号项目的升级过程及经验的更多相关文章

  1. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  2. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  3. ruby-on-rails - 项目升级后 Pow 不会更改 ruby​​ 版本 - 2

    我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby​​版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby​​版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘

  4. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

    我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

  5. ruby-on-rails - 新 Rails 项目 : 'bundle install' can't install rails in gemfile - 2

    我已经像这样安装了一个新的Rails项目:$railsnewsite它执行并到达:bundleinstall但是当它似乎尝试安装依赖项时我得到了这个错误Gem::Ext::BuildError:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcheckingforlibkern/OSAtomic.h...yescreatingMakefilemake"DESTDIR="cleanmake"DESTDIR="

  6. Ruby 从大范围中获取第 n 个项目 - 2

    假设我有这个范围:("aaaaa".."zzzzz")如何在不事先/每次生成整个项目的情况下从范围中获取第N个项目? 最佳答案 一种快速简便的方法:("aaaaa".."zzzzz").first(42).last#==>"aaabp"如果出于某种原因你不得不一遍又一遍地这样做,或者如果你需要避免为前N个元素构建中间数组,你可以这样写:moduleEnumerabledefskip(n)returnto_enum:skip,nunlessblock_given?each_with_indexdo|item,index|yieldit

  7. ruby - 在不使用 RVM 的情况下在 Mac 上卸载和升级 Ruby - 2

    我最近决定从我的系统中卸载RVM。在thispage提出的一些论点说服我:实际上,我的决定是,我根本不想担心Ruby的多个版本。我只想使用1.9.2-p290版本而不用担心其他任何事情。但是,当我在我的Mac上运行ruby--version时,它告诉我我的版本是1.8.7。我四处寻找如何简单地从我的Mac上卸载这个Ruby,但奇怪的是我没有找到任何东西。似乎唯一想卸载Ruby的人运行linux,而使用Mac的每个人都推荐RVM。如何从我的Mac上卸载Ruby1.8.7?我想升级到1.9.2-p290版本,并且我希望我的系统上只有一个版本。 最佳答案

  8. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  9. ruby - 如何在 Ruby 字符串中插入项目符号字符? - 2

    我正在尝试创建一个带有项目符号字符的Ruby1.9.3字符串。str="•"+"helloworld"但是,当我输入它时,我收到有关非ASCII字符的语法错误。我该怎么做? 最佳答案 你可以把Unicode字符放在那里。str="\u2022"+"helloworld" 关于ruby-如何在Ruby字符串中插入项目符号字符?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/1195

  10. ruby - 在 Rails 项目中测试本地版本的 gem - 2

    我的Rails站点使用了一个确实不是很好的gem。每次我需要做一些新的事情时,我最终不得不花费与向实际Rails项目添加代码一样多的时间来为gem添加功能。但我不介意,我将我的Gemfile设置为指向我的gem的GitHub分支(我尝试提交PR,但维护者似乎已经下台)。问题是我真的没有找到一种合理的方法来测试我添加到gem的新东西。在railsc中测试它会特别好,但我能想到的唯一方法是a)更改~/.rvm/gems/.../foo。rb,这看起来不对或者b)升级版本,推送到Github,然后运行​​bundleup,这除了耗时之外显然是一场灾难,因为我不确定我所做的promise是否正

随机推荐