天天看點

架構詳解——淘系圈品進化史第一階段

原創 周忠太(默達) 淘系技術  2020-12-09

引言

商品資料是營銷的基礎,很多營銷工具最終都會涉及到商品資料的處理,比如打标、修改商品的feature、調用各種下遊系統,單個商品可以通過同步方式處理,實際業務上會依據一定業務規則圈定大量商品并對其進行處理,是以,卡券商品設定引擎應運而生。

卡券商品設定引擎(俗稱圈品)的作用是,依據一定的業務規則從資料源擷取商品,篩選符合規則的商品并按照業務自定義的操作設定商品優惠。設定商品優惠主要是圍繞商品中心、營銷中心等多個域進行操作,圈品的一個重要能力就是保障商品優惠設定後各個域的資料一緻性。商品資料經常發生變化,變化後可能會使商品不符合圈品規則,圈品另外一個重要能力就是能夠監聽全量的商品中心變更。卡券商品設定引擎全局視角圖如下所示。

架構詳解——淘系圈品進化史第一階段

圈品三個關鍵要素:資料源、規則、業務處理,三要素都支援橫向擴充。資料源是圈品的資料來源,不同的資料源接入方式和查詢方式不同。規則用于資料過濾,隻有符合規則的資料才能接下去處理。符合規則的資料在業務上需要進行一定的處理,業務處理可以自定義。

從2017年發展至今,圈品經曆了4個雙11以及數不清的大促和日常活動,圈品目前擁有千萬級商品實時處理能力、資料一緻性保障能力、監聽全量商品變更能力以及平台化能力等。

本文将圈品的發展劃分為兩個階段,第一個階段,奠基了圈品的架構,第二階段,提升了系統的穩定性和性能、增加了一緻性保障能力。

第一階段

▐  概述

生命周期

圈品通過活動概念來進行生命周期的管理,圈品池關聯了規則和業務,圈品池詳情是商品的集合,商品處理完成後會儲存到商品池詳情中,活動、圈品池、圈品池詳情模型如圖2.1所示。

一個活動可以關聯多個圈品池,一個圈品池隻屬于一個活動,圈品池設定圈品規則後會按照業務自定義的動作完成商品的處理,活動過程中商品發生變化時會産生商品變更消息,圈品會通過監聽商品變更消息動态處理商品,活動結束後會觸發結束後的動作,生命周期如圖2.2所示。

首次設定圈品池規則後會觸發圈品從資料源拉取全量商品進行處理,我們稱之為全量圈品。在全量圈品完成後,資料源會發生變化或者商品資訊發生變化,這些發生變化的商品需要重新經過圈品處理,稱之為增量圈品。

架構詳解——淘系圈品進化史第一階段

圖2.1 卡券商品設定引擎模型圖

架構詳解——淘系圈品進化史第一階段

圖2.2 卡券商品p設定引擎生命周期

系統架構

圈品架構圖如圖2.3所示,圈品可以劃分為四個子產品,分别是資料源子產品、動作子產品、規則子產品和業務處理子產品,設定端劃分為三個部分:活動設定、圈品池設定、規則設定。接下來将圍繞圈品四個核心子產品進行講解。

架構詳解——淘系圈品進化史第一階段

圖2.3 圈品架構圖

▐  資料源子產品

資料源子產品是圈品的資料源來源,下面主要分四類進行講解,分别是商品清單、商家清單、同步庫表以及商品變更消息。

這些資料源又衍生出了多種圈品方式,比如商品清單圈品方式、賣家清單圈品方式、大促現貨圈品方式、營銷站點圈品方式、賣家大促商品圈品方式、飛豬賣家圈品方式、新零售攤位圈品方式等等。拿大促現貨圈品方式舉例,每次大促營銷平台招商都要招現貨商品,大促現貨圈品池方式可以圈指定大促的全部現貨商品,也可以結合類目、商品标等其他規則過濾商品。

商品清單

商品清單是最簡單的資料源,直接通過商品ID指定資料源的商品範圍,因為是直接填商品ID的方式,網絡傳輸限制最大支援10W商品。全量圈品過程中從圈品規則中擷取全量商品進行處理,增量圈品是通過監聽商品變更消息進行處理。

賣家清單

賣家清單資料源是賣家ID的集合,通過指定賣家ID來确定商品範圍。全量圈品的過程中根據賣家ID從店鋪搜尋接口中擷取賣家商品,增量圈品是通過監聽變更消息,賣家新釋出商品或者變更商品都會觸發商品變更消息,通過監聽商品變更消息便可以進行增量圈品。

