萬億級調用下的優雅——微信序列號生成器架構設計及演變(上)

版權聲明:本文由曾欽松原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/200數組

來源:騰雲閣 https://www.qcloud.com/community緩存

 

微信在立項之初,就已確立了利用數據版本號實現終端與後臺的數據增量同步機制,確保發消息時消息可靠送達對方手機,避免了大量潛在的家庭糾紛。時至今日,微信已經走過第五個年頭,這套同步機制仍然在消息收發、朋友圈通知、好友數據更新等須要數據同步的地方發揮着核心的做用。服務器

而在這同步機制的背後,須要一個高可用、高可靠的序列號生成器來產生同步數據用的版本號。這個序列號生成器咱們稱之爲seqsvr,目前已經發展爲一個天天萬億級調用的重量級系統,其中每次申請序列號平時調用耗時1ms,99.9%的調用耗時小於3ms,服務部署於數百臺4核CPU服務器上。本文會重點介紹seqsvr的架構核心思想,以及seqsvr隨着業務量快速上漲所作的架構演變。微信

背景

微信服務器端爲每一份須要與客戶端同步的數據(例如消息)都會賦予一個惟一的、遞增的序列號(後文稱爲sequence),做爲這份數據的版本號。在客戶端與服務器端同步的時候,客戶端會帶上已經同步下去數據的最大版本號,後臺會根據客戶端最大版本號與服務器端的最大版本號,計算出須要同步的增量數據,返回給客戶端。這樣不只保證了客戶端與服務器端的數據同步的可靠性,同時也大幅減小了同步時的冗餘數據。網絡

這裏不用樂觀鎖機制來生成版本號,而是使用了一個獨立的seqsvr來處理序列號操做,一方面由於業務有大量的sequence查詢需求——查詢已經分配出去的最後一個sequence,而基於seqsvr的查詢操做能夠作到很是輕量級,避免對存儲層的大量IO查詢操做;另外一方面微信用戶的不一樣種類的數據存在不一樣的Key-Value系統中,使用統一的序列號有助於避免重複開發,同時業務邏輯能夠很方便地判斷一個用戶的各種數據是否有更新。架構

從seqsvr申請的、用做數據版本號的sequence,具備兩種基本的性質:性能

  1. 遞增的64位整型變量ui

  2. 每一個用戶都有本身獨立的64位sequence空間spa

舉個例子,小明當前申請的sequence爲100,那麼他下一次申請的sequence,可能爲101,也多是110,總之必定大於以前申請的100。而小紅呢,她的sequence與小明的sequence是獨立開的,假如她當前申請到的sequence爲50,而後期間無論小明申請多少次sequence怎麼折騰,都不會影響到她下一次申請到的值(極可能是51)。架構設計

這裏用了每一個用戶獨立的64位sequence的體系,而不是用一個全局的64位(或更高位)sequence,很大緣由是全局惟一的sequence會有很是嚴重的申請互斥問題,不容易去實現一個高性能高可靠的架構。對微信業務來講,每一個用戶獨立的64位sequence空間已經知足業務要求。

目前sequence用在終端與後臺的數據同步外,同時也普遍用於微信後臺邏輯層的基礎數據一致性cache中,大幅減小邏輯層對存儲層的訪問。雖然一個用於終端——後臺數據同步,一個用於後臺cache的一致性保證,場景大不相同。

但咱們仔細分析就會發現,兩個場景都是利用sequence可靠遞增的性質來實現數據的一致性保證,這就要求咱們的seqsvr保證分配出去的sequence是穩定遞增的,一旦出現回退必然致使各類數據錯亂、消息消失;另外,這兩個場景都很是廣泛,咱們在使用微信的時候會不知不覺地對應到這兩個場景:小明給小紅髮消息、小紅拉黑小明、小明發一條失戀狀態的朋友圈,一次簡單的分手背後可能申請了無數次sequence。

微信目前擁有數億的活躍用戶,每時每刻都會有海量sequence申請,這對seqsvr的設計也是個極大的挑戰。那麼,既要sequence可靠遞增,又要能頂住海量的訪問,要如何設計seqsvr的架構?咱們先從seqsvr的架構原型提及。

架構原型

不考慮seqsvr的具體架構的話,它應該是一個巨大的64位數組,而咱們每個微信用戶,都在這個大數組裏獨佔一格8bytes的空間,這個格子就放着用戶已經分配出去的最後一個sequence:cur_seq。每一個用戶來申請sequence的時候,只須要將用戶的cur_seq+=1,保存回數組,並返回給用戶。

