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

  • 2020 年 3 月 27 日
  • 筆記

代码下载地址: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了,需要中心注册      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实例信息就介绍到这了,别看仅是一个信息类,内容还真不少。本文对每个属性均做了介绍,希望能作为字典可以查询,可以帮助到你。