Eureka 核心步骤源码解析

一、Eureka Client

Eureka Client做的事情主要包括:

  • 服务注册(Register)

    Eureka Client会向Eureka Server进行服务注册,Client在我们的实际应用中可以是服务提供者或者是服务消费者

  • 服务续约(Renew)

    Eureka Client需要定期(默认30秒)向Eureka Server发送一次心跳来续订租约,以便让Eureka Server知道自已仍在运行,如果Eureka Server在90秒内未收到续订心跳,则会将Client实例从其注册表中删除

  • 服务注册列表获取(Fetch)

    Eureka Client需要从Eueka Server获取服务注册列表来查找其他服务信息,获取服务列表后会在本地进行缓存,可以定期(默认30秒)更新服务注册列表信息

  • 服务下线(Cancel)

    Eureka Client在shutdown时会向Eureka Server发送服务下线请求,以便Server将实例从注册表中删除

下面我们跟着源码梳理以上的相关功能代码流程,只做流程大概分析梳理,不具体到细节,说明部分在代码中通过注释的方式呈现。

在容器启动时,会加载com.netflix.discovery.DiscoveryClient并调用其构造方法,如下:

@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                Provider backupRegistryProvider) {
    // 省略部分代码

    // 如果配置不用注册到Eureka && 配置不用从注册中心获取配置,则不用初始化相关组件
    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
    }

    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(),
                new ThreadFactoryBuilder()
                        .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                        .setDaemon(true)
                        .build()
        );  // use direct handoff

        // 注册信息缓存刷新的线程池
        cacheRefreshExecutor = new ThreadPoolExecutor(
                1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                new SynchronousQueue(),
                new ThreadFactoryBuilder()
                        .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                        .setDaemon(true)
                        .build()
        );  // use direct handoff
    } catch (Throwable e) {
        throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
    }
    // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
    // 初始化定时任务,服务心跳renew、服务注册、服务列表获取等功能在此处完成
    initScheduledTasks();
}

接着看initScheduledTasks方法

private void initScheduledTasks() {
    // 1.如果配置fetchRegistry=true,则定期执行获取服务列表的定时任务,默认30秒一次,通过registryFetchIntervalSeconds可以配置获取频率
    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,
                        // 具体执行逻辑在CacheRefreshThread.run()方法,后面再看
                        new CacheRefreshThread()
                ),
                registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }
    // 2.如果配置registerWithEureka=true,则定期向Eureka Server报送心跳,通过leaseRenewalIntervalInSeconds可以配置报送频率
    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,
                        // 具体执行逻辑在HeartbeatThread的run()方法,后面再看
                        new HeartbeatThread()
                ),
                renewalIntervalInSecs, TimeUnit.SECONDS);
        // 3.启动instanceInfoReplicator,服务注册就在这里完成,后面再看
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

再细看上述的3步流程

1)获取服务列表

跟踪CacheRefreshThread.run()方法

class CacheRefreshThread implements Runnable {
    public void run() {
        refreshRegistry();
    }
}
@VisibleForTesting
void refreshRegistry() {
    try {
        // 获取服务列表信息
        boolean success = fetchRegistry(remoteRegionsModified);
    } catch (Throwable e) {
        logger.error("Cannot fetch registry from server", e);
    }
}
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
    try {
        // If the delta is disabled or if it is the first time, get all
        // applications
        Applications applications = getApplications();

        if (clientConfig.shouldDisableDelta()
                || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
                || forceFullRegistryFetch
                || (applications == null)
                || (applications.getRegisteredApplications().size() == 0)
                || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
        {
            // 从Eureka Server全量获取服务列表缓存在本地
            getAndStoreFullRegistry();
        } else {
            // 更新服务列表
            getAndUpdateDelta(applications);
        }
        applications.setAppsHashCode(applications.getReconcileHashCode());
        logTotalInstances();
    } catch (Throwable e) {
        logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
        return false;
    } finally {
        if (tracer != null) {
            tracer.stop();
        }
    }
    return true;
}

2)服务心跳renew

跟踪HeartbeatThread.run()方法

private class HeartbeatThread implements Runnable {
    public void run() {
        if (renew()) {
            lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
        }
    }
}
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() == 404) {
            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() == 200;
    } catch (Throwable e) {
        logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
        return false;
    }
}

3)服务注册

跟踪InstanceInfoReplicator.start()方法

public void start(int initialDelayMs) {
    if (started.compareAndSet(false, true)) {
        // 这里设置标识,便于启动时完成服务注册
        instanceInfo.setIsDirty();  // for initial register
        // 实际上执行的是run()方法,看下面
        Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}
public void run() {
    try {
        discoveryClient.refreshInstanceInfo();

        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        // 在start()方法中设置了标识位,所以此处能满足条件调用register()方法进行服务注册,看下面
        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);
    }
}

服务注册最终是调用register()方法

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() == 204;
}

4)服务下线

在应用shutdown时,会调用com.netflix.discovery.DiscoveryClient的shutdown()方法,其内部完成了服务下线的功能。

