注册中心 eureka

121 阅读3分钟

1. 服务注册

分析要素:

1)eureka 发起的服务注册的位置;

2)eureka 发起注册携带的核心信息;

开始分析:

我们从使用角度为切入点分析:

EnableEurekaClient

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {

	/**
	 * If true, the ServiceRegistry will automatically register the local server.
	 */
	boolean autoRegister() default true;
}

EnableDiscoveryClientImportSelector

跟进查看:EnableDiscoveryClientImportSelector

@Order(Ordered.LOWEST_PRECEDENCE - 100)
public class EnableDiscoveryClientImportSelector extends SpringFactoryImportSelector<EnableDiscoveryClient> {

	@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;
	}

}

上面代码实质目的:将 org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration

实例化到容器,为 EurekaClientAutoConfiguration 配置加载提供条件支持;

在这里,AutoServiceRegistrationConfiguration 这种自动化的配置类,一般都会在spring.factories 中配置;如下:

spring-cloud-netflix-eureka-client-2.1.0.RELEASE.jar

|- META-INF

|- spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

EurekaClientAutoConfiguration

EurekaClientAutoConfiguration 代码太多,这里就不粘贴了,这个类是自动化配置的核心类;核心就是将eureka 相关的bean 添加到 spring的IOC map中;

🍌这里我们重点关注:EurekaAutoServiceRegistration

public class EurekaAutoServiceRegistration implements AutoServiceRegistration, SmartLifecycle, Ordered, SmartApplicationListener {
}

如上,我们看到此类实现了SmartLifecycle 接口,这个接口会在spring 加载完所有的bean后调用 start() 方法;跟踪代码执行,得到如下时序图:

注:SmartLifecycle 的解释可以参见:spring-boot 常用类

2. 服务续租

续租:

eureka-client 向 eureka-server 发起注册应用实例后获得 租约 (Lease);

eureka-client 定期向 eureka-server 发送租约(renew),避免租约过期;

默认情况下,租约有效期为 90s, 续租频率为 30s; 即:保证在网络异常的情况下,有三次重试的机会;

宏观续租

Eureka 在初始化时,会创建心跳线程,固定间隔向eureka-server 发起续租。实现代码如下;

DiscoveryClient

@Singleton
public class DiscoveryClient implements EurekaClient {
    
	@Inject
	DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
	                    Provider<BackupRegistry> backupRegistryProvider) {
	    // xxx ...
        
	 	// 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 	
	    
        // 初始化线程池
	    // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
	    initScheduledTasks();
        
        // xxx ...
	}

    /**
     * Initializes all scheduled tasks.
     */
    private void initScheduledTasks() {
    	// xxx ...
        
      	// 
    	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);
            
        }
    	
        // xxx ...
    
    }

}   

TimedSupervisorTask

public class TimedSupervisorTask extends TimerTask {
	public TimedSupervisorTask(String name, ScheduledExecutorService scheduler, ThreadPoolExecutor executor,
                               int timeout, TimeUnit timeUnit, int expBackOffBound, Runnable task) {
        this.scheduler = scheduler;
        this.executor = executor;
        this.timeoutMillis = timeUnit.toMillis(timeout);
        this.task = task;
        this.delay = new AtomicLong(timeoutMillis);
        this.maxDelay = timeoutMillis * expBackOffBound;

        // Initialize the counters and register.
        successCounter = Monitors.newCounter("success");
        timeoutCounter = Monitors.newCounter("timeouts");
        rejectedCounter = Monitors.newCounter("rejectedExecutions");
        throwableCounter = Monitors.newCounter("throwables");
        threadPoolLevelGauge = new LongGauge(MonitorConfig.builder("threadPoolUsed").build());
        Monitors.registerObject(name, this);
    }
    
    @Override
    public void run() {
        Future<?> future = null;
        try {
            // 执行任务
            future = executor.submit(task);
            threadPoolLevelGauge.set((long) executor.getActiveCount());
            future.get(timeoutMillis, TimeUnit.MILLISECONDS);  // block until done or timeout
            delay.set(timeoutMillis);
            threadPoolLevelGauge.set((long) executor.getActiveCount());
            successCounter.increment();
        } 
        
        // xxx ...
        
        // 超时后取消任务
        finally {
            if (future != null) {
                future.cancel(true);
            }
			
            // 如果scheduler 没有关闭,再次延迟执行续租
            if (!scheduler.isShutdown()) {
                scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
            }
        }
    }
}

如上述代码,续租的宏观逻辑如下图:

续租细节

DiscoveryClient.HeartbeatThread

 private class HeartbeatThread implements Runnable {

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

com.netflix.discovery.DiscoveryClient#renew

    /**
     * Renew with the eureka service by making the appropriate REST call
     */
    boolean renew() {
        EurekaHttpResponse<InstanceInfo> httpResponse;
        try {
            // 向eureka-server 发送心跳 续租
            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;
        }
    }

3. 服务下线

4. 面试怎么问?

4.1 客户端如何向服务端注册?

在客户端启动的时候,会创建一个保持心跳的定时任务,定时去和服务器发送心跳;

在第一次启动的手,如果心跳是 404; 标识:eureka 上没有这个服务;

客户端会发起注册,将自己的 ip、端口、实例id 注册上;

每个30秒一次,如果90s 内还没有,就认为这个服务挂了;

4.2 服务端如何保存客户端信息?

服务器上,是将客户端是数据保存在 ConconcrentHashMap 中;

4.3 客户端如何拉取服务端已保存的服务数据(是需要的时候去拉取,还 是先拉取保存到本地,使用的时候直接从本地获取)?

客户端拉取服务信息是通过定时任务拉取的,每次拉取的时候刷新本地的副本;

使用的时候直接从本地获取;

4.4 如何搭建高可用的Eureka 集群?

仅需要在eureka 上配置 其他注册中心地址就行;

这些注册中心会 进行通信; 同步所有的示例服务;

4.5 什么事服务续约?

心跳每隔30秒一次,如果90s 内还没有,就认为这个服务挂了;

4.6 神马事失效剔除?

心跳没有续上,90s 内,没发送心跳; eureka 会把这个服务放到剔除列表里面;

开启一个定时任务;对失效的服务进行剔除;

4.7 啥是cap ,并说明Eureka 包含CAP 中的哪些?

C: 一致性

A:可用性

P: 分区容错

Eureka 保证 一个注册中心挂了后,服务还能用,还可以向其他服务中注册;

4.8 Eureaka 的负载均衡策略有哪些?

随机

加权随机

轮询

加权轮询

最低并发策略

区域负载均衡

  • 在多个区进行部署时,可以根据各个区域的服务处理能力来选择示例;

重试策略

参见


Eureka源码分析之 Client的启动流程

eureka 注册流程分析