“不积跬步,无以至千里。”
这个专题开始,我们来深度刨析一下springcloud netflix技术栈核心组件的源码,虽然目前Alibaba系会更火一些,但我个人认为,微服务的始祖是netflix系组件,所谓一通百通,当我们把netflix组件原理全部吃透,彻底吸收微服务设计理念、核心思想的时候,后面的再出的所有微服务体系框架对你来说都不是问题。做技术这个事情,急不得,更不能什么火就去追,到最后什么都只是用用,一个程序员的核心竞争力应该是看源码的能力,尤其是阅读静态源码。而这些事情,我都会带着你看懂,这个专题我会采用小白都能看懂的方式,尽可能将源码解读细化,也会适当放入一些我自己手工绘制的图,帮助理解,话不多说,我们进入正题。
spring cloud eureka server和client是对netflix的eureka进行了封装,加了一些注解,对spring boot进行支持。所以看eureka的源码,应该先从netflix eureka开始看起。
本次netflix eureka解读的版本:1.10.14,其实1.7.2的版本我也看过,发现在架构设计、运行原理、核心流程上并没有太大的差别,可能某些功能在实现细节上会有些许的差别而已。
- eureka-client:eureka客户端代码模块
- eureka-core:eureka服务端代码模块,包括服务注册与发现,心跳,摘除故障服务实例功能
- eureka-resources:基于jsp开发的eureka控制台
- eureka-server:注册中心模块,包含以上三者
- jersey:mvc框架,使服务端和客户端通过restful接口进行网络通信
Eureka Server启动
Eureka-server是一个web应用,那么它的入口就是web.xml;
web.xml配置的Listener是EurekaBootStrap,所以这个类就是Eureka server启动的核心类,是我们重点要去看的;
<listener>
<listener-class>com.netflix.eureka.EurekaBootStrap</listener-class>
</listener>
进到EurekaBootStrap这个类里,发现它继承自ServletContextListener,那么就会调用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()
这个方法,大概梳理一下它做的几件事情:
-
使用Double check的单例模式搞出来一个ConfigurationManager的实例对象,这个对象你可以认为是一个配置管理组件,后面会通过Properties对象来加载配置,然后对配置进行管理的这么一个东东;
String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER); //顺着getConfigInstance方法往里走,最终实际上实例化的是ConcurrentCompositeConfiguration这样一个对象,在它的构造函数会初始化一些东西,然后把这个对象返回,就不细看了,不是核心。 public static AbstractConfiguration getConfigInstance() { if (instance == null) { synchronized (ConfigurationManager.class) { if (instance == null) { //这个instance就是AbstractConfiguration类型 instance = getConfigInstance(Boolean.getBoolean(DynamicPropertyFactory.DISABLE_DEFAULT_CONFIG)); } } } return instance; }
-
初始化数据中心,如果没有配置的话,默认设置为DEFAULT ;
String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER); if (dataCenter == null) { logger.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); }
-
初始化eurueka运行的环境,如果没有配置,默认设置为test环境。
String environment = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT);
if (environment == null) {
ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, TEST);
logger.info("Eureka environment value eureka.environment is not set, defaulting to test");
}
再说initEurekaServerContext()
这个方法,这是Eureka server初始化的一个核心,很多细节都在这个方法里:
第一步,先创建一个EurekaServerConfig
的实例对象,看名字,应该是Eureka server的配置类;
EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();
public DefaultEurekaServerConfig() {
init();
}
private void init() {
String env = ConfigurationManager.getConfigInstance().getString(
EUREKA_ENVIRONMENT, TEST);
ConfigurationManager.getConfigInstance().setProperty(
ARCHAIUS_DEPLOYMENT_ENVIRONMENT, env);
String eurekaPropsFile = EUREKA_PROPS_FILE.get();
try {
// ConfigurationManager
// .loadPropertiesFromResources(eurekaPropsFile);
//这个eurekaPropsFile就是eureka-server.properties文件
ConfigurationManager
.loadCascadedPropertiesFromResources(eurekaPropsFile);
} catch (IOException e) {
logger.warn(
"Cannot find the properties specified : {}. This may be okay if there are other environment "
+ "specific properties or the configuration is installed with a different mechanism.",
eurekaPropsFile);
}
}
在loadCascadedPropertiesFromResources
这个方法中先去读取eureka-sever.properties这个配置文件的内容到一个Properties对象里面去,然后把这个Properties对象里的配置项再load到一个AbstractConfiguration
的配置类中,这个AbstractConfiguration就是我们前面提到的配置管理组件;
public static void loadCascadedPropertiesFromResources(String configName) throws IOException {
//读取配置到Properties配置类
Properties props = loadCascadedProperties(configName);
if (instance instanceof AggregatedConfiguration) {
ConcurrentMapConfiguration config = new ConcurrentMapConfiguration();
config.loadProperties(props);
((AggregatedConfiguration) instance).addConfiguration(config, configName);
} else {
//load Properties中的配置项的配置管理组件
ConfigurationUtils.loadProperties(props, instance);
}
}
然后基于DefaultEurekaServerConfig的一系列getXxx()
方法提供配置的读取,实际上getXxx()方法就是从ConfigurationManager中去读取,只不过把硬编码的部分写在了方法中罢了,并且给每个属性提供了默认值;这种设计最大的好处就是,屏蔽了key的变更对调用者带来的影响,还是蛮不错的,值得借鉴。
public int getPeerEurekaNodesUpdateIntervalMs() {
return configInstance
.getIntProperty(namespace + "peerEurekaNodesUpdateIntervalMs",
(10 * 60 * 1000)).get();
}
这个configInstance是DynamicPropertyFactory类型的实例对象,实际上就是从AbstractConfiguration中加载的配置;
private static final DynamicPropertyFactory configInstance = com.netflix.config.DynamicPropertyFactory
.getInstance();
public static DynamicPropertyFactory getInstance() {
if (config == null) {
synchronized (ConfigurationManager.class) {
if (config == null) {
AbstractConfiguration configFromManager = ConfigurationManager.getConfigInstance();
if (configFromManager != null) {
initWithConfigurationSource(configFromManager);
initializedWithDefaultConfig = !ConfigurationManager.isConfigurationInstalled();
logger.info("DynamicPropertyFactory is initialized with configuration sources: " + configFromManager);
}
}
}
}
return instance;
}
第二步,搞了一个EurekaInstanceConfig
的对象出来,可以认为是跟服务实例相关的一个配置类,跟前面类似,将eureka-client.properties文件中的配置加载到配置管理组件(AbstractConfiguration)中,然后基于EurekaInstanceConfig提供的一系列getXxx()
方法来提供配置项的读取,并且提供了所有配置项的默认值;
EurekaInstanceConfig instanceConfig = isCloud(ConfigurationManager.getDeploymentContext())
? new CloudInstanceConfig()
: new MyDataCenterInstanceConfig();
//构造里没有代码,查看父类构造方法
public MyDataCenterInstanceConfig() {
}
public PropertiesInstanceConfig(String namespace, DataCenterInfo info) {
super(info);
this.namespace = namespace.endsWith(".")
? namespace
: namespace + ".";
appGrpNameFromEnv = ConfigurationManager.getConfigInstance()
.getString(FALLBACK_APP_GROUP_KEY, Values.UNKNOWN_APPLICATION);
//public static final String CONFIG_FILE_NAME = "eureka-client";
this.configInstance = Archaius1Utils.initConfig(CommonConstants.CONFIG_FILE_NAME);
}
基于刚才构建的EurekaInstanceConfig和InstanceInfo
对象,构建了一个ApplicationManager
,后面会基于这个ApplicationInfoManager对服务实例进行一些管理;
ApplicationInfoManager applicationInfoManager = null;
... ...
applicationInfoManager = new ApplicationInfoManager(
instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());
这里也不足为奇,因为在eureka集群模式下,eureka server同时也是一个eureka client,也要向其他的eureka server去进行注册,所以会有一些Instance和Application的概念出来。
这个InstanceInfo就是当前这个服务实例自身的信息,使用了构造器模式来创建;核心的思路是,从之前的那个EurekaInstanceConfig中,读取一些服务实例相关的配置信息,再构造了几个其他的对象,最终完成了InstanceInfo的构建;使用构造器Builder模式来进行复杂对象的构造和属性赋值还是蛮好的,构造完成后使用build方法获取复杂对象,值得借鉴。
InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(vipAddressResolver);
... ...
//使用构造器模式set属性
builder.setNamespace(config.getNamespace())
.setInstanceId(instanceId)
.setAppName(config.getAppname())
... ...
//构造这个instanceInfo
instanceInfo = builder.build();
instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
第三步,搞出来一个EurekaClientConfig
,跟之前一样,读取eureka-client.properties文件中跟EurekaClient相关的配置项,同样是基于getXxx()的一系列方法提供配置项的读取,并提供默认值(看样子是准备一条路走到黑… …);
EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();
第四步,基于ApplicationInfoManager和eureka client相关的配置构建了一个EurekaClient
,这个Eureka Client就是代表了Eureka的客户端,使用的是子类DiscoveryClient,这个DiscoveryClient的构造方法里涉及到了EurekaClient的启动流程,我们放在下一篇将,这里我们只要来看看Server的启动;是不是有些疑惑,为什么Server启动时会有Client的初始化流程??当然会有,Eureka集群模式下,每个Server同时也是Client,一方面提供注册表的抓取,客户端的心跳等等;另一方面也要向其他的Server去注册,以及抓取其他节点的注册表,合并到本地。
private EurekaClient eurekaClient;
... ...
eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
第五步,构造了一个PeerAwareInstanceRegistry
,这个东西,就是所谓的服务实例注册表,这个东西,我在后面会详细说,也是Eureka的一块核心机制,先知道这是什么就行了;
PeerAwareInstanceRegistry registry;
registry = new PeerAwareInstanceRegistryImpl(
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
eurekaClient
);
第六步,构造Eureka集群,PeerEurekaNodes
;每个Eureka的节点,顾名思义,就是Eureka集群相关的东西;
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
registry,
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
applicationInfoManager
);
第七步,构造一个Eureka server服务器上下文,EurekaServerContext
,然后放入一个EurekaServerContextHolder
中,后续需要使用,从holder中获取即可;把上下文这种可能需要全局使用的东西放进一个Holder中,在需要的时候从Holder中获取,也值得借鉴;
protected volatile EurekaServerContext serverContext;
serverContext = new DefaultEurekaServerContext(
eurekaServerConfig,
serverCodecs,
registry,
peerEurekaNodes,
applicationInfoManager
);
EurekaServerContextHolder.initialize(serverContext);
第八步,初始化serverContext,主要是把Eureka集群启动起来,让当前的Eureka server感知到所有的其他的Eureka server;
然后搞一个定时调度任务,就一个后台线程,每隔一定的时间,更新Eureka server集群的信息。
serverContext.initialize();
public void initialize() {
logger.info("Initializing ...");
//启动Eureka集群
peerEurekaNodes.start();
try {
registry.init(peerEurekaNodes);
} catch (Exception e) {
throw new RuntimeException(e);
}
logger.info("Initialized");
}
//进入start方法中,可以看到定时更新Eureka server集群信息的代码
updatePeerEurekaNodes(resolvePeerUrls());
然后基于Eureka server集群的信息,来初始化注册表,将集群中所有的Eureka server的注册表的信息都抓取到本地,这个逻辑都在init中,感兴趣可以去看看,这里就不贴出来了;
registry.init(peerEurekaNodes);
第九步,从相邻的一个Eureka server节点拷贝注册表的信息,如果拷贝失败,就找下一个;
registry.syncUp();
@Override
public int syncUp() {
// Copy entire entry from neighboring DS node
int count = 0;
for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
if (i > 0) {
try {
Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
} catch (InterruptedException e) {
logger.warn("Interrupted during registry transfer..");
break;
}
}
Applications apps = eurekaClient.getApplications();
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
try {
if (isRegisterable(instance)) {
register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
count++;
}
} catch (Throwable t) {
logger.error("During DS init copy", t);
}
}
}
}
return count;
}
第十步,注册监控状态。
EurekaMonitors.registerAllStats();
到此为止,Eureka server就启动并完成了初始化的工作;
读源码,千万不要有强迫症, 很多时候,刚开始看源码的时候,要允许自己对很多细节都不太清楚,但是能大体把握住大的流程就ok了。