成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

從5s優化到1s,搞懂起碼能拿40K!

開發 前端 開發工具
性能優化,有時候看起來是一個比較虛的技術需求。除非代碼慢的已經讓人無法忍受,否則,很少有公司會有覺悟投入資源去做這些工作。

[[422984]]

圖片來自 包圖網

即使你有了性能指標數據,也很難說服領導做一個由耗時 300ms 降低到 150ms 的改進,因為它沒有業務價值。

這很讓人傷心,但這是悲催的現實。性能優化,通常由有技術追求的人發起,根據觀測指標進行的正向優化。

他們通常具有工匠精神,對每一毫秒的耗時都吹毛求疵,力求完美。當然,前提是你得有時間。

優化背景和目標

我們本次的性能優化,就是由于達到了無法忍受的程度,才進行的優化工作,屬于事后補救,問題驅動的方式。這通常沒什么問題,畢竟業務第一嘛,迭代在填坑中進行。

先說背景。本次要優化的服務,請求響應時間十分的不穩定。隨著數據量的增加,大部分請求,要耗時 5-6 秒左右!超出了常人能忍受的范圍。

當然需要優化。為了說明要優化的目標,我大體畫了一下它的拓撲結構。如圖所示,這是一套微服務架構的服務。

其中,我們優化的目標,就處于一個比較靠上游的服務。它需要通過 Feign 接口,調用下游非常多的服務提供者,獲取數據后進行聚合拼接,最終通過 Zuul 網關和 Nnginx,來發送到瀏覽器客戶端。

為了觀測服務之間的調用關系和監控數據,我們接入了 Skywalking 調用鏈平臺和 Prometheus 監控平臺,收集重要的數據以便能夠進行優化決策。

要進行優化之前,我們需要首先看一下優化需要參考的兩個技術指標:

  • 吞吐量:單位時間內發生的次數。比如 QPS、TPS、HPS 等。
  • 平均響應時間:每個請求的平均耗時。

[[422985]]

平均響應時間自然是越小越好,它越小,吞吐量越高。吞吐量的增加還可以合理利用多核,通過并行度增加單位時間內的發生次數。

我們本次優化的目標,就是減少某些接口的平均響應時間,降低到 1 秒以內;增加吞吐量,也就是提高 QPS,讓單實例系統能夠承接更多的并發請求。

通過壓縮讓耗時急劇減少

我想要先介紹讓系統飛起來最重要的一個優化手段:壓縮。通過在 chrome 的 inspect 中查看請求的數據,我們發現一個關鍵的請求接口,每次要傳輸大約 10MB 的數據。這得塞了多少東西。

這么大的數據,光下載就需要耗費大量時間。如下圖所示,是我請求某網站主頁的某一個請求,其中的 content download,就代表了數據在網絡上的傳輸時間。如果用戶的帶寬非常慢,那么這個請求的耗時,將會是非常長的。

為了減少數據在網絡上的傳輸時間,可以啟用 gzip 壓縮。gzip 壓縮是屬于時間換空間的做法。對于大多數服務來說,最后一環是 Nginx,大多數人都會在 Nginx 這一層去做壓縮。

它的主要配置如下:

  1. gzip on
  2. gzip_vary on
  3. gzip_min_length 10240; 
  4. gzip_proxied expired no-cache no-store private auth; 
  5. gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; 
  6. gzip_disable "MSIE [1-6]\."

壓縮率有多驚人呢?我們可以看一下這張截圖。可以看到,數據壓縮后,由 8.95MB 縮減到了 368KB!瞬間就能夠被瀏覽器下載下來。

但是等等,Nginx 只是最外面的一環,還沒完,我們還可以讓請求更快一些。

請看下面的請求路徑,由于采用了微服務,請求的流轉就變得復雜起來:Nginx 并不是直接調用了相關得服務,它調用的是 Zuul 網關,Zuul 網關才真正調用的目標服務,目標服務又另外調用了其他服務。

