鵝廠大佬親身經歷證明,一週上線百萬級併發系統

Java鬥帝之路發表於2020-12-08

本文是魚皮在騰訊實習期間,從零開始一週緊急上線百萬高併發系統的相關經驗、思路及感悟,分享給大家。

 

花 5 分鐘閱讀本文,你將收穫:

1. 加深對實際工作環境、工作狀態的瞭解

2. 學習高併發系統的設計思路、技術選型及理解

3. 學習工作中對接多方的溝通技巧

4. 學會與測試打配合的技巧

5. 學習緊急事故的處理方式

6. 事後如何進行歸納總結

7. 感受筆者爆肝工作的痛苦與掙扎

前言

從年前開始和導師二人接手了一個緊急專案,年前加班做完一期後專案效果顯著,於是年後開工立刻加急開發二期,目標是一週上線。由於專案業務邏輯複雜、工期緊、人手缺、對接方多,難度很大,極具挑戰性,因此和導師二人開始了 007 的爆肝工作。

遠端辦公無疑為 007 無休工作制提供了有利條件,那段時間,我做夢都在敲程式碼。

專案介紹

首先要介紹下負責的專案及系統。專案背景、業務等資訊自然不能透露,這裡剝離業務,僅介紹關鍵系統模型,如下圖:

如圖,我負責的是一個狀態流轉系統和查詢系統,以及它們依賴的資料庫服務。

狀態流轉系統的作用是按照邏輯修改資料庫中某條資料的狀態欄位,並在修改成功後依據狀態向其他業務側傳送通知。

查詢系統,顧名思義就是從資料庫中查詢資料,包括最基礎的鑑權、查詢等功能。

先分析一下系統中一些難點:

1. 查詢系統是一個高扇入服務,被其他各業務側呼叫,會存在三個問題:

高併發:將各業務側請求量聚集,經評估,會產生百萬量級的高併發請求。

相容性:如何設計一套 API,滿足各業務側需求的同時容易被理解。

對接複雜:要同時與多個業務側的同學溝通來討論介面,想想就是一件很複雜的事情。

2. 狀態流轉系統的業務邏輯相當複雜。

3. 狀態流轉系統和查詢系統、其他業務側之間存在互動(比如互相傳送通知和呼叫),對時延、容錯性、一致性的要求很高。

分析出了難點,在寫程式碼之前,要先編寫可行的技術方案

 

設計思路

在實際工作中,編寫詳細的技術方案是非常有必要的。優秀的工程師會在技術方案中考慮到各種場景、評估各種風險、工作量估時、記錄各種問題等,不僅幫助自己梳理思路、歸納總結,同時也給其他人提供了參照以及說服力(比如你預期7天上線,沒有方案誰信你?)。

根據二八定理,複雜的系統中,編寫技術方案、梳理設計思路的時間和實際敲程式碼開發的時間比例為 8 : 2。

設計遵循的原則是 “貼合業務”沒有最好的架構,只有最適合業務的架構。切忌過度設計!

此外,還要考慮專案的緊急程度和人力成本,先保證可用,再追求極致。

一些簡單的設計這裡就略過了,下面針對系統難點和業務需求,列舉幾個重點設計及技術選型

 

1. 高併發

提到高併發,大家首先想到的是快取和負載均衡,缺一不可。

負載均衡說白了就是 “砸錢,加機器!”,但是為公司省機器、節約成本是每位後端工程師的信仰,這就要靠技術選型和架構設計來實現了。目標是儘可能利用每臺機器的資源,抗住最大的併發請求。

 

選型如下:

程式設計框架:選擇輕量級的 Restful 框架 Jersey,搭配輕量級依賴注入庫 Guice

Web伺服器:選擇高效能的輕量級 NIO 伺服器 Grizzly

快取:騰訊自研海量分散式儲存系統 CKV+(支援Redis協議,有資料監控平臺)

資料庫分庫分表:選用公司自研的基礎設施,不細說了

負載均衡:輕量級反向代理伺服器 Nginx 和 L5 負載均衡,百萬併發需要增加十餘臺機器

CDN 及預熱:能夠支援高效的檔案下載服務

其中,快取是抗住高併發流量的關鍵,須重點設計。

快取方案

1. 資料結構設計

