擁抱Kubernetes,再見了SpringBoot cronjob
“?寫給自己看,說給別人聽。你好,這是think123的第77篇原創(chuàng)文章”

這張圖是我媳婦兒用手機(jī)拍的,我覺得很棒。放出來讓大家欣賞下
項目開發(fā)中總是需要執(zhí)行一些定時任務(wù),比如定時處理數(shù)據(jù)之后發(fā)送郵件,定時更新緩存等等。
Java定時任務(wù)
基于 java.util.Timer 定時器,實現(xiàn)類似鬧鐘的定時任務(wù) 使用 Quartz、elastic-job、xxl-job 等開源第三方定時任務(wù)框架,適合分布式項目應(yīng)用 使用 Spring 提供的一個注解:@Schedule
項目框架使用的是SpringBoot,所以之前定時任務(wù)使用的是SpringBoot中的@Scheduled。可是這種方式并不適合我們現(xiàn)在的cloud環(huán)境,為了更加cloud native一點,我刪除了使用SpringBoot寫的37個定時任務(wù),改為使用Kubernetes cronjob的方式。
定時任務(wù)代碼編寫
public?interface?Command?{
????/**
?????*?遵循Unix約定,如果命令執(zhí)行正常,則返回0;否則為非0。
?????*/
????int?execute(String...?args);
}
首先定義一個接口,所有具體的定時任務(wù)都必須實現(xiàn)該接口。接下來是具體的某一個定時任務(wù)
@Component
@Slf4j
public?class?ProjectCommandLineRunner?implements?CommandLineRunner?{
??Map?commandMap?=?new?HashMap<>();
??@Autowired
??private?SendEmailCommand?sendEmailCommand;
??@PostConstruct
??private?void?init()?{
????commandMap.put("sendEmail",?sendEmailCommand);
??}
??@Override
??public?void?run(String...?args)?throws?Exception?{
????if?(args.length?==?0)?{
????????return;
????}
????if?(!commandMap.containsKey(args[0]))?{
????????log.error("'{}'?command?not?found",?args[0]);
????????System.exit(-1);
????}
????Command?command?=?commandMap.get(args[0]);
????String[]?arguments?=?Arrays.copyOfRange(args,?1,?args.length);
????System.exit(command.execute(arguments));
??}
}
@Component
@Slf4j
public?class?SendEmailCommand?implements?Command?{
??@Override
??public?int?execute(String...?args)?{
????try?{
??????//?省略業(yè)務(wù)邏輯代碼
??????log.info("send?email?success");
??????return?0;
????}?catch?(Exception?e)?{
??????log.error("send?email?error",?e);
??????return?-1;
????}
??}
}
上面的代碼我們采用了策略模式,后面即使新增其他定時任務(wù)也只是會改動很少的代碼。
本地調(diào)試
cronjob不用打包成單獨的鏡像,它直接和我們的web應(yīng)用公用同一個鏡像,本地調(diào)試的時候也是極為方便的,只需要我們啟動SpringBoot Application時指定參數(shù)即可

