一、什么是负载均衡?
做web开发都会接触到负载均衡,这里我们就不细说了。
(摘自百度百科)负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。
负载均衡主要分为软件负载和硬件负载,在微服务盛行的现在,软件负载在微服务里成为主流,netflix的ribbon就是其中之一
二、负载均衡要干什么事情?
我们这里只关注软件负载,硬件负载略过。软件负载也分为:
服务端负载(服务端发现模式)、客户端负载(客户端发现模式)
服务端负载
需要做三个事情:
1.接收请求
2.选择服务器地址
3.转发/执行请求(转发还是执行可以参考网关)
这里我们一笔带过,因为不是本文的重点
客户端负载
本文的重点,也是ribbon的实现方式:
如果让我们自己做一个软负载,试想一下怎么去做?
接收请求肯定是对应微服务自己的controller或者rpc服务接收请求
然后负载均衡这里负责:
1.选择服务器地址
2.发请求
选择服务器地址这里倒没什么问题,但是发请求是用httpclient还是resttemplate?由谁定?服务消费者自己定,还是负载均衡器定,服务消费者除了使用负载均衡器请求,有可能也可以直接发请求给服务提供者吧?
作为一个中间件,ribbon其实给到的答案也是由服务消费者自己去定义。
负载均衡器
负载均衡其实是一个很抽象的说法,为了让他更加形象化,我们抽象出一个可以量化的名词,负载均衡器:
1.每个服务提供者一个负载均衡器,还是所有服务提供者公用一个负载均衡器?
2.每个服务提供者的每个接口能否使用不同的负载均衡器?(dubbo就可以?)
ribbon给到的答案是:每个服务提供者(多节点)一个负载均衡器,负载均衡器之间互相独立且互不干扰
那么我们得出如下角色职责分工:
细心的观众会发现,为什么负载均衡器多了一个职责:记录请求统计信息?
这是因为负载均衡有的负载规则需要根据请求统计信息来决定选择哪个服务器,例如WeightedResponseTimeRule,根据平均请求响应时间来选择合适的服务器
我们再来看看ribbon官方是怎么定义的:
Components of load balancer(摘自netflix ribbon wiki)
Rule - a logic component to determine which server to return from a list
Ping - a component running in background to ensure liveness of servers
ServerList - this can be static or dynamic. If it is dynamic (as used by DynamicServerListLoadBalancer), a background thread will refresh and filter the list at certain interval
负载均衡器有三大组件:
1.负载规则 ,从服务器列表中决定用哪个服务器
2.ping任务 ,后台运行的任务,用来验证服务器是否可用
3.服务器列表 ,可以是静态也可以是动态,如果是动态,那么就要有一个后台线程定时去刷新和过滤列表。我们微服务基于服务发现的情况,服务器列表肯定都是动态增减的,而且ribbon都是配套eureka使用(也可以单独使用,但我们这里不去研究场景)
发现跟我们的想发还是有出入的,服务器列表我们肯定是有的,不然没法选择,主要还是多了一个ping任务
把我们上面的想法整合一下,得到如下架构图:
上图大家肯定有很多疑惑,不要慌,下面我们再来一一分析:
1.定时获取ServerList,去哪取?
2.定时执行ping任务,怎么ping
3.ServerList过滤,为什么要过滤,过滤什么?
其实结合我们使用ribbon都是结合eureka,单独使用的场景其实基本没有,所以我们这里主要关注结合eureka如何使用即可
那么结合eureka服务发现,上面的很多问题都迎刃而解
1.定时获取ServerList,去哪取?【去eureka client取】
2.定时执行ping任务,怎么ping【eureka client有定时从server刷新服务列表(30s频率),我们再去ping的话感觉没太大必要,所以结合eureka的话,我们直接拿来用就行了】
3.ServerList过滤,为什么要过滤,过滤什么?【这个我们后面会揭晓】
三、如何将Http Client发送请求与ribbon负载均衡器进行融合?
如果不做融合这个事情,我们的代码可能就是
1.获取服务器地址【负载均衡器】
2.发送请求【服务消费者】
3.将请求耗时等信息记录到负载均衡器【负载均衡器】
主要分为如上三步,这样我们又会面临操作jdbc类似的问题,每个开发写出来的代码都可能会不一样,严重的可能还会有bug
所以类似spring jdbctemplate,ribbon也想到用类似模板来解决这个事情:
为什么负载均衡器对外提供方法只有get,没有set?其实这里统计信息是引用类型,get到了之后做修改,地址不变值改变,所以没有问题,这里我们不纠结
伪代码如下:
AbstractLoadBalancerAwareClient{
public void executeWithLoadBalancer(){
selectServer()
this.execute()//【交由子类实现】
recordstats()
}
}
这样的话我们就能够控制动作一致,结果不一致(执行请求的细节交给子类,父类只管选择服务器、记录请求结果信息,将地址交给子类自己去使用)
所以netflix ribbon-loadbalancer包也是主要分为两块:负载均衡器(loadbalancer包)、客户端模板(client包)
feign结合ribbon
再进一步举例,spring-cloud-openfeign是怎么将feign与ribbon结合使用的?(右键新标签打开可查看大图)
其实就是我们刚刚说的,继承模板,只需要实现execute方法决定怎么发送请求即可,其他的交由模板去处理
四、ribbon的懒加载策略
准确来说,是spring-cloud-netflix-ribbon的懒加载策略,是spring cloud基于ribbon进行包装而来的,其最终目的还是为了给每个服务提供者提供各自独立的个性化配置,懒加载只是其中的实现过程
每个ribbon负载均衡器可以个性化配置的内容有哪些:
可参考spring-cloud-netflix-ribbon里
public SpringClientFactory() {
super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
}
RibbonClientConfiguration
默认所有的负载均衡器都使用如下默认的实现
①DefaultClientConfigImpl里的属性值如何换成自己的配置?具体有哪些配置?
service-hi.ribbon.ReadTimeout=339
<clientName>.<nameSpace>.<propertyName>=<value>
写在配置文件里即可,这里就能读到每个服务自己个性化的配置
如果想所有服务统一使用配置,则把服务名去掉即可
ribbon.MaxAutoRetries=100
如果想更换namespace也可以,将DefaultClientConfigImpl Bean换成自己的实现类即可
public class MyClientConfig extends DefaultClientConfigImpl {
// ...
public String getNameSpace() {
return "foo";
}
}
具体有哪些配置详见DefaultClientConfigImpl
②ZonePreferenceServerListFilter里的属性 zone
this.zone = ConfigurationManager.getDeploymentContext().getValue(ContextKey.zone);
这里的属性值,由spring-cloud-netflix-eureka-client的 EurekaRibbonClientConfiguration
@PostConstruct set进去,先取
ConfigurationManager.getDeploymentContext().setValue(ContextKey.zone,availabilityZone);
优先顺序如下
eureka.instance.metadataMap.zone = zone2 //不支持逗号分割
eureka.client.availability-zones.gz=zone2,zone1 //逗号分割后第一个,注意是只取当前配置region下的az
ribbon的配置有四种(优先级由高到低)
1.每个服务提供者自己个性化的配置 @RibbonClient
@RibbonClient(value="service-hi",configuration= ServiceHiRibbonConfiguration.class)
针对每个服务提供者自行配置,可配置的内容见上面的列表,举例如下:
//不需要@Configution注解
public class ServiceHiRibbonConfiguration {
@Bean
public IRule iRule(){
return new ZoneAvoidanceRule();
}
}
2.全局配置 @RibbonsClient
如果有引用spring-cloud-netflix-eureka-client就是走的全局配置
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration
@RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
public class RibbonEurekaAutoConfiguration {
}
@Configuration
public class EurekaRibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
...
自己也可以加全局配置,但是要注意添加@Order注解来控制bean的优先级,否则可能因为优先级低被覆盖,举例:
@RibbonClients(defaultConfiguration=MyRibbonDefaultConfiguration.class) ,全局配置,配置文件同上
//不需要@Configuration注解
public class MyRibbonDefaultConfiguration {
@Bean
public IRule iRule(){
return new ZoneAvoidanceRule();
}
}
另外需要注意@RibbonClients注解除了可以定义全局的配置,也可以给每个服务提供者配置
@RibbonClients(value = {
@RibbonClient(value="service-hi",configuration = ServiceHiRibbonConfiguration.class),
@RibbonClient(value="service-order",configuration = ServiceOrderRibbonConfiguration.class)
},defaultConfiguration = RibbonDefaultConfiration.class)
即没有自己配置的走全局配置,自己配置了的走自己的配置
3.默认配置 RibbonClientConfiguration
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
public SpringClientFactory() {
super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
}
@RibbonClient 和@RibbonClients都是注册BeanClass=RibbonClientSpecification 的spring bean
然后springclientfactory最后统统收到自己的 configurations属性里
最后获取配置的时候,通过如上的优先级顺序去注册和取对应的bean即可
SpringClientFactory extends NamedContextFactory
public abstract class NamedContextFactory
implements DisposableBean, ApplicationContextAware{
@Override
public void setApplicationContext(ApplicationContext parent) throws BeansException {
this.parent = parent;
}
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
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;
}
4.spring context里的bean
如上代码,NamedContextFactory继承了ApplicationContextAware,取bean时最后会把ApplicationContext作为parent注册进去,这样spring 扫描到的所有bean都会被取到
context.setParent(this.parent);
这里需要注意(摘自spring cloud netflix ribbon):
@RibbonClient(name = "custom", configuration = CustomConfiguration.class)
The CustomConfiguration clas must be a @Configuration class, but take care that it is not in a @ComponentScan for the main application context. Otherwise, it is shared by all the @RibbonClients. If you use @ComponentScan (or @SpringBootApplication), you need to take steps to avoid it being included (for instance, you can put it in a separate, non-overlapping package or specify the packages to scan explicitly in the @ComponentScan).
如果每个服务提供者自己个性化的配置,要注意configuration文件不要被spring扫描到,否则会被注册到spring bean,这样SpringClientFactory getBean时会拿到,这样就会影响到其他服务提供者(如果优先级是最后倒也还好,但是如果有使用@Order把优先级提高的话,就会造成影响)
其实经过测试,spring cloud Finchley.RELEASE版本,spring-cloud-netflix-ribbon 2.0.0.RELEASE版本 下,CustomConfiguration.class 是不需要@Configuration注解也可以正常使用,这样就可以避免上面的问题发生(spring-cloud-openfeign的个性化配置一样可以,且官方文档也已经明确提到CustomConfiguration可以不需要@Configuration注解)
ribbon也支持饿汉
不过好像没什么应用场景?,详见:
RibbonAutoConfiguration
@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
return new RibbonApplicationContextInitializer(springClientFactory(),
ribbonEagerLoadProperties.getClients());
}
通过如下配置即可,应用启动就会自动注册相关的bean到SpringClientFactory的contexts map里
ribbon.eager-load.clients=service-hi
ribbon.eager-load.clients=service-order
五、结合region AZ要干些什么事情?
region AZ的概念详见我们前文《服务发现之eureka》,我们的决策肯定是:优先使用相同zone的服务器
所以ServerList过滤时,只保留当前AZ的服务器,其他zone的过滤掉即可(如果当前AZ没有服务器地址,则保留其他AZ的来使用即可)
所以我们这里才需要ServerList过滤
六、将抽象出来的对象映射到类图
连连看,将如下类图与我们架构图里对象进行对应(右键新标签打开可查看大图)