zipkin为分布式链路调用监控系统,聚合各业务系统调用延迟数据,达到链路调用监控跟踪。

architecture


如图,在复杂的调用链路中假设存在一条调用链路响应缓慢,如何定位其中延迟高的服务呢?

  • 日志: 通过分析调用链路上的每个服务日志得到结果
  • zipkin:使用zipkinweb UI可以一眼看出延迟高的服务

如图所示,各业务系统在彼此调用时,将特定的跟踪消息传递至zipkin,zipkin在收集到跟踪信息后将其聚合处理、存储、展示等,用户可通过web UI方便 
获得网络延迟、调用链路、系统依赖等等。

zipkin主要涉及四个组件 collector storage search web UI

  • Collector接收各service传输的数据
  • Cassandra作为Storage的一种,也可以是mysql等,默认存储在内存中,配置cassandra可以参考这里
  • Query负责查询Storage中存储的数据,提供简单的JSON API获取数据,主要提供给web UI使用
  • Web 提供简单的web界面

2.安装

执行如下命令下载jar包

wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
  • 1
  • 1

其为一个spring boot 工程,直接运行jar

nohup java -jar zipkin.jar & 
  • 1
  • 1

访问 http://ip:9411 

概念:

使用zipkin涉及几个概念

  • Span:基本工作单元,一次链路调用(可以是RPC,DB等没有特定的限制)创建一个span,通过一个64位ID标识它, 
    span通过还有其他的数据,例如描述信息,时间戳,key-value对的(Annotation)tag信息,parent-id等,其中parent-id 
    可以表示span调用链路来源,通俗的理解span就是一次请求信息

  • Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识

  • Annotation: 注解,用来记录请求特定事件相关信息(例如时间),通常包含四个注解信息

    cs - Client Start,表示客户端发起请求

    sr - Server Receive,表示服务端收到请求

    ss - Server Send,表示服务端完成处理,并将结果发送给客户端

    cr - Client Received,表示客户端获取到服务端返回信息

  • BinaryAnnotation:提供一些额外信息,一般已key-value对出现

概念说完,来看下完整的调用链路 

上图表示一请求链路,一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来,如图 

整个链路的依赖关系如下: 

完成链路调用的记录后,如何来计算调用的延迟呢,这就需要利用Annotation信息

sr-cs 得到请求发出延迟

ss-sr 得到服务端处理延迟

cr-cs 得到真个链路完成延迟

brave

作为各调用链路,只需要负责将指定格式的数据发送给zipkin即可,利用brave可快捷完成操作。

首先导入jar包pom.xml

  1. <parent>

  2. <groupId>org.springframework.boot</groupId>

  3. <artifactId>spring-boot-starter-parent</artifactId>

  4. <version>1.3.6.RELEASE</version>

  5. </parent>

  6. <!-- https://mvnrepository.com/artifact/io.zipkin.brave/brave-core -->

  7. <dependencies>

  8. <dependency>

  9. <groupId>org.springframework.boot</groupId>

  10. <artifactId>spring-boot-starter-web</artifactId>

  11. </dependency>

  12. <dependency>

  13. <groupId>org.springframework.boot</groupId>

  14. <artifactId>spring-boot-starter-aop</artifactId>

  15. </dependency>

  16. <dependency>

  17. <groupId>org.springframework.boot</groupId>

  18. <artifactId>spring-boot-starter-actuator</artifactId>

  19. </dependency>

  20. <dependency>

  21. <groupId>io.zipkin.brave</groupId>

  22. <artifactId>brave-core</artifactId>

  23. <version>3.9.0</version>

  24. </dependency>

  25. <!-- https://mvnrepository.com/artifact/io.zipkin.brave/brave-http -->

  26. <dependency>

  27. <groupId>io.zipkin.brave</groupId>

  28. <artifactId>brave-http</artifactId>

  29. <version>3.9.0</version>

  30. </dependency>

  31. <dependency>

  32. <groupId>io.zipkin.brave</groupId>

  33. <artifactId>brave-spancollector-http</artifactId>

  34. <version>3.9.0</version>

  35. </dependency>

  36. <dependency>

  37. <groupId>io.zipkin.brave</groupId>

  38. <artifactId>brave-web-servlet-filter</artifactId>

  39. <version>3.9.0</version>

  40. </dependency>

  41. <dependency>

  42. <groupId>io.zipkin.brave</groupId>

  43. <artifactId>brave-okhttp</artifactId>

  44. <version>3.9.0</version>

  45. </dependency>

  46. <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->

  47. <dependency>

  48. <groupId>org.slf4j</groupId>

  49. <artifactId>slf4j-api</artifactId>

  50. <version>1.7.13</version>

  51. </dependency>

  52. <dependency>

  53. <groupId>org.apache.httpcomponents</groupId>

  54. <artifactId>httpclient</artifactId>

  55. <version>4.5.1</version>

  56. </dependency>

  57. </dependencies>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

