【Spring Cloud】协同开发利器之动态路由|Ribbon & LoadBalancer 解析篇

介绍

前段时间我写了一个库,用于解决开发环境中多人协同调试接口的问题:

网关转发或服务间调用时会负载均衡到别人的服务,导致调试效率变低

功能大概是这样的

【Spring Cloud】协同开发利器之动态路由|Ribbon & LoadBalancer 解析篇_第1张图片

通过配置我们需要调试的接口路由到我们自己的服务来解决上述问题

如果大家有兴趣可以看【Spring Cloud】协同开发利器之动态路由有具体的使用详解

今天主要是来讲一讲这个库的实现方式以及RibbonLoadBalancer的相关内容

负载均衡

首先先来说说负载均衡的组件

在之前的SpringCloud中,基本就是使用Ribbon来实现负载均衡的,不管是Zuul还是SpringCloudGateway还是OpenFeign

而现在最新的SpringCloudGatewayOpenFeign都改用了SpringCloudLoadBalancer

我记得SpringBoot2.6.x开始的版本就已经不用Ribbon

所以大家如果升级了SpringCloudSpringBoot的版本的话,需要注意Ribbon的配置可能会失效

Ribbon

Ribbon已经算是最老牌的负载均衡组件了,包含ILoadBalancerServerListIPingIRule这几个主要接口

  • ILoadBalancer通过ServerList来更新服务信息

  • ILoadBalancer通过IPing来更新服务的状态是否可用

  • ILoadBalancer通过IRule来选择可用的服务实例

下面我们一个一个来分析

ILoadBalancer

ILoadBalancerRibbon的核心组件

/**
 * Interface that defines the operations for a software loadbalancer. A typical
 * loadbalancer minimally need a set of servers to loadbalance for, a method to
 * mark a particular server to be out of rotation and a call that will choose a
 * server from the existing list of server.
 * 
 * @author stonse
 * 
 */
public interface ILoadBalancer {

   /**
    * Initial list of servers.
    * This API also serves to add additional ones at a later time
    * The same logical server (host:port) could essentially be added multiple times
    * (helpful in cases where you want to give more "weightage" perhaps ..)
    * 
    * @param newServers new servers to add
    */
   public void addServers(List<Server> newServers);
   
   /**
    * Choose a server from load balancer.
    * 
    * @param key An object that the load balancer may use to determine which server to return. null if 
    *         the load balancer does not use this parameter.
    * @return server chosen
    */
   public Server chooseServer(Object key);
   
   /**
    * To be called by the clients of the load balancer to notify that a Server is down
    * else, the LB will think its still Alive until the next Ping cycle - potentially
    * (assuming that the LB Impl does a ping)
    * 
    * @param server Server to mark as down
    */
   public void markServerDown(Server server);
   
   /**
    * @deprecated 2016-01-20 This method is deprecated in favor of the
    * cleaner {@link #getReachableServers} (equivalent to availableOnly=true)
    * and {@link #getAllServers} API (equivalent to availableOnly=false).
    *
    * Get the current list of servers.
    *
    * @param availableOnly if true, only live and available servers should be returned
    */
   @Deprecated
   public List<Server> getServerList(boolean availableOnly);

   /**
    * @return Only the servers that are up and reachable.
     */
    public List<Server> getReachableServers();

    /**
     * @return All known servers, both reachable and unreachable.
     */
   public List<Server> getAllServers();
}

ILoadBalancer最主要的方法就是chooseServer,用于返回一个服务实例

同时可以看到ILoadBalancer也提供服务实例Server的数据

继续看ILoadBalancer的实现类BaseLoadBalancer(省略部分代码)

public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {

    protected volatile List<Server> allServerList = Collections
            .synchronizedList(new ArrayList<Server>());
    
    protected volatile List<Server> upServerList = Collections
            .synchronizedList(new ArrayList<Server>());
    
    @Override
    public List<Server> getReachableServers() {
        return Collections.unmodifiableList(upServerList);
    }

    @Override
    public List<Server> getAllServers() {
        return Collections.unmodifiableList(allServerList);
    }
}

BaseLoadBalancer本身缓存了这些服务信息

而这些服务信息是通过我们接下来要讲的ServerListIPing来更新和维护

ServerList

ServerList主要用于提供服务实例信息

/**
 * Interface that defines the methods sed to obtain the List of Servers
 * @author stonse
 *
 * @param 
 */
public interface ServerList<T extends Server> {

    public List<T> getInitialListOfServers();
    
    /**
     * Return updated list of servers. This is called say every 30 secs
     * (configurable) by the Loadbalancer's Ping cycle
     * 
     */
    public List<T> getUpdatedListOfServers();   

}

