负载均衡Ribbon

上一章讲述了服务注册和发现组件Eureka
,同时跟踪了Eureka的部分源码深入讲解了Eureka的机制,其中还构建了高可用的Eureka Server。本章讲解如何使用RestTemplate和Ribbon相结合作为服务消费者去消费服务,同时从源码角度深入理解Ribbon。

RestTemplate简介

RestTemplate是用来消费REST服务的,它的主要方法都与REST的HTTP协议的一些方法紧密相连,列如REST中HEAD、GET、POST、PUT、DELETE、OPTIONS等方法对应为RestTemplate中headForHeaders()、getForObject()、postForObject()、put()和delete()等方法。RestTemplate支持XML、JSON数据格式,默认实现了序列化,可以自动将JSON字符串转换为实体。

//表示将请求返回的JSON字符串转换成一个User对象
User user = restTemplate.getForObject("https://www.xxx.com/",User.class)

Ribbon简介

负载均衡是指将服务分摊到多个执行单元上,常见的有两种。

  • 服务端负载均衡:独立的进程单元,通过负载均衡策略,请求转发到不同的执行器上,如Nenix。
  • 客户端负载均衡:将负载均衡的逻辑封装到服务消费者的客户端上,客户端维护服务提供者的信息列表,通过服务列表结合策略分摊到不同的服务提供者。而Ribbon就是属于这一种。

使用负载均衡带来的好处很明显:

  • 当集群里的1台或者多台服务器down的时候,剩余的没有down的服务器可以保证服务的继续使用。
  • 使用了更多的机器保证了机器的良性使用,不会由于某一高峰时刻导致系统cpu急剧上升。

Ribbon的子模块如下,很多子模块在开发环境中不一定用到。

  • ribbon-loadbanlance:可以独立使用或与其他模块一起使用的负载均衡器API。
  • ribbon-eureka:结合Eureka客户端API,为负载均衡提供动态服务注册列表信息。
  • ribbon-core:Ribbon的核心API。

RestTemplate和Ribbon结合消费服务

我们在上一章项目的基础上进行改造。

  • 首先我们在eureka-client Module中加一个controller如下
@RestController
public class HiController {
    @Value("${server.port}")
    private String port;
    @GetMapping("/hi")
    public String hi(String name) {
        return "hi " + name + ",I am from " + port;
    }
}

这么写的目的是需要以不同的端口启动两个eureka-client,访问接口时通过打印不同的端口来判断Ribbon是否实现了负载均衡的功能。

  • 启动eureka-server,启动两个eureka-client实例(启动方式在上一章有说明)

  • 新建一个spring-boot Module工程eureka-ribbon-client作为服务消费者。
    pom依赖如下

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>

在工程配置文件application.yml中做程序的相关配置如下


spring:
  application:
    name: eureka-ribbon-client
server:
  port: 8765
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
management:
  security:
    enabled: false

在启动类上添加注解@EnableEurekaClient开启 Eureka Client功能。

@SpringBootApplication
@EnableEurekaClient
public class EurekaRibbonClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaRibbonClientApplication.class, args);
    }
}

在程序的IOC容器中注入一个restTemplate的Bean,并在这个Bean上加上@LoadBalanced注解,此时RestTemplate就结合了Ribbon开启了负载均衡的功能。

@Configuration
public class RibbonConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

写一个service类,其中运用restTemplate调用之前启动的eureka-client的API接口。由于Ribbon自身维护有服务信息列表,所以在请求的Uri上不需要使用硬编码(如IP地址),只需要写服务名即可。

@Service
public class RibbonService {
    @Autowired
    private RestTemplate restTemplate;

    public String hiFromClient() {
        return restTemplate.getForObject("http://eureka-client/hi?name=dzy", String.class);
    }
}

最后写一个Controller类,来调用service中的方法。

@RestController
public class RibbonController {
    @Autowired
    private RibbonService ribbonService;

    @GetMapping("/hi")
    public String hi() {
        return ribbonService.hiFromClient();
    }
}

启动项目,确定在注册中心中已经注册。然后多次访问http://localhost:8765/hi,浏览器会交替出现两个服务提供者实例的信息,说明实现了负载均衡。

LoadBalanceClient简介

负载均衡的核心类为LoadBalanceClient,用LoadBalanceClient可以获取服务提供者的服务信息,多次访问以下代码接口,会轮流得到两个服务提供者的实例信息。

