高併發服務的幾條優化經驗

前言:如何優化高併發服務, 這裏指的是 qps 在 20 萬以上的在線服務, 注意不是離線服務, 在線服務會存在哪些挑戰呢?①無法做離線緩存, 所有的數據都是實時讀的 ②大量的請求會打到線上服務, 對於服務的響應時間要求較高, 一般都是限制要求在 300ms 以內, 如果超過這個時間那麼對用戶造成的體驗就會急劇下降 ③數據量較大, 單次如果超過 50W 的 qps, 單條 1kb,50 萬就是 5GB 了, 1 分鐘 30G, 對於底層的數據存儲與訪問都有巨大的壓力~ 如何應對這些棘手的問題, 本篇博客來討論一下

一:向關係型數據庫 sayno

一個真正的大型互聯網面向 c 端的服務都不會直接使用數據庫作爲自己的存儲系統, 無論你是採用的是分庫分表還是底層用了各種優秀的連接池等, mysql/oracle 在面對大型在線服務是存在天然的劣勢, 再如何優化, 也難以抵擋 qps 大於 50 萬流量帶來的衝擊。所以換個思路, 我們必須使用 nosql 類緩存系統, 比如 redis/mermCache 等作爲自己的 "數據庫", 而 mysql 等關係型數據庫只是一種兜底, 用於異步去寫作爲數據查詢的備份系統。

場景舉例: 京東雙 11 主會場, 上架了部分商品, 這部分商品都是在會場開始上架的時候直接寫入 redis 中的, 當上架完成之後, 通過異步消息寫入到 mysql 中。面向 c 端的查詢都是直接讀 redis, 而不是數據庫. 而 b 端的查詢, 可以走數據庫去查詢。這部分流量不是很高, 數據庫絕對可以抵擋的住。

二:多級緩存

都知道緩存是高併發提高性能的利器之一。而如何使用好緩存進而利用好多級緩存, 是需要我們去思考的問題。redis 目前是緩存的第一首選. 單機可達 6-8 萬的 qps, 在面對高併發的情況下, 我們可以手動的水平擴容, 以達到應對 qps 可能無線增長的場景。但是這種做法也存在弊端, 因爲 redis 是單線程的, 並且會存在熱點問題。雖然 redis 內部用 crc16 算法做了 hash 打散, 但是同一個 key 還是會落到一個單獨的機器上, 就會使機器的負載增加, redis 典型的存在緩存擊穿和緩存穿透兩個問題, 尤其在秒殺這個場景中, 如果要解決熱點問題, 就變的比較棘手。這個時候多級緩存就必須要考慮了, 典型的在秒殺的場景中, 單 sku 商品在售賣開始的瞬間, qps 會急劇上升. 而我們這時候需要用 memeryCache 來擋一層, memeryCache 是多線程的, 比 redis 擁有更好的併發能力, 並且它是天然可以解決熱點問題的。有了 memeryCache, 我們還需要 localCache,本地緩存, 這是一種以內存換速度的方式。本地緩存會接入用戶的第一層請求, 如果它找不到, 接下來走 memeryCache,然後走 redis,這套流程下來可以擋住百萬的 qps.

三:多線程

我記得在剛開始入行的時候, 每次面試都會被問到多線程, 那時候是一臉懵逼, 多線程有這麼厲害嗎? 幹嘛都說多線程, 爲什麼要使用多線程, 不用行不行? 要講明這個道理, 我先來說一個實例. 曾經我優化過一個接口, 很典型的一個場景。原始的方式是循環一個 30-40 萬的 list,list 執行的操作很簡單, 就是讀 redis 的數據, 讀一次大概需要 3ms 左右, 這是同步的方式,在預覽環境測試, 直接 30 秒 + 超時。後來優化的方式就是把原有的同步調用改爲線程池調用, 線程池裏的線程數或阻塞隊列大小需要自己調優, 最後實測接口 rt 只需要 3 秒。足以見多線程的威力。在多核服務的今天, 如果還不用多線程就是對服務器資源的一種浪費。這裏需要說一句, 使用多線層一定要做好監控, 你需要隨時知道線程的狀態, 如果線程數和 queueSize 設置的不恰當, 將會嚴重影響業務~ 當然多線程也要分場景, 如果爲了多線程而多線程反而是一種浪費, 因爲多線程調度的時候會造成線程在內核態和用戶態之間來回切換, 如果使用不當反而會有反作用