內網帶寬也是帶寬,網絡延遲也會影響調用速度,同樣也要壓縮起來。

  1. nginx->zuul->服務A->服務E 

要想 Feign 之間的調用全部都走壓縮通道,還需要額外的配置。我們是 Spring Boot 服務,可以通過 okhttp 的透明壓縮進行處理。

加入它的依賴:

  1. <dependency> 
  2.  <groupId>io.github.openfeign</groupId> 
  3.  <artifactId>feign-okhttp</artifactId> 
  4. </dependency> 

開啟服務端配置:

  1. server: 
  2.   port:8888 
  3.   compression: 
  4.     enabled:true 
  5.     min-response-size:1024 
  6.     mime-types:["text/html","text/xml","application/xml","application/json","application/octet-stream"

開啟客戶端配置:

  1. feign: 
  2.   httpclient: 
  3.     enabled:false 
  4.   okhttp: 
  5.     enabled:true 

經過這些壓縮之后,我們的接口平均響應時間,直接從 5-6 秒降低到了 2-3 秒,優化效果非常顯著。

當然,我們也在結果集上做了文章,在返回給前端的數據中,不被使用的對象和字段,都進行了精簡。

但一般情況下,這些改動都是傷筋動骨的,需要調整大量代碼,所以我們在這上面用的精力有限,效果自然也有限。

并行獲取數據,響應飛快

接下來,就要深入到代碼邏輯內部進行分析了。上面我們提到,面向用戶的接口,其實是一個數據聚合接口。

它的每次請求,通過 Feign,調用了幾十個其他服務的接口,進行數據獲取,然后拼接結果集合。

為什么慢?因為這些請求全部是串行的!Feign 調用屬于遠程調用,也就是網絡 I/O 密集型調用,多數時間都在等待,如果數據滿足的話,是非常適合并行調用的。

首先,我們需要分析這幾十個子接口的依賴關系,看一下它們是否具有嚴格的順序性要求。如果大多數沒有,那就再好不過了。

分析結果喜憂參半,這堆接口,按照調用邏輯,大體上可以分為 A,B 類。首先,需要請求 A 類接口,拼接數據后,這些數據再供 B 類使用。但在 A,B 類內部,是沒有順序性要求的。

也就是說,我們可以把這個接口,拆分成順序執行的兩部分,在某個部分都可以并行的獲取數據。

那就按照這種分析結果改造試試吧,使用 concurrent 包里的 CountDownLatch,很容易的就實現了并取功能。

  1. CountDownLatch latch = new CountDownLatch(jobSize); 
  2. //submit job 
  3. executor.execute(() -> {  
  4.     //job code 
  5.  latch.countDown();  
  6. });  
  7. executor.execute(() -> {  
  8.  latch.countDown();  
  9. });  
  10. ... 
  11. //end submit 
  12. latch.await(timeout, TimeUnit.MILLISECONDS);  

結果非常讓人滿意,我們的接口耗時,又減少了接近一半!此時,接口耗時已經降低到 2 秒以下。

你可能會問,為什么不用 Java 的并行流呢?關于并行流,不建議你使用它。

并發編程一定要小心,尤其是在業務代碼中的并發編程。我們構造了專用的線程池,來支撐這個并發獲取的功能。

  1. final ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 1,  
  2.             TimeUnit.HOURS, new ArrayBlockingQueue<>(100));  

壓縮和并行化,是我們本次優化中,最有效的手段。它們直接砍掉了請求大半部分的耗時,非常的有效。但我們還是不滿足,因為每次請求,依然有 1 秒鐘以上呢。

緩存分類,進一步加速

