天天看點

一個高并發項目到落地的心酸路

作者:dbaplus社群

前言

最近閑來沒事,摸魚看了不少高并發相關的文章,突然有感而發,想到了幾年前做的一個項目,也多少和高并發有點關系。

這裡我一邊回憶落地細節一邊和大家分享下,興許能給大家帶來點靈感。

一、需求及背景

先來介紹下需求,首先項目是一個志願填報系統,既然會扯上高并發,相信大家也能猜到大緻是什麼的志願填報。

核心功能是兩塊,一是給考生填報志願,二是給老師維護考生資料。

本來這個項目不是我們負責,奈何去年公司負責這個項目的組遭到了甲方嚴重的投訴,說很多考生用起來卡頓,甚至把沒填上志願的責任歸到系統上。

甲方明确要求,如果這年再出現這種情況,公司在該省的所有項目将面臨被替換的風險。

讨論來讨論去,最後公司将任務落到我們頭上時,已經是幾個月後的事了,到臨危受命階段,剩下不到半年時間。

雖然直屬上司讓我們不要有心理負擔,做好了表揚,做不好鍋也不是我們的,但明顯感覺到得到他的壓力,畢竟一個不小心就能上新聞。

二、分析

既然開始做了,再說那些有的沒的就沒用了,直接開始分析需求。

首先,業務邏輯并不算複雜,難點是在并發和資料準确性上。與客戶溝通後,大緻了解并發要求後,于是梳理了下。

  • 考生端登入接口、考生志願資訊查詢接口需要4W QPS
  • 考生儲存志願接口,需要2W TPS
  • 報考資訊查詢4W QPS
  • 老師端需要4k QPS
  • 導入等接口沒限制,可以異步處理,隻要保證将全部資訊更新一遍在20分鐘以内即可,同時故障恢複的時間必須在20分鐘以内(硬性要求)
  • 考生端資料要求絕對精準,不能出現遺漏、錯誤等和考生操作不一緻的資料
  • 資料脫敏,防僞
  • 資源是有限的,提供幾台實體機

大的要求就這麼多,主要是在有限資源下需要達到如此高的并發确實需要思考思考,一般的crud根本達不到要求。

三、方案研讨

接下來我會從當時我們切入問題的點開始,從前期設計到項目落地整個過程的問題及思考,一步步去展示這個項目如何實作的。

首先,我們沒有去設計表,沒有去設計接口,而是先去測試。測試什麼?測試我們需要用到或可能用到的中間件是否滿足需求。

MySQL

首先是MySQL,單節點MySQL測試它的讀和取性能,建立一張user表。

向裡面并發插入資料和查詢資料,得到的TPS大概在5k,QPS大概在1.2W。查詢的時候是帶id查詢,索引列的查詢不及id查詢,差距大概在1k。

insert和update存在細微并發差距,但基本可以忽略,影響更新性能目前最大的問題是索引。如果表中帶索引,将降低1k-1.5k的TPS。

目前結論是,MySQL不能達到要求,能不能考慮其他架構,比如MySQL主從複制,寫和讀分開。

測試後,還是放棄,主從複制結構會影響更新,大概下降幾百,而且單寫的TPS也不能達到要求。

至此結論是,MySQL直接上的方案肯定是不可行的。

Redis

既然MySQL直接查詢和寫入不滿足要求,自然而然想到加入redis緩存。于是開始測試緩存,也從單節點redis開始測試。

get指令QPS達到了驚人的10w,set指令TPS也有8W,意料之中也驚喜了下,仿佛看到了曙光。

但是,redis容易丢失資料,需要考慮高可用方案。

實作方案

既然redis滿足要求,那麼資料全從redis取,持久化仍然交給MySQL,寫庫的時候先發消息,再異步寫入資料庫。

最後大體就是redis + rocketMQ + MySQL的方案。看上去似乎挺簡單,當時我們也這樣以為,但是實際情況卻是,我們過于天真了。

這裡主要以最重要也是要求最高的儲存志願資訊接口開始攻略。

1)故障恢複

第一個想到的是,這些個節點挂了怎麼辦?

MySQL挂了比較簡單,他自己的機制就決定了他即使挂掉,重新開機後仍能恢複資料,這個可以不考慮。

