方案背景
Zipkin 作为一个链路跟踪工具,用来在微服务中记录每个链路调用的用时情况,链接调用情况包括:
(1)服务作为客户端从发起请求(cs)到收到响应(cr)的时间。
该情况一般在zuul网关或者存在一个服务调用另一个的时间存在。
(2)服务作为服务端从接收到请求(sr)到把将结果发送给客户端的时间(ss,即outputStream flush前的时间)。
一般来说,如果涉及一个服务调用另一个服务,则一次请求会产生至少两条json格式的日志。不涉及到调用其它服务的服务,则会产生一条日志。对于所产生的日志,通过异步http请求的方式发送到zipkin server中进行入库或存到zipkin server的缓存中。之后便可以通过zipkin server进行调用链的分析及跟踪。
基于以上的分析,如果基于默认的解决方案,调用量大的情况下,会导致带来以下问题:
(1)对于zipkin server来说,可能会由于频繁的接收数据而变慢或者直接瘫痪。
(2)对于zipkin client来说(微服务使用端),如果zipkin server响应不及时会导致异步的日志发送请求堆积。堆积会带来对内存的消耗,另外当堆积满了,会导致日志丢失。
(3)发送日志大小的限制。在默认的实现中(HttpZipkinSpanReporter),对于发送日志的数据量有限制大小(5 * 1024 * 1024)。如果日志量超过,则会丢失该日志。
(4)如果zipkin server挂了,会导致大量的数据丢失,还会使得zipkin client由于尝试连接zipkin server端而变得慢。
(5)如果对zipkin client产生的影响,就会影响到业务的正常访问。
基于上述分析,我们需要一种不会导致日志缺失并且对业务访问影响最小的方案。
实现分析
要达到不会导致日志缺失并且对业务访问影响最小,最简单的方式便是将zipkin的日志写到日志文件中。之后再通过分布式日志收集工具将日志收集起来。当需要看某个日志id的调用链路时,我们再通过日志收集工具把相关数据找出,然后用数据导入的方式将日志导致到zipkin servier中。最后,我们便可以利用zipkin server进行调用链路的分析了。
- 如何实现zipkin日志文件化
通过分析zipkin的日志发送机制,发现其主要是通过实现ZipkinSpanReporter接口的HttpZipkinSpanReporter来发送。而HttpZipkinSpanReporter在自动化配置中为以下:
注意,ConditionalOnMissingBean,这是条件化配置,意思是说当没有存在指定类型的bean(ZipkinSpanReporter)才会实例化该bean。因此,我们可以通过实现ZipkinSpanReporter接口的方式,在实现中通过写日志的方式来将日志写到文件中。
- 如何将zipkin日志收集起来
可以采用 logstash(日志收集)+ElasticSearch(数据存储)。当日志收集起来后,便可以通过elasticsearch 的HTTP RESTful API,根据日志id查询相关的数据。
- 如何将zipkin日志导入到zipkin server中
将日志中json格式的数据,通过HttpZipkinSpanReporter来向zipkin server进行数据导入。
方案实施
在接口实现中,将要记录的数据序列化成json格式,然后记录到日志文件中。
源码:
package com.yao.springcloud.sleuth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.sleuth.zipkin.ZipkinSpanReporter;
import zipkin.Span;
public class TraceZipkinSpanReporter implements ZipkinSpanReporter {
private Logger LOGGER = LoggerFactory.getLogger(TraceZipkinSpanReporter
.class);
@Override
public void report(Span span) {
if(span != null){
LOGGER.info(span.toString());
}
}
}
- 将日志数据导入zipkin server
spring-cloud-sleuth-zipkin提供了一个将日志导入zipkin server的方式,即HttpZipkinSpanReporter。我们只需要做以下事情:
(1)将zipkin 日志内容转换成HttpZipkinSpanReporter的发送对象。
(2)调用HttpZipkinSpanReporter进行发送。
源码:
package com.yao.sprintcloud.test;
import org.junit.Test;
import org.springframework.cloud.sleuth.metric.NoOpSpanMetricReporter;
import org.springframework.cloud.sleuth.zipkin.HttpZipkinSpanReporter;
import org.springframework.cloud.sleuth.zipkin.ZipkinSpanReporter;
import zipkin.Codec;
import zipkin.Span;
public class ZipkinImprotTest {
@Test
public void testPostDataToZipkinServer(){
String url = "http://localhost:8087";
//可以同时发送多条数据,以下是两条数据
String data = "{\"traceId\":\"b27d50e739a9241b\",\"id\":\"2641fb830471fe4e\",\"name\":\"http:/saybhello\",\"parentId\":\"b5d9d2f28ca9a6ee\",\"timestamp\":1546652853045000,\"duration\":19000,\"annotations\":[{\"timestamp\":1546652853045000,\"value\":\"cs\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"timestamp\":1546652853064000,\"value\":\"cr\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}}],\"binaryAnnotations\":[{\"key\":\"http.host\",\"value\":\"192.168.1.105\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"key\":\"http.method\",\"value\":\"GET\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"key\":\"http.path\",\"value\":\"/saybhello\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"key\":\"http.url\",\"value\":\"http://192.168.1.105:8085/saybhello?name=ServiceA\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"key\":\"spring.instance_id\",\"value\":\"192.168.1.105:serviceA:8084\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}}]}\n" +
"{\"traceId\":\"b27d50e739a9241b\",\"id\":\"b5d9d2f28ca9a6ee\",\"name\":\"http:/sayhello\",\"parentId\":\"b27d50e739a9241b\",\"annotations\":[{\"timestamp\":1546652852989000,\"value\":\"sr\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"timestamp\":1546652854469000,\"value\":\"ss\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}}],\"binaryAnnotations\":[{\"key\":\"lc\",\"value\":\"hystrix\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"key\":\"mvc.controller.class\",\"value\":\"ServiceAController\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"key\":\"mvc.controller.method\",\"value\":\"sayhello\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"key\":\"spring.instance_id\",\"value\":\"192.168.1.105:serviceA:8084\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}},{\"key\":\"thread\",\"value\":\"hystrix-serviceb-2\",\"endpoint\":{\"serviceName\":\"servicea\",\"ipv4\":\"192.168.1.105\",\"port\":8084}}]}";
Span span = Codec.JSON.readSpan(data.getBytes());
postDataToZipkin(url, span);
}
private void postDataToZipkin(String url, Span data){
ZipkinSpanReporter reporter = new HttpZipkinSpanReporter(url, 1,
false, new NoOpSpanMetricReporter());
reporter.report(data);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 附
源码地址:https://github.com/micat707/myprojects/tree/master/spring-cloud-solutions