Ribbon原始碼解析

pggsnap發表於2019-02-11

Spring-Cloud-Gateway 預設整合了一些負載均衡策略,比如輪詢、隨機、基於響應時間設定權重等等。由於業務需要,需要自定義一個策略,於是花時間先研究了下原始碼。先上結論:

一、結論

  1. LoadBalancerClient 介面中定義了 ServiceInstance choose(String serviceId) 方法,根據服務名獲取具體的服務例項;

  2. SpringClientFactory 類中定義了 Map<String, AnnotationConfigApplicationContext> contexts,其中 key 為 serviceId,value 為 AnnotationConfigApplicationContext 物件。這意味著可以為每個服務設定不同的負載均衡策略。AnnotationConfigApplicationContext 中主要儲存了 ILoadBalancer bean,定義了負載均衡的實現;而後者又依賴於:

    • IClientConfig:定義了客戶端配置,用於初始化客戶端以及負載均衡配置;
    • IRule:負載均衡的策略,比如輪詢等;
    • IPing:定義瞭如何確定服務例項是否正常;
    • ServerList:定義了獲取伺服器列表的方法;
    • ServerListFilter:根據配置或者過濾規則選擇特定的伺服器列表;
    • ServerListUpdater:定義了動態更新伺服器列表的策略。

    關係圖如下所示:

Ribbon原始碼解析

  1. AnnotationConfigApplicationContext 中確定 ILoadBalancer 的次序是:優先通過外部配置匯入,其次是配置類。

二、原始碼解析

1. LoadBalancerClient

LoadBalancerClient 是一個負載均衡客戶端,定義了 ServiceInstance choose(String serviceId) 方法,即通過服務名按照給定的策略獲取服務例項。

其介面定義如下:

public interface LoadBalancerClient extends ServiceInstanceChooser {
	...
}

public interface ServiceInstanceChooser {
    /**
     * Chooses a ServiceInstance from the LoadBalancer for the specified service.
     * @param serviceId The service ID to look up the LoadBalancer.
     * @return A ServiceInstance that matches the serviceId.
     */
    ServiceInstance choose(String serviceId);
}

public interface ServiceInstance {

	default String getInstanceId() {
		return null;
	}

	String getServiceId();
    
	String getHost();
    
	int getPort();
    
	boolean isSecure();
    
	URI getUri();
    
	Map<String, String> getMetadata();
    
	default String getScheme() {
		return null;
	}
}
複製程式碼

預設實現類是 RibbonLoadBalancerClient:

public class RibbonLoadBalancerClient implements LoadBalancerClient {

	@Override
	public ServiceInstance choose(String serviceId) {
	    return choose(serviceId, null);
	}

