美文网首页
springCloud --- 初级篇(2)

springCloud --- 初级篇(2)

作者: 贪挽懒月 | 来源:发表于2020-06-07 17:18 被阅读0次

    本系列笔记涉及到的代码在GitHub上,地址:https://github.com/zsllsz/cloud

    本文涉及知识点:

    • 注册中心consul;

    • eureka、zookeeper和consul的比较;

    • 服务调用之Ribbon;

    • 服务调用之openfeign;


    欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。


    一、服务的注册与发现之consul

    1、consul是什么?
    consul可以做服务的注册与发现,和eureka、zookeeper类似,也能做配置管理,zookeeper也能做配置管理。由go语言开发,和eureka一样有web可视化界面。

    2、安装和运行consul:

    • https://learn.hashicorp.com/consul/getting-started/install,官网这里有安装的视频教程。
    • 下载后解压,比如我的解压目录是 /opt/consul,解压后里面只有一个consul文件。
    • 执行echo $PATH命令查看哪些目录加到环境变量中了,如果你的/opt/consul目录没有加到环境变量中,那么要执行export PATH=$PATH:/opt/consul将该目录加到环境变量中。
    • 然后执行consul --version,如果显示版本信息,则安装成功。
    • 执行consul agent -dev -client 0.0.0.0 -ui,表示使用开发者模式启动;执行consul agent -server -data-dir /opt/consul -client 0.0.0.0 -ui表示以服务模式启动。
    • 访问 虚拟机ip:8500,就会出现consul的ui界面,如下:
    consul启动成功

    3、springcloud整合consul:

    • 建一个名为cloud-providerconsul-payment8005的module
    • 改pom,pom文件和8004的差不多,只不过是将zookeeper的依赖换成了consul。
    <dependencies>
            <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.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </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>
            </dependency>
            <!-- consul -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-consul-discovery</artifactId>
            </dependency>
    </dependencies>
    
    • 写yml:
    server:
      port: 8005
      
    spring:
      application:
        name: consul-provider-payment
      #consul相关配置
      cloud:
        consul:
          host: 192.168.0.104
          port: 8500
          discovery:
            service-name: ${spring.application.name}
    
    • 主启动类,和以前的一样,就是两个注解:
    @SpringBootApplication
    @EnableDiscoveryClient
    public class PaymentMain80065{
    
        public static void main(String[] args) throws Exception {
            SpringApplication.run(PaymentMain8006.class, args);
        }
    }
    
    • controller:
    @RestController
    @RequestMapping("/payment")
    public class PaymentController {
        @Value("${server.port}")
        private String port;
    
        @GetMapping("/consul")
        public String paymentzk() {
            return "该服务端口号为:" + port;
        }
    }
    
    • 启动该项目,然后再刷新consul的web页面,看到如下效果则该服务成功注册到了consul中。
    服务成功注册到consul

    接下来就消费端:

    • 新建一个名为cloud-consumerconsul-order80的module
    • pom文件:和8005差不多,主要依赖也就是consul。
    • yml文件:和8005也差不多,就是服务名和端口不一样。
    • 主启动类:和其他order80一样,就是那两个注解。
    • config:配置restTemplate的,和其他order80的一样。
    • controller:和consumerzk-order80的一样,只是调用地址变成了payment8005的服务名。

    最后启动payment8005和consumerconsul-order80,可以看到consul的web页面有两个我们自己的这两个服务,并且访问order80可以成功调用到payment8005。

    成功调用

    4、整合可能遇到的问题:

    • consul web页面的service checks报错,一个红叉:
      可以访问consul所在机器的 ip:8500/v1/agent/checks,会返回一个json串,看看json串中output字段的信息。如果信息为找不到域名,看看你的host文件是否有映射,把映射给去掉;如果是找不到IP,你可以自己先访问一下 localhost:服务端口/actuator/health,看看能否正确返回。

    二、eureka、zookeeper和consul的比较

    1、CAP理论:

    • C:强一致性,不同节点之间数据必须一致,不一致就报错
    • A:可用性,优先保证系统可用,即使不同节点之间数据不一致也不会报错
    • P:分区容错性,集群中的多台服务器,某一台宕机了,其他服务器依然能够正常提供服务。分布式架构一定要满足该点,不然和单点架构有什么区别。

    综上,CAP三者最多只能满足两个,且P是必须有的,因此要么是AP,要么是CP。

    2、eureka、zookeeper、consul在CAP理论中的区别:

    • eureka:AP,因为当某个服务宕机了,eureka并不会立即将该服务干掉,依旧会保留一段时间,这就是之前说的eureka自我保护机制。
    • zookeeper:CP,某个服务出现故障了,立即将该服务除名。
    • consul:CP,和zookeeper一样。

    三、服务的负载与调用之Ribbon

    1、是什么?
    是一套实现远程调用和负载均衡的客户端工具。之前我们在客户端order80调用服务端payment8001和payment8002是直接通过RestTemplate来实现的,而Ribbon呢,其实就是负载均衡 + RestTemplate。目前Ribbon也进入了维护模式,不晓得后面后续还会不会更新。

    2、能干嘛?
    上面说了主要是做服务调用和负载均衡(Load balance,简称LB)。关于负载均衡可能大家会想到nginx的负载均衡,有什么区别呢?

    • nginx的LB:服务端的LB,也叫集中式LB。
    • Ribbon的LB:客户端的LB,也叫进程内LB。

    也就是说,我有3个order80,部署在三台服务器,现有15个用户访问order80,nginx挡在最前面,通过负载均衡算法,让这15个请求分摊到这3台服务器上,这就是nginx干的事。然后order80需要调用payment8001,payment8001也在3台服务器上部署着,order80具体调用哪台服务器上的payment8001,这就通过Ribbon来搞定。

    3、怎么用?
    (1):引入依赖:本案例是集合eureka的,我们用的eureka-client的依赖就已经包含Ribbon了,所以不需要额外的依赖。

    (2):RestTemplate的xxxForObject方法和xxxForEntity方法的区别:

    • xxxForObject方法就是将响应体中的数据转成对象,返回的就是一个对象的json;
    • xxxForEntity方法返回的数据除了响应体还包含了响应头、响应状态码等信息。

    之前用的是xxxForObject方法,xxxForEntity用法如下:

    @GetMapping("/payment/get/info/{id}")
    public JsonResult<Payment> getPaymentInfo(@PathVariable("id") Long id){
        return restTemplate.getForEntity(PAYMENT_URL + "/payment/" + id, JsonResult.class).getBody();
    }
    

    这个方法和getForObject将会是一样的效果,因为我最后只是get了body。

    4、Ribbon负载均衡:

    • RoundRobinRule:轮询
    • RandomRule:随机
    • RetryRule:先按照轮询方式,如果轮询到某个服务失败了,将会在指定时间内进行重试
    • WeightedResponseTimeRule:对轮询的扩展,响应速度越快的权重越大,被选择的机会越大
    • BestAvailableRule:会先过滤经常出问题的服务,然后选择一个最闲的服务
    • AvailabilityFilteringRule:先过滤故障实例,再选择并发量最小的
    • ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server可用性选择服务器

    5、如何选择负载均衡算法?
    下面就修改和eureka集成的那个order80,来选择Ribbon的LB算法。
    注意:Ribbon的配置类不能放在与springboot启动类同级的包或者子包下,必须要让@SpringBootApplication注解扫描不到。

    • 新建配置类:
    @Configuration
    public class MyRibbonRule {
        // 随机
        @Bean
        public IRule randRule() {
            return new RandomRule();
        }
    }
    
    • 在启动类上加上如下注解:
    @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration=MyRibbonRule.class)
    

    这样就ok了,负载均衡算法就变成了随机。在上面讲了Ribbon自带有7中算法,在配置类中new对应的类即可。

    6、手写Ribbon负载均衡算法:

    • Ribbon负载均衡原理:轮询算法的原理如下:
    实际调用的服务器位置下标 = 接口第几次请求数 % 服务集群数 
    

    比如我们现在有payment8001和payment8002,那么服务集群数就是2,第1次请求时,1 % 2 = 1,实际调用的就是1号服务器;第2次请求时,2 % 2 = 0,实际调用的就是0号服务器。如果服务器重启了,那么就会开始新的一轮轮询。

    • 手写Ribbon负载均衡算法:
      首先我们把RestTemplate配置类中的@LoadBalanced注释掉,因为注释掉就无法使用Ribbon的LB算法了,才能证明我们自己写的生效了。
    @Component
    public class MyLb implements LoadBalancer{
        
        private AtomicInteger ainteger = new AtomicInteger(0);
        
        private final int getAndIncrement() {
            int current;
            int next;
            // 这里用到的是自旋锁,CAS
            do {
                current = this.ainteger.get();
                next = current >= Integer.MAX_VALUE ? 0 : (current + 1);
            } while(!this.ainteger.compareAndSet(current, next));
            System.out.println("next的值===========" + next);
            return next;
        }
    
        @Override
        public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
            int index = getAndIncrement() % serviceInstances.size();
            return serviceInstances.get(index);
        }
    
    }
    
    • getAndIncrement方法:这个方法采用了自旋锁,运行情况如下
    // ==================== 第一轮开始 ===================
    ainteger = 0,
    current = aintegere = 0,
    next = current + 1 = 1
    ainteger调用compareAndSet方法,期望值是0,更新值是1,
    此时ainteger内存值是0等于期望值,所以成功将ainteger改为1
    该方法返回true,取反就是false,因此跳出循环,将next = 1 返回,第一轮结束。
    // ==================== 第二轮开始 ===================
    ainteger = 1,
    current = ainteger = 1,
    next = current + 1 = 2,
    ainteger调用compareAndSet方法,期望值是1,更新值是2,
    此时ainteger内存值是1等于期望值,所以成功将ainteger改为2
    该方法返回true,取反就是false,因此跳出循环,将next = 2 返回,第二轮结束。
    

    所以该方法就是第一次调用就返回1,第二次调用就返回2……

    • instance方法:暂定我们服务数量是2,运行情况如下
    // =================== 第一轮开始 ====================
    index = 1 % 2 = 1,
    返回1号服务实例
    // =================== 第二轮开始 ====================
    index = 2 % 2 = 0,
    返回0号服务实例
    // =================== 第三轮开始 ====================
    index = 3 % 2 = 1,
    返回1号服务实例
    ……
    

    所以这两个方法就实现了轮询,接下来看在controller中如何用:

    @Autowired
    private LoadBalancer loadBalancer;
    @Autowired
    private DiscoveryClient discoveryClient;
    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping("/payment/get/lb/{id}")
    public JsonResult<Payment> getPaymentlb(@PathVariable("id") Long id){
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        if (instances.isEmpty()) {
            return null;
        }
        ServiceInstance service = loadBalancer.instance(instances);
        return restTemplate.getForEntity(service.getUri() + "/payment/" + id, JsonResult.class).getBody();
    }
    

    这样就自己实现了轮询算法。

    四、服务的负载与调用之OpenFeign

    1、是什么?
    可以理解为就是基于Ribbon又做了一层封装。以前是Ribbon + RestTemplate,OpenFeign相当于封装了它们俩,我们使用时就直接用OpenFeign的注解即可。

    2、怎么用?
    新建一个consumer-feign-order80的module,用openfeigh做服务的负载与调用,用eureka做服务的注册与发现。

    • pom.xml:
    <!-- eureka client -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- openfeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    

    引入openfeign后,发现它的包下有ribbon,进一步说明了openfeign就是基于ribbon再封装的。

    • yml:
    server:
      port: 80
    
    eureka:
      client:
        register-with-eureka: false #不把该项目注册进eureka了
        service-url:
           defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
    
    • 主启动类:
    @SpringBootApplication
    @EnableFeignClients // 激活openfeign
    public class OrderFeignMain80 {
        public static void main(String[] args) throws Exception {
            SpringApplication.run(OrderFeignMain80.class, args);
        }
    }
    
    • service:
      在这个service中,我们就去调用8001的controller,需要用8001controller中的哪个方法我们就调用哪个,并且参数那些都要和8001controller中的一致。写法如下:
    @FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 8001的微服务名称
    public interface PaymentFeignService {
        @GetMapping(value = "/payment/{id}")
        JsonResult<Payment> getPaymentById(@PathVariable("id") Long id);
    }
    

    加上@FeignClient,value就是8001的微服务名称,然后这个方法就是8001controller中对应的方法。注意路径要写对,不然就会404了。

    • controller:
      controller中就直接调用service就好了。
    @RestController
    @RequestMapping("/order")
    public class OrderFeignController {
    
        @Autowired
        private PaymentFeignService pfService;
        
        @GetMapping("/{id}")
        public JsonResult<Payment> getPaymentById(@PathVariable("id") Long id){
            return pfService.getPaymentById(id);
        }
    }
    

    最后访问localhost/order/1,也可以成功返回数据。

    调用成功

    3、openfeign超时控制:
    order80中通过openfeign去调用payment8001,可能8001处理这个业务需要3秒钟,但是order80等待了1秒钟(openfeign默认就是等待1秒钟)发现还没返回,它就认为失败了,然后就报超时的错误。

    • 演示超时的情况:
      8001和8002的controller中,在return结果之前让线程睡3秒钟。
    try {TimeUnit.SECONDS.sleep(3);} catch (Exception e) {e.printStackTrace();}
    

    然后再通过localhost/order/1去访问,就会发现返回超时错误:

    调用超时

    默认等1秒钟,但是如果服务端处理确实1秒钟不能完成,那么我们就需要在order80的配置文件钟配置这个超时时间。所以order80的yml就变成了下面这个亚子:

    server:
      port: 80
      
    eureka:
      client:
        register-with-eureka: false #不把该项目注册进eureka了
        service-url:
           defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
           
    ribbon:
      # 建立连接后从服务器获取可用资源的时间
      ReadTimeout: 5000
      # 建立连接所有的时间
      ConnectTimeout: 5000
    

    再次访问localhost/order/1,就会发现可以访问了。注意,在yml中加ribbon的超时控制配置时,ide没有提示是正常的,不要以为没有提示就是没有该属性。

    4、openfeign的日志打印:
    openfeign提供了日志打印功能,从而让我们了解到调用细节。我们可以对日志级别进行配置,它提供了以下日志级别:

    • NONE:不打印任何日志
    • BASIC:仅记录请求方法、URL、响应码和执行时间
    • HEADERS:除了BASIC的信息,还有请求和响应的头信息
    • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文以及元数据

    配置方法:

    • 配置bean:
    @Configuration
    public class FeignConfig {
        @Bean
        Logger.Level feignLoggerLevel(){
            return Logger.Level.FULL;
        }
    }
    
    • 在yml中开启openfeign的日志:
    logging:
      level:
        # openfeign日志以什么级别监控哪个接口
        com.zhusl.springcloud.service.PaymentFeignService: debug
    

    现在这样配置就表示以debug级别监控PaymentFeignService的FULL日志。

    五、初级篇总结:

    1、服务的注册与发现:

    • eureka:不需要自己安装应用,只需要新建服务当作eureka server,其他的服务当作eureka client主机进server即可。CAP理论中的AP,优先保证可用性。
    • zookeeper:需要自己安装zookeeper应用,CAP理论中的CP,优先保证数据的强一致性。
    • consul:也需要安装应用,也是保证CAP中的CP。

    2、服务负载与调用:

    • Ribbon:RestTemplate + 负载均衡。eureka client的jar自带了ribbon,无需额外引入。
    • Openfeign:相当于对ribbon再做了一层封装。直接在需要被调用的类上加注解即可。

    相关文章

      网友评论

          本文标题:springCloud --- 初级篇(2)

          本文链接:https://www.haomeiwen.com/subject/yqsmphtx.html