SpringCloud Netflix eureka 原理分析

  • SpringCloud Netflix eureka 原理分析
  • 前置说明
  • SpringCloud eureka
  • eureka 服务端的启动
  • springcloud ServiceRegistry
  • springcloud DiscoveryClient
  • 结束语


SpringCloud Netflix eureka 原理分析

前置说明

源码来自springcloud.E版本, 这里和上篇文章用的不是一个版本,不过大体也不会差太多
以下说明是个人观点,如有错误,欢迎评论处进行讨论

SpringCloud eureka

springcloud提供了分布式配置中心, 支持zookeeper, eureka, consul等, 本文就eureka作为注册中心来探究一下,springcloud如何实现注册和订阅服务的. 选择eureka的主要原因是, eureka的服务端被彻底的集成在springcloud中,只需要的一个注解@EnableEurekaServer 就能开启,便于我们分析服务端的大致实现.

当然,按照惯例,我们需要带着关键点去分析, 首先肯定是eureka 的服务端服务是如何启动的, 以及客户端如何进行注册服务到eureka上的, 以及客户端如何拉取服务(关于ribbon 的负载均衡就留到下次再分析)

eureka 服务端的启动

也是按照惯例, eureka 的启动肯定也需要一个自动配置类, 于是我们可以找到一个类EurekaServerAutoConfiguration, 简略代码如下

@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {

...
	
	//这个类和eureka节点间的进行复制,保持同步的类
@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());
	}

	// 这个类是用来管理节点的生命周期
	@Bean
	@ConditionalOnMissingBean
	public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry,
			ServerCodecs serverCodecs) {
		return new PeerEurekaNodes(registry, this.eurekaServerConfig,
				this.eurekaClientConfig, serverCodecs, this.applicationInfoManager);
	}

	// 这个类是门面类, 包含了上述的类, 并且对上述bean 的初始化等操作
	@Bean
	public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
			PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
		return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
				registry, peerEurekaNodes, this.applicationInfoManager);
	}
	// spring定义的启动类
	@Bean
	public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
			EurekaServerContext serverContext) {
		return new EurekaServerBootstrap(this.applicationInfoManager,
				this.eurekaClientConfig, this.eurekaServerConfig, registry,
				serverContext);
	}


	// 这个bean 很明显就是提供 dashboard 访问页面的接口, 而eureka本身的服务是通过Jersey进行暴露的,所以不是走springmvc
	@Bean
	@ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true)
	public EurekaController eurekaController() {
		return new EurekaController(this.applicationInfoManager);
	}
	
	//下面还有一些 和jersey相关的就不介绍了, 主要也是和springmvc一样,扫描一些类似controller的类,进行服务提供
	...

}

看完这个类, 发现这个类还导入一个配置类,EurekaServerInitializerConfiguration,

@Configuration
public class EurekaServerInitializerConfiguration
		implements ServletContextAware, SmartLifecycle, Ordered {
...

	@Override
	public void start() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					// 初始化整个eureka, 包括同步其他的节点
					eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
					log.info("Started Eureka Server"); //打印eureka的服务成功
					// 下面就是发布一下spring的事件 以及 改变运行状态
					publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
					EurekaServerInitializerConfiguration.this.running = true;
					publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
				}
				catch (Exception ex) {
				
					log.error("Could not initialize Eureka servlet context", ex);
				}
			}
		}).start();
	}

	@Override
	public void stop() {
		this.running = false;
		eurekaServerBootstrap.contextDestroyed(this.servletContext);
	}
	@Override
	public void stop(Runnable callback) {
		callback.run();
	}


}

其实也就是借助spring容器来管理 eureka 的启动的生命周期了, 因为spring去整合eureka来实现自动化配置,所以会有很多bean看起来很晕,这里可以建议直接看 eureka原生的bootstrap , 这里的对象初始化和启动就非常的清晰了.
那么服务端的启动就介绍到这, 其实本质就是一个servlet的项目,然后借助springboot内嵌的tomcat去完成一个启动,剩下的就是接受客户端的调用进行注册,以及高可用下的各个eureka节点直接的注册和同步.

eureka 这里介绍最重要的几个类 ApplicationResource 可以理解为一个服务注册的controller, 以及 AbstractInstanceRegistry为服务注册的核心的抽象类, PeerAwareInstanceRegistryImpl抽象注册类的实现,主要用于同步节点,还有就是ResponseCacheImpl这个eureka用于高性能体现的缓存机制.

