從0到1:餓了麼風控計數服務是如何煉成的

2017年4月份從餓了麼正式進入多活領域開始,也預示着餓了麼業務開始邁入下半場,此時風控團隊面臨着嚴峻的挑戰,風控須要在事前、事中、過後進行全方位的防護。redis

 

而計數器的業務幾乎貫穿了整個風控的需求,規則根據計數器攔截用戶風險動做,運營系統須要根據計數器分析出商家、用戶的刷單行爲。首先,各個系統充斥着大量重複相同的計數器代碼,其次開發團隊對於這樣重複勞動除了感受疲憊,還有點缺少技術含量,最後這樣的開發成本與模式,並不能快速知足風控業務的需求,此時一個通用的計數器服務迫在眉睫。數據庫

 

案例併發

 

首先回顧一下風控使用計數器的一個場景,讓你們瞭解計數器在風控的做用。異步

 

限制用戶下單數函數

 

假設天天用戶在餓了麼最多下10單,那麼用戶在下第11單的時候將會被風控拒掉,此場景的校驗流程以下:高併發

 

 

在這個場景中,涉及到計數器的部分包括以下幾部分:性能

 

  • 獲取計數器的邏輯編碼

  • 設置計數器的邏輯設計

  • 計數器入庫對象

 

方案:硬編碼

 

因爲歷史緣由,風控老計數器採用了硬編碼的方式,僞代碼以下:

 

 

優勢:

 

  • 當計數器種類較少,改動不頻繁的時候,開發效率高。

 

缺點:

 

  • 計數器改動成本高:例如改動計數器的存活週期,都須要走一遍發佈流程 。

  • 當計數器種類較多時,維護性差,大量重複勞動。

 

思考計數器新設計

 

在思考新設計以前,咱們先來總結一下老計數器的幾大缺點:

 

第一:重複勞動

 

以前計數器的相關邏輯,各個系統都進行了相應的開發,這段邏輯大部分是相同的,是屬於重複勞動的部分。

 

第二:key的生成規則須要暴露給其餘系統

 

若是A系統建立了計數器counter1,此時B系統和C系統須要使用計數器counter1,必須得知道A系統建立counter1時候 key的生成規則。

 

第三:計數器不可配置

 

以前計數器是硬編碼在系統中,這就意味着每次變動,例如更改計數器的生命週期和統計方式,都須要從新上線,而每次上線都須要通過alpha到生產一系列過程,耗時比較長,靈活性不夠高。

 

計數器模型

 

計數器的本質是對某一對象進行分組,對某一字段進行函數計算的過程。

 

select sum(字段) from table group by 對象。

 

若是將對象抽象成主體,字段抽象成客體,sum抽象成函數,那麼計數器模型組成以下:

 

計數器模型 = 主體+客體+函數

 

那麼計數器模型的設計如圖所示:

 

 

主要由三個部分組成:

 

計數器模型參數:計數器構成的三個要素分別是主體,客體,函數。

 

計數器邏輯執行器:主要用來執行計數器模型中函數部分,例如count、sum、max等。

 

結果:計數器邏輯執行的結果。

 

faraday系統設計

 

鑑於上面所說的計數器模型,咱們開發了faraday服務,新計數器主要分爲計數器視圖中心和faraday soa 服務,具體以下圖所示:

 

 

視圖配置中心:

 

主要提供給調用方人員配置計數器模型參數。

 

例如:計數器類型,是否持久化,存活週期等等。

 

配置完參數,調用方無須關注計數器的具體實現細節,內部的操做。

 

流程對於調用方來講是個黑匣子。

 

faraday 服務:

 

主要有三大模塊組成,分別是:

 

  • 計數器模型配置器。主要解析調用方的模型參數,並從配置視圖中心獲取計數器模型配置信息。

  • 計數器邏輯執行器。負責各類計數器的邏輯操做,例如:count、sum、max、min等。

  • 異步引擎。做用有2個方面,第一是異步化入庫,防止操做數據庫,致使接口性能下降,第二是化並行爲串行,下降高併發帶來的數據不一致問題。

 

faraday 總體流程圖以下:

 

 

計數器類型

 

風控主要使用的計數器類型是count、sum、max、min、top,爲了應對天天近800萬的訂單量,風控使用redis進行計數服務,主要是看中了redis不錯的單機性能。

 

