?作者 | 齊建朝
背景
近幾年春節(jié)期間,抖音都會為用戶帶來各式各樣的春節(jié)活動,每年都會有數(shù)億用戶參與其中。2022 年春節(jié),抖音支付也參與了春節(jié)活動,面向海量用戶發(fā)放抖音支付券,幫助用戶在抖音春節(jié)活動中獲得更好體驗。對抖音支付來說,這是一個很大的挑戰(zhàn),因為之前抖音支付團(tuán)隊沒有真正意義上經(jīng)歷過春節(jié)活動這種量級的應(yīng)用場景,這對抖音支付營銷系統(tǒng)是極大的考驗。
抖音支付營銷系統(tǒng)簡介
當(dāng)前營銷系統(tǒng)分層及架構(gòu)如下:
營銷團(tuán)隊業(yè)務(wù)主要分為三大方向,營銷投放、營銷活動、營銷資產(chǎn)。營銷投放主要負(fù)責(zé)營銷權(quán)益觸達(dá),將營銷利益點曝光給用戶,營銷活動主要負(fù)責(zé)營銷玩法的建設(shè)以及權(quán)益發(fā)放,營銷資產(chǎn)則主要負(fù)責(zé)用戶營銷資產(chǎn)的管理,比如優(yōu)惠券、立減的發(fā)放及使用等。對于春節(jié)發(fā)券,鏈路是由營銷活動對接春節(jié)主會場玩法,調(diào)用營銷資產(chǎn)接口將支付券發(fā)放到用戶手中。
挑戰(zhàn)
- 春節(jié)活動并發(fā)量大,同時參與領(lǐng)券的人數(shù)會非常多,高峰期對系統(tǒng)造成的沖擊會非常大。
- 春節(jié)活動面向全國用戶,受眾非常廣,用戶體驗非常重要,因此發(fā)券動作耗時需要盡可能的低。
- 春節(jié)活動參與人數(shù)基數(shù)非常大,春節(jié)期間預(yù)計發(fā)放的支付券數(shù)量也會非常多,資金安全需要重點保障。
方案
性能保障
異步發(fā)券-提升接口響應(yīng)速度
考慮到支付券使用大多在抖音電商場景,且春節(jié)期間用戶線上購物流量較小,用戶領(lǐng)到券后立即使用的概率較低,只需要保證用戶領(lǐng)券到實際感知券(查看券或使用券) 的時間延遲可控,那么用戶體驗就不會受到影響,因此我們采用了異步發(fā)券的模式,營銷收到上游發(fā)券請求后,落單后立即返回,通知上游受理成功。
異步發(fā)券帶來的一個問題是用戶領(lǐng)券后感知到領(lǐng)券成功,實際券最終沒有發(fā)給用戶,大部分為庫存不足、風(fēng)控攔截等原因。針對這個問題,我們與運營同學(xué)做了討論,春節(jié)期間營銷活動庫存會盡量配置的足夠多,風(fēng)控攔截率會降到最低,除異常刷單用戶外均不做攔截,盡量降低異步發(fā)券失敗的可能性。
雙層本地隊列-提升處理能力,平滑流量
將流量異步化后,因為活動流量大都是心電圖的結(jié)構(gòu),波峰波谷很明顯,我們借鑒了生產(chǎn)者-消費者模型,引入了隊列來平滑流量。一開始我們考慮了 RocketMQ,但一旦在活動發(fā)券核心鏈路上引入了額外的中間件,便對其產(chǎn)生了依賴,需要額外考慮它的可用性和容災(zāi)方案,且 RocketMq 屬于遠(yuǎn)程隊列,生產(chǎn)者消費者之間的延遲也不易控制,因此我們設(shè)計了一套本地隊列模型,來規(guī)避上述問題。
本地隊列模型如上,隊列消費端邏輯首先會從分布式限流器獲取令牌,獲取成功后再從隊列中獲取數(shù)據(jù),新建一個 goroutine 處理活動發(fā)獎邏輯,之后重復(fù)此流程。
雖然隊列本身有削峰的作用,但還是不能精確控制消費速率,當(dāng)上游流量過大時,隊列消費端消費速率也會隨之上升,有打崩系統(tǒng)的風(fēng)險,所以還是需要限流層做較為精準(zhǔn)的流量控制,但僅靠接口維度的限流粒度又太粗,所以在業(yè)務(wù)邏輯層又引入了一層業(yè)務(wù)限流。這里的分布式限流器使用的支付自研的分布式限流組件,優(yōu)先會從本地獲取令牌,當(dāng)本地令牌不足時,會從遠(yuǎn)程批量拉取令牌到本地。
另外這個模型我們使用了雙層隊列,第一層隊列用于保護(hù)營銷活動層,基于營銷活動處理能力設(shè)置限流;活動決策通過后,再將請求放入第二層隊列,第二層隊列的限流則是基于營銷資產(chǎn)的系統(tǒng)承載能力來設(shè)置。通過雙層隊列,可以規(guī)避掉營銷活動和營銷資產(chǎn)間的容量差異,兩方系統(tǒng)吞吐量都可以最大程度地發(fā)揮出來。
庫存扣減優(yōu)化-減熱點,降壓力
出于性能的考慮,營銷資產(chǎn)的券批次庫存放在了 Redis,目前營銷資產(chǎn)庫存操作的邏輯是:收到一個用戶的發(fā)券請求->讀取 Redis,查看發(fā)放的券批次庫存是否充足->寫入 Redis,扣減券批次庫存。
為了使 Redis 集群流量均勻,不同券批次的庫存數(shù)據(jù)被打散到了不同的 Redis 分片上,但是當(dāng)在一段時間內(nèi)集中發(fā)放某一券批次時,流量仍然會大量偏移到一個分片內(nèi),造成 Redis 數(shù)據(jù)熱點問題。如果想辦法能將某個券批次多次零散扣減庫存的操作合并到一起,那么數(shù)據(jù)熱點問題就可以會得到較大的緩解。
合并發(fā)券邏輯如上圖所示,營銷活動嘗試首先從二層隊列中非阻塞地獲取 N 條發(fā)券請求數(shù)據(jù),如果可以拿到,則將這些數(shù)據(jù)打包發(fā)送到營銷資產(chǎn),如果從隊列中拿到的數(shù)據(jù)小于 N 條,說明此刻隊列沒有更多的數(shù)據(jù),直接將券盡快發(fā)出;如果獲取不到數(shù)據(jù),則隨機(jī)睡眠一小段時間之后,重新嘗試獲取,如果經(jīng)過有限次重試后仍獲取不到數(shù)據(jù),則結(jié)束此次循環(huán)。
營銷資產(chǎn)收到合并發(fā)券的請求后,會嘗試對請求中相同券批次的發(fā)券請求進(jìn)行合并,扣減庫存時進(jìn)行集中扣減,比如之前為 N 個不同用戶發(fā)放同一券批次 A,每次庫存減 1,需要對 Redis 進(jìn)行 N 次寫操作;合并發(fā)券后,只需對 Redis 進(jìn)行 1 次寫操作,庫存扣減 N 即可。
另外,扣減庫存前的校驗邏輯,實際上不需要每次都去訪問 Redis,這次校驗本身只是一個前置校驗,最終扣減是否成功還是取決于之后扣減操作的執(zhí)行結(jié)果,校驗的最大作用是在 Redis 庫存不足后能將不必要的扣減動作攔截掉,并不需要十分精準(zhǔn),因此我們考慮在應(yīng)用本地內(nèi)存維護(hù)了一份券批次的庫存信息,定時將 Redis 庫存信息同步到本地,發(fā)券時只需要在本地內(nèi)存簡單校驗庫存信息即可,不需要再訪問遠(yuǎn)程的 Redis。
優(yōu)雅退出-完善系統(tǒng)魯棒性
使用本地隊列進(jìn)行數(shù)據(jù)處理的一大弊端是內(nèi)存易失性會使數(shù)據(jù)無法持久性地存儲,在應(yīng)用重新發(fā)布或升級時,本地隊列中的發(fā)券數(shù)據(jù)有可能會丟失,用戶發(fā)券請求無法得到正常處理。
為了內(nèi)存中的數(shù)據(jù)不丟失,我們需要能夠感知到應(yīng)用退出的信號,在應(yīng)用退出前將內(nèi)存中的數(shù)據(jù)處理掉。因此我們調(diào)研了字節(jié)云應(yīng)用實例的生命周期,在實例終止時,首先會將當(dāng)前應(yīng)用實例從服務(wù)注冊中心中摘除,此操作執(zhí)行后意味著當(dāng)前實例不再接收新的外部流量;之后會發(fā)送 SIGINT 退出信號給業(yè)務(wù)進(jìn)程。應(yīng)用收到 SIGINT 信號后,不再消費隊列中剩余的發(fā)券請求數(shù)據(jù),而是將這些數(shù)據(jù)發(fā)送到遠(yuǎn)程隊列中,由當(dāng)前仍存活的其他應(yīng)用實例來消費這些數(shù)據(jù)。
兜底補償-保證最終一致性
盡管已經(jīng)實現(xiàn)了應(yīng)用優(yōu)雅退出,但是在極端情況下,比如 panic、oom、物理機(jī)宕機(jī)等異常情況引起的應(yīng)用退出,應(yīng)用是接收不到 SIGINT 信號的,也就無法執(zhí)行優(yōu)雅退出的業(yè)務(wù)邏輯。因此我們額外增加了兜底補償機(jī)制,通過定時任務(wù)掃表,將卡在中間狀態(tài)時間較久的數(shù)據(jù)重新投遞到本地隊列中進(jìn)行處理。
既然有了定時任務(wù)做兜底補償,那么優(yōu)雅退出邏輯是否還有存在的必要呢?其實還是有必要的,當(dāng)應(yīng)用上線過程中會發(fā)生頻繁的應(yīng)用重啟,此時很可能存在大量本地隊列的請求未得到處理,如果僅依賴定時任務(wù)兜底處理,那么用戶從領(lǐng)券到實際收到券的時延可能會非常大,有可能會造成用戶體驗變差。因此優(yōu)雅退出和兜底補償是一個互補的關(guān)系,優(yōu)雅退出最大程度保證用戶體驗,兜底補償保證數(shù)據(jù)的最終一致性。
綠色通道-提升用戶體驗
異步發(fā)券的假設(shè)是用戶從領(lǐng)券動作發(fā)生,到實際感知到券的存在,中間是有一段時間緩沖的,但是用戶有可能領(lǐng)到券后直接進(jìn)入春節(jié)錢包查看,如果此時異步發(fā)券還未完成,有可能會造成客訴。針對這種情況,我們與上游春節(jié)主端做了約定,當(dāng)用戶領(lǐng)券后短時間內(nèi)進(jìn)入春節(jié)錢包查看優(yōu)惠券時,上游會再次調(diào)用發(fā)券接口,并增加綠色通道的標(biāo)識,我們收到這個標(biāo)識后會將異步發(fā)券變更為同步,優(yōu)先為當(dāng)前用戶發(fā)券,保證用戶體驗。
資金防控
除了性能方面的保障,資金安全也需要重點關(guān)注,在本次春節(jié)發(fā)券活動中,我們主要做了以下防控措施。
冪等校驗
每一次發(fā)券動作都會生成一個全局唯一的序列號,發(fā)券時會將序列號作為唯一索引落入數(shù)據(jù)庫中,當(dāng)發(fā)生用戶連續(xù)點擊領(lǐng)券或網(wǎng)絡(luò)異常重試等情況時,相同序列號由于唯一索引沖突落庫無法成功,從而避免重復(fù)發(fā)券帶來的資金損失。
用戶維度領(lǐng)取限制
通過序列號進(jìn)行冪等校驗可以解決一部分問題,但對于一些較為專業(yè)的黃牛黨來說,可能會繞過這個限制,通過偽造序列號的方式繞過冪等校驗。對于這種情況,我們維護(hù)了一份用戶券批次的領(lǐng)取數(shù)據(jù),發(fā)券會校驗每個用戶是否達(dá)到了領(lǐng)取上限,未達(dá)到領(lǐng)取上限則會正常發(fā)券,同時對用戶領(lǐng)取記錄進(jìn)行更新,否則終止發(fā)券動作。
券批次組互斥
用戶維度領(lǐng)取限制主要防止同一用戶多次領(lǐng)取同一批次的可能性,但是在整個春節(jié)活動中,運營同學(xué)可能會發(fā)放多個用途不同的券批次,但是發(fā)放的群體有可能會重疊甚至是同一批,如果對用戶多次發(fā)放不同批次的券,有可能會拉高營銷成本?;谝陨显?,我們抽取出了券批次組的概念,處于同一組的券批次,營銷目的基本是一致的,比如都是拉新、促活或留存的目的,當(dāng)用戶領(lǐng)取了券批次組內(nèi)的一種券后,用戶便不可以再領(lǐng)取組內(nèi)的其他券批次,即組內(nèi)的券批次間為一種互斥關(guān)系,通過這種方式避免造成營銷費用重復(fù)補貼。
庫存防超賣
上文提到過,營銷券批次的庫存數(shù)據(jù)存放在了 Redis,每次對 Redis 進(jìn)行庫存扣減時,可能會存在網(wǎng)絡(luò)超時、失敗等異常情況,造成扣減庫存的結(jié)果處于未知狀態(tài)。當(dāng)這種情況出現(xiàn)時,我們選擇"容忍",認(rèn)為發(fā)券失敗,直接結(jié)束發(fā)券邏輯,不做回滾處理;扣減庫存成功后,再為用戶實際發(fā)券,如果發(fā)券失敗,此時可以嘗試回滾 Redis 庫存,因為已經(jīng)確定本次請求成功扣減了庫存,但是回滾失敗,不再做額外的重試處理。
上述方案,有可能造成庫存少賣,但這種較保守的策略可以有效防止庫存超賣的可能性,可以看做在數(shù)據(jù)一致性和可用性之間的一種取舍與平衡。
風(fēng)控平臺接入
在發(fā)券鏈路中,我們也接入了字節(jié)內(nèi)部的風(fēng)控平臺,風(fēng)控平臺會采集分析用戶及設(shè)備等信息,通過風(fēng)險評估將黃牛及惡意用戶識別出來,攔截發(fā)券動作,避免潛在的資損產(chǎn)生。
數(shù)據(jù)監(jiān)控與核對
除了以上的資金防控措施外,我們還對發(fā)券活動做了大量監(jiān)控,包括券批次發(fā)券量,券批次發(fā)券速率、本地隊列堆積情況、本地隊列消費者速率等等,當(dāng)監(jiān)控數(shù)據(jù)出現(xiàn)同比或環(huán)比異常時會及時報警并人工介入排查。另外,當(dāng)券批次發(fā)放完畢后,我們會再次核對數(shù)據(jù)的一致性,包括比對用戶發(fā)放券張數(shù)與庫存消耗數(shù)量是否一致、校驗單用戶是否超過券批次領(lǐng)取上限等等。
總結(jié)
經(jīng)過以上方案的優(yōu)化,我們順利的支撐了今年的春節(jié)主會場發(fā)券活動,并取得了不錯的效果:
系統(tǒng)上
- 營銷整體對外可承接十萬級 TPS 的發(fā)券吞吐量。
業(yè)務(wù)上
- 春節(jié)期間發(fā)放了數(shù)千萬張抖音支付、DOU 分期券,支持了抖音支付、DOU 分期兩大核心業(yè)務(wù)的活動訴求。
- 99%的券可以在 0.5s 內(nèi)發(fā)放到用戶賬戶中,異步發(fā)券實際延遲很低,用戶體驗較好,符合業(yè)務(wù)預(yù)期。
后續(xù)規(guī)劃
經(jīng)過今年春節(jié)活動流量的考驗后,營銷沉淀下了不少經(jīng)驗和系統(tǒng)能力,不過仍有需要后續(xù)持續(xù)迭代和完善的地方:
- 異步發(fā)券能力標(biāo)準(zhǔn)化。我們初步嘗試了異步發(fā)券并應(yīng)用在春節(jié)活動中,取得的效果不錯,可以預(yù)見之后的 618、雙十一等大促節(jié)日仍會有很多適合異步發(fā)券的場景,因此我們準(zhǔn)備將營銷對外的發(fā)券接口標(biāo)準(zhǔn)化,將異步發(fā)券作為一種可選能力,與接入方、場景等關(guān)聯(lián)起來,做到發(fā)券模式的靈活選擇與配置。
- 本地隊列模式推廣。本次設(shè)計并實現(xiàn)的雙層本地隊列,很好地完成了發(fā)券任務(wù)處理,任務(wù)執(zhí)行相比遠(yuǎn)程隊列時延更低,隊列分層、限流、優(yōu)雅退出、補償?shù)容o助功能對系統(tǒng)魯棒性也有較好的保障,后續(xù)我們會將此模塊抽象成一個通用的小框架,使其可以支持更多適合異步處理的業(yè)務(wù)場景。