	public ServiceInstance choose(String serviceId, Object hint) {
		Server server = getServer(getLoadBalancer(serviceId), hint);
		if (server == null) {
			return null;
		}
		return new RibbonServer(serviceId, server, isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
	}
    
    protected ILoadBalancer getLoadBalancer(String serviceId) {
		return this.clientFactory.getLoadBalancer(serviceId);
	}

}
複製程式碼

第 9 行:getLoadBalancer(serviceId),先根據服務名獲取負載均衡策略;getServer(getLoadBalancer(serviceId), hint),然後根據策略選擇具體的服務例項。

第 18 行:根據服務名獲取負載均衡策略由 SpringClientFactory 類實現。

2. SpringClientFactory

SpringClientFactory 中儲存了每個服務的負載均衡的完整實現。

public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {
	public ILoadBalancer getLoadBalancer(String name) {
		return getInstance(name, ILoadBalancer.class);
	}
	
	@Override
	public <C> C getInstance(String name, Class<C> type) {
		C instance = super.getInstance(name, type);
		if (instance != null) {
			return instance;
		}
		IClientConfig config = getInstance(name, IClientConfig.class);
		return instantiateWithConfig(getContext(name), type, config);
	}
}

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
		implements DisposableBean, ApplicationContextAware {
	// contexts 儲存了每個 serviceId 對應的所有配置
	private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
	
	public <T> T getInstance(String name, Class<T> type) {
		AnnotationConfigApplicationContext context = getContext(name);
		if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
				type).length > 0) {
			return context.getBean(type);
		}
		return null;
	}
	
	// 返回 serviceId 對應的 AnnotationConfigApplicationContext
	// 如果不存在,呼叫 createContext(String name) 方法建立
	protected AnnotationConfigApplicationContext getContext(String name) {
		if (!this.contexts.containsKey(name)) {
			synchronized (this.contexts) {
				if (!this.contexts.containsKey(name)) {
					this.contexts.put(name, createContext(name));
				}
			}
		}
		return this.contexts.get(name);
	}
	
	// 建立服務 name 對應的 AnnotationConfigApplicationContext
	protected AnnotationConfigApplicationContext createContext(String name) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		// 1. 先檢查外部匯入的配置 configurations 中是否包含 serviceId 服務,有的話匯入
		if (this.configurations.containsKey(name)) {
			for (Class<?> configuration : this.configurations.get(name)
					.getConfiguration()) {
				context.register(configuration);
			}
		}
		// 2. 檢查外部匯入的配置中是否含有預設配置,有的話匯入
		for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
			if (entry.getKey().startsWith("default.")) {
				for (Class<?> configuration : entry.getValue().getConfiguration()) {
					context.register(configuration);
				}
			}
		}
		// 3. 將 PropertyPlaceholderAutoConfiguration 以及 defaultConfigType 註冊類中的 bean 註冊到 context 中(如果使用 SpringClientFactory 的預設 bean 的話,這裡的 this.defaultConfigType 指的是 RibbonClientConfiguration)
		context.register(PropertyPlaceholderAutoConfiguration.class,
				this.defaultConfigType);
		context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
				this.propertySourceName,
				Collections.<String, Object> singletonMap(this.propertyName, name)));
		if (this.parent != null) {
			// Uses Environment from parent as well as beans
			context.setParent(this.parent);
		}
		context.setDisplayName(generateDisplayName(name));
		context.refresh();
		return context;
	}
}
複製程式碼

3. ILoadBalancer

LoadBalancerClient 的 getServer 方法最終呼叫的是 ILoadBalancer 介面的 chooseServer 方法。

public class RibbonLoadBalancerClient implements LoadBalancerClient {
	protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		if (loadBalancer == null) {
			return null;
		}
		// Use 'default' on a null hint, or just pass it on?
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}
	...
}

public interface ILoadBalancer {
    /**
	 * Choose a server from load balancer.
	 */
	public Server chooseServer(Object key);
    ...
}
複製程式碼

根據 ILoadBalancer 的 chooseServer(Object key) 方法選擇服務例項。

ILoadBalancer 介面的實現類如下圖所示,預設的 bean 為 ZoneAwareLoadBalancer。

Ribbon原始碼解析

分析下 ILoadBase 的基本要素:

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);
    }
複製程式碼
  • ClientConfig:定義了客戶端配置,用於初始化客戶端以及負載均衡配置。
  • IRule:負載均衡的規則(策略),比如輪詢等。
  • IPing:定義瞭如何確定服務例項是否正常。
  • ServerList:定義了獲取伺服器列表的方法。
  • ServerListUpdater:定義了動態更新伺服器列表的策略。
  • ServerListFilter:根據配置或者過濾規則選擇特定的伺服器列表,使用 consul 作為註冊中心的話預設實現為 HealthServiceServerListFilter。

在 RibbonClientConfiguration 配置類中定義了預設實現:

@Configuration
@EnableConfigurationProperties
@Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {

	public static final int DEFAULT_CONNECT_TIMEOUT = 1000;
	public static final int DEFAULT_READ_TIMEOUT = 1000;
	public static final boolean DEFAULT_GZIP_PAYLOAD = true;

	@RibbonClientName
	private String name = "client";

	@Autowired
	private PropertiesFactory propertiesFactory;

	@Bean
	@ConditionalOnMissingBean
	public IClientConfig ribbonClientConfig() {
		DefaultClientConfigImpl config = new DefaultClientConfigImpl();
		config.loadProperties(this.name);
		config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
		config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
		config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
		return config;
	}

	@Bean
	@ConditionalOnMissingBean
	public IRule ribbonRule(IClientConfig config) {
		if (this.propertiesFactory.isSet(IRule.class, name)) {
			return this.propertiesFactory.get(IRule.class, config, name);
		}
		ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
		rule.initWithNiwsConfig(config);
		return rule;
	}

	@Bean
	@ConditionalOnMissingBean
	public IPing ribbonPing(IClientConfig config) {
		if (this.propertiesFactory.isSet(IPing.class, name)) {
			return this.propertiesFactory.get(IPing.class, config, name);
		}
		return new DummyPing();
	}

	@Bean
	@ConditionalOnMissingBean
	@SuppressWarnings("unchecked")
	public ServerList<Server> ribbonServerList(IClientConfig config) {
		if (this.propertiesFactory.isSet(ServerList.class, name)) {
			return this.propertiesFactory.get(ServerList.class, config, name);
		}
		ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
		serverList.initWithNiwsConfig(config);
		return serverList;
	}

	@Bean
	@ConditionalOnMissingBean
	public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
		return new PollingServerListUpdater(config);
	}

	@Bean
	@ConditionalOnMissingBean
	public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
			ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
			IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
		if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
			return this.propertiesFactory.get(ILoadBalancer.class, config, name);
		}
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
				serverListFilter, serverListUpdater);
	}

	@Bean
	@ConditionalOnMissingBean
	@SuppressWarnings("unchecked")
	public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
		if (this.propertiesFactory.isSet(ServerListFilter.class, name)) {
			return this.propertiesFactory.get(ServerListFilter.class, config, name);
		}
		ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
		filter.initWithNiwsConfig(config);
		return filter;
	}

	@Bean
	@ConditionalOnMissingBean
	public RibbonLoadBalancerContext ribbonLoadBalancerContext(ILoadBalancer loadBalancer,
															   IClientConfig config, RetryHandler retryHandler) {
		return new RibbonLoadBalancerContext(loadBalancer, config, retryHandler);
	}

	@Bean
	@ConditionalOnMissingBean
	public RetryHandler retryHandler(IClientConfig config) {
		return new DefaultLoadBalancerRetryHandler(config);
	}

	@Bean
	@ConditionalOnMissingBean
	public ServerIntrospector serverIntrospector() {
		return new DefaultServerIntrospector();
	}

	@PostConstruct
	public void preprocess() {
		setRibbonProperty(name, DeploymentContextBasedVipAddresses.key(), name);
	}

	static class OverrideRestClient extends RestClient {

		private IClientConfig config;
		private ServerIntrospector serverIntrospector;

		protected OverrideRestClient(IClientConfig config,
				ServerIntrospector serverIntrospector) {
			super();
			this.config = config;
			this.serverIntrospector = serverIntrospector;
			initWithNiwsConfig(this.config);
		}

		@Override
		public URI reconstructURIWithServer(Server server, URI original) {
			URI uri = updateToSecureConnectionIfNeeded(original, this.config,
					this.serverIntrospector, server);
			return super.reconstructURIWithServer(server, uri);
		}

		@Override
		protected Client apacheHttpClientSpecificInitialization() {
			ApacheHttpClient4 apache = (ApacheHttpClient4) super.apacheHttpClientSpecificInitialization();
			apache.getClientHandler().getHttpClient().getParams().setParameter(
					ClientPNames.COOKIE_POLICY, CookiePolicy.IGNORE_COOKIES);
			return apache;
		}

	}

}
複製程式碼

如果使用 consul 註冊中心的話,預設實現如下:

@Configuration
public class ConsulRibbonClientConfiguration {
	@Autowired
	private ConsulClient client;

	@Value("${ribbon.client.name}")
	private String serviceId = "client";

	protected static final String VALUE_NOT_SET = "__not__set__";

	protected static final String DEFAULT_NAMESPACE = "ribbon";

	public ConsulRibbonClientConfiguration() {
	}

	public ConsulRibbonClientConfiguration(String serviceId) {
		this.serviceId = serviceId;
	}

