saturn 源码解析

本文深入解析Saturn的namespace创建过程,包括域名写入数据库和注册中心的逻辑。接着讨论job的创建,特别是手动创建与从数据库复制的区别。同时,介绍了executor的启动,包括Job执行的细节,如分片管理和状态变化。最后,概述了namespace迁移的逻辑,即如何在不同集群间迁移。
摘要由CSDN通过智能技术生成

saturn-console

启动

saturn console本身为一个springboot项目,所有的bean都在saturn-console 下的applicationContext.xml中定义。所以启动主要就是初始化负责各种操作的bean

    <!-- -----  权限管理主要针对job -------->
<bean id="authorizationService"
          class="com.vip.saturn.job.console.service.impl.AuthorizationServiceImpl"/> 
    <bean id="authorizationManageServiceImpl"
          class="com.vip.saturn.job.console.service.impl.AuthorizationManageServiceImpl"/>

  <!-- -----  系统配置管理,console中的一些系统配置相关 -------->
	<bean id="systemConfigService"
		class="com.vip.saturn.job.console.service.impl.SystemConfigServiceImpl"/>

  <!-- -----  告警  -------->
	<bean id="alarmStatisticsService"
		class="com.vip.saturn.job.console.service.impl.AlarmStatisticsServiceImpl"/>
   <!-- -----  主页和统计信息 -------->
	<bean id="dashboardService"
		class="com.vip.saturn.job.console.service.impl.DashboardServiceImpl"/>
  <!-- -----  执行器管理 -------->
	<bean id="executorService"
		class="com.vip.saturn.job.console.service.impl.ExecutorServiceImpl"/>
   <!-- -----  job -------->
	<bean id="jobService"
		class="com.vip.saturn.job.console.service.impl.JobServiceImpl"/>
  <!-- -----  域和zk集群管理 -------->
	<bean id="namespaceZkClusterMappingService"
		class="com.vip.saturn.job.console.service.impl.NamespaceZkClusterMappingServiceImpl"/>
 <!-- -----  报告告警 -------->
	<bean id="reportAlarmService"
		class="com.vip.saturn.job.console.service.impl.ReportAlarmServiceImpl"/>
 <!-- ----- 判断cron -------->
	<bean id="utilsService"
		class="com.vip.saturn.job.console.service.impl.UtilsServiceImpl"/>
 <!-- -----  判断zk和数据库是不同 -------->
	<bean id="zkDBDiffService"
		class="com.vip.saturn.job.console.service.impl.ZkDBDiffServiceImpl"/>
 <!-- -----  查看和管理zk中的path -------->
	<bean id="zkTreeService"
		class="com.vip.saturn.job.console.service.impl.ZkTreeServiceImpl"/>
 <!-- ----- 更新job的配置 -------->
	<bean id="updateJobConfigService"
		class="com.vip.saturn.job.console.service.impl.UpdateJobConfigServiceImpl"/>
 <!-- -----  RestApi 针对job的一些http请求 -------->
	<bean id="restApiService" class="com.vip.saturn.job.console.service.impl.RestApiServiceImpl"/>
<!-- -----  将执行的结果(每天执行多少,失败等等)持久化到数据库 -------->
	<bean id="statisticPersistence"
		class="com.vip.saturn.job.console.service.impl.statistics.StatisticsPersistence"/>
<!-- -----  dashboard统计数据(执行多少,失败等等) -------->
	<bean id="statisticRefreshService"
		class="com.vip.saturn.job.console.service.impl.statistics.StatisticsRefreshServiceImpl"/>
<!-- -----  注册中心管理 -------->
	<bean id="registryCenterService"
		class="com.vip.saturn.job.console.service.impl.RegistryCenterServiceImpl"/>
<!-- -----  权限,登陆 -------->
    <bean id="authenticationService"
          class="com.vip.saturn.job.console.service.impl.AuthenticationServiceImpl"/>

上诉的bean中,大部分都是一些定时任务。重点关注namespacejob管理,包括创建,分片等等。

namespace的创建