四: 降級和熔斷

降級和熔斷是一種自我保護措施, 這和電路上的熔斷器的基本原理是一樣的, 防止電流過大引起火災等, 面對不可控的巨大流量請求很有可能會擊垮服務器的數據庫或者 redis, 使服務器宕機或者癱瘓造成不可挽回的損失。因爲我們服務的本身需要有防禦機制, 以抵擋外部服務對於自身的侵入導致服務受損引起連帶反應。降級和熔斷有所不同,兩者的區別在於降級是將一些線上主鏈路的功能關閉, 不影響到主鏈路. 熔斷的話, 是指 A 請求 B,B 檢測到服務流量多大啓動了熔斷, 那麼請求會直接進入熔斷池, 直接返回失敗。如何抉擇使用哪一個需要在實際中結合業務場景來考慮.

五: 優化 IO

很多人都會忽視 IO 這個問題, 頻繁的建聯和斷聯都是對系統的重負。在併發請求中, 如果存在單個請求的放大效那麼將會使 io 呈指數倍增加。舉個例子, 比如主會場的商品信息, 如果需要商品的某個具體的詳情, 而這個詳情需要調用下游來單個獲取. 隨着主會場商品的熱賣, 商品越來越多, 一次就要經過商品數 X 下游請求的數量, 在海量的 qps 請求下, IO 數被佔據, 大量的請求被阻塞, 接口的響應速度就會呈指數級下降。所以需要批量的請求接口, 所有的優化爲一次 IO

六: 慎用重試

重試作爲對臨時異常的一種處理的常見手法, 常見應對的方式是請求某個服務失敗或者寫數據庫了重新再試, 使用重試一定要注意以下幾點①控制好重試次數②重試的間隔時間得衡量好③是否重試要做到配置化。之前我們線上出了一個 bug,kafka 消費出現了嚴重的 lag, 單詞消耗時間是 10 幾秒, 看代碼之後發現是重試的次數過多導致的, 並且次數還不支持配置化修改, 所以當時的做法只能是臨時改代碼後上線. 重試作爲一種業務的二次嘗試, 極大提升了程序的請求 success, 但是也要注意以上幾點。

七: 邊界 case 的判斷和兜底

作爲互聯網老手, 很多人寫出的代碼都不錯, 但是在經歷過幾輪的故障 review 之後發現很多釀成重大事故的代碼背後都是缺少對一些邊界問題的處理, 所犯的錯誤非常簡單, 但是往往就是這些小問題就能釀成大事故. 曾經 review 過一次重大的事故, 後來發現最終的原因居然是沒有對空數組進行判空, 導致傳入下游的 rpc 是空的, 下游直接返回全量的業務數據, 影響數百萬用戶。這個代碼改動起來很簡單, 但是是令人需要反省的, 小小的不足釀成了大禍

八: 學會優雅的打印日誌

日誌作爲追溯線上問題的最佳利器, 可謂保留 bug 現場的唯一來源。雖然有 arthas 這樣的利器方便我們排查問題, 但是對於一些比較複雜的場景, 還是需要日誌來記錄程序的數據. 但是在高流量的場景中, 如果全量打印日誌對於線上來說就是一種災難, 有以下缺點①嚴重佔用磁盤, 估算以下, 如果接口的 qps 在 20 萬左右, 日誌一秒就幾千兆, 一天下來就是上千 GB ②大量的日誌需要輸出, 佔用了程序 IO, 增加了接口的 RT(響應時間) 如果需要解決這個問題,①我們可以利用限流組件來實現一個基於限流的日誌組件, 令牌桶算法可以限制打印日誌的流量, 比如一秒只允許打印一條日誌 ②基於白名單的日誌打印, 線上配置了白名單用戶纔可以打印出來, 節省了大量了無效日誌輸出

   總結: 本篇博客討論了高併發服務在面對大流量時的一些基本注意事項和應對的點, 當然實際線上的比目前的更復雜, 這裏只是給出幾條建議, 希望我們在高併發的路上保持敬畏, 繼續探索. 更好的深耕 c 端服務, 做更好的互聯網應用, 加油

作者:Yrion

來源:www.cnblogs.com/wyq178/p/15811956.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Q_0TROvwM-BVqlvqIznQvw