@RestController
public class RibbonController {
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/load-balancer")
    public String load() {
        ServiceInstance instance = loadBalancerClient.choose("eureka-client");
        return instance.getHost() + ":" + instance.getPort();
    }
}

LoadBalanceClient是从Eureka Client获取服务的注册列表信息,并将服务注册列表信息缓存了一份。当调用choose方法时,根据负载均衡策略选择一个服务实例的信息进行负载均衡。LoadBalanceClient也可以不从Eureka Client获取注册列表信息,这时需要自己维护一份服务注册列表信息,具体配置如下。

stores:
  ribbon:
    listOfServers: client1.com,client2.com
ribbon:
  eureka:
    enable: false

通过配置ribbon.eureka.enable为false来禁止调用从Eureka Client获取注册信息。通过stores.ribbon.listOfServices来配置服务实例的Url,此时在调用choose方法时会从配置的实例中来进行负载均衡。

Ribbon源码解析

在使用restTemplate时,只需要添加@LoadBalanced注解即可,非常的简单方便,现在我们看看它底层的原理。
LoadBalancerAutoConfiguration.java为实现客户端负载均衡器的自动化配置类。在这个类中实现了很多的功能,如下:
Ribbon要实现负载均衡自动化配置需要满足如下两个条件:

  • @ConditionalOnClass(RestTemplate.class):RestTemplate类必须存在于当前工程的环境中。
  • @ConditionalOnBean(LoadBalancerClient.class):在spring的Bean工程中必须有LoadBalancerClient.class的实现Bean。
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {}

在自动化配置中主要做三件事:

  • 创建一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
@Bean
        public LoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient,
                LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

  • 创建一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。
@Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(
                final LoadBalancerInterceptor loadBalancerInterceptor) {
            return new RestTemplateCustomizer() {
                @Override
                public void customize(RestTemplate restTemplate) {
                    List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                            restTemplate.getInterceptors());
                    list.add(loadBalancerInterceptor);
                    restTemplate.setInterceptors(list);
                }
            };
        }
  • 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。
@LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

接下来,我们看看LoadBalancerInterceptor拦截器是如何将一个普通的RestTemplate变成客户度负载均衡的。在拦截器中执行的是loadBalancer的execute方法。

@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, requestFactory.createRequest(request, body, execution));
    }

在execute主要调用了getServer方法。

@Override
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
        Server server = getServer(loadBalancer);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }
        RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
                serviceId), serverIntrospector(serviceId).getMetadata(server));

        return execute(serviceId, ribbonServer, request);
    }

protected Server getServer(ILoadBalancer loadBalancer) {
        if (loadBalancer == null) {
            return null;
        }
        return loadBalancer.chooseServer("default"); // TODO: better handling of key
    }

在getServer函数中,我们可以看到获取具体服务实例的时候,使用了Netflix Ribbon自身的ILoadBalancer接口中定义的chooseServer函数。该接口代码如下

public interface ILoadBalancer {
        //向负载均衡器中维护的实例列表中增加服务实例。
    public void addServers(List<Server> newServers);
        //通过某种策略,从负载均衡器中挑选出一个具体的服务实例。
    public Server chooseServer(Object key);
        //用来通知和标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单都会认为该服务实例时正常的。
    public void markServerDown(Server server);
        //获取当前服务的实例列表。
    public List<Server> getReachableServers();
        //获取所有已经的服务实例列表,包括正常服务和停止服务的实例。
    public List<Server> getAllServers();
}

在BaseLoadBalancer类实现了基础的负载均衡,其中chooseServer代码如下。

public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

在其中主要代码为rule.choose(key),调用的是IRule接口的choose方法。

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

choose方法的实现类如图


image.png

其中常用的实现类为:
RandomRule:随机选择一个UP的服务。
RoundRobinRule:轮询获取服务。
BestAvailableRule:跳过熔断的服务,获取请求数最少的服务.通常与ServerListSubsetFilter一起使用。
RetryRule:在RoundRobinRule的基础上,增加了重试的机制。
BestAvailableRule:表示请求数最少策略。

综上可以得出用@LoadBalanced标注的restTemplate在请求之前增加了拦截机制,在拦截器中是调用了LoadBalancerClient的execute方法,在其中根据负载均衡的策略进行服务实例的访问。

总结

在这一章节中,对RestTemplate,Ribbon进行了介绍,并实现了两者的结合来实现负载均衡消费服务。最后对Ribbon部分源码进行了简单解析。在下一章介绍声明式调用feign的使用。

PS:项目github地址:https://github.com/dzydzydzy/spring-cloud-example.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258