一文学会如何使用Ribbon进行负载均衡

本文主要通过一个简单案例来讲解spring cloud项目的搭建,以及服务之间的远程通信,然后从这个项目逐步延申,将cloud生态的组件依次加入。

接下来我们来搭建一个基于下单流程的微服务项目,具体流程如下。
在这里插入图片描述

1. 项目搭建

首先我们进行项目的搭建,项目的整体框架如下:
在这里插入图片描述

我们这里使用的版本关系如下:

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>

这里的各个项目间的依赖关系都是一些基本的maven知识,就不过多讲述,在文末会给出相应的代码地址。需要注意的是各个模块的端口号记得修改。

项目基本框架搭建完成之后,我们就来进行相应的编码,这里都采用最简单的方式,后期在进行相应的规范。我们自顶向下进行编码,首先编写下单接口:

根据上文给出的流程图,我们在下单的时候需要调用各个模块,然后将信息进行整合,大致如下:

@RestController
@RequestMapping("/order")
public class OrderController {

    @GetMapping
    public String order(){
        // todo 商品模块 查询商品信息

        // todo 营销模块 查询促销信息

        // todo 会员模块 查询会员信息

        // todo 订单模块 查询订单信息
        return "Success";
    }
}

接下来就是编写各个模块对外的接口了。

商品模块

@Slf4j
@RestController
public class GoodsService {

    /**
     * 根据ID查询商品信息
     *
     * @return
     */
    @GetMapping("/goods")
    public String getGoodsById() {
        return "返回商品信息";
    }
}

营销模块

@RestController
public class PromotionService {

    @GetMapping("/promotion")
    public String getPromotionById() {
        return "查询到指定商品的促销信息";
    }
}

下单模块

@Slf4j
@RestController
public class OrderService {

    @PostMapping("/order")
    public String createOrder(@RequestParam String goodsInfo, @RequestParam String promotionInfo) {
        log.info("开始创建订单,请求参数,{},{}", goodsInfo, promotionInfo);
        return "订单创建成功";
    }
}

2. 远程通信

2.1 RestTemplate

各个模块搭建完成之后,我们如何进行服务之间的远程通信呢?

这个时候我们可以使用RestTemplate,它是一个类似于OKHTTP,HttpClient的工具,可以帮助我们进行服务间的调用。

首先将其交给Spring容器管理。

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

然后修改我们的控制类:

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping
    public String order() {
        log.info("begin do order");

        String goodsInfo = restTemplate.getForObject("http://localhost:9090/goods", String.class);
        String promotionInfo = restTemplate.getForObject("http://localhost:9091/promotion", String.class);

        MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
        param.add("goodsInfo", goodsInfo);
        param.add("promotionInfo", promotionInfo);
        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(param, new HttpHeaders());
        ResponseEntity<String> response = restTemplate.postForEntity("http://localhost:9092/order", httpEntity, String.class);
        return response.getBody();
    }
}

这个时候将模块全都启动,访问该接口,可以发现返回订单创建成功。

2.2 负载均衡

这个时候我们整体流程跑通了,但是在正常的微服务架构中,我们不可能只是单节点的,我们需要保证它的高可用。

这时候我们可以将商品模块复制一个实例出来进行测试。注意需要指定端口号。
在这里插入图片描述

现在我们有两个商品模块了,我们调用的时候该如何操作呢?

最简单的就是随机访问任意一个地址,我们看下代码。

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    private String getGoodsServer(){
        String serverList ="http://localhost:9090/goods,http://localhost:9093/goods";
        String servers[] = serverList.split(",");
        Random random = new Random();
        return servers[random.nextInt(servers.length)];
    }

    @GetMapping
    public String order() {
        log.info("begin do order");

        String goodsInfo = restTemplate.getForObject(getGoodsServer(), String.class);
        String promotionInfo = restTemplate.getForObject("http://localhost:9091/promotion", String.class);

        MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
        param.add("goodsInfo", goodsInfo);
        param.add("promotionInfo", promotionInfo);
        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(param, new HttpHeaders());
        ResponseEntity<String> response = restTemplate.postForEntity("http://localhost:9092/order", httpEntity, String.class);
        return response.getBody();
    }
}

通过这种随机选择的方式,我们可以发现可以调用不同的节点。这就是我们平常说的负载均衡。

但是以上操作是不是觉得不太友好,所有地址都是写死的。这个时候就需要引出今天的主角Ribbon了,它可以帮助我们很轻松的实现负载均衡的功能。

3. Ribbon

首先我们在mall-protal项目中添加相关依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>

这里我们直接采用注解的方式来实现负载均衡。直接在RestTemplate这里添加@LoadBalanced注解即可。

@Configuration
public class RestTemplateConfig {

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

}

然后需要注意的是,使用ribbon之后,我们调用的url地址就不再是之前的地址了,而是变成下方的地址。

String url = "http://goods-service/goods";

使用服务名取代localhost:9090

我们需要在application文件中进行相关配置。

# 应用名称
spring.application.name=mall-protal
# 应用服务 WEB 访问端口
server.port=8080

goods-service.ribbon.listOfServers=http://localhost:9090,http://localhost:9093
marking-service.ribbon.listOfServers=http://localhost:9091
order-service.ribbon.listOfServers=http://localhost:9092

然后对之前的代码进行修改:

@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    private String getGoodsServer() {
        String serverList = "http://localhost:9090/goods,http://localhost:9093/goods";
        String servers[] = serverList.split(",");
        Random random = new Random();
        return servers[random.nextInt(servers.length)];
    }

    @GetMapping
    public String order() {
        log.info("begin do order");

        String url = "http://goods-service/goods";
        String goodsInfo = restTemplate.getForObject(url, String.class);
        String promotionInfo = restTemplate.getForObject("http://marking-service/promotion", String.class);

        MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
        param.add("goodsInfo", goodsInfo);
        param.add("promotionInfo", promotionInfo);
        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(param, new HttpHeaders());
        ResponseEntity<String> response = restTemplate.postForEntity("http://order-service/order", httpEntity, String.class);
        return response.getBody();
    }
}

这个时候重新运行项目,可以发现负载均衡实现成功,是不是比之前轻松多了。

4. 负载均衡算法

Ribbon具有七种负载均衡算法。

在这里插入图片描述

在这里插入图片描述

以上便是ribbon提供的负载均衡算法,我们可以在项目中指定使用何种算法进行负载均衡。配置方法如下所示:

goods-service.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

除了这几种方法外,我们也可以自定义负载均衡算法。

新建一个类继承AbstractLoadBalancerRule即可,然后实现相关方法,我们这里实现一个根据ip进行路由的负载均衡算法。

public class IpHashRule extends AbstractLoadBalancerRule {

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        //...
    }

    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }

        Server server = null;
        while (server == null) {
            //启动的服务列表(单纯使用ribbon时,是不会做心跳检测的)
            List<Server> reachableServers = lb.getReachableServers();
            List<Server> allServers = lb.getAllServers();
            int size = allServers.size();
            if (size == 0) {
                return null;
            }
            int index = ipAddressHash(size);
            server = reachableServers.get(index);
        }
        return server;
    }

    private int ipAddressHash(int serverCount) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String remoteAddr = requestAttributes.getRequest().getRemoteAddr();
        int code = Math.abs(remoteAddr.hashCode());
        return code % serverCount;
    }

    @Override
    public Server choose(Object o) {
        return choose(getLoadBalancer(), o);
    }
}

然后在配置文件中指定如下信息:

goods-service.ribbon.NFLoadBalancerRuleClassName=com.example.mallprotal.config.IpHashRule

这个时候重启项目查看效果,可以发现使用了我们的自定义负载均衡算法。
在这里插入图片描述

5. 连接数量

Ribbon可以通过下面的配置项,来限制httpclient连接池的最大连接数量、以及针对不同 host的最大连接数量。

# 最大连接数
ribbon.MaxTotalConnections=500 (默认值:200)
# 每个host最大连接数
ribbon.MaxConnectionsPerHost=500 (默认值:50)

这是因为Ribbon底层的网络通信,采用的是HttpClient中的PoolingHttpClientConnectionManager连接池,连接池的好处是避免频繁建立连接 (针对单个目标地址)带来的性能开销,但是维护过多的链接会对客户端造成内存以及维护上的成本。

所以,可以通过MaxTotalConnections限制总的连接数量,或者通过 MaxConnectionsPerHost限制针对每个host的最大连接数。

6. Ribbon核心之Ping机制

我们单独使用Ribbon的时候还会出现一个问题,我们上面测试负载均衡的时候,由于都是写的固定地址,所以如果有一个服务停止后,我们在访问会出现错误,所以我们就需要通过Ping机制去解决这种问题。

在ribbon负载均衡器中,提供了ping机制,每隔一段时间,就会去ping服务器,由 com.netflix.loadbalancer.IPing 接口去实现。

单独使用ribbon,不会激活ping机制,默认采用DummyPing(在RibbonClientConfiguration中实例化),isAlive()方法直接返回true。

ribbon和eureka集成,默认采用NIWSDiscoveryPing(在EurekaRibbonClientConfiguration中实例化的),只有服务器列表的实例状态为up的时候才会为Alive。

IPing中默认内置了一些实现方法如下:

  1. PingUrl: 使用httpClient对目标服务逐个实现Ping操作

  2. DummyPing: 默认认为对方服务是正常的,直接返回true

  3. NoOpPing:永远返回true

我们这里可以简单操作一下:

首先添加一个HTTP依赖,用来进行服务间的通信,因为restTemplate上面使用了Ribbon注解,所以我们这里不使用它进行通信。

<!--        网络通信-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

可以进行网络通信后,我们需要编写一个请求的接口,由于我们这里是开启了两个商品服务,所以我们需要在商品服务中编写一个健康检查接口。

@RestController
@Slf4j
public class HealthController {

    @GetMapping("/healthCheck")
    public String health(){
        log.info("healthCheck");
        return "SUCCESS";
    }

}

然后这个时候我们只需要实现IPing接口,编写服务检查的代码即可。

通过访问对应的检查接口,然后根据返回的接口状态来判断服务是否存活。

public class HealthChecker implements IPing {
    @Override
    public boolean isAlive(Server server) {
        //服务的名称
        String url = "http://" + server.getId() + "/healthCheck";
        boolean isAlive = true;
        HttpClient httpClient = new DefaultHttpClient();
        HttpUriRequest request = new HttpGet(url);
        try {
            HttpResponse response = httpClient.execute(request);
            isAlive = response.getStatusLine().getStatusCode() == 200;
        } catch (IOException e) {
            e.printStackTrace();
            isAlive = false;
        }
        return isAlive;
    }
}

写完之后,我们还需要进行相关配置,类似与我们指定负载均衡算法的方式:

#ping
goods-service.ribbon.NFLoadBalancerPingClassName=com.example.mallprotal.config.HealthChecker
#心跳时间间隔
goods-service.ribbon.NFLoadBalancerPingInterval=3

这个时候我们重新启动项目即可发现服务开始进行相关的健康检查。
在这里插入图片描述

7. 项目地址

项目地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

、楽.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值