玩转Sermant开发,开发者能力机制解析
本文分享自华为云社区《开发者能力机制解析,玩转Sermant开发》,作者:华为云开源 。
前言:
在《Sermant框架下的服务治理插件快速开发及使用指南》中带大家一起体验了Sermant插件的开发,快速的了解了Sermant插件开发的全过程,本着从入门到精通的思路,本文对在开发中所常用的能力,从机制上进行更深入的解析。
插件加载&插件调度
解析插件的加载和调度前,可以再回顾一下,Sermant作为一个基于Java字节码增强技术的插件化服务网格,在设计之初就为插件设计了完整的类隔离机制,在《Sermant类隔离架构解析——解决JavaAgent场景类冲突的实践》中进行的详尽的介绍和分析,避免让开发者陷入到复杂的类冲突问题中,从开发者视角来看,可以无需关注类冲突问题,也对Sermant的类隔离机制无感知,同时借助Sermant的局部类加载机制,可以更建议的开发出高性能的服务治理插件。
图- Sermant类隔离机制
插件加载
既然是开发Sermant插件,最先应该了解的是插件是如何加载和调度的,Sermant的插件化机制中得益于Java 的SPI机制,在很多高可扩展的项目中,都会利用SPI去加载自己的扩展,常见的使用SPI机制的场景包括日志框架、数据库驱动、序列化工具、缓存框架等。
Java SPI(Service Provider Interface)是Java提供的一种服务提供者接口,用于在运行时动态加载实现某个接口或者抽象类的类。通过SPI机制,提供实现的一方可以将自己的实现以插件的形式注入到系统中,而无需修改原有的代码。SPI机制是Java中一种基于接口编程的思想,它提高了代码的可扩展性和灵活性,使得应用程序更加易于扩展和维护。

图- Sermant SPI加载机制
在SPI中有三个关键的要素——接口定义、实现创建、配置文件。在Sermant中,框架中定义插件声明接口用于让插件开发者来定义插件的核心要素,插件开发者只需要按照接口契约,创建自身所需的插件声明实现即可,插件声明接口定义如下:
至于SPI机制中的另一个核心要素——配置文件,则需要开发者在插件声明实现创建完成后资源目录resources中添加META-INF/services目录,并在其中创建名为com.huaweicloud.sermant.core.plugin.agent.declarer.PluginDeclarer的SPI文件,并向其中添加插件声明实现的类名,这样再接入Sermant时,Sermant就可以按照配置的指定来将对应的插件声明加载起来。
插件调度
仅依赖SPI机制是无法支持Sermant强大的框架能力的,在Sermant体系中,每一种不同的治理能力都是一个独立的插件,并且每个服务治理能力的实现,都依赖于多个插件声明的组合和拦截器的组合,通俗来讲,每个插件中都依赖了多个字节码切面来完成完整的服务治理,多插件难免会出现使用相同的切点来执行字节码增强逻辑。在如此情况下,Sermant该如何保证各插件的执行顺序,并且保证不会重复的进行字节码的织入呢。
Sermant在最底层维护了一个切面的调度器,首先在插件加载的过程中,调度器会将插件的拦截器(Sermant中定义服务治理逻辑的组件)通过有序列表进行缓存,当字节码织入点被触发时,会进行拦截器的调度,此时Sermant将模仿方法堆栈的执行方式,先进入的方法后结束:

图- Sermant插件调度逻辑
当在进入目标方法时,调度器将对拦截器按照插件加载的方式执行,这样保证在进入方法时的运行顺序符合方法运行的规律;当执行出目标方法时,调度器将对拦截器按照插件加载顺序的逆序执行,这服务方法堆栈中,方法结束的规律。
基于上述逻辑,通过调度器这一层,可以保证不会对相同目标类目标方法进行重复的无意义的字节码增强,同时保证插件在相同目标的执行逻辑符合方法调用堆栈的逻辑,更符合切面程序的执行风格。并且可以通过控制插件来达到控制拦截器执行顺序的目的,也就是达到了控制插件顺序来控制服务治理生效时机的作用,这对一些特殊场景大有裨益。
开发者相关能力解析
除了插件化和类加载等框架的核心机制,插件开发者更多的需要了解Sermant所提供的一些开发这能力,只有更深入的了解这些能力,才能在服务治理插件开发的时候信手拈来。 插件简单来讲就是一系列切面的集合,最终完成了复杂的治理能力。在面向切面编程时,有两个核心的概念,即Join point(切点)——指定切面的横切位置;Advice(通知)——切面执行的具体行为。对应Sermant的插件开发中也有逻辑与之对应,在Sermant中声明切面位置的称之为插件声明,执行切面逻辑的称之为拦截器。
Sermant的插件声明可以基于类名、超类、注解等进行类定位,并通过方法名、类型、参数、返回值等进行方法定位,通过丰富的类匹配能力和方法匹配能力,可以更容易的指定自己期望的织入点。
Sermant的拦截器提供了Before、After、Throw三个关键的生命周期,并在其上提供了形如跳过方法执行,修改方法参数,修改方法返回,修改异常抛出等通用能力。