ServerListgetInitialListOfServers用于获得初始化的服务信息,不过我暂时没有看到哪里的实现有调用

getUpdatedListOfServers是用于更新服务信息,基本上所有的实现都是调用这个方法

以使用Eureka作为注册中心为例(省略部分代码)

public class DiscoveryEnabledNIWSServerList extends AbstractServerList<DiscoveryEnabledServer>{

    private final Provider<EurekaClient> eurekaClientProvider;

    @Override
    public List<DiscoveryEnabledServer> getInitialListOfServers(){
        return obtainServersViaDiscovery();
    }

    @Override
    public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
        return obtainServersViaDiscovery();
    }

    private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        //省略大部分代码
        //主要就是通过Eureka获得服务实例信息
        EurekaClient eurekaClient = eurekaClientProvider.get();
        List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
        return serverList;
    }
}

DiscoveryEnabledNIWSServerListServerList基于Eureka的实现,通过EurekaClient来获得服务信息

再来看看ILoadBalancer中是怎么调用ServerList

public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {

    //可以看作是DiscoveryEnabledNIWSServerList
    volatile ServerList<T> serverListImpl;

    protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
    };

    protected volatile ServerListUpdater serverListUpdater;

    public void enableAndInitLearnNewServersFeature() {
        //初始化时会调用
        serverListUpdater.start(updateAction);
    }
    
    public void updateListOfServers() {
        List<T> servers = serverListImpl.getUpdatedListOfServers();
        updateAllServerList(servers);
    }
}

DynamicServerListLoadBalancer继承了BaseLoadBalancer即实现了ILoadBalancer接口

有一个ServerListUpdater.UpdateAction的回调接口会调用updateListOfServers方法

updateListOfServers方法又会调用DiscoveryEnabledNIWSServerList(也就是ServerList)的getUpdatedListOfServers方法,通过Eureka来获得服务实例信息

然后在一个初始化就会被调用的方法enableAndInitLearnNewServersFeatureServerListUpdater调用start方法将ServerListUpdater.UpdateAction作为参数传入

接下来看看这个ServerListUpdater的实现

public class EurekaNotificationServerListUpdater implements ServerListUpdater {

    private final ExecutorService refreshExecutor;

    private volatile EurekaEventListener updateListener;
    private volatile EurekaClient eurekaClient;

    @Override
    public synchronized void start(final UpdateAction updateAction) {
        //省略大部分代码
        //监听Eureka事件来更新
        this.updateListener = new EurekaEventListener() {
            @Override
            public void onEvent(EurekaEvent event) {
                //省略了CacheRefreshedEvent判断
                refreshExecutor.submit(new Runnable() {
                    @Override
                    public void run() {
                        updateAction.doUpdate();
                    }
                });  
            }
        }
        eurekaClient.registerEventListener(updateListener);
    }

    @Override
    public synchronized void stop() {
        eurekaClient.unregisterEventListener(updateListener);
    }
}

EurekaNotificationServerListUpdaterstart方法往Eureka上注册了一个事件监听器,当监听到相应的事件(CacheRefreshedEvent)时就会调用ServerListUpdater.UpdateAction的更新方法

不知道大家看下来会不会有点绕,已经尽量把多余的代码去掉了,做个简短的总结:

  1. DynamicServerListLoadBalancer(这是一个ILoadBalancer)在初始化的时候通过EurekaNotificationServerListUpdaterEureka上注册了事件监听

  2. 当监听到EurekaCacheRefreshedEvent事件时调用DiscoveryEnabledNIWSServerList(这是一个ServerList)的getUpdatedListOfServers方法获得最新的服务实例信息并更新

ServerListUpdater还有另一个实现PollingServerListUpdater

public class PollingServerListUpdater implements ServerListUpdater {

    private volatile ScheduledFuture<?> scheduledFuture;

    @Override
    public synchronized void start(final UpdateAction updateAction) {
        final Runnable wrapperRunnable = new Runnable() {
            @Override
            public void run() {
                updateAction.doUpdate();
            };

            //定时更新
            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS
            );
    }
    
    ScheduledThreadPoolExecutor getRefreshExecutor() {
        ...
    }

    @Override
    public synchronized void stop() {
        scheduledFuture.cancel(true);
    }
}

PollingServerListUpdater是通过定时更新的方式来更新服务信息的

IPing

IPing是用于更新服务是否可用的状态的接口

/**
 * Interface that defines how we "ping" a server to check if its alive
 * @author stonse
 *
 */
public interface IPing {
    
    /**
     * Checks whether the given Server is "alive" i.e. should be
     * considered a candidate while loadbalancing
     * 
     */
    public boolean isAlive(Server server);
}

