因爲用戶對於數字很是的敏感(想一想你好不容易拉到一位粉絲,可是粉絲數沒漲的痛苦吧。),因此咱們要求數據很是準確,延遲極低(1s之內),服務穩定性極高(千萬別由於某大媽掃個地撥了插座就把數字弄沒了...)
對這一塊很是的感興趣,並且有靠譜的想法和建議,煩請私信簡歷給 @cydu 或者 @微博平臺架構,咱們這裏還有大量相似的問題期待着你來解決! 固然,也能夠直接評論一塊兒討論。
Update: 更新了數據持久化和一致性保證相關的內容,多謝 @lihan_harry @鄭環Zheng @51劉達 等同窗的提醒。
Update2: 更新了 對於weibo_id key的優化,使用前綴壓縮,能夠節省近一半的空間。 感謝 @吳廷彬 @drdrxp 的建議!
Update3: 更新了 對於value 使用二維數組,多列進行壓縮編碼的優化思路, 再次感謝 @吳廷彬 的建議,
Update4: 更新Redis方案下內存使用的估算, 感謝 @劉浩bupt 的提醒。
上週挖了一個坑
([微架構設計]微博計數器的設計(上) http://qing.weibo.com/1639780001/61bd0ea133002460.html ) ,
雖然挖這個坑的動機是很不純的(很明顯的招聘軟文, 很是欣慰的是確實收到了很多靠譜的簡歷, 但願簡從來得更猛烈一些! ), 可是和你們討論的過程當中,仍是收穫很大的, 也認識了很多新朋友。
對於一個簡單的計數服務來講,確實很是的簡單,咱們能夠有不少的解決方案:
方案一: 直接上mysql
這個不用多說了吧,足夠的簡單暴力。 可是在產品發展的初期快速迭代的階段,他可以解決不少的問題,也不失爲一個不錯的解決方案。
數據量過大怎麼辦?
對於一億甚至幾億如下的數據規模來講,拆表可以解決不少問題,對於微博計數器來講至少有兩種經典的拆法:
一. 按id取模,把數據拆分到N個表當中去。 這個方案的悲劇是: 擴展性很差,很差加表,數據一旦滿了,加起來很鬱悶。雖然能夠預先多分一些表,可是對於weibo這種快速增加的業務來講,嚴重影響了業務的快速增加需求。
二. 按id的時間來分段拆表,滿了就建新表。 這個方案的悲劇是: 冷熱不均,最近的weibo確定是被訪問最頻繁的,而老的庫又基本沒有訪問。 能夠經過冷熱庫混合部署的方案來緩解,可是部署和維護的成本很是大。
數據量從億上升到千億後,這個問題的本質就發生了變化,維護上千張表,熱點還各不相同須要常常切換調整,這是一件很是悲劇的事情。。。
訪問量太大怎麼辦?
應對訪問量,也有不少的經典的方法:
一. 上Cache(Eg: Memcache), 訪問時先訪問Cache,不命中時再訪問mysql. 這樣作有兩個鬱悶點: 空數據也得Cache(有一半以上的微博是沒有轉發也沒有評論的,可是依然有大量的訪問會查詢他); Cache頻繁失效(因爲計數更新很是快,因此常常須要失效Cache再重種,還會致使數據不一致);作爲最基礎的服務,使用複雜,客戶端須要關注的東西更多
二. 更好的硬件解決。 上FusionIO + HandleSocket + 大內存 優化. 經過硬件的方式也可以解決問題,可是這是最典型的Scale up的方案。雖然徹底不用開發,可是硬件成本不低,且對於更復雜的需求,以及流量快速的增加,也很難應對。
優勢:
一. 不用開發, 碼農們能夠用寫代碼的時間出去泡泡妞。
二. 方案成熟, 數據複製,管理,修復方案都很成熟。
缺點:
一. 對大數據量和高併發訪問支持很差,很是的力不從心。
二. 維護成本和硬件成本都很高。
總的來講: Mysql分表 + Cache/硬件 加速的方案 對於數據規模和訪問量不是特別巨大的狀況下,很是不錯的解決方案,可是量大了以後很是不合事宜.
既然 Mysql不行,那用NoSQL 呢?
方案二: Redis
作爲一個簡單的內存數據結構來講,Redis提供很是簡單易用的訪問接口,並且有至關不錯的單機性能。 經過incr實現的 Counter Pattern,用來作計數器服務,更是簡單輕鬆。 經過上層的分表,增長slave等方式,堆一些機器,也可以解決大數據量和高併發訪問的問題。
可是Redis是純內存的(vm機制不成熟並且即將被廢棄,咱們線上確定是不敢直接使用的!),因此成本也不算低,咱們簡單的來估算一下數據存儲量(下面都是按照Redis 2.4.16的實現,在64位系統,指針爲8字節來估算的) :
假設 key 爲8字節,value爲 4字節,經過incr存儲的話:
一個 value 經過 createStringObjectFromLongLong 建立一個robj,因爲value在LONG_MIN 和LONG_MAX 之間,因此能夠將value用 ptr指針來存儲,須要佔用 sizeof(robj) = 16 字節;
一個key(即微博id) 最長64位數字(Eg: 5612814510546515491),但經過 sdsdup 以字符串的形式存儲,至少須要 8(struct sdshdr)+19+1 = 28字節;
爲了存到Redis 的dict裏面,須要一個dictEntry對象,繼續 3*8 = 24字節;
放到db->dict->ht[0]->table中存儲dictEntry的指針,再要 8個字節;
存儲一個64位key,32位value的計數,Redis也至少須要耗費: 16 + 28 + 24 + 8 = 76 字節。 1000億個key全內存的話,就至少須要 100G * 76 = 7.6TB的內存了(折算76G內存機器也須要100臺!)。 咱們的有效數據實際上是 1000億*32位 = 400GB,可是卻須要7.6TB來存儲,內存的有效利用率約爲: 400GB/7600GB = 5.3%.
即便這樣,對於不少熱點的數據,只有一個副本,單機性能不夠,系統的穩定性也沒法保證(單機Down掉咋辦?), 還須要複製多份。 再算上爲了不內存碎片引入的jemalloc的內存開銷; 再算了dictExpand等須要的臨時內存空間; 再算上系統要用的內存開銷。。。那要的機器就更多了,保守估計須要300-400臺以上的機器。
總的來講: Redis作爲優秀的內存數據結構,接口方便,使用簡單,對於小型數據量的中高訪問量的計數類服務來講,是一個很不錯的選擇,可是對於微博計數器這種極端的應用場景,成本仍是沒法接受!
還有一些同窗提出了用 Cassandra,MongoDB 等其餘NoSQL的方案,不管是從可維護性的角度,仍是從機器利用率的角度,都很難以接受(有興趣的同窗能夠仔細分析一下)。
普通的NoSQL也不行,那怎麼辦? 嘗試定製咱們本身的Counter!
Update4:
//@劉浩bupt: @cydu 剛剛仔細閱讀了文中redis容量預估的部分,有兩點小瑕疵:1.對於value的存儲,文中估算了16個字節,其實這部分開銷是能夠節省的。createStringObjectFromLongLong函數,對於小於REDIS_SHARED_INTEGERS的value值,不會額外分配空間。REDIS_SHARED_INTEGERS默認是10000,調大一些能夠知足大部分需求
//@劉浩bupt: @cydu 2.是能夠評估下使用zipmap達到的內存利用率。redis不是隻有string->string的kv存儲,仍是有一些能夠挖掘的東西的。instagram在其工程博客中介紹過(http://t.cn/S7EUKe),改用zipmap後,其存儲1M的數據,內存佔用由70M優化到了16M。鑑於新浪微博大量的使用redis,定製redis實現服務也是個思路。
感謝 @劉浩bupt 同窗幫我指出對於Redis容量預估的不許確,經過Redis自帶的 REDIS_SHARED_INTEGERS 機制確實可能大量節省value所佔的內存,可是因爲這個方案須要依賴存儲shared_int的指針,不太好遷移到方案三裏面去。
Zipmap這個優化的思路是至關不錯的,對於通用的Redis的使用,咱們會持續關注。
方案三: Counter
計數器是一個普通的基礎服務,可是由於數據量太大了,從而量變引起了質變。 因此咱們作Counter時的一個思路就是: 犧牲部分的通用性,針對微博轉發和評論的大數據量和高併發訪問的特色來進行定點優化。
1. 大量微博(一半以上)是沒有轉發,或者沒有評論,甚至是沒有轉發也沒有評論。
針對這種狀況的優化: 拋棄 存儲+Cache的思路, 由於這些爲0的數據,也必須進到Cache中(不管是旁路仍是穿透),由於查詢量並不小,這對於咱們Cache的利用率影響很是很是的大(有一半的數據是空的。) 而咱們採用相似 存儲即Cache(存儲自己就在內存中) 時,這一類的數據是能夠不存儲的,當查不到的時候,就返回0。
經過這種狀況,1000億個數字,咱們能夠減小3/5,即最多隻須要存 400億個數字。這算是最經典的稀疏數組的優化存儲方式了。
2. 微博的評論數和轉發數 的關聯度很是的高。
他們都有相同的主Key, 有大量轉發的微博通常也有評論,有大量評論的通常轉發量也不小。 並且訪問量最大的Feed頁基本上取評論數的時候,也會取轉發數。。。
針對這種狀況的優化: 咱們將評論數和轉發數 能夠考慮存儲在一塊兒,這樣的話,能夠節省大量key的存儲空間。 由 微博ID+評論數; 微博ID+轉發數 變爲: 微博ID+評論數+轉發數的結構。
PS: 這個優化和上一個優化是有一些小衝突的,部分有轉發沒有評論的微博,須要多存一個0; 可是通過數據評估,咱們發現這個優化仍是至關必要的: a. key存儲的空間比評論數還要長,一個8字節,一個4字節; b. 對於應用層來講,批量請求能夠減小一次訪問,可以降請求的壓力,同時提高響應的時間;
(具體的數字不方便透露,可是這個結論你們能夠隨機抽取一批公開的微博來驗證)
3. 數據結構的優化
經過方案二中Redis對內存使用的分析,咱們發現是很是"奢侈"的, 大量的重複存儲着指針和預留的字段,並且形成大量的碎片內存的使用, 固然Redis主要是出於通用性的考慮。 針對這種狀況:
@果爸果爸 同窗設計了一個更輕量更簡單的數據結構,能更好的利用內存,核心思路:
a. 經過下面的item結構來存儲 轉發和評論數:
struct item{
int64_t weibo_id;
int repost_num;
int comment_num;
};
存儲數字,而不是字符串,沒有多餘的指針存儲, 這樣的話,兩個數字只佔 16個字節;
b. 程序啓動的時候,開闢一大片的內存 (table_size * sizeof(item)) 並清0他。
c. 插入時:
h1 = hash1(weibo_id);
h2 = hash2(weibo_id);
若是 h1%table_size 是空的,則把item存儲到這個位置上;
不然 s=1 並找 ( h1 + h2*s ) % table_size 的位置,若是還不空的話,s++繼續找空位。。。
d. 查詢時:
和插入的過程相似,找到一個數據後,比較weibo_id 和 item.weibo_id 是否一致,一致
則表示查到,不然查到空的則表示爲值爲0;
e. 刪除時:
查找到所在位置,設置特殊的標誌; 下次插入時,能夠填充這個標誌位,以複用內存。。。
通過咱們實測,當2億數據這種長度的數組中,容量不超過95%的時候,衝突率是能夠接受的
(最悲劇的時候可能須要作幾百次的內存操做才能找到相應的空位, 性能上徹底可以接受; )
通過這個優化以後,咱們的總數據量變成了:
400億 * 16B = 640GB; 基本是方案二的 十分之一還少!
4. 轉發和評論數 Value的優化
繼續觀察,咱們發現大量的微博,雖然有轉發和評論,可是值通常都比較小,幾百或者幾千的,
超過幾萬的weibo不多(數據調研顯示在十萬分之一如下)。
因此咱們把 item 升級爲:
struct item{
int64_t weibo_id;
unsigned short repost_num;
unsigned short comment_num;
};
對於轉發數和評論數大於 65535 的weibo,咱們在這裏記錄一個特殊的標誌位FFFF,而後去
另外的dict中去查找(那邊不作這個優化)。事實上,還能夠把 unsigned short優化爲 int:12 之類的極端狀況,可是更復雜,且收益通常,因此咱們仍是選用unsigned short。
通過這個優化後,咱們的總數據量變成了:
400億 * 12B = 480GB, 這個數據量已經差很少是單機可以存儲的容量了。
每秒的查詢量由100W變成了50W, 更新量每秒只有數萬沒有變化,和查詢量比能夠先忽略。
4.1 補充 Value的優化
@吳廷彬: 另外,64bit value能夠用utf-8的相似思想再壓縮。最後由於cpu/mem不是瓶頸,能夠將weibo_id和後面的value分開放在兩個數組裏面,對應的index同樣便可。而後會發現value數組裏面的64bit不少位全是0,或許能夠考慮以K爲單位的數據作簡單數據壓縮放入內存裏面,這個壓縮比應該是驚人的。
@吳廷彬: 回覆
@cydu:value能夠用二維數組怎麼樣。 若是1K爲單位壓縮則每一行表示1K個數據。而後對數據進行壓縮寫入。 通常可能每行只用100個字節?
@cydu: 這樣確實能夠,變長編碼會有意義,反正cpu應該不是瓶頸,有更新的時候整塊從新編碼,取也是全取出再解壓。還一個好處是我加列更方便了,如今我加列的代價實際上是很高的。
最先的時候,我也想過用變長壓縮,可是思路一直侷限在一個value裏面作壓縮,因爲只有兩列,咱們又是用定長的存儲,一方面變長有開銷(標誌位標誌用了多少位來表示),另外一方面定長開給的32位省出來也沒有合適的用處(能夠和key的優化結合起來,用更少的字段)。 @吳廷彬 一說二維數據,立馬變長壓縮的好處就顯現出來了。
我能夠把key單獨存儲,把value,按 1024個value甚至更多個value 壓縮到一個mini block中存儲,在定長的狀況下,這個mini block的size是 1024*32 = 4K. 可是事實上,這4K中包含了大量的 0, 我不用本身整複雜的變長編碼,直接拿這4K的數據作LZF壓縮,只存儲壓縮後的數據就好了, 取的時候先解壓縮。 具體的壓縮效率得看數據才能定,可是根據通常文本的壓縮到 50% 應該是很是輕鬆的,也就是說,至少能夠節省 400億 * 2 = 80GB的內存。
這個方案最大的一個好處還不在於這80GB的內存的節省,而是:
1. 我前面優化提到的 大於 65535 的轉發和評論,我能夠考慮簡單作了,反正變長嘛,不影響,整個方案是簡化了的。(固然須要具體的數據測試一下,驗證哪一個更好)
2. [
至關重要!!] 對於微博的計數,其實咱們是有加列的需求的,好比其餘的相似評論數的數字,我原來的方案中,加列的代價是至關高的,須要重開一個大數組,還要事先設好hint(對於新業務來講,hint值的很差選取,可是他對性能和內存的使用率影響又是致命的!),而這個方案,不管你加多少列都其實沒啥關係,
用內存的長度只和你真實的數據量相關!
通過這個優化後,我保守的估計,咱們可以在以前的基礎上,再節省 80GB的內存!
5. key的優化
@吳廷彬 很好的文章。weibo_id是8byte的,壓縮可以壓到接近4byte.假如一堆數據是AB,AC,AD,AE,AF, XE,XY,XZ.把他在內存裏面A開頭放在一坨內存,X開頭放在另一坨,裏面只用存B,C,D,E,F和Y,Z. 基本上能減小4個字節。能省掉40G*4=160G?redis
@drdrxp: 存儲分紅2^24個區,weibo_id%(2^24)指到區的號上,記錄中再用40bit 存儲weibo_id/(2^24),記錄中另外12bit 存轉發,12bit存評論, 1條記錄總共8字節,480G能夠優化到320G. 若是能實際考察下評論轉發數的分佈應該能夠更加優化1些.sql
感謝 @吳廷彬 @drdrxp 提的這個建議,這一塊的優化空間確實很是的大。後面其實有提到,咱們會根據時間段或者根據weibo id把大的table 劃分紅多個小的table(主要是爲了可以序列化到磁盤騰空間給更熱的數據)。 因此在一個小table裏面的數據都是weibo_id比較接近的,Eg: 5612814510546515491, 5612814510546515987, 咱們能夠把這64位key中相同的高32位歸併起來。作爲小table的屬性(prefix),就沒必要每一條都存儲了。 8字節的key,至少可以節省 4字節。
struct item{
int weibo_id_low;
unsigned short repost_num;
unsigned short comment_num;
};
通過這個優化後,咱們的總數據量變成了:
400億 * 8B = 320GB, ^_^
也感謝 @drdrxp 的建議,以前也考慮過12bit來存評論數和轉發數,確實可以優化很多,可是因爲多出來的bit不知道幹嗎,就沒搞了,呵呵。你的建議和 @吳廷彬 提的建議都主要是在key上作文章,很贊!
6. 批量查詢
對於Feed頁來講,一次取到N條微博,而後查詢他的計數,這裏能夠很好的批量化查詢來優化響應時間。
以一次批量訪問10個微博的計數來講,對於Counter碰到的壓力就是 5W requests/second, 100W keys/second;
對於全內存的簡單服務來講,單機已經基本可以扛 5W+ 的請求了。
7. 冷熱數據
繼續看這400億個數字,咱們發現,訪問熱點很是的集中,大量去年,甚至前年的weibo無人訪問。
本能的可能想到經典Cache的作法,熱的數據在內存,冷的數據放磁盤。 可是若是引入lru的話,意味
着咱們的struct item得膨脹,會佔用更多內存。並且對於0數據也得Cache。。。
對於這種狀況,咱們設計了一個很是簡單的內存和磁盤淘汰策略,根據weiboid的區間(實際上是時間)
來進行淘汰,按區間劃分,超過半年的dump到磁盤上去,半年內的繼續留存在內存,當少許用戶挖墳的時候
(訪問很老的微博並轉發/評論),咱們去查詢磁盤,並將查詢的結果放到 Cold Cache當中.
爲了方便把舊的數據dump到磁盤,咱們把那個大的table_size拆成多個小的table,每一個table都是不一樣的
時間區間內的weibo計數,dump的時候,以小的table爲單位。
爲了提升磁盤的查詢效率,dump以前先排序,並在內存中建好索引,索引建在Block上,而非key上。
一個Block能夠是4KB甚至更長,根據索引最多一次隨機IO把這個Block取出來,內存中再查詢完成;
通過這個優化,咱們能夠把將近200G的數據放到磁盤當中, 剩餘120GB的數據仍然留在內存當中。 並且即便隨着weibo數愈來愈多,咱們也依然只要保存120GB的數據在內存中就好了,磁盤的數據量會增長,熱點的數據也會變化,可是總的熱點數據的量是變化不多的!
8. 數據的持久化
對於Sorted 部分的數據,一旦刷到磁盤後,就只會讀,不會修改,除非在作和Cold Block作merge的時候,纔會重寫 (目前這一塊merge的邏輯沒有實現,由於必要性不高)。
對於內存中的數據,咱們會按期把 Block 完整的dump到 磁盤中,造成 unsorted block。 而後每一次內存操做都會有相應的Append log, 一旦機器故障了,能夠從 磁盤上的Block上加載,再追加Append log中的操做日誌來恢復數據。
固然,從整個架構上,一旦Counter崩潰等嚴重錯誤,致使數據錯誤,咱們還能夠經過 具體數據的存儲服務上把數據從新計算出來,恢復到Counter當中。 固然這種計數的代價是很是高的,你想一想姚晨那麼多粉絲,counter一遍很恐怖的, 咱們也另外作了一些二級索引之類的簡單優化。
9. 一致性保證
@l
ihan_harry上邊文章提到計數對正確性要求高,因爲計數不知足冪等性。那麼這個問題是怎麼解決的
@
cydu回覆
@lihan_harry :是這樣的,前面有一個消息隊列,經過相似於transid的方案來作除重,避免多加和少加; 固然這裏主要是指用主從的結構,incr累加,即便是最終一致也不至於太離譜; 另外,咱們還有作實際的存儲數據到Counter的按期數據校驗,之後面的數據存儲爲準
@
鄭環Zheng貌似還會有寫請求單點問題,老數據的刪除遞減走硬盤,多機房冗餘,機器假死宕機數據會不會丟失,刪微博的時候還要清空相關計算不呢
@
cydu回覆
@鄭環Zheng :是的,爲了incr 的準確性,仍是使用Master-Slave的結構,因此Master的單點問題依然存在,須要靠主從切換,以及過後的數據修復來提升數據的準確性。
10. 分佈式化
出於穩定性的數據冗餘的考慮,並且考慮到weibo如今數據增加的速度,在可預見的將來,數字會
變成1500億,2000億甚至更高。
咱們在上層仍是作了一些簡單的拆分的,按照weiboid取模,劃分到4套上(主要是考慮到後續數據的增加),
每套Master存儲後面又掛2個Slave, 一方面是均攤讀的壓力,另外一方面主要是容災(當主掛掉的時候,
還有副在,不影響讀,也可以切換)
so, 我仍是沒能單機扛住這1000億個數字,和每秒100W次的查詢。。。只好厚着臉皮問老大申請了十幾臺機器。
優勢: 單機性能真的很好,內存利用率很高,對後續擴展的支持也至關不錯。
缺點: 咱們碼農泡妞的時間少了,得抽空寫寫代碼。。。可是,若是不用寫碼的話,那碼農還能幹嗎呢?
總之: 對於這種極端的狀況,因此咱們採用了一樣極端的方式來優化,犧牲了部分的通用性。
方案四: Counter Service
方案三出來後, 微博計數的問題是解決了,可是咱們還有用戶關注粉絲計數呢,好友計數,會員計數...
數字社會嘛,天然是不少數字,每個數字背後都是一串串的故事。
針對這種狀況,咱們在Counter的基礎上,再把這個模塊服務化,對外提供一整套的 Counter Service,
並支持動態的Schema修改(主要是增長),這個服務的核心接口相似於下面這個樣子:
//增長計數, 計數的名字是: "weibo"
add counter weibo
// 向"weibo"這個計數器中增長一列,列名是 weibo_id, 最長爲64位,通常也是64位,默認值爲0, 並且這一列是key
add column weibo weibo_id hint=64 max=64 default=0 primarykey
// 向"weibo"這個計數器中增長一列,列名是 comment_num, 最長爲32位,通常是16位,默認值爲0
add column weibo comment_num hint=16 max=32 default=0 suffix=cntcm
// 向"weibo"這個計數器中增長一列,列名是 repost_num, 最長爲32位,通常是16位,默認值爲0
add column weibo repost_num hint=16 max=32 default=0 suffix=cntrn
// 向"weibo"這個計數器中增長一列,列名是 attitude_num, 最長爲32位,通常是8位,默認值爲0
add column weibo attitude_num hint=8 max=32 default=0 suffix=cntan
....
// 設置weibo計數中 weibo_id=1234 的相關計數,包括 comment_num, repost_num, attitude_num
set weibo 1234 111 222 333
// 獲取weibo計數中 weibo_id=1234 的相關計數,包括 comment_num, repost_num, attitude_num
get weibo 1234
// 獲取weibo計數中 weibo_id=1234 的相關 comment_num
get weibo 1234.cntcm
// 增長weibo計數中 weibo_id=1234 的相關 comment_num
incr weibo 1234.cntcm
....
當 add column的時候,咱們會根據hint值再增長一個大的table (table_size * sizeof(hint)),
可是這裏不存儲key,只有value,用原來item那個大table的相同key。 對於超過部分依然是走
另外的存儲。
經過計數器服務化以後,最大的好處就是,後面咱們再要加計數,有可能量沒有那麼大,能夠很快的
建立出來。。。
缺點:
對於非數值類的key名,可能會退化到字符串的存儲,咱們能夠經過簡化的base64等機制來縮短空間;
對於頻繁修改老的數據,致使cold buffer膨脹的問題,能夠經過按期的merge來緩解(相似於Leveldb的機制);
方案五: 你的方案
對於工程類的問題,其實永遠不會有標準的答案,一千個架構師能給出一萬個設計方案來,並且沒有
一個是標準的答案,只有最適合你的那一個! 這裏只簡單分享一下個人一個思考過程和不一樣階段最核心
關注的點,歡迎你們一塊兒討論。
期待你的思路和方案! 期待大家的簡歷, 請私信 @cydu 或者 @微博平臺架構。
固然,微博平臺除了計數器這一個典型的小Case外,還有更多更大的挑戰須要你的方案!