springcloud-eureka架构解读和启动原理
1 架构解读
参考:https://blog.csdn.net/cpongo4/article/details/89119437?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&utm_relevant_index=1
微服务架构最核心的是服务治理,服务治理最基础的组件是注册中心。比较流行的注册中心有:zookeeper和eureka,
两者区别:
ZK的设计原则是CP,即强一致性和分区容错性。它保证数据强一致性,但舍弃了可用性,如果出现网络问题可能会影响ZK的选举,导致ZK注册中心不可用。
eureka的设计原则是AP,即可用性和分区容错性。它保证了注册中心的可用性,但舍弃了数据一致性。
1.1 Eureka部署多机房的总体架构
组件调用关系:
服务提供者
启动后,向注册中心发起register请求,注册服务
在运行过程中,定时向注册中心发送renew心跳,证明“我还活着”。
停止服务提供者,向注册中心发起cancel请求,清空当前服务注册信息。
服务消费者
启动后,从注册中心拉取服务注册信息
在运行过程中,定时更新服务注册信息。
服务消费者发起远程调用:
服务消费者(北京)会从服务注册信息中选择同机房的服务提供者(北京),发起远程调用。只有同机房的服务提供者挂了才会选择其他机房的服务提供者(青岛)。
服务消费者(天津)因为同机房内没有服务提供者,则会按负载均衡算法选择北京或青岛的服务提供者,发起远程调用。
注册中心
启动后,从其他节点拉取服务注册信息。
运行过程中,定时运行evict任务,剔除没有按时renew的服务(包括非正常停止和网络故障的服务)。
运行过程中,接收到的register、renew、cancel请求,都会同步至其他注册中心节点。
1.2 数据存储结构
Eureka数据存储于内存中。
Eureka数据存储分为两层:数据存储层和缓存层。Eureka client在拉取服务信息时,先从缓存层获取,如果获取不到再把数据存储层的数据加载到缓存,再获取。值得注意的是,数据存储层的数据结构是服务信息,而缓存层保存的是经过处理加工过的,可以直接传输到Eureka client的数据结构。
数据存储层:
是一个ConcurrentHashMap registry;key是spring.application.name,value是一个ConcurrentHashMap。
内层的Map,key是InstanceId,value是一个Lease对象。Lease对象包含了服务详情。
缓存层:
一级缓存:ConcurrentHashMap<Key,Value> readOnlyCacheMap,本质上是HashMap,无过期时间,保存服务信息的对外输出数据结构。
二级缓存:Loading<Key,Value> readWriteCacheMap,本质上是guava的缓存,包含失效机制,保存服务信息的对外输出数据结构。
注:guava是一个本地缓存机制,支持多线程并发写入,过期策略,淘汰策略。
1.3 服务注册机制
1.4 服务续约机制
1.5 服务注销机制
1.6 服务剔除机制
1.7 服务获取机制
1.8 服务同步机制
这些机制会在后面陆续补充
2 原生eureka介绍
eureka是netflix公司开源的产品,spring-cloud整合了它。其实完全可以不依赖springcloud单独使用eureka
github下载源码,https://github.com/Netflix/eureka,使用idea打开,其目录结构如下
其中比较重要的几个module是eureka-server、eureka-core、eureka-client。
2.1 eureka-server
该module很简单,只有几个配置文件,我们会把这个module打包成war包,部署到tomcat中
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <listener> <listener-class>com.netflix.eureka.EurekaBootStrap</listener-class> </listener> <filter> <filter-name>statusFilter</filter-name> <filter-class>com.netflix.eureka.StatusFilter</filter-class> </filter> <filter> <filter-name>requestAuthFilter</filter-name> <filter-class>com.netflix.eureka.ServerRequestAuthFilter</filter-class> </filter> <filter> <filter-name>rateLimitingFilter</filter-name> <filter-class>com.netflix.eureka.RateLimitingFilter</filter-class> </filter> <filter> <filter-name>gzipEncodingEnforcingFilter</filter-name> <filter-class>com.netflix.eureka.GzipEncodingEnforcingFilter</filter-class> </filter> <filter> <filter-name>jersey</filter-name> <filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class> <init-param> <param-name>com.sun.jersey.config.property.WebPageContentRegex</param-name> <param-value>/(flex|images|js|css|jsp)/.*</param-value> </init-param> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>com.sun.jersey;com.netflix</param-value> </init-param> <!-- GZIP content encoding/decoding --> <init-param> <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name> <param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value> </init-param> <init-param> <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name> <param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value> </init-param> </filter> <filter-mapping> <filter-name>statusFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>requestAuthFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Uncomment this to enable rate limiter filter. <filter-mapping> <filter-name>rateLimitingFilter</filter-name> <url-pattern>/v2/apps</url-pattern> <url-pattern>/v2/apps/*</url-pattern> </filter-mapping> --> <filter-mapping> <filter-name>gzipEncodingEnforcingFilter</filter-name> <url-pattern>/v2/apps</url-pattern> <url-pattern>/v2/apps/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>jersey</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <welcome-file-list> <welcome-file>jsp/status.jsp</welcome-file> </welcome-file-list> </web-app>
在这个web.xml文件中有一个监听器listener,EurekaBootStrap,它实现了ServletContextListener接口,我们知道ServletContextListener的工作原理是,当tomcat启动时,会调用其contextInitialized方法,当tomcat停止时,会调用其contextDestroyed方法。
EurekaBootStrap是eureka-core中的类。
EurekaBootStrap.java
public class EurekaBootStrap implements ServletContextListener { @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); } } protected void initEurekaEnvironment() throws Exception { } protected void initEurekaServerContext() throws Exception { } @Override public void contextDestroyed(ServletContextEvent event) { try { logger.info("{} Shutting down Eureka Server..", new Date()); ServletContext sc = event.getServletContext(); sc.removeAttribute(EurekaServerContext.class.getName()); destroyEurekaServerContext(); destroyEurekaEnvironment(); } catch (Throwable e) { logger.error("Error shutting down eureka", e); } logger.info("{} Eureka Service is now shutdown...", new Date()); } }
这里初始化eureka环境和eurekaserver上下文。
3 启动原理
springboot中使用Eureka Server的方式很简单,只要在启动类上加上 @EnableEurekaServer 注解即可,例如
@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
现在我们来分析其启动原理。
2.1 @EnableEurekaServer
注解 @EnableEurekaServer 就是一个开关,或者一个Marker,加上该注解,表示我们需要加载Eureka相关的类,并注入到spring容器。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(EurekaServerMarkerConfiguration.class) public @interface EnableEurekaServer { }
当springboot启动时,会进行注解扫描,然后加载 @Import 注解引入的类,不明白springboot启动过程的可以看这一个思维导图https://www.cnblogs.com/zhenjingcool/p/15853923.html
@Import 注解引入 EurekaServerMarkerConfiguration 类。
@Configuration public class EurekaServerMarkerConfiguration { @Bean public Marker eurekaServerMarkerBean() { return new Marker(); } class Marker { } }
这个类很简单,就是注入一个 Marker 类,作为一个开关作用,在后面会用得到。
2.2 eureka-server的spring.factories
除了 @EnableEurekaServer 注解之外,我们还需要引入 spring-cloud-netflix-eureka-server 包,该包中有一个spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
springboot启动时会扫描CLASSPATH下的spring.factories文件,然后注入到容器。jar包中的classpath具体为什么路径,见https://www.cnblogs.com/zhenjingcool/p/15856424.html
我们来看一下 EurekaServerAutoConfiguration 这个类
@Configuration @Import(EurekaServerInitializerConfiguration.class) @ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class) @EnableConfigurationProperties({ EurekaDashboardProperties.class, InstanceRegistryProperties.class }) @PropertySource("classpath:/eureka/server.properties") public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter { }
我们首先看一下这个注解 @ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class) ,当容器中有 Marker 类时才注入 EurekaServerAutoConfiguration 类。
从2.1我们已经知道,我们已经向容器中注入了 Marker ,所以 EurekaServerAutoConfiguration 配置类以及其中的bean会被注入。
下面我们分析EurekaServerAutoConfiguration中配置的几个与Eureka server启动相关的bean
2.3 jerseyApplication
这里返回一个javax.ws.rs.core.Application类型的Jersey容器。这里遍历com.netflix.eureka包下的所有类,找到有 @Path 注解和 @Provider 注解的类classes,然后生成一个DefaultResourceConfig实例返回。其中DefaultResourceConfig是javax.ws.rs.core.Application的子类。
注:javax.ws.rs.core.Application是jsr311-api-1.1.1.jar中的类,引入该jar目的是使项目支持Jersey框架。Jersey框架是一个restful web service框架,类似于struct框架。
@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<>(); 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<>(); 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; }
扫描到的有 @Path 注解和 @Provider 注解的类都在eureka-core包中的resources路径下,该路径下放的是restful风格的资源,比如 ApplicationResource 中获取实例信息的接口
@Path("{id}") public InstanceResource getInstanceInfo(@PathParam("id") String id) { return new InstanceResource(this, id, serverConfig, registry); }
在这里,我们在springcloud中关联到eureka暴露的restful接口,比如服务注册、服务续约、服务下线等接口。
2.4 FilterRegistrationBean
在这里会实例化一个Jersey容器,它是一个web服务器,为springcloud提供eureka-core中的restful接口,比如上面提到的服务注册、服务续约、服务下线等接口。
其中参数eurekaJerseyApp就是2.3中的bean实例。
@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; }
其中 new ServletContainer(eurekaJerseyApp) 为创建Jersey服务器。
2.5 EurekaController
注入一个EurekaController,里面包含一系列接口,用于仪表盘页面查询服务状态。
@Bean @ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true) public EurekaController eurekaController() { return new EurekaController(this.applicationInfoManager); }
2.6 PeerAwareInstanceRegistry
对等节点感知实例注册器,各个节点是对等的,没有主从之分。
@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()); }
所需要的参数,比如serverCodecs、this.eurekaServerConfig都是在该类中的@Bean注入的,或者在eureka-client jar包中注入的。
InstanceRegistry 中有三个比较重要的方法分别接收客户端注册、续约、下线请求。
//接收客户端注册请求
public void register(final InstanceInfo info, final boolean isReplication) { handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication); super.register(info, isReplication); } //接收客户端下线请求 public boolean cancel(String appName, String serverId, boolean isReplication) { handleCancelation(appName, serverId, isReplication); return super.cancel(appName, serverId, isReplication); } //接收客户端续约请求 public boolean renew(final String appName, final String serverId, boolean isReplication) { log("renew " + appName + " serverId " + serverId + ", isReplication {}" + isReplication); List<Application> applications = getSortedApplications(); for (Application input : applications) { if (input.getName().equals(appName)) { InstanceInfo instance = null; for (InstanceInfo info : input.getInstances()) { if (info.getId().equals(serverId)) { instance = info; break; } } publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId, instance, isReplication)); break; } } return super.renew(appName, serverId, isReplication); }
2.7 EurekaServerBootstrap
为了整合spring和eureka,我们这里会初始化这个bean,这个bean完全模仿eureka中的EurekaBootstrap,使得eureka能够在springboot内嵌的tomcat中使用。
@Bean public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry, EurekaServerContext serverContext) { return new EurekaServerBootstrap(this.applicationInfoManager, this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext); }
但是奇怪的是,原生eureka中的EurekaBootstrap实现了ServletContextListener接口,使得tomcat启动后会自动执行contextInitialized方法,但是EurekaServerBootstrap没有实现这个接口,那其contextInitialized方法在哪里调用的呢?其实是在EurekaServerInitializerConfiguration中调用的,EurekaServerInitializerConfiguration实现了SmartLifecycle接口,在spring初始化完成后会调用其start方法,start方法中会进行调用
eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
下面我们看一下contextInitialized方法
public void contextInitialized(ServletContext context) { try { initEurekaEnvironment(); initEurekaServerContext(); context.setAttribute(EurekaServerContext.class.getName(), this.serverContext); } catch (Throwable e) { log.error("Cannot bootstrap eureka server :", e); throw new RuntimeException("Cannot bootstrap eureka server :", e); } }
initEurekaEnvironment()
protected void initEurekaEnvironment() throws Exception { // 设置数据中心
String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER); if (dataCenter == null) { log.info("Eureka data center value eureka.datacenter is not set, defaulting to default"); ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, DEFAULT); } else { ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, dataCenter); } // 设置环境
String environment = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT); if (environment == null) { ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, TEST); log.info("Eureka environment value eureka.environment is not set, defaulting to test"); } else { ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, environment); } }
initEurekaServerContext()
protected void initEurekaServerContext() throws Exception { // For backward compatibility JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),XStream.PRIORITY_VERY_HIGH); XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),XStream.PRIORITY_VERY_HIGH); EurekaServerContextHolder.initialize(this.serverContext); log.info("Initialized server context"); // 从邻近节点复制注册表 int registryCount = this.registry.syncUp(); this.registry.openForTraffic(this.applicationInfoManager, registryCount); // 注册所有监控统计信息 EurekaMonitors.registerAllStats(); }
至此,eureka server启动完毕。