Spring Cloud 入门到进阶 - 01 Eureka 服务实例的健康自检 (下)


博主整理的SpringCloud系列目录:>>戳这里<<

内容关联篇(建议先看):
Spring Cloud 入门到进阶 - 01 Eureka介绍及简单服务搭建(上)
Spring Cloud 入门到进阶 - 01 Eureka集群搭建(中)


一、Eureka 服务实例的健康自检

在默认情况下, Eureka 的客户端每隔 30 秒会发送 次心跳给服务器端,告知它仍然存活。但是,在实际环境中有可能出现这种情况,客户端表面上可以正常发送心跳,但实际上服务是不可用的。

例如一个需要访问数据的服务提供者,表面上可以正常响应,但是数据库己经无法访问:又如,服务提供者需要访问第三方的服务,而这些服务早己失效。对于这些情况,应当告诉服务器当前客户 的状态,调用者或者其他客户端无法获取这些有问题的实例。实现该功能,可以使用 Eureka 的健康检查控制器。

1、程序结构

将前面(传送门)我们学习并搭建的第一个Eureka项目中的服务器、服务提供者和服务调用者进行复制,命名如下。

  • health-handler-server 本例的 Eureka 服务器。
  • health-handler-provider 本例的服务提供者客户端。
  • health-handler-invoker 本例的服务调用者客户端。
    在这里插入图片描述

假设在实际环境中,服务提供者模块需要访问数据库,本例的 health-handler-provider 模块,将是进行健康自检的模块。

2、使用 Spring Boot Actuator

Spring Boot Actuator 模块主要用于系统监控,当应用程序整合了 Actuator 后,它就会自动提供多个服务端点,这些端点可以让外部看到应用程序的健康情况。

在本例中使用的是 /health 端点。修改 health-handler-providerpom.xml 文件,加入以下依赖:

<!-- 加入系统监控模块 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

加入依赖后,先启动 health-handler-server,再启动 health-handler-provider。这两个模块与上篇的项目基本类似,仅仅修改了部分类名。

启动完成后,在浏览器中访问 http://localhost:8080/actuator/health,可以看到输出如下:
在这里插入图片描述

3、实现应用健康自检

如果一个客户端本身没有问题,但是该模块所依赖的服务无法使用,那么对于服务器以及其他客户端来说,该客户端也是不可用的,最常见的就是访问数据库的模块。

我们需要做两件事:第一,让客户端自己进行检查,是否能连接数据库;第二,将连接数据库的结果与客户端的状态进行关联,并且将状态告诉服务器。

我们使用 Spring Boot Actuator 可以直接实现一个自定义的 HealthIndicator,根据是否能访问数据库,来决定应用自身的健康。

3.1 添加健康指示器

下面,我们为服务提供者添加健康指示器。

package com.swotxu.hhprovider.health;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

/**
 * 模拟检查数据库连接是否成功的健康指示器
 * @Date: 2020/7/4 16:18
 * @Author: swotXu
 */
@Component
public class MyHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        Health.Builder builder = new Health.Builder();
        // 数据库连接:成功返回 UP,失败返回 DOWN
        return HealthController.canVisitDb?
                builder.up().build() : builder.down().build();
    }
}

为了简单起见,使用 HealthController 类的 canVisitDb 变量来模拟是否能连接上数据库。该变量的相关代码如下:

package com.swotxu.hhprovider.health;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Date: 2020/7/4 16:20
 * @Author: swotXu
 */
@RestController
public class HealthController {

    // 标识当前数据库是否可以访问
    static Boolean canVisitDb = false;

    @RequestMapping(value = "/db/{canVisitDb}", method = RequestMethod.GET)
    public String setConnectState(@PathVariable("canVisitDb") Boolean canVisitDb) {
        HealthController.canVisitDb = canVisitDb;
        return String.format("当前数据库是否正常: %b", canVisitDb);
    }
}

控制器中的 canVisitDb 变量,可以通过 /db/false/db/true 两个地址来修改,配合健康指示器一起使用。如果该值为 true ,健康指示器将会返回“UP”状态,反之则返回“DOWN”的状态。

3.2 测试健康指示器

修改完后 ,启动服务器( health-handler-server )以及服务提供者( health-handler-provider)。访问 http://localhost:8080/actuator/health 可以看到服务提供者的健康状态。
在这里插入图片描述
在这里插入图片描述

3.3 添加健康检查处理器

如果服务提供者想把健康状态告诉服务器,还需要实现“健康检查处理器”。处理器会将应用的健康状态保存到内存中,状态一旦发生改变,就会重新向服务器进行注册,其他的客户端将拿不到这些不可用的实例。

下面,我们新建一个健康检查处理器:

package com.swotxu.hhprovider.health;

import com.netflix.appinfo.HealthCheckHandler;
import com.netflix.appinfo.InstanceInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Status;
import org.springframework.stereotype.Component;