用過快取的同學都瞭解,關於快取 Key 的設計是很重要的。根據業務來,保證快取 key 之間不衝突、便於查詢就好。此處我選擇請求引數 + 介面唯一 id 來拼接 key。並且分頁查詢介面可複用全量查詢介面的快取。

2. 快取降級

找不到對應 key / redis 連線失敗時直接查庫。

3. 快取更新

當資料庫發生修改時,需要對快取進行刪除。由於存在非必填的請求引數,因此快取 key 可能是一個模糊值。比如有 a、b 兩個請求引數,key 可能為 “a”,也可能為 “ab”。

針對請求欄位固定(所有欄位必填)的介面,更新快取時,直接拼接出唯一的 key 進行刪除即可。

而針對請求欄位不固定(存在非必填欄位)的介面,可使用 redis 的 scan 命令範圍掃描(不要用 keys 命令!)或者通過迴圈拼接出所有可能的 key。比如使用 scan 命令清除所有 key 字首為 user1 的快取。

4. 快取穿透

無論查詢出的列表是否為空,都寫入快取。但在業務會返回多種錯誤碼時,不建議採用這種方式,複雜度高,成本太大。

 

2. 相容性

相容性主要考察介面的設計,為相容多個業務側,需要將請求引數以及響應引數設定的儘可能靈活。在設計介面時,切忌一定要和所有的業務側對齊,否則一個欄位設計不當可能導致滿盤皆輸!

這裡有三個技巧

1. 提供可訪問連結的文件,供呼叫方即時查閱(比如騰訊文件)。

2. 請求引數不能過多,且要易於理解,不能為了強制相容而設定過於複雜的引數,必要時可針對某一業務側定製介面。

3. 響應引數儘量多(多不是濫),要知道每次增加返回欄位都要修改程式碼,而適當冗餘的欄位避免了此問題。

3. 訊息通知

上面介紹難點時提到:狀態流轉系統與查詢系統、其他業務側存在互相傳送通知的互動。當狀態流轉時,需要通知其他業務,還要查詢系統立即更新快取。對訊息的實時性要求很高。

這裡最初有兩種方案

1. 各系統提供回撥介面,用於接收通知。能保證實時性,但是各系統間緊耦合,不利於擴充套件。

2. 使用訊息佇列,實現應用解耦及非同步訊息。

最後還是採取了第二種方案,並選用騰訊自研的 TubeMQ(萬億級分散式訊息中介軟體,已開源Apache孵化),原因如下:

1. 狀態流轉系統的通知資料之後可能存在其他消費方,使用訊息佇列利於擴充套件,對程式碼侵入性也少。

2. 訊息佇列可持久化訊息

3. TubeMQ 支援消費方負載均衡,效能高

4. TubeMQ 容量大,可存放萬億數量級訊息

5. 支援公司自研元件,便於形成統一規範

在技術選型和確定方案時,不僅要關注當前的業務需求,也要有一定的前沿視角。

 

4. 風險評估

切忌,在選用中介軟體 / 框架前,要儘可能多的進行了解,評估其可能帶來的風險。一般公司內都有自己的知識庫,可以利用好內部資源或者找谷歌度娘。

這裡我評估了 TubeMQ 帶來的風險,從訊息可靠性、訊息順序性、訊息重複、監控告警等多個角度進行了分析,還是發現了一些可能的風險。比如當消費方消費資料狀態改變的訊息失敗時,快取未被及時更新,導致資料庫和快取中的資料不一致。

那麼,如何規避風險呢?我從訊息佇列生產方和消費方的角度設計了訊息可靠性和資料一致性的解決方案。

 

解決方案

生產方訊息可靠性:

1. Tube 可保證訊息一定送達,傳送失敗時會自動重發。

2. 傳送訊息結束時會觸發回撥,回撥裡可判斷訊息傳送及確認狀態,可將傳送失敗的訊息放入佇列,下次傳送優先從佇列裡取。

消費方訊息可靠性和資料一致性:

1.消費失敗時進行最多三次重試

2.重試後仍消費失敗,則記錄日誌,確保訊息不丟失

3.通過定時任務讀取日誌,嘗試再次消費失敗訊息,並進行告警

開發過程

其實開發過程沒什麼好說的,就是按照技術方案去敲程式碼。

這裡也有幾個小竅門

1. 同時開發多個專案時,可以每個專案一個獨立 Git 分支,合併的時候分批合併,否則別人閱讀你提交的程式碼時會非常累!

