vivo 帳號服務穩定性建設之路-平臺產品系列06
一、前言
vivo帳號是用戶暢享整個vivo生態服務的必備通行證,也是生態內各業務開展的基石。伴隨公司業務快速增長,帳號系統目前服務的在網用戶已達到2.7億,日均調用量破百億,作為一個典型的三高(高性能、高并發、高可用)屬性的系統,帳號系統的穩定性顯得尤為重要。而要保障系統的穩定性,我們需要綜合考慮多方面因素。本文將從應用服務、數據架構、監控三個維度出發,分享帳號服務端在穩定性建設方面的經驗總結。
二、應用服務治理
《架構整潔之道》書中將軟件的價值總結為“行為”、“架構”兩個維度。
行為價值:讓機器按照某種指定方式運轉,給系統的使用者創造或提高利潤。
架構價值:始終保持軟件的靈活性,以便讓我們可以靈活地改變機器的工作行為。
行為價值描述的是當下,對于用戶最直觀的感受就是易用性、功能豐富程度等。好的行為價值能夠吸引用戶,進而對服務提供者能有一個正向回報。
架構價值描述的是未來,指服務系統的內在結構、技術體系、穩定性等,這些價值雖然對用戶是不可見的,但它決定了服務的延續性。
應用服務的治理目的是讓系統保持“架構價值”,進而延續“行為價值”,我們在“服務治理”章節將重點介紹兩點內容:“服務拆分”、“關系治理”。
2.1 服務拆分
服務拆分是指將一個服務拆分為多個小型、相對獨立的微服務。服務拆分有非常多的收益,包括提高系統的可擴展性、可維護性、穩定性等等。下面將介紹我們在系統建設過程中遇到的拆分場景。
2.1.1 基于組織架構調整拆分
康威定律 ( Conway's Law) 由馬爾文·康威于1967年提出:"設計系統的架構受制于產生這些設計的組織的溝通結構。"。即系統設計本質上反映了企業的組織結構,系統各個模塊間的關系也反映了企業各個部門之間的信息流動和合作方式,內容示意如下圖(圖1):
圖1 (圖片來源:WORK LIFE)
組織架構調整是企業發展過程中常常需要面對的重要挑戰,其原因通常與市場需求、業務變化、協同效率相關。如果不及時跟進服務拆分,跨團隊協作不暢、溝通困難等問題就會接踵而來。本質上,這些問題都源于團隊分工和核心目標的差異。
案例介紹
vivo在互聯網早期就開展了游戲聯運業務,游戲聯運全稱是游戲聯合運營,具體指的是游戲研發廠商以合作分成的方式將產品嫁接到vivo平臺上運營。起初vivo互聯網團隊規模較小,和帳號相關的業務統一歸屬于現在的系統帳號團隊。在游戲聯運業務中,我們提供為不同的游戲創建對應的子帳號(即游戲小號)的服務,子帳號下包括游戲角色等相關信息。
隨著游戲業務快速發展,游戲事業部成立,其核心目標是服務好游戲用戶。而系統帳號的目標,則是要從整個vivo生態出發,為我們的手機用戶,提供簡單、安全的使用體驗。在組織架構變動后不久,兩個團隊便快速達成了業務邊界共識,并完成了對應服務的拆分。
圖2(游戲小號拆分)
2.1.2 基于穩定性述求拆分
針對組織架構調整導致的服務拆分,屬于外因,其內容范圍和時間點相對容易確定。而基于對穩定性的考慮進行的拆分,屬于內因,則需要在恰當的時機進行,以避免對業務正常版本迭代造成影響。在實踐過程中,拆分策略上我們更多是基于核心流程的拆分。
(1)核心行為拆分
一個業務系統中,都會存在核心流程。核心流程承擔了系統中核心的工作。以帳號為例:注冊、登錄、憑證校驗,毫無疑問就是系統中核心的流程,我們將核心流程獨立拆分,主要為了下面兩個目標達成:
服務隔離
避免不同流程之間的相互影響。以帳號憑證校驗流程為例,驗證邏輯固定,架構上只依賴分布式緩存。一旦和其它流程耦合,除了帶來更多外部依賴風險外,其它流程修改、發版同樣會影響到憑證校驗流程的穩定性。
資源隔離
服務拆分使得服務器資源得以隔離,這種隔離為橫向資源擴容提供了更加靈活的可能性。例如,對于核心流程服務,資源可以做適當冗余,動態擴縮容的策略可以定制等。
如何識別核心行為?
有些核心流程是顯而易見的,比如帳號中的注冊和登錄,但有些流程需要進行識別和判斷。我們的實踐是根據“業務價值”和“調用頻度”這兩個維度進行判斷,其中“業務價值”可以選擇與核心業務指標相關聯的流程,而“調用頻度”則對應流程的執行次數。將這兩個維度疊加,我們可以得到一個四象限矩陣圖。下圖是帳號業務的矩陣示意圖(圖3)。最核心的流程位于圖中右上角(價值高、調用高),這里有個原則,位于對角線的流程要盡可能的相互隔離;
圖3(矩陣圖)
(2)最少要素聚合
服務并非拆分得越細越好,過于細致的拆分會導致服務數量過多,反而增加了系統的復雜度和維護成本。為了避免過度拆分,我們可以對流程中依賴的業務要素進行分析,并適當進行流程間的聚合。以注冊為例,流程最簡化的情況下,只需圍繞帳號四要素(用戶名、密碼、郵箱、手機號)完成即可。而對于換綁手機號流程,它依賴于密碼或原手機號的驗證(四要素中的其中兩項)。因此,我們可以將注冊和手機號換綁這兩個流程合并到同一個服務中,以降低維護成本。
圖4(最小要素閉環)
(3)整體拆分示意
早期的帳號主服務包含了帳號登錄、注冊、憑證校驗、用戶資料查詢/修改等流程。如果需要對服務進行拆分,我們應該首先梳理核心流程。按照上面圖4的示意,我們應該先完成登錄、注冊、憑證校驗與用戶資料的拆分。用戶資料主要包含昵稱、頭像等擴展信息,不包括帳號主體的四個要素(用戶名、密碼、郵箱、手機號)。
對于登錄、注冊、憑證校驗這三個行為,隨著已注冊用戶數量的增加,登錄和憑證校驗的頻度遠遠超過注冊。因此,我們進行了二次拆分,將登錄和憑證校驗拆分為一個服務,將注冊拆分為另一個服務。拆分后的結構如下圖所示(圖5)。
圖5
(4)業務價值變化
業務價值是動態變化的,因此我們需要根據業務的變化來適時地調整服務拆分的結構。實踐案例有帳號信息服務中實名模塊的拆分。早期實名信息只是用在評論場景中,因此其價值和昵稱、頭像等信息區別不大。但隨著游戲業務深度開展以及國家防沉迷的要求,如果用戶未實名認證,則無法提供相關服務。實名信息對于游戲業務的重要性等同于憑證校驗。因此,我們將實名模塊拆分為獨立的服務,以便更好地支持業務的發展和變化。
2.1.3 拆分實施方案
在對成熟業務進行服務拆分時,穩定性是關鍵。必須確保對業務沒有任何影響,并且用戶無感知。為了降低拆分實施的難度,我們會采取先拆服務(圖6),再拆數據的方案。在服務拆分時,為了進一步降低風險,可以考慮下面兩點做法:
- 服務拆分階段,只做代碼遷移,不做代碼重構
- 引入灰度能力,通過可控的流量進行梯度驗證
圖6
需要再次強調灰度的重要性,用可控的流量去驗證拆分后的服務。這邊介紹兩種灰度實現思路:
- 在應用層中做轉發,具體處理細節:為新服務申請一個內網域名,在原有服務內進行攔截實現請求轉發的邏輯。
- 在架構的更加前置的環節,完成流量分配。例如:在入口網關層或反向代理層(如Nginx)進行流量轉發配置。
2.2 關系治理
服務之間的依賴關系對于服務架構來說是至關重要的。為了使服務間的依賴關系清晰、明確,我們可以采用以下幾個優化措施:首先服務之間的依賴關系應該是層次化的。每個服務應該處于一個特定的層次,依賴關系應該是層次化的,避免跨層級的依賴關系。其次依賴應該是單向的,要符合ADP(Acyclic Dependencies Principle)無依賴環原則。
2.2.1 ADP原則
ADP(Acyclic Dependencies Principle)無依賴環原則,下圖(圖7)中紅色線標識出來的依賴關系都是違背了ADP原則的存在。這種關系會影響“部署獨立”的目標達成。試想下A、B服務互相依賴的場景,一次需求需同時對A、B相互依賴的接口改造,發版順序應該是被依賴的先部署,相互依賴就進入了死循環。
圖7
2.2.2 關系處理
在服務架構中,服務之間的關系可以根據依賴的強度分為弱依賴和強依賴。當A服務依賴于B服務時,如果B服務異常故障時,不會影響A服務的業務流程,那么這種依賴關系被稱為弱依賴;反之,如果B服務出現故障會導致A服務無法正常工作,那么這種依賴關系被稱為強依賴。
(1)強依賴冗余
針對強依賴的關系,我們會采用冗余的策略,去保障核心服務流程的穩定性。在帳號系統中,“一鍵登錄”、“實名認證”都采用了同樣的方案。這種方案的實施前提是要能找到提供相同能力的多個服務,其次服務本身需要做一些適配工作,如下圖(圖8)增加流量分配處理模塊,作用是監控依賴服務的質量,動態調整流量分配比例等。
圖8
除了采用動態流量分配的實現,還可以選擇相對簡單的主次方案,即固定依賴其中一個服務,當該服務出現異常或熔斷時,再依賴另一個服務。這種主次方案可以在一定程度上提高服務的可用性,同時也相對簡單易行。
(2)弱依賴異步
異步常用方案是依賴獨立的消息組件(圖9),把原本同步調用的處理改為消息發送。這樣做除了能實現依賴關系的解耦,同時能增加系統吞吐量。回顧ADP原則中我們提到的循環依賴,是可以通過消息組件進行解耦規避的。
圖9
需要提醒的是使用消息組件會增加系統的復雜性,異步天生要比同步更復雜,需要額外考慮消息亂序、延遲、丟失等問題。針對這些問題可以嘗試下面方案:不在服務流程中直接發送消息,而是依賴服務流程產生的數據,進行消息生產,如下圖(圖10)。帳號系統中使用場景有帳號注冊、注銷后的業務通知。
圖10
選擇kafka組件是可以提供消息的有序性的特征。方案中從binlog采集、到推送消息,可以理解成是一個數據傳輸服務(Data Transmission Service,簡稱DTS),在vivo內部有自研的“魯班平臺”實現了DTS能力,對于讀者朋友可以借助類似開源的Canal項目達成同樣的效果。
三、數據架構治理
3.1 緩存
在高并發的系統架構中,緩存是提升系統性能最有效的方式之一。緩存可以分為本地緩存和分布式緩存兩種。在帳號系統中,為了應對不同的場景,我們采用了本地緩存和分布式緩存結合的方式。
3.1.1 本地緩存
本地緩存就是將數據緩存到服務本地內存中,好處是響應時間快、不受跨進程通信等外部因素影響。但弊端也非常多,受服務內存大小的限制,以及多節點的一致性問題等,在帳號中使用的場景是緩存相對固定不變的數據。
3.1.2 分布式緩存
分布式緩存能有效規避服務內存大小限制等問題,同時提供了相對數據庫更好的讀寫性能。但是引入分布式緩存同樣會帶來額外問題,其中最突出的就是數據一致性問題。
(1)數據一致性
處理數據一致性的方案有很多選擇,根據帳號使用的業務場景,我們選擇的方案是:Cache Aside Pattern。Cache Aside Pattern 具體邏輯如下:
- 數據查詢:從緩存取,命中直接返回,未命中則從數據庫取并設置到緩存。
- 數據更新:先更新數據到數據庫,后直接刪除緩存。
圖11 (Cache Aside Pattern示意圖)
處理的核心要點是數據更新時直接刪除緩存,而不是刷新緩存。這是為了規避,并發修改可能導致的數據不一致。當然Cache Aside Pattern是不能杜絕一致性問題。
主要是下面兩種場景:
第一種情況刪除緩存異常。這種要么可以嘗試重試,或直接依賴設定合理的過期時間來降低影響。
第二種情況是理論上的可能性,概率非常低。
一個讀操作,沒有命中緩存,到數據庫中取數據,此時來了一個寫操作,寫完數據庫后刪除了緩存,然后之前的讀再把老的數據寫入緩存。說它理論上存在是因為條件過于苛刻,首先需要發生在讀緩存時緩存失效,而且并發一個寫操作。然后我們知道數據庫的寫操作通常會比讀操作慢得多,而發生問題是要求讀操作必需在寫操作前進入數據庫操作,而又要晚于寫操作更新緩存,所以說它只是理論上的可能性。
基于上述情況綜合考慮,我們選擇的是Cache Aside Pattern方案,盡可能去降低并發臟數據發生的概率,而非通過復雜度更高的2PC或是Paxos協議保證強一致性。
(2)批量讀操作優化
盡管使用緩存可以顯著提升系統的性能,但并不能解決所有的性能問題。在帳號服務中,我們提供了用戶資料查詢能力,根據用戶標識獲取用戶的昵稱、頭像、簽名等信息。為了提高接口的性能,我們將相關信息緩存在Redis中。然而,隨著用戶量和調用量的快速增長,以及批量查詢的新增需求,Redis的容量和服務接口的性能都面臨著壓力。
為了解決這些問題,我們采取了一系列有針對性的優化措施:
首先,我們在將緩存數據寫入Redis前,先對其進行壓縮。這樣可以減小緩存數據的大小,從而降低了數據在網絡傳輸和存儲過程中的開銷。
接著,我們更換默認的序列化方式,選擇了protostuff作為替代方案。protostuff是一種高效的序列化框架,相比其他序列化框架具有以下優勢:
- 高性能:protostuff采用了零拷貝技術,直接將對象序列化為字節數組,避免了中間對象的創建和拷貝,從而大幅度提高了序列化和反序列化的性能。
- 空間效率:由于采用了緊湊的二進制格式,protostuff可以將對象序列化為更小的字節數組,從而節省了存儲空間。
- 易用性:protostuff是基于protobuf開發,但對Java語言的支持更加完善,只需要定義好Java對象的結構和注解,就可以進行序列化和反序列化操作。
序列化的方案還有很多,例如thrift等,關于它們的性能對比,可以參考下圖(圖12),讀者可以自己項目實際情況進行選擇。
圖12(圖片來源:Google Code)
最后,是Redis Pipeline命令的應用。Pipeline可以將多個Redis命令打包成一個請求,一次性發送給Redis服務器,從而減少了網絡延遲和服務器負載。Redis Pipeline的主要作用是提高Redis的吞吐量和降低延遲,尤其是在需要執行大量相同Redis命令的情況下,效果更加明顯。
以上優化最終給我們帶來了一半的Redis容量的節省和5倍左右的性能提升,但同時也增加了大概10%的額外CPU消耗。
3.2 數據庫
數據庫相對于應用服務,在高并發系統更容易成為系統的瓶頸。它無法做到和應用一樣便利的橫向擴容,所以數據庫的規劃工作一定要打提前量。
3.2.1 讀寫分離
帳號業務特點是讀多寫少,所以最早遇到的壓力是數據庫讀的壓力,而讀寫分離架構(圖13)可以有效降低主庫的負載。讀寫分離方案中由主庫承擔全部寫流量,從庫和主庫共同承擔讀流量。從庫同時可以配置多個,通過多個從庫來分擔高并發的查詢流量
圖13
保留主庫的讀能力,是因為 “主從同步延遲” 問題存在,對不能接受數據延遲的場景繼續查詢主庫。 讀寫分離方案的好處是簡單,幾乎沒有代碼改造成本,只需要新增數據庫的主從關系。缺點也比較多,比如無法解決TPS(寫) 高的問題,從庫也不能無節制添加,從庫數量過多會加重延遲問題。
3.2.2 分表分庫
讀寫分離肯定是解決不了所有的問題,一些場景需要結合分表分庫的方案。分表分庫的方案分為垂直拆分和水平拆分兩種,vivo互聯網技術公眾號有過分庫分表方案的詳解,這邊不在贅述,有興趣的可以前往閱讀 詳談水平分庫分表 。在這邊和大家聊聊分表分庫動機及一些輔助決策的經驗總結。
(1)分表解決什么問題
籠統的回答就是解決大表帶來的性能問題。具體影響在哪里?怎么判斷是不是要分表?
① 查詢效率
大表最直接給人的感受是會影響查詢效率,我們以 mysql-InnoDB為例分析下具體影響。InnoDB存儲引擎是以B+Tree結構組織索引,以主鍵索引(聚簇索引)為例,它的特性是葉子節點存放完整數據,非葉子節點存放鍵值+頁地址指針。這邊的節點,對應到存儲就是數據頁的概念。數據頁是InnoDB最小存儲單元,默認大小為16k。一個聚簇索引的示意圖(圖14)如下:
圖14
聚簇索引樹上做數據的查詢操作,是從根節點出發,節點內做二分查找來確定樹下一層的數據頁位子,到達葉子節點后同樣通過二分查找來定位數據。從這個查找過程,我們可以看出對查詢的影響,主要取決于索引樹的高度。多一個層高,會多出一次數據頁的load(內存不存在發生)和一次數據頁內的二分查找。
想評估數據量對查詢的影響,可以通過估算索引樹的高度和數據量的關系來達成。前面提到非葉子節點存放鍵值+頁地址指針,頁地址指針大小固定是6個字節,那么一個非葉子節點存儲量計算公式大概是 pagesize/(index size+6)。葉子節點存儲的是具體數據,存儲的數量公示可以簡化為pagesize/(data size),這樣樹的高度和數據量的關系如下:
根據公式,我們以自增BIGINT字段做主鍵,單行數據大小1k,數據頁大小為默認16K為例,3層的樹結構容納的數據量大概在兩千萬樣子。這個方式只是輔助你做估算,如果要確定真實值,是可以借助一些工具直接在數據頁中獲取。
了解了這些后再看分表方案背后的邏輯。水平拆分是主動控制表中的數據量,來達到控制樹高度的目的。而表的垂直拆分是增加葉子節點的容量,這樣相同高度的樹,可以容下更多數據。
② 表結構調整效率
業務變更偶爾會牽扯到表結構調整,例如:新增字段、調整字段大小、增加索引等等。你會發現表的數據量越大,一些DDL 的執行時間會越來越長,有些線上大表增加字段的執行時間可能會花費數天。具體哪些DDL會比較耗時呢?可以參考mysql官網關于online-ddl的操作說明(詳情),關注操作是否涉及Rebuilds Table,如果涉及,數據量越大越大越費時。
除了表結構調整、數據查詢這些影響外,數據量越大對于失誤的容錯性越差,這對于穩定性保障工作是個隱患。
基于上面的原因描述,業務中勁量把索引樹的高度控制在3層,這時候表數據量級大概在千萬級別。如果數據量增長超過這個預期后,就要評估數據表對業務的重要程度、使用場景等,然后適時進行表的拆分。
(2)分庫解決什么問題
分庫通常理解解決的是資源瓶頸的問題。單個數據庫,即使硬件再強大,它也是有連接數、磁盤空間等上限問題。分庫后就可以將不同的實例部署在不同的物理機上,突破磁盤、連接數等資源瓶頸,同時能提供更好的性能表現。
分庫的處理除了基于資源限制的考慮外,帳號中還會結合可靠性等述求,進行數據庫的拆分。這樣可以把核心模塊和非核心模塊隔離,減少之間的相互影響。目前帳號系統的拆后情況示意如下(圖15)。
圖15
拆分后的帳號主體庫,是最核心業務庫。庫里圍繞帳號四要素(用戶名、密碼、郵箱、手機號)組織數據,這樣帳號的核心流程登錄、注冊的數據依賴就不再受其他數據的干擾。這種拆分方式屬于垂直拆分,將表根據一定的規則劃入不同的庫。
(3)數據遷移實踐
分庫分表方案實施中代價最大的是數據遷移,帳號系統在垂直分庫實踐中主要利用mysql的主從復制機制來降低數據遷移的成本。先讓DBA在原有主庫上掛新的從庫,將表數據復制到新庫中。為保證數據一致性,線上切庫時分三步處理(圖16)。
- step1:禁寫主庫,確保主從數據同步一致 ;
- step2:斷開主從,新庫成為獨立主庫;
- step3:應用完成新庫路由切換(開關實現)。
圖16
這些操作在DBA的配合下,可以把對業務的影響控制在分鐘級,影響相對可控。而且整個方案代碼層面改造成本也非常小。唯一要注意的是一定要做上線前的演練。
除了上面垂直分庫的場景外,帳號還經歷過單個核心業務表數據量過億后的水平拆分,這個場景復制遷移的方案就不適用。拆分是在18年底實施的,方案借助開源的Canal實現數據遷移。整體方案如下(圖17)。
圖17
四、監控治理
監控治理的目的,是讓我們實時了解系統狀況,及時進行故障的預警,并能輔助快速的問題定位。早期帳號就經歷過,告警內容不全面,研發不能及時收到告警。有時收到了告警,但因為原因指向不明,告警問題排查困難,處理時間過長等。隨著持續治理,經過多次線上的驗證,我們能做到問題感知靈敏,處理迅速。
4.1 監控內容
我們把監控的內容歸納為三個維度(圖18),從上到下分別是:
- 上層的應用服務監控:監控應用層的狀況,例如:服務訪問的吞吐量、返回碼(失敗量)、響應時間、業務異常等;
- 中層獨立組件監控:獨立組件涵蓋服務運行的中間件,例如:Redis(緩存)、MQ(消息)、MySQL(存儲)、Tomcat(容器)、JVM 等;
- 底層系統資源監控:監控主機和底層資源,例如:CPU、內存、硬盤 I/O、網絡吞吐等;
監控內容涵蓋三層的原因,如果你只關注應用服務,如果問題發生,你只是知道了一個結果,無法進行快速定位分析,只能根據經驗排查各項的可能性,這樣的故障處理速度是沒辦法忍受的。而往往上層的應用的告警,可能就是一些組件或則底層系統資源的異常引起的。假設我們遇到服務響應時長告警時,如果這時候有對應JVM FGC 時長告警、或myql的慢查sql告警,這就很方便我們快速的明確優先排查的方向,確定后續的處理措施。
圖18
組件監控、底層資源監控除了有支撐定位問題的作用外,另一個目的是可以提前排除隱患。很多隱患一開始對應用服務影響比較有限,但這種影響會隨著調用量等外部因數變化慢慢放大。
監控內容的維護,三個維度的監控內容中,底層系統資源和中層獨立組件,內容相對固定,不需要經常維護。而上層的應用服務監控中涉及業務異常的,就需要隨著功能版本迭代,不停的做加減法。
4.2 關聯指標聚合
三個維度監控的內容,因為公司內分工的存在,研發、應用運維、系統運維,容易出現各管各的,監控指標也可能會分散在不同系統,這樣是非常不利于問題定位分析。最好的監控系統是能將這三個維度的指標進行打通,這樣問題分析處理會更加高效。下面是我們在跟蹤“偶發性dubbo服務線程滿”問題時的經歷。偶發性問題排查的難點,不能拿一次的分析結果定論。借助公司業務監控系統的幫助,我們排除了redis等中間組件的影響后,我們就開始將關注點放在了主機指標上,為了方便問題定位,我們自己做了 虛擬機反推 宿主物理 再到宿主機上所有虛擬機的關鍵指標(CPU、IO、NET)聚合,效果如下(圖19)。經過多次驗證后確定了宿主機上個別應用磁盤IO異常過高導致。
圖19
4.3 調用方區分
應用服務監控中,都會將服務接口調用量TOP N作為重點監控對象。但在中臺服務中,只到接口的顆粒度還不夠,需要細化到能區分調用方的維度,去監控具體某個接口上TOP N的調用方增長趨勢。這樣做的好處,一是監控的粒度越細,越能提前感知到風險。二是一旦確認是不合理流量時,也可以有針對性地做流控等處理。
五、總結
本文從服務拆分、關系治理、緩存、數據庫、監控治理幾個維度,介紹了帳號系統在穩定性建設方面做的一些經驗總結。然而,僅僅做到這些是遠遠不夠的。穩定性建設需要一套嚴謹科學的工程管理體系,涉及內容不僅包括研發的設計、開發和維護,還應該包含項目團隊中各個角色的工作內容。總而言之,穩定性建設需要在整個項目生命周期中不斷進行細致的規劃和實踐。我們也希望本文所述的經驗和思路,能夠對讀者在實踐中起到一定的指導作用。