通过isAlive来获得一个Server是否还存活来更新服务状态

BaseLoadBalancer内部会有一个定时任务去执行IPing接口(省略部分代码)

public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {

    private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();

    protected IPingStrategy pingStrategy = DEFAULT_PING_STRATEGY;

    protected IPing ping;
    
    protected Timer lbTimer;

    public BaseLoadBalancer() {
        setupPingTask();
    }

    void setupPingTask() {
        lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true);
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
    }

    class PingTask extends TimerTask {
    
        public void run() {
            new Pinger(pingStrategy).runPinger();
        }
    }

    class Pinger {

        private final IPingStrategy pingerStrategy;

        public Pinger(IPingStrategy pingerStrategy) {
            this.pingerStrategy = pingerStrategy;
        }

        public void runPinger() throws Exception {
            results = pingerStrategy.pingServers(ping, allServers);
        }
    }

    private static class SerialPingStrategy implements IPingStrategy {

        @Override
        public boolean[] pingServers(IPing ping, Server[] servers) {
            int numCandidates = servers.length;
            boolean[] results = new boolean[numCandidates];
            for (int i = 0; i < numCandidates; i++) {
                results[i] = false; /* Default answer is DEAD. */
                results[i] = ping.isAlive(servers[i]);
            }
            return results;
        }
    }
}

这部分代码应该不难看懂,通过Timer每隔一段时间对每个Server执行一次IPing接口来更新服务的状态

IRule

IRule是用于实现负载均衡策略的

/**
 * Interface that defines a "Rule" for a LoadBalancer. A Rule can be thought of
 * as a Strategy for loadbalacing. Well known loadbalancing strategies include
 * Round Robin, Response Time based etc.
 * 
 * @author stonse
 * 
 */
public interface IRule {
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

大家还记得ILoadBalancerchooseServer方法么

public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {

    private final static IRule DEFAULT_RULE = new RoundRobinRule();

    protected IRule rule = DEFAULT_RULE;

    /*
     * Get the alive server dedicated to key
     * 
     * @return the dedicated server
     */
    public Server chooseServer(Object key) {
        return rule.choose(key);
    }
}

ILoadBalancerchooseServer最终就是调用IRule来选择服务的

如果大家想要自定义服务均衡规则就可以实现IRule接口

Ribbon的负载均衡的逻辑大致就是这样

接下来讲讲如何适配OpenFeignGateway的动态路由

动态路由实现思路(OpenFeign)

要了解OpenFeign中配置的组件(通过组件来进行扩展)肯定是要从对应的自动配置中找了

首先我们找到OpenFeignspring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration

FeignRibbonClientAutoConfiguration看起来挺像我们要找的类

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled",
      matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
      OkHttpFeignLoadBalancedConfiguration.class,
      DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {

    //省略代码
}

看到HttpClientFeignLoadBalancedConfigurationOkHttpFeignLoadBalancedConfiguration我突然就想到了feign.httpclient.enabledfeign.okhttp.enabled这两个配置,哈哈哈

我们看看这两个类是怎么配置的

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
@Import(HttpClientFeignConfiguration.class)
class HttpClientFeignLoadBalancedConfiguration {

   @Bean
   @ConditionalOnMissingBean(Client.class)
   public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
         SpringClientFactory clientFactory, HttpClient httpClient) {
      ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
      return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
   }

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
@Import(OkHttpFeignConfiguration.class)
class OkHttpFeignLoadBalancedConfiguration {

   @Bean
   @ConditionalOnMissingBean(Client.class)
   public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
         SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
      OkHttpClient delegate = new OkHttpClient(okHttpClient);
      return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
   }

}

除了HTTP客户端不一样之外其他的配置都一样

于是CachingSpringLoadBalancerFactorySpringClientFactoryLoadBalancerFeignClient这几个类就引起了我的注意

CachingSpringLoadBalancerFactorySpringClientFactoryLoadBalancerFeignClient的参数,所以如果我们能在LoadBalancerFeignClient中做一些扩展的话应该是最简单的了

我们来看看LoadBalancerFeignClient(省略部分代码)

public class LoadBalancerFeignClient implements Client {

   private final Client delegate;

   private CachingSpringLoadBalancerFactory lbClientFactory;

   private SpringClientFactory clientFactory;

   @Override
   public Response execute(Request request, Request.Options options) throws IOException {
       return lbClient(clientName)
               .executeWithLoadBalancer(ribbonRequest, requestConfig)
               .toResponse();
   }

   private FeignLoadBalancer lbClient(String clientName) {
      return this.lbClientFactory.create(clientName);
   }
}