同步庫表

同步庫表是值使用精衛同步原資料庫到新的資料庫供圈品使用,采用這種方式比較靈活且不會對原資料源産生影響。全量圈品是通過掃表方式擷取全量的資料。增量圈品有兩個管道,第一是通過精衛監聽資料庫的變更,第二是監聽商品變更消息。

根據資料源的特性又可以衍生出多種圈品方式,營銷平台招商資料源支援大促現貨圈品方式、營銷平台站點方式、賣家大促商品圈品方式等,新零售商品資料源支援攤位和業務身份圈品方式。

商品變更消息

由于商品資訊變更會導緻商品不符合規則,需要對變更的商品進行增加或删除,比如小二設定圈選某個類目的商品,賣家可以對商品類目進行編輯,原來符合類目規則的商品變得不符合類目需要删除,原來不符合類目的商品現在符合類目需要進行增加。商品資訊的變更都會觸發商品變更消息,是以增量圈品中都有一種途徑就是處理商品變更消息。

商品變更消息日常平均qps在1w左右,峰值QPS可達4w多。在這一階段,因為每一個圈品池規則都是獨立的且無法确定一個商品與商品池的關系,是以每個圈品池都處理了全量商品變更消息。假設商品變更消息QPS是1w,目前有效圈品池有5000個,那麼圈品系統實際處理商品變更消息QPS是5000W。是以隻有進行本地計算的規則才能支援處理商品變更消息,即使是這樣,圈品系統也嚴重消耗機器性能,曾經圈品系統有600多台機器,CPU使用率也達到了60%以上。

▐  規則子產品

架構設計

圈品規則模型類圖如圖2.4所示,ItemPoolRule是圈品池規則類,其中relationRuleList是圈選規則,exclusionRuleList是排除規則,一個商品必須符合圈選規則且沒有命中排除規則,這個商品才算符合圈品池的規則。RelationRule是具體規則内容,RuleHandler是規則處理接口,所有規則必須實作RuleHandler,如ItemTagRuleHandler——商品标規則handler、SellerRuleHandler——賣家規則handler等。

架構詳解——淘系圈品進化史第一階段

圖2.4 規則模型類圖

規則樹

規則樹設計如圖2.5所示,每個節點表示一個規則節點,頂級規則必須是可以做為資料源的規則,如商品清單規則、賣家清單規則等。判斷商品是否符合規則可以定義為:一個商品如果符合從頂級規則到某個葉子鍊路上所有規則節點(即從規則樹中可以找到一條從頂級規則通往任意葉子節點的鍊路),則認為該商品符合規則。

架構詳解——淘系圈品進化史第一階段

圖2.5 規則樹設計

為了更好的了解,舉個例子,如下圖2.6所示,營運通過商品清單方式進行圈品,左邊鍊路是圈商品清單中符合二級類目規則的商品,右邊鍊路是圈商品清單中符合一級類目規則以及指定商品标的商品。

架構詳解——淘系圈品進化史第一階段

圖2.6 規則樹舉例

頂級規則

由于該章節與“分批處理子產品”章節耦合較強,是以可以先看下面章節後,再看該章節。

頂級規則即是規則也是資料源,圈品從頂級規則中擷取資料源中所有的商品。商品清單圈品方式做為頂級規則時,規則内容包含商品ID,這些商品ID就是資料源的商品。賣家清單圈品方式做為頂級規則時,規則内容包含賣家ID,從頂級規則擷取商品ID時,根據賣家ID調用店鋪搜尋接口擷取商品ID。大促現貨圈品方式做為頂級規則時,規則内容包含的是大促現貨活動ID,從頂級規則擷取商品ID時,根據活動ID從同步過來的招商現貨表中拉取商品。

★ 局限性

從“分批處理子產品”章節可以知道,這一階段圈品都是先count規則中包含商品總數,然後分頁處理,這種方式存在局限性,當頂級規則變得複雜的時候,就沒辦法處理了。

舉個稍微複雜的例子,賣家清單圈品方式規則内容包含很多個賣家時,如何處理呢?這一階段圈品的處理方式跟圖9縱向處理方式一樣,找到所有賣家中擁有最大商品數量做為count,然後分頁處理,每一頁的處理過程中都需要循環所有賣家,當賣家數量越大時,每頁包含的商品數量就越大,是以該圈品方式限制了最多隻能指定300個賣家。

