目錄
一、簡介
這篇文章總結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;
}
}