如何使用 OpenTelemetry 快速搭建一套可视化分布式监控系统

公众号关注 「奇妙的 Linux 世界」

设为「星标」,每天带你玩转 Linux !

0c995828ef7aecdd6af7c43b3bed7771.jpeg

OpenTelemetry 是一款数据收集中间件,我们可以使用它来生成,收集和导出监测数据(Metrics、Logs 和 Traces),这些数据可供支持 OpenTelemetry 的中间件存储,查询和显示,用以实现数据观测,性能分析,系统监控,服务告警等能力。

opentelemetry 项目开始于2019年,旨在提供基于云环境的可观测性软件的标准化方案,提供与三方无关的监控服务体系。项目迄今为止已获得了 Zipkin、Jaeger、skywalking、Prometheus等众多知名中间件的支持。

1、分布式监控系统介绍

随着 SOA,微服务架构及 PaaS、Devops 等技术的兴起,线上问题的追踪和排查变得更加困难。对线上业务的可观测性得到了越来越多企业的重视,由此涌现出了许多优秀的链路追踪及服务监控中间件。比较流行的有 Spring Cloud 全家桶自带的 Zipkin、点评的 CAT、Skywalking、Jaeger、Pinpoint。

一个典型的应用,通常有三种类型的数据需要被监控系统记录:Metric、logs and traces。让我们先了解下它们都是什么。

Metrics

提供进行运行时的指标信息。比如CPU使用率,内存使用情况,GC情况,网站流量等。

Logging

可以监控程序进程中的日志,比如集成Log4j记录的日志,或者程序运行中发生的事件或通知。

Tracing

也叫做分布式追踪,包含请求中每个子操作的开始和结束时间,传递的参数,请求间的调用链路,请求在各个链路上的耗时等信息。Tracing可以包含消息发送和接收,数据库访问,负载均衡等各种信息,让我们可以深入了解请求的执行情况。Tracing为我们提供了获取请求的时间主要消耗在哪里,请求的参数都是什么,如果发生了异常,那么异常是在哪个环节产生的等能力。

7857a169f73e82a7d1b1e9de54ca59c6.png

2、sample项目

本例中,我们使用spring cloud搭建一个简单的微服务,来体验下如何使用opentelemetry来进行系统监控,并在两个不同的监控系统(Zipkin,Jaeger)进行快速切换。项目由2个微服务,2个可视化监控系统,并使用opentelemetry 来集成微服务和监控系统。

  • gateway-service -使用spring cloud gateway搭建的服务网关

  • cloud-user-service -用户微服务,使用Spring boot + spring mvc

  • Zipkin - Zipkin监控系统服务端

  • Jaeger - Jaeger监控系统服务端

8ba018c6cae8facd7374d7bfe1e8c097.png

3、使用opentelemetry 集成Zipkin

示例中使用到的组件的版本:

  • java: 1.8

  • spring-cloud: 2020.0.2

  • spring-boot: 2.4.5

  • opentelemetry: 1.1.0

  • grpc: 1.36.1

3.1、cloud-user-service服务maven配置

引入 Spring cloud 和 opentelemetry

  1. <dependencyManagement>
  2.     <dependencies>
  3.         <dependency>
  4.             <groupId>org.springframework.cloud</groupId>
  5.             <artifactId>spring-cloud-dependencies</artifactId>
  6.             <version>${spring-cloud.version}</version>
  7.             <type>pom</type>
  8.             <scope>import</scope>
  9.         </dependency>
  10.         <dependency>
  11.             <groupId>io.opentelemetry</groupId>
  12.             <artifactId>opentelemetry-bom</artifactId>
  13.             <version>${opentelemetry.version}</version>
  14.             <type>pom</type>
  15.             <scope>import</scope>
  16.         </dependency>
  17.     </dependencies>
  18. </dependencyManagement>

