如何預估接口上線后的 QPS
問題引入
這個問題其實是一個非常實際的問題,因為我們在開發需求后,例如:新增了一個接口
有一個步驟是值得做的,那就是預估這個接口的QPS
因為我們是可以去調配對應服務器的數量和運行配置的
- 例如我可以從2個節點新增為4個節點
- 例如我可以將節點的內存從2G變成4G,2核CPU變成4核CPU
如果因為 沒有正確的預估 以及 沒有正確的調整,導致接口的QPS過高,扛不住系統崩潰,那就是嚴重的外網事故了
注意,這里說的預估QPS,是假設有一個真實的項目、真實的環境,預估這個接口上線后會有多少 QPS而不是在預估我們的系統接口本身能抗住多少QPS而不崩潰
如何預估
預估一個接口的 QPS,本質上是在考慮這個接口被調用的頻繁程度
1.分析業務
- 業務類型: 不同的業務類型的QPS是不太一樣的,再細化一些,接口的做的事情是不同的,QPS是不同的,例如這個接口是 獲取商城首頁數據(QPS估計會高)、刪除好友(QPS估計會低)
- 活躍用戶: 有多少活躍用戶,活躍用戶多(QPS估計會高),活躍用戶少(QPS估計會低)
2.計算平均的QPS:根據活躍用戶數和業務類型估算
- 假設有100w個活躍用戶(數值由活躍用戶決定)
- 假設經過歷史數據的統計,每個用戶平均每天產生10次請求(數值由業務類型決定)
- 則可估算出平均每秒請求量(QPS)為 (10 * 100w)/ 86400= 116
3.計算峰值的QPS:假設業務類型是有峰值情況(特殊事件帶來的流量波動)的,即我們的接口可能會達到更高的QPS
- 峰值情況通常是的平均值的數倍,假設峰值為平均值的3倍,則峰值QPS約為348
通過預估接口上限后可能的峰值QPS后,就可以相對應的調整服務器節點的數量、服務器的運行配置需要能抗住峰值QPS,記得給自己留點緩沖,即把值設置的略大一些
如何預估/回答接口本身能抗住多少 QPS
與哪些因素有關
后端服務器集群節點數量,數量越多,QPS越高
后端服務器節點的運行配置:運行內存、Cpu核數等等,硬件資源決定單節點處理能力
接口本身做的事情
- 做的事情多耗時長(預估QPS會相對應低)
- 做的事情少耗時短(預估QPS會相對應高)
系統架構:
- 完善的流量負載均衡架構,實現流量的有效分發和負載均衡,避免成為QPS瓶頸
- 緩存技術,減輕數據庫的壓力,避免數據庫成為QPS瓶頸
- 集群模式的數據庫,避免數據庫成為QPS瓶頸
- 靜態資源通過CDN加速,避免成為QPS瓶頸
如何預估?
首先需要知道一個請求處理完畢的時間(這個請求里可能做很多事情,但是這個我們暫時不管),一個請求處理完畢的時間我們是可以知道的。(我們去調用這個接口多次得到平均值即可)
理論計算公式
QPS(單節點) = 線程數(如 Tomcat 線程池)/ 平均請求處理時間(秒)
- 例如:若線程池大小為 200,單個請求處理時間為 50ms(0.05秒)
- SprigBoot 默認使用Tomcat,而Tomcat線程池的最大線程數就是200,所以在默認配置下,SprigBoot 應用可以并發處理 200 請求則 QPS(單節點) = 200 / 0.05= 4000
QPS(集群)= QPS(單節點)* 節點數
- 例如集群有4個節點,則 QPS(集群)= 4000 * 4 = 16000
單個請求處理的時間是受到 “系統架構”、“接口本身做的事情”“后端服務器節點運行配置”“后端服務器節點數量”等等因素共同影響的 但這實際上是一個近似已知值(我們去調用這個接口多次得到平均值即可)
QPS瓶頸
我們想想QPS的瓶頸在哪?其實很多方面,就像前面說的QPS與哪些因素有關但一般來說 QPS 最可能的瓶頸在于數據庫 (例如MySQL)
應該說多少QPS
面試官如果問接口的QPS多少,如何回答比較合理?
首先我們一定可以知道這個接口處理的平均耗時是多久?假設是200ms
一些項目背景暫時假設為如下情況,因個人而定
- 假設有 n 臺后端服務器
- 假設后端服務器的配置是 2核4G內存
- 假設接口做了一些事情
- 假設對于一些系統架構上,做了一些優化、或者沒有做優化
那么
- QPS(單節點) = 線程數(如 Tomcat 線程池)/ 平均請求處理時間(秒)若線程池大小為 200,單個請求處理時間為 100ms(0.01秒)則 QPS(單節點) = 200 / 0.01= 2000
- QPS(集群)= QPS(單節點)* 節點數 = 2000 * n
沒有壓測的情況下,我們就可以說這個理論計算得出的值。但是理論僅僅是理論如果有壓測,那其實是另外更好的一種情況,因為壓測得到的數據往往是更加準確的。例如壓測過程:使用 JMeter 進行了壓力測試,逐步增加并發線程數,直到接口的響應時間超過預設閾值(如 200ms)或錯誤率超過 1%。此時就可以知道接口可以抗住的QPS
示例:
面試官:“你在項目中負責的訂單查詢接口,QPS 是多少?”
你的回答: “訂單查詢接口的 QPS 在‘雙11’大促期間峰值達到 5,000,日常平均 QPS 約為 1,200。我們通過以下步驟得出這一數據:
- 壓測階段:JMeter 進行了壓力測試,逐步增加并發線程數,發現當 QPS 達到 2,000 時,響應時間達到300ms。
- 瓶頸分析:通過 Arthas 追蹤發現,80% 的請求耗時在數據庫查詢上。
- 優化措施(僅為舉例,具體情況依據個人而定):引入 Redis 緩存熱點商品數據,緩存命中率提升至 90%;對訂單表按用戶 ID 進行分庫分表,單表數據量從 1 億減少到 1 千萬;將日志記錄改為異步寫入 Kafka,減少主線程耗時。
- 優化結果:最終壓測 QPS 提升到 5,000,且響應時間穩定在 80ms 以內。 此外,線上監控顯示,實際高峰期的 QPS 與壓測結果基本一致,系統未出現超時或宕機。”
如何設計一個秒殺系統
秒殺系統場景特點
- 秒殺一般是訪問請求數量遠遠大于庫存數量,只有少部分用戶能夠秒殺成功
- 秒殺時大量用戶會在同一時間同時進行搶購,網站瞬時訪問流量激增
- 秒殺業務流程比較簡單,一般就是下訂單減庫存
秒殺架構設計理念
- 限流: 鑒于只有少部分用戶能夠秒殺成功,所以要限制大部分流量,只允許少部分流量進?服務后端秒殺程序。
- 削峰:對于秒殺系統瞬時會有大量用戶涌入,所以在搶購一開始會有很高的瞬間峰值。高峰值流量是壓垮系統很重要的原因,所以如何把瞬間的高流量變成一段時間平穩的流量也是設計秒殺系統很重要的思路。實現削峰
的常用的方法有前端添加一定難度的驗證碼,后端利用緩存和消息中間件等技術。 - 異步處理:秒殺系統是一個高并發系統,采用異步處理模式可以極大地提高系統并發量,其實異步處理就是削峰的一種實現方式。
- 內存緩存:秒殺系統最大的瓶頸一般都是數據庫讀寫,由于數據庫讀寫屬于磁盤IO,性能很低,如果能夠把部分數據或業務邏輯轉移到內存緩存,效率會有極大地提升。
- 可拓展:當然如果我們想支持更多用戶,更大的并發,最好就將系統設計成彈性可拓展的,如果流量來了,拓展機器就好了。像淘寶、京東等雙十一活動時會增加大量機器應對交易高峰。
設計思路
- 將請求攔截在系統上游,降低下游壓力:秒殺系統特點是并發量極大,但實際秒殺成功的請求數量卻很少,所以如果不在前端攔截很可能造成數據庫讀寫鎖沖突,甚至導致死鎖,最終請求超時
- 充分利用緩存:利?緩存預減庫存,攔截掉大部分請求
- 消息隊列:這是?個異步處理過程,后臺業務根據自己的處理能力,從消息隊列中主動的拉取請求消息進行業務處理
前端方案
- 頁面靜態化:將活動頁面上的所有可以靜態的元素全部靜態化,并盡量減少動態元素。通過CDN來抗峰值。
- 禁止重復提交:用戶提交之后按鈕置灰,禁止重復提交
- 用戶限流:在某一時間段內只允許用戶提交一次請求,比如可以采取IP限流
后端方案
服務端控制器層(網關層)
限制uid(UserID)訪問頻率:我們上面攔截了瀏覽器訪問的請求,但針對某些惡意攻擊或其它插件,在服務端控制層需要針對同?個訪問uid,限制訪問頻率。
服務層
? 上面只攔截了一部分訪問請求,當秒殺的用戶量很大時,即使每個用戶只有一個請求,到服務層的請求數量還是很大。比如我們有100W用戶同時搶100臺手機,服務層并發請求壓力至少為100W。
- 把需要秒殺的商品的主要信息以及庫存初始化到redis緩存中
- 做請求合法性的校驗(比如是否登錄),如果請求非法,直接給前端返回錯誤碼,進行相應的提示
- 進行內存標識的判斷 (true 已經秒殺結束,false 未秒殺結束,下面第4步會寫入),如果內存標識為true,直接返回秒殺結束
- redis中使用 decr 進行預減庫存操作,判斷:如果decr后庫存量小于0,則把內存標記置為true (已經秒殺結束,第3步會用到),且返回秒殺結束
- 用redis的布隆過濾器來判斷是否已經秒殺到了(下面第7步會寫入),防止重復秒殺,如果重復秒殺,直接返回重復秒殺的錯誤碼。具體做法是:先用redis的布隆過濾器來判斷是否秒殺過,如果布隆過濾器判斷已經秒殺過了, 則再次查庫確認是否秒殺過了,之所以再次查庫確認是因為布隆過濾器對可能存在的數據是有誤判率的;但是它對不存在的數據的判斷是百分百準確的,所以如果redis的布隆過濾器判斷沒秒殺過,就直接放過去進行秒殺
- 發送成功秒殺到的MQ消息給相應的業務端進行處理,并給用戶端返回排隊中,如果客戶端收到排隊中的消息,則自動進行輪詢查詢,直到返回秒殺成功或者秒殺失敗為止
- 相應的業務端進行處理:真正處理秒殺的業務端,再次進行校驗(比如秒殺是否結束,庫存是否充足等)、將用戶和商品id作為key存入redis的布隆過濾器來標識該用戶秒殺該商品成功(第5步會用到)、減庫存(這里的是真正的減庫存,操作數據庫的庫存)、生成秒殺訂單、返回秒殺成功
注意:就算請求走到了真正處理業務的這一端,也有可能秒殺失敗,比如秒殺結束,庫存不足,真正減庫存失敗,秒殺單生成失敗等等,一旦失敗,則返回秒殺結束
優化:將秒殺接口隱藏:用戶點擊秒殺按鈕的時候,根據用戶id生成唯一的加密串存入緩存并返回給客戶端,然后客戶端再次請求的時候帶著加密串過來,后端進行校驗是否合法,若不合法,直接返回請求非法
限制某個接口的訪問頻率:可以用攔截器配合自定義注解來實現,這么做可以和具體的業務分離減少?侵,使用起來也非常方便
數據庫層
- 數據庫層是最脆弱的一層,一般在應用設計時在上游就需要把請求攔截掉,數據庫層只承擔“能力范圍內”的訪問請求。所以,上面通過在服務層引入隊列和緩存,讓最底層的數據庫高枕無憂
- 為防止秒殺出現負數訂單數大于真正的庫存數,所以在真正減庫存,update 庫存的時候應該加上 where 庫存 > 0,而且需要給秒殺訂單表加上用戶id和商品id聯合的唯一索引
10億訂單表如何做分庫分表?
場景痛點:某電商平臺的MySQL訂單表達到7億行時,出現致命問題:
-- 簡單查詢竟需12秒!
SELECT * FROM orders WHERE user_id=10086 LIMIT 10;
?
-- 統計全表耗時278秒
SELECT COUNT(*) FROM orders;
核心矛盾:
-
B+樹索引深度達到5層,磁盤IO暴增。
-
單表超200GB導致備份時間窗突破6小時。
-
寫并發量達8000QPS,主從延遲高達15分鐘。
分庫分表核心策略
垂直拆分:先給數據做減法 ? ? ?
? ? 按列拆分:將一張寬表的字段按業務屬性分散到不同的表中?
優化效果: - 核心表體積減少60% - 高頻查詢字段集中提升緩存命中率
水平拆分:終極解決方案
按行拆分:將表數據按某種規則(如ID范圍、哈希值)分散到多個結構相同的表中。
?
分片鍵選擇三原則**:
? 1.離散性:避免數據熱點(如user_id優于status)
? 2.業務相關性:80%查詢需攜帶該字段
? 3.穩定性:值不隨業務變更(避免使用手機號)
?? 基因嵌入:將用戶的后幾位id編號(gene)嵌入到訂單ID的生成 ==>比較有名的就是淘寶19位訂單編號,后六位是用戶ID的后六位 然后再根據訂單編號的用戶ID進行分庫分表==>分8個庫 每個庫16張表? 注意這種建立起來的庫是用戶庫
關鍵突破**:通過基因嵌入,使相同用戶的訂單始終落在同一分片,同時支持通過訂單ID直接定位分片 ?
**跨分片查詢** ?
三個高頻場景對應三種查詢方式 :(缺少訂商家發貨)
- ?? 核心矛盾:商家與訂單是多對多關系,一個商家的訂單會分散在所有分片中。
- ?? 方案:Elasticsearch二級索引
- ?? 思想:創建一個輔助索引,也就是附錄==>通俗來講,就好比字典的拼音查詢(主查詢)以及偏旁筆畫查詢(二級查詢) ?
實施步驟:
將訂單數據同步到ES:
{"mappings": {"properties": {"merchant_id": { "type": "keyword" },"order_id": { "type": "keyword" },"user_gene": { "type": "integer" }}}
}
查詢實現:
SearchResponse response = client.prepareSearch("orders").setQuery(QueryBuilders.termQuery("merchant_id", merchantId)).addSort("create_time", SortOrder.DESC).setSize(100).get();
數據遷移
雙寫遷移方案:
灰度切換步驟:
- 開啟雙寫(新庫寫失敗需回滾舊庫)
- 全量遷移歷史數據(采用分頁批處理)
- 增量數據實時校驗(校驗不一致自動修復)
- 按用戶ID灰度流量切換(從1%到100%)
性能指標:
場景 | 拆分前 | 拆分后 |
---|---|---|
用戶訂單查詢 | 3200ms | 68ms |
商家訂單導出 | 超時失敗 | 8s完成 |
全表統計 | 不可用 | 1.2s(近似) |
總結
- 分片鍵選擇大于努力:基因分片是訂單系統的最佳拍檔。
- 擴容預留空間:建議初始設計支持2年數據增長。
- 避免過度設計:小表關聯查詢遠比分布式Join高。效
- 監控驅動優化:重點關注分片傾斜率>15%的庫。
如何實現A網站登錄后,B網站自動登錄
有這么個場景,公司下有多個不同域名的站點,我們期望用戶在任意一個站點下登錄后,在打開另外幾個站點時,也是已經登錄的狀態,這么一過程就是單點登錄。因為多個站點都是用的同一套用戶體系,所以單點登錄可以免去用戶重復登錄,讓用戶在站點切換的時候更加流暢,甚至是無感知。
題述問題,本質上是 單點登錄 的問題
單點登錄(Single Sign-On,簡稱 SSO) :是一種用戶認證機制,允許用戶在一次登錄后,訪問多個系統或應用程序而無需再次登錄
基于Cookie
適用情況: A網站和B網站必須是同一域名下的兩個網站,如 a.example.com 和 b.example.com
可以通過共享 Cookie 的方式來實現 SSO
- 優點是實現起來簡單,瀏覽器天然就支持
- 缺點是限制必須不能跨域,如果是兩個不相關的網站,就不能通過Cookie實現SSO
基于Token
? ? ? 使用標準化的令牌(如 JWT)來攜帶用戶認證信息。用戶登錄后,服務端頒發一個令牌(Token)給到前端,前端在訪問其他的網站的時候,把這個 Token 放到 HTTP Header 中帶過去,然后就可以基于這個Tokne來驗證用戶身份了
? ? ?這個方案可以解決跨域的問題,只要支持HTTP的協議即可。但是缺點是Token可能會被泄漏。存在一定的安全隱患
基于共享Session
用戶登錄后,系統將用戶信息存儲到公共的第三方存儲,如Redis或數據庫
例如使用Redis,那么所有業務系統都必須知道該公共Redis的實例信息,才能夠訪問獲取用戶信息
通過CAS
CAS 是 Central Auth Service,也就是認證中心服務
注意和并發編程中的 Compare And Swap 區分
多個需要實現單點登錄的系統,都接入這個統一認證中心,所有的登錄、認證都通過這個認證中心來實現
- 用戶訪問子系統時重定向到 CAS 服務器登錄
- CAS 服務器驗證用戶身份并返回一個票據(Ticket)
- 子系統使用票據向 CAS 驗證,獲取用戶信息
這個方案一般適合于企業內部做集成,可以讓業務系統不再關心登錄、授權,而是只關注業務邏輯,只需要做一次接入就行了。比如說公司的很多內部平臺之間,就可能是用了同一個登錄服務。
優點就是接入成本低,缺點就是這個認證中心本身的開發成本還是挺高的。