SpringCloud微服务

一 什么是微服务架构

微服务是系统架构上的一种设计风格, 它的主旨是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间通过基于HTTP的RESTful API进行通信协作。 被拆分成的每一个小型服务都围绕着系统中的某一项或一些耦合度较高的业务功能进行构建, 并且每个服务都维护着自身的数据存储、 业务开发、自动化测试案例以及独立部署机制。 由千有了轻量级的通信协作基础, 所以这些微服务可以使用不同的语言来编写。

二 Spring Cloud简介

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

Spring Cloud包含了多个子项目(针对分布式系统中涉及的多个不同开源产品,还可能会新增),如下所述。

- Spring Cloud Config: 配置管理工具, 支持使用Git存储 配置内容,可以使用它实现应用配置的外部化存储,并支持客户端配置信息刷新、 加密/解密配置内容 等。
- Spring Cloud Netflix: 核心 组件, 对多个Netflix OSS开源套件进行整合。
    1. Eureka: 服务治理组件, 包含服务注册中心、 服务注册与发现机制的实现。
    2. Hystrix: 容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。
    3. ribbon: 客户端负载均衡的服务调用组件。
    4. Feign: 基于ribbon 和 Hystrix 的声明式服务调用组件。
    5. Zuul: 网关组件, 提供智能路由、 访问过滤等功能。
    6. Archaius: 外部化配置组件。
- Spring Cloud Bus: 事件、 消息总线, 用于传播集群中的状态变化或事件, 以触发后续的处理, 比如用来动态刷新配置等。
- Spring Cloud Cluster: 针对 ZooKeeper、 Redis、 Hazelcast、 Consul 的选举算法和通用状态模式的实现。  
- Spring Cloud Cloudfoundry: 与 Pivotal Cloudfoundry 的整合支持。
- Spring Cloud Consul: 服务发现与配置管理工具。
- Spring Cloud Stream: 通过 Redis、 Rabbit 或者 Kafka 实现的消费微服务, 可以通过简单的声明式模型来发送和接收消息。
- Spring Cloud AWS: 用千简化整合 Amazon Web Service 的组件。
- Spring Cloud Security: 安全工具包, 提供在 Zuul 代理中对 0Auth2 客户端请求的中继器。
- Spring Cloud Sleuth: Spring Cloud 应用的分布式跟踪实现, 可以完美整合 Zipkin。
- Spring Cloud ZooKeeper: 基于 ZooKeeper 的服务发现与配置管理组件。
- Spring Cloud Starters: Spring Cloud 的基础组件, 它是基于Spring Boot 风格项目的基础依赖模块。
- Spring Cloud CLI: 用于在 Groovy 中快速创建 Spring Cloud 应用的 Spring Boot CLI插件。

三 微服务构建: Spring Boot

为了能够更合理地重写各属性的值,SpringBoot使用了下面这种较为特别的属性加载顺序:

1 在命令行中传入的参数。
2. SPRING APPLICATION JSON中的属性。 SPRING_APPLICATION—JSON是以JSON格式配置在系统环境变量中的内容。
3. java:comp/env中的JNDI 属性。
4. Java的系统属性, 可以通过System.getProperties()获得的内容。
5. 操作系统的环境变量 。
6. 通过random.*配置的随机属性。
7. 位于当前应用 jar 包之外,针对不同{profile}环境的配置文件内容,例如application-{profile}.properties或是YAML定义的配置文件。
8. 位于当前应用 jar 包之内 ,针对不同{profile}环境的配置文件内容,例如application-{profile}.properties或是YAML定义的配置文件。
9. 位于当前应用jar包之外的application.properties和YAML配置内容。
10. 位于当前应用jar包之内的application.properties和YAML配置内容。
11. 在@Configuration注解修改的类中,通过@PropertySource注解定义的属性。
12. 应用默认属性,使用SpringApplication.setDefaultProperties 定义的内容。

四 服务治理: Spring Cloud Eureka

