服务治理

服务注册

在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。当服务启动后,会向注册中心注册自己的服务,那么注册中心就会有一个服务清单。另外,服务注册中心还需要以心跳的方式去监测清单中的服务是否可用,若不可用需要从服务清单中剔除,达到排除故障服务的效果。

服务发现

由于在服务治理框架下运作,服务间的调用不再通过指定具体的实例地址来实现,而是通过向服务名发起请求调用实现。所以,服务调用方在调用服务提供方接口时候,并不知道具体的服务实例位置。因此,调用方需要向服务注册中心咨询服务,并获取所有服务的实例清单,以实现对具体服务实例的访问。框架为了性能等因素,不会采用每次都向服务注册中心获取服务方式,并且不同的应用场景在缓存和服务剔除等机制上也会有一些不同的实现策略。

服务治理机制

服务治理是如何运作的?

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

服务提供者

服务注册

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

服务同步

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

服务续约

在注册完服务之后,服务提供者会维护一个心跳用来持续告诉Eureka Server:“我还活着”,以防止 Eureka Server 的“剔除任务”将该服务实例从服务列表中排除出去,我们称该操作为服务续约(Renew)。

关于服务续约有两个重要属性,我们可以关注并根据需要来进行调整:
eureka.instance.lease-renewal-interval-in-seconds=30 // 定义服务续 约 任 务 的 调 用 间 隔 时 间 , 默 认 为 30 秒
eureka.instance.lease-expiration-duration-in-seconds=90 // 定义服务失效的时间,默认为90秒

服务消费者

获取服务

到这里,在服务注册中心已经注册了一个服务,并且该服务有两个实例。当我们启动服务消费者的时候,它会发送一个 REST 请求给服务注册中心,来获取上面注册的服务清单。为了性能考虑,EurekaServer会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新一次。

获取服务是服务消费者的基础,所以必须确保eureka.client.fetchregistry=true参数没有被修改成false,该值默认为true。

若希望修改缓存清 单 的 更 新 时 间 , 可 以 通 过 eureka.client.registry-fetch-intervalseconds=30参数进行修改,该参数默认值为30,单位为秒。

服务调用

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

服务下线

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

服务注册中心

失效剔除

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

自我保护

当我们在本地调试基于Eureka的程序时,基本上都会碰到这样一个问题,在服务注册中心的信息面板中出现类似下面的红色警告信息:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMINGINSTANCES ARE UP WHEN THEY’RE NOT.RENEWALS ARELESSER THAN THRESHOLD AND HENCE THE INSTANCES ARENOT BEING EXPIRED JUST TO BE SAFE.实际上,该警告就是触发了Eureka Server的自我保护机制。之前我们介绍过,服务注册到Eureka Server之后,会维护一个心跳连接,告诉Eureka Server自己还活着。Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server 会将当前的实例注册信息保护起来,让这些实例不会过期,尽可能保护这些注册信息。但是,在这段保护期间内实例若出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调用失败的情况,所以客户端必须要有容错机制,比如可以使用请求重试、断路器等机制。

源码分析

注册中心客户端主类配置@EnableDiscoveryClient

org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient实现了org.springframework.cloud.client.discovery.DiscoveryClient接口

EurekaDiscoveryClient是对org.springframework.cloud.client.discovery.DiscoveryClient接口的实现,是实现对Eureka发现服务的封装,该实现类依赖了com.netflix.discovery.EurekaClient接口,EurekaClient接口继承了LookupService接口。

如何获取Eureka服务地址列表

DiscoveryClient方法com.netflix.discovery.DiscoveryClient#getDiscoveryServiceUrls —> com.netflix.discovery.endpoint.EndpointUtils#getServiceUrlsFromConfig

public static List getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {List orderedUrls = new ArrayList();String region = getRegion(clientConfig);String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());if (availZones == null || availZones.length == 0) {availZones = new String[1];availZones[0] = DEFAULT_ZONE;}logger.debug("The availability zone for the given region {} are {}", region, availZones);int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);List serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);if (serviceUrls != null) {orderedUrls.addAll(serviceUrls);}int currentOffset = myZoneOffset == (availZones.length - 1) " />

调用真正获取ServiceUrls方法为:getEurekaServerServiceUrls,在EurekaClientConfigBean类的方法org.springframework.cloud.netflix.eureka.EurekaClientConfigBean#getEurekaServerServiceUrls实现:

public List getEurekaServerServiceUrls(String myZone) {String serviceUrls = this.serviceUrl.get(myZone);if (serviceUrls == null || serviceUrls.isEmpty()) {serviceUrls = this.serviceUrl.get(DEFAULT_ZONE);}if (!StringUtils.isEmpty(serviceUrls)) {final String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);List eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length);for (String eurekaServiceUrl : serviceUrlsSplit) {if (!endsWithSlash(eurekaServiceUrl)) {eurekaServiceUrl += "/";}eurekaServiceUrls.add(eurekaServiceUrl.trim());}return eurekaServiceUrls;}return new ArrayList();}

服务注册

