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最重要的几个点:
- eureka的rest接口文档, 可能版本不同会有所差异,不过大致相同
- eureka本身的缓存机制
- 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的注册和服务发现已经介绍完毕了, 可能还有遗漏或者是讲错的地方,希望看到的朋友可以指出.
最后来个简单的拓扑图