2. 給每個請求做一些打點資料上報,比如請求量、請求時間、失敗請求數,便於監控統計。

3. 多記錄日誌,詳細清晰的日誌可以幫助我們快速定位故障

 

問題解決

很多問題在本地開發時是察覺不到的,在測試及線上環境才會被發現。問題解決的過程就像坐雲霄飛車,經常的狀態是:測試 => 開發 => 測試 => 上線 => 開發 => 測試,迴圈往復。

兩個溫馨小貼士:

1. 遇到問題時,千萬不要慌,可以先深呼吸幾口氣,因為問題一定是可以解決的,解決不了那麼你可能要被解決了!

2. 解決問題後,千萬別激動,可以先深呼吸幾口氣,因為你還會產生新的問題,而且往往新問題更嚴重!

下面分享一些讓魚皮印象深刻的問題。

1. 事務提交時報錯?

原因:事務中呼叫的函式裡也有事務,因此事務裡套了事務,破壞了隔離性。

解決:修改程式碼,保證事務隔離性。

2. 依賴包存在,專案啟動卻報錯?

原因:存在多版本 jar 包,導致 Java 程式碼使用反射機制動態生成類時不知道使用哪個 jar 包裡的類。

解決:刪掉多餘版本 jar 包。

 

3. 快取未即時更新

原因:經排查,是由於實際的快取 key 數量可達千萬級,導致更新快取時使用 scan 命令掃描的效率過低,長達20多秒!

解決:修改更新快取的方案,不再使用 scan 命令,而是在業務程式碼中拼湊出所有可能的 keys,依次刪除。

 

以為這個問題這樣就結束了?不要忘記上面的小貼士:

“解決問題後,千萬別激動,可以先深呼吸幾口氣,因為你還會產生新的問題,而且往往新問題更嚴重!”

 

4. 快取仍未即時更新?

原因:某業務側要求資料強一致性,快取和資料庫中的狀態必須完全一致!而快取雖然是毫秒級更新,但無法做到實時一致。

解決:為該業務側定製一個介面,該介面不查詢快取,直接查資料庫,保證查到的資料一定是最新值。

 

5. 請求卡死

服務執行一段時間後,發現所有的請求都被阻塞了!心臟受不了。

原因:使用 jstack 列印執行緒資訊後分析 thread_dump 檔案,發現是由於快取類庫 Jedis 未手動釋放連線導致連線數耗盡,導致新的請求執行緒會不斷等待 Jedis 連線釋放,從而卡死。

解決:補充釋放 Jedis 連線的程式碼即可。

 

6. 線上環境分析日誌時突然告警,磁碟 IO 佔用超過 99%!

原因:誤用 cat 命令檢視未分割的原始日誌檔案,由於日誌檔案太大(幾十 GB),導致磁碟 IO 直接刷爆!

解決:使用 less、tail、head等命令代替 cat,並刪除已備份的大日誌檔案。

 

7. 程式閃退

排查:通常 JVM 程式閃退是有錯誤日誌的,但是並沒有找到,排查陷入絕境。沒辦法,只能祈禱問題不再復現。後來問題真的沒出現過了,謝謝!

原因:後來,經詢問,是有人手動 kill 掉了這個程式。好的,***。

8. 線上環境的訊息通知傳送成功了,怎麼沒有預期的資料更新效果?

定位思路:先看訊息是否被消費,再看對訊息的處理是否正確。

排查:檢視線上日誌,發現訊息並未被消費;但是檢視監控介面,發現訊息被測試環境的機器消費了!

原因:由於測試環境和線上環境屬於同一個消費組,當訊息到達時,同一個消費組只有一個消費者能夠成功消費該訊息,被測試環境消費掉了,導致線上環境資料沒更新。

發現這個問題的時候,已經是上線前一天的深夜。再申請一個消費組已經來不及了,情急之下,只能先下掉測試環境的服務。第二天申請好消費組後,根據環境去區分使用哪個消費組就可以了,這樣每個消費組都會獨立消費訊息,成功避免了訊息競爭。

 

9. 報告!流量太大,撐不住啊!

原因:機器不夠,需進行緊急擴容

解決:緊急新申請了 10 臺機器,完成初始化配置,成功部署新機器後,成功增大了併發度。

小技巧:多個機器做相同操作時,有兩種快捷的做法。