/**
 * 健康检查处理器,将数据库是否可用的状态通知给服务器
 *
 * @Date: 2020/7/4 17:10
 * @Author: swotXu
 */
@Slf4j
@Component
public class MyHealthCheckHandler implements HealthCheckHandler {

    @Autowired
    private MyHealthIndicator indicator;

    @Override
    public InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus instanceStatus) {
        Status status = indicator.health().getStatus();
        log.info("数据库连接是否正常: {}", status);

        return Status.UP.equals(status)?
                InstanceInfo.InstanceStatus.UP : InstanceInfo.InstanceStatus.DOWN;
    }
}

在自定义的健康检查处理器中,注入了前面编写的健康指示器,根据健康指示器的结果来返回不同的状态。

Eureka中会启动一个定时器,定时刷新本地实例的信息,并且执行“处理器”中的 getStatus 方法,再将服务实例的状态“更新”到服务器中。定时器默认 30 秒执行一次。

我们这里为了加快看到效果,可以修改 eureka.client.instance-info-replication-interval-seconds 配置。

修改服务提供者的 application.yml 配置如下:

spring:
  application:
    name: health-handler-provider
eureka:
  instance:
    hostname: localhost
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka
    instance-info-replication-interval-seconds: 10  # 改为每10秒向服务器提交一次健康信息

3.4 测试健康检查处理器

  1. 我们启动服务器,再启动服务提供者,在浏览器中访问 http://localhost:8761/ ,可以看到服务提供者的状态为 DOWN
    在这里插入图片描述
  2. 然后,我们访问 http://localhost:8080/db/true ,将数据库设置为“可以连接”状态。
    在这里插入图片描述
  3. 访问 http://localhost:8080/actuator/health ,检查是否设置成功。
    在这里插入图片描述
  4. 最后,等待10s以后,再访问 8761 端口,可以看到服务提供者的状态已经变为 UP
    在这里插入图片描述

4、服务查询

在上面我们讲了,通过浏览器访问 Eureka 界面可查看服务状态的改变。接下来,我们通过修改服务调用者的代码来查看应用健康自检的效果。

4.1、修改服务调用者

修改服务调用者(health-handler-invoker 模块),添加控制器,代码如下:

package com.swotxu.hhinvoker;

import com.netflix.appinfo.InstanceInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.eureka.EurekaServiceInstance;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * @Date: 2020/7/4 17:48
 * @Author: swotXu
 */
@RestController
@Configuration
public class HealthController {

    @Autowired
    private DiscoveryClient discoveryClient;

    @RequestMapping(value = "/health", method = RequestMethod.GET)
    public String health() {
        StringBuilder sb = new StringBuilder();
        // 查询服务列表
        List<ServiceInstance> instanceList = getServiceInstances();
        // 输出服务信息及状态
        for (ServiceInstance service : instanceList) {
            EurekaServiceInstance esi = (EurekaServiceInstance) service;
            InstanceInfo info = esi.getInstanceInfo();

            sb.append(String.format("AppName: %s ---- InstanceId: %s ---- Status: %s </br>"
                    , info.getAppName(), info.getInstanceId(), info.getStatus()));
        }
        return sb.toString();
    }

    /**
     * 查询可以服务
     * @return
     */
    private List<ServiceInstance> getServiceInstances() {
        List<ServiceInstance> result = new ArrayList<>();

        List<String> idList = discoveryClient.getServices();
        for (String id : idList) {
            List<ServiceInstance> instances = discoveryClient.getInstances(id);
            result.addAll(instances);
        }
        return result;
    }
}

在客户端中,如果需要查询集群中的服务,可以使用 Spring Cloud 的 DiscoveryClient 类,或者 Eureka 的 EurekaClient 类,Spring Cloud 对 Eureka 进行了封装。

本例中调用了 DiscoveryClient 类的方法来查询服务实例。在控制器的 health 方法中,仅将查询到的服务实例进行输出。

4.2、测试服务健康状态
  1. 运行服务器。
  2. 运行服务提供者(数据库连接状态默认为DOWN)。
  3. 运行服务调用者。
  4. 在浏览器中输入http://localhost:9000/health,可以看到只有服务调用者的信息。
    在这里插入图片描述
  5. 在浏览器中输入http://localhost:8080/db/true,将数据库设置为可以连接。
  6. 在浏览器中输入http://localhost:9000/health,可以看到此时出现了服务提供者的信息。
    在这里插入图片描述

根据输出结果可知,将数据库设置为不可连接后,可用的服务只剩下调用者自己,服务提供者已经不存在于服务列表中。

运行案例需要注意,默认情况下,客户端到服务器端抓取注册表会有一定的时间间隔,因此在设置数据库是否可以连接后,访问“调用者”查看效果时需要稍等一会儿。

二、Eureka 的常用配置

这里将介绍部分 Eureka 的常用配置。

1、心跳检测配置

客户端的实例会向服务器发送周期性的心跳,默认是 30 秒发送一次。

我们可以通过修改客户端的 eureka.instance.lease-renewal-interval-in-seconds 属性来改变这个时间。

服务器端接收心跳请求,如果在一定期限内没有接收到服务实例的心跳,那么会将该实例从注册表中清理掉,其他的客户端将会无法访问这个实例。这个期限默认值为 90 秒。
也就是说,服务器 90 秒没有收到客户端的心跳,就会将这个实例从列表中清理掉。

我们可以通过修改客户端的 eureka.instance.lease-expiration-duration-in-seconds 属性来改变这个时间。

要注意的是,清理注册表有一个定时器在执行,默认是 60 秒执一次,如果将 lease-expiration-duration-in-seconds 设置为小于 60 秒,虽然符合删除实例的条件,但是还没到 60 秒,这个实例将仍然存在注册表中 (因为还没有执行清理)。

我们可以在服务器端配置 eureka.server.eviction-interval-timer-in-ms 属性来修改注册表的清理间隔,注意该单位是毫秒。

需要特别注意,如果开启了自我保护模式,则实例不会被剔除。

在测试时,为避免受自我保护模式的影响,建议先关闭自我保护模式,在服务器中配置:eureka.server.enable-self-preservation = false

2、注册表抓取间隔

在默认情况下,客户端每隔 30 秒去服务器端抓取注册表(可用的服务列表),并且将服务器端的注册表保存到本地缓存中。

我们可以通过修改客户端的 eureka.client.registry-fetch-interval-seconds 属性来改变注册表抓取间隔,但仍然需要考虑性能,改为哪个值比较合适,需要在性能与实时性方面进行权衡。

3、配置与使用元数据

元数据都会保存在服务器的注册表中,并且使用简单的方式与客户端进行共享。框架自带的元数据,包括实例 id、主机名称、IP 地址等。
在正常情况下,自定义元数据不会改变客户端的行为,除非客户端知道这些元数据的含义。

如果我们需要自定义元数据并提供给其他客户端使用,可以配置 eureka.instance.metadata-map 属性来指定。

以下配置片段使用了元数据:

eureka:
  instance:
    hostname: localhost
    metadata-map:
      developer-name: swotxu

配置了一个名为 developer-name 的元数据,值为 swotxu,使用元数据的一方,可以调用 DiscoveryClient 的方法获取元数据,代码如下:

@Autowired
private DiscoveryClient discoveryClient;

@RequestMapping(value = "/metadata", method = RequestMethod.GET)
public String metadata() {
    StringBuilder sb = new StringBuilder();

    List<ServiceInstance> instanceList = discoveryClient.getInstances("HEALTH-HANDLER-INVOKER");
    for (ServiceInstance service : instanceList) {
        sb.append("developer-name: ").append(service.getMetadata().get("developer-name")).append("</br>");
    }
    return sb.toString();
}

在这里插入图片描述
使用方式较为简单,就不再赘述。

4、自我保护模式

在开发过程中 ,经常可以在 Eureka 的主界面中看到红色字体的提醒,内容如下:

EMERGENCY! 
EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. 
RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

在这里插入图片描述

出现该提示意味着 Eureka 进入了自我保护模式。根据前面章节的介绍可知,客户端会定时发送心跳给服务器端,如果心跳的失败率超过一定比例,服务会将这些实例保护起来,并不会马上将其从注册表中剔除。
此时对于另外的客户端来说,有可能会拿到一些无法使用的实例,这种情况可能会导致灾难的“蔓延”,这些情况可以使用容错机制予以解决,关于集群的容错机制,我们将在后面的内容中讲述。

在开发过程中,为服务器配置 eureka.server.enable-self-preservation 属性,将值设置为 false 来关闭自我保护机制。

关闭后再打开 Eureka 主界面,可以看到以下提示信息:

THE SELF PRESERVATION MODE IS TURNED OFF. 
THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.

自我保护模式己经关闭,在出现网络或者其他问题时,将不会保护过期的实例。

在这里插入图片描述

三、项目下载

码云Gitee仓库地址:https://gitee.com/swotxu/Spring-Cloud-Study.git >>戳这里<<
项目路径:Spring-Cloud-Study/01/springcloud03

四、Eureka 服务小结

通过这三篇的 Eureka 介绍,大家应该可以掌握 Eureka 的架构、如何使用 Eureka 搭建集群等内容,最基本的是,能对 Spring Cloud 等技术有一个初步认识。

如果想更进一步,可以学习编写服务实例的自检程序。实际环境中的服务不可迦免地依赖第三方环境,例如数据库、第三方服务等,因此,如果掌握了对程序的自检,可以使得我们的应用程序更加健壮。

下一篇,将介绍 String Cloud 中的负载均衡组件 Ribbon


持续更新中,别忘了点赞关注收藏~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章