count和sum可使用redis的incr 就能夠辦到。對於top類型的計數器,可使用redis的sorted set,利用sorted set的score進行排序。

 

計數器中的max和min,計算的是最大值和最小值,最大值和最小值在高併發存儲的時候會有一個問題,就以max爲例講解,如圖:

 

 

在併發的時候,當a和b同時讀到m的值是8,此時a=9,比m大,知足修改m的條件,去修改m=9;另外b=10,也知足修改m的條件,此時b也去修改m=10;由於修改的順序不一樣,有可能最終m=9,與咱們的預期值10不一致。

 

那麼有沒有什麼辦法完全解決這種場景呢,答案確定是有的,就是利用mq,將消息發給mq,而後部署一臺機器,去單點消費這個mq,再去比較m的值,就不會存在同時修改m值的問題。 在實際生產環境中,部署單個機器消費mq,確定是行不通的,由於這樣不知足可用性,並且單點消費還會出現服務掛掉,不消費mq,從而致使mq消息堆積等一系列問題。

 

在faraday中其實採用了一個折中方案,就是將消息發送給local queue,在local queue中先獲取m的值做比較,而後利用redis中的getset方法,再去比較一次,這樣能夠大大下降高併發帶來的賦值不一致問題,具體流程如圖所示:

 

 

時間窗口設計

 

計數器設計中有一大難點是時間窗口的設計。當初想到的方案有2種:

 

第一種是每隔xx時間,例如:每隔1天,每隔3小時,每隔5分鐘。若是計數器選擇的類型是每隔1小時,就將一天劃分紅24個1小時;若是是每隔2小時,就將一天劃分紅12個2小時;這種時間劃分有一個缺點,若是是每隔5小時這種的,是沒有辦法整除的。因此這種時間窗口方案被咱們拋棄了。

 

第二種是最近xx時間,例如:最近1天,最近3小時,最近5分鐘。咱們仍是以最近xx小時舉例,無論咱們設置的是最近3小時,仍是最近5小時,咱們在redis中都是以小時帳的形式進行存儲,獲取的時候,只要將前幾小時的小時帳進行合併就能夠了,那麼同理若是是天,就是日帳,月就是月帳。如圖所示:

 

 

對於這種時間窗口有一個弊端,若是滑動時間窗口粒度很長,計算的複雜度就會越高,例如 統計最近10小時的計數器,就須要累加10個小時帳;爲了應對這種粒度很長的時間窗口,咱們提出了中間值的概念,將歷史的時間窗口計數器計算成中間值,那麼不管滑動窗口多長,只須要計算一次,即: 計數器 = 當前時間窗口值 + 中間值

 

最終風控採用的時間窗口是第二種方案。

 

計數器key的設計

 

faraday 中key主要由 編號+主體+客體+統計方式+時間戳 組成,因爲Redis是純內存的,因此成本也不算低。爲了下降成本,咱們須要縮減key的長度,首先去掉了一些沒必要要的前綴,這些前綴加起來是33個byte,若是以10億個計數器計算,去掉這些前綴,能夠節省近31G的內存空間。另外對於時間戳,咱們是採用yyyyMMdd 這種字符串形式存儲,第一比較容易閱讀,第二相比timestamp存儲的字節更少。

 

faraday的接入步驟

 

視圖配置

 

計數器的配置主要是由計數器的使用者自助完成。計數器在後臺配置如圖所示:

 

 

調用faraday服務

 

調用方只須要配置幾行代碼就能夠完成計數器服務調用,僞代碼以下:

 

 

新計數器的優點

 

最後新計數器的優點也很明顯,主要體如今如下幾點。

 

第一:避免重複勞動

 

將分離在各個系統的計數器邏輯,抽象成一個服務,避免了重複勞動,調用方系統不須要關心計數器邏輯的實現細節,而key的生成規則對調用方系統是透明的,調用方只須要傳幾個參數就能夠獲取和設置計數器的值。

 

第二:快速知足風控業務需求

 

新計數器能夠在後臺系統動態配置,計數器的特性能夠在線動態修改和生效,避免了每次更改都要走上線流程的繁瑣步驟。

 

第三:對計數器有更強的把控能力

 

新技術器是一個服務,一個系統,讓專業的系統去作專業的事情,更有利於職責分離,當計數器出現問題的時候,更有利於排查問題,而不是像以前那樣去check各個系統。

相關文章
相關標籤/搜索