天天看點

「後端」支付寶定時任務怎麼做?三層分發任務處理架構介紹

作者:架構思考
本文将從單機定時排程開始,循序漸進地帶領大家了解五福定制三層分發任務處理架構。

一、背景介紹

技術同學對定時任務肯定不陌生。定時任務一般用來定時批量進行業務處理。支付寶卡包券到期提醒、删除過期失效券,五福大促批量給使用者發放添福紅包等場景,都是通過定時任務觸發來完成的。

作者有幸參與了2023兔年五福大促的開發,主導完成了福氣樂園分會場平分5000萬大獎需求。通過學習并運用五福定制三層分發任務處理架構,最終平穩絲滑的完成了平分大獎需求任務。本文将從單機定時排程開始,循序漸進地帶領大家了解五福定制三層分發任務處理架構。

二、定時任務分類

「後端」支付寶定時任務怎麼做?三層分發任務處理架構介紹

本文将定時任務分為單機和叢集兩大類别,其中單機又分為定時排程和定時排程加批處理架構,叢集分為三層分發和五福定制三層分發任務處理架構。

2.1、單機任務

單機定時任務毫無疑問是在單台機器上運作的定時任務。在業務量級不大,沒有進行分庫分表時,往往單機定時任務即可滿足業務需求。

從複雜度上來說,單機定時任務又可分為簡單的定時排程和定時排程+批處理兩種。

1、定時排程

在Spring中可以通過@Scheduled 來啟用定時任務。觸發的方式有兩種,分别是:cron 表達式和 fixedRated類配置參數。常用的案例:

// cron表達式
@Scheduled(cron="0 0/30 9-17 * * ?") //按cron規則執行,朝九晚五工作時間内每半小時
@Scheduled(cron="0 0 12 ? * WED") //按cron規則執行,表示每個星期三中午12點


// fixedRated類配置
@Scheduled(fixedRate=5000) //上一次開始執行時間點後5秒再次執行;
@Scheduled(fixedDelay=3000) //上一次執行完畢時間點後3秒再次執行;
@Scheduled(initialDelay=1000, fixedDelay=2000) //第一次延遲1秒執行,然後在上一次執行完畢時間點後2秒再次執行;           
「後端」支付寶定時任務怎麼做?三層分發任務處理架構介紹

定時排程往往用于業務處理流程比較簡單的場景,比如定時生成簡單報表,發送通知。對于複雜耗時的場景,處理效率不高,業務高峰期會積壓大量待處理資料,影響業務。

2、定時排程+批處理

為了解決複雜耗時場景下定時排程效率不高的問題,可以引入批處理架構。定時排程與批處理架構相結合,可以大幅提高資料處理的效率,提升系統穩定性,保障業務穩定運作。

以Spring Batch批處理架構為例,任務處理流程如下:

「後端」支付寶定時任務怎麼做?三層分發任務處理架構介紹

Spring Batch批處理架構将任務拆分成多個Step,同時每個Step裡面又分為itemReader,itemProcessor, itemWriter。通過将任務分層細化,能夠讓多個階段并行處理,提高任務處理效率。批處理架構結合定時排程架構,可以在單機情況下,對大量複雜的業務進行高效的批處理。

2.2、叢集任務

在分庫分表大業務流量情況下,單機定時任務已無法滿足業務需求了,這時就産生了叢集定時任務。在支付寶技術架構下,使用者資料按照eid進行分庫分表,同時進行Zone次元的隔離。此時單機定時任務無法對全量資料做處理,于是支付寶便有了自己的分布式任務排程中間件Antscheduler,配合三層分發任務處理架構,就可以對大量資料進行定時批量處理。

1、三層分發

「後端」支付寶定時任務怎麼做?三層分發任務處理架構介紹

上圖描述了三層分發實作定時任務處理的過程:

1.Antscheduler任務排程中間件按照配置好的規則,定時往消息中心投遞消息。