Spring Cloud Eureka 是 Spring Cloud Netflix 微服务套件中的一部分, 它基于 NetflixEureka 做了二次封装, 主要负责完成微服务架构中的服务治理功能。 Spring Cloud 通过为Eureka 增加了 Spring Boot 风格的自动化配置,我们只需通过简单引入依赖和注解配置就能让 Spring Boot 构建的微服务应用轻松地与 Eureka 服务治理体系进行整合。

4.1 服务治理

服务治理可以说是微服务架构中最为核心和基础的模块, 它主要用来实现各个微服务实例的自动化注册与发现。

为了解决微服务架构中的服务实例维护问题, 产生了大量的服务治理框架和产品。 这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。

  • 服务注册:

在服务治理框架中, 通常都会构建一个注册中心, 每个服务单元向注册中心登记自己提供的服务, 将主机与端口号、 版本号、 通信协议等一些附加信息告知注册中心, 注册中心按服务名分类组织服务清单。 比如, 我们有两个提供服务A的进程分别运行于 192.168.0.100:8000和192.168.0.101:8000位置上,
另外还有三个 提供服务B的进程分别运行千192.168.0.100:9000 、192.168.0.101:9000、 192.168.0.102:9000位置上。 当这些进程均启动,并向注册中心注册自己的服务之后, 注册中心就会维护类似下面的一个服务清单。另外, 服务注册中心还需要以心跳的方式去监测清单中的服务是否可用, 若不可用需要从服务清单中剔除, 达到排除故障服务的效果。

服务名 位置
服务A 192.168.0.100:8000、192.168.0.101:8000
服务B 192.168.0.100:9000、192.168.0.101:9000、192.168.0.102:9000
  • 服务发现:

由于在服务治理框架下运作, 服务间的调用不再通过指定具体的实例地址来实现, 而是通过向服务名发起请求调用实现。 所以, 服务调用方在调用服务提供方接口的时候, 并不知道具体的服务实例位置。 因此, 调用方需要向服务注册中心咨询服务, 并获取所有服务的实例清单, 以实现对具体服务实例的访问。 比如,现有服务C希望调用服务A, 服务C就需要向注册中心发起咨询服务请求, 服务注册中心就会将服务A的位置清单返回给服务C, 如按上例服务A的情况,C便获得了服务A的两个可用位置 192.168.0.100:8000和192.168.0.101:8000。当服务C要发起调用的时候, 便从该清单中以某种轮询策略取出一 个位置来进行服务调用, 这就是后续我们将会介绍的客户端负载均衡。 这里我们只是列举了一种简单的服务治理逻辑, 以方便理解服务治理框架的基本运行思路。 实际的框架为了性能等因素, 不会采用每次都向服务注册中心获取服务的方式, 并且不同的应用场景在缓存和服务剔除等机制上也会有一些不同的实现策略。

4.2 Netflix Eureka

Spring Cloud Eureka, 使用Netflix Eureka来实现服务注册与发现, 它既包含了服务端组件,也包含了客户端组件,并且服务端与客户端均采用Java编写,所以Eureka主要适用于通过Java实现的分布式系统,或是与NM兼容语言构建的系统。但是, 由于Eureka服务端的服务治理机制提供了完备的RESTful APL所以它也支持将非Java语言构建的微服务应用纳入Eureka的服务治理体系中来。只是在使用其他语言平台的时候,需要自己来实现Eureka的客户端程序。

Eureka服务端,我们也称为服务注册中心。 它同其他服务注册中心一样,支持高可用配置。它依托于强一致性提供良好的服务实例可用性,可以应对多种不同的故障场景。 如果Eureka以集群模式部署,当集群中有分片出现故障时,那么Eureka就转入自我保护模式。它允许在分片故障期间继续提供服务的发现和注册,当故障分片恢复运行时, 集群中的其他分片会把它们的状态再次同步回来。

Eureka客户端,主要处理服务的注册与发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。

4.3 高可用注册中心