再舉個複雜的例子,假設一個品牌團中有多個賣家,一個賣家有很多商品,現在需要圈選多個品牌團下面所有賣家的所有商品,如何做呢?這一階段圈品還無法處理這麼複雜的規則,具體做法詳見第二階段。

▐  分批處理子產品

分布式處理

圈品将全量商品進行分頁處理拆分成很多部分,然後通過metaq進行分布式處理,流程圖如圖2.7所示。

當觸發全量圈品的時候會産生一條記錄規則變化的metaq消息,規則變化消息通過規則變化動作子產品進行處理。規則變化動作子產品首先計算資料源中最大可能的商品數量,然後再通過分頁處理分成很多部分,每一部分産生一條商品增加類型的消息,商品增加消息通過商品增加動作子產品進行處理。商品增加動作子產品首先從資料源拉取該部分對應的商品ID集合,然後過濾圈品池規則,最後選擇對應的業務進行處理。

架構詳解——淘系圈品進化史第一階段

圖2.7 分布式處理流程圖

分頁處理

分頁處理首先是計算全部最大可能的商品數量,然後按照固定間隔進行分頁,商品增加和商品删除消息包含的關鍵資訊是:start、end,

拿最簡單的商品清單圈品方式來舉例,假設營運填入了5w個商品ID,那麼分頁處理可以是500個商品ID做為一頁,第一頁start=0、end=500,最後一頁start=49500,end=50000,每一頁需要處理的商品ID都是确定的。

但是資料源往往不是這麼簡單,拿一個稍微複雜的大促現貨圈品方式舉例,從招商同步的現貨商品存儲在64張表中,按照商品ID進行分庫分表,其中大促活動ID是索引字段,如何高效擷取指定大促活動的全部商品ID呢。資料量較小的時候,我們可以通過資料庫count和limit分批取出,資料量大的時候使用limit就會有大翻頁問題。

為了避免使用limit在大翻頁時性能差的問題,圈品的處理方式如下圖2.8所示,把它看成橫向方式。首先通過大促活動ID,計算每張表的min(id)、max(id),總數count就等于所有表max(id)-min(id)相加,然後按間隔劃分任務,實際間隔是5000,為了畫圖友善圖中間隔是10,是以每個商品增加消息包含的資訊隻需要start和end。

架構詳解——淘系圈品進化史第一階段

圖2.8 橫向分頁處理

在處理商品增加消息時,需要循環64張表中求min和max直到找到該start和end在哪張表中,然後在該表中根據start和end取出符合的商品,核心代碼邏輯如下所示。

public List<CampaignItemRelationDTO> getCampaignItemRelationList(int start, int end,
                                                                     Function<Integer, Long> getMaxId,
                                                                     Function<Integer, Long> getMinId,
                                                                     Function<CampaignItemRelationQuery, List<CampaignItemRelationDTO>> queryItems) {
        List<CampaignItemRelationDTO> relationList = Lists.newArrayList();
        for (int i = 0; i < 64; i++) {
            //min以及max的值均走緩存,不會對db産生壓力
            long minId = getMinId.apply(i);
            long maxId = getMaxId.apply(i);
            long tableTotal = maxId - minId + 1;
            if (minId <= 0 || maxId <= 0) {
                continue;
            }
            //起始減本表内總量,如果大于0,則一定是從下一張表開始的,直接跳出循環,降低start以及end繼續
            if (start - tableTotal > 0) {
                start -= tableTotal;
                end -= tableTotal;
                continue;
            }
            // 進入到這裡,說明一定已經有一部分落在這裡了,那麼繼續周遊取值
            // 先判定是否是最後一張表,如果是,則去除需要的 ,然後傳回,如果不是最後一張表,那麼需要取出本張表中所需的資料,然後進行下次疊代
            // 判定為最後一張表的條件是 表的起始點+pageSize < maxId,即(minId+start)+(end-start) <= maxId,簡化為 minId + end <= maxId
            if (minId + end <= maxId) {
                //如果minId + end 還小于本表的最大值,那麼說明min以及max均落入了表内,那麼隻取本表的資料即可
                relationList.addAll(queryItems.apply(getQuery(start + minId, end + minId, i)));
                break;
            } else {
                //走入這裡,說明資料進行了跨表
                //首先取出本表符合條件的全部資料,然後将起始值設定為0,然後降低
                relationList.addAll(queryItems.apply(getQuery(start + minId, maxId, i)));
                //新的結束值應該為pageSize-目前表中取得的數量總量,即(end-start)-(tableTotal-start),簡化後得到end-tableTotal
                end = (int) (end - tableTotal);
                start = 0;
            }
        }
        return relationList;
    }      

    這種處理方式存在幾個缺點:

    1. 對于資料集中的表來說是一種不錯的方法,但對于資料稀疏型表來說就非常低效,如果資料分布很稀疏,count很大,分批處理後任務數量非常大,最後獲得的商品ID也就幾百個,比如,新零售圈品方式由于架構限制,也采用了一樣的分頁處理方式,一次全量圈品商品增加消息量可達20w,實際可能隻獲得了幾百個商品。
    2. 每個消息處理都需要循環查詢很多張表直到start、end所在的那張表,通過max和min判斷start和end是否出自該表,頻繁取max、min也會給DB造成壓力,為了避免對DB的壓力,又需要利用緩存max、min。