console创建域,需要传入namespacezkCluster,后台由RegistryCenterControllercreateNamespace进行创建。核心逻辑如下:

  @Transactional(rollbackFor = {
   Exception.class})
	@Override
	public void createNamespace(NamespaceDomainInfo namespaceDomainInfo) throws SaturnJobConsoleException {
   
		String namespace = namespaceDomainInfo.getNamespace();
		String zkClusterKey = namespaceDomainInfo.getZkCluster();
    // 从注册中心查询是否存在该zk集群
		ZkCluster currentCluster = getZkCluster(zkClusterKey);

		if (currentCluster == null) {
   
			throw new SaturnJobConsoleHttpException(HttpStatus.BAD_REQUEST.value(),
					String.format(ERR_MSG_TEMPLATE_FAIL_TO_CREATE, namespace, "not found zkcluster" + zkClusterKey));
		}
    // 判断当前所有的集群下是否有该namespace,也就是说namespace是所有集群唯一的
		if (checkNamespaceExists(namespace)) {
   
			throw new SaturnJobConsoleHttpException(HttpStatus.BAD_REQUEST.value(),
					String.format(ERR_MSG_NS_ALREADY_EXIST, namespace));
		}

		try {
   
			// 创建 namespaceInfo
			NamespaceInfo namespaceInfo = constructNamespaceInfo(namespaceDomainInfo);
			namespaceInfoService.create(namespaceInfo);
			// 创建 zkcluster 和 namespaceInfo 关系,并写入数据库,如果从现有数据库中发现有该namespace,则更新数据,否则插入新数据。
			namespaceZkClusterMapping4SqlService.insert(namespace, "", zkClusterKey, NAMESPACE_CREATOR_NAME);
			// refresh 数据到注册中心,该方法不是通过直接刷新数据到zk,而是通过刷新sys_config 中的uid来异步进行刷新
			notifyRefreshRegCenter();
		} catch (Exception e) {
   
			log.error(e.getMessage(), e);
			throw new SaturnJobConsoleHttpException(HttpStatus.INTERNAL_SERVER_ERROR.value(),
					String.format(ERR_MSG_TEMPLATE_FAIL_TO_CREATE, namespace, e.getMessage()));
		}
	}


上文中代码会和RegistryCenterServiceImpl进行交互。

	public void init() {
   
		getConsoleClusterId();
		localRefresh();
		initLocalRefreshThreadPool();
		startLocalRefreshTimer();
		startLocalRefreshIfNecessaryTimer();
	}

	private void initLocalRefreshThreadPool() {
   
		localRefreshThreadPool = Executors
				.newSingleThreadExecutor(new ConsoleThreadFactory("refresh-RegCenter-thread", false));
	}

	private void startLocalRefreshTimer() {
   

		//每隔5分钟执行一次localRefresh

	}

	private void startLocalRefreshIfNecessaryTimer() {
   
  /* 每一秒钟检查,当system的配置发生变化,当前的uuid 和最新的uuid 不同的时候,执行loaclRefresh */
	}

	private synchronized void localRefresh() {
   
		// 有删减
      // 刷新注册中心,主要包括,对比zk集群是否变化,包括域的关闭和迁移
			refreshRegistryCenter();
    	// console 的选主和数据更新,在console的系统配置中设置的zk集群才能够被当前的console管理。
    	// 该数据为mysql中的数据,dashboard的选主逻辑为在zk中注册$SaturnSlef/saturn-console/dashboard/leader
			refreshDashboardLeaderTreeCache();
    	// 是否需要创建或者迁移namespaceShardingManager,每一个域名都对应一个namespaceShardingManager
			refreshNamespaceShardingListenerManagerMap();

	}

该类的初始化主要为上文的代码。所以,域名的创建逻辑是直接写入数据库,然后通过refresh写入zk。一个namespace的创建就完成了,主要是在数据库中写入数据(namespace_zkcluster_mapping),更新sys_config中的uid异步刷新。刷新后,注册中心会有该namespace的节点,以及针对dashboard的选主和分片管理,其中每一个namespace会有一个分片管理NamespaceShardingManager,该类主要用于管理执行器的上下线,分片等信息。

  	private void start0() throws Exception {
   
     //
		shardingTreeCacheService.start();
		// create ephemeral node $SaturnExecutors/leader/host & $Jobs
    // 主要是针对saturn的执行器选举,为console节点
		namespaceShardingService.leaderElection();
    // 针对已经存在的job增加JobServersTriggerShardingListener,用于当executor上线下后进行分片,节点为$Jobs/${jobName}/servers
    // JobServersTriggerShardingListener 
		addJobListenersService.addExistJobPathListener();
		// 上下线Listener,节点为/$SaturnExecutors/executors
    //ExecutorOnlineOfflineTriggerShardingListener  ExecutorTrafficTriggerShardingListener
		addOnlineOfflineListener();
		// 分片 节点为/$SaturnExecutors/sharding
    //SaturnExecutorsShardingTriggerShardingListener
		addExecutorShardingListener();
		// 选主
    // /$SaturnExecutors/leader
		addLeaderElectionListener();
		// 新增与删除/$Jobs
    // AddOrRemoveJobListener
		addNewOrRemoveJobListener();
	}

