購物網(wǎng)站策劃案廈門谷歌seo公司
高并發(fā)寫優(yōu)化理論
對于高并發(fā)的讀QPS優(yōu)化手段較多,最經(jīng)濟簡單的方式是上緩存。但是對于高并發(fā)寫TPS該如何提升?業(yè)界常用的有分庫分表、異步寫入等技術(shù)手段。但是分庫分表對于業(yè)務(wù)的改造十分巨大,涉及遷移數(shù)據(jù)的麻煩工作,不會作為常用的優(yōu)化手段。異步寫入到時經(jīng)常在實際工作中使用,但是也不適合所有場景,特別對于帶有事務(wù)的寫入請求,帶事務(wù)的寫入請求通常是需要同步告知用戶處理結(jié)果,所以不適用異步處理。
我們都知道批處理會比單條處理快很多,只需要發(fā)起一次網(wǎng)絡(luò)請求,在網(wǎng)絡(luò)層面節(jié)省了N次TCP連接獲取和發(fā)送數(shù)據(jù)的步驟。實際我測試過,通過shark抓包,發(fā)現(xiàn)建立一條TCP連接可能需要耗費10ms~50ms左右。如果是跨洲際的TCP連接更久,可能耗費幾百毫秒。單是節(jié)省的多次TCP連接就能節(jié)省不少時間,其次還有程序代碼的循環(huán)執(zhí)行時間。所以將多個寫請求聚合成一個合適大小的批量寫請求,一次性將數(shù)據(jù)發(fā)送給服務(wù)器進行批量寫入是最高效的。
MySQL的組提交原理
在MySQL層面,為了保證事務(wù)的可靠性和數(shù)據(jù)同步給備節(jié)點、從節(jié)點的可靠性。通常會開啟雙一設(shè)置。在雙一設(shè)置開啟后,就會在事務(wù)提交前將redo log、binlog落盤,事務(wù)才返回成功,這就是WAL機制。
sync_binlog=1
innodb_flush_log_at_trx_commit=1
我們知道由于WAL機制,寫入請求在修改了數(shù)據(jù)頁后不會立即刷回磁盤,而是通過記錄rodo log和binlog保證事務(wù)的持久性和同步給從節(jié)點。寫rodo log和binlog就是順序?qū)懭氲?#xff0c;涉及磁盤的順序?qū)憴C制。磁盤順序?qū)憰入S機寫快很多。MySQL為了進一步提升多個事務(wù)在高并發(fā)下寫入binlog的性能,采用了“組提交”的概念。顧名思義就是將多個事務(wù)在單位時間內(nèi)聚集起來,一起寫入磁盤,就變成了多事務(wù)的批量順序?qū)懭?#xff0c;性能高很多。
這里簡單介紹組提交。首先MySQL有2個參數(shù)控制組提交的等待時間和組大小。
binlog_group_commit_sync_delay=N:在等待N μs后,開始事務(wù)刷盤(圖中Sync binlog)
binlog_group_commit_sync_no_delay_count=N:如果隊列中的事務(wù)數(shù)達到N個,就忽視binlog_group_commit_sync_delay的設(shè)置,直接開始刷盤
解釋下這張圖。首先在第一步就已經(jīng)將redo log刷到磁盤了,接下來就是將多個事務(wù)聚合在一個組調(diào)用write函數(shù)寫入OS的緩沖。第一個到達的事務(wù)就會開啟一個新組,等待N個事務(wù)到達或者等待N微秒之后主動提交。假設(shè)事務(wù)T1到達并開啟新組1,等待T2來到加入組1,等待時間滿后T1主動調(diào)用write函數(shù)將T1、T2事務(wù)都寫入OS緩沖。此時T1、T2組成的組1進入第二個階段,準備調(diào)用flush函數(shù)將緩沖區(qū)的數(shù)據(jù)刷入磁盤。組1在第二階段繼續(xù)等待新事務(wù)加入,此時有新組到達就會將組2和組1合并新組,再調(diào)用flush函數(shù)將組1、組2數(shù)據(jù)刷入磁盤。整個過程是批量+順序?qū)懭氪疟P,是很高效的。
我的組提交Spring組件
?我把這個組提交管理器的組件放到我GitHub上了,大家覺得不錯的請Star,或覺得有優(yōu)化空間的請?zhí)岢鰉r,有錯誤的請斧正。
GitHub-組提交管理器
我們基于以上的理論分析,可以得出如果我們在高并發(fā)寫入的時候能夠模仿MySQL的組提交,實現(xiàn)一個主動等待和被動喚醒提交的組提交機制,將多個寫入請求合成一個請求發(fā)送給MySQL就能提高寫入性能。
總結(jié)MySQL的組提交機制原理:
- 第一個到達的線程開啟新組作為本組Leader領(lǐng)導(dǎo)本組的數(shù)據(jù)提交
- Leader等待指定X毫秒時間,時間到后主動發(fā)起提交
- 第K個線程到達,若發(fā)現(xiàn)本組負載滿了喚醒Leader進行本組提交
- 組與組之間互不阻塞,單位時間內(nèi)可能有多個組并發(fā)提交
基于以上原理,我設(shè)計了兩個類:GroupManager組管理器、GroupCommit組提交對象。GroupManager負責接收外部線程提交的數(shù)據(jù),然后放到當前組里。并且實現(xiàn)整個組提交的流程。GroupCommit是一個組的具象化對象,提供一個組的入隊,提交數(shù)據(jù),掛起等待,喚醒Leader等基礎(chǔ)方法,給GroupManager調(diào)用以實現(xiàn)組提交機制。
為了避免高并發(fā)時多線程競相進入組內(nèi),導(dǎo)致組錯亂,使用了兩把鎖解決。大部分線程都會被擋在第一關(guān),每次只會放一個線程進到臨界區(qū)嘗試入組。入組之前要先獲得當前組的鎖,為什么要第二把鎖?因為Leader會主動醒來提交本組的數(shù)據(jù)隊列,所以提交時要確保所有資源都是排他的,需要組內(nèi)鎖來保證。入組的線程搶到組內(nèi)鎖之后就代表可以安全入組,此時有三種情況:
- 如果此時入組前發(fā)現(xiàn)組已經(jīng)滿了就開一個新組自己當Leader并喚醒當前組的Leader讓它趕快提交
- 如果入組后發(fā)現(xiàn)組滿了,喚醒當前組Leader讓它趕快提交,自己則掛起等待提交后喚醒
- 入組后發(fā)現(xiàn)還未滿,掛起自己等待喚醒
線程在獲取到組內(nèi)鎖后都會立即釋放GroupManager的鎖,目的是讓后續(xù)線程如果發(fā)現(xiàn)當前組滿了,就立即開新組提交,提高效率。
系統(tǒng)架構(gòu)
?因為我們工作中大多數(shù)使用的是Tomcat容器,目前Tomcat的IO處理模型是Reactor+線程池的模式。
在整個系統(tǒng)架構(gòu)層面,組提交影響性能的有兩個參數(shù):組大小和等待時間。組大小就是在組內(nèi)掛起等待的線程數(shù),等待時間是Leader主動等待的毫秒數(shù)。組大小直接影響到剩余可工作的線程數(shù),Tomcat線程數(shù)量默認200,通常我們根據(jù)業(yè)務(wù)場景和硬件資源調(diào)整,線程數(shù)量也就幾百左右。如果組大小太大同時等待時間太久!!直接把Tomcat所有線程都掛起了這時服務(wù)器就假死了,所以對組大小的設(shè)置建議通過壓測來確定,按照下面的壓測經(jīng)驗一般建議設(shè)置為Tomcat線程數(shù)量的1/4~1/2。這樣最大1/2能確保還有一半線程可以服務(wù)其它請求。
等待時間,因為這個參數(shù)會導(dǎo)致接口RT上升,建議設(shè)置在5ms~20ms之間。我們生產(chǎn)MySQL的組提交等待時間設(shè)置500微秒,是很短的。我經(jīng)過反復(fù)壓測和調(diào)參發(fā)現(xiàn),純MySQL插入操作,等待時間5ms左右就合適了。
總結(jié)起來,組大小和等待時間需要根據(jù)業(yè)務(wù)類型和Tomcat線程數(shù)量和CPU數(shù)量,經(jīng)過測試來決定一個合適的參數(shù),沒有通用的方法論能決定。
在整個系統(tǒng)架構(gòu)層面,負載均衡器和服務(wù)器Pod,Tomcat線程池和多個組提交的關(guān)系。
壓測報告
環(huán)境介紹
- Mac OS M2 10核16G,SSD
- MySQL 8.0
- JDK8u221
- SpringBoot,Tomcat線程池400
- Druid數(shù)據(jù)庫連接池 40連接數(shù)
- Jmeter 5.3,700線程并發(fā),循環(huán)1000,共70萬請求
- JVM參數(shù)設(shè)置
-XX:-ClassUnloadingWithConcurrentMark -Xms4g -Xmx4g -Xmn3g -XX:G1HeapRegionSize=4m -XX:InitiatingHeapOccupancyPercent=30 -XX:MaxGCPauseMillis=200 -XX:MaxMetaspaceSize=268435456 -XX:MetaspaceSize=268435456 -XX:ParallelGCThreads=10 -XX:+ParallelRefProcEnabled -XX:-ReduceInitialCardMarks -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
MySQL沒有經(jīng)過調(diào)優(yōu)都是默認的參數(shù)。MySQL和應(yīng)用服務(wù)還有Jmeter都是在Mac上運行的。對比兩種測試用例:1.使用組提交組件 2.單條數(shù)據(jù)寫入。
@PostMapping("/submit")public Boolean submit() {long tid = Thread.currentThread().getId();log.info("threadId={}", tid);OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderNo(UUID.randomUUID().toString());orderInfo.setAddressId(123321123321123L);orderInfo.setMerchantId(123321123321123L);orderInfo.setUserId(123321123321123L);orderInfo.setOrderAmount(BigDecimal.valueOf(123123L));return groupManager.queueGroup(orderInfo);}@PostMapping("/submit2")public Boolean submit2() {OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderNo(UUID.randomUUID().toString());orderInfo.setAddressId(123321123321123L);orderInfo.setMerchantId(123321123321123L);orderInfo.setUserId(123321123321123L);orderInfo.setOrderAmount(BigDecimal.valueOf(123123L));return orderInfoService.save(orderInfo);}
經(jīng)過反復(fù)實驗以及調(diào)整組提交的組大小、等待時間參數(shù),得出組大小200,等待時間5ms,得出的TPS是比較好的。TPS達到近8800。接口錯誤率幾乎沒有
單提交(每次請求提交一次)所有配置和環(huán)境一致的情況下。并發(fā)700,循環(huán)1000次,70萬請求。TPS在5200。錯誤率0
可以看出組提交比單提交TPS高出68%左右,優(yōu)化比較明顯。如果能針對組大小和等待時間繼續(xù)調(diào)整優(yōu)化,可能TPS會更高。RT上平均時間比但提交快了1倍,但是P99、P95、P90都比單提交要慢1倍。
?