实战监听 Eureka client 的缓存更新

语言: CN / TW / HK

欢迎访问我的 GitHub

这里分类和汇总了欣宸的全部原创(含配套源码): https://github.com/zq2599/blog_demos

从 Eureka server 获取服务列表

  • Spring cloud 环境中的应用,如果注册到 Eureka server,就会从 Eureka server 获取所有应用的注册信息(也叫服务列表),然后保存到本地,这个操作是周期性的,默认每三十秒一次;

  • 以下是来自官方的架构图,可以看到 Application Service 向 Eureka Server 有 Get Registry 的请求:

参考文章

  • 如果您有兴趣,想深入了解 spring 广播机制 或者 Eureka client 更新服务列表 ,推荐您参考以下两篇文章:

  1. 《spring4.1.8扩展实战之三:广播与监听 》

  2. 《Spring Cloud 源码分析之 Eureka 篇第五章:更新服务列表 》

实战内容

  • 本文是一篇实战的文章,实战内容如下:

  1. 启动 Eureka server;

  2. 开发一个应用 springcloudcustomizelistener,启动后会注册到 Eureka server;

  3. 此时该应用身份为 Eureka client,会周期性的从 Eureka server 获取服务列表(已有逻辑);

  4. 每次成功获取的服务列表成功都会存入本地缓存(已有逻辑);

  5. 存入缓存后,会在 spring 容器内发送广播(已有逻辑);

  6. 本次实战的重点就是自定义一个监听器来接收上述广播,收到广播后把详情用日志打印出来;

  7. 再启动另一个应用 springclouddeepprovider,也会注册到 Eureka server;

  8. 再去观察 springcloudcustomizelistener 的广播监听日志,会发现 springclouddeepprovider 的注册信息;

