微服務架構中完成一項功能常常會在多個服務之間遠程調用(RPC),造成調用鏈。每一個服務節點可能在不一樣的機器上甚至是不一樣的集羣上,須要能追蹤整個調用鏈,以便在服務調用出錯或延時較高時準肯定位問題。
如下內容引用 Dapper,大規模分佈式系統的跟蹤系統 譯文 ,介紹了分佈式服務追蹤的重要性以及設計原則:java
當代的互聯網的服務,一般都是用複雜的、大規模分佈式集羣來實現的。互聯網應用構建在不一樣的軟件模塊集上,這些軟件模塊,有多是由不一樣的團隊開發、可能使用不一樣的編程語言來實現、有可能布在了幾千臺服務器,橫跨多個不一樣的數據中心。所以,就須要一些能夠幫助理解系統行爲、用於分析性能問題的工具。
舉一個跟搜索相關的例子,這個例子闡述了Dapper能夠應對哪些挑戰。好比一個前段服務可能對上百臺查詢服務器發起了一個Web查詢,每個查詢都有本身的Index。這個查詢可能會被髮送到多個的子系統,這些子系統分別用來處理廣告、進行拼寫檢查或是查找一些像圖片、視頻或新聞這樣的特殊結果。根據每一個子系統的查詢結果進行篩選,獲得最終結果,最後彙總到頁面上。咱們把這種搜索模型稱爲「全局搜索」(universal search)。總的來講,這一次全局搜索有可能調用上千臺服務器,涉及各類服務。並且,用戶對搜索的耗時是很敏感的,而任何一個子系統的低效都致使致使最終的搜索耗時。若是一個工程師只能知道這個查詢耗時不正常,可是他無從知曉這個問題究竟是由哪一個服務調用形成的,或者爲何這個調用性能差強人意。首先,這個工程師可能沒法準確的定位到此次全局搜索是調用了哪些服務,由於新的服務、乃至服務上的某個片斷,都有可能在任什麼時候間上過線或修改過,有多是面向用戶功能,也有多是一些例如針對性能或安全認證方面的功能改進。其次,你不能苛求這個工程師對全部參與此次全局搜索的服務都瞭如指掌,每個服務都有多是由不一樣的團隊開發或維護的。再次,這些暴露出來的服務或服務器有可能同時還被其餘客戶端使用着,因此此次全局搜索的性能問題甚至有多是由其餘應用形成的。舉個例子,一個後臺服務可能要應付各類各樣的請求類型,而一個使用效率很高的存儲系統,好比Bigtable,有可能正被反覆讀寫着,由於上面跑着各類各樣的應用。
上面這個案例中咱們能夠看到,對Dapper咱們只有兩點要求:無所不在的部署,持續的監控。無所不在的重要性不言而喻,由於在使用跟蹤系統的進行監控時,即使只有一小部分沒被監控到,那麼人們對這個系統是否是值得信任都會產生巨大的質疑。另外,監控應該是7x24小時的,畢竟,系統異常或是那些重要的系統行爲有可能出現過一次,就很難甚至不太可能重現。那麼,根據這兩個明確的需求,咱們能夠直接推出三個具體的設計目標:mysql
在 spring cloud 技術棧中, spring cloud Sleuth 借鑑了 Google Dapper 的實現, 提供了分佈式服務追蹤的解決方案。git
Spring Cloud Sleuth 提供了兩種追蹤信息收集的方式,一種是經過 http 的方式,一種是經過 異步消息 的方式,這裏以生產環境經常使用的 異步消息 的收集方式爲例。
在以前建立的項目上作修改,增長 Spring Cloud Sleuth 分佈式服務追蹤功能。
修改 add-service-demo
的 pom.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-stream
和 spring-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>
在日誌信息中增長用來追蹤的 TraceId、SpanId ,Export 表示是否導出到 zipkin 。
以前在 基於docker部署的微服務架構(四): 配置中心 的內容中已經配置了 rabbitmq,用於 spring cloud bus,因此這裏就不用再配消息隊列了,用以前配置的 rabbitmq 就能夠了。
這時候啓動 add-service-demo 工程,能夠看到控制檯輸出的日誌信息增長了 TraceId、SpanId 的相關信息,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.java
:sql
@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-demo 、 random-service-demo 、 service-gateway-demo ,經過網關調用接口 http://localhost/add-service/randomAdd。查看日誌能夠發現 從 service-gateway-demo 到 add-service-demo 再到 random-service-demo 中輸出的日誌信息,包含相同的 TraceId ,代表處於一個調用鏈。
經過上邊的配置,在服務調用的過程當中 spring cloud sleuth 自動幫咱們添加了 TraceId 、 SpanId 等服務追蹤須要的內容。如今還須要集中收集這些信息,並提供可視化界面把這些信息展現出來。
Zipkin 是 Twitter 的一個開源項目,容許開發者收集各個服務上的監控數據,並提供查詢接口。spring cloud sleuth 對 zipkin 作了封裝,提供了兩種數據保存方式:內存和 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-sleuth
、spring-cloud-stream-binder-rabbit
,這兩個和以前項目中引入的同樣,都是消息驅動的 sleuth 相關依賴。zipkin-autoconfigure-ui
引入了 zipkin 相關依賴,最後引入了 mysql 和 jdbc 的依賴,用於保存追蹤數據。
在 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.type
爲 mysql。
在 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 中查詢不到數據,這是由於 sleuth 有一個採樣率的概念,並不會發送全部的數據,能夠經過配置 spring.sleuth.sampler.percentage
指定數據採樣的百分比。
重複屢次訪問 http://localhost/add-service/randomAdd 調用接口,就能在 zipkin 中查詢到數據了。
還能夠查看服務間的調用鏈:
這部份內容和前面幾篇文章基本相同,都是把容器間的訪問地址和 --link
參數對應,再也不贅述。
若是使用 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 熔斷系統。