在经过了两年的准备,以及迁移了几个应用项目积累了让我有信心的经验之后,我最近在开始将团队里面最大的一个项目,从 .NET Framework 4.5 迁移到 .NET 6 上。这是一个从 2016 时开始开发,最多有 50 多位开发者参与,代码的 MR 数量过万,而且整个团队没有一个人能说清楚项目里面的所有功能。此项目引用了团队内部的大量的基础库,有很多基础库长年不活跃。此应用项目当前也有近千万的用户量,迁移的过程也需要准备很多补救方法。如此复杂的一个项目,自然需要用到很多黑科技才能完成到 .NET 6 的落地。本文将告诉大家这个过程里,我踩到的坑,以及学到的知识,和为什么会如此做
前文
准确来说,我在这个过程其实算是最后一公里,我估算了工作量,大概将这个项目从 .NET Framework 4.5 迁移到 .NET 6 上的工时约 1.5 年人。虽然我现在说的是我用了五周的时间就完成了,但实际上在此前的准备工作是没有被我算上的。此前的工作包括什么?还包括将各大基础库更改为支持 dotnet core 的版本,填补 dotnet core 和 dotnet framework 的差异,例如 .NET Remoting 和 WCF 等 IPC 的缺失。更新打包平台和构建平台,使支持 dotnet core 的构建和打包。更新软件的 OTA 也就是软件自动更新功能,用于支持复杂的灰度发布功能和测试 .NET 6 环境支持。逐步从边缘到核心,逐个应用项目迁移,进行踩坑和积累经验
在做足了准备之后,再加上足量的勇气,以及一个好的时机,在整个团队的支持下,我就开始进行最后一公里的迁移
其实在进行最后的从 .NET Framework 4.5 换到 .NET 6 之前,整个团队包括我都是完全没有想到还有如此多的坑需要填的。这个庞大的项目用了多少奇奇怪怪的黑科技还是没有人知道的。在记录本文时,我和伙伴们说,也许世界上没有其他的团队也会遇到咱的问题了
背景
一个从 2016 时开始开发,最多有 50 多位开发者参与,而且这些开发者们没几位是省油的,有任何东西都需要自己造的开发者,有任何东西只要能用别人做好的绝不自己造的开发者,有写代码上过央视的开发者,有参与制定国家标准的开发者,有一个类里面一定要用满奇特的设计模式的开发者,有在代码注释里面一定要放大佛的开发者,有学到啥黑科技就一定要用上的开发者,有只要代码和人一个能跑就好的开发者,有睁着眼睛说瞎话代码和注释完全是两回事的开发者,有代码注释是文言文的开发者,有代码注释是全英文的开发者,有注释和文档远超过代码量的开发者,有中文还没学好的开发者,有喜欢挖坑而且必须自己踩的开发者,有啥东西都需要加日志的开发者,有十分帅穿着西装写代码的开发者,有穿着女装写代码的开发者,有在代码里面卖萌的开发者,有 这个函数只有我才能调用
的开发者,有相同的逻辑一定要用不同的方式实现的开发者,有在奔跑的坦克上换引擎的开发者
在本次迁移的过程,还有一些坑需要填。其中一个就是 dotnet core 里面,没有一个多 Exe 入口的客户端应用的最佳实践。这里面涉及到客户端应用独立管理运行时环境时,多个 Exe 的冲突处理和安装完成之后的文件夹体积的矛盾。这个也是本文分享的重点
本次还带了一些需求,包括: 在确定系统环境满足的情况下,低限度依赖系统,且需要做到不会被用户系统上所安装的 dotnet 运行时所影响。另外,考虑到后续要支持产品线内多个应用都共用运行时,但此运行时不能和其他团队,其他公司所共有避免被魔改,还需要进行一些尝试逻辑。最后,对使用的 WPF 版本是要求定制的,也就是说需要在官方发布版本的基础上,更改部分逻辑,满足特殊的产品需求
这就意味着将 dotnet 重新分发,设置为团队完全控制的库。这个变更之后,在更新到 .NET 6 之后,可以执行完全的自主控制 dotnet 框架,包括 WPF 框架。于是可以做的事情就更加多了,无法实现的东西就更少了
为了做到对 WPF 更多的定制化,我将 WPF 框架的地位从原先的应用运行时层,更改为基础库层,地位和 团队里面的基础组件 等 CBB 相同,只是作为底层库而存在,架构上和 最底层的基础库 平级
本次遇到的问题分为两个大类,一个是此项目本身的复杂度带来的问题,另一个是 dotnet 带来的问题。本文只记录 dotnet 所带来的问题,其中更多部分是因为特殊需求定制而导致问题
开发架构
原本的应用开发架构上,所依赖的 .NET Framework 是作为系统组件的存在。系统组件受到系统环境的影响,在国内妖魔鬼怪的环境下,系统组件被魔改被损坏是常态。采用 .NET Framework 的应用有着很大的客服成本,需要帮助用户解决环境问题。随着用户量越来越大,这部分的客服成本也越来越大。这也就是为什么有能投入到如此多资源来更新项目的原因之一
原本的应用开发架构分层如下图
在更新到 dotnet 之后,运行时是在系统层的上方。如此的设计即可减少系统环境的影响,解决大量的应用环境问题
从上图可以看到 WPF 是作为运行时的部分存在,但这不利于后续对 WPF 的定制化。我所在的团队期望能完全将 WPF 进行控制,对 WPF 框架做深度定制。当然,本身团队也有此能力,因为我也算是 WPF 框架的官方开发者。这部分深度的定制将会根据定制的不同,部分进行开源
变更后当前的开发架构分层如下图
让 WPF 作为基础库的一部分而存在,而不再放入运行时里面。计划是产品项里面的多个产品项目是共用 .NET 运行时,单个各个产品之间自己带 WPF 的负载,作为基础库
所遇到的问题
在进行最后一公里的更新就遇到了一些 dotnet core 机制上没有最佳实践的问题
多 AppHost 入口应用的依赖问题
多 Exe 应用的客户端依赖问题是其中的一个机制性问题。当前正在迁移的项目是一个多进程模型的应用,有很多 Exe 的存在。然而 dotnet core 当前没有一个最佳实践可以让多个 Exe 之间完美共享运行时且不受系统所安装的全局 dotnet 运行时影响,同时照顾到安装完成之后的文件夹体积
我列出的问题点如下
多个 Exe 文件之间,如何共享运行时,如果不共享文件夹,各自独立发布,那将让输出文件夹体积非常大
多个 Exe 文件,如果在相同的文件夹进行发布,将会相互覆盖相同的名字的程序集。根据 dotnet 的引用依赖策略,如果有版本不兼容情况,将出现 FileLoadException 错误
不能使用 Program File 共享的全局程序集,因为这个文件夹里面的内容可能被其他公司的应用更改从而损坏,无法使用 dotnet core 环境独立的能力
不能使用 Program File 共享的全局程序集,因为团队内将会对 dotnet 运行时进行定制,例如定制 WPF 程序集,将 WPF 的地位从运行时更改为基础库。这部分定制不能污染其他应用
发布到用户端的运行时版本只能选用稳定的版本,而开发者会使用较新的 SDK 版本,开发构建输出的程序集将引用较新 SDK 版本,如应用运行加载的只是发布到用户端的运行时版本,将会因为版本低于构建版本而出错
发布到用户端的运行时版本,是包含了定制版本的运行时,例如定制的 WPF 程序集。开发时应该引用定制的 WPF 程序集,但是不能引用低于构建版本的用户端的运行时版本
另外由于 dotnet core 和 dotnet framework 对 exe 有机制性的变更,如 dotnet core 的 exe 只是一个 apphost 而已,默认不包含 IL 数据。而 dotnet framework 下默认 exe 里面是包含应用入口以及 IL 数据程序集的。这就导致了原本的 NuGet 分发里面有很多不支持的部分,好在这部分的坑踩平了
然而在进行 AppHost 的定制的时候,却一定和 NuGe