首頁 > 軟體

Eureka原始碼解析服務離線狀態變更

2022-10-18 14:02:49

環境

1. 服務離線的方式

服務離線,即某服務不能對外提供服務了。服務離線的原因有兩種:服務下架與服務下線。

  • 服務下架:表示這個已經被kill掉了,不能對外提供服務,自己也不能存取
  • 服務下線:只是該服務不能被 eureka server端發現(不能註冊),不能被遠端存取,但是可以自己存取自己的服務

1.1 基於Actuator監控器實現

提交如下POST請求,可實現相應的服務離線操作:

  • 服務下架:http://localhost:埠號/actuator/shutdown 無需請求體
  • 服務下線:http://localhost:埠號/actuator/serviceregistry 請求體為(該方法稱為服務平滑上下 線)
{
    "status":"OUT_OF_SERVICE"  或 "UP"
}

注意,從Spring Cloud 2020.0.0版本開始,服務平滑上下線的監控終端由service-registry變更為 了serviceregistry

1.2 直接向Eureka Server提交請求

可以通過直接向Eureka Server提交不同的請求的方式來實現指定服務離線操作:

服務下架:通過向eureka server傳送DELETE請求來刪除指定client的服務

http://${server}:${port}/eureka/apps/${serviceName}/${instanceId}

服務下線:通過向eureka server傳送PUT請求來修改指定client的status,其中${value}的取值 為:OUT_OF_SERVICE或UP

http://${server}:${port}/eureka/apps/${serviceName}/${instanceId}/stat us?value=${value}

1.3 特殊狀態CANCEL_OVERRIDE

使用者提交的狀態修改請求中指定的狀態,除了InstanceInfo的內建列舉類InstanceStatus中定義的狀態 外,還可以是CANCEL_OVERRIDE狀態

若使用者提交的狀態為CANCEL_OVERRIDE,則Client會通過Jersey向Server提交一個DELETE請求,用於 在Server端將對應InstanceInfooverridenStatus修改為UNKNWON,即刪除了原來的overridenStatus 的狀態值。此時,該Client傳送的心跳Server是不接收的。Server會向該Client返回404

2. 服務下架原始碼

public class EurekaClientAutoConfiguration {
   @Configuration(proxyBeanMethods = false)
   @ConditionalOnRefreshScope
   protected static class RefreshableEurekaClientConfiguration {
      @Bean(destroyMethod = "shutdown")
      @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
      @org.springframework.cloud.context.config.annotation.RefreshScope
      @Lazy
      public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config,
            EurekaInstanceConfig instance, @Autowired(required = false) HealthCheckHandler healthCheckHandler) {
      }
  }
}

Actuator監聽到服務下架時,會呼叫DiscoveryClient.shutdown()方法:

// 服務下架
@PreDestroy
@Override
public synchronized void shutdown() {
    if (isShutdown.compareAndSet(false, true)) {
        logger.info("Shutting down DiscoveryClient ...");
        // 登出狀態改變監聽器
        if (statusChangeListener != null && applicationInfoManager != null) {
            applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
        }
        // todo 取消定時任務
        cancelScheduledTasks();
        // If APPINFO was registered
        if (applicationInfoManager != null
                && clientConfig.shouldRegisterWithEureka()
                && clientConfig.shouldUnregisterOnShutdown()) {
            applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
            // todo 服務下架
            unregister();
        }
        if (eurekaTransport != null) {
            eurekaTransport.shutdown();
        }
        heartbeatStalenessMonitor.shutdown();
        registryStalenessMonitor.shutdown();
        Monitors.unregisterObject(this);
        logger.info("Completed shut down of DiscoveryClient");
    }
}

有兩個核心方法,我們分別看一下。

2.1 cancelScheduledTasks()

取消定時任務。

private void cancelScheduledTasks() {
    if (instanceInfoReplicator != null) {
        instanceInfoReplicator.stop();
    }
    if (heartbeatExecutor != null) {
        heartbeatExecutor.shutdownNow();
    }
    if (cacheRefreshExecutor != null) {
        cacheRefreshExecutor.shutdownNow();
    }
    if (scheduler != null) {
        scheduler.shutdownNow();
    }
    if (cacheRefreshTask != null) {
        cacheRefreshTask.cancel();
    }
    if (heartbeatTask != null) {
        heartbeatTask.cancel();
    }
}

2.2 unregister()

傳送服務下架請求。