rocketMQ一般情況下挂掉了可能會丢失資料,經過測試發現,在高并發下,确實存在丢消息的現象。原因是它為了更加高效,預設采用的是異步落盤的模式,這裡為了保證消息的絕對不丢失,修改成同步落盤模式。

然後是最關鍵的redis,不管哪種模式,redis在高并發下挂掉,都會存在丢失資料的風險。資料丢失對于這個項目格外緻命,優先級甚至高于并發的要求。

于是,問題難點來到了如何保證redis資料正确,讨論過後,決定開啟redis事務。

儲存接口的流程就變成了以下步驟:

  • redis 開啟事務,更新redis資料
  • rocketMQ同步落盤
  • redis 送出事務
  • MySQL異步入庫

我們來看下這個接口可能存在的問題。

第一步,如果redis開始事務或更新redis資料失敗,頁面報錯,對于資料正确性沒有影響

第二步,如果rocketMQ落盤報錯,那麼就會有兩種情況。

情況一,落盤失敗,消息發送失敗,好像沒什麼影響,直接報錯就可。

情況二,如果發送消息成功,但提示發送失敗(無論什麼原因),這時候将導緻MySQL和redis資料的最終不一緻。

如何處理?怎麼知道是redis的有問題還是MySQL的有問題?出現這種情況時,如果考生不繼續操作,那麼這條錯誤的資料必定無法被更新正确。

考慮到這個問題,我們決定引入一個時間戳字段,同時啟動一個定時任務,比較MySQL和redis不一緻的情況,并自主修複資料。

首先,redis中記錄時間戳,同時在消息中也帶上這個時間戳并在入庫時記錄到表中。

然後,定時任務30分鐘執行一次,比較redis中的時間戳是否小于MySQL,如果小于,便更新redis中資料。如果大于,則不做處理。

同時,這裡再做一層優化,淩晨的時候執行一個定時任務,比較redis中時間戳大于MySQL中的時間戳,連續兩天這條資料都存在且沒有更新操作,将提示給我們手動運維。

然後是第三步,消息送出成功但是redis事務送出失敗,和第二步處理結果一緻,将被第二個定時任務處理。

這樣看下來,即使redis崩掉,也不會丢失資料。

第一輪壓測

接口實作後,當時懷着期待,信心滿滿去做了壓測,結果也是當頭棒喝。

首先,資料準确性确實沒有問題,不管突然kill掉哪個環節,都能保證資料最終一緻性。

但是,TPS卻隻有4k不到的樣子,難道是節點少了?

于是多加了幾個節點,但是仍然沒有什麼起色。問題還是想簡單了。

重新分析

經過這次壓測,之後一個關鍵的問題被提了出來,影響接口TPS的到底是什麼???

一番讨論過後,第一個結論是:一個接口的響應時間,取決于它最慢的響應時間累加,我們需要知道,這個接口到底慢在哪一步或哪幾步?

于是用arthas看了看到底慢在哪裡?

結果卻是,最慢的竟然是redis修改資料這一步!這和測試的時候完全不一樣。于是針對這一步,我們又繼續深入探讨。

結論是:

redis本身是一個很優秀的中間件,并發也确實可以,選型時的測試沒有問題。

問題出在IO上,我們是将考生的資訊用json字元串存儲到redis中的(為什麼不儲存成其他資料結構,因為我們提前測試過幾種可用的資料結構,發現redis儲存json字元串這種性能是最高的),而考生資料雖然單條大小不算大,但是在高并發下的上行帶寬卻是被打滿的。

于是針對這種情況,我們在儲存到redis前,用gzip壓縮字元串後儲存到redis中。

為什麼使用gzip壓縮方式,因為我們的志願資訊是一個數組,很多重複的資料其實都是字段名稱,gzip和其他幾個壓縮算法比較後,綜合考慮到壓縮率和性能,在當時選擇了這種壓縮算法。

針對超過限制的字元串,我們同時會将其拆封成多個(實際沒有超過三個的)key存儲。

繼續壓測

又一輪壓測下來,效果很不錯,TPS從4k來到了8k。不錯不錯,但是遠遠不夠啊,目标2W,還沒到它的一半。

