微服务架构:Spring-Cloud 组件与集群

Spring-Cloud项目的搭建

因为 spring-cloud 是基于 spring-boot 项目来的,所以我们项目得是一个 spring-boot 项目,至于 spring-boot 项目,
这里我们先不讨论,这里要注意的一个点是 spring-cloud 的版本与 spring-boot 的版本要对应下图:

 

spring-boot

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.2.RELEASE</version>
</parent>

spring-cloud:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Finchley.SR2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement> 

eureka是什么?

eureka Netflflix 的子模块之一,也是一个核心的模块, eureka 里有 2 个组件,一个是 EurekaServer( 一个独立的项
) 这个是用于定位服务以实现中间层服务器的负载平衡和故障转移,另一个便是 EurekaClient (我们的微服务)
它是用于与 Server 交互的,可以使得交互变得非常简单 : 只需要通过服务标识符即可拿到服务。
spring-cloud的关系:
 
Spring Cloud 封装了 Netflix公司开发的 Eureka 模块来实现服务注册和发现 ( 可以对比 Zookeeper)
Eureka 采用了 C-S 的设计架构。 Eureka Server 作为服务注册功能的服务器,它是服务注册中心。
而系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接。这样系统的维护人员就可
以通过 Eureka Server 来监控系统中各个微服务是否正常运行。 SpringCloud 的一些其他模块(比如 Zuul )就可以
通过 Eureka Server 来发现系统中的其他微服务,并执行相关的逻辑。
如何使用?

spring-cloud 项目里面加入依赖:
单机版 eureka 服务端依赖和配置+启动类
 
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>
server:
  port: 8081
eureka:
  server:
   enable-self-preservation: false  #关闭自我保护机制
   eviction-interval-timer-in-ms: 4000 #设置清理间隔(单位:毫秒 默认是60*1000)
  instance:
    hostname: eureka-server8001 #修改的本机hosts,可忽略
  client:
    registerWithEureka: false #不把自己作为一个客户端注册到自己身上
    fetchRegistry: false  #不需要从服务端获取注册信息(因为在这里自己就是服务端,而且已经禁用自己注册了)
    serviceUrl:
     defaultZone: http://${eureka.instance.home}:${eureka.port}/eureka #可填写自己具体的local host + port

    

@SpringBootApplication
@EnableEurekaServer
public class EurekaServer8001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServer8001.class);
    }
}

eureka 客户端

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>
server:
  port: 8082
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.instance.home}:${eureka.port}/eureka 
#可填写自己具体的local host + port
#eureka服务端提供的注册地址 参考服务端配置的这个路径
  instance:
    instance-id: order8082 #此实例注册到eureka服务端的唯一的实例ID
    prefer-ip-address: true #是否显示IP地址
    leaseRenewalIntervalInSeconds: 10 #eureka客户需要多长时间发送心跳给eureka服务器,表明它仍然活着,默认为30 秒 (与下面配置的单位都是秒)
    leaseExpirationDurationInSeconds: 30 #Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒

spring:
  application:
    name: order-server #此实例注册到eureka服务端的name
@SpringBootApplication
@EnableEurekaClient
public class OrderApplication6001 {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication6001.class);
    }
}

集群 eureka 服务端依赖和配置+启动类

 这是我自己的集群环境和修改本机的hosts文件

server:
  port: 8001
eureka:
  server:
   enable-self-preservation: false  #关闭自我保护机制
   eviction-interval-timer-in-ms: 4000 #设置清理间隔(单位:毫秒 默认是60*1000)
  instance:
    hostname: eureka-server8001
  client:
    registerWithEureka: false #不把自己作为一个客户端注册到自己身上
    fetchRegistry: false  #不需要从服务端获取注册信息(因为在这里自己就是服务端,而且已经禁用自己注册了)
    serviceUrl:
     defaultZone: http://eureka-server8002:8002/eureka,http://eureka-server8003:8003/eureka,http://eureka-server8004:8004/eureka,http://eureka-server8005:8005/eureka

启动 eureka 集群后的主页面

红色说明没有本机eureka信息是因为eureka的server会把自己的注册信息与其他的server同步, 所以这里我们不需要注册到自己身上,因为另外两台服务器会配置本台服务器。

客户端 的注册配置(一部分)注册到所有 服务端机器