在微服务架构这样的分布式环境中, 我们需要充分考虑发生故障的情况, 所以在生产环境中必须对各个组件进行高可用部署, 对于微服务如此, 单节点的服务注册中心这在生产环境中显然并不合适,我们需要构建高可用的服务注册中心以增强系统的可用性。
Eureka Server的设计一开始就考虑了高可用问题, 在Eureka的服务治理设计中, 所有节点即是服务提供方, 也是服务消费方, 服务注册中心也不例外。 通过在单节点的配置中, 设置过下面这两个参数, 让服务注册中心不注册自己:

eureka.client.register-with-eureka=false 由于该应用为注册中心,所以设置为 false, 代表不向注册中心注册自己。
eureka.client.fetch-registry=false 由于注册中心的职责就是维护服务实例,它并不需要去检索服务, 所以也设置为 false。

Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心, 以实现服务清单的互相同步, 达到高可用的效果。

4.4 服务发现与消费

服务发现的任务由Eureka的客户端完成,而服务消费的任务由ribbon完成 。Ribbon是一个基于HTTP和TCP的客户端负载均衡器,它可以在通过客户端中配置的 ribbonServerList服务端列表去轮询访问以达到均衡负载的作用。

当Ribbon与Eureka联合使用时,ribbon的服务实例清单ribbonServerList会被DiscoveryEnabledNIWSServerList重写, 扩展成从Eureka注册中心中获取服务端列表。同时它也会用 NIWSDiscoveryPing来取代工ping, 它将职责委托给Eureka 来确定服务端是否已经启动 。

4.5Eureka详解

Eureka 服务治理体系中的三个核心角色: 服务注册中心、 服务提供者以及服务消费者。

4.5.1 基础架构

  • 服务注册中心:Eureka 提供的服务端, 提供服务注册与发现的功能
  • 服务提供者:提供服务的应用, 可以是 Spring Boot 应用, 也可以是其他技术平台且遵循 Eureka 通信机制的应用。它将自己提供的服务注册到 Eureka
  • 服务消费者:消费者应用从服务注册中心获取服务列表, 从而使消费者可以知道去何处调用其所需要的服务

4.5.2 服务治理机制

以下图为例, 以此来理解基于Eureka 实现的服务治理体系是如何运作起来的。

服务治理机制

• "服务注册中心-1" 和 “服务注册中心-2", 它们互相注册组成了高可用集群。
• "服务提供者” 启动了两个实例, 一个注册到 “服务注册中心-1" 上, 另外一个注册到 “服务注册中心-2" 上。
• 还有两个 “服务消费者“, 它们也都分别只指向了一个注册中心。

服务提供者

服务注册

“服务提供者” 在启动的时候会通过发送REST请求的方式将自己注册到EurekaServer上, 同时带上了自身服务的一些元数据信息。Eureka Server接收到这个REST请求之后,将元数据信息存储在一个双层结构Map中, 其中第一层的key是服务名, 第二层的key是具体服务的实例名。

在服务注册时, 需要确认一下 eureka.client.register-with-eureka=true参数是否正确, 该值默认为true。 若设置为false将不会 启动注册操作。

服务同步

如架构图中所示, 这里的两个服务提供者分别注册到了两个不同的服务注册中心上,也就是说, 它们的信息分别被两个服务注册中心所维护。 此时, 由于服务注册中心之间因互相注册为服务, 当服务提供者发送注册请求到一个服务注册中心时, 它会将该请求转发给集群中相连的其他注册中心, 从而实现注册中心之间的服务同步 。 通过服务同步,两个服务提供者的服务信息就可以通过这两台服务注册中心中的任意一台获取到。

服务续约

在注册完服务之后,服务提供者会维护一个心跳用来持续告诉EurekaSe1-ver: "我还活着”, 以防止Eureka Server的“剔除任务 ” 将该服务实例从服务列表中排除出去,我们称该操作为服务续约(Renew)。
关于服务续约有两个重要属性,我们可以关注并根据需要来进行调整:

eureka.instance.lease-renewal-interval-in-seconds=30 用于定义服务续约任务的调用间隔时间,默认为30秒
eureka.instance.lease-expiration-duration-in-seconds=90 用于定义服务失效的时间,默认为90秒。

服务消费者

获取服务

当启动服务消费者的时候, 它会发送一个REST请求给服务注册中心,来获取上面注册的服务清单 。 为了性能考虑, Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新 一次。

获取服务是服务消费者的基础,所以必须确保eureka.client.fetch-registry=true参数没有被修改成false, 该值默认为七rue。若希望修改缓存清单的 更新时间,可以通过 eureka.client.registry-fetch-interval-seconds= 30参数进行修改,该参数默认值为30, 单位为秒。

服务调用

服务消费者在 获取服务清单后,通过服务名可以获得具体提供服务的实例名和该实例的元数据信息。 因为有这些服务实例的详细信息, 所以客户端可以根据自己的需要决定具体调用哪个实例,在ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡。

对于访问实例的选择,Eureka中有Region和Zone的概念, 一个Region中可以包含多个Zone, 每个服务客户端需要被注册到 一个Zone中, 所以每个客户端对应一个Region和一个Zone。 在进行服务调用的时候,优先访问同处一个 Zone 中的服务提供方, 若访问不到,就访问其他的Zone。

服务下线

在系统运行过程中必然会面临关闭或重启服务的某个实例的情况, 在服务关闭期间,我们自然不希望客户端会继续调用关闭了的实例。 所以在客户端程序中, 当服务实例进行正常的关闭操作时, 它会触发一个服务下线的REST请求给Eurke a Server, 告诉服务注册中心:“我要下线了”。 服务端在接收到请求之后, 将该服务状态置为下线(DOWN), 并把该下线事件传播出去。

服务注册中心

失效剔除

有些时候, 我们的服务实例并不一定会正常下线, 可能由于内存溢出、 网络故障等原因使得服务不能正常工作, 而服务注册中心并未收到 “服务下线” 的请求。 为了从服务列表中将这些无法提供服务的实例剔除, Eureka Server在启动的时候会创建一个定时任务,默认每隔一段时间(默认为60秒) 将当前清单中超时(默认为90秒)没有续约的服务剔除出去。

自我保护

服务注册到EurekaServer之后,会维护一个心跳连接,告诉EurekaServer自己还活着。EurekaServer
在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%, 如果出现低于的情况(在单机调试的时候很容易满足, 实际在生产环境上通常是由于网络不稳定导致), EurekaServer会将当前的实例注册信息保护起来, 让这些实例不会过期, 尽可能保护这些注册信息。 但是, 在这段保护期间内实例若出现问题, 那么客户端很容易拿到实际已经不存在的服务实例, 会出现调用失败的清况, 所以客户端必须要有容错机制, 比如可以使用请求重试、 断路器等机制。

由于本地调试很容易触发注册中心的保护机制, 这会使得注册中心维护的服务实例不那么准确。 所以, 我们在本地进行开发的时候, 可以使用eureka.server.enableself-preservation = false参数来关闭护机制, 以确保注册中心可以将不可用的实例正确剔除。

4.5.3 源码分析

首先, 对于服务注册中心、 服务提供者、 服务消费者这三个主要元素来说, 后两者(也就是 Eureka 客户端)在整个运行机制中是大部分通信行为的主动发起者, 而注册中心主要是处理请求的接收者。所以, 我们可以从 Eureka 的客户端作为入口看看它是如何完成这些主动通信行为的。我们在将一个普通的 Spring Boot 应用注册到 Eureka Server 或是从 Eureka Server 中获取服务列表时, 主要就做了两件事:

• 在应用主类中配置了@EnableDiscoveryClient注解。
• 在 app让cation.properties 中用 eureka .client.serviceUrl.defaultZone参数指定了服务注册中心的位置。

顺着上面的线索, 我们来看看@EnableDiscoveryClient 的源码, 具体如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
    boolean autoRegister() default true;
}

从该注解的注释中我们可以知道,它主要用来开启discoveryClient 的实例。通过搜索 DiscoveryClient, 我们可以发现有一个类和一个接口。通过梳理可以得到如下图所示的关系:

服务发现类图

其中左边的org.springframework.cloud.client.discovery.DiscoveryClient是Spring Cloud的接口,它定义了用来发现服务的常用抽象方法, 通过该接口可以有效地屏蔽服务治理的实现细节, 所以使用 Spring Cloud 构建的微服务应用可以方便地切换不同服务治理框架, 而不改动程序代码, 只需要另外添加一些针对服务治理框架的配置即可。

org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient是对该接口的实现, 从命名来判断, 它实现的是对 Eureka 发现服务的封装。所以 EurekaDiscoveryClient 依赖了 Netflix Eureka的com.netflix.discovery.EurekaClient接口,EurekaClient 继承了 LookupService 接口, 它们都是Netflix开源包中的内容, 主要定义了针对Eureka的发现服务的抽象方法, 而真正实现发现服务的 则是Netflix包中的com.netf巨x.discovery.DiscoveryClient类。

详细看看 DiscoveryClient 类,首先是该类头部的注释部分:

这个类用于帮助与Eureka Server互相协作。Eureka Client负责下面的任务:
- 向Eureka Server注册服务实例 
- 向Eureka Server服务租约
- 当服务关闭期间, 向Eureka Server取消租约 
- 查询Eureka Server中的服务实例列表
Eureka Client还需要配置一个Eureka Server的 URL列表。

在具体研究 Eureka Client 负责完成的任务之前, 我们先看看在哪里对 Eureka Server 的 URL 列表进行配置。根据我们配置的属性名eureka.client.serviceUrl.defaultZone, 通 过 serviceUrl 可 以找 到该属性相关的加载属性, 但是在 SR5 版本中它们 都被 @Deprecated 标注为不再建议使用,并@link到了替代类com.netflix.discovery. endpoint.EndpointUtils, 所以我们可以在该类中找到下面逻辑函数。

Region、 Zone

客户端依次加载了两个内容, 第一个是Region, 第二个是Zone, 从其加载逻辑上我们可以判断它们之间的关系:
• 通过getRegion函数, 我们可以看到它从配置中读取了 一个Region返回, 所以 一 个微服务应用只可以属于 一个Region, 如果不特别配置, 默认为default 若我们要自己设置, 可以通过eureka.client.region属性来定义。
• 通过 getAvailabilityZones 函数, 可以知道当我们没有特别为Region配置 Zone的时候,将默 认采用defaulZt one , 这也是我们之前配置参数 eureka. client.serviceUrl.defaultZone的由来。若要为应用指定Zone, 可以通过 eureka.client.availability-zones属性来进行设置。从该函数的return内容, 我们可以知道Zone能够设置多个, 并且通过逗号分隔来配置。 由此, 我们可以判断Region与Zone是一对多的关系。

serviceUrls

在获取了 Region 和 Zone 的信息之后, 才开始真正加载Eureka Server的具体地址。 具体获取 serviceUrls 的实现, 我们可以详细查看 getEurekaServerServiceUrls 函数的具体实现类 EurekaClientConfigBean, 该类是 EurekaClientConfig 和 EurekaConstants接口的实现,用来加载配置文件中的内容。

当我们在微服务应用中使用 Ribbon 来实现服务调用时, 对千 Zone 的设置可以在负载 均衡时实现区域亲和特性: Ribbon 的默认策略会优先访问同客户端处于 一个 Zone 中的服 务端实例,只有当同 一个 Zone 中没有可用服务端实例的时候才会访问其他 Zone 中的实例。 所以通过 Zone 属性的定义,配合实际部署的物理结构,我们就可以有效地设计出对区域性 故障的容错集群。

服务注册

在理解了多个服务注册中心信息的加载后,我们再回头看看 DiscoveryClient 类是 如何实现 “ 服务注册 “ 行为的, 通过查看它的构造类, 可以找到它调用了下面这个函数:

  /**
     * Initializes all scheduled tasks.
     */
    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            cacheRefreshTask = new TimedSupervisorTask(
                    "cacheRefresh",
                    scheduler,
                    cacheRefreshExecutor,
                    registryFetchIntervalSeconds,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new CacheRefreshThread()
            );
            scheduler.schedule(
                    cacheRefreshTask,
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        if (clientConfig.shouldRegisterWithEureka()) {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

            // Heartbeat timer
            heartbeatTask = new TimedSupervisorTask(
                    "heartbeat",
                    scheduler,
                    heartbeatExecutor,
                    renewalIntervalInSecs,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new HeartbeatThread()
            );
            scheduler.schedule(
                    heartbeatTask,
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            // InstanceInfo replicator
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    logger.info("Saw local status change event {}", statusChangeEvent);
                    instanceInfoReplicator.onDemandUpdate();
                }
            };

            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }

从上面的函数中, 可以看到一个与服务注册相关的判断语旬 if (clientConfig. shouldRegisterWithEureka ())。 在该分支内, 创建了一个 InstanceinfoReplicator 类的实例, 它会执行一个定时任务, 而这个定时任务的具体工作可以查看该类的 run() 函数, 具体如下所示:

 public void run() {
        try {
            discoveryClient.refreshInstanceInfo();

            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                discoveryClient.register();
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
            logger.warn("There was a problem with the instance info replicator", t);
        } finally {
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

其中discoveryClient.register () ;是真正触发调用注册的地方。继续查看 register ()的实现内容, 如下所示:

/**
     * Register with the eureka service by making the appropriate REST call.
     */
    boolean register() throws Throwable {
        logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
        EurekaHttpResponse<Void> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
    }

通过属性命名, 大家基本也能猜出来, 注册操作也是通过REST请求的方式进行的。同时, 我们能看到发起注册请求的时候, 传入了一个 com.netfix.appinfo. Instanceinfo 对象, 该对象就是注册时客户端给服务端的服务的元数据。

服务获取与服务续约

DiscoveryClient 的initScheduledTasks 函 数,其中还有两个定时任务,分别是 “ 服务获取 ” 和 “ 服务续约":

“ 服务获取 ” 任务相对于 “ 服务续约 ” 和 “ 服务注册 “ 任务更为独立。”服务续约 ” 与 “ 服务注册 “ 在同一个 if 逻辑中,这个不难理解,服务注册到 Eureka Server 后, 自然需要一个心跳去续约, 防止被剔除,所以它们肯定是成对出现的。从源码中,我们更清楚地看到了之前所提到的,对于服务续约相关的时间控制参数:

eureka.instance.lease-renewal-interval-in-seconds= 30
eureka.instance.lease-expiration-duration-in-seconds= 90

而 “ 服务获取 ” 的逻辑在独立的一个if判断中, 其判断依据就是我们之前所提到的 eureka.client.fetch-registry=true参数,它默认为 true, 为了定期更新客户端的服务清单, 以保证客户端能够访问确实健康的服务实例,“服务获取” 的请求不会只限于服务启动,而是一个定时执行的任务,任务运行中的 registryFetchintervalSeconds 参数对应的的eureka.client.registry-fetch-interval-seconds= 30 配置参数, 它默认为 30 秒。

4.5.4 服务注册中心处理

服务注册接收请求

   @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                                @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
        // validate that the instanceinfo contains all the necessary required fields
        if (isBlank(info.getId())) {
            return Response.status(400).entity("Missing instanceId").build();
        } else if (isBlank(info.getHostName())) {
            return Response.status(400).entity("Missing hostname").build();
        } else if (isBlank(info.getIPAddr())) {
            return Response.status(400).entity("Missing ip address").build();
        } else if (isBlank(info.getAppName())) {
            return Response.status(400).entity("Missing appName").build();
        } else if (!appName.equals(info.getAppName())) {
            return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
        } else if (info.getDataCenterInfo() == null) {
            return Response.status(400).entity("Missing dataCenterInfo").build();
        } else if (info.getDataCenterInfo().getName() == null) {
            return Response.status(400).entity("Missing dataCenterInfo Name").build();
        }

        // handle cases where clients may be registering with bad DataCenterInfo with missing data
        DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
        if (dataCenterInfo instanceof UniqueIdentifier) {
            String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
            if (isBlank(dataCenterInfoId)) {
                boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
                if (experimental) {
                    String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                    return Response.status(400).entity(entity).build();
                } else if (dataCenterInfo instanceof AmazonInfo) {
                    AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
                    String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
                    if (effectiveId == null) {
                        amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
                    }
                } else {
                    logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
                }
            }
        }

        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }

在对注册信息进行了 一 堆校验之后,会调用org.springframework.cloud. netflix.eureka.server.InstanceRegistry对象中的register(Instanceinfo info, int leaseDuration, boolean isReplication)函数来进行服务注册:

  @Override
    public void register(final InstanceInfo info, final boolean isReplication) {
        int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
        if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
            leaseDuration = info.getLeaseInfo().getDurationInSecs();
        }
        super.register(info, leaseDuration, isReplication);
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    }