加入 opentelemetry 依赖项

  1. <dependency>
  2.     <groupId>io.opentelemetry</groupId>
  3.     <artifactId>opentelemetry-api</artifactId>
  4. </dependency>
  5. <dependency>
  6.     <groupId>io.opentelemetry</groupId>
  7.     <artifactId>opentelemetry-sdk</artifactId>
  8. </dependency>
  9. <dependency>
  10.     <groupId>io.opentelemetry</groupId>
  11.     <artifactId>opentelemetry-exporter-otlp</artifactId>
  12. </dependency>
  13. <dependency>
  14.     <groupId>io.opentelemetry</groupId>
  15.     <artifactId>opentelemetry-semconv</artifactId>
  16.     <version>1.1.0-alpha</version>
  17. </dependency>
  18. <dependency>
  19.     <groupId>io.grpc</groupId>
  20.     <artifactId>grpc-protobuf</artifactId>
  21.     <version>${grpc.version}</version>
  22. </dependency>
  23. <dependency>
  24.     <groupId>io.grpc</groupId>
  25.     <artifactId>grpc-netty-shaded</artifactId>
  26.     <version>${grpc.version}</version>
  27. </dependency>
  28. <dependency>
  29.     <groupId>io.opentelemetry</groupId>
  30.     <artifactId>opentelemetry-exporter-zipkin</artifactId>
  31. </dependency>

3.2、配置opentelemetry

  1. @Configuration
  2. public class TraceConfig {
  3.     private static final String ENDPOINT_V2_SPANS = "/api/v2/spans";
  4.     private final AppConfig appConfig;
  5.     @Autowired
  6.     public TraceConfig(AppConfig appConfig) {
  7.         this.appConfig = appConfig;
  8.     }
  9.     @Bean
  10.     public OpenTelemetry openTelemetry() {
  11.         SpanProcessor spanProcessor = getOtlpProcessor();
  12.         Resource serviceNameResource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, appConfig.getApplicationName()));
  13.         // Set to process the spans by the Zipkin Exporter
  14.         SdkTracerProvider tracerProvider =
  15.                 SdkTracerProvider.builder()
  16.                         .addSpanProcessor(spanProcessor)
  17.                         .setResource(Resource.getDefault().merge(serviceNameResource))
  18.                         .build();
  19.         OpenTelemetrySdk openTelemetry =
  20.                 OpenTelemetrySdk.builder().setTracerProvider(tracerProvider)
  21.                         .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
  22.                         .buildAndRegisterGlobal();
  23.         // add a shutdown hook to shut down the SDK
  24.         Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close));
  25.         // return the configured instance so it can be used for instrumentation.
  26.         return openTelemetry;
  27.     }
  28.     private SpanProcessor getZipkinProcessor() {
  29.         String host = "localhost";
  30.         int port = 9411;
  31.         String httpUrl = String.format("http://%s:%s", host, port);
  32.         ZipkinSpanExporter zipkinExporter = ZipkinSpanExporter.builder().setEndpoint(httpUrl + ENDPOINT_V2_SPANS).build();
  33.         return SimpleSpanProcessor.create(zipkinExporter);
  34.     }
  35. }

3.3、在cloud-user-service中使用opentelemetry

当我们完成了配置后,就可以在spring boot项目中,通过autowired来使用opentelemetry。