	@Bean
	@ConditionalOnMissingBean
	public ServerList<?> ribbonServerList(IClientConfig config, ConsulDiscoveryProperties properties) {
		ConsulServerList serverList = new ConsulServerList(client, properties);
		serverList.initWithNiwsConfig(config);
		return serverList;
	}

	@Bean
	@ConditionalOnMissingBean
	public ServerListFilter<Server> ribbonServerListFilter() {
		return new HealthServiceServerListFilter();
	}

	@Bean
	@ConditionalOnMissingBean
	public IPing ribbonPing() {
		return new ConsulPing();
	}

	@Bean
	@ConditionalOnMissingBean
	public ConsulServerIntrospector serverIntrospector() {
		return new ConsulServerIntrospector();
	}

	@PostConstruct
	public void preprocess() {
		setProp(this.serviceId, DeploymentContextBasedVipAddresses.key(), this.serviceId);
		setProp(this.serviceId, EnableZoneAffinity.key(), "true");
	}

	protected void setProp(String serviceId, String suffix, String value) {
		// how to set the namespace properly?
		String key = getKey(serviceId, suffix);
		DynamicStringProperty property = getProperty(key);
		if (property.get().equals(VALUE_NOT_SET)) {
			ConfigurationManager.getConfigInstance().setProperty(key, value);
		}
	}

	protected DynamicStringProperty getProperty(String key) {
		return DynamicPropertyFactory.getInstance().getStringProperty(key, VALUE_NOT_SET);
	}

	protected String getKey(String serviceId, String suffix) {
		return serviceId + "." + DEFAULT_NAMESPACE + "." + suffix;
	}

}
複製程式碼

可以看到,改寫了幾個 bean 的預設實現:

  • ServerList:ConsulServerList;
  • ServerListFilter:HealthServiceServerListFilter;
  • IPing:ConsulPing。

繼續檢視原始碼,最終定位到由 IRule 介面的 choose(Object key) 方法來選擇服務例項。

public class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T> {
	@Override
    public Server chooseServer(Object key) {
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            logger.debug("Zone aware logic disabled or there is only one zone");
            return super.chooseServer(key);
        }
        ...
    }
    ...
}

public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {
	public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }
}

public interface IRule {
    public Server choose(Object key);  
}
複製程式碼

來看下 IRule 介面的實現類,

Ribbon原始碼解析

預設的 ZoneAvoidanceRule 是根據 zone 區域以及可用性來進行選擇。

ZoneAvoidanceRule 繼承了 PredicateBasedRule 類,

public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
	@Override
    public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        // lb.getAllServers(): 獲取所有服務例項
        // 然後根據 serviceId 以及負載均衡策略選擇例項
        Optional<Server> server = 	getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }       
    }
}

// 獲取所有服務例項方法如下
public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {
    @Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
    protected volatile List<Server> allServerList = Collections
            .synchronizedList(new ArrayList<Server>());
    
    // 直接返回 allServerList,說明在生成 BaseLoadBalancer 物件的時候已經完成了初始化操作
    @Override
    public List<Server> getAllServers() {
        return Collections.unmodifiableList(allServerList);
    }
}

複製程式碼

看下 ZoneAwareLoadBalancer bean 是如何生成的:

public class RibbonClientConfiguration {
	@Bean
	@ConditionalOnMissingBean
	public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
			ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
			IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
		if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
			return this.propertiesFactory.get(ILoadBalancer.class, config, name);
		}
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
				serverListFilter, serverListUpdater);
	}
}

public class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T> {
    public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
                                 IPing ping, ServerList<T> serverList, ServerListFilter<T> filter,
                                 ServerListUpdater serverListUpdater) {
        super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
    }
}

public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
    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(clientConfig) 方法,繼續分析該方法:

public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
	void restOfInit(IClientConfig clientConfig) {
        boolean primeConnection = this.isEnablePrimingConnections();
        this.setEnablePrimingConnections(false);
        // enableAndInitLearnNewServersFeature() 方法裡面定義了一個定時任務,會定時執行 updateListOfServers()
        enableAndInitLearnNewServersFeature();
        
        // 立刻執行
        updateListOfServers();
        
        if (primeConnection && this.getPrimeConnections() != null) {
            this.getPrimeConnections()
                    .primeConnections(getReachableServers());
        }
        this.setEnablePrimingConnections(primeConnection);
        LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
    }

	public void enableAndInitLearnNewServersFeature() {
        LOGGER.info("Using serverListUpdater {}", serverListUpdater.getClass().getSimpleName());
        serverListUpdater.start(updateAction);
    }
    
    @VisibleForTesting
    public void updateListOfServers() {
        List<T> servers = new ArrayList();
        if (this.serverListImpl != null) {
            // 1. 獲取所有伺服器列表,呼叫 ConsulServerList 類
            // ConsulServerList 的實現是通過呼叫 consul 提供的 http 介面:/health/service/:service,通過設定 passing(指定是否需要返回所有健康檢查都通過的例項)引數為 false 返回所有例項
            servers = this.serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
            if (this.filter != null) {
                // 2. 獲取過濾後的伺服器列表(返回通過健康檢查的服務例項)
                // 呼叫 HealthServiceServerListFilter 類
                servers = this.filter.getFilteredListOfServers((List)servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
            }
        }

        // 3. 更新可用的服務例項
        this.updateAllServerList((List)servers);
    }
    
    protected void updateAllServerList(List<T> ls) {
        // other threads might be doing this - in which case, we pass
        if (serverListUpdateInProgress.compareAndSet(false, true)) {
            try {
                for (T s : ls) {
                    s.setAlive(true); // set so that clients can start using these
                                      // servers right away instead
                                      // of having to wait out the ping cycle.
                }
                setServersList(ls);
                super.forceQuickPing();
            } finally {
                serverListUpdateInProgress.set(false);
            }
        }
    }
}

public class PollingServerListUpdater implements ServerListUpdater {
    @Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    if (!isActive.get()) {
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {
                        updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };
			// 定義了一個執行緒組,用來執行定時任務,執行緒名為 PollingServerListUpdater-%d
            // 初次執行延遲時間為 1s,之後執行間隔時間預設為 30s
            // 定時任務主要是上面的 updateAction.doUpdate() 方法
            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }
    
    // 定時任務的間隔時間優先從 clientConfig 中獲取,如果沒有設定的話,預設為 30s
    private static long getRefreshIntervalMs(IClientConfig clientConfig) {
        return clientConfig.get(CommonClientConfigKey.ServerListRefreshInterval, LISTOFSERVERS_CACHE_REPEAT_INTERVAL);
    }
}

複製程式碼

以上程式碼的邏輯是:通過呼叫 restOfInit 方法對所有服務例項列表、可用服務例項列表等進行初始化;並且通過在 PollingServerListUpdater 類中設定了一個執行緒組執行定時任務,預設間隔 30s,用於更新所有服務例項以及通過健康檢查的例項列表。

以上程式碼用到了 IRule,ServerList,ServerListFilter 等介面,那麼 IPing 的作用又是什麼呢?

繼續檢視原始碼,

