APM 监控系统之 OSGI 架构实践

一年下来,接入了上千个系统,快的几分钟,慢的不到 10 分钟,最多就是处理一下 jar 包依赖冲突问题,所以能这么迅速的推广。

不过前几天遇到一个客户,他们的系统采用的是 OSGI 的框架,OSGI 这个词相信大部分人是听过没用过的。

当我们的监控系统遇到了 OSGI 架构系统也是碰撞出了激烈的火花,足足用了两天的时间,才搞定了各种水土不服。

我把期间的各种酸甜苦辣记录下来,希望为奋斗在 APM 一线的同行们提供一点点帮助。

在整个过程中,我们遇到了四个技术关键点:

  • 把一个普通的 jar 包 Bundle 化

  • OSGI 的类加载机制

  • 引入第三方非 Bundle 化的 jar 包

  • 通过 Activator 在 Bundle 的启动和停止实现回调

项目背景

首先我简单介绍下两个项目的背景:


监控系统

这是一个利用动态字节码技术,自动的无侵入的对目标系统实行秒级实时监控,监控内容包括但不限于容量、性能、成功率、调用链、应用拓扑等。


目标系统

目标系统分为两部分:

  • 以 war 包形式发布,部署到 Web 容器内,接收 http 请求。

  • 采用 OSGI 架构,每一个 Bundle 是一个业务服务 (可以理解为 Bundle 就是微服务)。

这两部分通过 servlet 的桥接方式连接,其中一些公用 Bundle(如 slf4j) 会放在 war 包的指定目录中,每个业务 Bundle 不需要再单独引入。

这个架构是我在解决了所有问题之后才总结出来的,在这里提前抛出是为了方便理解后边的内容。
7e53b143e4554cf8a36206e9fdd031ff.png

刚开始的时候,我们就把它当做一个普通的系统来接入,把我们的 jar 包放到了目标系统 war 包的 WEB-INF/lib 下。

加入启动脚本,再启动系统,可以看到我们的 jar 打印出的启动日志,然后就没有然后了,除了那一行日志,没有任何效果。

这个时候,我就意识到了 OSGI 系统不是这么玩的,于是开始 OSGI 的漫漫探索之路……

当看到 OSGI 的每一个 Bundle 包都是用单独 Class Loader 负责加载时,我仿佛找到了解决方案。

我们把监控 jar 包打到了每一个业务的 Bundle 里边,然后重启,结果监控成功了,问题解决了。

从纯技术角度来看貌似是没什么问题,然而噩梦才刚刚开始。

由于监控 jar 包打入了业务 Bundle 内,带来了两个比较棘手的问题:

  • 技术上每个业务 Bundle 都需要这么去引入,加大了开发工作量,这个和我们极致的设计理念是不符的。

  • 业务上每一个业务 Bundle 就成为了一个单独应用,客户方表示我们这个是一个应用,而不是多个应用。

所以还是要解决之前的问题,把整个目标系统所有的 Bundle 当做一个应用去接入监控。

我不熟悉目标系统和 OSGI 架构,客户开发方又不了解监控系统的原理和实现过程,经过了长时间的各种尝试和交流,仍没有丝毫进展。

于是我们决定采用一个最原始、最简单、最笨,也是最有效的方法,从“hello world”开始。

当一个系统出现问题,你又不知道问题出在哪部分的时候,要么把这些“部分”一个一个去掉,直到发现没有“问题”为止;要么从“零”开始,然后一个一个的加入,直到出现“问题”为止,我们采取了后者。

首先,针对我们的监控 jar 包去掉基于动态字节码的自动监控,采用编程式的直接 API 调用,并且将 API 接口做了 Mock,去掉了所有的第三方依赖。

修改目标系统的代码,在希望被监控的方法中直接调用监控 API。打包,部署,启动,被监控 Bundle 不能启动,报错……

Missing Constraint: Import-Package: com………………sgm……

经过和对方开发人员的沟通和网上查阅 OSGI 相关资料,了解到这是一个 OSGI 打包的规范问题。

之前我们的监控 jar 包是没有按照 OSGI 的规范去打包的,打的只是一个普通的 jar 包。

所以在 OSGI 框架下,其他的 Bundle 是无法访问到我们的 jar 包中的类,造成启动报错的问题。

这里遇到了我们第一个要解决的主要问题:

普通 jar 包 Bundle 化

先看下边的两张图:
f2cad2a0f5d645719a8aab7a165c35b1.png

1b7a80180ca54700af16a6cad8d6f78a.png

上边的两张图是 jar 包中 META-INF/MANIFEST.MF 文件,第一张图是普通 jar 包,第二图是具有 OSGI 规范的 jar 包。