图- Sermant拦截器提供的能力
拦截器的Before逻辑将会被Sermant的切面调度器在方法执行前按照插件的加载顺序进行调度,这里我们可以通过Sermant提供的API来终止方法的执行,并且可以获取到当前拦截的对象的相关信息,并且还可以获取和修改方法的入参,这里就要注意了,修改入参可能会被其他插件所感知,这里就体现了切面调度器的重要性,如果修改参数产生了预期外的影响,可以通过调整插件顺序的方式来避免这种影响。
插件的After和Throw逻辑将会在目标方法执行结束时,被Sermant的切面调度器统一按照插件加载顺序的逆序进行调度,在此时我们还可以再次来修改方法的返回值和异常。在After中如果需要修改方法的返回值,则也同Before逻辑一样,需要注意拦截器的执行顺序,如果产生了预期外的影响,可以尝试通过调整插件顺序来进行避免。
在Throw逻辑中,只有当方法抛出异常时,Sermant才能触发拦截器处理Throw逻辑,如果异常在方法中被捕获,则无法触发Throw的拦截器处理逻辑,如果在Throw逻辑中将异常修改为null,此时方法将不再会抛出异常。
统一动态配置
在《如何利用动态配置中心在JavaAgent中实现微服务的多样化治理》中,已经对动态配置进行了详细的介绍,本文就不再进行详细的叙述,Sermant动态配置模型是一种基于分层模型设计的配置管理方案,它的核心组件包括Group和Key。Sermant通过不同的Group(分组信息)来对配置项进行隔离,使配置管理更具灵活性和可扩展性;同时,通过Key对配置项进行具体属性的标识,实现了对配置项的精准控制和高效维护,其在主流的配置中心中的概念对应关系如下:

图- Sermant统一动态配置相关概念
Sermant为开发者和使用者屏蔽了配置中心的差异,可以无需修改任何代码,就可以让Sermant对接多种配置中心,开发者只需要通过Group和Key进行配置的划分,无需了解各配置中心的实际字段,就可以开发出不依赖配置中心的动态服务治理能力。
统一日志解析
日志是在程序开发中不可或缺的能力,通过日志可以快速找出程序运行时的状态及遇到的问题。Sermant的日志有两个很重要诉求,第一个就是需要隔离,避免Sermant日志系统对微服务带来不良的影响,例如破坏了微服务的日志配置,Sermant日志和微服务日志交叉输出,影响微服务日志检索定位问题。第二个就是需要有监控能力,可以高性能的将执行过程中的异常信息通过Sermant Backend可观测,及时发现边车运行的异常问题。
Sermant的统一日志,首先Sermant框架通过自定义的类加载器将日志引擎和微服务的日志引擎进行隔离,这样避免共用日志引擎,并且限制日志引擎的资源加载只在Sermant自定义的类加载器中进行:
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// 由于类隔离的原因针对StaticLoggerBinder不再通过父类加载器获取重复资源,只返回加载器内的资源
if ("org/slf4j/impl/StaticLoggerBinder.class".equals(name)) {
return findResources(name);
}
return super.getResources(name);
}
第二步Sermant框架通过自定义类加载器来限制日志引擎所能加载到的配置,通过限制"logback.xml"文件资源的加载,来限制日志的配置:
@Override
public URL getResource(String name) {
URL url = null; // 针对日志配置文件,定制化getResource方法,获取FrameworkClassloader下资源文件中的logback.xml
if (CommonConstant.LOG_SETTING_FILE_NAME.equals(name)) {
File logSettingFile = BootArgsIndexer.getLogSettingFile();
if (logSettingFile.exists() && logSettingFile.isFile()) {
try {
url = logSettingFile.toURI().toURL();
} catch (MalformedURLException e) {
url = findResource(name);
}
} else {
url = findResource(name);
}
}
if (url == null) {
url = super.getResource(name);
}
return url;
}
最后通过JUL桥接日志,借助于jul-to-slf4j (opens new window)的桥接能力将JUL日志桥接到日志引擎。最终,Sermant开发者在使用统一日志时,通过JUL接口来构造日志即可,无需再依赖其他第三方日志门面依赖,仅需使用Java 原生日志接口,日志和微服务完全隔离的,避免了边车日志系统对微服务日志系统带来不良的影响。

