微服务架构是一个分布式架构,依据业务进行模块的划分,一个分布式系统中往往有很多个服务模块。由于服务模块数量众多,业务的复杂性,如果出现了错误和异常将很难去定位,主要体现在:一个请求可能会流经多个服务模块,各个服务模块的调用是错综复杂的。因此,必须要有微服务中请求链路的跟踪,能够清晰展示请求的调用线路,流经了那些模块,模块的调用顺序,模块中关键执行结果,从而达到请求的步骤清晰可见,便于问题的排查。
那么一个请求链路跟踪需要哪些功能呢?
- 能够清晰展示请求的调用链路,链路的父子层级;
- 某个模块中能够埋点(记录关键代码块执行情况,记录关心的参数信息);
- 分析系统性能,能够展示模块调用与执行耗时,一个请求完整的调用与执行耗时。
设计:
- 每一个请求到达网关时生成一个唯一标识traceId,这个traceId会随着调用链路传递;
- 请求流经服务时生成一个步骤唯一标识spanId,这个spanId会传递到下一个服务中;
- 请求流经服务时记录请求接受以及响应时间,方便算出请求在服务中总耗时;
- 使用拦截器以及threadlocal记录链路信息以及埋点信息;
- 请求在每一个服务响应时组装链路实体写入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();
}
}