基於docker部署的微服務架構(九): 分佈式服務追蹤 Spring Cloud Sleuth

前言

微服務架構中完成一項功能常常會在多個服務之間遠程調用(RPC),造成調用鏈。每一個服務節點可能在不一樣的機器上甚至是不一樣的集羣上,須要能追蹤整個調用鏈,以便在服務調用出錯或延時較高時準肯定位問題。
如下內容引用 Dapper,大規模分佈式系統的跟蹤系統 譯文 ,介紹了分佈式服務追蹤的重要性以及設計原則:java

當代的互聯網的服務,一般都是用複雜的、大規模分佈式集羣來實現的。互聯網應用構建在不一樣的軟件模塊集上,這些軟件模塊,有多是由不一樣的團隊開發、可能使用不一樣的編程語言來實現、有可能布在了幾千臺服務器,橫跨多個不一樣的數據中心。所以,就須要一些能夠幫助理解系統行爲、用於分析性能問題的工具。
舉一個跟搜索相關的例子,這個例子闡述了Dapper能夠應對哪些挑戰。好比一個前段服務可能對上百臺查詢服務器發起了一個Web查詢,每個查詢都有本身的Index。這個查詢可能會被髮送到多個的子系統,這些子系統分別用來處理廣告、進行拼寫檢查或是查找一些像圖片、視頻或新聞這樣的特殊結果。根據每一個子系統的查詢結果進行篩選,獲得最終結果,最後彙總到頁面上。咱們把這種搜索模型稱爲「全局搜索」(universal search)。總的來講,這一次全局搜索有可能調用上千臺服務器,涉及各類服務。並且,用戶對搜索的耗時是很敏感的,而任何一個子系統的低效都致使致使最終的搜索耗時。若是一個工程師只能知道這個查詢耗時不正常,可是他無從知曉這個問題究竟是由哪一個服務調用形成的,或者爲何這個調用性能差強人意。首先,這個工程師可能沒法準確的定位到此次全局搜索是調用了哪些服務,由於新的服務、乃至服務上的某個片斷,都有可能在任什麼時候間上過線或修改過,有多是面向用戶功能,也有多是一些例如針對性能或安全認證方面的功能改進。其次,你不能苛求這個工程師對全部參與此次全局搜索的服務都瞭如指掌,每個服務都有多是由不一樣的團隊開發或維護的。再次,這些暴露出來的服務或服務器有可能同時還被其餘客戶端使用着,因此此次全局搜索的性能問題甚至有多是由其餘應用形成的。舉個例子,一個後臺服務可能要應付各類各樣的請求類型,而一個使用效率很高的存儲系統,好比Bigtable,有可能正被反覆讀寫着,由於上面跑着各類各樣的應用。
上面這個案例中咱們能夠看到,對Dapper咱們只有兩點要求:無所不在的部署,持續的監控。無所不在的重要性不言而喻,由於在使用跟蹤系統的進行監控時,即使只有一小部分沒被監控到,那麼人們對這個系統是否是值得信任都會產生巨大的質疑。另外,監控應該是7x24小時的,畢竟,系統異常或是那些重要的系統行爲有可能出現過一次,就很難甚至不太可能重現。那麼,根據這兩個明確的需求,咱們能夠直接推出三個具體的設計目標:mysql

  1. 低消耗:跟蹤系統對在線服務的影響應該作到足夠小。在一些高度優化過的服務,即便一點點損耗也會很容易察覺到,並且有可能迫使在線服務的部署團隊不得不將跟蹤系統關停。
  2. 應用級的透明:對於應用的程序員來講,是不須要知道有跟蹤系統這回事的。若是一個跟蹤系統想生效,就必須須要依賴應用的開發者主動配合,那麼這個跟蹤系統也太脆弱了,每每因爲跟蹤系統在應用中植入代碼的bug或疏忽致使應用出問題,這樣纔是沒法知足對跟蹤系統「無所不在的部署」這個需求。面對當下想Google這樣的快節奏的開發環境來講,尤爲重要。
  3. 延展性:Google至少在將來幾年的服務和集羣的規模,監控系統都應該能徹底把控住。
  4. 一個額外的設計目標是爲跟蹤數據產生以後,進行分析的速度要快,理想狀況是數據存入跟蹤倉庫後一分鐘內就能統計出來。儘管跟蹤系統對一小時前的舊數據進行統計也是至關有價值的,但若是跟蹤系統能提供足夠快的信息反饋,就能夠對生產環境下的異常情況作出快速反應。