2.消息中心将定時任務消息分别投遞到每個Zone中的一台機器。

3.接收到消息的機器進入三層分發的第一層,即Splitor處理流程。通常是擷取目前Zone的eid分片,比如00~24。

4.第一層Splitor處理完了之後,通過TR oneway的調用方式,在目前Zone對三層分發的第二層發起調用,即進入Loader處理流程。此時調用得到擴散,eid分片為00~24的情況下,會發起25次TR調用,即最多會進入目前Zone的25台機器進行Loader處理。

5.Loader通常是擷取傳遞過來eid分片的資料。比如一台機器的Loader接收到eid為20,則該Loader從eid為20分片的DB擷取100條待處理資料。

6.第二層Loader擷取到待處理資料後,同樣是通過TR oneway的調用方式,在目前Zone對三層分發的第三層發起調用,即進入Executor處理流程。此時調用進一步得到擴散,一個Loader擷取的100條資料,此時會發起100次TR調用,即最多會進入目前Zone的100台機器進行Executor處理。

7.Executor通常是進行真正業務處理邏輯的地方。比如對每條資料做狀态變更、發送eid次元消息等等。

三層分發能很好的将任務進行分層拆分擴散,充分利用機器資源,盡量做到負載均衡。但三層分發也存在一些缺陷,主要展現在以下幾個方面:

1.定時排程間隔和間隔内能夠處理的資料量很難完全比對。排程間隔時間太長,機器資源沒有得到有效利用,會導緻處理效率低下,任務會積壓;排程間隔時間太短,有可能會導緻資料被重複撈取處理,為此要做額外的防重複處理邏輯。

2.無法做到平滑的任務處理。由于在Loader層擷取要處理的任務數,交由Executor層執行時,并不能限制任務執行的qps,同時待處理任務數變多時,整個叢集任務的qps就變得很高,對DB和其他外圍系統來說,存在穩定性風險。

3.無法最大化利用叢集機器資源。考慮到穩定性和高可用,設計上每個Zone是有A/B分組的,每個分組都能擷取到本Zone的eid分片,是以配置Antscheduler排程規則時,通常隻會選擇Zone的A/B組中的一組開啟任務排程。三層分發内部每個層級之間的TR調用又隻能在本Zone同組内進行,A/B組之間無法進行TR調用,是以浪費了一半的機器資源。

2、五福定制三層分發

五福大促有很多業務場景都是需要通過定時任務來進行處理的,比如生肖卡提醒、AI年畫提醒,福氣樂園平分5000萬大獎。五福大促對穩定性和可用性的要求是非常高的,為了解決三層分發處理架構缺陷帶來的效率和穩定性風險,五福在三層分發基礎上做了定制化改造,改造的目标主要有兩點:

1)最大化利用叢集機器資源。做到真正的負載均衡,同時也能夠提升叢集的任務處理容量。

2)平滑的任務處理。減少任務調用的尖刺,避免對DB和外部系統造成穩定性風險。

下面分别從優化目标的兩點來進行闡述。

1、最大化利用叢集機器資源

由于三層分發預設隻會在同一個Zone的A/B組中開啟一組,導緻浪費了一半的機器。顯而易見,要最大化利用叢集機器資源,就需要讓A/B組的機器都能夠參與到任務處理當中。優化的步驟如下:

1.Antscheduler定時排程平台同時開啟A/B分組排程。

2.增加任務配置,配置的目的是讓A組的機器隻處理奇數位eid、B組的機器隻處理偶數位eid。

3.Splitor層根據任務配置,将本Zone全量eid進行分組,A組隻處理奇數位eid,B組隻處理偶數位eid。

上面三步做完以後,就能讓叢集所有的機器都能參與到任務處理當中,進而最大化的利用了機器資源。

「後端」支付寶定時任務怎麼做?三層分發任務處理架構介紹

