祖傳代碼如何優化性能?

来源:https://www.cnblogs.com/zhuochongdashi/archive/2022/03/22/16041006.html
-Advertisement-
Play Games

hello大家好呀,我是小樓~ 今天又帶來一次性能優化的分享,這是我剛進公司時接手的祖傳(壞笑)項目,這個項目在我的文章中屢次被提及,我在它上面做了很多的性能優化,比如《記一次提升18倍的性能優化》這篇文章,比較偏向某個細節的優化,本文更偏向巨集觀上的性能優化,可以說是個老演員了。 背景 為了新朋友能 ...


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

历史好文推荐


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • word-break: normal; // 此值為瀏覽器的預設屬性:以單詞為單位; keep-all 這個值由於相容性差,很少用;word-wrap: normal; // 此值為瀏覽器的預設屬性:以單詞為單位; 純中文:自動換行,一個漢字看做一個單詞; 純英文或純數字:看做一個單詞,不換行; 遇 ...
  • 經過前面兩天的學習,已經對Node.js有了一個初步的認識,今天繼續學習其他內容,並加以整理分享,如有不足之處,還請指正。 ...
  • 外觀模式是什麼 外觀模式是一種結構性設計模式,它能為程式庫、框架或者其他複雜的子系統提供一個統一的高層界面,使子系統更容易使用。外觀模式就是聚合多個介面實現,對外只暴露單個介面。隱藏子系統的複雜性。調用方不關心實現步驟。 為什麼要用外觀模式 當子系統提供的功能很多,而我們子需要多個子系統中很少的幾個 ...
  • 1. Consul 簡介 Consul是 HashiCorp 公司推出的開源工具,用於實現分散式系統的服務發現與配置。與其它分散式服 務註冊與發現的方案,Consul 的方案更“一站式”,內置了服務註冊與發現框 架、分佈一致性協議實 現、健康檢查、Key/Value 存儲、多數據中心方案,不再需要依 ...
  • 1.依賴配置 1.1 pom文件 <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <flink.version>1.13.0< ...
  • Hibernate 是一個開放源代碼的對象關係映射框架,它對 JDBC 進行了非常輕量級的對象封裝,它將 POJO 與資料庫表建立映射關係,是一個全自動的 orm 框架 ...
  • Java程式設計基礎之面向對象(下) (補充了上的一些遺漏的知識,同時加入了自己的筆記的ヾ(•ω•`)o) (至於為什麼分P,啊大概是為了自己查筆記方便(?)應該是(〃` 3′〃)) (但是u1s1,學完了面向對象後反而更懵逼,下一步先刷演算法吧,然後Java的學習也跟上,今年爭取考完二級證書(o-ω ...
  • (Java 常見排序演算法) 彙總 序號 排序演算法 平均時間 最好情況 最差情況 穩定度 額外空間 備註 相對時間 1 冒泡演算法 O(n2) O(n) O(n2) 穩定 O(1) n 越小越好 182 ms 2 選擇演算法 O(n2) O(n2) O(n2) 不穩定 O(1) n 越小越好 53 ms ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...