void unregister() {
    // It can be null if shouldRegisterWithEureka == false
    if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
        try {
            logger.info("Unregistering ...");
            EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
            logger.info(PREFIX + "{} - deregister  status: {}", appPathIdentifier, httpResponse.getStatusCode());
        } catch (Exception e) {
            logger.error(PREFIX + "{} - de-registration failed{}", appPathIdentifier, e.getMessage(), e);
        }
    }
}
@Override
public EurekaHttpResponse<Void> cancel(String appName, String id) {
    String urlPath = "apps/" + appName + '/' + id;
    ClientResponse response = null;
    try {
        Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
        addExtraHeaders(resourceBuilder);
        // delete 請求
        response = resourceBuilder.delete(ClientResponse.class);
        return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP DELETE {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}

服務下架請求:DELETE請求,path:"apps/" + appName + '/' + id;

3. 服務下線原始碼分析(狀態變更)

Eureka 整合了 Actuator ,可以通過 Actuator 變更範例在伺服器端的狀態。spring cloud整合eureka,入口在 spring-cloud-common下的spring.factories:

@Configuration(proxyBeanMethods = false)
public class ServiceRegistryAutoConfiguration {
   @ConditionalOnBean(ServiceRegistry.class)
   @ConditionalOnClass(Endpoint.class)
   protected class ServiceRegistryEndpointConfiguration {
      @Autowired(required = false)
      private Registration registration;
      @Bean
      @ConditionalOnAvailableEndpoint
      public ServiceRegistryEndpoint serviceRegistryEndpoint(ServiceRegistry serviceRegistry) {
         ServiceRegistryEndpoint endpoint = new ServiceRegistryEndpoint(serviceRegistry);
         endpoint.setRegistration(this.registration);
         return endpoint;
      }
   }
}

ServiceRegistryAutoConfiguration是一個設定類,往容器中注入ServiceRegistryEndpoint

@Endpoint(id = "serviceregistry")
public class ServiceRegistryEndpoint {
    ...
   @WriteOperation
   public ResponseEntity<?> setStatus(String status) {
      Assert.notNull(status, "status may not by null");
      if (this.registration == null) {
         return ResponseEntity.status(HttpStatus.NOT_FOUND).body("no registration found");
      }
      // 變更狀態
      this.serviceRegistry.setStatus(this.registration, status);
      return ResponseEntity.ok().build();
   }
   @ReadOperation
   public ResponseEntity getStatus() {
      if (this.registration == null) {
         return ResponseEntity.status(HttpStatus.NOT_FOUND).body("no registration found");
      }
       // 獲取狀態
      return ResponseEntity.ok().body(this.serviceRegistry.getStatus(this.registration));
   }
}

3.1 變更狀態

核心方法ServiceRegistry#setStatus:

@Override
public void setStatus(EurekaRegistration registration, String status) {
   // 獲取範例資訊
   InstanceInfo info = registration.getApplicationInfoManager().getInfo();
   // TODO: howto deal with delete properly?
   if ("CANCEL_OVERRIDE".equalsIgnoreCase(status)) {
      // 如果變更狀態請求傳過來 status = "CANCEL_OVERRIDE",向伺服器端發起 Jersey 刪除狀態請求
      registration.getEurekaClient().cancelOverrideStatus(info);
      return;
   }
   // TODO: howto deal with status types across discovery systems?
   InstanceInfo.InstanceStatus newStatus = InstanceInfo.InstanceStatus.toEnum(status);
   // 如果不是刪除狀態,則向伺服器端發起 Jersey 變更狀態請求
   registration.getEurekaClient().setStatus(newStatus, info);
}

核心流程有2個,分別為

statusCANCEL_OVERRIDE:

public void cancelOverrideStatus(InstanceInfo info) {
   getEurekaHttpClient().deleteStatusOverride(info.getAppName(), info.getId(), info);
}
@Override
public EurekaHttpResponse<Void> deleteStatusOverride(String appName, String id, InstanceInfo info) {
    String urlPath = "apps/" + appName + '/' + id + "/status";
    ClientResponse response = null;
    try {
        Builder requestBuilder = jerseyClient.resource(serviceUrl)
                .path(urlPath)
                .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString())
                .getRequestBuilder();
        addExtraHeaders(requestBuilder);
        // DELETE 請求
        response = requestBuilder.delete(ClientResponse.class);
        return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP DELETE {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}

刪除deleteStatusOverride請求: DELETE請求 path:"apps/" + appName + '/' + id + "/status"

直接呼叫setStatus()方法:

@Override
public EurekaHttpResponse<Void> statusUpdate(String appName, String id, InstanceStatus newStatus, InstanceInfo info) {
    String urlPath = "apps/" + appName + '/' + id + "/status";
    ClientResponse response = null;
    try {
        Builder requestBuilder = jerseyClient.resource(serviceUrl)
                .path(urlPath)
                .queryParam("value", newStatus.name())
                .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString())
                .getRequestBuilder();
        addExtraHeaders(requestBuilder);
        // PUT 請求
        response = requestBuilder.put(ClientResponse.class);
        return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}

變更狀態請求:PUT請求,path為 :"apps/" + appName + '/' + id + "/status"

3.2 獲取狀態

// EurekaServiceRegistry.class
public Object getStatus(EurekaRegistration registration) {
    String appname = registration.getApplicationInfoManager().getInfo().getAppName();
    String instanceId = registration.getApplicationInfoManager().getInfo().getId();
    // 獲取本地範例資訊
    InstanceInfo info = registration.getEurekaClient().getInstanceInfo(appname,
	    instanceId);
    HashMap<String, Object> status = new HashMap<>();
    if (info != null) {
        // 從範例資訊取出相應狀態返回
	status.put("status", info.getStatus().toString());
	status.put("overriddenStatus", info.getOverriddenStatus().toString());
    }
    else {
        // 如果範例資訊不存在,則返回 UNKNOWN 狀態
	status.put("status", UNKNOWN.toString());
    }
    return status;
}

參考文章

eureka-0.10.11原始碼(註釋)

springcloud-source-study學習github地址

以上就是Eureka原始碼解析服務離線狀態變更的詳細內容,更多關於Eureka 服務離線狀態變更的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com