常用效能最佳化手段及在風控系統中的應用

架構師修行手冊發表於2023-10-26

來源:嗶哩嗶哩技術

引言


效能最佳化是個恆久的話題,隨著產品的演進,業務的增長,系統能力總有達到瓶頸的一天,它不可或缺的陪伴著我們走向壯大再走向衰敗,是我們面臨的不可迴避的問題。下圖1展示了風控系統近半年來承載流量的增長趨勢,可見最近半年來流量高速增長,且對於可預見的未來而言,接入流量還會持續高增。伴隨著流量的增長,系統各方面--儲存、計算、IO等都表現出一定的瓶頸,透過原始簡單的水平擴容並不能解決所有的問題,而且還會帶來成本的上升。因此,我們近期對系統進行了一系列最佳化改造, 目的是滿足未來一段時間內業務的增長使用,降低介面的耗時滿足某些延時敏感型業務的需要,同時也伴隨著一定的IT成本最佳化。本文結合常見的效能最佳化手段(預取、批次、非同步、壓縮、快取),及在風控系統中的實踐進行總結,希望能給讀者對於效能最佳化實踐帶來一些參考。


常用效能最佳化手段及在風控系統中的應用

圖1:風控引擎流量增長趨勢


預取——特徵預計算


預取作為一種提速手段,通常與快取搭配使用,在快取空間換時間的基礎上更進一步,以時間換時間,透過預載入來提升效能。常見的使用有,資料庫從磁碟載入頁時的預讀多個頁減少磁碟IO、CPU快取載入一片連續的記憶體空間提高計算的速度,也就是我們常說的CPU對陣列友好對連結串列不友好的原因。

在Gaia風控引擎中,一次業務請求在引擎內部的執行流程如下圖2所示,會經歷場景因子(特徵)->規則->決策的計算過程, 而計算因子是整個鏈路最耗時的部分,通常佔請求響應時間的70%以上,包括對賬號資訊、名單庫、模型資料、使用者畫像、裝置指紋、三方特徵等多個下游的讀擴散–特徵因子獲取,加上場景的上百條規則執行,請求耗時常規在250ms左右,這也是22年中以前我們承諾給主站大部分業務的響應時間,直到電商業務的接入,對我們引擎的響應時間提出了100ms以內響應的要求,迫使我們對引擎進行了一些最佳化,其中之一就是近線引擎帶來的特徵預取最佳化,其演進流程如下圖3示:


常用效能最佳化手段及在風控系統中的應用

圖2:風控引擎一次請求執行過程


常用效能最佳化手段及在風控系統中的應用

圖3:風控特徵獲取流程


基於一個前提:對同一個主體,按照其行為時序資料消費,slb資料來源消費處理完成大多數時候都要比業務請求風控早。因此,我們透過對slb實時流資料消費預讀取下游特徵,並將結果快取在redis中,當業務請求風控時,直接獲取redis的資料,避免或減少rpc回源特徵,以此來降低風控介面的耗時。

透過這套機制,我們按照特徵變動頻率對特徵分層設定不同的快取時間,同時在一些對資料一致性要求較低的場景對特徵開啟快取讀,其快取命中率能達到90%以上,核心業務場景如電商交易,介面響應耗時從80ms下降到25ms。


常用效能最佳化手段及在風控系統中的應用

圖4:特徵快取命中率


常用效能最佳化手段及在風控系統中的應用

圖5:電商交易場景請求風控介面TP99耗時曲線圖


批次——特徵批次獲取


批次一般是對I/O操作的最佳化,同樣可看作是時間換時間,透過合併、批次進行讀取或寫入以減少對I/O的操作來提升吞吐和效能。常見的使用有,kafka消費資料的時候批次拉取指定的資料條數,mysql寫入redolog/binlog時的組提交(group commit)機制,都是透過批次的最佳化來減少I/O提升效能的。

在Gaia引擎中最常用的特徵為對賬號管控/風控名單值的獲取,一次業務風險判斷請求會涉及到對請求主體(mid、buvid、ip、ua)的各種黑/白名單值獲取,以主體mid為例,往往會併發呼叫下游名單介面多次,判斷mid是否在同裝置多賬號黑名單、虛假賬號黑名單、通用白名單等名單中,從而形成不同的因子供規則使用,這種方式就會造成對同下遊的同介面的讀擴散流量放大浪費資源,且要保持下游介面低延遲往往需要下游進行擴容保證CPU等資源使用率在一定的水位以下。因此,為了降低自身及下游的服務資源和I/O,最佳化的手段就是將多次請求合併為一次請求,其最佳化流程如下圖6所示:


