九大服務(wù)架構(gòu)性能優(yōu)化方式
作者 | jialiangsun
最近做了一些服務(wù)性能優(yōu)化,文章池服務(wù)平均耗時(shí)跟p99耗時(shí)都下降80%左右,事件底層頁服務(wù)平均耗時(shí)下降50%多左右,主要優(yōu)化項(xiàng)目中一些不合理設(shè)計(jì),例如服務(wù)間使用json傳輸數(shù)據(jù),監(jiān)控上報(bào)處理邏輯在主流程中,重復(fù)數(shù)據(jù)每次都請求下游服務(wù),多個(gè)耗時(shí)操作串行請求等,這些問題都對服務(wù)有著嚴(yán)重的性能影響。
在服務(wù)架構(gòu)設(shè)計(jì)時(shí)通??梢允褂靡恍┲虚g件去提升服務(wù)性能,例如使用mysql,redis,kafka等,因?yàn)檫@些中間件有著很好的讀寫性能。除了使用中間件提升服務(wù)性能外,也可以通過探索它們通過什么樣的底層設(shè)計(jì)實(shí)現(xiàn)的高性能,將這些設(shè)計(jì)應(yīng)用到我們的服務(wù)架構(gòu)中。
常用的性能優(yōu)化方法可以分為以下幾種:
性能優(yōu)化九大方式
1.緩存
性能優(yōu)化,緩存為王,所以開始先介紹一下緩存。緩存在我們的架構(gòu)設(shè)計(jì)中無處不在的,常規(guī)請求是瀏覽器發(fā)起請求,請求服務(wù)端服務(wù),服務(wù)端服務(wù)再查詢數(shù)據(jù)庫中的數(shù)據(jù),每次讀取數(shù)據(jù)都會(huì)至少需要兩次網(wǎng)絡(luò)I/O,性能會(huì)差一些,我們可以在整個(gè)流程中增加緩存來提升性能。首先是瀏覽器測,可以通過Expires、Cache-Control、Last-Modified、Etag等相關(guān)字段來控制瀏覽器是否使用本地緩存。
其次我們可以在服務(wù)端服務(wù)使用本地緩存或者一些中間件來緩存數(shù)據(jù),例如redis。redis之所以這么快,主要因?yàn)閿?shù)據(jù)存儲(chǔ)在內(nèi)存中,不需要讀取磁盤,因?yàn)閮?nèi)存讀取速度通常是磁盤的數(shù)百倍甚至更多;
然后在數(shù)據(jù)庫測,通常使用的是mysql,mysql的數(shù)據(jù)存儲(chǔ)到磁盤上,但是mysql為了提升讀寫性能,會(huì)利用bufferpool緩存數(shù)據(jù)頁。mysql讀取時(shí)會(huì)按照頁的粒度將數(shù)據(jù)頁讀取到bufferpool中,bufferpool中的數(shù)據(jù)頁使用LRU算法淘汰長期沒有用到的頁面,緩存最近訪問的數(shù)據(jù)頁。
此外小到cpu的l1、l2、l3級cache,大到瀏覽器緩存都是為了提高性能,緩存也是進(jìn)行服務(wù)性能優(yōu)化的重要手段,使用緩存時(shí)需要考慮以下幾點(diǎn):
(1) 使用什么樣的緩存
使用緩存時(shí)可以使用redis或者機(jī)器內(nèi)存來緩存數(shù)據(jù),使用redis的好處可以保證不同機(jī)器讀取數(shù)據(jù)的一致性,但是讀取redis會(huì)增加一次I/O,使用內(nèi)存緩存數(shù)據(jù)時(shí)可能會(huì)出現(xiàn)讀取數(shù)據(jù)不一致,但是讀取性能好。例如文章的閱讀數(shù)數(shù)據(jù),如果使用機(jī)器內(nèi)存作為緩存,容易出現(xiàn)不同機(jī)器上緩存數(shù)據(jù)的不一致,用戶不同刷次會(huì)請求到不同服務(wù)端機(jī)器,讀取的閱讀數(shù)不一致,可能會(huì)出現(xiàn)閱讀數(shù)變小的情況,用戶體驗(yàn)不好。對于閱讀數(shù)這種經(jīng)常變更的數(shù)據(jù)比較適合使用redis來統(tǒng)一緩存。
也可以將兩者結(jié)合提升服務(wù)的性能,例如在內(nèi)容池服務(wù),利用redis跟機(jī)器內(nèi)存緩存熱點(diǎn)文章詳情,優(yōu)先讀取機(jī)器內(nèi)存中的數(shù)據(jù),數(shù)據(jù)不存在的時(shí)候會(huì)讀取redis中的緩存數(shù)據(jù),當(dāng)redis中的數(shù)據(jù)也不存在的時(shí)候,會(huì)讀取下游持久化存儲(chǔ)中的全量數(shù)據(jù)。其中內(nèi)存級緩存過期時(shí)間為15s,在數(shù)據(jù)變更的時(shí)候不保證數(shù)據(jù)一致性,通過數(shù)據(jù)自然過期來保證最終一致性。redis中緩存數(shù)據(jù)需要保證與持久化存儲(chǔ)中數(shù)據(jù)一致性,如何保證一致性在后續(xù)講解??梢愿鶕?jù)自己的業(yè)務(wù)場景可以選擇合適的緩存方案。
使用緩存時(shí)可以使用redis或者機(jī)器內(nèi)存來緩存數(shù)據(jù),使用redis的好處可以保證不同機(jī)器讀取數(shù)據(jù)的一致性,但是讀取redis會(huì)增加一次I/O,使用內(nèi)存緩存數(shù)據(jù)時(shí)可能會(huì)出現(xiàn)讀取數(shù)據(jù)不一致,但是讀取性能好。例如文章的閱讀數(shù)數(shù)據(jù),如果使用機(jī)器內(nèi)存作為緩存,容易出現(xiàn)不同機(jī)器上緩存數(shù)據(jù)的不一致,用戶不同刷次會(huì)請求到不同服務(wù)端機(jī)器,讀取的閱讀數(shù)不一致,可能會(huì)出現(xiàn)閱讀數(shù)變小的情況,用戶體驗(yàn)不好。對于閱讀數(shù)這種經(jīng)常變更的數(shù)據(jù)比較適合使用redis來統(tǒng)一緩存。
也可以將兩者結(jié)合提升服務(wù)的性能,例如在內(nèi)容池服務(wù),利用redis跟機(jī)器內(nèi)存緩存熱點(diǎn)文章詳情,優(yōu)先讀取機(jī)器內(nèi)存中的數(shù)據(jù),數(shù)據(jù)不存在的時(shí)候會(huì)讀取redis中的緩存數(shù)據(jù),當(dāng)redis中的數(shù)據(jù)也不存在的時(shí)候,會(huì)讀取下游持久化存儲(chǔ)中的全量數(shù)據(jù)。其中內(nèi)存級緩存過期時(shí)間為15s,在數(shù)據(jù)變更的時(shí)候不保證數(shù)據(jù)一致性,通過數(shù)據(jù)自然過期來保證最終一致性。redis中緩存數(shù)據(jù)需要保證與持久化存儲(chǔ)中數(shù)據(jù)一致性,如何保證一致性在后續(xù)講解??梢愿鶕?jù)自己的業(yè)務(wù)場景可以選擇合適的緩存方案。
(2) 緩存常見問題
- 緩存雪崩:緩存雪崩是指緩存中的大量數(shù)據(jù)同時(shí)失效或者過期,導(dǎo)致大量的請求直接讀取到下游數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫瞬時(shí)壓力過大,通常的解決方案是將緩存數(shù)據(jù)設(shè)置的過期時(shí)間隨機(jī)化。在事件服務(wù)中就是利用固定過期時(shí)間+隨機(jī)值的方式進(jìn)行文章的淘汰,避免緩存雪崩。
- 緩存穿透:緩存穿透是指讀取下游不存在的數(shù)據(jù),導(dǎo)致緩存命中不了,每次都請求下游數(shù)據(jù)庫。這種情況通常會(huì)出現(xiàn)在線上異常流量攻擊或者下游數(shù)據(jù)被刪除的狀況,針對緩存穿透可以使用布隆過濾器對不存在的數(shù)據(jù)進(jìn)行過濾,或者在讀取下游數(shù)據(jù)不存在的情況,可以在緩存中設(shè)置空值,防止不斷的穿透。事件服務(wù)可能會(huì)出現(xiàn)查詢文章被刪除的情況,就是利用設(shè)置空值的方法防止被刪除數(shù)據(jù)的請求不斷穿透到下游。
- 緩存擊穿: 緩存擊穿是指某個(gè)熱點(diǎn)數(shù)據(jù)在緩存中被刪除或者過期,導(dǎo)致大量的熱點(diǎn)請求同時(shí)請求數(shù)據(jù)庫。解決方案可以對于熱點(diǎn)數(shù)據(jù)設(shè)置較長的過期時(shí)間或者利用分布式鎖避免多個(gè)相同請求同時(shí)訪問下游服務(wù)。在新聞業(yè)務(wù)中,對于熱點(diǎn)新聞經(jīng)常會(huì)出現(xiàn)這種情況,事件服務(wù)利用golang的singlefilght保證同一篇文章請求在同一時(shí)刻只有一個(gè)會(huì)請求到下游,防止緩存擊穿。
- 熱點(diǎn)key: 熱點(diǎn)key是指緩存中被頻繁訪問的key,導(dǎo)致緩存該key的分片或者redis訪問量過高??梢詫⒖蔁狳c(diǎn)key分散存儲(chǔ)到多個(gè)key上,例如將熱點(diǎn)key+序列號的方式存儲(chǔ),不同key存儲(chǔ)的值都是相同的,在訪問時(shí)隨機(jī)訪問一個(gè)key,分散原來單key分片的壓力;此外還可以將key緩存到機(jī)器內(nèi)存中,避免redis單節(jié)點(diǎn)壓力過大,在新聞業(yè)務(wù)中,對于熱點(diǎn)文章就是采用這種方式,將熱點(diǎn)文章存儲(chǔ)到機(jī)器內(nèi)存中,避免存儲(chǔ)熱點(diǎn)文章redis單分片請求量過大。
key val => key1 val 、 key2 val、 key3 val 、 key4 val
(3) 緩存淘汰
緩存的大小是有限的,因?yàn)樾枰獙彺嬷袛?shù)據(jù)進(jìn)行淘汰,通??梢圆捎秒S機(jī)、LRU或者LFU算法等淘汰數(shù)據(jù)。LRU是一種最常用的置換算法,淘汰最近最久未使用的數(shù)據(jù),底層可以利用map+雙端隊(duì)列的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)。
最原生的LRU算法是存在一些問題的,不知道大家在使用過有沒有遇到過問題。首先需要注意的是在數(shù)據(jù)結(jié)構(gòu)中有互斥鎖,因?yàn)間olang對于map的讀寫會(huì)產(chǎn)生panic,導(dǎo)致服務(wù)異常。使用互斥鎖之后會(huì)導(dǎo)致整個(gè)緩存性能變差,可以采用分片的思想,將整個(gè)LRUCache分為多個(gè),每次讀取時(shí)讀取其中一個(gè)cache片,降低鎖的粒度來提升性能,常見的本地緩存包通常就利用這種方式實(shí)現(xiàn)的。
type LRUCache struct {
sync.Mutex
size int
capacity int
cache map[int]*DLinkNode
head, tail *DLinkNode
}
type DLinkNode struct {
key,value int
pre, next *DLinkNode
}
mysql也會(huì)利用LRU算法對buffer pool中的數(shù)據(jù)頁進(jìn)行淘汰。由于mysql存在預(yù)讀,在讀取磁盤時(shí)并不是按需讀取,而是按照整個(gè)數(shù)據(jù)頁的粒度進(jìn)行讀取,一個(gè)數(shù)據(jù)頁會(huì)存儲(chǔ)多條數(shù)據(jù),除了讀取當(dāng)前數(shù)據(jù)頁,可能也會(huì)將接下來可能用到的相鄰數(shù)據(jù)頁提前緩存到bufferpool中,如果下次讀取的數(shù)據(jù)在緩存中,直接讀取內(nèi)存即可,不需要讀取磁盤,但是如果預(yù)讀的數(shù)據(jù)頁一直沒有被訪問,那就會(huì)存在預(yù)讀失效的情況,淘汰原來使用到的數(shù)據(jù)頁。mysql將buffer pool中的鏈表分為兩部分,一段是新生代,一段是老生代,新老生代的默認(rèn)比是7:3,數(shù)據(jù)頁被預(yù)讀的時(shí)候會(huì)先加到老生代中,當(dāng)數(shù)據(jù)頁被訪問時(shí)才會(huì)加載到新生代中,這樣就可以防止預(yù)讀的數(shù)據(jù)頁沒有被使用反而淘汰熱點(diǎn)數(shù)據(jù)頁。此外mysql通常會(huì)存在掃描表的請求,會(huì)順序請求大量的數(shù)據(jù)加載到緩存中,然后將原本緩存中所有熱點(diǎn)數(shù)據(jù)頁淘汰,這個(gè)問題通常被稱為緩沖池污染,mysql中的數(shù)據(jù)頁需要在老生代停留時(shí)間超過配置時(shí)間才會(huì)老生代移動(dòng)到新生代時(shí)來解決緩存池污染。
redis中也會(huì)利用LRU進(jìn)行淘汰過期的數(shù)據(jù),如果redis將緩存數(shù)據(jù)都通過一個(gè)大的鏈表進(jìn)行管理,在每次讀寫時(shí)將最新訪問的數(shù)據(jù)移動(dòng)到鏈表隊(duì)頭,那樣會(huì)嚴(yán)重影響redis的讀寫性能,此外會(huì)增加額外的存儲(chǔ)空間,降低整體存儲(chǔ)數(shù)量。redis是對緩存中的對象增加一個(gè)最后訪問時(shí)間的字段,在對對象進(jìn)行淘汰的時(shí)候,會(huì)采用隨機(jī)采樣的方案,隨機(jī)取5個(gè)值,淘汰最近訪問時(shí)間最久的一個(gè),這樣就可以避免每次都移動(dòng)節(jié)點(diǎn)。但是LRU也會(huì)存在緩存污染的情況,一次讀取大量數(shù)據(jù)會(huì)淘汰熱點(diǎn)數(shù)據(jù),因此redis可以選擇利用LFU進(jìn)行淘汰數(shù)據(jù),是將原來的訪問時(shí)間字段變更為最近訪問時(shí)間+訪問次數(shù)的一個(gè)字段,這里需要注意的是訪問次數(shù)并不是單純的次數(shù)累加,而是根據(jù)最近訪問時(shí)間跟當(dāng)前時(shí)間的差值進(jìn)行時(shí)間衰減的,簡單說也就是訪問越久以及訪問次數(shù)越少計(jì)算得到的值也越小,越容易被淘汰。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} obj ;
可以看出不同中間件對于傳統(tǒng)的LRU淘汰策略都進(jìn)行了一定優(yōu)化來保證服務(wù)性能,我們也可以參考不同的優(yōu)化策略在自己的服務(wù)中進(jìn)行緩存key的淘汰。
(4) 緩存數(shù)據(jù)一致性
當(dāng)數(shù)據(jù)庫中的數(shù)據(jù)變更時(shí),如何保證緩存跟數(shù)據(jù)庫中的數(shù)據(jù)一致,通常有以下幾種方案:更新緩存再更新DB,更新DB再更新緩存,先更新DB再刪除緩存,刪除緩存再更新DB。這幾種方案都有可能會(huì)出現(xiàn)緩存跟數(shù)據(jù)庫中的數(shù)據(jù)不一致的情況,最常用的還是更新DB再刪除緩存,因?yàn)檫@種方案導(dǎo)致數(shù)據(jù)不一致的概率最小,但是也依然會(huì)存在數(shù)據(jù)不一致的問題。例如在T1時(shí)緩存中無數(shù)據(jù),數(shù)據(jù)庫中數(shù)據(jù)為100,線程B查詢緩存沒有查詢到數(shù)據(jù),讀取到數(shù)據(jù)庫的數(shù)據(jù)100然后去更新緩存,但是此時(shí)線程A將數(shù)據(jù)庫中的數(shù)據(jù)更新為99,然后在T4時(shí)刻刪除緩存中的數(shù)據(jù),但是此時(shí)緩存中還沒有數(shù)據(jù),在T5的時(shí)候線程B才更新緩存數(shù)據(jù)為100,這時(shí)候就會(huì)導(dǎo)致緩存跟數(shù)據(jù)庫中的數(shù)據(jù)不一致。
為保證緩存與數(shù)據(jù)庫數(shù)據(jù)的一致性。常用的解決方案有兩種,一種是延時(shí)雙刪,先刪除緩存,后續(xù)更新數(shù)據(jù)庫,休眠一會(huì)再刪除緩存。文章池服務(wù)中就是利用這種方案保證數(shù)據(jù)一致性,如何實(shí)現(xiàn)延遲刪除,是通過go語言中channel實(shí)現(xiàn)簡單延時(shí)隊(duì)列,沒有引入第三方的消息隊(duì)列,主要為了防止服務(wù)的復(fù)雜化;另外一種可以訂閱DB的變更binlog,數(shù)據(jù)更新時(shí)只更新DB,通過消費(fèi)DB的binlog日志,解析變更操作進(jìn)行緩存變更,更新失敗時(shí)不進(jìn)行消息的提交,通過消息隊(duì)列的重試機(jī)制實(shí)現(xiàn)最終一致性。
2.并行化處理
redis在版本6.0之前都是號稱單線程模型,主要是利用epllo管理用戶海量連接,使用一個(gè)線程通過事件循環(huán)來處理用戶的請求,優(yōu)點(diǎn)是避免了線程切換和鎖的競爭,以及實(shí)現(xiàn)簡單,但是缺點(diǎn)也比較明顯,不能有效的利用cpu的多核資源。隨著數(shù)據(jù)量和并發(fā)量的越來越大,I/O成了redis的性能瓶頸點(diǎn),因此在6.0版本引入了多線程模型。redis的多線程將處理過程最耗時(shí)的sockect的讀取跟解析寫入由多個(gè)I/O 并發(fā)完成,對于命令的執(zhí)行過程仍然由單線程完成。
mysql的主從同步過程從數(shù)據(jù)庫通過I/Othread讀取住主庫的binlog,將日志寫入到relay log中,然后由sqlthread執(zhí)行relaylog進(jìn)行數(shù)據(jù)的同步。其中sqlthread就是由多個(gè)線程并發(fā)執(zhí)行加快數(shù)據(jù)的同步,防止主從同步延遲。sqlthread多線程化也經(jīng)歷了多個(gè)版本迭代,按表維度分發(fā)到同一個(gè)線程進(jìn)行數(shù)據(jù)同步,再到按行維度分發(fā)到同一個(gè)線程。
小到線程的并發(fā)處理,大到redis的集群,以及kafka的分topic分區(qū)都是通過多個(gè)client并行處理提高服務(wù)的讀寫性能。在我們的服務(wù)設(shè)計(jì)中可以通過創(chuàng)建多個(gè)容器對外服務(wù)提高服務(wù)的吞吐量,服務(wù)內(nèi)部可以將多個(gè)串行的I/O操作改為并行處理,縮短接口的響應(yīng)時(shí)長,提升用戶體驗(yàn)。對于I/O存在相互依賴的情況,可以進(jìn)行多階段分批并行化處理,另外一種常見的方案就是利用DAG加速執(zhí)行,但是需要注意的是DAG會(huì)存在開發(fā)維護(hù)成本較高的情況,需要根據(jù)自己的業(yè)務(wù)場景選擇合適的方案。并行化也不是只有好處沒有壞處的,并行化有可能會(huì)導(dǎo)致讀擴(kuò)散嚴(yán)重,以及線程切換頻繁存在一定的性能影響。
3.批量化處理
kafka的消息發(fā)送并不是直接寫入到broker中的,發(fā)送過程是將發(fā)送到同一個(gè)topic同一個(gè)分區(qū)的消息通過main函數(shù)的partitioner組件發(fā)送到同一個(gè)隊(duì)列中,由sender線程不斷拉取隊(duì)列中消息批量發(fā)送到broker中。利用批量發(fā)送消息處理,節(jié)省大量的網(wǎng)絡(luò)開銷,提高發(fā)送效率。
redis的持久化方式有RDB跟AOF兩種,其中AOF在執(zhí)行命令寫入內(nèi)存后,會(huì)寫入到AOF緩沖區(qū),可以選擇合適的時(shí)機(jī)將AOF緩沖區(qū)中的數(shù)據(jù)寫入到磁盤中,刷新到磁盤的時(shí)間通過參數(shù)appendfsync控制,有三個(gè)值always、everysec、no。其中always會(huì)在每次命令執(zhí)行完都會(huì)刷新到磁盤來保證數(shù)據(jù)的可靠性;everysec是每秒批量寫入到磁盤,no是不進(jìn)行同步操作,由操作系統(tǒng)決定刷新到寫回磁盤,當(dāng)redis異常退出時(shí)存在丟數(shù)據(jù)的風(fēng)險(xiǎn)。AOF命令刷新到磁盤的時(shí)機(jī)會(huì)影響redis服務(wù)寫入性能,通常配置為everysec批量寫入到磁盤,來平衡寫入性能和數(shù)據(jù)可靠性。
我們讀取下游服務(wù)或者數(shù)據(jù)庫的時(shí)候,可以一次多查詢幾條數(shù)據(jù),節(jié)省網(wǎng)絡(luò)I/O;讀取redis的還可以利用pipeline或者lua腳本處理多條命令,提升讀寫性能;前端請求js文件或者小圖片時(shí),可以將多個(gè)js文件或者圖片合并到一起返回,減少前端的連接數(shù),提升傳輸性能。同樣需要注意的是批量處理多條數(shù)據(jù),有可能會(huì)降低吞吐量,以及本身下游就不支持過多的批量數(shù)據(jù),此時(shí)可以將多條數(shù)據(jù)分批并發(fā)請求。對于事件底層頁服務(wù)中不同組件下配置的不同文章id,會(huì)統(tǒng)一批量請求下游內(nèi)容服務(wù)獲取文章詳情,對于批量的條數(shù)也會(huì)做限制,防止單批數(shù)據(jù)量過大。
4.數(shù)據(jù)壓縮合并
redis的AOF重寫是利用bgrewriteaof命令進(jìn)行AOF文件重寫,因?yàn)锳OF是追加寫日志,對于同一個(gè)key可能存在多條修改修改命令,導(dǎo)致AOF文件過大,redis重啟后加載AOF文件會(huì)變得緩慢,導(dǎo)致啟動(dòng)時(shí)間過長。可以利用重寫命令將對于同一個(gè)key的修改只保存一條記錄,減小AOF文件體積。
大數(shù)據(jù)領(lǐng)域的Hbase、cassandra等nosql數(shù)據(jù)庫寫入性能都很高,它們的底層存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)就是LSM樹(log structured merge tree),這種數(shù)據(jù)結(jié)構(gòu)的核心思想是追加寫,積攢一定的數(shù)據(jù)后合并成更大的segement,對于數(shù)據(jù)的刪除也只是增加一條刪除記錄。同樣對一個(gè)key的修改記錄也有多條。這種存儲(chǔ)結(jié)構(gòu)的優(yōu)點(diǎn)是寫入性能高,但是缺點(diǎn)也比較明顯,數(shù)據(jù)存在冗余和文件體積大。主要通過線程進(jìn)行段合并將多個(gè)小文件合并成更大的文件來減少存儲(chǔ)文件體積,提升查詢效率。
對于kafka進(jìn)行傳輸數(shù)據(jù)時(shí),在生產(chǎn)者端和消費(fèi)者端可以開啟數(shù)據(jù)壓縮。生產(chǎn)者端壓縮數(shù)據(jù)后,消費(fèi)者端收到消息會(huì)自動(dòng)解壓,可以有效減小在磁盤的存儲(chǔ)空間和網(wǎng)絡(luò)傳輸時(shí)的帶寬消耗,從而降低成本和提升傳輸效率。需要注意生產(chǎn)者端和消費(fèi)者端指定相同的壓縮算法。
在降本增效的浪潮中,降低redis成本的一種方式,就是對存儲(chǔ)到redis中的數(shù)據(jù)進(jìn)行壓縮,降低存儲(chǔ)成本,重構(gòu)后的內(nèi)容微服務(wù)通過持久化存儲(chǔ)全量數(shù)據(jù),采用snappy壓縮,壓縮后只是原來數(shù)據(jù)的40%-50%;還有一種方式是將服務(wù)之間的調(diào)用從http的json改為trpc的pb協(xié)議,因?yàn)閜b協(xié)議編碼后的數(shù)據(jù)更小,提升傳輸效率,在服務(wù)優(yōu)化時(shí),將原來請求tab的協(xié)議從json轉(zhuǎn)成pb,降低幾毫秒的時(shí)延,此外內(nèi)容微服務(wù)存儲(chǔ)的數(shù)據(jù)采用flutbuffer編碼,相比較于protobuffer有著更高的壓縮比跟更快的編解碼速度;對于JS/CSS多個(gè)文件下發(fā)也可以進(jìn)行混淆和壓縮傳遞;對于存儲(chǔ)在es中的數(shù)據(jù)也可以手動(dòng)調(diào)用api進(jìn)行段合并,減小存儲(chǔ)數(shù)據(jù)的體積,提高查詢速度;在我們工作中還有一個(gè)比較常見的問題是接口返回的冗余數(shù)據(jù)特別多,一個(gè)接口服務(wù)下發(fā)的數(shù)據(jù)大而全,而不是對于當(dāng)前場景做定制化下發(fā),不滿足接口最小化原則,白白浪費(fèi)了很多帶寬資源和降低傳輸效率。
5.無鎖化
redis通過單線程避免了鎖的競爭,避免了線程之間頻繁切換才有這很好的讀寫性能。
go語言中提供了atomic包,主要用于不同線程之間的數(shù)據(jù)同步,不需要加鎖,本質(zhì)上就是封裝了底層cpu提供的原子操作指令。此外go語言最開始的調(diào)度模型時(shí)GM模型,所有的內(nèi)核級線程想要執(zhí)行g(shù)oroutine需要加鎖從全局隊(duì)列中獲取,所以不同線程之間的競爭很激烈,調(diào)度效率很差。
后續(xù)引入了P(Processor),每一個(gè)M(thread)要執(zhí)行G(gorontine)的時(shí)候需要綁定一個(gè)P,其中P中會(huì)有一個(gè)待執(zhí)行G的本地隊(duì)列,只由當(dāng)前M可以進(jìn)行讀寫(少數(shù)情況會(huì)存在偷其他協(xié)程的G),讀取P本地隊(duì)列時(shí)不需要進(jìn)行加鎖,通過降低鎖的競爭大幅度提升調(diào)度G的效率。
mysql利用mvcc實(shí)現(xiàn)多個(gè)事務(wù)進(jìn)行讀寫并發(fā)時(shí)保證數(shù)據(jù)的一致性和隔離型,也是解決讀寫并發(fā)的一種無鎖化設(shè)計(jì)方案之一。它主要通過對每一行數(shù)據(jù)的變更記錄維護(hù)多個(gè)版本鏈來實(shí)現(xiàn)的,通過隱藏列rollptr和undolog來實(shí)現(xiàn)快照讀。在事務(wù)對某一行數(shù)據(jù)進(jìn)行操作時(shí),會(huì)根據(jù)當(dāng)前事務(wù)id以及事務(wù)隔離級別判斷讀取那個(gè)版本的數(shù)據(jù),對于可重復(fù)讀就是在事務(wù)開始的時(shí)候生成readview,在后續(xù)整個(gè)事務(wù)期間都使用這個(gè)readview。mysql中除了使用mvcc避免互斥鎖外,bufferpool還可以設(shè)置多個(gè),通過多個(gè)bufferpool降低鎖的粒度,提升讀寫性能,也是一種優(yōu)化方案。
日常工作 在讀多寫少的場景下可以利用atomic.value存儲(chǔ)數(shù)據(jù),減少鎖的競爭,提升系統(tǒng)性能,例如配置服務(wù)中數(shù)據(jù)就是利用atomic.value存儲(chǔ)的;syncmap為了提升讀性能,優(yōu)先使用atomic進(jìn)行read操作,然后再進(jìn)行加互斥鎖操作進(jìn)行dirty的操作,在讀多寫少的情況下也可以使用syncmap。
秒殺系統(tǒng)的本質(zhì)就是在高并發(fā)下準(zhǔn)確的增減商品庫存,不出現(xiàn)超賣少賣的問題。因此所有的用戶在搶到商品時(shí)需要利用互斥鎖進(jìn)行庫存數(shù)量的變更。互斥鎖的存在必然會(huì)成為系統(tǒng)瓶頸,但是秒殺系統(tǒng)又是一個(gè)高并發(fā)的場景,所以如何進(jìn)行互斥鎖優(yōu)化是提高秒殺系統(tǒng)性能的一個(gè)重要優(yōu)化手段。
無鎖化設(shè)計(jì)方案之一就是利用消息隊(duì)列,對于秒殺系統(tǒng)的秒殺操作進(jìn)行異步處理,將秒殺操作發(fā)布一個(gè)消息到消息隊(duì)列中,這樣所有用戶的秒殺行為就形成了一個(gè)先進(jìn)先出的隊(duì)列,只有前面先添加到消息隊(duì)列中的用戶才能搶購商品成功。從隊(duì)列中消費(fèi)消息進(jìn)行庫存變更的線程是個(gè)單線程,因此對于db的操作不會(huì)存在沖突,不需要加鎖操作。
另外一種優(yōu)化方式可以參考golang的GMP模型,將庫存分成多份,分別加載到服務(wù)server的本地,這樣多機(jī)之間在對庫存變更的時(shí)候就避免了鎖的競爭。如果本地server是單進(jìn)程的,因此也可以形成一種無鎖化架構(gòu);如果是多進(jìn)程的,需要對本地庫存加鎖后在進(jìn)行變更,但是將庫存分散到server本地,降低了鎖的粒度,提高整個(gè)服務(wù)性能。
6.順序?qū)?/h4>
mysql的InnoDB存儲(chǔ)引擎在創(chuàng)建主鍵時(shí)通常會(huì)建議使用自增主鍵,而不是使用uuid,最主要的原因是InnoDB底層采用B+樹用來存儲(chǔ)數(shù)據(jù),每個(gè)葉子結(jié)點(diǎn)是一個(gè)數(shù)據(jù)頁,存儲(chǔ)多條數(shù)據(jù)記錄,頁面內(nèi)的數(shù)據(jù)通過鏈表有序存儲(chǔ),數(shù)據(jù)頁間通過雙向鏈表存儲(chǔ)。由于uuid是無序的,有可能會(huì)插入到已經(jīng)空間不足的數(shù)據(jù)頁中間,導(dǎo)致數(shù)據(jù)頁分裂成兩個(gè)新的數(shù)據(jù)頁以便插入新數(shù)據(jù),影響整體寫入性能。
此外mysql中的寫入過程并不是每次將修改的數(shù)據(jù)直接寫入到磁盤中,而是修改內(nèi)存中buffer pool內(nèi)存儲(chǔ)的數(shù)據(jù)頁,將數(shù)據(jù)頁的變更記錄到undolog和binlog日志中,保證數(shù)據(jù)變更不丟失,每次記錄log都是追加寫到日志文件尾部,順序?qū)懭氲酱疟P。對數(shù)據(jù)進(jìn)行變更時(shí)通過順序?qū)憀og,避免隨機(jī)寫磁盤數(shù)據(jù)頁,提升寫入性能,這種將隨機(jī)寫轉(zhuǎn)變?yōu)轫樞驅(qū)懙乃枷朐诤芏嘀虚g件中都有所體現(xiàn)。
kakfa中的每個(gè)分區(qū)是一個(gè)有序不可變的消息隊(duì)列,新的消息會(huì)不斷的添加的partition的尾部,每個(gè)partition由多個(gè)segment組成,一個(gè)segment對應(yīng)一個(gè)物理日志文件,kafka對segment日志文件的寫入也是順序?qū)?。順序?qū)懭氲暮锰幨潜苊饬舜疟P的不斷尋道和旋轉(zhuǎn)次數(shù),極大的提高了寫入性能。
順序?qū)懼饕獣?huì)應(yīng)用在存在大量磁盤I/O操作的場景,日常工作中創(chuàng)建mysql表時(shí)選擇自增主鍵,或者在進(jìn)行數(shù)據(jù)庫數(shù)據(jù)同步時(shí)順序讀寫數(shù)據(jù),避免底層頁存儲(chǔ)引擎的數(shù)據(jù)頁分裂,也會(huì)對寫入性能有一定的提升。
7.分片化
redis對于命令的執(zhí)行過程是單線程的,單機(jī)有著很好的讀寫性能,但是單機(jī)的機(jī)器容量跟連接數(shù)畢竟有限,因此單機(jī)redis必然會(huì)存在讀寫上限跟存儲(chǔ)上限。redis集群的出現(xiàn)就是為了解決單機(jī)redis的讀寫性能瓶頸問題,redis集群是將數(shù)據(jù)自動(dòng)分片到多個(gè)節(jié)點(diǎn)上,每個(gè)節(jié)點(diǎn)負(fù)責(zé)數(shù)據(jù)的一部分,每個(gè)節(jié)點(diǎn)都可以對外提供服務(wù),突破單機(jī)redis存儲(chǔ)限制跟讀寫上限,提高整個(gè)服務(wù)的高并發(fā)能力。除了官方推出的集群模式,代理模式codis等也是將數(shù)據(jù)分片到不同節(jié)點(diǎn),codis將多個(gè)完全獨(dú)立的redis節(jié)點(diǎn)組成集群,通過codis轉(zhuǎn)發(fā)請求到某一節(jié)點(diǎn),來提高服務(wù)存儲(chǔ)能力和讀寫性能。
同樣的kafka中每個(gè)topic也支持多個(gè)partition,partition分布到多個(gè)broker上,減輕單臺(tái)機(jī)器的讀寫壓力,通過增加partition數(shù)量可以增加消費(fèi)者并行消費(fèi)消息,提高kafka的水平擴(kuò)展能力和吞吐量。
新聞每日會(huì)生產(chǎn)大量的圖文跟視頻數(shù)據(jù),底層是通過tdsql存儲(chǔ),可以分采分片化的存儲(chǔ)思想,將圖文跟視頻或者其他介質(zhì)存儲(chǔ)到不同的數(shù)據(jù)庫或者數(shù)據(jù)表中,同一種介質(zhì)每日的生產(chǎn)量也會(huì)很大,這時(shí)候就可以對同一種介質(zhì)拆分成多個(gè)數(shù)據(jù)表,進(jìn)一步提高數(shù)據(jù)庫的存儲(chǔ)量跟吞吐量。另外一種角度去優(yōu)化存儲(chǔ)還可以將冷熱數(shù)據(jù)分離,最新的數(shù)據(jù)采用性能好的機(jī)器存儲(chǔ),之前老數(shù)據(jù)訪問量低,采用性能差的機(jī)器存儲(chǔ),節(jié)省成本。
在微服務(wù)重構(gòu)過程中,需要進(jìn)行數(shù)據(jù)同步,將總庫中存儲(chǔ)的全量數(shù)據(jù)通過kafka同步到內(nèi)容微服務(wù)新的存儲(chǔ)中,預(yù)期同步qps高達(dá)15k。由于kafka的每個(gè)partition只能通過一個(gè)消費(fèi)者消費(fèi),要達(dá)到預(yù)期qps,因此需要?jiǎng)?chuàng)建750+partition才能夠?qū)崿F(xiàn),但是kafka的partition過多會(huì)導(dǎo)致rebalance很慢,影響服務(wù)性能,成本和可維護(hù)行都不高。采用分片化的思想,可以將同一個(gè)partition中的數(shù)據(jù),通過一個(gè)消費(fèi)者在內(nèi)存中分片到多個(gè)channel上,不同的channel對應(yīng)的獨(dú)立協(xié)程進(jìn)行消費(fèi),多協(xié)程并發(fā)處理消息提高消費(fèi)速度,消費(fèi)成功后寫入到對應(yīng)的成功channel,由統(tǒng)一的offsetMaker線程消費(fèi)成功消息進(jìn)行offset提交,保證消息消費(fèi)的可靠性。
避免請求
為提升寫入性能,mysql在寫入數(shù)據(jù)的時(shí)候,對于在bufferpool中的數(shù)據(jù)頁,直接修改bufferpool的數(shù)據(jù)頁并寫redolog;對于不在內(nèi)存中的數(shù)據(jù)頁并不會(huì)立刻將磁盤中的數(shù)據(jù)頁加載到bufferpool中,而是僅僅將變更記錄在緩沖區(qū),等后續(xù)讀取磁盤上的數(shù)據(jù)頁到bufferpool中時(shí)會(huì)進(jìn)行數(shù)據(jù)合并,需要注意的是對于非唯一索引才會(huì)采用這種方式,對于唯一索引寫入的時(shí)候需要每次都將磁盤上的數(shù)據(jù)讀取到bufferpool才能判斷該數(shù)據(jù)是否已存在,對于已存在的數(shù)據(jù)會(huì)返回插入失敗。
另外mysql查詢例如select * from table where name = 'xiaoming' 的查詢,如果name字段存在二級索引,由于這個(gè)查詢是*,表示需要所在行的所有字段,需要進(jìn)行回表操作,如果僅需要id和name字段,可以將查詢語句改為select id , name from tabler where name = 'xiaoming' ,這樣只需要在name這個(gè)二級索引上就可以查到所需數(shù)據(jù),避免回表操作,減少一次I/O,提升查詢速度。
web應(yīng)用中可以使用緩存、合并css和js文件等,避免或者減少http請求,提升頁面加載速度跟用戶體驗(yàn)。
在日常移動(dòng)端開發(fā)應(yīng)用中,對于多tab的數(shù)據(jù),可以采用懶加載的方式,只有用戶切換到新的tab之后才會(huì)發(fā)起請求,避免很多無用請求。服務(wù)端開發(fā)隨著版本的迭代,有些功能字段端上已經(jīng)不展示,但是服務(wù)端依然會(huì)返回?cái)?shù)據(jù)字段,對于這些不需要的數(shù)據(jù)字段可以從數(shù)據(jù)源獲取上就做下線處理,避免無用請求。另外在數(shù)據(jù)獲取時(shí)可以對請求參數(shù)的合法性做準(zhǔn)確的校驗(yàn),例如請求投票信息時(shí),運(yùn)營配置的投票ID可能是“” 或者“0”這種不合法參數(shù),如果對請求參數(shù)不進(jìn)行校驗(yàn),可能會(huì)存在很多無用I/O請求。另外在函數(shù)入口處通常會(huì)請求用戶的所有實(shí)驗(yàn)參數(shù),只有在實(shí)驗(yàn)期間才會(huì)用到實(shí)驗(yàn)參數(shù),在實(shí)驗(yàn)下線后并沒有下線ab實(shí)驗(yàn)平臺(tái)的請求,可以在非實(shí)驗(yàn)期間下線這部分請求,提升接口響應(yīng)速度。
8.池化
golang作為現(xiàn)代原生支持高并發(fā)的語言,池化技術(shù)在它的GMP模型就存在很大的應(yīng)用。對于goroutine的銷毀就不是用完直接銷毀,而是放到P的本地空閑隊(duì)列中,當(dāng)下次需要?jiǎng)?chuàng)建G的時(shí)候會(huì)從空閑隊(duì)列中直接取一個(gè)G復(fù)用即可;同樣的對于M的創(chuàng)建跟銷毀也是優(yōu)先從全局隊(duì)列中獲取或者釋放。此外golang中sync.pool可以用來保存被重復(fù)使用的對象,避免反復(fù)創(chuàng)建和銷毀對象帶來的消耗以及減輕gc壓力。
mysql等數(shù)據(jù)庫也都提供連接池,可以預(yù)先創(chuàng)建一定數(shù)量的連接用于處理數(shù)據(jù)庫請求。當(dāng)請求到來時(shí),可以從連接池中選擇空閑連接來處理請求,請求結(jié)束后將連接歸還到連接池中,避免連接創(chuàng)建和銷毀帶來的開銷,提升數(shù)據(jù)庫性能。
在日常工作中可以創(chuàng)建線程池用來處理請求,在請求到來時(shí)同樣的從鏈接池中選擇空閑的線程來處理請求,處理結(jié)束后歸還到線程池中,避免線程創(chuàng)建帶來的消耗,在web框架等需要高并發(fā)的場景下非常常見。
9.異步處理
異步處理在數(shù)據(jù)庫中同樣應(yīng)用廣泛,例如redis的bgsave,bgrewriteof就是分別用來異步保存RDB跟AOF文件的命令,bgsave執(zhí)行后會(huì)立刻返回成功,主線程fork出一個(gè)線程用來將內(nèi)存中數(shù)據(jù)生成快照保存到磁盤,而主線程繼續(xù)執(zhí)行客戶端命令;redis刪除key的方式有del跟unlink兩種,對于del命令是同步刪除,直接釋放內(nèi)存,當(dāng)遇到大key時(shí),刪除操作會(huì)讓redis出現(xiàn)卡頓的問題,而unlink是異步刪除的方式,執(zhí)行后對于key只做不可達(dá)的標(biāo)識(shí),對于內(nèi)存的回收由異步線程回收,不阻塞主線程。
mysql的主從同步支持異步復(fù)制、同步復(fù)制跟半同步復(fù)制。異步復(fù)制是指主庫執(zhí)行完提交的事務(wù)后立刻將結(jié)果返回給客戶端,并不關(guān)心從庫是否已經(jīng)同步了數(shù)據(jù);同步復(fù)制是指主庫執(zhí)行完提交的事務(wù),所有的從庫都執(zhí)行了該事務(wù)才將結(jié)果返回給客戶端;半同步復(fù)制指主庫執(zhí)行完后,至少一個(gè)從庫接收并執(zhí)行了事務(wù)才返回給客戶端。有多種主要是因?yàn)楫惒綇?fù)制客戶端寫入性能高,但是存在丟數(shù)據(jù)的風(fēng)險(xiǎn),在數(shù)據(jù)一致性要求不高的場景下可以采用,同步方式寫入性能差,適合在數(shù)據(jù)一致性要求高的場景使用。 此外對于kafka的生產(chǎn)者跟消費(fèi)者都可以采用異步的方式進(jìn)行發(fā)送跟消費(fèi)消息,但是采用異步的方式有可能會(huì)導(dǎo)致出現(xiàn)丟消息的問題。對于異步發(fā)送消息可以采用帶有回調(diào)函數(shù)的方式,當(dāng)發(fā)送失敗后通過回調(diào)函數(shù)進(jìn)行感知,后續(xù)進(jìn)行消息補(bǔ)償。
在做服務(wù)性能優(yōu)化中,發(fā)現(xiàn)之前的一些監(jiān)控上報(bào),曝光上報(bào)等操作都在主流程中,可以將這部分功能做異步處理,降低接口的時(shí)延。此外用戶發(fā)布新聞后,會(huì)將新聞寫入到個(gè)人頁索引,對圖片進(jìn)行加工處理,標(biāo)題進(jìn)行審核,或者給用戶增加活動(dòng)積分等操作,都可以采用異步處理,這里的異步處理是將發(fā)送消息這個(gè)動(dòng)作發(fā)送消息到消息隊(duì)列中,不同的場景消費(fèi)消息隊(duì)列中的消息進(jìn)行各自邏輯的處理,這種設(shè)計(jì)保證了寫入性能,也解耦不同場景業(yè)務(wù)邏輯,提高系統(tǒng)可維護(hù)性。
總結(jié)
本文主要總結(jié)進(jìn)行服務(wù)性能優(yōu)化的幾種方式,每一種方式在我們常用的中間件中都有所體現(xiàn),我想這也是我們常說多學(xué)習(xí)這些中間件的意義,學(xué)習(xí)它們不僅僅是學(xué)會(huì)如何去使用它們,也是學(xué)習(xí)它們底層優(yōu)秀的設(shè)計(jì)思想,理解為什么要這樣設(shè)計(jì),這種設(shè)計(jì)有什么好處,后續(xù)我們在架構(gòu)選型或者做服務(wù)性能優(yōu)化時(shí)都會(huì)有一定的幫助。此外性能優(yōu)化方式也給出了具體的落地實(shí)踐,
希望通過實(shí)際的應(yīng)用例子加強(qiáng)對這種優(yōu)化方式的理解。此外要做服務(wù)性能優(yōu)化,還是要從自身服務(wù)架構(gòu)出發(fā),分析服務(wù)調(diào)用鏈耗時(shí)分布跟cpu消耗,優(yōu)化有問題的rpc調(diào)用和函數(shù)。