1. 利用 SSH 連線工具自帶的並行操作功能,自動給所有機器鍵入命令( XShell 軟體支援)

2. 配置好一臺機器後,可使用 rsync 命令同步配置至其他機器

 

10. 上線前一天你跟我說介面設計有問題?

原因:溝通出現嚴重問題!

工作中,一些同事因為自身業務繁忙,可能在核對介面設計方案的時候沒有注意。等他們忙完了,會反覆 @ 你、私聊你詢問。我們一定不要這樣!

解決:緊急電話會議,拉群核對方案

 

11. 線上出 bug 了!

線上出 bug,是一件很大的事,必須緊急響應。在夢裡也得給我爬起來!

原因:測試環境和線上環境未必完全一致,且測試環境未必能測出所有問題。因此驗證時通常需要預釋出環境,資料使用線上資料,但卻是獨立的伺服器,保證不影響線上。

解決:緊急排查定位問題,三分鐘成功修復!

修復 bug 有一定的技巧,分享下個人的排錯路徑:

截圖 / 問題 => 請求 => bug 是否可復現,和測試緊密配合 => 資料 => 資料來源(真實資料與介面資料是否一致) => 資料處理

解釋一下:

通常發現問題的是運維、使用者或者測試,他們會丟擲一個問題或者問題的相關的截圖,這時,我們要快速想到這個問題對應的功能(即對應的請求/介面),然後讓問題描述者儘可能多的提供資訊(比如請求引數、問題時間等)。

如果問題時間較久,看日誌及監控不易排查,可以詢問是否可以造一個復現該問題的case,這樣只需觀察最新的日誌即可,方便排錯。

定位到請求後,我們要分析請求及響應的哪些資料是異常的,即定位關鍵資料,然後定位資料來源(是從資料庫查的,還是從快取查的),並觀察響應資料與真實資料來源是否一致。如果不一致,可能是業務邏輯中對資料的處理出現了問題,再進一步去做分析。

高效溝通建議:描述問題,儘量用資料說話,給出截圖的同時,要提供完整的資料、請求等資訊,有助他人分析。

 

12. 線上出現部分錯誤資料

這是一個可以預見的問題。還好已經在專案中配置了郵件告警,能夠報告錯誤資料的資訊,錯誤資料量也不大。

解決:修復導致錯誤資料的 bug 後,編寫程式迴圈所有錯誤資訊並生成請求程式碼,然後手動執行請求程式碼,重新整理線上不同步資料即可。

建議:設計時還是要儘可能考慮到風險,可以按照問題的嚴重程度做分級報警策略(簡訊 > 郵件 > 通訊軟體)。

 

13. 線上機器 OOM!

上線三天後發現的問題,部分線上機器竟然出現了OOM(堆記憶體溢位)的情況,導致服務不可用。經排查,是使用的第三方中介軟體的當前版本存在 bug, 所以說在使用元件前要充分調研和風險評估,選擇正確的版本。

 

血淚教訓

1. 有問題一定儘可能在測試環境去解決,否則線上出問題對心臟很不友好。

2. 不要盲目樂觀,以為上線就沒問題,要多驗證,保持警惕。

3. 使用第三方依賴時,一定要嚴格核對依賴版本號,確保穩定版本。使用老版本或版本不一致可能導致嚴重 bug!

 

上線後如果發現問題,會經歷如下流程,我稱它為 hapy 流程

比如當發現 DB 服務的 bug 後,你只需要改 DB 服務的一行程式碼。然而還要做

1. 修改DB服務的一行程式碼

2. 跑單元測試

3. DB服務打成依賴包

4. 修改“狀態流轉系統”、“查詢系統”對DB服務的依賴包(改動版本號/更新本地快取拉取最新包)

5. 重新發布“狀態流轉系統”、“查詢系統”至測試環境

6. 可能還要重新交給測試的同學進行迴歸測試

7. 測試通過,再次提交“狀態流轉系統”、“查詢系統”的程式碼,發起 CR(程式碼審查)

8. 找同事或 Leader 讀程式碼,通過 CR

9. 合併分支

10. 釋出 “狀態流轉系統”、“查詢系統” 至線上環境,每發一臺機器,都要進行一次驗證(滾動部署)。

11. 再次發現新的bug

這是一件噁心到爆炸的事情,但是在第 2、6、8 步驟時,是存在空餘等待時間的。這時我們可以做做其他工作,記錄一下工作內容、問題等。

 

