LTS簡介以及與SpringBoot的簡單集成
一 什麼是LTS
關於定時任務,雖然Spring提供了基於註解@EnableScheduling
@Scheduled
的實現方式。其實現是通過線程池ScheduledThreadPoolExecutor
的方式,具體這裏就不多做介紹啦,有興趣的小夥伴可以自行了解下~
但是以上只適用於單機的情況下,如果是分佈式項目的話,就會顯得有些力不從心了。所以這裏給大家介紹一個分佈式任務調度框架
-> LTS,它是阿里巴巴的一個開源項目。
借用官方文檔的一句話就是:
LTS 着力於解決分佈式任務調度問題,將任務的提交者和執行者解耦,解決任務執行的單點故障,支持動態擴容,出錯重試等機制。代碼程序設計上,參考了優秀開源項目Dubbo,Hadoop的部分思想。
二 LTS架構總覽
以下圖片來自官方文檔
乍一看這都啥跟啥啊,有點麻煩的樣子,別急,且聽我慢慢道來,咱們看圖說話。
從上圖可以看出,LTS包含以下幾種節點類型:
JobClient
-> 負責提交任務,並接收任務執行反饋結果。JobTracker
-> 負責任務調度,接收並分配任務。TaskTracker
-> 負責執行任務,執行完之後將任務執行結果反饋給JobTracker
。Monitor
-> 負責收集各個節點的監控信息,包括任務監控信息,節點JVM監控信息。Admin
-> 則是後臺管理,負責節點管理,任務隊列管理,監控管理等。
LTS的這五種節點都是無狀態的,都可以部署多個,動態擴容,來實現負載均衡,實現更大的負載量, 並且框架採用FailStore策略使LTS具有很好的容錯能力。
既然節點可以以集羣的方式部署,那麼肯定少不了註冊中心啦!如上圖第一行所示:
註冊中心
可以使Zookeeper
或者Redis
,官方推薦
使用Zookeeper
作爲註冊中心(劃重點,要考的)。
繼續看圖,FailStore
-> 顧名思義就是失敗存儲
,主要用於在部分場景遠程RPC調用失敗的情況,採取現存儲本地KV文件系統,待遠程通信恢復的時候再進行數據補償。
主要用於節點容錯,當遠程數據交互失敗之後,存儲在本地,等待遠程通信恢復的時候,再將數據提交。
接着是FailStore的右邊,有個QueueManager
-> 任務隊列
,主要用於存儲任務數據和任務執行日誌等。支持mysql
和mongodb
實現,官方推薦使用mysql
,當然也可以自己擴展實現,例如Oracle。
然後是節點組NodeGroup
,每個節點組的節點都是平等的,對外提供相同的服務。每個節點組中都有一個master節點,動態選舉得到的。
最後是ClusterName
,也就是LTS集羣
,就如上圖所示,整個圖就是一個集羣
,包含LTS的五種節點。
OK,大概瞭解了LTS的架構之後,讓我們繼續瞭解一下LTS的執行流程吧!
三 LTS執行流程
以下圖片來自官方文檔
說了這麼多,那麼LTS都支持什麼類型的任務呢?目前支持以下幾種任務:
實時任務,提交之後就會馬上執行的任務;
定時任務,在指定時間點執行的任務,譬如 今天3點執行(單次)。
Cron任務:CronExpression,和quartz類似(但是不是使用quartz實現的)譬如 0 0/1 * ?
Repeat任務(重複任務):譬如每隔5分鐘執行一次,重複50次就停止。
由於篇幅有限,關於LTS的介紹就先到這裏,更多介紹小夥伴可以參閱 官方文檔 ~
LTS gihub地址 -> LTS項目源碼
LTS 例子地址 -> LTS使用實例
你可以直接下載LTS使用實例,按照官方指示,在本地運行起來。 裏面集成了spring以及springboot等的使用~
好了,接下來,咱們就正式進入SprigBoot和LTS的集成。
四 SpringBoot集成LTS
特別說明:本示例的主要目的僅僅是告訴大家如何使用LTS,所以偷了個懶,將所有節點都揉合到了一個工程,實際項目是分開部署的,因需而定。
整個工程其實很簡單,不信你看:
1. 準備工作
- 新建SpringBoot工程(這不廢話嗎);
- 導入相應的依賴,如果你是導入的官方example,則不需要做這些工作。如果是新建SpringBoot工程,我的版本是
2.2.1.RELEASE
,則不要直接把官方的example pom依賴複製過來,因爲版本兼容問題,可能會導致錯誤~ 以下是我的pom文件依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lts-->
<dependency>
<groupId>com.github.ltsopensource</groupId>
<artifactId>lts</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>2.0-beta10</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.14</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.26</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
</dependencies>
注意:如果你用的是Redis作爲註冊中心,mongodb作爲任務隊列,那麼請引入相應的依賴,我這裏用的是Zookeeper和mysql。
- 然後是配置文件信息,如下:
##########################################
# jobclient->負責提交任務以及接收任務執行結果 #
##########################################
#集羣名稱
lts.jobclient.cluster-name=test_cluster
#註冊中心
lts.jobclient.registry-address=zookeeper://127.0.0.1:2181
#JobClient節點組名稱
lts.jobclient.node-group=test_jobClient
#是否使用RetryClient
lts.jobclient.use-retry-client=true
#失敗存儲,用於服務正常後再次執行(容錯處理)
lts.jobclient.configs.job.fail.store=mapdb
#######################################
# jobtracker->負責調度任務 接收並分配任務 #
#######################################
lts.jobtracker.cluster-name=test_cluster
lts.jobtracker.listen-port=35001
lts.jobtracker.registry-address=zookeeper://127.0.0.1:2181
lts.jobtracker.configs.job.logger=mysql
lts.jobtracker.configs.job.queue=mysql
lts.jobtracker.configs.jdbc.url=jdbc:mysql://127.0.0.1:3306/lts
lts.jobtracker.configs.jdbc.username=root
lts.jobtracker.configs.jdbc.password=root
###########################################################
# tasktracker->負責執行任務 執行完任務將執行結果反饋給JobTracker #
###########################################################
lts.tasktracker.cluster-name=test_cluster
lts.tasktracker.registry-address=zookeeper://127.0.0.1:2181
#TaskTracker節點組默認是64個線程用於執行任務
#lts.tasktracker.work-threads=64
lts.tasktracker.node-group=test_trade_TaskTracker
#lts.tasktracker.dispatch-runner.enable=true
#lts.tasktracker.dispatch-runner.shard-value=taskId
lts.tasktracker.configs.job.fail.store=mapdb
################################################################
# jmonitor->負責收集各個節點的監控信息,包括任務監控信息,節點JVM監控信息 #
################################################################
lts.monitor.cluster-name=test_cluster
lts.monitor.registry-address=zookeeper://127.0.0.1:2181
lts.monitor.configs.job.logger=mysql
lts.monitor.configs.job.queue=mysql
lts.monitor.configs.jdbc.url=jdbc:mysql://127.0.0.1:3306/lts
lts.monitor.configs.jdbc.username=root
lts.monitor.configs.jdbc.password=root
肯定有人要問了,哪來的lts數據庫,所以,還需要新建一個數據庫,名爲lts
,其他不用管,因爲表信息在項目啓動之後會自動創建的。
除了application.properties
配置文件,還需要新建log4j.properties
日誌配置文件,如下:
log4j.rootLogger=INFO,stdout
log4j.appender.stdout.Threshold=INFO
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d [%t] (%F:%L) %-5p %c %x - %m%n
說明:以上配置信息,均可以在官方示例lts-example
找到。
完成以上準備工作之後,接着便是實現任務提交以及任務執行了。
2. 啓動類設置
其實很簡單,加上相應的註解即可:
@SpringBootApplication
@EnableJobClient //JobClient
@EnableTaskTracker //TaskTracker
@EnableJobTracker //JobTracker
@EnableMonitor //Monitor
public class LtstestApplication {
public static void main(String[] args) {
SpringApplication.run(LtstestApplication.class, args);
}
}
哈哈,是不是和集成微服務很像啊,也是需要添加相應的註解,具體作用一目瞭然,就不多做贅述啦~
3. JobClient提交任務
關於Jobclient使用的官方建議
:
一般在一個JVM中只需要一個JobClient實例即可,不要爲每種任務都新建一個JobClient實例,這樣會大大的浪費資源,因爲一個JobClient可以提交多種任務。
本示例中,我直接寫在了TestController
中,模擬了提交兩個不同的任務:
@Autowired
private JobClient jobClient;
@GetMapping("test01")
public Map<String, Object> test01() {
//模擬提交一個任務
Job job = new Job();
job.setTaskId("task-AAAAAAAAAAAAAAA");
job.setCronExpression("0/3 * * * * ?");
//設置任務類型 區分不同的任務 執行不同的業務邏輯
job.setParam("type", "aType");
job.setNeedFeedback(true);
//任務觸發時間 如果設置了 cron 則該設置無效
// job.setTriggerTime(DateUtils.addDay(new Date(), 1).getTime());
//任務執行節點組
job.setTaskTrackerNodeGroup("test_trade_TaskTracker");
//當任務隊列中存在這個任務的時候,是否替換更新
job.setReplaceOnExist(false);
Map<String, Object> submitResult = new HashMap<String, Object>(4);
try {
//任務提交返回值 response
Response response = jobClient.submitJob(job);
submitResult.put("success", response.isSuccess());
submitResult.put("msg", response.getMsg());
submitResult.put("code", response.getCode());
} catch (Exception e) {
log.error("提交任務失敗", e);
throw new RuntimeException("提交任務失敗");
}
return submitResult;
}
@GetMapping("test02")
public Map<String, Object> test02() {
//模擬提交一個任務
Job job = new Job();
job.setTaskId("task-BBBBBBBBBBBBBBB");
job.setCronExpression("0/6 * * * * ?");
//設置任務類型 區分不同的任務 執行不同的業務邏輯
job.setParam("type", "bType");
job.setNeedFeedback(true);
//任務觸發時間 如果設置了 cron 則該設置無效
// job.setTriggerTime(DateUtils.addDay(new Date(), 1).getTime());
//任務執行節點組
job.setTaskTrackerNodeGroup("test_trade_TaskTracker");
//當任務隊列中存在這個任務的時候,是否替換更新
job.setReplaceOnExist(false);
Map<String, Object> submitResult = new HashMap<String, Object>(4);
try {
Response response = jobClient.submitJob(job);
submitResult.put("success", response.isSuccess());
submitResult.put("msg", response.getMsg());
submitResult.put("code", response.getCode());
} catch (Exception e) {
log.error("提交任務失敗", e);
throw new RuntimeException("提交任務失敗");
}
return submitResult;
}
注意看:JobClient我們可以直接引入,然後構建一個Job,通過JobClient進行提交。任務提交之後,JobTracker會對任務進行分發,分發方式有如下兩種:
TaskTracker會定時發送pull請求給JobTracker, 默認1s一次, 在發送pull請求之前,會檢查當前TaskTracker是否有可用的空閒線程,如果沒有則不會發送pull請求,同時也會檢查本節點機器資源是否足夠,主要是檢查cpu和內存使用率,默認超過90%就不會發送pull請求,當JobTracker收到TaskTracker節點的pull請求之後,再從任務隊列中取出相應的已經到了執行時間點的任務 push給TaskTracker,這裏push的個數等於TaskTracker的空餘線程數。
還有一種途徑是,每個TaskTracker線程處理完當前任務之後,在反饋給JobTracker的時候,同時也會詢問JobTracker是否有新的任務需要執行,如果有JobTracker會同時返回給TaskTracker一個新的任務執行。所以在任務量足夠大的情況下,每個TaskTracker基本上是滿負荷的執行的。
4. TaskTracker執行任務
關於TaskTracker使用的官方建議
:
一個JVM一般也儘量保持只有一個TaskTracker實例即可,多了就可能造成資源浪費。
當遇到一個TaskTracker要運行多種任務的時候,在一個JVM中,最好使用一個TaskTracker去運行多種任務,因爲一個JVM中使用多個TaskTracker實例比較浪費資源(當然當你某種任務量比較多的時候,可以將這個任務單獨使用一個TaskTracker節點來執行)。
上面提交了兩個任務,分別是任務A
和任務B
,所以這裏演示的是一個TaskTracker執行多種不同的任務
。
任務的執行必須實現JobRunner
接口,如下任務A
:
public class JobRunnerA implements JobRunner {
@Override
public Result run(JobContext jobContext) throws Throwable {
// TODO A類型Job的邏輯
System.out.println("我是Runner A");
return null;
}
}
任務B
同理,就不重複貼出代碼了。
需要指出的是,在SpringBoot中,任務的執行需要添加@JobRunner4TaskTracker
註解,但是有且只能有一個@JobRunner4TaskTracker
註解。所以,對於同一個TaskTracker執行不同的任務
,需要進行調度執行,如下:
/**
* 總入口,在 taskTracker.setJobRunnerClass(JobRunnerDispatcher.class)
* JobClient 提交 任務時指定 Job 類型 job.setParam("type", "aType")
*/
@JobRunner4TaskTracker
public class JobRunnerDispatcher implements JobRunner {
private static final Logger log = LoggerFactory.getLogger(JobRunnerDispatcher.class);
private static final ConcurrentHashMap<String/*type*/, JobRunner>
JOB_RUNNER_MAP = new ConcurrentHashMap<String, JobRunner>();
static {
JOB_RUNNER_MAP.put("aType", new JobRunnerA()); // 也可以從Spring中拿
JOB_RUNNER_MAP.put("bType", new JobRunnerB());
}
@Override
public Result run(JobContext jobContext) throws Throwable {
Job job = jobContext.getJob();
String type = job.getParam("type");
return JOB_RUNNER_MAP.get(type).run(jobContext);
}
}
說明:該JobRunnerDispatcher
類同樣實現了JobRunner
接口,並且添加了 @JobRunner4TaskTracker
註解,表示該類纔是真正會執行任務的地方。通過該類,實現不同的任務執行。
實際上,到這裏基本整個LTS任務從提交到執行就已經完成了,也就是簡單的集成完成了。
可以直接啓動項目了~
5. master節點監聽以及任務完成處理類
這個不必多說,直接看代碼好了(來自lts-example
):
/**
* 主節點監聽
*/
@MasterNodeListener
public class MasterNodeChangeListener implements MasterChangeListener {
private static final Logger log = LoggerFactory.getLogger(MasterNodeChangeListener.class);
/**
* @param master master節點
* @param isMaster 表示當前節點是不是master節點
*/
@Override
public void change(Node master, boolean isMaster) {
// 一個節點組master節點變化後的處理 , 譬如我多個JobClient, 但是有些事情只想只有一個節點能做。
if (isMaster) {
log.info("我變成了節點組中的master節點了, 恭喜, 我要放大招了");
} else {
log.info(StringUtils.format("master節點變成了{},不是我,我不能放大招,要猥瑣", master));
}
}
}
/**
* 任務完成處理類
*/
@Component
public class JobCompletedHandlerImpl implements JobCompletedHandler {
private static final Logger log = LoggerFactory.getLogger(JobCompletedHandlerImpl.class);
@Override
public void onComplete(List<JobResult> jobResults) {
//對任務執行結果進行處理 打印相應的日誌信息
if (CollectionUtils.isNotEmpty(jobResults)) {
for (JobResult jobResult : jobResults) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
log.info("任務執行完成taskId={}, 執行完成時間={}, job={}",
jobResult.getJob().getTaskId(), time, jobResult.getJob().toString());
}
}
}
}
6. Admin後臺管理
這個直接去官方源碼複製下來admin
模塊,然後丟到Tomcat本地啓動就OK了~
7.項目啓動
啓動項目,分別調用接口 /test01() /test02()
可以看到,master節點的變化是在監聽中的,以及不同的任務分別在執行。
OK,那麼這些信息,在後臺管理
能看到嗎,答案是肯定的如圖:
可以看到,通過後臺管理,我們可以暫停或者刪除任務,以及添加新的任務。當然還有很多其他功能…
OK,關於LTS的簡單介紹,以及與SpringBoot的集成,就到這裏啦~
希望對看過的小夥伴能有幫助~