接下来我们定制一个WebFilter来拦截所有的Http请求,并在Filter类中进行埋点。

  1. @Component
  2. public class TracingFilter implements Filter {
  3.     private final AppConfig appConfig;
  4.     private final OpenTelemetry openTelemetry;
  5.     @Autowired
  6.     public TracingFilter(AppConfig appConfig, OpenTelemetry openTelemetry) {
  7.         this.appConfig = appConfig;
  8.         this.openTelemetry = openTelemetry;
  9.     }
  10.     @Override
  11.     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  12.         HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
  13.         Span span = getServerSpan(openTelemetry.getTracer(appConfig.getApplicationName()), httpServletRequest);
  14.         try (Scope scope = span.makeCurrent()) {
  15.             filterChain.doFilter(servletRequest, servletResponse);
  16.         } catch (Exception ex) {
  17.             span.setStatus(StatusCode.ERROR, "HTTP Code: " + ((HttpServletResponse)servletResponse).getStatus());
  18.             span.recordException(ex);
  19.             throw ex;
  20.         } finally {
  21.             span.end();
  22.         }
  23.     }
  24.     private Span getServerSpan(Tracer tracer, HttpServletRequest httpServletRequest) {
  25.         TextMapPropagator textMapPropagator = openTelemetry.getPropagators().getTextMapPropagator();
  26.         Context context = textMapPropagator.extract(Context.current(), httpServletRequest, new TextMapGetter<HttpServletRequest>() {
  27.             @Override
  28.             public Iterable<String> keys(HttpServletRequest request) {
  29.                 List<String> headers = new ArrayList();
  30.                 for (Enumeration names = request.getHeaderNames(); names.hasMoreElements();) {
  31.                     String name = (String)names.nextElement();
  32.                     headers.add(name);
  33.                 }
  34.                 return headers;
  35.             }
  36.             @Override
  37.             public String get(HttpServletRequest request, String s) {
  38.                 return request.getHeader(s);
  39.             }
  40.         });
  41.         return tracer.spanBuilder(httpServletRequest.getRequestURI()).setParent(context).setSpanKind(SpanKind.SERVER).setAttribute(SemanticAttributes.HTTP_METHOD, httpServletRequest.getMethod()).startSpan();
  42.     }
  43. }

在示例代码中,我们实现了一个匿名类来从HttpServletRequest中解析tracing上下文信息。

在创建Span的同时,我们在Span中写入了Http请求的一些关键属性,并且为所有的异常做了跟踪记录。

3.4、编写服务代码

接下来我们通过一段简单的代码来模拟查询用户以及抛出异常

  1. @GetMapping("/{id}")
  2. public ResponseEntity<User> get(@PathVariable("id") Long id) {
  3.     if (0 >= id) {
  4.         throw new IllegalArgumentException("Illegal argument value");
  5.     }
  6.     return ResponseEntity.ok(userService.get(id));
  7. }

3.5、配置gateway-service

我们使用和cloud-user-service同样的配置来配置gateway-service。

3.6、在gateway-service中,集成opentelemetry

这里和cloud-user-service有些不同,由于gateway-service是基于webflux构建的。我们这次使用WebFilter和GlobalFilter来拦截网关上的http请求。

在WebFilter中,添加opentelemetry来记录收到的http请求

  1. @Override
  2. public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
  3.     ServerHttpRequest serverHttpRequest = serverWebExchange.getRequest();
  4.     Span span = getServerSpan(openTelemetry.getTracer(appConfig.getApplicationName()), serverHttpRequest);
  5.     Scope scope = span.makeCurrent();
  6.     serverWebExchange.getResponse().getHeaders().add("traceId", span.getSpanContext().getTraceId());
  7.     span.setAttribute("params", serverHttpRequest.getQueryParams().toString());
  8.     return webFilterChain.filter(serverWebExchange)
  9.             .doFinally((signalType) -> {
  10.                 scope.close();
  11.                 span.end();
  12.             })
  13.             .doOnError(span::recordException);
  14. }
  15. private Span getServerSpan(Tracer tracer, ServerHttpRequest serverHttpRequest) {
  16.     return tracer.spanBuilder(serverHttpRequest.getPath().toString()).setNoParent().setSpanKind(SpanKind.SERVER).setAttribute(SemanticAttributes.HTTP_METHOD, serverHttpRequest.getMethod().name()).startSpan();
  17. }

