Spring Cloud第四篇 | 客戶端負載均衡Ribbon
本文是Spring Cloud專欄的第四篇文章,瞭解前三篇文章內容有助於更好的理解本文:
-
Spring Cloud第一篇 | Spring Cloud前言及其常用元件介紹概覽
-
Spring Cloud第二篇 | 使用並認識Eureka註冊中心
-
Spring Cloud第三篇 | 搭建高可用Eureka註冊中心
一、Ribbon是什麼
Ribbon是一個基於HTTP和TCP的客戶端負載均衡器,當使用Ribbon對服務進行訪問的時候,他會擴充套件Eureka客戶端的服務發現功能,實現從Eureka註冊中心獲取服務端列表,並通過Eureka客戶端來確定服務端是否已經啟動。Ribbon在Eureka客戶端服務發現的基礎上,實現對服務例項的選擇策略,從而實現對服務的負載均衡消費。負載均衡在系統架構中是一個非常重要的內容,因為負載均衡是對系統的高可用、網路的壓力的緩衝和處理能力擴容的重要手段之一,我們通常說的負載均衡都是指的是服務端的負載均衡,其中分為硬體負載均衡和軟體負載均衡。
-
硬體負載均衡:主要通過伺服器節點之間安裝專門用於負載均衡的裝置,比如F5,深信服,Array等。
-
軟體負載均衡:則是通過伺服器上安裝一些具有負載功能或模組的軟體來完成請求分發工作,比如Nginx、LVS、HAProxy等。
硬體負載均衡的裝置或是軟體負載均衡的軟體模組都會維護一個下掛可用的服務端清單,通過心跳檢測來剔除故障的服務端節點保證清單中都是可以正常訪問的服務端節點。當客戶端傳送請求到負載均衡的裝置時候,該裝置按某種演算法(比如線性輪詢、按權重負載、按流量負載等)從維護的可用服務端清單中取出一臺服務端地址,然後進行轉發。
Ribbon是Netflix釋出的開源專案,主要功能是提供客戶端的軟體負載均衡演算法,是一個基於HTTP和TCP的客戶端負載均衡工具。Spring Cloud對Ribbon做了二次封裝,可以讓我們使用 RestTemplate的服務請求,自動轉換成客戶端負載均衡的服務呼叫。Ribbon支援多種負載均衡演算法,還支援自定義的負載均衡演算法。Ribbon只是一個工具類框架,比較小巧, Spring Cloud對它封裝後使用也非 常方便,它不像服務註冊中心、配置中心、AP閘道器那樣需要獨立部署, Ribbon 只需要在程式碼直接使用即可。
Ribbon與 Nginx的區別:
-
都是軟負載
-
Ribbon是客戶端負載均衡
-
Nginx是伺服器段負載均衡
區別在於:
服務清單所儲存的位置不同,在客戶端負載均衡中,所有客戶端節點下的服務端清單,需要自己從服務註冊中心上獲取,比如Eureka服務註冊中心。同服務端負載均衡的架構類似,在客戶端負載均衡中也需要心跳去維護服務端清單的健康性,只是這個步驟需要與服務註冊中心配合完成,在SpringCloud實現的服務治理框架中,預設會建立針對各個服務治理框架到的Ribbon自動化整合配置,比如Eureka中的org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,在實際使用的時候,我們可以通過檢視這個類的實現,以找到他們的配置詳情來幫助我們更好的使用它。
通過Spring Cloud Ribbon的封裝,我們在微服務架構中使用客戶端負載均衡呼叫非常的簡單,只需要如下兩步:
-
服務提供者只需要啟動多個服務例項並註冊到一個註冊中心或是多個相關聯的服務註冊中心上
-
服務消費者直接通過呼叫被@LoadBalanced註解修飾過的RestTemplate來實現面向服務的介面呼叫。
這樣我們就可以將服務提供者的高可用以及服務消費者的負載均衡用一起實現了。
-
服務端的負載均衡是提前配置好的:Nginx
-
客戶端的負載均衡是從註冊中心找的:Ribbon
在SpringCloud中,Ribbon主要與RestTemplate物件配合使用,Ribbon會自動化配置RestTemplate物件,通過@LoadBalance開啟RestTemplate物件呼叫時的負載均衡,Ribbon所處的作用如圖:
圖片來源網路
二、Ribbon實現客戶端負載均衡
1、前面提到過,通過Spring Cloud Ribbon的封裝,我們在微服務架構中使用客戶端負載均衡呼叫非常的簡單,只需要如下兩步:
-
服務提供者只需要啟動多個服務例項並註冊到一個註冊中心或是多個相關聯的服務註冊中心上
-
服務消費者直接通過呼叫被@LoadBalanced註解修飾過的RestTemplate來實現面向服務的介面呼叫。
2、我們複製服務提供者(springcloud-service-provider)並且命名為springcloud-service-provider-02,修改controlle響應結果內容,區別服務提供者(springcloud-service-provider)的內容。修改服務提供者(springcloud-service-provider-02)埠為8081,具體詳細程式碼檢視案例原始碼。註冊中心我們以後使用8700單節點,只是為了方便。
3、在消費者的RestTemplate中新增如下程式碼:
//使用Ribbon實現負載均衡呼叫,預設是輪詢 @LoadBalanced //加入ribbon的支援,那麼在呼叫時,即可改為使用服務名稱來訪問 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); }
4、檢視Eureka的web頁面顯示提供者兩個例項
5、啟動消費者,進行訪問如圖:
provider-01和provider-02交替出現,可以看出預設是輪詢策略。
三、Ribbon負載均衡策略
Ribbon的負載均衡策略是由IRule介面定義,該介面由如下實現:
RandomRule |
隨機 |
RoundRobinRule |
輪詢 |
AvailabilityFilteringRule |
先過濾掉由於多次訪問故障的服務,以及併發連線數超過閥值的服務,然後對剩下的服務按照輪詢策略進行訪問 |
WeightedResponseTimeRule |
根據平均響應時間計算所有服務的權重,響應時間越快服務權重就越大被選中的概率即越高,如果服務剛啟動時間統計資訊不足,,則使用RoundRobinRule策略,待統計資訊足夠,會切換到該WeightedResponseTimeRule策略 |
RetryRule |
先按照RoundRobinRule策略分發,如果分發到的服務不能訪問,則在指定的時間內重試,如果不行的話,則分發到其他可用的服務 |
BestAvailableRule |
先過濾掉由於多次訪問的故障的服務,然後選擇一個併發量最小的服務 |
ZoneAvoidanceRule |
綜合判斷服務節點所在區域的效能和服務節點的可用性,來決定選擇哪個服務 |
TIP:結合Ribbon負載均衡,預設的是輪詢,重新注入IRule可以實現負載均衡的其他策略
四、Rest請求模板類解讀
當我們從服務消費端去呼叫服務提供者的服務的時候,使用了一個極其方便的物件叫RestTemplate,當時我們只使用了 RestTemplate中最簡單的一個功能getForEntity發起了一個get請求去呼叫服務端的資料,同時,我們還通過配置@Loadbalanced註解開啟客戶端負載均衡, RestTemplate的功能非常強大, 那麼接下來就來詳細的看一下RestTemplate中幾種常見請求方法的使用。在日常操作中,基於Rest的方式通常是四種情況,它們分表是
-
GET請求-查詢資料
-
POST請求-新增資料
-
PUT請求-修改資料
-
DELETE-刪除資料
1、RestTemplate的GET請求
Get請求可以有兩種方式
第一種:getForEntity(..)
該方法返回一個ResponseEntity<T>物件,ResponseEntity<T>是Spring對HTTP請求響應的封裝,包括了幾個重要的元素,比如響應碼,contentType,contentLength,響應訊息體等
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/hello", String.class); String body = forEntity.getBody(); HttpStatus statusCode = forEntity.getStatusCode(); int statusCodeValue = forEntity.getStatusCodeValue(); HttpHeaders headers = forEntity.getHeaders(); System.out.println(body); System.out.println(statusCode); System.out.println(statusCodeValue); System.out.println(headers);
以上程式碼, getForEntity方法第—個引數為要呼叫的服務的地址,即服務提供者提供的http://SPRINGCLOUD-SERVICE-PROVIDER/provider/hello介面地址,注意這裡是通過服務名呼叫而不是服務地址,如果改為服務地址就無法使用Ribbon實現客戶端負載均衡了。getForEntity方法第二個引數String.class表示希望返回的body型別是 String 型別,如果希望返回一個物件,也是可以的,比如User物件
/** * 呼叫get請求,返回一個User物件 * @return */ @RequestMapping("/user") public User user(){ //邏輯判斷省略 ResponseEntity<User> forEntity = restTemplate.getForEntity("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/user", User.class); System.out.println(forEntity.getBody().getId()+""+forEntity.getBody().getName()+""+forEntity.getBody().getPhone()); return forEntity.getBody(); }
另外兩個過載方法:
@Override public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType); return nonNull(execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables)); } @Override public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType); return nonNull(execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables)); }
比如:
/** * 給服務傳引數Get請求 * @return */ @RequestMapping("/getUser") public User getUser(){ //邏輯判斷省略 String [] arr={"2","xxx","4545645456"}; Map<String,Object> map=new HashMap<>(); map.put("id",1); map.put("name","wwwwww"); map.put("phone","1213213213123"); //ResponseEntity<User> forEntity = restTemplate.getForEntity("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/getUser?id={0}&name={1}&phone={2}", User.class,arr); //ResponseEntity<User> forEntity = restTemplate.getForEntity("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/getUser?id={id}&name={name}&phone={phone}", User.class,map); /* * restTemplate.getForObject在getForObject在getForEntity在次封裝,直接獲取返回值型別,相當於ResponseEntity中的getBody */ User user1 = restTemplate.getForObject("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/getUser?id={id}&name={name}&phone={phone}", User.class, map); //System.out.println(forEntity.getBody().getId()+""+forEntity.getBody().getName()+""+forEntity.getBody().getPhone()); System.out.println(user1.getId()+""+user1.getName()+""+user1.getPhone()); return user1; }
可以用一個數字做佔位符,最後是一個可變長度的引數,來來替換前面的佔位符也可以前面使用name={name}這種形式,最後一個引數是一個map,map的key即為前邊佔位符的名字,map的value為引數值
第二種:getForObject(..)
與getForEntity使用類似,只不過getForobject是在getForEntity基礎上進行了再次封裝,可以將http的響應體body資訊轉化成指定的物件,方便我們的程式碼開發,當你不需要返回響應中的其他資訊,只需要body體資訊的時候,可以使用這個更方便,它也有兩個過載的方法,和getForEntity相似
@Override @Nullable public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables); } @Override @Nullable public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables); } @Override @Nullable public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor); }
上面例子已經涉及到了,此處不再囉嗦。
2、RestTemplate的POST請求
restTemplate.postForEntity(); restTemplate.postForObject(); restTemplate.postForLocation();
例如:
/** * 呼叫POST請求 * @return */ @RequestMapping("/addUser") public User addUser(){ //邏輯判斷省略 String [] arr={"2","xxx","4545645456"}; //不能使用map傳遞引數 Map<String,Object> map=new HashMap<>(); map.put("id",1); map.put("name","wwwwww"); map.put("phone","1213213213123"); /** *要傳的表單資訊,引數資料(很坑人) */ MultiValueMap<String,Object> multiValueMap=new LinkedMultiValueMap<>(); multiValueMap.add("id",1); multiValueMap.add("name","xxxxx"); multiValueMap.add("phone","000000000"); //使用jdk中的map傳引數,接收不到 ResponseEntity<User> userResponseEntity = restTemplate.postForEntity( "http://SPRINGCLOUD-SERVICE-PROVIDER/provider/addUser", multiValueMap, User.class); System.out.println(userResponseEntity.getBody().getId()+""+userResponseEntity.getBody().getName()+""+userResponseEntity.getBody().getPhone()); return userResponseEntity.getBody(); }
3、RestTemplate的PUT請求
restTemplate.put();
例如:
/** * 呼叫PUT請求 * @return */ @RequestMapping("/updateUser") public String updateUser(){ //邏輯判斷省略 String [] arr={"2","xxx","4545645456"}; //不能使用map傳遞引數 Map<String,Object> map=new HashMap<>(); map.put("id",1); map.put("name","wwwwww"); map.put("phone","1213213213123"); /** *要傳的表單資訊,引數資料(很坑人) */ MultiValueMap<String,Object> multiValueMap=new LinkedMultiValueMap<>(); multiValueMap.add("id",1); multiValueMap.add("name","xxxxx"); multiValueMap.add("phone","000000000"); //使用jdk中的map傳引數,接收不到 restTemplate.put("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/updateUser", multiValueMap); return "SUCCESS"; }
4、RestTemplate的DELETE請求
restTemplate.delete();
例如:
/** * 呼叫DELETE請求 * @return */ @RequestMapping("/deleteUser") public String deleteUser(){ //邏輯判斷省略 String [] arr={"2","xxx","4545645456"}; Map<String,Object> map=new HashMap<>(); map.put("id",1); map.put("name","wwwwww"); map.put("phone","1213213213123"); /** *要傳的表單資訊,引數資料(很坑人),只有post,PUT請求採用這種map傳引數 */ /* MultiValueMap<String,Object> multiValueMap=new LinkedMultiValueMap<>(); multiValueMap.add("id",1); multiValueMap.add("name","xxxxx"); multiValueMap.add("phone","000000000"); */ //使用jdk中的map傳引數,接收不到,不能使用MultiValueMap,接收不到引數 restTemplate.delete("http://SPRINGCLOUD-SERVICE-PROVIDER/provider/deleteUser?id={id}&name={name}&phone={phone}", map); return "SUCCESS"; }
詳細參考案例原始碼:https://gitee.com/coding-farmer/spirngcloud-learn