終於到了我們的重點,微服務了。
與使用OkHttp3來實現的客戶端類似,Feign介面本來也就是一個Http呼叫,依然可以使用Http頭傳值的方式,將Trace
往下傳。
本文更多的是關於SpringCloud
的一些知識,你需要了解一些基本的Spring
相關的知識。
更多系列,請關注公眾號小姐姐味道,本文相關程式碼的github地址,見:
https://github.com/sayhiai/example-jaeger-opentracing-tutorial-004
複製程式碼
安裝Consul
SpringCloud的註冊中心,我們選用Consul。
consul也是用golang開發的。從consul官網下載二進位制包以後,解壓。
./consul agent -bind 127.0.0.1 -data-dir . -node my-register-center -bootstrap-expect 1 -ui -dev
複製程式碼
使用以上指令碼快速啟動,即可使用。
訪問 http://localhost:8500/ui/ 可以看到Consul的web頁面。
構建微服務服務端和客戶端
maven依賴
以bom方式引入springboot和springcloud的元件。
spring-boot-dependencies 2.1.3.RELEASE
spring-cloud-dependencies Greenwich.SR1
複製程式碼
都是熱乎乎的新鮮版本。
接下來下,引入其他必須的包
opentracing-util 0.32.0
jaeger-client 0.35.0
logback-classic 1.2.3
opentracing-spring-jaeger-cloud-starter 2.0.0
spring-boot-starter-web
spring-boot-starter-aop
spring-boot-starter-actuator
spring-cloud-starter-consul-discovery
spring-cloud-starter-openfeign
複製程式碼
構建服務端
服務端App的埠是8888
@SpringBootApplication
@EnableAutoConfiguration
@EnableDiscoveryClient
@ComponentScan(basePackages = {
"com.sayhiai.example.jaeger.totorial04.controller",
})
public class App extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
複製程式碼
在application.yml中,配置Consul作為配置中心。
cloud:
consul:
host: 127.0.0.1
port: 8500
discovery:
register: true
tags: version=1.0,author=xjjdog
healthCheckPath: /actuator/health
healthCheckInterval: 5s
複製程式碼
建立Rest服務/hello
@PostMapping("/hello")
@ResponseBody
public String hello(@RequestBody String name) {
return "hello " + name;
}
複製程式碼
構建Feign客戶端
Feign客戶端的App埠是9999
,同樣是一個SpringCloud服務。
建立FeignClient
@FeignClient("love-you-application")
public interface LoveYouClient {
@PostMapping("/hello")
@ResponseBody
public String hello(@RequestBody String name);
}
複製程式碼
建立呼叫入口/test
@GetMapping("/test")
@ResponseBody
public String hello() {
String rs = loveYouClient.hello("小姐姐味道");
return rs;
}
複製程式碼
整合jaeger
目前,已經有相關SpringCloud的輪子了,我們就不重複製造了。
首先,我們看一下使用方法,然後,說明一下背後的原理。瞭解原理之後,你將很容易的給自己開發的中介軟體加入Trace功能。
輪子在這裡,引入相應maven包即可使用:
https://github.com/opentracing-contrib/java-spring-jaeger
複製程式碼
<dependency>
<groupId>io.opentracing.contrib</groupId>
<artifactId>opentracing-spring-jaeger-cloud-starter</artifactId>
</dependency>
複製程式碼
加入配置生效
在application.yml
中,加入以下配置,就可以得到呼叫鏈功能了。
配置指明瞭trace的存放地址,並將本地log開啟。
opentracing.jaeger.http-sender.url: http://10.30.94.8:14268/api/traces
opentracing.jaeger.log-spans: true
複製程式碼
訪問 localhost:9999/test,會得到以下呼叫鏈。
可以看到。Feign的整個呼叫過程都被記錄下來了。原理
Feign的呼叫
Feign通過Header傳遞引數
首先看下Feign的Request建構函式。
public static Request create(
String method,
String url,
Map<String, Collection<String>> headers,
byte[] body,
Charset charset) {
return new Request(method, url, headers, body, charset);
}
複製程式碼
如程式碼,完全可以通過在headers引數中追加我們需要的資訊進行傳遞。
接著原始碼往下找: Client**->** LoadBalancerFeignClient execute()-> executeWithLoadBalancer()-> IClient**->**
再往下,IClient實現有 OkHttpLoadBalancingClient RibbonLoadBalancingHttpClient(基於apache的包) 等,它們都可以很容易的設定其Header
最終,我們的請求還是由這些底層的庫函式發起,預設的是HttpURLConnection。
讀過Feign和Ribbon原始碼的人都知道,這部分程式碼不是一般的亂,但好在上層的Feign是一致的。
使用委託包裝Client
通過實現feign.Client
介面,結合委託,可以重新封裝execute
方法,然後將資訊inject
進Feign的scope中。
使用Aop自動攔截Feign呼叫
@Aspect
class TracingAspect {
@Around("execution (* feign.Client.*(..)) && !within(is(FinalType))")
public Object feignClientWasCalled(final ProceedingJoinPoint pjp) throws Throwable {
Object bean = pjp.getTarget();
if (!(bean instanceof TracingClient)) {
Object[] args = pjp.getArgs();
return new TracingClientBuilder((Client) bean, tracer)
.withFeignSpanDecorators(spanDecorators)
.build()
.execute((Request) args[0], (Request.Options) args[1]);
}
return pjp.proceed();
}
}
複製程式碼
利用spring boot starter技術,我們不需要任何其他改動,就可以擁有trace功能了。
Web端的傳送和接收
瞭解spring的人都知道,最適合做http頭資訊新增和提取的地方,就是攔截器和過濾器。
傳送
對於普通的http請求客戶端來說,是通過新增一個
ClientHttpRequestInterceptor
攔截器來實現的。過程不再表訴,依然是使用inject等函式進行頭資訊設定。
接收
而對於接收,則使用的是Filter進行實現的。通過實現一個普通的servlet filter。可以通過extract
函式將trace資訊提取出來,然後將context作為Request的attribute進行傳遞。
相關程式碼片段如下。
if (servletRequest.getAttribute(SERVER_SPAN_CONTEXT) != null) {
chain.doFilter(servletRequest, servletResponse);
} else {
SpanContext extractedContext = tracer.extract(Format.Builtin.HTTP_HEADERS,
new HttpServletRequestExtractAdapter(httpRequest));
final Span span = tracer.buildSpan(httpRequest.getMethod())
.asChildOf(extractedContext)
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
.start();
httpRequest.setAttribute(SERVER_SPAN_CONTEXT, span.context());
複製程式碼
就這樣,整個鏈條就穿插起來啦。