主要的方法就是这个execute方法了,然后executeWithLoadBalancer这个方法好像直接返回响应了,所以服务选择的逻辑应该是包含在lbClient或是executeWithLoadBalancer里面

lbClient调用了CachingSpringLoadBalancerFactorycreate方法得到了一个FeignLoadBalancer(这里可以稍微眼熟一下这个类)

我们接着来看CachingSpringLoadBalancerFactory

public class CachingSpringLoadBalancerFactory {

    protected final SpringClientFactory factory;

    public FeignLoadBalancer create(String clientName) {
        //省略部分代码
        ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
        //省略部分代码
        return client;
    }
}

还记得Ribbon中的ILoadBalancer

ILoadBalancer是通过SpringClientFactory来获得的

public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {

    public ILoadBalancer getLoadBalancer(String name) {
        //我们可以在这里扩展ILoadBalancer
        return getInstance(name, ILoadBalancer.class);
    }
    
    //省略其他代码
}

所以我们可以通过扩展SpringClientFactory来插入我们的逻辑

@AllArgsConstructor
public class CustomSpringClientFactory extends SpringClientFactory {

    private final SpringClientFactory factory;

    @Override
    public ILoadBalancer getLoadBalancer(String name) {
        return new CustomLoadBalancer(factory.getLoadBalancer(name));
    }

    @AllArgsConstructor
    public static class CustomLoadBalancer implements ILoadBalancer {

        private final ILoadBalancer loadBalancer;

        @Override
        public Server chooseServer(Object key) {
            //如果key能把url传进来就可以在这里扩展
            return loadBalancer.chooseServer(key);
        }
    }
}

这里我来说明一下为什么要持有一个内部的SpringClientFactory而不是直接调用super方法

因为通过这种方式我们可以结合BeanPostProcessor来扩展任何SpringClientFactory

如果你的项目中已经重写了SpringClientFactory,我的扩展也不会和你自定义的发生冲突,而是扩展了你自定义的实现

@AllArgsConstructor
public class SpringClientFactoryEnhancer implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
        if (bean instanceof SpringClientFactory) {
            //这个SpringClientFactory可以是默认的实现也可以是项目中自定义的
            //这样就不会有任何冲突和影响
            return new CustomSpringClientFactory((SpringClientFactory) bean);
        }
        return bean;
    }
}

现在我们已经扩展了SpringClientFactory获得的ILoadBalancer

你以为这样就可以了么?不不不,当你测试之后会发现CustomLoadBalancerchooseServer传入的keynull,我们需要在这个地方传入url才能做动态路由的匹配

所以我们要继续扩展来传入url

那么CustomLoadBalancerchooseServer传入的key是从哪里传进来的呢?

lbClientexecuteWithLoadBalancer这两个方法,之前跟了一下lbClient,现在我们来跟一下executeWithLoadBalancer,这个方法是lbClient返回的FeignLoadBalancer(就是之前提示大家眼熟一下的类)当中的方法

public abstract class AbstractLoadBalancerAwareClient<S extends ClientRequest, T extends IResponse> extends LoadBalancerContext implements IClient<S, T>, IClientConfigAware {
  
    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        return command.submit(new ServerOperation<T>() {
            @Override
            public Observable<T> call(Server server) {
                //这里已经选完了服务
                //代码略过
            }
        })
        .toBlocking()
        .single();
    }   
}

FeignLoadBalancer继承了AbstractLoadBalancerAwareClient,所以调用的是父类的实现

这里先构造了一个LoadBalancerCommand然后调用submit方法并回调选择的服务

所以服务选择就是在submit里面

这里大家也先注意一下AbstractLoadBalancerAwareClient继承了LoadBalancerContext,即FeignLoadBalancer就是LoadBalancerContext

我们继续跟到submit里面

public class LoadBalancerCommand<T> {

    private final URI    loadBalancerURI;
    private final Object loadBalancerKey;
    
    //这个就是FeignLoadBalancer
    private final LoadBalancerContext loadBalancerContext;

    private Observable<Server> selectServer() {
        //省略部分代码
        Server server = loadBalancerContext
        .getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);
        //省略部分代码
    }     
   
    public Observable<T> submit(final ServerOperation<T> operation) {
        //省略部分代码
        selectServer();
        //省略部分代码
    }
}

submit中我发现调用了selectServer,而selectServer又调用了loadBalancerContext(就是FeignLoadBalancer)的getServerFromLoadBalancer方法,并传入了loadBalancerURIloadBalancerKey

我感觉这个loadBalancerKey就是我们要找的key,所以我们继续跟进去看看(省略部分代码)

public abstract class AbstractLoadBalancerAwareClient<S extends ClientRequest, T extends IResponse> extends LoadBalancerContext implements IClient<S, T>, IClientConfigAware {
  