spring cloud 技術棧中, spring cloud Sleuth 借鑑了 Google Dapper 的實現, 提供了分佈式服務追蹤的解決方案。git

引入 Spring Cloud Sleuth 追蹤系統

Spring Cloud Sleuth 提供了兩種追蹤信息收集的方式,一種是經過 http 的方式,一種是經過 異步消息 的方式,這裏以生產環境經常使用的 異步消息 的收集方式爲例。
在以前建立的項目上作修改,增長 Spring Cloud Sleuth 分佈式服務追蹤功能。
修改 add-service-demopom.xml 文件,增長相關依賴:程序員

<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-sleuth-stream</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>

spring-cloud-starter-sleuth 引入 sleuth 基礎jar包, spring-cloud-sleuth-streamspring-cloud-stream-binder-rabbit 引入經過 異步消息 收集追蹤信息的相關jar包,spring-cloud-starter-feign 引入了 feign,用來遠程調用別的服務(在 基於docker部署的微服務架構(二): 服務提供者和調用者 中有介紹),稍後會建立一個提供隨機數的服務,用來展現服務調用鏈。
而後修改 log4j2.xml 配置文件, 修改日誌格式爲:github

<Property name="PID">????</Property>
    <Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
    <Property name="LOG_LEVEL_PATTERN">%5p</Property>
    <Property name="logFormat">
        %d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN} [@project.artifactId@,%X{X-B3-TraceId},%X{X-B3-SpanId},%X{X-Span-Export}] ${sys:PID} --- [%15.15t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}
    </Property>

在日誌信息中增長用來追蹤的 TraceIdSpanIdExport 表示是否導出到 zipkin
以前在 基於docker部署的微服務架構(四): 配置中心 的內容中已經配置了 rabbitmq,用於 spring cloud bus,因此這裏就不用再配消息隊列了,用以前配置的 rabbitmq 就能夠了。
這時候啓動 add-service-demo 工程,能夠看到控制檯輸出的日誌信息增長了 TraceIdSpanId 的相關信息,INFO [add-service-demo,,,] 18668,可是如今尚未具體的內容,由於沒有發生服務調用。web

建立一個新的工程 random-service-demo,用來生成一個隨機整數。新建 maven 項目,修改 pom.xml 文件,引入相關依賴:spring

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.2.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>

    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-clients</artifactId>
        <version>0.10.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-sleuth-stream</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<properties>
    <!-- 指定java版本 -->
    <java.version>1.8</java.version>
    <!-- 鏡像前綴,推送鏡像到遠程庫時須要,這裏配置了一個阿里雲的私有庫 -->
    <docker.image.prefix>
        registry.cn-hangzhou.aliyuncs.com/ztecs
    </docker.image.prefix>
    <!-- docker鏡像的tag -->
    <docker.tag>demo</docker.tag>

    <!-- 激活的profile -->
    <activatedProperties></activatedProperties>

    <kafka.bootstrap.servers>10.47.160.238:9092</kafka.bootstrap.servers>
</properties>

這裏一樣引入了 Sleuth 相關內容。
建立啓動入口類 RandomServiceApplication.javasql

@EnableDiscoveryClient
  @SpringBootApplication
  public class RandomServiceApplication {

      public static void main(String[] args) {
          SpringApplication.run(RandomServiceApplication.class, args);
      }

  }

resources 中的配置文件能夠徹底複用 add-service-demo 中的配置,由於最終的配置是從 配置中心 中拉取的, resources 只須要配置 config-server 的相關內容便可。
git 倉庫中增長 random-service-demo-dev.yml 配置文件,內容:docker

server:
    port: 8200

  spring:
    rabbitmq:
    host: 10.47.160.238
    port: 5673
    username: guest
    password: guest

配置了端口和消息隊列。apache

建立一個 RandomController.java 對外提供隨機數服務:

@RestController
  @RefreshScope
  public class RandomController {

      private static final Logger logger = LoggerFactory.getLogger(RandomController.class);

      @RequestMapping(value = "/random", method = RequestMethod.GET)
      public Integer random() {
          logger.info(" >>> random");
          Random random = new Random();
          return random.nextInt(10);
      }

  }

業務邏輯很簡單,生成一個 0 ~ 10 的隨機整數並返回。