defaultZone: http://eureka-server8001:8001/eureka/,http://eureka-server8002:8002/eureka/,http://eureka-server8003:8003/eureka/,http://eureka-server8004:8004/eureka/,http://eureka-server8005:8005/eureka/ 
我们这里只截取了要改动的那一部分。 就是 原来是注册到那一个地址上面,现在是要写五个 eureka 注册地址,但
是不是代表他会注册五次,因为我们 eureka server 的注册信息是同步的,这里只需要注册一次就可以了,但是为
什么要写五个地址呢。因为这样就可以做到高可用的配置:打个比方有 5 台服务器。但是突然宕机了一台, 但是其
4 台还健在,依然可以注册我们的服务,换句话来讲, 只要有一台服务还建在,那么就可以注册服务,这里 需要
理解一下。
这里效果图就不发了, 和之前单机的没什么两样,只是你服务随便注册到哪个 eureka server 上其他的 eureka
server 上都有该服务的注册信息。
ribbon 和 feign 组件
 
首先先看下ribbon 和 fegin 的定义
Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端 负载均衡的工具。
简单的说, Ribbon Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将 Netflix 的中间层服
务连接在一起。 Ribbon 客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中
列出 Load Balancer (简称 LB )后面所有的机器, Ribbon 会自动的帮助你基于某种规则(如简单轮询,随机连接
等)去连接这些机器。我们也很容易使用 Ribbon 实现自定义的负载均衡算法。
 
Feign 是一个声明式 WebService 客户端。使用 Feign 能让编写 Web Service 客户端更加简单 , 它的使用方法是定义一
个接口,然后在上面添加注解,同时也支持 JAX-RS 标准的注解。 Feign 也支持可拔插式的编码器和解码器。 Spring
Cloud Feign 进行了封装,使其支持了 Spring MVC 标准注解和 HttpMessageConverters Feign 可以与 Eureka
Ribbon 组合使用以支持负载均衡。
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>

下面通过两张图看下服务端的负载均衡和客户端的负载均衡有什么区别

服务端的负载均衡以Nginx 为例  客户端发起请求后通过算法分配到具体的某一台服务器

客户端的负载均衡以ribbon 为例 客户端先通过算法得到需要分配的某一台服务器再发起请求

我们先看下IRule 的实现者的负载均衡策略

我们按照客户端负载均衡的图片看下代码

order6001/order6002

//控制器
@RestController
public class OrderController {

    @RequestMapping("/getOrder")
    @ResponseBody
    public Object getOrder(){
        Map<String,String> map = new HashMap<>();
        map.put("/getOrder","port::order6001");
        return map;
    }
}
//启动类
@SpringBootApplication
@EnableEurekaClient
public class OrderApplication6001 {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication6001.class);
    }
}

application.yml 参考上面的客户端的集群环境

shopping-cart3001/shopping-cart3002/shopping-cart3003
//IRule 负载均衡策略配置
@Configuration
public class OrderRuleConfig {

    @Bean
    public IRule iRule(){
        return new RoundRobinRule();
    }
}
//控制器
@RestController
public class ShoppingCartController {

    @Autowired
    private ShoppingToOrderFeginClient shoppingToOrderFeginClient;

    @RequestMapping("/getShoppingCart")
    public Object getShoppingCart(){
        Map<String,String> map = new HashMap<>();
        map.put("/getShoppingCart","port::shopping-cart::3001");
        return map;
    }
    //使用Fegin 客户端 跨系统shopping调用order服务
    @RequestMapping("/ShoppingToOrderFeginClient")
    public Object getShoppingToOrderFeginClient(){
        return shoppingToOrderFeginClient.getOrderFeginClient();
    }


}
//Fegin 客户端配置
@FeignClient(name = "ORDER-SERVER")
public interface ShoppingToOrderFeginClient {
    // 类似使用  dubbo 在Order系统中暴露服务提供调用
    @RequestMapping("/getOrder")
    public Object getOrderFeginClient();
}
//启动类
@SpringBootApplication
@EnableEurekaClient
@RibbonClients({
        @RibbonClient(name = "ORDER-SERVER", configuration = OrderRuleConfig.class)
})
@EnableFeignClients
public class ShoppingCartApplication3001 {
    public static void main(String[] args) {
        SpringApplication.run(ShoppingCartApplication3001.class);
    }
}
user8000
@Configuration
public class OrderRuleConfig {

