源宝导读:插件系统大大提高了系统的扩展性,有利于模块化开发。系统发布后,当我们需要对系统进行扩充,可以再不编译的情况下更新系统的插件即可。基于热拔插的软件系统提高了持续交付能力,在添加新特性的同时保持核心结构稳定。本文将介绍研发协同平台代理服务在插件化设计方面的技术实践。
一、背景
插件系统大大提高了系统的扩展性,有利于模块化开发。系统发布后,当我们需要对系统进行扩充,可以再不编译的情况下更新系统的插件即可。基于热拔插的软件系统提高了持续交付能力,在添加新特性的同时保持核心结构稳定。
ERP是To B产品, To B产品的持续交付和To C产品的持续交付是有很大不同的。To B产品需要交付给成千上万家客户,每家客户的产品和环境都不一样,要保证ERP产品能持续的、稳定的交付给成千上万家客户,在客户端需要一个代理服务来协同完成。由此,ERP代理服务应运而生。
二、设计架构
ERP 代理服务在客户端负责产品的持续交付承担在重要的角色,ERP 产品每次的最终交付到客户,它需要在客户端完成很多重要的工作:更新包到客户机 IIS、收集客户机更新日志、服务器运行状态、同时由于客户机网络环境安全限制时也承担着信息的中转服务,为了日后公司产品的多样化、环境的多变性、产品的迅速更新,我们需要随时做出的相应响应,并能迅速的完成支撑,因此 ERP 代理服务在保证自身稳定的同时,需要快速的更新自己的产品所承担的功能职责。
ERP代理服务的核心需求如下:
自更新。
自恢复。
灰度更新。
业务逻辑可升级。
业务逻辑可灵活扩展。
较好的容错机制,在网络不稳定或其他其他因素的影响下,也能稳定持续运行。
基于以上需求,我们新设计了 ERP 代理服务的架构方案,我们将原有 ERP 代理服务改造为热拔插的插件支撑方案。
三、框架设计
代理服务通过支持三种类型的插件热拔插,实现多种多样的需求:
类库插件:实现服务端下发命令执行业务处理。
Web 插件:可灵活的提供 API 或者视图在页面中呈现。
Console 插件:通过外部启动 exe 程序的方式,守护进程或者其他特殊需求。
插件功能高内聚,与框架低耦合,开发人员根据规范,开发完成并进行单元测试通过后,打包并安装到宿主中,即可使用。
3.1、插件加载模式
通过向更新服务获取用户可用的插件集合,动态新增、升级、卸载插件,通过在内存中加载 DLL 的方式热拔插插件,能有效的更新客户端服务器上的代理服务功能,静默升级插件后续将让我们的服务有很高的拓展性。
3.2、目前规划的插件
针对现有的业务场景我们规划了多个插件,并用于实现不同的功能模块:
更新包插件:用于更新ERP产品到客户服务器的站点中。
更新服务API插件:用于提供产品注册等接口给ERP站点调用,将相应信息上报至服务端。
守护插件:守护代理服务运行状况,负责重启、更新代理服务。
其中部分插件为热加载方式,我们可快速的进行插件迭代开发,并发布上线(同时支持灰度发布),能快速的应对产品需求。
四、实现方案
我们从现有的 ERP 代理服务,以及 ERP 的交付特性,来考虑我们系统的可拓展性、可维护性,同时在一定程度上引入更新的技术栈来。我们从三个部分实现我们的插件系统:
主程序开发。
公共接口的基础插件类库开发。
各类插件开发。
以上,我们需要实现各部分之间的接口规范。
4.1、技术调研
我们调研了最新的 ASP.NET CORE 3.1(https://docs.microsoft.com/zh-cn/aspnet/core/?view=aspnetcore-3.1) 技术栈,它能很好的支持我们的热拔插的系统设计方案,微软也给出了简单的示例,以下是提供一些关键技术文档进行参考:
ASP.NET CORE 3.1 热拔插插件的实现方案。
(https://docs.microsoft.com/zh-cn/dotnet/standard/assembly/unloadability )
ASP.NET CORE 支持独立部署方式避免系统需要安装.NETCORE SDK。
(https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/?view=aspnetcore-3.1)
引入 NLog 日志组件。
(https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-3)
在以上技术栈的基础上,我们对基础知识加以深入研究,引入项目的过程中不断思考优化项目结构。
4.2、实现解析
我们改变原有ERP代理服务的插件方案,采用 AssemblyLoadContext
来实现热拔插插件方案,同时重新调整主程序框架根据支持多种的插件的的加载,将公共服务接口提升到基础插件类库中以便继承插件可以通过依赖注入的方式使用我们的公共接口。
4.2.1、封装插件加载器SDK
功能特性
支持将dll以文件流的方式加载(可卸载)。
支持将占用dll文件的方式加载(可卸载)。
支持插件文件变更卸载插件并自动重新加载。
设计目录:
核心代码:
// 程序集加载
protected override Assembly Load(AssemblyName assemblyName)
{var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);if (assemblyPath == null){var localFile = Path.Combine(Path.GetDirectoryName(MainAssemblyToLoadPath), assemblyName.Name + ".dll");if (File.Exists(localFile)){assemblyPath = localFile;}else{return null;}}// 内存方式加载if (IsLoadInMemory){using var file = File.Open(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read);return LoadFromStream(file);}else{return LoadFromAssemblyPath(assemblyPath);}
}
/// <summary>/// 卸载插件/// </summary>[MethodImpl(MethodImplOptions.NoInlining)]public bool Unload(){ if (!_context.IsCollectible) { Console.WriteLine($"AssemblyLoadContext 不是可回收的"); return false; } if (!_context.IsLoadInMemory) { var mainAssemblyToLoadPath = _context.MainAssemblyToLoadPath; // 判断资源是否存在引用 var weakRef = new WeakReference(_context, trackResurrection: true); _context.Unload(); _context = null; // 尝试至多20次垃圾回收,将资源引用释放 for (int i = 0; weakRef.IsAlive && (i < 20); i++) { GC.Collect(); GC.WaitForPendingFinalizers(); // 睡眠500毫秒,确保GC回收完毕,资源引用释放 Thread.Sleep(500); } if (weakRef.IsAlive) { Console.WriteLine($"程序集({mainAssemblyToLoadPath})回收失败,请确认程序集未被使用"); } return !weakRef.IsAlive; } else { _context.Unload(); _context = null; return true; }}
4.2.2、 主程序功能模块解析
功能特性及结构分层:
全局异常过滤。
计划任务:命令定时执行任务、定时上报任务、定时更新插件任务。
插件加载器:类库插件加载器、Web插件加载器、控制台(exe)类插件加载器。
中间件:请求记录日志中间件。
应用程序全局配置。
服务层:命令服务、插件缓存、插件服务、上报服务。
目录结构:
应用设计结构:
4.2.2.1、 启动说明
ERP代理服务启动前会注册所需服务:
注册配置中心服务。
注册日志服务。
注册全局异常处理。
注册更新服务。
注册Web插件View视图引擎目录。
注册各类型插件加载器(类库插件、Web插件、控制台类插件)。
注册插件缓存服务、命令服务、上报服务、插件管理服务。
注册上报调度任务、命令执行调度任务、更新及加载插件调度任务。
并根据配置中心配置的服务启动地址启动,改地址用于ERP产品调用代理服务上报产品信息等接口调用。
4.2.2.2、API接口服务说明
目前由更新服务API插件提供给ERP产品将相应信息提交至更新服务,以便后续我们能很好的利用信息完成客户的产品交付:
注册产品信息。
获取服务器状态信息。
获取更新包更新状态。
获取更新页面地址。
获取更新服务状态。
同时由于该插件可以后期升级并热加载,我们可以根据需求快速迭代插件,为ERP产品提供更多的所需服务。
4.2.2.3、调度任务说明
应用启动后,通过异步方式启动调度任务,在调度任务中完成各自的职责。
命令任务:通常用于执行发布通知更新包任务或即时更新插件。
上报任务:用于上报客户相关信息、插件运行状态等信息。
更新插件任务:用于异步加载插件,不影响主程序运行,并定期同步客户端插件与服务端插件版本信息。
通过将核心任务使用调度任务异步运行的方式,避免影响主程序。
五、写在最后
目前我们的项目已经进入尾声,我们已基本在运用这套方案实现了原有ERP代理服务的功能,同时我们更加友好的支持灰度插件发布上线。未来我们还会加入日志主动或被动上报、客户服务器监控,ERP站点运行监控等功能插件来帮助我们产品发展的越来越好。
最后,我们在实践中也遇到了一些问题:
当使用 XmlSerializer 操作相关资源时,由于改对象目前还不支持可卸载,暂不能用于可卸载插件中 ——— 详情请查阅 Issue
(https://github.com/dotnet/runtime/issues/1388)。
当插件依赖了 System.Data.SqlClient 类库时,会导致报异常:
SqlClient is not supported on this platform. ——— 详情请查阅 Issue。
(https://github.com/dotnet/SqlClient/issues/115)
此时我们可改用
Microsoft.Data.SqlClient
包。(https://devblogs.microsoft.com/dotnet/introducing-the-new-microsoftdatasqlclient/)
我们在努力克服困难的同时也在实践中成长进步。
------ END ------
作者简介
曾同学: 研发工程师,负责研发协同平台产品的研发工作。
也许您还想看
研发协同平台持续集成实践
研发协同平台持续集成2.0架构演进
研发协同平台微服务监控的技术实践
ERP开放平台定制化远程高效协作秘笈