public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {
    // 1. 建構函式中有個設定 setupPingTask 方法
	public BaseLoadBalancer() {
        this.name = DEFAULT_NAME;
        this.ping = null;
        setRule(DEFAULT_RULE);
        setupPingTask();
        lbStats = new LoadBalancerStats(DEFAULT_NAME);
    }
    
    // 2. 該方法中針對每個服務都設定了一個執行緒,定時執行 PingTask
    void setupPingTask() {
        // 可以跳過
        if (canSkipPing()) {
            return;
        }
        if (lbTimer != null) {
            lbTimer.cancel();
        }
        lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
                true);
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000); // 預設10s
        forceQuickPing();
    }
    
    class PingTask extends TimerTask {
        public void run() {
            try {
            	new Pinger(pingStrategy).runPinger();
            } catch (Exception e) {
                logger.error("LoadBalancer [{}]: Error pinging", name, e);
            }
        }
    }
    
    class Pinger {
		// 3. 具體的 PingTask 內容
        public void runPinger() throws Exception {
            if (!pingInProgress.compareAndSet(false, true)) { 
                return; // Ping in progress - nothing to do
            }
      
            Server[] allServers = null;
            boolean[] results = null;

            Lock allLock = null;
            Lock upLock = null;

            try {
                allLock = allServerLock.readLock();
                allLock.lock();
                // 3.1 如果採用註冊中心的話,allServerList 是從註冊中心獲取的伺服器列表
                allServers = allServerList.toArray(new Server[allServerList.size()]);
                allLock.unlock();

                int numCandidates = allServers.length;
                // 3.2 檢查每個服務例項的健康狀態,健康為 true
                // DynamicServerListLoadBalancer 中的 updateListOfServers 設定了 allServers 為通過健康檢查的服務例項
                results = pingerStrategy.pingServers(ping, allServers);

                final List<Server> newUpList = new ArrayList<Server>();
                final List<Server> changedServers = new ArrayList<Server>();

                for (int i = 0; i < numCandidates; i++) {
                    boolean isAlive = results[i];
                    Server svr = allServers[i];
                    boolean oldIsAlive = svr.isAlive();

                    svr.setAlive(isAlive);
					// 3.3 比較和之前的狀態是否不一樣
                    if (oldIsAlive != isAlive) {
                        changedServers.add(svr);
                        logger.debug("LoadBalancer [{}]:  Server [{}] status changed to {}", 
                    		name, svr.getId(), (isAlive ? "ALIVE" : "DEAD"));
                    }

                    // 3.4 更新本地變數 newUpList
                    if (isAlive) {
                        newUpList.add(svr);
                    }
                }
                upLock = upServerLock.writeLock();
                upLock.lock();
                // 3.5 更新可用的服務例項列表
                upServerList = newUpList;
                upLock.unlock();

                notifyServerStatusChangeListener(changedServers);
            } finally {
                pingInProgress.set(false);
            }
        }
    }
}

複製程式碼

經過以上程式碼,得出的流程大概是:

  • PollingServerListUpdater 以及 DynamicServerListLoadBalancer 負責每隔 30s 從註冊中心獲取所有的服務例項,並且篩選出狀態正常的例項加入到 allServerList 以及 upServerList 中;
  • BaseLoadBalancer 中的 setupPingTask() 方法負責每隔 10s "ping" 下這些 allServerList 中的伺服器,如果狀態異常,則從 upServerList 中剔除。

如果有服務例項掛了,給人的感覺是最多會有 10s 左右的時間部分請求可能會報錯,很不幸,這是不對的。最多有可能達到 30s 左右。

深入 3.2 results = pingerStrategy.pingServers(ping, allServers) 這行原始碼,最終定位到(如果採用 ConsulPing 的話):

public class ConsulPing implements IPing {
	@Override
	public boolean isAlive(Server server) {
		boolean isAlive = true;

		if (server != null && server instanceof ConsulServer) {
			ConsulServer consulServer = (ConsulServer) server;
			return consulServer.isPassingChecks();
		}

		return isAlive;
	}
}

複製程式碼

這裡的入參是 DynamicServerListLoadBalancer 返回的狀態正常的伺服器列表,那麼其執行 ConsulPing 的 isAlive 方法,其結果肯定為 true;原因在於呼叫 ConsulPing 的 isAlive 方法,並沒有執行真正意義上的 "ping" 操作(比如再次呼叫 consul 的 api 介面去確認該服務例項是否正常),而是直接返回 ConsulServer 物件中的健康檢查狀態。

三、不算問題的問題

  1. BaseLoadBalancer 類,為每個服務都設定了一個執行緒用於定期執行 PingTask 任務;作為閘道器,如果需要對接上百個微服務的話,就會產生上百個執行緒,而每個執行緒大部分時間處於閒置狀態。可以考慮設定一個較小的執行緒池替代。
  2. Consul 作為註冊中心的話,ConsulPing 的實現沒有起到期望的作用。可以考慮跳過 Ping 檢查(比如設定預設 IPing 為 DummyPing),通過縮小 PollingServerListUpdater 中定時任務的執行間隔來降低個別服務例項掉線的影響。

相關文章