億級用戶中心的設計與實踐

用戶中心,顧名思義就是管理用戶的地方,幾乎是所有互聯網公司最爲核心的子系統之一。它的核心功能是登錄與註冊,主要功能是修改密碼、換綁手機號碼、獲取用戶信息、修改用戶信息和一些延伸服務,同時還有登錄之後生成 Token 以及校驗 Token 的功能。下面我們從幾個維度來拆解用戶中心。

一、服務架構

用戶中心既需要爲用戶提供服務,也會承擔其他業務的頻繁調用;既然需要爲用戶提供服務,它就會自帶一些業務邏輯,比如用戶在登錄過程中需要風控或短信的校驗,那麼就會存在不可用的風險。而比如獲取用戶信息的接口,則沒有那麼多的依賴,可能只需要調用數據庫或者緩存就可以。獲取用戶信息接口要求穩定,而核心的登錄註冊接口也需要穩定,但是當我們在接口層面加一些策略或者修改的時候,不希望因爲上線問題導致整個服務不可用,而且上線後,需要對整個服務功能做全量的迴歸,導致資源嚴重浪費。

因此,基於業務特性,我們可以將用戶中心拆成 3 個獨立的微服務: 網關服務,核心服務,異步消費者服務。網關服務,提供 http 服務,聚合了各種業務邏輯和服務調用,比如登錄時候需要校驗的風控或者短信;核心服務,處理簡單的業務邏輯以及數據存儲,核心服務處在調用鏈路的終端,幾乎不依賴調用其他服務,比如校驗 Token 或者獲取用戶信息,他們就只依賴於 redis 或者數據庫;而異步消費者服務,則處理並消費異步消息。下文會詳細介紹。

圖片

這樣的設計之後,當有新功能上線時,核心服務和異步消費服務幾乎不需要重新發布,只需要發佈網關服務,依賴我們核心服務的第三方非常放心,層級也非常的清晰。當然,這樣做的代價就是服務的調用鏈路變長了。由於涉及到網關和核心服務,就需要發佈兩個服務,而且要做兼容性測試。

二、接口設計

用戶中心的接口涉及到用戶的核心信息,安全性要求高;同時,承接了較多第三方的調用,可用性要求也高。因此,對用戶中心的接口做以下設計:

首先,接口可以拆分爲面向 Web 和麪向 App 的接口。Web 接口需要做到跨域情況下的單點登錄,加密、驗籤和 token 校驗的方式也同 App 端的不一樣。

其次,對核心接口做特殊處理。比如登錄接口,在邏輯和鏈路上做了一些優化。爲什麼要對這些接口做特殊處理呢?假如用戶不能登錄,用戶會非常恐慌,客訴量會立馬上來。

那怎麼做呢?一方面,我們將用戶核心信息表做簡單。用戶的信息當中會包含 userId、手機號碼、密碼、頭像、暱稱等字段,假如把用戶的這些所有信息都保存在一張表中,那麼這張表將會異常龐大,變更字段變得異常困難。因此,需要將用戶表拆分,將核心的信息保存在用戶表中,比如 userId、username、手機號碼、密碼、鹽值(隨機生成)等;而一些如性別,頭像,暱稱等信息保存在用戶資料表中。

另一方面,我們需要將登錄的核心鏈路做短,短到只依賴於讀庫。一般情況下,用戶登錄後,需要記錄用戶登錄信息,調用風控或者短信等服務。對於登錄鏈路來說,任何一個環節出現問題都有可能導致用戶無法登錄,那麼怎麼樣才能做到最短的鏈路呢?方法就是依賴的服務可自動降級。比如說反欺詐校驗出問題了,那麼它自動降級後使用它的默認策略,極端情況下只做密碼校驗,主庫掛了之後還能到從庫讀取用戶信息。

最後就是接口的安全性校驗。對 App 接口我們需要做防重放和驗籤。驗籤可能大家比較熟悉,但是對防重放這個概念可能相對陌生。防重放,顧名思義就是防止請求重複發送。用戶請求在特定時間段內只能請求一次。即使用戶請求被攻擊者挾持,在一段時間內也無法重複請求。如果攻擊者想要篡改用戶請求再發送,對不起,請求不會通過。得益於大數據的支持,結合終端,我們還可以把每個用戶行爲畫像存儲在系統中(或者調用第三方服務)。用戶發起請求後,我們的接口會根據用戶畫像對用戶進行諸如手機號碼校驗、實名認證、人臉或者活體校驗。

圖片

三、分庫分表

隨着用戶的增長,數據超過了 1 億,怎麼辦?常見的辦法就是分庫分表。我們來分析一下用戶中心常見的一些表結構:用戶信息表,第三方登錄關聯表,用戶事件表。從上述表中可以看出來,用戶相關的數據表增長相對緩慢,因爲用戶增長是有天花板的。用戶事件表的增長是呈指數級增長,因爲每個用戶登錄、變更等密碼及變更手機號碼等操作是不限次數。

