從零到千萬,伺服器的架構怎麼變?泣血乾貨集。長文,慎入。
三年前,原本我只是個不學無術的數據小碼農,空有一腔熱情;而當時公司也處在艱難的轉型期,舊產品不見起色,新產品前途未卜。想見著也不可能用這麼小的數據玩出花來,而新產品的數據也不是一時半會能成規模。還是本著最大限度學習的心思,鼓足勇氣和老闆提換崗,要去扛後臺開發的大旗,最大程度參與到產品的一線去。一個小決定,換來的是整整半年的不眠之夜,眼見著第1個用戶到第500萬個用戶,眼見著1台到4台再到10台伺服器,眼見著後臺業務由單一的播放到能播放能上傳再到有完整的社交交互。從剛開始三天兩頭崩潰出事故,到最終一點不怕市場的同事搞拉新的活動,什麼狀況都能做到心中有數、遇事不慌。回頭一想嚇一大跳:自己並不是後臺工程師科班出身,從來對語言和框架的爭論無感無力,網路編程的基礎知識更是差強人意,但是憑著小米步槍,憑著奇技淫巧,憑著持續思考和不斷嘗試,居然也能搭建起一個支撐千萬級別用戶的後臺框架。總結那半年,留下了5條事關生死的建議,在這裡泣血奉上。
數據的讀寫是伺服器性能的核心
一個完整的後臺服務,組件其實就只分3種:接入、邏輯和數據。這好比一家飯店,後臺工程師就是開店的老闆,客人數量小於1萬,服務流程是第一位的,老闆們吭哧吭哧忙著寫邏輯;1萬到10萬之間,接入組件的設計會是重中之重:一個店的服務能力有限,老闆們忙著多開幾個分店,讓客人分流,而決定客人到哪一個分店的,就是接入組件;但是用戶一旦大於10萬,數據的讀寫能力就決定了這家超級飯店的服務容量,不管開多少個分店,都要保證數據是一致的,讀起來又快又準,而寫數據不會影響到讀的性能。表結構怎麼設計,資料庫怎麼分佈(主從、讀寫分離、分庫、分表),緩存怎麼選怎麼分佈,就是老闆們最重要的工作(讓老闆高興的是,名片也可以改印個高大上的抬頭:架構師)。
一旦用戶量過了十萬,要再想光靠資料庫一部卡車打天下就不太現實了,而緩存(物理存儲地在記憶體,天生比資料庫讀寫性能強)這匹野馬的出現就滿足了我們對於速度的極致需求。緩存對伺服器的架構帶來了兩個深遠的影響:一是熱數據和冷數據的分離:熱數據訪問的人多,緩存擋在前面,為資料庫分擔巨大的讀壓力;而熱數據從產品的角度也更應獲得快速的響應。二是數據一致性的門檻提高,更新資料庫的同時必須更新緩存,一旦緩存更新失敗,資料庫也一定要回滾而保證數據的一致性,不能鬧給客人上冷盤的笑話。當然緩存存什麼、怎麼存,也是大有一番學問,容我下一小節再講。但緩存的重要性總結一句話:沒有緩存是萬萬不能的。無論你是選老馬Memcached還是火熱的頭馬Redis,一定要在資料庫感受到壓力之前上馬,並且做好緩存備份和恢復的預案。當然,平安無事你是沒辦法感受到緩存的好處的,它就像一個平時提醒你吃飯睡覺多喝熱水的備胎,只有當她棄你而去之時,你看著伺服器嘩嘩成百倍上漲的響應時間,恨不得找塊豆腐一頭撞死。
列表、實體和冗餘
Web時代,由於翻頁前後用戶出現了界面的切換,用戶對於列表本身的變化並不敏感(假如翻頁的同時列表新加入了內容,只要保證用戶瀏覽的這個片段沒有重覆就可以),但是移動端這種滾動列表的設計簡直就是所有後臺工程師的夢魘(加入用戶上拉列表獲取更多的同時新加入了內容,那用戶會看到相鄰兩個重覆的內容,然後就氣炸了,什麼破APP!),應對「列表重覆」這個難題的方法出一本書都夠了。因為這個需求,我們只能放棄了原有的自增ID,採用時間戳作為獲取列表片段的方式:簡單來講,就是客戶端每次都上報一個當前頁最後一個內容的時間戳,伺服器再去取比這個時間更舊的若幹個內容。這裡必須要感謝Redis的作者提供瞭如此豐富的緩存使用的API,我覺得Redis最出色的一點就是把列表的所有使用場景都設想得很通透。
實體就是熱數據,熱數據的緩存有兩問:一是存什麼?有人會說簡單,把整個結構體轉化為一個JSON存進去不就得了?但這其實是有問題的,當你的伺服器要面對數十萬同時到來的用戶,可能短短一瞬就要做數以千萬計的JSON到結構體之間的來回切換,而這個過程的效率實際上是很不理想的,那麼也許你要想一些更快的方案(此處買個關子)。二是怎麼存?雪崩效應並不罕見,一旦源數據改變,一時間許多個線程同時去訪問更新緩存的API,伺服器瞬間堵死,想到後臺工程師會因此而失業,我默默加了一個鎖。
小張是端菜的服務員,這次上菜,他要先去冷盤區取個土豆絲、再去葷菜區取個東坡肉、順到素菜區取個手撕包菜、最後到飲料區再拎兩瓶果汁,聽起來很低效,對不?這和數據獲取的過程是類似的,資料庫的表設計首要考慮的是歸類,比如用戶的信息存一張表,用戶和小組的關係再存一張表,那麼如果有一個場景需要讀用戶以及他最後訪問過的小組,就得做兩次的數據表讀取,一旦這個場景頻繁出現,適當的數據冗餘(把用戶最後訪問的小組ID加入到用戶表的欄位中)就能夠降低資料庫的讀取壓力。所以表設計一定一定一定(重要的事情說三遍)要考慮業務場景。
非同步,是不是真非同步?
有的小盆友跑來問我,我這個伺服器框架選的牛啊,非同步多線程的,單進程併發一萬多輕而易舉,怎麼還是慢啊?我說,「非同步」這個詞可不要說得太輕鬆,底層非同步了,流程里的每個步驟是不是非同步的呢?資料庫讀寫、緩存讀寫、外部介面的訪問,這些都不能非同步吧?既然不是非同步,卡在哪裡你還不知道呢,還不趕緊打日誌。還是說說最令我崩潰的一個案例:某次伺服器炸了,打多少次日誌都沒辦法定位到卡住的原因。最後猜是怎麼著?竟然是日誌組件(Log4j)就不是非同步的,打日誌這個步驟就卡住了,欲哭無淚。
日誌、監控和有損服務
一個高級飯店要有廚師,要有大堂經理,要有端盤子的,要有收銀的,但千萬別忘了還要有保全。他雖然不是飯店成功與否的核心因素,但是如果缺了他,危機時刻就會應付不來。下麵這三位哥們就是伺服器的保全:日誌、監控和有損服務。
先說日誌,日誌是很微妙的,打多了不行,影響性能、占據空間,打少了,關鍵問題排查不出原因。那麼哪些是必打的呢?我認為有三點:一是行為的基本屬性,無非是何時何地何人,時間、用戶ID、IP、版本(存下來除了排錯,還可以用來做數據分析);二是往返的參數,尤其是客戶端上報的參數,伺服器返回的數據也許會很大,不建議所有都列印,可以列印統計數據,比如返回了多少個小組之類;三是報錯信息,底層一定要catch所有的出錯信息,並把它打到單獨的日誌里。
再說監控,日誌是一旦發現了問題幫助我們找出問題的原因的工具,那麼什麼能幫我們發現問題呢?答案是監控和告警。監控與日誌不同,要抓核心的數據,不能多,我建議取三個數據:用戶的併發訪問數、讀取的人均響應時間、寫入的人均響應時間,告警的話再加上伺服器的崩潰、重啟的次數,以及主機性能相關的指標(CPU、記憶體、硬碟等)。
「發生這種事,大家都不想的。餓不餓,我給你煮碗面?」,伺服器運氣不好崩潰了,我便常常用這句TVB的經典臺詞與小伙伴們調侃。其實無論事前機關算盡,成長期的APP總會遇到伺服器出狀況的。但是,以我有限的經驗,伺服器的問題往往不出在自身,而是它所依賴組件導致的問題,比如Memcached機器dump、轉碼服務隊列阻塞、或者圖片存儲空間爆滿等等。那麼在問題被解決之前,總不能幹瞪眼,看著用戶投訴一波波來吧?我們會想,對於現在的業務來說,最不能崩潰的場景是什麼?比如播放是我們的最基礎服務,那我們死也要保證任何外部組件的崩潰都不能影響熱門內容的播放,因此我們要把這部分少而重要的熱數據載入到記憶體,以防止外部存儲出了什麼問題,伺服器自己還有碗面吃。真正是,自己的事情自己乾,靠天靠地靠祖宗,不算是好漢。
服務分離與複製
伺服器體系越長越大,我們首要做的事情是分封,兒子長大了,總要給他一塊地盤,當個小王,從此自己打拼去。於是數據讀寫被抽象成服務了,同時對APP和前端負責,做最大的一個王;編碼解碼抽象成服務了,反正編碼解碼是給UGC用戶提供的,想當明星的人總要等得起;日誌存儲和解析也抽象成服務了,反正有少許的丟失我們也不介意。錶面看來伺服器被拆得支離破碎,增加了網路時延,是一筆不划算的生意,但實際上對伺服器的穩定性大有助益。為什麼?一是大王國被拆成小王國了,定位問題更容易,遷移和複製也更簡單,數據讀寫有壓力?沒問題!再給兩塊地盤。二是在整個鏈條上,任何一個環節都是多點,俗話說,不把雞蛋都放在一個籃子,任何一臺伺服器dump都不會要了我們的命。
細枝末節且不提,總結當時半年內伺服器高速發展期留下來的經驗,我認為最重要的就是這五點,業務場景不同,伺服器的架構和側重點也肯定會略有差異;不過這五點基本等同於錦囊,等同於基石,等同於保命符,做好了,這飯店生意一定蒸蒸日上。恭喜你,老闆!
更多精彩內容,歡迎關註微信公眾號「碼農咖啡館」