微服务架构核心要素-链路监控

Libo.X 1年前
596次浏览 0人关注 复制链接 所属标签: 微服务 调用链跟踪 监控
一、背景

分布式架构在系统稳定性和性能方面发挥着巨大的作用,分布式技术也在不断的发展和演进,从以前的代表着松耦合的SOA架构与ESB的组合,到近几年的微服务。
微服务架构以它的开发、部署速度快,更为松耦合,每个微服务独立部署运行,经过一番验证之后,得到了行业内广大攻城狮的追捧。

微服务架构提高了系统整体稳定性和性能,方便接入CI/CD的同时也会带着诸多问题,其中随着模块的不断拆分,系统模块间的调用越来越复杂,对定位故障带来了很大的困难,整个调用链不透明,犹如被蒙上了一块黑纱,当线上遇到故障时,整个排查过程非常痛苦。这个时候分布式追踪系统应运而生,为我们揭开了这块黑纱,帮助我们更快的定位到系统故障点。

二、分布式追踪系统
分布式系统调用过程

opentracing 协议

opentracing是一套分布式追踪协议,与平台,语言无关,统一接口,方便开发接入不同的分布式追踪系统。

简单理解opentracing
一个完整的opentracing调用链包含 Trace + span + 无限极分类

  • Trace:追踪对象,一个Trace代表了一个服务或者流程在系统中的执行过程,如:test.com,redis,mysql等执行过程。一个Trace由多个span组成
  • span:记录Trace在执行过程中的信息,如:查询的sql,请求的HTTP地址,RPC调用,开始、结束、间隔时间等。
  • 无限极分类:服务与服务之间使用无限极分类的方式,通过HTTP头部或者请求地址传输到最低层,从而把整个调用链串起来。

目前有很多开源的分布式追踪系统支持Opentracing协议,比较成熟的工具是Zipkin,另外Uber推出的为微服务跟踪而生的Jaeger,类似于Zipkin,但在资源占用和客户端语言覆盖以及侵入性方便更有优势,本文主要是介绍Jaeger这款工具的原理及使用。

三、Jaeger介绍
架构图:


jaeger-client:Jaeger 的客户端,实现了 OpenTracing 的 API,支持主流编程语言。客户端直接集成在目标 Application 中,其作用是记录和发送 Span 到 Jaeger Agent。在 Application 中调用 Jaeger Client Library 记录 Span 的过程通常被称为埋点。
jaeger-agent:暂存 Jaeger Client 发来的 Span,并批量向 Jaeger Collector 发送 Span,一般每台机器上都会部署一个 Jaeger Agent。官方的介绍中还强调了 Jaeger Agent 可以将服务发现的功能从 Client 中抽离出来,不过从架构角度讲,如果是部署在 Kubernetes 或者是 Nomad 中,Jaeger Agent 存在的意义并不大。
jaeger-collector:接受 Jaeger Agent 发来的数据,并将其写入存储后端,目前支持采用 Cassandra 和 Elasticsearch 作为存储后端。个人还是比较推荐用 Elasticsearch,既可以和日志服务共用同一个 ES,又可以使用 Kibana 对 Trace 数据进行额外的分析。架构图中的存储后端是 Cassandra,旁边还有一个 Spark,讲的就是可以用 Spark 等其他工具对存储后端中的 Span 进行直接分析。
jaeger-query & jaeger-ui:读取存储后端中的数据,以直观的形式呈现。

采样率

可以支持设置采样率是 Jaeger 的一个亮点,在生产环境中,如果对每个请求都开启 Trace,必然会对系统性能带来一定压力,除此之外,数量庞大的 Span 也会占用大量的存储空间。为了尽量消除分布式追踪采样对系统带来的影响,设置采样率是一个很好的办法。Jaeger 支持四种采样类别,分别是 const、probabilistic、rateLimiting 和 remote。const 意为常量,采样率的可设置的值为 0 和 1,分别表示关闭采样和全部采样。probabilistic 是按照概率采样,取值可在 0 至 1 之间,例如设置为 0.5 的话意为只对 50% 的请求采样。rateLimiting 则是设置每秒的采样次数上限。remote 是遵循远程设置,取值的含义和 probabilistic 相同,都意为采样的概率,只不过设置为 remote 后,Client 会从 Jaeger Agent 中动态获取采样率设置。

安装

下载

