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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值