常用效能最佳化手段及在風控系統中的應用

圖6:名單類因子讀取最佳化流程


透過將多次獨立下游請求獲取給定黑/白名單轉化為一次批次請求獲取主體所有有效名單,同時結合本地記憶體判斷因子請求名單與所有名單是否有交集來實現原有相同的功能,並透過local cache及singleflight最佳化,降低對下游介面呼叫量69%。


常用效能最佳化手段及在風控系統中的應用

圖7:實驗環境名單庫介面批次最佳化效果


非同步——累積因子同步改非同步計算最佳化


非同步通常和同步一起對比,其區別在於發起請求之後是立即返回還是等待結果,常用於在系統內部有大量I/O操作時,透過非同步提升吞吐。常見的使用有,基於write-back模式向快取寫入資料,mysql非同步傳輸binlog進行主從複製等。

在Gaia引擎中,一次請求會涉及對很多特徵因子的計算,其中,最常用的是基於redis實現的累積因子,其包含如下幾種型別(見表1),以count(distinct)型別為例,一次計算過程包含1寫zadd,1讀zcount,機率觸發zrem清除不在有效時間視窗內的過期資料,其最多對redis請求3次,最少/均攤2次,且zset這幾個操作的時間複雜度都在O(log n)以上,加上一次請求往往會對多個累積因子進行計算(讀寫擴散),這給redis叢集帶來了較大的計算壓力,由於overload對叢集例項擴容的限制,我們對redis叢集的水平擴容也遇到了瓶頸。考慮當前引擎流量的情況:爬蟲類業務與常規業務流量佔比為1.5:1,且爬蟲類業務流量還在持續高漲,鑑於爬蟲類業務風控的特性(非資產安全,容忍一定的漏過),我們以犧牲資料一致性為代價,對爬蟲類業務的累積因子進行了非同步計算最佳化,以減少對redis的呼叫,其計算演進流程如下圖8所示:


型別功能底層實現使用頻率
count計數incr
count(distinct)去重計數zset
sum累積求和incrby
avg累積求平均incrby

表1:風控引擎支援累積因子型別


常用效能最佳化手段及在風控系統中的應用

圖8:累積因子計算(最佳化前/後)流程


基於railgun(關於B站自研非同步事件處理平臺,可參閱:從1到億,如何玩好非同步訊息?CQRS架構下的非同步事件治理實踐)提供的記憶體佇列聚合功能,我們對累積因子寫操作進行了非同步化改造,並結合聚合功能,在設定的時間/數量閾值條件下,對相同累積key進行聚合並在記憶體中去重後批次寫入redis,將多次同步redis I/O減少為非同步寫1次。而最佳化的效果從三個角度評估,從成本角度看:其對redis的呼叫qps減少了35%以上(如圖9);從介面耗時看:TP99有一定的下降; 從對風控規則的召回影響看,前後召回趨勢基本一致,且打擊總量差距不大,在可接受的範圍內。


常用效能最佳化手段及在風控系統中的應用

圖9:單場景累積因子計算最佳化前/後對redis的呼叫量情況


常用效能最佳化手段及在風控系統中的應用

圖10:累積因子計算最佳化前/後介面耗時情況


常用效能最佳化手段及在風控系統中的應用

圖11:累積因子計算最佳化前/後規則召回的情況


壓縮——日誌儲存最佳化


壓縮是常見的用於資料傳輸、持久化等過程中減少頻寬、儲存佔用的方法,本質是透過編碼的方式提高資料密度,減少資料的冗餘度,一般以資料壓縮速度和壓縮率兩個指標來衡量壓縮過程的效能。常用的無失真壓縮方式有:gzip、xz、lz4、zlib、zstd等。

對於風控業務來說,每一次風控請求會產生包含輸入引數、計算詳情(計算規則、特徵因子等中間結果的快照)、打擊決策等多個維度的日誌資料。我們需要提供準實時的查詢能力,用於輔助人工判定風控決策的召回和誤傷等情況。由於一次風控分析可能經歷了上百條規則、上千個特徵的計算,單條日誌資料的平均大小達到了11KB左右,最大的高達幾十KB。基於風控日誌的特點,我們把常用特徵值(mid、buvid、ip等)和日誌主體精簡出來,使用ES儲存並提供關鍵字查詢。其他詳情(引數、中間結果等)則依託於B站自研的KV儲存taishan KV(*關於B站自研分散式KV儲存的介紹可以參閱:B站分散式KV儲存實踐),以請求traceId為key進行gzip壓縮後儲存。查詢時先基於ES查詢日誌主體,再對分頁記錄點查日誌詳情併合並展示,其流程如圖12所示。這些最佳化幫助風控度過了22年之前接入場景大量增長的階段,但隨著持續接入反爬蟲等大流量讀場景與降本增效帶來的雙重壓力下,風控日誌儲存陷入了瓶頸。