利用spring boot创建工程

Application.Java

  1. package com.lkl.zipkin;

  2. import org.springframework.boot.SpringApplication;

  3. import org.springframework.boot.autoconfigure.SpringBootApplication;

  4. /**

  5. *

  6. * Created by liaokailin on 16/7/27.

  7. */

  8. @SpringBootApplication

  9. public class Application {

  10. public static void main(String[] args) {

  11. SpringApplication app = new SpringApplication(Application.class);

  12. app.run(args);

  13. }

  14. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

建立controller对外提供服务

HomeController.java

  1. RestController

  2. @RequestMapping("/")

  3. public class HomeController {

  4. @Autowired

  5. private OkHttpClient client;

  6. private Random random = new Random();

  7. @RequestMapping("start")

  8. public String start() throws InterruptedException, IOException {

  9. int sleep= random.nextInt(100);

  10. TimeUnit.MILLISECONDS.sleep(sleep);

  11. Request request = new Request.Builder().url("http://localhost:9090/foo").get().build();

  12. Response response = client.newCall(request).execute();

  13. return " [service1 sleep " + sleep+" ms]" + response.body().toString();

  14. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

HomeController中利用OkHttpClient调用发起http请求。在每次发起请求时则需要通过brave记录Span信息,并异步传递给zipkin 
作为被调用方(服务端)也同样需要完成以上操作.

ZipkinConfig.java

  1. package com.lkl.zipkin.config;

  2. import com.github.kristofa.brave.Brave;

  3. import com.github.kristofa.brave.EmptySpanCollectorMetricsHandler;

  4. import com.github.kristofa.brave.SpanCollector;

  5. import com.github.kristofa.brave.http.DefaultSpanNameProvider;

  6. import com.github.kristofa.brave.http.HttpSpanCollector;

  7. import com.github.kristofa.brave.okhttp.BraveOkHttpRequestResponseInterceptor;

  8. import com.github.kristofa.brave.servlet.BraveServletFilter;

  9. import okhttp3.OkHttpClient;

  10. import org.springframework.beans.factory.annotation.Autowired;

  11. import org.springframework.context.annotation.Bean;

  12. import org.springframework.context.annotation.Configuration;

  13. /**

  14. * Created by liaokailin on 16/7/27.

  15. */

  16. @Configuration

  17. public class ZipkinConfig {

  18. @Autowired

  19. private ZipkinProperties properties;

  20. @Bean

  21. public SpanCollector spanCollector() {

  22. HttpSpanCollector.Config config = HttpSpanCollector.Config.builder().connectTimeout(properties.getConnectTimeout()).readTimeout(properties.getReadTimeout())

  23. .compressionEnabled(properties.isCompressionEnabled()).flushInterval(properties.getFlushInterval()).build();

  24. return HttpSpanCollector.create(properties.getUrl(), config, new EmptySpanCollectorMetricsHandler());

  25. }

  26. @Bean

  27. public Brave brave(SpanCollector spanCollector){

  28. Brave.Builder builder = new Brave.Builder(properties.getServiceName()); //指定state

  29. builder.spanCollector(spanCollector);

  30. builder.traceSampler(Sampler.ALWAYS_SAMPLE);

  31. Brave brave = builder.build();

  32. return brave;

  33. }

  34. @Bean

  35. public BraveServletFilter braveServletFilter(Brave brave){

  36. BraveServletFilter filter = new BraveServletFilter(brave.serverRequestInterceptor(),brave.serverResponseInterceptor(),new DefaultSpanNameProvider());

  37. return filter;

  38. }

  39. @Bean

  40. public OkHttpClient okHttpClient(Brave brave){

  41. OkHttpClient client = new OkHttpClient.Builder()

  42. .addInterceptor(new BraveOkHttpRequestResponseInterceptor(brave.clientRequestInterceptor(), brave.clientResponseInterceptor(), new DefaultSpanNameProvider()))

  43. .build();

  44. return client;

  45. }

  46. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • SpanCollector 配置收集器

  • Brave 各工具类的封装,其中builder.traceSampler(Sampler.ALWAYS_SAMPLE)设置采样比率,0-1之间的百分比

  • BraveServletFilter 作为拦截器,需要serverRequestInterceptor,serverResponseInterceptor 分别完成srss操作

  • OkHttpClient 添加拦截器,需要clientRequestInterceptor,clientResponseInterceptor 分别完成cscr操作,该功能由 
    brave中的brave-okhttp模块提供,同样的道理如果需要记录数据库的延迟只要在数据库操作前后完成cscr即可,当然brave提供其封装。

以上还缺少一个配置信息ZipkinProperties.java

  1. package com.lkl.zipkin.config;

  2. import org.springframework.boot.context.properties.ConfigurationProperties;

  3. import org.springframework.context.annotation.Configuration;

  4. /**

  5. * Created by liaokailin on 16/7/28.

  6. */

  7. @Configuration

  8. @ConfigurationProperties(prefix = "com.zipkin")

  9. public class ZipkinProperties {

  10. private String serviceName;

  11. private String url;

  12. private int connectTimeout;

  13. private int readTimeout;

  14. private int flushInterval;

  15. private boolean compressionEnabled;

  16. public String getUrl() {

  17. return url;

  18. }

  19. public void setUrl(String url) {

  20. this.url = url;

  21. }

  22. public int getConnectTimeout() {

  23. return connectTimeout;

  24. }

  25. public void setConnectTimeout(int connectTimeout) {

  26. this.connectTimeout = connectTimeout;

  27. }

  28. public int getReadTimeout() {

  29. return readTimeout;

  30. }

  31. public void setReadTimeout(int readTimeout) {

  32. this.readTimeout = readTimeout;

  33. }

  34. public int getFlushInterval() {

  35. return flushInterval;

  36. }

  37. public void setFlushInterval(int flushInterval) {

  38. this.flushInterval = flushInterval;

  39. }

  40. public boolean isCompressionEnabled() {

  41. return compressionEnabled;

  42. }

  43. public void setCompressionEnabled(boolean compressionEnabled) {

  44. this.compressionEnabled = compressionEnabled;

  45. }

  46. public String getServiceName() {

  47. return serviceName;

  48. }

  49. public void setServiceName(String serviceName) {

  50. this.serviceName = serviceName;

  51. }

  52. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

则可以在配置文件application.properties中配置相关信息

  1. com.zipkin.serviceName=service1

  2. com.zipkin.url=http://110.173.14.57:9411

  3. com.zipkin.connectTimeout=6000

  4. com.zipkin.readTimeout=6000

  5. com.zipkin.flushInterval=1

  6. com.zipkin.compressionEnabled=true

  7. server.port=8080

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

那么其中的service1即完成,同样的道理,修改配置文件(调整com.zipkin.serviceName,以及server.port)以及controller对应的方法构造若干服务

service1 中访问http://localhost:8080/start需要访问http://localhost:9090/foo,则构造server2提供该方法

server2配置

  1. com.zipkin.serviceName=service2

  2. com.zipkin.url=http://110.173.14.57:9411

  3. com.zipkin.connectTimeout=6000

  4. com.zipkin.readTimeout=6000

  5. com.zipkin.flushInterval=1

  6. com.zipkin.compressionEnabled=true

  7. server.port=9090

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

controller方法

  1. @RequestMapping("foo")

  2. public String foo() throws InterruptedException, IOException {

  3. Random random = new Random();

  4. int sleep= random.nextInt(100);

  5. TimeUnit.MILLISECONDS.sleep(sleep);

  6. Request request = new Request.Builder().url("http://localhost:9091/bar").get().build(); //service3

  7. Response response = client.newCall(request).execute();

  8. String result = response.body().string();

  9. request = new Request.Builder().url("http://localhost:9092/tar").get().build(); //service4

  10. response = client.newCall(request).execute();

  11. result += response.body().string();

  12. return " [service2 sleep " + sleep+" ms]" + result;

  13. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

server2中调用server3server4中的方法

方法分别为

  1. @RequestMapping("bar")

  2. public String bar() throws InterruptedException, IOException { //service3 method

  3. Random random = new Random();

  4. int sleep= random.nextInt(100);

  5. TimeUnit.MILLISECONDS.sleep(sleep);

  6. return " [service3 sleep " + sleep+" ms]";

  7. }

  8. @RequestMapping("tar")

  9. public String tar() throws InterruptedException, IOException { //service4 method

  10. Random random = new Random();

  11. int sleep= random.nextInt(1000);

  12. TimeUnit.MILLISECONDS.sleep(sleep);

  13. return " [service4 sleep " + sleep+" ms]";

  14. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

将工程修改后编译成jar形式

执行

  1. nohup java -jar server4.jar &

  2. nohup java -jar server3.jar &

  3. nohup java -jar server2.jar &

  4. nohup java -jar server1.jar &

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

访问http://localhost:8080/start后查看zipkinweb UI

点击条目可以查看具体的延迟信息

服务之间的依赖为 

brave 源码

以上完成了基本的操作,下面将从源码角度来看下brave的实现

首先从SpanCollector来入手

  1. @Bean

  2. public SpanCollector spanCollector() {

  3. HttpSpanCollector.Config config = HttpSpanCollector.Config.builder().connectTimeout(properties.getConnectTimeout()).readTimeout(properties.getReadTimeout())

  4. .compressionEnabled(properties.isCompressionEnabled()).flushInterval(properties.getFlushInterval()).build();

  5. return HttpSpanCollector.create(properties.getUrl(), config, new EmptySpanCollectorMetricsHandler());

  6. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

从名称上看HttpSpanCollector是基于httpspan收集器,因此超时配置是必须的,默认给出的超时时间较长,flushInterval表示span的传递 
间隔,实际为定时任务执行的间隔时间.在HttpSpanCollector中覆写了父类方法sendSpans

  1. @Override

  2. protected void sendSpans(byte[] json) throws IOException {

  3. // intentionally not closing the connection, so as to use keep-alives

  4. HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();

  5. connection.setConnectTimeout(config.connectTimeout());

  6. connection.setReadTimeout(config.readTimeout());

  7. connection.setRequestMethod("POST");

  8. connection.addRequestProperty("Content-Type", "application/json");

  9. if (config.compressionEnabled()) {

  10. connection.addRequestProperty("Content-Encoding", "gzip");

  11. ByteArrayOutputStream gzipped = new ByteArrayOutputStream();

  12. try (GZIPOutputStream compressor = new GZIPOutputStream(gzipped)) {

  13. compressor.write(json);

  14. }

  15. json = gzipped.toByteArray();

  16. }

  17. connection.setDoOutput(true);

  18. connection.setFixedLengthStreamingMode(json.length);

  19. connection.getOutputStream().write(json);

  20. try (InputStream in = connection.getInputStream()) {

  21. while (in.read() != -1) ; // skip

  22. } catch (IOException e) {

  23. try (InputStream err = connection.getErrorStream()) {

  24. if (err != null) { // possible, if the connection was dropped

  25. while (err.read() != -1) ; // skip

  26. }

  27. }

  28. throw e;

  29. }

  30. }

  31. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

可以看出最终span信息是通过HttpURLConnection实现的,同样道理就可以推理bravebrave-spring-resttemplate-interceptors模块的实现, 
只是换了一种http封装。

Brave

  1. @Bean

  2. public Brave brave(SpanCollector spanCollector){

  3. Brave.Builder builder = new Brave.Builder(properties.getServiceName()); //指定state

  4. builder.spanCollector(spanCollector);

  5. builder.traceSampler(Sampler.ALWAYS_SAMPLE);

  6. Brave brave = builder.build();

  7. return brave;

  8. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Brave类包装了各种工具类

  1. public Brave build() {

  2. return new Brave(this);

  3. }

  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

创建一个Brave

  1. private Brave(Builder builder) {

  2. serverTracer = ServerTracer.builder()

  3. .randomGenerator(builder.random)

  4. .spanCollector(builder.spanCollector)

  5. .state(builder.state)

  6. .traceSampler(builder.sampler).build();

  7. clientTracer = ClientTracer.builder()

  8. .randomGenerator(builder.random)

  9. .spanCollector(builder.spanCollector)

  10. .state(builder.state)

  11. .traceSampler(builder.sampler).build();

  12. localTracer = LocalTracer.builder()

  13. .randomGenerator(builder.random)

  14. .spanCollector(builder.spanCollector)

  15. .spanAndEndpoint(SpanAndEndpoint.LocalSpanAndEndpoint.create(builder.state))

  16. .traceSampler(builder.sampler).build();

  17. serverRequestInterceptor = new ServerRequestInterceptor(serverTracer);

  18. serverResponseInterceptor = new ServerResponseInterceptor(serverTracer);

  19. clientRequestInterceptor = new ClientRequestInterceptor(clientTracer);

  20. clientResponseInterceptor = new ClientResponseInterceptor(clientTracer);

  21. serverSpanAnnotationSubmitter = AnnotationSubmitter.create(SpanAndEndpoint.ServerSpanAndEndpoint.create(builder.state));

  22. serverSpanThreadBinder = new ServerSpanThreadBinder(builder.state);

  23. clientSpanThreadBinder = new ClientSpanThreadBinder(builder.state);

  24. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

封装了*Tracer,*Interceptor,*Binder

其中 serverTracer当服务作为服务端时处理span信息,clientTracer当服务作为客户端时处理span信息

Filter

BraveServletFilterhttp模块提供的拦截器功能,传递serverRequestInterceptor,serverResponseInterceptor,spanNameProvider等参数 
其中spanNameProvider表示如何处理span的名称,默认使用method名称,spring boot中申明的filter bean 默认拦截所有请求

  1. @Override

  2. public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {

  3. String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();

  4. boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;

  5. if (hasAlreadyFilteredAttribute) {

  6. // Proceed without invoking this filter...

  7. filterChain.doFilter(request, response);

  8. } else {

  9. final StatusExposingServletResponse statusExposingServletResponse = new StatusExposingServletResponse((HttpServletResponse) response);

  10. requestInterceptor.handle(new HttpServerRequestAdapter(new ServletHttpServerRequest((HttpServletRequest) request), spanNameProvider));

  11. try {

  12. filterChain.doFilter(request, statusExposingServletResponse);

  13. } finally {

  14. responseInterceptor.handle(new HttpServerResponseAdapter(new HttpResponse() {

  15. @Override

  16. public int getHttpStatusCode() {

  17. return statusExposingServletResponse.getStatus();

  18. }

  19. }));

  20. }

  21. }

  22. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

首先来看requestInterceptor.handle方法,

  1. public void handle(ServerRequestAdapter adapter) {

  2. serverTracer.clearCurrentSpan();

  3. final TraceData traceData = adapter.getTraceData();

  4. Boolean sample = traceData.getSample();

  5. if (sample != null && Boolean.FALSE.equals(sample)) {

  6. serverTracer.setStateNoTracing();

  7. LOGGER.fine("Received indication that we should NOT trace.");

  8. } else {

  9. if (traceData.getSpanId() != null) {

  10. LOGGER.fine("Received span information as part of request.");

  11. SpanId spanId = traceData.getSpanId();

  12. serverTracer.setStateCurrentTrace(spanId.traceId, spanId.spanId,

  13. spanId.nullableParentId(), adapter.getSpanName());

  14. } else {

  15. LOGGER.fine("Received no span state.");

  16. serverTracer.setStateUnknown(adapter.getSpanName());

  17. }

  18. serverTracer.setServerReceived();

  19. for(KeyValueAnnotation annotation : adapter.requestAnnotations())

  20. {

  21. serverTracer.submitBinaryAnnotation(annotation.getKey(), annotation.getValue());

  22. }

  23. }

  24. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

其中serverTracer.clearCurrentSpan()清除当前线程上的span信息,调用ThreadLocalServerClientAndLocalSpanState中的

  1. @Override

  2. public void setCurrentServerSpan(final ServerSpan span) {

  3. if (span == null) {

  4. currentServerSpan.remove();

  5. } else {

  6. currentServerSpan.set(span);

  7. }

  8. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

currentServerSpanThreadLocal对象

private final static ThreadLocal<ServerSpan> currentServerSpan = new ThreadLocal<ServerSpan>() {
  • 1
  • 1

回到ServerRequestInterceptor#handle()方法中final TraceData traceData = adapter.getTraceData()

  1. @Override

  2. public TraceData getTraceData() {

  3. final String sampled = serverRequest.getHttpHeaderValue(BraveHttpHeaders.Sampled.getName());

  4. if (sampled != null) {

  5. if (sampled.equals("0") || sampled.toLowerCase().equals("false")) {

  6. return TraceData.builder().sample(false).build();

  7. } else {

  8. final String parentSpanId = serverRequest.getHttpHeaderValue(BraveHttpHeaders.ParentSpanId.getName());

  9. final String traceId = serverRequest.getHttpHeaderValue(BraveHttpHeaders.TraceId.getName());

  10. final String spanId = serverRequest.getHttpHeaderValue(BraveHttpHeaders.SpanId.getName());

  11. if (traceId != null && spanId != null) {

  12. SpanId span = getSpanId(traceId, spanId, parentSpanId);

  13. return TraceData.builder().sample(true).spanId(span).build();

  14. }

  15. }

  16. }

  17. return TraceData.builder().build();

  18. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

其中SpanId span = getSpanId(traceId, spanId, parentSpanId) 将构造一个SpanId对象

  1. private SpanId getSpanId(String traceId, String spanId, String parentSpanId) {

  2. return SpanId.builder()

  3. .traceId(convertToLong(traceId))

  4. .spanId(convertToLong(spanId))

  5. .parentId(parentSpanId == null ? null : convertToLong(parentSpanId)).build();

  6. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

traceId,spanId,parentId关联起来,其中设置parentId方法为

  1. public Builder parentId(@Nullable Long parentId) {

  2. if (parentId == null) {

  3. this.flags |= FLAG_IS_ROOT;

  4. } else {

  5. this.flags &= ~FLAG_IS_ROOT;

  6. }

  7. this.parentId = parentId;

  8. return this;

  9. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果parentId为空为根节点,则执行this.flags |= FLAG_IS_ROOT ,因此后续在判断节点是否为根节点时,只需要执行(flags & FLAG_IS_ROOT) == FLAG_IS_ROOT即可.

构造完SpanId后看

  1. serverTracer.setStateCurrentTrace(spanId.traceId, spanId.spanId,

  2. spanId.nullableParentId(), adapter.getSpanName());

  • 1
  • 2
  • 1
  • 2

设置当前Span

  1. public void setStateCurrentTrace(long traceId, long spanId, @Nullable Long parentSpanId, @Nullable String name) {

  2. checkNotBlank(name, "Null or blank span name");

  3. spanAndEndpoint().state().setCurrentServerSpan(

  4. ServerSpan.create(traceId, spanId, parentSpanId, name));

  5. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ServerSpan.create创建Span信息

  1. static ServerSpan create(long traceId, long spanId, @Nullable Long parentSpanId, String name) {

  2. Span span = new Span();

  3. span.setTrace_id(traceId);

  4. span.setId(spanId);

  5. if (parentSpanId != null) {

  6. span.setParent_id(parentSpanId);

  7. }

  8. span.setName(name);

  9. return create(span, true);

  10. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

构造了一个包含Span信息的AutoValue_ServerSpan对象

通过setCurrentServerSpan设置到当前线程上

继续看serverTracer.setServerReceived()方法

  1. public void setServerReceived() {

  2. submitStartAnnotation(zipkinCoreConstants.SERVER_RECV);

  3. }

  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

为当前请求设置了server received event

  1. void submitStartAnnotation(String annotationName) {

  2. Span span = spanAndEndpoint().span();

  3. if (span != null) {

  4. Annotation annotation = Annotation.create(

  5. currentTimeMicroseconds(),

  6. annotationName,

  7. spanAndEndpoint().endpoint()

  8. );

  9. synchronized (span) {

  10. span.setTimestamp(annotation.timestamp);

  11. span.addToAnnotations(annotation);

  12. }

  13. }

  14. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里为Span信息设置了Annotation信息,后续的

  1. for(KeyValueAnnotation annotation : adapter.requestAnnotations())

  2. {

  3. serverTracer.submitBinaryAnnotation(annotation.getKey(), annotation.getValue());

  4. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

设置了BinaryAnnotation信息,adapter.requestAnnotations()在构造HttpServerRequestAdapter时已完成

  1. @Override

  2. public Collection<KeyValueAnnotation> requestAnnotations() {

  3. KeyValueAnnotation uriAnnotation = KeyValueAnnotation.create(

  4. TraceKeys.HTTP_URL, serverRequest.getUri().toString());

  5. return Collections.singleton(uriAnnotation);

  6. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

以上将Span信息(包括sr)存储在当前线程中,接下来继续看BraveServletFilter#doFilter方法的finally部分

  1. responseInterceptor.handle(new HttpServerResponseAdapter(new HttpResponse() {

  2. @Override //获取http状态码

  3. public int getHttpStatusCode() {

  4. return statusExposingServletResponse.getStatus();

  5. }

  6. }));

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

handle方法

  1. public void handle(ServerResponseAdapter adapter) {

  2. // We can submit this in any case. When server state is not set or

  3. // we should not trace this request nothing will happen.

  4. LOGGER.fine("Sending server send.");

  5. try {

  6. for(KeyValueAnnotation annotation : adapter.responseAnnotations())

  7. {

  8. serverTracer.submitBinaryAnnotation(annotation.getKey(), annotation.getValue());

  9. }

  10. serverTracer.setServerSend();

  11. } finally {

  12. serverTracer.clearCurrentSpan();

  13. }

  14. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

首先配置BinaryAnnotation信息,然后执行serverTracer.setServerSend,在finally中清除当前线程中的Span信息(不管前面是否清楚成功,最终都将执行该不走),ThreadLocal中的数据要做到有始有终

serverTracer.setServerSend()

  1. public void setServerSend() {

  2. if (submitEndAnnotation(zipkinCoreConstants.SERVER_SEND, spanCollector())) {

  3. spanAndEndpoint().state().setCurrentServerSpan(null);

  4. }

  5. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

终于看到spanCollector收集器了,说明下面将看是收集Span信息,这里为ss注解

  1. boolean submitEndAnnotation(String annotationName, SpanCollector spanCollector) {

  2. Span span = spanAndEndpoint().span();

  3. if (span == null) {

  4. return false;

  5. }

  6. Annotation annotation = Annotation.create(

  7. currentTimeMicroseconds(),

  8. annotationName,

  9. spanAndEndpoint().endpoint()

  10. );

  11. span.addToAnnotations(annotation);

  12. if (span.getTimestamp() != null) {

  13. span.setDuration(annotation.timestamp - span.getTimestamp());

  14. }

  15. spanCollector.collect(span);

  16. return true;

  17. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

首先获取当前线程中的Span信息,然后处理注解信息,通过annotation.timestamp - span.getTimestamp()计算延迟, 
调用spanCollector.collect(span)进行收集Span信息,那么Span信息是同步收集的吗?肯定不是的,接着看

调用spanCollector.collect(span)则执行FlushingSpanCollector中的collect方法

  1. @Override

  2. public void collect(Span span) {

  3. metrics.incrementAcceptedSpans(1);

  4. if (!pending.offer(span)) {

  5. metrics.incrementDroppedSpans(1);

  6. }

  7. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

首先进行的是metrics统计信息,可以自定义该SpanCollectorMetricsHandler信息收集各指标信息,利用如grafana等展示信息

pending.offer(span)span信息存储在BlockingQueue中,然后通过定时任务去取出阻塞队列中的值,偷偷摸摸的上传span信息

定时任务利用了Flusher类来执行,在构造FlushingSpanCollector时构造了Flusher

  1. static final class Flusher implements Runnable {

  2. final Flushable flushable;

  3. final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

  4. Flusher(Flushable flushable, int flushInterval) {

  5. this.flushable = flushable;

  6. this.scheduler.scheduleWithFixedDelay(this, 0, flushInterval, SECONDS);

  7. }

  8. @Override

  9. public void run() {

  10. try {

  11. flushable.flush();

  12. } catch (IOException ignored) {

  13. }

  14. }

  15. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

创建了一个核心线程数为1的线程池,每间隔flushInterval秒执行一次Span信息上传,执行flush方法

  1. @Override

  2. public void flush() {

  3. if (pending.isEmpty()) return;

  4. List<Span> drained = new ArrayList<Span>(pending.size());

  5. pending.drainTo(drained);

  6. if (drained.isEmpty()) return;

  7. int spanCount = drained.size();

  8. try {

  9. reportSpans(drained);

  10. } catch (IOException e) {

  11. metrics.incrementDroppedSpans(spanCount);

  12. } catch (RuntimeException e) {

  13. metrics.incrementDroppedSpans(spanCount);

  14. }

  15. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

首先将阻塞队列中的值全部取出存如集合中,最后调用reportSpans(List<Span> drained)抽象方法,该方法在AbstractSpanCollector得到覆写

  1. @Override

  2. protected void reportSpans(List<Span> drained) throws IOException {

  3. byte[] encoded = codec.writeSpans(drained);

  4. sendSpans(encoded);

  5. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

转换成字节流后调用sendSpans抽象方法发送Span信息,此时就回到一开始说的HttpSpanCollector通过HttpURLConnection实现的sendSpans方法。

具体使用可以参考:https://github.com/liaokailin/zipkin#architecture,下载这个maven项目并按照里面的说明运行即可。

分布式系统监控系统zipkin入门相关推荐

  1. 开源监控系统 Prometheus 入门

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | 公众号「yangyidba」 一 简介 Pro ...

  2. 【Prometheus】 Prometheus 入门到实战搭建监控系统

    Prometheus (普罗米修斯)是一款基于时序数据库的开源监控告警系统,说起 Prometheus 则不得不提 SoundCloud,这是一个在线音乐分享的平台,类似于做视频分享的 YouTube ...

  3. 视频监控系统由哪几部分组成?(视频监控入门基础-附思维导图)

    视频监控系统是安全技术防范工程的核心部分,也是很多弱电工程新人踏入行业中最先接触的部分.以前就经常有人咨询白哥,初入弱电行业该如何学起,我给的建议就是以视频监控系统为起点.为支点,围绕这套系统不断扩大 ...

  4. Prometheus监控系统入门与部署

    Prometheus监控系统入门与部署 本文介绍新一代的监控系统 Prometheus,并指导用户如何一步一步搭建一个 Prometheus 系统. 什么是 Prometheus ? Promethe ...

  5. 一篇文章带你入门zabbix监控系统

    目录 一.监控介绍 二.监控软件区别 三.zabbix监控架构 四.zabbix监控介绍 1.zabbix优点 2.zabbix缺点 3.zabbix监控系统监控对象 4.zabbix监控方式 五.z ...

  6. Nightingale滴滴夜莺监控系统入门(三)--页面功能说明

    Nightingale滴滴夜莺监控系统入门(三) 功能模块 V3.4.1 用户资源中心 资产管理系统 任务执行中心 监控告警系统 监控看图 监控大盘 告警策略 部署客户端 生产环境开放服务端端口 部署 ...

  7. Nightingale滴滴夜莺监控系统入门(五)--采集功能

    Nightingale滴滴夜莺监控系统入门(五)–采集功能 不知不觉夜莺已经更新到3.6版本,后续会议3.6来演示夜莺支持采集[端口][进程][日志][自定义插件]以及在3.5版本以后支持的主动采集[ ...

  8. Prometheus 监控系统入门与实践

    原文地址:https://www.ibm.com/developerworks/cn/cloud/library/cl-lo-prometheus-getting-started-and-practi ...

  9. 华为吴晟:分布式监控系统的设计与实现

    微服务架构其实就是将单一的应用程序划分成为一组小的服务,其中每个服务都是独立的业务单元,同时又能够被独立开发.运行.测试以及部署.简单来说,它的本质其实就是拆分和独立,这也决定了微服务的部署应该是分布 ...

最新文章

  1. 在apache中使用 memcache 来作 session 存储
  2. 第十章:Java_IO流
  3. Schedulerx2.0工作流支持数据传输
  4. java中fis和fos_java中-的流-与操作
  5. 美团点评成中国第三大互联网公司!
  6. 博士和博士后的有什么区别?
  7. python读取字符串按列分配后按行读出
  8. pc 浏览器最小字体12px
  9. [数学建模] TOPSIS法(考虑权重和不考虑权重)--评价类问题
  10. asdm java设置,[小技巧] 在CISCO ASA 5505防火墙上开启ASDM图形界面
  11. 生活-急救常识(2)
  12. Windows10系统设置共享文件夹和访问共享文件夹方法
  13. 【读书笔记】Flickr 网站用户标签的质量控制对策
  14. 如何分配资源和管理资源
  15. bittorrent协议
  16. 中国裸眼3D视频广告定制市场动态分析与发展策略研究报告2022-2028年
  17. Mysql DBA 高级运维学习之路-mysql数据库乱码问题
  18. JTS Java空间几何计算、距离、最近点、subLine等计算
  19. 多线程采集表情包,下一届斗图王者属于你
  20. python中的pai怎么打_python 调用win32pai 操作cmd的方法

热门文章

  1. Python程序开发——第八章 文件
  2. Shell脚本函数(函数传参、递归、创建库)
  3. exfat linux 读写速度,Ubuntu / Xubuntu : 读写 exFAT 文件系统
  4. php 匹配正则,php正则匹配类
  5. python删除文本中指定内容_Python实现删除文件中含“指定内容”的行示例
  6. 地理防灾减灾思维导图_17张思维导图,让你轻松学好高中地理必修一
  7. gddr6速率_GDDR6 显存两年后问世:比 GDDR5X 更快,速率可达 16Gbps
  8. python文本分析的开源工具_重磅开源:TN文本分析语言
  9. python项目部署到url_项目上线部署
  10. mina mysql_Mina学习笔记(二)