Eureka client 缓存服务列表的源码简介

  • 实战前,先对 Eureka client 缓存服务列表的实现源码做个简介,这样才能做出匹配的监听器;

  • 应用作为 Eureka Client 的启动时,在 com.netflix.discovery.DiscoveryClient 类的 initScheduledTasks 方法中,会启动周期性任务,每隔 30 秒从 Eureka server 获取服务列表信息,如下图,红框中的 TimedSupervisorTask 负责周期性执行,绿框中的 CacheRefreshThread 负责具体的更新逻辑:

  • 在 CacheRefreshThread 类中经过层层调用,获取服务列表并更新本地缓存的逻辑在 fetchRegistry 方法中实现,如下图,红框中的 getAndStoreFullRegistry 方法负责全量更新,绿框中的 getAndUpdateDelta 方法负责增量更新,黄框中的 onCacheRefreshed 方法就是今天的重点:发送广播,广播类型是 服务列表的本地缓存已更新

  • onCacheRefreshed 方法在子类 CloudEurekaClient 中被重写,可见这里发送了一个普通的 spring 容器内广播,类型是 HeartbeatEvent,我们可以自定义监听类来接收广播,并通过泛型规定只接受 HeartbeatEvent 类型:

    @Override    protected void onCacheRefreshed() {        if (this.cacheRefreshedCount != null) { //might be called during construction and will be null            long newCount = this.cacheRefreshedCount.incrementAndGet();            log.trace("onCacheRefreshed called with count: " + newCount);            //spring容器内广播,HeartbeatEvent实例在创建时收到两个参数:CloudEurekaClient实例和缓存刷新次数            this.publisher.publishEvent(new HeartbeatEvent(this, newCount));        }    }

复制代码

实战应用设定

  • 本次实战要搭建一个小的 Spring Cloud 环境,包括以下应用:

源码下载

  • springclouddeepeureka 和 springclouddeepprovider 这两个应用,在文章《Spring Cloud 源码分析之 Eureka 篇第一章:准备工作》中已有详细介绍,本文中就不多说了,您可以参考文章,也可以在 github 下载这两个应用的源码,地址和链接信息如下表所示:

  • 这个 git 项目中有多个文件夹,本章源码分别在 springclouddeepeureka、springclouddeepprovider 这两个文件夹下,如下图红框所示:

启动 springclouddeepeureka

  • 应用 springclouddeepeureka 开发完成后就立即启动,在浏览器访问地址:http://localhost:8081,可见 Eureka server 已经启动,不过还没有任何应用注册上来,如下图:

开发 springcloudcustomizelistener

  • 接下来一起开发应用 springcloudcustomizelistener,在此应用中添加自定义的 spring 广播监听器,如果您不想敲代码,也可以从 github 上直接下载源码,地址和链接信息如下表所示:

  • 这个 git 项目中有多个文件夹,本章源码在 springcloudcustomizelistener 文件夹下,如下图红框所示:

  • 一起来开发吧:

  • 创建一个 springboot 的 web 应用,pom.xml 内容如下,注意为了在日志中展示更详细的内容,依赖了 fastjson 库:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>
<groupId>com.bolingcavalry</groupId> <artifactId>springcloudcustomizelistener</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging>
<name>springcloudcustomizelistener</name> <description>Demo project for Spring Boot</description>
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> </parent>
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.28</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Edgware.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>

复制代码

  • 应用配置文件 application.yml 的内容如下:

server:  port: 8085spring:  application:    name: springcloud-customize-listenereureka:  client:    serviceUrl:      defaultZone: http://localhost:8081/eureka/  instance:    prefer-ip-address: true

复制代码

  • 创建监听器 EurekaCacheRefreshListener.java,前面已经分析过 HeartbeatEvent 实例的两个成员变量,值为缓存刷新次数和 CloudEurekaClient 实例,在收到广播时,将这两个成员变量都在日志中打印出来:

@Componentpublic class EurekaCacheRefreshListener implements ApplicationListener<HeartbeatEvent> {
private static final Logger logger = LoggerFactory.getLogger(EurekaCacheRefreshListener.class);
@Override public void onApplicationEvent(HeartbeatEvent event) { Object count = event.getValue(); Object source = event.getSource();
logger.info("start onApplicationEvent, count [{}], source :\n{}", count, JSON.toJSON(source)); }}

复制代码

  • 启动应用,等待大约 30 秒左右,EurekaCacheRefreshListener 中的日志就会在控制台输出,内容十分丰富,建议您复制这些内容去在线格式化 JSON 的网站做一下格式化再来看,下面列出部分关键信息,其他的内容已经略去:

{    "instanceRemoteStatus": "UP",    "lastSuccessfulHeartbeatTimePeriod": 316,    "allKnownRegions": [        "us-east-1"    ],    ...    此处省略部分内容    ...    "applications": {        "appsHashCode": "UP_1_",        "registeredApplications": [            {                "instances": [                    {                        "hostName": "192.168.31.104",                        "overriddenStatus": "UNKNOWN",                        "metadata": {                            "jmx.port": "64656",                            "management.port": "8085"                        },                        "statusPageUrl": "http://192.168.31.104:8085/info",                        "secureVipAddress": "springcloud-customize-listener",                        "leaseInfo": {                            "renewalIntervalInSecs": 30,                            "registrationTimestamp": 1537827838033,                            "evictionTimestamp": 0,                            "renewalTimestamp": 1537827987419,                            "durationInSecs": 90,                            "serviceUpTimestamp": 1537827837461                        },                        "homePageUrl": "http://192.168.31.104:8085/",                        "countryId": 1,                        "sID": "na",                        "securePort": 443,                        "dataCenterInfo": {                            "name": "MyOwn"                        },                        "instanceId": "DESKTOP-82CCEBN:springcloud-customize-listener:8085",                        "coordinatingDiscoveryServer": false,                        "id": "DESKTOP-82CCEBN:springcloud-customize-listener:8085",                        "vIPAddress": "springcloud-customize-listener",                        "dirty": false,                        "lastUpdatedTimestamp": 1537827838033,                        "healthCheckUrl": "http://192.168.31.104:8085/health",                        "appName": "SPRINGCLOUD-CUSTOMIZE-LISTENER",                        "lastDirtyTimestamp": 1537827837401,                        "iPAddr": "192.168.31.104",                        "version": "unknown",                        "actionType": "ADDED",                        "port": 8085,                        "healthCheckUrls": [                            "http://192.168.31.104:8085/health"                        ],                        "status": "UP"                    }                ]

复制代码

  • 如上所示,当前应用的实例信息在本地已经缓存了;

启动 springclouddeepprovider

  • 应用 springclouddeepprovider 是最后一个启动的应用,启动该应用后,再去观察 springcloudcustomizelistener 的日志,发现应用 springclouddeepprovider 的注册信息已经获取到了:

{                "instances": [                    {                        "hostName": "192.168.119.1",                        "overriddenStatus": "UNKNOWN",                        "metadata": {                            "jmx.port": "58420",                            "management.port": "8082"                        },                        "statusPageUrl": "http://192.168.119.1:8082/info",                        "secureVipAddress": "springcloud-deep-provider",                        "leaseInfo": {                            "renewalIntervalInSecs": 30,                            "registrationTimestamp": 1537840715486,                            "evictionTimestamp": 0,                            "renewalTimestamp": 1537840715486,                            "durationInSecs": 90,                            "serviceUpTimestamp": 1537840715486                        },                        "homePageUrl": "http://192.168.119.1:8082/",                        "countryId": 1,                        "sID": "na",                        "securePort": 443,                        "dataCenterInfo": {                            "name": "MyOwn"                        },                        "instanceId": "localhost:springcloud-deep-provider:8082",                        "coordinatingDiscoveryServer": false,                        "id": "localhost:springcloud-deep-provider:8082",                        "vIPAddress": "springcloud-deep-provider",                        "dirty": false,                        "lastUpdatedTimestamp": 1537840715486,                        "healthCheckUrl": "http://192.168.119.1:8082/health",                        "appName": "SPRINGCLOUD-DEEP-PROVIDER",                        "lastDirtyTimestamp": 1537840715451,                        "iPAddr": "192.168.119.1",                        "version": "unknown",                        "actionType": "ADDED",                        "port": 8082,                        "healthCheckUrls": [                            "http://192.168.119.1:8082/health"                        ],                        "status": "UP"                    }                ],                "name": "SPRINGCLOUD-DEEP-PROVIDER",                "instancesAsIsFromEureka": [                    {                        "hostName": "192.168.119.1",                        "overriddenStatus": "UNKNOWN",                        "metadata": {                            "jmx.port": "58420",                            "management.port": "8082"                        },                        "statusPageUrl": "http://192.168.119.1:8082/info",                        "secureVipAddress": "springcloud-deep-provider",                        "leaseInfo": {                            "renewalIntervalInSecs": 30,                            "registrationTimestamp": 1537840715486,                            "evictionTimestamp": 0,                            "renewalTimestamp": 1537840715486,                            "durationInSecs": 90,                            "serviceUpTimestamp": 1537840715486                        },                        "homePageUrl": "http://192.168.119.1:8082/",                        "countryId": 1,                        "sID": "na",                        "securePort": 443,                        "dataCenterInfo": {                            "name": "MyOwn"                        },                        "instanceId": "localhost:springcloud-deep-provider:8082",                        "coordinatingDiscoveryServer": false,                        "id": "localhost:springcloud-deep-provider:8082",                        "vIPAddress": "springcloud-deep-provider",                        "dirty": false,                        "lastUpdatedTimestamp": 1537840715486,                        "healthCheckUrl": "http://192.168.119.1:8082/health",                        "appName": "SPRINGCLOUD-DEEP-PROVIDER",                        "lastDirtyTimestamp": 1537840715451,                        "iPAddr": "192.168.119.1",                        "version": "unknown",                        "actionType": "ADDED",                        "port": 8082,                        "healthCheckUrls": [                            "http://192.168.119.1:8082/health"                        ],                        "status": "UP"                    }                ]            }

复制代码

  • 至此,本次实战就完成了,通过开发自定义的广播监听器,我们对 Eureka 的注册发现机制有了进一步了解,在您的 Spring Cloud 学习过程中,希望本文能祝您一臂之力;

欢迎关注 InfoQ:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...