主頁 > 後端開發 > 祖傳代碼如何優化性能?

祖傳代碼如何優化性能?

2022-03-23 07:58:02 後端開發

hello大家好呀,我是小樓~

今天又帶來一次性能優化的分享,這是我剛進公司時接手的祖傳(壞笑)專案,這個專案在我的文章中屢次被提及,我在它上面做了很多的性能優化,比如《記一次提升18倍的性能優化》這篇文章,比較偏向某個細節的優化,本文更偏向宏觀上的性能優化,可以說是個老演員了,

image

背景

為了新朋友能快速進入場景,再描述一遍這個專案的背景,這個專案是一個自研的Dubbo注冊中心,上一張架構圖

image

  • Consumer 和 Provider 的服務發現請求(注冊、注銷、訂閱)都發給 Agent,由它全權代理
  • Registry 和 Agent 保持 Grpc 長鏈接,長鏈接的目的主要是 Provider 方有變更時,能及時推送給相應的 Consumer,為了保證資料的正確性,做了推拉結合的機制,Agent 會每隔一段時間去 Registry 拉取訂閱的服務串列
  • Agent 和業務服務部署在同一臺機器上,類似 Service Mesh 的思路,盡量減少對業務的入侵,這樣就能快速的迭代了

這里的Registry就是今天的主角,熟悉Dubbo的朋友可以把它當做是一個zookeeper,不熟悉的朋友可以就把它當做是一個Web應用,提供了注冊、注銷、訂閱介面,雖然它是用Go寫的,但本文和Go本身關系不大,也用用一些偽代碼來示意,所以也可以放心大膽地看下去,

一定要做性能優化嗎

在做性能優化之前,我們得回答幾個問題,性能優化帶來的收益是什么?為什么一定要做優化性能?不優化行不行?

性能優化無非有兩個目的:

  • 減少資源消耗,降低成本
  • 提高系統穩定性

如果只是為了降低成本,最好做之前估算一下大概能降低多少成本,如果吭哧吭哧干了大半個月,結果只省下了一丁點的資源,那是得不償失的,

回到這個注冊中心,為什么要做性能優化呢?

Dubbo應用啟動時,會向注冊中心發起注冊,如果注冊失敗,則會阻塞應用的啟動,

起初這個專案問題并不大,因為接入的應用并不多,而當我接手專案時,接入的應用越來越多,

話分兩頭,另一邊集團也在逐漸使用容器替代虛擬機和物理機,在高峰期會用擴容的方式來抗住流量高峰,快速擴容就要求服務能在短時間內大量啟動,無疑對注冊中心是一個大的考驗,

而導致這次優化的直接導火索是集團內的一次演練,他們發現一個配置中心的啟動依賴,性能達不到標準而導致擴容失敗,于是復盤下來,所有的啟動依賴必須達到一定的性能要求,而這個標準被定為1000qps,

于是就有了本文,

指標度量

如果不能度量,就沒法優化,

首先是把幾個核心介面加上metric,主要是請求量、耗時(p99 / p95 / p90)、錯誤請求量,無論是哪個專案,這點算是基本的了,如果沒加,得好好反思了,

其次對專案進行一次壓測,不知道現在的性能,后面的優化也無法證明其效果了,

以注冊介面為例,當時注冊的性能大概是40qps,記住這個值,看我們是如何一步一步達到1000qps的,

壓測成功的請求標準是:p99耗時在1秒以內,且無報錯,

瓶頸在哪里

性能優化的最關鍵之處在于找到瓶頸在哪,否則就是無頭蒼蠅,到處瞎碰,

注冊介面到底干了什么呢?我這里畫個簡圖

image

  • 整個流程加鎖,防止并發操作
  • Create App和Create Cluster是創建應用和集群,只會在應用第一次創建,如果創建過就直接跳過
  • Insert Endpoint是插入注冊資料,即ip和port
  • 系統的底層存盤是基于MySQL,Lock和UnLock也是基于MySQL實作的悲觀鎖

從這個流程圖就能看出來,瓶頸大概率在鎖上,這是個悲觀鎖,而且粒度是App,把整個流程鎖住,同一時刻相同應用的請只允許一個通過,可想而知性能有多差,

至于MySQL如何實作一個悲觀鎖,我相信你會的,所以我就不展開,

為了證明猜想,我用了一個非常笨但很有效的方法,在每一個關鍵節點執行之后,記錄下耗時,最后列印到日志里,這樣就能一眼看出到底哪里慢,果然最慢的就是加鎖,

鎖優化

在優化鎖之前,我們先搞清楚為什么要加鎖,在我反復測驗,讀代碼,看檔案之后,發現事情其實很簡單,這個鎖是為了防止App、Cluster、Endpoint重復寫入,

為什么防止重復寫入要這么折騰呢?一個資料庫的唯一索引不就搞定了?這無法考證,但現狀就是這樣,如何破解呢?

  • 首先是看這些表能否加唯一索引,有則盡量加上
  • 其次資料庫悲觀鎖能否換成Redis的樂觀鎖?

這個其實是可以的,原因在于客戶端具有重試機制,如果并發沖突了,則發起重試,我們堵這個概率很小,