public synchronized void shutdown() {
        if (isShutdown.compareAndSet(false, true)) {
            logger.info("Shutting down DiscoveryClient ...");

            if (statusChangeListener != null && applicationInfoManager != null) {
                applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
            }

            cancelScheduledTasks();

            // If APPINFO was registered
            if (applicationInfoManager != null
                    && clientConfig.shouldRegisterWithEureka()
                    && clientConfig.shouldUnregisterOnShutdown()) {
                // 设置应用状态为DOWN
                applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                // 服务下线
                unregister();
            }

            if (eurekaTransport != null) {
                eurekaTransport.shutdown();
            }

            heartbeatStalenessMonitor.shutdown();
            registryStalenessMonitor.shutdown();

            logger.info("Completed shut down of DiscoveryClient");
        }
    }
void unregister() {
    // It can be null if shouldRegisterWithEureka == false
    if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
        try {
            logger.info("Unregistering ...");
            EurekaHttpResponse 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);
        }
    }
}

在服务注册、心跳renew、服务下线的方法调用中,通过debug发现实际上是调用AbstractJerseyEurekaHttpClient的相关方法实现。

// 服务注册register
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
// 心跳renew
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
// 服务下线cancel
EurekaHttpResponse httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());

AbstractJerseyEurekaHttpClient类中服务注册的register()方法为例:

public EurekaHttpResponse 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();
            }
        }
    }

serviceUrl就是配置文件eureka.client.serviceUrl.defaultZone = http://127.0.0.1:8761/eureka/,向Eureka Server请求实际上是通过Jersey框架完成。(Jersey是一个REST框架)

AbstractJerseyEurekaHttpClient类实现了EurekaHttpClient接口,接口中定义了服务注册与发现的相关方法。

public interface EurekaHttpClient {

    EurekaHttpResponse register(InstanceInfo info);

    EurekaHttpResponse cancel(String appName, String id);

    EurekaHttpResponse sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus);

    EurekaHttpResponse statusUpdate(String appName, String id, InstanceStatus newStatus, InstanceInfo info);

    EurekaHttpResponse deleteStatusOverride(String appName, String id, InstanceInfo info);

    EurekaHttpResponse getApplications(String... regions);

    EurekaHttpResponse getDelta(String... regions);

    EurekaHttpResponse getVip(String vipAddress, String... regions);

    EurekaHttpResponse getSecureVip(String secureVipAddress, String... regions);

    EurekaHttpResponse getApplication(String appName);

    EurekaHttpResponse getInstance(String appName, String id);

    EurekaHttpResponse getInstance(String id);

    void shutdown();
}

二、Eureka Server

Eureka Server做的事情主要包括:

  • 维护服务注册信息列表
  • 接收来自Eureka Client的register、renew、cancel请求
  • Eureka Server多节点之间的数据复制同步

查看spring-cloud-netflix-eureka-server-x.x.x.RELEASE,在META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

在项目启动时EurekaServerAutoConfiguration会加载至spring容器

public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
    // Eureka Server的相关配置类
    @Configuration
    protected static class EurekaServerConfigBeanConfiguration {
        @Bean
        @ConditionalOnMissingBean
        public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
            EurekaServerConfigBean server = new EurekaServerConfigBean();
            if (clientConfig.shouldRegisterWithEureka()) {
                // Set a sensible default if we are supposed to replicate
                server.setRegistrySyncRetries(5);
            }
            return server;
        }
    }
    // dashboard页面控制器
    @Bean
    @ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true)
    public EurekaController eurekaController() {
        return new EurekaController(this.applicationInfoManager);
    }
    // 处理Eureka Client的register、renew、cancel等请求
    @Bean
    public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
            ServerCodecs serverCodecs) {
        this.eurekaClient.getApplications(); // force initialization
        return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
                serverCodecs, this.eurekaClient,
                this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(),
                this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
    }
    // 处理Eureka Server多节点同步
    @Bean
    @ConditionalOnMissingBean
    public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry,
            ServerCodecs serverCodecs) {
        return new RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig,
                this.eurekaClientConfig, serverCodecs, this.applicationInfoManager);
    }
}

ApplicationResourceInstanceResource处理来自Eureka Client的register、renew、cancel等请求,以ApplicationResource中的服务注册接口为例。

@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
                            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    // 省略相关代码
    registry.register(info, "true".equals(isReplication));
    return Response.status(204).build();  // 204 to be backwards compatible
}

实际上调用的是PeerAwareInstanceRegistryImpl中的register方法

public void register(final InstanceInfo info, final boolean isReplication) {
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    // 调用AbstractInstanceRegistry父类的方法
    super.register(info, leaseDuration, isReplication);
    // 向其他Eureka Server节点同步注册信息
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

服务注册的信息保存在一个map的数据结构中

private final ConcurrentHashMap>> registry
            = new ConcurrentHashMap>>();

其他renew、cancel逻辑也在PeerAwareInstanceRegistryImpl,这里不再赘述。

三、推荐阅读

  • https://github.com/Netflix/eureka
  • dive-into-eureka

如果文章对你有帮助的话,给文章点个赞吧。

如果有写得不正确的地方,欢迎指出。

文章首发公众号:会跳舞的机器人,欢迎扫码关注。

你可能感兴趣的:(Eureka 核心步骤源码解析)