針對于第一個缺點:資料稀疏型的資料源消息數量過大,可以在不改動架構的同時進行改善,隻需換個角度計算總數count,如下圖2.9所示,count取的是所有表中的最大值和最小值的差,這樣即使是稀疏型資料源,count值也不會很大,然後任務處理的時候根據start和end循環從所有表中取出對應的商品ID。而且這種方式也會稍微減少取max和min的次數。如果密集型資料源采用這種分頁處理方式,将會導緻單頁資料量過大問題。

架構詳解——淘系圈品進化史第一階段

圖2.9 縱向分頁處理方式

針對于第二個缺點:頻繁取max和min問題,上面的處理方式是用全局的眼光計算count,然後分頁處理,是以無法直接定位start和end應該取自哪張表,其實,可以針對于每個表單獨分頁處理,消息中不僅包含start、end,還包含分表的index資訊。但是這種方式依然存在對稀疏型資料源劃分任務數過多的問題,而且現在圈品分批架構也不支援這種方式。

▐  動作子產品

動作子產品的作用是處理圈品metaq消息,動作子產品與消息類型是一一對應的,動作子產品分為:規則變化、商品增加、商品删除。

規則變化動作

規則變化動作子產品處理規則變化類型的消息,該動作主要處理流程是,調用分批處理子產品進行分批,然後将每批包含的資訊通過metaq發送出去,也就是産出商品增加和商品删除消息。

商品增加動作

商品增加動作子產品處理商品增加類型的消息,動作處理流程圖如下圖2.10所示。

架構詳解——淘系圈品進化史第一階段

圖2.10 商品增加動作處理流程圖

商品删除動作

商品删除動作子產品處理商品删除類型的消息,動作處理流程圖與圖2.10類似,隻是最後業務處理子產品調用商品删除處理的方法。

▐  業務處理子產品

業務處理子產品架構類圖如圖2.11所示,每一種業務都需要實作TargetHandler,其中handle方法處理圈品增加,rollback方法處理圈品删除。目前已經接入的幾個大業務分别是:品類券、免息券、會員卡等。

架構詳解——淘系圈品進化史第一階段

圖2.11 業務處理類圖

▐  階段總結

這一階段,圈品從無到有,誕生于品類券,又脫胎于品類券,在業務方面,支撐了品類券、免息券、會員卡等業務,在性能方面,能處理百萬級甚至千萬級商品。系統是在不斷發展中完善,這一階段的圈品存在以下不足點。

處理商品變更消息性能問題

2.2.4中講解了處理商品變更的必要性以及存在的問題,當有效的圈品池越來越多時,處理商品變更消息QPS越來越高,系統性能越來越差,而且很多規則需要調用HSF或者查詢緩存之類的耗時操作,是以這些規則無法支援處理商品變更消息。這一階段,承載圈品系統叢集CPU一直都在50%以上,即便叢集擁有600多台機器。

複雜頂級規則處理問題

面對複雜頂級規則,圈品沒有很好的辦法處理,然而在業務快速變化情況下,圈品需要有能力應對複雜規則,即使目前沒有出現太複雜的頂級規則,圈品在處理賣家清單圈品方式也存在局限性。