    @Bean
    public IRule iRule(){
        return new RoundRobinRule();
    }
}
@Configuration
public class ShoppingRuleConfig {

    @Bean
    public IRule iRule(){
        return new RoundRobinRule();
    }
}
@RestController
public class UserController {

    @Autowired
    private OrderFeginClient orderFeginClient;

    @Autowired
    private ShoppingFeginClient shoppingFeginClient;

    @RequestMapping("/getUserToOrder")// user 调用 order 服务 
    @ResponseBody
    public Object getUserToOrder(){
        return orderFeginClient.getOrderFeginClient();
    }

    @RequestMapping("/getUserToShoppingCart")// user 调用 ShoppingCart 服务
    @ResponseBody
    public Object getUserToShoppingCart(){
        return shoppingFeginClient.getShoppingCartFeginClient();
    }

    @RequestMapping("/getUserToShoppingCartToOrder") // user 调用 ShoppingCart 服务 ,ShoppingCart 再调用 Order服务
    @ResponseBody
    public Object getShoppingToOrderFeginClient(){
        return shoppingFeginClient.getShoppingToOrderFeginClient();
    }

}
@FeignClient(name = "ORDER-SERVER")
public interface OrderFeginClient {

    @RequestMapping("/getOrder")
    public Object getOrderFeginClient();
}
@FeignClient(name = "SHOPPING-SERVER")
public interface ShoppingFeginClient {

    @RequestMapping("/getShoppingCart")
    public Object getShoppingCartFeginClient();

    @RequestMapping("/ShoppingToOrderFeginClient")
    public Object getShoppingToOrderFeginClient();
}
@SpringBootApplication
@EnableEurekaClient
@RibbonClients({
        @RibbonClient(name = "ORDER-SERVER", configuration = OrderRuleConfig.class),
        @RibbonClient(name = "SHOPPING-SERVER", configuration = ShoppingRuleConfig.class)
})
@EnableFeignClients
public class UserApplication8000 {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication8000.class);
    }
}

eureka主界面

测试访问,由于负载均衡策略设置的为轮询,所以结果是循环切换
http://localhost:8000/getUserToOrder
{"/getOrder":"port::order6001"}/{"/getOrder":"port::order6002"}

http://localhost:8000/getUserToShoppingCart

{"/getShoppingCart":"port::shopping-cart::3001"}/

{"/getShoppingCart":"port::shopping-cart::3002"}/

{"/getShoppingCart":"port::shopping-cart::3003"}

Hystrix 断路器 组件

hystrix 是什么?
Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比
如超时、异常等, Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分
布式系统的弹性。
断路器 本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调
用方返回一个符合预期的、可处理的备选响应( FallBack ),而不是长时间的等待或者抛出调用方无法处理的异
常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至
雪崩。
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>

如果通过 nginx负载均衡算法 分配到的那一台服务器刚好404了怎么办呢,难道一直卡着吗,后面的所有请求都在浪费系统资源,最终导致系统崩溃

在多系统相互调用之间a->b->c->d,如果b服务404了 ,是不是也卡住了 ,后面的所有请求也都在浪费系统资源,最终导致系统崩溃

Hystrix 降级配置
@RequestMapping("/getUserToOrder")
@ResponseBody
@HystrixCommand(fallbackMethod = "getUserToOrderFallback")
public Object getUserToOrder(){
    return orderFeginClient.getOrderFeginClient();
}

public Object getUserToOrderFallback(){
    return "断路器生效,系统正在维护中";
}

启动类添加@EnableHystrix 注解

当调用getUserToOrder方法时该服务发生故障 那么Hystrix可以做到降级的作用 会执行getUserToOrderFallback方法,可以快速的响应

Hystrix 熔断配置 application.yml 

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 20000
      circuitBreaker:
        requestVolumeThreshold: 5 #当前线程请求超过5次 熔断配置 半开失败一次后尝试调用,全开失败五次进入fallbackMethod方法
#        sleepWindowInMilliseconds : 5000   时间

Hystrix 限流配置

hystrix 通过线程池的方式来管理你的微服务调用,他默认是一个线程池( 10 大小) 管理你的所有微服务,你可以
给某个微服务开辟新的线程池:
threadPoolKey 就是在线程池唯一标识, hystrix 会拿你这个标识去计数,看线程占用是否超过了, 超过了就会直
接降级该次调用
比如, 这里 coreSize 给他值为 5 那么假设你这个方法调用时间是 3s 执行完, 那么在 3s 内如果有超过 2 个请求进来的
话, 剩下的请求则全部降级
 
@RequestMapping("/getUserToOrder")
@ResponseBody
@HystrixCommand(fallbackMethod = "getUserToOrderFallback"/*降级配置或者实现具体fegin接口*/,threadPoolKey = "order6001",
threadPoolProperties = {@HystrixProperty(name = "coreSize",value = "5")}/*限流配置 最多5个线程 其余fallback*/)
public Object getUserToOrder(){
    return orderFeginClient.getOrderFeginClient();
}

配置通用的fallbackMethod 方法 即 实现 fegin 接口即可 eg:

@FeignClient(name = "ORDER-SERVER",fallback = OrderFeginClientImpl.class)
public interface OrderFeginClient {

    @RequestMapping("/getOrder")
    public Object getOrderFeginClient();
}
@Service
public class OrderFeginClientImpl implements OrderFeginClient {
    @Override
    public Object getOrderFeginClient() {
        return "断路器生效,系统正在维护中";
    }
}
feign 整合 hystrix:
feign 默认是支持 hystrix 的, 但是在 Spring - cloud Dalston 版本之后就默认关闭了, 因为不一定业务需求要用的
到,
所以现在要使用首先得打开他,在 yml 文件加上如下配置 :
feign:
  hystrix:
    enabled: true
zuul 网关组件
Zuul 包含了对请求的路由和过滤两个最主要的功能:
其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础而过滤器功能则负责对
请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础 .
Zuul Eureka 进行整合,将 Zuul 自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获得其他微服务的消息,
也即以后的访问微服务都是通过 Zuul 跳转后获得。
注意: Zuul 服务最终还是会注册进 Eureka,也属于一个客户端
 
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>

启动类添加 @EnableZuulProxy 注解

zuul/zuul-server9001/zuul-server9002 application.yml 部分 其余和集群中的客户端配置一样
zuul-application.yml 部分
spring:
  application:
    name: zuul #此实例注册到eureka服务端的name

zuul:
  prefix: /api # 添加前缀
  ignored-services: "*"#user-server # 如果是 "*" 就是所有
  routes:
    user:
      serviceId: zuul-server
      path: /zuul/**
zuul-server9001/zuul-server9002 application.yml 部分
spring:
  application:
    name: zuul-server #此实例注册到eureka服务端的name

zuul:
#  prefix: /api # 添加前缀
  ignored-services: "*"#user-server # 如果是 "*" 就是所有
  routes:
    user:
      serviceId: user-server
      path: /user/**
user8000服务的控制器
@RequestMapping("/test")
@ResponseBody
public Object getTest(){
    // 原始访问路径 http://localhost:8000/test
    // application.yml
    //     spring:application:name: user-server #此实例注册到eureka服务端的name
    // 路由访问路径 http://localhost:8080/user-server/test
    // 设置路由规则后的访问路径
    //     http://localhost:8080/user/test
    // 设置zuul集群后的访问路径 
    //     http://localhost:8080/api/zuul/user/test
    /**
     * zuul:
     *    prefix: /api # 添加前缀
     *   ignored-services: user-server # 如果是 "*" 就是过滤所有
     *   routes:
     *     user:
     *       serviceId: user-server
     *       path: /user/**
     */

    return "路由测试访问";
}

zuul 的过滤器

过滤器 (fifilter) zuul 的核心组件 zuul 大部分功能都是通过过滤器来实现的。
zuul 中定义了 4 种标准过滤器类型,这 些过滤器类型对应于请求的典型生命周期。
PRE :这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份 验证、在 集群中选择请求的微服务、记录调试信息等。
ROUTING :这种过滤器将请求路由到微服务。这种过滤器 用于构建发送给微服 务的请求,并使用 Apache HttpCIient Netfilx Ribbon 请求微服务
POST: 这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准 的 HTTP Header 、收集统计信息和指标、将响应从微服务发送给客户端等。
ERROR :在其他阶段发生错误时执行该过滤器。
如果要编写一个过滤器,则需继承 ZuulFilter 类 实现其中方法 :
 