既然知道了,那这样的 OSGI 的包怎么打?我们用 maven 来编译,顺理成章的就找到了 maven-Bundle-plugin 的这个插件,使用很简单,代码如下:
22ea566f29a047019b36a68afed0f1a5.png

在这么多属性中,Import-Package 和 Export-Package 尤其重要,一个 jar 包就是一个 Bundle,Bundle 和 Bundle 之间的访问全靠这两个属性来控制。

**Import-Package:** 说明了这个 Bundle jar 需要调用外部其他 Bundle 的哪些包 (package)。

**Export-Package:** 说明了这个 Bundle jar 可以提供哪些包 (package) 供其他 Bundle 去调用。

当一个 Bundle 启动的时候,会分为 Resolved(解析)、Installed(安装)、Started(激活) 等几个步骤。

解析就是检查 jar 是不是正常,安装的时候会把 Export-Package 中的指定的包进行注册,这就知道哪些包由哪些 Bundle 来提供。

激活的时候会检查当前这个 Bundle 的 Import-Package 依赖的包是不是都有对应的 Bundle 来提供,否则激活失败。

修改监控客户端,把父项目、子项目、子子项目全部按照 OSGI 规范打包,又是一遍部署重启,这次被监控的业务 Bundle 没有报错,而是监控客户端启动报错:

Missing Constraint: Import-Package: org.slf4j

有了刚才的经验,一看就知道是没有 slf4j,可是对方开发人员反馈 slf4j 是有的,他们自己也在用,这就奇怪了。

这时我发现还有在监控客户端里 MANIFEST.MF 的一句话 org.slf4j;version=“[1.7,2)”,version 这个词引起了我的注意。

原来在目标系统里的 slf4j 是 1.5 版本,而监控客户端要求 1.7 版本,slf4j 降级再试。

这次目标系统成功调用到了监控客户端的 API,打印出了日志,万里长征终于迈出了第一步,但这只是一个 Mock 的版本,真正的监控程序还没有加入。

先小结一下:首先在 OSGI 的框架下,所有 jar 必须按照 OSGI 的规范去打包,否则不能使用。

其次要对 Import-Package 和 Export-Package 配置好,需要依赖外部哪些包,自己又可以提供哪些包供外部使用,同时注意版本,依赖时一般都是要高于哪个版本。

接下来,就要加入真正的代码跑一跑,根据前面的经验还是要慢慢来,我们分两步加入代码。

第一次先加入监控代码,待成功后加入网络传输部分的代码,后边遇到的坑让我们发现这个决定是正确的。

打包部署启动调用这个流程已经很熟练了,报错还是如期出现了,这次报错的是 com.lmax 的 disruptor 这个第三方开源 jar 包:

Missing Constraint: Import-Package: sun.misc

纳尼?sun.misc 没有?这不是 jdk 里的吗,为什么会没有呢?于是,又是一番资料查找,终于明白了其中的道理。

这里就要讲到 OSGI 的类加载机制了,它并不是我们常说的双亲委托机制。

关于这个问题,网上已经有很多文章介绍了,但这里我还是结合这个例子简单的说一下。

OSGI 的类加载机制

首先每一个 Bundle(jar) 都会被一个单独的 ClassLoader 去加载,当一个 Bundle 的 ClassLoader 尝试去加载一个类的时候:

  • 如果这个类的包名是 java.* 开头的,那么直接交给 bootstrap Class Loader 去加载。

  • 查看这个类是否在 OSGI 的配置文件中有 org.osgi.framework.bootdelegation 属性定义,如果定义了,交给 bootstrap ClassLoader 去加载这个类。

  • 查看这个类是否在 OSGI 的配置文件中有 org.osgi.framework.system.packages 属性定义,如果定义了,交给父类加载器去加载,一般就是 App Class Loader。

  • 查看这个类是否在本 Bundle 的 MANIFEST.MF 文件的 Import-Package 中有定义,如果定义了,交给 Export-Package 这个类的 Bundle 去加载。

  • 在上边条件都不满足的时候,那这个类就是自己的 Bundle 的内部类,由自己的 Class Loader 去加载。

现在我们来看一下,下边这张图是 disruptor 的 MANIFEST.MF 文件:
f9231dea349c444bb354aca7e28c4cea.png

在 disruptor 里使用了 sun.misc.Unsafe 类,在启动 disruptor 的时候,需要加载 sun.misc.Safe。

那么 1,2,3 点都不满足,命中了第 4 点,就开始寻找带有 Export-Package 的 Bundle,那肯定找不到。

遵循上边的原则,在配置文件中加入了 org.osgi.framework.bootdelegation=javax.,sun.

同时又把 disruptor 中的 Import-Package 删掉了,问题成功解决了,又向胜利迈进了一步。

