负载均衡的基本概念
负载均衡是系统高可用、缓解网络流量和处理能力扩容的重要手段,广义的负载均衡指的是服务端负载均衡,如硬件负载均衡(F5)和软件负载均衡(Nginx)。负载均衡设备会维护一份可用的服务器的信息,当客户端请求到达负载均衡设备之后,设备会根据一定的负载均衡算法从可用的服务器列表中取出一台可用的服务器,然后将请求转发到该服务器。对应的负载均衡架构如下图所示:
负载均衡架构示意图
- 独立进程单元,通过负载均衡策略,将请求转发到不同的执行单元,如Nginx;
- 将负载均衡逻辑以代码形式封装在服务消费者的客户端上,客户端维护一份服务提供者的信息列表,通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的,如Ribbon。
Ribbon是Netflix发布的云中间层服务开源项目,其主要功能是提供客户端实现负载均衡算法。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,Ribbon是一个客户端负载均衡器,我们可以在配置文件中Load Balancer后面的所有机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器,我们也很容易使用Ribbon实现自定义的负载均衡算法。
Ribbon的策略
- Ribbon + RestTemplate
- Ribbon + Feign
RestTemplate + Ribbon消费服务
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
ServiceInstance choose(String serviceId);
负载均衡策略
- RoundRobinRule 【轮询】默认尝试10次
- RandomRule 【随机】
- AvailabilityFilteringRule 【可用过滤】会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数超过阈值的服务,然后对剩余的服务列表进行轮询
- WeightedResponseTimeRule 【响应时间权重】根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高。刚启动时,如果统计信息不足,则使用轮询策略,等信息足够,切换到 WeightedResponseTimeRule
- RetryRule 【在选定负载均衡策略上使用轮询的方式重试】先按照轮询策略获取服务,如果获取失败则在指定时间内重试,获取可用服务
- BestAvailableRule 【选择最小请求数的服务器】选过滤掉多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务,如果没找到,使用随机轮询策略选取;
- ZoneAvoidanceRule (默认) 【根据服务器所属服务区的整体运行状况来轮询选择】符合判断server所在区域的性能和server的可用性选择服务,根据服务器所属服务区的运行状况和可用性来进行负载均衡。
IPing
- DummyPing 直接返回true
- NIWSDiscoveryPing 根据DiscoveryEnabledServer的InstanceInfo的status进行判断,如果为UP,表明可用;
- NoOpPing 不真实ping,直接返回true;
- PingConstant 固定返回某服务是否可用,是一个常量值;
- PingUrl 使用HttpClient进行ping操作,根据返回结果判定是否可用。
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() { List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>(); if (eurekaClientProvider == null || eurekaClientProvider.get() == null) { logger.warn("EurekaClient has not been initialized yet, returning an empty list"); return new ArrayList<DiscoveryEnabledServer>(); } EurekaClient eurekaClient = eurekaClientProvider.get(); if (vipAddresses!=null){ for (String vipAddress : vipAddresses.split(",")) { // if targetRegion is null, it will be interpreted as the same region of client List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); for (InstanceInfo ii : listOfInstanceInfo) { if (ii.getStatus().equals(InstanceStatus.UP)) { if(shouldUseOverridePort){ if(logger.isDebugEnabled()){ logger.debug("Overriding port on client name: " + clientName + " to " + overridePort); } // copy is necessary since the InstanceInfo builder just uses the original reference, // and we don't want to corrupt the global eureka copy of the object which may be // used by other clients in our system InstanceInfo copy = new InstanceInfo(ii); if(isSecure){ ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build(); }else{ ii = new InstanceInfo.Builder(copy).setPort(overridePort).build(); } } DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr); serverList.add(des); } } if (serverList.size()>0 && prioritizeVipAddressBasedServers){ break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers } } } return serverList; }
对于ribbon从Eureka Client获取注册信息,由于服务可能存在更新,所以需要定时从Eureka Client获取最新的注册信息。
class PingTask extends TimerTask { public void run() { try { new Pinger(pingStrategy).runPinger(); } catch (Exception e) { logger.error("LoadBalancer [{}]: Error pinging", name, e); } } }
在LoadBalancerAutoConfiguration类中,首先维护了一个被LoadBalanced修饰的RestTemplate对象的list。初始化过程中,通过调用customer.customize(restTemplate)方法给RestTemplate增加拦截器LoadBalancerInterceptor,LoadBalancerInterceptor用于实时拦截,在LoadBalancerInterceptor中实现了负载均衡的方法。
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)); }
结论