作者:vivo 網際網路平臺產品研發團隊- Shi Jianhua、Sun Song
帳號是一個核心的基礎服務,對於基礎服務而言穩定性就是生命線。在這篇文章中,將與大家分享我們在帳號穩定性建設方面的經驗和探索。
vivo帳號是使用者暢享整個vivo生態服務的必備通行證,也是生態內各業務開展的基石。伴隨公司業務快速增長,帳號系統目前服務的在網使用者已達到2.7億,日均呼叫量破百億,作為一個典型的三高(高效能、高並行、高可用)屬性的系統,帳號系統的穩定性顯得尤為重要。而要保障系統的穩定性,我們需要綜合考慮多方面因素。本文將從應用服務、資料架構、監控三個維度出發,分享帳號伺服器端在穩定性建設方面的經驗總結。
《架構整潔之道》書中將軟體的價值總結為「行為」、「架構」兩個維度。
行為價值:讓機器按照某種指定方式運轉,給系統的使用者創造或提高利潤。
架構價值:始終保持軟體的靈活性,以便讓我們可以靈活地改變機器的工作行為。
行為價值描述的是當下,對於使用者最直觀的感受就是易用性、功能豐富程度等。好的行為價值能夠吸參照戶,進而對服務提供者能有一個正向回報。
架構價值描述的是未來,指服務系統的內在結構、技術體系、穩定性等,這些價值雖然對使用者是不可見的,但它決定了服務的延續性。
應用服務的治理目的是讓系統保持「架構價值」,進而延續「行為價值」,我們在「服務治理」章節將重點介紹兩點內容:「服務拆分」、「關係治理」。
服務拆分是指將一個服務拆分為多個小型、相對獨立的微服務。服務拆分有非常多的收益,包括提高系統的可延伸性、可維護性、穩定性等等。下面將介紹我們在系統建設過程中遇到的拆分場景。
康威定律 ( Conway's Law) 由馬爾文·康威於1967年提出:"設計系統的架構受制於產生這些設計的組織的溝通結構。"。即系統設計本質上反映了企業的組織結構,系統各個模組間的關係也反映了企業各個部門之間的資訊流動和合作方式,內容示意如下圖(圖1):
圖1 (圖片來源:WORK LIFE)
組織架構調整是企業發展過程中常常需要面對的重要挑戰,其原因通常與市場需求、業務變化、協同效率相關。如果不及時跟進服務拆分,跨團隊共同作業不暢、溝通困難等問題就會接踵而來。本質上,這些問題都源於團隊分工和核心目標的差異。
案例介紹
vivo在網際網路早期就開展了遊戲聯運業務,遊戲聯運全稱是遊戲聯合運營,具體指的是遊戲研發廠商以合作分成的方式將產品嫁接到vivo平臺上運營。起初vivo網際網路團隊規模較小,和帳號相關的業務統一歸屬於現在的系統帳號團隊。在遊戲聯運業務中,我們提供為不同的遊戲建立對應的子帳號(即遊戲小號)的服務,子帳號下包括遊戲角色等相關資訊。
隨著遊戲業務快速發展,遊戲事業部成立,其核心目標是服務好遊戲使用者。而系統帳號的目標,則是要從整個vivo生態出發,為我們的手機使用者,提供簡單、安全的使用體驗。在組織架構變動後不久,兩個團隊便快速達成了業務邊界共識,並完成了對應服務的拆分。
圖2(遊戲小號拆分)
針對組織架構調整導致的服務拆分,屬於外因,其內容範圍和時間點相對容易確定。而基於對穩定性的考慮進行的拆分,屬於內因,則需要在恰當的時機進行,以避免對業務正常版本迭代造成影響。在實踐過程中,拆分策略上我們更多是基於核心流程的拆分。
(1)核心行為拆分
一個業務系統中,都會存在核心流程。核心流程承擔了系統中核心的工作。以帳號為例:註冊、登入、憑證校驗,毫無疑問就是系統中核心的流程,我們將核心流程獨立拆分,主要為了下面兩個目標達成:
服務隔離
避免不同流程之間的相互影響。以帳號憑證校驗流程為例,驗證邏輯固定,架構上只依賴分散式快取。一旦和其它流程耦合,除了帶來更多外部依賴風險外,其它流程修改、發版同樣會影響到憑證校驗流程的穩定性。
資源隔離
服務拆分使得伺服器資源得以隔離,這種隔離為橫向資源擴容提供了更加靈活的可能性。例如,對於核心流程服務,資源可以做適當冗餘,動態擴縮容的策略可以客製化等。
如何識別核心行為?
有些核心流程是顯而易見的,比如帳號中的註冊和登入,但有些流程需要進行識別和判斷。我們的實踐是根據「業務價值」和「呼叫頻度」這兩個維度進行判斷,其中「業務價值」可以選擇與核心業務指標相關聯的流程,而「呼叫頻度」則對應流程的執行次數。將這兩個維度疊加,我們可以得到一個四象限矩陣圖。下圖是帳號業務的矩陣示意圖(圖3)。最核心的流程位於圖中右上角(價值高、呼叫高),這裡有個原則,位於對角線的流程要儘可能的相互隔離;
圖3(矩陣圖)
(2)最少要素聚合
服務並非拆分得越細越好,過於細緻的拆分會導致服務數量過多,反而增加了系統的複雜度和維護成本。為了避免過度拆分,我們可以對流程中依賴的業務要素進行分析,並適當進行流程間的聚合。以註冊為例,流程最簡化的情況下,只需圍繞帳號四要素(使用者名稱、密碼、郵箱、手機號)完成即可。而對於換綁手機號流程,它依賴於密碼或原手機號的驗證(四要素中的其中兩項)。因此,我們可以將註冊和手機號換綁這兩個流程合併到同一個服務中,以降低維護成本。
圖4(最小要素閉環)
(3)整體拆分示意
早期的帳號主服務包含了帳號登入、註冊、憑證校驗、使用者資料查詢/修改等流程。如果需要對服務進行拆分,我們應該首先梳理核心流程。按照上面圖4的示意,我們應該先完成登入、註冊、憑證校驗與使用者資料的拆分。使用者資料主要包含暱稱、頭像等擴充套件資訊,不包括帳號主體的四個要素(使用者名稱、密碼、郵箱、手機號)。
對於登入、註冊、憑證校驗這三個行為,隨著已註冊使用者數量的增加,登入和憑證校驗的頻度遠遠超過註冊。因此,我們進行了二次拆分,將登入和憑證校驗拆分為一個服務,將註冊拆分為另一個服務。拆分後的結構如下圖所示(圖5)。
圖5
(4)業務價值變化
業務價值是動態變化的,因此我們需要根據業務的變化來適時地調整服務拆分的結構。實踐案例有帳號資訊服務中實名模組的拆分。早期實名資訊只是用在評論場景中,因此其價值和暱稱、頭像等資訊區別不大。但隨著遊戲業務深度開展以及國家防沉迷的要求,如果使用者未實名認證,則無法提供相關服務。實名資訊對於遊戲業務的重要性等同於憑證校驗。因此,我們將實名模組拆分為獨立的服務,以便更好地支援業務的發展和變化。
在對成熟業務進行服務拆分時,穩定性是關鍵。必須確保對業務沒有任何影響,並且使用者無感知。為了降低拆分實施的難度,我們會採取先拆服務(圖6),再拆資料的方案。在服務拆分時,為了進一步降低風險,可以考慮下面兩點做法:
服務拆分階段,只做程式碼遷移,不做程式碼重構
引入灰度能力,通過可控的流量進行梯度驗證
圖6
需要再次強調灰度的重要性,用可控的流量去驗證拆分後的服務。這邊介紹兩種灰度實現思路:
在應用層中做轉發,具體處理細節:為新服務申請一個內網域名,在原有服務內進行攔截實現請求轉發的邏輯。
在架構的更加前置的環節,完成流量分配。例如:在入口閘道器層或反向代理層(如Nginx)進行流量轉發設定。
服務之間的依賴關係對於服務架構來說是至關重要的。為了使服務間的依賴關係清晰、明確,我們可以採用以下幾個優化措施:首先服務之間的依賴關係應該是層次化的。每個服務應該處於一個特定的層次,依賴關係應該是層次化的,避免跨層級的依賴關係。其次依賴應該是單向的,要符合ADP(Acyclic Dependencies Principle)無依賴環原則。
ADP(Acyclic Dependencies Principle)無依賴環原則,下圖(圖7)中紅色線標識出來的依賴關係都是違背了ADP原則的存在。這種關係會影響「部署獨立」的目標達成。試想下A、B服務互相依賴的場景,一次需求需同時對A、B相互依賴的介面改造,發版順序應該是被依賴的先部署,相互依賴就進入了死迴圈。
圖7
在服務架構中,服務之間的關係可以根據依賴的強度分為弱依賴和強依賴。當A服務依賴於B服務時,如果B服務異常故障時,不會影響A服務的業務流程,那麼這種依賴關係被稱為弱依賴;反之,如果B服務出現故障會導致A服務無法正常工作,那麼這種依賴關係被稱為強依賴。
(1)強依賴冗餘
針對強依賴的關係,我們會採用冗餘的策略,去保障核心服務流程的穩定性。在帳號系統中,「一鍵登入」、「實名認證」都採用了同樣的方案。這種方案的實施前提是要能找到提供相同能力的多個服務,其次服務本身需要做一些適配工作,如下圖(圖8)增加流量分配處理模組,作用是監控依賴服務的質量,動態調整流量分配比例等。
圖8
除了採用動態流量分配的實現,還可以選擇相對簡單的主次方案,即固定依賴其中一個服務,當該服務出現異常或熔斷時,再依賴另一個服務。這種主次方案可以在一定程度上提高服務的可用性,同時也相對簡單易行。
(2)弱依賴非同步
非同步常用方案是依賴獨立的訊息元件(圖9),把原本同步呼叫的處理改為訊息傳送。這樣做除了能實現依賴關係的解耦,同時能增加系統吞吐量。回顧ADP原則中我們提到的迴圈依賴,是可以通過訊息元件進行解耦規避的。
圖9
需要提醒的是使用訊息元件會增加系統的複雜性,非同步天生要比同步更復雜,需要額外考慮訊息亂序、延遲、丟失等問題。針對這些問題可以嘗試下面方案:不在服務流程中直接傳送訊息,而是依賴服務流程產生的資料,進行訊息生產,如下圖(圖10)。帳號系統中使用場景有帳號註冊、登出後的業務通知。
圖10
選擇kafka元件是可以提供訊息的有序性的特徵。方案中從binlog採集、到推播訊息,可以理解成是一個資料傳輸服務(Data Transmission Service,簡稱DTS),在vivo內部有自研的「魯班平臺」實現了DTS能力,對於讀者朋友可以藉助類似開源的Canal專案達成同樣的效果。
在高並行的系統架構中,快取是提升系統效能最有效的方式之一。快取可以分為本地快取和分散式快取兩種。在帳號系統中,為了應對不同的場景,我們採用了本地快取和分散式快取結合的方式。
本地快取就是將資料快取到服務本地記憶體中,好處是響應時間快、不受跨程序通訊等外部因素影響。但弊端也非常多,受服務記憶體大小的限制,以及多節點的一致性問題等,在帳號中使用的場景是快取相對固定不變的資料。
分散式快取能有效規避服務記憶體大小限制等問題,同時提供了相對資料庫更好的讀寫效能。但是引入分散式快取同樣會帶來額外問題,其中最突出的就是資料一致性問題。
(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消耗。
資料庫相對於應用服務,在高並行系統更容易成為系統的瓶頸。它無法做到和應用一樣便利的橫向擴容,所以資料庫的規劃工作一定要打提前量。
帳號業務特點是讀多寫少,所以最早遇到的壓力是資料庫讀的壓力,而讀寫分離架構(圖13)可以有效降低主庫的負載。讀寫分離方案中由主庫承擔全部寫流量,從庫和主庫共同承擔讀流量。從庫同時可以設定多個,通過多個從庫來分擔高並行的查詢流量
圖13
保留主庫的讀能力,是因為 「主從同步延遲」 問題存在,對不能接受資料延遲的場景繼續查詢主庫。 讀寫分離方案的好處是簡單,幾乎沒有程式碼改造成本,只需要新增資料庫的主從關係。缺點也比較多,比如無法解決TPS(寫) 高的問題,從庫也不能無節制新增,從庫數量過多會加重延遲問題。
讀寫分離肯定是解決不了所有的問題,一些場景需要結合分表分庫的方案。分表分庫的方案分為垂直拆分和水平拆分兩種,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
監控治理的目的,是讓我們實時瞭解系統狀況,及時進行故障的預警,並能輔助快速的問題定位。早期帳號就經歷過,告警內容不全面,研發不能及時收到告警。有時收到了告警,但因為原因指向不明,告警問題排查困難,處理時間過長等。隨著持續治理,經過多次線上的驗證,我們能做到問題感知靈敏,處理迅速。
我們把監控的內容歸納為三個維度(圖18),從上到下分別是:
上層的應用服務監控:監控應用層的狀況,例如:服務存取的吞吐量、返回碼(失敗量)、響應時間、業務異常等;
中層獨立元件監控:獨立元件涵蓋服務執行的中介軟體,例如:Redis(快取)、MQ(訊息)、MySQL(儲存)、Tomcat(容器)、JVM 等;
底層系統資源監控:監控主機和底層資源,例如:CPU、記憶體、硬碟 I/O、網路吞吐等;
監控內容涵蓋三層的原因,如果你只關注應用服務,如果問題發生,你只是知道了一個結果,無法進行快速定位分析,只能根據經驗排查各項的可能性,這樣的故障處理速度是沒辦法忍受的。而往往上層的應用的告警,可能就是一些元件或則底層系統資源的異常引起的。假設我們遇到服務響應時長告警時,如果這時候有對應JVM FGC 時長告警、或myql的慢查sql告警,這就很方便我們快速的明確優先排查的方向,確定後續的處理措施。
圖18
元件監控、底層資源監控除了有支撐定位問題的作用外,另一個目的是可以提前排除隱患。很多隱患一開始對應用服務影響比較有限,但這種影響會隨著呼叫量等外部因數變化慢慢放大。
監控內容的維護,三個維度的監控內容中,底層系統資源和中層獨立元件,內容相對固定,不需要經常維護。而上層的應用服務監控中涉及業務異常的,就需要隨著功能版本迭代,不停的做加減法。
三個維度監控的內容,因為公司內分工的存在,研發、應用運維、系統運維,容易出現各管各的,監控指標也可能會分散在不同系統,這樣是非常不利於問題定位分析。最好的監控系統是能將這三個維度的指標進行打通,這樣問題分析處理會更加高效。下面是我們在跟蹤「偶發性dubbo服務執行緒滿」問題時的經歷。偶發性問題排查的難點,不能拿一次的分析結果定論。藉助公司業務監控系統的幫助,我們排除了redis等中間元件的影響後,我們就開始將關注點放在了主機指標上,為了方便問題定位,我們自己做了 虛擬機器器反推 宿主物理 再到宿主機上所有虛擬機器器的關鍵指標(CPU、IO、NET)聚合,效果如下(圖19)。經過多次驗證後確定了宿主機上個別應用磁碟IO異常過高導致。
圖19
應用服務監控中,都會將服務介面呼叫量TOP N作為重點監控物件。但在中臺服務中,只到介面的顆粒度還不夠,需要細化到能區分呼叫方的維度,去監控具體某個介面上TOP N的呼叫方增長趨勢。這樣做的好處,一是監控的粒度越細,越能提前感知到風險。二是一旦確認是不合理流量時,也可以有針對性地做流控等處理。
本文從服務拆分、關係治理、快取、資料庫、監控治理幾個維度,介紹了帳號系統在穩定性建設方面做的一些經驗總結。然而,僅僅做到這些是遠遠不夠的。穩定性建設需要一套嚴謹科學的工程管理體系,涉及內容不僅包括研發的設計、開發和維護,還應該包含專案團隊中各個角色的工作內容。總而言之,穩定性建設需要在整個專案生命週期中不斷進行細緻的規劃和實踐。我們也希望本文所述的經驗和思路,能夠對讀者在實踐中起到一定的指導作用。
參考文獻: