Eureka作为服务注册中心对整个微服务架构起着最核心的整合作用,因此对Eureka还是有很大的必要进行深入研究。
Eureka 1.x版本是纯基于servlet的应用。为了与spring cloud结合使用,除了本身eureka代码,还有个粘合模块spring-cloud-netflix-eureka-server。在我们启动EurekaServer实例的时候,只用加入对于spring-cloud-starter-eureka-server的依赖即可。之后通过@EnableEurekaServer注解即可启动一个Eureka服务器实例。先来看看这个注解是如何启动一个Eureka服务的
Eureka启动,原生启动与SpringCloudEureka启动异同
我们先看看作为原生的EurekaServer启动的过程,作为一个Servlet应用,他的启动入口就是他的主要ServletContextListener类(这里是EurekaBootStrap)的contextInitialized方法
@Override
public void contextInitialized(ServletContextEvent event) {
try {
initEurekaEnvironment();
initEurekaServerContext();
ServletContext sc = event.getServletContext();
sc.setAttribute(EurekaServerContext.class.getName(), serverContext);
} catch (Throwable e) {
logger.error("Cannot bootstrap eureka server :", e);
throw new RuntimeException("Cannot bootstrap eureka server :", e);
}
}
可以看出主要做了两件事,initEurekaEnvironment()与initEurekaServerContext()
对于initEurekaEnvironment()只是初始化一些必要的环境变量,由于Eureka配置基于Spring的配置中间件Archaius,这些环境变量都是针对这个配置中间件使用的。
initEurekaServerContext()是我们重点需要关心的,它初始化了EurekaServer需要的所有组件:
protected void initEurekaServerContext() throws Exception {
EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();
//设置json与xml序列化工具
JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
logger.info("Initializing the eureka client...");
logger.info(eurekaServerConfig.getJsonCodecName());
ServerCodecs serverCodecs = new DefaultServerCodecs(eurekaServerConfig);
ApplicationInfoManager applicationInfoManager = null;
//初始化EurekaClient,EurekaClient用来与其他EurekaServer进行交互
//有可能通过guice初始化Eureka,这时eurekaClient和ApplicationInfoManager通过依赖注入先被初始化
if (eurekaClient == null) {
EurekaInstanceConfig instanceConfig = isCloud(ConfigurationManager.getDeploymentContext())
? new CloudInstanceConfig()
: new MyDataCenterInstanceConfig();
applicationInfoManager = new ApplicationInfoManager(
instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());
EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();
eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
} else {
applicationInfoManager = eurekaClient.getApplicationInfoManager();
}
//初始化PeerAwareInstanceRegistry, 这个类里面的方法就是与集群内其他EurekaServer实例保持业务同步的机制
PeerAwareInstanceRegistry registry;
if (isAws(applicationInfoManager.getInfo())) {
registry = new AwsInstanceRegistry(
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
eurekaClient
);
awsBinder = new AwsBinderDelegate(eurekaServerConfig, eurekaClient.getEurekaClientConfig(), registry, applicationInfoManager);
awsBinder.start();
} else {
registry = new PeerAwareInstanceRegistryImpl(
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
eurekaClient
);
}
//初始化PeerEurekaNodes,里面有定时维护Eureka集群的业务逻辑
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
registry,
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
applicationInfoManager
);
//初始化EurekaServer上下文
serverContext = new DefaultEurekaServerContext(
eurekaServerConfig,
serverCodecs,
registry,
peerEurekaNodes,
applicationInfoManager
);
EurekaServerContextHolder.initialize(serverContext);
serverContext.initialize();
logger.info("Initialized server context");
//从其他节点中读取注册信息,并开放服务注册
// Copy registry from neighboring eureka node
int registryCount = registry.syncUp();
registry.openForTraffic(applicationInfoManager, registryCount);
// Register all monitoring statistics.
EurekaMonitors.registerAllStats();
}
总结下来,总共如下几点:
1.初始化并设置序列化反序列化工具
2.初始化通信客户端EurekaClient
3.初始化集群通信类PeerAwareInstanceRegistry与PeerEurekaNodes
4.初始化EurekaServer上下文serverContext
5.从其他节点中读取注册信息,并开放服务注册
然后,由于原生的EurekaServer利用Jersey框架初始化restApi,这里还有:
6.载入Jersey,初始化Restful服务api
我们先不谈就里面的细节,先看看在Spring-cloud下的eureka初始化是否有区别:
对于胶水代码,实现了大致同样的但是略微有些区别的功能:
@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {
}
@EnableEurekaServer注解主要是引入EurekaServerMarkerConfiguration这个配置类,而这个配置类也很简单:
@Configuration
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
return new Marker();
}
class Marker {
}
}
这个符合基本上所有Spring-cloud生态圈的starter套路。通过一个类似于Marker的bean来启用某个组件。核心启动通过ConditionalOnBean来加载某些配置,在这里这个类是:
EurekaServerAutoConfiguration,由于Eurekaserver本身是一个Servlet应用,这个类相当于胶水代码,将Eurekaserver需要初始化的类载入到Spring容器中管理。对于一个Servlet应用,主要初始化入口就是实现ServletContextListener的类,对于Eurekaserver是EurekaBootStrap,EurekaBootStrap初始化Eurekaserver需要的类。EurekaServerAutoConfiguration相当于将EurekaBootStrap初始化的类也初始化,同时载入到Spring容器中管理。
同时,由于原有的EurekaServer的接口依赖Jersey,这里的EurekaServerAutoConfiguration也要扫描Jersey实现其应该暴露的接口。同时,spring-cloud-starter-eureka-server有自己的界面,并没有使用原有的Eureka界面,也是在这个类里面加载的配置。
所以,这里加载的Bean有:
1.Eureka DashBoard,其实就是一个Controller:
这个控制台就是我们通过springcloud启动eureka之后,通过浏览器访问eureka暴露的端口,看到的,例如这个:
http://eureka.didispace.com/
可以看出,这个Controller只有eureka.dashboard.enable=true的时候才会加载,如果不想启用控制台可以设置为false
@Bean
@ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true)
public EurekaController eurekaController() {
return new EurekaController(this.applicationInfoManager);
}
序列化反序列化工具:
@Bean public ServerCodecs serverCodecs() { return new CloudServerCodecs(this.eurekaServerConfig); }
初始化集群通信类PeerAwareInstanceRegistry与PeerEurekaNodes:
@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); }
初始化PeerAwareInstanceRegistry代码中,我们看到this.eurekaClient.getApplications(); 其实这段代码没有必要写,已经没有预期的作用了。我们先来看一下EurekaClient基本原理,同样的,这里也是简单过一下,日后会详细分析。
EurekaClient的默认实现是DiscoveryClient,这个可以通过查看EurekaClientAutoConfiguration看到。
EurekaClient主要功能就是两个:一个是从Eurekaserver上面获取所有注册的服务,另一个是将自己的服务注册到Eurekaserver上面。由于每次都是获取全集,所以在注册的服务非常多的时候,这个对网络流量和Eurekaserver性能消耗比较大。所以每个EurekaClient做了自己的内部缓存。两个功能的机制如图:
而this.eurekaClient.getApplications();只是简单的读取一下Eurekaserver所有注册的服务信息缓存(一个AtomicReference类),个人感觉没什么作用,猜想是原来的EurekaClient代码没有Eurekaserver所有注册的服务信息缓存,调用getApplications()就是从服务器网络读取,而后来EurekaClient更新了自己的代码,加入了缓存,而胶水代码没有更新。
而且,目前的Eureka原生代码中已经在Eurekaclient初始化的时候就强制读取一次网络获取Eurekaserver的所有注册的服务信息。这段胶水代码就更没有必要了。
之后,注意这里实现类是InstanceRegistry而不是PeerAwareInstanceRegistryImpl。InstanceRegistry继承了PeerAwareInstanceRegistryImpl,并修正了原生Eureka一些设计上的与SpringCloud不兼容的地方,而且增加了context事件为了以后做新功能做准备(猜测)。
首先,先简单过一下PeerAwareInstanceRegistry的功能,日后我们还会更细致的剖析:
PeerAwareInstanceRegistry主要负责集群中每一个EurekaServer实例的服务注册信息的同时,并且实现了一个很著名很重要的机制:自我保护机制。
先介绍两个变量:expectedNumberOfRenewsPerMin和numberOfRenewsPerMinThreshold。其中numberOfRenewsPerMinThreshold就是RenewalPercentThreshold*numberOfRenewsPerMinThreshold;RenewalPercentThreshold是可配置的一个介于0,1之间double类型参数。还有一个计数变量renewsLastMin,记录了上一分钟收到的renew请求(服务维持注册)的次数
这个自我保护机制是这样的:
每次每个服务新注册时,会给expectedNumberOfRenewsPerMin加2的原因是默认半分钟服务向Eurekaserver心跳Renew一次。
还有另一个机制,就是在启动时,默认会从其他集群节点上面读取所有服务注册信息。如果一个节点都没有访问成功(例如这个启动的节点就是集群中的第一个节点),这时peerInstancesTransferEmptyOnStartup就会为true,就会禁止用户注册,直到集群中有其他节点或者超过WaitTimeInMsWhenSyncEmpty设置的时间。
这个机制显然不够友好,所以胶水代码初始化PeerAwareInstanceRegistry的扩展InstanceRegistry,加入了两个配置参数,
ExpectedNumberOfRenewsPerMin和DefaultOpenForTrafficCount。ExpectedNumberOfRenewsPerMin默认为1,这个为了初始化
expectedNumberOfRenewsPerMin为一个大于0的数,这样expectedNumberOfRenewsPerMin在单机模式下也会刷新,这个很简单,看一下代码就知道,这里不再赘述。重点说一下DefaultOpenForTrafficCount,这个默认为1。看下InstanceRegistry的代码:
public InstanceRegistry(EurekaServerConfig serverConfig,
EurekaClientConfig clientConfig, ServerCodecs serverCodecs,
EurekaClient eurekaClient, int expectedNumberOfRenewsPerMin,
int defaultOpenForTrafficCount) {
super(serverConfig, clientConfig, serverCodecs, eurekaClient);
this.expectedNumberOfRenewsPerMin = expectedNumberOfRenewsPerMin;
this.defaultOpenForTrafficCount = defaultOpenForTrafficCount;
}
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
super.openForTraffic(applicationInfoManager,
count == 0 ? this.defaultOpenForTrafficCount : count);
}
构造器初始化配置,openForTraffic在原生代码是之前提到的EurekaBootstrap contextInitialized代码调用的;这里是在EurekaServerInitializerConfiguration里面初始化。这里的openForTraffic将原来为0的参数改为defaultOpenForTrafficCount就是1,传入原来的openForTraffic方法。这样保证了Eurekaserver即使是单例也能立刻正常工作;因为在单利模式下,原来的openForTraffic方法传入的参数为0(可以参考之前列出的EurekaBootstrap contextInitialized代码)
4.Eureka运行上下文
@Bean
public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
registry, peerEurekaNodes, this.applicationInfoManager);
}
5.Eureka启动类
@Bean
public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
EurekaServerContext serverContext) {
return new EurekaServerBootstrap(this.applicationInfoManager,
this.eurekaClientConfig, this.eurekaServerConfig, registry,
serverContext);
}
6.Jersey暴露接口初始化
之后我们会重点关注暴露的接口
@Bean
public FilterRegistrationBean jerseyFilterRegistration(
javax.ws.rs.core.Application eurekaJerseyApp) {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new ServletContainer(eurekaJerseyApp));
bean.setOrder(Ordered.LOWEST_PRECEDENCE);
bean.setUrlPatterns(
Collections.singletonList(EurekaConstants.DEFAULT_PREFIX + "/*"));
return bean;
}
/** * Construct a Jersey {@link javax.ws.rs.core.Application} with all the resources * required by the Eureka server. */
@Bean
public javax.ws.rs.core.Application jerseyApplication(Environment environment,
ResourceLoader resourceLoader) {
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
false, environment);
// Filter to include only classes that have a particular annotation.
//
provider.addIncludeFilter(new AnnotationTypeFilter(Path.class));
provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class));
// Find classes in Eureka packages (or subpackages)
//
Set<Class<?>> classes = new HashSet<Class<?>>();
for (String basePackage : EUREKA_PACKAGES) {
Set<BeanDefinition> beans = provider.findCandidateComponents(basePackage);
for (BeanDefinition bd : beans) {
Class<?> cls = ClassUtils.resolveClassName(bd.getBeanClassName(),
resourceLoader.getClassLoader());
classes.add(cls);
}
}
// Construct the Jersey ResourceConfig
//
Map<String, Object> propsAndFeatures = new HashMap<String, Object>();
propsAndFeatures.put(
// Skip static content used by the webapp
ServletContainer.PROPERTY_WEB_PAGE_CONTENT_REGEX,
EurekaConstants.DEFAULT_PREFIX + "/(fonts|images|css|js)/.*");
DefaultResourceConfig rc = new DefaultResourceConfig(classes);
rc.setPropertiesAndFeatures(propsAndFeatures);
return rc;
}
@Bean
public FilterRegistrationBean traceFilterRegistration(
@Qualifier("webRequestLoggingFilter") Filter filter) {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(filter);
bean.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return bean;
}
以上就是Eureka初始化基本流程,下一张我们会更深入针对每个组件进行分析