SpringCloud服務發現組件Eureka源碼解析(啓動流程)

本文從源碼視角簡述Eureka Server的啓動流程。
一.入口
eureka-server包結構如下:
在這裏插入圖片描述
可以看到,一共包含5個目錄,其中/eureka存放配置信息,/static/eureka與/templates/eureka存放靜態文件,/org目錄存放Java相關代碼,/META-INF存放應用的主要信息。
打開/META-INF中的spring.factories,內容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

@EnableAutoConfiguration註解在SpringBoot應用啓動的時候會自動加載。
二.流程
1.加載EurekaServerAutoConfiguration
由上可知,在SpringBoot應用啓動時會加載EurekaServerAutoConfiguration這個Bean。

@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
//省略以上代碼
	@Bean
	public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
			PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
		return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
				registry, peerEurekaNodes, this.applicationInfoManager);
	}
//省略以下代碼
}

根據上述代碼,發現主要內容如下:
<1>通過@Configuration註解,聲明一個配置類。
<2>通過@Import註解,導入了EurekaServerInitializerConfiguration。
<3>通過@ConditionalOnBean註解,當EurekaServerMarkerConfiguration這個Bean存在時才注入。這個非常重要,主要通過它來控制是否開啓Eureka Server。
SpringBoot在集成Eureka Server時,需要在主類上加入@EableEurekaServer註解。@EnableEurekaServer 內容如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {
}

可以看到,在@EableEurekaServer註解中導入了EurekaServerMarkerConfiguration。也就是說,加入@EableEurekaServer註解,就意味着開啓了Eureka Server。
<4>通過@EnableConfigurationProperties,@PropertySource註解,導入配置信息。
<5>定義了EurekaServerContext這個Bean。
2.加載EurekaServerInitializerConfiguration
在上一步的加載中,導入了EurekaServerInitializerConfiguration。該Bean實現了SmartLifecycle接口,當Spring容器加載完所有Bean並完成初始化之後,會回調實現該接口的類中的start方法。
<1>EurekaServerInitializerConfiguration的start方法

@Configuration
public class EurekaServerInitializerConfiguration
		implements ServletContextAware, SmartLifecycle, Ordered {
	@Override
	public void start() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
                    //1.初始化Eureka Server,並啓動
				    eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
					log.info("Started Eureka Server");
                    //2.發佈Eureka Server的註冊事件
					publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
					//3.設置Eureka Server狀態爲已啓動
					EurekaServerInitializerConfiguration.this.running = true;
					//4.發送Eureka Server的啓動事件 
					publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
				}
				catch (Exception ex) {
					// Help!
					log.error("Could not initialize Eureka servlet context", ex);
				}
			}
		}).start();
	}

<2>EurekaServerBootstrap的contextInitialized方法
在上一步,通過contextInitialized方法,初始化Eureka Server,並啓動,繼續跟蹤:

	public void contextInitialized(ServletContext context) {
		try {
		    //1.初始化Eureka環境變量
			initEurekaEnvironment();
			//2.初始化Eureka上下文
			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,initEurekaServerContext,分別跟蹤:
<3>EurekaServerBootstrap的initEurekaEnvironment方法

	protected void initEurekaEnvironment() throws Exception {
		log.info("Setting the eureka configuration..");

		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);
		}
	}

沒什麼好說的,設置一些變量值,如果沒有,就取默認值。
<4>EurekaServerBootstrap的initEurekaServerContext方法

	protected void initEurekaServerContext() throws Exception {
		//省略以上代碼
        //1.初始化Eureka Server上下文
		EurekaServerContextHolder.initialize(this.serverContext);

		log.info("Initialized server context");

		//2.從相鄰的Eureka節點複製註冊表 
		int registryCount = this.registry.syncUp();
		//3.開啓定時任務,用於清理60秒沒有心跳的客戶端
		this.registry.openForTraffic(this.applicationInfoManager, registryCount);

		//4.註冊所有監控統計信息
		EurekaMonitors.registerAllStats();
	}

通過該方法,實現了Eureka Server上下文初始化,註冊表複製以及定時任務的開啓。
3.加載DefaultEurekaServerContext
在第一步EurekaServerAutoConfiguration中,定義了一個Bean:EurekaServerContext。
這個Bean通過DefaultEurekaServerContext注入。在DefaultEurekaServerContext中,存在initialize方法,該方法被@PostConstruct註解,因此該方法會在servlet被加載時運行。
<1>DefaultEurekaServerContext的initialize方法