    public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
        ILoadBalancer lb = getLoadBalancer();
        Server svc = lb.chooseServer(loadBalancerKey);
    }
}

果然,loadBalancerKey就是我们要传的key,并且是在LoadBalancerCommand中配置的(其实我本来可以直接重写getServerFromLoadBalancer这个方法把original(也就是URI)传进去,但是我当时并没有想到,脑子短路了)

LoadBalancerCommand其实就是在FeignLoadBalancerexecuteWithLoadBalancer方法中构建的(回顾一下)

public abstract class AbstractLoadBalancerAwareClient<S extends ClientRequest, T extends IResponse> extends LoadBalancerContext implements IClient<S, T>, IClientConfigAware {
  
    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        return command.submit(new ServerOperation<T>() {
            @Override
            public Observable<T> call(Server server) {
                //这里已经选完了服务
                //代码略过
            }
        })
        .toBlocking()
        .single();
    }   
}

我们看到buildLoadBalancerCommand方法中传入了一个request,既然是request那肯定有url的数据吧

public class FeignLoadBalancer extends
      AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> {
      
    protected static class RibbonRequest extends ClientRequest implements Cloneable {

    protected RibbonRequest(Client client, Request request, URI uri) {
        //省略部分代码
    }
}

泛型中的request就是FeignLoadBalancer.RibbonRequest,继承了ClientRequest,而且传入了URI,继续进去看看ClientRequest

public class ClientRequest implements Cloneable {

    protected Object loadBalancerKey = null;

    public final Object getLoadBalancerKey() {
        return loadBalancerKey;
    }

    protected final ClientRequest setLoadBalancerKey(Object loadBalancerKey) {
        this.loadBalancerKey = loadBalancerKey;
        return this;
    }
}

确实可以配置一个loadBalancerKey

虽然确认了loadBalancerKey可以配置,但是这个方法用protected是什么意思?而且FeignLoadBalancer.RibbonRequest也是protected

那就只能继承FeignLoadBalancerFeignLoadBalancer.RibbonRequest

public class CustomFeignLoadBalancer extends FeignLoadBalancer {
    
    @Override
    public RibbonResponse executeWithLoadBalancer(RibbonRequest request, IClientConfig requestConfig) throws ClientException {
        //在这里返回CustomRibbonRequest
        return super.executeWithLoadBalancer(new CustomRibbonRequest(request), requestConfig);
    }

    public static class CustomRibbonRequest extends RibbonRequest {

        public CustomRibbonRequest(RibbonRequest request) {
            super(request.getClient(), request.getRequest(), request.getUri());
            //在这里把uri设置为loadBalancerKey
            setLoadBalancerKey(uri);
        }
    }
}

这样是不是就大功告成了呢?

很遗憾,你会发现还是null,不应该啊,明明传了loadBalancerKey怎么还是null呢?LoadBalancerCommand.Builder里面不是有配置的方法么

public class LoadBalancerCommand<T> {

    public static class Builder<T> {
        
        private Object loadBalancerKey;
        
        public Builder<T> withServerLocator(Object key) {
            this.loadBalancerKey = key;
            return this;
        }
    }
}

你确定那你传了么?不妨让我们看看buildLoadBalancerCommand这个方法是怎么构造LoadBalancerCommand

public abstract class AbstractLoadBalancerAwareClient<S extends ClientRequest, T extends IResponse> extends LoadBalancerContext implements IClient<S, T>, IClientConfigAware {
  
    protected LoadBalancerCommand<T> buildLoadBalancerCommand(final S request, final IClientConfig config) {
        RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, config);
        LoadBalancerCommand.Builder<T> builder = LoadBalancerCommand.<T>builder()
            .withLoadBalancerContext(this)
            .withRetryHandler(handler)
            .withLoadBalancerURI(request.getUri());
        customizeLoadBalancerCommandBuilder(request, config, builder);
        return builder.build();
    }  
}

没有调用LoadBalancerCommand.BuilderwithServerLocator方法!!!

没想到吧,就不帮你传,诶嘿,就是玩儿

我当时气的,真是服了这个老六,你自己传个null我就不说什么了,我设置了你都不帮我传一下?

我之前以为FeignLoadBalancer.RibbonRequest有这个字段应该就会处理,结果人家都不带鸟一下

那就只能看看有没有其他地方可以修改这个LoadBalancerCommand.Builder

于是就发现buildLoadBalancerCommand调用了customizeLoadBalancerCommandBuilder方法,看名字应该是用于自定义扩展的,哼,算你还有点良心

public class CustomFeignLoadBalancer extends FeignLoadBalancer {
    
    protected void customizeLoadBalancerCommandBuilder(RibbonRequest request, IClientConfig config, LoadBalancerCommand.Builder<RibbonResponse> builder) {
        //在这里设置url
        builder.withServerLocator(request.getUri());
    }
}

最后通过扩展customizeLoadBalancerCommandBuilderkey传进去了(真的是绕了一大圈)

总结

大致上扩展的思路就是这样了,做个简单的总结吧

  1. 扩展SpringClientFactoryCustomSpringClientFactory)返回CustomLoadBalancer来插入动态路由的逻辑
  2. 扩展FeignLoadBalancerCustomFeignLoadBalancer)在LoadBalancerCommand.Builder中配置loadBalancerKey(或是重写getServerFromLoadBalancer方法)
  3. 扩展CachingSpringLoadBalancerFactoryCustomCachingSpringLoadBalancerFactory)返回CustomFeignLoadBalancer
  4. 使用BeanPostProcessor增强SpringClientFactoryCachingSpringLoadBalancerFactory

动态路由实现思路(Gateway)

接下来我们看看Gateway怎么扩展动态路由

SpringCloudGateway中是通过LoadBalancerClientFilter来做负载均衡的

@Deprecated
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {

   protected final LoadBalancerClient loadBalancer;

   @Override
   public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      //省略部分代码
      final ServiceInstance instance = choose(exchange);
      //省略部分代码
      return chain.filter(exchange);
   }

   protected ServiceInstance choose(ServerWebExchange exchange) {
      return loadBalancer.choose(
            ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
   }
}

我们发现最终是调用LoadBalancerClient来处理的,而且传入的是serviceId

exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)这句代码就是拿到URI(里面的格式应该是:lb://service-id/path),然后getHost就是拿到的serviceId

接下来看LoadBalancerClient是怎么选择服务的(省略部分代码)

public class RibbonLoadBalancerClient implements LoadBalancerClient {

    private SpringClientFactory clientFactory;

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

    public ServiceInstance choose(String serviceId, Object hint) {
        Server server = getServer(getLoadBalancer(serviceId), hint);
    }

    protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
        return loadBalancer.chooseServer(hint != null ? hint : "default");
    }

    protected ILoadBalancer getLoadBalancer(String serviceId) {
        return this.clientFactory.getLoadBalancer(serviceId);
    }
}

SpringClientFactory是不是很眼熟,我们之前刚刚扩展过,用来返回CustomLoadBalancer,这里就可以直接用啦

而且我们看到RibbonLoadBalancerClient有一个choose方法可以直接传入hint作为key

所以我们直接重写LoadBalancerClientFilterchoose方法传入url就行啦

public class CustomLoadBalancerClientFilter extends LoadBalancerClientFilter {

    @Override
    protected ServiceInstance choose(ServerWebExchange exchange) {
        URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String serviceId = Objects.requireNonNull(uri).getHost();
        if (loadBalancer instanceof RibbonLoadBalancerClient) {
            return ((RibbonLoadBalancerClient) loadBalancer).choose(serviceId, uri);
        } else {
            return loadBalancer.choose(serviceId);
        }
    }
}
总结

不得不说网关扩展起来还是简单很多

  1. 扩展SpringClientFactoryCustomSpringClientFactory)返回CustomLoadBalancer来插入动态路由的逻辑
  2. 扩展LoadBalancerClientFilterCustomLoadBalancerClientFilter)重写choose方法传入url
  3. 使用BeanPostProcessor增强SpringClientFactoryLoadBalancerClientFilter

SpringCloudLoadBalaner

接下来说说SpringCloud中提供的LoadBalancer

SpringBoot2.6.x之后的GatewayOpenFeign都已经切换成ReactiveLoadBalancer来实现负载均衡了

我们先看下ReactiveLoadBalancer这个接口的定义

public interface ReactiveLoadBalancer<T> {

   /**
    * Default implementation of a request.
    */
   Request<DefaultRequestContext> REQUEST = new DefaultRequest<>();

   /**
    * Choose the next server based on the load balancing algorithm.
    * @param request - incoming request
    * @return publisher for the response
    */
   @SuppressWarnings("rawtypes")
   Publisher<Response<T>> choose(Request request);

   default Publisher<Response<T>> choose() { // conflicting name
      return choose(REQUEST);
   }

   interface Factory<T> {

      default LoadBalancerProperties getProperties(String serviceId) {
         return null;
      }

      ReactiveLoadBalancer<T> getInstance(String serviceId);

      /**
       * Allows accessing beans registered within client-specific LoadBalancer contexts.
       * @param name Name of the beans to be returned
       * @param type The class of the beans to be returned
       * @param  The type of the beans to be returned
       * @return a {@link Map} of beans
       * @see @LoadBalancerClient
       */
      <X> Map<String, X> getInstances(String name, Class<X> type);

      /**
       * Allows accessing a bean registered within client-specific LoadBalancer
       * contexts.
       * @param name Name of the bean to be returned
       * @param clazz The class of the bean to be returned
       * @param generics The classes of generic types of the bean to be returned
       * @param  The type of the bean to be returned
       * @return a {@link Map} of beans
       * @see @LoadBalancerClient
       */
      <X> X getInstance(String name, Class<?> clazz, Class<?>... generics);

   }

}

我们可以看到ReactiveLoadBalancer中的choose方法就是用来选择服务的方法,可以对应到ILoadBalancerchooseServer方法

并且定义了Factory可想而知是用的工厂模式

ReactiveLoadBalancer的命名可以看出来是基于响应式的

其中引用了reactivestreams库(算是响应式编程的规范,只定义了接口)

对于reactivestreams的实现比较常见的有2种,reactorrxjava

Spring在这里选择的是reactor,所以就有了ReactorLoadBalancer

public interface ReactorLoadBalancer<T> extends ReactiveLoadBalancer<T> {

   /**
    * Choose the next server based on the load balancing algorithm.
    * @param request - an input request
    * @return - mono of response
    */
   @SuppressWarnings("rawtypes")
   Mono<Response<T>> choose(Request request);

   default Mono<Response<T>> choose() {
      return choose(REQUEST);
   }

}

如果当时选择的是rxjava可能现在的名称就是RxJavaLoadBalancer了哈哈哈

我们继续看ReactorLoadBalancer有哪些实现

public interface ReactorServiceInstanceLoadBalancer extends ReactorLoadBalancer<ServiceInstance> {

}

ServiceInstanceSpringCloud中服务发现使用的服务实例接口,这里先是使用泛型,再具体用ServiceInstance实现,从抽象的角度来说确实扩展性更高,但是以目前的情况来看估计也就只有ServiceInstance这一种实现了

再往下就是具体的实现类了(省略部分代码)

public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {

   ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
}


public class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer {

   private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
}

一个是轮询,一个是随机

里面有一个ServiceInstanceListSupplier

public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceInstance>>> {

   String getServiceId();

   default Flux<List<ServiceInstance>> get(Request request) {
      return get();
   }

   static ServiceInstanceListSupplierBuilder builder() {
      return new ServiceInstanceListSupplierBuilder();
   }

}

这个接口就是用来提供服务信息的

不知道大家还记不记得我们之前在分析Ribbon的时候

ILoadBalancer自身提供了getReachableServersgetAllServers这两个方法来获得服务信息,另外使用IRule来实现负载均衡规则

SpringCloudLoadBalacer是自身实现负载均衡规则,用ServiceInstanceListSupplier来提供服务信息

你品,你细品,哈哈哈,开个玩笑,大家觉得哪个设计的更好呢

接下来就讲讲SpringCloudLoadBalacer中动态路由的实现思路

动态路由实现思路(OpenFeign)

首先我们回到OpenFeignspring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration

我们看到最下面的配置FeignLoadBalancerAutoConfiguration

@ConditionalOnClass(Feign.class)
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@AutoConfigureAfter(FeignRibbonClientAutoConfiguration.class)
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods = false)
@Import({ HttpClientFeignLoadBalancerConfiguration.class,
      OkHttpFeignLoadBalancerConfiguration.class,
      DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {

}

是不是好像和使用RibbonOpenFeign的配置有点像,也有HttpClientOkHttp,我们来看看这两个配置

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
@Import(HttpClientFeignConfiguration.class)
class HttpClientFeignLoadBalancerConfiguration {

   @Bean
   @ConditionalOnMissingBean
   public Client feignClient(BlockingLoadBalancerClient loadBalancerClient,
         HttpClient httpClient) {
      ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
      return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
   }

}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
@ConditionalOnBean(BlockingLoadBalancerClient.class)
@Import(OkHttpFeignConfiguration.class)
class OkHttpFeignLoadBalancerConfiguration {

   @Bean
   @ConditionalOnMissingBean
   public Client feignClient(okhttp3.OkHttpClient okHttpClient,
         BlockingLoadBalancerClient loadBalancerClient) {
      OkHttpClient delegate = new OkHttpClient(okHttpClient);
      return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
   }

}

模式和用Ribbon配置的一样,只是这里返回了FeignBlockingLoadBalancerClient,我们直接跟进去看

public class FeignBlockingLoadBalancerClient implements Client {

    private final BlockingLoadBalancerClient loadBalancerClient;

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        //省略部分代码
        ServiceInstance instance = loadBalancerClient.choose(serviceId);
        //省略部分代码
    }
}

FeignBlockingLoadBalancerClient对应的是RibbonLoadBalancerFeignClient

这里通过BlockingLoadBalancerClientchoose方法来选择服务,我们继续看BlockingLoadBalancerClient(省略部分代码)

public class BlockingLoadBalancerClient implements LoadBalancerClient {

   private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;

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

   @Override
   public <T> ServiceInstance choose(String serviceId, Request<T> request) {
      ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
      if (loadBalancer == null) {
         return null;
      }
      Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
      if (loadBalancerResponse == null) {
         return null;
      }
      return loadBalancerResponse.getServer();
   }
}

有没有见到一个眼熟的类ReactiveLoadBalancer.Factory

通过ReactiveLoadBalancer.Factory来获得一个ReactiveLoadBalancer,再通过ReactiveLoadBalancerchoose方法选择一个服务

另外大家看ReactiveLoadBalancerchoose方法已经把request传进去了,所以我们不需要像扩展Ribbon时找地方传url(真不明白Ribbon为啥不把request传进去,偏偏传个null,传个URI也好啊)

当我看到这里时,我已经有对应的思路了,我们应该可以扩展ReactiveLoadBalancer.FactoryReactiveLoadBalancer来嵌入我们动态路由的逻辑,就像扩展Ribbon时的CustomSpringClientFactoryCustomLoadBalancer

先来看ReactiveLoadBalancer.Factory的实现类

public class LoadBalancerClientFactory extends NamedContextFactory<LoadBalancerClientSpecification>
      implements ReactiveLoadBalancer.Factory<ServiceInstance> {

   @Override
    public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
        //我们可以在这里扩展ReactiveLoadBalancer
        return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
    }
}

我们可以通过扩展LoadBalancerClientFactory来插入我们的逻辑实现动态路由

@AllArgsConstructor
public class CustomLoadBalancerClientFactory extends LoadBalancerClientFactory {

    private final LoadBalancerClientFactory factory;

    @Override
    public <T> T getInstance(String name, Class<T> type) {
        return (T) new CustomReactorLoadbalancer(factory.getInstance(name, type));
    }
    
    public class CustomReactorLoadbalancer implements ReactorServiceInstanceLoadBalancer {

        private final ReactiveLoadBalancer<ServiceInstance> loadBalancer;

        @Override
        public Mono<Response<ServiceInstance>> choose(Request request) {
            //通过传入的request实现动态路由的扩展
            loadBalancer.choose(request);
            //省略部分代码
        }
    }
}

这里为什么要持有一个内部LoadBalancerClientFactory实例而不是调用super方法就不用我再讲了吧

总结

这样基本上就扩展完成啦,也做一个总结

  1. 扩展LoadBalancerClientFactoryCustomLoadBalancerClientFactory)返回CustomReactorLoadbalancer来插入动态路由的逻辑
  2. 使用BeanPostProcessor增强LoadBalancerClientFactory

动态路由实现思路(Gateway)

SpringCloudGateway现在已经改用了ReactiveLoadBalancerClientFilter来处理负载均衡

public class ReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {

   private final LoadBalancerClientFactory clientFactory;

   @Override
   public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
       //省略部分代码
      return choose(exchange);
      //省略部分代码
   }

   private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
      URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
      ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory
            .getInstance(uri.getHost(), ReactorLoadBalancer.class,
                  ServiceInstance.class);
      return loadBalancer.choose(createRequest());
   }
}

ReactiveLoadBalancerClientFilter通过LoadBalancerClientFactory来获得ReactorLoadBalancer

大家还记得LoadBalancerClientFactory么?没错,就是ReactiveLoadBalancer.Factory的实现类,我们在OpenFeign中已经扩展过了,直接拿来用就行了

总结

这样网关也扩展好啦

  1. 扩展LoadBalancerClientFactoryCustomLoadBalancerClientFactory)返回CustomReactorLoadbalancer来插入动态路由的逻辑
  2. 使用BeanPostProcessor增强LoadBalancerClientFactory

所以OpenFeignGateway的扩展是一样的,两边都能兼容

结束

最后再安利一下我的库【Spring Cloud】协同开发利器之动态路由

如果有兴趣也可以支持一下其他的库哦


其他的文章

【Spring Cloud】协同开发利器之动态路由

【Spring Cloud】一个配置注解实现 WebSocket 集群方案

【Spring Boot】WebSocket 的 6 种集成方式

【Java】简单优雅的加载外部 jar 中的 Class|插件化

【Spring Boot】一个注解实现下载接口

你可能感兴趣的:(spring,cloud,ribbon,spring,boot)