负载均衡是指将负载分摊至多个执行单元上,常见的负载均衡有如下两种
1.服务器负载均衡.如Nginx:通过Nginx负载均衡策略,将请求转发至后端服务,如下图所示
2.客户端负载均衡:以代码的形式封装至服务消费者服务上,消费者维护一份服务提供者信息列表,通过负载均衡策略将分摊给多个服务提供者,从而达到负载均衡目的
在上一篇基础上完成该案例演示,服务具体信息如下表所示
服务名 | 服务端口 | 作用 |
---|---|---|
eureka-server | 8761 | 注册中心 |
eureka-client | 8762,8763 | 服务提供者 |
eureka-ribbon-client | 8764 | 负载均衡客户端 |
新建服务eureka-ribbon-client
引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
配置application.yml
spring:
application:
name: eureka-ribbon-client
server:
port: 8764
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
书写启动类
注意启动类中需加载
RestTemplate
类
@EnableEurekaClient
@SpringBootApplication
public class EurekaRibbonClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaRibbonClientApplication.class, args);
}
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
写一个Restful API接口,用于远程调用8762
和8763
两个服务
@Service
public class RibbonService {
@Resource
private RestTemplate restTemplate;
public String hi(String name){
return restTemplate.getForObject("http://eureka-client/hi?name={1}", String.class, name);
}
}
访问ribbon-client-service服务,会轮流输出两个服务提供者信息,表示负载均衡起作用了
负载均衡器的核心类为LoadBalancerClient,我们可以通过LoadBalancerClient控制访问的服务,在此,新增一接口"/testLoadBalancer",通过LoadBalancerClient访问服务提供者。
新增接口
@RestController
public class EurekaRibbonClient {
@Resource
private LoadBalancerClient loadBalancerClient;
@GetMapping("/testLoadBalancer")
public String testRibbon() {
ServiceInstance instance = loadBalancerClient.choose("eureka-client");
return instance.getHost() + ":" + instance.getPort();
}
}
启动服务,访问http://localhost:8764/testLoadBalancer
,输出结果如下
127.0.0.1:8762
127.0.0.1:8763
Ribbon禁止从Eureka注册中心获取服务注册信息,而是自己维护服务实例列表
(1)配置application.yml
ribbon: # 禁止Ribbon从Euraka获取注册信息
eureka:
enabled: false
stores: # 设置本地服务注册信息
ribbon:
listOfServers: example.com,goole.com
(2)书写程序
@RestController
public class EurekaRibbonClient {
@Resource
private LoadBalancerClient loadBalancerClient;
@GetMapping("/testLoadBalancer")
public String testRibbon() {
ServiceInstance instance = loadBalancerClient.choose("stores");
return instance.getHost() + ":" + instance.getPort();
}
}
(3) 启动工程,访问http://localhost:8764/testLoadBalancer
,结果展示如下
example.com:80
google.com:80
结论
(1) Ribbon通过LoadBalancerClient从注册中心获取所有注册服务信息,并缓存至Ribbon本地
(2) LoadBalancerClient的choose根据传入的serviceId从注册服务列表中获取服务实例信息(ip及端口)
(3) 如果禁止Ribbon从Eureka获取注册列表信息,则需自己维护一份服务注册列表信息,根据自己维护的注册列表信息,实现负载均衡
Ribbon负载均衡过程
(1) 通过LoadBalancerAutoConfiguration
类中如下代码,维护一个RestTemplate列表,同时初始化时,给每个restTemplate对象增加一个拦截器
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
(2) 拦截器主要对每个请求路径进行解析,最后将解析出的serviceId(serviceName)将给LoadBalancerClient处理
@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,
this.requestFactory.createRequest(request, body, execution));
}
(3) LoadBalancerClient则会根据传入的serviceId获取服务注册列表、缓存服务注册列表、检测服务注册列表是否变化、ping下游服务是否可用、根据配置的负载均衡策略进行服务调用
LoadBalancerClient实现功能过程
(1) 核心类LoadBalancerClient
的choose()最终通过ILoadBalancer
的实现类DynamicServerListLoadBalancer
实现
(2) DynamicServerListLoadBalancer
构造方法中需要初始:IClientConfig
、IRule
、IPing
、ServerList
、ServerListFilter
五个属性
属性类 | 作用 |
---|---|
IClientConfig | 获取配置负载均衡客户端 |
IRule | 配置负载均衡策略 |
IPing | 检测负载均衡的服务是否可用 |
ServerList | 获取注册中心服务注册列表 |
ServerListFilter | 根据配置去过滤或动态获取符合条件的server列表方法 |
(3) 负载均衡策略
负载均衡策略的选择是根据IRule子类完成的,默认走的是轮询策略,常见的几个实现类如下表所示:
类名 | 作用 |
---|---|
BestAvailableRule | 选择最小请求数 |
ClientConfigEnabledRoundRobinRule | 轮询 |
RandomRule | 随机 |
RetryRule | 根据轮询方式重试 |
WeightedResponseTimeRule | 根据响应时间分配权重,权重越低,被选择可能性越低 |
ZoneAvoidanceRule | 根据server的zone区域和可用性来轮训选择 |
IRule有3个方法,分别是choose(serviceId),setLoadBalancer(),getLoadBalancer()三个方法,分别是根据servcieId获取服务实例信息,设置和获取ILoadBalancer
(4) 检测要负载均衡的服务是否可用(IPing),Iping通过其子类完成服务检测,主要由如下几个子类实现:
类名 | 作用 |
---|---|
PingUrl | 真实地去ping某个url |
PingConstant | 固定返回某个服务是否可用,默认为true |
NoOpPing | 不去ping,直接返回true,即可用 |
DummyPing | 直接返回true,并实现了initWithNiwsConfig方法 |
NIWSDiscoveryPing | 根据DiscoveryEnabledServer的instanceInfo的InstanceStatus去判断,如果InstanceStatus.UP,则可用,否则不可用 |
(5) 获取注册服务列表(serverList)
方法名 | 作用 |
---|---|
DynamicServerListLoadBalancer构造函数 | 初始化上述所需属性 |
DynamicServerListLoadBalancer->initWithNiwsConfig() | 初始化信息 |
initWithNiwsConfig->restOfInit->updateListOfServers() | 获取服务注册列表 |
updateListOfServers->serverListImpl.getUpdatedListOfServers() | 具体获取注册列表服务实现 |
serverListImpl实现ServerList,实现类DiscoveryEnabledServer->obtainServersViaDiscovery->eurekaClientProvider.get() | 获取Eureka中注册的服务 |
LegacyEurekaClientProvider->get() | 具体实现 |
最终获取服务列表代码
class LegacyEurekaClientProvider implements Provider<EurekaClient> {
private volatile EurekaClient eurekaClient;
@Override
public synchronized EurekaClient get() {
if (eurekaClient == null) {
eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient();
}
return eurekaClient;
}
}
(6) 负载均衡获取服务服务注册时间
BaseLoadBalancer构造函数中有一方法setupPingTask()
,方法具体实现如下,根据如下代码可知,每隔10秒向EurekaClient发送一次心跳检测。
void setupPingTask() {
if (canSkipPing()) {
return;
}
if (lbTimer != null) {
lbTimer.cancel();
}
lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
true);
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
forceQuickPing();
}
(7) 查看定时任务代码,分析心跳检测具体实现
查看pingTask()方法,主要实现逻辑在runPinger(),而在runPinger()方法中通过pingerStrategy.pingServers(ping, allServers)
获取服务可用性,检测是否与之前相同,如果相同则不拉取,如果不同则调用notifyServerStatusChangeListener(changedServers);
向注册中心拉取服务列表,最终实现本地服务注册列表的更新
class PingTask extends TimerTask {
public void run() {
try {
new Pinger(pingStrategy).runPinger();
} catch (Exception e) {
logger.error("LoadBalancer [{}]: Error pinging", name, e);
}
}
}
----------------------------------------------------------------------------------------
public void runPinger() throws Exception {
...省略
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);
if (oldIsAlive != isAlive) {
changedServers.add(svr);
logger.debug("LoadBalancer [{}]: Server [{}] status changed to {}",
name, svr.getId(), (isAlive ? "ALIVE" : "DEAD"));
}
if (isAlive) {
newUpList.add(svr);
}
}
upLock = upServerLock.writeLock();
upLock.lock();
upServerList = newUpList;
upLock.unlock();
notifyServerStatusChangeListener(changedServers);
} finally {
pingInProgress.set(false);
}
}
1.创建配置类
2.根据需求创建所需策略类
public class RibbonConfiguration{
@Bean
public IRule ribbonRule(){
return new RandomRule();
}
}
1.创建配置类
2.自定义注解
3.启动类中排除自定义配置类
@Configuration
@AvoidScan
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new RandomRule();
}
}
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@RibbonClient(name="provider-service",configuration = RibbonConfiguration.class)
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type= FilterType.ANNOTATION,value = {AvoidScan.class})})
public class RibbonFeignApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonFeignApplication.class, args);
}
}
下表为配置策略相关类
配置项 | 说明 |
---|---|
clientName.ribbon.NFLoadBalancerClassName | 指定ILoadBalancer的实现类 |
clientName.ribbon.NFLoadBalancerRuleClassName | 指定IRule的实现类 |
clientName.ribbon.NFLoadBalancerPingClassName | 指定IPing的实现类 |
clientName.ribbon.NFWSServerListClassName | 指定ServerList的实现类 |
clientName.ribbon.NIWSServerListFilterClassName | 指定ServerListFilter的实现类 |
provider-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ConnectTimeout: 30000
ReadTimeout: 30000
MaxAutoRetries: 1 #对第一次请求的服务重试次数
MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务)
OkToRetryOnAllOperations: true
ribbon:
eager-load: # ribbon的饥饿加载,应对第一次调用加载时间长,超时而调用不成功情况
enabled: true
clients: provider-service