[享学Eureka] 二、Eureka的最核心概念:InstanceInfo实例信息

程序有问题时不要担心,如果所有东西都没问题,你就失业了。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

如果说Eureka里最核心的一个对象/类是什么,我想当属InstanceInfo了。它贯穿于Client、Server俩端,承载着一个实例的所有描述,它是事件的主体,一切皆围绕着它来进行。

另外,还需注意的是Eureka使用的是Guice作为它的依赖注入DI基础组件,因此源码处你进场能看见@Singleton、@Inject等注解的使用,为了不妨碍你的阅读和研究,建议可先认识下Google Guice这个轻量级依赖注入的相关支持,这里我也很暖心的给你准备好了直达电梯:3分钟带你了解轻量级依赖注入框架Google Guice【享学Java】


正文

本文会逐个介绍InstanceInfo以及LeaseInfo的属性,最后辅以一个示例讲解。可以作为字典、参考文章来使用,建议搜藏or转发。


InstanceInfo 实例信息

InstanceInfo代表一个实例的信息,是一个趋近于POJO的类,因此理解起来并不难。但由于字段众多(30+个),因此先围绕它理解一圈,基本能窥探到注册中心的全貌,所以还是蛮重要的。


成员属性

// @ProvidedBy是Guice的注解,用于在Guice的DI依赖注入时生成一个InstanceInfo实例
@ProvidedBy(EurekaConfigBasedInstanceInfoProvider.class)
// 序列化/反序列化化使用定制的序列化器
@Serializer("com.netflix.discovery.converters.EntityBodyConverter")
// 支持xml的序列化/反序列化  它俩都用instance前缀包裹着。形如{"instance" : {...}} 才行
@XStreamAlias("instance") 
// 支持json的序列化/反序列化
@JsonRootName("instance") 
public class InstanceInfo {

    private volatile String instanceId;
    private volatile String appName;
    @Auto
    private volatile String appGroupName;
    private volatile String ipAddr;
    @Deprecated
    private volatile String sid = SID_DEFAULT;
    private volatile int port = DEFAULT_PORT;
    private volatile int securePort = DEFAULT_SECURE_PORT;
    @Auto
    private volatile String homePageUrl;
    @Auto
    private volatile String statusPageUrl;
    @Auto
    private volatile String healthCheckUrl;
    @Auto
    private volatile String secureHealthCheckUrl;
    @Auto
    private volatile String vipAddress;
    @Auto
    private volatile String secureVipAddress;
    @XStreamOmitField
    private String statusPageRelativeUrl;
    @XStreamOmitField
    private String statusPageExplicitUrl;
    @XStreamOmitField
    private String healthCheckRelativeUrl;
    @XStreamOmitField
    private String healthCheckSecureExplicitUrl;
    @XStreamOmitField
    private String vipAddressUnresolved;
    @XStreamOmitField
    private String secureVipAddressUnresolved;
    @XStreamOmitField
    private String healthCheckExplicitUrl;
    @Deprecated
    private volatile int countryId = DEFAULT_COUNTRY_ID; // Defaults to US
    private volatile boolean isSecurePortEnabled = false;
    private volatile boolean isUnsecurePortEnabled = true;
    private volatile DataCenterInfo dataCenterInfo;
    private volatile String hostName;
    private volatile InstanceStatus status = InstanceStatus.UP;
    private volatile InstanceStatus overriddenStatus = InstanceStatus.UNKNOWN;
    @XStreamOmitField
    private volatile boolean isInstanceInfoDirty = false;
    private volatile LeaseInfo leaseInfo;
    @Auto
    private volatile Boolean isCoordinatingDiscoveryServer = Boolean.FALSE;
    @XStreamAlias("metadata")
    private volatile Map<String, String> metadata;
    @Auto
    private volatile Long lastUpdatedTimestamp;
    @Auto
    private volatile Long lastDirtyTimestamp;
    @Auto
    private volatile ActionType actionType;
    @Auto
    private volatile String asgName;
    private String version = VERSION_UNKNOWN;

}

需要强调一点:在Guice下注入该实例时由EurekaConfigBasedInstanceInfoProvider负责创建;但是在Spring Cloud下该实例由自己提供的InstanceInfoFactory完成创建的。