接下來在 add-service-demo 工程中增長一個隨機數相加的接口,調用 random-service-demo 生成隨機數,並把隨機數相加做爲結果返回。
AddServiceApplication.java 中增長 @EnableFeignClients 註解,開啓 feign 客戶端遠程調用。
增長 RandomService.java 用來遠程調用 random-service-demo 中的接口:

@FeignClient("RANDOM-SERVICE-DEMO")
  public interface RandomService {
      @RequestMapping(method = RequestMethod.GET, value = "/random")
      Integer random();
  }

AddController.java 中增長 randomAdd 方法,並對外暴露接口。在方法中兩次調用 random-service-demo 生成隨機數的接口,把隨機數相加做爲結果返回:

@Autowired
private RandomService randomService;

private static Logger logger = LoggerFactory.getLogger(AddController.class);

@RequestMapping(value = "/randomAdd", method = RequestMethod.GET)
public Map<String, Object> randomAdd() {
    logger.info(">>> randomAdd");
    Integer random1 = randomService.random();
    Integer random2 = randomService.random();
    Map<String, Object> returnMap = new HashMap<>();
    returnMap.put("code", 200);
    returnMap.put("msg", "操做成功");
    returnMap.put("result", random1 + random2);

    return returnMap;
}

修改服務網關 service-gateway-demo 引入 sleuth, 修改 pom.xml 引入依賴(參照 add-service-demo ),修改 log4j2.xml 中的日誌格式(參照 add-service-demo )。

啓動 add-service-demorandom-service-demoservice-gateway-demo ,經過網關調用接口 http://localhost/add-service/randomAdd。查看日誌能夠發現 從 service-gateway-demoadd-service-demo 再到 random-service-demo 中輸出的日誌信息,包含相同的 TraceId ,代表處於一個調用鏈。

使用zipkin收集追蹤信息並展示

經過上邊的配置,在服務調用的過程當中 spring cloud sleuth 自動幫咱們添加了 TraceIdSpanId 等服務追蹤須要的內容。如今還須要集中收集這些信息,並提供可視化界面把這些信息展現出來。
ZipkinTwitter 的一個開源項目,容許開發者收集各個服務上的監控數據,並提供查詢接口。spring cloud sleuthzipkin 作了封裝,提供了兩種數據保存方式:內存和 mysql ,這裏以生產環境中使用的 mysql 持久化方式爲例。

建立一個 maven 工程 zipkin-server-demo,修改 pom.xml 文件增長相關依賴:

<parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>1.4.2.RELEASE</version>
   </parent>

   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter</artifactId>
           <exclusions>
               <exclusion>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-starter-logging</artifactId>
               </exclusion>
           </exclusions>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-log4j2</artifactId>
       </dependency>
       <dependency>
           <groupId>org.apache.kafka</groupId>
           <artifactId>kafka-clients</artifactId>
           <version>0.10.0.1</version>
       </dependency>

       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-sleuth</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
       </dependency>
       <dependency>
           <groupId>io.zipkin.java</groupId>
           <artifactId>zipkin-autoconfigure-ui</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-jdbc</artifactId>
       </dependency>
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
       </dependency>
   </dependencies>

   <dependencyManagement>
       <dependencies>
           <dependency>
               <groupId>org.springframework.cloud</groupId>
               <artifactId>spring-cloud-dependencies</artifactId>
               <version>Camden.SR2</version>
               <type>pom</type>
               <scope>import</scope>
           </dependency>
       </dependencies>
   </dependencyManagement>

   <properties>
       <!-- 指定java版本 -->
       <java.version>1.8</java.version>
       <!-- 鏡像前綴,推送鏡像到遠程庫時須要,這裏配置了一個阿里雲的私有庫 -->
       <docker.image.prefix>
           registry.cn-hangzhou.aliyuncs.com/ztecs
       </docker.image.prefix>
       <!-- docker鏡像的tag -->
       <docker.tag>demo</docker.tag>

       <!-- 激活的profile -->
       <activatedProperties></activatedProperties>

       <kafka.bootstrap.servers>10.47.160.238:9092</kafka.bootstrap.servers>
   </properties>

簡單說明下引入的依賴:spring-cloud-sleuth-zipkin-stream 引入了經過消息驅動的方式收集追蹤信息所須要的 zipkin 依賴, spring-cloud-starter-sleuthspring-cloud-stream-binder-rabbit,這兩個和以前項目中引入的同樣,都是消息驅動的 sleuth 相關依賴。zipkin-autoconfigure-ui 引入了 zipkin 相關依賴,最後引入了 mysqljdbc 的依賴,用於保存追蹤數據。