在注册函数中, 调用com.netflix.eureka.registry.AbstractlnstanceRegistry父类中的
注册实现,将InstanceInfo中的元数据信息存储在 一个ConcurrentHashMap对象中。 正如我们之前所说的, 注册中心存储了两层Map结构, 第一 层的key 存储服务名:
Insztancelnfo中的appName属性, 第二层的key存储实例名: Instancelnfo中的 instanceId属性。

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry

五 客户端负载均衡 Spring Cloud Ribbon

负载均衡在系统架构中是一 个非常重要, 并且是不得不去实施的内容。 因为负载均衡 是对系统的高可用、 网络压力的缓解和处理能力扩容的重要手段之一 。 我们通常所说的负 载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。 硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如 F5 等;而软件负载均衡则 是通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求分发工作,比如 Nginx 等。

硬件负载均衡的设备或是软件负载均衡的软件模块都会维护一 个下挂可用的服务端清 单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。 当客户端发送请求到负载均衡设备的时候 , 该设备按某种算法(比如线性轮询、 按权重负载、 按流量负载等)从维护的可用服务端清单中取出 一台服务端的地址, 然后进行转发。

而客户端负载均衡和服务端负载均衡最大的不同点在于上面所提到的服务清单所存储 的位置。 在客户端负载均衡中, 所有客户端节点都维护着自己要访问的服务端清单, 而这些服务端的清单来自于服务注册中心。在Spring Cloud实现的服务治理框架中, 默认会创建针对各 个服务治理框架的沁bbon自动化整合配置, 比如Eureka 中的 org.springframework. cloud.netflix.ribbon.eureka. RibbonEurekaAutoConfiguration , Consul 中的org.springframework.cloud.consul.discovery. RibbonConsulAuto- Configuration。
通过Spring CloudR巾bon的封装, 我们在微服务架构中使用客户端负载均衡调用非常
简单, 只需要如下两步:

- 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
- 服务消费者直接通过调用被@LoadBalanced 注解修饰过的 RestTemplate来实现面向服务的接口调用。

5.1 ribbon源码分析

六 服务容错保护: Spring Cloud Hystrix

七 卢朋式服务调用: Spring Cloud Feign

八 API网关服务:Spring Cloud Zuul

九 分布式配置中心: Spring Cloud Config

十 消息总线: Spring Cloud Bus

十一 消息驱动的微服务: Spring Cloud Stream

十二 分布式服务跟踪: Spring Cloud Sleuth

一条小咸鱼