Spring Cloud下完全没有使用Guice来管理依赖,而是自己实现的管理,毕竟它也支持@Inject等标准注解嘛,接手过来比较容易

  • instanceId:实例id。在同一个应用appName的范围内是必须是惟一的
    • 你常见的在Spring Cloud的配置是:eureka.instance.instance-id = ${spring.cloud.client.ipAddress}:${spring.application.name}:${server.port}:@project.version@
      • 在eureka项目本身的配置是:eureka.instanceId = xxxxxxx,请注意区别
      • 此处project.version是引用maven里面的属性,因为Spring Boot的parent包将maven中默认的${*}修改成了@*@,所以引用maven属性要用@@
  • appName:应用名。如ACCOUNT(同一应用可以有N多个实例)
    • 此属性最终会被序列化app这个key,如app=ACCOUNT
  • appGroupName:应用组名。多个应用可分组,很少用,一般为null
  • ipAddr:本实例的ip地址。如ipAddr=192.168.1.100
  • sid:已过期属性。不用搭理
  • port:端口号。默认值是7001
  • securePort:安全端口号。默认值是7002
  • homePageUrl:主页。如homePageUrl=http://localhost:8080
    • 一般使用占位符形式配置xxx.homePageUrl = http://${mynamespace.hostname}:7001
    • 这个占位符在运行期会被用hostname属性替换掉,而hostname属于必配的属性
  • statusPageUrl:状态页。如http://localhost:8080/actuator/info
    • 同上,一般也使用占位符形式
  • healthCheckUrl:健康检查的URL(Rest)。如http://localhost:8080/actuator/health
  • secureHealthCheckUrl:一般不用,为null即可。
  • vipAddress:逻辑地址。如vipAddress=ACCOUNT
    • 关于它在eureka中如何使用,会有详解
  • secureVipAddress:略
  • statusPageRelativeUrl:相对URL。最终会拼接全了给statusPageUrl赋值,如你/api/v1/status最终给statusPageUrl赋值为(举个例子):http://localhost:8080//api/v1/status
  • statusPageExplicitUrl:明确的URL。最终处理后给statusPageUrl赋值。如你配置了${mynamespace.hostname}/api/v1/status,那么最终占位符部分会被替换为hostname的值,从而效果同上
  • healthCheckRelativeUrl:略
  • healthCheckSecureExplicitUrl:略
  • vipAddressUnresolved:略
  • secureVipAddressUnresolved:略
  • healthCheckExplicitUrl:略
  • countryId:过期。国家ID,默认值是1表示美国,不用搭理
  • isSecurePortEnabled:是否启用安全端口,securePort有值就是true,显然默认是false
  • isUnsecurePortEnabled:默认是true
  • dataCenterInfo:数据中心。
    • 关于数据中心的概念,后文也会有详细分析其使用
  • hostName:必须的。主机名,如hostName=LP-BJ4556.baidu.work
    • 说明:Spring Cloud下的服务注册实例id默认使用主机名,而非ip地址,毕竟http://hostname:port这种方式也是可以访问的接口的(当然这个可配)
  • status:实例状态。默认值是InstanceStatus.UP,是个枚举
    • starting:实例初始化状态,此状态主要给实例预留初始化时间
    • down:当健康检查失败时,实例的状态转变到down
    • up:正常服务状态
    • out_of_service:不参与接收服务 。但是服务正常
    • unknown:未知状态
  • overriddenStatus:eureka解决状态覆盖而存在的属性字段。它的默认是是InstanceStatus.UNKNOWN
    • 状态覆盖也是ereuka里的一个小亮点,后有详细介绍
  • isInstanceInfoDirty:标注实例数据是否是脏的(client和server端对比)
    • true:表示 InstanceInfo 在 Eureka-Client 和 Eureka-Server 数据不一致,需要注册。
      • 每次 InstanceInfo 发生属性变化时,以及InstanceInfo 刚被创建时,会标记此值是true
      • 当符合条件时,InstanceInfo 不会立即向 Eureka-Server 注册,而是后台线程定时注册(当然若开启了eureka.shouldOnDemandUpdateStatusChange = true时是立即注册)
        • 该配置属于EurekaClientConfig端配置哦,不属于Instance配置
    • false:表示一致,不需要做额外动作
  • leaseInfo:一个com.netflix.appinfo.LeaseInfo对象,表示续约相关信息,见文下
  • isCoordinatingDiscoveryServer:是否是协调Server。默认是false,不用搭理
  • metadata自定义元数据,可以是任何k-v
    • 关于eureka的元数据还是比较重要的,后有专门详解
  • lastUpdatedTimestamp:上次修改时间
  • lastDirtyTimestamp:上次标记为Dirty的时间
  • actionType:动作类型。如ADDED/MODIFIED/DELETED
  • asgName: 与此实例相关联 AWS自动缩放组名称
    • 此项配置是在AWS环境专门使用的实例启动,它已被用于流量停用后自动把一个实例退出服务。实际可忽略
  • version:版本。默认值是unknown,已标记过期,可不用搭理

