[享学Eureka] 二十五、DiscoveryClient透彻解析(二):初始化逻辑详解

大多数人想要改造这个世界,但却罕有人想改造自己。

–> 返回专栏总目录 <–
代码下载地址: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,默认值是true
  • eureka.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 = xxxeureka.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的乌托邦】。
往期精选

发布了393 篇原创文章 · 获赞 856 · 访问量 55万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/105374718