日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

性能优化那些事

發布時間:2025/3/21 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 性能优化那些事 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

寫在之前的話,最近一年多來幾乎沒更新博客,更多的原因是自知資歷尚潛,要學習的東西太多,要接觸的東西也太多,沒有足夠的精力投入到博客上,或許有一天時機成熟會再提高更新頻率吧,但有一點不會變的是,學習的路上數十年如一日,我會一直堅持,爭取有更多的機會可以走出來,但前提是我有了足夠的深度和廣度。

謝謝大家的支持。

——————————————————————————————————————————————————————————-
從今年一月份開始,我們團隊陸續完成了郵件服務的架構升級。新平臺上線運行的過程中也發生了一系列的性能問題,即使很多看起來來微不足道的點也會讓整個系統運行得不是那么平穩,今天就將這段時間的問題以及解決方案統一整理下,希望能起到拋磚的作用,讓讀者在遇到類似問題的時候能多一個解決方案。

新平臺上線后第一版架構如下:

整個平臺的數據流程是:

  • 數據通過MQ消息和遠程服務調用進入新平臺;
  • 通過生產平臺生成郵件發送任務數據,同步Push進Redis隊列中;
  • 發送平臺輪詢Redis隊列并Pull消息到本地,然后連接郵件服務商服務器進行郵件發送,發送完畢后將結果更新回數據庫;
  • 有一個全局的Checker掃表,將待發送的郵件任務投遞到Redis隊列中(包括發送失敗,需要重試的任務)。
  • 這版架構上線后,我們遇到的第一個問題:數據庫讀寫壓力過大后影響整體服務穩定。

    表現為:

  • 數據庫主庫壓力高,同時伴有大量的讀,寫操作。
  • 遠程服務接口性能不穩定,業務繁忙時數據庫的插入操作延遲升高,接口響應變慢,接口監控頻繁報警,影響業務方。
  • 經過分析后,我們做了如下優化:

  • 數據庫做讀寫分離,將Checker的掃表操作放到從庫上去(主從庫中的同步延遲不影響我們發送,這次掃描不到的下次掃表到即可,因為每條郵件任務上有版本號控制,所以也不擔心會掃描到“舊記錄”的問題)。
  • 將Push到Redis的操作變成批量+異步的方式,減少接口同步執行邏輯中的操作,主庫只做最簡單的單條數據的Insert和Update,提高數據庫的吞吐量,盡量避免因為大量的讀庫請求引起數據庫的性能波動。
  • 這么做還有一個原因是經過測試,對于Redis的lpush命令來說每次Push1K大小的元素和每次Push20K的元素耗時沒有明顯增加。

    因此,我們使用了EventDrieven模型將Push操作改成了定時+批量+異步的方式往Redis Push郵件任務,這版優化上線后數據庫主庫CPU利用率基本在5%以下。

    總結:這次優化的經驗可以總結為:用異步縮短住業務流程 +用批量提高執行效率+數據庫讀寫分離分散讀寫壓力。

    優化后的架構圖:

    優化上線后,我們又遇到了第二個問題:JVM假死。

    表現為:

  • 單位時間內JVM Full GC次數明顯升高,GC后內存居高不下,每次GC能回收的內存非常有限。
  • 接口性能下降,處理延遲升高到幾十秒。
  • 應用基本不處理業務。
  • JVM進程還在,能響應jmap,jstack等命令。
  • jstack命令看到絕大多數線程處于block狀態。
  • 堆信息大致如下(注意紅色標注的點):

    如上兩圖,可以看到RecommendGoodsService 類占用了60%以上的內存空間,持有了34W個 “郵件任務對象”,非常可疑。

    分析后發現制造平臺在生成“郵件任務對象”后使用了異步隊列的方式處理對象中的推薦商品業務,因為某個低級的BUG導致處理隊列的線程數只有5個,遠低于預期數量,?因此隊列長度劇增導致的堆內存不夠用,觸發JVM的頻繁GC,導致整個JVM大量時間停留在”stop the world ” 狀態,JVM響應變得非常慢,最終表現為JVM假死,接口處理延遲劇增。

    總結:

  • 我們要盡量讓代碼對GC友好,絕大部分時候讓GC線程“短,平,快”的運行并減少Full GC的觸發機率。
  • 我們線上的容器都是多實例部署的,部署前通常也會考慮吞吐量問題,所以JVM直接掛掉一兩臺并不可怕,對于業務的影響也有限,但JVM的假死則是非常影響系統穩定性的,與其奈活,不如快死!
  • 相信很多團隊在使用線程池異步處理的時候都是使用的無界隊列存放Runnable任務的,此時一定要非常小心,無界意味著一旦生產線程快于消費線程,隊列將快速變長,這會帶來兩個非常不好的問題:

  • 從線程池到無界隊列到無界隊列中的元素全是強引用,GC無法釋放。
  • 隊列中的元素因為等不到消費線程處理,會在Young GC幾次后被移到年老代,年老代的回收則是靠Full GC才能回收,回收成本非常高。
  • 經過一段時間的運行,我們將JVM內存從2G調到了3G,于是我們又遇到了第三個問題:內存變大的煩惱

    JVM內存調大后,我們的JVM的GC次數減少了非常多,運行一段時間后加上了很多新功能,為了提高處理效率和減少業務之間的耦合,我們做了很多異步化的處理。更多的異步化意味著更多的線程和隊列,如上述經驗,很多元素被移到了年老代去,內存越用越小,如果正好在業務量不是特別大時,整個堆會呈現一個“穩步上升”的態勢,下一步就是內存閥值的持續報警了。

    所以,無界隊列的使用是需要非常小心的。

    我們把郵件服務分為生產郵件和促銷郵件兩部分,代碼90%是復用的,但獨立部署,獨立的數據庫,促銷郵件上線后,我們又遇到了老問題:數據庫主庫壓力再次CPU100%

    在經過生產郵件3個月的運行及優化后,我們對代碼做了少許的改動用于支持促銷郵件的發送,促銷的業務可以概括為:瞬間大量數據寫入,Checker每次需要掃描上百萬的數據,整個系統需要在大量待發送數據中維持一個較穩定的發送速率。上線后,數據庫又再次報出異常:

  • 主庫的寫有大量的死鎖異常(原來的生產郵件就有,不過再促銷郵件的業務形態中影響更明顯)。
  • 從庫有大量的全表掃描,讀壓力非常高。
  • 死鎖的問題,原因是這樣的:

    條件1.如果有Transaction1需要對ABC記錄加鎖,已經對A,B記錄加了X鎖,此刻在嘗試對C記錄枷鎖。

    條件2.如果此前Transaction2已經對C記錄加了獨占鎖,此刻需要對B記錄加X鎖。

    就會產生dead lock。實際情況是:如果兩條update語句同一時刻既需要掃描ABC又需要掃描DCB,那么死鎖就出現了。

    盡管Mysql做了優化,比如增加超時時間:innodb_lock_wait_timeout,超時后會自動釋放,釋放的結果是Transaction1和Transaction2全部Rollback(死鎖問題并沒有解決,如果不幸,下次執行還會重現)。再如果每個Transaction都是update數萬,數十萬的記錄(我們的業務就是),那事務的回滾代價就非常高了。

    解決辦法很多,比如先select出來再做逐條做update,或者update加上一個limit限制每次的更新次數,同時避免兩個Transaction并發執行等等。我們選擇了第一種,因為我們的業務對于時間上要求并不高,可以“慢慢做”。

    全表掃表的問題發生在Checker上,我們封裝了很多操作郵件任務的邏輯在不同的Checker中,比如:過期Checker,重置Checker,Redis Push Checker等等。他們負責將郵件任務更新為業務需要的各種狀態,大部分時候他們是并行執行的,會產生很多select請求。嚴重時,讀庫壓力基本維持在95%上長達數小時。

    全表掃描99%的原因是因為select沒有使用索引,所以往往開發同學的第一反應是加索引,然后讓數據庫“死扛”讀壓力 ,但索引是有成本的,占用硬盤空間不說,insert/delete操作都需要維護索引,

    其實我們還有另外好幾種方案可以選擇,比如:是不是需要這么頻繁的執行select? 是不是每次都要select這么多數據?是不是需要同一時間并發執行?

    我們的解決辦法是:合理利用索引+降低掃描頻率+掃描適量記錄。

    首先,將Checker里的SQL統一化,每個Checker產生的SQL只有條件不同,使用的字段基本一樣,這樣可以很好的使用索引。

    其次,我們發現發送端的消費能力是整個郵件發送流程的制約點,消費能力決定了某個時間內需要多少郵件發送任務,Checker掃描的量只要剛剛夠發送端滿負荷發送就可以了,因此,Checker不再每個幾分鐘掃表一次,只在隊列長度低于某個下限值時才掃描,

    并且一次掃描到隊列的上限值,多一個都不掃。

    經過以上優化后,促銷的庫也沒有再報警了。

    直到兩周以前,我們又遇到了一個新問題:發送節點CPU100%.

    這個問題的表象為:CPU正常執行業務時保持在80%以上,高峰時超過95%數小時。監控圖標如下:

    在說這個問題前,先看下發送節點的線程模型:

    Redis中根據目標郵箱的域名有一到多個Redis隊列,每個發送節點有一個跟目標郵箱相對應的FetchThread用于從Redis Pull郵件發送任務到發送節點本地,然后通過一個BlockingQueue將任務傳遞給DeliveryThread,DeliveryThread連接具體郵件服務商的服務器發送郵件。考慮到每次連接郵件服務商的服務器是一個相對耗時的過程,因此同一個域名的DeliveryThread有多個,是多線程并發執行的。

    既然表象是CPU100%,根據這個線程模型,第一步懷疑是不是線程數太多,同一時間并發導致的。查看配置后發現線程數只有幾百個,同時一時間執行的只有十多個,是相對合理的,不應該是引起CPU100%的根因。

    但是在檢查代碼時發現有這么一個業務場景:

  • 由于JIMDB的封裝,發送平臺采用的是輪詢的方式從Redis隊列中Pull郵件發送任務,Redis隊列為空時FetchThread會sleep一段時間,然后再檢查。
  • 從業務上說網易+騰訊的郵件占到了整個郵件總量的70%以上,對非前者的FetchThread來說,Pull不到幾率非常高。
  • 那就意味著發送節點上的很多FetchThread執行的是不必要的喚醒->檢查->sleep的流程,白白的浪費CPU資源。

    于是我們利用事件驅動的思想將模型稍稍改變一下:

    每次FetchThread對應的Redis隊列為空時,將該線程阻塞到Checker上,由Checker統一對多個Redis隊列的Pull條件做判斷,符合Pull條件后再喚醒FetchThread。

    Pull條件為:

    1.FetchThread的本地隊列長度小于初始長度的一半。

    2.Redis隊列不為空。

    同時滿足以上兩個條件,視為可以喚醒對應的FetchThread。

    以上的改造本質上還是在降低線程上下文切換的次數,將簡單工作歸一化,并將多路并發改為阻塞+事件驅動和降低拉取頻率,進一步減少線程占用CPU的時間片的機會。

    上線后,發送節點的CPU占用率有了20%左右的下降,但是并沒有直接將CPU的利用率優化為非常理想的情況(20%以下),我們懷疑并沒有找到真正的原因。

    于是我們接著對郵件發送流程做了進一步的梳理,發現了一個非常奇怪的地方,代碼如下:

    我們在發送節點上使用了Handlebars做郵件內容的渲染,在初始化時使用了Concurrent相關的Map做模板的緩存,但是每次渲染前卻要重新new一個HandlebarUtil,那每個HandlebarUtil豈不是用的都是不同的TemplateCache對象?既然如此,為什么要用Concurrent(意味著線程安全)的Map?

    進一步閱讀源碼后發現無論是Velocity還是Handlebars在渲染先都需要對模板做語法解析,構建抽象語法樹(AST),直至生成Template對象。構建的整個過程是相對消耗計算資源的,因此猜想Velocity或者Handlebars會對Template做緩存,只對同一個模板解析一次。

    為了驗證猜想,可以把渲染的過程單獨運行下:

    可以看到Handlebars的確可以對Template做了緩存,并且每次渲染前會優先去緩存中查找Template。而除了同樣執行5次,耗時開銷特別大以外,CPU的開銷也同樣特別大,上圖為使用了緩存CPU利用率,下圖為沒有使用到緩存的CPU利用率:

    找到了原因,修改就比較簡單了保證handlebars對象是單例的,能夠盡量使用緩存即可。

    上線后結果如下:

    至此,整個性能優化工作已經基本完成了,從每個案例的優化方案來看,有以下幾點經驗想和大家分享:

  • 性能優化首先應該定位到真正原因,從原因下手去想方案。
  • 方案應該貼合業務本身,從客觀規律、業務規則的角度去分析問題往往更容易找到突破點。
  • 一個細小的問題在業務量巨大的時候甚至可能壓垮服務的根因,開發過程中要注意每個細節點的處理。
  • 平時多積累相關工具的使用經驗,遇到問題時能結合多個工具定位問題。
  • 原文出處:?liuinsect
    from:?http://www.importnew.com/22118.html

    總結

    以上是生活随笔為你收集整理的性能优化那些事的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。