Netflix Eureka - 服务续约

261 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


Netflix Eureka 目录汇总

  1. eureka server启动以及初始化

  2. eureka client启动以及初始化

  3. 服务注册

    3.1. 可重入读写锁-读锁

  4. 服务发现

    4.1. 全量抓取注册表

    4.2. 注册表多级缓存机制

    4.3. 注册表多级缓存过期机制(主动、定时、被动)

    4.4. 增量抓取注册表

     4.4.1. 一致性Hash对比机制
    
     4.4.2. 可重入读写锁-写锁
    
  5. 服务续约


Netflix Eureka 时间间隔简要

读写缓存 - 定时过期(180秒)

只读缓存 - 被动过期(30秒(整30秒))

定时抓取增量注册表(30秒)

定时删除超过3分钟的服务实例变更记录(30秒)

服务续约间隔(30秒)


服务续约

服务续约:eureka client每隔一定的时间,会给eureka server发送心跳,保持心跳,让eureka server知道自己还活着

eureka client 发送服务续约请求

DiscoveryClient构造函数

  1. 保存配置信息
  2. 初始化线程池(调度、心跳、缓存刷新)
  3. 初始化网络通信组件
  4. 全量拉取注册表
  5. 初始化调度任务
    5.1. 服务发现
    5.2. 服务注册
  6. 监视器注册
@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;
    } else {
        // args == null
        this.healthCheckCallbackProvider = null;
        this.healthCheckHandlerProvider = null;
        this.preRegistrationHandler = null;
    }

    // 应用信息管理器
    this.applicationInfoManager = applicationInfoManager;

    // 实例信息
    InstanceInfo myInfo = applicationInfoManager.getInfo();

    // EurekaClientConfig
    clientConfig = config;
    // @Deprecated
    staticClientConfig = clientConfig;

    // EurekaTransportConfig
    transportConfig = config.getTransportConfig();
    instanceInfo = myInfo;
    if (myInfo != null) {
        // appName:服务名称。通过eureka-client.properties文件获取eureka.name=eureka
        // id:instanceId 如果为空,则返回hostname。一个服务可以存在多个服务实例,构建成集群。id(instanceId)表示一个服务实例
        appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
    } else {
        logger.warn("Setting instanceInfo to a passed in null value");
    }

    // TODO LEON
    this.backupRegistryProvider = backupRegistryProvider;

    this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
    // 本地服务实例缓存
    localRegionApps.set(new Applications());

    fetchRegistryGeneration = new AtomicLong(0);

    // null
    remoteRegionsToFetch = new AtomicReference<>(clientConfig.fetchRegistryForRemoteRegions());
    remoteRegionsRef = new AtomicReference<>(
            // true
            remoteRegionsToFetch.get() == null ?
                    null :
                    remoteRegionsToFetch.get().split(","));

    // 期望抓取注册表
    if (config.shouldFetchRegistry()) {
        // 允许抓取注册表
        this.registryStalenessMonitor = new ThresholdLevelsMetric(this,
                METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L,
                240L, 480L});
    } else {
        this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
    }

    // 允许将自己注册到注册中心
    if (config.shouldRegisterWithEureka()) {
        this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this,
                METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L,
                120L, 240L, 480L});
    } else {
        this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
    }

    logger.info("Initializing Eureka in region {}", clientConfig.getRegion());

    // 如果既不允许注册服务到注册中心,也不允许抓取注册表, 清理资源并结束方法
    if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
        logger.info("Client configured to neither register nor query for data.");
        scheduler = null;
        heartbeatExecutor = null;
        cacheRefreshExecutor = null;
        eurekaTransport = null;
        instanceRegionChecker =
                new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config),
                        clientConfig.getRegion());

        // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
        // to work with DI'd DiscoveryClient
        DiscoveryManager.getInstance().setDiscoveryClient(this);
        DiscoveryManager.getInstance().setEurekaClientConfig(config);

        initTimestampMs = System.currentTimeMillis();
        logger.info("Discovery Client initialized at timestamp {} with initial instances " +
                "count: {}", initTimestampMs, this.getApplications().size());

        return;  // no need to setup up an network tasks and we are done
    }

    // 初始化3个线程池(调度、心跳、缓存刷新)
    try {
        // 支持调度的线程池
        // default size of 2 - 1 each for heartbeat and cacheRefresh
        scheduler = Executors.newScheduledThreadPool(2,
                new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-%d").setDaemon(true).build());

        // 支持心跳的线程池
        heartbeatExecutor = new ThreadPoolExecutor(1,
                clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder().setNameFormat(
                "DiscoveryClient-HeartbeatExecutor-%d").setDaemon(true).build());  //
        // use direct handoff

        // 支持缓存刷新的线程池
        cacheRefreshExecutor = new ThreadPoolExecutor(1,
                clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder().setNameFormat(
                "DiscoveryClient-CacheRefreshExecutor-%d").setDaemon(true).build());  // use
        // direct handoff

        // 网络通信组件
        eurekaTransport = new EurekaTransport();
        // 初始化网络通信组件
        scheduleServerEndpointTask(eurekaTransport, args);

        AzToRegionMapper azToRegionMapper;
        if (clientConfig.shouldUseDnsForFetchingServiceUrls()) { // false
            azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
        } else {
            azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
        }
        if (null != remoteRegionsToFetch.get()) {
            azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
        }
        instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper,
                clientConfig.getRegion());
    } catch (Throwable e) {
        throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
    }

    // 允许拉取注册表,并且拉取(全量、增量)注册表成功
    if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
        // 抓取失败,从备份里抓取
        fetchRegistryFromBackup();
    }

    // call and execute the pre registration handler before all background tasks (inc
    // registration) is started
    if (this.preRegistrationHandler != null) {
        this.preRegistrationHandler.beforeRegistration();
    }
    // 初始化调度任务(注册、发现)
    initScheduledTasks();

    try {
        // 注册监视器
        Monitors.registerObject(this);
    } catch (Throwable e) {
        logger.warn("Cannot register timers", e);
    }

    // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
    // to work with DI'd DiscoveryClient
    DiscoveryManager.getInstance().setDiscoveryClient(this);
    DiscoveryManager.getInstance().setEurekaClientConfig(config);

    initTimestampMs = System.currentTimeMillis();
    logger.info("Discovery Client initialized at timestamp {} with initial instances count: " + "{}", initTimestampMs, this.getApplications().size());
}