针对上面不同的listerner最后的执行的方法都是AbstractAsyncShardingTask中的run方法。当前我们创建了一个namespace且需要分片的时候,对应的NamespaceShardingManager会在zk上注册多个节点,并且监听对应的executor上下线,job创建等事件。分片是由当前executor的主节点来进行的,也就是说当新建一个namespace的时候,该namespace所有的executor分片由获取到该namespaceleader进行操作,每一步操作都会判断当前操作的对象是否为namespaceleader(是否已经创建latch节点,以及host是否就是当前节点)。

选举:

	public void leaderElection() throws Exception {
   
		lock.lockInterruptibly();
		try {
   
			if (hasLeadership()) {
   
				return;
			}
			log.info("{}-{} leadership election start", namespace, hostValue);
			try (LeaderLatch leaderLatch = new LeaderLatch(curatorFramework,
					SaturnExecutorsNode.LEADER_LATCHNODE_PATH)) {
   
				leaderLatch.start();
				int timeoutSeconds = 60;
				if (leaderLatch.await(timeoutSeconds, TimeUnit.SECONDS)) {
   
					if (!hasLeadership()) {
   
						becomeLeader();
					} else {
   
						log.info("{}-{} becomes a follower", namespace, hostValue);
					}
				} else {
   
					log.error("{}-{} leadership election is timeout({}s)", namespace, hostValue, timeoutSeconds);
				}
			} catch (InterruptedException e) {
   
				log.info("{}-{} leadership election is interrupted", namespace, hostValue);
				throw e;
			} catch (Exception e) {
   
				log.error(namespace + "-" + hostValue + " leadership election error", e);
				throw e;
			}
		} finally {
   
			lock.unlock();
		}
	}

选举其实就是在$SaturnExecutors/leader节点下写入latch节点,成为leader后,回持久化$JOB节点在zk,然后将host写入$SaturnExecutors/leader/host里。

如果当前操作实例成为leader,则进行jobexecutor管理工作,包括上下线,分片等。

console所有的操作最后其实都是异步执行AbstractAsyncShardingTask里面的方法:

	public void run() {
   
		logStartInfo();
		boolean isAllShardingTask = this instanceof ExecuteAllShardingTask;
		try {
   
			// 如果当前变为非leader,则直接返回
			if (!namespaceShardingService.isLeadershipOnly()) {
   
				return;
			}

			// 如果需要全量分片,且当前线程不是全量分片线程,则直接返回,没必要做分片,由于console在选举成功后就会设置全量分片为true,而且立马将全量分片的task
      //提交给线程池。所以一开始是执行全量分片的
			if (namespaceShardingService.isNeedAllSharding() && !isAllShardingTask) {
   
				log.info("the {} will be ignored, because there will be {}", this.getClass().getSimpleName(),
						ExecuteAllShardingTask.class.getSimpleName());
				return;
			}
			// 从zk中获取所有的job
			List<String> allJobs = getAllJobs();
      // 获取所有enable的job
			List<String> allEnableJobs = getAllEnableJobs(allJobs);
      //最后在线的executor。位于$SaturnExecutors/sharding/content,该节点包含了executor的ip,分片值,以及负载等信息
			List<Executor> oldOnlineExecutorList = getLastOnlineExecutorList();
      // 如果当前是全分片,则从/$SaturnExecutors/executors 下拉去所有在线的executor,否则为null
			List<Executor> customLastOnlineExecutorList = customLastOnlineExecutorList();
      // 如果不是全量分片,则copy最后在县的executor。
			List<Executor> lastOnlineExecutorList = customLastOnlineExecutorList == null
					? copyOnlineExecutorList(oldOnlineExecutorList) : customLastOnlineExecutorList;
      //最后没有被摘取流量的executor,$SaturnExecutors/executors/xx/noTraffic true 已经被摘流量;false,otherwise;
			List<Executor> lastOnlineTrafficExecutorList = getTrafficExecutorList(lastOnlineExecutorList);
			List<Shard> shardList = new ArrayList<>();
			// 摘取,该方法为抽象方法,
			if (pick(allJobs, allEnableJobs, shardList, lastOnlineExecutorList, lastOnlineTrafficExecutorList)) {
   
				// 放回
				putBackBalancing(allEnableJobs, shardList, lastOnlineExecutorList, lastOnlineTrafficExecutorList);
				// 如果当前变为非leader,则返回
				if (!namespaceShardingService.isLeadershipOnly()) {
   
					return;
				}
				// 持久化分片结果
				if (shardingContentIsChanged(oldOnlineExecutorList, lastOnlineExecutorList)) {
   
					namespaceShardingContentService.persistDirectly(lastOnlineExecutorList);
				}
				// notify the shards-changed jobs of all enable jobs.
				Map<String, Map<String, List<Integer>>> enabledAndShardsChangedJobShardContent = getEnabledAndShardsChangedJobShardContent(
						isAllShardingTask, allEnableJobs, oldOnlineExecutorList, lastOnlineExecutorList);
				namespaceShardingContentService
						.persistJobsNecessaryInTransaction(enabledAndShardsChangedJobShardContent);
				// sharding count ++
				increaseShardingCount();
			}
		} catch (InterruptedException e) {
   
			log.info("{}-{} {} is interrupted", namespaceShardingService.getNamespace(),
					namespaceShardingService.getHostValue(), this.getClass().getSimpleName());
			Thread.currentThread().interrupt();
		} catch (Throwable t) {
   
			log.error(t.getMessage(), t);
			if (!isAllShardingTask) {
    // 如果当前不是全量分片,则需要全量分片来拯救异常
				namespaceShardingService.setNeedAllSharding(true);
				namespaceShardingService.shardingCountIncrementAndGet();
				executorService.submit(new ExecuteAllShardingTask(namespaceShardingService));
			} else {
    // 如果当前是全量分片,则告警并关闭当前服务,重选leader来做事情
				raiseAlarm();
				shutdownNamespaceShardingService();
			}
		} finally {
   
			if (isAllShardingTask) {
    // 如果是全量分片,不再进行全量分片
				namespaceShardingService.setNeedAllSharding(false);
			}
			namespaceShardingService.shardingCountDecrementAndGet();
		}
	}