以上圖為例,Zone_01對應DB的eid分片為00~24,經過Splitor處理之後,Zone_01(A)組機器獲得任務處理的eid都是奇數位,即01、03、...23;同理,Zone_01(B)組機器獲得任務處理的eid都是偶數位,即00、02、...24。後續Loader、Executor處理都在本組内進行。通過把eid分片像A/B分組那樣進行奇/偶分組,就能讓所有機器都能夠參與到任務處理當中。下面是eid分組的核心代碼:

/**
* 根據配置中心的dataFlag過濾
* 1、預設ALL不區分
* 2、ODD 表示僅分發奇數表号
* 3、EVEN 辨別僅分發偶數表号
*
* @param eidList
*/
public void filteByDataFlag(List<String> eidList) {
    String dataFlag = SchedulerConfigDrmUtil.getIndexFilterFlag();
    int strategy = -1;
    if (StringUtil.equalsIgnoreCase("ODD", dataFlag)) {
        strategy = 1;
    } else if (StringUtil.equalsIgnoreCase("EVEN", dataFlag)) {
        strategy = 0;
    }
    if (strategy == -1) {
        // ALL
        return;
    }
    // filter
    Iterator<String> it = eidList.iterator();
    while (it.hasNext()) {
        String str = it.next();
        int index = NumberUtils.toInt(str, -1);
        if (index % 2 != strategy) {
            it.remove();
        }
    }
}           

通過代碼可知,推送任務配置時,将A組機器的值推成“ODD”,B組機器的值推成“EVEN”,即可實作A/B組的所有機器同時執行定時任務的效果。

2、平滑的任務處理

預設的三層分發通過Loader層擷取待處理的任務,然後交由Executor來執行,無法保證整個叢集任務處理的量級。在待處理任務變多,或者叢集機器擴縮容變化頻繁的情況下,任務處理的峰值量級無法保證。同時由于各個層級之間的調用是TR oneway調用,是感覺不到調用結果的,也就更難保證任務的平滑調用。為了達到任務平滑調用的目的,五福場景對Loader撈取任務數和單機任務qps做了優化調整,保證了叢集任務處理效率在預期範圍之内。優化的步驟如下:

1.新增任務配置。核心配置資訊包括期望叢集執行任務總的qps、任務排程間隔,叢集參與任務機器數。

2.計算單機qps和每次DB撈取任務的數量。

3.單機執行時,根據計算好的qps來進行限流調用。

上面三步做好之後,整個叢集就能夠按照預期的qps進行平滑的任務處理。

為什麼這三步做完了之後就能達到預期的效果呢? 重點看下任務配置的解析代碼:

/**
 * 計算定時任務的相關配置
 *
 * 主要計算:
 *    scheduleSingleLimit 單機限流值
 *    scheduleLoaderCount loader撈取條數
 *
 * @param scheduleConfig
 */
public static SchedulerConfig calculateScheduleConfig(SchedulerConfig scheduleConfig) {
    final int qpsLimit = scheduleConfig.getScheduleWholeLimit();
    final int machineCounts = scheduleConfig.getScheduleMachineCounts();
    MtLogger.info(LOGGER,
        "【計算定時任務配置】-開始 任務名稱:{0},機器數量:{1},任務吞吐量:{2}.", scheduleConfig.getScheduleType(),
        machineCounts, qpsLimit);


    //定時任務的排程頻率是scheduleRate 秒執行一次 是以scheduleRate秒中内叢集的整體吞吐量=qps限制*scheduleRate
    final int scheduleRate = scheduleConfig.getScheduleRatePerSec();
    long totalLoaderCounts = qpsLimit * TimeUnit.SECONDS.toSeconds(scheduleRate);


    //定時任務撈取的表數量為1000 是以到每個表的限制=totalLoaderCounts/1000
    long loaderCountPerTask = totalLoaderCounts / 1000;
    if (loaderCountPerTask < 1) {
        loaderCountPerTask = 1;
    }
    scheduleConfig.setScheduleLoaderCount((int) loaderCountPerTask);


    //整體限流通過單機限流實作 整體限流=單機限流*machineCounts
    final double singleQps = ((double) qpsLimit / machineCounts);
    //建立的限流需要1秒的預熱
    scheduleConfig.setScheduleSingleLimit(RateLimiter.create(singleQps, 1, TimeUnit.SECONDS));


    MtLogger.info(LOGGER,
        "【計算定時任務配置】-結束 任務名稱:{0},撈取條數:{1},單機限流:{2}.", scheduleConfig.getScheduleType(),
        scheduleConfig.getScheduleLoaderCount(),
        scheduleConfig.getScheduleSingleLimit().getRate());


    return scheduleConfig;
}           

