技術背景
上一篇教程中,我們利用Consul注冊中心,實現了服務的注冊和發現功能,這一篇我們來聊聊服務的調用。單體應用中,代碼可以直接依賴,在代碼中直接調用即可,但在微服務架構是分布式架構,服務都運行在各自的進程之中,甚至部署在不同的主機和不同的地區。這個時候就需要相關的遠程調用技術了。
Spring Cloud體系里應用比較廣泛的服務調用方式有兩種:
1. 使用 RestTemplate 進行服務調用,可以通過 Ribbon 注解 RestTemplate 模板,使其擁有負載均衡的功能。
2. 使用 Feign 進行聲明式服務調用,聲明之后就像調用本地方法一樣,Feign 默認使用 Ribbon實現負載均衡。
兩種方式都可以實現服務之間的調用,可根據情況選擇使用,下面我們分別用實現案例來進行講解。
服務提供者
新建項目
新建一個項目 kitty-producer,添加以下依賴。
Swagger:API文檔。
Consul :注冊中心。
Spring Boot Admin:服務監控。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.louis</groupId> <artifactId>kitty-producer</artifactId> <version>${project.version}</version> <packaging>jar</packaging> <name>kitty-producer</name> <description>kitty-producer</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.version>1.0.0</project.version> <java.version>1.8</java.version> <swagger.version>2.8.0</swagger.version> <mybatis.spring.version>1.3.2</mybatis.spring.version> <druid.version>1.1.10</druid.version> <spring.boot.admin.version>2.0.0</spring.boot.admin.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <dependencies> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <!--spring-boot-admin--> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>${spring.boot.admin.version}</version> </dependency> <!--consul--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
配置文件
在配置文件添加內容如下,將服務注冊到注冊中心並添加服務監控相關配置。
application.yml
server: port: 8003 spring: application: name: kitty-producer cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 注冊到consul的服務名稱 boot: admin: client: url: "http://localhost:8000" # 開放健康檢查接口 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS
啟動類
修改啟動器類,添加 @EnableDiscoveryClient 注解,開啟服務發現支持。
KittyProducerApplication.java
package com.louis.kitty.producer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication public class KittyProducerApplication { public static void main(String[] args) { SpringApplication.run(KittyProducerApplication.class, args); } }
添加服務
新建一個 HelloController,提供一個 hello 接口, 返回字符串信息。
package com.louis.kitty.producer.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @RequestMapping("/hello") public String hello() { return "hello kitty !"; } }
為了模擬均衡負載,復制一份上面的項目,重命名為 kitty-producer2 ,修改對應的端口為 8004,修改 hello 方法的返回值為:"hello kitty 2!"。
依次啟動注冊中心、服務監控和兩個服務提供者,啟動成功之后刷新Consul管理界面,發現我們注冊的kitty-producer服務,並有2個節點實例。
訪問: http://localhost:8500, 查看兩個服務提供者已經注冊到注冊中心。
訪問: http://localhost:8000, 查看兩個服務提供者已經成功顯示在監控列表中。
訪問 http://localhost:8003/hello,返回結果如下。
訪問 http://localhost:8004/hello,返回結果如下。
服務消費者
新建項目
新建一個項目 kitty-producer,添加以下依賴。
Swagger:API文檔。
Consul :注冊中心。
Spring Boot Admin:服務監控。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>com.louis</groupId> <artifactId>kitty-consumer</artifactId> <version>${project.version}</version> <packaging>jar</packaging> <name>kitty-consumer</name> <description>kitty-consumer</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.version>1.0.0</project.version> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <dependencies> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <!--spring-boot-admin--> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>${spring.boot.admin.version}</version> </dependency> <!--consul--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
添加配置
修改配置文件如下。
application.yml
server: port: 8005 spring: application: name: kitty-consumer cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 注冊到consul的服務名稱 boot: admin: client: url: "http://localhost:8000" # 開放健康檢查接口 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS
啟動類
修改啟動器類,添加 @EnableDiscoveryClient 注解,開啟服務發現支持。
KittyConsumerApplication.java
package com.louis.kitty.consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication public class KittyConsumerApplication { public static void main(String[] args) { SpringApplication.run(KittyConsumerApplication.class, args); } }
服務消費者
添加消費服務測試類,添加兩個接口,一個查詢所有我們注冊的服務,另一個從我們注冊的服務中選取一個服務,采用輪詢的方式。
ServiceController.java
package com.louis.kitty.consumer.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ServiceController { @Autowired private LoadBalancerClient loadBalancerClient; @Autowired private DiscoveryClient discoveryClient; /** * 獲取所有服務 */ @RequestMapping("/services") public Object services() { return discoveryClient.getInstances("kitty-producer"); } /** * 從所有服務中選擇一個服務(輪詢) */ @RequestMapping("/discover") public Object discover() { return loadBalancerClient.choose("kitty-producer").getUri().toString(); } }
添加完成之后,啟動項目,訪問:http://localhost:8500,服務消費者已經成功注冊到注冊中心。
訪問:http://localhost:8000,服務消費者已經成功顯示在監控列表中。
訪問 http://localhost:8005/services,返回兩個服務,分別是我們注冊的8003和8004。
[{ "serviceId": "kitty-producer", "host": "GG20J1G2E.logon.ds.ge.com", "port": 8003, "secure": false, "metadata": { "secure": "false" }, "uri": "http://GG20J1G2E.logon.ds.ge.com:8003", "scheme": null }, { "serviceId": "kitty-producer", "host": "GG20J1G2E.logon.ds.ge.com", "port": 8004, "secure": false, "metadata": { "secure": "false" }, "uri": "http://GG20J1G2E.logon.ds.ge.com:8004", "scheme": null }]
反復訪問 http://localhost:8005/discover,結果交替返回服務8003和8004,因為默認的負載均衡器是采用輪詢的方式。
8003 和 8004 兩個服務會交替出現,從而實現了獲取服務端地址的均衡負載。
大多數情況下我們希望使用均衡負載的形式去獲取服務端提供的服務,因此使用第二種方法來模擬調用服務端提供的 hello 方法。
創建 CallHelloController.java
package com.louis.kitty.consumer.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class CallHelloController { @Autowired private LoadBalancerClient loadBalancer; @RequestMapping("/call") public String call() { ServiceInstance serviceInstance = loadBalancer.choose("kitty-producer"); System.out.println("服務地址:" + serviceInstance.getUri()); System.out.println("服務名稱:" + serviceInstance.getServiceId()); String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class); System.out.println(callServiceResult); return callServiceResult; } }
使用 RestTemplate 進行遠程調用。添加完之后重啟 kitty-consumer 項目。
在瀏覽器中訪問地址: http://localhost:8005/call 依次往復返回結果如下:
負載均衡器(Ribbon)
在上面的教程中,我們是這樣調用服務的,先通過 LoadBalancerClient 選取出對應的服務,然后使用 RestTemplate 進行遠程調用。
LoadBalancerClient 就是負載均衡器,默認使用的是 Ribbon 的實現 RibbonLoadBalancerClient,采用的負載均衡策略是輪詢。
1. 查找服務,通過 LoadBalancer 查詢服務。
ServiceInstance serviceInstance = loadBalancer.choose("kitty-producer");
2.調用服務,通過 RestTemplate 遠程調用服務。
String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class);
這樣就完成了一個簡單的服務調用和負載均衡。接下來我們說說Ribbon。
Ribbon是Netflix發布的負載均衡器,它有助於控制HTTP和TCP的客戶端的行為。為Ribbon配置服務提供者地址后,Ribbon就可基於某種負載均衡算法,自動地幫助服務消費者去請求。Ribbon默認為我們提供了很多負載均衡算法,例如輪詢、隨機等。當然,我們也可為Ribbon實現自定義的負載均衡算法。
ribbon內置負載均衡策略:
策略名 | 策略聲明 | 策略描述 | 實現說明 |
BestAvailableRule | public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule | 選擇一個最小的並發請求的server | 逐個考察Server,如果Server被tripped了,則忽略,在選擇其中ActiveRequestsCount最小的server |
AvailabilityFilteringRule | public class AvailabilityFilteringRule extends PredicateBasedRule | 過濾掉那些因為一直連接失敗的被標記為circuit tripped的后端server,並過濾掉那些高並發的的后端server(active connections 超過配置的閾值) | 使用一個AvailabilityPredicate來包含過濾server的邏輯,其實就就是檢查status里記錄的各個server的運行狀態 |
WeightedResponseTimeRule | public class WeightedResponseTimeRule extends RoundRobinRule | 根據響應時間分配一個weight,響應時間越長,weight越小,被選中的可能性越低。 | 一個后台線程定期的從status里面讀取評價響應時間,為每個server計算一個weight。Weight的計算也比較簡單responsetime 減去每個server自己平均的responsetime是server的權重。當剛開始運行,沒有形成status時,使用roubine策略選擇server。 |
RetryRule | public class RetryRule extends AbstractLoadBalancerRule | 對選定的負載均衡策略機上重試機制。 | 在一個配置時間段內當選擇server不成功,則一直嘗試使用subRule的方式選擇一個可用的server |
RoundRobinRule | public class RoundRobinRule extends AbstractLoadBalancerRule | roundRobin方式輪詢選擇server | 輪詢index,選擇index對應位置的server |
RandomRule | public class RandomRule extends AbstractLoadBalancerRule | 隨機選擇一個server | 在index上隨機,選擇index對應位置的server |
ZoneAvoidanceRule | public class ZoneAvoidanceRule extends PredicateBasedRule | 復合判斷server所在區域的性能和server的可用性選擇server | 使用ZoneAvoidancePredicate和AvailabilityPredicate來判斷是否選擇某個server,前一個判斷判定一個zone的運行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用於過濾掉連接數過多的Server。 |
修改啟動類
我們修改一下的啟動器類,注入 RestTemplate,並添加 @LoadBalanced 注解(用於攔截請求),以使用 ribbon 來進行負載均衡。
KittyConsumerApplication.java
package com.louis.kitty.consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableDiscoveryClient @SpringBootApplication public class KittyConsumerApplication { public static void main(String[] args) { SpringApplication.run(KittyConsumerApplication.class, args); } @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
添加服務
新建一個 RibbonHelloController 類,注入 RestTemplate,並調用服務提供者的hello服務。
package com.louis.kitty.consumer.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class RibbonHelloController { @Autowired private RestTemplate restTemplate; @RequestMapping("/ribbon/call") public String call() { // 調用服務, service-producer為注冊的服務名稱,LoadBalancerInterceptor會攔截調用並根據服務名找到對應的服務 String callServiceResult = restTemplate.getForObject("http://kitty-producer/hello", String.class); return callServiceResult; } }
測試效果
啟動消費者服務,訪問 http://localhost:8005/ribbon/call,依次返回結果如下:
說明 ribbon 的負載均衡已經成功啟動了。
負載策略
修改負載均衡策略很簡單,只需要在配置文件指定對應的負載均衡器即可。如這里把策略修改為隨機策略。
application.yml
#ribbon 負載均衡策略配置, service-producer為注冊的服務名 service-producer: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
如上,修改成隨機負載均衡策略之后,負載均衡器會隨機選取注冊的服務。
服務消費(Feign)
Spring Cloud Feign是一套基於Netflix Feign實現的聲明式服務調用客戶端。它使得編寫Web服務客戶端變得更加簡單。我們只需要通過創建接口並用注解來配置它既可完成對Web服務接口的綁定。它具備可插拔的注解支持,包括Feign注解、JAX-RS注解。它也支持可插拔的編碼器和解碼器。Spring Cloud Feign還擴展了對Spring MVC注解的支持,同時還整合了Ribbon來提供均衡負載的HTTP客戶端實現。
添加依賴
修改 kitty-consumer 的 pom 文件,添加 feign 依賴。
pom.xml
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
啟動類
修改啟動器類,添加 @EnableFeignClients 注解開啟掃描Spring Cloud Feign客戶端的功能:
KittyConsumerApplication.java
package com.louis.kitty.consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication public class KittyConsumerApplication { public static void main(String[] args) { SpringApplication.run(KittyConsumerApplication.class, args); } @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
添加Feign接口
添加 KittyProducerService接口, 在類頭添加注解 @FeignClient("kitty-producer") ,kitty-producer是要調用的服務名。
添加跟調用目標方法一樣的方法聲明,只需要方法聲明,不需要具體實現,注意跟目標方法定義保持一致。
KittyProducerService.java
package com.louis.kitty.consumer.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; @FeignClient(name = "kitty-producer") public interface KittyProducerService { @RequestMapping("/hello") public String hello(); }
添加控制器
添加 FeignHelloController控制器,注入 KittyProducerService,就可以像使用本地方法一樣進行調用了。
FeignHelloController.java
package com.louis.kitty.consumer.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.louis.kitty.consumer.feign.KittyProducerService; @RestController public class FeignHelloController { @Autowired private KittyProducerService kittyProducerService; @RequestMapping("/feign/call") public String call() { // 像調用本地服務一樣 return kittyProducerService.hello(); } }
測試效果
啟動成功之后,訪問 http://localhost:8005/feign/call,發現調用成功,且依次往復返回如下結果。
因為Feign是聲明式調用,會產生一些相關的Feign定義接口,建議將Feign定義的接口都統一放置管理,以區別內部服務。
源碼下載
后端:https://gitee.com/liuge1988/kitty
前端:https://gitee.com/liuge1988/kitty-ui.git
作者:朝雨憶輕塵
出處:https://www.cnblogs.com/xifengxiaoma/
版權所有,歡迎轉載,轉載請注明原文作者及出處。