常用效能最佳化手段及在風控系統中的應用

圖12:舊風控日誌詳情儲存與查詢過程


以taishan KV儲存的日誌詳情為例,總儲存達到了16TB左右。因此,我們期望能夠利用壓縮率更高的編碼方式和壓縮演算法替代json+gzip的組合,進一步最佳化日誌儲存。透過調研常見壓縮演算法,結合日誌詳情無壓縮速度要求的特點,我們採集了線上真實日誌作為實驗集,選取了protobuf、msgpack等編碼方式和xz、zstd兩種演算法進行了多次對比實驗。

在第一輪實驗中,我們對比了單條日誌在不同編碼方式下不同壓縮演算法的壓縮率。實驗隨機取同一場景下任意一條日誌詳情進行編碼和壓縮,重複多次後取各階段資料長度平均值。其中,由於風控日誌包含了許多巢狀和非固定結構,難以使用protobuf等需要預定義結構的序列化方式。因此我們嘗試了另一種高效的通用序列化框架msgpack。結果如表2所示。從結果上看,msgpack雖然編碼後比json更簡短,但由於產生了許多非文字結構,最終壓縮率不及json。xz演算法由於壓縮率無明顯優勢且記憶體佔用較大而被棄用(圖13)。無字典模式下的zstd壓縮率略弱於gzip。


編碼方式
訊息長度
gzipxzzstd(無字典)
json22551028
10921075
msgpack1938108811321119

表2:風控日誌在不同編碼方式、壓縮演算法下的壓縮效果(單位:位元組)


常用效能最佳化手段及在風控系統中的應用

圖13:各壓縮演算法壓縮風控日誌的記憶體佔用


在第二輪實驗中,我們使用json編碼方式,對比了gzip和zstd有無字典兩種模式下的壓縮率。其中,字典1由1萬條UAT爬蟲場景日誌訓練獲得,與線上日誌差異較大。字典2由10000條線上爬蟲場景日誌訓練。首先是單條日誌壓縮時不同zstd字典的影響,如表3所示。結果上,不使用字典時壓縮率最差,使用不匹配的字典略有提升。而使用匹配的字典後,zstd的壓縮率有顯著的提高。然後是對爬蟲場景不同數量的日誌進行批次壓縮對壓縮率的影響,如表4所示。zstd在小日誌壓縮上使用匹配的字典壓縮效率較好,隨著每批次數量增多,壓縮率會相對降低,最終與gzip相當。批次壓縮能夠顯著提高兩種演算法的總體壓縮率,但單次超過10條以後遇到了邊際效應,收益急速降低。此外,我們基於任意單條日誌進行了多輪效能測試,表5展示了其中5輪測試結果。從壓縮效能角度分析,無論是否使用字典,zstd壓縮的效率都遠超過gzip。


日誌來源場景
訊息長度gzip

zstd

(無字典)

zstd

(字典1)

zstd

(字典2)

說明
裂變拉新分享252773869456442364412非同場景字典
爬蟲428314341503996438同場景字典

表3:風控日誌詳情在zstd演算法下使用不同字典的壓縮效果

(單位:位元組)


每批

日誌數

總計

日誌數

gzip

zstd

(無字典)

zstd

(字典1)

zstd

(字典2)

1
100

154370

(1.000)

160582

(1.040)


111708

(0.723)


59195

(0.383)

 


10
100

58984

(1.000)


67036

(1.137)


59942

(1.016) 


47684

(0.808)


20
100

56085

(1.000)


63103

(1.125)


58509

(1.043)


56085

(1.000)


50
100

49152

(1.000)


55107

(1.121)


52927

(1.077)


48891

(0.995)


100
100

52439

(1.000)


56926

(1.086)


56076

(1.069)


53624

(1.023) 


1
1000

1607512

(1.000)


1668363

(1.038)


1154260

(0.718)


629463

(0.392)


100
1000

536579

(1.000) 


580076

(1.081)


572053

(1.066)


547021

(1.019) 


500
1000

546506

(1.000)


570690

(1.044)


571252

(1.045)


565319

(1.034) 


表4:不同數量的日誌壓縮後資料大小總和與壓縮率對比(單位:位元組)