图- Sermant日志系统
除此之外,Sermant中改造了日志处理器,通过包装日志的桥接处理器,在高级别日志构造时通过Sermant的事件系统进行监控:
public class SermantBridgeHandler extends SLF4JBridgeHandler {
@Override
protected void callLocationAwareLogger(LocationAwareLogger lal, LogRecord record) {
// 覆写SLF4JBridgeHandler的日志转换方法,上报日志事件
int julLevelValue = record.getLevel().intValue();
if (julLevelValue > Level.INFO.intValue() && julLevelValue <= Level.WARNING.intValue()) {
// 记录警告级别日志
LogEventCollector.getInstance().offerWarning(record);
} else if (julLevelValue > Level.WARNING.intValue()) {
// 记录错误级别日志
LogEventCollector.getInstance().offerError(record);
}
super.callLocationAwareLogger(lal, record);
}
}
针对高级别的日志进行监控,可以通过配置事件系统将高级别日志进行异常的上报,通过Sermant Backend可以第一时间发现边车运行的异常状态。
结语
本文针对Sermant插件开发中的总会接触到的一些能力进行了更深层次的解析,基于更深入的了解,在插件开发时,才能更灵活的使用Sermant提供的丰富开发者能力,希望本篇文章可以对广大插件开发者带来一定的启发,除了上述能力,在插件开发中还可能需要用到利用Archetype能力快速构建项目并使用如心跳、链路标记等加速服务治理的开发,如何构建局部类加载环境等更多的开发指导可见Sermant开发者指南。
开发完成后,如想在k8s环境下快速部署Sermant、动态的执行Sermant的安装和卸载、重复安装插件以及完成边车的自监控等,可通过Sermant用户使用手册学习更多技巧。
-----------------------------------------------------------------------------------------
Sermant作为专注于服务治理领域的字节码增强框架,致力于提供高性能、可扩展、易接入、功能丰富的服务治理体验,并会在每个版本中做好性能、功能、体验的看护,广泛欢迎大家的加入。
- Sermant官网:
- GitHub仓库地址:
玩转Sermant开发,开发者能力机制解析的更多相关文章
- 用RegularJS开发小程序 — mpregular解析
本文来自网易云社区. Mpregular 是基于 RegularJS(简称 Regular) 的小程序开发框架.开发者可以将直接用 RegularJS 开发小程序,或者将现有的 RegularJS 应 ...
- 知名互联网公司校招 Java 开发岗面试知识点解析
天之道,损有余而补不足,是故虚胜实,不足胜有余. 本文作者在一年之内参加过多场面试,应聘岗位均为 Java 开发方向.在不断的面试中,分类总结了 Java 开发岗位面试中的一些知识点. 主要包括以下几 ...
- 转 Java Classloader机制解析
转 Java Classloader机制解析 发表于11个月前(2014-05-09 11:36) 阅读(693) | 评论(0) 9人收藏此文章, 我要收藏 赞1 慕课网,程序员升职加薪神器,点 ...
- 【FastDev4Android框架开发】RecyclerView完全解析之下拉刷新与上拉加载SwipeRefreshLayout(三十一)
转载请标明出处: http://blog.csdn.net/developer_jiangqq/article/details/49992269 本文出自:[江清清的博客] (一).前言: [好消息] ...
- CDN 的缓存与回源机制解析
CDN的缓存与回源机制解析 CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器.这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户 ...
- iOS开发中的Html解析方法
iOS开发中的Html解析方法 本文作者为大家介绍了在iOS开发中的Html解析方法,并同时提供了Demo代码的下载链接,Demo 解析了某个网站(具体可在代码中查看)的html网页,提取了图片以及标 ...
- Java并发编程:Concurrent锁机制解析
Java并发编程:Concurrent锁机制解析 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: # ...
- Windows内核开发-6-内核机制 Kernel Mechanisms
Windows内核开发-6-内核机制 Kernel Mechanisms 一部分Windows的内核机制对于驱动开发很有帮助,还有一部分对于内核理解和调试也很有帮助. Interrupt Reques ...
- Android开发:碎片Fragment完全解析fragment_main.xml/activity_main.xml
Android开发:碎片Fragment完全解析 为了让界面可以在平板上更好地展示,Android在3.0版本引入了Fragment(碎片)功能,它非常类似于Activity,可以像 Activi ...
- 8天玩转并行开发——第三天 plinq的使用
原文 8天玩转并行开发——第三天 plinq的使用 相信在.net平台下,我们都玩过linq,是的,linq让我们的程序简洁优美,简直玩的是爱不释手,但是传统的linq只是串行代码,在并行的 年代如果 ...
随机推荐
- Nuxt.js 生成sitemap站点地图文件
Nuxt.js 生成sitemap站点地图文件 背景介绍 使用nuxt框架生成静态文件支持SEO优化,打包之后需要生成一个 sitemap.xml 文件方便提交搜索引擎进行收录.官网有提供一个插件 ...
- docker入门加实战—Docker镜像和Dockerfile语法
docker入门加实战-Docker镜像和Dockerfile语法 镜像 镜像就是包含了应用程序.程序运行的系统函数库.运行配置等文件的文件包.构建镜像的过程其实就是把上述文件打包的过程. 镜像结构 ...
- LLM在text2sql上的应用
一.前言: 目前,大模型的一个热门应用方向text2sql它可以帮助用户快速生成想要查询的SQL语句.那对于用户来说,大部分简单的sql都是正确的,但对于一些复杂逻辑来说,需要用户在产出SQL的基础上 ...
- Chromium GPU资源共享
资源共享指的是在一个 Context 中的创建的 Texture 资源可以被其他 Context 所使用.一般来讲只有相同 share group Context 创建的 Texture 才可以被共享 ...
- windows开发环境备份,再也不怕重装系统了
每次重装系统后,都要重新安装软件,配置环境变量,极为繁琐.故作环境环境变量备份,常用软件恢复记录,前提是你的软件要安装在非系统盘,D/E盘等 软件安装在非系统盘 开发软件安装在非系统盘,建好目录.重装 ...
- git 删除远程分支,重新提交代码
最近提交代码,分支名出错了,要更正分支名并且重新提交代码,这里记录一下. 说明一下,我之前的分支名是:feature_mobile_duty,更正后的分支名是feature-mobile-duty,是 ...
- Windows上的多jdk版本管理工具
前言 Java在Windows上因为版本太多导致难以管理,这个项目可以很好的解决这点 项目地址 GitHub - ystyle/jvms: JDK Version Manager (JVMS) for ...
- JavaScript 简介与引用
作者:WangMin 格言:努力做好自己喜欢的每一件事 我们通常写好的HTML网页是处于一个静态的效果,在用户体验这一方面就不是很好,给人一种死板的感觉.这里我们就可以用到JavaScript来为网页 ...
- 文心一言 VS 讯飞星火 VS chatgpt (129)-- 算法导论11.1 4题
四.用go语言,我们希望在一个非常大的数组上,通过利用直接寻址的方式来实现一个字典.开始时该数组中可能包含一些无用信息,但要对整个数组进行初始化是不太实际的,因为该数组的规模太大.请给出在大数组上实现 ...
- 2023-11-11:用go语言,字符串哈希+二分的例题。 给定长为 n 的源串 s,以及长度为 m 的模式串 p, 要求查找源串中有多少子串与模式串匹配, s‘ 与 s 匹配,当且仅当 s‘ 与 s
2023-11-11:用go语言,字符串哈希+二分的例题. 给定长为 n 的源串 s,以及长度为 m 的模式串 p, 要求查找源串中有多少子串与模式串匹配, s' 与 s 匹配,当且仅当 s' 与 s ...