cd /data/opentracing/
wget https://github.com/jaegertracing/jaeger/releases/download/v1.7.0/jaeger-1.7.0-linux-amd64.tar.gz
tar -zxvf jaeger-1.7.0-linux-amd64.tar.gz
cd jaeger-1.7.0-linux-amd64

启动

vim jaeger-collector.sh


#!/bin/bash

lockfile="./jaeger_collector_pidfile"

op=$1
[ -z "$op" ] && op=start;
p2=$2

status(){
  [ -e "$lockfile" ] && {
    pid=`cat $lockfile`
    echo "pid=$pid"
    echo `ps -ef| grep ${pid}`
  } || {
    echo "not run."
  }
}

start(){
  [ -e "$lockfile" ] && {
    echo "$lockfile is exsits! start finished!"
    exit 1
  } || {
    echo "starting..."
    `nohup ./jaeger-collector \
          --span-storage.type="elasticsearch" \
          --es.username="xxxxx" \
          --es.password="xxxxx" \
          --es.server-urls="http://192.168.220.211:9200" \
          --log-level=debug > jaeger_collector_log.log 2>&1 & echo $! > ${lockfile}`
    pid=`cat ${lockfile}`
    echo "starting finished! pid=$pid "
    exit 0
  }
}

stop(){
  [ ! -e "$lockfile" ] && {
    echo "$lockfile is not exsits! stop finished! "
    exit 1
  } || {
    echo "stop..."
    pid=`cat ${lockfile}`
    echo "kill pid=$pid"
    kill -9 $pid
    rm -f $lockfile
    echo "stoping finished!"
    exit 0
  }
}

case $op in
status)
status
;;
start)
start
;;
stop)
stop
;;
*)
esac

exit 0;


# 启动 collector
sh jaeger-collector.sh start
vim jaeger-agent.sh

#!/bin/bash
# email: litaiqing@analysys.com.cn

lockfile="./jaeger_agent_pidfile"

op=$1
[ -z "$op" ] && op=start;
p2=$2

status(){
  [ -e "$lockfile" ] && {
    pid=`cat $lockfile`
    echo "pid=$pid"
    echo `ps -ef| grep ${pid}`
  } || {
    echo "not run."
  }
}

start(){
  [ -e "$lockfile" ] && {
    echo "$lockfile is exsits! start finished!"
    exit 1
  } || {
    echo "starting..."
    `nohup ./jaeger-agent --collector.host-port=192.168.220.227:14267 --discovery.min-peers=1 --log-level=debug > jaeger_agent_log.log 2>&1 & echo $! > ${lockfile}`
    pid=`cat ${lockfile}`
    echo "starting finished! pid=$pid "
    exit 0
  }
}

stop(){
  [ ! -e "$lockfile" ] && {
    echo "$lockfile is not exsits! stop finished! "
    exit 1
  } || {
    echo "stop..."
    pid=`cat ${lockfile}`
    echo "kill pid=$pid"
    kill -9 $pid
    rm -f $lockfile
    echo "stoping finished!"
    exit 0
  }
}

case $op in
status)
status
;;
start)
start
;;
stop)
stop
;;
*)
esac

exit 0;


// 启动 agent
sh jaeger-agent.sh start
vim jaeger-query.sh
#!/bin/bash

lockfile="./jaeger_query_pidfile"

op=$1
[ -z "$op" ] && op=start;
p2=$2

status(){
  [ -e "$lockfile" ] && {
    pid=`cat $lockfile`
    echo "pid=$pid"
    echo `ps -ef| grep ${pid}`
  } || {
    echo "not run."
  }
}

start(){
  [ -e "$lockfile" ] && {
    echo "$lockfile is exsits! start finished!"
    exit 1
  } || {
    echo "starting..."
    nohup ./jaeger-query --span-storage.type="elasticsearch" --es.server-urls="http://192.168.220.211:9200" --es.username="admin" --es.password="admin" >jaeger_query_log.log 2>&1 & echo $! > ${lockfile}
    pid=`cat ${lockfile}`
    echo "starting finished! pid=$pid "
    exit 0
  }
}

stop(){
  [ ! -e "$lockfile" ] && {
    echo "$lockfile is not exsits! stop finished! "
    exit 1
  } || {
    echo "stop..."
    pid=`cat ${lockfile}`
    echo "kill pid=$pid"
    kill -9 $pid
    rm -f $lockfile
    echo "stoping finished!"
    exit 0
  }
}

case $op in
status)
status
;;
start)
start
;;
stop)
stop
;;
*)
esac

