上一章 客户端负载均衡-Ribbon 基础篇 主要介绍一些负载均衡、Ribbon 、RestTemplate 的基本概念和用法,RestTemplate 和 Ribbon 如何结合使用,对 Ribbon 有一个基本的印象。
本章我们对 Ribbon 进行详细的源码分析。
下图为 Ribbon 中的一些主要组件,以及一些相关的实现类。
组件的作用
组件 | 作用 |
---|---|
ILoaderBalancer | 定义一系列的操作接口,比如选择服务实例 |
IRule | 算法策略,内置算法策略来为服务实例的选择提供服务 |
ServerList | 负责服务实例信息的获取,可以获取配置文件中的,也可以从注册中心获取 |
ServerListFilter | 过滤掉某些不想要的服务实例信息 |
ServerListUpdater | 更新本地缓存的服务实例信息 |
IPing | 对已有的服务实例进行可用性检查,保证选择的服务都是可用的 |
下面我们通过 Ribbon 的使用场景来分别介绍这些组件,当我们需要通过 Ribbon 选择一个可用的服务实例信息,进行远程调用时,Ribbon 会根据指定的算法从服务列表中选择一个服务实例进行返回。
在这个选择服务实例的过程中,服务实例信息是怎么来的呢?
ServerList :存储服务实例组件,存储分为静态和动态两种方式
ServerListFilter :在某些场景下我们可能需要过滤一部分服务实例信息,这个时候可以用 ServerListFilter 组件来实现过滤操作。
ServerListUpdater :Ribbon 会将服务实例在本地内存中存储一份,这样就不需要每次都去注册中心获取信息,这种场景的问题在于当服务实例增加或者减少后,本地怎么更新呢?这个时候就需要用到 ServerListUpdater 组件,ServerListUpdater 组件就是用于服务实例更新操作。
IPing:如果缓存到本地的服务实例信息已经无法提供服务了,IPing 可以检测服务实例信息是否可用。
IRule:Ribbon 会根据指定的算法来选择一个可用的实例信息,IRule 组件提供了很多种算法策略来选择实例信息。
ILoadBalancer:使用 Ribbon 的入口了,我们要选择一个可用的服务,怎么选择?问谁要这个服务?这时ILoadBalancer 就上场了,ILoadBalancer 中定义了软件负载均衡操作的接口,比如动态更新一组服务列表,根据指定算法从现有服务器列表中选择一个可用的服务等操作。
我们再上一章的示例 客户端负载均衡-Ribbon 基础篇-5.RestTemplate & Ribbon 示例 基础上进行简单的修改
演示手动静态配置 ServerList
修改 ribbon-demo 配置文件 application.yaml
server:
port: 8180
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:8761/eureka/
spring:
application:
name: ribbon-demo
ribbon:
eureka:
enabled: false
user-service:
ribbon:
listOfServers: localhost:8181,localhost:8182
启动相关应用测试,可以看到 EurekaServer 是关闭状态(这里不需要注册中心)
测试负载均衡结果,浏览器输入 http://127.0.0.1:8180/ribbon/getUser
第一次刷新,结果如下图所示:
第二次刷新,结果如下图所示:
多次刷新,每次的返回结果的 port 端口号都不一样,说明静态配置有效。
手动配置 listOfServers 可以让我们在某些场景下更加方便的进行调试工作,在正式的使用中,所有的服务实例信息都是从注册中心拉取的,也就是从我们前面讲的 Eureka 中获取。
所以我们还是将示例还原从 Eureka 注册中心获取,分析下 @LoadBalanced 实现负载均衡的原理。
首先我们可以搜索源码,看看哪些地方用到了 @LoadBalance,这里我们可以找到 LoadBalancerAutoConfiguration,看名称是负载均衡器的自动配置类。
这里采用 @Autowired 集合注入的方式将所有标有 @LoadBalanced 注解的 RestTemplate 对象都注入到 restTemplates 集合中。
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
...
然后循环遍历标注 @LoadBalanced 的 restTemplates 集合对象,设置拦截器
@Bean 配置 SmartInitializingSingleton 对象
@Bean 方法注入 RestTemplateCustomizer 对象
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
继续往下探源码,最终会执行 LoadBalancerInterceptor#intercept 的拦截方法中
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
...
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
然后调用 RibbonLoadBalancerClient#execute 方法
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
关键方法 getServer(),此方法就是用于选择具体的服务实例,最终会交给 ILoadBalancer 类去选择服务实例。
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
IRule 接口通过传入的参数 “default” 选择默认的负载均衡策略,从 Serverlist 中选择一个实例返回。
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
通过上面的过程我们已经了解到,@LoadBalancer 拦截和选择实例的过程,最后调用对应的服务实例,那么服务实例集合又是如何进行获取和更新的呢?
调用链路如下:
DynamicServerListLoadBalancer#initWithNiwsConfig -> restOfInit -> updateListOfServers -> DiscoveryEnabledNIWSServerList#getUpdatedListOfServers -> obtainServersViaDiscovery -> eurekaClient.getInstancesByVipAddress
最终通过 EurekaClient 来获取服务注册列表信息,具体的细节可以更加以上这个调用链路来进行追踪。
调试截图:
可以看到本示例中,在第一次调用 http://127.0.0.1:8180/ribbon/getUser
接口的时候触发这个过程,从 Eureka 中获取了两个服务实例信息。
这个地方可以设置为启动的时候自动获取,在本章的 第9小节 中会介绍。
从第二小节 组件的作用和联系 的图中,我们可以看到有三种方式可以对 Server 实例信息进行更新
com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateListOfServers
在这个方法中有个判断
如果 filter 不为空,意思就是我们如果设置了 ServerListFiter,这里的 server 实例列表就会根据我们设置的 ServerListFiter 来进行过滤操作
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
ServerListUpdater 有下面两个实现类
这里我们来看看 ServerListUpdater 默认情况下的调用链路
DynamicServerListLoadBalancer#initWithNiwsConfig -> restOfInit-> enableAndInitLearnNewServersFeature -> serverListUpdater.start(updateAction)
start() 方法源码如下:
@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);
}
}
};
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,
refreshIntervalMs,
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}
调试截图
BaseLoadBalancer 构造方法-> setupPingTask() 方法
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
定时每 10 秒钟执行 PingTask 任务
class PingTask extends TimerTask {
public void run() {
try {
new Pinger(pingStrategy).runPinger();
} catch (Exception e) {
logger.error("LoadBalancer [{}]: Error pinging", name, e);
}
}
}
查看 Pinger 的 runPinger 方法,最终根据 pingerStrategy.pingServers(ping, allServers);
来获取服务的可用性,如果返回的结果与之前相同,则不向 EurekaClient 获取注册列表;如果不同,则通知 ServerStatusChangeListener
服务注册列表发生了变化,进行更新或者重新拉取。
代码较多这里就不贴出来了,源码位置如下:com.netflix.loadbalancer.BaseLoadBalancer.Pinger#runPinger
IPing 如何判断服务是否可用?
IPing 用于向 server 发送 ping
来判断该 server 是否有响应,从而判断该 server 是否可用。
public interface IPing {
/**
* Checks whether the given Server
is "alive" i.e. should be
* considered a candidate while loadbalancing
*
*/
public boolean isAlive(Server server);
}
IPing 的实现类
IRule 类关系图如下:
自定义负载均衡算法有实现和继承两种方式
这里我们使用第二种方式来实现一个随机算法,代码如下:
public class MyRule extends AbstractLoadBalancerRule {
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
String clientName = clientConfig.getClientName();
System.out.println(clientName);
}
@Override
public Server choose(Object key) {
ILoadBalancer loadBalancer = getLoadBalancer();
List<Server> allServers = loadBalancer.getAllServers();
return allServers.get(new Random().nextInt(allServers.size()));
}
}
在配置文件 application.yaml 中指定规则
# 指定user-service的负载策略
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.csdn.ribbon.rule.MyRule
浏览器输入 http://127.0.0.1:8180/ribbon/getUser
测试效果如下:
或者
端口号随机出现。
除了通过配置文件的方式来指定自定义的负载均衡策略,也可以通过配置 Bean 的方式
新增两个配置 Bean 即可
BeanConfiguration:
public class BeanConfiguration {
@Bean
public IRule myRule() {
return new MyRule();
}
}
RibbonClientConfig:
@RibbonClient(name="user-service", configuration=BeanConfiguration.class)
public class RibbonClientConfig {
}
测试效果一样。
这点是在开发过程中相关度比较大的,就是某些场景可能更适合轮询算法,但是单纯的轮询算法可能不是你想要的,这个时候就需要在轮询的基础上,加上一些你自己的逻辑,组成一个新的算法,让 Ribbon 使用这个算法来进行服务实例的选择。
灰度发布是能够平滑过渡的一种发布方式,在发布过程中,先发布一部分应用,让指定的用户使用刚发布的应用,等到测试没有问题后,再将其他的全部应用发布。如果新发布的有问题,只需要将这部分恢复即可,不用恢复所有的应用。
多版本隔离跟灰度发布类似,为了兼容或者过度,某些应用会有多个版本,这个时候如何保证 1.0 版本的客户端不会调用到 1.1 版本的服务,就是我们需要考虑的问题。
当线上某个实例发生故障后,为了不影响用户,我们一般都会先留存证据,比如:线程信息、JVM 信息等,然后将这个实例重启或直接停止。然后线下根据一些信息分析故障原因,如果我能做到故障隔离,就可以直接将出问题的实例隔离,不让正常的用户请求访问到这个出问题的实例,只让指定的用户访问,这样就可以单独用特定的用户来对这个出问题的实例进行测试、故障分析等。
从本章 第5小节 的调试过程中,我们发现了一个细节问题,就是 Ribbon 在进行客户端负载均衡时并不是在启动时就加载上下文,而是在第一次请求时才去创建,因此第一次调用会比较慢,有可能会引起调用超时。可以通过指定 Ribbon 客户端的名称,在启动时加载这些子应用程序上下文的方式,来避免这个问题。
增加配置信息如下,我们这里只需要更加载 user-service 服务:
ribbon:
eager-load:
enabled : true
clients : user-service
配置完成后,在 DynamicServerListLoadBalancer#updateListOfServers 240 行打上断点,启动应用,可以看到会进入到此方法中来更新 ServerList 的服务实例信息。
我们来分析下这个参数是如何工作的
第一步 RibbonAutoConfiguration 自动配置类中,可以看到条件配置注解是 @ConditionalOnProperty(value = "ribbon.eager-load.enabled")
,意思就是配置文件中如果有 ribbon.eager-load.enabled = true,这个 @Bean 才能生效
@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
return new RibbonApplicationContextInitializer(springClientFactory(),
ribbonEagerLoadProperties.getClients());
}
继续看这个 RibbonEagerLoadProperties 配置 Bean,可以看到这里面 还有一个 clients 属性
@ConfigurationProperties(prefix = "ribbon.eager-load")
public class RibbonEagerLoadProperties {
private boolean enabled = false;
private List<String> clients;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getClients() {
return clients;
}
public void setClients(List<String> clients) {
this.clients = clients;
}
}
最后会调用 RibbonApplicationContextInitializer#initialize 方法,如果 clientNames 不为 null 才能执行后续操作,所以 clients 也需要配置才行。
protected void initialize() {
if (clientNames != null) {
for (String clientName : clientNames) {
this.springClientFactory.getContext(clientName);
}
}
}
最终我们得到结论只要配置了下面两个参数,就可以在启动的时候提前进行 ServerList 的初始化
描述 | 配置 |
---|---|
负债均衡器操作接口 | |
负债均衡算法 | |
服务器可用性检查 | |
服务器列表获取 | |
服务器列表过滤 |
从 1.2.0 版本开始,支持通过属性配置的方式来定义 Ribbon Client。配置格式也是标准的,clientName 就是服务名称,比如 user-service,当我们需要配置一个自定义算法的时候,那就是 user-service.ribbon.NFLoadBalancerRuleClassName = 算法类的路径。
《深入理解 Spring Cloud 与微服务架构》 方志朋
《300分钟搞懂 Spring Cloud》尹吉欢