測試序號gzip

zstd

(無字典)

zstd

(字典1)

zstd

(字典2)

1
1231422750931425
24474
2
1393872824631014
22763
3
148184
3711837409
60840
4
158618
251682936926504
5
1703123378247756
28951

表5:風控日誌在gzip與zstd演算法壓縮效能對比(單位:ns/op)


綜合以上實驗,雖然zstd演算法在壓縮效率上遠優於gzip,但僅在使用匹配的字典集時,對單條日誌的壓縮率遠優於gzip。另外,無論哪種壓縮演算法都在批次壓縮中收益明顯,最高能夠減少60%的儲存。最後,由於我們對壓縮效率的需求較低,且訓練zstd字典等改造成本較大等原因,我們選擇對現有的風控日誌詳情進行批次壓縮改造(圖14)。實現上,基於railgun提供的聚合佇列功能,我們將消費的日誌分成n條若干批次,分配一個批處理ID(BatchId)並儲存到日誌主體中,日誌以BatchId為key批次gzip壓縮後存入taishan KV。查詢時,獲取分頁下待查的BatchId,去重後批次從taishan KV拉取資料,解壓後合併到日誌中。對於查詢效率,最差情況下,每個BatchId都沒有去重,即每條資料多查詢了n-1條,請求數量不變,資料量變大N倍。實際查詢中,由於大多數查詢結果都是同一時段的連續資料集,因此實際去重效果較好,查詢效率略有提升。從儲存最佳化上看,taishan KV寫入QPS由8k下降至1k左右,每秒寫入量由78MB下降為55MB,降幅約30%。表儲存TTL為7天,7日儲存下降約6TB,降幅約38%。


常用效能最佳化手段及在風控系統中的應用

圖14:風控日誌詳情批次儲存與查詢過程


快取——多級快取+布隆過濾器


快取是最常見的加速資料交換的技術,通常基於記憶體等高速儲存器實現,其本質就是用空間換時間,犧牲一定的資料實時性,以減少各類IO的頻率,提升整體響應速度。常見的快取方案包括Cache Aside、Read/Write Through、Write-back等,適用於不同的業務場景。

在Gaia引擎中,名單庫服務是風險特徵判斷的重要組成部分,採用了最常用的Cache Aside模式。名單庫服務的需求是一種經典的讀多寫少場景:引擎將分屬不同名單的風險主體持久化儲存(100QPS以下),同時提供介面查詢指定主體是否屬於某一名單(上萬QPS)。因此,初期的名單庫採用了localCache+Redis Cache+MySQL儲存的模式實現查詢過程:寫入時刪除快取,查詢時依次查詢Cache,直到回源DB查詢,並將實際值或空值寫入Cache。這一模式在低流量條件下表現優異,直到風控接入流量急速增長至十萬以上時,出現了越來越多的瓶頸問題,如:Redis CPU負載高、記憶體佔用高、DB回源超時等,DB儲存的名單個體數目也超過了3千萬並且持續快速增長。這迫使我們做了許多最佳化來滿足後續潛在的百萬級QPS查詢需求。其中最有效的就是布隆過濾器(Bloom Filter,BF)多級快取最佳化,具體的最佳化過程如圖15所示。對於寫過程來說,新增更新服務訂閱了名單表的binlog,提供BF的全量/增量更新。對於讀過程來說,在原有多級快取前新增BF Cache查詢,在確認特徵不存在的情況下直接返回結果。


常用效能最佳化手段及在風控系統中的應用

圖15:名單庫服務BF多級快取最佳化過程——新老流程對比


由於名單庫查詢時,大多數使用者並無風險,名單庫查詢存在普遍的快取穿透問題。因此名單庫查詢天然配適BF的使用場景,但要實際落地,仍然面臨著許多問題:

  1. HotKey和BigKey問題。由於全集記錄超過3千萬條,單個BF容量越大,value越大,越容易出現集中訪問同一個key的熱點問題。因此需要對BF進行合理的拆分。

  2. 維護問題。BF需要維護一個全集資料,因此無論是本地還是分散式實現,都需要具備基於全量資料構建的能力。從資料安全性角度出發,BF存在人為操作等導致非預期異常的可能,BF需要具備備份和快速恢復能力。

  3. 資料一致性問題。一方面,由於BF能夠確定的表述一個key不存在於全集中,因此需要保證DB與BF的最終一致性。為了保證新記錄一定存入BF,插入BF需要支援無限重試。另一方面,由於BF存在假陽率,且不能刪除個體,隨著名單過期、key數量逼近BF容量等情況的發生,BF實際假陽率會逐級升高導致過濾效果變差。因此需要支援BF容量擴充和實現定期重建的能力。

