SpringCloud-服务治理-Eureka

本篇是从基础方便讲解一些springcloud-服务治理-Eureka中的一些理论性的故事;具体的代码不详细展示;后面的文章会将源码进行整理,并且将源码的github地址上传。

1.什么是服务治理

专治分布式系统

(一)高可用性:服务治理框架保证服务的可用性

(二)分布式调用:微服务节点通常散落在不同的网络环境中,

大型互联网公司甚至会使用两地三机房或跨洲际机房做异地容灾。这要求服务治理框架具备在复杂网络环境下准备获知服务节点网络地址(IP,端口以及服务名称)的能力。作为服务消费者,就可以借助服务治理框架的,向服务节点发起请求。

(三)生命周期管理:微服务将自己的一生交给了服务治理框架。从服务上线,储蓄运行,直到生命结束,服务治理贯穿整个微服务生命周期。

(四)健康度检查:服务治理框架会精准识别存活的节点;

服务治理的解决方案:

服务注册-服务提供方自报家门;

服务发现-服务消费者拉取注册数据;

心跳检测,服务续约,服务剔除-一套由服务方和注册中心配合完成的去伪存真的过程;

服务下线:服务提供方发起主动下线;

2.服务治理的技术选型

分布式系统CAP理论 :分布式系统只能三占二,不能全占;

不是CP,就是AP

一致性©:数据一致性(强一致性)

可用性(A):(电商类要求:99.999%,宕机<5min)

分区容错性§:两地三机房

Eureka:NetFlix组件

Nacos:Alibaba

Consul:Spring以及其他开源厂商
服务治理组件的比较:

3.注册中心?

即获取所有服务节点的身份信息和服务名称,从注册中心的角度来说:有两种实现方案:

由注册中心主动访问网络节点中的所有机器;

注册中心坐等服务节点上门注册;

明显第二种方案更好;分析下第一种方式的弊端:

模型复杂:对于跨局域网的分布式系统来说,响应模型更加复杂;

网络消耗大:网络环境里会掺杂大量非服务节点;这些节点无需对发送的广播请求做出响应;这种广播模式无疑增加了网络通信的成本;

服务器压力增加:不仅要求注册中心向网络中所有节点发送广播请求,还需要对客户端的应答做出响应。考虑到注册中心的节点数远远少于服务节点,我们需尽可能弟减轻服务中心承载的业务;

注册中心日常任务

服务内容:提供的微服务是什么;

服务地址:ip地址+端口;

服务状态:通常是up

心跳检测和服务剔除

注册信息同步;

关注三个维度的信息,分别是Region, Zone和URL。

