Skip to content

微服务架构是一个分布式架构,依据业务进行模块的划分,一个分布式系统中往往有很多个服务模块。由于服务模块数量众多,业务的复杂性,如果出现了错误和异常将很难去定位,主要体现在:一个请求可能会流经多个服务模块,各个服务模块的调用是错综复杂的。因此,必须要有微服务中请求链路的跟踪,能够清晰展示请求的调用线路,流经了那些模块,模块的调用顺序,模块中关键执行结果,从而达到请求的步骤清晰可见,便于问题的排查。

那么一个请求链路跟踪需要哪些功能呢?
  1. 能够清晰展示请求的调用链路,链路的父子层级;
  2. 某个模块中能够埋点(记录关键代码块执行情况,记录关心的参数信息);
  3. 分析系统性能,能够展示模块调用与执行耗时,一个请求完整的调用与执行耗时。
设计:
  1. 每一个请求到达网关时生成一个唯一标识traceId,这个traceId会随着调用链路传递;
  2. 请求流经服务时生成一个步骤唯一标识spanId,这个spanId会传递到下一个服务中;
  3. 请求流经服务时记录请求接受以及响应时间,方便算出请求在服务中总耗时;
  4. 使用拦截器以及threadlocal记录链路信息以及埋点信息;
  5. 请求在每一个服务响应时组装链路实体写入mq,消费端消费消息落入mongodb方便以后查询。
流程图
graph TB
A[网关] -->|生成traceId以及spanId放入请求头中|B[服务]
B -->|取出请求头中traceId以及父级spanId 生成自身spanId放入请求头中|C[服务]
C -->|traceId 父级spanId spanId|E[服务]
C -->|traceId 父级spanId spanId|F[服务]
E -->G[...]
F -->H[...]
关键代码
  • 网关过滤器一
import cn.imadc.sugar.core.context.MyRequestContext;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.Cookie;
import java.util.UUID;

@Component
public class TraceFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 一般是第一个执行的过滤器
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 3;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        // 仿照zuul中的RequestContext写的一个会话信息存储类
        // 便于服务中记录调用链路以及埋点数据
        MyRequestContext currentContext = MyRequestContext.getCurrentContext();

        // 记录请求到达时间
        currentContext.setAcccptTimestamp();
         
        // 生成一个请求唯一标识
        String traceId = UUID.randomUUID().toString();
        
        // 生成一个步骤唯一标识
        String spanId = UUID.randomUUID().toString();

        // 记录traceId以及步骤唯一标识
        currentContext.setTraceId(traceId);
        currentContext.setSpanId(spanId);

        // 使用zuul中的RequestContext将traceId以及spanId放入请求头中向后面的服务传递
        RequestContext context = RequestContext.getCurrentContext();
        context.addZuulRequestHeader("traceId", traceId);
        context.addZuulRequestHeader("spanId", spanId);

        // 将traceId放入cookie中,便于前端查看请求标识
        Cookie traceCookie = new Cookie("traceId", traceId);
        traceCookie.setPath("/");
        traceCookie.setHttpOnly(true);
        context.getResponse().addCookie(traceCookie);

        return null;
    }
}
  • 网关过滤器二
import cn.imadc.sugar.core.context.MyRequestContext;
import com.alibaba.fastjson.JSONObject;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

@Component
public class PostFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        try {

            MyRequestContext context = MyRequestContext.getCurrentContext();

            // 构建trace体,记录traceId、父级服务spanId、服务spanId
            JSONObject traceObject = new JSONObject();
            traceObject.put("traceId", context.getTraceId());
            traceObject.put("parentSpanId", context.getParentSpanId());
            traceObject.put("spanId", context.getSpanId());

            // 取出请求到达网关的时间
            long acceptTimestamp = context.getAcceptTimestamp();
            traceObject.put("acceptTimestamp", acceptTimestamp);

            // 记录请求响应时间
            long responseTimestamp = System.currentTimeMillis();
            traceObject.put("responseTimestamp", responseTimestamp);

            // 计算请求耗时
            long costTimestamp = responseTimestamp - acceptTimestamp;
            traceObject.put("costTimestamp", costTimestamp);
            
            // TODO 将trace体写入到MQ中

        } catch (Exception e) {

        }
        return null;
    }
}
  • feign请求头配置

import cn.imadc.sugar.core.constant.ApplicationInterceptorConstant;
import cn.imadc.sugar.core.context.MyRequestContext;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;