我們發現,有些數據的獲取,是放在循環中的,有很多無效請求,這不能忍。

  1. for(List){ 
  2.     client.getData(); 

如果將這些常用的結果緩存起來,那么就可以大大減少網絡 IO 請求的次數,增加程序的運行效率。

緩存在大多數應用程序的優化中,作用非常大。但由于壓縮和并行效果的對比,緩存在我們這個場景中,效果不是非常的明顯,但依然減少了大約三四十毫秒的請求時間。

我們是這么做的:首先,我們將一部分代碼邏輯簡單,適合 Cache Aside Pattern 模式的數據,放在了分布式緩存 Redis 中。

具體來說,就是讀取的時候,先讀緩存,緩存讀不到的時候,再讀數據庫;更新的時候,先更新數據庫,再刪除緩存(延時雙刪)。

使用這種方式,能夠解決大部分業務邏輯簡單的緩存場景,并能解決數據的一致性問題。

但是,僅僅這么做是不夠的,因為有些業務邏輯非常的復雜,更新的代碼發非常的分散,不適合使用 Cache Aside Pattern 進行改造。

我們了解到,有部分數據,具有以下特點:

  • 這些數據,通過耗時的獲取之后,在極端的時間內,會被再次用到。
  • 業務數據對它們的一致性要求,可以控制在秒級別以內。
  • 對于這些數據的使用,跨代碼、跨線程,使用方式多樣。

針對于這種情況,我們設計了存在時間極短的堆內內存緩存,數據在 1 秒之后,就會失效,然后重新從數據庫中讀取。加入某個節點調用服務端接口是 1 秒鐘 1k 次,我們直接給降低到了 1 次。

在這里,使用了 Guava 的 LoadingCache,減少的 Feign 接口調用,是數量級的。

  1. LoadingCache<String, String> lc = CacheBuilder 
  2.       .newBuilder() 
  3.       .expireAfterWrite(1,TimeUnit.SECONDS) 
  4.       .build(new CacheLoader<String, String>() { 
  5.       @Override 
  6.       public String load(String key) throws Exception { 
  7.             return slowMethod(key); 
  8. }}); 

MySQL 索引的優化

我們的業務系統,使用的是 MySQL 數據庫,由于沒有專業 DBA 介入,而且數據表是使用 JPA 生成的。在優化的時候,發現了大量不合理的索引,當然是要優化掉。

由于 SQL 具有很強的敏感性,我這里只談一些在優化過程中碰到的索引優化規則問題,相信你一樣能夠在自己的業務系統中進行類比。

索引非常有用,但是要注意,如果你對字段做了函數運算,那索引就用不上了。

常見的索引失效,還有下面兩種情況:

  • 查詢的索引字段類型,與用戶傳遞的數據類型不同,要做一層隱式轉換。比如 varchar 類型的字段上,傳入了 int 參數。
  • 查詢的兩張表之間,使用的字符集不同,也就無法使用關聯字段作為索引。

MySQL 的索引優化,最基本的是遵循最左前綴原則,當有 a、b、c 三個字段的時候,如果查詢條件用到了 a,或者 a、b,或者 a、b、c,那么我們就可以創建(a,b,c)一個索引即可,它包含了 a 和 ab。

當然,字符串也是可以加前綴索引的,但在平常應用中較少。有時候,MySQL 的優化器,會選擇了錯誤的索引,我們需要使用 force index 指定所使用的索引。

在 JPA 中,就要使用 nativeQuery,來書寫綁定到 MySQL 數據庫的 SQL 語句,我們盡量的去避免這種情況。

另外一個優化是減少回表。由于 InnoDB 采用了 B+ 樹,但是如果不使用非主鍵索引,會通過二級索引(secondary index)先查到聚簇索引(clustered index),然后再定位到數據。多了一步,產生回表。

使用覆蓋索引,可以一定程度上避免回表,是常用的優化手段。具體做法,就是把要查詢的字段,與索引放在一起做聯合索引,是一種空間換時間的做法。

JVM 優化

我通常將 JVM 的優化放在最后一環。而且,除非系統發生了嚴重的卡頓,或者 OOM 問題,都不會主動對其進行過度優化。

很不幸的是,我們的應用,由于開啟了大內存(8GB+),在 JDK 1.8 默認的并行收集器下,經常發生卡頓。雖然不是很頻繁,但動輒幾秒鐘,已經嚴重影響到部分請求的平滑性。

程序剛開始,是光禿禿跑在 JVM 下的,GC 信息,還有 OOM,什么都沒留下。為了記錄 GC 信息,我們做了如下的改造。

第一步,加入 GC 問題排查的各種參數。

  1. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/xxx.hprof  -DlogPath=/opt/logs/ -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintTenuringDistribution -Xloggc:/opt/logs/gc_%p.log -XX:ErrorFile=/opt/logs/hs_error_pid%p.log 

這樣,我們就可以拿著生成的 GC 文件,上傳到 gceasy 等平臺進行分析。可以查看 JVM 的吞吐量和每個階段的延時等。

第二步,開啟 Spring Boot 的 GC 信息,接入 Promethus 監控。在 pom 中加入依賴。

  1. <dependency> 
  2.   <groupId>org.springframework.boot</groupId> 
  3.   <artifactId>spring-boot-starter-actuator</artifactId> 
  4. </dependency> 

然后配置暴露點就可以了。這樣,我們就擁有了實時的分析數據,有了優化的依據。

  1. management.endpoints.web.exposure.include=health,info,prometheus 

在觀測了 JVM 的表現之后,我們切換成了 G1 垃圾回收器。G1 有最大停頓目標,可以讓我們的 GC 時間更加的平滑。

它主要有以下幾個調優參數:

  • -XX:MaxGCPauseMillis:設置目標停頓時間,G1 會盡力達成。
  • -XX:G1HeapRegionSize:設置小堆區大小。這個值為 2 的次冪,不要太大,也不要太小。如果是在不知道如何設置,保持默認。
  • -XX:InitiatingHeapOccupancyPercent:當整個堆內存使用達到一定比例(默認是 45%),并發標記階段就會被啟動。
  • -XX:ConcGCThreads:并發垃圾收集器使用的線程數量。默認值隨 JVM 運行的平臺不同而不同。不建議修改。

切換成 G1 之后,這種不間斷的停頓,竟然神奇的消失了!期間,還發生過很多次內存溢出的問題,不過有 MAT 這種神器的加持,最終都很 easy 的被解決了。

其他優化

在工程結構和架構方面,如果有硬傷的話,那么代碼優化方面,起到的作用其實是有限的,就比如我們這種情況。

但主要代碼還是要整一下容得。有些處于高耗時邏輯中的關鍵的代碼,我們對其進行了格外的關照。

按照開發規范,對代碼進行了一次統一的清理。其中,有幾個印象比較深深刻的點。

有同學為了能夠復用 map 集合,每次用完之后,都使用 clear 方法進行清理。

  1. map1.clear(); 
  2. map2.clear(); 
  3. map3.clear(); 
  4. map4.clear(); 

這些 map 中的數據,特別的多,而 clear 方法有點特殊,它的時間復雜度事 O(n) 的,造成了較高的耗時。

  1. public void clear() { 
  2.     Node<K,V>[] tab; 
  3.     modCount++; 
  4.     if ((tab = table) != null && size > 0) { 
  5.         size = 0; 
  6.         for (int i = 0; i < tab.length; ++i) 
  7.             tab[i] = null
  8.     } 

同樣的線程安全的隊列,有 ConcurrentLinkedQueue,它的 size() 方法,時間復雜度非常高,不知怎么就被同事給用上了,這都是些性能殺手。

  1. public int size() { 
  2.         restartFromHead: for (;;) { 
  3.             int count = 0; 
  4.             for (Node<E> p = first(); p != null;) { 
  5.                 if (p.item != null
  6.                     if (++count == Integer.MAX_VALUE) 
  7.                         break;  // @see Collection.size() 
  8.                 if (p == (p = p.next)) 
  9.                     continue restartFromHead; 
  10.             } 
  11.             return count
  12.         } 

另外,有些服務的 web 頁面,本身響應就非常的慢,這是由于業務邏輯復雜,前端 JavaScript 本身就執行緩慢。

這部分代碼優化,就需要前端的同事去處理了,如圖,使用 chrome 或者 firefox 的 performance 選項卡,可以很容易發現耗時的前端代碼。

總結

性能優化,其實也是有套路的,但一般團隊都是等發生了問題才去優化,鮮有未雨綢繆的。但有了監控和 APM 就不一樣,我們能夠隨時拿到數據,反向推動優化過程。

有些性能問題,能夠在業務需求層面,或者架構層面去解決。凡是已經帶到代碼層,需要程序員介入的優化,都已經到了需求方和架構方不能再亂動,或者不想再動的境地。

性能優化首先要收集信息,找出瓶頸點,權衡 CPU、內存、網絡、、IO 等資源,然后盡量的減少平均響應時間,提高吞吐量。

緩存、緩沖、池化、減少鎖沖突、異步、并行、壓縮,都是常見的優化方式。在我們的這個場景中,起到最大作用的,就是數據壓縮和并行請求。

當然,加上其他優化方法的協助,我們的業務接口,由 5-6 秒的耗時,直接降低到了 1 秒之內,這個優化效果還是非常可觀的。估計在未來很長一段時間內,都不會再對它進行優化了。

作者:小姐姐味道

編輯:陶家龍

出處:轉載自公眾號小姐姐味道 (ID:xjjdog)

 

責任編輯:武曉燕 來源: 小姐姐味道
相關推薦

2019-06-20 11:20:25

sql優化數據庫

2023-05-14 17:16:22

分類樹SpringBoot

2013-10-25 17:14:20

iOS7性能

2023-12-25 08:24:03

雙異步數據庫Excel

2013-09-23 10:43:42

2013-09-12 10:40:43

2024-09-29 08:21:06

2025-02-14 09:30:42

2020-09-11 19:41:06

KubernetesK8SK3S

2024-01-26 14:35:03

鑒權K8sNode

2022-12-07 17:33:50

K8Skubernetes

2013-09-11 10:56:02

蘋果iPhone 5S

2013-09-11 11:05:04

蘋果iPhone 5S

2017-10-30 16:37:36

5s刷臉

2012-11-28 19:05:20

2013-09-17 15:29:51

AndroidiPhone 5S

2013-09-16 16:20:33

iPhone 5S指紋識別

2013-10-10 13:46:18

iOS 764位

2014-01-19 16:15:24

京東商城酷派

2012-10-19 14:30:54

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品久久久久久久久免费 | 国产精品一区二区免费 | 欧美精品一区二区三区在线播放 | 亚洲精品99 | 久久久久网站 | 亚洲一区在线日韩在线深爱 | 神马福利 | 99久久精品免费看国产高清 | 久久婷婷色 | 久久久精彩视频 | 久久久免费电影 | 国产精品久久久久久久久久久久冷 | 国产日韩欧美精品一区二区三区 | 久久国产欧美一区二区三区精品 | 免费人成在线观看网站 | 在线看一区二区三区 | 久久一久久 | 亚洲免费观看视频网站 | 国内精品久久久久 | 欧美三级三级三级爽爽爽 | 欧美日韩在线成人 | 国产激情一区二区三区 | 国产激情一区二区三区 | 国产精品天堂 | 国产精品一二三区 | 国产精品亚洲成在人线 | 午夜亚洲 | 美日韩免费视频 | 黄色一级免费看 | 一级全黄少妇性色生活免费看 | 精品在线一区二区三区 | 色av一区二区 | 一级毛片在线播放 | 国产精品一区二区免费 | 人人看人人草 | 亚洲视频一区在线观看 | 欧美色综合天天久久综合精品 | 精品国产1区2区3区 在线国产视频 | 在线中文视频 | 久久久久网站 | 日韩高清一区 |