exit 0;


// 启动 query
sh jaeger-query.sh start
SpringBoot 集成

Pom 引入依赖

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

<dependency>
    <groupId>io.opentracing.contrib</groupId>
    <artifactId>opentracing-spring-jaeger-starter</artifactId>
    <version>0.2.1</version>
</dependency>

<dependency>
    <groupId>io.opentracing.contrib</groupId>
    <artifactId>opentracing-web-servlet-filter</artifactId>
    <version>0.2.0</version>
</dependency>

编写配置类

package cn.analysys.common.config;

import java.util.Collection;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.jaegertracing.spi.Reporter;
import io.opentracing.contrib.java.spring.jaeger.starter.ReporterAppender;

@Configuration
public class JaegerConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public ReporterAppender reporterAppender() {
        return new ReporterAppender() {
            @Override
            public void append(Collection<Reporter> reporters) {

            }
        };
    }

}
package cn.analysys.common.config;

import java.util.regex.Pattern;

import javax.servlet.ServletContextEvent;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

import cn.analysys.common.properties.JaegerProperties;

@EnableConfigurationProperties({ JaegerProperties.class })
public class JaegerSerlvetContextListener implements javax.servlet.ServletContextListener {

    @Autowired
    private JaegerProperties jaegerProperties;

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        servletContextEvent.getServletContext()
                .setAttribute(io.opentracing.contrib.web.servlet.filter.TracingFilter.SKIP_PATTERN,
                    Pattern.compile(jaegerProperties.getFilterPaths()));
    }

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
    }

}
package cn.analysys.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OwlTrace {

}
package cn.analysys.common.config;

import java.util.LinkedHashMap;
import java.util.Map;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.tag.Tags;

@Configuration
public class OwlTraceAutoConfiguration {
    static final String TAG_COMPONENT = "java";

    @Autowired
    @Lazy
    Tracer tracer;

    @Bean
    public TracingAspect pxTracingAspect() {
        return new TracingAspect();
    }

    @Aspect
    class TracingAspect {
        @Around("@annotation(cn.analysys.common.annotation.OwlTrace)")
        public Object pxTraceProcess(ProceedingJoinPoint pjp) throws Throwable {
            Span span = null;
            if (tracer != null) {
                final String cls = pjp.getTarget().getClass().getName();
                final String mName = pjp.getSignature().getName();
                span
                        = tracer.buildSpan(cls + "." + mName).withTag(Tags.COMPONENT.getKey(), TAG_COMPONENT).withTag("class", cls)
                                .withTag("method", mName).startActive(false).span();
            }
            try {
                return pjp.proceed();
            } catch (Throwable t) {
                Map<String, Object> exceptionLogs
                        = new LinkedHashMap<>(
                                2);
                exceptionLogs.put("event", Tags.ERROR.getKey());
                exceptionLogs.put("error.object", t);
                span.log(exceptionLogs);
                Tags.ERROR.set(span, true);
                throw t;
            } finally {
                if (tracer != null && span != null) {
                    span.finish();
                }
            }
        }
    }
}

application.yml

opentracing:
  jaeger:
    log-spans: true
    udp-sender:
      host: 192.168.60.253
      port: 6831
spring:
  aop:
    auto: true
    proxy-target-class: true

编写业务代码

package cn.analysys.common.guide.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import cn.analysys.common.annotation.OwlTrace;

@RestController
public class HelloController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/error/{num}")
    @OwlTrace
    public String error(@PathVariable(value = "num", required = true) Integer num) {
        ResponseEntity<String> response = restTemplate.getForEntity("http://192.168.62.35:9233/hello", String.class);
        if (num < 10) {
            throw new RuntimeException("hello error!");
        }
        return "Error + " + response.getBody();
    }

    @RequestMapping("/hello")
    @OwlTrace
    public String hello() {
        return "Hello from Spring Boot!";
    }

    @RequestMapping("/chaining")
    @OwlTrace
    public String chaining() {
        ResponseEntity<String> response = restTemplate.getForEntity("http://192.168.62.35:9233/hello", String.class);
        return "Chaining + " + response.getBody();
    }

}

启动应用,访问各个业务连接后,查看Jaeger UI。

3条回答
随心而动 1年前

很棒~

有用0 评论0
liweitao 1年前

写的很细致,真棒

有用0 评论0
用户1551180780 1年前

写的真棒,太厉害了

有用0 评论0