教你三招從讓效能從20s最佳化到500ms
導讀 | 本文將會接著介面效能最佳化這個話題,從實戰的角度出發,聊聊我是如何最佳化一個慢查詢介面的。 |
介面效能問題,對於從事後端開發的同學來說,是一個繞不開的話題。想要最佳化一個介面的效能,需要從多個方面著手。
其實,我之前也寫過一篇介面效能最佳化相關的文章《聊聊介面效能最佳化的11個小技巧》,發表之後在全網廣受好評,感興趣的小夥們可以仔細看看。
本文將會接著介面效能最佳化這個話題,從實戰的角度出發,聊聊我是如何最佳化一個慢查詢介面的。
上週我最佳化了一下線上的批次評分查詢介面,將介面效能從最初的20s,最佳化到目前的500ms以內。
總體來說,用三招就搞定了。
到底經歷了什麼?
我們每天早上上班前,都會收到一封線上慢查詢介面彙總郵件,郵件中會展示介面地址、呼叫次數、最大耗時、平均耗時和traceId等資訊。
我看到其中有一個批次評分查詢介面,最大耗時達到了20s,平均耗時也有2s。
用skywalking檢視該介面的呼叫資訊,發現絕大數情況下,該介面響應還是比較快的,大部分情況都是500s左右就能返回,但也有少部分超過了20s的請求。
這個現象就非常奇怪了。
莫非跟資料有關?
比如:要查某一個組織的資料,是非常快的。但如果要查平臺,即組織的根節點,這種情況下,需要查詢的資料量非常大,介面響應就可能會非常慢。
但事實證明不是這個原因。
很快有個同事給出了答案。
他們在結算單列表頁面中,批次請求了這個介面,但他傳參的資料量非常大。
怎麼回事呢?
當初說的需求是這個介面給分頁的列表頁面呼叫,每頁大小有:10、20、30、50、100,使用者可以選擇。
換句話說,呼叫批次評價查詢介面,一次性最多可以查詢100條記錄。
但實際情況是:結算單列表頁面還包含了很多訂單。基本上每一個結算單,都有多個訂單。呼叫批次評價查詢介面時,需要把結算單和訂單的資料合併到一起。
這樣導致的結果是:呼叫批次評價查詢介面時,一次性傳入的引數非常多,入參list中包含幾百、甚至幾千條資料都有可能。
如果一次性傳入幾百或者幾千個id,批次查詢資料還好,可以走主鍵索引,查詢效率也不至於太差。
但那個批次評分查詢介面,邏輯不簡單。
虛擬碼如下:
public Listquery(Listlist) { //結果 Listresult = Lists.newArrayList(); //獲取組織id ListorgIds = list.stream().map(SearchEntity::getOrgId).collect(Collectors.toList()); //透過regin呼叫遠端介面獲取組織資訊 ListorgList = feginClient.getOrgByIds(orgIds); for(SearchEntity entity : list) { //透過組織id找組織code String orgCode = findOrgCode(orgList, entity.getOrgId()); //透過組合條件查詢評價 ScoreSearchEntity scoreSearchEntity = new ScoreSearchEntity(); scoreSearchEntity.setOrgCode(orgCode); scoreSearchEntity.setCategoryId(entity.getCategoryId()); scoreSearchEntity.setBusinessId(entity.getBusinessId()); scoreSearchEntity.setBusinessType(entity.getBusinessType()); ListresultList = scoreMapper.queryScore(scoreSearchEntity); if(CollectionUtils.isNotEmpty(resultList)) { ScoreEntity scoreEntity = resultList.get(0); result.add(scoreEntity); } } return result; }
其實在真實場景中,程式碼比這個複雜很多,這裡為了給大家演示,簡化了一下。
最關鍵的地方有兩點:
- 在介面中遠端呼叫了另外一個介面
- 需要在for迴圈中查詢資料
其中的第1點,即:在介面中遠端呼叫了另外一個介面,這個程式碼是必須的。
因為如果在評價表中冗餘一個組織code欄位,萬一哪天組織表中的組織code有修改,不得不透過某種機制,通知我們同步修改評價表的組織code,不然就會出現資料不一致的問題。
很顯然,如果要這樣調整的話,業務流程上要改了,程式碼改動有點大。
所以,還是先保持在介面中遠端呼叫吧。
這樣看來,可以最佳化的地方只能在:for迴圈中查詢資料。
由於需要在for迴圈中,每條記錄都要根據不同的條件,查詢出想要的資料。
由於業務系統呼叫這個介面時,沒有傳id,不好在where條件中用id in (...),這方式批次查詢資料。
其實,有一種辦法不用迴圈查詢,一條sql就能搞定需求:使用or關鍵字拼接,例如:(org_code='001' and category_id=123 and business_id=111 and business_type=1) or (org_code='002' and category_id=123 and business_id=112 and business_type=2) or (org_code='003' and category_id=124 and business_id=117 and business_type=1)...
這種方式會導致sql語句會非常長,效能也會很差。
其實還有一種寫法:
where (a,b) in ((1,2),(1,3)...)
不過這種sql,如果一次性查詢的資料量太多的話,效能也不太好。
居然沒法改成批次查詢,就只能最佳化單條查詢sql的執行效率了。
首先從索引入手,因為改造成本最低。
第一次最佳化是最佳化索引。
評價表之前建立一個business_id欄位的普通索引,但是從目前來看效率不太理想。
由於我果斷加了聯合索引:
alter table user_score add index `un_org_category_business` (`org_code`,`category_id`,`business_id`,`business_type`) USING BTREE;
該聯合索引由:org_code、category_id、business_id和business_type四個欄位組成。
經過這次最佳化,效果立竿見影。
批次評價查詢介面最大耗時,從最初的20s,縮短到了5s左右。
由於需要在for迴圈中,每條記錄都要根據不同的條件,查詢出想要的資料。
只在一個執行緒中查詢資料,顯然太慢。
那麼,為何不能改成多執行緒呼叫?
第二次最佳化,查詢資料庫由單執行緒改成多執行緒。
但由於該介面是要將查詢出的所有資料,都返回回去的,所以要獲取查詢結果。
使用多執行緒呼叫,並且要獲取返回值,這種場景使用java8中的CompleteFuture非常合適。
程式碼調整為:
CompletableFuture[] futureArray = dataList.stream() .map(data -> CompletableFuture .supplyAsync(() -> query(data), asyncExecutor) .whenComplete((result, th) -> { })).toArray(CompletableFuture[]::new); CompletableFuture.allOf(futureArray).join();
CompleteFuture的本質是建立執行緒執行,為了避免產生太多的執行緒,所以使用執行緒池是非常有必要的。
優先推薦使用ThreadPoolExecutor類,我們自定義執行緒池。
具體程式碼如下:
ExecutorService threadPool = new ThreadPoolExecutor( 8, //corePoolSize執行緒池中核心執行緒數 10, //maximumPoolSize 執行緒池中最大執行緒數 60, //執行緒池中執行緒的最大空閒時間,超過這個時間空閒執行緒將被回收 TimeUnit.SECONDS,//時間單位 new ArrayBlockingQueue(500), //佇列 new ThreadPoolExecutor.CallerRunsPolicy()); //拒絕策略
也可以使用ThreadPoolTaskExecutor類建立執行緒池:
@Configuration public class ThreadPoolConfig { /** * 核心執行緒數量,預設1 */ private int corePoolSize = 8; /** * 最大執行緒數量,預設Integer.MAX_VALUE; */ private int maxPoolSize = 10; /** * 空閒執行緒存活時間 */ private int keepAliveSeconds = 60; /** * 執行緒阻塞佇列容量,預設Integer.MAX_VALUE */ private int queueCapacity = 1; /** * 是否允許核心執行緒超時 */ private boolean allowCoreThreadTimeOut = false; @Bean("asyncExecutor") public Executor asyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setKeepAliveSeconds(keepAliveSeconds); executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut); // 設定拒絕策略,直接在execute方法的呼叫執行緒中執行被拒絕的任務 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 執行初始化 executor.initialize(); return executor; } }
經過這次最佳化,介面效能也提升了5倍。
從5s左右,縮短到1s左右。
但整體效果還不太理想。
經過前面的兩次最佳化,批次查詢評價介面效能有一些提升,但耗時還是大於1s。
出現這個問題的根本原因是:一次性查詢的資料太多。
那麼,我們為什麼不限制一下,每次查詢的記錄條數呢?
第三次最佳化,限制一次性查詢的記錄條數。其實之前也做了限制,不過最大是2000條記錄,從目前看效果不好。
限制該介面一次只能查200條記錄,如果超過200條則會報錯提示。
如果直接對該介面做限制,則可能會導致業務系統出現異常。
為了避免這種情況的發生,必須跟業務系統團隊一起討論一下最佳化方案。
主要有下面兩個方案:
在結算單列表頁中,每個結算單預設只展示1個訂單,多餘的分頁查詢。
這樣的話,如果按照每頁最大100條記錄計算的話,結算單和訂單最多一次只能查詢200條記錄。
這就需要業務系統的前端做分頁功能,同時後端介面要調整支援分頁查詢。
但目前現狀是前端沒有多餘開發資源。
由於人手不足的原因,這套方案目前只能暫時擱置。
業務系統後端之前是一次性呼叫評價查詢介面,現在改成分批呼叫。
比如:之前查詢500條記錄,業務系統只呼叫一次查詢介面。
現在改成業務系統每次只查100條記錄,分5批呼叫,總共也是查詢500條記錄。
這樣不是變慢了嗎?
答:如果那5批呼叫評價查詢介面的操作,是在for迴圈中單執行緒順序的,整體耗時當然可能會變慢。
但業務系統也可以改成多執行緒呼叫,只需最終彙總結果即可。
此時,有人可能會問題:在評價查詢介面的伺服器多執行緒呼叫,跟在其他業務系統中多執行緒呼叫不是一回事?
還不如把批次評價查詢介面的伺服器中,執行緒池的最大執行緒數調大一點?
顯然你忽略了一件事:線上應用一般不會被部署成單點。絕大多數情況下,為了避免因為伺服器掛了,造成單點故障,基本會部署至少2個節點。這樣即使一個節點掛了,整個應用也能正常訪問。
當然也可能會出現這種情況:假如掛了一個節點,另外一個節點可能因為訪問的流量太大了,扛不住壓力,也可能因此掛掉。
換句話說,透過業務系統中的多執行緒呼叫介面,可以將訪問介面的流量負載均衡到不同的節點上。
他們也用8個執行緒,將資料分批,每批100條記錄,最後將結果彙總。
經過這次最佳化,介面效能再次提升了1倍。
從1s左右,縮短到小於500ms。
溫馨提醒一下,無論是在批次查詢評價介面查詢資料庫,還是在業務系統中呼叫批次查詢評價介面,使用多執行緒呼叫,都只是一個臨時方案,並不完美。
這樣做的原因主要是為了先快速解決問題,因為這種方案改動是最小的。
要從根本上解決問題,需要重新設計這一套功能,需要修改表結構,甚至可能需要修改業務流程。但由於牽涉到多條業務線,多個業務系統,只能排期慢慢做了。
原文來自:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2906617/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 使用 查詢分離 後 從20s最佳化到500ms
- 從20s優化到500ms,我用了這三招優化
- Linux效能調優從最佳化思路說起Linux
- 手把手教你Python(從不懂到入門)Python
- 重新架構:從 Redis 到 SQLite 效能提升架構RedisSQLite
- 從2s最佳化到0.1s
- 手把手教你從模型訓練到部署(一)模型
- 教你從0到1搭建小程式音視訊
- Agent 工具開發指南:從設計到最佳化
- 行列式求值,從 $n!$ 最佳化到 $n^3$
- Spark Streaming的最佳化之路—從Receiver到Direct模式Spark模式
- 教你快速從SQL過度到Elasticsearch的DSL查詢SQLElasticsearch
- [轉]從0到1教你設計業務系統
- RAG應用效能最佳化全景圖:從查詢到生成的6個關鍵階段
- 前端效能優化–從 10 多秒到 1.05 秒前端優化
- 前端效能優化--從 10 多秒到 1.05 秒前端優化
- 手把手教你React Native實戰從 React到Rn《二》React Native
- 移動web效能優化從入門到進階Web優化
- 網站效能優化從入門到粗通(PHP 篇)網站優化PHP
- 從2012到2021,從土木到程式設計師程式設計師
- 只需6步,教你從零開發一個簽到小程式
- 從零到一教你基於vue開發一個元件庫Vue元件
- 手把手教你如何從0到1做短影片運營
- 效能測試公開課來啦!從效能測試方案到效能調優,從負載均衡到中介軟體測試,全方位講解效能測試核心內容負載
- 【Python從入門到精通】(七)Python字典(dict)讓人人都Python
- 架構師日記-從程式碼到設計的效能最佳化指南 | 京東雲技術團隊架構
- 效能優化資料庫篇-從單機到叢集優化資料庫
- 資料同步:教你如何實時把資料從 MySQL 同步到 OceanBaseMySql
- 從DDPM到DDIM
- 從RNN到BERTRNN
- C++ 從&到&&C++
- 從HTTP到HTTPSHTTP
- 從DevOps到ContainerOpsdevAI
- 從SpringBoot到SpringMVCSpring BootSpringMVC
- 從Windows到LinuxWindowsLinux
- Webpack5構建效能最佳化:構建耗時從150s到60s再到10sWeb
- 從零開始入門 K8s | etcd 效能最佳化實踐K8S
- Java中的集合框架深度解析:從ArrayList到ConcurrentHashMap的效能考量Java框架HashMap