接下来在GlobalFilter中,记录路由到微服务的http请求

  1. @Override
  2. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain gatewayFilterChain) {
  3.     Span span = getClientSpan(openTelemetry.getTracer(appConfig.getApplicationName()), exchange);
  4.     Scope scope = span.makeCurrent();
  5.     inject(exchange);
  6.     return gatewayFilterChain.filter(exchange)
  7.             .then(Mono.fromRunnable(() -> {
  8.                         scope.close();
  9.                         span.end();
  10.                     })
  11.             );
  12. }
  13. private void inject(ServerWebExchange serverWebExchange) {
  14.     HttpHeaders httpHeaders = new HttpHeaders();
  15.     TextMapPropagator textMapPropagator = openTelemetry.getPropagators().getTextMapPropagator();
  16.     textMapPropagator.inject(Context.current(), httpHeaders, HttpHeaders::add);
  17.     ServerHttpRequest request = serverWebExchange.getRequest().mutate()
  18.             .headers(headers -> headers.addAll(httpHeaders))
  19.             .build();
  20.     serverWebExchange.mutate().request(request).build();
  21. }
  22. private Span getClientSpan(Tracer tracer, ServerWebExchange serverWebExchange) {
  23.     ServerHttpRequest serverHttpRequest = serverWebExchange.getRequest();
  24.     URI routeUri = serverWebExchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
  25.     return tracer.spanBuilder(routeUri.getPath()).setSpanKind(SpanKind.CLIENT).setAttribute(SemanticAttributes.HTTP_METHOD, serverHttpRequest.getMethod().name()).startSpan();
  26. }

为了传递tracing的上下文信息,我们需要调用inject方法,把tracing上下文信息写入到路由请求的头信息里面。

4、运行服务

现在,让我们访问网关 http://localhost:8080/user/0 来观察Zipkin对于服务访问和异常的记录情况。

0ae6efe170be4fbcac956ea0858f21fa.png

可以看到在 Tracing 方面,Zikin 整体表现还不错,有异常的链路也使用红色做了标记。Zipkin 没有打印出异常的堆栈信息,我们需要为此做额外的处理才行。

5、使用Jaeger对接opentelemetry

使用otlp exporter来替换之前使用的zipkin exporter。

  1. <dependency>
  2.     <groupId>io.opentelemetry</groupId>
  3.     <artifactId>opentelemetry-exporter-otlp</artifactId>
  4. </dependency>

在配置类中,使用otlp processor替换之前的zipkin processor。这样就完成了Zipkin到Jaeger的切换。

  1. private SpanProcessor getOtlpProcessor(){
  2.     OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder().setTimeout(2, TimeUnit.SECONDS).build();
  3.     return BatchSpanProcessor.builder(spanExporter)
  4.             .setScheduleDelay(100, TimeUnit.MILLISECONDS)
  5.             .build();
  6. }

6、再次运行服务

我们再次运行服务并访问网关http://localhost:8080/user/0 来观察Jaeger对于服务访问和异常的记录情况。

首先看主界面,Jaeger直接标记了请求中包含异常。

2396f39df9d840bcb1844cd957d1c03d.jpeg

再看下访问的详情,Jaeger记录并显示了异常的堆栈信息。这对我们分析线上异常非常有帮助。

9493e5ca2b3304b747da641ef6c3e3b5.png 36dac10d50d5e429d95773d943c295b5.png
Jaeger提供的DAG图

对比Zipkin,Jaeger提供了更加丰富的功能和更美观的可视化界面。

7、总结

本文介绍了使用opentelemetry 来搭建监控系统,以及如何集成到Zipkin和Jaeger。

利用opentelemetry的标准化能力,我们可以方便地记录更加详细的链路监控信息。

opentelemetry自推出以来,得到了越来越多厂商的关注和支持。对于分布式监控系统这个新生事物,opentelemetry是否能成为最终的事实标准,让我们拭目以待。

本文转载自:「简书」,原文:https://url.hi-linux.com/UF0HF,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com。

54da2b52ab017894ebc92126dfc6bbe6.gif

最近,我们建立了一个技术交流微信群。目前群里已加入了不少行业内的大神,有兴趣的同学可以加入和我们一起交流技术,在 「奇妙的 Linux 世界」 公众号直接回复 「加群」 邀请你入群。