Ribbon簡介
什麼是Ribbon?
Ribbon是springcloud下的客戶端負載均衡器,消費者在通過服務別名呼叫服務時,需要通過Ribbon做負載均衡獲取實際的服務呼叫地址,然後通過httpclient的方式進行本地RPC遠端呼叫。
Ribbon原理
Ribbon負載均衡演算法主要是輪詢演算法,分為以下幾步:
- 根據服務別名,從eureka獲取服務提供者的列表
- 將列表快取到本地
- 根據具體策略獲取服務提供者
Ribbon的核心是負載均衡管理,另還有5個大功能點。如下圖:
原始碼分析
事前準備
-
先搭建一個SpringCloud的專案,也可以從我的github上下載。地址:https://github.com/mmcLine/spring-cloud-study
-
拷貝以下程式碼
@Configuration
public class RestTemplateConfiguration {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
@Autowired
private RestTemplate restTemplate;
@GetMapping("/testRibbon/{id}")
public User getTodayStatistic(@PathVariable("id") Integer id){
String url ="http://STUDY-USER/user/getUserById?id="+id;
return restTemplate.getForObject(url, User.class);
}
程式碼都準備好了,可以開始分析了。
- 執行呼叫
為什麼這麼就能呼叫到服務提供者的方法?
打斷點,可以看到restTemplate裡有兩個攔截器,根據名字可以推斷RetryLoadBalancerInterceptor是關鍵。
跟蹤到RetryLoadBalancerInterceptor類
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
//獲取到service的name
final String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
//根據serviceName和LoadBalancerClient,LoadBalancedRetryPolicy裡面包含了RibbonLoadBalancerContext和ServiceInstanceChooser
final LoadBalancedRetryPolicy retryPolicy = lbRetryFactory.createRetryPolicy(serviceName,
loadBalancer);
RetryTemplate template = createRetryTemplate(serviceName, request, retryPolicy);
//執行方法會進入到doExecute方法
return template.execute(context -> {
ServiceInstance serviceInstance = null;
if (context instanceof LoadBalancedRetryContext) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
serviceInstance = lbContext.getServiceInstance();
}
if (serviceInstance == null) {
serviceInstance = loadBalancer.choose(serviceName);
}
ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer.execute(
serviceName, serviceInstance,
requestFactory.createRequest(request, body, execution));
int statusCode = response.getRawStatusCode();
if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody());
response.close();
throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy);
}
return response;
}, new LoadBalancedRecoveryCallback<ClientHttpResponse, ClientHttpResponse>() {
//This is a special case, where both parameters to LoadBalancedRecoveryCallback are
//the same. In most cases they would be different.
@Override
protected ClientHttpResponse createResponse(ClientHttpResponse response, URI uri) {
return response;
}
});
}
doExecute方法:
protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,
RecoveryCallback<T> recoveryCallback, RetryState state)
throws E, ExhaustedRetryException {
//省略部分程式碼
/*
* We allow the whole loop to be skipped if the policy or context already
* forbid the first try. This is used in the case of external retry to allow a
* recovery in handleRetryExhausted without the callback processing (which
* would throw an exception).
*/
//執行邏輯的關鍵方法
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
}
繼續跟蹤canRetry方法
@Override
public boolean canRetry(RetryContext context) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext)context;
if(lbContext.getRetryCount() == 0 && lbContext.getServiceInstance() == null) {
//We haven't even tried to make the request yet so return true so we do
//設定選中的服務提供者
lbContext.setServiceInstance(serviceInstanceChooser.choose(serviceName));
return true;
}
return policy.canRetryNextServer(lbContext);
}
我們跟蹤serviceInstanceChooser.choose(serviceName)看看怎麼通過serviceName選服務提供者的。
@Override
public ServiceInstance choose(String serviceId) {
//選擇server
Server server = getServer(serviceId);
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
}
跟蹤getServer方法
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
//可以看出是loadBalancer在選擇
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
繼續深入
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
//有一個呼叫次數在+1
counter.increment();
if (rule == null) {
return null;
} else {
try {
//委託給了IRule,所以Irule是負載均衡的關鍵,最後來總結
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
檢視Irule的實現
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
//lb.getAllServers裡面是所有的服務提供者列表
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
跟蹤chooseRoundRobinAfterFiltering方法
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
//拿到篩選後的servers
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
//incrementAndGetModulo方法拿到下標,然後根據list.get取到一個服務
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
至此就拿到了具體的服務提供者。
但是到這裡還有個問題?
- 怎麼根據服務名拿到server的?
有一個ServerList介面是用於拿到服務列表的。我們使用的loadBalancer(ZoneAwareLoadBalancer)的父類DynamicServerListLoadBalancer類的構造方法裡,有一個restOfinit方法
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
ServerList<T> serverList, ServerListFilter<T> filter,
ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping);
this.serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if (filter instanceof AbstractServerListFilter) {
((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
}
restOfInit(clientConfig);
}
跟蹤restOfInit方法
void restOfInit(IClientConfig clientConfig) {
boolean primeConnection = this.isEnablePrimingConnections();
// turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
this.setEnablePrimingConnections(false);
enableAndInitLearnNewServersFeature();
//用於獲取所有的serverList
updateListOfServers();
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections()
.primeConnections(getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}
繼續跟蹤updateListOfServers方法
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
//查詢serverList
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
繼續跟蹤原始碼到obtainServersViaDiscovery方法,
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
//eurekaClientProvider.get()會去獲取EurekaClient
if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
logger.warn("EurekaClient has not been initialized yet, returning an empty list");
return new ArrayList<DiscoveryEnabledServer>();
}
EurekaClient eurekaClient = eurekaClientProvider.get();
//vipAddresses就是serviceName
if (vipAddresses!=null){
for (String vipAddress : vipAddresses.split(",")) {
// if targetRegion is null, it will be interpreted as the same region of client
//此處獲取到服務的資訊
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
for (InstanceInfo ii : listOfInstanceInfo) {
if (ii.getStatus().equals(InstanceStatus.UP)) {
if(shouldUseOverridePort){
if(logger.isDebugEnabled()){
logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
}
// copy is necessary since the InstanceInfo builder just uses the original reference,
// and we don't want to corrupt the global eureka copy of the object which may be
// used by other clients in our system
InstanceInfo copy = new InstanceInfo(ii);
if(isSecure){
ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
}else{
ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
}
}
DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);
des.setZone(DiscoveryClient.getZone(ii));
serverList.add(des);
}
}
if (serverList.size()>0 && prioritizeVipAddressBasedServers){
break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
}
}
}
return serverList;
}
綜合上面可以看出,最終是通過eurekaClient去拿到服務列表的。
那麼如果服務列表發生變化怎麼重新整理呢?
是通過CacheRefreshThread在定時執行緒池裡面執行,核心拉取方法是fetchRegistry
Iping
Iping是用於探測服務列表中的服務是否正常,如果不正常,則從eureka拉取服務列表並更新。
在BaseLoadBalancer裡面有一個setupPingTask方法,啟動定時任務,30秒一次定時向EurekaClient傳送“ping”
public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
IPing ping, IPingStrategy pingStrategy) {
logger.debug("LoadBalancer [{}]: initialized", name);
this.name = name;
this.ping = ping;
this.pingStrategy = pingStrategy;
setRule(rule);
setupPingTask();
lbStats = stats;
init();
}
Iping的具體邏輯在PingTask類裡。
Irule總結:
Irule是負載均衡的規則:
我這裡預設是使用的是ZoneAvoidanceRule,還有很多種策略:
- RandomRule: 隨機
- RoundRobinRule: 輪詢
- RetryRule: 先按照RoundRobinRule的策略獲取服務,如果獲取服務失敗則在指定時間內會進行重試,獲取可用的服務
- WeightedResponseTimeRule: 對RoundRobinRule的擴充套件,響應速度越快的例項選擇權重越大,越容易被選擇
- BestAvailableRule:會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然後選擇一個併發量最小的服務
- AvailabilityFilteringRule:先過濾掉故障例項,再選擇併發較小的例項
- ZoneAvoidanceRule:預設規則,複合判斷server所在區域的效能和server的可用性選擇伺服器
properties配置方式如下:
STUDY-USER是服務名
STUDY-USER.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule