天天看点

Spring Cloud 之 Eureka.

一、微服务概述

1. 什么是微服务

 简单地说, 微服务是系统架构上的一种设计风格, 它的主旨是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间基于 RPC 进行通信协作。 被拆分成的每一个小型服务都围绕着系统中的某一项或一些耦合度较高的业务功能进行构建, 并且每个服务都维护着自身的数据存储(划重点,每个微服务都有自己的数据库实例)、 业务开发、自动化测试案例以及独立部署机制。

2. 微服务的特性

  • 服务组件化 :一个独立的系统拆成多个小型服务。
  • 以业务划分服务 :微服务应该以业务来划分,而不是按能力或其他因素来划分(比如之前做的一个项目直接将缓存能力建成了一个微服务组件)。
  • 智能端点和哑管道

    :服务之间通过 RPC 的方式调用,通常会使用以下两种服务调用方式:

    第一种:使用 HTTP 的 RESTfl API 或轻量级的消息发送协议, 实现信息传递与服务调用的触发。

    第二种:通过在轻量级消息总线上传递消息, 类似 RabbitMQ 等 一些提供可靠异步交换的中间件。

  • 去中心化处理 :不同的微服务组件可以选择不同的技术方案,甚至可以选择不同的语言。只有实现了对技术平台的透明, 才能更好地发挥不同语言对不同业务处理能力的优势, 从而打造更为强大的大型系统。
  • 去中心化管理数据 :采用分布式数据库,不同的微服务组件有着自己单独的数据库实例。虽然数据管理的去中心化可以让数据管理更加细致化,通过采用更合适的技术可让数据存储和性能达到最优。但是,由于数据存储于不同的数据库实例中后,数据一致性也成为微服务架构中亟待解决的问题之一。
  • 基础设施自动化 :自动化测试(每次部署前的强心剂, 尽可能地获得对正在运行的软件的信心)、自动化部署(解放烦琐枯燥的重复操作以及对多环境的配置管理)等。
  • 容错设计 :在微服务架构中,快速检测出故障源并尽可能自动恢复服务是必须被设计和考虑的。
  • 演进式设计 :没有必要一开始就把服务拆分的又细又多,可以随着业务系统的发展,把压力大的服务或者稳定不变化的模块做拆分合并动作。

2. 微服务的缺陷

  1. 运维的成本提高。这个是不可避免的,原来只需要运维一个单一独立的系统,现在要管理几个或者几十个微服务。
  2. 接口的一致性问题。A 微服务修改了接口,需要调用方B、C 服务同时做出修改。除非开发过程中严格遵守开闭原则(在这个敏捷流式开发的背景下,几乎很难做到)。
  3. 分布式的复杂性。网络延迟、分布式事务、异步消息等。
tips

:分布式事务本身的实现难度就非常大,所以在微服务架构中,我们更强调在各服务之间进行 “ 无事务 ” 的调用,而对于数据一致性,只要求数据在最后的处理状态是一致的即可;若在过程中发现错误,通过补偿机制来进行处理,使得错误数据能够达到最终的一致性。

二、Spring Cloud 简介

 Spring Cloud 是一个基于Spring Boot实现的微服务架构开发工具。它为微服务架构中涉及的配置管理、服务治理、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。

 Spring Cloud 的出现,可以说是对微服务架构的巨大支持和强有力的技术后盾。它是一个解决微服务架构实施的综合性解决框架,它整合了诸多被广泛实践和证明过的框架作为实施的基础部件,又在该体系基础上创建了一些非常优秀的边缘组件。举个 Dubbo 和 Spring Cloud 差异性的例子:在使用 Dubbo 开发过程中,分布式配置中心(百度的 Disconf、Netflix的Archaius、360的QConf、淘宝的 Diamond 等)、链接跟踪(京东的 Hydra、Twitter的 Zipkin 等)...一系列需要的组件,我都要去找第三方进行集成,还要考虑版本兼容的问题。而 Spring Cloud 就是一个微服务解决方案的“全家桶”,几乎我需要的全部微服务组件,我都能在其中找到“原装组件”:分布式配置中心(Config)、链接跟踪(Sleuth)、批量任务(Task),而且可以完美兼容。

三、Eureka 简介

 服务治理体系可以说是微服务架构中最为核心和基础的模块, 它主要用来实现各个微服务实例的自动化注册与发现。服务治理体系中的三个核心角色: 服务注册中心、 服务提供者以及服务消费者。而 Eureka Server 就承担了 Spring Cloud 的服务注册中心。接下来捋一捋 Eureka Server 进行服务治理的过程:

  1. 服务注册 :”服务提供者”在启动的时候会通过发送 REST 请求的方式将自己注册到 Eureka Server 上,同时带上了自身服务的一些元数据信息(hostName 之类的)。Eureka Server 接收到这个 REST 请求之后,将元数据信息存储在一个双层结构Map中,其中第一层的key是服务名,第二层的key是具体服务的实例名。
  2. 服务同步 :由于服务注册中心之间互相注册为服务(Eureka Server 高可用场景),当服务提供者发送注册请求到一个服务注册中心时,它会将该请求转发给集群中相连的其他注册中心, 从而实现注册中心之间的服务同步。通过服务同步,两个服务提供者的服务信息就可以通过这两台服务注册中心中的任意一台获取到。
  3. 服务续约 :在注册完服务之后,“服务提供者”会维护一个心跳用来持续告诉 Eureka Sever "我还活着 ”, 以防止Eureka Server 的 “ 剔除任务 ” 将该服务实例从服务列表中排除出去。
  4. 服务消费 :当我们启动“服务消费者”的时候,它会发送一个 REST 请求给服务注册中心,来获取上面注册的服务清单 。为了性能考虑, Eureka Serer会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新一次。
  5. 服务调用 :“服务消费者”在 获取服务清单后,通过服务名可以获得具体提供服务的实例名和该实例的元数据信息。因为有这些服务实例的详细信息,所以客户端可以根据自己的需要决定具体调用哪个实例,在 Ribbon 中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡。
  6. 服务下线 :服务实例进行正常的关闭操作时,它会触发一个服务下线的 REST 请求给 Eureka Server,告诉服务注册中心:“我要下线了 ”。服务端在接收到请求之后,将该服务状态置为下线(DOWN), 并把该下线事件传播出去。
  7. 失效剔除 :有些时候,我们的服务实例并不一定会正常下线,可能由于内存溢出、网络故障等原因使得服务不能正常工作,而服务注册中心并未收到 “服务下线 ” 的请求。为了从服务列表中将这些无法提供服务的实例剔除,Eureka Server 在启动的时候会创建一个定时任务,默认每隔 一段时间(默认为60秒) 将当前清单中超时(默认为90秒)没有续约的服务剔除出去。
  8. 自我保护 :Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%, 如果出现低于的情况(在单机调试的时候很容易满足, 实际在生产环境上通常是由于网络不稳定导致),Eureka Server 会将当前的实例注册信息保护起来, 让这些实例不会过期, 尽可能保护这些注册信息。

:Spring Cloud Eureka实现的服务治理机制强调了CAP原理中的AP, 即可用性与分区容错性,它与Zoo Keeper这类强调CP( 一致性、分区容错性)的服务治理框架最大的区别就是,Eureka为了实现更高的服务可用性,牺牲了一定的一致性,在极端情况下它宁愿接受故障实例也不要丢掉“健康”实例,比如,当服务注册中心的网络发生故障断开时,由于所有的服务实例无法维持续约心跳,在强调 AP的服务治理中将会把所有服务实例都剔除掉,而 Eureka 则会因为超过 85% 的实例丢失心跳而会触发保护机制,注册中心将会保留此时的所有节点,以实现服务间依然可以进行互相调用的场景,即使其中有部分故障节点,但这样做可以继续保障大多数的服务正常消费。

四、Eureka 实战

SpringBoot 版本号:2.1.6.RELEASE

SpringCloud 版本号:Greenwich.RELEASE

1. 服务注册中心

  • pom.xml
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
           
  • application.yml
server:
  port: 1111

eureka:
  instance:
    hostname: localhost
    prefer-ip-address: true
  client:
    # 表示不向注册中心注册自己
    register-with-eureka: false
    # 注册中心的职责是维护实例,不需要去检索服务
    fetch-registry: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
   server:
     # 是否要打开自我保护机制
     enable-self-preservation: true
           

eureka 的配置项主要有三项:instance、client、server。“instance”维护该服务的实例信息,包括 hostname、port 这类描述实例特征的元数据信息;“client”主要是服务注册数据的配置,比如超时时间、服务缓存时间等;“server”是服务注册中心特有的配置,配置 Eureka Server 的相关配置项,比如上面的是否打开自我保护。

  • Application.java
//启动一个服务注册中心
@EnableEurekaServer
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
           

至此,我们一个Eureka Server — 服务注册中心就搭建好了。

2. 服务提供者

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
           
server:
  port: 2222

spring:
  application:
    name: cloud-eureka-client

eureka:
  # 服务注册相关的配置信息
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka/
  instance:
    # 是否优先使用IP地址作为主机名的标识
    prefer-ip-address: true

           

就这样,我们的一个 Eureka Client 算是注册到 Eureka Server 上了。接下来,让我们试试用 DiscoveryClient 发现我们的服务信息:

// 自动化配置, 创建 DiscoveryClient 接口针对 Eureka 客户端的 EurekaDiscoveryClient 实例
@EnableDiscoveryClient
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application .class, args);
    }
}
           
@RestController
public class HelloController {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DiscoveryClient discoveryClient;
    @Value("${spring.application.name}")
    private String serviceId;

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String index() {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
        ServiceInstance instance = instances.get(0);
        logger.info("/hello, host:" + instance.getHost() + ", serviceId:" + instance.getServiceId());
        return "Hello World";
    }
}
           

3. 服务消费者

 有了服务注册中心和服务提供者,我们还差一个服务消费者,服务消费者需要依赖 Spring Cloud Ribbon。

 Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模板请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API 网关的请求转发等内容实际上都是通过 Ribbon 来实现的。

Ribbon 中内置了多种负载均衡策略,包括 RoundRobinRule 轮询策略、RandomRule 随机策略、BestAvailableRule 最大可用策略、WeightedResponseTimeRule 带有加权的轮询策略等。

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
           
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }


    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
           
  • ConsumerController.java
@RestController
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/ribbon-consumer")
    public String helloConsumer() {
        // 这里访问的是服务名,而不是一个具体的地址(为了实现负载均衡策略),在服务治理框架中,这是一个非常重要的特性。
        ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
        return result.getBody();
    }
}
           

接下来我们来捋一捋 Spring Cloud Ribbon 实现客户端负载均衡的步骤:

  1. 首先被 @loadBalanced 注解的 RestTemplate 发起的服务请求会被 LoadBalancerInterceptor 拦截下来。
  2. LoadBalancerInterceptor 把请求重新组织后,带上 serviceName(服务名) 交由 LoadBalancerClient 去处理(在 Eureka 和 Ribbon 的集成中,处理由 RibbonLoadBalancerClient 进行)。
  3. RibbonLoadBalancerClient 根据 serviceName 得到负载均衡策略 ILoadBalancer(默认的是 ZoneAwareLoadBalancer 区域亲和策略)。
  4. ILoadBalancer 根据负载均衡规则选取一台服务器 Server。
  5. 有了 Server 信息以后,正式发起一次请求,具体的请求动作在 AsyncLoadBalancerInterceptor 进行。它先将 restTemplate 中的 serviceName 转换成 host:port 的形式(这个在 ServiceRequestWrapper 中实现),然后发起一个异步请求。

五、附加

  • 默认情况下,Eureka 使用 Jersey 和 XStream 配合 JSON 作为 Server 与 Client 之间的通信协议。
  • YAML 的意思其实是: Yet Another Markup Language — 仍是一种标记语言(这个看着有点想笑)。
  • 在 SpringBoot 的属性配置文件中,可以通过使用 ${random} 配置来产生随机的 int 值、long 值或者 string 字符串。
  • 在 Spring Boot 中,多环境配置的文件名需要满足 application-{profile}.properties的格式, 其中{profile}对应你的环境标识。通过启动参数 --spring.profiles.active=test 来指定激活的环境变量。
  • 负载均衡:
    Spring Cloud 之 Eureka.

演示源代码 :https://github.com/JMCuixy/spring-cloud-demo

内容参考《Spring Cloud 微服务实战》