對應(yīng)的定時任務(wù)執(zhí)行完成之后就會,application就會退出。
cronjob yaml
一個基本的cronjob yaml如下所示
apiVersion:?batch/v1beta1
kind:?CronJob
metadata:
??name:?send-email-job
spec:
??failedJobsHistoryLimit:?3
??successfulJobsHistoryLimit:?1
??startingDeadlineSeconds:?180
??concurrencyPolicy:?Forbid
??schedule:?"0?4?*?*?1-5"
??jobTemplate:
????spec:
??????template:
????????spec:
??????????containers:
????????????-?name:?send-email-job
??????????????image:?harbor.xxx.com/think123/project
??????????????imagePullPolicy:?Always
??????????????command:?["java"]
??????????????args:?["-jar","/app/target/think123-task.jar","sendEmail"]
??????????????envFrom:
????????????????-?configMapRef:
????????????????????name:?smcp-config
????????????????-?secretRef:
????????????????????name:?smcp-service-secret
??????????????resources:
????????????????requests:
??????????????????cpu:?"250m"
??????????????????memory:?1024Mi
????????????????limits:
??????????????????cpu:?"500m"
??????????????????memory:?1024Mi
??????????restartPolicy:?Never
在定時任務(wù)中,可能某個job還沒有執(zhí)行完,另外一個job就產(chǎn)生了。這個時候我們可以通過spec.concurrencyPolicy字段來定義具體的處理策略。
concurrencyPolicy=Allow,這也是默認(rèn)情況,這意味著這些Job可以同時存在; concurrencyPolicy=Forbid,這意味著不會創(chuàng)建新的Pod,該創(chuàng)建周期被跳過; concurrencyPolicy=Replace,這意味著新產(chǎn)生的Job會替換舊的、沒有執(zhí)行完的Job
幾個關(guān)鍵參數(shù)解釋如下:
schedule : Unix Cron格式的表達(dá)式,cron表達(dá)式中的五個部分分別代表:分鐘、小時、日、月、星期。 startingDeadlineSeconds :表示在過去的多少秒(這里設(shè)置的180)里,如果job創(chuàng)建失敗的數(shù)據(jù)達(dá)到了100次,那么這個job就不會被創(chuàng)建執(zhí)行了。 restartPolicy: 重啟策略(有Never和OnFailure兩個選項)。當(dāng)job正常結(jié)束之后是否需要重啟
restartPolicy在Job對象里只允許被設(shè)置為Never和OnFailure;而在Deployment對象里,restartPolicy則只允許被設(shè)置為Always。
實際上在jobTemplate.spec.template中可以像pod中那樣,指定volume,指定nodeSelector,都是可以的。這個template實際上就是指的pod的template。比如上面示例我們就指定了環(huán)境變量,我們的一些參數(shù)就可以通過環(huán)境變量進(jìn)行注入,比如redis地址,mongodb用戶名密碼等。
實際使用
上面的yaml雖然可以直接使用,但是我們用不著每個job都去寫一份同樣的模板,實際中我們會使用Kustomize控制模板來生成job。比如我們有一個新的任務(wù),是計算熱點文章并更新redis
對于這個任務(wù)而言,變化的主要有兩個地方,第一個是定時任務(wù)的時間不同,第二個是指定的參數(shù)不同。所以我們的每個任務(wù)只需要更新這兩個參數(shù)就行了
關(guān)于kustomize的使用可以參考我之前的kustomize的介紹,打包的話可以看看springboot build的文章
對于模板設(shè)定,我們形成了下面的目錄結(jié)構(gòu)
$?tree
.
|--?base
|???|--?cronjob.yaml
|???`--?kustomization.yaml
`--?overlay
????`--?beta
????????|--?kustomization.yaml
????????`--?send-email-patch-args.yaml
cronjob.yaml作為所有job的模板,而send-emial-patch-args.yaml則是針對具體的job的一個替換。涉及到的yaml文件內(nèi)容如下:
#?base/kustomization.yaml
apiVersion:?kustomize.config.k8s.io/v1beta1
kind:?Kustomization
resources:
-?cronjob.yaml
#?base/cronjob.yaml
apiVersion:?batch/v1beta1
kind:?CronJob
metadata:
??name:?think123-
spec:
??failedJobsHistoryLimit:?3
??successfulJobsHistoryLimit:?1
??startingDeadlineSeconds:?180
??concurrencyPolicy:?Forbid
??schedule:?"0?0?1?*?*"
??jobTemplate:
????spec:
??????template:
????????spec:
??????????containers:
????????????-?name:?cron-job
??????????????image:?harbor.xxx.com/think123/my-task
??????????????imagePullPolicy:?Always
??????????????args:
????????????????-?"help"
??????????????envFrom:
????????????????-?configMapRef:
????????????????????name:?smcp-config
????????????????-?secretRef:
????????????????????name:?smcp-service-secret
??????????????resources:
????????????????requests:
??????????????????cpu:?"250m"
??????????????????memory:?1024Mi
????????????????limits:
??????????????????cpu:?"500m"
??????????????????memory:?1024Mi
??????????restartPolicy:?Never
#?overlay/beta/kustomization.yaml
apiVersion:?kustomize.config.k8s.io/v1beta1
kind:?Kustomization
nameSuffix:?send-email-job
resources:
-?../../base/
patchesStrategicMerge:
??-?send-email-patch-args.yaml
#?overlay/beta/send-email-patch-args.yaml
apiVersion:?batch/v1beta1
kind:?CronJob
metadata:
??name:?think123-
spec:
??schedule:?"0?4?*?*?1-5"
??jobTemplate:
????spec:
??????template:
????????spec:
??????????containers:
????????????-?name:?send-email-job
??????????????args:?["sendEmail"]
你可以使用kustomize build beta > send-email-cron-job.yaml命令,然后查看send-email-cron-job.yaml文件,就可以看到生成的具體的cronjob的詳細(xì)。
kustomize的文檔可以參考: https://kubernetes-sigs.github.io/kustomize/api-reference/
為什么要用Kubernetes Cron Job
使用SpringBoot的定時任務(wù)不香嗎?為什么要還要引入新的東西。再想這個問題的時候,想想為什么你在SpringBoot中不寫Servlet,不是一樣可以嗎?
其實想想還是有原因的,首先我們的服務(wù)是分布式的,我們的定時任務(wù)應(yīng)該只需要運行一次,而不是每個實例都運行一次,如果用SpringBoot的task那么我們需要用代碼來保證這個行為。
如果引入分布式任務(wù)框架,又是引入了一堆其他新的東西,比如注冊中心等等,而且還要去學(xué)習(xí)一項新的技術(shù)。
而我們的服務(wù)由于是通過Kubernetes部署的,我們的job再使用Kubernetes來,更是相得益彰。
作者:think123, 一個試圖把問題想簡單的程序員。
"三思而后行 , think23"