@Produces({"application/xml", "application/json"})
public class ApplicationResource {
	// 获取实例接口
	@GET
    public Response getApplication(@PathParam("version") String version,
                                   @HeaderParam("Accept") final String acceptHeader,
                                   @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept) {
      ...
        Key cacheKey = new Key(
                Key.EntityType.Application,
                appName,
                keyType,
                CurrentRequestVersion.get(),
                EurekaAccept.fromString(eurekaAccept)
        );

        String payLoad = responseCache.get(cacheKey);

        if (payLoad != null) {
            logger.debug("Found: {}", appName);
            return Response.ok(payLoad).build();
        } else {
            logger.debug("Not Found: {}", appName);
            return Response.status(Status.NOT_FOUND).build();
        }
    }

	// 注册的接口
	@POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                                @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
        // validate that the instanceinfo contains all the necessary required fields
        ...
 		// 一堆有的没的处理,然后调用注解方法
        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }
}

// eureka对服务实例进行多层次的缓存, 提升性能,降低单个map存储高并发下的性能问题
public class ResponseCacheImpl implements ResponseCache {
 	private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>();
 	private final LoadingCache<Key, Value> readWriteCacheMap;
 
	@VisibleForTesting
    Value getValue(final Key key, boolean useReadOnlyCache) {
        Value payload = null;
        try {
            if (useReadOnlyCache) {
                final Value currentPayload = readOnlyCacheMap.get(key);
                if (currentPayload != null) {
                    payload = currentPayload;
                } else {
                    payload = readWriteCacheMap.get(key);
                    readOnlyCacheMap.put(key, payload);
                }
            } else {
                payload = readWriteCacheMap.get(key);
            }
        } catch (Throwable t) {
            logger.error("Cannot get value for key :" + key, t);
        }
        return payload;
    }
}

// 该类主要是和其他eureka进行节点同步的
@Singleton
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
  @Override
    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();
        }
        // 调用父类的注册接口
        super.register(info, leaseDuration, isReplication);
        // 同步其他节点
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    }
}

// 抽象注册类
public abstract class AbstractInstanceRegistry implements InstanceRegistry {

// 具体的注册方法
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {
            read.lock(); // 获取锁
            // registry为存放服务注册map, 存放着最新的服务列表
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            // 获取对应id 的服务实例
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    registrant = existingLease.getHolder();
                }
            } else { //如果是新注册的
                // The lease does not exist and hence it is a new registration
                synchronized (lock) {
                    if (this.expectedNumberOfRenewsPerMin > 0) {
                        this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
                        this.numberOfRenewsPerMinThreshold =
                                (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
                    }
                }
            }
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            gMap.put(registrant.getId(), lease); //放入
            synchronized (recentRegisteredQueue) {
                recentRegisteredQueue.add(new Pair<Long, String>( //添加到最新注册队列
                        System.currentTimeMillis(),
                        registrant.getAppName() + "(" + registrant.getId() + ")"));
            }
            // This is where the initial state transfer of overridden status happens
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
               
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                    overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            // Set the status based on the overridden status rules
            InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            // If the lease is registered with UP status, set lease service up timestamp
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
   
        } finally {
            read.unlock();
        }
    }
}

这里建议读者可以自行了解一下eureka最重要的几个点:

  1. eureka的rest接口文档, 可能版本不同会有所差异,不过大致相同
  2. eureka本身的缓存机制
  3. eureka 的节点间的实例复制聊聊eureka的PeerAwareInstanceRegistryImpl

接下来看客户端注册的实现.

springcloud ServiceRegistry

springcloud对客户端注册, 提供了很明显的接口设计 ServiceRegistry 这个接口是服务注册接口, Registration这个接口为注册实例的接口, 那么具体来看一下springcloud如何实现客户端的自动注册

这次主要的自动配置类是EurekaClientAutoConfiguration

@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@Import(DiscoveryClientOptionalArgsConfiguration.class)
@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class,
		CommonsClientAutoConfiguration.class, ServiceRegistryAutoConfiguration.class })
@AutoConfigureAfter(name = {"org.springframework.cloud.autoconfigure.RefreshAutoConfiguration",
		"org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration",
		"org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration"})
public class EurekaClientAutoConfiguration {

...

// 这个bean就是实现了ServiceRegistry的注册实例
@Bean
	public EurekaServiceRegistry eurekaServiceRegistry() {
		return new EurekaServiceRegistry();
	}
}