@Order(ApplicationInterceptorConstant.FeignClientConfigurationOrder)
@Configuration
@EnableFeignClients(basePackages = {"cn.imadc.sugar"})
public class FeignClientConfiguration implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        
        MyRequestContext currentContext = MyRequestContext.getCurrentContext();
        String traceId = currentContext.getTraceId();
        String spanId = currentContext.getSpanId();

        requestTemplate.header("traceId", traceId);
        requestTemplate.header("spanId", spanId);

    }
}
  • 服务内拦截器
import cn.imadc.sugar.core.context.MyRequestContext;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Slf4j
public class ApplicationRequestInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        MyRequestContext currentContext = MyRequestContext.getCurrentContext();
        // 记录请求到达服务时间
        currentContext.setAcccptTimestamp();

        currentContext.setRequest(request);
        currentContext.setResponse(response);

        // 从请求中取出traceId并记录
        String traceId = currentContext.getTraceId();
        currentContext.setTraceId(traceId);

        // 从请求中取出父级spanId并记录
        String parentSpanId = currentContext.getHeader("spanId");
        currentContext.setParentSpanId(parentSpanId);

        // 生成服务自身spanId并记录
        String spanId = UUID.randomUUID().toString();
        currentContext.setSpanId(spanId);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        MyRequestContext currentContext = MyRequestContext.getCurrentContext();

        // 服务响应时构建trace体
        JSONObject traceObject = new JSONObject();
        // 记录traceId
        traceObject.put("traceId", currentContext.getTraceId());
        
        // 记录父级spanId
        traceObject.put("parentSpanId", currentContext.getParentSpanId());
        
        // 记录服务spanId
        traceObject.put("spanId", currentContext.getSpanId());
        
        // 记录请求到达服务时间
        long acceptTimestamp = currentContext.getAcceptTimestamp();
        traceObject.put("acceptTimestamp", acceptTimestamp);

        // 记录请求响应时间
        long responseTimestamp = System.currentTimeMillis();
        traceObject.put("responseTimestamp", responseTimestamp);
        
        // 计算服务内请求耗时
        long costTimestamp = responseTimestamp - acceptTimestamp;
        traceObject.put("costTimestamp", costTimestamp);

        // 记录埋点信息
        traceObject.put("tracePoint", currentContext.getTracePoint());

        log.error("traceObject:" + trace.toJSONString() + "\n");
        
        // TODO 将trace体写入到MQ中

        MyRequestContext.getCurrentContext().unset();
    }
}
  • tracePoint
import lombok.Getter;
import lombok.Setter;

/**
 * 埋点信息
 */
@Getter
@Setter
public class TracePointWrapper {

    private String traceId;
    private String spanIdId;
    private String parentSpanId;

    private String traceKey;
    private String traceVal;

    public TracePointWrapper(String traceKey, String traceVal) {
        this.traceKey = traceKey;
        this.traceVal = traceVal;
    }

}
  • tracePoint用法

// 比如有一个登陆方法
public void login(String loginName, String password) {
    MyRequestContext myRequestContext = MyRequestContext.getCurrentContext();

    // 埋点
    myRequestContext.setTracePoint("loginName", loginName);
    myRequestContext.setTracePoint("password", password);

    // 业务逻辑:执行登录
    

}
  • 会话信息存储类
import cn.imadc.sugar.core.wrapper.TracePointWrapper;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 请求上下文信息
 */
public class MyRequestContext extends ConcurrentHashMap {
    protected static Class<? extends MyRequestContext> contextClass = MyRequestContext.class;
    protected static final ThreadLocal<? extends MyRequestContext> threadLocal = ThreadLocal.withInitial(() -> {
        try {
            return (MyRequestContext) MyRequestContext.contextClass.newInstance();
        } catch (Throwable var2) {
            throw new RuntimeException(var2);
        }
    });

    // 不可实例化
    private MyRequestContext() {
    }

    /**
     * 获取上下文信息
     *
     * @return
     */
    public static MyRequestContext getCurrentContext() {
        return threadLocal.get();
    }

    /**
     * 添加至上下文
     *
     * @param key
     * @param value
     */
    public void set(String key, Object value) {
        if (value != null) {
            this.put(key, value);
        } else {
            this.remove(key);
        }
    }

    /**
     * 获取当前request
     *
     * @return
     */
    public HttpServletRequest getRequest() {
        return (HttpServletRequest) this.get("request");
    }

    /**
     * 设置当前request
     */
    public void setRequest(HttpServletRequest request) {
        this.put("request", request);
    }