eureka server 处理服务续约请求

根据appName和instanceId,从注册表中获取Lease<InstanceInfo>,更新Lease对象的lastUpdateTimestamp时间戳

ApplicationsResource:
ApplicationResource:
InstanceResource:

ApplicationsResource

@Path("/{version}/apps")
@Produces({"application/xml", "application/json"})
public class ApplicationsResource {
    
    /**
     * Gets information about a particular {@link com.netflix.discovery.shared.Application}.
     *
     * @param version the version of the request.
     * @param appId   the unique application identifier (which is the name) of the
     *                application.
     * @return information about a particular application.
     */
    @Path("{appId}")
    public ApplicationResource getApplicationResource(
            @PathParam("version") String version,
            @PathParam("appId") String appId) {
        CurrentRequestVersion.set(Version.toEnum(version));
        return new ApplicationResource(appId, serverConfig, registry);
    }
}

ApplicationResource

@Produces({"application/xml", "application/json"})
public class ApplicationResource {
    
    /**
     * Gets information about a particular instance of an application.
     *
     * @param id the unique identifier of the instance.
     * @return information about a particular instance.
     */
    @Path("{id}")
    public InstanceResource getInstanceInfo(@PathParam("id") String id) {
        return new InstanceResource(this, id, serverConfig, registry);
    }
}

InstanceResource

@Produces({"application/xml", "application/json"})
public class InstanceResource {

    /**
     * A put request for renewing lease from a client instance.
     *
     * @param isReplication
     *            a header parameter containing information whether this is
     *            replicated from other nodes.
     * @param overriddenStatus
     *            overridden status if any.
     * @param status
     *            the {@link InstanceStatus} of the instance.
     * @param lastDirtyTimestamp
     *            last timestamp when this instance information was updated.
     * @return response indicating whether the operation was a success or
     *         failure.
     */
    /**
     * 处理服务续约操作
     *
     * @param isReplication
     * @param overriddenStatus
     * @param status
     * @param lastDirtyTimestamp
     * @return
     */
    @PUT
    public Response renewLease(
            // x-netflix-discovery-replication
            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
            @QueryParam("overriddenstatus") String overriddenStatus,
            @QueryParam("status") String status,
            @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {

        boolean isFromReplicaNode = "true".equals(isReplication);
        // 调用注册表(registry)的续约方法(renew),更新lastUpdateTimestamp(System.currentTimeMillis() + duration)
        boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);

        // Not found in the registry, immediately ask for a register
        // 服务没有注册,返回404。eureka client 会立即发起服务注册请求。
        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 = null;
        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;
    }
}

服务续约流程

(1)DiscoveryClient初始化的时候,初始化3个线程池,其中有一个就是心跳线程(HeartbeatThread

(2)默认每隔30秒发送一次心跳,每隔30秒执行一次HeartbeatThread线程的逻辑,发送租约续约请求

(3)发送续约请求,调用eurekaTransport.registrationClient.sendHeartBeat()方法,发送PUT请求【http://localhost:8080/v2/apps/ServiceA/InstanceId001

(4)eureka server 项目的ApplicationsResource类接收【/{version}/apps/{appId}】,进入ApplicationResource类接收【/{id}】,进入InstanceResource类接收【PUT请求】执行【renewLease()】方法逻辑

(5)通过注册表的renew()方法,完成服务续约,实际执行AbstractInstanceRegistry.renew()方法

(6)根据服务名和实例id,从注册表中获取Lease,更新【Lease】对象的【lastUpdateTimestamp】时间戳【lastUpdateTimestamp = System.currentTimeMillis() + duration;】【90秒】。

(7)如果续约失败,返回HTTP Status NOT_FOUND给 eureka client

(8)eureka client 发现如果是404,表示续约失败,会重新发起服务注册请求;如果返回续约成功,更新lastSuccessfulHeartbeatTimestamp为当前时间戳

(9)eureka client 再次发起服务注册请求时,会进入eureak server服务注册处理逻辑的服务实例已存在分支。从注册表中获取已存在的服务实例,比较【要新注册的服务实例】与【已存在的服务实例】的【lastDirtyTimestamp】值,留存【lastDirtyTimestamp】大的【服务实例】,放入注册表registry