系統穩定性和可控性問題

  1. 穩定性問題:通過2.4節可以了解到,在進行大量圈品的時候,隻要觸發圈品變更,規則變化消息立馬會裂變出更多的圈品消息,圈品metaq消息堆積量可達到百萬,由于下遊系統限流導緻大量異常,系統負載又高,消息處理又耗時長,metaq消息處理存在雪崩風險,有時一條消息重複處理上萬次。
  2. 可控性問題:由于觸發圈品變更時,會立馬裂變出更多的消息,消息大量堆積時,圈品不能選擇性處理、不能停止處理消息、不能選擇性忽略消息等等,這就意味着系統發生問題的時候,沒有抓手進行控制,隻有眼巴巴的看着。舉個執行個體,兩條消息重複執行幾萬次,一個是删除該商品,一個是增加該商品,不停的給商品打标去标,商品産生大量商品變更,導緻搜尋引擎同步延遲,當時就隻能眼巴巴看着。再舉個例子,由于某一種圈品規則代碼有bug會導緻fullGc,然後該規則相關的圈品池産生了大量消息,由于無法選擇性處理消息,導緻整個圈品系統癱瘓。

分批處理缺陷問題

在2.4.2章節中講到了第一階段分頁處理的缺陷,不同資料源的分頁處理不能一概而論,架構應該給予更多的靈活性。

資料一緻性問題

在進行大量圈品時,系統或下遊系統異常無法避免,是以資料有可能存在不一緻的情況。對于業務來說,該增加的商品沒有增加,可能還能接受,如果該删除的商品沒有删除,那麼就很可能資損了。

第二階段

第二階段,針對于第一階段的問題進行優化,新架構圖如圖3.1所示,其中黃色部分是新增部分。圈品總體可以劃分為六大塊,分别是資料源子產品、動作子產品、規則子產品、業務處理子產品、排程子產品和設定端。排程子產品是新增部分中最重要的,首先新增了任務的模型,如圖3.2所示,任務會先儲存到DB中,scheduleX秒級定時觸發排程邏輯,最後通過metaq分發任務進行分布式處理。圖中紅色線條表示全量圈品的流程,圖中橘黃色表示增量圈品的流程。下面将分别詳細介紹新增部分。

架構詳解——淘系圈品進化史第一階段

圖3.1 第二階段圈品架構圖

架構詳解——淘系圈品進化史第一階段

圖3.2 任務模型

▐  商品變更消息優化

商品變更消息處理流程圖如圖3.3所示。第一步,建立圈品池與商品的泛化關系,第二步,通過Blink根據商品與圈品池的泛化關系過濾商品變更消息,剩下少量的商品變更消息,第三步,根據圈品池與商品的泛化關系判斷哪些圈品池需要處理該商品變更消息。

過濾後的商品變更消息日常平均qps在200左右,而且隻有與該商品相關的圈品池才需要處理該商品變更消息,是以,具體到某些圈品池上來看,其處理商品變更消息的qps在100以内,同時系統性能消耗也大大降低,叢集機器從巅峰時期700多台降低到現在300多台(由于叢集還承載其他業務,實際圈品需要的機器數量可以壓縮到100台以内)。

架構詳解——淘系圈品進化史第一階段

圖3.3 商品變更消息流程圖

商品變更消息過濾的關鍵點在于如何建立圈品池與商品的泛化關系,這裡的思想是根據具體規則盡量大範圍的圈定可能的商品。

比如賣家圈品方式,當小二填寫賣家清單後,這個圈品池與哪些賣家有關系就已經确定了,除此之外的賣家肯定不會跟這個圈品池發生關系,是以可以将賣家與圈品池的關系存入tair,供Blink過濾商品變更消息使用。賣家與圈品池的關系是比較通用的思路,其他圈品方式也可以轉化成這種關系,比如商品清單圈品方式,當小二填入商品ID後,這些商品屬于哪些賣家就确定了,除此之外的賣家的商品不會與該圈品池發生關系。

當然,賣家與圈品池的關系也有不适用的時候,比如大促活動圈品池方式,一次大促活動可能有幾十萬的賣家參與,而且賣家會不斷的報名參加大促,是以很難擷取圈品池與賣家的關系。針對大促活動圈品方式,可以建立tmc_tag與圈品池之間的關系,大促商品都有統一的tmc_tag,是以可以通過将tmc_tag與圈品池的關系存在diamonds供Blink過濾商品變更消息使用。總之,其他圈品方式根據具體規則找到圈品池與商品的泛化關系,可以通過商品上的資訊和泛化關系判斷商品與商品池是否存在關系。

▐  複雜頂級規則處理

