1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        接口性能優(yōu)化技巧

        共 5533字,需瀏覽 12分鐘

         ·

        2022-05-24 16:50

        文章來源:http://b.nxw.so/1jSSgg



        目錄


        • 背景

        • 哪些問題會引起接口性能問題

        • 問題解決

        • 總結



        背景




        我負責的系統(tǒng)在去年初就完成了功能上的建設,然后開始進入到推廣階段。隨著推廣的逐步深入,收到了很多好評的同時也收到了很多對性能的吐槽。


        剛剛收到吐槽的時候,我們的心情是這樣的:

        當越來越多對性能的吐槽反饋到我們這里的時候,我們意識到,接口性能的問題的優(yōu)先級必須提高了。


        然后我們就跟蹤了 1 周的接口性能監(jiān)控,這個時候我們的心情是這樣的:

        有 20 多個慢接口,5 個接口響應時間超過 5s,1 個超過 10s,其余的都在 2s 以上,穩(wěn)定性不足 99.8%。


        作為一個優(yōu)秀的后端程序員,這個數(shù)據(jù)肯定是不能忍的,我們馬上就進入了漫長的接口優(yōu)化之路。本文就是對我們漫長工作歷程的一個總結。


        哪些問題會引起接口性能問題


        這個問題的答案非常多,需要根據(jù)自己的業(yè)務場景具體分析。


        這里做一個不完全的總結:

        • 數(shù)據(jù)庫慢查詢

        • 業(yè)務邏輯復雜

        • 線程池設計不合理

        • 鎖設計不合理

        • 機器問題(fullGC,機器重啟,線程打滿)

        • 萬金油解決方式


        問題解決


        | 慢查詢(基于 mysql)

        ①深度分頁


        所謂的深度分頁問題,涉及到 mysql 分頁的原理。通常情況下,mysql 的分頁是這樣寫的:
        select?name,code?from?student?limit?100,20


        含義當然就是從 student 表里查 100 到 120 這 20 條數(shù)據(jù),mysql 會把前 120 條數(shù)據(jù)都查出來,拋棄前 100 條,返回 20 條。


        當分頁所以深度不大的時候當然沒問題,隨著分頁的深入,sql 可能會變成這樣:
        select?name,code?from?student?limit?1000000,20


        這個時候,mysql 會查出來 1000020 條數(shù)據(jù),拋棄 1000000 條,如此大的數(shù)據(jù)量,速度一定快不起來。


        那如何解決呢?一般情況下,最好的方式是增加一個條件:
        select?name,code?from?student?where?id>1000000??limit?20


        這樣,mysql 會走主鍵索引,直接連接到 1000000 處,然后查出來 20 條數(shù)據(jù)。但是這個方式需要接口的調用方配合改造,把上次查詢出來的最大 id 以參數(shù)的方式傳給接口提供方,會有溝通成本(調用方:老子不改!)。


        ②未加索引


        這個是最容易解決的問題,我們可以通過:
        show?create?table?xxxx(表名)


        查看某張表的索引。具體加索引的語句網(wǎng)上太多了,不再贅述。不過順便提一嘴,加索引之前,需要考慮一下這個索引是不是有必要加,如果加索引的字段區(qū)分度非常低,那即使加了索引也不會生效。


        另外,加索引的 alter 操作,可能引起鎖表,執(zhí)行 sql 的時候一定要在低峰期(血淚史!?。。。?/span>


        ③索引失效


        這個是慢查詢最不好分析的情況,雖然 mysql 提供了 explain 來評估某個 sql 的查詢性能,其中就有使用的索引。


        但是為啥索引會失效呢?mysql 卻不會告訴咱,需要咱自己分析。大體上,可能引起索引失效的原因有這幾個(可能不完全):

        需要特別提出的是,關于字段區(qū)分性很差的情況,在加索引的時候就應該進行評估。如果區(qū)分性很差,這個索引根本就沒必要加。


        區(qū)分性很差是什么意思呢,舉幾個例子,比如:

        • 某個字段只可能有 3 個值,那這個字段的索引區(qū)分度就很低。

        • 再比如,某個字段大量為空,只有少量有值;

        • 再比如,某個字段值非常集中,90% 都是 1,剩下 10% 可能是 2,3,4....


        進一步的,那如果不符合上面所有的索引失效的情況,但是 mysql 還是不使用對應的索引,是為啥呢?


        這個跟 mysql 的 sql 優(yōu)化有關,mysql 會在 sql 優(yōu)化的時候自己選擇合適的索引,很可能是 mysql 自己的選擇算法算出來使用這個索引不會提升性能,所以就放棄了。


        這種情況,可以使用 force index 關鍵字強制使用索引(建議修改前先實驗一下,是不是真的會提升查詢效率):
        select?name,code?from?student?force?index(XXXXXX)?where?name?=?'天才'?


        其中 xxxx 是索引名。

        ④join 過多 or 子查詢過多


        我把 join 過多和子查詢過多放在一起說了。一般來說,不建議使用子查詢,可以把子查詢改成 join 來優(yōu)化。同時,join 關聯(lián)的表也不宜過多,一般來說 2-3 張表還是合適的。


        具體關聯(lián)幾張表比較安全是需要具體問題具體分析的,如果各個表的數(shù)據(jù)量都很少,幾百條幾千條,那么關聯(lián)的表的可以適當多一些,反之則需要少一些。


        另外需要提到的是,在大多數(shù)情況下 join 是在內存里做的,如果匹配的量比較小,或者 join_buffer 設置的比較大,速度也不會很慢。


        但是,當 join 的數(shù)據(jù)量比較大的時候,mysql 會采用在硬盤上創(chuàng)建臨時表的方式進行多張表的關聯(lián)匹配,這種顯然效率就極低,本來磁盤的 IO 就不快,還要關聯(lián)。


        一般遇到這種情況的時候就建議從代碼層面進行拆分,在業(yè)務層先查詢一張表的數(shù)據(jù),然后以關聯(lián)字段作為條件查詢關聯(lián)表形成 map,然后在業(yè)務層進行數(shù)據(jù)的拼裝。


        一般來說,索引建立正確的話,會比 join 快很多,畢竟內存里拼接數(shù)據(jù)要比網(wǎng)絡傳輸和硬盤 IO 快得多。


        ⑤in 的元素過多


        這種問題,如果只看代碼的話不太容易排查,最好結合監(jiān)控和數(shù)據(jù)庫日志一起分析。如果一個查詢有 in,in 的條件加了合適的索引,這個時候的 sql 還是比較慢就可以高度懷疑是 in 的元素過多。


        一旦排查出來是這個問題,解決起來也比較容易,不過是把元素分個組,每組查一次。想再快的話,可以再引入多線程。


        進一步的,如果in的元素量大到一定程度還是快不起來,這種最好還是有個限制:
        select?id?from?student?where?id?in?(1,2,3?......?1000)?limit?200


        當然了,最好是在代碼層面做個限制:

        if?(ids.size()?>?200)?{
        ????throw?new?Exception("單次查詢數(shù)據(jù)量不能超過200");
        }

        ⑥單純的數(shù)據(jù)量過大


        這種問題,單純代碼的修修補補一般就解決不了了,需要變動整個的數(shù)據(jù)存儲架構?;蛘呤菍Φ讓?mysql 分表或分庫+分表;或者就是直接變更底層數(shù)據(jù)庫,把 mysql 轉換成專門為處理大數(shù)據(jù)設計的數(shù)據(jù)庫。


        這種工作是個系統(tǒng)工程,需要嚴密的調研、方案設計、方案評審、性能評估、開發(fā)、測試、聯(lián)調,同時需要設計嚴密的數(shù)據(jù)遷移方案、回滾方案、降級措施、故障處理預案。


        除了以上團隊內部的工作,還可能有跨系統(tǒng)溝通的工作,畢竟做了重大變更,下游系統(tǒng)的調用接口的方式有可能會需要變化。


        出于篇幅的考慮,這個不再展開了,筆者有幸完整參與了一次億級別數(shù)據(jù)量的數(shù)據(jù)庫分表工作,對整個過程的復雜性深有體會,后續(xù)有機會也會分享出來。


        | 業(yè)務邏輯復雜

        ①循環(huán)調用


        這種情況,一般都循環(huán)調用同一段代碼,每次循環(huán)的邏輯一致,前后不關聯(lián)。


        比如說,我們要初始化一個列表,預置 12 個月的數(shù)據(jù)給前端:
        List?list?=?new?ArrayList<>();
        for(int?i?=?0?;?i?12?;?i?++)?{
        ????Model?model?=?calOneMonthData(i);?//?計算某個月的數(shù)據(jù),邏輯比較復雜,難以批量計算,效率也無法很高
        ????list.add(model);
        }


        這種顯然每個月的數(shù)據(jù)計算相互都是獨立的,我們完全可以采用多線程方式進行:
        //?建立一個線程池,注意要放在外面,不要每次執(zhí)行代碼就建立一個,具體線程池的使用就不展開了
        public?static?ExecutorService?commonThreadPool?=?new?ThreadPoolExecutor(5,?5,?300L,
        ????????TimeUnit.SECONDS,?new?LinkedBlockingQueue<>(10),?commonThreadFactory,?new?ThreadPoolExecutor.DiscardPolicy());

        //?開始多線程調用
        List>?futures?=?new?ArrayList<>();
        for(int?i?=?0?;?i?12?;?i?++)?{
        ????Future?future?=?commonThreadPool.submit(()?->?calOneMonthData(i););
        ????futures.add(future);
        }

        //?獲取結果
        List?list?=?new?ArrayList<>();
        try?{
        ???for?(int?i?=?0?;?i???????list.add(futures.get(i).get());
        ???}
        }?catch?(Exception?e)?{
        ???LOGGER.error("出現(xiàn)錯誤:",?e);
        }


        ②順序調用


        如果不是類似上面循環(huán)調用,而是一次次的順序調用,而且調用之間沒有結果上的依賴,那么也可以用多線程的方式進行,例如:
        代碼上看:
        A?a?=?doA();
        B?b?=?doB();

        C?c?=?doC(a,?b);

        D?d?=?doD(c);
        E?e?=?doE(c);

        return?doResult(d,?e);


        那么可用 CompletableFuture 解決:
        CompletableFuture?futureA?=?CompletableFuture.supplyAsync(()?->?doA());
        CompletableFuture?futureB?=?CompletableFuture.supplyAsync(()?->?doB());
        CompletableFuture.allOf(futureA,futureB)?//?等a?b?兩個任務都執(zhí)行完成

        C?c?=?doC(futureA.join(),?futureB.join());

        CompletableFuture?futureD?=?CompletableFuture.supplyAsync(()?->?doD(c));
        CompletableFuture?futureE?=?CompletableFuture.supplyAsync(()?->?doE(c));
        CompletableFuture.allOf(futureD,futureE)?//?等d?e兩個任務都執(zhí)行完成

        return?doResult(futureD.join(),futureE.join());


        這樣 A B 兩個邏輯可以并行執(zhí)行,D E 兩個邏輯可以并行執(zhí)行,最大執(zhí)行時間取決于哪個邏輯更慢。


        | 線程池設計不合理

        有的時候,即使我們使用了線程池讓任務并行處理,接口的執(zhí)行效率仍然不夠快,這種情況可能是怎么回事呢?


        這種情況首先應該懷疑是不是線程池設計的不合理。我覺得這里有必要回顧一下線程池的三個重要參數(shù):核心線程數(shù)、最大線程數(shù)、等待隊列。


        這三個參數(shù)是怎么打配合的呢?當線程池創(chuàng)建的時候,如果不預熱線程池,則線程池中線程為 0。當有任務提交到線程池,則開始創(chuàng)建核心線程。

        當核心線程全部被占滿,如果再有任務到達,則讓任務進入等待隊列開始等待。

        如果隊列也被占滿,則開始創(chuàng)建非核心線程運行。

        如果線程總數(shù)達到最大線程數(shù),還是有任務到達,則開始根據(jù)線程池拋棄規(guī)則開始拋棄。

        那么這個運行原理與接口運行時間有什么關系呢?


        在排查的時候,只要找到了問題出現(xiàn)的原因,那么解決方式也就清楚了,無非就是調整線程池參數(shù),按照業(yè)務拆分線程池等等。


        | 鎖設計不合理

        鎖設計不合理一般有兩種:鎖類型使用不合理 or 鎖過粗。


        鎖類型使用不合理的典型場景就是讀寫鎖。也就是說,讀是可以共享的,但是讀的時候不能對共享變量寫;而在寫的時候,讀寫都不能進行。


        在可以加讀寫鎖的時候,如果我們加成了互斥鎖,那么在讀遠遠多于寫的場景下,效率會極大降低。


        鎖過粗則是另一種常見的鎖設計不合理的情況,如果我們把鎖包裹的范圍過大,則加鎖時間會過長,例如:
        public?synchronized?void?doSome()?{
        ????File?f?=?calData();
        ????uploadToS3(f);
        ????sendSuccessMessage();
        }


        這塊邏輯一共處理了三部分,計算、上傳結果、發(fā)送消息。顯然上傳結果和發(fā)送消息是完全可以不加鎖的,因為這個跟共享變量根本不沾邊。


        因此完全可以改成:

        public?void?doSome()?{
        ????File?f?=?null;
        ????synchronized(this)?{
        ????????f?=?calData();
        ????}
        ????uploadToS3(f);
        ????sendSuccessMessage();
        }


        | 機器問題(fullGC,機器重啟,線程打滿)

        造成這個問題的原因非常多,筆者就遇到了定時任務過大引起 fullGC,代碼存在線程泄露引起 RSS 內存占用過高進而引起機器重啟等待諸多原因。


        需要結合各種監(jiān)控和具體場景具體分析,進而進行大事務拆分、重新規(guī)劃線程池等等工作。


        | 萬金油解決方式

        萬金油這個形容詞是從我們單位某位老師那里學來的,但是筆者覺得非常貼切。這些萬金油解決方式往往能解決大部分的接口緩慢的問題,而且也往往是我們解決接口效率問題的最終解決方案。


        當我們實在是沒有辦法排查出問題,或者實在是沒有優(yōu)化空間的時候,可以嘗試這種萬金油的方式。


        ①緩存


        緩存是一種空間換取時間的解決方案,是在高性能存儲介質上(例如:內存、SSD 硬盤等)存儲一份數(shù)據(jù)備份。


        當有請求打到服務器的時候,優(yōu)先從緩存中讀取數(shù)據(jù)。如果讀取不到,則再從硬盤或通過網(wǎng)絡獲取數(shù)據(jù)。


        由于內存或 SSD 相比硬盤或網(wǎng)絡 IO 的效率高很多,則接口響應速度會變快非常多。緩存適合于應用在數(shù)據(jù)讀遠遠大于數(shù)據(jù)寫,且數(shù)據(jù)變化不頻繁的場景中。


        從技術選型上看,有這些:


        當然,memcached 現(xiàn)在用的很少了,因為相比于 redis 他不占優(yōu)勢。tair 則是阿里開發(fā)的一個分布式緩存中間件,他的優(yōu)勢是理論上可以在不停服的情況下,動態(tài)擴展存儲容量,適用于大數(shù)據(jù)量緩存存儲。


        相比于單機 redis 緩存當然有優(yōu)勢,而他與可擴展 Redis 集群的對比則需要進一步調研。


        進一步的,當前緩存的模型一般都是 key-value 模型。如何設計 key 以提高緩存的命中率是個大學問,好的 key 設計和壞的 key 設計所提升的性能差別非常大。


        而且,key 設計是沒有一定之規(guī)的,需要結合具體的業(yè)務場景去分析。各個大公司分享出來的相關文章,緩存設計基本上是最大篇幅。


        ②回調 or 反查


        這種方式往往是業(yè)務上的解決方式,在訂單或者付款系統(tǒng)中應用的比較多。


        舉個例子:當我們付款的時候,需要調用一個專門的付款系統(tǒng)接口,該系統(tǒng)經過一系列驗證、存儲工作后還要調用銀行接口以執(zhí)行付款。


        由于付款這個動作要求十分嚴謹,銀行側接口執(zhí)行可能比較緩慢,進而拖累整個付款接口性能。


        這個時候我們就可以采用 fast success 的方式:當必要的校驗和存儲完成后,立即返回 success,同時告訴調用方一個中間態(tài)“付款中”。


        而后調用銀行接口,當獲得支付結果后再調用上游系統(tǒng)的回調接口返回付款的最終結果“成果”or“失敗”。這樣就可以異步執(zhí)行付款過程,提升付款接口效率。


        當然,為了防止多業(yè)務方接入的時候回調接口不統(tǒng)一,可以把結果拋進 kafka,讓調用方監(jiān)聽自己的結果。

        總結


        本文是筆者對工作中遇到的性能優(yōu)化問題的一個簡單的總結,可能有不完備的地方, 歡迎技術交流。

        瀏覽 27
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

          <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            2亚洲女互喂奶挤奶 | 国产a一片 | 巨胸喷奶水wwww贱多视频 | 91porny真实丨国产jk | 91麻豆视频免费 | 午夜精品一区二区三区免费视频 | 精品一区二区免费视频 | 精品xxxx户外露出视频 | 9l农村站街老熟女露脸 | 九色丨porny丨自拍视频 |