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启动完毕。