DiscoveryClient类构造函数,com.netflix.discovery.DiscoveryClient#DiscoveryClient(com.netflix.appinfo.ApplicationInfoManager, com.netflix.discovery.EurekaClientConfig, com.netflix.discovery.AbstractDiscoveryClientOptionalArgs, javax.inject.Provider, com.netflix.discovery.shared.resolver.EndpointRandomizer) 中调用了com.netflix.discovery.DiscoveryClient#initScheduledTasks方法,主要实现:

com.netflix.discovery.DiscoveryClient#initScheduledTaskscom.netflix.discovery.DiscoveryClient#initScheduledTasksInstanceInfoReplicatorprivate void initScheduledTasks() {if (clientConfig.shouldFetchRegistry()) {// registry cache refresh timerint 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 timerheartbeatTask = new TimedSupervisorTask("heartbeat",scheduler,heartbeatExecutor,renewalIntervalInSecs,TimeUnit.SECONDS,expBackOffBound,new HeartbeatThread());scheduler.schedule(heartbeatTask,renewalIntervalInSecs, TimeUnit.SECONDS);// InstanceInfo replicatorinstanceInfoReplicator = new InstanceInfoReplicator(this,instanceInfo,clientConfig.getInstanceInfoReplicationIntervalSeconds(),2); // burstSizestatusChangeListener = new ApplicationInfoManager.StatusChangeListener() {@Overridepublic String getId() {return "statusChangeListener";}@Overridepublic void notify(StatusChangeEvent statusChangeEvent) {if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {// log at warn level if DOWN was involvedlogger.warn("Saw local status change event {}", statusChangeEvent);} else {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判断,第一个是shouldFetchRegistry是否去Eureka服务获取eureka注册信息,第二个shouldRegisterWithEureka是否去注册eureka信息以便让其他实例发现。此处是服务注册,关注第二个if里面实现内容,发现有个InstanceInfoReplicator,进去发现是个实现Runnable接口的类,主要看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);}}

主要实现com.netflix.discovery.DiscoveryClient#register

/** * Register with the eureka service by making the appropriate REST call. */boolean register() throws Throwable {logger.info(PREFIX + "{}: registering service...", appPathIdentifier);EurekaHttpResponse 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();}

此处便是eureka服务注册的核心,可发现注册操作是通过REST请求方式进行的

顺便说明:

RESTful是HTTP接口调用的一种特殊实现,遵循REST架构风格的规范,能够提供更加标准化、统一化、可读性和易用性的API设计。RESTful调用相对于HTTP接口调用来说,具有更加清晰明了、易于理解和维护的API设计,扩展性和灵活性也更强。

服务获取与服务续约

com.netflix.discovery.DiscoveryClient#initScheduledTasks方法中可看出,有两个定时任务,一个是服务获取 和 服务续约

// 服务获取cacheRefreshTask = new TimedSupervisorTask("cacheRefresh",scheduler,cacheRefreshExecutor,registryFetchIntervalSeconds,TimeUnit.SECONDS,expBackOffBound,new CacheRefreshThread()); scheduler.schedule(cacheRefreshTask,registryFetchIntervalSeconds, TimeUnit.SECONDS); // 服务续约 heartbeatTask = new TimedSupervisorTask("heartbeat",scheduler,heartbeatExecutor,renewalIntervalInSecs,TimeUnit.SECONDS,expBackOffBound,new HeartbeatThread());scheduler.schedule(heartbeatTask,renewalIntervalInSecs, TimeUnit.SECONDS);

服务获取主要实现:com.netflix.discovery.DiscoveryClient.CacheRefreshThread

class CacheRefreshThread implements Runnable {public void run() {refreshRegistry();}}@VisibleForTestingvoid refreshRegistry() {try {boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries();boolean remoteRegionsModified = false;// This makes sure that a dynamic change to remote regions to fetch is honored.String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions();if (null != latestRemoteRegions) {String currentRemoteRegions = remoteRegionsToFetch.get();if (!latestRemoteRegions.equals(currentRemoteRegions)) {// Both remoteRegionsToFetch and AzToRegionMapper.regionsToFetch need to be in syncsynchronized (instanceRegionChecker.getAzToRegionMapper()) {if (remoteRegionsToFetch.compareAndSet(currentRemoteRegions, latestRemoteRegions)) {String[] remoteRegions = latestRemoteRegions.split(",");remoteRegionsRef.set(remoteRegions);instanceRegionChecker.getAzToRegionMapper().setRegionsToFetch(remoteRegions);remoteRegionsModified = true;} else {logger.info("Remote regions to fetch modified concurrently," +" ignoring change from {} to {}", currentRemoteRegions, latestRemoteRegions);}}} else {// Just refresh mapping to reflect any DNS/Property changeinstanceRegionChecker.getAzToRegionMapper().refreshMapping();}}boolean success = fetchRegistry(remoteRegionsModified);if (success) {registrySize = localRegionApps.get().size();lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();}if (logger.isDebugEnabled()) {StringBuilder allAppsHashCodes = new StringBuilder();allAppsHashCodes.append("Local region apps hashcode: ");allAppsHashCodes.append(localRegionApps.get().getAppsHashCode());allAppsHashCodes.append(", is fetching remote regions? ");allAppsHashCodes.append(isFetchingRemoteRegionRegistries);for (Map.Entry entry : remoteRegionVsApps.entrySet()) {allAppsHashCodes.append(", Remote region: ");allAppsHashCodes.append(entry.getKey());allAppsHashCodes.append(" , apps hashcode: ");allAppsHashCodes.append(entry.getValue().getAppsHashCode());}logger.debug("Completed cache refresh task for discovery. All Apps hash code is {} ",allAppsHashCodes);}} catch (Throwable e) {logger.error("Cannot fetch registry from server", e);}}

服务续约主要实现:com.netflix.discovery.DiscoveryClient.HeartbeatThread

private class HeartbeatThread implements Runnable {public void run() {if (renew()) {lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();}}}/** * Renew with the eureka service by making the appropriate REST call */boolean renew() {EurekaHttpResponse httpResponse;try {httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {REREGISTER_COUNTER.increment();logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());long timestamp = instanceInfo.setIsDirtyWithTime();boolean success = register();if (success) {instanceInfo.unsetIsDirty(timestamp);}return success;}return httpResponse.getStatusCode() == Status.OK.getStatusCode();} catch (Throwable e) {logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);return false;}}