    /**
     * 获取当前response
     *
     * @return
     */
    public HttpServletResponse getResponse() {
        return (HttpServletResponse) this.get("response");
    }

    /**
     * 设置当前response
     *
     * @param response
     */
    public void setResponse(HttpServletResponse response) {
        this.set("response", response);
    }

    /**
     * 获取当前会话cookie信息
     *
     * @param cookieName cookie名称
     * @return
     */
    public String getCookie(String cookieName) {
        HttpServletRequest httpServletRequest = getRequest();
        if (null == httpServletRequest) return null;
        Cookie[] cookies = httpServletRequest.getCookies();
        if (null == cookies) return null;
        for (Cookie cookie : cookies) {
            if (StringUtils.equals(cookie.getName(), cookieName))
                return cookie.getValue();
        }
        return null;
    }

    /**
     * 获取当前会话cookie信息
     *
     * @param cookieName  cookie名称
     * @param cookieValue cookie值
     * @return
     */
    public void setCookie(String cookieName, String cookieValue) {
        HttpServletResponse httpServletResponse = getResponse();
        if (null == httpServletResponse) return;

        httpServletResponse.addCookie(new Cookie(cookieName, cookieValue));
    }

    /**
     * 获取当前会话header信息
     *
     * @param headerName header名称
     * @return
     */
    public String getHeader(String headerName) {
        HttpServletRequest httpServletRequest = getRequest();
        if (null == httpServletRequest) return null;
        return httpServletRequest.getHeader(headerName);
    }

    /**
     * 设置请求跟踪标识
     *
     * @param traceId
     */
    public void setTraceId(String traceId) {
        if (StringUtils.isBlank(traceId)) return;
        this.put("traceId", traceId);
    }

    /**
     * 获取请求跟踪标识
     *
     * @return
     */
    public String getTraceId() {
        String traceId;

        traceId = getHeader("traceId");
        if (null != traceId) return traceId;

        if (null != this.get("traceId")) return this.get("traceId").toString();
        traceId = getCookie("traceId");
        if (null != traceId) return traceId;

        return null;
    }

    /**
     * 设置请求跨度标识
     *
     * @param spanId
     */
    public void setSpanId(String spanId) {
        if (StringUtils.isBlank(spanId)) return;
        this.put("spanId", spanId);
    }

    /**
     * 获取请求跨度标识
     *
     * @return
     */
    public String getSpanId() {
        return null != this.get("spanId") ? this.get("spanId").toString() : null;
    }

    /**
     * 设置请求父级跨度标识
     *
     * @param parentSpanId
     */
    public void setParentSpanId(String parentSpanId) {
        if (StringUtils.isBlank(parentSpanId)) return;
        this.put("parentSpanId", parentSpanId);
    }

    /**
     * 获取请求父级跨度标识
     *
     * @return
     */
    public String getParentSpanId() {
        return null != this.get("parentSpanId") ? this.get("parentSpanId").toString() : null;
    }

    /**
     * 设置会话建立时间
     */
    public void setAcccptTimestamp() {
        this.put("acceptTimestamp", System.currentTimeMillis());
    }

    /**
     * 获取会话建立时间
     *
     * @return
     */
    public long getAcceptTimestamp() {
        return null != this.get("acceptTimestamp") ? Long.parseLong(this.get("acceptTimestamp").toString()) : null;
    }

    /**
     * 获取所有的埋点
     *
     * @return
     */
    public synchronized List<TracePointWrapper> getTracePoint() {
        if (this.contains("tracePointWrapperList")) return (List<TracePointWrapper>) this.get("tracePointWrapperList");
        List<TracePointWrapper> tracePointWrapperList = new ArrayList<>();
        this.put("tracePointWrapperList", tracePointWrapperList);
        return tracePointWrapperList;
    }


    /**
     * 设置请求埋点
     *
     * @param traceKey 埋点键
     * @param traceVal 埋点值
     */
    public void setTracePoint(String traceKey, String traceVal) {
        TracePointWrapper tracePointWrapper = new TracePointWrapper(traceKey, traceVal);
        tracePointWrapper.setTraceId(getTraceId());
        tracePointWrapper.setParentSpanId(getParentSpanId());
        tracePointWrapper.setSpanIdId(getSpanId());

        List<TracePointWrapper> tracePointWrapperList = getTracePoint();
        tracePointWrapperList.add(tracePointWrapper);
    }

    /**
     * 清空当前会话上下文信息
     */
    public void unset() {
        threadLocal.remove();
    }
}

总访问量
总访问人数 人次