在微服務的概念成型以前,絕大部分基於WEB的應用都是使用單體的風格來進行構建的。在單體架構中,應用程序做爲單個可部署的軟件製品交付,
全部的UI(用戶接口)、業務、數據庫訪問邏輯都被打包在一個應用程序中而且部署在一個應用服務器上。
隨着單體應用的規模和複雜度的增加,在該應用上進行開發的團隊的溝通與合做成本沒有減小。
當各個團隊須要修改代碼時,整個應用程序都要從新構建、從新測試和部署。
微服務的概念最初是在2014年先後蔓延到軟件開發社區當中[馬丁福勒]
微服務是一個小的、鬆耦合的分佈式服務。微服務容許將一個大型應用分解爲具備嚴格職責定義的便於管理的組件。
信奉的概念:分解分離的應用程序的功能,使他們徹底彼此獨立。
1.應用程序邏輯分解爲具備明肯定義了職責範圍的細粒度組件、這些組件互相協調提供解決方案 2.每一個組件都有一個小的職責領域、而且徹底獨立部署。可跨多個應用程序複用。
3.微服務通訊基於一些基本的原則、採用HTTP和JSON輕量級通訊協議,在消費者和服務者間進行數據交換。
4.構建在微服務上的應用程序可以使用多種編程語言和技術進行構建
5.小、獨立、分佈書、鬆耦合
1.其軟件的複雜度上升 2.客戶期待更快的交付 3.性能和可伸縮性
基礎設施即服務(Infrastructure as a Service ) IaaS
平臺其服務(Platform as a Service ) PaaS
軟件即服務 (Software as a Service ) SasS
容器即服務 (Container as a CaaS ) CaaS
參考 html
1.物理服務器 2.虛擬機鏡像 3.虛擬容器
1.大小適中 2.位置透明 3.有彈性 4.可重複 5.可伸縮
服務粒度:java
將業務劃分爲微服務
通訊協議:git
xml
json
thrift
接口設計:github
如何設計實際的接口服務,便於開發人員進行服務調用
服務的配置管理:web
如何管理微服務的配置,以便在不一樣的雲環境之間移動時,沒必要要跟改核心應用程序代碼或配置
服務之間的事件處理:redis
如何使用事件解藕微服務,以便最小化服務之間的硬編碼依賴關係 ,並提升應用程序的彈性。
服務發現算法
如何使微服務變得能夠被發現,以便將客戶端應用程序在不須要將服務位置硬編碼到應用程序中
服務路由spring
如何爲全部的服務提供單個入口點,以便將安全策略和路由規則統一應用於微服務應用程序中的多個服務和服務實例
客戶端負載均衡sql
如何在服務器客戶端上緩存服務實例的位置,以便對微服務的多個實例調用負載均衡到該微服務的全部健康實例
斷路器模式docker
如何阻止客戶繼續調用出現故障的或遭遇性能問題的服務
後備模式
當服務調用失敗,如何提供「插件」機制,容許服務的客戶端嘗試經過調用微服務以外的其餘方法來執行工做
艙壁模式
微服務應用程序採用多個分佈式資源來執行工做
驗證
如何肯定調用服務的客戶端就是他們聲稱的那個主體
受權
如何肯定調用微服務的客戶端是否容許執行他們正在進行的操做
憑證管理和傳播
如何避免客戶端每次都要提供憑據信息才能訪問事物中涉及的服務調用
日誌關聯
一個用戶會調用多個服務,如何將這些服務所產生的日誌關聯到一塊兒
日誌聚合
將微服務(及其各個實例)生成的全部日誌合併到一個可查詢的數據庫中以及使用關聯ID來協助搜索聚合日誌
微服務跟蹤
將全部服務中的可視化客戶端事物流程,事物所涉及的服務的性能特性
構建和部署管道
如何構建一個可重複的構建和部署過程,只需一鍵便可構建和部署到組織中的任何環境
基礎設施即代碼
如何將服務的基礎設施做爲可在源代碼下執行和管理的代碼去對待
不可變服務
一旦建立了微服務鏡像,如何確保它在部署以後永遠不會更改
鳳凰服務器
服務器運行時間越長就愈加生配置漂移。如何確保微服務的服務器按期被拆卸,並從新建立一個不可變的鏡像
spring cloud config 經過集中式服務來處理應用程序配置數據的管理,所以應用程序配置數據(特別是環境特定配置數據)與部署的微服務徹底分離。這確保了不管啓動多少個微服務實例,這些微服務實例始終都有相同的配置。spring cloud config 擁有本身的屬性管理存儲庫,也能夠與如下開源項目集成 Consul------是一個開源的服務發現工具,容許服務實例向該服務註冊本身。服務客戶端能夠向Consul 諮詢服務實例的位置。consul 還包括能夠被spring cloud config使用的基於鍵值存儲的數據庫,可以用來存儲應用程序的配置數據。 Eureka-----=是一個開源的Netflix項目,像Consul同樣,提供相似的服務發現功能。Eureka一樣有一個能夠被Spring cloud config 使用的鍵值數據庫。
經過spring cloud 服務發現,開發人員能夠從客戶端消費的服務中抽象出部署服務器的物理位置(IP或者服務器名稱)服務消費者經過邏輯名稱而不是物理位置來調用服務器的業務邏輯。
spring cloud 服務發現也處理服務實例的註冊和註銷(在服務實例啓動和關閉時)Spring cloud 服務法系可使用Consul 和 Eureka做爲服務發現引擎
spirng cloud 與 Netflix 的開源項目進行大量的整合,對於微服務客戶端彈性模式,spring cloud 封裝了Netflix Hystrix 庫和Netflix Ribbon 項目,開發人員能夠輕鬆的在微服務中使用他們。 Netflix Hystrix庫 ----> 可快速實現服務客戶端彈性模式,如斷路器模式,艙壁模式 Netflix Ribbo ----> 項目簡化了與諸如Eureka這樣的服務發現代理的集成,但他也爲服務消費者提供了客戶端對服務器的調用的負載均衡。即便在服務代理暫時不可用的狀況下,客戶端也能夠進行服務調用。
spring cloud 使用 Netflix Zuul 項目做爲微服務應用程序提供服務路由功能。Zuul是代理服務請求的服務網關,確保在調用目標以前,對微服務的全部調用都通過一個前門,經過集中的服務調用,開發人員能夠強制執行標準服務策略,如安全受權驗證,內容過濾和路由規則
spring cloud stream 是一種可以讓開發人員輕鬆的將輕量級消息處理集成到微服務中的支持技術。
開發人員能夠構建智能的微服務,它可使用在應用程序中出現的異步事件,此外使用spirng cloud steam 能夠快速將微服務與消息代理進行整合,如RabbitMQ、Kafka
spring cloud sleuth 容許將惟一跟蹤標識符集成到應用程序所使用的HTTP調用和消息通道(RabbitMQ、Kafka)之中。這些跟蹤ID可以讓開發人員在事物流經應用程序中的不一樣服務時跟蹤事務。有了spring cloud sleuth,這些跟蹤ID將子自動添加到微服務生成的任何日誌記錄當中
spring cloud sleuth 與日誌聚合技術工具(如:Parpertrail)和跟蹤工具(如:Zipkin)結合時可以展示出真正的威力。
Papertail 是一個基於雲的日誌記錄平臺,用於將日誌從不一樣的微服務實時聚合到一個可查詢的數據庫中。
Zipkin能夠獲取spring cloud sleuth 生成的數據,並容許開發人員可視化單個事物涉及的服務調用流程
spirng cloud security 是一個驗證和受權框架,能夠控制哪些人能夠服務服務,以及他們能夠用服務作什麼。
spring cloud security 是基於令牌的,容許服務經過驗證服務器發出的令牌彼此進行通訊。接收調用的每一個服務能夠檢查HTTP調用中提供的令牌,以確認用戶的身份以及用戶對該服務的訪問權限。
此外、spring cloud security 支持JWT , JWT 框架標準化了建立OAuth2令牌的格式,併爲建立的令牌進行數字簽名提供了標準。
package com.mikey.microservicedemo; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @SpringBootApplication @RestController @RequestMapping("hello") @EnableCircuitBreaker//TODO:使服務可以使用 Hystrix 和 Ribbon 庫 @EnableEurekaClient //TODO: 告訴服務,他因該使用Eureka服務發現來代理註冊自身,而且服務調用是使用服務發現來「查找」遠程服務的位置 public class MicroservicedemoApplication { public static void main(String[] args) { SpringApplication.run(MicroservicedemoApplication.class, args); } @HystrixCommand(threadPoolKey = "helloThreadPool") public String helloRemoteServiceCall(String fistName,String lastName) {//TODO:包裝器使用Hystrix斷路器調用helloRemoteServiceCall方法 RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> restExchange = restTemplate.exchange( "http://logical-service-id/name/[ca]{firstName}/{lastName}", //TODO:使用一個裝飾好的RestTemplate類來獲取一個「邏輯」服務ID,Eureka在幕後查找服務的物理位置 HttpMethod.GET, null, String.class, fistName,lastName); return restExchange.getBody(); } }
1.微服務是很是小的功能部件,負責一個特定的範圍領域
2.微服務並無行業標準,與其餘早期的web服務協議不一樣,微服務採用原則導向的方法,並與REST和JSON的概念相一致!
3.編寫微型服務很容易,但徹底能夠將其用於生產環境則須要額外的深謀遠慮,微服務開發模式:核心開發模式,路由模式,客戶端彈性模式,安全模式,日誌記錄和跟蹤模式以及構建和部署模式。
4.雖然微服務和語言無關,兩個Spring框架,spring boot 和 spring cloud 有助於構建微服務。
5.spring boot 用於簡化基於REST的JSON微服務的構建,其目標是讓用戶只須要少許的註解,就能快速構建微服務。
6.spirng cloud 是Netflix 和 HashiCorp等公司開源技術的集合,他們已經用spring 註解進行了包裝,從而顯著簡化了這些服務 的設置和配置。
1.緊耦合 2.有漏洞 3.單體的
1.有約束的 2.鬆耦合的 3.抽象的 4.獨立的
1.擁有龐大而多樣化的用戶羣體 2.極高的運行時間要求 3.不均勻的容量需求
1.開始的時候可讓微服務涉及的範圍跟普遍一些,而後將其重構到更小的服務 2.重點關注服務如何相互交互 3.隨着對問題域的理解不斷增加,服務的職責將隨着時間的推移而改變
1.服務承擔過多的職責 2.該服務正在跨大量表來管理數據 3.測試用例過多 4.問題域的一部分像兔子同樣繁殖 5.微服務彼此間嚴重相互依賴 6.微服務成爲簡單的CRUD
1.擁抱REST的理念 2.使用URI來傳達意圖 3.請求和響應使用JSON 4.使用HTTP狀態碼來傳達結果
1.構建分佈式系統的複雜性 2.虛擬服務器/容器散亂 3.應用程序的類型 4.數據事務和一致性
1.要想經過微服務得到成功,須要綜合架構師、軟件開發人員、DevOps的視角 2.微服務是一種強大的架構範型、他有缺點和優勢。並不是全部的應用程序都應該所微服務應用程序。 3.從架構師的角度來看,微服務是小型的、獨立的和分佈式的。微服務應具備狹窄的邊界,並管理一小組數據。 4.從開發人員的角度來看,微服務一般使用REST風格的設計構建,JSON做爲服務發送和接收數據的淨荷 5.Spring Boot是構建微服務的理想框架,由於它容許開發人員使用簡單的註解便可構建基於REST的JSON服務 6.從DevOps的角度來看。微服務如何打包、部署和監控相當重要 7.開箱即用。Spring boot容許用戶單個可執行JAR文件交付服務。JAR文件中的嵌入式Tomcat服務器承載該服務 8.Spring boot框架附帶的Spring Actuator會公開有關服務運行健康情況信息以及有關服務運行時的信息
原則: 1.分離------>咱們但願將服務配置信息與服務的實際物理部署徹底分開。應用程序配置不該該與服務實例一塊兒部署。相反,配置信息應該做爲環境變量傳遞給正在啓動的服務,或者在服務啓動時從集中式存儲庫中讀取。 2.抽象------>將訪問配置數據的功能抽象到一個服務接口中。應用程序使用基於REST的JSON服務來檢索配置數據,而不是編寫直接訪問服務存儲庫的代碼(也就是從文件或者JDBC從數據庫獲取。 3.集中------>由於基於雲的應用程序可能會有數百個服務,因此最小化用於保存配置信息的不一樣存儲庫的數量是相當重要的。將應用程序配置集中在儘量少的存儲庫中。 4.穩定------>由於應用程序的配置信息與部署的訪問徹底隔離並集中存放,因此無論採用何種方案實現。相當重要的一點就是保證其高可用和英語冗餘 關鍵點:將配置信息與實際代碼分開以後,開發人員將建立一個須要進行管理和版本控制的外部依賴項。
1.當一個微服務實例出現時,它將調用一個服務點來讀取其所在環境的特定配置信息。配置管理的鏈接信息(鏈接憑證,服務端點)將在微服務啓動時被傳遞給微服務
2.實際的配置信息駐留在存儲庫中,基於配置存儲庫的實現,能夠選擇使用不一樣的實現來保存配置數據。配置存儲庫的實現選擇能夠包括源代碼控制下的文件,關係型數據庫或鍵值數據庫
3.應用程序配置數據的實際管理與應用程序的部署方式無關。配置管理的更改一般經過構建和部署管道來處理,其中配置的更改能夠經過版本信息進行標記,並經過不一樣的環境進行部署
4.進行配置管理更改時,必須通知使用該應用程序配置數據的服務,並刷新應用程序數據的副本。
Project Name | Descripton | Characteristics |
Etcd | Open source project written in Go. Used |
Very fast and scalable |
Eureka | Written by Netflix. Extremely battle-tested. |
Distribute key-value store. |
Consul | Written by Hashicorp. Similar to Etcd and |
Fast |
ZooKeeper | An Apache project that offers distributed |
Oldest, most battle-tested of the solutions |
Spring Cloud configuration server | An open source project that offers a |
Non-distributed key/value store |
須要完成的內容: 1.建立一個Spring Cloud配置服務器,並演示兩種不一樣的機制來提供應用程序配置數據,一種使用文件系統,另外一種使用Git存儲庫。 2.繼續構建許可證服務以從數據庫中檢索數據 3.將Spring Cloud 配置服務掛鉤(hook)到許可證服務,以提供應用程序配置數據。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.thoughtmechanix</groupId> <artifactId>licensing-service</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>Eagle Eye Licensing Service</name> <description>Licensing Service</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.4.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.1-901.jdbc4</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-rsa</artifactId> </dependency> </dependencies> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <start-class>com.thoughtmechanix.licenses.Application</start-class> <docker.image.name>johncarnell/tmx-licensing-service</docker.image.name> <docker.image.tag>chapter3</docker.image.tag> </properties> <build> <plugins> <!-- We use the Resources plugin to filer Dockerfile and run.sh, it inserts actual JAR filename --> <!-- The final Dockerfile will be created in target/dockerfile/Dockerfile --> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resources</id> <!-- here the phase you need --> <phase>validate</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${basedir}/target/dockerfile</outputDirectory> <resources> <resource> <directory>src/main/docker</directory> <filtering>true</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.10</version> <configuration> <imageName>${docker.image.name}:${docker.image.tag}</imageName> <dockerDirectory>${basedir}/target/dockerfile</dockerDirectory> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
編寫配置文件:
resources
--licensingservice
example.property: "I AM IN THE DEFAULT"
spring.jpa.database: "POSTGRESQL"
spring.datasource.platform: "postgres"
spring.jpa.show-sql: "true"
spring.database.driverClassName: "org.postgresql.Driver"
spring.datasource.url: "jdbc:postgresql://database:5432/eagle_eye_local"
spring.datasource.username: "postgres"
spring.datasource.password: "{cipher}4788dfe1ccbe6485934aec2ffeddb06163ea3d616df5fd75be96aadd4df1da91"
spring.datasource.testWhileIdle: "true"
spring.datasource.validationQuery: "SELECT 1"
spring.jpa.properties.hibernate.dialect: "org.hibernate.dialect.PostgreSQLDialect"
redis.server: "redis"
redis.port: "6379"
signing.key: "345345fsdfsf5345"
--organizationservice
example.organization.property: "I AM THE DEFAULT ORGANIZATION SERVICE"
值的注意的是:
在構建配置服務時,它將成爲在環境中運行的另外一個微服務。一旦創建配置服務,服務的內容就能夠經過基於HTTP的REST端點進行訪問。
The naming convention for the application configuration files are appname-env.yml.
for example :
licensingservice.yml
licensingservice-dev.yml
licensingservice-prod.yml
package com.thoughtmechanix.confsvr; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.config.server.EnableConfigServer; @SpringBootApplication @EnableConfigServer //TODO: 使服務成爲Spring Cloud Config 服務 public class ConfigServerApplication { public static void main(String[] args) { SpringApplication.run(ConfigServerApplication.class, args); } }
### Classpath and file-based solution ### server: port: 8888 spring: profiles: active: native cloud: config: server: native: searchLocations: file://<chapter 3>/confsvr/src/main/resources/config/licensingservice, file://<chapter 3>confsvr/src/main/resources/config/organizationservice ## #searchLocations: classpath:config/,classpath:config/licensingservice
訪問:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.thoughtmechanix</groupId> <artifactId>licensing-service</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>Eagle Eye Licensing Service</name> <description>Licensing Service</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.4.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 告訴Springboot將要在服務中使用Java Persistence Api (jpa)--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <!-- 告訴springboot拉取spring cloud config 客戶端所需的全部依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <!-- 告訴springboot拉取Postgres Jdbc驅動器--> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.1-901.jdbc4</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-rsa</artifactId> </dependency> </dependencies> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <start-class>com.thoughtmechanix.licenses.Application</start-class> <docker.image.name>johncarnell/tmx-licensing-service</docker.image.name> <docker.image.tag>chapter3</docker.image.tag> </properties> <build> <plugins> <!-- We use the Resources plugin to filer Dockerfile and run.sh, it inserts actual JAR filename --> <!-- The final Dockerfile will be created in target/dockerfile/Dockerfile --> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resources</id> <!-- here the phase you need --> <phase>validate</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${basedir}/target/dockerfile</outputDirectory> <resources> <resource> <directory>src/main/docker</directory> <filtering>true</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.10</version> <configuration> <imageName>${docker.image.name}:${docker.image.tag}</imageName> <dockerDirectory>${basedir}/target/dockerfile</dockerDirectory> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
spring: #三個必要配置的屬性
application:
name: licensingservice #指定許可證服務的名稱,以便Spring Cloud Config 客戶端知道正在查找哪一個服務
profiles:
active:
default #指定服務應運行的默認profile oprofile映射到環境
cloud:
config:
uri: http://localhost:8888 #指定Spring cloud config 服務器的位置
https://github.com/carnellj/spmia-chapter3
package com.thoughtmechanix.licenses.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ServiceConfig{
@Value("${example.property}")
private String exampleProperty;
public String getExampleProperty(){
return exampleProperty;
}
}
server:
port: 8888
spring:
cloud:
config:
server:
encrypt.enabled: false
git:
uri: https://github.com/carnellj/config-repo/
searchPaths: licensingservice,organizationservice
username: native-cloud-apps
password: 0ffended
package com.thoughtmechanix.licenses; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.context.config.annotation.RefreshScope; @SpringBootApplication @RefreshScope //TODO:配置服務器刷新屬性 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
默認狀況下,spring cloud 配置服務器在應用程序配置文件中以純文本格式存儲全部屬性,包括像數據庫憑據這樣的敏感信息。
Spring Cloud Config 支持使用對稱加密(共享密鑰)和非對稱加密(公鑰/私鑰)
適用java8 JCE 下載地址: https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
解壓:unzip -O CP936 jce_policy-8.zip
查看安裝教程
cat README.txt
---------------------------------------------------------------------- Installation ---------------------------------------------------------------------- Notes: o Unix (Solaris/Linux/Mac OS X) and Windows use different pathname separators, so please use the appropriate one ("\", "/") for your environment. o <java-home> (below) refers to the directory where the JRE was installed. It is determined based on whether you are running JCE on a JRE or a JRE contained within the Java Development Kit, or JDK(TM). The JDK contains the JRE, but at a different level in the file hierarchy. For example, if the JDK is installed in /home/user1/jdk1.8.0 on Unix or in C:\jdk1.8.0 on Windows, then <java-home> is: /home/user1/jdk1.8.0/jre [Unix] C:\jdk1.8.0\jre [Windows] If on the other hand the JRE is installed in /home/user1/jre1.8.0 on Unix or in C:\jre1.8.0 on Windows, and the JDK is not installed, then <java-home> is: /home/user1/jre1.8.0 [Unix] C:\jre1.8.0 [Windows] o On Windows, for each JDK installation, there may be additional JREs installed under the "Program Files" directory. Please make sure that you install the unlimited strength policy JAR files for all JREs that you plan to use. Here are the installation instructions: 1) Download the unlimited strength JCE policy files. 2) Uncompress and extract the downloaded file. This will create a subdirectory called jce. This directory contains the following files: README.txt This file local_policy.jar Unlimited strength local policy file US_export_policy.jar Unlimited strength US export policy file 3) Install the unlimited strength policy JAR files. In case you later decide to revert to the original "strong" but limited policy versions, first make a copy of the original JCE policy files (US_export_policy.jar and local_policy.jar). Then replace the strong policy files with the unlimited strength versions extracted in the previous step. The standard place for JCE jurisdiction policy JAR files is: <java-home>/lib/security [Unix] <java-home>\lib\security [Windows]
關於對稱密鑰,要注意如下幾點: 1.對稱密鑰的長度應該是12個或更多字符,最好是一個隨機的字符集。 2.不要丟失對稱密鑰。一旦使用加密密鑰加密某些東西,若是沒有對稱密鑰就沒法解密。
對稱加密密鑰 環境變量
export ENCRYPT_KEY = IMSYMMETRIC
在啓動Spring Cloud Config 實例時,Spring Cloud Config 將檢測到環境變量ENVRYPT_KEY 已設置,並自動將兩個新端點(/ecrypt和/decrypt)添加到Spring Cloud Config 服務。咱們將使用/encrypr端點進行加密。
要解密這個值,可使用/decrypt端點,在調用中傳遞已加密的字符串。
如今可使用如下語法將已加密的屬性添加到Github或基於文件系統的許可證服務的配置文件中
spring cloud config 服務器要求全部已加密的屬性前加上{cipher}。{cipher}告訴spirng cloud config 它正在處理已加密的值,啓動Spring cloud config 服務器,並使用GET方法訪問: http://loaclhost:8888/licensingservice/default
可是訪問default時仍是以明文的形式傳遞
要讓客戶端對屬性進行加密,須要作如下三件事情: 1.配置Spring Cloud Config 不要在服務器端解密屬性 2.在許可證服務器上設置對稱密鑰 3.將spring-security-rsa JAR 添加到許可證服務的pom.xml文件中
spring: cloud: config: server: encrypt.enabled: false #配置Spring Cloud Config 不要在服務器端解密屬性
由於許可證服務如今負責解密已經加密的屬性,因此須要在許可證服務上設置對稱加密,方法是確保ENCRYPT_KEY 環境變量與Spring Cloud Config 服務器使用的對稱密鑰相同(如IMSYMMETRIC)
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-rsa</artifactId> </dependency>
1.Spring Cloud 配置服務器容許使用特定值建立應用程序屬性 2.Spring 使用 Spring profile 來啓動服務,以肯定要從 Spring Cloud Config 服務檢索哪些環境變量屬性。 3.Spring Cloud 配置服務可使用基於文件或者Git的應用程序配置存儲庫來存儲應用程序屬性。 4.Spring Cloud 配置服務運行使用對稱加密和非對稱加密對敏感屬性文件進行加密。
這種模型適用於在企業數據中心內部運行的應用程序,以及在一組靜態服務器上運行少許服務的狀況,但對基於雲的微服務應用程序來講,這種模型並不適用
緣由: 1.單點故障------雖然負載均衡能夠實現高可用,但這是整個基礎設施的單點故障 2.有限的水平可伸縮------在服務器集羣中到單個負載均衡集羣的狀況下,跨多個服務器水平伸縮負載均衡基礎設施的能力有限。 3.靜態管理------大多數傳統的負載均衡器不是爲快速註冊和註銷服務設計的,他們使用集中式數據庫來存儲規則的路由,添加新路由的惟一方法一般是經過供應商專有API(Application Programming Interface 應用程序編程接口)來進行添加。 4.複雜------因爲負載均衡器充當服務的代理,它必須將服務消費者的請求映射到物理服務。
特色
1.高可用------服務發現須要可以支持「熱」集羣環境,在服務發現集羣中能夠跨多個節點共享服務查找。若是一個節點變得不可用,集羣的其餘節點應該可以接管工做 2.點對點------服務發現集羣中的每一個節點共享服務實例的狀態 3.負載均衡------服務發現須要在全部的服務實例之間動態的對請求進行負載均衡,以確保服務調用分佈在由它管理的全部服務實例上。在許多方面,服務發現取代了許多早期Web應用程序實現中使用的跟靜態的、手動管理的負載均衡器
4.有彈性------服務發現的客戶端應該在本地「緩存」服務信息。本地緩存容許服務發現功能逐步降級,這樣若是服務發現服務變得不可用,應用程序仍然能夠基於本地緩存中維護的信息來運行和定位服務。
5.容錯------服務發現須要檢測出服務實例何時是不健康的,並從能夠接收客戶端請求的可用服務列表中移除該實例。服務發現應該在沒有人爲干預的狀況下,對這些故障進行檢測,並採起行動。
6.瞭解基於雲的服務發現代理的工做方式的概念架構
7.展現即便在服務發現代理不可用時,客戶端緩存和負載均衡如何使服務能繼續發揮做用
8.瞭解而後使用Spring Cloud 和 Netflix的Eureka服務發現代理實現服務發現功能
服務註冊
服務地址的客戶端查找
信息共享
健康檢測
服務一般只在一個服務發現實例中進行註冊。大多數服務發現的實現使用數據傳播的點對點模型,每一個服務實例的數據都被傳遞到服務發現集羣中的全部其餘節點。
每一個服務實例將經過服務發現服務去推送服務實例狀態,或者服務發現服務從服務實例拉取狀態。任何未能返回良好的健康檢查信息的服務都將從服務實例池中刪除。
分析:
服務在向服務發現服務進行註冊以後,這個服務就能夠被須要使用這項服務功能的應用程序或其餘服務使用,客戶端可使用不一樣的模型來發現服務,在每次調用服務時,
客戶端能夠只依賴於服務發現引擎來解析服務位置,使用這種方法,每次調用註冊的微服務實例時,服務發現引擎就會被調用。可是,這種方法很脆弱,由於服務客戶端
徹底依賴服務發現引擎來查找和調用服務。
一種更健壯的方法是使用所謂的客戶端負載均衡,以下
在這個模型中,當服務消費者須要調用一個服務時: 1.它將聯繫服務發現服務,獲取它請求的全部服務實例,而後在服務消費者的機器上本地緩存數據。 2.每當客戶端須要調用該服務時,服務消費者將從緩存中查找該服務的位置信息。一般客戶端緩存將使用簡單負載均衡算法,如:輪詢負載均衡算法,以確保服務調用分佈在多個服務實例之間。 3.而後,客戶端將按期與服務發現服務進行聯繫,並刷新服務實例的緩存。客戶端緩存最終會一致,可是始終存在這樣的風險: 客戶端聯繫服務發現實例以進行刷新和調用時,調用可能胡會被定向到不健康的服務器上。 若是在調用服務的過程當中,服務調用失敗,那麼本地的服務發現緩存將失效,服務發現客戶端,將嘗試從服務發現代理刷新數據
建立一個服務發現代理來實現服務發現,而後經過代理註冊兩個服務。接着經過使用服務發現檢索到的信息,讓一個服務調用另外一個服務。
服務發現:spring cloud 和 Netflix 的Eureka
負載均衡:spirng cloud 和 Netflix 的Ribbon
過程圖:
1.隨着服務的啓動,許可證和組織服務將經過Eureka服務進行註冊。這個註冊過程將告訴Eureka每一個服務實例的物理位置和端口號,以及正在啓動的服務的服務ID。 2.當許可證服務調用組織服務時,許可證服務將使用Netflix Ribbon 庫來提供客戶端負載均衡。Ribbon將聯繫Eureka服務去檢索服務位置信息,而後在本地進行緩存。 3.Netflix Ribbon庫將按期對Eureka服務進行ping操做,並刷新服務位置的本地緩存。任何新的組織服務實例如今都將在本地對許可證服務可見,而任何不健康實例都將從本地緩存中移除。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.thoughtmechanix</groupId> <artifactId>eurekasvr</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>Eureka Server</name> <description>Eureka Server demo project</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.4.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies> <!--Docker build Config--> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <start-class>com.thoughtmechanix.eurekasvr.EurekaServerApplication</start-class> <java.version>1.8</java.version> <docker.image.name>johncarnell/tmx-eurekasvr</docker.image.name> <docker.image.tag>chapter4</docker.image.tag> </properties> <build> <plugins> <!-- We use the Resources plugin to filer Dockerfile and run.sh, it inserts actual JAR filename --> <!-- The final Dockerfile will be created in target/dockerfile/Dockerfile --> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resources</id> <!-- here the phase you need --> <phase>validate</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${basedir}/target/dockerfile</outputDirectory> <resources> <resource> <directory>src/main/docker</directory> <filtering>true</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.10</version> <configuration> <imageName>${docker.image.name}:${docker.image.tag}</imageName> <dockerDirectory>${basedir}/target/dockerfile</dockerDirectory> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
#Default port is 8761 server: port: 8761 #Eureka 服務器將要監聽的端口 eureka: client: registerWithEureka: false #不要使用Eureka服務進行註冊 fetchRegistry: false #不要在本地緩存註冊表信息 server: waitTimeInMsWhenSyncEmpty: 5 #在服務器接收請求以前等待的初始時間 serviceUrl: defaultZone: http://localhost:8761
package com.thoughtmechanix.eurekasvr; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
每次服務註冊須要30s的時間才能顯示在Eureka服務中,由於Eureka須要從服務器接收三次連續心跳包ping,每次心跳包ping間隔10s,而後才能使用這個服務。
引入Eureka庫,以即可以使用Eureka註冊服務
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>
eureka: instance: preferIpAddress: true #註冊服務的IP而不是服務名稱 client: registerWithEureka: true #向Eureka註冊服務 fetchRegistry: true serviceUrl: #拉取註冊表的本地副本 defaultZone: http://localhost:8761/eureka/ #Eureka服務的位置
spring: application: name: organizationservice #將使用Eureka註冊的服務的邏輯名稱 profiles: active: default cloud: config: enabled: true
爲何偏向於IP地址
在默認狀況下,Eureka在嘗試註冊服務時,將會使用主機名讓外界與它進行聯繫。這種方式,在基於服務器的環境中運行良好,在這樣的環境中,服務將被分配一個DNS支持的主機名。可是在基於容器的部署中(如Docker),容器將以隨機生成的主機名啓動,而且該容器沒有DNS記錄。
若是沒有將eureka.instance.preperIpAddress設置爲true,那麼客戶端將沒法正確的解析主機名的位置,由於該容器不存在DNS記錄,設置preferIpAddress屬性將通知Eureka服務,客戶端想要經過IP地址進行通告。
eureka.client.registerWithEureka屬性是一個觸發器,它能夠告訴組織服務經過Eureka註冊自己。
這個屬性用於告知Spring Eureka客戶端以獲取註冊表的本地副本。將此屬性設置爲true將在本地緩存註冊表,而不是每次查找服務都調用Eureka服務。每隔30s,客戶端軟件就會從新聯繫Eureka服務以便查看註冊表是否有任何變化
eureka.serviceUrl.defaultZone包含客戶端用於解析服務位置的Eureka服務的列表,該列表以逗號進行分隔
Eureka高可用
創建多個URL服務並不足以實現高可用。eureka.serviceUrl.defaultZone屬性僅爲客戶端提供一個進行通訊的Eureka服務列表。除此以外還須要創建多個Eureka服務,以便相互複製註冊表的內容。
一組Eureka註冊表相互之間使用點對點通訊模型進行通訊,在這種模型中,必須對每一個Eureka服務進行配置,以瞭解集羣中的其餘節點。
查看服務的全部實例:
http://<eureka service>:8761/eureka/apps/<APPID>
For instance, to see the organization service in the registry you can call
http://localhost:8761/eureka/apps/organizationservice
//在Eureka和服務啓動時要保持耐心: 當服務經過Eureka註冊時,Eureka將在30s內等待3次連續的健康檢查,而後才能經過Eureka獲取該服務。這個熱身過程讓開發者們感到疑惑,由於若是他們在服務啓動後當即調用他們的服務,他們會認爲Eureka尚未註冊他們的服務。
這一點在Docker 環境運行的代碼示例中很明顯,由於Eureka服務和應用程序服務(許可證服務和組織服務)都是在同一時間啓動的。在生產環境中,Eureka服務已經在運行,若是讀者正在部署現有的服務,那麼舊服務仍然能夠用於接收請求。
研究3個不一樣的Spring/Netflix客戶端庫,服務消費者可使用他們來和Ribbon進行交互。從最低級別到高級別,這些庫包含了不一樣的與Ribbon進行交互的抽象層次:
Spring DiscoveryClient
啓用了RestTemplate的Spring DiscoveryClient
Netflix Feign客戶端
package com.thoughtmechanix.licenses.controllers; import com.thoughtmechanix.licenses.model.License; import com.thoughtmechanix.licenses.services.LicenseService; import com.thoughtmechanix.licenses.config.ServiceConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.ResponseStatus; import java.util.List; @RestController @RequestMapping(value="v1/organizations/{organizationId}/licenses") public class LicenseServiceController { @Autowired private LicenseService licenseService; @Autowired private ServiceConfig serviceConfig; @RequestMapping(value="/",method = RequestMethod.GET) public List<License> getLicenses( @PathVariable("organizationId") String organizationId) { return licenseService.getLicensesByOrg(organizationId); } @RequestMapping(value="/{licenseId}",method = RequestMethod.GET) public License getLicenses( @PathVariable("organizationId") String organizationId, @PathVariable("licenseId") String licenseId) { return licenseService.getLicense(organizationId, licenseId, ""); } @RequestMapping(value="/{licenseId}/{clientType}",method = RequestMethod.GET) //TODO:clientType肯定Spring REST要使用的客戶端的類型 public License getLicensesWithClient( @PathVariable("organizationId") String organizationId, @PathVariable("licenseId") String licenseId, @PathVariable("clientType") String clientType) { return licenseService.getLicense(organizationId,licenseId, clientType); } @RequestMapping(value="{licenseId}",method = RequestMethod.PUT) public void updateLicenses( @PathVariable("licenseId") String licenseId, @RequestBody License license) { licenseService.updateLicense(license); } @RequestMapping(value="/",method = RequestMethod.POST) public void saveLicenses(@RequestBody License license) { licenseService.saveLicense(license); } @RequestMapping(value="{licenseId}",method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteLicenses( @PathVariable("licenseId") String licenseId, @RequestBody License license) { licenseService.deleteLicense(license); } }
package com.thoughtmechanix.licenses.services; import com.thoughtmechanix.licenses.clients.OrganizationDiscoveryClient; import com.thoughtmechanix.licenses.clients.OrganizationFeignClient; import com.thoughtmechanix.licenses.clients.OrganizationRestTemplateClient; import com.thoughtmechanix.licenses.config.ServiceConfig; import com.thoughtmechanix.licenses.model.License; import com.thoughtmechanix.licenses.model.Organization; import com.thoughtmechanix.licenses.repository.LicenseRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.UUID; @Service public class LicenseService { @Autowired private LicenseRepository licenseRepository; @Autowired ServiceConfig config; @Autowired OrganizationFeignClient organizationFeignClient; @Autowired OrganizationRestTemplateClient organizationRestClient; @Autowired OrganizationDiscoveryClient organizationDiscoveryClient; private Organization retrieveOrgInfo(String organizationId, String clientType){ Organization organization = null; switch (clientType) { case "feign": System.out.println("I am using the feign client"); organization = organizationFeignClient.getOrganization(organizationId); break; case "rest": System.out.println("I am using the rest client"); organization = organizationRestClient.getOrganization(organizationId); break; case "discovery": System.out.println("I am using the discovery client"); organization = organizationDiscoveryClient.getOrganization(organizationId); break; default: organization = organizationRestClient.getOrganization(organizationId); } return organization; } /** * 服務方法 * @param organizationId * @param licenseId * @param clientType * @return */ public License getLicense(String organizationId,String licenseId, String clientType) { License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId); Organization org = retrieveOrgInfo(organizationId, clientType); return license .withOrganizationName( org.getName()) .withContactName( org.getContactName()) .withContactEmail( org.getContactEmail() ) .withContactPhone( org.getContactPhone() ) .withComment(config.getExampleProperty()); } public List<License> getLicensesByOrg(String organizationId){ return licenseRepository.findByOrganizationId( organizationId ); } public void saveLicense(License license){ license.withId( UUID.randomUUID().toString()); licenseRepository.save(license); } public void updateLicense(License license){ licenseRepository.save(license); } public void deleteLicense(License license){ licenseRepository.delete( license.getLicenseId()); } }
使用Spring DiscoveryClient查找服務實例
/* *Spring DiscoveryClient 提供了對Ribbon和Ribbon中緩存的註冊服務的最底層訪問。使用DiscoveryClient能夠查詢Ribbon註冊的全部服務以及這些服務對應的URL */
package com.thoughtmechanix.licenses; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.List; @SpringBootApplication @EnableDiscoveryClient //TODO:激活Spring DiscoveryClient /// @EnableDiscoveryClient註解是Spring cloud的觸發器,其做用是使應用程序可以使DiscoveryClient和Ribbon庫 @EnableFeignClients public class Application { @LoadBalanced @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
package com.thoughtmechanix.licenses.clients; import com.thoughtmechanix.licenses.model.Organization; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import java.util.List; @Component public class OrganizationDiscoveryClient { @Autowired private DiscoveryClient discoveryClient; public Organization getOrganization(String organizationId) { RestTemplate restTemplate = new RestTemplate(); //獲取組織服務的全部實例的列表 List<ServiceInstance> instances = discoveryClient.getInstances("organizationservice"); if (instances.size()==0) return null; //檢索要調用的服務端點 String serviceUri = String.format("%s/v1/organizations/%s",instances.get(0).getUri().toString(), organizationId); ResponseEntity< Organization > restExchange = restTemplate.exchange( serviceUri, HttpMethod.GET, null, Organization.class, organizationId); return restExchange.getBody(); } }
只有在服務須要查詢Ribbon以瞭解哪些服務器和服務實例已經經過它註冊時,才應該直接使用DiscoveryClient 上述代碼存在如下問題 1.沒有利用Ribbon的客戶端負載均衡------儘管經過直接調用DiscoveryClient能夠獲取服務列表,可是要調用哪些返回的服務實例就成了開發人員的責任。 2.開發人員作了太多工做------如今開發人員必須構建一個用來調用服務的URL。儘管這是一件小事,可是編寫的代碼越少意味着須要調試的代碼越少。
/* *一旦應用程序類中經過@EnableDiscoveryClient註解啓用了Spring Discovery由Spirng 框架管理的全部RestTemplate都將注入一個啓用Ribbon的攔截器,這個攔截器將改變使用RestTemplate類建URL的行爲。直接實例化RestTemplate類能夠避免這種行爲 */
使用帶有Ribbon功能的Sping RestTemplate調用服務
package com.thoughtmechanix.licenses; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.List; @SpringBootApplication @EnableDiscoveryClient //TODO:激活Spring DiscoveryClient /// @EnableDiscoveryClient註解是Spring cloud的觸發器,其做用是使應用程序可以使DiscoveryClient和Ribbon庫 @EnableFeignClients public class Application { @LoadBalanced //TODO:告訴Spring Cloud建立一個支持Ribbon的RestTemplate類 @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
package com.thoughtmechanix.licenses.clients; import com.thoughtmechanix.licenses.model.Organization; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class OrganizationRestTemplateClient { @Autowired RestTemplate restTemplate; public Organization getOrganization(String organizationId){ ResponseEntity<Organization> restExchange = restTemplate.exchange( //使用支持Ribbon的RestTemplate時,使用Eureka服務ID來構建目標URL "http://organizationservice/v1/organizations/{organizationId}", HttpMethod.GET, null, Organization.class, organizationId); return restExchange.getBody(); } }
URL中的服務器名稱與經過Eureka註冊的服務組織服務的應用程序ID-------organiaztionservice相匹配 http://{applicationid}/v1/organizations/{organizationId}
啓動Ribbon的RestTemplate類,將解析傳遞給他的URL,並使用傳遞的內容做爲服務器名稱,該服務器名稱做爲從Ribbon查詢服務實例的鍵,實際的服務位置和端口與開發人員徹底抽象隔離。
此外,經過使用RestTemplate類,Ribbon將在全部服務實例之間輪詢負載均衡全部請求。
使用Netflix Feign客戶端調用服務
Netflix 的Feign客戶端是Spring啓用Ribbon的RestTemplate類的替代方案。Feign庫將採用不一樣的方法來調用REST服務,
方法是讓開發人員首先定義一個Java接口,而後使用Spirng Cloud註解來標註接口,以映射Ribbon將要調用的基於Eureka的服務。
Spirng Cloud框架將動態生成一個代理類,用於調用目標REST服務。
package com.thoughtmechanix.licenses; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.List; @SpringBootApplication @EnableDiscoveryClient //TODO:激活Spring DiscoveryClient /// @EnableDiscoveryClient註解是Spring cloud的觸發器,其做用是使應用程序可以使DiscoveryClient和Ribbon庫 @EnableFeignClients //TODO:Feign客戶端 public class Application { @LoadBalanced //TODO:告訴Spring Cloud建立一個支持Ribbon的RestTemplate類 @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
package com.thoughtmechanix.licenses.clients; import com.thoughtmechanix.licenses.model.Organization; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @FeignClient("organizationservice") public interface OrganizationFeignClient { @RequestMapping( method= RequestMethod.GET, value="/v1/organizations/{organizationId}", consumes="application/json") Organization getOrganization(@PathVariable("organizationId") String organizationId); }
要使用OrganizationFeignClient類,開發人員須要作的只是自動裝配並使用它。Feign客戶端代碼將爲開發人員承擔全部的編碼工做
錯誤處理
使用標準的Spring RestTemplate類時,全部的服務調用的HTTP狀態碼都將經過ResponseEntity類的getStatusCode()方法返回。經過Feign客戶端,任何被調用的服務返回的HTTP狀態碼4xxx-5xx都將映射爲FeignException。FeignException包含能夠被解析爲特定的錯誤消息的JSON體
1.服務發現模式用於抽象服務的物理位置。 2.諸如Eureka這樣的服務發現引擎能夠在不影響客戶端的狀況下,無縫的向環境中添加和從環境中移除服務實例。 3.經過在進行服務調用的客戶端緩存服務的物理位置,客戶端負載均衡就能夠提供額外的性能和彈性 4.Eureka是Netflix項目,在與Spirng Cloud一塊兒使用時,很容易對Eureka進行創建和配置 5.在Spring cloud 、Netflix Eureka 、Netflix Ribbon中使用了3種不一樣機制來調用服務: 1使用Spring Cloud 服務DiscoveryClient 2使用Spring Cloud 和支持Ribbon 的RestTemplate 3使用Spring Cloud 和Netflix的Feign客戶端
在遠程服務發生錯誤或表現不佳時保護遠程資源(另外一個微服務的調用或數據庫查詢)的客戶端免於崩潰。這些模式的目標是讓客戶端的「快速失敗」,而不消耗諸如數據庫鏈接和線程池之類的寶貴資源。
客戶端服務器負載均衡器位於服務消費者之間,因此負載均衡器能夠檢測服務實例是否拋出錯誤或表現不佳,若是客戶端負載均衡器檢測到問題,它能夠從服務位置池中移除該服務實例,並防止未來的服務調用訪問該服務實例
斷路器模式是模仿電路斷路器的客戶端彈性模式,在電氣系統中,斷路器將檢測是否有過多電流流過電線,若是斷路器檢測到問題,它將斷開與電氣系統的其他部分的鏈接,並保護下游部件不被燒燬。 //有了軟件斷路器,當遠程服務被調用時,斷路器將監視這個調用,若是調用時間太長,斷路器將會介入並中斷調用。此外,斷路器將監視全部遠程資源的調用,若是對某一個遠程資源的調用失敗次數太多,那麼斷路器就會出現並採起快速失敗,阻止未來調用失敗的遠程資源。
當遠程服務調用失敗時,服務消費者將執行替代代碼路徑,並嘗試經過其餘方式執行操做,而不是生成一個異常。這一般涉及從另外一個數據源查找數據或將用戶的請求進行排隊來處理。用戶的調用結果不會顯示爲提示問題的異常,但用戶可能被告知,他們的請求在晚些時候被知足。
艙壁模式是創建在造船的概念上的,採用艙壁設計,一艘船被劃分爲徹底隔離和防水的隔間,這稱爲艙壁。即便船的船體被擊穿,因爲船體被劃分爲水密艙(艙壁),艙壁將水限制在被擊穿的船的區域內,防止整艘船灌水被並沉沒。 //一樣的概念能夠將應用於必須與多個遠程資源交互的服務。經過使用艙壁模式,能夠把遠程資源的調用分到線程池中,並下降一個緩慢的遠程資源調用拖垮整個應用程序的風險,線程池充當服務的「艙壁」每一個遠程資源都是隔離的,
並分配給線程池。若是一個服務響應緩慢,那麼這種服務調用的線程池就會飽和並中止處理請求,而對其餘服務的服務調用則不會變得飽和,由於他們被分配給了其餘線程池。
爲何客戶端負載均衡很重要?
斷路跳閘
//1.快速失敗------當遠程服務處於降級狀態時,應用程序將會快速失敗,並防止一般會拖垮整個應用程序的資源耗盡問題的出現。在大多數中斷狀況下,最好是部分服務關閉而不是徹底關閉 //2.優雅的失敗------經過超時和快速失敗,斷路器模式使應用程序開發人員有能力優雅的失敗,或尋求替代機制來執行用戶的意圖。例如,若是用戶嘗試從一個數據源檢索數據,而且該數據源正在經歷服務降級,那麼應用程序開發人員能夠嘗試從其餘地方檢索該數據。 //3.有了斷路器模式做爲中介,斷路器能夠按期檢查所請求的資源是否從新上線,並在沒有人爲干預的狀況下從新容許對該資源進行訪問。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.thoughtmechanix</groupId> <artifactId>licensing-service</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>Eagle Eye Licensing Service</name> <description>Licensing Service</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.4.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.1-901.jdbc4</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-rsa</artifactId> </dependency> <!-- <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-javanica</artifactId> <version>1.5.9</version> </dependency> --> </dependencies> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <start-class>com.thoughtmechanix.licenses.Application</start-class> <docker.image.name>johncarnell/tmx-licensing-service</docker.image.name> <docker.image.tag>chapter6</docker.image.tag> </properties> <build> <plugins> <!-- We use the Resources plugin to filer Dockerfile and run.sh, it inserts actual JAR filename --> <!-- The final Dockerfile will be created in target/dockerfile/Dockerfile --> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resources</id> <!-- here the phase you need --> <phase>validate</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${basedir}/target/dockerfile</outputDirectory> <resources> <resource> <directory>src/main/docker</directory> <filtering>true</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.10</version> <configuration> <imageName>${docker.image.name}:${docker.image.tag}</imageName> <dockerDirectory>${basedir}/target/dockerfile</dockerDirectory> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
package com.thoughtmechanix.licenses; import com.thoughtmechanix.licenses.utils.UserContextInterceptor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.List; @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker // TODO:告訴Spring cloud服務將要使用Hystrix public class Application { @LoadBalanced @Bean public RestTemplate getRestTemplate(){ RestTemplate template = new RestTemplate(); List interceptors = template.getInterceptors(); if (interceptors==null){ template.setInterceptors(Collections.singletonList(new UserContextInterceptor())); } else{ interceptors.add(new UserContextInterceptor()); template.setInterceptors(interceptors); } return template; } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Hystrix和spirng cloud使用@HystrixCommand註解來將Java類方法標記爲由Hystrix斷路器進行管理。當spring框架看到@HystrixCommand時,它將動態生成一個代理,該代理將包裝該方法,並經過專門用於處理遠程的線程池來管理對該方法的全部調用。
package com.thoughtmechanix.licenses.services; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import com.thoughtmechanix.licenses.clients.OrganizationRestTemplateClient; import com.thoughtmechanix.licenses.config.ServiceConfig; import com.thoughtmechanix.licenses.model.License; import com.thoughtmechanix.licenses.model.Organization; import com.thoughtmechanix.licenses.repository.LicenseRepository; import com.thoughtmechanix.licenses.utils.UserContext; import com.thoughtmechanix.licenses.utils.UserContextHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.UUID; @Service public class LicenseService { @Autowired private LicenseRepository licenseRepository; @Autowired ServiceConfig config; @Autowired OrganizationRestTemplateClient organizationRestClient; private static final Logger logger = LoggerFactory.getLogger(LicenseService.class); @HystrixCommand public License getLicense(String organizationId,String licenseId) throws InterruptedException { License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId); Organization org = getOrganization(organizationId); return license .withOrganizationName( org.getName()) .withContactName( org.getContactName()) .withContactEmail( org.getContactEmail() ) .withContactPhone( org.getContactPhone() ) .withComment(config.getExampleProperty()); } @HystrixCommand private Organization getOrganization(String organizationId) { return organizationRestClient.getOrganization(organizationId); } private void randomlyRunLong(){ Random rand = new Random(); int randomNum = rand.nextInt((3 - 1) + 1) + 1; if (randomNum==3) sleep(); } private void sleep(){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } @HystrixCommand(fallbackMethod = "buildFallbackLicenseList", threadPoolKey = "licenseByOrgThreadPool", threadPoolProperties = {@HystrixProperty(name = "coreSize",value="30"), @HystrixProperty(name="maxQueueSize", value="10"), }, commandProperties={ @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"), @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"), @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"), @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"), @HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")} ) public List<License> getLicensesByOrg(String organizationId){ randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); } private List<License> buildFallbackLicenseList(String organizationId){ List<License> fallbackList = new ArrayList<>(); License license = new License() .withId("0000000-00-00000") .withOrganizationId( organizationId ) .withProductName("Sorry no licensing information currently available"); fallbackList.add(license); return fallbackList; } public void saveLicense(License license){ license.withId( UUID.randomUUID().toString()); licenseRepository.save(license); } public void updateLicense(License license){ licenseRepository.save(license); } public void deleteLicense(License license){ licenseRepository.delete( license.getLicenseId()); } }
對組織微服務的調用超時
咱們可使用方法級註解使被標記的調用擁有斷路器功能,其優勢在於不管是訪問數據庫仍是調用微服務,它都是相同的註解。
for instance:在許可證服務中咱們須要查找與許可證關聯的組織的名稱。若是要使用斷路器來包裝對組織服務的調用的話
一個簡單的方法就是將RestTemplate調用分解到本身的方法,並使用@HystrixCommand註解進行標註:
@HystrixCommand
private Organization getOrganization(String organizationId) {
return organizationRestClient.getOrganization(organizationId);
}
//注意:在默認狀況下,在指定不帶屬性的@HystrixCommand註解時,這個註解會將全部的遠程服務調用都放在同一線程池下。
定製斷路器的超時時間
@HystrixCommand( commandProperties= {@HystrixProperty( name="execution.isolation.thread.timeoutInMilliseconds", value="12000")}) public List<License> getLicensesByOrg(String organizationId){ randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); }
fallbackMethod定義了一個方法,若是來自Hystrix的調用失敗,那麼就會調用該方法。
注意://1.後備方法必須在同一個類中 2.後備方法與原方法必須具備相同的方法簽名。
(1)後備是一種在資源超時或失敗提供行動方案的機制,若是發現本身使用後備來捕獲超時異常,而後只是作日誌記錄,就應該在try...catch...上
(2)注意使用後備方法所執行的操做。若是在後備服務中調用另外一個分佈式服務,就可能須要使用@HystrixCommand註解來包裝後備方法。記住在主要
動方案中經歷的相同的失敗有可能也會影響次要的後備方案。
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList") public List<License> getLicensesByOrg(String organizationId){ randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); } private List<License> buildFallbackLicenseList(String organizationId){ List<License> fallbackList = new ArrayList<>(); License license = new License() .withId("0000000-00-00000") .withOrganizationId( organizationId ) .withProductName( "Sorry no licensing information currently available"); fallbackList.add(license); return fallbackList; }
結果
//艙壁模式將遠程資源調用隔離在他們的本身的線程中,以便控制單個表現不佳的服務,而不會使該容器崩潰。
Hystrix使用線程池來委派全部對遠程服務的請求,在默認狀況下,全部的Hystrix命令都將共享同一個線程池來處理請求
多種資源類型共享默認的Hystrix線程池:
Hystrix命令綁定到隔離的線程池:
//(1)爲getLicensesByOrg()調用創建一個單獨的線程池 //(2)設置線程池的線程數 //(3)設置單個線程繁忙時可排隊的請求數的隊列大小
// 線程池大小 Netflix推薦如下公式:
// 服務在健康狀態時每秒支撐的最大請求數X第99百分位延遲時間(以秒爲單位)+用於緩衝的少許額外線程
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList", threadPoolKey = "licenseByOrgThreadPool", threadPoolProperties = {@HystrixProperty(name = "coreSize",value="30"), @HystrixProperty(name="maxQueueSize", value="10")} ) public List<License> getLicensesByOrg(String organizationId){ return licenseRepository.findByOrganizationId(organizationId); )
//Hystrix不只能超時長時間運行的調用,它還會監控調用失敗的次數,若是調用失敗的次數足夠多,那麼Hystrix會在請求發送到遠程資源以前,經過調用失敗來阻止將來的調用到達服務。
package com.thoughtmechanix.licenses.services; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import com.thoughtmechanix.licenses.clients.OrganizationRestTemplateClient; import com.thoughtmechanix.licenses.config.ServiceConfig; import com.thoughtmechanix.licenses.model.License; import com.thoughtmechanix.licenses.model.Organization; import com.thoughtmechanix.licenses.repository.LicenseRepository; import com.thoughtmechanix.licenses.utils.UserContext; import com.thoughtmechanix.licenses.utils.UserContextHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.UUID; @Service public class LicenseService { @Autowired private LicenseRepository licenseRepository; @Autowired ServiceConfig config; @Autowired OrganizationRestTemplateClient organizationRestClient; private static final Logger logger = LoggerFactory.getLogger(LicenseService.class); @HystrixCommand public License getLicense(String organizationId,String licenseId) throws InterruptedException { License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId); Organization org = getOrganization(organizationId); return license .withOrganizationName( org.getName()) .withContactName( org.getContactName()) .withContactEmail( org.getContactEmail() ) .withContactPhone( org.getContactPhone() ) .withComment(config.getExampleProperty()); } @HystrixCommand private Organization getOrganization(String organizationId) { return organizationRestClient.getOrganization(organizationId); } private void randomlyRunLong(){ Random rand = new Random(); int randomNum = rand.nextInt((3 - 1) + 1) + 1; if (randomNum==3) sleep(); } private void sleep(){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } @HystrixCommand(fallbackMethod = "buildFallbackLicenseList", threadPoolKey = "licenseByOrgThreadPool", threadPoolProperties = {@HystrixProperty(name = "coreSize",value="30"), @HystrixProperty(name="maxQueueSize", value="10"), }, commandProperties={ @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"), @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"), @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"), @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"), @HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")} ) public List<License> getLicensesByOrg(String organizationId){ randomlyRunLong(); return licenseRepository.findByOrganizationId(organizationId); } private List<License> buildFallbackLicenseList(String organizationId){ List<License> fallbackList = new ArrayList<>(); License license = new License() .withId("0000000-00-00000") .withOrganizationId( organizationId ) .withProductName("Sorry no licensing information currently available"); fallbackList.add(license); return fallbackList; } public void saveLicense(License license){ license.withId( UUID.randomUUID().toString()); licenseRepository.save(license); } public void updateLicense(License license){ licenseRepository.save(license); } public void deleteLicense(License license){ licenseRepository.delete( license.getLicenseId()); } }
Hystrix庫是高度可配的,可讓開發人員嚴格控制它定義的斷路器模式和艙壁模式的行爲。開發人員能夠經過修改Hystrix斷路器的配置,控制Hystrix在超時遠程調用以前須要等待的時間。開發人員還能夠控制Hystrix斷路器什麼時候跳匝以及重置斷路器。
三個配置級別:
1.整個應用程序級別的默認值
2.類級別的默認值
3.在類中定義的線程池級別
@HystrixCommond註解的配置值
屬性名稱 | 默認值 | 描述 |
fallbackMethod | None | Identifies the method within the class that will be called if |
threadPoolKey | None | Gives the @HystrixCommand a unique name and creates |
threadPoolProperties | None | Core Hystrix annotation attribute that’s used to configure |
coreSize | 10 | Sets the size of the thread pool. |
maxQueueSize | -1 | Maximum queue size that will set in front of the thread |
circuitBreaker.request- |
20 | Sets the minimum number of requests that must be pro- |
circuitBreaker.error- |
50 | The percentage of failures that must occur within the roll- |
circuitBreaker.sleep- |
5000 | The number of milliseconds Hystrix will wait before trying a |
metricsRollingStats. |
10,000 | The number of milliseconds Hystrix will collect and monitor |
metricsRollingStats |
10 | The number of metrics buckets Hystrix will maintain within |
當一個@HystrixCommand被執行時,它可使用兩種不一樣的隔離策略------THREAD(線程)(默認)和SEMAPHORE(信號量)來運行。
當執行@hystrixcommand時,可使用兩種不一樣的隔離來運行它策略:線程和信號量。默認狀況下,hystrix使用線程隔離運行。用於保護調用的每一個hystrix命令都運行在一個獨立的線程池中不與進行調用的父線程共享其上下文。這意味着海星能夠中斷在其控制下的線程的執行,而沒必要擔憂 中斷與執行原始任務的父線程相關聯的任何其餘活動召喚。經過基於信號量的隔離,hystrix管理受保護的分佈式調用。在不啓動新線程的狀況下經過@hystrixcommand註釋中斷調用超時時的父線程。在同步容器服務器環境中(tomcat),中斷父線程將致使引起異常沒法被開發人員捕獲。這可能會致使 開發人員編寫代碼,由於他們沒法捕獲拋出的異常或執行任何資源清理或錯誤處理。要控制命令池的隔離設置,能夠設置命令-@hystrixcommand註釋的properties屬性。例如,若是你想在hystrix命令上設置隔離級別以使用信號量隔離, 你會用:
@HystrixCommand(
commandProperties = {
@HystrixProperty(
name="execution.isolation.strategy", value="SEMAPHORE")})
在默認狀況下,Hystrix不會將父線程的上下文傳播到由Hystrix命令管理的線程中。例如,在默認狀況下,對被父線程調用並由@HystrixCommand保護的方法而言,在父線程中設置ThreadLoacl值的值都是不可用的(THREAD隔離級別)
讓咱們看一個具體的例子。一般以REST爲基礎要將上下文信息傳遞給服務調用的環境這將幫助您在操做上管理服務。例如,您能夠經過rest調用的http頭中的相關id或身份驗證令牌而後被傳播到任何下游服務調用。相關ID容許您具備惟一的標識符,能夠在單個服務調用中跨多個服務調用進行跟蹤交易。
要使此值在服務調用中的任何位置均可用,可使用spring 過濾器類來攔截進入rest服務的每一個調用,並從傳入的http請求並將此上下文信息存儲在自定義用戶中-內容對象。而後,只要您的代碼須要在rest服務中訪問這個值調用,代碼能夠從threadlocal存儲變量檢索usercontext
並讀取值。下面的清單顯示了一個示例spring過濾器,您能夠在您的受權服務中使用。
package com.thoughtmechanix.licenses.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Component public class UserContextFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; /** * 檢索調用的HTTP首部中設置的值,將這些值賦給存儲在UserContextHolder中的UserContent */ UserContextHolder.getContext().setCorrelationId( httpServletRequest.getHeader(UserContext.CORRELATION_ID) ); UserContextHolder.getContext().setUserId(httpServletRequest.getHeader(UserContext.USER_ID)); UserContextHolder.getContext().setAuthToken(httpServletRequest.getHeader(UserContext.AUTH_TOKEN)); UserContextHolder.getContext().setOrgId(httpServletRequest.getHeader(UserContext.ORG_ID)); logger.debug("License Service Incoming Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); filterChain.doFilter(httpServletRequest, servletResponse); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }
package com.thoughtmechanix.licenses.utils; import org.springframework.util.Assert; public class UserContextHolder { //當前線程 private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>(); public static final UserContext getContext(){ UserContext context = userContext.get(); if (context == null) { context = createEmptyContext(); userContext.set(context); } return userContext.get(); } public static final void setContext(UserContext context) { Assert.notNull(context, "Only non-null UserContext instances are permitted"); userContext.set(context); } public static final UserContext createEmptyContext(){ return new UserContext(); } }
//一旦提交了這個調用,當它流經UserContent、LicenseServiceController、LicenseServer類時,咱們能夠看到3條日誌消息記錄傳入的關聯ID: UserContext Correlation id: TEST-CORRELATION-ID LicenseServiceController Correlation id: TEST-CORRELATION-ID LicenseService.getLicenseByOrg Correlation:
//正如預期的那樣,一旦這個調用使用了Hystrix保護的LicenseService.getLicenseByOrg()方法,就沒法獲得關聯ID的值,幸運的是,Hystrix和Spring Cloud提供了一種機制,能夠將父線程的上下文傳播到有Hystrix線程池管理的線程。這種機制稱爲「HystrixConcurrencyStrategy」
HystrixConcurrencyStrategy實戰
自定義HystrixConcurrentcyStrategy須要執行如下3個操做: 1.定義自定義的Hystrix併發策略 2.定義一個Callable類,將UserContext注入Hystrix命令中 3.配置Spring Cloud以使用自定義Hystrix策略
1.定義自定義的Hystrix併發策略
package com.thoughtmechanix.licenses.hystrix; import com.netflix.hystrix.HystrixThreadPoolKey; import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable; import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle; import com.netflix.hystrix.strategy.properties.HystrixProperty; import com.thoughtmechanix.licenses.utils.UserContext; import com.thoughtmechanix.licenses.utils.UserContextHolder; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy{ private HystrixConcurrencyStrategy existingConcurrencyStrategy; /** * Spring cloud已經定義了一個併發類。將已存在的併發策略傳入自定義的HystrixConcurrencyStrategy的類的構造器中 * @param existingConcurrencyStrategy */ public ThreadLocalAwareStrategy( HystrixConcurrencyStrategy existingConcurrencyStrategy) { this.existingConcurrencyStrategy = existingConcurrencyStrategy; } @Override public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) { return existingConcurrencyStrategy != null ? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize) : super.getBlockingQueue(maxQueueSize); } @Override public <T> HystrixRequestVariable<T> getRequestVariable( HystrixRequestVariableLifecycle<T> rv) { return existingConcurrencyStrategy != null ? existingConcurrencyStrategy.getRequestVariable(rv) : super.getRequestVariable(rv); } @Override public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { return existingConcurrencyStrategy != null ? existingConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue) : super.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } @Override public <T> Callable<T> wrapCallable(Callable<T> callable) { return existingConcurrencyStrategy != null ? existingConcurrencyStrategy .wrapCallable(new DelegatingUserContextCallable<T>(callable, UserContextHolder.getContext())) : super.wrapCallable(new DelegatingUserContextCallable<T>(callable, UserContextHolder.getContext())); } }
2.定義一個Callable類,將UserContext注入Hystrix命令中
package com.thoughtmechanix.licenses.hystrix; import com.thoughtmechanix.licenses.utils.UserContext; import com.thoughtmechanix.licenses.utils.UserContextHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import java.util.concurrent.Callable; public final class DelegatingUserContextCallable<V> implements Callable<V> { private static final Logger logger = LoggerFactory.getLogger(DelegatingUserContextCallable.class); private final Callable<V> delegate; //private final UserContext delegateUserContext; private UserContext originalUserContext; /** * 原始Callable類將被傳遞到自定義的Callable類,自定義Callable將調用Hystrix保護的代碼和來自父線程的UserContent * @param delegate * @param userContext */ public DelegatingUserContextCallable(Callable<V> delegate, UserContext userContext) { Assert.notNull(delegate, "delegate cannot be null"); Assert.notNull(userContext, "userContext cannot be null"); this.delegate = delegate; this.originalUserContext = userContext; } public DelegatingUserContextCallable(Callable<V> delegate) { this(delegate, UserContextHolder.getContext()); } /** * call方法在被@HystriCommand註解保護的方法以前調用 * @return * @throws Exception */ public V call() throws Exception { /** * 已設置UserContent.存儲UserContent的ThreadLocal變量與運行受Hystrix保護的方法的線程相關聯 */ UserContextHolder.setContext( originalUserContext ); try { /** * UserContent設置後,在Hystrix保護的方法上調用call方法,如LicenseServer.getLicenseByOrg方法 */ return delegate.call(); } finally { this.originalUserContext = null; } } public String toString() { return delegate.toString(); } public static <V> Callable<V> create(Callable<V> delegate, UserContext userContext) { return new DelegatingUserContextCallable<V>(delegate, userContext); } }
3.配置Spring Cloud以使用自定義Hystrix策略
package com.thoughtmechanix.licenses.hystrix; import com.netflix.hystrix.strategy.HystrixPlugins; import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; @Configuration public class ThreadLocalConfiguration { /** * 當構造配置對象時,它將自動裝配在現有的HystrixConcurrencyStrategy中 */ @Autowired(required = false) private HystrixConcurrencyStrategy existingConcurrencyStrategy; @PostConstruct public void init() { // Keeps references of existing Hystrix plugins. //由於要註冊一個新的併發策略,因此要獲取全部其餘的Hystrix組件,而後從新設置Hystrix插件 HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance() .getEventNotifier(); HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance() .getMetricsPublisher(); HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance() .getPropertiesStrategy(); HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance() .getCommandExecutionHook(); HystrixPlugins.reset(); /** * 使用Hystrix插件註冊自定義的Hystrix併發策略(ThreadConcurrencyStrategy) */ HystrixPlugins.getInstance().registerConcurrencyStrategy(new ThreadLocalAwareStrategy(existingConcurrencyStrategy)); /** * 而後從新註冊Hystrix插件使用全部Hystrix插件 */ HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); } }
//記住:Hystrix只容許有一個HystrixConcurrencyStrategy..spring將嘗試自動裝配在現有的任何HystrixConcurrencyStrategy(若是它存在)中。最後完成全部的工做後,咱們使用Hystrix插件把在init()方法獲取的原始Hystrix組件從新註冊回來。
1.在設計高分佈式應用程序(如:基於微服務的應用程序)時,必須考慮客戶端彈性。 2.服務的完全故障(如:服務器崩潰)是很容易檢測和處理的。 3.一個性能不佳的服務可能會引發資源耗盡的連鎖效應,由於調用客戶端的線程被阻塞,以等待服務完成。 4.三種核心客戶端彈性模式:斷路器模式、後備模式、艙壁模式。 5.斷路器模式試圖殺死運行緩慢和降級的系統調用,這樣的調用就會快速失敗,並防止資源耗盡。 6.後備模式容許開發人員在遠程服務調用失敗或斷路器跳匝的狀況下,定義替代代碼路徑。 7.艙壁模式經過將對遠程服務的調用隔離到他們的線程池中,使遠程資源調用彼此分離。就算一組服務調用失敗,這些失敗也不會致使應用程序容器中的全部資源耗盡。 8.Spring cloud 和 Netflix Hystrix庫提供斷路器、後備模式、艙壁模式的實現。 9.Hystrix庫是高度可配置的,能夠在全局、類和線程池級別配置。 10.Hystrix支持兩種隔離模型:THREAD和SEMAPHONRE。 11.Hystrix默認隔離模型THREAD徹底隔離Hystrix保護的調用,但不會將父線程的上下文傳播到Hystrix管理的線程。 12.Hystrixd 另外一種隔離模型SEMAPHORE不使用單獨的線程進行Hystrix調用。雖然這更有效率,但若是Hystrix中斷了調用,它也會讓服務變得不可預測。 13.Hystrix容許經過自定義HystrixConcurrencyStrategy實現,將線程上下文注入Hystrix管理的線程中。
像微服務架構這樣的分佈式架構中,須要確保跨多個服務調用的關鍵行爲的正常運行,如安全、日誌記錄和用戶跟蹤。要實現此功能,開發人員須要在全部服務中始終如一的強制這些特性,而不要每一個開發團隊都構建本身的解決方案。雖然可使用公共庫或框架來幫助在單個服務中直接構建這些功能,但這樣會形成如下影響: 1.在構建每一個服務中很難始終實現這些功能。 2.正確的實現這些功能是一個挑戰。 3.這會在全部的服務中建立一個頑固的依賴。
使用Spring Cloud和Zuul來完成如下操做
1.將全部的服務調用放在一個URL後面,並使用服務發現將這些調用映射到實際的服務實例。 2.將關聯ID注入流經服務網關的每一個服務調用中 3.在從客戶端發回的HTTP響應中注入關聯ID 4.構建一個動態路由機制,將各個具體的組織路由到服務實例端點,該端點與其餘人使用的服務實例不一樣。
無服務網關:服務客戶端將爲每一個服務調用不一樣的端點。
有服務網關:服務網關分離調用的URL,並將路徑映射到服務網關後面的服務。
靜態路由------服務網關將全部的服務調用放在單個URL和API路由的後面。這簡化了開發,由於開發人員只需知道全部的服務的一個服務端點就能夠。 動態路由------服務網關能夠檢查傳入的服務請求,根據來自傳入請求的數據和服務調用者的身份執行智能路由。 驗證和受權-----因爲全部服務調用都要通過服務網關進行路由,因此服務網關是檢查服務調用者是否已經進行了驗證並被受權進行服務調用的天然場所。 度量數據收集和日誌記錄------當服務調用經過服務網關時,可使用服務網關來收集數據和日誌信息,還可使用服務網關確保在用戶請求上提供關鍵信息以確保日誌統一。
這並不意味着不該該從單個服務中收集度量數據,而是經過服務網關能夠集中收集許多基本度量數據,如服務調用次數和服務響應時間。
難道服務網關不是單點故障和潛在瓶頸嗎?
在單獨的服務組面前,負載均衡仍然頗有用,在這種狀況下將負載均衡器放到多個服務網關實例前面的是一個恰當的設計,它確保服務網關實現能夠伸縮。將負載均衡器置於全部服務實例的前面並非一個好主意,由於它將會成爲瓶頸。
要保持服務網關編寫的代碼是無狀態的。不要在內存中爲服務網關存儲任何信息。若是不當心就有可能限制網關的可伸縮性,致使不得不確保數據在全部服務網關實例中被複制。
要保持爲服務網關編寫的代碼是輕量的。服務網關是服務調用「阻塞點」,具備多個數據庫調用的複雜代碼多是服務服務網關中難以追蹤的性能問題的根源。
Spring cloud 和 Netfilx Zuul簡介
Spring cloud 集成了Netflix 開源項目Zuul。Zuul是一個服務網關,它很是容易經過Spirng cloud註解進行建立和使用。Zuul提供了許多功能,具體包括如下: 1.將應用程序中的全部服務的路由映射到一個URL------Zuul不限於一個URL。在Zuul中,開發人員能夠定義多個路由條目,使路由映射很是細粒度(每一個服務端點都有本身的路由映射)。而後Zuul最多見的用例是構建一個單一的入口點,全部服務客戶端調用都要通過這個入口。
2.構建能夠對經過網關的請求進行檢查和操做的過濾器------這些過濾器容許開發人員在代碼中注入策略執行點,以一致的方式對全部服務調用執行大量操做。
//1.創建一個Zuul Springboot項目,並配置適當的Maven依賴項 //2.使用Spring Cloud 註解修改這個Spring Boot 項目,將其聲明爲Zuul服務 //3.配置Zuul以便Eureka進行通訊(可選)
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency>
package com.thoughtmechanix.zuulsvr; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @EnableZuulProxy //TODO:Zuul服務器 public class ZuulServerApplication { @LoadBalanced @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ZuulServerApplication.class, args); } }
注意:
@EnableZuulServer和EnableZuulProxy區別
EnableZuulServer:此註解將建立一個Zuul服務器,它不會加載任何Zuul反向代理過濾器,也不會使用Netflix Eureka進行服務發現。開發人員想要構建本身的路由服務,而不使用任何Zuul預置的功能時會使用@EnableZuulServer舉例來說,
當開發人員須要使用Zuul與Eureka以外的其餘服務發現引擎(如:Consul)進行集成的時候。
spring: application: name: zuulservice profiles: active: default cloud: config: enabled: true server: port: 5555 #Setting logging levels logging: level: com.netflix: WARN org.springframework.web: WARN com.thoughtmechanix: DEBUG eureka: instance: preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/ # # # debug: # request: true # #zuul: # prefix: /api # routes: # organizationservice: /organization/** # licensingservice: /licensing/**
Zuul的核心是一個反向代理。反向代理是一箇中間件服務器,它位於嘗試訪問資源的客戶端和資源自己之間。客戶端甚至不知道它正在與代理以外的服務器進行通訊。反向代理負責捕獲客戶端的請求,而後表明客戶端調用遠程資源。在微服務架構的狀況下,Zuul(反向代理)從客戶端接收微服務調用並將其轉發給下游服務。服務
客戶端認爲它只與Zuul通訊。Zuul要與下游服務進行溝通,Zuul必須知道如何將進來的調用映射到下游路由。Zuul有幾種機制來作到這一點,包括:
1.經過服務發現自動映射路由
2.使用服務發現手動映射路由
3.使用靜態URL手動映射路由
Zuul的全部映射都是經過application.yal文件中定義路由來完成的。可是,zuul能夠根據其服務ID自動路由請求,而不須要配置。若是沒有指定任何路由,zuul將自動使用正在調用的服務的Eureka服務ID,並將其映射到下游服務實例,
例如若是要調用organizationserice並經過Zuul使用自動路由,則可使用如下URL做爲端點,讓客戶端調用Zuul服務實例: http://localhost:5555/organizationservice/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a
server: port: 5555 #Setting logging levels logging: level: com.netflix: WARN org.springframework.web: WARN com.thoughtmechanix: DEBUG eureka: instance: preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/ zuul: prefix: /api routes: organizationservice: /organization/**
使用Eureka的Zuul的優勢在於,開發人員不只能夠擁有一個能夠發出調用的單個端點,有了Eureka開發人員還能夠添加和刪除服務的實例,而無需修改Zuul,例如能夠向Eureka添加新服務,Zuul將自動路由到該服務,由於Zuul會與Eureka進行通訊,瞭解實際服務端點的位置。 訪問:http://local-host:5555/routes
Zuul容許開發人員更細粒度的明肯定義路由映射,而不是單純依賴服務的Eureka服務ID建立的自動路由。
Component | describe |
spring-cloud-aws | |
spring-cloud-bus | |
spring-cloud-cli | |
spring-cloud-commons | |
spring-cloud-contract | |
spring-cloud-config | |
spring-cloud-netflix | |
spring-cloud-security | |
spring-cloud-cloudfoundry | |
spring-cloud-consul | |
spring-cloud-sleuth | |
spring-cloud-stream | |
spring-cloud-zookeeper | |
spring-boot | |
spring-cloud-task | |
spring-cloud-vault | |
spring-cloud-gateway | |
spring-cloud-openfeign | |
spring-cloud-function | |
spring-cloud-function | |
spring-cloud-function | |
spring-cloud-function |