这里继续上篇eureka的文章,讲下ribbon、openfeign、hystrix的使用
一、Ribbon
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。
提供负载均衡的功能,上篇讲eureka时,有两个服务提供者时,消费者通过服务名访问提供者,此时不知道要使用哪个提供者的服务,这时就需要一个RestTemplate+注解 @LoadBalanced来实现对于多个提供者的负载均衡。此时这个 @LoadBalanced就是Ribbon组件提供的负载均衡,默认算法是轮询。
@Configuration public class ApplicationContextConfig { @LoadBalanced @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
此时通过
restTemplate.postForObject("http://cloud-payment-service/payment/create",请求参数, CommonResult.class);来远程调用服务,CommonResult:返回类型
当然也有postForObject/postForEntity、getForObject方法/getForEntity等方法可以使用。
Ribbon要实现远程调用:负载均衡+RestTemplate调用,此时就会比较麻烦,后面openfeign会对其进行下一步封装。
上面说到,ribbon默认使用的是轮询算法,其实ribbon有其他很多算法:
下面我们就针对上面将轮询算法更换为随机算法(在cloud-comsumer-order80服务中修改)
1、更换负载均衡算法:(Eureka依赖本身集成了ribbon所以不用自己加依赖)
(1)、创建规则类这个自定义配置类不能放在 @ComponentScan 所扫描的当前包下以及子包下,否则自定义的配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了。所以我们在java目录下新建 com.myrules.MyselfRule类,这里我们创建出随机规则。
启动类添加
@RibbonClient(name = "cloud-payment-service", configuration = MyselfRule.class)
cloud-payment-service为提供者服务名。ribbon根据提供者的服务名随机负载均衡到某个服务者
测试:http://localhost/consumer/payment/get/1发现端口号随机变化则成功
2、轮询算法的实现
rest 接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标
每次服务器重启后rest接口数从1开始
List instances = discoveryClient.getInstances(“CLOUD-PROVIDER-SERVICE”)
List[0] instances = 127.0.0.1:8002
List[1] instances = 127.0.0.1:8001
例如我们现在有两台机子去负载均衡
接口类关系图:
Ribbon源码:(轮询算法源码)
IRule接口:
//IRule接口 public interface IRule{ /* * choose one alive server from lb.allServers or * lb.upServers according to key * * @return choosen Server object. NULL is returned if none * server is available */ //选择哪个服务实例 public Server choose(Object key); public void setLoadBalancer(ILoadBalancer lb); public ILoadBalancer getLoadBalancer(); }
RoundRobinRule 轮询源码:
public class RoundRobinRule extends AbstractLoadBalancerRule { private AtomicInteger nextServerCyclicCounter; private static final boolean AVAILABLE_ONLY_SERVERS = true; private static final boolean ALL_SERVERS = false; private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class); public RoundRobinRule() { nextServerCyclicCounter = new AtomicInteger(0); } public RoundRobinRule(ILoadBalancer lb) { this(); setLoadBalancer(lb); } public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { log.warn("no load balancer"); return null; } Server server = null; int count = 0; while (server == null && count++ < 10) { List<Server> reachableServers = lb.getReachableServers(); List<Server> allServers = lb.getAllServers(); int upCount = reachableServers.size(); int serverCount = allServers.size(); if ((upCount == 0) || (serverCount == 0)) { log.warn("No up servers available from load balancer: " + lb); return null; } int nextServerIndex = incrementAndGetModulo(serverCount); server = allServers.get(nextServerIndex); if (server == null) { /* Transient. */ Thread.yield(); continue; } if (server.isAlive() && (server.isReadyToServe())) { return (server); } // Next. server = null; } if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; } /** * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}. * * @param modulo The modulo to bound the value of the counter. * @return The next value. */ private int incrementAndGetModulo(int modulo) { for (;;) { int current = nextServerCyclicCounter.get(); int next = (current + 1) % modulo; if (nextServerCyclicCounter.compareAndSet(current, next)) return next; } } @Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { } }
3、模拟源码写轮询算法实现
(1)、改controller(在两个提供者上都增加一个接口)(8001、8002)用来测试自定义的算法是否成功
@GetMapping("/payment/lb") public String getPaymentLB(){ return serverPort; }
80项目(消费者,服务调用方)注掉@LoadBalanced(ribbon的,现在我们使用自定义的负载均衡算法)
80项目增加一个接口和一个实现类(在cloud包下,即可以被扫描到的路径)
public interface LoadBalancer { ServiceInstance instances(List<ServiceInstance> serviceInstances); }
@Component public class MyLB implements LoadBalancer { private AtomicInteger atomicInteger=new AtomicInteger(0); public final int getAndIncrement(){ int current; int next; do{ current=atomicInteger.get(); next=current>=2147483647?0:current+1; }while (!this.atomicInteger.compareAndSet(current,next)); System.out.println("***第几次访问,次数: "+next); return next; } public ServiceInstance instances(List<ServiceInstance> serviceInstances){ int index=getAndIncrement()%serviceInstances.size(); return serviceInstances.get(index); } }
该实现类即具体的算法实现
- 这里首先有一个原子类int型,初始值为0,这里用了一个自旋锁,让他判断每次是不是我们之前的那个值,如果是就+1代表访问次数又增加一次,不是就继续循环直到判断为真跳出循环,这里保证了不用synchronized方法也能在高并发下实现线程安全的增加次数
- 第二个方法instances()实现了用当前访问次数去%一个集群的数量,使这个值永远不超过集群数量,然后得到这个值作为获取单个实例的下标,返回一个当前应该返回的集群实例
80项目controller层新增方法getPaymentLB():
@Resource private LoadBalancer loadBalancer; @Resource private DiscoveryClient discoveryClient; @GetMapping(value = "/consumer/payment/lb") public String getPaymentLB(){ List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); if(instances==null||instances.size()<0){ return null; } ServiceInstance instances1 = loadBalancer.instances(instances); URI uri = instances1.getUri(); return restTemplate.getForObject(uri+"/payment/lb",String.class); }
首先获取集群中的实例,然后判断是否为空,把获取到的list集群传给刚写的方法中获取到负载均衡获取到的当前实例,然后获取到实例地址,最后restTemplate去调用服务就会用到我们之前写的负载均衡去调用
测试
调用方法会一直轮询调用,体现了我们刚才的负载均衡机制
二、openfeign
上面对于ribbon进行了入门讲解,此时你是否会发现,ribbon要实现远程调用,需要满足两个条件:
1、restTemplate2、负载均衡(@LoadBalanced)
这样看来是比较麻烦的,此时openfeign则对ribbon进行了封装,我们不在需要使用restTemplate远程调用,openfeign使用了声明式调用,大大降低使用难度。
一. 什么是Feign?
Feign 是一个声明式 WebService 客户端。使用 Feign 能让编写Web Service 客户端更加简单。
Feign 旨在使编写Java Http 客户端变得更容易。
前面在使用 Ribbon+RestTemplate时,利用RestTemplate 对http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign 在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign 的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注衣一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。Feign集成了Ribbon
利用Ribbon维护了Payment 的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign 只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。二. OpenFeign与Feign区别
openfeign就是对feign的又一层封装
OpenFeign
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
Feign
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency>
三. 服务调用cloud-consumer-feign-order80模块
3.1 目录结构
pom:
<dependencies> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--eureka client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--一般基础通用配置--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.cloud</groupId> <artifactId>cloud-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
yml:
server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
feign:
@FeignClient(value = "cloud-payment-service")//调用的服务名 @Component public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id); }
controller:
@RestController public class OrderFeignController { @Resource private PaymentFeignService paymentFeignService; @GetMapping(value = "/consumer/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id){ CommonResult paymentById = paymentFeignService.getPaymentById(id); return paymentById ; } }
启动类:
@SpringBootApplication @EnableFeignClients public class OrderOpenFeignMain80 { public static void main(String[] args) { SpringApplication.run(OrderOpenFeignMain80.class,args); } }
测试:http://localhost/consumer/payment/get/1
四. 超时控制
由于Feign天生支持Ribbon所以在超时控制这块由Ribbon来控制(openfeign默认调用1s没返回则表示超时)
此时要修改超时时间:
yml:#设置feign 客户端超时时间(openFeign默认支持ribbon) ribbon: #指的是建立连接后从服务器读取到可用资源所用的时间 ReadTimeout: 5000 #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 ConnectTimeout: 5000
五. 日志打印
Feign 提供了日志打印功能,可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。
说白了就是对接口的调用情况进行监控和输出
- NONE:默认的,不显示任何日志
- BASIC:仅记录请求方法、URL、响应状态码及执行时间
- HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息
- FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据
80服务添加一个配置类:
@Configuration public class FeignConfig { @Bean Logger.Level feignloggerLevel(){ return Logger.Level.FULL; } }
yml:要暴露哪个服务,把服务的全限定名加到level配置文件下面(这里暴露的是feign接口)
logging: level: #feign日志以什么级别监控哪个接口 com.cloud.feign.PaymentFeignService: debug
这样每调用一次feign接口,控制台就会打印该次调用的信息
讲到底,我们之前讲的ribbon已经被openfeign集成了。下篇讲huystrix。