因此,首先我們可以先把用戶信息表垂直切分。正如上面說的,將用戶 ID、密碼、手機號、鹽值等常見字段從用戶信息表中拆分,其他用戶相關的信息用單獨一張表。另外,把用戶事件表遷移至其他庫中。相比於水平切分,垂直切分的代價相對較少,操作起來相對簡單。用戶核心信息表由於數據量相對較少,即使是億級別的數據,利用數據庫緩存的機制,也能夠解決性能問題。

其次,我們可以利用前後臺業務的特性採用不同的方式來區別對待。對於用戶側前臺訪問:用戶通過 username/mobile 登錄或者通過 uid 來查詢用戶信息。用戶側信息的訪問通常是單條數據的查詢,我們可以通過索引多次查詢來解決一致性和高可用問題。對於運營側後臺訪問:根據年齡、性別、登錄時間段、註冊時間段等來進行查詢,基本上都是批量分頁查詢。但是由於是內部系統,查詢量低,對一致性要求低。如果用戶側和運營側的查詢採用同一個數據庫,那麼運營側的排序查詢會導致整個庫的 CPU 上升,查詢效率下降,影響到用戶側。因此,運營側使用的數據庫可以是和用戶側同樣的 MySQL 離線庫,如果想要增加運營側的查詢效率,可以採用 ES 非關係型數據庫。ES 支持分片與複製,方便水平分割和擴展,複製保證了 ES 的高可用與高吞吐,同時能夠滿足運營側的查詢需求。

最後,如果還是要水平切分來保證系統的性能,那麼我們採取什麼樣的切分方式呢?常見的方法有索引表法和基因法。索引表法的思路主要是 UID 能夠直接定位到庫,但是手機號碼或者 username 是無法直接定位到庫的,需要建立一個索引表來記錄 mobile 與 UID 或者 username 與 UID 的映射關係的方式來解決這個問題。通常這類數據比較少,可以不用分庫分表,但是相比直接查詢,多了一次數據庫查詢的同時,在新增數據的時候還多了一次映射關係的插入,事務變大。基因法的思路是我們將 username 或者 mobile 融入到 UID 中。具體做法如下:

  1. 用戶註冊時,根據用戶的手機號碼,利用函數生成 N bit 的基因 mobile_gen,使得 mobile_gen=f(mobile);

  2. 生成 M bit 全局唯一的 id,作爲用戶標識;

  3. 拼接 M 和 N,作爲 UID 賦給用戶;

  4. 根據 N bit 來取餘來插入到特定數據庫;

  5. 查找用戶數據的時候,將用戶 UID 的後 N bit 取餘來落到最終的庫中。

從上述過程中看,基因法只適用於某類經常查詢的場景,比如用手機號碼登錄,如果用戶使用 username 登錄就比較麻煩了。因此大家也可以根據自己的業務場景來選擇不同的方式水平切分。

四、Token 之柔性降級

用戶登錄之後,另一個重要的事情就是 Token 的生成與校驗。用戶的 Token 分爲兩類, 一類是 web 端登陸生成的 Token, 這個 Token 可以和 Cookie 結合, 達到單點登陸的效果,在此不細說了。另外一類就是 APP 端登錄生成的 Token。用戶在我們的 APP 輸入用戶名密碼之後,服務端會對用戶的用戶名密碼進行校驗,成功之後從系統配置中心獲取加密算法的版本以及祕鑰,並按照一定的格式排列用戶 ID,手機號、隨機碼以及過期時間,經過一系列的加密之後,生成了 Token 之後並將其存入 Redis 緩存。而 Token 的校驗就是把用戶 ID 和 Token 組合並校驗是否在 Redis 中存在。那麼假如 Redis 不可用了怎麼辦呢?這裏有一個高可用和自動降級的設計。當 Redis 不可用的時候, 服務端會生成一個特殊格式的 Token。當校驗 Token 的時候,會對 Token 的格式進行一個判斷。

圖片

假如判斷爲 Redis 不可用時生成的 Token,那麼服務端會對 Token 進行解密,而 Token 的生成是由用戶 ID,手機號、隨機碼和過期時間等數據按照特定順序排列並加密而來的, 那麼解密出來的數據中也包含了 ID,手機號碼,隨機碼和過期時間。服務端會根據獲取到的數據查詢數據庫, 比對之後告訴用戶是否登錄成功。由於內存緩存 redis 和數據庫緩存性能的差距,在 redis 不可用的情況下,降級有可能會導致數據庫無法及時響應,因此需要在降級的方法上加入限流。

圖片

五、數據安全