构建方式

任何对象都需要构建嘛,哪怕是自动的。可以想到这么多属性,那必然采取的就是Builder方式构建喽。其实它提供了全参数的构造器,但那不会使用~

若我们自己构建,肯定这么玩:

InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder()
        .setInstanceId("account-001")
        .setHostName("localhost")
        .setIPAddr("127.0.0.1")
        .setDataCenterInfo(new MyDataCenterInfo(DataCenterInfo.Name.MyOwn))
        .setAppName("account") // 大小写无所谓
        .build();

对于使用Builder构建也比较简单,但有如下方法特别指出,有一定小逻辑,可以稍加注意:

InstanceInfo.Builder
	
	// 命名空间  一个InstanceInfo一个命名空间
	// 默认的命名空间值是:eureka 这就是为何你的配置都是eureka.xxx的原因喽
	private String namespace;
    public Builder setNamespace(String namespace) {
        this.namespace = namespace.endsWith(".") ? namespace : namespace + ".";
        return this;
    }


	// 支持相对URL:explicitUrl 以及绝对URL:explicitUrl  
	// 相对URL会帮你拼接上http、端口等前缀
	// 绝对url就帮你处理下占位符即可(若无占位符就不处理喽)
    public Builder setHomePageUrl(String relativeUrl, String explicitUrl) {
        String hostNameInterpolationExpression = "${" + namespace + "hostname}";
        if (explicitUrl != null) {
            result.homePageUrl = explicitUrl.replace(
                    hostNameInterpolationExpression, result.hostName);
        } else if (relativeUrl != null) {
            result.homePageUrl = HTTP_PROTOCOL + result.hostName + COLON
                    + result.port + relativeUrl;
        }
        return this;
    }
    // 当然,你也可以来一个完全绝对的URL(并不太推荐)
    public Builder setHomePageUrlForDeser(String homePageUrl) {
        result.homePageUrl = homePageUrl;
        return this;
    }

	... // 像什么setStatusPageUrl、setHealthCheckUrls等都支持这两种方式

	// appName应用名称是必须的。其实还有InstanceId hostName等也都是必须的
    public InstanceInfo build() {
        if (!isInitialized()) {
            throw new IllegalStateException("name is required!");
        }
        return result;
    }
    public boolean isInitialized() {
        return (result.appName != null);
    }

关于各URL的设值,推荐使用相对URL。另外,该实例的创建一般不通过手动显示各种肤质,而是通过配置的方式俩指定,具体可参见下篇文章EurekaInstanceConfig配置接口。


属性读取

同样的道理,亦仅需关注其一些特殊方法而已:

InstanceInfo:

	// key使用的是app
    @JsonProperty("app")
    public String getAppName() {
        return appName;
    }

	// 返回实例的唯一id。并不是直接返回instanceId哦
    @JsonIgnore
    public String getId() {
    	// 若你自己配置了instanceId就直接返回
        if (instanceId != null && !instanceId.isEmpty()) {
            return instanceId;
		
		// 否则取值字数据中心。
		// 说明:`MyDataCenterInfo`实现了UniqueIdentifier
		// AmazonInfo实现了此接口
        } else if (dataCenterInfo instanceof UniqueIdentifier) {
            String uniqueId = ((UniqueIdentifier) dataCenterInfo).getId();
            if (uniqueId != null && !uniqueId.isEmpty()) {
                return uniqueId;
            }
        }
        return hostName;
    }

    @JsonIgnore
    public boolean isPortEnabled(PortType type) {
        if (type == PortType.SECURE) {
            return isSecurePortEnabled;
        } else {
            return isUnsecurePortEnabled;
        }
    }

	// 只要状态Status发生了变更,那么一定会setIsDirty();
	// 标记此实例已经dirty了,需要中心注册
	// 特别需要强调的是:此方法若set成功了,是有返回值的。返回的是prev之前的状态哦~
	// 因此你可以通过返回值的是否为null俩判断InstanceInfo实例状态是否真的发生了变更
    public synchronized InstanceStatus setStatus(InstanceStatus status) {
        if (this.status != status) {
            InstanceStatus prev = this.status;
            this.status = status;
            setIsDirty();
            return prev;
        }
        return null;
    }
    // 当然也可以对dirty进行免疫
    public synchronized void setStatusWithoutDirty(InstanceStatus status) {
        if (this.status != status) {
            this.status = status;
        }
    }
    public synchronized void setOverriddenStatus(InstanceStatus status) {
        if (this.overriddenStatus != status) {
            this.overriddenStatus = status;
        }
    }

	//增加元数据信息时,也会标记dirty了
	// 它会被ApplicationInfoManager#registerAppMetadata调用
    synchronized void registerRuntimeMetadata(Map<String, String> runtimeMetadata) {
        metadata.putAll(runtimeMetadata);
        setIsDirty();
    }

