2017年4月份從餓了么正式進入多活領域開始,也預示著餓了么業務開始邁入下半場,此時風控團隊面臨著嚴峻的挑戰,風控需要在事前、事中、事后進行全方位的防御。
而計數器的業務幾乎貫穿了整個風控的需求,規則根據計數器攔截用戶風險動作,運營系統需要根據計數器分析出商家、用戶的刷單行為。首先,各個系統充斥著大量重復相同的計數器代碼,其次開發團隊對于這樣重復勞動除了感覺疲憊,還有點缺乏技術含量,后這樣的開發成本與模式,并不能快速滿足風控業務的需求,此時一個通用的計數器服務迫在眉睫。
案例
首先回顧一下風控使用計數器的一個場景,讓大家了解計數器在風控的作用。
限制用戶下單數
假設每天用戶在餓了么多下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各個系統。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。