随着SOA,微服务架构及PaaS,Devops等技术的兴起,线上问题的追踪和排查变得更加困难。对线上业务的可观测性得到了越来越多企业的重视,由此涌现出了许多优秀的链路追踪及服务监控中间件。比较流行的有Spring Cloud全家桶自带的Zipkin,点评的CAT, 华为的skywalking,Uber的Jaeger, naver的Pinpoint。
一个典型的应用,通常有三种类型的数据需要被监控系统记录:Metric, logs and traces。让我们先了解下它们都是什么。
Metrics
提供进行运行时的指标信息。比如CPU使用率,内存使用情况,GC情况,网站流量等。
Logging
可以监控程序进程中的日志,比如集成Log4j记录的日志,或者程序运行中发生的事件或通知。
Tracing
也叫做分布式追踪,包含请求中每个子操作的开始和结束时间,传递的参数,请求间的调用链路,请求在各个链路上的耗时等信息。Tracing可以包含消息发送和接收,数据库访问,负载均衡等各种信息,让我们可以深入了解请求的执行情况。Tracing为我们提供了获取请求的时间主要消耗在哪里,请求的参数都是什么,如果发生了异常,那么异常是在哪个环节产生的等能力。
image.png

2、opentelemetry简介

image.png
opentelemetry是一款数据收集中间件。我们可以使用它来生成,收集和导出监测数据(Metrics,Logs and traces),这些数据可供支持OpenTelemetry的中间件存储,查询和显示,用以实现数据观测,性能分析,系统监控,服务告警等能力。
opentelemetry项目开始于2019年,旨在提供基于云环境的可观测性软件的标准化方案,提供与三方无关的监控服务体系。项目迄今为止已获得了Zipkin, Jaeger, skywalking, Prometheus等众多知名中间件的支持。

3、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监控系统服务端
    image.png

    4、使用opentelemetry 集成Zipkin

    示例中使用到的组件的版本:
    java: 1.8
    spring-cloud: 2020.0.2
    spring-boot: 2.4.5
    opentelemetry: 1.1.0
    grpc: 1.36.1

    4.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>

    4.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. }

    4.3、在cloud-user-service中,使用opentelemetry

    当我们完成了配置后,就可以在spring boot项目中,通过autowired来使用opentelemetry。
    接下来我们定制一个WebFilter来拦截所有的Http请求,并在Filter类中进行埋点。 ```go @Component public class TracingFilter implements Filter { private final AppConfig appConfig; private final OpenTelemetry openTelemetry;

    @Autowired public TracingFilter(AppConfig appConfig, OpenTelemetry openTelemetry) {

    1. this.appConfig = appConfig;
    2. this.openTelemetry = openTelemetry;

    }

    @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

    1. HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
    2. Span span = getServerSpan(openTelemetry.getTracer(appConfig.getApplicationName()), httpServletRequest);
    3. try (Scope scope = span.makeCurrent()) {
    4. filterChain.doFilter(servletRequest, servletResponse);
    5. } catch (Exception ex) {
    6. span.setStatus(StatusCode.ERROR, "HTTP Code: " + ((HttpServletResponse)servletResponse).getStatus());
    7. span.recordException(ex);
    8. throw ex;
    9. } finally {
    10. span.end();
    11. }

    }

    private Span getServerSpan(Tracer tracer, HttpServletRequest httpServletRequest) {

    1. TextMapPropagator textMapPropagator = openTelemetry.getPropagators().getTextMapPropagator();
    2. Context context = textMapPropagator.extract(Context.current(), httpServletRequest, new TextMapGetter<HttpServletRequest>() {
    3. @Override
    4. public Iterable<String> keys(HttpServletRequest request) {
    5. List<String> headers = new ArrayList();
    6. for (Enumeration names = request.getHeaderNames(); names.hasMoreElements();) {
    7. String name = (String)names.nextElement();
    8. headers.add(name);
    9. }
    10. return headers;
    11. }
    12. @Override
    13. public String get(HttpServletRequest request, String s) {
    14. return request.getHeader(s);
    15. }
    16. });
  1. return tracer.spanBuilder(httpServletRequest.getRequestURI()).setParent(context).setSpanKind(SpanKind.SERVER).setAttribute(SemanticAttributes.HTTP_METHOD, httpServletRequest.getMethod()).startSpan();
  2. }

}

  1. 在示例代码中,我们实现了一个匿名类来从HttpServletRequest中解析tracing上下文信息。<br />在创建Span的同时,我们在Span中写入了Http请求的一些关键属性,并且为所有的异常做了跟踪记录。
  2. <a name="vKpw9"></a>
  3. ## 4.4、编写服务代码
  4. 接下来我们通过一段简单的代码来模拟查询用户以及抛出异常
  5. ```go
  6. @GetMapping("/{id}")
  7. public ResponseEntity<User> get(@PathVariable("id") Long id) {
  8. if (0 >= id) {
  9. throw new IllegalArgumentException("Illegal argument value");
  10. }
  11. return ResponseEntity.ok(userService.get(id));
  12. }

