- A+
一、背景
车家号作为一个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
作者| 张保维