Region代表地理上的分区,而Zone则是这个分区下的机房。大多数情况下我们的配置中心只存在一个机房,这时配置URL就好了(比如http://localhost:20000/eureka/a/),这是注册中心的IP地址,同时Eureka会为我们指定一个默认的Region和Zone。

某些情况下当开发人员主动指定了注册中心的region和zone,比如在多机房的网络环境中,我给上海机房的注册中心指定了region=east_china, zone=sh

4.服务注册?

服务注册起手式:

扫描注解(@EnableDiscoveryClient中autoregister);

发起注册(DiscoveryCilent中的register,使用代理+装饰器模式(SessionedEurekaHttpClient));

装饰器+代理(SessionedEurekaHttpClient父类EurekaHttpClientDecorator)

代理注册:代理+回调

服务注册实现:

1.代理模式注册 RetryableEurekaHttpClient(这不是最里层的那个)重试

2.获取HttpClient 这里的HttpClient是RetryableEurekaHttpClient里面的代理对象,它里面封装了上次同步成功的注册中心地址等信息。假如代理对象为空,那我们就不知道该连向哪个注册中心了,这时候我们就要从Server列表中找一台服务器。

3.获取Server列表 :客户端的Server列表是开发人员通过上帝视角直接配置的,那么第一步就是获取这些已经配置好的Server列表信息。

4.发送注册请求 最里层的装饰器,调用了JerseyApplicationCLient的register方法向注册中心发起最后一击。

5.服务注册源码探秘-1

@EnableDiscoveryClient

首先,关注到@Import(EnableDiscoveryClientImportSelector.class)。然后细看

EnableDiscoveryClientImportSelector.class。注意,观看其中的源码部分

核心示例:AnnotationMetadata metadata(存放的就是启动类引入的注解

@SpringBootApplication;@EnableDiscoveryClient)

java 复制代码
@Override
public String[] selectImports(AnnotationMetadata metadata) {
   String[] imports = super.selectImports(metadata);

   AnnotationAttributes attributes = AnnotationAttributes.fromMap(
         metadata.getAnnotationAttributes(getAnnotationClass().getName(), true));

   boolean autoRegister = attributes.getBoolean("autoRegister");

   if (autoRegister) {
      List<String> importsList = new ArrayList<>(Arrays.asList(imports));
      importsList.add(   "org.springframework.cloud.client.serviceregistry.
AutoServiceRegistrationConfiguration");
      imports = importsList.toArray(new String[0]);
   }
   else {
      Environment env = getEnvironment();
      if (ConfigurableEnvironment.class.isInstance(env)) {
         ConfigurableEnvironment configEnv = (ConfigurableEnvironment) env;
         LinkedHashMap<String, Object> map = new LinkedHashMap<>();
         map.put("spring.cloud.service-registry.auto-registration.enabled", false);
         MapPropertySource propertySource = new MapPropertySource(
               "springCloudDiscoveryClient", map);
         configEnv.getPropertySources().addLast(propertySource);
      }

   }

   return imports;
}

核心关注:AutoServiceRegistrationConfiguration

java 复制代码
@Configuration
@EnableConfigurationProperties(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
public class AutoServiceRegistrationConfiguration {

}

核心关注:AutoServiceRegistrationProperties.class

打开发现就是个普通的类。此时查找的话必然就是看任何程序对其的引用。

Find useage(alt+F7).查找对它的引用。此种情况,关于Abstarct相关的程序引用(经验之谈)

关注到:AbstractAutoServiceRegistration。然后关注到这部分代码:

this.serviceRegistry = serviceRegistry;

java 复制代码
 protected AbstractAutoServiceRegistration(ServiceRegistry<R> serviceRegistry,
      AutoServiceRegistrationProperties properties) {
   this.serviceRegistry = serviceRegistry;
   this.properties = properties;
}

关注到ServiceRegistry,跳转到。是一个有实现类的接口,跳转到实现类:

EurekaServiceRegistry。关注register方法

java 复制代码
public void register(EurekaRegistration reg) {
   maybeInitializeClient(reg);

   if (log.isInfoEnabled()) {
      log.info("Registering application "
            + reg.getApplicationInfoManager().getInfo().getAppName()
            + " with eureka with status "
            + reg.getInstanceConfig().getInitialStatus());
   }

   reg.getApplicationInfoManager()
         .setInstanceStatus(reg.getInstanceConfig().getInitialStatus());

   reg.getHealthCheckHandler().ifAvailable(healthCheckHandler -> reg
         .getEurekaClient().registerHealthCheck(healthCheckHandler));
}

maybeInitializeClient(reg);中的方法就是强制预加载一些代理

java 复制代码
private void maybeInitializeClient(EurekaRegistration reg) {
   // force initialization of possibly scoped proxies
   reg.getApplicationInfoManager().getInfo();
   reg.getEurekaClient().getApplications();
}

关于到:reg.getApplicationInfoManager().getInfo()

java 复制代码
public InstanceInfo getInfo() {
    return instanceInfo;
}

对应evaluate的信息是:

关注到debug栈中实际启动的方法:

java 复制代码
public void start() {
   // only set the port if the nonSecurePort or securePort is 0 and this.port != 0
   if (this.port.get() != 0) {
      if (this.registration.getNonSecurePort() == 0) {
         this.registration.setNonSecurePort(this.port.get());
      }

      if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
         this.registration.setSecurePort(this.port.get());
      }
   }

   // only initialize if nonSecurePort is greater than 0 and it isn't already running
   // because of containerPortInitializer below
   if (!this.running.get() && this.registration.getNonSecurePort() > 0) {

      this.serviceRegistry.register(this.registration);

      this.context.publishEvent(new InstanceRegisteredEvent<>(this,
            this.registration.getInstanceConfig()));
      this.running.set(true);
   }
}

this.serviceRegistry.register(this.registration);

这一部并没有发起实际的调用。

所以关注下面的代码:

java 复制代码
this.context.publishEvent(new InstanceRegisteredEvent<>(this,
      this.registration.getInstanceConfig()));

关注到信息:registration.getInstanceConfig():将publishEvent到context(添加InstanceRegisteredEvent)中。

真正的注册实现是在DiscoveryClient中的register方法,代码如下:

java 复制代码
boolean register() throws Throwable {
    logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
    EurekaHttpResponse<Void> 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();
}

核心代码:httpResponse = eurekaTransport.registrationClient.register(instanceInfo);

再往下挖掘:

public class SessionedEurekaHttpClient extends EurekaHttpClientDecorator

关注到:EurekaHttpClientDecorator

基本是最后的核心代码:

public abstract class EurekaHttpClientDecorator implements EurekaHttpClient

java 复制代码
@Override
public EurekaHttpResponse<Void> register(final InstanceInfo info) {
    return execute(new RequestExecutor<Void>() {
        @Override
        public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
            return delegate.register(info);
        }

        @Override
        public RequestType getRequestType() {
            return RequestType.Register;
        }
    });
}

针对EurekaHttpClientDecorator。它的实现在于SessionedEurekaHttpClient

return execute(new RequestExecutor()

SessionedEurekaHttpClient中具体的代码:

java 复制代码
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    long now = System.currentTimeMillis();
    long delay = now - lastReconnectTimeStamp;
    if (delay >= currentSessionDurationMs) {
        logger.debug("Ending a session and starting anew");
        lastReconnectTimeStamp = now;
        currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
        TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
    }

    EurekaHttpClient eurekaHttpClient = eurekaHttpClientRef.get();
    if (eurekaHttpClient == null) {
        eurekaHttpClient = TransportUtils.getOrSetAnotherClient(eurekaHttpClientRef, clientFactory.newClient());
    }
    return requestExecutor.execute(eurekaHttpClient);
}

下面流程到:JerseyApplicationClient的顶层AbstractJerseyEurekaHttpClient

java 复制代码
public EurekaHttpResponse<Void> register(InstanceInfo info) {
    String urlPath = "apps/" + info.getAppName();
    ClientResponse response = null;
    try {
        Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
        addExtraHeaders(resourceBuilder);
        response = resourceBuilder
                .header("Accept-Encoding", "gzip")
                .type(MediaType.APPLICATION_JSON_TYPE)
                .accept(MediaType.APPLICATION_JSON)
                .post(ClientResponse.class, info);
        return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                    response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}
java 复制代码
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();

这一步完成就创建成功了

关键操作:SpringCloud经常用,面试经常问

delegate.compareAndSet(currentHttpClient, null);

6.服务发现

基于客户端的服务发现:

基于服务端的服务发现:router:服务端负载均衡器或网关层

对比:

7.服务消费者

关键技术点:

Private LoadBalancerClient:简单的负载均衡器:

Spring Cloud LoadBalancer是Spring Cloud的一个组件,它提供了一种Spring Cloud LoadBalancer是Spring Cloud的一个组件,它提供了一种在微服务架构中实现客户端负载均衡的方式。在Spring Cloud中,可以使用LoadBalancerClient接口来操作负载均衡器。这个接口是Spring Cloud提供的一个非阻塞式的客户端,用于访问服务的实例。

下面是使用LoadBalancerClient实现负载均衡的一般步骤:

首先,需要创建一个Spring Boot应用作为服务提供者,启动多个节点并确保它们在相同的端口号上运行。这些节点将作为被调用的服务。

然后,创建另一个Spring Boot应用作为服务消费者,这个应用会启动一个服务并实现LoadBalancerClient负载均衡。在这个应用中,你可以使用LoadBalancerClient的getInstances()方法获取服务提供者的所有实例。这个方法返回一个ServiceInstance对象列表,包含了所有可用的服务实例信息。

最后,从ServiceInstance列表中选择一个服务实例并发送请求。为了实现轮询负载均衡,可以使用RoundRobinLoadBalancer类。该类的构造函数接受一个ServiceInstanceListSupplier参数,这个参数是一个函数式接口,用于获取服务实例列表。

然后,可以创建一个新的RoundRobinLoadBalancer对象,并将之前获取的服务实例列表传递给它。最后,通过调用RoundRobinLoadBalancer的choose()方法来选择一个服务实例并发送请求。

这样,就可以在使用Spring Cloud构建的微服务架构中实现客户端负载均衡了。

8.心跳监测与服务剔除

客户端发起:由一个个服务结点根据配置时间主动发起的

同步状态:多个状态(UP,OUT_OF_SERVICE等待)

服务剔除:对一段时间无响应的服务,便要主动从注册列表中剔除,

以防止服务调用方请求失败

服务续约:底层是心跳,包含了一套"脏数据"处理流程

同步信息:

同步数十个属性到注册中心,重点信息如下:
访问地址 :就是eureka注册中心的地址;
访问路径 :放到了访问的url中。例如:app/ a p p n a m e / {app_name}/ appname/{instance_id}

appname是服务注册时提供的服务名,instance_id是当前这个服务节点的唯一编号。
服务状态 :UP, DOWN, STARTING, OUT_OF_SERVICE, UNKNOWN
最后一次同步注册的时间 :LastIDirtyTimeStamp:最复杂。当前服务结点最后一次与服务中心失去同步时的时间,InstanceInfo封装了该属性以及另外一个属性isInstanceInfoDirty

当isInstanceInfoDirty=true的时候,表示当前结点自从LastIDirtyTimeStamp以后的时间都处于未同步的状态。

两个核心指标:
客户端指标

java 复制代码
eureka.instance.lease-renewal-interval-in-seconds=10
eureka.instance.lease-expiration-duration-in-seconds=20

第一个指标决定了每个多久向服务器发送一次心跳包

第二个参数告诉服务器,若果我在x秒内没有心跳,那就代表我挂掉了。

通常第一个时间一定是小于第二个时间的。毕竟两次心跳的间隔时间,还得加

几秒的网络延迟,才是判断服务器是否挂掉的最小时间。

服务剔除:

启动定时任务 注册中心在启动的时候也会同步开启一个后台任务,默认每间隔60秒触发服务剔除任务,当然我们也可以通过在服务端```eureka.server.eviction-interval-timer-in-ms=30000``做如下参数配置修改触发间隔,这里将间隔设置成了30秒。此处建议不要设置的时间过短。
调用evict 通过AbstractInstanceRegistry的eviction方法直接运行。

自保开启 服务自保是注册中心的保命招,一旦自保开启,则注册中心就会中断服务剔除操作。
遍历过期服务 注册中心会遍历所有服务节点,揪出所有过期服务。如何判断一个服务是过期服务呢,只要满足以下两点中任意一点就可以当做过期

已被标记为过期(evictionTimestamp > 0)

最后一次心跳时间 + 服务端配置的心跳间隔时间 < 当前时间
计算可剔除的服务总个数 所有服务是否能被全部剔除呢?当然不是,服务中心也要顾及自身的稳定性,因此他设置了一个系数(默认0.85),可剔除的服务数量,不能大于已注册服务的总数量乘以这个系数。比如当前有100个服务,其中99个已经断了气,那么注册中心实际上只能剔除100*0.85 = 85个服务节点,而不是99个。
乱序剔除服务 这里你就当做是随到哪个过期服务就把它踢下线。

9.服务续约

续约和心跳的关系:

第一步:将服务结点的状态同步到注册中心,意思就是通知注册中心我还能工作。

这一步需要借助客户端的心跳功能来主动发送。

第二步:当心跳包到达注册中心的时候,就要注册中心。他有一套判别机制,来判断当前续约心跳是否合理。根据判断结果修改当前instance在注册中心记录的同步时间。

服务剔除并不会和心跳以及续约直接打交道,而是通过查验服务节点在注册中心记录的同步时间来决定是否剔除这个节点。心跳,续约,剔除是一套相互拮抗,共同作用的一套机制。


1.服务续约请求 :DiscoverClient(从此类的renew方法开始)类,他是所有操作的门面入口。
2.发送心跳 服务续约借助心跳来实现,因此发给注册中心的参数和上一小节的心跳部分写到的一样,两个重要参数分别是服务的状态(UP)和lastDirtyTimeStamp

如果续约成功,注册中心则会返回200的HTTP code

如果续约不成功,注册中心返回404,这里的404并不是说没有找到注册中心的地址,而是注册中心认为当前服务节点并不存在。这个时候再怎么续约也不灵验了,客户端需要触发一次重新注册操作。

3.在重新注册之前,客户端会做下面两个小操作,然后再主动调用服务册流程。

设置lastDirtyTimeStamp 由于重新注册意味着服务节点和注册中心的信息不同步,因此需要将当前系统时间更新到"lastDirtyTimeStamp"

标记自己为脏节点

4.当注册成功的时候,清除脏节点标记,但是lastDirtyTimeStamp不会清除,因为这个属性将会在后面的服务续约中作为参数发给注册中心,以便服务中心判断节点的同步状态。

注册中心续约校验

注册中心开放了一系列http接口,他们都存放在com.netflix.eureka.resourcess包下。。

接受请求 InstanceResource下的renewLease方法接到了服务节点的续约请求。
尝试续约 注册中心此时会做几样简单的例行检查,如果没有通过,则通通返回404,不接受申辩。

续约失败!重新注册!
脏数据校验 如果续约校验没问题,接下来就要进行脏数据检查。到了服务续约最难的地方了,脏数据校验逻辑之复杂。往细了说,就是当客户端发来的lastDirtyTimeStamp,晚于注册中心保存的lastDirtyTimeStamp时(每个节点在中心都有一个脏数据时间),说明在从服务节点上次注册到这次续约之间,发生了注册中心不知道的事儿(数据不同步)。这可不行,这搞得我注册中心的工作不好有序开展,回去重新注册吧。续约不通过,返回404。

10.【源码品读】心跳和服务续约

客户端心跳发送的内容是什么?
客户端续约流程:
服务端租约更新流程:

首先关注到开始的地方:

java 复制代码
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                Provider<BackupRegistry> backupRegistryProvider) {
    if (args != null) {
        this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
        this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
        this.eventListeners.addAll(args.getEventListeners());
        this.preRegistrationHandler = args.preRegistrationHandler;
    }

中间的跳过,直到发送心跳的地方:

java 复制代码
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
initScheduledTasks();

进到这个方法里,首先看到缓存刷新:

java 复制代码
private void initScheduledTasks() {
    if (clientConfig.shouldFetchRegistry()) {
        // registry cache refresh timer
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        scheduler.schedule(
                new TimedSupervisorTask(
                        "cacheRefresh",
                        scheduler,
                        cacheRefreshExecutor,
                        registryFetchIntervalSeconds,
                        TimeUnit.SECONDS,
                        expBackOffBound,
                        new CacheRefreshThread()
                ),
                registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }

再往下关注到:

java 复制代码
if (clientConfig.shouldRegisterWithEureka()) {
    int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
    int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
    logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

    // Heartbeat timer
    scheduler.schedule(
            new TimedSupervisorTask(
                    "heartbeat",
                    scheduler,
                    heartbeatExecutor,
                    renewalIntervalInSecs,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new HeartbeatThread()
            ),
            renewalIntervalInSecs, TimeUnit.SECONDS);

然后到HeartbeatThread()方法中:

java 复制代码
private class HeartbeatThread implements Runnable {

    public void run() {
        if (renew()) {
            lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
        }
    }
}

再关注到renew():这里有开始像注册中心一样的嵌套

java 复制代码
Renew在客户端来说是发送了一个心跳,在服务端是来说是服务续约
boolean renew() {
    EurekaHttpResponse<InstanceInfo> 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;
    }
}

第一层嵌套的内容是:然后是Retry再下一层是redirecting.再下一层是:AbstractJerseyEurekaHttpClient

以上都是客户端的流程;

服务端的流程

先浅看一下接收端部分:因为是这里心跳包是来自于服务的提供者

所以为false

java 复制代码
@PUT
public Response renewLease(
        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
        @QueryParam("overriddenstatus") String overriddenStatus,
        @QueryParam("status") String status,
        @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
    boolean isFromReplicaNode = "true".equals(isReplication);
    boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);

    // Not found in the registry, immediately ask for a register
    if (!isSuccess) {
        logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
        return Response.status(Status.NOT_FOUND).build();
    }
    // Check if we need to sync based on dirty time stamp, the client
    // instance might have changed some value
    Response response;
    if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
        response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
        // Store the overridden status since the validation found out the node that replicates wins
        if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
                && (overriddenStatus != null)
                && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
                && isFromReplicaNode) {
            registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
        }
    } else {
        response = Response.ok().build();
    }
    logger.debug("Found (Renew): {} - {}; reply status={}", app.getName(), id, response.getStatus());
    return response;
}

关注到:

java 复制代码
boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
Register.renew方法

然后到InstanceRegistry中

java 复制代码
public boolean renew(final String appName, final String serverId,
      boolean isReplication) {
   log("renew " + appName + " serverId " + serverId + ", isReplication {}"
         + isReplication);
   List<Application> applications = getSortedApplications();
   for (Application input : applications) {
      if (input.getName().equals(appName)) {
         InstanceInfo instance = null;
         for (InstanceInfo info : input.getInstances()) {
            if (info.getId().equals(serverId)) {
               instance = info;
               break;
            }
         }
         publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId,
               instance, isReplication));
         break;
      }
   }
   return super.renew(appName, serverId, isReplication);
}

然后返回上层方法:super.renew(appName, serverId, isReplication)

关注到:AbstractInstanceRegistry

java 复制代码
public boolean renew(String appName, String id, boolean isReplication) {
    RENEW.increment(isReplication);
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToRenew = null;
    if (gMap != null) {
        leaseToRenew = gMap.get(id);
    }
    if (leaseToRenew == null) {
        RENEW_NOT_FOUND.increment(isReplication);
        logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
        return false;
    } else {
        InstanceInfo instanceInfo = leaseToRenew.getHolder();
        if (instanceInfo != null) {
            // touchASGCache(instanceInfo.getASGName());
            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                    instanceInfo, leaseToRenew, isReplication);
            if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
                        + "; re-register required", instanceInfo.getId());
                RENEW_NOT_FOUND.increment(isReplication);
                return false;
            }
            if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                logger.info(
                        "The instance status {} is different from overridden instance status {} for instance {}. "
                                + "Hence setting the status to overridden status", instanceInfo.getStatus().name(),
                                instanceInfo.getOverriddenStatus().name(),
                                instanceInfo.getId());
                instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);

            }
        }
        renewsLastMin.increment();
        leaseToRenew.renew();
        return true;
    }
}

然后关注到:leaseToRenew.renew();

最后返回到InstanceResource中

java 复制代码
public Response renewLease(
        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
        @QueryParam("overriddenstatus") String overriddenStatus,
        @QueryParam("status") String status,
        @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
    boolean isFromReplicaNode = "true".equals(isReplication);
    boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);

    // Not found in the registry, immediately ask for a register
    if (!isSuccess) {
        logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
        return Response.status(Status.NOT_FOUND).build();
    }
    // Check if we need to sync based on dirty time stamp, the client
    // instance might have changed some value
    Response response;
    if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
        response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
        // Store the overridden status since the validation found out the node that replicates wins
        if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
                && (overriddenStatus != null)
                && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
                && isFromReplicaNode) {
            registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
        }
    } else {
        response = Response.ok().build();
    }
    logger.debug("Found (Renew): {} - {}; reply status={}", app.getName(), id, response.getStatus());
    return response;
}

11.服务自保

服务自保:

把当前所有节点保留

在实际应用中,并不是所有无心跳的服务都不可用,也许因为短暂

的网络抖动等原因,导致服务结点与注册中心续约不上,但是服务

节点之间的调用还是可用状态,如果这时强行剔除服务节点,可能

会造成大范围的业务停滞。

服务自保的触发机关:由两个开关进行控制

自动开关:

这时服务自保开启后的警告,就是说挂掉的服务有可能会被错误的当做UP,在

一定的时间内容续约成功的节点的个数占已注册总服务的比值,已经低于限定值,

因此所有节点都不会过期,服务自保开启。

这是一个服务自保的自动触发开关。服务自保机制会检查过去15分钟以内,所有续约成功的节点,咱所有或注册节点的比例,如果低于一个限定值(0.85)就开始服务自保模式。

服务自保模式往往是为了应对短暂的网络环境问题,在理想情况下服务节点的续约成功率应该接近100%,如果突然发生网络问题,比如一部分机房无法连接到注册中心,这时候续约成功率有可能大幅降低。但考虑到Eureka采用客户端的服务发现模式,客户端手里有所有节点的地址,如果服务节点只是因为网络原因无法续约但其自身服务是可用的,那么客户端仍然可以成功发起调用请求。这样就避免了被服务剔除给错杀。

手动开关:

服务自保的总闸,一下配置将强制关闭服务自保,即便上面的自动开关被触发,

也不能开启自保功能

eureka.server.enable-self-preservation=false

12.架构思考

1.如何保证注册中心的高可用化,单中心宕机的思考
加入单中心节点扑街了怎么办?

新服务上线,老服务下线。对于高可用部分,就必须要有故障容错(比如服务降级)。

故障恢复(比如,在单功能模块不可用时尝试重启)

如何做高可用改造:集群+互备

操作流程:

对host文件动个手脚

启动双注册中心

单节点注册+同步机制

客户端配置双节点

host配置图片:

Eureka-client配置

配置单台服务器:peer1与peer2是互通的。所以eureka-client在两台机器都有。

eureka:

client:

serviceUrl:

defaultZone: http://localhost:20000/eureka/

配置多台服务器:直接注册到peer1与peer2上

eureka:

client:

serviceUrl:

defaultZone: http://peer1:20000/eureka/,http://peer2:20001/eureka/

相关推荐
先睡6 小时前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
Bug退退退12311 小时前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq
guojl16 小时前
RestTemplate使用手册
spring cloud·微服务
guojl16 小时前
RestTemplate原理分析
spring cloud·微服务
booooooty16 小时前
基于Spring AI Alibaba的多智能体RAG应用
java·人工智能·spring·多智能体·rag·spring ai·ai alibaba
极光雨雨16 小时前
Spring Bean 控制销毁顺序的方法总结
java·spring
Ken_111516 小时前
SpringCloud系列(51)--SpringCloud Stream之使用分组解决消息重复消费问题
spring cloud
Spirit_NKlaus17 小时前
解决HttpServletRequest无法获取@RequestBody修饰的参数
java·spring boot·spring
Li&&Tao18 小时前
docker 常用命令
docker·容器·eureka
lwb_011818 小时前
SpringCloud——Gateway新一代网关
spring·spring cloud·gateway