客戶端負載均衡 Spring Cloud Ribbon
1.綜述
對于任何一個高可用高負載的系統來說,負載均衡是一個必不可少的名稱。在大型分布式計算體系中,某個服務在單例的情況下,很難應對各種突發(fā)情況。因此,負載均衡是為了讓系統在性能出現瓶頸或者其中一些出現狀態(tài)下可以進行分發(fā)業(yè)務量的解決方案。
Spring Cloud Ribbon 是一個基于Http和TCP的客服端負載均衡工具,它是基于Netflix Ribbon實現的。它不像服務注冊中心、配置中心、API網關那樣獨立部署,但是它幾乎存在于每個微服務的基礎設施中。
包括前面的提供的聲明式服務調用也是基于該Ribbon實現的。理解Ribbon對于我們使用Spring Cloud來講非常的重要,因為負載均衡是對系統的高可用、網絡壓力的緩解和處理能力擴容的重要手段之一。在上節(jié)的例子中,我們采用了聲明式的方式來實現負載均衡。
實際上,內部調用維護了一個RestTemplate對象,該對象會使用Ribbon的自動化配置,同時通過@LoadBalanced開啟客戶端負載均衡。其實RestTemplate是Spring自己提供的對象,不是新的內容。
ribbon最核心的概念是:一個被命名的client,也即一個具有唯一名字的客戶端每一個負載均衡都是整體組件的一部分,它們相互協作調用遠程服務。每一個client都會通過類RibbonClientConfiguration創(chuàng)建一個新的子spring ApplicationContext,這個子ApplicationContext上下文的名子就是client的名字(如:@FeignClient("user-server"),每個ribbon客戶端包含:
ILoadBalancer, RestClient, ServerListFilter, ServerList,IRule。
ILoadBalancer:是負載均衡的入口類。
ServerList:存儲遠程服務所有可用節(jié)點
ServerListFilter:用于過濾非法的遠程節(jié)點(如:不可用等)
IRule:負載算法(策略),用于從可用節(jié)點選擇一個合適的節(jié)點。
RestClient:遠程調用
攔截&請求:

2.GET請求
第一種getForEntity函數,該方法返回的是ResponseEntity,該對象是字符串對HTTP請求響應的封裝。
姓名,年齡兩個參數對應的{1},{2}代表兩個占位符,如要傳遞多個參數以此類推。
String.class返回值,如果需要返回對象對象的.class
getBody()是返回的ResponseEntity對象中的體內容。
/**
* Ribbon Get測試
* @return
*/
@RequestMapping(value="RibbonGet",method=RequestMethod.GET)
public String RibbonGet(@RequestParam("name") String name,@RequestParam("age") String age) {
return restTemplate.getForEntity("http://OrderService/GetTest?name={1}&age={2}", String.class,name,age).getBody();
}
注意占位符名稱
/**
* Ribbon Get測試
* @return
*/
@RequestMapping(value="RibbonGet",method=RequestMethod.GET)
public String RibbonGet(@RequestParam("name") String name,@RequestParam("age") String age) {
//傳遞Map集合
Map<String, Object> params=new HashMap<>();
params.put("name", name);
params.put("age", age);
return restTemplate.getForEntity("http://OrderService/GetTest?name={name}&age={age}", String.class,params).getBody();
}
第二種getForObject函數,該方法可以理解為對getForEntity的進一步封裝,返回的直接就是身體,如果不需要關注請求響應除身體外,該函數就非常好用。
/**
* Ribbon Get測試
* @return
*/
@RequestMapping(value="RibbonGet",method=RequestMethod.GET)
public String RibbonGet(@RequestParam("name") String name,@RequestParam("age") String age) {
return restTemplate.getForObject("http://OrderService/GetTest?name="+name+"&age="+age, String.class);
}
3.POST請求
第一種postForEntity函數,該方法返回的是ResponseEntity <T>,其中?為請求響應的體類型。
/**
* Ribbon Post請求
* @param id
* @return
*/
@RequestMapping(value="ribbonPostOrder",method=RequestMethod.POST)
public User PostBeanTest(@RequestParam("id") Integer id) {
User user=new User();
user.setId(id);
ResponseEntity<User> entity = restTemplate.postForEntity("http://OrderService/PostBeanTest", user, User.class);
return entity.getBody();
}
第二種postForObject函數,和postForEntity類似,簡化了機身的處理。
/**
* Ribbon Post請求
* @param id
* @return
*/
@RequestMapping(value="ribbonPostOrder",method=RequestMethod.POST)
public User PostBeanTest(@RequestParam("id") Integer id){
User user=new User();
user.setId(id);
return restTemplate.postForObject("http://OrderService/PostBeanTest", user, User.class);
}
4.PUT請求
把函數為無效類型,所以沒有返回內容。
/**
* Ribbon Put請求
* @param id
* @return
*/
@RequestMapping(value="ribbonPutOrder",method=RequestMethod.POST)
public String ribbonPutOrder(@RequestParam("id") Integer id,@RequestParam("name") String name){
User user=new User();
user.setId(id);
user.setName(name);
restTemplate.put("http://OrderService/PutTest",user);
return "PUT成功!";
}
5.DELETE請求
deleet函數為無效類型,所以沒有返回內容。
/**
* Ribbon Delete請求
* @param id
* @return
*/
@RequestMapping(value="ribbonDeleteOrder",method=RequestMethod.POST)
public String ribbonDeleteOrder(@RequestParam("id") Integer id) {
restTemplate.delete("http://OrderService/DeleteTest/?id={1}",id);
return "刪除成功";
}
6.配置
自動化配置
由于Ribbon中定義的每一個接口都有多種不同的策略實現,同時這些之間又有一定的依賴關系。
com.netflix.client.config.IClientConfig:Ribbon的客戶端配置,默認采用com.netflix.client.config.DefaultClientConfigImpl實現。com.netflix.loadbalancer.IRule:Ribbon的負載均衡策略,默認采用com.netflix.loadbalancer.ZoneAvoidanceRule實現,該策略能夠在多區(qū)域環(huán)境下選出最佳區(qū)域的實例進行訪問。com.netflix.loadbalancer.IPing:Ribbon的實例檢查策略,默認采用com.netflix.loadbalancer.NoOpPing實現,該檢查策略是一個特殊的實現,實際上它并不會檢查實例是否可用,而是始終返回true,默認認為所有服務實例都是可用的。com.netflix.loadbalancer.ServerList:服務實例清單的維護機制,默認采用com.netflix.loadbalancer.ConfigurationBasedServerList實現。com.netflix.loadbalancer.ServerListFilter:服務實例清單過濾機制,默認采org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter,該策略能夠優(yōu)先過濾出與請求方處于同區(qū)域的服務實例。com.netflix.loadbalancer.ILoadBalancer:負載均衡器,默認采用com.netflix.loadbalancer.ZoneAwareLoadBalancer實現,它具備了區(qū)域感知的能力。
上面的配置是在項目中沒有引入spring Cloud Eureka,如果引入了Eureka和Ribbon依賴時,自動化配置會有一些不同。
通過自動化配置的實現,可以輕松的實現客戶端的負載均衡。同時,針對一些個性化需求,我們可以方便的替換上面的這些默認實現,只需要在springboot應用中創(chuàng)建對應的實現實例就能覆蓋這些默認的配置實現。
@Configuration
public class MyRibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new RandomRule();
}
}
這樣就會使用P使用了RandomRule實例替代了默認的com.netflix.loadbalancer.ZoneAvoidanceRule。
也可以使用@RibbonClient注解實現更細粒度的客戶端配置
Camden版本對RabbitClient配置的優(yōu)化
上面的方式主要是通過獨立創(chuàng)建一個Configuration類來定義IPing,IRule等接口的具體實現Bean,然后通過RabbonClient時指定要使用的具體Configuration類來覆蓋自動化配置的默認實現。在spring Cloud Ribbon Camden版本中對RibbonClient定義個性化配置的方法做出進一步優(yōu)化。可以直接使用<clientName>.ribbon.<key>=<value>的形式進行配置。
user-service.ribbon.NFLoadBalancerPingClassName=com.netfix.loadbalancer.PingUrl
user-service是服務名,NFLoadBalancerPingClassName參數是用來指定IPing接口實現類。在Camden版本中,Spring Cloud Ribbon新增了一個org.springframework.cloud.netflix.ribbon.PropertiesFactory類動態(tài)的為RibbonClient創(chuàng)建這些接口實現。