圖1. 小明申請了一個sequence,返回101

預分配中間層

任何一件看起來很簡單的事,在海量的訪問量下都會變得不簡單。前文提到,seqsvr須要保證分配出去的sequence遞增(數據可靠),還須要知足海量的訪問量(天天接近萬億級別的訪問)。知足數據可靠的話,咱們很容易想到把數據持久化到硬盤,可是按照目前每秒千萬級的訪問量(~10^7 QPS),基本沒有任何硬盤系統能扛住。

後臺架構設計不少時候是一門關於權衡的哲學,針對不一樣的場景去考慮能不能下降某方面的要求,以換取其它方面的提高。仔細考慮咱們的需求,咱們只要求遞增,並無要求連續,也就是說出現一大段跳躍是容許的(例如分配出的sequence序列:1,2,3,10,100,101)。因而咱們實現了一個簡單優雅的策略:

  1. 內存中儲存最近一個分配出去的sequence:cur_seq,以及分配上限:max_seq

  2. 分配sequence時,將cur_seq++,同時與分配上限max_seq比較:若是cur_seq > max_seq,將分配上限提高一個步長max_seq += step,並持久化max_seq

  3. 重啓時,讀出持久化的max_seq,賦值給cur_seq


圖2. 小明、小紅、小白都各自申請了一個sequence,但只有小白的max_seq增長了步長100

這樣經過增長一個預分配sequence的中間層,在保證sequence不回退的前提下,大幅地提高了分配sequence的性能。實際應用中每次提高的步長爲10000,那麼持久化的硬盤IO次數從以前~10^7 QPS下降到~10^3 QPS,處於可接受範圍。在正常運做時分配出去的sequence是順序遞增的,只有在機器重啓後,第一次分配的sequence會產生一個比較大的跳躍,跳躍大小取決於步長大小。

分號段共享存儲

請求帶來的硬盤IO問題解決了,能夠支持服務平穩運行,但該模型仍是存在一個問題:重啓時要讀取大量的max_seq數據加載到內存中。

咱們能夠簡單計算下,以目前uid(用戶惟一ID)上限2^32個、一個max_seq 8bytes的空間,數據大小一共爲32GB,從硬盤加載須要很多時間。另外一方面,出於數據可靠性的考慮,必然須要一個可靠存儲系統來保存max_seq數據,重啓時經過網絡從該可靠存儲系統加載數據。若是max_seq數據過大的話,會致使重啓時在數據傳輸花費大量時間,形成一段時間不可服務。

爲了解決這個問題,咱們引入號段Section的概念,uid相鄰的一段用戶屬於一個號段,而同個號段內的用戶共享一個max_seq,這樣大幅減小了max_seq數據的大小,同時也下降了IO次數。

圖3. 小明、小紅、小白屬於同個Section,他們共用一個max_seq。在每一個人都申請一個sequence的時候,只有小白突破了max_seq上限,須要更新max_seq並持久化

目前seqsvr一個Section包含10萬個uid,max_seq數據只有300+KB,爲咱們實現從可靠存儲系統讀取max_seq數據重啓打下基礎。

工程實現

工程實如今上面兩個策略上作了一些調整,主要是出於數據可靠性及災難隔離考慮

  1. 把存儲層和緩存中間層分紅兩個模塊StoreSvr及AllocSvr。StoreSvr爲存儲層,利用了多機NRW策略來保證數據持久化後不丟失;AllocSvr則是緩存中間層,部署於多臺機器,每臺AllocSvr負責若干號段的sequence分配,分攤海量的sequence申請請求。

  2. 整個系統又按uid範圍進行分Set,每一個Set都是一個完整的、獨立的StoreSvr+AllocSvr子系統。分Set設計目的是爲了作災難隔離,一個Set出現故障只會影響該Set內的用戶,而不會影響到其它用戶。


圖4. 原型架構圖

小結

寫到這裏把seqsvr基本原型講完了,正是如此簡單優雅的模型,可靠、穩定地支撐着微信五年來的高速發展。五年裏訪問量一倍又一倍地上漲,seqsvr自己也作過大大小小的重構,但seqsvr的分層架構一直沒有改變過,而且在可預見的將來裏也會一直保持不變。 原型跟生產環境的版本存在必定差距,最主要的差距在於容災上。像微信的IM類應用,對系統可用性很是敏感,而seqsvr又處於收發消息、朋友圈等功能的關鍵路徑上,對可用性要求很是高,出現長時間不可服務是分分鐘寫故障報告的節奏。下一篇文章會講講seqsvr的容災方案演變。

相關文章
相關標籤/搜索