前段时间我写了一个库,用于解决开发环境中多人协同调试接口的问题:
网关转发或服务间调用时会负载均衡到别人的服务,导致调试效率变低
功能大概是这样的
通过配置我们需要调试的接口路由到我们自己的服务来解决上述问题
如果大家有兴趣可以看【Spring Cloud】协同开发利器之动态路由有具体的使用详解
今天主要是来讲一讲这个库的实现方式以及Ribbon
和LoadBalancer
的相关内容
首先先来说说负载均衡的组件
在之前的SpringCloud
中,基本就是使用Ribbon
来实现负载均衡的,不管是Zuul
还是SpringCloudGateway
还是OpenFeign
而现在最新的SpringCloudGateway
和OpenFeign
都改用了SpringCloudLoadBalancer
我记得SpringBoot2.6.x
开始的版本就已经不用Ribbon
了
所以大家如果升级了SpringCloud
和SpringBoot
的版本的话,需要注意Ribbon
的配置可能会失效
Ribbon
已经算是最老牌的负载均衡组件了,包含ILoadBalancer
,ServerList
,IPing
,IRule
这几个主要接口
ILoadBalancer
通过ServerList
来更新服务信息
ILoadBalancer
通过IPing
来更新服务的状态是否可用
ILoadBalancer
通过IRule
来选择可用的服务实例
下面我们一个一个来分析
以ILoadBalancer
是Ribbon
的核心组件
/**
* 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
本身缓存了这些服务信息
而这些服务信息是通过我们接下来要讲的ServerList
和IPing
来更新和维护
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();
}
ServerList
的getInitialListOfServers
用于获得初始化的服务信息,不过我暂时没有看到哪里的实现有调用
而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;
}
}
DiscoveryEnabledNIWSServerList
是ServerList
基于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
来获得服务实例信息
然后在一个初始化就会被调用的方法enableAndInitLearnNewServersFeature
中ServerListUpdater
调用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);
}
}
EurekaNotificationServerListUpdater
的start
方法往Eureka
上注册了一个事件监听器,当监听到相应的事件(CacheRefreshedEvent
)时就会调用ServerListUpdater.UpdateAction
的更新方法
不知道大家看下来会不会有点绕,已经尽量把多余的代码去掉了,做个简短的总结:
DynamicServerListLoadBalancer
(这是一个ILoadBalancer
)在初始化的时候通过EurekaNotificationServerListUpdater
往Eureka
上注册了事件监听
当监听到Eureka
的CacheRefreshedEvent
事件时调用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
是用于更新服务是否可用的状态的接口
/**
* 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
是用于实现负载均衡策略的
/**
* 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();
}
大家还记得ILoadBalancer
的chooseServer
方法么
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);
}
}
ILoadBalancer
的chooseServer
最终就是调用IRule
来选择服务的
如果大家想要自定义服务均衡规则就可以实现IRule
接口
Ribbon
的负载均衡的逻辑大致就是这样
接下来讲讲如何适配OpenFeign
和Gateway
的动态路由
要了解OpenFeign
中配置的组件(通过组件来进行扩展)肯定是要从对应的自动配置中找了
首先我们找到OpenFeign
的spring.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 {
//省略代码
}
看到HttpClientFeignLoadBalancedConfiguration
和OkHttpFeignLoadBalancedConfiguration
我突然就想到了feign.httpclient.enabled
和feign.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
客户端不一样之外其他的配置都一样
于是CachingSpringLoadBalancerFactory
,SpringClientFactory
,LoadBalancerFeignClient
这几个类就引起了我的注意
CachingSpringLoadBalancerFactory
和SpringClientFactory
是LoadBalancerFeignClient
的参数,所以如果我们能在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
调用了CachingSpringLoadBalancerFactory
的create
方法得到了一个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
你以为这样就可以了么?不不不,当你测试之后会发现CustomLoadBalancer
的chooseServer
传入的key
是null
,我们需要在这个地方传入url
才能做动态路由的匹配
所以我们要继续扩展来传入url
那么CustomLoadBalancer
的chooseServer
传入的key
是从哪里传进来的呢?
lbClient
和executeWithLoadBalancer
这两个方法,之前跟了一下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
方法,并传入了loadBalancerURI
和loadBalancerKey
我感觉这个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
其实就是在FeignLoadBalancer
的executeWithLoadBalancer
方法中构建的(回顾一下)
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
那就只能继承FeignLoadBalancer
和FeignLoadBalancer.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.Builder
的withServerLocator
方法!!!
没想到吧,就不帮你传,诶嘿,就是玩儿
我当时气的,真是服了这个老六,你自己传个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());
}
}
最后通过扩展customizeLoadBalancerCommandBuilder
将key
传进去了(真的是绕了一大圈)
大致上扩展的思路就是这样了,做个简单的总结吧
SpringClientFactory
(CustomSpringClientFactory
)返回CustomLoadBalancer
来插入动态路由的逻辑FeignLoadBalancer
(CustomFeignLoadBalancer
)在LoadBalancerCommand.Builder
中配置loadBalancerKey
(或是重写getServerFromLoadBalancer
方法)CachingSpringLoadBalancerFactory
(CustomCachingSpringLoadBalancerFactory
)返回CustomFeignLoadBalancer
BeanPostProcessor
增强SpringClientFactory
和CachingSpringLoadBalancerFactory
接下来我们看看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
所以我们直接重写LoadBalancerClientFilter
的choose
方法传入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);
}
}
}
不得不说网关扩展起来还是简单很多
SpringClientFactory
(CustomSpringClientFactory
)返回CustomLoadBalancer
来插入动态路由的逻辑LoadBalancerClientFilter
(CustomLoadBalancerClientFilter
)重写choose
方法传入url
BeanPostProcessor
增强SpringClientFactory
和LoadBalancerClientFilter
接下来说说SpringCloud
中提供的LoadBalancer
SpringBoot2.6.x
之后的Gateway
和OpenFeign
都已经切换成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
方法就是用来选择服务的方法,可以对应到ILoadBalancer
的chooseServer
方法
并且定义了Factory
可想而知是用的工厂模式
从ReactiveLoadBalancer
的命名可以看出来是基于响应式的
其中引用了reactivestreams
库(算是响应式编程的规范,只定义了接口)
对于reactivestreams
的实现比较常见的有2种,reactor
和rxjava
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> {
}
ServiceInstance
是SpringCloud
中服务发现使用的服务实例接口,这里先是使用泛型,再具体用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
自身提供了getReachableServers
和getAllServers
这两个方法来获得服务信息,另外使用IRule
来实现负载均衡规则
而SpringCloudLoadBalacer
是自身实现负载均衡规则,用ServiceInstanceListSupplier
来提供服务信息
你品,你细品,哈哈哈,开个玩笑,大家觉得哪个设计的更好呢
接下来就讲讲SpringCloudLoadBalacer
中动态路由的实现思路
首先我们回到OpenFeign
的spring.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 {
}
是不是好像和使用Ribbon
的OpenFeign
的配置有点像,也有HttpClient
和OkHttp
,我们来看看这两个配置
@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
对应的是Ribbon
的LoadBalancerFeignClient
这里通过BlockingLoadBalancerClient
的choose
方法来选择服务,我们继续看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
,再通过ReactiveLoadBalancer
的choose
方法选择一个服务
另外大家看ReactiveLoadBalancer
的choose
方法已经把request
传进去了,所以我们不需要像扩展Ribbon
时找地方传url
(真不明白Ribbon
为啥不把request
传进去,偏偏传个null
,传个URI
也好啊)
当我看到这里时,我已经有对应的思路了,我们应该可以扩展ReactiveLoadBalancer.Factory
和ReactiveLoadBalancer
来嵌入我们动态路由的逻辑,就像扩展Ribbon
时的CustomSpringClientFactory
和CustomLoadBalancer
先来看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
方法就不用我再讲了吧
这样基本上就扩展完成啦,也做一个总结
LoadBalancerClientFactory
(CustomLoadBalancerClientFactory
)返回CustomReactorLoadbalancer
来插入动态路由的逻辑BeanPostProcessor
增强LoadBalancerClientFactory
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
中已经扩展过了,直接拿来用就行了
这样网关也扩展好啦
LoadBalancerClientFactory
(CustomLoadBalancerClientFactory
)返回CustomReactorLoadbalancer
来插入动态路由的逻辑BeanPostProcessor
增强LoadBalancerClientFactory
所以OpenFeign
和Gateway
的扩展是一样的,两边都能兼容
最后再安利一下我的库【Spring Cloud】协同开发利器之动态路由
如果有兴趣也可以支持一下其他的库哦
【Spring Cloud】协同开发利器之动态路由
【Spring Cloud】一个配置注解实现 WebSocket 集群方案
【Spring Boot】WebSocket 的 6 种集成方式
【Java】简单优雅的加载外部 jar 中的 Class|插件化
【Spring Boot】一个注解实现下载接口