次元定義        

第一階段中已經解釋了頂級規則是能夠做為資料源的規則,為了更好支援複雜資料源規則,引入了次元的概念,然後通過降維将複雜規則變成簡單規則,最後的目的是從資料源中擷取所包含的商品ID。

定義1:單個商品ID為零維,即沒有次元

定義2:能夠直接擷取多個商品ID的規則為一維,例如商品清單規則,單個賣家規則

定義3:二維規則由多個一維規則組成,例如多個賣家規則

定義4:三維規則由多個二維規則組成,更高維規則由多個比它低一維的規則組成

從上面定義可以看出,賣家清單規則既有可能是一維規則,也有可能是二維規則,當規則隻包含一個賣家時為一維規則,當規則包含多個賣家時為二維規則。為了更好了解,拿上面的複雜規則來講解,一個賣家有很多商品,一個品牌團有很多商家報名,如果現在營運設定圈多個品牌團下面所有商品,下圖3.5所示是該規則降維的過程。

架構詳解——淘系圈品進化史第一階段

圖3.5 規則降維過程

規則變化動作調整

在第一階段,規則變化動作處理流程就是調用分批處理子產品進行分批,然後将每批包含的資訊通過metaq發送出去,也就是産出商品增加和商品删除消息。現在,規則變化動作處理流程調整為如下圖3.6所示,首先需要判斷規則是否為一維規則,隻有一維規則才能直接通過分批處理,否則就要進行降維,産生的降維任務由規則降維動作進行處理。

架構詳解——淘系圈品進化史第一階段

圖3.6 新規則變化動作處理流程圖

增加規則降維動作

規則降維動作處理規則降維類型的任務,動作處理流程圖如下圖3.7所示。RuleHandler中自定義的降級方法指定了下一次元的規則,是以一次降維任務隻能将規則降低一個次元。

降維隻針對做為資料源的頂級規則,是以,首先遞歸擷取頂級規則,接着調用自定義降維方法處理頂級規則後得到更低一次元的頂級規則集合,然後使用降維後的頂級規則替換規則樹中的頂級規則得到新的規則樹集合,最後,将新規則樹生成規則變化任務,由規則變化動作判斷是否繼續降維。

架構詳解——淘系圈品進化史第一階段

圖3.7 規則降維動作處理流程圖

▐  新增排程子產品

任務排程

新增任務模型如圖3.2所示,任務相當于第一階段中的圈品消息,第二階段中任務是需要先落庫,然後由排程器來進行排程的。       

排程器是任務扭轉的動力,所有類型的任務都會插入DB中由排程器統一排程。任務表中已完成的任務會隔一段時間清理,即使是這樣,任務表也有可能存在幾百萬任務,而且圈品的速度很大程度由排程器決定,是以對排程器的性能要求是很高的,不僅如此,排程器應該具備更多的靈活性。

排程器處理流程圖如圖3.8所示,通過scheduleX秒級定時觸發排程邏輯,然後通過metaq分發任務ID,其實分發任務ID也可以通過scheduleX來完成,最初的實作也就是通過scheduleX來進行任務ID的分發,最後還是改成了通過metaq來分發任務,因為scheduleX分發大量任務時存在不可接受的延遲。

講回到圖3.8,任務排程的基礎是知道未完成任務的分布,為了避免統計未完成任務分布時産生慢sql,這裡做了一個很重要的動作,即下文第一步。

第一步,首先擷取未完成任務所屬的圈品池ID的分布,由于這裡隻根據狀态統計圈品池ID,狀态和圈品池都有索引,利用了覆寫索引,是以性能很高;

第二步,随機選擇十個圈品池保證任務排程配置設定均衡,同時減少任務統計的耗時;

第三步,統計這十個圈品池的未完成任務數量的分布;

第四步,根據第三步的數量統計以及系統配置,配置設定每個圈品池參與排程的任務數量;

第五步,根據任務配置設定數量擷取任務ID;

第六步,通過metaq将任務ID分批發送到不同的機器進行處理;

第七步,接收metaq消息;

第八步,将任務ID送出異步處理,這裡為了提升處理速度,維護了一個線程池,任務ID隻需要送出到阻塞隊列中;

第九步,任務ID送出異步處理後,立馬更新任務狀态為進行中,避免任務再次被排程,進行中的任務不屬于未完成的任務。

架構詳解——淘系圈品進化史第一階段

圖3.8 任務排程流程圖