服务续约只是发送了一个REST请求

服务注册中心处理

服务注册

/** * Registers information about a particular instance for an * {@link com.netflix.discovery.shared.Application}. * * @param info *{@link InstanceInfo} information of the instance. * @param isReplication *a header parameter containing information whether this is *replicated from other nodes. */@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 fieldsif (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 dataDataCenterInfo 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(com.netflix.appinfo.InstanceInfo, boolean)方法

@Overridepublic void register(final InstanceInfo info, final boolean isReplication) {handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);super.register(info, isReplication);}
private void handleRegistration(InstanceInfo info, int leaseDuration,boolean isReplication) {log("register " + info.getAppName() + ", vip " + info.getVIPAddress()+ ", leaseDuration " + leaseDuration + ", isReplication "+ isReplication);publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration,isReplication));}

com.netflix.eureka.registry.AbstractInstanceRegistry#register

ConcurrentHashMap/** * Registers a new instance with a given duration. * * @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean) */public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {try {read.lock();Map<String, Lease> gMap = registry.get(registrant.getAppName());REGISTER.increment(isReplication);if (gMap == null) {final ConcurrentHashMap<String, Lease> gNewMap = new ConcurrentHashMap<String, Lease>();gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);if (gMap == null) {gMap = gNewMap;}}Lease existingLease = gMap.get(registrant.getId());// Retain the last dirty timestamp without overwriting it, if there is already a leaseif (existingLease != null && (existingLease.getHolder() != null)) {Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);// this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted// InstanceInfo instead of the server local copy.if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");registrant = existingLease.getHolder();}} else {// The lease does not exist and hence it is a new registrationsynchronized (lock) {if (this.expectedNumberOfClientsSendingRenews > 0) {// Since the client wants to register it, increase the number of clients sending renewsthis.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;updateRenewsPerMinThreshold();}}logger.debug("No previous lease information found; it is new registration");}Lease lease = new Lease(registrant, leaseDuration);if (existingLease != null) {lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());}gMap.put(registrant.getId(), lease);recentRegisteredQueue.add(new Pair(System.currentTimeMillis(),registrant.getAppName() + "(" + registrant.getId() + ")"));// This is where the initial state transfer of overridden status happensif (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "+ "overrides", registrant.getOverriddenStatus(), registrant.getId());if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {logger.info("Not found overridden id {} and hence adding it", registrant.getId());overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());}}InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());if (overriddenStatusFromMap != null) {logger.info("Storing overridden status {} from map", overriddenStatusFromMap);registrant.setOverriddenStatus(overriddenStatusFromMap);}// Set the status based on the overridden status rulesInstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);registrant.setStatusWithoutDirty(overriddenInstanceStatus);// If the lease is registered with UP status, set lease service up timestampif (InstanceStatus.UP.equals(registrant.getStatus())) {lease.serviceUp();}registrant.setActionType(ActionType.ADDED);recentlyChangedQueue.add(new RecentlyChangedItem(lease));registrant.setLastUpdatedTimestamp();invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());logger.info("Registered instance {}/{} with status {} (replication={})",registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);} finally {read.unlock();}}

可发现是先将事件传播出去(通过Spring的事件监听方式),再调用父类中(com.netflix.eureka.registry.AbstractInstanceRegistry#register)的注册实现。

通过源码发现实例元数据信息室存储在一个ConcurrentHashMap中

private final ConcurrentHashMap<String, Map> registry = new ConcurrentHashMap<String, Map>();

第一层key为:存储服务名 (registrant.getAppName())

第二层key为:存储实例名(registrant.getId())

参考:

Spring Cloud微服务实战