數據安全對用戶中心來說非常重要。敏感數據需要脫敏處理,對密碼更是要做多重的加密處理。應用雖然有自己的安全策略,但如果把黑客限制在登錄之前,那應用的安全性將得到大幅度的提升。互聯網上用戶明文數據遭到泄露的案件屢屢發生,因此各大企業對數據安全的認識也提到了前所未有的高度。而即使使用了 MD5 和 salt 的加密方式,依然可以使用彩虹表的方式來破解。那麼用戶中心對用戶信息是怎麼保存的呢?

首先,正如上文中提到的用戶密碼、手機號等登錄信息和其他的信息分離,而且在不同的數據庫中。其次,對用戶設置的密碼進行了黑名單校驗,只要符合條件的弱密碼,都會拒絕提交,因爲不管使用了什麼加密方式的弱密碼,都極其容易破解。爲什麼呢?因爲人的記性很差,大部分人總是最傾向於選擇生日,單詞等來當密碼。6 位純數字可以生成 100 萬個不同的密碼,8 位小寫字母和數字的組合大概可以生成 2.8 萬億個不同的密碼。一個規模爲 7.8 萬億的密碼庫足以覆蓋大部分用戶的密碼,對於不同的加密算法都可以擁有這樣一個密碼庫,這也就是爲什麼大部分網站都建議用戶使用 8 位以上數字加字母密碼的原因。當然,如果一方面加了鹽值,另一方面對密鑰分開保管,破解難度會指數級增加。

最後,可以用 bcrypt/scrypt 的方式來加密。bcrypt 算法是基於 Blowfish 塊密鑰算法來實現的,bcrypt 內部實現了隨機加鹽處理,使用 bcrypt 之後每次加密後的密文都不一樣,同時還會使用內存初始化 hash 過程。由於使用內存,雖然在 CPU 上運行很快,但是在 GPU 上並行運算並不快。隨着新的 FPGA 集成了大型 RAM,解決了內存密集 IO 的問題,但是破解難度依然不小。而 scrypt 算法彌補了 bcrypt 算法的不足,它將 CPU 計算與內存使用開銷都指數級提升了。bcrypt 和 scrypt 算法能夠有效抵禦彩虹表,但是安全性的提升帶來了用戶登錄性能的下降。用戶登錄註冊並不是一個高併發的接口,所以影響並不會特別大。因此在安全和性能方面需要依據業務類型和大小來做平衡,並不是所有的應用都需要使用這種加密方式來保護用戶密碼。

六、異步消費設計

此處的異步消費,就是上文提到的異步消費服務。用戶在做完登錄註冊等操作後,需要記錄用戶的操作日誌。同時,用戶註冊登錄完畢後,下游業務需要對用戶增加積分,贈送禮券等獎勵操作。這些系統如果都同步依賴於用戶中心,那麼整個用戶中心將異常龐大,鏈路非常冗長,也不符合業內的 “大系統做小 “的原則。依賴的服務不可用之後將會造成用戶無法登錄註冊。因此,用戶中心在用戶操作完之後,將用戶事件入庫後發送至 MQ,第三方業務監聽用戶事件。用戶中心和下游業務解耦,同時用戶操作事件入庫後,在 MQ 不可用或者消息丟失的時候可做補償處理。用戶的畫像數據也在很大程度上來源於此處的數據。

七、靈活多樣的監控

用戶中心涉及到用戶的登錄註冊更改密碼等核心功能,能否及時發現系統的問題成爲關鍵指標,因此對業務的監控顯得尤爲重要。需要對用戶中心重要接口的 QPS、機器的內存使用量、垃圾回收的時間、服務的調用時間等做詳細的監控。當某個接口的調用量下降的時候,監控會及時發出報警。除了這些監控之外,還有對數據庫 Binlog 的寫入,前端組件,以及基於 ZipKin 全鏈路調用時間的監控,實現從用戶發起端到結束端的全面監控,哪怕出現一點問題,監控隨時會告訴你哪裏出問題了。比如運營互動推廣註冊量下降的時候,用戶中心就會發出報警,可以及時通知業務方改正問題,挽回損失。

八、總結

本文從服務架構設計,接口設計,token 降級,數據安全和監控等方面介紹了億級用戶中心的設計,當然用戶中心的設計遠不止這些,還會包含用戶數據的分庫分表,熔斷限流,第三方登錄等,在本文中就不一一贅述。儘管本文中設計的用戶中心能夠滿足大部分公司的需求,但是還存在一些比較大的挑戰:在鑑權服務增長的情況下,如何平滑的從用戶中心剝離;監控的侵入性以及監控的粒度的完善;另外服務的安全性、可用性、性能的提升永遠都沒有盡頭,也是我們孜孜追求的目標。在未來的日子裏,希望能夠通過大家的努力,使用戶中心的技術體系更上一層樓。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/c8-dMWzOkRq9gHu8llW_2A