目录
一、简介
这篇文章总结feign链路跟踪器的实现
二、思路
上篇文章中总结了mvc的链路跟踪器,我们可以知道要实现链路跟踪器需要在前后拦截请求,那么这里我们应该怎么处理呢?既然使用的是spring,那么很容易想到使用AOP来进行拦截。
还有个细节需要注意,因为feign可能会调用hystrix,这个时候hystrixCommand会新产生一个线程,这时候需要通过http将链路信息传递过去。参看《springCloud微服务系列——OAuth2+JWT模式下的feign+hystrix处理》
@Slf4j
public class LuminaryRequestInterceptor implements RequestInterceptor {
public static String TOKEN_HEADER = "authorization";
/* (non-Javadoc)
* <p>Title: apply</p>
* <p>Description: </p>
* @param arg0
* @see feign.RequestInterceptor#apply(feign.RequestTemplate)
*/
@Override
public void apply(RequestTemplate template) {
HttpServletRequest request = getHttpServletRequest();
if(request == null) {
log.warn("request is null in feign request interceptor");
return;
}
Map<String, String> headerMap = getHeaders(request);
if(headerMap.get(TOKEN_HEADER) != null) {
template.header(TOKEN_HEADER, headerMap.get(TOKEN_HEADER));
}
if(request.getAttribute(TraceInfo.TRACE_ID_KEY) != null) {
template.header(TraceInfo.TRACE_ID_KEY, (String) request.getAttribute(TraceInfo.TRACE_ID_KEY));
}
if(request.getAttribute(TraceInfo.RPC_ID_KEY) != null) {
template.header(TraceInfo.RPC_ID_KEY, (String) request.getAttribute(TraceInfo.RPC_ID_KEY));
}
}
private HttpServletRequest getHttpServletRequest() {
try {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} catch (Exception e) {
return null;
}
}
private Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}
}
三、获取riboon负载均衡结果
feign链路跟踪的时候还有个获取负载均衡结果的问题,我们需要在跟踪的时候拿到具体调用的是哪个服务。feign的负载均衡使用的是riboon,在riboon中通过eureka拿到服务列表,通过ping的方式筛选出可用的服务。
源码分析
其中紫色表示类,绿色表示方法,蓝色表示内部方法
我们可以看到最终是注册成了一个观察者,在特定的事件中通过loadBalancerContext的getServerFromLoadBalancer方法,loadBalancerContext中又通过ILoadBalancer调用chooseServer方法,最终通过IRule调用choose获得负载均衡结果。
LoadBalancerFeignClient
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
AbstractLoadBalancerAwareClient
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig);
LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder()
.withLoadBalancerContext(this)
.withRetryHandler(handler)
.withLoadBalancerURI(request.getUri())
.build();
try {
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
LoadBalancerCommand
private Observable<Server> selectServer() {
return Observable.create(new OnSubscribe<Server>() {
@Override
public void call(Subscriber<? super Server> next) {
try {
Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);
next.onNext(server);
next.onCompleted();
} catch (Exception e) {
next.onError(e);
}
}
});
}
Observable
public static <T> Observable<T> create(OnSubscribe<T> f) {
return new Observable<T>(RxJavaHooks.onCreate(f));
}
RxJavaHooks
public static <T> Observable.OnSubscribe<T> onCreate(Observable.OnSubscribe<T> onSubscribe) {
Func1<Observable.OnSubscribe, Observable.OnSubscribe> f = onObservableCreate;
if (f != null) {
return f.call(onSubscribe);
}
return onSubscribe;
}
LoadBalancerContext
public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
String host = null;
int port = -1;
if (original != null) {
host = original.getHost();
}
if (original != null) {
Pair<String, Integer> schemeAndPort = deriveSchemeAndPortFromPartialUri(original);
port = schemeAndPort.second();
}
// Various Supported Cases
// The loadbalancer to use and the instances it has is based on how it was registered
// In each of these cases, the client might come in using Full Url or Partial URL
ILoadBalancer lb = getLoadBalancer();
if (host == null) {
// Partial URI or no URI Case
// well we have to just get the right instances from lb - or we fall back
if (lb != null){
Server svc = lb.chooseServer(loadBalancerKey);
if (svc == null){
throw new ClientException(ClientException.ErrorType.GENERAL,
"Load balancer does not have available server for client: "
+ clientName);
}
host = svc.getHost();
if (host == null){
throw new ClientException(ClientException.ErrorType.GENERAL,
"Invalid Server for :" + svc);
}
logger.debug("{} using LB returned Server: {} for request {}", new Object[]{clientName, svc, original});
return svc;
} else {
// No Full URL - and we dont have a LoadBalancer registered to
// obtain a server
// if we have a vipAddress that came with the registration, we
// can use that else we
// bail out
if (vipAddresses != null && vipAddresses.contains(",")) {
throw new ClientException(
ClientException.ErrorType.GENERAL,
"Method is invoked for client " + clientName + " with partial URI of ("
+ original
+ ") with no load balancer configured."
+ " Also, there are multiple vipAddresses and hence no vip address can be chosen"
+ " to complete this partial uri");
} else if (vipAddresses != null) {
try {
Pair<String,Integer> hostAndPort = deriveHostAndPortFromVipAddress(vipAddresses);
host = hostAndPort.first();
port = hostAndPort.second();
} catch (URISyntaxException e) {
throw new ClientException(
ClientException.ErrorType.GENERAL,
"Method is invoked for client " + clientName + " with partial URI of ("
+ original
+ ") with no load balancer configured. "
+ " Also, the configured/registered vipAddress is unparseable (to determine host and port)");
}
} else {
throw new ClientException(
ClientException.ErrorType.GENERAL,
this.clientName
+ " has no LoadBalancer registered and passed in a partial URL request (with no host:port)."
+ " Also has no vipAddress registered");
}
}
} else {
// Full URL Case
// This could either be a vipAddress or a hostAndPort or a real DNS
// if vipAddress or hostAndPort, we just have to consult the loadbalancer
// but if it does not return a server, we should just proceed anyways
// and assume its a DNS
// For restClients registered using a vipAddress AND executing a request
// by passing in the full URL (including host and port), we should only
// consult lb IFF the URL passed is registered as vipAddress in Discovery
boolean shouldInterpretAsVip = false;
if (lb != null) {
shouldInterpretAsVip = isVipRecognized(original.getAuthority());
}
if (shouldInterpretAsVip) {
Server svc = lb.chooseServer(loadBalancerKey);
if (svc != null){
host = svc.getHost();
if (host == null){
throw new ClientException(ClientException.ErrorType.GENERAL,
"Invalid Server for :" + svc);
}
logger.debug("using LB returned Server: {} for request: {}", svc, original);
return svc;
} else {
// just fall back as real DNS
logger.debug("{}:{} assumed to be a valid VIP address or exists in the DNS", host, port);
}
} else {
// consult LB to obtain vipAddress backed instance given full URL
//Full URL execute request - where url!=vipAddress
logger.debug("Using full URL passed in by caller (not using load balancer): {}", original);
}
}
// end of creating final URL
if (host == null){
throw new ClientException(ClientException.ErrorType.GENERAL,"Request contains no HOST to talk to");
}
// just verify that at this point we have a full URL
return new Server(host, port);
}
BaseLoadBalancer
public String choose(Object key) {
if (rule == null) {
return null;
} else {
try {
Server svr = rule.choose(key);
return ((svr == null) ? null : svr.getId());
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server", name, e);
return null;
}
}
}
扩展点
从源码来看,我们如果能获得LoadBalancerContext或者获得ILoaderBalancer都可以获得负载均衡的结果。
我们通过官方文档,发现可以自定义一些组件
思路来了,我们可以自定义ILoadBalancer,但是这里其实有个坑,自定义的ILoadBalancer获得的server为null,我们再往上走一步,自定义IRule就可以了
最后我们需要实现监听者模式,因为我们并不知道负载均衡结果获取的时机,而我们要把该结果保存到链路信息中
四、示例代码
@Slf4j
public class LuminaryRibbonRule extends ZoneAvoidanceRule implements ServerPublisher {
public static Queue<ServerListener> serverListeners;
public static void regist(ServerListener serverListener) {
if(serverListeners == null)
synchronized (LuminaryRibbonRule.class) {
if(serverListeners == null) {
serverListeners = new ConcurrentLinkedQueue<ServerListener>();
}
}
serverListeners.add(serverListener);
}
/* (non-Javadoc)
* <p>Title: notifyAll</p>
* <p>Description: </p>
* @param serverEvent
* @see com.luminary.component.ribbon.publisher.ServerPublisher#notifyAll(com.luminary.component.ribbon.event.ServerEvent)
*/
@Override
public void notifyAll(ServerEvent serverEvent) {
for(ServerListener serverListener : serverListeners) {
serverListener.chooseServer(serverEvent);
}
}
@Override
public Server choose(Object key) {
Server server = super.choose(key);
if(server != null) {
log.info("select server:"+server.getHostPort());
}
ServerEvent serverEvent = new ServerEvent();
serverEvent.setServer(server);
notifyAll(serverEvent);
return server;
}
}
public interface ServerPublisher {
void notifyAll(ServerEvent serverEvent);
}
public interface ServerListener {
/**
*
* <p>Title: chooseServer</p>
* <p>Description: 选择服务事件监听逻辑</p>
* @param serverEvent
*/
void chooseServer(ServerEvent serverEvent);
}
@Data
public class ServerEvent {
private Server server;
}
@Slf4j
@Aspect
@Component
public class FeignTracker extends GenericTracker implements Tracker<TraceHolder> {
@Value("${spring.profiles.active:default}")
private String profile;
@Autowired
private TraceClient traceClient;
/**
* <p>Title: </p>
* <p>Description: </p>
* @param traceClient
*/
public FeignTracker(TraceClient traceClient) {
super(traceClient);
this.traceClient = traceClient;
}
@Around(value = "@annotation(trace)")
public Object proceed(ProceedingJoinPoint joinPoint, Trace trace) throws Throwable {
if(!trace.value().getName().equals(this.getClass().getName())) {
return joinPoint.proceed();
}
FeignTraceHolder traceHolder = new FeignTraceHolder();
Object result = null;
try {
log.info("feign tracker");
Gson gson = new Gson();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
String methodName = method.getName();
FeignClient feignClient = AnnotationUtils.findAnnotation(method.getDeclaringClass(), FeignClient.class);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
PutMapping putMapping = method.getAnnotation(PutMapping.class);
DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String serverHost = "";
if(getMapping != null) {
serverHost = StringUtils.join(getMapping.value(), "/");
}
else if(postMapping != null) {
serverHost = StringUtils.join(postMapping.value(), "/");
}
else if(putMapping != null) {
serverHost = StringUtils.join(putMapping.value(), "/");
}
else if(deleteMapping != null) {
serverHost = StringUtils.join(deleteMapping.value(), "/");
}
else if(requestMapping != null) {
serverHost = StringUtils.join(requestMapping.value(), "/");
}
Map<String, Object> requestMap = new HashMap<String, Object>();
List<String> requestList = new ArrayList<String>();
Object[] args = joinPoint.getArgs();
for(Object arg : args) {
requestList.add(arg.toString());
}
requestMap.put("params", requestList);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
traceHolder.setProfile(profile);
traceHolder.setRpcType(RpcTypeEnum.HTTP.name());
traceHolder.setServiceCategory("feign");
traceHolder.setServiceName(feignClient.name());
traceHolder.setMethodName(methodName);
traceHolder.setRequestParam(gson.toJson(requestMap));
traceHolder.setServiceHost(serverHost);
traceHolder.setClientHost(HostUtil.getIP(request));
LuminaryRibbonRule.regist(traceHolder);
preHandle(traceHolder);
request.setAttribute(TraceInfo.TRACE_ID_KEY, traceHolder.getEntity().getTraceId());
request.setAttribute(TraceInfo.RPC_ID_KEY, traceHolder.getEntity().getRpcId());
result = joinPoint.proceed();
traceHolder.getEntity().setResponseInfo(result.toString());
postHandle(traceHolder);
} catch (Exception e) {
log.error(e.getMessage(), e);
exceptionHandle(traceHolder, e);
}
return result;
}
}