接下来,就要把网络传输这部分加入进来了,系统就可以监控了,数据需要发送出来才可以真正的使用。

有了之前的经验,感觉应该问题不大,然而现实情况并不是这样……

当把这部分代码和依赖加入后,各种的 Missing Constraint:Import-Package:xxx。

我发现所有的依赖的第三方 jar 都在里边,为什么还会报错,我开始怀疑是这些第三方 jar 本身的问题。

打开这些 jar 文件的 MANIFEST.MF 文件查看,果然如此,我们依赖的这些 jar 有多一半都不是 Bundle 化的 jar 包,这里就涉及到了本文第三个要解决的核心问题。

OSGI 如何加载第三方非 Bundle 化的 jar 包

OSGI 如何加载第三方非 Bundle 化的 jar 包,有如下几种方式:

  • 通过父类加载器加载,也就是配置 org.osgi.framework.system.packages。

  • 将 jar 转换成 Bundle,然后 Export-Package。

  • 把 jar 打包进引用方的 Bundle。

第一种方式需要目标系统配置,同样不符合我们的设计理念,显然不合适,于是我们首先尝试了第二种方式,重新打包那些非 Bundle 的第三方 jar。

在这个过程中,我们发现这绝对是个苦逼的活,需要找到源码,下载源码,修改 pom,有的找源码很费劲,有的还是 ant 编译的,有的虽然是 maven 管理,但又依赖了父项目……

总之想顺利的重新打包是个很费劲的事,看来只剩下第三条路了,这又要退回到之前第一个问题,如何打一个 Bundle jar。

之前我们是把 maven 工程的每一个子项目分别 Bundle 化,如果要把第三方 jar 打入 Bundle,那就有可能一个第三方 jar 被多次打入不同的子项目 Bundle,造成浪费。

所以决定放弃对原有项目的每个子项目单独 Bundle 化的方案,而是新建一个子项目,由这个子项目引入所有的其他子项目和第三方依赖 jar,把他们所有打成一个大的 Bundle jar。
1bf2e5da194a40f099cd18a1e755da14.png

看一下上边的配置,比之前多了一个 Private-Package,就是说哪些 Package 是这个 Bundle 的内部包,也就是要打入最后 Bundle jar 的东西。

现在通过编程式 API 直接调用的方式已经可以监控到目标系统了,最后要做的就是引入运行时字节码增强技术。

还是按照常规的方式,把我们的 Agent 通过 javaagent 方式启动,有了之前的经验,我知道这个是被 bootstrap Class Loader 加载的,于是就直接在 org.osgi.framework.bootdelegation 中加入了监控 Agent 的包名。

结果启动正常,但是无法自动监控,看了日志后发现是监控客户端没有启动。

监控客户端的启动是通过在每个类加载的时候,尝试性的使用它的类加载去加载监控客户端的启动类。

如果可以加载上,那么就启动成功了,因为启动程序是放在了启动类的 static 块中,且启动类是一个单例模式,记录着被监控应用的信息。

从这个启动日志来分析,被监控系统有 200 多个 Bundle,每个 Bundle 启动都去加载监控客户端,然后我们希望监控客户端被它自己的类加载器去加载。这里就是本文第四个核心问题。

如何在 Bundle 启动的时候去做一些初始化操作

在 OSGI 的规范里提供了一个叫 Bundle Activator 的接口,里边有 start 和 stop 两个方法。

顾名思义,在 Bundle 启动和停止的时候会回调这两个方法,这就好办了,我们可以在 start 方法中实现启动监控客户端的代码。
a24c1421411b4e8da3681834f75c5fd5.png

上边的 Bundle-Activator 这个标签,指定这个 Bundle 的 Activator 是哪个类。

至此所有的问题都得到了解决,这次 OSGI 应用接入 APM 监控足足花掉了两天的时间。

由于负责监控系统的人员并没有使用过 OSGI,被监控目标系统的开发人员也不知道监控的原理是什么,一开始我们都以两个产品整体去接入,做了许多的无用功,耽误了很多时间。

小结

从这个案例可以看出,当你所做的东西需要应用到一个你不熟悉的技术领域的时候,又不可能有足够的时间去学习这个领域的知识,有一个最好的办法就是改造你所做的系统,从零开始,逐渐加码,去适应那个不熟悉的领域。

千万不要想着能整体一下解决所有问题,因为可能要解决所有问题有 100 个技术点需要去修改。

这 100 个问题同时暴露,你只有把它们同时都修改了,才能看到你的成果,这是根本不可能的事情。

而这 100 个问题一个个暴露出来,把一个不可能完成的大任务拆分成若干个可完成的小任务,修改一个,看到一步成功的效果,问题就得到了解决。

转载声明:本文转自【51CTO 技术栈】