针对上面的pick方法:

ExecuteAllShardingTask : 域下重排,移除已经存在所有executor,重新获取executors,重新获取作业shards
  
ExecuteExtractTrafficShardingTask : 摘取executor流量,标记该executor的noTraffic为true,并移除其所有作业分片,只摘取所有非本地作业分片,设置totalLoadLevel为0
  
ExecuteJobDisableShardingTask : 作业禁用,摘取所有executor运行的该作业的shard,注意要相应地减loadLevel,不需要放回
  
ExecuteJobEnableShardingTask: 作业启用,获取该作业的shards,注意要过滤不能运行该作业的executors

ExecuteJobForceShardShardingTask: 作业重排,移除所有executor的该作业shard,重新获取该作业的shards,finally删除forceShard结点
 
ExecuteJobServerOfflineShardingTask: 作业的executor下线,将该executor运行的该作业分片都摘取,如果是本地作业,则移除

ExecuteJobServerOnlineShardingTask : 作业的executor上线,executor级别平衡摘取,但是只能摘取该作业的shard;添加的新的shard
  
ExecuteOfflineShardingTask : executor下线,摘取该executor运行的所有非本地模式作业,移除该executor

总的来说,其实就是完成$SaturnExecutors/sharding/content下的内容,下面的内容是一个数组,从0开始,节点中记录的对象为

[{
   "executorName":"aaa","ip":"0.0.0.0","noTraffic":false,"jobNameList":["job1","job2"],"shardList":[{
   "jobName":"job1","item":0,loadlevel:1},{
   "jobName":"job2","item":0,loadlevel:1}]}]

后面的shardList就是分片后的结果。

上述就是一个namespace的创建过程中的逻辑。

namespace 迁移

console中能够将namespace迁移到其他的zk集群中。迁移主要逻辑就是修改数据库和注册中心的数据。迁移过程过程会记录失败和成功的个数到temporary_shared_status表中。更新过程是先从数据库中拿到当前namespacezk集群名称,然后将当前的节点移动到目的zk集群,删除当前对应的节点,然后刷新数据库中的数据。

如果在迁移过程中,删除了zk节点,但是namespace还没有刷新到数据库中,那么需要通过diff方法将数据补齐。暂时没有发现其他的途径

job

job的创建

job的创建和复制本质上是一样的,只是一个是手写配置,一个是从数据库中拉数据。具体的代码在JobServiceImplcreateJob中。

private void addOrCopyJob(String namespace, JobConfig jobConfig, String jobNameCopied, String createdBy)
			throws SaturnJobConsoleException {
   
    // 一半情况下为空
		List<JobConfig> unSystemJobs = getUnSystemJobs(namespace);
		Set<JobConfig> streamChangedJobs = new HashSet<>();
		validateJobConfig(namespace, jobConfig, unSystemJobs, streamChangedJobs);
		// 如果数据存在相同作业名,则抛异常
		// 直接再查一次,不使用unSystemJobs,因为也不能与系统作业名相同
		String jobName = jobConfig.getJobName();
		if (currentJobConfigService.findConfigByNamespaceAndJobName(namespace, jobName) != null) {
   
			throw new SaturnJobConsoleException(ERROR_CODE_BAD_REQUEST<
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值