EaseAgent 无侵入式观测系统

背景

随着微服务的广泛应用,故障和问题定位变的非常困难,完善的可观测性已经变成了分布式系统的刚需,用于定位问题的分布式问题追踪系统更是可观测性的重中之重。随着需求量的变大,市场上出现了很多APM(Application Performance Management)产品来解决分布式系统的可观测性问题。如下仅是 MegaCloud 部分微服务,但已可见应用复杂性。

起初,这些APM产品相互竞争,大家各自为政,每个厂商都以自己专有的规范和标准在做产品。这导致了无论开源还是闭源,每一家都要做一个完整的APM系统,厂家不停的制造轮子,由于商业竞争的关系,彼此也不通用不互联,还出现非常多的混乱和口水战。后来,整个业界开始慢慢形成一些行业标准和规范来解决这些异构和不标准的系统间的互联问题。

以下是常见的产品和规范:

开源类产品商用产品规范
Zipkin[1]DataDog[2]OpenZipkin[3]
JaegerDynaTrace[2]OpenTracing[3]
SkyWalkingAppDynamic[2]OpenCensus[3]
PinpointNew Relic[2]OpenTelemetry[3]
Elastic APMGoogle Cloud Trace
Azure Application Insights
AWS X-Ray

[1] 开源产品中,Zipkin 是 Twitter 根据谷歌 Dapper 做的一个开源产品,是所有APM中最忠实于 Google Dapper 论文的实现,也是一个很开放的老牌产品,Spring Cloud 的 Sleuth 使用了 Zipkin。

[2] 商业产品有很多,比较有名的是:Dynatrace、AppDyncmic、New Relic 和 Datadog 等。

[3] OpenZipkin、OpenTracing 和 OpenCensus 是三个不一样的标准,OpenTelemetry 正在整合 OpenTracing 和OpenCensus,但还没有完成。

既然分布式 APM 的产品那么多,还彼此分裂,作为一个用户,该使用哪一款产品呢?最好的选择自然是谁也不偏向,跟行业标准看齐,一旦有标准存在,就能充分利用社区成果,享受开源福利。但是,我们看到即使是开源 APM,很多实现在数据格式上也是相对专有封闭。而相较于其他的开源产品,Zipkin 相对开放一些,也是最符合Dapper论文的实现;另外,OpenTelemetry 会是未来最终的标准和规范。我们觉得,对于用户选型需要有两个方面的事宜需要慎重的考虑:

  • 在软件架构上要做到足够的开放,容易与其它的开源软件组件进行集成

  • 在标准上要能够向更为标准的协议看齐

动机和初衷

我们为什么要重新开发一个Java Agent呢?主要的动机和理由有以下几个点。

  1. All-in-One Agent 统一的Java探针

    我们希望 EaseAgent 是一个统一的Java 探针,不希望它是分裂的。现在太多的监控系统,都是各干各的,Tracing 一块,Metric 一块,日志又一块,MySQL 等中间件等等一堆。但是,单单有数据是没用的,只有将他们关联起来才会有有价值的信息。数据在不同地方的话,没有关联起来。所以我们需要一个把所有数据放在一起的探针。目前市场上的 Agent,要不就是只采集Tracing, 要不就是只做 Metrics, 没有一个即做 Metrics 又做 Tracing 的统一 Agent。

  2. Easy to extend and customize 易于扩展和定制

    我们希望它容易扩展,及时更新,因为不同的用户会使用不同的的开发框架,不同的类库,不同的中间件,不同运行环境,未来可能还有新的框架、类库和中间件甚至还有用户自研的中间件出现。因此一个设计良好的Java Agent必须能快速应对变化, 做到能够通过一种简单的机制对它进行扩展和定制

  3. Just a lightweight Java Agent 轻量级的一个Java Agent

    APM 是一个旁路系统,旁路系统不应该影响或应该尽量小的影响主业务逻辑运行,所以我们希望它是一个轻量的,自由的,开放的收集器和探针,而不是绑定了的一整套APM系统。它应该可以跟其他的系统很容易的集成在不影响或尽量小影响系统的情况下完成系统的观测性数据采集。

  4. Not only a monitoring agent 不仅仅只是一个监控探针

    我们觉得,观测一个系统并不是我们的最终目标,我们最终问题是提高系统的SLA。所以它不仅仅只是收集数据,它还可以协助其他控制系统对整个分布式微服务架构进行管理和增强以提最终高服务的SLA

部分开源Java Agent的对比

基于以上几个维度的考量,我们对部分开源 Java Agent 进行了对比,列表如下。从列表可见,符合轻量、标准、开放和易扩展的 Java Agent 组件是稀少缺失的,这也正是我们最初开发EaseAgent的缘起。

