線上高併發應用重構(寫)填坑經驗分享(一)

丶謙信發表於2021-12-30

        今年在公司重構(寫)了一個老專案,踩了無數的坑。

        中間好幾次遇到問題,甚至感覺專案可能要失敗了,好在最後終於成功上線了。

        雖然被坑的不要不要的,但也從中領悟到了不少東西,在這裡記錄一下,順便分享給大家樂呵樂呵。

 

        先簡單介紹下專案,一個面向C端使用者的服務,主要提供包括動態、評論、圈子、好友、關注、Feed等常見的社群功能,另外還有其他一些個性化的功能。

        日活比較高,整個服務QPS上萬。高頻業務,單個介面QPS上千。單項業務資料量過億,比如評論。

圖1.qps監控圖

        在上述高併發、海量資料的情況下,整個系統設計時需要注意的坑,和我總結的一些經驗:

資料庫層面

MySQL分庫分表

因為是重寫整個專案,包括重新設計底層資料庫,必然要考慮到分庫分表。

最初在網上參考了一些分庫分表的原則,實際操作中,發現大部分資料都有些縹緲。

如果是簡單的應用怎麼分表,甚至不分都可以。所以這些原則你也不能說它是錯的,但在你最需要參考的時候,這些原則往往不夠深入。

分享下我個人總結的一些經驗:

先說分庫

分庫的主要目標,應該是緩解主庫(Master)的壓力。

絕大部分服務都是讀多寫少,在讀寫分離,1主1備N從的情況下,即便為了保證一致性,部分讀請求路由到主庫,主庫壓力依舊很低。

通過監控服務的寫請求量和資料庫伺服器的CPU壓力等效能指標,只要主庫壓力不大,就沒必要分庫。讀庫如果壓力大,直接加從庫例項即可。

一種極端的情況,就是分表數量過多了,一個庫裡表數量遞增,成萬上億了,那還是分庫的好。

還有一點,從運維的角度考慮,單庫冷備,資料不應該超過500GB。如果單庫資料量達到1個TB,運維也不好備份,為了正常備份也要分庫。

 圖2.資料庫監控圖(圖中藍色線條代表主庫,基本上是趴著不動躺平的)

再說分表

在請求量不大 或 資料量不大的情況下,分不分表都無所謂。

考慮mysql的效能、樹的深度等,可以簡單的認為單表500W左右即可。

但實際中往往需要結合具體的業務設計和查詢場景。

比如,1張幾千萬資料量的訂單表,如果業務上,只需要根據主鍵或唯一索引,每次查詢一條記錄,那麼不分表也是完全可行的。

但有時出於運維需要,分表會更方便一些,比如研發人員可能會想手寫一些SQL上去進行一些範圍查詢,為排查問題提供一些方便。(這裡說的方便是指相對單表幾千萬,如果查詢欄位沒有索引,範圍查詢基本不可用。當然從操作步驟上看,肯定比查1個表繁瑣了)

特別需要注意的是,如果一項業務資料需要高頻的用到 count語句查詢總數 或  order by進行排序,我建議分的表越多越好,管他3721先分1000張表再說。

多分表的好處就是,只要表中的資料量足夠少,即便你索引設計的不好,甚至查詢完全不走索引,也不容易產生慢查詢。哈哈哈!

小結:這次重構就被老系統的1000張表給坑了,因為每張表只有幾萬條資料,我覺得太浪費了,  想當然的縮到了20張表。

           但又沒有很好的去分析查詢場景,設計索引。導致上線時,只放了1%的量,就崩了,看監控全部都是慢查詢。

           當然,最終我是通過優化索引來解決慢查詢,而不是加分表數量。但在有些情況下,這也是一種思路。

 

MySQL索引、欄位設計

之前自己設計表,總喜歡加些固定欄位,比如create_time, create_user, is_delete等,因為運維方便。

重構了這個系統之後發現,在高併發海量資料的情況下,效能是首要問題,有時候多加這些欄位反而成了負擔。(當然,大部分情況下,create_time還是必要的)

欄位能少則少,名字能短則短,型別能用tinyint就不要用int。

 

 

 

“桌子有多小就要多小,椅子有多擠就要多擠,不要讓客人坐得那麼舒服,吃完就趕快走。吸管有多粗就要多粗,冰有多大塊就放多大塊,這樣汽水就可以一口喝完再買另一杯了。你是新來的嗎,這還要我教,一點變通都不會,笨蛋。”

——周星馳《食神》

 

索引這塊低頻小資料量無所謂,高頻海量資料務必所有查詢走索引。

再看一些實際例子,

1. is_delete 欄位(邏輯刪除)

假設以評論為例,單表500w,單條動態下平均上萬條評論。

業務場景中要查詢動態下的所有評論,where 子句要加上條件 is_delete = 0。

如果查詢出符合條件的結果集,有幾萬甚至十幾萬條,不把 is_delete 欄位加到聯合索引中,這必將是一條慢查詢,再加上高併發,只要幾百的qps,很容易把服務打崩。

每個查詢加上這麼一個條件又有點畫蛇添足,除非運維需要,基本上不會有業務要查詢 is_delete = 1的情況。索性直接物理刪除,再加個歸檔表,要找回時,去歸檔表裡找。

這樣就不用在每個聯合索引裡多加一個欄位了。

2. tinyint 和 int 

tinyint 主要用於一些狀態標誌位,比如 稽核狀態:0-未稽核 1-稽核通過 2-稽核未通過。

使用tinyint 一是節約空間,二是方便識別,一看就知道是標誌位。

另外這種標誌位經常出現在查詢條件中,但又不會單獨作為查詢條件,因此建立索引時,必然是在聯合索引中出現。而聯合索引是有長度限制的,雖然大部分時候都不會遇到,但還是值得注意。

另外有的人標誌位喜歡用byte,但在程式碼裡要轉型就很蛋疼了。

3.聯合索引的設計

就一個原則:查詢條件裡有的,都加進去。

除了要把 where 子句中的條件欄位加進去外,在有order by 的情況下,還要把 order by 的欄位加到最後。

比如:查詢動態id是123,狀態是稽核通過且上線的20條評論,按時間倒序排列。

select * from comment where news_id = 123 and audit_status = 1 and online_status = 1 order by ctime desc limit 20

那我們應該建立聯合索引 news_id,  audit_status, online_status, ctime

注意:在網上參考資料時,很多都說索引的建立原則,欄位的區分度要高。

個人感覺這個原則並沒什麼道理,至少在建立聯合索引時不適用。

在建立單一索引時,我也沒有想到適用的具體場景。

比如有單表5千萬條身份資訊,其中20條gender=1,5千萬條gender=0。

如果你就是要查詢gender=1的列表,如果不在gender列建立索引,即便只有20條資料,也必然是個慢查詢。

小結:索引的建立,必須針對具體的查詢語句。結合實際查詢場景,去考慮如何建立索引。

 


   歡迎關注我的公眾號,可以免費提供技術諮詢

 

 

相關文章