一、RPC和HTTP
應用服務間通信調用的方式主要有兩種,一種是HTTP,另一種是RPC。
RPC形式的常見代表是Dubbo。
Dubbo的定位就是一款RPC服務調用框架,基於Dubbo開發的應用還是要依賴周邊的平臺和生態,比如配合以Zookeeper實現的服務註冊中心一起使用。Dubbo不僅提供了服務註冊和發現、負載均衡等面向分佈式系統的基礎能力,還提供了開發測試階段的Mock機制。在SpringCloud流行之前,Dubbo應用的十分廣泛。
HTTP形式的常見代表是SpringCloud。
Dubbo自身的定位就只是一款RPC服務調用框架,而SpringCloud的目標是微服務下的一站式解決方案。
SpringCloud中服務調用採用的是http的restful方式,http-restful方式的特點是輕量,易用,便於跨語言跨平臺。
SpringCloud中有兩種restful調用方式:RestTemplate、Feign。
在比對完RPC和HTTP之後,我將主要實踐SpringCloud中的服務調用。
二、基於RestTemplate的服務調用
RestTemplate是一款Http客戶端,功能和HttpClient類似,但RestTemplate用法更簡單。
場景:現在系統中有Product服務和Order服務,我需要在Order服務中去調用Product服務的接口。
Product服務(被調用方)
在product服務中定義了一個controller,其中提供了一個獲取product信息的接口:
/**
* @Auther: jesses
* @Description: product服務中的controller
*/
@RestController
public class ProductController {
@GetMapping("getMsg")
public String getMsg(){
return "this is product's message";
}
}
Order服務(調用方)
使用RestTemplate方式調用上面的product接口,有三種實現方式。
- 方式一、直接new RestTemplate()調用:
/**
* @Auther: jesses
* @Description: order服務中的controller
*/
@RestController
@Slf4j
public class ClientController {
@GetMapping("/getProductMsg")
public String getProductMsg() {
//第一種方式
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject("http://127.0.0.1:9080/getMsg", String.class);
log.info("response={}", response);
return response;
}
}
可以看到這種方式是寫死Url,如果是在生產環境,別人的product服務部署到的IP地址,調用方卻未必知道。
不知道服務地址,如何調用?這種方式肯定是存在缺陷的。
而且可能product被部署了多臺實例,做了集羣。我們肯定是想調用其中某一臺就夠了,這就涉及到需要實現負載均衡。
- 方式二、利用LoadBalancerClient獲取服務實例
這次我啓動了兩臺product實例。port分別是9080、9081。
可以看到,在product服務中,配置了服務名爲product:
SpringCloud提供了LoadBalancerClient,通過服務名來獲取product服務的實例對象,從而獲得IP和PORT:
/**
* @Auther: jesses
* @Description: order服務中的controller
*/
@RestController
@Slf4j
public class ClientController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/getProductMsg")
public String getProductMsg() {
ServiceInstance serviceInstance = loadBalancerClient.choose("PRODUCT");
String url = String.format("http://%s:%s%s",
serviceInstance.getHost(),
serviceInstance.getPort(),
"/getMsg");
return new RestTemplate().getForObject(url, String.class);
}
}
- 方式三、註解配置RestTemplate
在order服務中定義一個配置類,用於配置RestTemplate,使用@LoadBalanced註解配置負載均衡器,將其註冊進spring容器。
/**
* @Auther: jesses
* @Description: 配置restTemplate,使用註解配置LoadBalanced
*/
@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
直接注入配置了的 restTemplate,將調用的ip和port替換爲服務名:
/**
* @Auther: zhaoshuai
* @Date: 2020/5/4
* @Description: order服務中的controller
*/
@RestController
@Slf4j
public class ClientController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/getProductMsg")
public String getProductMsg() {
return restTemplate.getForObject("http://PRODUCT/getMsg", String.class);
}
}
第三種方式只是使用註解簡化了第二種,實際上實現機制是一樣的。
這三種方式歸根結底還是基於RestTemplate的服務調用。
三、基於feign組件的服務調用
3.1 基於Ribbon實現的負載均衡
Feign是一款客戶端http調用組件,屬於SpringCloudNetflix的組件之一,而Feign是依賴於Ribbon組件的,
所以先來了解Ribbon及其負載均衡策略。
Ribbon實現負載均衡的核心要素有三點:
- 服務發現:根據服務名,找到該服務的所有實例
- 負載均衡策略:根據負載均衡策略,從多個服務實例中選擇一個有效的服務
- 服務監聽:檢測失效的服務,做到高效剔除。
概括Ribbon實現負載均衡的流程,大致是先通過ServerList獲取所有可用服務列表,之後通過ServerListFilter過濾掉一部分,最後通過IRule選擇一個目標實例。
接下來查看部分源碼看Ribbon的具體實現:
3.1.1 Ribbon的服務發現:
之前在RestTemplate調用服務的第二種方式用到了loadBalancerClient.choose("PRODUCT") 這個API,現在查看這個api的實現。
LoadBalancerClient繼承了ServiceInstanceChooser接口,
choose()方法的實現來自於LoadBalancerClient的實現類RibbonLoadBalancerClient:
在choose()實現中調用了getServer(),繼續進入getServer()內查看:
到getServer()方法的底層可以發現調用了chooseServer()這個API,再次進入查看:
可以發現獲取所有服務列表的api方法List<Server> getAllServers();就定義在接口ILoadBalancer中。
查看getAllServers()的實現,在此施加斷點。
調用第二種restTemplate方式的controller接口,進入斷點後發現的確返回了product服務的實例列表:
3.1.2 Ribbon的負載均衡策略
再查看ILoadBalancer接口中的另一個api,即chooseServer(),查看其在BaseLoadBalancer實現類中的方法實現:
可以看到,其中是通過一個rule對象調用choose()方法的,
在BaseLoadBalancer構造器中對rule對象進行了初始化,賦予其一個默認的輪詢規則RoundRobinRule:
既然默認的負載均衡策略是輪詢,如果想要配置其他規則,如何修改?
3.2 基於Feign實現的服務調用
第一步,在調用方服務引入依賴:
要使用feign,需要先引入其依賴spring-cloud-starter-openfeign。
需要注意的是,在較低版本的springcloud中,使用的是artifactId爲spring-cloud-starter-feign的依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.0.M3</version>
</dependency>
第二步,在調用方服務的啓動類上配置開啓feign客戶端,@EnableFeignClient:
第三步,定義feign接口:
例如,在被調用方product服務的controller中,我提供了兩個接口,查詢商品列表接口、扣庫存接口。
/**
* @Auther: jesses
* @Description: product服務controller
*/
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
/**根據商品ids查詢商品列表*/
@PostMapping("/listForOrder")
public List<ProductInfo> listForOrder(@RequestBody List<String> productIdList){
return productService.findList(productIdList);
}
/**減庫存*/
@PostMapping("/decreaseStock")
public void decreaseStock(@RequestBody List<CartDTO> cartDTOList){
productService.decreaseStock(cartDTOList);
}
}
現在我需要在order服務調用product服務的這兩個接口。
在order服務中定義feign客戶端及接口:
1.定義一個interface用作feign客戶端定義
2.使用@FeignClient(name="${application name of Called App}")註解標註。註解的name屬性爲被調用方服務名。
3.接口的@RequestMapping中method和value值,必須和被調用方服務定義的一致。
4.feign接口與被調用方服務匹配的要素只在於@FeignClient中name屬性值,以及@RequestMapping中的value和method,
方法名可以不同,不影響使用。
/**
* @Auther: jesses
* @Description: 調用product服務的feign客戶端
*/
@FeignClient(name = "product")
public interface ProductClient {
@PostMapping("/product/listForOrder")
List<ProductInfo> listForOrder(@RequestBody List<String> productIdList);
@PostMapping("/product/decreaseStock")
void decreaseStock(@RequestBody List<CartDTO> cartDTOList);
}
之後在order服務的controller使用@Autowired注入ProductClient的Bean。直接調用api即可:
Feign本質上是一款http客戶端,基於feign做服務調用,開發體驗如通調用本地方法一樣,感知不到是在調用遠程接口。
Feign內部也使用了Ribbon實現了服務調用的負載均衡。