EaseAgent 2.0 的特点

EaseAgent 2.0 版有如下的特点

  1. 标准化,高度开放,易于集成其他开源产品。可以直接连接Kafka,Prometheus,Zipkin,Tempo/Grafana,符合标准的组件都支持集成。

  2. 易于定制扩展,通过插件机制和 Java-Macher-DSL,十几行代码里面就能做增强扩展。

  3. Cloud Native 亲和性。不同于多数 Java Agent 仅用于观测性的数据采集,EaseAgent 配合 ServiceMesh 提供控制能力,实现服务治理、流量重定向和生产环境压力测试等。

以下即是生产环境压力测试的方案简图,更详细的可参看技术文章:无侵入式生产线全链路压力测试,详细描述了整个技术方案和实现。

全面的数据采集能力

Tracing

EaseAgent 的 Tracing 数据格式当前完全兼容 OpenZipkin 格式,并且支持通过 Reporter 模块的 Encoder/Sender 插件进行格式和网络接口的扩展,并且可以导出为 OpenTelemetry 格式数据到兼容后端平台。

Metrics

EaseAgent 作为一个单独的 Java Agent,就可以收集各种中间件包括但不限于HTTP请求的如下相关的数据:

  • Throughput (m1, m5, m15)
  • Error throughput (m1err, m5err, m15err)
  • Error throughput percentage (m1err, m5err, m15err)
  • Latency (p25, p50, p75, p95, p98, p99)
  • Execution duration (min, mean, max)

更多具体的指标数据详情,可参看:EaseAgent 2.0 收集Prometheus指标一览

Application Log

日志采集,我们按照 OpenTelemetry 标准的日志数据模型设计了 EaseAgent 的应用日志采集模块,当前支持专有的 JSON 日志格式导出,但因其标准化,可以很容易地通过 Encoder 插件导出为其他格式的日志采集,欢迎了解和参与 Encoder 插件开发。

EaseAgent 的开放性

EaseAgent 得益于标准化,它可以非常快速地跟 Prometheus/Zipkin/Tempo/Grafana 等 开源方案集成,从而形成完整的 APM 系统。我们在MegaEase官方的演示项目—— EaseAgent Spring PetClinic 中,对 Spring 官方的演示项目 Spring-PetClinic 不做任何改动的情况下,通过 EaseAgent 进行了无侵入的运行指标的监测,并将指标数据导出到 Prometheus 和 Grafana/Tempo,形成一套完成 APM 系统,以下是相关的数据展现。

Grafana Metrics 面板

EaseAgent的指标包括应用层和中间件的指标,其中又有吞吐率,异常吞吐率,延迟等。

Grafana Tracing 面板

Tracing 数据展示,可以从 Grafana 的 Explore 中搜索查看。

EaseAgent 总体设计

接下来,我们将介绍 EaseAgent 2.0 的架构和插件设计,以及如何进行插件开发。

整体架构

EaseAgent 2.0 的架构如下图所示,在 2.0 版本的 EaseAgent 中,我们引入了插件增强机制,以满足不同业务对 Agent 进行扩展的需求。

所有的 Java Agent 的核心支点都是对特定的方法进行增强,以实现增强业务,如 Tracing 和 Metric 业务。同样的,EaseAgent 的增强插件机制的出发点是设计一套易于理解使用并且高效可靠的插件接口框架,能让用户方便地对特定方法进行增强以实现业务需求。

为了易于理解和使用,我们将插件抽象成三要素:Points、Interceptors 和 AgentPlugin。

  • Points 用于确定在何处增强;
  • Interceptors 用于定义做什么,即在 Points 确定的增强点所要执行的业务逻辑;
  • AgentPlugin 则让插件可配置并且配置可运行时动态更新。

在架构上我们需要解决下面几个问题:

  • 让多个插件可以对一个方法进行多重增强。我们让同一方法上的多个插件的 Interceptors 组成一个 Interceptors Chain,并让每一个增强方法点都只增强一段简短字节码,同时为方法点分配一个唯一Id(unique Index),用于增强字节码运行时作为数组索引获取对应的 Interceptors Chains;

  • 插件间既可以无感知又可以相互协作。在一个 Interceptors Chain 中,各 Interceptors 之间的优先级可编排,还需要提供了 Interceptors 之间数据交换的机制。

另外,增强 Interceptors 是为了实现业务需求,我们为最为常见的 Tracing 和 Metric 业务提供了封装的 API 接口,这样增强插件就可以通过API的辅助较为快速地完成 Tracing 和 Metric 采集需求。而 Report 组件则负责完成数据格式封装和上传后端服务器的职责,它同样可定制和扩展,以满足不同数据格式和网络架构地需求。