public class DefaultEurekaServerContext implements EurekaServerContext {
    @PostConstruct
    @Override
    public void initialize() throws Exception {
        logger.info("Initializing ...");
        //1.啓動線程,讀取其他集羣節點的信息
        peerEurekaNodes.start();
        //2.初始化其他集羣節點
        registry.init(peerEurekaNodes);
        logger.info("Initialized");
    }
    //省略其他代碼
}

<2>start方法
繼續跟蹤:如何啓動線程並讀取其他集羣節點的信息

    public void start() {
       //省略以上代碼
        try {
            //1.第一次進入時,直接更新集羣節點信息
            updatePeerEurekaNodes(resolvePeerUrls());
            //2.創建一個線程,用來更新集羣節點信息
            Runnable peersUpdateTask = new Runnable() {
                @Override
                public void run() {
                    try {
                        updatePeerEurekaNodes(resolvePeerUrls());
                    } catch (Throwable e) {
                        logger.error("Cannot update the replica Nodes", e);
                    }

                }
            };
            //3.定時執行上面創建的線程,定時更新集羣節點信息
            taskExecutor.scheduleWithFixedDelay(
                    peersUpdateTask,
                    serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
                    serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
                    TimeUnit.MILLISECONDS
            );
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
        for (PeerEurekaNode node : peerEurekaNodes) {
            logger.info("Replica node URL:  " + node.getServiceUrl());
        }
    }

在上面的分析中,可以看到:第一次進入時,直接更新集羣節點信息,後續再創建定時任務去定時執行updatePeerEurekaNodes方法。爲什麼這裏要先執行一遍呢?
因爲更新集羣節點信息時,需要先初始化Eureka集羣其他節點的列表。所以需要先調用updatePeerEurekaNodes方法做一次初始化。而定時任務執行的週期默認是10分鐘,如果等定時調度來執行初始化的話,太慢。因此需要先執行一遍。
<3>updatePeerEurekaNodes方法
最後,看下updatePeerEurekaNodes方法是如何更新集羣節點信息的。

    protected void updatePeerEurekaNodes(List<String> newPeerUrls) {
        if (newPeerUrls.isEmpty()) {
            logger.warn("The replica size seems to be empty. Check the route 53 DNS Registry");
            return;
        }

        Set<String> toShutdown = new HashSet<>(peerEurekaNodeUrls);
        toShutdown.removeAll(newPeerUrls);
        Set<String> toAdd = new HashSet<>(newPeerUrls);
        toAdd.removeAll(peerEurekaNodeUrls);
        
        //1.校驗新的URL集合與舊有的URL集合是否一致。如果一致,則不需要更新,直接返回
        if (toShutdown.isEmpty() && toAdd.isEmpty()) { // No change
            return;
        }
        
        //2.移除舊集合中不可用的節點信息
        List<PeerEurekaNode> newNodeList = new ArrayList<>(peerEurekaNodes);
        if (!toShutdown.isEmpty()) {
            logger.info("Removing no longer available peer nodes {}", toShutdown);
            int i = 0;
            while (i < newNodeList.size()) {
                PeerEurekaNode eurekaNode = newNodeList.get(i);
                if (toShutdown.contains(eurekaNode.getServiceUrl())) {
                    newNodeList.remove(i);
                    eurekaNode.shutDown();
                } else {
                    i++;
                }
            }
        }

        //3.添加新增加的節點信息
        if (!toAdd.isEmpty()) {
            logger.info("Adding new peer nodes {}", toAdd);
            for (String peerUrl : toAdd) {
                newNodeList.add(createPeerEurekaNode(peerUrl));
            }
        }

        //4.更新peerEurekaNodes與peerEurekaNodeUrls
        this.peerEurekaNodes = newNodeList;
        this.peerEurekaNodeUrls = new HashSet<>(newPeerUrls);
    }

該方法根據傳入的新集羣URL集合完成節點的更新,首先校驗新舊URL集合是否一致,如果不一致,則移除舊集合中不可用的節點信息,並添加新增加的節點信息,最後更新。

自此,Eureka Server算是啓動完畢了。
三.總結
最後,通過一個圖和一段話來總結整個過程,如下:
在這裏插入圖片描述
EurekaServerAutoConfiguration是整個Eureka Server的啓動入口,啓動內容包括:
1.導入EurekaServerInitializerConfiguration
<1>設置Eureka Server環境變量,如果沒有設置,則取默認值
<2>初始化Eureka Server上下文,主要完成註冊表的複製,開啓定時任務來定期清理客戶端
2.定義DefaultEurekaServerContext
初始化加載後,通過initialize方法創建定時任務,定時更新集羣節點信息

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章