通過代碼可知,推送的任務配置最終會生成兩個重要的配置資訊:

1.單個Loader撈取的任務數。叢集qps和排程間隔确定了一個排程間隔内要處理的任務數,結合eid分片數量(五福是千庫千表)确定每個Loader要撈取的任務數。

2.單機Executor執行任務時的qps。預期叢集qps和叢集機器數确定了單機執行任務時的qps,單機上通過Guava的RateLimit來達到限流的效果。如果請求超過了限制的qps,請求将會被阻塞。

下面以福氣樂園平分5000萬大獎的任務配置作為樣例來計算:

「後端」支付寶定時任務怎麼做?三層分發任務處理架構介紹

相比通常情況下指定Loader每次撈取的任務數,五福是通過叢集qps和任務排程間隔來确定Loader需要撈取的任務數。是以一個排程間隔内的任務數和叢集能夠執行的任務數是比對上的,加上通過單機qps限制達到叢集qps限制的效果,進而讓整個定時任務做到了平滑調用。簡而言之,優化後的排程邏輯,能夠讓定時排程任務在機器次元和時間次元都能均勻平穩的執行。

「後端」支付寶定時任務怎麼做?三層分發任務處理架構介紹

上面的截圖是福氣樂園平分5000萬大獎真實發獎時的調用監控。通過監控可以看到,每隔5秒鐘,整個叢集Loader撈取的任務數是40000個。整個任務生命周期内,Executor執行任務的qps在8000上下小範圍波動,沒有出現大範圍的波動,進而達到了平滑任務處理的預期效果。

五福定制三層分發任務處理架構,能夠很好的解決任務效率和平滑調用的問題,但也存在一些使用限制。每種任務的任務配置需要人工手動推送,而其中的機器數實際上很多時候并不是固定的。推送的機器數和實際叢集擁有的機器數不比對的結果就是單機限流失準,導緻叢集限流不符合預期,也就導緻任務間隔内撈取的任務數和能處理的任務數無法比對上。五福場景下,機器數确定後一般不會出現大範圍變動,而且五福各種業務都屬于重點高保業務,機器變動後大家都能及時感覺到,是以這種定制邏輯在五福場景下是可以很好的運作起來。日常的各種業務,通常叢集機器數并不是固定的,存在各種擴縮容的情況,五福定制三層分發任務處理架構依舊不能保證任務的平滑調用。

三、結語

從單機到叢集,再到五福定制叢集定時任務,本文逐漸做了一個架構設計上的原理介紹。每種定時任務都有自己的優點和缺陷,也都有自己的應用場景。在工作中,要結合目前的業務情況,選擇合适的定時任務進行業務處理,避免設計上的失誤導緻業務受損。以五福定制三層分發任務處理架構為例,雖然日常業務中,因為機器數量不固定,依舊無法做到任務的平滑調用,但我們可以借鑒最大化利用叢集機器資源這一點,同時開啟A/B組的定時任務,進而實作任務排程真正的負載均衡,提高系統整體的穩定性。

文章來源:金盛傑(司旭)_阿裡開發者_https://mp.weixin.qq.com/s/6zY3ZtilM1jA5gMPMDRQyA