總結

首先總結一下這個專案各階段的耗時:

理解需求:5%

開發:15%

溝通確認問題:30%

測試及驗證:30%

上線及驗證:20%

其中,修復 bug 貫穿後面的幾個流程,大概佔了總時間的60%。

 


專案過程存在的問題:

1. 前期未參與需求評審,瞭解的資訊較少。

2. 上線前一天晚上,竟然還在臨時對齊介面?這是在溝通方案階段應該確認好的。

3. 大約 80% 的時間花在溝通、查詢資料、提供資料及驗證。

4. 自己沒測試完,就開始串測,導致同一個 bug 被多方發現,反覆 @,導致改 bug 效率低下。

5. 對自研中介軟體的不熟悉,導致花費的時間成本較高。

6. 全域性觀還不夠,不能提前預見到一些可能的問題。

7. 對中介軟體調研不夠,在最初未核對依賴版本號導致線上機器 OOM。

 

自我感覺良好的地方:

1. 和測試同學配合緊密,互相體諒,測試效率較高

2. 為查詢系統編寫了詳細的介面文件,上傳至公司知識庫供實時查閱

3. 最快 3 分鐘緊急修復線上 bug

4. 最快 30 分鐘從接受需求到上線

5. 在發現中介軟體問題時,即時和對接方溝通,設計出了對其無任何影響的低成本解決方案

6. 積極幫助其他同學查詢資料,排查問題

7. 編寫指令碼高效解決部分錯誤資料

 

成長與收穫:

1. 抗壓熬夜能力 ↑

2. 設計思維能力 ↑

3. 溝通能力 ↑

4. 解決問題能力 ↑

5. 高階命令熟悉度 ↑

6. 中介軟體熟悉度 ↑

7. 叢集管理能力 ↑

8. 拒絕需求能力 ↑

9. 吐槽能力 ↑

10. 吹 ? 能力 ↑

後續

專案上線後,通過總結覆盤,發現了專案中值得優化的地方,也思考到了一些更健全的機制,將逐漸去實現。比如:

1. 兩個系統中有部分相同的配置

目前採用複製貼上的方式去同步相同的配置,這種方式的優點是比較簡單。但缺點也很明顯,如果一個系統的配置改了,而忘了修改另一個系統的配置,就會出現錯誤。

事實上,可以引入一個配置中心,集中管理多個系統的配置檔案,並且支援手動修改、多環境、灰度、配置版本回退等功能。

可以採用阿里的 Nacos 或攜程的 Apollo,提供了介面來管理配置。

 

2. 曾經的程式閃退問題,必須重視!

無法保證程式不閃退,但是可以對程式實時監控,並自動對閃退程式進行重啟。

實現方式有兩種:

1. 使用工具,例如 supervisor 或 monit,可以對程式進行管理和閃退重啟

2. 編寫 shell 指令碼,再通過定時任務,實現週期性觀察程式狀態及重啟。推薦將定時任務接入分散式任務排程平臺,尤其當定時任務很多時,進行視覺化的管理和方便的控制排程是必要的!

 

3. 訊息佇列可靠性保障

1. 訊息重傳機制:如方案所說,設計重傳佇列,再次傳送時優先取重傳佇列中的訊息傳送。但注意要避免佇列無限重傳,須給每個訊息設定重傳次數閾值。

2. 郵件告警:如果訊息重傳次數超過閾值,直接傳送郵件告警,不再將該訊息入隊。

 

工作真是簡單而不簡單,誰說後端只是 CRUD(增刪改查)?


END

 

推薦閱讀:

位元組跳動總結的設計模式 PDF 火了,完整版開放分享

刷Github時發現了一本阿里大神的演算法筆記!標星70.5K

如果能聽懂這個網約車實戰,哪怕接私活你都可以月入40K

為什麼阿里巴巴的程式設計師成長速度這麼快,看完他們的內部資料我懂了

程式設計師達到50W年薪所需要具備的知識體系。

關於【暴力遞迴演算法】你所不知道的思路



看完三件事❤️

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。

關注公眾號 『 Java鬥帝 』,不定期分享原創知識。

同時可以期待後續文章ing?

 

原文地址:https://mp.weixin.qq.com/s/j-D16UMks51uy-NbRShK5Q

 

相關文章