另外,它还提供一个static的工具方法可以直接调用:获取该Info所在的可用区zone

InstanceInfo:
	
	// 获取到当前实例InstanceInfo所在的zone区   
	// availZones:可用区(若为空就是default,否则就取第一个)
	// 若实例使用的数据中心是Amazon类型,那就从其元数据里面拿出availabilityZone可用区(若配置了的话)
    public static String getZone(String[] availZones, InstanceInfo myInfo) {
        String instanceZone = ((availZones == null || availZones.length == 0) ? "default" : availZones[0]);
        if (myInfo != null && myInfo.getDataCenterInfo().getName() == DataCenterInfo.Name.Amazon) {

            String awsInstanceZone = ((AmazonInfo) myInfo.getDataCenterInfo())
                    .get(AmazonInfo.MetaDataKey.availabilityZone);
            if (awsInstanceZone != null) {
                instanceZone = awsInstanceZone;
            }

        }
        return instanceZone;
    }

逻辑总结为一句话:若你使用的是Amazon数据中心类型,那么你可以通过元数据availabilityZone来配置当前实例所在的zone(关于AmazonInfo的元数据后文也有所讲解)。否则它所在的zone就是方法入参或者是defualt


LeaseInfo 续租信息

续租信息。续租是Eureka里特别重要的一个概念,Eureka会决定根据此租约中的EurekaInstanceConfig.getLeaseExpirationDurationInSeconds()中设置的持续时间将实例从其视图中移除。租约还记录了上次续租的时间

// 它的JSON形式表示形式是:{"leaseInfo":{xxx}}
@JsonRootName("leaseInfo")
public class LeaseInfo {

	// 默认值们
    public static final int DEFAULT_LEASE_RENEWAL_INTERVAL = 30;
    public static final int DEFAULT_LEASE_DURATION = 90;
	
    private int renewalIntervalInSecs = DEFAULT_LEASE_RENEWAL_INTERVAL;
    private int durationInSecs = DEFAULT_LEASE_DURATION;

    private long registrationTimestamp;
    private long lastRenewalTimestamp;
    private long evictionTimestamp;
    private long serviceUpTimestamp;

}
  • renewalIntervalInSecs:续租间隔时间(多长时间续约一次),默认是30s。
    • 用于Client客户端:每隔30s上报续约一次
  • durationInSecs:续约持续时间(过期时间),默认是90s。90s倒计时,期间没有收到续约就会执行对应动作
    • 用于Server服务端,90s内木有收到心跳,就T除掉对应实例
  • registrationTimestamp:租约的注册时间
  • lastRenewalTimestamp:最近一次的续约时间(服务端记录,用于倒计时的起始值)
  • evictionTimestamp:下线时间(服务的上、下线属于比较频繁的操作。但是此时服务实例并未T除去)
  • serviceUpTimestamp:上线时间

代码示例

创建一个InstanceInfo这里就不手动Builder了,这里我使用Guice依赖注入的方式来得到一个实例:

@Test
public void fun5(){
    Injector injector = Guice.createInjector(new EurekaModule());

    InstanceInfo instance1 = injector.getInstance(InstanceInfo.class);
    InstanceInfo instance2= injector.getInstance(InstanceInfo.class);
    System.out.println(instance1.getId());
    System.out.println(instance2.getId());

    System.out.println(System.identityHashCode(instance1));
    System.out.println(System.identityHashCode(instance2));
}

说明,因为我没有配置数据中心,因此我需要加个配置eureka.validateInstanceId = false,这样运行程序,输出为:

2.0.0.2
2.0.0.2
1434234664
1434234664

what???竟然是单例。是的,InstanceInfo全局仅需要一个,毕竟你一个应用就是一个实例嘛,那么为何呢???

究其原因就在这里:

@ProvidedBy(EurekaConfigBasedInstanceInfoProvider.class)
public class InstanceInfo { ... }

@Singleton // 单例
public class EurekaConfigBasedInstanceInfoProvider implements Provider<InstanceInfo> { ... }

Spring Cloud下使用的是InstanceInfoFactory俩创建实例,然后交给ApplicationInfoManager去管理的~


总结

关于Eureka的最核心概念:InstanceInfo实例信息就介绍到这了,别看仅是一个信息类,内容还真不少。本文对每个属性均做了介绍,希望能作为字典可以查询,可以帮助到你。
分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了370 篇原创文章 · 获赞 548 · 访问量 49万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/105090695