上面兩條優化下來只解決了部分問題,還有的表實在無法添加唯一索引,比如這里App、Cluster由于一些特殊原因無法添加唯一索引,他們發生沖突的概率很高,同一個集群發布時,很可能是100臺機器同時拉起,只有一臺成功,剩余99臺在創建App或者Cluster時被鎖擋住了,發起重試,重試又可能沖突,大家都陷入了無限重試,最終超時,我們的服務也可能被重試流量打垮,

這該怎么辦?這時我想起了剛學Java時練習寫單例模式中,有個叫「雙重校驗鎖」的東西,我們看代碼

public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {
    }
    private static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        
        return instance;
    }
}

再結合我們的場景,App和Cluster只在創建時需要保證唯一性,后續都是先查詢,如果存在就不需要再執行插入,我們寫出偽代碼

app = DB.get("app_name")
if app == null {
    redis.lock()
    app = DB.get("app_name")
    if app == null {
        app = DB.instert("app_name")
    }
    redis.unlock()
}

是不是和雙重校驗鎖一模一樣?為什么這樣會性能更高呢?因為App和Cluster的特性是只在第一次時插入,真正需要鎖住的概率很小,就拿擴容的場景來說,必然不會走到鎖的邏輯,只有應用初次創建時才會真正被Lock,

性能優化有一點是很重要的,就是我們要去優化執行頻率非常高的場景,這樣收益才高,如果執行的頻率很低,那么我們是可以選擇性放棄的,

經過這輪優化,注冊的性能從40qps提升到了430qps,10倍的提升,

讀走快取

經過上一輪的優化,我們還有個結論能得出來,一個應用或集群的基本資訊基本不會變化,于是我在想,是否可以讀取這些資訊時直接走Redis快取呢?

于是將資訊基本不變的物件加上了快取,再測驗,發現qps從430提升到了440,提升不是很多,但蒼蠅再小,好歹是塊肉,

CPU優化

上一輪的優化效果不理想,但在壓測時注意到了一個問題,我發現Registry的CPU降低的很厲害,感覺瓶頸從鎖轉移到了CPU,說到CPU,這好辦啊,上火焰圖,Go自帶的pprof就能干,

image

可以清楚地看到是ParseUrl占用了太多的CPU,這里簡單科普下,Dubbo傳參很多是靠URL傳參的,注冊中心拿到Dubbo的URL,需要去決議其中的引數,比如ip、port等資訊就存在于URL之中,

一開始拿到這個CPU profile的結果是有點難受的,因為ParseUrl是封裝的標準包里的URL決議方法,想要寫一個比它還高效的,基本可以勸退,

但還是順騰摸瓜,看看哪里呼叫了這個方法,不看不知道,一看嚇一跳,原來一個請求里的URL,會執行程序中多次決議URL,為啥代碼會這么寫?可能是其中邏輯太復雜,一層一層的嵌套,但各個方法之間的傳參又不統一,所以帶來了這么糟糕的寫法,

這種情況怎么辦呢?

  • 重構,把URL的決議統一放在一個地方,后續傳參就傳決議后的結果,不需要重復決議
  • 對URL決議的方法,以每次請求的會話為粒度加一層快取,保證只決議一次

我選擇了第二種方式,因為這樣對代碼的改動小,畢竟我剛接手這么龐大、混亂的代碼,最好能不動就不動,能少動就少動,

而且這種方式我很熟悉,在Dubbo的原始碼中就有這樣的處理,Dubbo在反序列化時,如果是重復的物件,則直接走快取而不是再去構造一遍,代碼位于org.apache.dubbo.common.utils.PojoUtils#generalize

截取一點感受下

private static Object generalize(Object pojo, Map<Object, Object> history) {
    ...
    Object o = history.get(pojo);
    if (o != null) {
        return o;
    }
    history.put(pojo, pojo);
    ...
}

根據這個思路,把ParseUrl改成帶cache的模式

func parseUrl(url, cache) {
    if cache.get(url) != null {
        return cache.get(url)
    }
    u = parseUrl0(url)
    cache.put(url, u)
    return u
}

因為是會話級別的快取,所以每個會話會new一個cache,這樣能保證一個會話中對相同的url只決議一次,

可以看下這次優化的成果,qps直接到1100,達到目標~

image

最后說兩句

可能有人看完就要噴了,這哪是性能優化?這分明是填坑!對,你說的沒錯,只不過這坑是別人挖的,

本文就以一種最小的代價來搞定對祖傳代碼的性能優化,當然并不是鼓勵大家都去取巧,這專案我也正在重構,只是每個階段都有不同的解法,比如老板要求你2周內接手一個新專案,并完成性能優化上線,重構是不可能的,

希望通過本文你能學到一些性能優化的基本知識,從為什么要做的拷問出發,建立度量體系,找出瓶頸,一步一步進行優化,根據資料反饋及時調整優化方向,

今天到此為止,我們下期再見,


搜索關注微信公眾號"捉蟲大師",后端技術分享,架構設計、性能優化、原始碼閱讀、問題排查、踩坑實踐,

image

歷史好文推薦

  • 《慘,給Go提的代碼被批麻了》
  • 《大廠偏愛的Agent技術究竟是個啥》
  • 《剛出爐的《Java開發手冊黃山版》,我幫你們圈出了改動點!》
  • 《這個Dubbo注冊中心擴展,有點意思!》

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/447041.html

標籤:其他

上一篇:Java基礎——日期類Date

下一篇:演算法 | Java 常見排序演算法(純代碼)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more