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
中,大部分都是一些定时任务。重点关注namespace
和job
管理,包括创建,分片等等。
namespace的创建
在console
创建域,需要传入namespace
和zkCluster
,后台由RegistryCenterController
中createNamespace
进行创建。核心逻辑如下:
@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
分片由获取到该namespace
的leader
进行操作,每一步操作都会判断当前操作的对象是否为namespace
的leader
(是否已经创建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
,则进行job
和executor
管理工作,包括上下线,分片等。
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
表中。更新过程是先从数据库中拿到当前namespace
的zk
集群名称,然后将当前的节点移动到目的zk集群,删除当前对应的节点,然后刷新数据库中的数据。
如果在迁移过程中,删除了zk
节点,但是namespace
还没有刷新到数据库中,那么需要通过diff
方法将数据补齐。暂时没有发现其他的途径
job
job的创建
job
的创建和复制本质上是一样的,只是一个是手写配置,一个是从数据库中拉数据。具体的代码在JobServiceImpl
的createJob
中。
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<