在Camden版本中我們可以通過配置的方式,更加方便的為RibbonClient指定ILoadBalancer,IPing,IRule,ServerList,ServerListFilter的定制化實現。
參數配置
對于Ribbon的參數通常有二種方式:全局配置以及指定客戶端配置
- 全局配置的方式很簡單
只需要使用ribbon.<key>=<value>格式進行配置即可。其中,<key>代表了Ribbon客戶端配置的參數名,<value>則代表了對應參數的值。比如,我們可以想下面這樣配置Ribbon的超時時間
ribbon.ConnectTimeout=250
全局配置可以作為默認值進行設置,當指定客戶端配置了相應的key的值時,將覆蓋全局配置的內容
- 指定客戶端的配置方式
<client>.ribbon.<key>=<value>的格式進行配置.<client>表示服務名,比如沒有服務治理框架的時候(如Eureka),我們需要指定實例清單,可以指定服務名來做詳細的配置,
user-service.ribbon.listOfServers=localhost:8080,localhost:8081,localhost:8082
對于Ribbon參數的key以及value類型的定義,可以通過查看com.netflix.client.config.CommonClientConfigKey類。
與Eureka結合
當在spring Cloud的應用同時引入Spring cloud Ribbon和Spring Cloud Eureka依賴時,會觸發(fā)Eureka中實現的對Ribbon的自動化配置。這時的serverList的維護機制實現將被com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList的實例所覆蓋,該實現會講服務清單列表交給Eureka的服務治理機制來進行維護。IPing的實現將被
com.netflix.niws.loadbalancer.NIWSDiscoveryPing的實例所覆蓋,該實例也將實例接口的任務交給了服務治理框架來進行維護。默認情況下,用于獲取實例請求的ServerList接口實現將采用Spring Cloud Eureka中封裝的
org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList,其目的是為了讓實例維護策略更加通用,所以將使用物理元數據來進行負載均衡,而不是使用原生的AWS AMI元數據。
在與Spring cloud Eureka結合使用的時候,不需要再去指定類似的user-service.ribbon.listOfServers的參數來指定具體的服務實例清單,因為Eureka將會為我們維護所有服務的實例清單,而對于Ribbon的參數配置,我們依然可以采用之前的兩種配置方式來實現。
此外,由于spring Cloud Ribbon默認實現了區(qū)域親和策略,所以,可以通過Eureka實例的元數據配置來實現區(qū)域化的實例配置方案。比如可以將不同機房的實例配置成不同的區(qū)域值,作為跨區(qū)域的容器機制實現。而實現也非常簡單,只需要服務實例的元數據中增加zone參數來指定自己所在的區(qū)域,比如:
eureka.instance.metadataMap.zone=shanghai
在Spring Cloud Ribbon與Spring Cloud Eureka結合的工程中,我們可以通過參數禁用Eureka對Ribbon服務實例的維護實現。這時又需要自己去維護服務實例列表了。
ribbon.eureka.enabled=false.
7. 重試機制
由于Spring Cloud Eureka實現的服務治理機制強調了cap原理的ap機制(即可用性和可靠性),與zookeeper這類強調cp(一致性,可靠性)服務質量框架最大的區(qū)別就是,Eureka為了實現更高的服務可用性,犧牲了一定的一致性,在極端情況下寧愿接受故障實例也不要丟棄"健康"實例。
比如說,當服務注冊中心的網絡發(fā)生故障斷開時候,由于所有的服務實例無法維護續(xù)約心跳,在強調ap的服務治理中將會把所有服務實例剔除掉,而Eureka則會因為超過85%的實例丟失心跳而觸發(fā)保護機制,注冊中心將會保留此時的所有節(jié)點,以實現服務間依然可以進行互相調用的場景,即使其中有部分故障節(jié)點,但這樣做可以繼續(xù)保障大多數服務的正常消費。
在Camden版本,整合了spring retry來增強RestTemplate的重試能力,對于我們開發(fā)者來說,只需要簡單配置,即可完成重試策略。
spring.cloud.loadbalancer.retry.enabled=true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
user-service.ribbon.ConnectTimeout=250
user-service.ribbon.ReadTimeout=1000
user-service.ribbon.OkToRetryOnAllOperations=true
user-service.ribbon.MaxAutoRetriesNextServer=2
user-service.ribbon.maxAutoRetries=1
spring.cloud.loadbalancer.retry.enabled:該參數用來開啟重試機制,它默認是關閉的。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:斷路器的超時時間需要大于Ribbon的超時時間,不然不會觸發(fā)重試。
user-service.ribbon.ConnectTimeout:請求連接超時時間。
user-service.ribbon.ReadTimeout:請求處理的超時時間
user-service.ribbon.OkToRetryOnAllOperations:對所有操作請求都進行重試。
user-service.ribbon.MaxAutoRetriesNextServer:切換實例的重試次數。
user-service.ribbon.maxAutoRetries:對當前實例的重試次數。
根據以上配置,當訪問到故障請求的時候,它會再嘗試訪問一次當前實例(次數由maxAutoRetries配置),如果不行,就換一個實例進行訪問,如果還是不行,再換一個實例訪問(更換次數由MaxAutoRetriesNextServer配置),如果還不行,返回失敗。

8.Ribbon 提供的主要負載均衡策略:
1:簡單輪詢負載均衡(RoundRobin)
以輪詢的方式依次將請求調度不同的服務器,即每次調度執(zhí)行i = (i + 1) mod n,并選出第i臺服務器。
2:隨機負載均衡 (Random)
隨機選擇狀態(tài)為UP的Server
3:加權響應時間負載均衡 (WeightedResponseTime)
根據響應時間分配一個weight,響應時間越長,weight越小,被選中的可能性越低。
4:區(qū)域感知輪詢負載均衡(ZoneAvoidanceRule)
復合判斷server所在區(qū)域的性能和server的可用性選擇server
less is more.