当前位置: 首页 > 科技观察

一波三折,APM监控系统探索OSGI架构

时间:2023-03-15 00:46:35 科技观察

2017年,我们一直在升级改造分布式服务跟踪系统。为了更好地服务于开发和运维人员,数以千万计的微服务系统快速发现和定位问题。一年来,上千个系统联通,快则几分钟,慢则不到10分钟。顶多是处理一下jar包依赖冲突,才能这么快的提升。不过前几天遇到一个客户,他们的系统使用的是OSGI框架。相信大多数人都听说过OSGI这个术语,但从未使用过。当我们的监控系统遇到OSGI架构系统的时候,也是碰撞出了激烈的火花。解决各种水土不服用了整整两天。期间把所有的酸甜苦辣都记录下来,希望能给奋斗在APM第一线的同仁提供一点帮助。整个过程中,我们遇到了四个关键技术点:打包一个普通的jar包。OSGI的类加载机制。引入第三方非bundlejar包。回调是通过Activator实现的,用于启动和停止Bundle。项目背景首先简单介绍一下接下来两个项目的背景:监控系统:这是一个采用动态字节码技术对目标系统进行自动化、无侵入的秒级实时监控。监控内容包括但不限于容量、性能、成功率、调用链、应用拓扑等,详情请参考:http://os.51cto.com/art/201709/552641.htm。目标系统:分为两部分,第一部分以war包的形式发布,部署在web容器中,接收http请求;第二部分采用OSGI架构,每一个Bundle都是一个业务服务(可以理解为一个Bundle是一个微服务)。这两部分通过servlet桥接连接。一些publicbundle(比如slf4j)会放在war包的指定目录下,每个业务bundle不需要单独导入。这个架构是我在解决所有问题后总结出来的,在这里提前抛出,方便后面内容的理解。一开始我们把它当做普通系统连接,把我们的jar包放到目标系统war包的WEB-INF/lib下,添加启动脚本,然后启动系统,可以看到我们打印的启动日志jar,然后就没有了,除了那一行log,没有任何作用。这时候我才明白OSGI系统不是这么玩的,于是开始了漫长的OSGI探索之路……当看到OSGI的每一个Bundle包都是由一个单独的ClassLoader加载的时候,我好像找到了解决办法.我们把监控jar包放到各个业务的Bundle中,然后重启。结果,监控成功,问题解决。从纯技术的角度来看,似乎没有问题,但噩梦才刚刚开始。由于将监控jar包打入业务bundle,带来了两个比较棘手的问题:首先,技术上,每个业务bundle都需要这样导入,增加了开发工作量。这与我们最终的设计理念是不一致的;第二,业务中的每个业务Bundle成为一个单独的应用,client表示我们是一个应用,而不是多个应用。所以我们还是要解决前面的问题,将整个目标系统的所有bundle作为一个应用来访问和监控。不熟悉目标系统和OSGI架构,客户端开发人员不了解监控系统的原理和实现过程。经过长时间的各种尝试和交流,仍然没有任何进展。于是我们决定采用最原始、最简单、最愚蠢、最有效的方法,从“helloworld”开始。当一个系统出现问题,而你又不知道问题出在哪里时,你要么将这些“部件”一个一个地拆掉,直到找不到“问题”为止;或者从“零”开始,一个一个地加起来,直到出现“问题”为止,我们采取了后者。首先,对于我们的监控jar包,去掉了基于动态字节码的自动监控,采用程序化直接调用API,mockAPI接口,去掉所有第三方依赖。修改目标系统代码,在需要监控的方法中直接调用监控API。打包,部署,启动,被监控的Bundle无法启动,报错……MissingConstraint:Import-Package:com…………sgm…….沟通后和对方的开发者在网上查阅了OSGI的相关资料,得知这是OSGI打包的规范问题。之前我们的监控jar包没有按照OSGI规范打包,只是一个普通的jar包,所以在OSGI框架下,其他bundle无法访问我们jar包中的类,导致启动报错的问题。这里遇到第一个我们要解决的主要问题:1.普通jar包Bundle首先看下面两张图:上面两张图是jar包中的META-INF/MANIFEST.MF文件,第一张图是一个普通的jar包,第二张图是一个OSGI规范的jar包。既然知道了,那么如何制作这样一个OSGI包呢?我们使用maven编译,顺理成章的找到了maven-Bundle-plugin这个插件。使用起来非常简单。代码如下:org.apache.felixmaven-Bundle-plugin3.3.0trueBundle-manifestprocess-classes清单org.slf4jcom.wangyin.sgm.clientJavaSE-1.6在这么多属性中,Import-Package和Export-Package尤为重要,一个jar包就是一个Bundle,Bundle与Bundle之间的访问就是通过这两个属性来控制的。Import-Package:表示这个Bundlejar需要调用其他外部Bundle的哪些包。Export-Package:表示这个Bundlejar可以提供哪些包供其他Bundle调用。当一个Bundle启动时,会分为Resolved(解析)、Installed(安装)、Started(激活)等几个步骤。解决方法是检查jar是否正常,安装时会导出Export-Package中指定的包。注册以了解哪些包由哪些捆绑包提供。激活时会检查当前Bundle的Import-Package所依赖的package是否有对应的Bundle提供,否则激活失败。修改监控客户端,按照OSGI规范对父工程、子工程、子子工程进行打包,重新部署重启。这次监控的businessbundle没有报错,但是监控客户端开始报错:MissingConstraint:Import-Package:org.slf4j有了刚才的经验,一看就知道没有slf4j,但是另一个开发人员报告说slf4j存在,而且他们自己也在使用它,这很奇怪。这时候发现监控客户端的MANIFEST.MF里面有一句org.slf4j;version="[1.7,2)"。版本这个词引起了我的注意。原来目标系统中的slf4j是1.5版本,但是监控客户端需要1.7版本。slf4j被降级并再次尝试。这次目标系统成功调用了监控客户端的API,并打印出了日志。万里长征终于迈出了第一步。但这只是一个Mock版本,还没有加入真正的监控程序。先总结一下:首先,在OSGI的框架下,所有的jar都必须按照OSGI规范进行打包,否则无法使用;其次,Import-Package和Export-Package一定要配置好,哪些包需要外部依赖,哪些包可以自己提供给外部使用,注意版本,一般是哪个版本高依靠它的时候。接下来,我们需要添加真正的代码并运行它。按照以往的经验,还是要慢慢来。我们分两步添加代码。第一次,我们添加了监控代码。到坑里让我们发现这个决定是正确的。打包、部署、启动、调用的过程已经很熟练了,错误还是如期出现。这次报错是com.lmax的disruptor,一个第三方开源的jar包:MissingConstraint:Import-Package:sun.miscNani?没有sun.misc?jdk里不是有这个吗,怎么没有呢?于是,又是一番查资料,终于明白了真相。这里要说的是OSGI的类加载机制,并不是我们常说的双亲委派机制。关于这个问题网上已经有很多文章了,这里我就以这个例子来简单说一下。2.OSGI的类加载机制首先,每个Bundle(jar)都会被一个单独的ClassLoader加载。当一个Bundle的ClassLoader试图加载一个类时:如果这个类的包名以java.*开头,那么就直接交给bootstrapClassLoader去加载。检查OSGI配置文件的org.osgi.framework.bootdelegation属性中是否定义了该类。如果定义了,就交给bootstrapClassLoader去加载这个类。检查该类是否由OSGI配置文件中的org.osgi.framework.system.packages属性定义。如果定义了,就会被父类加载器加载,通常是AppClassLoader。检查该Bundle的MANIFEST.MF文件的Import-Package中是否定义了该类。如果定义了,就会被Export-Package类的Bundle加载。当不满足上述条件时,那么这个类就是自己Bundle的内部类,由自己的ClassLoader加载。下面我们来看一下,下图是disruptor的MANIFEST.MF文件:disruptor中使用了sun.misc.Unsafe类,在disruptor启动的时候需要加载sun.misc.Safe,然后1,2,3一点都不满意,打到第4点,开始找BundlewithExport-Package,肯定找不到。按照以上原则,在配置文件中添加org.osgi.framework.bootdelegation=javax.*,sun.*,同时删除disruptor中的Import-Package,问题顺利解决,我们搬家迈向胜利的一步。接下来还要添加网络传输部分,可以对系统进行监控,数据需要发送出去才可以使用。有了之前的经验,感觉应该问题不大,但实际情况并非如此……添加这部分代码和依赖后,各种MissingConstraint:Import-Package:xxx。发现依赖的第三方jar都在里面,为什么会报错,我开始怀疑是这些第三方jar本身的问题。打开这些jar文件的MANIFEST.MF文件查看。果然,我们依赖的jar有一半以上不是捆绑的jar包。这就涉及到本文要解决的第三个核心问题。3.OSGI如何加载第三方非bundlejar包OSGI如何加载第三方非bundlejar包有以下几种方式:通过父类加载器加载,即配置org.osgi.framework.system.packages。将jar转换为Bundle,然后是Export-Package。将jar打包到参考方的Bundle中。第一种方式需要目标系统配置,也不符合我们的设计理念,显然不合适,于是我们先尝试第二种方式,将那些非Bundle的第三方jar重新打包。在这个过程中,我们发现这绝对是一个辛苦的工作。我们需要找到源码,下载源码,修改pom。Parentproject...总之,顺利重新打包非常难。看来只剩下第三种方式了,这又得回到之前的第一个问题,如何制作Bundlejar。之前,我们分别捆绑maven项目的各个子项目。如果要打包第三方jar,有可能一个第三方jar会被多次打包到不同的子项目中,造成浪费。所以我决定放弃将原项目的各个子项目单独捆绑的想法,而是新建一个子项目,将其他所有的子项目和第三方依赖的jar都导入进来,全部打成一个大束罐。org.osgi.framework,org.slf4jcom.wangyin.sgm.clientcom.wangyin.*,com.lmax.disruptor.*,……看上面的配置,比之前多了一个Private-Package,表示哪些包是这个Bundle的内部包,也就是要进入最后的Bundlejar的事情。既然可以通过直接调用编程式API来监控目标系统,那么最后要做的就是引入运行时字节码增强技术。还是按照常规方式,通过javaagent启动我们的Agent。有了之前的经验,我知道这是由bootstrapClassLoader加载的,所以我直接在org.osgi.framework.bootdelegation中添加了监控Agent的包名。结果启动正常,但无法自动监控。看了日志,发现监控客户端没有启动。监控客户端的启动是通过在每个类加载时尝试使用其类加载来加载监控客户端的启动类。如果可以加载,则启动成功,因为启动程序放在启动类的static块中,而启动类是单例模式,记录了被监控应用程序的信息。从这个启动日志分析,被监控的系统中有200多个Bundle,每个Bundle开始加载监控客户端,然后我们希望监控客户端由自己的类加载器加载。这是本文的第四个核心问题。4.Bundle启动时如何做一些初始化操作。OSGI规范提供了一个叫做BundleActivator的接口,它有两个方法,start和stop。顾名思义,这两个方法会在Bundle启动和停止时被回调,这个好办,我们可以在start方法中实现启动监控客户端的代码。com.wangyin.sgm.client.Activatororg.osgi.framework,org.slf4jcom.wangyin.sgm.clientcom.wangyin.*,com.lmax.disruptor.*,org.apache.flume.*,org.apache.avro.*,com.thoughtworks.*,,org.apache.commons.compress.*,org.apache.commons.lang.*,org.codehaus.jackson.*,org.jboss.netty.*,org.apache.velocity.*,org.xerial.snappy,org.tukaani.xz.*JavaSE-1.6上面的Bundle-Activator标签指定了这个Bundle的Activator是哪个类。至此,所有的问题都解决了。OSGI应用访问APM监控用了两天时间。由于负责监控系统的人从来没有使用过OSGI,所以被监控目标系统的开发者不知道监控原理是什么。一开始我们把两个产品作为一个整体来对接,做了很多无用功,耽误了很多时间。从这个案例可以看出,当你所做的事情需要应用到一个你不熟悉的技术领域时,不可能有足够的时间去学习这个领域的知识。我们做的系统是从零开始,逐渐增加以适应陌生的领域。不要认为你可以整体解决所有的问题,因为可能有100个技术点需要修改才能解决所有的问题。这100个问题同时暴露。如果您同时修改所有结果,您只能看到您的结果。这根本不可能。并将这100个问题一一暴露出来,把一个不可能完成的大任务拆分成几个可以完成的小任务,修改一个,一步成功,看到效果,问题就解决了。作者简介:张晨,高级研发工程师,目前就职于京东金融,曾就职于搜狐等互联网公司。擅长Java底层技术的研发和疑难问题的定位。2015年至今,从事智能运维监控平台的研发与实践,参与并主导了APM等产品的研发与应用,经历了千万级别的运维保障618、双11多次TPS,支撑京东金融大量业务应用的增长。沈建林,曾在多家知名第三方支付公司担任系统架构师,致力于基础中间件和支付核心平台的研发,主导了RPC服务框架、数据库分库分表、统一日志平台、分布式服务跟踪、流程编排等一系列中间件的设计与开发,并参与了多家支付公司支付核心系统的建设。现任京东金融集团高级架构师,负责基础开发部基础中间件的设计与开发。擅长基础中间件设计与开发,专注于大型分布式系统、JVM原理与调优、服务治理与监控等领域。