作为程序员,我们大部分的职业生涯都在追求什么?

当然是打造一个高可用、高性能、易扩展的系统,在互联网的背景下,我们还要需要应对高并发,可伸缩等互联网的特殊场景。

为了达到这个目的,在整体设计上我们有服务化/微服务/容器化等解决方案,在应用单体上我们也常用分层设计/模块化/微核插件化等设计模式,而skywalking的基本设计模式就是基于微内核+插件:通过建立标准的插件规范,通过微核控制各个插件的生命周期,为skywalking提供了强大的扩展性。

经典的微核案例

微核+插件在JAVA生态中最经典的应该是Eclipse:

一、eclipse的插件模型

在eclipse中,插件是在eclipse工作平台上的一个能提供某种服务的组件。一个组件就是一个对象,该对象能够在运行前被配置到一个系统中。eclipse的运行时系统就提供了这样的功能,支持组件的激活,并使一些插件集协作来提供一个无缝的开发环境。在一个eclipse实例中,一个插件被一个或一些插件运行时类或插件类所管理。也就是说,插件类提供了配置和管理插件实例的功能。一个插件类必须扩展自org.eclipse.core.runtime.Plugin,该类是一个提供了基本插件管理功能的抽象类。

每个插件有一个全局唯一的标识符id,用于标识该插件。

二、插件的配置和激活

插件的安装很简单,只需将插件文件夹拷贝到plugins文件夹内,这样该插件就可以在适当的时候被eclipse运行时系统激活。激活意味着加载它的运行时类并将其实例化。插件类就是在插件激活startup或反激活shutdown时完成一些特殊工作的类。

eclipse包含一个插件管理核心,称为”eclipse平台“或”eclipse运行时“。每次启动时会自动加载一些核心插件。其它非核心插件只有在有需求时,才被激活。

eclipse模型中,一个插件可能和其它插件有下列两种关系:

1.依赖require  :该插件的运行需要其它插件的支持。

2.扩展extension:该插件扩展了原插件的功能。

摘自:https://blog.csdn.net/zhangxiang_nuaa_hust/article/details/83293792

可以看到,参考其他的具体实践,我们可以大概的确定微核+插件的主要开发模式:

  • 微核主要提供可用资源的虚拟化或者标准化的规范,暴露统一接口,供插件使用(很类似操作系统的内核,主要是通过制定规范,依托硬件驱动,为用户系统提供可用资源的标准输入输出接口
  • 插件生命周期的维护,包括插件的加载、激活、停用,特别场景可能还需要热加载的支持

skywalking的实现

skywalking作为一个APM实现,主要是由两部分实现,agent(采集tracing和metric数据)和后端的collector(通过RPC接受agent数据然后分析汇总放入存储容器中),具体可以参考如下的架构图:

架构图

而在skywalking的agent和collector的设计中,均采用了微核+插件的设计模式,保证了skywalking的扩展性。

skywalking-agent

skywalking的agent核心代码非常少,主要负责三个方面的业务:

  • 建立二进制增强的增强器规范
  • 建立agent本身业务以及不依赖于二进制增强的监控业务的本地服务规范
  • 对接java agent启动器,加载满足规范的增强器并通过bytebuddy进行增强,加载满足规范的本地服务并控制他们的生命周期

由于bytebuddy已经提供了java agent的支持,所以agent的开发主要集中在增强器规范的定制和具体实现的加载。增强器的规范可以参考下面的规范:

插件规范

最终主要提供了对增强器的适配场景(enhancePlugin)、拦截器的具体实现(interceptPint)的统一规范和标准实现。其中enhancePlugin的模式基本和增强器的设计基本一致的:

增强器规范

而具体实现则由几个spi接口实现:

  • InstanceConstructorInterceptor:拦截增强构造方法
  • InstanceMethodsAroundInterceptor:拦截增强普通方法
  • StaticMethodsAroundInterceptor:拦截增强静态方法

具体格式参考:

切面

值得关注的是在增强器的适配场景中,设计者增加了witness的配置(参考:org.apache.skywalking.apm.agent.core.plugin.AbstractClassEnhancePluginDefine#witnessClasses),即通过检查某个class是否被加载过来做版本的适配。

skywalking-oap(collector)

skywalking的collector和agent的实现模式类似,不过在collector中插件被叫做module,由ModuleManager加载,ModuleProvider控制生命周期,最终不同的ModuleProvider集成在一起成为collector,最终由BootstrapFlow启动。而collector和agent不同的是collector多了一个ModuleConfig,用于服务端进行不同环境的配置需求。

而且由于collector要适配更多的技术选型,例如存储现在就可以支持用于demo的H2内存数据库,也可以使用ElasticSearch作为测试和生产环境。所以collector不仅通过module来做插件化,同时还在core约定了各个module的标准接口,最后再通过各个module真正的实现类支持这些接口,这样既能通过插件化继续扩展增加新的业务功能,同时也有标准接口,方便不同的技术选型。

微核+插件式扩展的适用场景

微核的这种设计模式既然这么好,那么是不是我们都应该用呢?答案明显不是。强大的扩展性,必然代码一些混乱,例如:

  • 模块之间的复杂关系
  • 模块之间的重叠和冗余实现
  • 缺少主流程引导,业务梳理困难

所以也有另外一种模式,可以参考dubbo,通过清晰的流程和边界来实现核心业务,同时预留一定的扩展SPI,通过加载不同的SPI实现来实现可扩展,参考:

dubbo