<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    聊聊接口優化的幾種方法

    VSole2022-07-20 17:24:37

    背景

    我負責的系統到2021年初完成了功能上的建設,開始進入到推廣階段。隨著推廣的逐步深入,收到了很多好評的同時也收到了很多對性能的吐槽。剛剛收到吐槽的時候,我們的心情是這樣的:

    我有點不信

    當越來越多對性能的吐槽反饋到我們這里的時候,我們意識到,接口性能的問題的優先級必須提高了。然后我們就跟蹤了1周的接口性能監控,這個時候我們的心情是這樣的:

    自己體會這種心情

    有20多個慢接口,5個接口響應時間超過5s,1個超過10s,其余的都在2s以上,穩定性不足99.8%。作為一個優秀的后端程序員,這個數據肯定是不能忍的,我們馬上就進入了漫長的接口優化之路。本文就是對我們漫長工作歷程的一個總結。

    正文開始!

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

    這個問題的答案非常多,需要根據自己的業務場景具體分析。這里做一個不完全的總結:

    • 數據庫慢查詢
    • 深度分頁問題
    • 未加索引
    • 索引失效
    • join過多
    • 子查詢過多
    • in中的值太多
    • 單純的數據量過大
    • 業務邏輯復雜
    • 循環調用
    • 順序調用
    • 線程池設計不合理
    • 鎖設計不合理
    • 機器問題(fullGC,機器重啟,線程打滿)

    問題解決

    本文列進的慢查詢問題默認都是基于 MySQL。

    慢查詢(基于mysql)深度分頁

    所謂的深度分頁問題,涉及到mysql分頁的原理。通常情況下,mysql的分頁是這樣寫的:

    select name,code from student limit 100,20
    

    含義當然就是從student表里查100到120這20條數據,mysql會把前120條數據都查出來,拋棄前100條,返回20條。當分頁所以深度不大的時候當然沒問題,隨著分頁的深入,sql可能會變成這樣:

    select name,code from student limit 1000000,20
    

    這個時候,mysql會查出來1000020條數據,拋棄1000000條,如此大的數據量,速度一定快不起來。那如何解決呢?一般情況下,最好的方式是增加一個條件:

    select name,code from student where id>1000000  limit 20
    

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

    慢查詢未加索引

    這個是最容易解決的問題,我們可以通過

    show create table xxxx(表名)
    

    查看某張表的索引。具體加索引的語句網上太多了,不再贅述。不過順便提一嘴,加索引之前,需要考慮一下這個索引是不是有必要加,如果加索引的字段區分度非常低,那即使加了索引也不會生效。另外,加索引的alter操作,可能引起鎖表,執行sql的時候一定要在低峰期(血淚史!!!!)

    慢查詢索引失效

    這個是慢查詢最不好分析的情況,雖然mysql提供了explain來評估某個sql的查詢性能,其中就有使用的索引。但是為啥索引會失效呢?mysql卻不會告訴咱,需要咱自己分析。大體上,可能引起索引失效的原因有這幾個(可能不完全):

    慢查詢索引失效

    需要特別提出的是,關于字段區分性很差的情況,在加索引的時候就應該進行評估。如果區分性很差,這個索引根本就沒必要加。區分性很差是什么意思呢,舉幾個例子,比如:

    • 某個字段只可能有3個值,那這個字段的索引區分度就很低。
    • 再比如,某個字段大量為空,只有少量有值;
    • 再比如,某個字段值非常集中,90%都是1,剩下10%可能是2,3,4....

    進一步的,那如果不符合上面所有的索引失效的情況,但是mysql還是不使用對應的索引,是為啥呢?這個跟mysql的sql優化有關,mysql會在sql優化的時候自己選擇合適的索引,很可能是mysql自己的選擇算法算出來使用這個索引不會提升性能,所以就放棄了。這種情況,可以使用force index 關鍵字強制使用索引(建議修改前先實驗一下,是不是真的會提升查詢效率):

    select name,code from student force index(XXXXXX) where name = '天才' 
    

    其中xxxx是索引名。

    join過多 or 子查詢過多

    我把join過多 和子查詢過多放在一起說了。一般來說,不建議使用子查詢,可以把子查詢改成join來優化。同時,join關聯的表也不宜過多,一般來說2-3張表還是合適的。具體關聯幾張表比較安全是需要具體問題具體分析的,如果各個表的數據量都很少,幾百條幾千條,那么關聯的表的可以適當多一些,反之則需要少一些。

    另外需要提到的是,在大多數情況下join是在內存里做的,如果匹配的量比較小,或者join_buffer設置的比較大,速度也不會很慢。但是,當join的數據量比較大的時候,mysql會采用在硬盤上創建臨時表的方式進行多張表的關聯匹配,這種顯然效率就極低,本來磁盤的IO就不快,還要關聯。

    一般遇到這種情況的時候就建議從代碼層面進行拆分,在業務層先查詢一張表的數據,然后以關聯字段作為條件查詢關聯表形成map,然后在業務層進行數據的拼裝。一般來說,索引建立正確的話,會比join快很多,畢竟內存里拼接數據要比網絡傳輸和硬盤IO快得多。

    in的元素過多

    這種問題,如果只看代碼的話不太容易排查,最好結合監控和數據庫日志一起分析。如果一個查詢有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("單次查詢數據量不能超過200");
    }
    

    單純的數據量過大

    這種問題,單純代碼的修修補補一般就解決不了了,需要變動整個的數據存儲架構。或者是對底層mysql分表或分庫+分表;或者就是直接變更底層數據庫,把mysql轉換成專門為處理大數據設計的數據庫。這種工作是個系統工程,需要嚴密的調研、方案設計、方案評審、性能評估、開發、測試、聯調,同時需要設計嚴密的數據遷移方案、回滾方案、降級措施、故障處理預案。除了以上團隊內部的工作,還可能有跨系統溝通的工作,畢竟做了重大變更,下游系統的調用接口的方式有可能會需要變化。

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

    業務邏輯復雜

    循環調用

    這種情況,一般都循環調用同一段代碼,每次循環的邏輯一致,前后不關聯。比如說,我們要初始化一個列表,預置12個月的數據給前端:

    List list = new ArrayList<>();
    for(int i = 0 ; i < 12 ; i ++) {
        Model model = calOneMonthData(i); // 計算某個月的數據,邏輯比較復雜,難以批量計算,效率也無法很高
        list.add(model);
    }
    

    這種顯然每個月的數據計算相互都是獨立的,我們完全可以采用多線程方式進行:

    // 建立一個線程池,注意要放在外面,不要每次執行代碼就建立一個,具體線程池的使用就不展開了
    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 < futures.size() ; i ++) {
          list.add(futures.get(i).get());
       }
    } catch (Exception e) {
       LOGGER.error("出現錯誤:", e);
    }
    

    順序調用

    如果不是類似上面循環調用,而是一次次的順序調用,而且調用之間沒有結果上的依賴,那么也可以用多線程的方式進行,例如:

    順序調用

    代碼上看:

    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 兩個任務都執行完成
    
    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兩個任務都執行完成
    
    return doResult(futureD.join(),futureE.join());
    
    

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

    線程池設計不合理

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

    這種情況首先應該懷疑是不是線程池設計的不合理。我覺得這里有必要回顧一下線程池的三個重要參數:核心線程數、最大線程數、等待隊列。這三個參數是怎么打配合的呢?當線程池創建的時候,如果不預熱線程池,則線程池中線程為0。當有任務提交到線程池,則開始創建核心線程。

    線程池設計不合理

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

    讓任務進入等待隊列開始等待

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


    創建非核心線程運行

    如果線程總數達到最大線程數,還是有任務到達,則開始根據線程池拋棄規則開始拋棄。


    根據線程池拋棄規則拋棄任務

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

    • 核心線程設置過小:核心線程設置過小則沒有達到并行的效果
    • 線程池公用,別的業務的任務執行時間太長,占用了核心線程,另一個業務的任務到達就直接進入了等待隊列
    • 任務太多,以至于占滿了線程池,大量任務在隊列中等待

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

    鎖設計不合理

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

    鎖類型使用不合理的典型場景就是讀寫鎖。也就是說,讀是可以共享的,但是讀的時候不能對共享變量寫;而在寫的時候,讀寫都不能進行。在可以加讀寫鎖的時候,如果我們加成了互斥鎖,那么在讀遠遠多于寫的場景下,效率會極大降低。

    鎖過粗則是另一種常見的鎖設計不合理的情況,如果我們把鎖包裹的范圍過大,則加鎖時間會過長,例如:

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

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

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

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

    造成這個問題的原因非常多,筆者就遇到了定時任務過大引起fullGC,代碼存在線程泄露引起RSS內存占用過高進而引起機器重啟等待諸多原因。需要結合各種監控和具體場景具體分析,進而進行大事務拆分、重新規劃線程池等等工作

    萬金油解決方式

    萬金油這個形容詞是從我們單位某位老師那里學來的,但是筆者覺得非常貼切。這些萬金油解決方式往往能解決大部分的接口緩慢的問題,而且也往往是我們解決接口效率問題的最終解決方案。當我們實在是沒有辦法排查出問題,或者實在是沒有優化空間的時候,可以嘗試這種萬金油的方式。

    緩存

    緩存是一種空間換取時間的解決方案,是在高性能存儲介質上(例如:內存、SSD硬盤等)存儲一份數據備份。當有請求打到服務器的時候,優先從緩存中讀取數據。如果讀取不到,則再從硬盤或通過網絡獲取數據。由于內存或SSD相比硬盤或網絡IO的效率高很多,則接口響應速度會變快非常多。緩存適合于應用在數據讀遠遠大于數據寫,且數據變化不頻繁的場景中。從技術選型上看,有這些:

    • 簡單的map
    • guava等本地緩存工具包
    • 緩存中間件:redistairmemcached

    當然,memcached現在用的很少了,因為相比于redis他不占優勢。tair則是阿里開發的一個分布式緩存中間件,他的優勢是理論上可以在不停服的情況下,動態擴展存儲容量,適用于大數據量緩存存儲。相比于單機redis緩存當然有優勢,而他與可擴展Redis集群的對比則需要進一步調研。

    進一步的,當前緩存的模型一般都是key-value模型。如何設計key以提高緩存的命中率是個大學問,好的key設計和壞的key設計所提升的性能差別非常大。而且,key設計是沒有一定之規的,需要結合具體的業務場景去分析。各個大公司分享出來的相關文章,緩存設計基本上是最大篇幅。

    回調 or 反查

    這種方式往往是業務上的解決方式,在訂單或者付款系統中應用的比較多。舉個例子:當我們付款的時候,需要調用一個專門的付款系統接口,該系統經過一系列驗證、存儲工作后還要調用銀行接口以執行付款。由于付款這個動作要求十分嚴謹,銀行側接口執行可能比較緩慢,進而拖累整個付款接口性能。這個時候我們就可以采用fast success的方式:當必要的校驗和存儲完成后,立即返回success,同時告訴調用方一個中間態“付款中”。而后調用銀行接口,當獲得支付結果后再調用上游系統的回調接口返回付款的最終結果“成果”or“失敗”。這樣就可以異步執行付款過程,提升付款接口效率。當然,為了防止多業務方接入的時候回調接口不統一,可以把結果拋進kafka,讓調用方監聽自己的結果。

    結語

    本文是筆者對工作中遇到的性能優化問題的一個簡單的總結,可能有不完備的地方,歡迎大家討論交流。

    mysql線程池
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    任務模塊專注于任務的執行等操作,開發和維護更加簡單和高效;接收“調度中心”的執行請求、終止請求和日志請求等。XXL-JOB的不同任務之間并行調度、并行執行。同時支持任務終止。當任務”路由策略”選擇”故障轉移”時,當調度中心每次發起調度請求時,會按照順序對執行器發出心跳檢測請求,第一個檢測為存活狀態的執行器將會被選定并發送調度請求。
    背景我負責的系統到2021年初完成了功能上的建設,開始進入到推廣階段。隨著推廣的逐步深入,收到了很多好評的同時也收到了很多對性能的吐槽。作為一個優秀的后端程序員,這個數據肯定是不能忍的,我們馬上就進入了漫長的接口優化之路。
    在近些年網絡空間安全形勢愈發嚴峻的情況下,對網絡協議分析提出了越來越高的要求,其中,對未知協議分類分析更是亟需攻克的難點。針對未知協議的分類問題,提出一種基于層次聚類的多策略未知協議分類方法。
    為簡化應用軟件的網絡通信開發工作,提出了一種基于異步網絡通信機制的通用高性能網絡框架方案,滿足應用軟件的網絡通信和業務處理要求,提高軟件開發成果的復用性。方案基于Boost.Asio庫實現Proactor模式的跨平臺高性能異步網絡通信,使用可擴展標記語言(Extensible Markup Language,XML)實現框架內外的功能業務關聯配置滿足通用要求,通過將網絡連接及關聯數據抽象為...
    在開始介紹如何優化sql前,先附上mysql內部邏輯圖讓大家有所了解連接器:?優先在緩存中進行查詢,如果查到了則直接返回,如果緩存中查詢不到,在去數據庫中查詢。
    目前業界常見的延時消息方案
    前兩天做了一個導入的功能,導入開始的時候非常慢,導入2w條數據要1分多鐘,后來一點一點的優化,從直接把list懟進Mysql中,到分配把list導入Mysql中,到多線程把list導入Mysql中。 時間是一點一點的變少了。非常的爽,最后變成了10s以內。 下面就展示一下過程。
    crawlergo是一個使用chrome headless模式進行URL收集的瀏覽器爬蟲。它對整個網頁的關鍵位置與DOM渲染階段進行HOOK,自動進行表單填充并提交,配合智能的JS事件觸發,盡可能的收集網站暴露出的入口。內置URL去重模塊,過濾掉了大量偽靜態URL,對于大型網站仍保持較快的解析與抓取速度,最后得到高質量的請求結果集合。調研1.
    可以認為IAM分成兩類,一個是AWS提供的IAM,這是一個完整的身份管理系統,但AWS只提供了系統,基于該系統的配置及信息維護,由客戶完全負責。AWS 提供了虛擬網絡及其之上的VPC,子網,ACL,安全組等,客戶需要準確設計配置自己的網絡,以確保正確的隔離和防護。用戶控制權限的修改通常由特權用戶或者管理員組實現。
    因為 web 服務器同時連接了外網和內網,所以必須首先拿下。這里有關 web 服務器的滲透不展開講了,無非也就是利用漏洞,諸如:弱口令、上傳漏洞、遠程代碼執行、各種 cms 漏洞,總之都是可以找到寫入 webshell 的方法。成功寫入 webshell 后,接著就要上傳木馬控制 web 服務器,這里可以用 Metasploit或 Cobaltstrike。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类