resources 目錄中新建配置文件 application.yml

server:
    port: 9411

  spring:
    profiles:
      active: @activatedProperties@
    rabbitmq:
      host: 10.47.160.114
      port: 5673
      username: guest
      password: guest
    datasource:
      schema: classpath:/mysql.sql
      url: jdbc:mysql://10.47.160.114:3306/sleuth_log
      username: soa
      password: 123456
      initialize: true
      continueOnError: true
    sleuth:
      enabled: false
    output:
      ansi:
        enabled: ALWAYS

  zipkin:
    storage:
      type: mysql

配置了 zipkin web頁面的端口 9411 ,配置 mysql 和初始化腳本, 並指定 zipkin.storage.typemysql
resources 目錄中建立 mysql 初始化腳本 mysql.sql

CREATE TABLE IF NOT EXISTS zipkin_spans (
    `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
    `trace_id` BIGINT NOT NULL,
    `id` BIGINT NOT NULL,
    `name` VARCHAR(255) NOT NULL,
    `parent_id` BIGINT,
    `debug` BIT(1),
    `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
    `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query'
  ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

  ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore insert on duplicate';
  ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) COMMENT 'for joining with zipkin_annotations';
  ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
  ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
  ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';

  CREATE TABLE IF NOT EXISTS zipkin_annotations (
    `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
    `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
    `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
    `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
    `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
    `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
    `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
    `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
    `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
    `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
    `endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
  ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

  ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
  ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
  ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
  ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
  ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces';
  ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';

  CREATE TABLE IF NOT EXISTS zipkin_dependencies (
    `day` DATE NOT NULL,
    `parent` VARCHAR(255) NOT NULL,
    `child` VARCHAR(255) NOT NULL,
    `call_count` BIGINT
  ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

  ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);

此腳本初始化了 zipkin 保存追蹤數據須要的表。
新建 log4j2.xml 配置文件,能夠把其餘項目中的複製過來( add-service-demo 等),內容都是同樣的。

建立啓動入口類 ZipkinServerApplication.java

@SpringBootApplication
  @EnableZipkinStreamServer
  public class ZipkinServerApplication {

      public static void main(String[] args) {
          SpringApplication.run(ZipkinServerApplication.class, args);
      }

  }

運行 main 方法啓動 zipkin,訪問 http://localhost:9411 打開頁面。
zipkin

有可能在 zipkin 中查詢不到數據,這是由於 sleuth 有一個採樣率的概念,並不會發送全部的數據,能夠經過配置 spring.sleuth.sampler.percentage 指定數據採樣的百分比。
重複屢次訪問 http://localhost/add-service/randomAdd 調用接口,就能在 zipkin 中查詢到數據了。
zipkin data

zipkin data 2

還能夠查看服務間的調用鏈: zipkin dependency

使用docker-maven-plugin打包並生成docker鏡像

這部份內容和前面幾篇文章基本相同,都是把容器間的訪問地址和 --link 參數對應,再也不贅述。

demo源碼 spring-cloud-4.0目錄

grok插件解析日誌內容

若是使用 ELK 進行日誌分析的話,可使用 grok 插件解析 spring cloud sleuth 追蹤系統的日誌信息(關於 ELK 系統的部署,能夠參閱 基於docker部署的微服務架構(七): 部署ELK日誌統計分析系統 )。
修改 logstash 的配置文件,增長 grok filter:

filter {
    grok {
      match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}---\s+\[%{DA
  TA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
    }
  }

這樣就能夠解析日誌信息了。

最後

分佈式服務追蹤在微服務架構中是很是重要的一部分,在發生異常時須要經過追蹤系統來定位問題。Spring Cloud Sleuth 基於 Google Dapper 提供了一個簡單易用的分佈式追蹤系統。
在生產環境中,只有追蹤系統還不夠,在服務調用發生錯誤時,好比:網絡延時、資源繁忙等,這種錯誤每每會形成阻塞,形成後續訪問困難,在高併發狀況下,調用服務失敗時若是沒有隔離措施,會波及到整個服務端,進而使整個服務端崩潰。
因此還須要一個熔斷系統,對服務依賴作隔離和容錯。下一篇將會介紹 hystrix 熔斷系統。

相關文章
相關標籤/搜索