大多数人想要改造这个世界,但却罕有人想改造自己。
–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning
目录
前言
上篇文章 主要介绍了DiscoveryClient
的成员属性,达30+个之多。我们发现其成员属性中,绝大多数否是final修饰,也就是说他们大多均得在DiscoveryClient
初始化阶段完成赋值,由此你也能感受到它初始化的“压力”。
确实,DiscoveryClient
的初始化阶段是它,甚至是整个Eureka Client最为重要的逻辑片段之一,属于掌握Eureka一定、必须精通的知识点。在上文已经了解了它的成员属性以及含义后,本文在其基础上继续深入,深入讲解DiscoveryClient
的初始化逻辑。
正文
一个类的初始化必然和构造器分不开,DiscoveryClient
拥有非常多的构造器,但是多个构造器之间都是层层递进的关系,最终会调用“最底层”一个大而全的构造方法完成所有的初始化工作。
DiscoveryClient的构造器们
DiscoveryClient
属于对外API,为了方便使用所以它就提供了较多的构造器(有些已被标注为过期),它是一个很庞大的体系很是壮观,如下截图所示:
DiscoveryClient
一共9个构造器之多,可分类为:
- public构造器:共7个
- 4个已标记过期,3个正常
- non-public构造器:共2个
- 1个已标记过期,1个正常
已过期的构造器(5个)
DiscoveryClient:
@Deprecated
public DiscoveryClient(InstanceInfo myInfo, EurekaClientConfig config) {
this(myInfo, config, null);
}
@Deprecated
public DiscoveryClient(InstanceInfo myInfo, EurekaClientConfig config, DiscoveryClientOptionalArgs args) {
this(ApplicationInfoManager.getInstance(), config, args);
}
@Deprecated
public DiscoveryClient(InstanceInfo myInfo, EurekaClientConfig config, DiscoveryClientOptionalArgs args) {
this(ApplicationInfoManager.getInstance(), config, args);
}
@Deprecated
public DiscoveryClient(ApplicationInfoManager applicationInfoManager, final EurekaClientConfig config, DiscoveryClientOptionalArgs args) {
this(applicationInfoManager, config, (AbstractDiscoveryClientOptionalArgs) args);
}
以上几个构造器它传入的是InstanceInfo
实例,默许ApplicationInfoManager
这个单例已经实例化完成,其实这是不安全的(不能保证嘛),因此不建议再使用~
另外入参中,希望使用更抽象的AbstractDiscoveryClientOptionalArgs
代替DiscoveryClientOptionalArgs
,这样Jersey2.x也能够很容易的接入进来~
最后还剩下一个过期构造器,它是non-public的,因此就略过了~
正常可用的构造器(4个)
DiscoveryClient:
public DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config) {
this(applicationInfoManager, config, null);
}
public DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args) {
this(applicationInfoManager, config, args, ResolverUtils::randomize);
}
public DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, EndpointRandomizer randomizer) {
this(...);
}
// 最大而全的构造器访问权限是defualt:并不对外暴露
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
... // 复杂的初始化代码
}
这4个可用的构造器(其实只有3个,因为最后一个访问权限是default)具有非常明显的递进关系:从上至下一层层依赖,2个参数到5个参数,这种门面设计模式是作为一个公有API该有的样子。
总结一下:留给我们“可以使用”的构造器共有3个,看起来也就不算很多了嘛,把思路理了理是不是一下子清爽很多呢?但是,深入理解其初始化逻辑依旧任重而道远,做好准备,马上开始。
DiscoveryClient初始化逻辑
DiscoveryClient
的初始化逻辑是整个Eureka Client中最重要的一块,没有之一,内容全部放在了其最“底层”的构造器里,下面我们就来会会它。
5大形参说明
DiscoveryClient
的最全构造器,它有5个形参:
DiscoveryClient:
// 需要注意到的是:DI依赖注入默认就是使用的此构造器哦
@Inject // 它是com.google.inject.Inject,Spring并不认识~~~
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
...
}
applicationInfoManager
:实例管理器。携带有InstanceInfo信息,并且对它进行管理,它是个单例,全局仅需一份(InstanceInfo
也只需一份)EurekaClientConfig config
:客户端配置。注意:包含EurekaTransportConfig
配置AbstractDiscoveryClientOptionalArgs args
:从上篇文章知道DiscoveryClient
需要在初始化阶段完成赋值的属性很多,难道都写在构造器形参里吗?当然不是,因此就使用到了它对一些参数进行了“封装”,并且都是可选的。所以简单粗暴的就把它理解为一个POJO吧~backupRegistryProvider
:备用注册中心,用于容灾,一般可不指定,为null即可。EndpointRandomizer endpointRandomizer
:端点随机器,一般固定为ResolverUtils::randomize
,当然喽你也可以自己实现,只是木有必要~
初始化逻辑
下面开始正式进入到最为重要的初始化逻辑部分:根据构造器传入的5个入参,完成DiscoveryClient
的整个初始化逻辑。
由于初始化逻辑链路悠长,为了方便讲解和理解,我把它拆分为如下几个部分分别讲述。
1、基础属性赋值
DiscoveryClient:
// 构造器
DiscoveryClient( ... ){
if (args != null) {
this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
this.eventListeners.addAll(args.getEventListeners());
this.preRegistrationHandler = args.preRegistrationHandler;
} else {
this.healthCheckCallbackProvider = null;
this.healthCheckHandlerProvider = null;
this.preRegistrationHandler = null;
}
// InstanceInfo实例信息来自于applicationInfoManager
this.applicationInfoManager = applicationInfoManager;
InstanceInfo myInfo = applicationInfoManager.getInfo();
instanceInfo = myInfo;
if (myInfo != null) {
appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
} else {
logger.warn("Setting instanceInfo to a passed in null value");
}
clientConfig = config;
staticClientConfig = clientConfig;
transportConfig = config.getTransportConfig();
this.backupRegistryProvider = backupRegistryProvider;
this.endpointRandomizer = endpointRandomizer;
// 用于打乱List<String> urls
// 打乱因子和instanceInfo的主机名有关
this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
// 本地注册表,初始化时new一个空的放着
// 此时里面的各种缓存均还为空
localRegionApps.set(new Applications());
// 没fetch一次就+1
fetchRegistryGeneration = new AtomicLong(0);
// 配置key为:`eureka.fetchRemoteRegionsRegistry = xxx` 默认值是null
remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
// 上的数组表示形式。逗号分隔
remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
... // 省略registryStalenessMonitor/heartbeatStalenessMonitor监控统计的初始化代码
// 到此:输出一句info信息
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
}
这步最为简单,唯一值得说道的两点:
instanceInfo
本实例来自于:applicationInfoManager.getInfo()transportConfig
传输配置来自于:config.getTransportConfig()
当你在日志里看到Initializing Eureka in region
话语时(info日志级别),说明基础属性的赋值完成了。
2、初始化场景
DiscoveryClient
客户端的初始化有个分水岭:是否注册自己。配置不同,初始化进程也就是不一样的。共两种场景,这是通过if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry())
这个条件来区分的,对应的配置项为:
eureka.registration.enabled = xxx
,默认值是trueeureka.shouldFetchRegistry = xxx
,默认值是true
说明:他俩一般同true,同false
2.1、不注册自己场景
只有上述两个配置同时为false才会是此case(当然一般都是同false),表示该客户端既不注册自己,也不去远端fetch查询数据,属于一种简单场景。
DiscoveryClient:
// 构造器
DiscoveryClient( ... ){
...
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
// 同时为false,就会进入到此分支:客户端配置为既不注册也不查询数据
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
logger.info("Client configured to neither register nor query for data.");
// 一些列的参数,都强制置为null,因为已无意义,交给JVM回收吧
scheduler = null;
heartbeatExecutor = null;
cacheRefreshExecutor = null;
eurekaTransport = null;
// 基于配置的region和zone的映射关系校验器
instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());
// 这两行的目的是:使得原始方式也能兼容到现在的DI注入方式
// 所以通过这两行代码把必要参数设置进去
// 当然喽:DiscoveryManager已被标记为过期
// 这两行代码在2.2方式里也有~~~~~~~
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
// 记录初始化完成的时间戳,并且输出日志
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", initTimestampMs, this.getApplications().size());
// 这个return告诉到此就初始化完成了,不需要再设置些网络任务
// no need to setup up an network tasks and we are done
return;
}
...
}
若不需要注册自己和拉取数据,初始化逻辑非常清爽、简单。因为它不再需要哪些复杂的功能如心跳、拉取数据、自动注册等等逻辑。它的使用场景一般在:你的应用作为一个纯Eureka Server
的时候,就并不需要把自己注册到注册中心去以及去拉取注册表信息了,所以关闭这两项是个很好的选择:
eureka:
client:
# 不用注册自己到注册中心里(毕竟别人把你的实例拉取过去也没啥用)
register-with-eureka: true
# 不需要获取注册表信息(因为eureka server不需要请求别的服务)
fetch-registry: false
2.2、注册自己场景(默认)
这是默认情况(两个条件默认均为true),属复杂场景。
DiscoveryClient:
// 构造器
DiscoveryClient( ... ){
...
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
... // 2.1的代码,略
// ==========默认均会走到这里==============
// 一个大try 包住下面所有网络相关的代码
try {
// 调度器。core核心大小为2,给心跳和refresh线程刚刚好
// 此线程名为:DiscoveryClient-%d 是守护线程
scheduler = Executors.newScheduledThreadPool(2, ...);
// 执行线程池:核心线程数是1,最大是5(可配置)
// 此线程名为:DiscoveryClient-HeartbeatExecutor-%d 是守护线程
heartbeatExecutor = new ThreadPoolExecutor(
1,
clientConfig.getHeartbeatExecutorThreadPoolSize(),
// 线程一有空闲就立马回收掉
0, TimeUnit.SECONDS,
// use direct handoff
new SynchronousQueue<Runnable>(),
...);
// 同heartbeatExecutor,唯一不同的是线程名为:DiscoveryClient-CacheRefreshExecutor-%d
cacheRefreshExecutor= new ThreadPoolExecutor( ... );
// EurekaTransport为一静态内部类,也是一POJO,管理着很多属性而已~~~
// 如bootstrapResolver、registrationClient、queryClient等
// 不过刚new出来的时,各个属性都是null,下面就是复杂的赋值方法
eurekaTransport = new EurekaTransport();
// 为eurekaTransport里管理的各个属性赋值
scheduleServerEndpointTask(eurekaTransport, args);
...
// 显示配置了clientConfig.shouldUseDnsForFetchingServiceUrls() = true才会是基于DNS的
// 否则默认均是基于config配置的映射关系
instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
} catch (Throwable e) {
// 若这里失败,就报告初始化失败。比如没资源了?
throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}
// 若允许获取注册表的话,这里就执行一次获取:fetchRegistry(false)
// 若返回false(获取失败),那就尝试从备选的注册中心里获取
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
// 在所有后台任务(inc注册)启动之前调用并执行预注册处理程序
// ~~~~~~~预处理程序,貌似我们一般没啥必要~~~~~~~
if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}
// 如果你开启了:在初始化阶段就执行注册,那就注册一下
// 注意:shouldEnforceRegistrationAtInit() 默认是false的,一般不建议改为true
// 若改为true了:那若eureka-server挂了,你client端启动都成问题了,所以不建议
if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
try {
// register()注册若返回false:表示注册失败,那就抛出异常,终止启动程序(影响很大)
if (!register() ) {
throw new IllegalStateException("Registration error at startup. Invalid server response.");
}
} catch (Throwable th) {
logger.error("Registration error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
// 最后:初始化各种调度、定时任务
// 如心跳续租、定时注册、定时拉取信息等
initScheduledTasks();
// 下面代码完全同2.1所述,本处略
DiscoveryManager.getInstance().setDiscoveryClient(this);
...
}
该步骤乃DiscoveryClient
最为重要的初始化流程,一定要融会贯通。为了辅助理解,我,我把此部分流程(核心流程)绘图如下:
当你在日志里看到Discovery Client initialized at timestamp {} with initial instances count:
话语时(info日志级别),那就说明整个DiscoveryClient
初始化完成了。
主case问答/互动
1、线程池初始化时,何时需要调整最大线程数?
答:心跳和缓存的执行线程池分别可通过eureka.client.heartbeat.threadPoolSize = xxx
和eureka.client.cacheRefresh.threadPoolSize = xxx
设置,默认值是5。当你的心跳非常频繁(如心跳2s一次),且Server端响应相对较慢时,可适当调高此值,一般并不建议修改此值。
2、初始化时什么时候会后可让其不去fetchRegistry(false)
获取注册表信息?
答:默认在初始化时会去获取一次注册表信息,若访问不通就抛错,程序终止。所以当你的server暂时不可用or网络很不稳定时,可以关闭此选项eureka.shouldFetchRegistry = false
。
注意:关闭此选项后只是在初始化时不去获取了,保证让你能正常启动应用,后面会有任务定时/周期获取到注册表信息的,无需担心
3、预处理程序PreRegistrationHandler
有何意义?
答:暂时我也还没想到有什么用武之地,做些校验啥的?我觉得也阔仪吧,总之是个扩展点
4、什么时候我们在初始化阶段就需要register()
注册本实例?
答:默认在初始化阶段是不注册自己的。当你觉得你初始化阶段就已经能对外提供服务了,可开启此选项eureka.shouldEnforceRegistrationAtInit = true
,一般不建议开启。
说明:开启的好处时响应更及时,坏处是还没初始化好就被高流量压崩了~
5、DiscoveryManager
能用吗?
答:DiscoveryManager
它作为DiscoveryClient
管理器管理着它,最原始的方式其实可以这么初始化一个DiscoveryClient
的:DiscoveryClient.getInstance().initComponent(config,eurekaConfig)
。而从源码处可以看到关于DiscoveryManager
处理的两行代码:
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
完成了初始化,因此DiscoveryManager
是阔仪使用的,没有问题。它常用的方法为:
DiscoveryManager:
public LookupService getLookupService() {
return discoveryClient;
}
public EurekaClient getEurekaClient() {
return discoveryClient;
}
另需要注意的是:
DiscoveryManager
虽然也可以用但是并不建议用,毕竟它已被标记为@Deprecated
总结
关于DiscoveryClient透彻解析(二):最重要的初始化逻辑就先介绍到这,本文使用“万字长文”详细介绍了DiscoveryClient
初始化逻辑的几乎每一步(当然喽,封装的几个烦方法细节没有去详解,其实对于流程的理解也确实没有必要,所以我会放在后面)。
希望你通过这两篇文章,能够先彻彻底底的了解构建出一个DiscoveryClient
实例出来,到底做了些什么,完成了那些东西,这样能为后续接口方法的实现、理解,甚至对Eureka Server的节点复制等理解打好坚实基础。
声明
原创不易,码字更不易,感谢关注。分享本文到你的朋友圈是被授权的,但拒绝抄袭。【左边扫码加我wx / wx号:fsx641385712
】,邀你加入 【Java高工、架构师】 系列纯纯纯技术群,亦可扫码加入我的知识星球【BAT的乌托邦】。
- 3分钟带你了解轻量级依赖注入框架Google Guice【享学Java】
- [享学Eureka] 一、源生Eureka介绍 — 基于注册中心的服务发现
- [享学Eureka] 二、Eureka的最核心概念:InstanceInfo实例信息
- [享学Eureka] 三、Eureka配置之:EurekaInstanceConfig实例配置
- [享学Eureka] 四、Eureka配置之:EurekaClientConfig客户端配置
- [享学Eureka] 五、Eureka核心概念:应用(Application)和注册表(Applications)
- [享学Eureka] 六、InstanceInfo实例管理器:ApplicationInfoManager
- [享学Eureka] 七、远程通信模块:EurekaHttpClient接口抽象以及基于Jersey的Low-Level实现JerseyApplicationClient
- [享学Eureka] 八、远程通信模块:手动构建JerseyApplicationClient客户端完成服务注册、服务下线…
- [享学Eureka] 九、远程通信模块:使用TransportClientFactory构建底层请求客户端完成服务注册、服务下线
- [享学Eureka] 十、迷人小工具之TimedSupervisorTask:自动调节执行间隔的周期性任务
- [享学Eureka] 十一、迷人小工具之EndpointUtils:从配置文件中解析出serviceUrl(非常重要)
- [享学Eureka] 十二、远程通信模块:集群解析器ClusterResolver(一) ConfigClusterResolver
- [享学Eureka] 十三、集群解析器ClusterResolver(二):ApplicationsResolver和EurekaHttpResolver
- [享学Eureka] 十四、集群解析器ClusterResolver(三):ZoneAffinityClusterResolver区域感知解析器
- [享学Eureka] 十五、集群解析器ClusterResolver(四):AsyncResolver异步解析器
- [享学Eureka] 十六、远程通信模块:Top Level部分之EurekaHttpClientFactory和SessionedEurekaHttpClient
- [享学Eureka] 十七、远程通信模块:RetryableEurekaHttpClient高可用Client端的重试机制
- [享学Eureka] 十八、远程通信模块:结合代码示例详解transport.retryableClientQuarantineRefreshPercentage配置项
- [享学Eureka] 十九、远程通信模块:EurekaHttpClients工具快速构建ClusterResolver集群解析器
- [享学Eureka] 二十、远程通信模块:EurekaHttpClients工具快速构建EurekaHttpClient请求客户端
- [享学Eureka] 二十一、LookupService服务发现之客户端实现:EurekaClient接口
- [享学Eureka] 二十二、DiscoveryClient服务注册的小工具:InstanceInfoReplicator
- [享学Eureka] 二十三、DiscoveryClient前置知识:BackupRegistry备用注册中心、HealthCheckHandler健康检查处理器…
- [享学Eureka] 二十四、DiscoveryClient透彻解析(一):功能概述 + 成员属性详解