節點不夠?加了幾個節點,有效果,但不多,最終過不了1W。

繼續深入分析,它慢在哪?最後發現卡在了rocketMQ同步落盤上。

同步落盤效率太低?于是壓測一波發現,确實如此。

因為同步落盤無論怎麼走,都會卡在rocketMQ寫磁盤的地方,而且因為前面已經對字元串壓縮,也沒有帶寬問題。問題到這突然停滞,不知道怎麼處理rocketMQ這個點。

同時,另一個同僚在測試查詢接口時也帶來了噩耗,查詢接口在1W2左右的地方就上不去了,原因還是卡在帶寬上,即使壓縮了字元串,帶寬仍被打滿。

怎麼辦?考慮許久,最後決定采用較正常的處理方式,那就是資料分區,既然單個rocketMQ服務性能不達标,那麼就水準擴充,多增加幾個rocketMQ。

不同考生通路的MQ不一樣,同時redis也可以資料分區,幸運的是正好redis有哈希槽的架構支援這種方式。

而剩下的問題就是如何解決考生分區的方式,開始考慮的是根據id進行求餘的分區,但後來發現這種分區方式資料分布極其不均勻。

後來稍作改變,根據證件号後幾位取餘分區,資料分布才較為均勻。有了大體解決思路,一頓操作後繼續開始壓測。

一點小意外

壓測之後,結果再次不如人意,TPS和QPS雙雙不增反降,繼續通過arthas排查。

最後發現,redis哈希槽通路時會在主節點先計算key的槽位,而後再将請求轉到對應的節點上通路,這個計算過程竟然讓性能下降了20%-30%。

于是重新修改代碼,在java記憶體中先計算出哈希槽位,再直接通路對應槽位的redis。如此重新壓測,QPS達到了驚人的2W,TPS也有1W2左右。

不錯不錯,但是也隻到了2W,再想上去,又有了瓶頸。

不過這次有了不少經驗,馬上便發現了問題所在,問題來到了nginx,仍然是一樣的問題,帶寬!

既然知道原因,解決起來也比較友善,我們将唯一有大帶寬的實體機上放上兩個節點nginx,通過vip代理出去,通路時會根據考生分區資訊通路不同的位址。

壓測

已經記不清第幾輪壓測了,不過這次的結果還算滿意,主要查詢接口QPS已經來到了驚人的4W,甚至個别接口來到6W甚至更高。

勝利已經在眼前,唯一的問題是,TPS上去不了,最高1W4就跑不動了。

什麼原因呢?查了每台redis主要性能名額,發現并沒有達到redis的性能瓶頸(上行帶寬在65%,cpu使用率也隻有50%左右)。

MQ呢?MQ也是一樣的情況,那出問題的大機率就是java服務了。分析一波後發現,cpu基本跑到了100%,原來每個節點的最大連結數基本占滿,但帶寬竟然還有剩餘。

靜下心來繼續深入探讨,連接配接數為什麼會滿了?原因是當時使用的SpringBoot的内置容器tomcat,無論如何配置,最大連接配接數最大同時也就支援1k多點。

那麼很簡單的公式就能出來,如果一次請求的響應時間在100ms,那麼1000 * 1000 / 100 = 10000。

也就是說單節點最大支援的并發也就1W,而現在我們儲存的接口響應時間卻有300ms,那麼最大并發也就是3k多,目前4個分區,看來1W4這個TPS也好像找到了出處了。

接下來就是優化接口響應時間的環節,基本是一步一步走,把能優化的都優化了一遍,最後總算把響應時間控制在了100ms以内。

那麼照理來說,現在的TPS應該會來到驚人的4W才對。

再再次壓測

懷着忐忑又激動的心情,再一次進入壓測環節,于是,TPS竟然來到了驚人的2W5。

當時真心激動了一把,但是冷靜之後卻也奇怪,按正常邏輯,這裡的TPS應該能達到3W6才對。

為了找到哪裡還有未發現的坑(怕上線後來驚喜),我們又進一步做了分析,最後在日志上找到了些許端倪。

個别請求在連結redis時報了連結逾時,存在0.01%的接口響應時間高于平均值。