@Component
public class LogFilter extends ZuulFilter {
    @Override
    public String filterType () {
        return FilterConstants . ROUTE_TYPE ;
    }
    @Override
    public int filterOrder () {
        return FilterConstants . PRE_DECORATION_FILTER_ORDER ;
    }
    @Override
    public boolean shouldFilter () {
        return true ;
    }
    @Override
    public Object run () throws ZuulException {
        RequestContext currentContext = RequestContext . getCurrentContext ();
        HttpServletRequest request = currentContext . getRequest ();
        String remoteAddr = request . getRemoteAddr ();
        System . out . println ( " 访问者 IP " + remoteAddr + " 访问地址 :" + request . getRequestURI ());
        return null ;
    }
}
由代码可知,自定义的 zuul Filter 需实现以下几个方法。
filterType: 返回过滤器的类型。有 pre route post error 等几种取值,分别对应上文的几种过滤器。
详细可以参考 com.netflix.zuul.ZuulFilter.filterType() 中的注释。
filter0rder: 返回一个 int 值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字。
shouldFilter :返回一个 boolean 值来判断该过滤器是否要执行, true 表示执行, false 表示不执行。
run :过滤器的具体逻辑。
禁用 zuul 过滤器 Spring Cloud 默认为 Zuul 编写并启用了一些过滤器,例如 DebugFilter FormBodyWrapperFilter
等,这些过滤器都存放在 spring-cloud-netflix-core 这个 jar 包 里,一些场景下,想要禁用掉部分过滤器,该怎么办
呢? 只需在 application.yml 里设置 zuul...disable=true 例如,要禁用上面我们写的过滤器,这样配置就行了:
zuul.LogFilter.pre.disable=true

eureka 集群主界面

zuul 容错与回退直接看官网的demo

class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute () {
        // 制定为哪个微服务提供回退(这里写微服务名 写 * 代表所有微服务)
        return "*" ;
    }
    // 此方法需要返回一个 ClientHttpResponse 对象 ClientHttpResponse 是一个接口,具体的回退逻辑要实现此接口
    //route :出错的微服务名 cause :出错的异常对象
    @Override
    public ClientHttpResponse fallbackResponse ( String route , final Throwable cause ) {
        // 这里可以判断根据不同的异常来做不同的处理, 也可以不判断
        // 完了之后调用 response 方法并根据异常类型传入 HttpStatus
        if ( cause instanceof HystrixTimeoutException ) {
            return response ( HttpStatus . GATEWAY_TIMEOUT );
        } else {
            return response ( HttpStatus . INTERNAL_SERVER_ERROR );
        }
    }
    private ClientHttpResponse response ( final HttpStatus status ) {
    // 这里返回一个 ClientHttpResponse 对象 并实现其中的方法,关于回退逻辑的详细,便在下面的方法中
        return new ClientHttpResponse () {
            @Override
            public HttpStatus getStatusCode () throws IOException {
                // 返回一个 HttpStatus 对象 这个对象是个枚举对象, 里面包含了一个 status code 和 reasonPhrase信息
                return status ;
            }
            @Override
            public int getRawStatusCode () throws IOException {
                // 返回 status code 比如 404 500
                return status . value ();
            }
            @Override
            public String getStatusText () throws IOException {
                // 返回一个 HttpStatus 对象的 reasonPhrase 信息
                return status . getReasonPhrase ();
            }
            @Override
            public void close () {
                //close 的时候调用的方法, 讲白了就是当降级信息全部响应完了之后调用的方法
            }
            @Override
            public InputStream getBody () throws IOException {
                // 吧降级信息响应回前端
                return new ByteArrayInputStream ( " 降级信息 " . getBytes ());
            }
            @Override
            public HttpHeaders getHeaders () {
                // 需要对响应报头设置的话可以在此设置
                HttpHeaders headers = new HttpHeaders ();
                headers . setContentType ( MediaType . APPLICATION_JSON );
                return headers ;
            }
        };
    }
}

eureka 集群主界面

 最终调用图

HystrixDashbord 监控中心

猜你喜欢

转载自blog.csdn.net/qq_38108719/article/details/104887686