對比第一階段中圈品消息模式,第二階段任務首先儲存到DB,然後由排程器進行排程,排程器能夠提供更多的靈活性,可以擷取以下優點:

  1. 任務優先級可根據圈品池進行調整,部分圈品池出現問題不會影響整體;
  2. 任務排程速度可調整、可暫停,可以根據任務類型配置設定處理速度;
  3. 任務排程可監控、可精确統計;
  4. 圈品過程可查詢、可追蹤;

任務統計

一個完善的平台少不了系統可視化,任務處理進度是可視化中重要的部分。任務數量統計就少不了group by和count,任務表最大的時候可能存在上百萬的資料,同步方式進行統計肯定是不行的,是以采用如下圖3.9所示異步方式。利用覆寫索引方式得到圈品池ID的分布,每個圈品池的任務不會很大,是以每個圈品池分開統計将不會産生慢sql。

架構詳解——淘系圈品進化史第一階段

圖3.9 任務統計思路

分批處理新思路

在第一階段中提到了分批處理的缺陷問題,這裡将讨論如何解決整個問題,新的分批處理方式還在開發當中,設計思路按照該章節所講。

★ 架構設計

圈品擁有各種各樣資料源,每種資料源的特性都有不同,是以無法用一種通用的分批方式處理所有資料源。是以圈品分批處理的架構應該更加通用,讓每種資料源都能自定義自己的分批處理方式。

在架構方面的調整如下圖3.11所示,新增基礎分批對象Pageable,考慮到和老架構到相容,Pageable包含老架構的使用的分批參數start、end、pageSize,自定義分批對象TablePageable或其他都繼承自Pageable,RuleHandler增加自定義分批方式getPageabelList,老架構的分批方式可以寫在AbstractRuleHandler的getPageableList中,需要自定義分批方式的RuleHandler覆寫getPageabelList便可。

架構詳解——淘系圈品進化史第一階段

圖3.11 分批處理新架構類圖

★ 分頁處理思路

在第一階段中,分頁處理為了避免limit大翻頁問題,采用了通過主鍵id進行分頁的方式。在這裡先讨論下為什麼limit會存在大翻頁問題,以及優化方案。

如下sql所示,當N值很大時,這個sql的查詢效率會很差,并發查詢時甚至會拖垮資料庫,因為執行這個sql時需要先回表查詢N+M行,然後根據limit傳回M行,前面查詢的N行最後被丢棄(具體讨論可參考limit為什麼會慢)。一般遇到這種情況,業務上都是不允許大翻頁,應該根據條件過濾,但是圈品要分批取出所有資料,是以圈品就繞不開這個問題。

SELECT * FROM table WHERE campaing_id = 1024 LIMIT N,M      

在這裡總結了兩種解決思路,圈品為了擷取所有有效的商品,是以無需考慮資料整體的分頁,可以将分表獨立分頁處理。

id與limit組合優化

前面分析了使用limit大翻頁最大的問題是查詢前N(即offset)條資料所耗費的時間,在理想的情況下,id是連續自增,可以在where條件中使用id來代替offset,sql即如下所示。

SELECT * FROM table WHERE campaing_id = 1024 and id > N LIMIT M      

優化思路中所說的理想情況,幾乎沒有場景能夠達到要求,但是這也不影響該思路的應用,根據上一次翻頁結果id使用limit查詢下一批,如果id不連續,limit将可以跳過很多不連續id,減少查詢次數。

結合圈品實際情況使用該思路,首先通過min和max得到資料在表中分布的最小值和最大值,針對稀疏型資料分批間隔可以很大(為了解決任務數量過多問題,比如間隔是2W,即end-start=2w),start和end分别是每批資料對應的開始id和結束id,然後根據id做為where條件使用limit取下一頁資料,接着根據下一頁最大id做為where條件使用limit取後面的資料,一直循環下去,直到id>end,對于稀疏型資料,也許循環1-2次就完成了。流程圖如下圖3.12所示。

架構詳解——淘系圈品進化史第一階段

圖3.12 圈品分頁處理優化思路

覆寫索引優化

當sql查詢是完全命中索引,即傳回參數和查詢條件都有索引時,利用覆寫索引方式查詢性能很高。先通過limit查詢出對應的主鍵id,然後再根據主鍵id查詢對應的資料,由于無需從磁盤中取資料,是以limit方式比之前性能要高,sql如下所示。