由於Redis布隆過濾器外掛完整的支援了BF的操作和自動擴容,我們選擇使用Redis作為BF的分散式實現。對於HotKey和BigKey問題,我們對BF進行了隨機分片。為了保證BF Key分佈均勻,人為的將分片總數BF_SLICE_SIZE定義為4倍Redis Slot數量,即65536個。每一個分片Key的命名格式為"{libKey_bf_" + sliceIndex + "}",其中sliceIndex為分片序號,使用花括號來保證相同字首的BF利用rename命令迭代替換時處於同一個Slot中。名單個體將按照sliceIndex = HashKey % BF_SLICE_SIZE計算自己所屬的分片,其中HashKey的取值為名單個體值的IEEE CRC32絕對值。此外,我們對BF設定了獨立的本地快取以減少實際呼叫。由於BF只增不減的特點,對於陽性結果,我們設定了較長TTL。而對於陰性結果,則按業務容忍度設定了秒級TTL,保證及時獲取新插入個體。

對於維護問題,我們實現了完整的構建工具。同時,基於安全性考慮實現了備份和快速恢復流程。基於狀態機,我們定義了BF的構建過程:初始化、非同步構建、同步測試、BF更新等。整個構建過程使用分散式鎖保證唯一性,基於railgun定時任務週期性觸發,整個過程記錄構建進度並提供實時展示和查詢。全過程如圖16所示。初始化時,會Dump生成正在執行的BF備份檔案並儲存到物件儲存。生成新的BF臨時分片,以"_temp"尾綴區分。更新服務基於狀態變化將增量個體雙寫到臨時BF中。非同步構建過程會分批獲取完整的名單表,批次寫入存量個體到臨時BF中。之後進行同步測試,利用少量增量和存量個體模擬查詢臨時BF,當所有測試個體都存在於BF時透過測試。最後,將臨時BF原子性地替換正式BF,完成構建過程。快速恢復過程基於構建的整體流程,部分模組略有差異:初始化階段會獲取備份檔案並嘗試restore資料到臨時BF中。非同步構建時則基於備份時間點開始獲取存量資料。


常用效能最佳化手段及在風控系統中的應用

圖16:BF構建與快速恢復流程


對於資料一致性問題,我們提供了完整的控制、評估和測試BF一致性的流程。為了保證資料安全,我們定義了BF測試模式和正常模式兩種執行方式,並可以按比例配置執行,如圖17所示。測試模式下,查詢的BF值不會生效,流量進入快取查詢流程。之後基於查詢實際值對比結果進行監控報點並建立真陰性比例四個9等監控告警。處於正常模式則會對BF假陽等情況進行報點等。實際上線後,服務長期保持一定比例的流量(當前為0.1%)執行測試模式,用於持續評估線上BF執行狀態。圖18展示了BF查詢結果佔比,約95%的查詢為陰性,最佳化收益明顯,如表6所示。在後續壓測中,名單庫服務具備了支撐百萬級流量的能力。


常用效能最佳化手段及在風控系統中的應用

圖17:名單庫服務BF多級快取最佳化過程


常用效能最佳化手段及在風控系統中的應用

圖18:名單庫服務線上流量BF查詢結果佔比


最佳化專案最佳化前最佳化後降幅說明
服務CPU使用率50.5%17.5%65%日峰值同比
Redis 記憶體佔用256GB50GB80%日峰值同比
Redis 網路IO174/187Mbps13.7/6.7Mbps92%/96%日峰值同比
Redis CPU使用率42%4%90%日峰值同比
BF Redis 記憶體佔用0
7GB-
新增10個節點
BF Redis 網路IO0
71.4/7.5Mbps-
新增10個節點
BF Redis CPU使用率
0
10%-
新增10個節點
MySQL 讀QPS12k
600
95%日峰值同比

表6:名單庫BF多級快取最佳化收益對比


總結


效能最佳化的手段有多種方式,本篇文章只是結合近期在風控系統中的應用實踐進行的一個總結,需要說明的是,其中有的最佳化手段有利也有弊,得到的同時也在失去,可見,任何最佳化手段都得以業務可接受為前提,因地制宜才是正道。正如Linux效能最佳化大師Brendan Gregg一再強調的:切忌過早最佳化、過度最佳化。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027824/viewspace-2991291/,如需轉載,請註明出處,否則將追究法律責任。

相關文章