通过 Sentinel 非侵入实现流量链

「这是我参与11月更文挑战的第20天,活动详情查看:2021 最后一次更文挑战

什么叫“侵入”?什么叫“无侵入”?

在学习 Sentinel,讨论无侵入监控时,你可能会问:Sentinel 不是“无侵入”的啊,接入 Sentinel 是需要引入客户端 jar 包的?

其实 Sentinel 是体感“无侵入”,而代码“侵入”的,其原因我慢慢解答。

目前很多说法是:像 SkyWalking 这种 APM 工具,通过探针实现字节码增强的方式才叫作“无侵入”,这样的理解是错误的。

APM 工具的实现方案有两种:一种是黑盒方案,另一种是标记方案。当今开源的 APM 工具都是使用标记方案实现,所以对应用服务是有侵入的。

而“侵入”的意思是:APM 客户端工具会向应用服务内部织入监控代码,这一点我们可以通过对应用服务进行远程调试,或是对 Class 文件进行反解析来证实。

那之所以会有“只有 SkyWalking 这类通过探针实现的 APM 工具才是无侵入”的错觉,归根结底是使用 APM 产品时的不同“体感”所致。

我们可以将侵入度按照不同“体感”,划分为以下两种:

  • 第一种以 SkyWalking 为代表的零接入“体感”的 APM 工具。

它们使用探针实现字节码增强技术,解决了织入监控代码难的问题(对比其他工具,它可以在不实现监控框架暴露的拦截器或过滤器的情况下,在任意地方织入监控代码)。通过面向切面的思想,使用线程本地变量在任务线程的生命周期中完成监控标记的无侵入传递。

  • 第二种以 Sentinel 为代表的低侵入“体感”的 APM 工具。

由于无法使用字节码增强技术,所以织入代码只能通过框架暴露的拦截器或过滤器实现监控代码的织入。但这种方式依然可以使用与第一种方式一样的通过面向切面的思想,使用线程本地变量在任务线程的生命周期中完成监控标记无侵入传递。接入监控时,显式地引入客户端 jar 包,即可完成接入。

对比两种方式,虽然第二种在接入效率上有些欠缺,但它是变通的。它将企业内部所有服务接入统一的脚手架,通过脚手架基建绕过一线开发来引入 APM 工具的客户端,从而降低侵入“体感”。这与 SRE 通过在应用服务的启动命令中,增加探针参数来绕过一线开发管理 APM 客户端思想是一致的。

具体的实践方案可以参考阿里云 Java 脚手架,在依赖组件中添加 Sentinel,即可在新项目的脚手架基建中增加 Sentinel 的流量管控。

再回看 APM 有关侵入的问题,我们可以清晰地将其总结为两句话:

  • APM 工具都会侵入应用服务,只不过织入监控代码的技术方案有所不同;

  • 通过 AOP 思想,使用线程本地变量实现监控任务线程的生命周期,这种方式就是释放一线开发人员编写监控代码的无侵入方案。

Sentinel 也是“无侵入”的 APM 工具这个问题就解开了。那根据上面总结的第一句话,你是不是又对 APM 工具织入的监控代码产生兴趣了呢?接下来,我就以“织入监控代码是如何构建资源树结构”为主题,与你详解通用框架监控方案。

监控示例:Sentinel 资源树构建的基本原理

正式开讲前,我们先来熟悉下聚合搜索工具的示例项目。聚合搜索工具的核心伪代码,如下所示:

class Controller{
  public Response mergeSearch(String param){
    baidu = search("https://www.baidu.com/s?wd=" + param);
    google = search("https://www.google.com/search?q=" + param);
    return baidu + google;
  }

  private Response search(String param){
    return OkHttpClient.get(param);
  }
}
复制代码

聚合搜索服务可以同时将多个搜索引擎的搜索数据聚合起来。比如,用户对聚合搜索服务输入关键词 APM,那聚合搜索服务会将 baidu 和 google 搜索出来的结果,聚合返回给用户。

接入 Sentinel

通过在 pom 文件中,引入并配置 Sentinel 的 webmvc 和 okhttp 适配器的客户端 jar 包,将聚合服务接入 Sentinel 后,我们会得到如下的簇点链路图:

image.png

Sentinel 的监控流程与 Spring AOP 思想一致,通过 Spring MVC 和 OkHttp 框架暴露出的拦截器,对流量进行面向切面监控。只不过在监控过程中,使用了线程本地变量存储了监控信息,当请求再次被拦截时,识别线程本地变量存储的监控信息,构建出资源树。

这就是 Sentinel 资源树构建的基本原理,总的来说,还是很好理解的。

Sentinel 技术骨架

学习基本原理入门后,我们再回来学习 Sentinel 的技术骨架,Sentinel 对流量的管控是通过责任链设计模式实现的。

责任链设计模式是:将定义规则的对象根据指定顺序连成一条链,实现定义规则对象间的解耦,请求按照指定顺序被处理,直到有规则被匹配到为止。

Sentinel 就是使用责任链设计模式实现了功能插槽链(Slot chain),如下图所示:

image.png

每个被管控的资源,都会创建一系列的功能插槽链,每个功能插槽都有自己的职责,官方提供了 7 个不同职责的插槽链。今天只讲第一个功能插槽 NodeSelectorSlot,它的职责是构建资源节点树。

NodeSelectorSlot 示例

NodeSelectorSlot 是负责收集请求所关联的资源节点的路径,将这些节点资源的调用路径,以树状结构存储起来。

我们先看下官方的接入示例代码,学习下 Sentinel 的几个核心对象,接入示例代码如下:

  ContextUtil.enter("entrance1", "appA");
  Entry nodeA = SphU.entry("nodeA");
  if (nodeA != null) {
    nodeA.exit();
  }
  ContextUtil.exit();
复制代码

上述代码中,ContextUtil.enter 会在线程本地变量中创建一个名为“entrance 1”的上下文(Context)。上下文对象维护着当前调用链的元数据,其重要的属性如下。

  • name:用于标识上下文的名称,如接入示例代码中的 entrance 1。

  • entranceNode:当前调用链路的入口节点。节点有 4 种类型,它们有着继承关系。

  • EntranceNode 继承 DefaultNode:表示一棵资源节点树的入口节点,通过此节点可以获取所有子节点。

  • DefaultNode 继承 StatisticNode:节点中,存储着指定上下文相关的统计信息,当一个上下文对象被多次调用 SphU.entry 方法时,该节点就会关联多个叶子节点。

  • ClusterNode 继承 StatisticNode:节点中,存储着资源总体运行的是统计信息,包括响应时间、线程数等,相同资源贡献一个 ClusterNode 节点。

  • curEntry:当前调用链路的当前资源节点。

  • origin:当前调用链路的调用源名称。

接入示例的第一行代码中的“appA”就是调用源标识;紧接着通过 SphU.entry 请求一个 token,如果此时方法执行成功,就代表当前流量未达到被限制的阈值,可以被放行去执行之后的业务代码;在执行完业务代码后,调用 nodeA.exit 和 ContextUtil.exit 方法去告诉 Sentinel 当前监控点可以退出。

猜你喜欢

转载自juejin.im/post/7033940468106788878
今日推荐