目前,我们支持的插件如下:

  • Data Collection
    • Collecting Metric & Tracing Logs.
      • JDBC 4.0
      • HTTP Servlet、HTTP Filter
      • Spring Boot >=2.2.x: WebClient 、 RestTemplate、FeignClient
      • RabbitMQ Client >=5.x、 Kafka Client >=2.4.x
      • Jedis >=3.5.x、 Lettuce >=5.3.x (sync、async)
      • ElasticSearch Client >= 7.x (sync、async)
      • MongoDB Client >=4.0.x (sync、async)
    • Collecting Access Logs.
      • HTTP Servlet、HTTP Filter
      • Spring Cloud Gateway
    • Instrumenting the traceId and spanId into user application logging automatically
    • Supplying the health check endpoint
    • Supplying the readiness check endpoint for SpringBoot >=2.2.x
  • Data Reports
    • Console Reporter
    • Prometheus Exports
    • Http Reporter
    • Kafka Reporter
    • Custom Reporter

增强插件设计

下面,我们简单看一下我们的插件设计的重点。

我们将插件抽象成了三要素,对应三个接口。插件开发就是实现三个接口,分别完成了增强点定义、增强业务逻辑和配置信息定义。工程上,一个完整的插件模块,也就是一个插件项目,可以包含多个插件,如下图所示,HttpServlet 插件子项目里包含了多个插件实现。

增强点Points

增强点定义通过 Points 接口类实现来完成,Points 包含三个接口方法:

public interface Points {
    /**
     * return the defined class matcher matching a class or a group of classes
     */
    IClassMatcher getClassMatcher();

    /**
     * return the defined method matcher
     */
    Set<IMethodMatcher> getMethodMatcher();

    /**
     * When returning true, add a dynamic field to matched classes
     * The dynamically added member can be accessed by AgentDynamicFieldAccessor
     */
    default boolean isAddDynamicField() {
        return false;
    }
}

Points 的三个接口方法分别用来确定一个类匹配器、一组方法匹配器及是否对类扩展成员。扩展出来的成员可以用于数据传递和交换,更重要的是类匹配器和方法匹配器,EaseAgent 会在类匹配器匹配到的类中,使用方法匹配器来确定要进行增强的方法。

对于 EaseAgent 的类匹配器和方法匹配器的设计细节可以参看我们文档 Matcher DSL或者观看分享视频从 44:10 时见开始部分。

拦截器Interceptor

拦截器 Interceptor 实现具体的业务逻辑,是插件三要素中最重要的接口,因业务不同实现可能千差万别,是插件开发的核心接口。

Interceptor 需要为插件开发者提供什么样的能力,以满足各种场景下的插件扩展开发呢?插件业务即可能是数据面的数据采集业务,如 Tracing,也可能是控制面的行为控制业务,如 Redirection 重定向插件,因此 Interceptor 既要让业务逻辑有访问当前方法调用实例、参数、返回值的能力,同时也需要提供修改参数、返回值的能力。如下 Interceptor 接口定义,这些读写能力正是通过 MethodInfo 参数及其接口提供给业务逻辑的。

public interface Interceptor extends Ordered {
    void before(MethodInfo methodInfo, Context context);

    void after(MethodInfo methodInfo, Context context);

    default String getType() {
        return Order.TRACING.getName();
    }
    ......
}

Interceptor 之间可彼此独立不感知,但有时需要协作。关联到一个增强方法上的多个 Interceptors 形成一个Interceptors Chain,其优先次序的编排,是通过 Ordered 接口来确定的;而协作需要的数据传递交换,具体实现可以根据业务场景灵活多样,但 Context 参数通过 put/get 接口提供了基本的传递交换能力。

相关的设计细节可以参看我们 插件开发方档

Agent Plugin

AgentPlugin 接口是三要素中最简单的接口,只有两个接口方法:

public interface AgentPlugin extends Ordered {

     String getNamespace();
     String getDomain();
}

这两个接口实现为所有绑定到该 Plugin 的 Interceptor 确定了插件配置项的配置前缀:

plugin.[domain].[namespace].[type].key=value

其中 domain 和 namespace 就是由 AgentPlugin 接口确定;type 则由 Interceptor 的 getType() 接口确定。在Interceptor 的业务实现逻辑中,任何时候通过 Context::getConfig() 获得的配置都是最新的以 key-value 组成的配置键值对,也就是说,在外部通过 HTTP-Config-API 调用更新配置后,配置将自动更新为最新配置。

以上,简要的说明了插件三要素的设计和使用方式。下面是两个示例:

关于如何对Agent 进行Debug,也可以参看我们的社区里的开发文档和相关的视频分享