于是我們将目光投向了redis連接配接數上,繼續一輪監控,最終在業務實作上找到了答案。

一次儲存志願的接口需要執行5次redis操作,分别是擷取鎖、擷取考生資訊、擷取志願資訊、修改志願資訊、删除鎖,同時還有redis的事務。

而與之相比,查詢接口隻處理了兩次操作,是以對于一次儲存志願的操作來看,單節點的redis最多支援6k多的并發。

為了驗證這個觀點,我們嘗試将redis事務和加鎖操作去掉,做對照組壓測,發現并發确實如預期的一樣有所提升(其實還擔心一點,就是搶鎖逾時)。

準備收工

至此,好像項目的高并發需求都已完成,其他的就是完善完善細節即可。

于是又又又一次迎來了壓測,這一次不負衆望,重要的兩個接口均達到了預期。

這之後便開始真正進入業務實作環節,待整個功能完成,在曆時一個半月帶兩周的加班後,終于迎來了提測。

提測後的問題

功能提測後,第一個問題又又又出現在了redis,當高并發下突然kill掉redis其中一個節點。

因為用的是哈希槽的方式,如果挂掉一個節點,在恢複時重新算槽将非常麻煩且效率很低,如果不恢複,那麼将嚴重影響并發。

于是經過讨論之後,決定将redis也進行手動分區,分區邏輯與MQ的一緻。

但是如此做,對管理端就帶來了一定影響,因為管理端是清單查詢,是以管理端擷取資料需要從多個節點的redis中同時擷取。

于是管理端單獨寫了一套擷取資料分區的排程邏輯。

第二個問題是管理端接口的性能問題,雖然管理端的要求沒考生端高,但扛不住他是分頁啊,一次查10個,而且還需要拼接各種資料。

不過有了前面的經驗,很快就知道問題出在了哪裡,關鍵還是redis的連接配接數上,為了降低連結數,這裡采用了pipeline拼接多個指令。

上線

一切準備就緒後,就準備開始上線。說一下應用布置情況,8+4+1+2個節點的java服務,其中8個節點考生端,4個管理端,1個定時任務,2個消費者服務。

3個ng,4個考生端,1個管理端。

4個RocketMQ。

4個redis。

2個mysql服務,一主一從,一個定時任務服務。

1個ES服務。

最後順利上線,雖然發生了個别線上問題,但總體有驚無險,而正式回報的并發數也遠沒有到達我們的系統極限,開始準備的水準擴充方案也沒有用上,無數次預演過各個節點的當機和增加分區,一般在10分鐘内恢複系統,不過好在沒有派上用場。

最後

整個項目做下來感覺越來越偏離面試中的高并發模式,說實在的也是無奈之舉。偏離的主要原因我認為是項目對資料準确性的要求更高,同時需要完成高并發的要求。

但是經過這個項目的洗禮,在其中也收獲頗豐,懂得了去監控服務性能名額,然後也加深了中間件和各種技術的了解。

做完之後雖然累,但也很開心,畢竟在有限的資源下去分析性能瓶頸并完成項目要求後,還是挺有成就感的。

再說點題外話,雖然項目成功挽回了公司在該省的形象,也受到了總公司和上司表揚,但最後也就這樣了,實質性的東西一點沒有,這也是我離開這家公司的主要原由。不過事後回想,這段經曆确實讓人難忘,也給我後來的工作帶來了很大的幫助。

從以前的crud,變得能去解決接口性能問題。這之前一遇上,可能兩眼茫然或是碰運氣,現在慢慢會根據蛛絲馬迹去探究優化方案。

不知道我在這個項目的經曆是否能引起大家共鳴?希望這篇文章能對你有所幫助。

作者丨青鳥218

來源丨juejin.cn/post/7346021356679675967

dbaplus社群歡迎廣大技術人員投稿,投稿郵箱:[email protected]

活動推薦

2024 XCOPS智能運維管理人年會·廣州站将于5月24日舉辦,深究大模型、AI Agent等新興技術如何落地于運維領域,賦能企業智能運維水準提升,建構全面運維自治能力!

一個高并發項目到落地的心酸路

會議詳情:2024 XCOPS智能運維管理人年會-廣州站