4.5、配置gateway-service

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

4.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. }
  18. 接下来在GlobalFilter中,记录路由到微服务的http请求
  19. @Override
  20. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain gatewayFilterChain) {
  21. Span span = getClientSpan(openTelemetry.getTracer(appConfig.getApplicationName()), exchange);
  22. Scope scope = span.makeCurrent();
  23. inject(exchange);
  24. return gatewayFilterChain.filter(exchange)
  25. .then(Mono.fromRunnable(() -> {
  26. scope.close();
  27. span.end();
  28. })
  29. );
  30. }
  31. private void inject(ServerWebExchange serverWebExchange) {
  32. HttpHeaders httpHeaders = new HttpHeaders();
  33. TextMapPropagator textMapPropagator = openTelemetry.getPropagators().getTextMapPropagator();
  34. textMapPropagator.inject(Context.current(), httpHeaders, HttpHeaders::add);
  35. ServerHttpRequest request = serverWebExchange.getRequest().mutate()
  36. .headers(headers -> headers.addAll(httpHeaders))
  37. .build();
  38. serverWebExchange.mutate().request(request).build();
  39. }
  40. private Span getClientSpan(Tracer tracer, ServerWebExchange serverWebExchange) {
  41. ServerHttpRequest serverHttpRequest = serverWebExchange.getRequest();
  42. URI routeUri = serverWebExchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
  43. return tracer.spanBuilder(routeUri.getPath()).setSpanKind(SpanKind.CLIENT).setAttribute(SemanticAttributes.HTTP_METHOD, serverHttpRequest.getMethod().name()).startSpan();
  44. }

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

5、运行服务

现在,让我们访问网关http://localhost:8080/user/0 来观察Zipkin对于服务访问和异常的记录情况。
image.png
可以看到在Tracing方面,Zikin整体表现还不错,有异常的链路也使用红色做了标记。Zipkin没有打印出异常的堆栈信息,我们需要为此做额外的处理才行。

6、使用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. }

7、再次运行服务

我们再次运行服务并访问网关http://localhost:8080/user/0 来观察Jaeger对于服务访问和异常的记录情况。
首先看主界面,Jaeger直接标记了请求中包含异常。
再看下访问的详情,Jaeger记录并显示了异常的堆栈信息。这对我们分析线上异常非常有帮助。
image.png
Jaeger提供的DAG图
image.png
对比Zipkin,Jaeger提供了更加丰富的功能和更美观的可视化界面。

8、总结

本文介绍了使用opentelemetry 来搭建监控系统,以及如何集成到Zipkin和Jaeger。
利用opentelemetry的标准化能力,我们可以方便地记录更加详细的链路监控信息。
opentelemetry自推出以来,得到了越来越多厂商的关注和支持。对于分布式监控系统这个新生事物,opentelemetry是否能成为最终的事实标准,让我们拭目以待。

References

opentelemetry官网
Metrics, tracing, and logging
5 Reasons why OpenTelemetry will boost Observability and Monitoring
Exporting Open Telemetry Data to Jaeger