常用性能優(yōu)化手段及在風(fēng)控系統(tǒng)中的應(yīng)用
引言
性能優(yōu)化是個(gè)恒久的話題,隨著產(chǎn)品的演進(jìn),業(yè)務(wù)的增長(zhǎng),系統(tǒng)能力總有達(dá)到瓶頸的一天,它不可或缺的陪伴著我們走向壯大再走向衰敗,是我們面臨的不可回避的問題。下圖1展示了風(fēng)控系統(tǒng)近半年來承載流量的增長(zhǎng)趨勢(shì),可見最近半年來流量高速增長(zhǎng),且對(duì)于可預(yù)見的未來而言,接入流量還會(huì)持續(xù)高增。伴隨著流量的增長(zhǎng),系統(tǒng)各方面--存儲(chǔ)、計(jì)算、IO等都表現(xiàn)出一定的瓶頸,通過原始簡(jiǎn)單的水平擴(kuò)容并不能解決所有的問題,而且還會(huì)帶來成本的上升。因此,我們近期對(duì)系統(tǒng)進(jìn)行了一系列優(yōu)化改造, 目的是滿足未來一段時(shí)間內(nèi)業(yè)務(wù)的增長(zhǎng)使用,降低接口的耗時(shí)滿足某些延時(shí)敏感型業(yè)務(wù)的需要,同時(shí)也伴隨著一定的IT成本優(yōu)化。本文結(jié)合常見的性能優(yōu)化手段(預(yù)取、批量、異步、壓縮、緩存),及在風(fēng)控系統(tǒng)中的實(shí)踐進(jìn)行總結(jié),希望能給讀者對(duì)于性能優(yōu)化實(shí)踐帶來一些參考。
圖1:風(fēng)控引擎流量增長(zhǎng)趨勢(shì)
預(yù)取——特征預(yù)計(jì)算
預(yù)取作為一種提速手段,通常與緩存搭配使用,在緩存空間換時(shí)間的基礎(chǔ)上更進(jìn)一步,以時(shí)間換時(shí)間,通過預(yù)加載來提升性能。常見的使用有,數(shù)據(jù)庫(kù)從磁盤加載頁時(shí)的預(yù)讀多個(gè)頁減少磁盤IO、CPU緩存加載一片連續(xù)的內(nèi)存空間提高計(jì)算的速度,也就是我們常說的CPU對(duì)數(shù)組友好對(duì)鏈表不友好的原因。
在Gaia風(fēng)控引擎中,一次業(yè)務(wù)請(qǐng)求在引擎內(nèi)部的執(zhí)行流程如下圖2所示,會(huì)經(jīng)歷場(chǎng)景因子(特征)->規(guī)則->決策的計(jì)算過程, 而計(jì)算因子是整個(gè)鏈路最耗時(shí)的部分,通常占請(qǐng)求響應(yīng)時(shí)間的70%以上,包括對(duì)賬號(hào)信息、名單庫(kù)、模型數(shù)據(jù)、用戶畫像、設(shè)備指紋、三方特征等多個(gè)下游的讀擴(kuò)散–特征因子獲取,加上場(chǎng)景的上百條規(guī)則執(zhí)行,請(qǐng)求耗時(shí)常規(guī)在250ms左右,這也是22年中以前我們承諾給主站大部分業(yè)務(wù)的響應(yīng)時(shí)間,直到電商業(yè)務(wù)的接入,對(duì)我們引擎的響應(yīng)時(shí)間提出了100ms以內(nèi)響應(yīng)的要求,迫使我們對(duì)引擎進(jìn)行了一些優(yōu)化,其中之一就是近線引擎帶來的特征預(yù)取優(yōu)化,其演進(jìn)流程如下圖3示:
圖2:風(fēng)控引擎一次請(qǐng)求執(zhí)行過程
圖3:風(fēng)控特征獲取流程
基于一個(gè)前提:對(duì)同一個(gè)主體,按照其行為時(shí)序數(shù)據(jù)消費(fèi),slb數(shù)據(jù)源消費(fèi)處理完成大多數(shù)時(shí)候都要比業(yè)務(wù)請(qǐng)求風(fēng)控早。因此,我們通過對(duì)slb實(shí)時(shí)流數(shù)據(jù)消費(fèi)預(yù)讀取下游特征,并將結(jié)果緩存在redis中,當(dāng)業(yè)務(wù)請(qǐng)求風(fēng)控時(shí),直接獲取redis的數(shù)據(jù),避免或減少rpc回源特征,以此來降低風(fēng)控接口的耗時(shí)。
通過這套機(jī)制,我們按照特征變動(dòng)頻率對(duì)特征分層設(shè)置不同的緩存時(shí)間,同時(shí)在一些對(duì)數(shù)據(jù)一致性要求較低的場(chǎng)景對(duì)特征開啟緩存讀,其緩存命中率能達(dá)到90%以上,核心業(yè)務(wù)場(chǎng)景如電商交易,接口響應(yīng)耗時(shí)從80ms下降到25ms。
圖4:特征緩存命中率
圖5:電商交易場(chǎng)景請(qǐng)求風(fēng)控接口TP99耗時(shí)曲線圖
批量——特征批量獲取
批量一般是對(duì)I/O操作的優(yōu)化,同樣可看作是時(shí)間換時(shí)間,通過合并、批量進(jìn)行讀取或?qū)懭胍詼p少對(duì)I/O的操作來提升吞吐和性能。常見的使用有,kafka消費(fèi)數(shù)據(jù)的時(shí)候批量拉取指定的數(shù)據(jù)條數(shù),mysql寫入redolog/binlog時(shí)的組提交(group commit)機(jī)制,都是通過批量的優(yōu)化來減少I/O提升性能的。
在Gaia引擎中最常用的特征為對(duì)賬號(hào)管控/風(fēng)控名單值的獲取,一次業(yè)務(wù)風(fēng)險(xiǎn)判斷請(qǐng)求會(huì)涉及到對(duì)請(qǐng)求主體(mid、buvid、ip、ua)的各種黑/白名單值獲取,以主體mid為例,往往會(huì)并發(fā)調(diào)用下游名單接口多次,判斷mid是否在同設(shè)備多賬號(hào)黑名單、虛假賬號(hào)黑名單、通用白名單等名單中,從而形成不同的因子供規(guī)則使用,這種方式就會(huì)造成對(duì)同下游的同接口的讀擴(kuò)散流量放大浪費(fèi)資源,且要保持下游接口低延遲往往需要下游進(jìn)行擴(kuò)容保證CPU等資源使用率在一定的水位以下。因此,為了降低自身及下游的服務(wù)資源和I/O,優(yōu)化的手段就是將多次請(qǐng)求合并為一次請(qǐng)求,其優(yōu)化流程如下圖6所示:
圖6:名單類因子讀取優(yōu)化流程
通過將多次獨(dú)立下游請(qǐng)求獲取給定黑/白名單轉(zhuǎn)化為一次批量請(qǐng)求獲取主體所有有效名單,同時(shí)結(jié)合本地內(nèi)存判斷因子請(qǐng)求名單與所有名單是否有交集來實(shí)現(xiàn)原有相同的功能,并通過local cache及singleflight優(yōu)化,降低對(duì)下游接口調(diào)用量69%。
圖7:實(shí)驗(yàn)環(huán)境名單庫(kù)接口批量?jī)?yōu)化效果
異步——累積因子同步改異步計(jì)算優(yōu)化
異步通常和同步一起對(duì)比,其區(qū)別在于發(fā)起請(qǐng)求之后是立即返回還是等待結(jié)果,常用于在系統(tǒng)內(nèi)部有大量I/O操作時(shí),通過異步提升吞吐。常見的使用有,基于write-back模式向緩存寫入數(shù)據(jù),mysql異步傳輸binlog進(jìn)行主從復(fù)制等。
在Gaia引擎中,一次請(qǐng)求會(huì)涉及對(duì)很多特征因子的計(jì)算,其中,最常用的是基于redis實(shí)現(xiàn)的累積因子,其包含如下幾種類型(見表1),以count(distinct)類型為例,一次計(jì)算過程包含1寫zadd,1讀zcount,概率觸發(fā)zrem清除不在有效時(shí)間窗口內(nèi)的過期數(shù)據(jù),其最多對(duì)redis請(qǐng)求3次,最少/均攤2次,且zset這幾個(gè)操作的時(shí)間復(fù)雜度都在O(log n)以上,加上一次請(qǐng)求往往會(huì)對(duì)多個(gè)累積因子進(jìn)行計(jì)算(讀寫擴(kuò)散),這給redis集群帶來了較大的計(jì)算壓力,由于overload對(duì)集群實(shí)例擴(kuò)容的限制,我們對(duì)redis集群的水平擴(kuò)容也遇到了瓶頸??紤]當(dāng)前引擎流量的情況:爬蟲類業(yè)務(wù)與常規(guī)業(yè)務(wù)流量占比為1.5:1,且爬蟲類業(yè)務(wù)流量還在持續(xù)高漲,鑒于爬蟲類業(yè)務(wù)風(fēng)控的特性(非資產(chǎn)安全,容忍一定的漏過),我們以犧牲數(shù)據(jù)一致性為代價(jià),對(duì)爬蟲類業(yè)務(wù)的累積因子進(jìn)行了異步計(jì)算優(yōu)化,以減少對(duì)redis的調(diào)用,其計(jì)算演進(jìn)流程如下圖8所示:
類型 | 功能 | 底層實(shí)現(xiàn) | 使用頻率 |
count | 計(jì)數(shù) | incr | 高 |
count(distinct) | 去重計(jì)數(shù) | zset | 高 |
sum | 累積求和 | incrby | 低 |
avg | 累積求平均 | incrby | 低 |
表1:風(fēng)控引擎支持累積因子類型
圖8:累積因子計(jì)算(優(yōu)化前/后)流程
基于railgun(關(guān)于B站自研異步事件處理平臺(tái),可參閱:從1到億,如何玩好異步消息?CQRS架構(gòu)下的異步事件治理實(shí)踐)提供的內(nèi)存隊(duì)列聚合功能,我們對(duì)累積因子寫操作進(jìn)行了異步化改造,并結(jié)合聚合功能,在設(shè)置的時(shí)間/數(shù)量閾值條件下,對(duì)相同累積key進(jìn)行聚合并在內(nèi)存中去重后批量寫入redis,將多次同步redis I/O減少為異步寫1次。而優(yōu)化的效果從三個(gè)角度評(píng)估,從成本角度看:其對(duì)redis的調(diào)用qps減少了35%以上(如圖9);從接口耗時(shí)看:TP99有一定的下降; 從對(duì)風(fēng)控規(guī)則的召回影響看,前后召回趨勢(shì)基本一致,且打擊總量差距不大,在可接受的范圍內(nèi)。
圖9:?jiǎn)螆?chǎng)景累積因子計(jì)算優(yōu)化前/后對(duì)redis的調(diào)用量情況
圖10:累積因子計(jì)算優(yōu)化前/后接口耗時(shí)情況
圖11:累積因子計(jì)算優(yōu)化前/后規(guī)則召回的情況
壓縮——日志存儲(chǔ)優(yōu)化
壓縮是常見的用于數(shù)據(jù)傳輸、持久化等過程中減少帶寬、存儲(chǔ)占用的方法,本質(zhì)是通過編碼的方式提高數(shù)據(jù)密度,減少數(shù)據(jù)的冗余度,一般以數(shù)據(jù)壓縮速度和壓縮率兩個(gè)指標(biāo)來衡量壓縮過程的效能。常用的無損壓縮方式有:gzip、xz、lz4、zlib、zstd等。
對(duì)于風(fēng)控業(yè)務(wù)來說,每一次風(fēng)控請(qǐng)求會(huì)產(chǎn)生包含輸入?yún)?shù)、計(jì)算詳情(計(jì)算規(guī)則、特征因子等中間結(jié)果的快照)、打擊決策等多個(gè)維度的日志數(shù)據(jù)。我們需要提供準(zhǔn)實(shí)時(shí)的查詢能力,用于輔助人工判定風(fēng)控決策的召回和誤傷等情況。由于一次風(fēng)控分析可能經(jīng)歷了上百條規(guī)則、上千個(gè)特征的計(jì)算,單條日志數(shù)據(jù)的平均大小達(dá)到了11KB左右,最大的高達(dá)幾十KB。基于風(fēng)控日志的特點(diǎn),我們把常用特征值(mid、buvid、ip等)和日志主體精簡(jiǎn)出來,使用ES存儲(chǔ)并提供關(guān)鍵字查詢。其他詳情(參數(shù)、中間結(jié)果等)則依托于B站自研的KV存儲(chǔ)taishan KV,以請(qǐng)求traceId為key進(jìn)行g(shù)zip壓縮后存儲(chǔ)。查詢時(shí)先基于ES查詢?nèi)罩局黧w,再對(duì)分頁記錄點(diǎn)查日志詳情并合并展示,其流程如圖12所示。這些優(yōu)化幫助風(fēng)控度過了22年之前接入場(chǎng)景大量增長(zhǎng)的階段,但隨著持續(xù)接入反爬蟲等大流量讀場(chǎng)景與降本增效帶來的雙重壓力下,風(fēng)控日志存儲(chǔ)陷入了瓶頸。
圖12:舊風(fēng)控日志詳情存儲(chǔ)與查詢過程
以taishan KV存儲(chǔ)的日志詳情為例,總存儲(chǔ)達(dá)到了16TB左右。因此,我們期望能夠利用壓縮率更高的編碼方式和壓縮算法替代json+gzip的組合,進(jìn)一步優(yōu)化日志存儲(chǔ)。通過調(diào)研常見壓縮算法,結(jié)合日志詳情無壓縮速度要求的特點(diǎn),我們采集了線上真實(shí)日志作為實(shí)驗(yàn)集,選取了protobuf、msgpack等編碼方式和xz、zstd兩種算法進(jìn)行了多次對(duì)比實(shí)驗(yàn)。
在第一輪實(shí)驗(yàn)中,我們對(duì)比了單條日志在不同編碼方式下不同壓縮算法的壓縮率。實(shí)驗(yàn)隨機(jī)取同一場(chǎng)景下任意一條日志詳情進(jìn)行編碼和壓縮,重復(fù)多次后取各階段數(shù)據(jù)長(zhǎng)度平均值。其中,由于風(fēng)控日志包含了許多嵌套和非固定結(jié)構(gòu),難以使用protobuf等需要預(yù)定義結(jié)構(gòu)的序列化方式。因此我們嘗試了另一種高效的通用序列化框架msgpack。結(jié)果如表2所示。從結(jié)果上看,msgpack雖然編碼后比json更簡(jiǎn)短,但由于產(chǎn)生了許多非文本結(jié)構(gòu),最終壓縮率不及json。xz算法由于壓縮率無明顯優(yōu)勢(shì)且內(nèi)存占用較大而被棄用(圖13)。無字典模式下的zstd壓縮率略弱于gzip。
編碼方式 | 消息長(zhǎng)度 | gzip | xz | zstd(無字典) |
json | 2255 | 1028 | 1092 | 1075 |
msgpack | 1938 | 1088 | 1132 | 1119 |
表2:風(fēng)控日志在不同編碼方式、壓縮算法下的壓縮效果(單位:字節(jié))
圖13:各壓縮算法壓縮風(fēng)控日志的內(nèi)存占用
在第二輪實(shí)驗(yàn)中,我們使用json編碼方式,對(duì)比了gzip和zstd有無字典兩種模式下的壓縮率。其中,字典1由1萬條UAT爬蟲場(chǎng)景日志訓(xùn)練獲得,與線上日志差異較大。字典2由10000條線上爬蟲場(chǎng)景日志訓(xùn)練。首先是單條日志壓縮時(shí)不同zstd字典的影響,如表3所示。結(jié)果上,不使用字典時(shí)壓縮率最差,使用不匹配的字典略有提升。而使用匹配的字典后,zstd的壓縮率有顯著的提高。然后是對(duì)爬蟲場(chǎng)景不同數(shù)量的日志進(jìn)行批量壓縮對(duì)壓縮率的影響,如表4所示。zstd在小日志壓縮上使用匹配的字典壓縮效率較好,隨著每批次數(shù)量增多,壓縮率會(huì)相對(duì)降低,最終與gzip相當(dāng)。批量壓縮能夠顯著提高兩種算法的總體壓縮率,但單次超過10條以后遇到了邊際效應(yīng),收益急速降低。此外,我們基于任意單條日志進(jìn)行了多輪性能測(cè)試,表5展示了其中5輪測(cè)試結(jié)果。從壓縮性能角度分析,無論是否使用字典,zstd壓縮的效率都遠(yuǎn)超過gzip。
日志來源場(chǎng)景 | 消息長(zhǎng)度 | gzip | zstd (無字典) | zstd (字典1) | zstd (字典2) | 說明 |
裂變拉新分享 | 25277 | 3869 | 4564 | 4236 | 4412 | 非同場(chǎng)景字典 |
爬蟲 | 4283 | 1434 | 1503 | 996 | 438 | 同場(chǎng)景字典 |
表3:風(fēng)控日志詳情在zstd算法下使用不同字典的壓縮效果
(單位:字節(jié))
每批 日志數(shù) | 總計(jì) 日志數(shù) | 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:不同數(shù)量的日志壓縮后數(shù)據(jù)大小總和與壓縮率對(duì)比(單位:字節(jié))
測(cè)試序號(hào) | gzip | zstd (無字典) | zstd (字典1) | zstd (字典2) |
1 | 123142 | 27509 | 31425 | 24474 |
2 | 139387 | 28246 | 31014 | 22763 |
3 | 148184 | 37118 | 37409 | 60840 |
4 | 158618 | 25168 | 29369 | 26504 |
5 | 170312 | 33782 | 47756 | 28951 |
表5:風(fēng)控日志在gzip與zstd算法壓縮性能對(duì)比(單位:ns/op)
綜合以上實(shí)驗(yàn),雖然zstd算法在壓縮效率上遠(yuǎn)優(yōu)于gzip,但僅在使用匹配的字典集時(shí),對(duì)單條日志的壓縮率遠(yuǎn)優(yōu)于gzip。另外,無論哪種壓縮算法都在批量壓縮中收益明顯,最高能夠減少60%的存儲(chǔ)。最后,由于我們對(duì)壓縮效率的需求較低,且訓(xùn)練zstd字典等改造成本較大等原因,我們選擇對(duì)現(xiàn)有的風(fēng)控日志詳情進(jìn)行批量壓縮改造(圖14)。實(shí)現(xiàn)上,基于railgun提供的聚合隊(duì)列功能,我們將消費(fèi)的日志分成n條若干批次,分配一個(gè)批處理ID(BatchId)并存儲(chǔ)到日志主體中,日志以BatchId為key批量gzip壓縮后存入taishan KV。查詢時(shí),獲取分頁下待查的BatchId,去重后批量從taishan KV拉取數(shù)據(jù),解壓后合并到日志中。對(duì)于查詢效率,最差情況下,每個(gè)BatchId都沒有去重,即每條數(shù)據(jù)多查詢了n-1條,請(qǐng)求數(shù)量不變,數(shù)據(jù)量變大N倍。實(shí)際查詢中,由于大多數(shù)查詢結(jié)果都是同一時(shí)段的連續(xù)數(shù)據(jù)集,因此實(shí)際去重效果較好,查詢效率略有提升。從存儲(chǔ)優(yōu)化上看,taishan KV寫入QPS由8k下降至1k左右,每秒寫入量由78MB下降為55MB,降幅約30%。表存儲(chǔ)TTL為7天,7日存儲(chǔ)下降約6TB,降幅約38%。
圖14:風(fēng)控日志詳情批量存儲(chǔ)與查詢過程
緩存——多級(jí)緩存+布隆過濾器
緩存是最常見的加速數(shù)據(jù)交換的技術(shù),通?;趦?nèi)存等高速存儲(chǔ)器實(shí)現(xiàn),其本質(zhì)就是用空間換時(shí)間,犧牲一定的數(shù)據(jù)實(shí)時(shí)性,以減少各類IO的頻率,提升整體響應(yīng)速度。常見的緩存方案包括Cache Aside、Read/Write Through、Write-back等,適用于不同的業(yè)務(wù)場(chǎng)景。
在Gaia引擎中,名單庫(kù)服務(wù)是風(fēng)險(xiǎn)特征判斷的重要組成部分,采用了最常用的Cache Aside模式。名單庫(kù)服務(wù)的需求是一種經(jīng)典的讀多寫少場(chǎng)景:引擎將分屬不同名單的風(fēng)險(xiǎn)主體持久化存儲(chǔ)(100QPS以下),同時(shí)提供接口查詢指定主體是否屬于某一名單(上萬QPS)。因此,初期的名單庫(kù)采用了localCache+Redis Cache+MySQL存儲(chǔ)的模式實(shí)現(xiàn)查詢過程:寫入時(shí)刪除緩存,查詢時(shí)依次查詢Cache,直到回源DB查詢,并將實(shí)際值或空值寫入Cache。這一模式在低流量條件下表現(xiàn)優(yōu)異,直到風(fēng)控接入流量急速增長(zhǎng)至十萬以上時(shí),出現(xiàn)了越來越多的瓶頸問題,如:Redis CPU負(fù)載高、內(nèi)存占用高、DB回源超時(shí)等,DB存儲(chǔ)的名單個(gè)體數(shù)目也超過了3千萬并且持續(xù)快速增長(zhǎng)。這迫使我們做了許多優(yōu)化來滿足后續(xù)潛在的百萬級(jí)QPS查詢需求。其中最有效的就是布隆過濾器(Bloom Filter,BF)多級(jí)緩存優(yōu)化,具體的優(yōu)化過程如圖15所示。對(duì)于寫過程來說,新增更新服務(wù)訂閱了名單表的binlog,提供BF的全量/增量更新。對(duì)于讀過程來說,在原有多級(jí)緩存前新增BF Cache查詢,在確認(rèn)特征不存在的情況下直接返回結(jié)果。
圖15:名單庫(kù)服務(wù)BF多級(jí)緩存優(yōu)化過程——新老流程對(duì)比
由于名單庫(kù)查詢時(shí),大多數(shù)用戶并無風(fēng)險(xiǎn),名單庫(kù)查詢存在普遍的緩存穿透問題。因此名單庫(kù)查詢天然配適BF的使用場(chǎng)景,但要實(shí)際落地,仍然面臨著許多問題:
- HotKey和BigKey問題。由于全集記錄超過3千萬條,單個(gè)BF容量越大,value越大,越容易出現(xiàn)集中訪問同一個(gè)key的熱點(diǎn)問題。因此需要對(duì)BF進(jìn)行合理的拆分。
- 維護(hù)問題。BF需要維護(hù)一個(gè)全集數(shù)據(jù),因此無論是本地還是分布式實(shí)現(xiàn),都需要具備基于全量數(shù)據(jù)構(gòu)建的能力。從數(shù)據(jù)安全性角度出發(fā),BF存在人為操作等導(dǎo)致非預(yù)期異常的可能,BF需要具備備份和快速恢復(fù)能力。
- 數(shù)據(jù)一致性問題。一方面,由于BF能夠確定的表述一個(gè)key不存在于全集中,因此需要保證DB與BF的最終一致性。為了保證新記錄一定存入BF,插入BF需要支持無限重試。另一方面,由于BF存在假陽率,且不能刪除個(gè)體,隨著名單過期、key數(shù)量逼近BF容量等情況的發(fā)生,BF實(shí)際假陽率會(huì)逐級(jí)升高導(dǎo)致過濾效果變差。因此需要支持BF容量擴(kuò)充和實(shí)現(xiàn)定期重建的能力。
由于Redis布隆過濾器插件完整的支持了BF的操作和自動(dòng)擴(kuò)容,我們選擇使用Redis作為BF的分布式實(shí)現(xiàn)。對(duì)于HotKey和BigKey問題,我們對(duì)BF進(jìn)行了隨機(jī)分片。為了保證BF Key分布均勻,人為的將分片總數(shù)BF_SLICE_SIZE定義為4倍Redis Slot數(shù)量,即65536個(gè)。每一個(gè)分片Key的命名格式為"{libKey_bf_" + sliceIndex + "}",其中sliceIndex為分片序號(hào),使用花括號(hào)來保證相同前綴的BF利用rename命令迭代替換時(shí)處于同一個(gè)Slot中。名單個(gè)體將按照sliceIndex = HashKey % BF_SLICE_SIZE計(jì)算自己所屬的分片,其中HashKey的取值為名單個(gè)體值的IEEE CRC32絕對(duì)值。此外,我們對(duì)BF設(shè)置了獨(dú)立的本地緩存以減少實(shí)際調(diào)用。由于BF只增不減的特點(diǎn),對(duì)于陽性結(jié)果,我們?cè)O(shè)置了較長(zhǎng)TTL。而對(duì)于陰性結(jié)果,則按業(yè)務(wù)容忍度設(shè)置了秒級(jí)TTL,保證及時(shí)獲取新插入個(gè)體。
對(duì)于維護(hù)問題,我們實(shí)現(xiàn)了完整的構(gòu)建工具。同時(shí),基于安全性考慮實(shí)現(xiàn)了備份和快速恢復(fù)流程?;跔顟B(tài)機(jī),我們定義了BF的構(gòu)建過程:初始化、異步構(gòu)建、同步測(cè)試、BF更新等。整個(gè)構(gòu)建過程使用分布式鎖保證唯一性,基于railgun定時(shí)任務(wù)周期性觸發(fā),整個(gè)過程記錄構(gòu)建進(jìn)度并提供實(shí)時(shí)展示和查詢。全過程如圖16所示。初始化時(shí),會(huì)Dump生成正在運(yùn)行的BF備份文件并存儲(chǔ)到對(duì)象存儲(chǔ)。生成新的BF臨時(shí)分片,以"_temp"尾綴區(qū)分。更新服務(wù)基于狀態(tài)變化將增量個(gè)體雙寫到臨時(shí)BF中。異步構(gòu)建過程會(huì)分批獲取完整的名單表,批量寫入存量個(gè)體到臨時(shí)BF中。之后進(jìn)行同步測(cè)試,利用少量增量和存量個(gè)體模擬查詢臨時(shí)BF,當(dāng)所有測(cè)試個(gè)體都存在于BF時(shí)通過測(cè)試。最后,將臨時(shí)BF原子性地替換正式BF,完成構(gòu)建過程??焖倩謴?fù)過程基于構(gòu)建的整體流程,部分模塊略有差異:初始化階段會(huì)獲取備份文件并嘗試restore數(shù)據(jù)到臨時(shí)BF中。異步構(gòu)建時(shí)則基于備份時(shí)間點(diǎn)開始獲取存量數(shù)據(jù)。
圖16:BF構(gòu)建與快速恢復(fù)流程
對(duì)于數(shù)據(jù)一致性問題,我們提供了完整的控制、評(píng)估和測(cè)試BF一致性的流程。為了保證數(shù)據(jù)安全,我們定義了BF測(cè)試模式和正常模式兩種運(yùn)行方式,并可以按比例配置運(yùn)行,如圖17所示。測(cè)試模式下,查詢的BF值不會(huì)生效,流量進(jìn)入緩存查詢流程。之后基于查詢實(shí)際值對(duì)比結(jié)果進(jìn)行監(jiān)控報(bào)點(diǎn)并建立真陰性比例四個(gè)9等監(jiān)控告警。處于正常模式則會(huì)對(duì)BF假陽等情況進(jìn)行報(bào)點(diǎn)等。實(shí)際上線后,服務(wù)長(zhǎng)期保持一定比例的流量(當(dāng)前為0.1%)運(yùn)行測(cè)試模式,用于持續(xù)評(píng)估線上BF運(yùn)行狀態(tài)。圖18展示了BF查詢結(jié)果占比,約95%的查詢?yōu)殛幮?,?yōu)化收益明顯,如表6所示。在后續(xù)壓測(cè)中,名單庫(kù)服務(wù)具備了支撐百萬級(jí)流量的能力。
圖17:名單庫(kù)服務(wù)BF多級(jí)緩存優(yōu)化過程
圖18:名單庫(kù)服務(wù)線上流量BF查詢結(jié)果占比
優(yōu)化項(xiàng)目 | 優(yōu)化前 | 優(yōu)化后 | 降幅 | 說明 |
服務(wù)CPU使用率 | 50.5% | 17.5% | 65% | 日峰值同比 |
Redis 內(nèi)存占用 | 256GB | 50GB | 80% | 日峰值同比 |
Redis 網(wǎng)絡(luò)IO | 174/187Mbps | 13.7/6.7Mbps | 92%/96% | 日峰值同比 |
Redis CPU使用率 | 42% | 4% | 90% | 日峰值同比 |
BF Redis 內(nèi)存占用 | 0 | 7GB | - | 新增10個(gè)節(jié)點(diǎn) |
BF Redis 網(wǎng)絡(luò)IO | 0 | 71.4/7.5Mbps | - | 新增10個(gè)節(jié)點(diǎn) |
BF Redis CPU使用率 | 0 | 10% | - | 新增10個(gè)節(jié)點(diǎn) |
MySQL 讀QPS | 12k | 600 | 95% | 日峰值同比 |
表6:名單庫(kù)BF多級(jí)緩存優(yōu)化收益對(duì)比
總結(jié)
性能優(yōu)化的手段有多種方式,本篇文章只是結(jié)合近期在風(fēng)控系統(tǒng)中的應(yīng)用實(shí)踐進(jìn)行的一個(gè)總結(jié),需要說明的是,其中有的優(yōu)化手段有利也有弊,得到的同時(shí)也在失去,可見,任何優(yōu)化手段都得以業(yè)務(wù)可接受為前提,因地制宜才是正道。正如Linux性能優(yōu)化大師Brendan Gregg一再?gòu)?qiáng)調(diào)的:切忌過早優(yōu)化、過度優(yōu)化。