// eureka 客户端注册器
@Bean(destroyMethod = "shutdown")
		@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
		public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config) {
			return new CloudEurekaClient(manager, config, this.optionalArgs,
					this.context);
		}

// 存放 eureka客户端注册器
@Bean
	@ConditionalOnBean(AutoServiceRegistrationProperties.class)
	@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
	public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient, CloudEurekaInstanceConfig instanceConfig, ApplicationInfoManager applicationInfoManager) {
		return EurekaRegistration.builder(instanceConfig)
				.with(applicationInfoManager)
				.with(eurekaClient)
				.with(healthCheckHandler)
				.build();
	}

// 该类实现了spring的生命周期接口,会在启动的时候调用start方法
@Bean
	@ConditionalOnBean(AutoServiceRegistrationProperties.class)
	@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
	public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationContext context, EurekaServiceRegistry registry, EurekaRegistration registration) {
		return new EurekaAutoServiceRegistration(context, registry, registration);
	}

}

// 代表eureka 客户端启动注册的生命周期管理器
public class EurekaAutoServiceRegistration implements AutoServiceRegistration, SmartLifecycle, Ordered {
@Override
	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) {
			// 具体的注册接口, 里面会通过创建心跳任务,然后就一系列操作最后通过DiscoveryClient的registry进行注册
			this.serviceRegistry.register(this.registration);

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

没错客户端的注册就是这么简单,下面的服务发现也是很简单的

springcloud DiscoveryClient

springcloud一样为发现提供了抽象接口 DiscoveryClient, 不要和eureka本身的DiscoveryClient搞混了, 具体还是在EurekaClientAutoConfiguration这个自动配置类中

@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@Import(DiscoveryClientOptionalArgsConfiguration.class)
@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class,
		CommonsClientAutoConfiguration.class, ServiceRegistryAutoConfiguration.class })
@AutoConfigureAfter(name = {"org.springframework.cloud.autoconfigure.RefreshAutoConfiguration",
		"org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration",
		"org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration"})
public class EurekaClientAutoConfiguration {
	@Bean
	public DiscoveryClient discoveryClient(EurekaInstanceConfig config, EurekaClient client) {
		return new EurekaDiscoveryClient(config, client);
	}
}

// springcloud 服务发现的实现类
public class EurekaDiscoveryClient implements DiscoveryClient {

// 获取所有实例
	@Override
	public List<ServiceInstance> getInstances(String serviceId) {
		List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,
				false);
		List<ServiceInstance> instances = new ArrayList<>();
		for (InstanceInfo info : infos) {
			instances.add(new EurekaServiceInstance(info));
		}
		return instances;
	}
}

// 获取所有的服务列表名称
@Override
	public List<String> getServices() {
		Applications applications = this.eurekaClient.getApplications();
		if (applications == null) {
			return Collections.emptyList();
		}
		List<Application> registered = applications.getRegisteredApplications();
		List<String> names = new ArrayList<>();
		for (Application app : registered) {
			if (app.getInstances().isEmpty()) {
				continue;
			}
			names.add(app.getName().toLowerCase());

		}
		return names;
	}

没错服务发现就是这么简单, 获取服务会通过其本地缓存的列表, 然后会有个定时任务去从服务端获取(这里明显感觉到深深的恶意, 各种缓存,这样一个服务掉线要很久才能察觉到哦), 下面简单介绍eureka的DiscoveryClient几个方法

@Singleton // springcloud并没有使用到Guice, 所以这个注解是无效的,但是spring也会保证单例
public class DiscoveryClient implements EurekaClient {
    private final AtomicReference<Applications> localRegionApps = new AtomicReference<Applications>();

// 刷新本地服务缓存的任务
 class CacheRefreshThread implements Runnable {
        public void run() {
            refreshRegistry();
        }
    }
    // 根据实例id 获取服务列表的方法
    @Override
    public List<InstanceInfo> getInstancesById(String id) {
        List<InstanceInfo> instancesList = new ArrayList<InstanceInfo>();
        for (Application app : this.getApplications()
                .getRegisteredApplications()) {
            InstanceInfo instanceInfo = app.getByInstanceId(id);
            if (instanceInfo != null) {
                instancesList.add(instanceInfo);
            }
        }
        return instancesList;
    }
...
}

结束语

到这里的springcloud netflix eureka的注册和服务发现已经介绍完毕了, 可能还有遗漏或者是讲错的地方,希望看到的朋友可以指出.

最后来个简单的拓扑图

springcloud服务注册中心eureka底层源码 springcloud注册服务原理_服务端