SELECT * FROM table AS t1 
INNER JOIN (
  SELECT id FROM table WHERE campaing_id = 1024 LIMIT N,M 
) AS t2 ON t1.id = t2.id      

“覆寫索引優化“到底能優化到什麼程度呢,對此進行了一個測試,表item_pool_detail_0733包含10953646條資料,通過item_pool_id = 1129181 and status = -1條件篩選後剩下3865934條資料,item_pool_id和status建立了聯合索引。

測試1

我們看下offset較小的時候,sql和執行計劃如下所示,執行平均耗時83ms,可以看到在offset較小的時候,sql性能是可以的。

SQL:

SELECT *  FROM `item_pool_detail_0733` WHERE item_pool_id = 1129181 and status = -1  LIMIT 3860000,100      

執行計劃:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE item_pool_detail_0733 idx_pool_status,idx_itempoolid idx_pool_status 12 const,const 5950397 100.00

測試2

當offset較大的時候,sql如下所示,執行計劃和上面是一樣的,執行平均耗時6371ms,這個時候sql性能就很差了。

SELECT *  FROM `item_pool_detail_0733` WHERE item_pool_id = 1129181 and status = -1  LIMIT 3860000,100      

測試3

現在使用“覆寫索引優化”思路優化測試2的sql,sql和執行計劃如下所示,執行平均耗時1262ms,相比優化之前執行耗時6371ms,性能提高了5倍多,但是1s多的執行耗時對于圈品來說也是難以接受的。

實際情況下,分庫分表會把資料均勻分布在所有表中,是以,單表過濾後還剩下300w多資料的情況是很少的,為此,我接着測試資料量不同時該sql的性能。當LIMIT 2000000,100時,執行平均耗時697ms;當LIMIT 1000000,100時,執行平均耗時390ms;當LIMIT 500000,100時,執行平均耗時230ms;

SELECT * FROM `item_pool_detail_0733` as t1 
INNER JOIN (
    SELECT id  FROM `item_pool_detail_0733` WHERE item_pool_id = 1129181 and status = -1 LIMIT 3860000,100
) as t2 on t1.id = t2.id      
PRIMARY <derived2> ALL 3860100
t1 eq_ref 8 t2.id
2 DERIVED Using index

通過上面的測試可以看出,使用limit查詢50w資料時,性能還可以,而且圈品的資料源都是根據商品ID進行分庫分表,是以,根據過濾條件過濾後剩餘的資料幾乎都能在50w以内。如果圈品采用這個思路優化分頁處理,那麼将可以完全解決第一階段中的兩個缺點問題,而且分頁處理邏輯相比之前簡單很多。

▐  資料一緻性保障

資料一緻性保障解決方案充分複用了圈品架構,隻需三步,第一,在動作子產品增加一緻性檢查動作,第二,新增自動産出一緻性檢查任務的scheduleX任務,第三,在業務處理子產品中增加自定義一緻性檢查方法。

新增一緻性檢查動作

一緻性檢查動作處理一緻性檢查任務,其處理流程圖如圖3.13所示。       

架構詳解——淘系圈品進化史第一階段

圖3.13 一緻性檢查動作處理流程圖

自動産出一緻性檢查任務

圈品目前有效的圈品池已經超過5000,如果一次性檢查5000個圈品池,那麼産出的檢查任務數量可能上百萬,是以,需要一個定時任務不斷監控任務表中未完成任務的數量,在數量較少的情況下一次選擇幾個圈品池産出一緻性檢查任務。一緻性檢查任務的産出依然是複用圈品規則變化處理流程。

業務自定義一緻性檢查

新業務處理類圖在之前的基礎之上增加自定義一緻性檢查方法consistencyCheck,需要自定義一緻性檢查的業務實作該方法便可。

架構詳解——淘系圈品進化史第一階段

圖3.14 新業務處理類圖

▐  性能資料

卡券商品設定引擎的性能主要通過三個名額衡量,分别是:任務排程吞吐量、商品處理速度。

任務排程吞吐量

一個任務可能包含幾個商品也有可能包含上千個商品,取決于資料源的稀疏程度,當資料是稀疏的時候,任務數量将會很多,這個時候圈品的速度就取決于任務排程的速度。目前,任務排程的速度可以達到5w個/分鐘,這還并不是最大值,還有上升的空間。

商品處理速度

商品的處理速度受下遊系統的影響需要限流,不考慮業務處理速度,理論上處理商品速度可以達到6w TPS。